diff --git a/docs/static/MC-LEGO-loader-eyes.gif b/docs/static/MC-LEGO-loader-eyes.gif
new file mode 100644
index 00000000..fc2fde33
Binary files /dev/null and b/docs/static/MC-LEGO-loader-eyes.gif differ
diff --git a/docs/static/fonts/icons/iconfont.css b/docs/static/fonts/icons/iconfont.css
new file mode 100644
index 00000000..f81b9f9f
--- /dev/null
+++ b/docs/static/fonts/icons/iconfont.css
@@ -0,0 +1,30 @@
+@font-face {
+ font-family: "iconfont";
+ src: url("iconfont.eot?e05611aaee246c1da118a83eaf515de9?#iefix") format("embedded-opentype"),
+url("iconfont.woff2?e05611aaee246c1da118a83eaf515de9") format("woff2"),
+url("iconfont.woff?e05611aaee246c1da118a83eaf515de9") format("woff");
+}
+
+.icon {
+ line-height: 1;
+}
+
+.icon:before {
+ font-family: iconfont !important;
+ font-style: normal;
+ font-weight: normal !important;
+ vertical-align: top;
+}
+
+.icon-ultrasonic:before {
+ content: "\f101";
+}
+.icon-color:before {
+ content: "\f102";
+}
+.icon-touch:before {
+ content: "\f103";
+}
+.icon-gyro:before {
+ content: "\f104";
+}
diff --git a/docs/static/fonts/icons/iconfont.eot b/docs/static/fonts/icons/iconfont.eot
new file mode 100644
index 00000000..e4aa0b6d
Binary files /dev/null and b/docs/static/fonts/icons/iconfont.eot differ
diff --git a/docs/static/fonts/icons/iconfont.svg b/docs/static/fonts/icons/iconfont.svg
new file mode 100644
index 00000000..36778eab
--- /dev/null
+++ b/docs/static/fonts/icons/iconfont.svg
@@ -0,0 +1,24 @@
+
+
+
diff --git a/docs/static/fonts/icons/iconfont.ttf b/docs/static/fonts/icons/iconfont.ttf
new file mode 100644
index 00000000..1eb7bf3d
Binary files /dev/null and b/docs/static/fonts/icons/iconfont.ttf differ
diff --git a/docs/static/fonts/icons/iconfont.woff b/docs/static/fonts/icons/iconfont.woff
new file mode 100644
index 00000000..ab8f84de
Binary files /dev/null and b/docs/static/fonts/icons/iconfont.woff differ
diff --git a/docs/static/fonts/icons/iconfont.woff2 b/docs/static/fonts/icons/iconfont.woff2
new file mode 100644
index 00000000..a27e0d1f
Binary files /dev/null and b/docs/static/fonts/icons/iconfont.woff2 differ
diff --git a/legoresources/SVGassets/.DS_Store b/legoresources/SVGassets/.DS_Store
deleted file mode 100644
index 5008ddfc..00000000
Binary files a/legoresources/SVGassets/.DS_Store and /dev/null differ
diff --git a/libs/color-sensor/color.ts b/libs/color-sensor/color.ts
index 3821e83f..40e721c9 100644
--- a/libs/color-sensor/color.ts
+++ b/libs/color-sensor/color.ts
@@ -79,7 +79,9 @@ namespace sensors {
}
_query() {
- if (this.mode == ColorSensorMode.Color)
+ if (this.mode == ColorSensorMode.Color
+ || this.mode == ColorSensorMode.AmbientLightIntensity
+ || this.mode == ColorSensorMode.ReflectedLightIntensity)
return this.getNumber(NumberFormat.UInt8LE, 0)
return 0
}
diff --git a/libs/core/_locales/core-strings.json b/libs/core/_locales/core-strings.json
index 4a4819d0..a456a014 100644
--- a/libs/core/_locales/core-strings.json
+++ b/libs/core/_locales/core-strings.json
@@ -68,6 +68,7 @@
"motors|block": "motors",
"output|block": "output",
"screen|block": "screen",
+ "sensors|block": "sensors",
"serial|block": "serial",
"{id:category}Brick": "Brick",
"{id:category}Control": "Control",
diff --git a/libs/core/input.cpp b/libs/core/input.cpp
new file mode 100644
index 00000000..b1069502
--- /dev/null
+++ b/libs/core/input.cpp
@@ -0,0 +1,11 @@
+#include "pxt.h"
+
+namespace sensors {
+
+/**
+ * Mark a sensor as used
+ */
+//%
+void __sensorUsed(int port, int type) {
+}
+}
\ No newline at end of file
diff --git a/libs/core/input.ts b/libs/core/input.ts
index 20b1dd67..2d3e8f4f 100644
--- a/libs/core/input.ts
+++ b/libs/core/input.ts
@@ -142,6 +142,11 @@ namespace sensors.internal {
this._port = port_ - 1
init()
sensorInfos[this._port].sensors.push(this)
+ this.markUsed();
+ }
+
+ markUsed() {
+ sensors.__sensorUsed(this._port, this._deviceType());
}
_activated() { }
diff --git a/libs/core/output.cpp b/libs/core/output.cpp
index bc08bd13..3d157ba4 100644
--- a/libs/core/output.cpp
+++ b/libs/core/output.cpp
@@ -20,3 +20,13 @@ void target_init() {
}
}
+
+namespace motors {
+
+/**
+ * Mark a motor as used
+ */
+//%
+void __motorUsed(int port, bool large) {
+}
+}
\ No newline at end of file
diff --git a/libs/core/output.ts b/libs/core/output.ts
index b9f9fa6f..b4e4d1fe 100644
--- a/libs/core/output.ts
+++ b/libs/core/output.ts
@@ -229,6 +229,11 @@ namespace motors {
constructor(port: Output, large: boolean) {
super(port, () => this.__init(), (speed) => this.__setSpeed(speed), (steps, stepsOrTime, speed) => this.__move(steps, stepsOrTime, speed));
this._large = large;
+ this.markUsed();
+ }
+
+ markUsed() {
+ motors.__motorUsed(this._port, this._large);
}
private __init() {
@@ -339,6 +344,11 @@ namespace motors {
constructor(ports: Output) {
super(ports, () => this.__init(), (speed) => this.__setSpeed(speed), (steps, stepsOrTime, speed) => this.__move(steps, stepsOrTime, speed));
+ this.markUsed();
+ }
+
+ markUsed() {
+ motors.__motorUsed(this._port, true);
}
private __init() {
diff --git a/libs/core/pxt.json b/libs/core/pxt.json
index 716d8e58..e2f9c559 100644
--- a/libs/core/pxt.json
+++ b/libs/core/pxt.json
@@ -18,6 +18,7 @@
"output.cpp",
"output.ts",
"core.ts",
+ "input.cpp",
"input.ts",
"shims.d.ts",
"enums.d.ts",
diff --git a/libs/core/shims.d.ts b/libs/core/shims.d.ts
index 8924385e..407a657b 100644
--- a/libs/core/shims.d.ts
+++ b/libs/core/shims.d.ts
@@ -120,5 +120,21 @@ declare namespace output {
//% shim=output::createBuffer
function createBuffer(size: int32): Buffer;
}
+declare namespace motors {
+
+ /**
+ * Mark a motor as used
+ */
+ //% shim=motors::__motorUsed
+ function __motorUsed(port: int32, large: boolean): void;
+}
+declare namespace sensors {
+
+ /**
+ * Mark a sensor as used
+ */
+ //% shim=sensors::__sensorUsed
+ function __sensorUsed(port: int32, type: int32): void;
+}
// Auto-generated. Do not edit. Really.
diff --git a/libs/core/sim/analogSensor.ts b/libs/core/sim/analogSensor.ts
deleted file mode 100644
index 39efaebb..00000000
--- a/libs/core/sim/analogSensor.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-namespace pxsim {
- enum ThresholdState {
- High,
- Low,
- Normal
- }
-
- export class AnalogSensorState {
- public sensorUsed: boolean = false;
-
- private level: number;
- private state = ThresholdState.Normal;
-
- constructor(public id: number, private min = 0, private max = 255, private lowThreshold = 64, private highThreshold = 192) {
- this.level = Math.ceil((max - min) / 2);
- }
-
- public setUsed() {
- if (!this.sensorUsed) {
- this.sensorUsed = true;
- runtime.queueDisplayUpdate();
- }
- }
-
- public setLevel(level: number) {
- this.level = this.clampValue(level);
-
- if (this.level >= this.highThreshold) {
- this.setState(ThresholdState.High);
- }
- else if (this.level <= this.lowThreshold) {
- this.setState(ThresholdState.Low);
- }
- else {
- this.setState(ThresholdState.Normal);
- }
- }
-
- public getLevel(): number {
- return this.level;
- }
-
- public setLowThreshold(value: number) {
- this.lowThreshold = this.clampValue(value);
- this.highThreshold = Math.max(this.lowThreshold + 1, this.highThreshold);
- }
-
- public setHighThreshold(value: number) {
- this.highThreshold = this.clampValue(value);
- this.lowThreshold = Math.min(this.highThreshold - 1, this.lowThreshold);
- }
-
- private clampValue(value: number) {
- if (value < this.min) {
- return this.min;
- }
- else if (value > this.max) {
- return this.max;
- }
- return value;
- }
-
- private setState(state: ThresholdState) {
- if (this.state === state) {
- return;
- }
-
- this.state = state;
- switch (state) {
- case ThresholdState.High:
- board().bus.queue(this.id, DAL.ANALOG_THRESHOLD_HIGH);
- break;
- case ThresholdState.Low:
- board().bus.queue(this.id, DAL.ANALOG_THRESHOLD_LOW);
- break;
- case ThresholdState.Normal:
- break;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/libs/core/sim/pins.ts b/libs/core/sim/pins.ts
deleted file mode 100644
index 2ba0961c..00000000
--- a/libs/core/sim/pins.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-
-namespace pxsim.pins {
- export class CommonPin extends Pin {
- used: boolean;
- }
-
- export class DigitalPin extends CommonPin {
- }
-
- export class AnalogPin extends CommonPin {
-
- }
-
- export function markUsed(name: CommonPin) {
- if (!name.used) {
- name.used = true;
- runtime.queueDisplayUpdate();
- }
- }
-}
-
-namespace pxsim.DigitalPinMethods {
- export function digitalRead(name: pins.DigitalPin): number {
- return name.digitalReadPin();
- }
-
- /**
- * Set a pin or connector value to either 0 or 1.
- * @param value value to set on the pin, 1 eg,0
- */
- export function digitalWrite(name: pins.DigitalPin, value: number): void {
- name.digitalWritePin(value);
- }
-
- /**
- * Configures this pin to a digital input, and generates events where the timestamp is the duration
- * that this pin was either ``high`` or ``low``.
- */
- export function onPulsed(name: pins.DigitalPin, pulse: number, body: RefAction): void {
- // TODO
- }
-
- /**
- * Returns the duration of a pulse in microseconds
- * @param value the value of the pulse (default high)
- * @param maximum duration in micro-seconds
- */
- export function pulseIn(name: pins.DigitalPin, pulse: number, maxDuration = 2000000): number {
- // TODO
- return 500;
- }
-
- /**
- * Configures the pull of this pin.
- * @param pull one of the mbed pull configurations: PullUp, PullDown, PullNone
- */
- export function setPull(name: pins.DigitalPin, pull: number): void {
- name.setPull(pull);
- }
-
- /**
- * Do something when a pin is pressed.
- * @param body the code to run when the pin is pressed
- */
- export function onPressed(name: pins.DigitalPin, body: RefAction): void {
- }
-
- /**
- * Do something when a pin is released.
- * @param body the code to run when the pin is released
- */
- export function onReleased(name: pins.DigitalPin, body: RefAction): void {
- }
-
- /**
- * Get the pin state (pressed or not). Requires to hold the ground to close the circuit.
- * @param name pin used to detect the touch
- */
- export function isPressed(name: pins.DigitalPin): boolean {
- return name.isTouched();
- }
-}
-
-namespace pxsim.AnalogPinMethods {
- /**
- * Read the connector value as analog, that is, as a value comprised between 0 and 1023.
- */
- export function analogRead(name: pins.AnalogPin): number {
- pins.markUsed(name);
- return name.analogReadPin();
- }
-
- /**
- * Set the connector value as analog. Value must be comprised between 0 and 1023.
- * @param value value to write to the pin between ``0`` and ``1023``. eg:1023,0
- */
- export function analogWrite(name: pins.AnalogPin, value: number): void {
- pins.markUsed(name);
- name.analogWritePin(value);
-
- }
-
- /**
- * Configures the Pulse-width modulation (PWM) of the analog output to the given value in
- * **microseconds** or `1/1000` milliseconds.
- * If this pin is not configured as an analog output (using `analog write pin`), the operation has
- * no effect.
- * @param micros period in micro seconds. eg:20000
- */
- export function analogSetPeriod(name: pins.AnalogPin, micros: number): void {
- pins.markUsed(name);
- name.analogSetPeriod(micros);
- }
-
- /**
- * Writes a value to the servo, controlling the shaft accordingly. On a standard servo, this will
- * set the angle of the shaft (in degrees), moving the shaft to that orientation. On a continuous
- * rotation servo, this will set the speed of the servo (with ``0`` being full-speed in one
- * direction, ``180`` being full speed in the other, and a value near ``90`` being no movement).
- * @param value angle or rotation speed, eg:180,90,0
- */
- export function servoWrite(name: pins.AnalogPin, value: number): void {
- pins.markUsed(name);
- name.servoWritePin(value);
- }
-
- /**
- * Configures this IO pin as an analog/pwm output, configures the period to be 20 ms, and sets the
- * pulse width, based on the value it is given **microseconds** or `1/1000` milliseconds.
- * @param micros pulse duration in micro seconds, eg:1500
- */
- export function servoSetPulse(name: pins.AnalogPin, micros: number): void {
- pins.markUsed(name);
- // TODO fix pxt
- // name.servoSetPulse(micros);
- }
-}
-
-namespace pxsim.PwmPinMethods {
- export function analogSetPeriod(name: pins.AnalogPin, micros: number): void {
- name.analogSetPeriod(micros);
- }
-
- export function servoWrite(name: pins.AnalogPin, value: number): void {
- name.servoWritePin(value);
- }
-
- export function servoSetPulse(name: pins.AnalogPin, micros: number): void {
- name.servoSetPulse(name.id, micros);
- }
-}
-
-namespace pxsim.pins {
- export function pulseDuration(): number {
- // bus last event timestamp
- return 500;
- }
-
- export function createBuffer(sz: number) {
- return pxsim.BufferMethods.createBuffer(sz)
- }
-
- export function spiWrite(value: number): number {
- // TODO
- return 0;
- }
-
- export function i2cReadBuffer(address: number, size: number, repeat?: boolean): RefBuffer {
- // fake reading zeros
- return createBuffer(size)
- }
-
- export function i2cWriteBuffer(address: number, buf: RefBuffer, repeat?: boolean): void {
- // fake - noop
- }
-}
-
diff --git a/libs/ev3/ns.ts b/libs/ev3/ns.ts
index 18f414f1..10eb2464 100644
--- a/libs/ev3/ns.ts
+++ b/libs/ev3/ns.ts
@@ -1,17 +1,21 @@
//% color="#68C3E2" weight=100
//% groups='["Light", "Buttons", "Screen"]'
+//% labelLineWidth=0
namespace brick {
}
//% color="#C8509B" weight=95 icon="\uf192"
+//% labelLineWidth=0
//% groups='["Ultrasonic Sensor", "Touch Sensor", "Color Sensor", "Infrared Sensor", "Remote Infrared Beacon", "Gyro Sensor"]'
+//% groupIcons='["\uf101","\uf103","\uf102","","","\uf104"]'
namespace sensors {
}
//% color="#A5CA18" weight=90 icon="\uf185"
//% groups='["Motion", "Sensors", "Chassis"]'
+//% labelLineWidth=0
namespace motors {
}
diff --git a/package.json b/package.json
index 550722b6..212e7882 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,8 @@
"@types/bluebird": "2.0.33",
"@types/jquery": "3.2.16",
"@types/marked": "0.3.0",
- "@types/node": "8.0.53"
+ "@types/node": "8.0.53",
+ "webfonts-generator": "^0.4.0"
},
"dependencies": {
"pxt-common-packages": "0.14.13",
diff --git a/pxtarget.json b/pxtarget.json
index 1b2c1bf9..8b1610e4 100644
--- a/pxtarget.json
+++ b/pxtarget.json
@@ -20,7 +20,7 @@
"simulator": {
"autoRun": true,
"streams": true,
- "aspectRatio": 0.67,
+ "aspectRatio": 0.5,
"parts": false,
"enableTrace": true,
"boardDefinition": {
@@ -136,7 +136,9 @@
},
"monacoColors": {
"editor.background": "#ecf6ff"
- }
+ },
+ "simAnimationEnter": "horizontal flip in",
+ "simAnimationExit": "horizontal flip out"
},
"ignoreDocsErrors": true
}
diff --git a/sim/dalboard.ts b/sim/dalboard.ts
index c86d963d..9ad960b8 100644
--- a/sim/dalboard.ts
+++ b/sim/dalboard.ts
@@ -22,17 +22,7 @@ namespace pxsim {
D13
}
- export class DalBoard extends CoreBoard implements
- AccelerometerBoard,
- CommonBoard,
- // LightBoard,
- LightSensorBoard,
- MicrophoneBoard,
- MusicBoard,
- SlideSwitchBoard,
- TemperatureBoard,
- InfraredBoard,
- CapTouchBoard {
+ export class EV3Board extends CoreBoard {
// state & update logic for component services
// neopixelState: CommonNeoPixelState;
buttonState: EV3ButtonState;
@@ -44,80 +34,42 @@ namespace pxsim {
edgeConnectorState: EdgeConnectorState;
capacitiveSensorState: CapacitiveSensorState;
accelerometerState: AccelerometerState;
- audioState: AudioState;
touchButtonState: TouchButtonState;
irState: InfraredState;
- lightState: EV3LightState;
- screenState: EV3ScreenState;
view: SVGSVGElement;
+ outputState: EV3OutputState;
+ analogState: EV3AnalogState;
+ uartState: EV3UArtState;
+ motorState: EV3MotorState;
+ screenState: EV3ScreenState;
+ audioState: AudioState;
+
+ inputNodes: SensorNode[] = [];
+ brickNode: BrickNode;
+ outputNodes: MotorNode[] = [];
+
+ private motorMap: pxt.Map = {
+ 0x01: 0,
+ 0x02: 1,
+ 0x04: 2,
+ 0x08: 3
+ }
+
constructor() {
super()
this.bus.setNotify(DAL.DEVICE_ID_NOTIFY, DAL.DEVICE_ID_NOTIFY_ONE);
- //components
+ this.brickNode = new BrickNode();
- this.builtinParts["buttons"] = this.buttonState = new EV3ButtonState();
- this.builtinParts["light"] = this.lightState = new EV3LightState();
- this.builtinParts["screen"] = this.screenState = new EV3ScreenState();
- this.builtinParts["audio"] = this.audioState = new AudioState();
-
- /*this.builtinParts["neopixel"] = this.neopixelState = new CommonNeoPixelState();
- this.builtinParts["buttonpair"] = this.buttonState = new CommonButtonState();
-
- this.builtinParts["switch"] = this.slideSwitchState = new SlideSwitchState();
- this.builtinParts["lightsensor"] = this.lightSensorState = new AnalogSensorState(DAL.DEVICE_ID_LIGHT_SENSOR, 0, 255);
- this.builtinParts["thermometer"] = this.thermometerState = new AnalogSensorState(DAL.DEVICE_ID_THERMOMETER, -5, 50);
- this.builtinParts["soundsensor"] = this.microphoneState = new AnalogSensorState(DAL.DEVICE_ID_TOUCH_SENSOR + 1, 0, 255);
- this.builtinParts["capacitivesensor"] = this.capacitiveSensorState = new CapacitiveSensorState({
- 0: 0,
- 1: 1,
- 2: 2,
- 3: 3,
- 6: 4,
- 9: 5,
- 10: 6,
- 12: 7
- });
-
- this.builtinParts["accelerometer"] = this.accelerometerState = new AccelerometerState(runtime);
- this.builtinParts["edgeconnector"] = this.edgeConnectorState = new EdgeConnectorState({
- pins: [
- pxsim.CPlayPinName.A0,
- pxsim.CPlayPinName.A1,
- pxsim.CPlayPinName.A2,
- pxsim.CPlayPinName.A3,
- pxsim.CPlayPinName.A4,
- pxsim.CPlayPinName.A5,
- pxsim.CPlayPinName.A6,
- pxsim.CPlayPinName.A7,
- pxsim.CPlayPinName.A8,
- pxsim.CPlayPinName.A9,
- pxsim.CPlayPinName.D4,
- pxsim.CPlayPinName.D5,
- pxsim.CPlayPinName.D6,
- pxsim.CPlayPinName.D7,
- pxsim.CPlayPinName.D8,
- pxsim.CPlayPinName.D13
- ]
- });
- this.builtinParts["microservo"] = this.edgeConnectorState;
-
- this.builtinVisuals["microservo"] = () => new visuals.MicroServoView();
- this.builtinPartVisuals["microservo"] = (xy: visuals.Coord) => visuals.mkMicroServoPart(xy);
- this.touchButtonState = new TouchButtonState([
- pxsim.CPlayPinName.A1,
- pxsim.CPlayPinName.A2,
- pxsim.CPlayPinName.A3,
- pxsim.CPlayPinName.A4,
- pxsim.CPlayPinName.A5,
- pxsim.CPlayPinName.A6,
- pxsim.CPlayPinName.A7
- ]);
-
- this.builtinParts["ir"] = this.irState = new InfraredState();*/
+ this.outputState = new EV3OutputState();
+ this.analogState = new EV3AnalogState();
+ this.uartState = new EV3UArtState();
+ this.motorState = new EV3MotorState();
+ this.screenState = new EV3ScreenState();
+ this.audioState = new AudioState();
}
receiveMessage(msg: SimulatorMessage) {
@@ -182,11 +134,45 @@ namespace pxsim {
getDefaultPitchPin() {
return this.edgeConnectorState.getPin(CPlayPinName.D6);
}
+
+ getBrickNode() {
+ return this.brickNode;
+ }
+
+ getMotor(port: number, large?: boolean): MotorNode[] {
+ if (port == 0xFF) return this.getMotors(); // Return all motors
+ const motorPort = this.motorMap[port];
+ if (this.outputNodes[motorPort] == undefined) {
+ this.outputNodes[motorPort] = large ?
+ new LargeMotorNode(motorPort) : new MediumMotorNode(motorPort);
+ }
+ return [this.outputNodes[motorPort]];
+ }
+
+ getMotors() {
+ return this.outputNodes;
+ }
+
+ getSensor(port: number, type: number): SensorNode {
+ if (this.inputNodes[port] == undefined) {
+ switch (type) {
+ case DAL.DEVICE_TYPE_GYRO: this.inputNodes[port] = new GyroSensorNode(port); break;
+ case DAL.DEVICE_TYPE_COLOR: this.inputNodes[port] = new ColorSensorNode(port); break;
+ case DAL.DEVICE_TYPE_TOUCH: this.inputNodes[port] = new TouchSensorNode(port); break;
+ case DAL.DEVICE_TYPE_ULTRASONIC: this.inputNodes[port] = new UltrasonicSensorNode(port); break;
+ }
+ }
+ return this.inputNodes[port];
+ }
+
+ getInputNodes() {
+ return this.inputNodes;
+ }
}
export function initRuntimeWithDalBoard() {
U.assert(!runtime.board);
- let b = new DalBoard();
+ let b = new EV3Board();
runtime.board = b;
runtime.postError = (e) => {
// TODO
@@ -194,6 +180,10 @@ namespace pxsim {
}
}
+ export function ev3board(): EV3Board {
+ return runtime.board as EV3Board;
+ }
+
if (!pxsim.initCurrentRuntime) {
pxsim.initCurrentRuntime = initRuntimeWithDalBoard;
}
diff --git a/sim/state/analog.ts b/sim/state/analog.ts
new file mode 100644
index 00000000..12a48029
--- /dev/null
+++ b/sim/state/analog.ts
@@ -0,0 +1,68 @@
+namespace pxsim {
+
+ enum AnalogOff {
+ InPin1 = 0, // int16[4]
+ InPin6 = 8, // int16[4]
+ OutPin5 = 16, // int16[4]
+ BatteryTemp = 24, // int16
+ MotorCurrent = 26, // int16
+ BatteryCurrent = 28, // int16
+ Cell123456 = 30, // int16
+ Pin1 = 32, // int16[300][4]
+ Pin6 = 2432, // int16[300][4]
+ Actual = 4832, // uint16[4]
+ LogIn = 4840, // uint16[4]
+ LogOut = 4848, // uint16[4]
+ NxtCol = 4856, // uint16[36][4] - NxtColor*4
+ OutPin5Low = 5144, // int16[4]
+ Updated = 5152, // int8[4]
+ InDcm = 5156, // int8[4]
+ InConn = 5160, // int8[4]
+ OutDcm = 5164, // int8[4]
+ OutConn = 5168, // int8[4]
+ Size = 5172
+ }
+
+ export class EV3AnalogState {
+
+ constructor() {
+ let data = new Uint8Array(5172)
+ MMapMethods.register("/dev/lms_analog", {
+ data,
+ beforeMemRead: () => {
+ //console.log("analog before read");
+ const inputNodes = ev3board().getInputNodes();
+ for (let port = 0; port < DAL.NUM_INPUTS; port++) {
+ const node = inputNodes[port];
+ if (node) {
+ data[AnalogOff.InConn + port] = node.isUart() ? DAL.CONN_INPUT_UART : DAL.CONN_INPUT_DUMB;
+ if (node.isAnalog() && node.hasData()) {
+ //data[AnalogOff.InPin6 + 2 * port] = node.getValue();
+ util.map16Bit(data, AnalogOff.InPin6 + 2 * port, node.getValue())
+ }
+ }
+ }
+ },
+ read: buf => {
+ let v = "vSIM"
+ for (let i = 0; i < buf.data.length; ++i)
+ buf.data[i] = v.charCodeAt(i) || 0
+ console.log("analog read");
+ console.log(buf.data);
+ return buf.data.length
+ },
+ write: buf => {
+ console.log("analog write");
+ console.log(buf);
+ return 2
+ },
+ ioctl: (id, buf) => {
+ console.log("analog ioctl");
+ console.log(id);
+ console.log(buf);
+ return 2;
+ }
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/brick.ts b/sim/state/brick.ts
new file mode 100644
index 00000000..f4664948
--- /dev/null
+++ b/sim/state/brick.ts
@@ -0,0 +1,27 @@
+///
+
+namespace pxsim {
+
+ export class PortNode extends BaseNode {
+ id = NodeType.Port;
+
+ constructor(port: number) {
+ super(port);
+ }
+ }
+
+
+ export class BrickNode extends BaseNode {
+ id = NodeType.Brick;
+
+ buttonState: EV3ButtonState;
+ lightState: EV3LightState;
+
+ constructor() {
+ super(-1);
+
+ this.buttonState = new EV3ButtonState();
+ this.lightState = new EV3LightState();
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/color.ts b/sim/state/color.ts
new file mode 100644
index 00000000..ca5bab18
--- /dev/null
+++ b/sim/state/color.ts
@@ -0,0 +1,46 @@
+///
+
+namespace pxsim {
+
+ export enum ColorSensorMode {
+ Reflected = 0,
+ Ambient = 1,
+ Colors = 2,
+ RefRaw = 3,
+ RgbRaw = 4,
+ ColorCal = 5
+ }
+
+ export enum ThresholdState {
+ Normal = 1,
+ High = 2,
+ Low = 3,
+ }
+
+ export class ColorSensorNode extends UartSensorNode {
+ id = NodeType.ColorSensor;
+
+ private color: number;
+
+ constructor(port: number) {
+ super(port);
+ }
+
+ getDeviceType() {
+ return DAL.DEVICE_TYPE_COLOR;
+ }
+
+ setColor(color: number) {
+ this.color = color;
+ this.changed = true;
+ this.valueChanged = true;
+
+ runtime.queueDisplayUpdate();
+
+ }
+
+ getValue() {
+ return this.color;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/control.ts b/sim/state/control.ts
index 0a8f0e73..dabc588b 100644
--- a/sim/state/control.ts
+++ b/sim/state/control.ts
@@ -63,7 +63,6 @@ namespace pxsim.MMapMethods {
export function write(m: MMap, data: Buffer): number {
return m.impl.write(data)
-
}
export function read(m: MMap, data: Buffer): number {
diff --git a/sim/state/gyro.ts b/sim/state/gyro.ts
new file mode 100644
index 00000000..91fab7fc
--- /dev/null
+++ b/sim/state/gyro.ts
@@ -0,0 +1,47 @@
+namespace pxsim {
+ const enum GyroSensorMode {
+ None = -1,
+ Angle = 0,
+ Rate = 1,
+ }
+
+ export class GyroSensorNode extends UartSensorNode {
+ id = NodeType.GyroSensor;
+
+ private angle: number = 0;
+ private rate: number = 0;
+
+ constructor(port: number) {
+ super(port);
+ }
+
+ getDeviceType() {
+ return DAL.DEVICE_TYPE_GYRO;
+ }
+
+ setAngle(angle: number) {
+ if (this.angle != angle) {
+ this.angle = angle;
+ this.changed = true;
+ this.valueChanged = true;
+
+ runtime.queueDisplayUpdate();
+ }
+ }
+
+ setRate(rate: number) {
+ if (this.rate != rate) {
+ this.rate = rate;
+ this.changed = true;
+ this.valueChanged = true;
+
+ runtime.queueDisplayUpdate();
+ }
+ }
+
+ getValue() {
+ return this.mode == GyroSensorMode.Angle ? this.angle :
+ this.mode == GyroSensorMode.Rate ? this.rate : 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/input.ts b/sim/state/input.ts
new file mode 100644
index 00000000..a00ceec2
--- /dev/null
+++ b/sim/state/input.ts
@@ -0,0 +1,18 @@
+
+namespace pxsim.motors {
+
+ export function __motorUsed(port: number, large: boolean) {
+ console.log("MOTOR INIT " + port);
+ const motors = ev3board().getMotor(port, large);
+ runtime.queueDisplayUpdate();
+ }
+}
+
+namespace pxsim.sensors {
+
+ export function __sensorUsed(port: number, type: number) {
+ console.log("SENSOR INIT " + port + ", type: " + type);
+ const sensor = ev3board().getSensor(port, type);
+ runtime.queueDisplayUpdate();
+ }
+}
\ No newline at end of file
diff --git a/sim/state/light.ts b/sim/state/light.ts
index c46db57b..8cab2a82 100644
--- a/sim/state/light.ts
+++ b/sim/state/light.ts
@@ -12,7 +12,7 @@ namespace pxsim {
namespace pxsim.output {
export function setLights(pattern: number) {
- const lightState = (board() as DalBoard).lightState;
+ const lightState = ev3board().getBrickNode().lightState;
lightState.lightPattern = pattern;
runtime.queueDisplayUpdate();
}
diff --git a/sim/state/motor.ts b/sim/state/motor.ts
new file mode 100644
index 00000000..5398c98a
--- /dev/null
+++ b/sim/state/motor.ts
@@ -0,0 +1,49 @@
+namespace pxsim {
+
+ enum MotorDataOff {
+ TachoCounts = 0, // int32
+ Speed = 4, // int8
+ Padding = 5, // int8[3]
+ TachoSensor = 8, // int32
+ Size = 12
+ }
+
+ export class EV3MotorState {
+
+ constructor() {
+ let data = new Uint8Array(12 * DAL.NUM_OUTPUTS)
+ MMapMethods.register("/dev/lms_motor", {
+ data,
+ beforeMemRead: () => {
+ console.log("motor before read");
+ for (let port = 0; port < DAL.NUM_OUTPUTS; ++port) {
+ data[MotorDataOff.TachoCounts * port] = 0; // Tacho count
+ data[MotorDataOff.Speed * port] = 50; // Speed
+ data[MotorDataOff.TachoSensor * port] = 0; // Count
+ }
+ },
+ read: buf => {
+ let v = "vSIM"
+ for (let i = 0; i < buf.data.length; ++i)
+ buf.data[i] = v.charCodeAt(i) || 0
+ console.log("motor read");
+ console.log(buf.data);
+ return buf.data.length
+ },
+ write: buf => {
+ if (buf.data.length == 0) return 2;
+ const cmd = buf.data[0];
+ console.log("motor write");
+ console.log(buf);
+ return 2
+ },
+ ioctl: (id, buf) => {
+ console.log("motor ioctl");
+ console.log(id);
+ console.log(buf);
+ return 2;
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/motors.ts b/sim/state/motors.ts
new file mode 100644
index 00000000..02a02e58
--- /dev/null
+++ b/sim/state/motors.ts
@@ -0,0 +1,73 @@
+namespace pxsim {
+
+ export class MotorNode extends BaseNode {
+ isOutput = true;
+
+ public angle: number = 0;
+
+ private speed: number;
+ private large: boolean;
+ private rotation: number;
+ private polarity: boolean;
+
+ constructor(port: number) {
+ super(port);
+ }
+
+ setSpeed(speed: number) {
+ if (this.speed != speed) {
+ this.speed = speed;
+ this.changed = true;
+ runtime.queueDisplayUpdate();
+ }
+ }
+
+ setLarge(large: boolean) {
+ this.large = large;
+ }
+
+ getSpeed() {
+ return this.speed;
+ }
+
+ stepSpeed(speed: number, angle: number, brake: boolean) {
+ // TODO: implement
+ }
+
+ setPolarity(polarity: number) {
+ // Either 1 or 255 (reverse)
+ this.polarity = polarity === 255;
+ // TODO: implement
+ }
+
+ reset() {
+ // TODO: implement
+ }
+
+ stop() {
+ // TODO: implement
+ }
+
+ start() {
+ // TODO: implement
+ runtime.queueDisplayUpdate();
+ }
+ }
+
+ export class MediumMotorNode extends MotorNode {
+ id = NodeType.MediumMotor;
+
+ constructor(port: number) {
+ super(port);
+ }
+ }
+
+ export class LargeMotorNode extends MotorNode {
+ id = NodeType.LargeMotor;
+
+ constructor(port: number) {
+ super(port);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/sim/state/nodeTypes.ts b/sim/state/nodeTypes.ts
new file mode 100644
index 00000000..515818a6
--- /dev/null
+++ b/sim/state/nodeTypes.ts
@@ -0,0 +1,36 @@
+namespace pxsim {
+ export enum NodeType {
+ Port = 0,
+ Brick = 1,
+ TouchSensor = 2,
+ MediumMotor = 3,
+ LargeMotor = 4,
+ GyroSensor = 5,
+ ColorSensor = 6,
+ UltrasonicSensor = 7
+ }
+
+ export interface Node {
+ id: number;
+ didChange(): boolean;
+ }
+
+ export class BaseNode implements Node {
+ public id: number;
+ public port: number;
+ public isOutput = false;
+
+ private used = false;
+ protected changed = true;
+
+ constructor(port: number) {
+ this.port = port;
+ }
+
+ didChange() {
+ const res = this.changed;
+ this.changed = false;
+ return res;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/output.ts b/sim/state/output.ts
new file mode 100644
index 00000000..e85ca5a4
--- /dev/null
+++ b/sim/state/output.ts
@@ -0,0 +1,104 @@
+namespace pxsim {
+
+ export class EV3OutputState {
+
+ constructor() {
+ let data = new Uint8Array(10)
+ MMapMethods.register("/dev/lms_pwm", {
+ data,
+ beforeMemRead: () => {
+ //console.log("pwm before read");
+ for (let i = 0; i < 10; ++i)
+ data[i] = 0
+ },
+ read: buf => {
+ let v = "vSIM"
+ for (let i = 0; i < buf.data.length; ++i)
+ buf.data[i] = v.charCodeAt(i) || 0
+ console.log("pwm read");
+ return buf.data.length
+ },
+ write: buf => {
+ if (buf.data.length == 0) return 2;
+ const cmd = buf.data[0];
+ switch (cmd) {
+ case DAL.opProgramStart: {
+ // init
+ console.log('init');
+ return 2;
+ }
+ case DAL.opOutputReset: {
+ // reset
+ const port = buf.data[1];
+ const motors = ev3board().getMotor(port);
+ motors.forEach(motor => motor.reset());
+ return 2;
+ }
+ case DAL.opOutputStepSpeed: {
+ // step speed
+ const port = buf.data[1];
+ const speed = buf.data[2];
+ // note that b[3] is padding
+ const step1 = buf.data[4];
+ const step2 = buf.data[5]; // angle
+ const step3 = buf.data[6];
+ const brake = buf.data[7];
+ //console.log(buf);
+ const motors = ev3board().getMotor(port);
+ motors.forEach(motor => motor.stepSpeed(speed, step2, brake === 1));
+ return 2;
+ }
+ case DAL.opOutputStop: {
+ // stop
+ const port = buf.data[1];
+ const brake = buf.data[2];
+ const motors = ev3board().getMotor(port);
+ motors.forEach(motor => motor.stop());
+ return 2;
+ }
+ case DAL.opOutputSpeed: {
+ // setSpeed
+ const port = buf.data[1];
+ const speed = buf.data[2];
+ const motors = ev3board().getMotor(port);
+ motors.forEach(motor => motor.setSpeed(speed));
+ return 2;
+ }
+ case DAL.opOutputStart: {
+ // start
+ const port = buf.data[1];
+ const motors = ev3board().getMotor(port);
+ motors.forEach(motor => motor.start());
+ return 2;
+ }
+ case DAL.opOutputPolarity: {
+ // reverse
+ const port = buf.data[1];
+ const polarity = buf.data[2];
+ const motors = ev3board().getMotor(port);
+ motors.forEach(motor => motor.setPolarity(polarity));
+ return 2;
+ }
+ case DAL.opOutputSetType: {
+ const port = buf.data[1];
+ const large = buf.data[2] == 0x07;
+ const motors = ev3board().getMotor(port);
+ motors.forEach(motor => motor.setLarge(large));
+ return 2;
+ }
+ }
+
+ console.log("pwm write");
+ console.log(buf);
+ return 2
+ },
+ ioctl: (id, buf) => {
+ console.log("pwm ioctl");
+ console.log(id);
+ console.log(buf);
+ return 2;
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/screen.ts b/sim/state/screen.ts
index 2ceaa428..342b0430 100644
--- a/sim/state/screen.ts
+++ b/sim/state/screen.ts
@@ -7,7 +7,6 @@ namespace pxsim {
export class EV3ScreenState {
- shouldUpdate: boolean;
points: Uint8Array;
constructor() {
this.points = new Uint8Array(visuals.SCREEN_WIDTH * visuals.SCREEN_HEIGHT)
@@ -24,13 +23,13 @@ namespace pxsim {
setPixel(x: number, y: number, v: number) {
this.applyMode(OFF(x, y), v)
- this.shouldUpdate = true;
+ runtime.queueDisplayUpdate();
}
clear() {
for (let i = 0; i < this.points.length; ++i)
this.points[i] = 0;
- this.shouldUpdate = true;
+ runtime.queueDisplayUpdate();
}
blitLineCore(x: number, y: number, w: number, buf: RefBuffer, mode: Draw, offset = 0) {
@@ -59,7 +58,7 @@ namespace pxsim {
}
}
- this.shouldUpdate = true;
+ runtime.queueDisplayUpdate();
}
clearLine(x: number, y: number, w: number) {
@@ -82,12 +81,12 @@ namespace pxsim.screen {
function YY(v: number) { return v >> 16 }
export function _setPixel(x: number, y: number, mode: Draw) {
- const screenState = (board() as DalBoard).screenState;
+ const screenState = ev3board().screenState;
screenState.setPixel(x, y, mode);
}
export function _blitLine(xw: number, y: number, buf: RefBuffer, mode: Draw) {
- const screenState = (board() as DalBoard).screenState;
+ const screenState = ev3board().screenState;
screenState.blitLineCore(XX(xw), y, YY(xw), buf, mode)
}
@@ -99,7 +98,7 @@ namespace pxsim.screen {
return ((x + 7) >> 3)
}
export function clear(): void {
- const screenState = (board() as DalBoard).screenState;
+ const screenState = ev3board().screenState;
screenState.clear()
}
@@ -240,7 +239,7 @@ namespace pxsim.ImageMethods {
}
export function draw(buf: RefBuffer, x: number, y: number, mode: Draw): void {
- const screenState = (board() as DalBoard).screenState;
+ const screenState = ev3board().screenState;
if (!screen.isValidImage(buf))
return;
diff --git a/sim/state/sensor.ts b/sim/state/sensor.ts
new file mode 100644
index 00000000..2f79393d
--- /dev/null
+++ b/sim/state/sensor.ts
@@ -0,0 +1,73 @@
+
+namespace pxsim {
+
+ export class SensorNode extends BaseNode {
+
+ protected mode: number;
+ protected valueChanged: boolean;
+
+ constructor(port: number) {
+ super(port);
+ }
+
+ public isUart() {
+ return true;
+ }
+
+ public isAnalog() {
+ return false;
+ }
+
+ public getValue() {
+ return 0;
+ }
+
+ setMode(mode: number) {
+ this.mode = mode;
+ }
+
+ getMode() {
+ return this.mode;
+ }
+
+ getDeviceType() {
+ return DAL.DEVICE_TYPE_NONE;
+ }
+
+ public hasData() {
+ return true;
+ }
+
+ valueChange() {
+ const res = this.valueChanged;
+ this.valueChanged = false;
+ return res;
+ }
+ }
+
+ export class AnalogSensorNode extends SensorNode {
+
+ constructor(port: number) {
+ super(port);
+ }
+
+ public isUart() {
+ return false;
+ }
+
+ public isAnalog() {
+ return true;
+ }
+ }
+
+ export class UartSensorNode extends SensorNode {
+
+ constructor(port: number) {
+ super(port);
+ }
+
+ hasChanged() {
+ return this.changed;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/touch.ts b/sim/state/touch.ts
new file mode 100644
index 00000000..7542ad5b
--- /dev/null
+++ b/sim/state/touch.ts
@@ -0,0 +1,42 @@
+namespace pxsim {
+
+ export const TOUCH_SENSOR_ANALOG_PRESSED = 2600;
+
+ export class TouchSensorNode extends AnalogSensorNode {
+ id = NodeType.TouchSensor;
+
+ private pressed: boolean[];
+
+ constructor(port: number) {
+ super(port);
+ this.pressed = [];
+ }
+
+ public setPressed(pressed: boolean) {
+ this.pressed.push(pressed);
+ this.changed = true;
+ this.valueChanged = true;
+ }
+
+ public isPressed() {
+ return this.pressed;
+ }
+
+ public getValue() {
+ if (this.pressed.length) {
+ if (this.pressed.pop())
+ return TOUCH_SENSOR_ANALOG_PRESSED;
+ }
+ return 0;
+ }
+
+ getDeviceType() {
+ return DAL.DEVICE_TYPE_TOUCH;
+ }
+
+ public hasData() {
+ return this.pressed.length > 0;
+ }
+ }
+}
+
diff --git a/sim/state/uart.ts b/sim/state/uart.ts
new file mode 100644
index 00000000..eaf8fb92
--- /dev/null
+++ b/sim/state/uart.ts
@@ -0,0 +1,156 @@
+namespace pxsim {
+
+ enum UartOff {
+ TypeData = 0, // Types[8][4]
+ Repeat = 1792, // uint16[300][4]
+ Raw = 4192, // int8[32][300][4]
+ Actual = 42592, // uint16[4]
+ LogIn = 42600, // uint16[4]
+ Status = 42608, // int8[4]
+ Output = 42612, // int8[32][4]
+ OutputLength = 42740, // int8[4]
+ Size = 42744
+ }
+
+ enum UartStatus {
+ UART_PORT_CHANGED = 1,
+ UART_DATA_READY = 8
+ }
+
+ enum IO {
+ UART_SET_CONN = 0xc00c7500,
+ UART_READ_MODE_INFO = 0xc03c7501,
+ UART_NACK_MODE_INFO = 0xc03c7502,
+ UART_CLEAR_CHANGED = 0xc03c7503,
+ IIC_SET_CONN = 0xc00c6902,
+ IIC_READ_TYPE_INFO = 0xc03c6903,
+ IIC_SETUP = 0xc04c6905,
+ IIC_SET = 0xc02c6906,
+ TST_PIN_ON = 0xc00b7401,
+ TST_PIN_OFF = 0xc00b7402,
+ TST_PIN_READ = 0xc00b7403,
+ TST_PIN_WRITE = 0xc00b7404,
+ TST_UART_ON = 0xc0487405,
+ TST_UART_OFF = 0xc0487406,
+ TST_UART_EN = 0xc0487407,
+ TST_UART_DIS = 0xc0487408,
+ TST_UART_READ = 0xc0487409,
+ TST_UART_WRITE = 0xc048740a,
+ }
+
+
+ enum DevConOff {
+ Connection = 0, // int8[4]
+ Type = 4, // int8[4]
+ Mode = 8, // int8[4]
+ Size = 12
+ }
+
+ enum UartCtlOff {
+ TypeData = 0, // Types
+ Port = 56, // int8
+ Mode = 57, // int8
+ Size = 58
+ }
+
+ enum TypesOff {
+ Name = 0, // int8[12]
+ Type = 12, // int8
+ Connection = 13, // int8
+ Mode = 14, // int8
+ DataSets = 15, // int8
+ Format = 16, // int8
+ Figures = 17, // int8
+ Decimals = 18, // int8
+ Views = 19, // int8
+ RawMin = 20, // float32
+ RawMax = 24, // float32
+ PctMin = 28, // float32
+ PctMax = 32, // float32
+ SiMin = 36, // float32
+ SiMax = 40, // float32
+ InvalidTime = 44, // uint16
+ IdValue = 46, // uint16
+ Pins = 48, // int8
+ Symbol = 49, // int8[5]
+ Align = 54, // uint16
+ Size = 56
+ }
+
+ export class EV3UArtState {
+
+ constructor() {
+ let data = new Uint8Array(UartOff.Size);
+ MMapMethods.register("/dev/lms_uart", {
+ data,
+ beforeMemRead: () => {
+ //console.log("uart before read");
+ const inputNodes = ev3board().getInputNodes();
+ for (let port = 0; port < DAL.NUM_INPUTS; port++) {
+ const node = inputNodes[port];
+ if (node) {
+ // Actual
+ const index = 0; //UartOff.Actual + port * 2;
+ data[UartOff.Raw + DAL.MAX_DEVICE_DATALENGTH * 300 * port + DAL.MAX_DEVICE_DATALENGTH * index] = node.getValue();
+ // Status
+ data[UartOff.Status + port] = node.valueChange() ? UartStatus.UART_PORT_CHANGED : UartStatus.UART_DATA_READY;
+ }
+ }
+ },
+ read: buf => {
+ let v = "vSIM"
+ // for (let i = 0; i < buf.data.length; ++i)
+ // buf.data[i] = v.charCodeAt(i) || 0
+ console.log("uart read");
+ console.log(buf.data);
+ return buf.data.length
+ },
+ write: buf => {
+ console.log("uart write");
+ console.log(buf);
+ return 2
+ },
+ ioctl: (id, buf) => {
+ switch (id) {
+ case IO.UART_SET_CONN: {
+ // Set mode
+ console.log("IO.UART_SET_CONN");
+ for (let port = 0; port < DAL.NUM_INPUTS; port++) {
+ const connection = buf.data[DevConOff.Connection + port]; // CONN_NONE, CONN_INPUT_UART
+ const type = buf.data[DevConOff.Type + port];
+ const mode = buf.data[DevConOff.Mode + port];
+ console.log(`${port}, mode: ${mode}`)
+ const node = ev3board().getInputNodes()[port];
+ if (node) node.setMode(mode);
+ }
+ return 2;
+ }
+ case IO.UART_CLEAR_CHANGED: {
+ console.log("IO.UART_CLEAR_CHANGED")
+ for (let port = 0; port < DAL.NUM_INPUTS; port++) {
+ const connection = buf.data[DevConOff.Connection + port]; // CONN_NONE, CONN_INPUT_UART
+ const type = buf.data[DevConOff.Type + port];
+ const mode = buf.data[DevConOff.Mode + port];
+ const node = ev3board().getInputNodes()[port];
+ if (node) node.setMode(mode);
+ }
+ return 2;
+ }
+ case IO.UART_READ_MODE_INFO: {
+ console.log("IO.UART_READ_MODE_INFO")
+ const port = buf.data[UartCtlOff.Port];
+ const mode = buf.data[UartCtlOff.Mode];
+ const node = ev3board().getInputNodes()[port];
+ if (node) buf.data[UartCtlOff.TypeData + TypesOff.Type] = node.getDeviceType(); // DEVICE_TYPE_NONE, DEVICE_TYPE_TOUCH,
+ return 2;
+ }
+ }
+ console.log("uart ioctl");
+ console.log(id);
+ console.log(buf);
+ return 2;
+ }
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/ultrasonic.ts b/sim/state/ultrasonic.ts
new file mode 100644
index 00000000..dcee3213
--- /dev/null
+++ b/sim/state/ultrasonic.ts
@@ -0,0 +1,31 @@
+///
+
+namespace pxsim {
+ export class UltrasonicSensorNode extends UartSensorNode {
+ id = NodeType.UltrasonicSensor;
+
+ private distance: number = 50;
+
+ constructor(port: number) {
+ super(port);
+ }
+
+ getDeviceType() {
+ return DAL.DEVICE_TYPE_ULTRASONIC;
+ }
+
+ setDistance(distance: number) {
+ if (this.distance != distance) {
+ this.distance = distance;
+ this.changed = true;
+ this.valueChanged = true;
+
+ runtime.queueDisplayUpdate();
+ }
+ }
+
+ getValue() {
+ return this.distance;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/state/util.ts b/sim/state/util.ts
new file mode 100644
index 00000000..31ce4feb
--- /dev/null
+++ b/sim/state/util.ts
@@ -0,0 +1,7 @@
+namespace pxsim.util {
+
+ export function map16Bit(buffer: Uint8Array, index: number, value: number) {
+ buffer[index] = (value >> 8) & 0xff;
+ buffer[index+1] = value & 0xff;
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/assets/Color Sensor.svg b/sim/visuals/assets/Color Sensor.svg
new file mode 100644
index 00000000..5b5d6de7
--- /dev/null
+++ b/sim/visuals/assets/Color Sensor.svg
@@ -0,0 +1,32 @@
+
diff --git a/sim/visuals/assets/ColorSensorsvg.ts b/sim/visuals/assets/ColorSensorsvg.ts
new file mode 100644
index 00000000..7863b173
--- /dev/null
+++ b/sim/visuals/assets/ColorSensorsvg.ts
@@ -0,0 +1,34 @@
+namespace pxsim {
+ export const COLOR_SENSOR_SVG = ``;
+}
\ No newline at end of file
diff --git a/sim/visuals/assets/EV3.svg b/sim/visuals/assets/EV3.svg
new file mode 100644
index 00000000..7dbd5fa7
--- /dev/null
+++ b/sim/visuals/assets/EV3.svg
@@ -0,0 +1,103 @@
+
diff --git a/sim/visuals/assets/EV3svg.ts b/sim/visuals/assets/EV3svg.ts
new file mode 100644
index 00000000..da81e2f6
--- /dev/null
+++ b/sim/visuals/assets/EV3svg.ts
@@ -0,0 +1,108 @@
+
+namespace pxsim.visuals {
+ export const EV3_SVG = ``;
+}
diff --git a/sim/visuals/assets/Large Motor.svg b/sim/visuals/assets/Large Motor.svg
new file mode 100644
index 00000000..68a857fe
--- /dev/null
+++ b/sim/visuals/assets/Large Motor.svg
@@ -0,0 +1,74 @@
+
diff --git a/sim/visuals/assets/LargeMotorsvg.ts b/sim/visuals/assets/LargeMotorsvg.ts
new file mode 100644
index 00000000..3e791aac
--- /dev/null
+++ b/sim/visuals/assets/LargeMotorsvg.ts
@@ -0,0 +1,76 @@
+namespace pxsim {
+ export const LARGE_MOTOR_SVG = ``;
+}
\ No newline at end of file
diff --git a/sim/visuals/assets/MediumMotor.svg b/sim/visuals/assets/MediumMotor.svg
new file mode 100644
index 00000000..be04262c
--- /dev/null
+++ b/sim/visuals/assets/MediumMotor.svg
@@ -0,0 +1,28 @@
+
diff --git a/sim/visuals/assets/MediumMotorsvg.ts b/sim/visuals/assets/MediumMotorsvg.ts
new file mode 100644
index 00000000..32354672
--- /dev/null
+++ b/sim/visuals/assets/MediumMotorsvg.ts
@@ -0,0 +1,30 @@
+namespace pxsim.visuals {
+ export const MEDIUM_MOTOR_SVG = ``;
+}
\ No newline at end of file
diff --git a/sim/visuals/assets/Portsvg.ts b/sim/visuals/assets/Portsvg.ts
new file mode 100644
index 00000000..4f5f9475
--- /dev/null
+++ b/sim/visuals/assets/Portsvg.ts
@@ -0,0 +1,13 @@
+
+namespace pxsim.visuals {
+ export const PORT_SVG = ``;
+}
\ No newline at end of file
diff --git a/sim/visuals/assets/Touch sensor.svg b/sim/visuals/assets/Touch sensor.svg
new file mode 100644
index 00000000..a2ebeb69
--- /dev/null
+++ b/sim/visuals/assets/Touch sensor.svg
@@ -0,0 +1,61 @@
+
diff --git a/sim/visuals/assets/TouchSensorsvg.ts b/sim/visuals/assets/TouchSensorsvg.ts
new file mode 100644
index 00000000..ec82b7b2
--- /dev/null
+++ b/sim/visuals/assets/TouchSensorsvg.ts
@@ -0,0 +1,64 @@
+
+namespace pxsim.visuals {
+ export const TOUCH_SENSOR_SVG = ``;
+}
diff --git a/sim/visuals/assets/gyro.svg b/sim/visuals/assets/gyro.svg
new file mode 100644
index 00000000..0dfdd4cc
--- /dev/null
+++ b/sim/visuals/assets/gyro.svg
@@ -0,0 +1,51 @@
+
diff --git a/sim/visuals/assets/gyrosvg.ts b/sim/visuals/assets/gyrosvg.ts
new file mode 100644
index 00000000..f484ed2f
--- /dev/null
+++ b/sim/visuals/assets/gyrosvg.ts
@@ -0,0 +1,54 @@
+namespace pxsim {
+
+ export const GYRO_SVG = ``;
+}
\ No newline at end of file
diff --git a/sim/visuals/assets/port.svg b/sim/visuals/assets/port.svg
new file mode 100644
index 00000000..78f2de14
--- /dev/null
+++ b/sim/visuals/assets/port.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/sim/visuals/assets/ultra sonic.svg b/sim/visuals/assets/ultra sonic.svg
new file mode 100644
index 00000000..c92a8484
--- /dev/null
+++ b/sim/visuals/assets/ultra sonic.svg
@@ -0,0 +1,77 @@
+
diff --git a/sim/visuals/assets/ultrasonicsvg.ts b/sim/visuals/assets/ultrasonicsvg.ts
new file mode 100644
index 00000000..9c9191a6
--- /dev/null
+++ b/sim/visuals/assets/ultrasonicsvg.ts
@@ -0,0 +1,79 @@
+namespace pxsim {
+ export const ULTRASONIC_SVG = ``;
+}
\ No newline at end of file
diff --git a/sim/visuals/board.svg b/sim/visuals/board.svg
deleted file mode 100644
index 5c777f03..00000000
--- a/sim/visuals/board.svg
+++ /dev/null
@@ -1,1198 +0,0 @@
-
-
-
-
diff --git a/sim/visuals/board.ts b/sim/visuals/board.ts
index c6d1fafb..d74b6359 100644
--- a/sim/visuals/board.ts
+++ b/sim/visuals/board.ts
@@ -1,5 +1,8 @@
+///
+
namespace pxsim.visuals {
- const MB_STYLE = `
+
+ const EV3_STYLE = `
svg.sim {
margin-bottom:1em;
}
@@ -22,26 +25,6 @@ namespace pxsim.visuals {
stroke-width: 1px;
}
- .sim-light-level-button {
- stroke:#f1c40f;
- stroke-width: 1px;
- }
-
- .sim-pin-level-button {
- stroke:darkorange;
- stroke-width: 1px;
- }
-
- .sim-sound-level-button {
- stroke:#7f8c8d;
- stroke-width: 1px;
- }
-
- .sim-antenna {
- stroke:#555;
- stroke-width: 2px;
- }
-
.sim-text {
font-family:"Lucida Console", Monaco, monospace;
font-size:8px;
@@ -55,170 +38,38 @@ namespace pxsim.visuals {
fill:#000;
}
- .sim-text-pin {
- font-family:"Lucida Console", Monaco, monospace;
- font-size:5px;
- fill:#fff;
- pointer-events: none;
- }
-
- .sim-thermometer {
- stroke:#aaa;
- stroke-width: 1px;
- }
-
- #rgbledcircle:hover {
- r:8px;
- }
-
- #SLIDE_HOVER {
+ /* Color Grid */
+ .sim-color-grid-circle:hover {
+ stroke-width: 0.4;
+ stroke: #000;
cursor: pointer;
}
- .sim-slide-switch:hover #SLIDE_HOVER {
- stroke:orange !important;
- stroke-width: 1px;
- }
-
- .sim-slide-switch-inner.on {
- fill:#ff0000 !important;
- }
-
- /* animations */
- .sim-theme-glow {
- animation-name: sim-theme-glow-animation;
- animation-timing-function: ease-in-out;
- animation-direction: alternate;
- animation-iteration-count: infinite;
- animation-duration: 1.25s;
- }
- @keyframes sim-theme-glow-animation {
- from { opacity: 1; }
- to { opacity: 0.75; }
- }
-
- .sim-flash {
- animation-name: sim-flash-animation;
- animation-duration: 0.1s;
- }
-
- @keyframes sim-flash-animation {
- from { fill: yellow; }
- to { fill: default; }
- }
-
- .sim-flash-stroke {
- z-index: 0;
- animation-name: sim-flash-stroke-animation;
- animation-duration: 0.4s;
- animation-timing-function: ease-in;
- }
-
- @keyframes sim-flash-stroke-animation {
- from { stroke: yellow; }
- to { stroke: default; }
- }
-
-
- .sim-sound-stroke {
- animation-name: sim-sound-stroke-animation;
- animation-duration: 0.4s;
- }
-
- @keyframes sim-sound-stroke-animation {
- from { stroke: yellow; }
- to { stroke: default; }
- }
-
- /* wireframe */
- .sim-wireframe * {
- fill: none;
- stroke: black;
- }
- .sim-wireframe .sim-display,
- .sim-wireframe .sim-led,
- .sim-wireframe .sim-led-back,
- .sim-wireframe .sim-head,
- .sim-wireframe .sim-theme,
- .sim-wireframe .sim-button-group,
- .sim-wireframe .sim-button-label,
- .sim-wireframe .sim-button,
- .sim-wireframe .sim-text-pin
- {
- visibility: hidden;
- }
- .sim-wireframe .sim-label
- {
- stroke: none;
- fill: #777;
- }
- .sim-wireframe .sim-board {
- stroke-width: 2px;
+ .sim-color-wheel-half:hover {
+ stroke-width: 1;
+ stroke: #000;
+ fill: gray !important;
+ cursor: pointer;
}
`;
- const pinNames: { 'name': string, 'touch': number, 'text': any, 'id'?: number, tooltip?: string }[] = [
- { 'name': "PIN_A0", 'touch': 0, 'text': null, 'id': pxsim.CPlayPinName.A0, tooltip: "A0 - Speaker" },
- { 'name': "PIN_A1", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A1, tooltip: "~A1" },
- { 'name': "PIN_A2", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A2, tooltip: "~A2" },
- { 'name': "PIN_A3", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A3, tooltip: "~A3" },
- { 'name': "PIN_A4", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A4, tooltip: "A4 - SCL" },
- { 'name': "PIN_A5", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A5, tooltip: "A5 - SDA" },
- { 'name': "PIN_A6", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A6, tooltip: "A6 - RX" },
- { 'name': "PIN_A7", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A7, tooltip: "A7 - TX" },
- { 'name': "GND_0", 'touch': 0, 'text': null, tooltip: "Ground" },
- { 'name': "GND_1", 'touch': 0, 'text': null, tooltip: "Ground" },
- { 'name': "GND_2", 'touch': 0, 'text': null, tooltip: "Ground" },
- { 'name': "VBATT", 'touch': 0, 'text': null, tooltip: "Battery power" },
- { 'name': "PWR_0", 'touch': 0, 'text': null, tooltip: "+3.3V" },
- { 'name': "PWR_1", 'touch': 0, 'text': null, tooltip: "+3.3V" },
- { 'name': "PWR_2", 'touch': 0, 'text': null, tooltip: "+3.3V" }
- ];
- const MB_WIDTH = 99.984346;
- const MB_HEIGHT = 151.66585;
+ const EV3_WIDTH = 99.984346;
+ const EV3_HEIGHT = 151.66585;
export const SCREEN_WIDTH = 178;
export const SCREEN_HEIGHT = 128;
export interface IBoardTheme {
accent?: string;
display?: string;
- pin?: string;
- pinTouched?: string;
- pinActive?: string;
- ledOn?: string;
- ledOff?: string;
buttonOuter?: string;
buttonUps: string[];
buttonDown?: string;
- virtualButtonOuter?: string;
- virtualButtonUp?: string;
- virtualButtonDown?: string;
- lightLevelOn?: string;
- lightLevelOff?: string;
- soundLevelOn?: string;
- soundLevelOff?: string;
- gestureButtonOn?: string;
- gestureButtonOff?: string;
}
export var themes: IBoardTheme[] = ["#3ADCFE"].map(accent => {
return {
accent: accent,
- pin: "#D4AF37",
- pinTouched: "#FFA500",
- pinActive: "#FF5500",
- ledOn: "#ff7777",
- ledOff: "#fff",
buttonOuter: "#979797",
- buttonUps: ["#FFF", "#4D4D4D", "#FFF", "#FFF", "#FFF", "#FFF", '#FFF'],
- buttonDown: "#000",
- virtualButtonDown: "#FFA500",
- virtualButtonOuter: "#333",
- virtualButtonUp: "#FFF",
- lightLevelOn: "yellow",
- lightLevelOff: "#555",
- soundLevelOn: "#7f8c8d",
- soundLevelOff: "#555",
- gestureButtonOn: "#FFA500",
- gestureButtonOff: "#B4009E"
+ buttonUps: ["#a8aaa8", "#393939", "#a8aaa8", "#a8aaa8", "#a8aaa8", '#a8aaa8'],
+ buttonDown: "#000"
}
});
@@ -233,651 +84,357 @@ namespace pxsim.visuals {
wireframe?: boolean;
}
- export class EV3BoardSvg implements BoardView {
+ export class EV3View implements BoardView {
+ public static BOARD_WIDTH = 500;
+ public static BOARD_HEIGHT = 500;
+
+ public wrapper: HTMLDivElement;
public element: SVGSVGElement;
private style: SVGStyleElement;
private defs: SVGDefsElement;
- private g: SVGGElement;
- private buttons: SVGElement[];
- private buttonABText: SVGTextElement;
- private light: SVGElement;
+ private layoutView: LayoutView;
+
+ private controlGroup: ViewContainer;
+ private selectedNode: NodeType;
+ private selectedPort: number;
+ private controlView: View;
+ private cachedControlNodes: { [index: string]: View[] } = {};
+ private cachedDisplayViews: { [index: string]: LayoutElement[] } = {};
+
+ private closeGroup: ViewContainer;
+ private closeIconView: View;
+
private screenCanvas: HTMLCanvasElement;
private screenCanvasCtx: CanvasRenderingContext2D;
private screenCanvasData: ImageData;
- private screenXYText: SVGTextElement;
- private pins: SVGElement[];
- private pinControls: { [index: number]: AnalogPinControl };
- private systemLed: SVGCircleElement;
- private irReceiver: SVGElement;
- private irTransmitter: SVGElement;
- private redLED: SVGRectElement;
- private slideSwitch: SVGGElement;
- private lightLevelButton: SVGCircleElement;
- private lightLevelGradient: SVGLinearGradientElement;
- private lightLevelText: SVGTextElement;
- private soundLevelButton: SVGCircleElement;
- private soundLevelGradient: SVGLinearGradientElement;
- private soundLevelText: SVGTextElement;
- private thermometerGradient: SVGLinearGradientElement;
- private thermometer: SVGRectElement;
- private thermometerText: SVGTextElement;
- private antenna: SVGPolylineElement;
- private shakeButtonGroup: SVGElement;
- private shakeText: SVGTextElement;
- public board: pxsim.DalBoard;
- private pinNmToCoord: Map = {
- };
+
+ private screenCanvasTemp: HTMLCanvasElement;
+
+ private screenScaledWidth: number;
+ private screenScaledHeight: number;
+
+ private width = 0;
+ private height = 0;
+
+ private g: SVGGElement;
+
+ public board: pxsim.EV3Board;
constructor(public props: IBoardProps) {
this.buildDom();
+ const dalBoard = board();
+ dalBoard.updateSubscribers.push(() => this.updateState());
if (props && props.wireframe)
svg.addClass(this.element, "sim-wireframe");
- /*
if (props && props.theme)
this.updateTheme();
- */
+
if (props && props.runtime) {
- this.board = this.props.runtime.board as pxsim.DalBoard;
+ this.board = this.props.runtime.board as pxsim.EV3Board;
this.board.updateSubscribers.push(() => this.updateState());
this.updateState();
- this.attachEvents();
}
-
- let board = this;
- window.setInterval(function(){
- board.updateScreen();
- }, 30);
}
public getView(): SVGAndSize {
return {
- el: this.element,
+ el: this.wrapper as any,
y: 0,
x: 0,
- w: MB_WIDTH,
- h: MB_HEIGHT
+ w: EV3View.BOARD_WIDTH,
+ h: EV3View.BOARD_WIDTH
};
}
public getCoord(pinNm: string): Coord {
- return this.pinNmToCoord[pinNm];
+ // Not needed
+ return undefined;
}
public highlightPin(pinNm: string): void {
- //TODO: for instructions
+ // Not needed
}
public getPinDist(): number {
+ // Not needed
return 10;
}
- private recordPinCoords() {
- pinNames.forEach((pin, i) => {
- const nm = pin.name;
- const p = this.pins[i];
- const r = p.getBoundingClientRect();
- this.pinNmToCoord[nm] = [r.left + r.width / 2, r.top + r.height / 2];
- });
- console.log(JSON.stringify(this.pinNmToCoord, null, 2))
- }
-
- private updateTheme() {
+ public updateTheme() {
let theme = this.props.theme;
-
- svg.fill(this.buttons[0], theme.buttonUps[0]);
- svg.fill(this.buttons[1], theme.buttonUps[1]);
- svg.fill(this.buttons[2], theme.buttonUps[2]);
-
- if (this.shakeButtonGroup) {
- svg.fill(this.shakeButtonGroup, this.props.theme.gestureButtonOff);
- }
-
- svg.setGradientColors(this.lightLevelGradient, theme.lightLevelOn, theme.lightLevelOff);
-
- svg.setGradientColors(this.thermometerGradient, theme.ledOff, theme.ledOn);
- svg.setGradientColors(this.soundLevelGradient, theme.soundLevelOn, theme.soundLevelOff);
-
- for (const id in this.pinControls) {
- this.pinControls[id].updateTheme();
- }
+ this.layoutView.updateTheme(theme);
}
public updateState() {
- let state = this.board;
- if (!state) return;
- let theme = this.props.theme;
-
- let bpState = state.buttonState;
- let buttons = bpState.buttons;
- this.buttons.forEach((button, i) => {
- svg.fill(button, buttons[i].pressed ? theme.buttonDown : theme.buttonUps[i]);
- })
-
- this.updateLight();
+ this.updateVisibleNodes();
this.updateScreen();
- /*
-
- this.updatePins();
- this.updateTilt();
- this.updateNeoPixels();
- this.updateSwitch();
- this.updateSound();
- this.updateLightLevel();
- this.updateSoundLevel();
- this.updateButtonAB();
- this.updateGestures();
- this.updateTemperature();
- this.updateInfrared();
- */
-
- if (!runtime || runtime.dead) svg.addClass(this.element, "grayscale");
- else svg.removeClass(this.element, "grayscale");
}
- private lastFlashTime: number = 0;
- private flashSystemLed() {
- /*
- if (!this.systemLed)
- this.systemLed = svg.child(this.g, "circle", { class: "sim-systemled", cx: 75, cy: MB_HEIGHT - 171, r: 2 })
- let now = Date.now();
- if (now - this.lastFlashTime > 150) {
- this.lastFlashTime = now;
- svg.animate(this.systemLed, "sim-flash")
- }
- */
- }
-
- private lastIrReceiverFlash: number = 0;
- public flashIrReceiver() {
- /*
- if (!this.irReceiver)
- this.irReceiver = this.element.getElementById("path2054") as SVGElement;
- let now = Date.now();
- if (now - this.lastIrReceiverFlash > 200) {
- this.lastIrReceiverFlash = now;
- svg.animate(this.irReceiver, 'sim-flash-stroke')
- }
- */
- }
-
- private lastIrTransmitterFlash: number = 0;
- public flashIrTransmitter() {
- /*
- if (!this.irTransmitter)
- this.irTransmitter = this.element.getElementById("path2062") as SVGElement;
- let now = Date.now();
- if (now - this.lastIrTransmitterFlash > 200) {
- this.lastIrTransmitterFlash = now;
- svg.animate(this.irTransmitter, 'sim-flash-stroke')
- }*/
- }
-
- private updateInfrared() {
- const state = this.board;
- if (!state) return;
-
- if (state.irState.packetReceived) {
- state.irState.packetReceived = false;
- this.flashIrReceiver();
- }
- }
-
- private lastLightPattern: number = -1;
- private updateLight() {
- let state = this.board;
- if (!state || !state.lightState) return;
-
- const lightPattern = state.lightState.lightPattern;
- if (lightPattern == this.lastLightPattern) return;
- this.lastLightPattern = lightPattern;
- switch(lightPattern) {
- case 0: // LED_BLACK
- svg.fill(this.light, "#FFF");
- break;
- case 1: // LED_GREEN
- svg.fill(this.light, "#00ff00");
- break;
- case 2: // LED_RED
- svg.fill(this.light, "#ff0000");
- break;
- case 3: // LED_ORANGE
- svg.fill(this.light, "#FFA500");
- break;
- case 4: // LED_GREEN_FLASH
- break;
- case 5: // LED_RED_FLASH
- break;
- case 6: // LED_ORANGE_FLASH
- break;
- case 7: // LED_GREEN_PULSE
- break;
- case 8: // LED_RED_PULSE
- break;
- case 9: // LED_ORANGE_PULSE
- break;
- }
- }
-
- private updateScreen() {
- let state = this.board;
- if (!state || !state.screenState) return;
-
- if (!state.screenState.shouldUpdate) return;
- state.screenState.shouldUpdate = false;
-
- this.screenCanvasData = this.screenCanvasCtx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
-
- let sp = 3
- const points = state.screenState.points
- const data = this.screenCanvasData.data
- for (let i = 0; i < points.length; ++i ) {
- data[sp] = points[i]
- sp += 4;
- }
-
- this.screenCanvasCtx.putImageData(this.screenCanvasData, 0, 0);
- }
-
- /*
- private updateNeoPixels() {
- let state = this.board;
- if (!state || !state.neopixelState) return;
- let neopixels = state.neopixelState.getNeoPixels();
- for (let i = 0; i < state.neopixelState.NUM_PIXELS; i++) {
- let rgb = neopixels[i];
- let p_inner = this.element.getElementById(`LED${i}`) as SVGPathElement;
-
- if (!rgb || (rgb.length == 3 && rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0)) {
- // Clear the pixel
- svg.fill(p_inner, `rgb(200,200,200)`);
- svg.filter(p_inner, null);
- p_inner.style.stroke = `none`
- continue;
+ private updateVisibleNodes() {
+ const inputNodes = ev3board().getInputNodes();
+ inputNodes.forEach((node, index) => {
+ const view = this.getDisplayViewForNode(node.id, index);
+ if (view) {
+ this.layoutView.setInput(index, view);
+ view.updateState();
}
+ });
- let hsl = visuals.rgbToHsl(rgb);
- let [h, s, l] = hsl;
- let lx = Math.max(l * 1.3, 85);
- // at least 10% luminosity
- l = l * 90 / 100 + 10;
- if (p_inner) {
- p_inner.style.stroke = `hsl(${h}, ${s}%, ${Math.min(l * 3, 75)}%)`
- p_inner.style.strokeWidth = "1.5";
- svg.fill(p_inner, `hsl(${h}, ${s}%, ${lx}%)`)
+ this.getDisplayViewForNode(ev3board().getBrickNode().id, -1).updateState();
+
+ const outputNodes = ev3board().getMotors();
+ outputNodes.forEach((node, index) => {
+ const view = this.getDisplayViewForNode(node.id, index);
+ if (view) {
+ this.layoutView.setOutput(index, view);
+ view.updateState();
}
- if (p_inner) svg.filter(p_inner, `url(#neopixelglow)`);
+ });
+
+ const selected = this.layoutView.getSelected();
+ if (selected && (selected.getId() !== this.selectedNode || selected.getPort() !== this.selectedPort)) {
+ this.selectedNode = selected.getId();
+ this.selectedPort = selected.getPort();
+ this.controlGroup.clear();
+ const control = this.getControlForNode(this.selectedNode, selected.getPort());
+ if (control) {
+ this.controlView = control;
+ this.controlGroup.addView(control);
+ }
+ this.closeIconView.setVisible(true);
+ } else if (!selected) {
+ this.controlGroup.clear();
+ this.controlView = undefined;
+ this.selectedNode = undefined;
+ this.selectedPort = undefined;
+ this.closeIconView.setVisible(false);
}
- }
- */
- private updateSound() {
- let state = this.board;
- if (!state || !state.audioState) return;
- let audioState = state.audioState;
-
- // FIXME
- // let soundBoard = this.element.getElementById('g4656') as SVGGElement;
- // if (audioState.isPlaying()) {
- // svg.addClass(soundBoard, "sim-sound-stroke");
- // } else {
- // svg.removeClass(soundBoard, "sim-sound-stroke");
- // }
+ this.resize();
}
- private updatePins() {
- let state = this.board;
- if (!state || !state.edgeConnectorState || !state.capacitiveSensorState) return;
- state.edgeConnectorState.pins.forEach((pin, i) => this.updatePin(pin, i));
+ public resize() {
+ const bounds = this.element.getBoundingClientRect();
+ this.width = bounds.width;
+ this.height = bounds.height;
+ this.layoutView.layout(bounds.width, bounds.height);
+
+ if (this.selectedNode) {
+ const scale = this.width / this.closeIconView.getInnerWidth() / 10;
+ // Translate close icon
+ this.closeIconView.scale(Math.max(0, Math.min(1, scale)));
+ const closeIconWidth = this.closeIconView.getWidth();
+ const closeIconHeight = this.closeIconView.getHeight();
+ const closeCoords = this.layoutView.getCloseIconCoords(closeIconWidth, closeIconHeight);
+ this.closeIconView.translate(closeCoords.x, closeCoords.y);
+ }
+
+ if (this.controlView) {
+ const h = this.controlView.getInnerHeight();
+ const w = this.controlView.getInnerWidth();
+ const bh = this.layoutView.getModuleBounds().height - this.closeIconView.getHeight();
+ const bw = this.layoutView.getModuleBounds().width - (this.width * MODULE_INNER_PADDING_RATIO * 2);
+ this.controlView.scale(Math.min(bh / h, bw / w), false);
+
+ const controlCoords = this.layoutView.getSelectedCoords();
+ this.controlView.translate(controlCoords.x, controlCoords.y);
+ }
+
+ this.updateScreen();
}
- private updatePin(pin: Pin, index: number) {
- if (!pin || !this.pins[index]) return;
+ private getControlForNode(id: NodeType, port: number) {
+ if (this.cachedControlNodes[id] && this.cachedControlNodes[id][port]) {
+ return this.cachedControlNodes[id][port];
+ }
- if ((pin as pins.CommonPin).used) {
- if (this.pinControls[pin.id] === undefined) {
- const pinName = pinNames.filter((a) => a.id === pin.id)[0];
- if (pinName) {
- this.pinControls[pin.id] = new AnalogPinControl(this, this.defs, pin.id, pinName.name);
- }
- else {
- // TODO: Surface pin controls for sensor pins in some way?
- this.pinControls[pin.id] = null;
+ let view: View;
+ switch (id) {
+ case NodeType.ColorSensor: {
+ const state = ev3board().getInputNodes()[port] as ColorSensorNode;
+ if (state.getMode() == ColorSensorMode.Colors) {
+ view = new ColorGridControl(this.element, this.defs, state, port);
+ } else if (state.getMode() == ColorSensorMode.Reflected) {
+ view = new ColorWheelControl(this.element, this.defs, state, port);
+ } else if (state.getMode() == ColorSensorMode.Ambient) {
+ view = new ColorWheelControl(this.element, this.defs, state, port);
}
+ break;
}
-
- if (this.pinControls[pin.id]) {
- this.pinControls[pin.id].updateValue();
+ case NodeType.UltrasonicSensor: {
+ const state = ev3board().getInputNodes()[port] as UltrasonicSensorNode;
+ view = new DistanceSliderControl(this.element, this.defs, state, port);
+ break;
+ }
+ case NodeType.GyroSensor: {
+ const state = ev3board().getInputNodes()[port] as GyroSensorNode;
+ view = new RotationSliderControl(this.element, this.defs, state, port);
+ break;
+ }
+ case NodeType.MediumMotor:
+ case NodeType.LargeMotor: {
+ // const state = ev3board().getMotor(port)[0];
+ // view = new MotorInputControl(this.element, this.defs, state, port);
+ // break;
}
}
- }
- private updateLightLevel() {
- let state = this.board;
- if (!state || !state.lightSensorState.sensorUsed) return;
-
- if (!this.lightLevelButton) {
- let gid = "gradient-light-level";
- this.lightLevelGradient = svg.linearGradient(this.defs, gid)
- let cy = 15;
- let r = 10;
- this.lightLevelButton = svg.child(this.g, "circle", {
- cx: `12px`, cy: `${cy}px`, r: `${r}px`,
- class: 'sim-light-level-button',
- fill: `url(#${gid})`
- }) as SVGCircleElement;
- let pt = this.element.createSVGPoint();
- svg.buttonEvents(this.lightLevelButton,
- (ev) => {
- let pos = svg.cursorPoint(pt, this.element, ev);
- let rs = r / 2;
- let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255)));
- if (level != this.board.lightSensorState.getLevel()) {
- this.board.lightSensorState.setLevel(level);
- this.applyLightLevel();
- }
- }, ev => { },
- ev => { })
- this.lightLevelText = svg.child(this.g, "text", { x: 23, y: cy + r - 15, text: '', class: 'sim-text' }) as SVGTextElement;
- this.updateTheme();
+ if (view) {
+ if (!this.cachedControlNodes[id]) this.cachedControlNodes[id] = [];
+ this.cachedControlNodes[id][port] = view;
+ return view;
}
- svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightSensorState.getLevel() * 100 / 255))) + '%')
- this.lightLevelText.textContent = state.lightSensorState.getLevel().toString();
+ return undefined;
}
- private applyLightLevel() {
- let lv = this.board.lightSensorState.getLevel();
- svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(lv * 100 / 255))) + '%')
- this.lightLevelText.textContent = lv.toString();
- }
-
- private updateSoundLevel() {
- let state = this.board;
- if (!state || !state.microphoneState.sensorUsed) return;
-
- if (!this.soundLevelButton) {
- let gid = "gradient-sound-level";
- this.soundLevelGradient = svg.linearGradient(this.defs, gid)
- let cy = 165;
- let r = 10;
- this.soundLevelButton = svg.child(this.g, "circle", {
- cx: `12px`, cy: `${cy}px`, r: `${r}px`,
- class: 'sim-sound-level-button',
- fill: `url(#${gid})`
- }) as SVGCircleElement;
-
- let pt = this.element.createSVGPoint();
- svg.buttonEvents(this.soundLevelButton,
- (ev) => {
- let pos = svg.cursorPoint(pt, this.element, ev);
- let rs = r / 2;
- let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255)));
- if (level != this.board.microphoneState.getLevel()) {
- this.board.microphoneState.setLevel(255 - level);
- this.applySoundLevel();
- }
- }, ev => { },
- ev => { })
- this.soundLevelText = svg.child(this.g, "text", { x: 23, y: cy + r - 3, text: '', class: 'sim-text' }) as SVGTextElement;
- this.updateTheme();
+ private getDisplayViewForNode(id: NodeType, port: number): LayoutElement {
+ if (this.cachedDisplayViews[id] && this.cachedDisplayViews[id][port]) {
+ return this.cachedDisplayViews[id][port];
}
- svg.setGradientValue(this.soundLevelGradient, Math.min(100, Math.max(0, Math.floor((255 - state.microphoneState.getLevel()) * 100 / 255))) + '%')
- this.soundLevelText.textContent = state.microphoneState.getLevel().toString();
- }
-
- private applySoundLevel() {
- let lv = this.board.microphoneState.getLevel();
- svg.setGradientValue(this.soundLevelGradient, Math.min(100, Math.max(0, Math.floor((255 - lv) * 100 / 255))) + '%')
- this.soundLevelText.textContent = lv.toString();
- }
-
- private updateTemperature() {
- let state = this.board;
- if (!state || !state.thermometerState || !state.thermometerState.sensorUsed) return;
-
- // Celsius
- let tmin = -5;
- let tmax = 50;
- if (!this.thermometer) {
- let gid = "gradient-thermometer";
- this.thermometerGradient = svg.linearGradient(this.defs, gid);
- this.thermometer = svg.child(this.g, "rect", {
- class: "sim-thermometer",
- x: 170,
- y: 3,
- width: 7,
- height: 32,
- rx: 2, ry: 2,
- fill: `url(#${gid})`
- });
- this.thermometerText = svg.child(this.g, "text", { class: 'sim-text', x: 148, y: 10 }) as SVGTextElement;
- this.updateTheme();
-
- let pt = this.element.createSVGPoint();
- svg.buttonEvents(this.thermometer,
- (ev) => {
- let cur = svg.cursorPoint(pt, this.element, ev);
- let t = Math.max(0, Math.min(1, (35 - cur.y) / 30))
- state.thermometerState.setLevel(Math.floor(tmin + t * (tmax - tmin)));
- this.updateTemperature();
- }, ev => { }, ev => { })
+ let view: LayoutElement;
+ switch (id) {
+ case NodeType.TouchSensor:
+ view = new TouchSensorView(port); break;
+ case NodeType.MediumMotor:
+ view = new MediumMotorView(port); break;
+ case NodeType.LargeMotor:
+ view = new LargeMotorView(port); break;
+ case NodeType.GyroSensor:
+ view = new GyroSensorView(port); break;
+ case NodeType.ColorSensor:
+ view = new ColorSensorView(port); break;
+ case NodeType.UltrasonicSensor:
+ view = new UltrasonicSensorView(port); break;
+ case NodeType.Brick:
+ //return new BrickView(0);
+ view = this.layoutView.getBrick(); break;
}
- let t = Math.max(tmin, Math.min(tmax, state.thermometerState.getLevel()))
- let per = Math.floor((state.thermometerState.getLevel() - tmin) / (tmax - tmin) * 100)
- svg.setGradientValue(this.thermometerGradient, 100 - per + "%");
-
- let unit = "°C";
- if (state.thermometerUnitState == pxsim.TemperatureUnit.Fahrenheit) {
- unit = "°F";
- t = ((t * 18) / 10 + 32) >> 0;
+ if (view) {
+ if (!this.cachedDisplayViews[id]) this.cachedDisplayViews[id] = [];
+ this.cachedDisplayViews[id][port] = view;
+ return view;
}
- this.thermometerText.textContent = t + unit;
+
+ return undefined;
}
- private updateButtonAB() {
- let state = this.board;
- if (state.buttonState.usesButtonAB) {
- (this.buttons[2]).style.visibility = "visible";
- this.updateTheme();
- }
- }
-
- private updateGestures() {
- let state = this.board;
- if (state.accelerometerState.useShake && !this.shakeButtonGroup) {
- const btnr = 2;
- const width = 22;
- const height = 10;
-
- let btng = svg.child(this.g, "g", { class: "sim-button-group" });
- this.shakeButtonGroup = btng;
- this.shakeText = svg.child(this.g, "text", { x: 81, y: 32, class: "sim-text small" }) as SVGTextElement;
- this.shakeText.textContent = "SHAKE"
-
- svg.child(btng, "rect", { class: "sim-button", x: 79, y: 25, rx: btnr, ry: btnr, width, height });
- svg.fill(btng, this.props.theme.gestureButtonOff);
- this.shakeButtonGroup.addEventListener(pointerEvents.down, ev => {
- let state = this.board;
- svg.fill(btng, this.props.theme.gestureButtonOn);
- svg.addClass(this.shakeText, "inverted");
- })
- this.shakeButtonGroup.addEventListener(pointerEvents.leave, ev => {
- let state = this.board;
- svg.fill(btng, this.props.theme.gestureButtonOff);
- svg.removeClass(this.shakeText, "inverted");
- })
- this.shakeButtonGroup.addEventListener(pointerEvents.up, ev => {
- let state = this.board;
- svg.fill(btng, this.props.theme.gestureButtonOff);
- //this.board.bus.queue(DAL.DEVICE_ID_GESTURE, 11); // GESTURE_SHAKE
- svg.removeClass(this.shakeText, "inverted");
- })
- }
- }
-
- private updateTilt() {
- if (this.props.disableTilt) return;
- let state = this.board;
- if (!state || !state.accelerometerState.accelerometer.isActive) return;
-
- const x = state.accelerometerState.accelerometer.getX();
- const y = state.accelerometerState.accelerometer.getY();
- const af = 8 / 1023;
- const s = 1 - Math.min(0.1, Math.pow(Math.max(Math.abs(x), Math.abs(y)) / 1023, 2) / 35);
-
- this.element.style.transform = `perspective(30em) rotateX(${y * af}deg) rotateY(${x * af}deg) scale(${s}, ${s})`
- this.element.style.perspectiveOrigin = "50% 50% 50%";
- this.element.style.perspective = "30em";
- }
-
- private updateXY() {
- this.screenXYText.textContent = `x:${this.currentCanvasX}, y:${this.currentCanvasY}`;
- }
-
- private currentCanvasX = 178;
- private currentCanvasY = 128;
private buildDom() {
- this.element = new DOMParser().parseFromString(BOARD_SVG, "image/svg+xml").querySelector("svg") as SVGSVGElement;
- svg.hydrate(this.element, {
- "version": "1.0",
- "viewBox": `0 0 ${MB_WIDTH} ${MB_HEIGHT}`,
- "class": "sim",
- "x": "0px",
- "y": "0px",
- "width": MB_WIDTH + "px",
- "height": MB_HEIGHT + "px",
- });
- this.style = svg.child(this.element, "style", {});
- this.style.textContent = MB_STYLE;
+ this.wrapper = document.createElement('div');
+ this.wrapper.style.display = 'inline';
- this.defs = svg.child(this.element, "defs", {});
- this.g = svg.elt("g");
- this.element.appendChild(this.g);
+ this.element = svg.elt("svg", { height: "100%", width: "100%" }) as SVGSVGElement;
- const btnids = ["BTN_4", "BTN_2", "BTN_5", "BTN_3", "BTN_1", "BTN_BACK"];
- this.buttons = btnids.map(n => this.element.getElementById(n) as SVGElement);
- this.buttons.forEach(b => svg.addClass(b, "sim-button"));
+ this.defs = svg.child(this.element, "defs") as SVGDefsElement;
- this.light = this.element.getElementById("BOARD_Light") as SVGElement;
+ this.style = svg.child(this.element, "style", {}) as SVGStyleElement;
+ this.style.textContent = EV3_STYLE;
- const screen = this.element.getElementById("Screen");
- const foreignObjectG = svg.child(screen, "g", {
- transform: "scale(1.75)"
+ this.layoutView = new LayoutView();
+ this.layoutView.inject(this.element);
+
+ this.controlGroup = new ViewContainer();
+ this.controlGroup.inject(this.element);
+
+ this.closeGroup = new ViewContainer();
+ this.closeGroup.inject(this.element);
+
+ // Add EV3 module element
+ this.layoutView.setBrick(new BrickView(-1));
+
+ this.closeIconView = new CloseIconControl(this.element, this.defs, new PortNode(-1), -1);
+ this.closeIconView.registerClick(() => {
+ this.layoutView.clearSelected();
+ this.updateState();
})
- const foreignObject = svg.child(foreignObjectG, "foreignObject", {
- x: "119", y: "105", width: "178", height: "128"
+ this.closeGroup.addView(this.closeIconView);
+ this.closeIconView.setVisible(false);
+
+ this.resize();
+ this.updateState();
+
+ // Add Screen canvas to board
+ this.buildScreenCanvas();
+
+ this.wrapper.appendChild(this.element);
+ this.wrapper.appendChild(this.screenCanvas);
+ this.wrapper.appendChild(this.screenCanvasTemp);
+
+ window.addEventListener("resize", e => {
+ this.resize();
});
+ }
- const foBody = document.createElementNS("http://www.w3.org/1999/xhtml", "body") as HTMLElement;
- foBody.style.width = `${SCREEN_WIDTH}px`;
- foBody.style.height = `${SCREEN_HEIGHT}px`;
- foBody.style.position = 'fixed';
- foBody.style.backgroundColor = `none`;
- foreignObject.appendChild(foBody);
-
+ private buildScreenCanvas() {
this.screenCanvas = document.createElement("canvas");
- this.screenCanvas.id = "Screen_canvas";
+ this.screenCanvas.id = "board-screen-canvas";
+ this.screenCanvas.style.position = "absolute";
this.screenCanvas.style.cursor = "crosshair";
this.screenCanvas.onmousemove = (e: MouseEvent) => {
const x = e.clientX;
const y = e.clientY;
- this.currentCanvasX = x;
- this.currentCanvasY = y;
- this.updateXY();
+ const bBox = this.screenCanvas.getBoundingClientRect();
+ this.updateXY(Math.floor((x - bBox.left) / this.screenScaledWidth * SCREEN_WIDTH),
+ Math.floor((y - bBox.top) / this.screenScaledHeight * SCREEN_HEIGHT));
}
this.screenCanvas.onmouseleave = () => {
- this.currentCanvasX = SCREEN_WIDTH;
- this.currentCanvasY = SCREEN_HEIGHT;
- this.updateXY();
+ this.updateXY(SCREEN_WIDTH, SCREEN_HEIGHT);
}
- foBody.appendChild(this.screenCanvas);
- //foreignObject.appendChild(this.screenCanvas);
this.screenCanvas.width = SCREEN_WIDTH;
this.screenCanvas.height = SCREEN_HEIGHT;
+
this.screenCanvasCtx = this.screenCanvas.getContext("2d");
- this.screenXYText = this.element.getElementById('xyPos') as SVGTextElement;
- this.updateXY();
+ this.screenCanvasTemp = document.createElement("canvas");
+ this.screenCanvasTemp.style.display = 'none';
}
- private attachEvents() {
- Runtime.messagePosted = (msg) => {
- switch (msg.type || "") {
- case "serial": this.flashSystemLed(); break;
- case "irpacket": this.flashIrTransmitter(); break;
- }
+ private updateScreen() {
+ let state = ev3board().screenState;
+
+ const bBox = this.layoutView.getBrick().getScreenBBox();
+ if (!bBox || bBox.width == 0) return;
+
+ const scale = bBox.width / SCREEN_WIDTH;
+ this.screenScaledWidth = bBox.width;
+ this.screenScaledHeight = this.screenScaledWidth / SCREEN_WIDTH * SCREEN_HEIGHT;
+
+ this.screenCanvas.style.top = `${bBox.top}px`;
+ this.screenCanvas.style.left = `${bBox.left}px`;
+ this.screenCanvas.width = this.screenScaledWidth;
+ this.screenCanvas.height = this.screenScaledHeight;
+
+ this.screenCanvasData = this.screenCanvasCtx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
+
+ let sp = 3
+ const points = state.points
+ const data = this.screenCanvasData.data
+ for (let i = 0; i < points.length; ++i) {
+ data[sp] = points[i]
+ sp += 4;
}
- /*
- let tiltDecayer = 0;
- this.element.addEventListener(pointerEvents.move, (ev: MouseEvent) => {
- let state = this.board;
- if (!state.accelerometerState.accelerometer.isActive) return;
+ // Move the image to another canvas element in order to scale it
+ this.screenCanvasTemp.style.width = `${SCREEN_WIDTH}`;
+ this.screenCanvasTemp.style.height = `${SCREEN_HEIGHT}`;
- if (tiltDecayer) {
- clearInterval(tiltDecayer);
- tiltDecayer = 0;
- }
+ this.screenCanvasTemp.getContext("2d").putImageData(this.screenCanvasData, 0, 0);
- let bbox = this.element.getBoundingClientRect();
- let ax = (ev.clientX - bbox.width / 2) / (bbox.width / 3);
- let ay = (ev.clientY - bbox.height / 2) / (bbox.height / 3);
+ this.screenCanvasCtx.scale(scale, scale);
+ this.screenCanvasCtx.drawImage(this.screenCanvasTemp, 0, 0);
+ }
- let x = - Math.max(- 1023, Math.min(1023, Math.floor(ax * 1023)));
- let y = Math.max(- 1023, Math.min(1023, Math.floor(ay * 1023)));
- let z2 = 1023 * 1023 - x * x - y * y;
- let z = Math.floor((z2 > 0 ? -1 : 1) * Math.sqrt(Math.abs(z2)));
+ private updateXY(width: number, height: number) {
+ const screenWidth = Math.max(0, Math.min(SCREEN_WIDTH, width));
+ const screenHeight = Math.max(0, Math.min(SCREEN_HEIGHT, height));
+ console.log(`width: ${screenWidth}, height: ${screenHeight}`);
- state.accelerometerState.accelerometer.update(x, y, z);
- this.updateTilt();
- }, false);
- this.element.addEventListener(pointerEvents.leave, (ev: MouseEvent) => {
- let state = this.board;
- if (!state.accelerometerState.accelerometer.isActive) return;
-
- if (!tiltDecayer) {
- tiltDecayer = setInterval(() => {
- let accx = state.accelerometerState.accelerometer.getX(MicroBitCoordinateSystem.RAW);
- accx = Math.floor(Math.abs(accx) * 0.85) * (accx > 0 ? 1 : -1);
- let accy = state.accelerometerState.accelerometer.getY(MicroBitCoordinateSystem.RAW);
- accy = Math.floor(Math.abs(accy) * 0.85) * (accy > 0 ? 1 : -1);
- let accz = -Math.sqrt(Math.max(0, 1023 * 1023 - accx * accx - accy * accy));
- if (Math.abs(accx) <= 24 && Math.abs(accy) <= 24) {
- clearInterval(tiltDecayer);
- tiltDecayer = 0;
- accx = 0;
- accy = 0;
- accz = -1023;
- }
- state.accelerometerState.accelerometer.update(accx, accy, accz);
- this.updateTilt();
- }, 50)
- }
- }, false);
- */
- let bpState = this.board.buttonState;
- let stateButtons = bpState.buttons;
- this.buttons.forEach((btn, index) => {
- let button = stateButtons[index];
-
- btn.addEventListener(pointerEvents.down, ev => {
- button.setPressed(true);
- svg.fill(this.buttons[index], this.props.theme.buttonDown);
- })
- btn.addEventListener(pointerEvents.leave, ev => {
- button.setPressed(false);
- svg.fill(this.buttons[index], this.props.theme.buttonUps[index]);
- })
- btn.addEventListener(pointerEvents.up, ev => {
- button.setPressed(false);
- svg.fill(this.buttons[index], this.props.theme.buttonUps[index]);
- })
- })
+ // TODO: add a reporter for the hovered XY position
}
}
}
\ No newline at end of file
diff --git a/sim/visuals/boardsvg.ts b/sim/visuals/boardsvg.ts
deleted file mode 100644
index 1a703aca..00000000
--- a/sim/visuals/boardsvg.ts
+++ /dev/null
@@ -1,1200 +0,0 @@
-namespace pxsim.visuals {
- export const BOARD_SVG = `
-
-
-`;
-}
\ No newline at end of file
diff --git a/sim/visuals/boardview.ts b/sim/visuals/boardview.ts
index aa79f666..e7c140ba 100644
--- a/sim/visuals/boardview.ts
+++ b/sim/visuals/boardview.ts
@@ -1,6 +1,6 @@
namespace pxsim.visuals {
mkBoardView = (opts: BoardViewOptions): BoardView => {
- return new visuals.EV3BoardSvg({
+ return new visuals.EV3View({
runtime: runtime,
theme: visuals.randomTheme(),
disableTilt: false,
diff --git a/sim/visuals/controlView.ts b/sim/visuals/controlView.ts
new file mode 100644
index 00000000..6d0fa6a9
--- /dev/null
+++ b/sim/visuals/controlView.ts
@@ -0,0 +1,51 @@
+///
+
+namespace pxsim.visuals {
+
+ export const CONTROL_WIDTH = 87.5;
+ export const CONTROL_HEIGHT = 175;
+
+ export abstract class ControlView extends SimView implements LayoutElement {
+ private background: SVGSVGElement;
+
+ abstract getInnerView(parent: SVGSVGElement, globalDefs: SVGDefsElement): SVGElement;
+
+ constructor(protected parent: SVGSVGElement, protected globalDefs: SVGDefsElement, protected state: T, protected port: number) {
+ super(state);
+ }
+
+ getInnerWidth(): number {
+ return CONTROL_WIDTH;
+ }
+
+ getInnerHeight(): number {
+ return CONTROL_HEIGHT;
+ }
+
+ getPaddingRatio() {
+ return 0;
+ }
+
+ getWiringRatio() {
+ return 0.5;
+ }
+
+ public hasClick() {
+ return false;
+ }
+
+ buildDom(width: number): SVGElement {
+ this.background = svg.elt("svg", { height: "100%", width: "100%"}) as SVGSVGElement;
+ this.background.appendChild(this.getInnerView(this.parent, this.globalDefs));
+ return this.background;
+ }
+
+ onComponentVisible() {
+
+ }
+
+ getWeight() {
+ return 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/controls/closeIcon.ts b/sim/visuals/controls/closeIcon.ts
new file mode 100644
index 00000000..02b5307f
--- /dev/null
+++ b/sim/visuals/controls/closeIcon.ts
@@ -0,0 +1,29 @@
+
+
+namespace pxsim.visuals {
+
+ export class CloseIconControl extends ControlView {
+ private closeGroup: SVGGElement;
+
+ getInnerView() {
+ this.closeGroup = svg.elt("g") as SVGGElement;
+ this.closeGroup.style.cursor = 'pointer';
+ const circleCloseWrapper = pxsim.svg.child(this.closeGroup, "g");
+ pxsim.svg.child(circleCloseWrapper, "circle", { 'cx': "16", 'cy': "16", 'r': "16", 'style': "fill: #fff" });
+ pxsim.svg.child(circleCloseWrapper, "circle", { 'cx': "16", 'cy': "16", 'r': "15", 'style': "fill: none;stroke: #a8aaa8;stroke-width: 2px" });
+ pxsim.svg.child(this.closeGroup, "rect", { 'x': "10", 'y': "16", 'width': "18", 'height': "2", 'transform': "translate(-9.46 17.41) rotate(-45)", 'style': "fill: #a8aaa8" });
+ pxsim.svg.child(this.closeGroup, "rect", { 'x': "18", 'y': "8", 'width': "2", 'height': "18", 'transform': "translate(-9.46 17.41) rotate(-45)", 'style': "fill: #a8aaa8" });
+
+ return this.closeGroup;
+ }
+
+ public getInnerHeight() {
+ return 32;
+ }
+
+ public getInnerWidth() {
+ return 32;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sim/visuals/controls/colorGrid.ts b/sim/visuals/controls/colorGrid.ts
new file mode 100644
index 00000000..f8faca73
--- /dev/null
+++ b/sim/visuals/controls/colorGrid.ts
@@ -0,0 +1,41 @@
+
+
+namespace pxsim.visuals {
+
+ export class ColorGridControl extends ControlView {
+ private group: SVGGElement;
+
+ getInnerView() {
+ this.group = svg.elt("g") as SVGGElement;
+ this.group.setAttribute("transform", `translate(17, ${35 + this.getHeight() / 4}) scale(5)`)
+
+ const colorIds = ['red', 'yellow', 'blue', 'green', 'black', 'grey'];
+ const colors = ['#f12a21', '#ffd01b', '#006db3', '#00934b', '#000', '#6c2d00'];
+ const colorValue = [5, 4, 2, 3, 1, 7];
+
+ let cy = -4;
+ for (let c = 0; c < colorIds.length; c++) {
+ const cx = c % 2 == 0 ? 2.2 : 8.2;
+ if (c % 2 == 0) cy += 5;
+ const circle = pxsim.svg.child(this.group, "circle", { 'class': 'sim-color-grid-circle', 'cx': cx, 'cy': cy, 'r': '2', 'style': `fill: ${colors[c]}` });
+ circle.addEventListener(pointerEvents.down, ev => {
+ this.setColor(colorValue[c]);
+ })
+ }
+
+ const whiteCircleWrapper = pxsim.svg.child(this.group, "g", { 'id': 'white-cirlce-wrapper' });
+ pxsim.svg.child(whiteCircleWrapper, "circle", { 'class': 'sim-color-grid-circle', 'cx': 2.2, 'cy': '16', 'r': '2', 'style': `fill: #fff` });
+ pxsim.svg.child(whiteCircleWrapper, "circle", { 'cx': 2.2, 'cy': '16', 'r': '2', 'style': `fill: none;stroke: #94989b;stroke-width: 0.5px` });
+ whiteCircleWrapper.addEventListener(pointerEvents.down, ev => {
+ this.setColor(6);
+ })
+ return this.group;
+ }
+
+ private setColor(color: number) {
+ const state = this.state;
+ state.setColor(color);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sim/visuals/controls/colorWheel.ts b/sim/visuals/controls/colorWheel.ts
new file mode 100644
index 00000000..099314de
--- /dev/null
+++ b/sim/visuals/controls/colorWheel.ts
@@ -0,0 +1,34 @@
+
+
+namespace pxsim.visuals {
+
+ export class ColorWheelControl extends ControlView {
+ private group: SVGGElement;
+
+ private static COLOR_DARK = 1;
+ private static COLOR_LIGHT = 99;
+
+ getInnerView() {
+ this.group = svg.elt("g") as SVGGElement;
+ this.group.setAttribute("transform", `translate(12, ${this.getHeight() / 2 - 15}) scale(2.5)`)
+
+ const circle = pxsim.svg.child(this.group, "g");
+ const lightHalf = pxsim.svg.child(circle, "path", { 'class': 'sim-color-wheel-half', 'd': 'M19,28.76a11.71,11.71,0,1,1,4.58-.92A11.74,11.74,0,0,1,19,28.76Z', 'transform': 'translate(-6.5 -4.5)', 'style': `fill: #fff;stroke: #000;stroke-miterlimit: 10` });
+ pxsim.svg.child(circle, "path", { 'd': 'M19,28.52a11.42,11.42,0,0,0,4.48-.9,11.75,11.75,0,0,0,3.67-2.47,11.55,11.55,0,0,0,2.46-3.67,11.48,11.48,0,0,0,0-9,11.41,11.41,0,0,0-6.13-6.13,11.48,11.48,0,0,0-9,0,11.41,11.41,0,0,0-6.13,6.13,11.48,11.48,0,0,0,0,9,11.55,11.55,0,0,0,2.46,3.67,11.75,11.75,0,0,0,3.67,2.47,11.42,11.42,0,0,0,4.48.9M19,29A12,12,0,1,1,31,17,12,12,0,0,1,19,29Z', 'transform': 'translate(-6.5 -4.5)', 'style': `fill: #fff;stroke: #000;stroke-miterlimit: 10` });
+ lightHalf.addEventListener(pointerEvents.down, ev => {
+ this.setColor(ColorWheelControl.COLOR_LIGHT);
+ })
+ const darkHalf = pxsim.svg.child(this.group, "path", { 'class': 'sim-color-wheel-half', 'd': 'M19,5c.16,8.54,0,14.73,0,24A12,12,0,0,1,19,5Z', 'transform': 'translate(-6.5 -4.5)' });
+ darkHalf.addEventListener(pointerEvents.down, ev => {
+ this.setColor(ColorWheelControl.COLOR_DARK);
+ })
+ return this.group;
+ }
+
+ private setColor(color: number) {
+ const state = this.state;
+ state.setColor(color);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sim/visuals/controls/distanceSlider.ts b/sim/visuals/controls/distanceSlider.ts
new file mode 100644
index 00000000..5535fb0c
--- /dev/null
+++ b/sim/visuals/controls/distanceSlider.ts
@@ -0,0 +1,120 @@
+
+
+namespace pxsim.visuals {
+
+ export class DistanceSliderControl extends ControlView {
+ private group: SVGGElement;
+ private gradient: SVGLinearGradientElement;
+ private slider: SVGGElement;
+
+ private static SLIDER_HANDLE_HEIGHT = 31;
+
+ private isVisible = false;
+
+ getInnerView(parent: SVGSVGElement, globalDefs: SVGDefsElement) {
+ let gid = "gradient-slider-" + this.getId();
+ this.group = svg.elt("g") as SVGGElement;
+ this.gradient = createGradient(gid, this.getGradientDefinition());
+ this.gradient.setAttribute('x1', '-438.37');
+ this.gradient.setAttribute('y1', '419.43');
+ this.gradient.setAttribute('x2', '-438.37');
+ this.gradient.setAttribute('y2', '418.43');
+ this.gradient.setAttribute('gradientTransform', 'matrix(50, 0, 0, -110, 21949.45, 46137.67)');
+ this.gradient.setAttribute('gradientUnits', 'userSpaceOnUse');
+ globalDefs.appendChild(this.gradient);
+
+ this.group = svg.elt("g") as SVGGElement;
+
+ const sliderGroup = pxsim.svg.child(this.group, "g");
+ sliderGroup.setAttribute("transform", `translate(0, ${10 + this.getTopPadding()})`)
+
+ const rect = pxsim.svg.child(sliderGroup, "rect", { 'x': this.getLeftPadding(), 'y': 2, 'width': this.getWidth() - this.getLeftPadding() * 2, 'height': this.getContentHeight(), 'style': `fill: url(#${gid})` });
+
+ this.slider = pxsim.svg.child(sliderGroup, "g", { "transform": "translate(0,0)" }) as SVGGElement;
+ const sliderInner = pxsim.svg.child(this.slider, "g");
+ pxsim.svg.child(sliderInner, "rect", { 'width': this.getWidth(), 'height': DistanceSliderControl.SLIDER_HANDLE_HEIGHT, 'rx': '2', 'ry': '2', 'style': 'fill: #f12a21' });
+ pxsim.svg.child(sliderInner, "rect", { 'x': '0.5', 'y': '0.5', 'width': this.getWidth() - 1, 'height': DistanceSliderControl.SLIDER_HANDLE_HEIGHT - 1, 'rx': '1.5', 'ry': '1.5', 'style': 'fill: none;stroke: #b32e29' });
+
+ const dragSurface = svg.child(this.group, "rect", {
+ x: 0,
+ y: 0,
+ width: this.getInnerWidth(),
+ height: this.getInnerHeight(),
+ opacity: 0,
+ cursor: '-webkit-grab'
+ })
+
+ let pt = parent.createSVGPoint();
+ let captured = false;
+
+ touchEvents(dragSurface, ev => {
+ if (captured && (ev as MouseEvent).clientY != undefined) {
+ ev.preventDefault();
+ this.updateSliderValue(pt, parent, ev as MouseEvent);
+ }
+ }, ev => {
+ captured = true;
+ if ((ev as MouseEvent).clientY != undefined) {
+ this.updateSliderValue(pt, parent, ev as MouseEvent);
+ }
+ }, () => {
+ captured = false;
+ }, () => {
+ captured = false;
+ })
+
+ return this.group;
+ }
+
+ private getLeftPadding() {
+ return this.getInnerWidth() * 0.12;
+ }
+
+ private getTopPadding() {
+ return this.getInnerHeight() / 4;
+ }
+
+ private getContentHeight() {
+ return this.getInnerHeight() * 0.6;
+ }
+
+ onBoardStateChanged() {
+ if (!this.isVisible) {
+ return;
+ }
+ const node = this.state;
+ const percentage = node.getValue();
+ const y = this.getContentHeight() * percentage / 100;
+ this.slider.setAttribute("transform", `translate(0, ${y - DistanceSliderControl.SLIDER_HANDLE_HEIGHT / 2})`);
+ }
+
+ onComponentVisible() {
+ super.onComponentVisible();
+ this.isVisible = true;
+ this.onBoardStateChanged();
+ }
+
+ onComponentHidden() {
+ this.isVisible = false;
+ }
+
+ private updateSliderValue(pt: SVGPoint, parent: SVGSVGElement, ev: MouseEvent) {
+ let cur = svg.cursorPoint(pt, parent, ev);
+ const height = this.getContentHeight(); //DistanceSliderControl.SLIDER_HEIGHT;
+ let t = Math.max(0, Math.min(1, (this.getTopPadding() + height + this.top / this.scaleFactor - cur.y / this.scaleFactor) / height))
+
+ const state = this.state;
+ state.setDistance((1 - t) * (100));
+ }
+
+ private getGradientDefinition(): LinearGradientDefinition {
+ return {
+ stops: [
+ { offset: 0, color: '#626262' },
+ { offset: 1, color: "#ddd" }
+ ]
+ };
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sim/visuals/controls/rotationSlider.ts b/sim/visuals/controls/rotationSlider.ts
new file mode 100644
index 00000000..c3539b4b
--- /dev/null
+++ b/sim/visuals/controls/rotationSlider.ts
@@ -0,0 +1,94 @@
+
+
+namespace pxsim.visuals {
+
+ export class RotationSliderControl extends ControlView {
+ private group: SVGGElement;
+ private slider: SVGGElement;
+
+ private isVisible = false;
+
+ private static SLIDER_WIDTH = 70;
+ private static SLIDER_HEIGHT = 78;
+
+ getInnerView(parent: SVGSVGElement, globalDefs: SVGDefsElement) {
+ this.group = svg.elt("g") as SVGGElement;
+
+ const sliderGroup = pxsim.svg.child(this.group, "g");
+ sliderGroup.setAttribute("transform", `translate(5, ${10 + this.getTopPadding()})`)
+
+ const rotationLine = pxsim.svg.child(sliderGroup, "g");
+ pxsim.svg.child(rotationLine, "path", { 'transform': 'translate(5.11 -31.1)', 'd': 'M68.71,99.5l6.1-8S61.3,79.91,42.69,78.35,12,83.14,6.49,85.63a48.69,48.69,0,0,0-9.6,5.89L3.16,99.3S19.27,87.7,37.51,87.94,68.71,99.5,68.71,99.5Z', 'style': 'fill: #626262' });
+
+ this.slider = pxsim.svg.child(sliderGroup, "g") as SVGGElement;
+ const handleInner = pxsim.svg.child(sliderGroup, "g");
+ pxsim.svg.child(this.slider, "circle", { 'cx': 9, 'cy': 50, 'r': 13, 'style': 'fill: #f12a21' });
+ pxsim.svg.child(this.slider, "circle", { 'cx': 9, 'cy': 50, 'r': 12.5, 'style': 'fill: none;stroke: #b32e29' });
+
+ const dragSurface = svg.child(this.group, "rect", {
+ x: 0,
+ y: 0,
+ width: this.getInnerWidth(),
+ height: this.getInnerHeight(),
+ opacity: 0,
+ cursor: '-webkit-grab'
+ })
+
+ let pt = parent.createSVGPoint();
+ let captured = false;
+
+ touchEvents(dragSurface, ev => {
+ if (captured && (ev as MouseEvent).clientX != undefined) {
+ ev.preventDefault();
+ this.updateSliderValue(pt, parent, ev as MouseEvent);
+ }
+ }, ev => {
+ captured = true;
+ if ((ev as MouseEvent).clientX != undefined) {
+ this.updateSliderValue(pt, parent, ev as MouseEvent);
+ }
+ }, () => {
+ captured = false;
+ }, () => {
+ captured = false;
+ })
+
+ return this.group;
+ }
+
+ private getTopPadding() {
+ return this.getInnerHeight() / 4;
+ }
+
+ onBoardStateChanged() {
+ if (!this.isVisible) {
+ return;
+ }
+ const node = this.state;
+ const percentage = node.getValue();
+ const x = RotationSliderControl.SLIDER_WIDTH * percentage / 100;
+ const y = Math.abs((percentage - 50) / 50) * 10;
+ this.slider.setAttribute("transform", `translate(${x}, ${y})`);
+ }
+
+ onComponentVisible() {
+ super.onComponentVisible();
+ this.isVisible = true;
+ this.onBoardStateChanged();
+ }
+
+ onComponentHidden() {
+ this.isVisible = false;
+ }
+
+ private updateSliderValue(pt: SVGPoint, parent: SVGSVGElement, ev: MouseEvent) {
+ let cur = svg.cursorPoint(pt, parent, ev);
+ const width = CONTROL_WIDTH; //DistanceSliderControl.SLIDER_HEIGHT;
+ let t = Math.max(0, Math.min(1, (width + this.left / this.scaleFactor - cur.x / this.scaleFactor) / width))
+
+ const state = this.state;
+ state.setAngle((1 - t) * (100));
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sim/visuals/layoutView.ts b/sim/visuals/layoutView.ts
new file mode 100644
index 00000000..a72cc5f0
--- /dev/null
+++ b/sim/visuals/layoutView.ts
@@ -0,0 +1,298 @@
+///
+///
+///
+
+namespace pxsim.visuals {
+ export const DEFAULT_WIDTH = 350;
+ export const DEFAULT_HEIGHT = 700;
+
+ export const BRICK_HEIGHT_RATIO = 1 / 3;
+ export const MODULE_AND_WIRING_HEIGHT_RATIO = 1 / 3; // For inputs and outputs
+
+ export const MODULE_HEIGHT_RATIO = MODULE_AND_WIRING_HEIGHT_RATIO * 3 / 4;
+ export const WIRING_HEIGHT_RATIO = MODULE_AND_WIRING_HEIGHT_RATIO / 4;
+
+ export const MODULE_INNER_PADDING_RATIO = 1 / 35;
+
+ export interface LayoutElement extends View {
+ getId(): number;
+ getPort(): number;
+ getPaddingRatio(): number;
+ getWiringRatio(): number;
+ setSelected(selected: boolean): void;
+ }
+
+ export class LayoutView extends ViewContainer {
+ private inputs: LayoutElement[] = [];
+ private outputs: LayoutElement[] = [];
+
+ private inputWires: WireView[] = [];
+ private outputWires: WireView[] = [];
+
+ private selected: number;
+ private selectedIsInput: boolean;
+ private brick: BrickView;
+ private offsets: number[];
+ private contentGroup: SVGGElement;
+ private scrollGroup: SVGGElement;
+ private renderedViews: Map = {};
+
+ private childScaleFactor: number;
+
+ private totalLength: number;
+ private height: number;
+ private hasDimensions = false;
+
+ constructor() {
+ super();
+
+ this.outputs = [
+ new PortView(0, 'A'),
+ new PortView(1, 'B'),
+ new PortView(2, 'C'),
+ new PortView(3, 'D')
+ ];
+
+ this.brick = new BrickView(0);
+
+ this.inputs = [
+ new PortView(0, '1'),
+ new PortView(1, '2'),
+ new PortView(2, '3'),
+ new PortView(3, '4')
+ ];
+
+ for (let port = 0; port < DAL.NUM_OUTPUTS; port++) {
+ this.outputWires[port] = new WireView(port);
+ }
+ for (let port = 0; port < DAL.NUM_INPUTS; port++) {
+ this.inputWires[port] = new WireView(port);
+ }
+ }
+
+ public layout(width: number, height: number) {
+ this.hasDimensions = true;
+ this.width = width;
+ this.height = height;
+ this.scrollGroup.setAttribute("width", width.toString());
+ this.scrollGroup.setAttribute("height", height.toString());
+ this.position();
+ }
+
+ public setBrick(brick: BrickView) {
+ this.brick = brick;
+ this.position();
+ }
+
+ public getBrick() {
+ return this.brick;
+ }
+
+ public setInput(port: number, child: LayoutElement) {
+ if (this.inputs[port]) {
+ // Remove current input
+ this.inputs[port].dispose();
+ }
+ this.inputs[port] = child;
+ this.position();
+ }
+
+ public setOutput(port: number, child: LayoutElement) {
+ if (this.outputs[port]) {
+ // Remove current input
+ this.outputs[port].dispose();
+ }
+ this.outputs[port] = child;
+ this.position();
+ }
+
+ public onClick(index: number, input: boolean, ev: any) {
+ this.setSelected(index, input);
+ }
+
+ public clearSelected() {
+ this.selected = undefined;
+ this.selectedIsInput = undefined;
+ }
+
+ public setSelected(index: number, input?: boolean) {
+ if (index !== this.selected || input !== this.selectedIsInput) {
+ this.selected = index;
+ this.selectedIsInput = input;
+ const node = this.getSelected();
+ if (node) node.setSelected(true);
+
+ //this.redoPositioning();
+ runtime.queueDisplayUpdate();
+ }
+ }
+
+ public getSelected() {
+ if (this.selected !== undefined) {
+ return this.selectedIsInput ? this.inputs[this.selected] : this.outputs[this.selected];
+ }
+ return undefined;
+ }
+
+ protected buildDom(width: number) {
+ this.contentGroup = svg.elt("g") as SVGGElement;
+ this.scrollGroup = svg.child(this.contentGroup, "g") as SVGGElement;
+ return this.contentGroup;
+ }
+
+ public getInnerWidth() {
+ if (!this.hasDimensions) {
+ return 0;
+ }
+ return this.width;
+ }
+
+ public getInnerHeight() {
+ if (!this.hasDimensions) {
+ return 0;
+ }
+ return this.height;
+ }
+
+ public updateTheme(theme: IBoardTheme) {
+ this.inputs.forEach(n => {
+ n.updateTheme(theme);
+ })
+ this.brick.updateTheme(theme);
+ this.outputs.forEach(n => {
+ n.updateTheme(theme);
+ })
+ }
+
+ private position() {
+ if (!this.hasDimensions) {
+ return;
+ }
+
+ this.offsets = [];
+
+ const selectedNode = this.getSelected();
+
+ const contentWidth = this.width || DEFAULT_WIDTH;
+ const contentHeight = this.height || DEFAULT_HEIGHT;
+
+ const moduleHeight = this.getModuleHeight();
+
+ const brickHeight = this.getBrickHeight();
+ this.brick.inject(this.scrollGroup);
+ const brickWidth = this.brick.getInnerWidth() / this.brick.getInnerHeight() * brickHeight;
+ const brickPadding = (contentWidth - brickWidth) / 2;
+
+ const modulePadding = contentWidth / 35;
+ const moduleSpacing = contentWidth / 4;
+ const moduleWidth = moduleSpacing - (modulePadding * 2);
+ let currentX = modulePadding;
+ let currentY = 0;
+ this.outputs.forEach((n, i) => {
+ const outputPadding = moduleWidth * n.getPaddingRatio();
+ const outputWidth = moduleWidth - outputPadding * 2;
+ n.inject(this.scrollGroup, outputWidth);
+ n.resize(outputWidth);
+ const nHeight = n.getHeight() / n.getWidth() * outputWidth;
+ n.translate(currentX + outputPadding, currentY + moduleHeight - nHeight);
+ n.setSelected(n == selectedNode);
+ if (n.hasClick()) n.registerClick((ev: any) => {
+ this.onClick(i, false, ev);
+ })
+ currentX += moduleSpacing;
+ })
+
+ currentX = 0;
+ currentY = moduleHeight;
+
+ const wireBrickSpacing = brickWidth / 5;
+ const wiringYPadding = 10;
+ let wireStartX = 0;
+ let wireEndX = brickPadding + wireBrickSpacing;
+ let wireEndY = currentY + this.getWiringHeight() + wiringYPadding;
+ let wireStartY = currentY - wiringYPadding;
+
+ // Draw output lines
+ for (let port = 0; port < DAL.NUM_OUTPUTS; port++) {
+ if (!this.outputWires[port].isRendered()) this.outputWires[port].inject(this.scrollGroup);
+ this.outputWires[port].updateDimensions(wireStartX + moduleSpacing * this.outputs[port].getWiringRatio(), wireStartY, wireEndX, wireEndY);
+ this.outputWires[port].setSelected(this.outputs[port].getId() == NodeType.Port);
+ wireStartX += moduleSpacing;
+ wireEndX += wireBrickSpacing;
+ }
+
+ currentX = brickPadding;
+ currentY += this.getWiringHeight();
+
+ // Render the brick in the middle
+ this.brick.resize(brickWidth);
+ this.brick.translate(currentX, currentY);
+
+ currentX = modulePadding;
+ currentY += brickHeight + this.getWiringHeight();
+
+ this.inputs.forEach((n, i) => {
+ const inputPadding = moduleWidth * n.getPaddingRatio();
+ const inputWidth = moduleWidth - inputPadding * 2;
+ n.inject(this.scrollGroup, inputWidth);
+ n.resize(inputWidth);
+ n.translate(currentX + inputPadding, currentY);
+ n.setSelected(n == selectedNode);
+ if (n.hasClick()) n.registerClick((ev: any) => {
+ this.onClick(i, true, ev);
+ })
+ currentX += moduleSpacing;
+ })
+
+ wireStartX = moduleSpacing / 2;
+ wireEndX = brickPadding + wireBrickSpacing;
+ wireEndY = currentY - this.getWiringHeight() - wiringYPadding;
+ wireStartY = currentY + wiringYPadding;
+
+ // Draw input lines
+ for (let port = 0; port < DAL.NUM_INPUTS; port++) {
+ if (!this.inputWires[port].isRendered()) this.inputWires[port].inject(this.scrollGroup);
+ this.inputWires[port].updateDimensions(wireStartX, wireStartY, wireEndX, wireEndY);
+ this.inputWires[port].setSelected(this.inputs[port].getId() == NodeType.Port);
+ wireStartX += moduleSpacing;
+ wireEndX += wireBrickSpacing;
+ }
+ }
+
+ public getSelectedCoords() {
+ const selected = this.getSelected();
+ if (!selected) return undefined;
+ const port = this.getSelected().getPort();
+ return {
+ x: this.getSelected().getPort() * this.width / 4 + this.width * MODULE_INNER_PADDING_RATIO,
+ y: this.selectedIsInput ? this.getModuleHeight() + 2 * this.getWiringHeight() + this.getBrickHeight() : this.getModuleHeight() / 4
+ }
+ }
+
+ public getCloseIconCoords(closeIconWidth: number, closeIconHeight: number) {
+ return {
+ x: this.getSelected().getPort() * this.width / 4 + this.getModuleBounds().width / 2 - closeIconWidth / 2,
+ y: this.selectedIsInput ? this.getModuleHeight() + 2 * this.getWiringHeight() + this.getBrickHeight() + this.getModuleHeight() - closeIconHeight : 0
+ }
+ }
+
+ public getModuleHeight() {
+ return (this.height || DEFAULT_HEIGHT) * MODULE_HEIGHT_RATIO;
+ }
+
+ public getBrickHeight() {
+ return (this.height || DEFAULT_HEIGHT) * BRICK_HEIGHT_RATIO;
+ }
+
+ public getWiringHeight() {
+ return (this.height || DEFAULT_HEIGHT) * WIRING_HEIGHT_RATIO;
+ }
+
+ public getModuleBounds() {
+ return {
+ width: this.width / 4,
+ height: this.getModuleHeight()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/brickView.ts b/sim/visuals/nodes/brickView.ts
new file mode 100644
index 00000000..9ff648d2
--- /dev/null
+++ b/sim/visuals/nodes/brickView.ts
@@ -0,0 +1,196 @@
+///
+
+namespace pxsim.visuals {
+
+ export class BrickView extends StaticModuleView implements LayoutElement {
+
+ private static EV3_SCREEN_ID = "ev3_screen";
+ private static EV3_LIGHT_ID = "btn_color";
+
+ private buttons: SVGElement[];
+ private light: SVGElement;
+
+ private currentCanvasX = 178;
+ private currentCanvasY = 128;
+
+ constructor(port: number) {
+ super(EV3_SVG, "board", NodeType.Brick, port);
+ }
+
+ protected buildDomCore() {
+ // Setup buttons
+ const btnids = ["btn_up", "btn_enter", "btn_down", "btn_right", "btn_left", "btn_back"];
+ this.buttons = btnids.map(n => this.content.getElementById(this.normalizeId(n)) as SVGElement);
+ this.buttons.forEach(b => svg.addClass(b, "sim-button"));
+
+ this.light = this.content.getElementById(this.normalizeId(BrickView.EV3_LIGHT_ID)) as SVGElement;
+ }
+
+ private setStyleFill(svgId: string, fillUrl: string) {
+ const el = (this.content.getElementById(svgId) as SVGRectElement);
+ if (el) el.style.fill = `url("#${fillUrl}")`;
+ }
+
+ public hasClick() {
+ return false;
+ }
+
+ public shouldUpdateState() {
+ return true;
+ }
+
+ public updateState() {
+ this.updateLight();
+ }
+
+ public updateThemeCore() {
+ let theme = this.theme;
+ svg.fill(this.buttons[0], theme.buttonUps[0]);
+ svg.fill(this.buttons[1], theme.buttonUps[1]);
+ svg.fill(this.buttons[2], theme.buttonUps[2]);
+ }
+
+ private lastLightPattern: number = -1;
+ private lastLightAnimationId: any;
+ private updateLight() {
+ let state = ev3board().getBrickNode().lightState;
+
+ const lightPattern = state.lightPattern;
+ if (lightPattern == this.lastLightPattern) return;
+ this.lastLightPattern = lightPattern;
+ if (this.lastLightAnimationId) cancelAnimationFrame(this.lastLightAnimationId);
+ switch (lightPattern) {
+ case 0: // LED_BLACK
+ this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-black`));
+ //svg.fill(this.light, "#FFF");
+ break;
+ case 1: // LED_GREEN
+ this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-green`));
+ //svg.fill(this.light, "#00ff00");
+ break;
+ case 2: // LED_RED
+ this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-red`));
+ //svg.fill(this.light, "#ff0000");
+ break;
+ case 3: // LED_ORANGE
+ this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-orange`));
+ //svg.fill(this.light, "#FFA500");
+ break;
+ case 4: // LED_GREEN_FLASH
+ this.flashLightAnimation('green');
+ break;
+ case 5: // LED_RED_FLASH
+ this.flashLightAnimation('red');
+ break;
+ case 6: // LED_ORANGE_FLASH
+ this.flashLightAnimation('orange');
+ break;
+ case 7: // LED_GREEN_PULSE
+ this.pulseLightAnimation('green');
+ break;
+ case 8: // LED_RED_PULSE
+ this.pulseLightAnimation('red');
+ break;
+ case 9: // LED_ORANGE_PULSE
+ this.pulseLightAnimation('orange');
+ break;
+ }
+ }
+
+ private flashLightAnimation(id: string) {
+ let fps = 3;
+ let now;
+ let then = Date.now();
+ let interval = 1000 / fps;
+ let delta;
+ let that = this;
+ function draw() {
+ that.lastLightAnimationId = requestAnimationFrame(draw);
+ now = Date.now();
+ delta = now - then;
+ if (delta > interval) {
+ then = now - (delta % interval);
+ that.flashLightAnimationStep(id);
+ }
+ }
+ draw();
+ }
+
+ private flash: boolean;
+ private flashLightAnimationStep(id: string) {
+ if (this.flash) {
+ this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-${id}`));
+ } else {
+ this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-black`));
+ }
+ this.flash = !this.flash;
+ }
+
+
+ private pulseLightAnimation(id: string) {
+ let fps = 8;
+ let now;
+ let then = Date.now();
+ let interval = 1000 / fps;
+ let delta;
+ let that = this;
+ function draw() {
+ that.lastLightAnimationId = requestAnimationFrame(draw);
+ now = Date.now();
+ delta = now - then;
+ if (delta > interval) {
+ // update time stuffs
+ then = now - (delta % interval);
+ that.pulseLightAnimationStep(id);
+ }
+ }
+ draw();
+ }
+
+ private pulse: number = 0;
+ private pulseLightAnimationStep(id: string) {
+ switch (this.pulse) {
+ case 0: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-black`)); break;
+ case 1: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-black`)); break;
+ case 2: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-black`)); break;
+ case 3: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-black`)); break;
+ case 4: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-black`)); break;
+ case 5: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-${id}`)); break;
+ case 6: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-${id}`)); break;
+ case 7: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-black`)); break;
+ case 8: this.setStyleFill(this.normalizeId(BrickView.EV3_LIGHT_ID), this.normalizeId(`linear-gradient-${id}`)); break;
+
+ }
+ this.pulse++;
+ if (this.pulse == 9) this.pulse = 0;
+ }
+
+ public attachEvents() {
+ let bpState = ev3board().getBrickNode().buttonState;
+ let stateButtons = bpState.buttons;
+ this.buttons.forEach((btn, index) => {
+ let button = stateButtons[index];
+
+ btn.addEventListener(pointerEvents.down, ev => {
+ button.setPressed(true);
+ svg.fill(this.buttons[index], this.theme.buttonDown);
+ })
+ btn.addEventListener(pointerEvents.leave, ev => {
+ button.setPressed(false);
+ svg.fill(this.buttons[index], this.theme.buttonUps[index]);
+ })
+ btn.addEventListener(pointerEvents.up, ev => {
+ button.setPressed(false);
+ svg.fill(this.buttons[index], this.theme.buttonUps[index]);
+ })
+ })
+ }
+
+ public getScreenBBox() {
+ if (!this.content) return undefined;
+ const screen = this.content.getElementById(this.normalizeId(BrickView.EV3_SCREEN_ID));
+ if (!screen) return undefined;
+ return screen.getBoundingClientRect();
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/colorSensorView.ts b/sim/visuals/nodes/colorSensorView.ts
new file mode 100644
index 00000000..ae321d0f
--- /dev/null
+++ b/sim/visuals/nodes/colorSensorView.ts
@@ -0,0 +1,16 @@
+///
+
+namespace pxsim.visuals {
+ export class ColorSensorView extends StaticModuleView implements LayoutElement {
+
+ private control: ColorGridControl;
+
+ constructor(port: number) {
+ super(COLOR_SENSOR_SVG, "color", NodeType.ColorSensor, port);
+ }
+
+ public getPaddingRatio() {
+ return 1 / 8;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/gyroSensorView.ts b/sim/visuals/nodes/gyroSensorView.ts
new file mode 100644
index 00000000..c8280f8d
--- /dev/null
+++ b/sim/visuals/nodes/gyroSensorView.ts
@@ -0,0 +1,14 @@
+///
+
+namespace pxsim.visuals {
+ export class GyroSensorView extends StaticModuleView implements LayoutElement {
+
+ constructor(port: number) {
+ super(GYRO_SVG, "gyro", NodeType.GyroSensor, port);
+ }
+
+ public getPaddingRatio() {
+ return 1 / 4;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/largeMotorView.ts b/sim/visuals/nodes/largeMotorView.ts
new file mode 100644
index 00000000..6dfdc850
--- /dev/null
+++ b/sim/visuals/nodes/largeMotorView.ts
@@ -0,0 +1,62 @@
+///
+
+namespace pxsim.visuals {
+ export class LargeMotorView extends StaticModuleView implements LayoutElement {
+
+ private static ROTATING_ECLIPSE_ID = "1eb2ae58-2419-47d4-86bf-4f26a7f0cf61";
+
+ private lastMotorAnimationId: any;
+
+ constructor(port: number) {
+ super(LARGE_MOTOR_SVG, "large-motor", NodeType.LargeMotor, port);
+ }
+
+ updateState() {
+ const motorState = ev3board().getMotors()[this.port];
+ if (!motorState) return;
+ const speed = motorState.getSpeed();
+ if (this.lastMotorAnimationId) cancelAnimationFrame(this.lastMotorAnimationId);
+
+ if (!speed) return;
+ this.playMotorAnimation(motorState);
+ }
+
+ private playMotorAnimation(state: MotorNode) {
+ // Max medium motor RPM is 170 according to http://www.cs.scranton.edu/~bi/2015s-html/cs358/EV3-Motor-Guide.docx
+ const rotationsPerMinute = 170; // 170 rpm at speed 100
+ const rotationsPerSecond = rotationsPerMinute / 60;
+ const fps = MOTOR_ROTATION_FPS;
+ const rotationsPerFrame = rotationsPerSecond / fps;
+ let now;
+ let then = Date.now();
+ let interval = 1000 / fps;
+ let delta;
+ let that = this;
+ function draw() {
+ that.lastMotorAnimationId = requestAnimationFrame(draw);
+ now = Date.now();
+ delta = now - then;
+ if (delta > interval) {
+ then = now - (delta % interval);
+ that.playMotorAnimationStep(state.angle);
+ const rotations = state.getSpeed() / 100 * rotationsPerFrame;
+ const angle = rotations * 360;
+ state.angle += angle;
+ }
+ }
+ draw();
+ }
+
+ private playMotorAnimationStep(angle: number) {
+ const holeEl = this.content.getElementById(this.normalizeId(LargeMotorView.ROTATING_ECLIPSE_ID))
+ const width = 34;
+ const height = 34;
+ const transform = `rotate(${angle} ${width / 2} ${height / 2})`;
+ holeEl.setAttribute("transform", transform);
+ }
+
+ getWiringRatio() {
+ return 0.62;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/mediumMotorView.ts b/sim/visuals/nodes/mediumMotorView.ts
new file mode 100644
index 00000000..e53c8449
--- /dev/null
+++ b/sim/visuals/nodes/mediumMotorView.ts
@@ -0,0 +1,68 @@
+///
+
+namespace pxsim.visuals {
+
+ export const MOTOR_ROTATION_FPS = 32;
+
+ export class MediumMotorView extends StaticModuleView implements LayoutElement {
+
+ private static ROTATING_ECLIPSE_ID = "Hole";
+
+ private hasPreviousAngle: boolean;
+ private previousAngle: number;
+
+ private lastMotorAnimationId: any;
+
+ constructor(port: number) {
+ super(MEDIUM_MOTOR_SVG, "medium-motor", NodeType.MediumMotor, port);
+ }
+
+ public getPaddingRatio() {
+ return 1 / 10;
+ }
+
+ updateState() {
+ const motorState = ev3board().getMotors()[this.port];
+ if (!motorState) return;
+ const speed = motorState.getSpeed();
+ if (this.lastMotorAnimationId) cancelAnimationFrame(this.lastMotorAnimationId);
+
+ if (!speed) return;
+ this.playMotorAnimation(motorState);
+ }
+
+ private playMotorAnimation(state: MotorNode) {
+ // Max medium motor RPM is 250 according to http://www.cs.scranton.edu/~bi/2015s-html/cs358/EV3-Motor-Guide.docx
+ const rotationsPerMinute = 250; // 250 rpm at speed 100
+ const rotationsPerSecond = rotationsPerMinute / 60;
+ const fps = MOTOR_ROTATION_FPS;
+ const rotationsPerFrame = rotationsPerSecond / fps;
+ let now;
+ let then = Date.now();
+ let interval = 1000 / fps;
+ let delta;
+ let that = this;
+ function draw() {
+ that.lastMotorAnimationId = requestAnimationFrame(draw);
+ now = Date.now();
+ delta = now - then;
+ if (delta > interval) {
+ then = now - (delta % interval);
+ that.playMotorAnimationStep(state.angle);
+ const rotations = state.getSpeed() / 100 * rotationsPerFrame;
+ const angle = rotations * 360;
+ state.angle += angle;
+ }
+ }
+ draw();
+ }
+
+ private playMotorAnimationStep(angle: number) {
+ const holeEl = this.content.getElementById(this.normalizeId(MediumMotorView.ROTATING_ECLIPSE_ID))
+ const width = 47.9;
+ const height = 47.2;
+ const transform = `translate(-1.5 -1.49) rotate(${angle} ${width / 2} ${height / 2})`;
+ holeEl.setAttribute("transform", transform);
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/portView.ts b/sim/visuals/nodes/portView.ts
new file mode 100644
index 00000000..5399713f
--- /dev/null
+++ b/sim/visuals/nodes/portView.ts
@@ -0,0 +1,25 @@
+///
+
+namespace pxsim.visuals {
+
+ export class PortView extends StaticModuleView implements LayoutElement {
+
+ constructor(port: NodeType, private label: string) {
+ super(PORT_SVG, "port", NodeType.Port, port);
+ }
+
+ protected buildDomCore() {
+ const textLabel = this.content.getElementById(this.normalizeId("port_text")) as SVGTextElement;
+ textLabel.textContent = this.label;
+ textLabel.style.userSelect = 'none';
+ }
+
+ public getPaddingRatio() {
+ return 1 / 6;
+ }
+
+ public hasClick() {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/staticView.ts b/sim/visuals/nodes/staticView.ts
new file mode 100644
index 00000000..1527c433
--- /dev/null
+++ b/sim/visuals/nodes/staticView.ts
@@ -0,0 +1,123 @@
+namespace pxsim.visuals {
+
+ export class StaticModuleView extends View implements LayoutElement {
+ protected content: SVGSVGElement;
+
+ protected controlShown: boolean;
+ protected selected: boolean;
+
+ constructor(protected xml: string, protected prefix: string, protected id: NodeType, protected port: NodeType) {
+ super();
+ this.xml = this.normalizeXml(xml);
+ }
+
+ private normalizeXml(xml: string) {
+ const prefix = this.prefix;
+ xml = xml.replace(/id=\"(.*?)\"/g, (m: string, id: string) => {
+ return `id="${this.normalizeId(id)}"`;
+ });
+ xml = xml.replace(/url\(#(.*?)\)/g, (m: string, id: string) => {
+ return `url(#${this.normalizeId(id)}`;
+ });
+ xml = xml.replace(/xlink:href=\"#(.*?)\"/g, (m: string, id: string) => {
+ return `xlink:href="#${this.normalizeId(id)}"`;
+ });
+ return xml;
+ }
+
+ protected normalizeId(svgId: string) {
+ return `${this.prefix}-${svgId}`;
+ }
+
+ public getId() {
+ return this.id;
+ }
+
+ public getPort() {
+ return this.port;
+ }
+
+ public getPaddingRatio() {
+ return 0;
+ }
+
+ public getWiringRatio() {
+ return 0.5;
+ }
+
+ protected buildDom(width: number): SVGElement {
+ this.content = svg.parseString(this.xml);
+ this.updateDimensions(width);
+ this.buildDomCore();
+ this.attachEvents();
+ if (this.hasClick())
+ this.content.style.cursor = "pointer";
+ return this.content;
+ }
+
+ protected buildDomCore() {
+
+ }
+
+ public getInnerHeight() {
+ if (!this.content) {
+ return 0;
+ }
+ if (!this.content.hasAttribute("viewBox")) {
+ return parseFloat(this.content.getAttribute("height"));
+ }
+ return parseFloat(this.content.getAttribute("viewBox").split(" ")[3]);
+ }
+
+ public getInnerWidth() {
+ if (!this.content) {
+ return 0;
+ }
+ if (!this.content.hasAttribute("viewBox")) {
+ return parseFloat(this.content.getAttribute("width"));
+ }
+ return parseFloat(this.content.getAttribute("viewBox").split(" ")[2]);
+ }
+
+ public attachEvents() {
+ }
+
+ public resize(width: number) {
+ this.updateDimensions(width);
+ }
+
+ private updateDimensions(width: number) {
+ if (this.content) {
+ const currentWidth = this.getInnerWidth();
+ const currentHeight = this.getInnerHeight();
+ const newHeight = currentHeight / currentWidth * width;
+ this.content.setAttribute('width', `${width}`);
+ this.content.setAttribute('height', `${newHeight}`);
+ }
+ }
+
+ public hasClick() {
+ return true;
+ }
+
+ public setSelected(selected: boolean) {
+ this.selected = selected;
+ this.updateOpacity();
+ }
+
+ protected updateOpacity() {
+ if (this.rendered) {
+ const opacity = this.selected ? "0.5" : "1";
+ if (this.hasClick()) {
+ this.setOpacity(opacity);
+ if (this.selected) this.content.style.cursor = "";
+ else this.content.style.cursor = "pointer";
+ }
+ }
+ }
+
+ protected setOpacity(opacity: string) {
+ this.element.setAttribute("opacity", opacity);
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/touchSensorView.ts b/sim/visuals/nodes/touchSensorView.ts
new file mode 100644
index 00000000..0c623218
--- /dev/null
+++ b/sim/visuals/nodes/touchSensorView.ts
@@ -0,0 +1,67 @@
+///
+
+namespace pxsim.visuals {
+ export class TouchSensorView extends StaticModuleView implements LayoutElement {
+
+ private static RECT_ID = ["touch_gradient4", "touch_gradient3", "touch_gradient2", "touch_gradient1"];
+ private static TOUCH_GRADIENT_UNPRESSED = ["linear-gradient-2", "linear-gradient-3", "linear-gradient-4", "linear-gradient-5"];
+ private static TOUCH_GRADIENT_PRESSED = ["linear-gradient-6", "linear-gradient-7", "linear-gradient-8", "linear-gradient-9"];
+
+ private unpressedGradient: string;
+ private pressedGradient: string;
+
+ private xLinkGradients: string[];
+
+ constructor(port: number) {
+ super(TOUCH_SENSOR_SVG, "touch", NodeType.TouchSensor, port);
+ }
+
+ public getPaddingRatio() {
+ return 1 / 10;
+ }
+
+ public hasClick() {
+ return false;
+ }
+
+ private setAttribute(svgId: string, attribute: string, value: string) {
+ const el = this.content.getElementById(svgId);
+ if (el) el.setAttribute(attribute, value);
+ }
+
+ private setStyleFill(svgId: string, fillUrl: string) {
+ const el = (this.content.getElementById(svgId) as SVGRectElement);
+ if (el) el.style.fill = `url("#${fillUrl}")`;
+ }
+
+ public attachEvents() {
+ this.content.style.cursor = "pointer";
+ const btn = this.content;
+ const state = ev3board().getSensor(this.port, DAL.DEVICE_TYPE_TOUCH) as TouchSensorNode;
+ btn.addEventListener(pointerEvents.down, ev => {
+ this.setPressed(true);
+ state.setPressed(true);
+ })
+ btn.addEventListener(pointerEvents.leave, ev => {
+ this.setPressed(false);
+ state.setPressed(false);
+ })
+ btn.addEventListener(pointerEvents.up, ev => {
+ this.setPressed(false);
+ state.setPressed(false);
+ })
+ }
+
+ private setPressed(pressed: boolean) {
+ if (pressed) {
+ for (let i = 0; i < 4; i ++) {
+ this.setStyleFill(`${this.normalizeId(TouchSensorView.RECT_ID[i])}`, `${this.normalizeId(TouchSensorView.TOUCH_GRADIENT_PRESSED[i])}`);
+ }
+ } else {
+ for (let i = 0; i < 4; i ++) {
+ this.setStyleFill(`${this.normalizeId(TouchSensorView.RECT_ID[i])}`, `${this.normalizeId(TouchSensorView.TOUCH_GRADIENT_UNPRESSED[i])}`);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/nodes/ultrasonicView.ts b/sim/visuals/nodes/ultrasonicView.ts
new file mode 100644
index 00000000..d370bd35
--- /dev/null
+++ b/sim/visuals/nodes/ultrasonicView.ts
@@ -0,0 +1,10 @@
+///
+
+namespace pxsim.visuals {
+ export class UltrasonicSensorView extends StaticModuleView implements LayoutElement {
+
+ constructor(port: number) {
+ super(ULTRASONIC_SVG, "ultrasonic", NodeType.UltrasonicSensor, port);
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/pincontrol.ts b/sim/visuals/pincontrol.ts
deleted file mode 100644
index c3e729b0..00000000
--- a/sim/visuals/pincontrol.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-namespace pxsim.visuals {
- export class AnalogPinControl {
- private outerElement: SVGElement;
-
- private innerCircle: SVGCircleElement;
- private gradient: SVGLinearGradientElement;
- private currentValue: number;
- private pin: Pin;
-
- constructor(private parent: EV3BoardSvg, private defs: SVGDefsElement, private id: CPlayPinName, name: string) {
- this.pin = board().edgeConnectorState.getPin(this.id);
-
- // Init the button events
- this.outerElement = parent.element.getElementById(name) as SVGElement;
- svg.addClass(this.outerElement, "sim-pin-touch");
- this.addButtonEvents();
-
-
- // Init the gradient controls
- // const gid = `gradient-${CPlayPinName[id]}-level`;
- // this.innerCircle = parent.element.getElementById("PIN_CONNECTOR_" + CPlayPinName[id]) as SVGCircleElement;
- // this.gradient = svg.linearGradient(this.defs, gid);
- // this.innerCircle.setAttribute("fill", `url(#${gid})`);
- // this.innerCircle.setAttribute("class", "sim-light-level-button")
- // this.addLevelControlEvents()
-
- this.updateTheme();
- }
-
- public updateTheme() {
- const theme = this.parent.props.theme;
- svg.setGradientColors(this.gradient, theme.lightLevelOff, 'darkorange');
- }
-
- public updateValue() {
- const value = this.pin.value;
-
- if (value === this.currentValue) {
- return;
- }
-
- this.currentValue = value;
-
- // svg.setGradientValue(this.gradient, 100 - Math.min(100, Math.max(0, Math.floor(value * 100 / 1023))) + '%')
- // if (this.innerCircle.childNodes.length) {
- // this.innerCircle.removeChild(this.innerCircle.childNodes[0])
- // }
-
- svg.title(this.outerElement, value.toString());
- }
-
- private addButtonEvents() {
- this.outerElement.addEventListener(pointerEvents.down, ev => {
- this.pin.touched = true;
- svg.addClass(this.outerElement, "touched");
-
- (pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(true);
- })
- this.outerElement.addEventListener(pointerEvents.leave, ev => {
- this.pin.touched = false;
- svg.removeClass(this.outerElement, "touched");
-
- (pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(false);
- })
- this.outerElement.addEventListener(pointerEvents.up, ev => {
- this.pin.touched = false;
- svg.removeClass(this.outerElement, "touched");
-
- (pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(false);
- })
- }
-
- private addLevelControlEvents() {
- const cy = parseFloat(this.innerCircle.getAttribute("cy"));
- const r = parseFloat(this.innerCircle.getAttribute("r"));
- const pt = this.parent.element.createSVGPoint();
-
- svg.buttonEvents(this.innerCircle,
- (ev) => {
- const pos = svg.cursorPoint(pt, this.parent.element, ev);
- const rs = r / 2;
- const level = Math.max(0, Math.min(1023, Math.floor((1 - (pos.y - (cy - rs)) / (2 * rs)) * 1023)));
-
- if (level != this.pin.value) {
- this.pin.value = level;
- this.updateValue();
- }
- }, ev => { },
- ev => { });
- }
- }
-}
\ No newline at end of file
diff --git a/sim/visuals/util.ts b/sim/visuals/util.ts
new file mode 100644
index 00000000..af77e9fe
--- /dev/null
+++ b/sim/visuals/util.ts
@@ -0,0 +1,99 @@
+namespace pxsim.visuals {
+ export interface LinearGradientDefinition {
+ stops: LinearGradientStop[];
+ }
+
+ export interface LinearGradientStop {
+ offset: string | number;
+ color: string;
+ }
+
+ export type TouchCallback = (event: MouseEvent | TouchEvent | PointerEvent) => void;
+
+ export function touchEvents(e: SVGElement | SVGElement[], move?: TouchCallback, down?: TouchCallback, up?: TouchCallback, leave?: TouchCallback) {
+ if (Array.isArray(e)) {
+ e.forEach(el => bindEvents(el, move, down, up, leave));
+ }
+ else {
+ bindEvents(e, move, down, up, leave);
+ }
+ }
+
+ function bindEvents(e: SVGElement, move?: TouchCallback, down?: TouchCallback, up?: TouchCallback, leave?: TouchCallback) {
+ if ((window as any).PointerEvent) {
+ if (down) e.addEventListener("pointerdown", down);
+ if (up) e.addEventListener("pointerup", up);
+ if (leave) e.addEventListener("pointerleave", leave);
+ if (move) e.addEventListener("pointermove", move);
+ }
+ else {
+ if (down) e.addEventListener("mousedown", down);
+ if (up) e.addEventListener("mouseup", up);
+ if (leave) e.addEventListener("mouseleave", leave);
+ if (move) e.addEventListener("mousemove", move);
+
+ if (pxsim.svg.isTouchEnabled()) {
+ if (down) e.addEventListener("touchstart", down);
+ if (up) e.addEventListener("touchend", up);
+ if (leave) e.addEventListener("touchcancel", leave);
+ if (move) e.addEventListener("touchmove", move);
+ }
+ }
+ }
+
+ export function createGradient(id: string, opts: LinearGradientDefinition) {
+ const g = svg.elt("linearGradient") as SVGLinearGradientElement;
+ g.setAttribute("id", id);
+
+ opts.stops.forEach(stop => {
+ let offset: string;
+
+ if (typeof stop.offset === "number") {
+ offset = stop.offset + "%"
+ }
+ else {
+ offset = stop.offset as string;
+ }
+
+ svg.child(g, "stop", { offset, "stop-color": stop.color });
+ });
+
+ return g;
+ }
+
+ export function updateGradient(gradient: SVGLinearGradientElement, opts: LinearGradientDefinition) {
+ let j = 0;
+
+ forEachElement(gradient.childNodes, (e, i) => {
+ if (i < opts.stops.length) {
+ const stop = opts.stops[i];
+ e.setAttribute("offset", offsetString(stop.offset));
+ e.setAttribute("stop-color", stop.color);
+ }
+ else {
+ gradient.removeChild(e);
+ }
+ j = i + 1;
+ });
+
+ for (; j < opts.stops.length; j++) {
+ const stop = opts.stops[j];
+ svg.child(gradient, "stop", { offset: offsetString(stop.offset), "stop-color": stop.color });
+ }
+ }
+
+ export function forEachElement(nodes: NodeList, cb: (e: Element, i: number) => void) {
+ let index = 0;
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ cb(node as Element, index);
+ ++index;
+ }
+ }
+ }
+
+ function offsetString(offset: string | number) {
+ return (typeof offset === "number") ? offset + "%" : offset;
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/view.ts b/sim/visuals/view.ts
new file mode 100644
index 00000000..9b5d27e6
--- /dev/null
+++ b/sim/visuals/view.ts
@@ -0,0 +1,257 @@
+namespace pxsim.visuals {
+ export abstract class View {
+ protected element: SVGGElement;
+ protected rendered = false;
+ protected visible = false;
+ protected width: number = 0;
+ protected left: number = 0;
+ protected top: number = 0;
+ protected scaleFactor: number = 1;
+
+ protected theme: IBoardTheme;
+
+ protected abstract buildDom(width: number): SVGElement;
+ public abstract getInnerWidth(): number;
+ public abstract getInnerHeight(): number;
+
+ public inject(parent: SVGElement, width?: number, visible = true) {
+ this.width = width;
+ parent.appendChild(this.getView());
+
+ if (visible) {
+ this.visible = true;
+ this.onComponentInjected();
+ }
+ }
+
+ public getWidth() {
+ return this.scaleFactor == undefined ? this.getInnerWidth() : this.getInnerWidth() * this.scaleFactor;
+ }
+
+ public getHeight() {
+ return this.scaleFactor == undefined ? this.getInnerHeight() : this.getInnerHeight() * this.scaleFactor;
+ }
+
+ public onComponentInjected() {
+ // To be overridden by sub class
+ }
+
+ public onComponentVisible() {
+ // To be overridden by sub class
+ }
+
+ public onComponentHidden() {
+ // To be overridden by sub class
+ }
+
+ public translate(x: number, y: number, applyImmediately = true) {
+ this.left = x;
+ this.top = y;
+
+ if (applyImmediately) {
+ this.updateTransform();
+ }
+ }
+
+ public scale(scaleFactor: number, applyImmediately = true) {
+ this.scaleFactor = scaleFactor;
+
+ if (applyImmediately) {
+ this.updateTransform();
+ }
+ }
+
+ public shouldUpdateState() {
+ return true;
+ }
+
+ public updateState() {
+ }
+
+ public updateTheme(theme: IBoardTheme) {
+ this.theme = theme;
+ this.updateThemeCore();
+ }
+
+ public updateThemeCore() {
+ }
+
+ public setVisible(visible: boolean) {
+ if (this.rendered) {
+ this.getView().style.display = visible ? 'block' : 'none';
+ }
+ }
+
+ public hasClick() {
+ return true;
+ }
+
+ private onClickHandler: (ev: any) => void;
+ public registerClick(handler: (ev: any) => void) {
+ this.onClickHandler = handler;
+ this.getView().addEventListener(pointerEvents.up, this.onClickHandler);
+ }
+
+ public dispose() {
+ if (this.onClickHandler) this.getView().removeEventListener(pointerEvents.up, this.onClickHandler)
+ View.dispose(this);
+ }
+
+ protected getView() {
+ if (!this.rendered) {
+ this.element = svg.elt("g") as SVGGElement;
+ View.track(this);
+
+ const content = this.buildDom(this.width);
+ if (content) {
+ this.element.appendChild(content);
+ }
+ this.updateTransform();
+ this.rendered = true;
+ }
+ return this.element;
+ }
+
+ public resize(width: number) {
+ this.width = width;
+ }
+
+ private updateTransform() {
+ if (this.rendered) {
+ let transform = `translate(${this.left} ${this.top})`;
+
+ if (this.scaleFactor !== 1) {
+ transform += ` scale(${this.scaleFactor})`;
+ }
+
+ this.element.setAttribute("transform", transform);
+ }
+ }
+
+ private static currentId = 0;
+ private static allViews: Map = {};
+
+ protected static getInstance(element: Element) {
+ if (element.hasAttribute("ref-id")) {
+ return View.allViews[element.getAttribute("ref-id")];
+ }
+
+ return undefined;
+ }
+
+ private static track(view: View) {
+ const myId = "id-" + (View.currentId++);
+ view.element.setAttribute("ref-id", myId);
+ View.allViews[myId] = view;
+ }
+
+ private static dispose(view: View) {
+ if (view.element) {
+ const id = view.element.getAttribute("ref-id");
+ // TODO: Remove from DOM
+ view.element.parentNode.removeChild(view.element);
+ delete View.allViews[id];
+ }
+ }
+ }
+
+ export abstract class SimView extends View implements LayoutElement {
+ constructor(protected state: T) {
+ super();
+ }
+
+ public getId() {
+ return this.state.id;
+ }
+
+ public getPort() {
+ return this.state.port;
+ }
+
+ public getPaddingRatio() {
+ return 0;
+ }
+
+ public getWiringRatio() {
+ return 0.5;
+ }
+
+ public setSelected(selected: boolean) { }
+
+ protected getView() {
+ if (!this.rendered) {
+ this.subscribe();
+ }
+ return super.getView();
+ }
+
+ protected onBoardStateChanged() {
+ // To be implemented by sub class
+ }
+
+ protected subscribe() {
+ board().updateSubscribers.push(() => {
+ if (this.state.didChange()) {
+ this.onBoardStateChanged();
+ }
+ });
+ }
+ }
+
+ export class ViewContainer extends View {
+ public getInnerWidth() {
+ return 0;
+ }
+
+ public getInnerHeight() {
+ return 0;
+ }
+
+ public addView(view: View) {
+ view.inject(this.element);
+ }
+
+ public clear() {
+ forEachElement(this.element.childNodes, e => {
+ this.element.removeChild(e);
+ });
+ }
+
+ public onComponentInjected() {
+ const observer = new MutationObserver(records => {
+ records.forEach(r => {
+ forEachElement(r.addedNodes, node => {
+ const instance = View.getInstance(node);
+ if (instance) {
+ instance.onComponentVisible();
+ }
+ });
+ forEachElement(r.removedNodes, node => {
+ const instance = View.getInstance(node);
+ if (instance) {
+ instance.onComponentHidden();
+ }
+ });
+ })
+ });
+
+ observer.observe(this.element, {
+ childList: true,
+ subtree: true
+ });
+ }
+
+ protected buildDom(width: number): SVGElement {
+ return undefined;
+ }
+ }
+
+ function forEachElement(nodes: NodeList, cb: (e: Element) => void) {
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ cb(node as Element);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sim/visuals/wireView.ts b/sim/visuals/wireView.ts
new file mode 100644
index 00000000..df1dd2a6
--- /dev/null
+++ b/sim/visuals/wireView.ts
@@ -0,0 +1,99 @@
+///
+
+namespace pxsim.visuals {
+
+ export class WireView extends View implements LayoutElement {
+ private wire: SVGSVGElement;
+ private path: SVGPathElement;
+ private selected: boolean;
+ private hasDimensions: boolean;
+
+ protected startX: number;
+ protected startY: number;
+ protected endX: number;
+ protected endY: number;
+
+ constructor(private port: number) {
+ super();
+ }
+
+ isRendered() {
+ return !!this.wire;
+ }
+
+ updateDimensions(startX: number, startY: number, endX: number, endY: number) {
+ this.startX = startX;
+ this.startY = startY;
+ this.endX = endX;
+ this.endY = endY;
+ this.hasDimensions = true;
+ this.updatePath();
+ }
+
+ buildDom(width: number): SVGElement {
+ this.wire = svg.elt("svg", { height: "100%", width: "100%" }) as SVGSVGElement;
+ this.path = pxsim.svg.child(this.wire, "path", {
+ 'd': '',
+ 'fill': 'transparent',
+ 'stroke': '#5A5A5A',
+ 'stroke-width': '3px'
+ }) as SVGPathElement;
+ this.setSelected(true);
+ return this.wire;
+ }
+
+ updatePath() {
+ if (!this.hasDimensions) return;
+ const height = this.endY - this.startY;
+ const quarterHeight = height / 4;
+ const middleHeight = this.port == 1 || this.port == 2 ? quarterHeight : quarterHeight * 2;
+ let d = `M${this.startX} ${this.startY}`;
+ d += ` L${this.startX} ${this.startY + middleHeight}`;
+ d += ` L${this.endX} ${this.startY + middleHeight}`;
+ d += ` L${this.endX} ${this.endY}`;
+ this.path.setAttribute('d', d);
+ }
+
+ getId() {
+ return -2;
+ }
+
+ getPort() {
+ return this.port;
+ }
+
+ getPaddingRatio() {
+ return 0;
+ }
+
+ getWiringRatio() {
+ return 0.5;
+ }
+
+ getInnerWidth(): number {
+ return CONTROL_WIDTH;
+ }
+
+ getInnerHeight(): number {
+ return CONTROL_HEIGHT;
+ }
+
+ public setSelected(selected: boolean) {
+ this.selected = selected;
+ this.updateOpacity();
+ }
+
+ protected updateOpacity() {
+ const opacity = this.selected ? "0.2" : "1";
+ this.setOpacity(opacity);
+ }
+
+ protected setOpacity(opacity: string) {
+ this.element.setAttribute("opacity", opacity);
+ }
+
+ public hasClick() {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/svgicons/color.svg b/svgicons/color.svg
new file mode 100644
index 00000000..e6b38a43
--- /dev/null
+++ b/svgicons/color.svg
@@ -0,0 +1,9 @@
+
diff --git a/svgicons/generateIcons.js b/svgicons/generateIcons.js
new file mode 100644
index 00000000..30863fa8
--- /dev/null
+++ b/svgicons/generateIcons.js
@@ -0,0 +1,18 @@
+const webfontsGenerator = require('webfonts-generator');
+
+webfontsGenerator({
+ files: [
+ './ultrasonic.svg',
+ "./color.svg",
+ "./touch.svg",
+ "./gyro.svg"
+ ],
+ dest: '../docs/static/fonts/icons/',
+ round: 10
+}, function(error) {
+ if (error) {
+ console.log('Fail!', error);
+ } else {
+ console.log('Done!');
+ }
+})
\ No newline at end of file
diff --git a/svgicons/gyro.svg b/svgicons/gyro.svg
new file mode 100644
index 00000000..bb9ea16d
--- /dev/null
+++ b/svgicons/gyro.svg
@@ -0,0 +1,8 @@
+
diff --git a/svgicons/touch.svg b/svgicons/touch.svg
new file mode 100644
index 00000000..99a419a6
--- /dev/null
+++ b/svgicons/touch.svg
@@ -0,0 +1,25 @@
+
+
+
diff --git a/svgicons/ultrasonic.svg b/svgicons/ultrasonic.svg
new file mode 100644
index 00000000..dd77834c
--- /dev/null
+++ b/svgicons/ultrasonic.svg
@@ -0,0 +1,25 @@
+
+
+
diff --git a/theme/blockly.less b/theme/blockly.less
index 2890b5ab..a2571983 100644
--- a/theme/blockly.less
+++ b/theme/blockly.less
@@ -4,6 +4,20 @@
@import 'blockly-core';
+/* Toolbox icons */
+
+/* Fonts for toolbox icons */
+@font-face {
+ font-family: 'legoIcons';
+ src: data-uri("../docs/static/fonts/icons/iconfont.woff2");
+}
+
+.blocklyFlyoutLabel:not(.blocklyFlyoutHeading) .blocklyFlyoutLabelIcon {
+ font-family: 'legoIcons';
+ fill: white;
+ font-size: 1.7rem;
+}
+
/* Toolbox padding */
.blocklyToolboxDiv, .monacoToolboxDiv {
padding: 0.5rem;
diff --git a/theme/site/collections/menu.variables b/theme/site/collections/menu.variables
index 65ebc7f4..54e59af3 100644
--- a/theme/site/collections/menu.variables
+++ b/theme/site/collections/menu.variables
@@ -4,4 +4,5 @@
@invertedItemTextColor: @white;
-@border: none;
\ No newline at end of file
+@border: none;
+@boxShadow: none;
\ No newline at end of file
diff --git a/theme/site/globals/site.variables b/theme/site/globals/site.variables
index 1d3779ea..9b19db37 100644
--- a/theme/site/globals/site.variables
+++ b/theme/site/globals/site.variables
@@ -88,6 +88,9 @@
@pageBackground: #fff;
+
+@inputPlaceholderColor: lighten(@inputColor, 80);
+
/*******************************
PXT Overrides
*******************************/
@@ -117,8 +120,8 @@
Background
--------------------*/
-@simulatorBackground: #fcfbfa;
-@editorToolsBackground: #fcfbfa;
+@simulatorBackground: #fff; /*#fcfbfa; */
+@editorToolsBackground: #fff; /*#fcfbfa; */
@blocklySvgColor: #ecf6fe;
@homeScreenBackground: #EAEEEF;
diff --git a/theme/style.less b/theme/style.less
index 5f4fb52c..6e4a45a0 100644
--- a/theme/style.less
+++ b/theme/style.less
@@ -89,10 +89,16 @@
/* Small Monitor */
@media only screen and (min-width: @computerBreakpoint) and (max-width: @largestSmallMonitor) {
+ #filelist, #downloadArea {
+ border-top: 2px solid #ECF6FF;
+ }
}
/* Large Monitor */
@media only screen and (min-width: @largeMonitorBreakpoint) {
+ #filelist, #downloadArea {
+ border-top: 2px solid #ECF6FF;
+ }
}
/* Mobile, Tablet AND thin screen */
@media only screen and (max-width: @largestTabletScreen) and (max-height: @thinEditorBreakpoint) {