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 2781c74a..882fe880 100644 --- a/libs/core/output.ts +++ b/libs/core/output.ts @@ -236,6 +236,11 @@ namespace motors { constructor(port: Output, large: boolean) { super(port); this._large = large; + this.markUsed(); + } + + markUsed() { + motors.__motorUsed(this._port, this._large); } protected __init() { @@ -347,6 +352,11 @@ namespace motors { constructor(ports: Output) { super(ports); + this.markUsed(); + } + + markUsed() { + motors.__motorUsed(this._port, true); } protected __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 @@ + + + + + + + + Color Sensor + + + + + + + + + + + + + + + + + + + + + + + + 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 = ` + + + + + + + Color Sensor + + + + + + + + + + + + + + + + + + + + + + + +`; +} \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EV3 + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EV3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; +} 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 @@ + + + + + + + + + + + + + Large Motor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 = ` + + + + + + + + + + + + Large Motor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; +} \ 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 @@ + + + + + + + + MediumMotor + + + + + + + + + + + + + + + + + + + + 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 = ` + + + + + + + Medium Motor + + + + + + + + + + + + + + + + + + + +`; +} \ 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 = ` + port + + + + + B + + +`; +} \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Touch sensor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 = ` + + + + + + + + + + + + + + + + + + + + + + + + + Touch sensor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; +} 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 @@ + + + + + + + + + + + + + + + gyro + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 = ` + + + + + + + + + + + + + + gyro + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; +} \ 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 @@ + + port + + + + + B + + + \ 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 @@ + + + + + + + + + + ultra sonic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 = ` + + + + + + + + + ultra sonic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; +} \ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - x, y - - diff --git a/sim/visuals/board.ts b/sim/visuals/board.ts index c6d1fafb..e6a2c589 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.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 = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - x, y - - - -`; -} \ 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 @@ + + color + + + + + + + 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 @@ + + gyro + + + + + + 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) {