From 6320379d029f1a6ef458bbedd9e5e68051449f9e Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Mon, 18 Dec 2017 13:04:17 -0800 Subject: [PATCH 1/2] Initial sim implementation --- docs/static/MC-LEGO-loader-eyes.gif | Bin 0 -> 70781 bytes docs/static/fonts/icons/iconfont.css | 30 + docs/static/fonts/icons/iconfont.eot | Bin 0 -> 2504 bytes docs/static/fonts/icons/iconfont.svg | 24 + docs/static/fonts/icons/iconfont.ttf | Bin 0 -> 2336 bytes docs/static/fonts/icons/iconfont.woff | Bin 0 -> 1464 bytes docs/static/fonts/icons/iconfont.woff2 | Bin 0 -> 1044 bytes legoresources/SVGassets/.DS_Store | Bin 6148 -> 0 bytes libs/color-sensor/color.ts | 4 +- libs/core/_locales/core-strings.json | 1 + libs/core/input.cpp | 11 + libs/core/input.ts | 5 + libs/core/output.cpp | 10 + libs/core/output.ts | 10 + libs/core/pxt.json | 1 + libs/core/shims.d.ts | 16 + libs/core/sim/analogSensor.ts | 81 -- libs/core/sim/pins.ts | 177 ---- libs/ev3/ns.ts | 4 + package.json | 3 +- pxtarget.json | 6 +- sim/dalboard.ts | 140 ++- sim/state/analog.ts | 68 ++ sim/state/brick.ts | 27 + sim/state/color.ts | 46 + sim/state/control.ts | 1 - sim/state/gyro.ts | 47 + sim/state/input.ts | 18 + sim/state/light.ts | 2 +- sim/state/motor.ts | 49 + sim/state/motors.ts | 73 ++ sim/state/nodeTypes.ts | 36 + sim/state/output.ts | 104 ++ sim/state/screen.ts | 15 +- sim/state/sensor.ts | 73 ++ sim/state/touch.ts | 42 + sim/state/uart.ts | 156 +++ sim/state/ultrasonic.ts | 31 + sim/state/util.ts | 7 + sim/visuals/assets/Color Sensor.svg | 32 + sim/visuals/assets/ColorSensorsvg.ts | 34 + sim/visuals/assets/EV3.svg | 103 ++ sim/visuals/assets/EV3svg.ts | 108 +++ sim/visuals/assets/Large Motor.svg | 74 ++ sim/visuals/assets/LargeMotorsvg.ts | 76 ++ sim/visuals/assets/MediumMotor.svg | 28 + sim/visuals/assets/MediumMotorsvg.ts | 30 + sim/visuals/assets/Portsvg.ts | 13 + sim/visuals/assets/Touch sensor.svg | 61 ++ sim/visuals/assets/TouchSensorsvg.ts | 64 ++ sim/visuals/assets/gyro.svg | 51 + sim/visuals/assets/gyrosvg.ts | 54 ++ sim/visuals/assets/port.svg | 10 + sim/visuals/assets/ultra sonic.svg | 77 ++ sim/visuals/assets/ultrasonicsvg.ts | 79 ++ sim/visuals/board.svg | 1198 ----------------------- sim/visuals/board.ts | 979 ++++++------------- sim/visuals/boardsvg.ts | 1200 ------------------------ sim/visuals/boardview.ts | 2 +- sim/visuals/controlView.ts | 51 + sim/visuals/controls/closeIcon.ts | 29 + sim/visuals/controls/colorGrid.ts | 41 + sim/visuals/controls/colorWheel.ts | 34 + sim/visuals/controls/distanceSlider.ts | 120 +++ sim/visuals/controls/rotationSlider.ts | 94 ++ sim/visuals/layoutView.ts | 298 ++++++ sim/visuals/nodes/brickView.ts | 196 ++++ sim/visuals/nodes/colorSensorView.ts | 16 + sim/visuals/nodes/gyroSensorView.ts | 14 + sim/visuals/nodes/largeMotorView.ts | 62 ++ sim/visuals/nodes/mediumMotorView.ts | 68 ++ sim/visuals/nodes/portView.ts | 25 + sim/visuals/nodes/staticView.ts | 123 +++ sim/visuals/nodes/touchSensorView.ts | 67 ++ sim/visuals/nodes/ultrasonicView.ts | 10 + sim/visuals/pincontrol.ts | 92 -- sim/visuals/util.ts | 99 ++ sim/visuals/view.ts | 257 +++++ sim/visuals/wireView.ts | 99 ++ svgicons/color.svg | 9 + svgicons/generateIcons.js | 18 + svgicons/gyro.svg | 8 + svgicons/touch.svg | 25 + svgicons/ultrasonic.svg | 25 + theme/blockly.less | 14 + theme/site/collections/menu.variables | 3 +- theme/site/globals/site.variables | 7 +- theme/style.less | 6 + 88 files changed, 3949 insertions(+), 3552 deletions(-) create mode 100644 docs/static/MC-LEGO-loader-eyes.gif create mode 100644 docs/static/fonts/icons/iconfont.css create mode 100644 docs/static/fonts/icons/iconfont.eot create mode 100644 docs/static/fonts/icons/iconfont.svg create mode 100644 docs/static/fonts/icons/iconfont.ttf create mode 100644 docs/static/fonts/icons/iconfont.woff create mode 100644 docs/static/fonts/icons/iconfont.woff2 delete mode 100644 legoresources/SVGassets/.DS_Store create mode 100644 libs/core/input.cpp delete mode 100644 libs/core/sim/analogSensor.ts delete mode 100644 libs/core/sim/pins.ts create mode 100644 sim/state/analog.ts create mode 100644 sim/state/brick.ts create mode 100644 sim/state/color.ts create mode 100644 sim/state/gyro.ts create mode 100644 sim/state/input.ts create mode 100644 sim/state/motor.ts create mode 100644 sim/state/motors.ts create mode 100644 sim/state/nodeTypes.ts create mode 100644 sim/state/output.ts create mode 100644 sim/state/sensor.ts create mode 100644 sim/state/touch.ts create mode 100644 sim/state/uart.ts create mode 100644 sim/state/ultrasonic.ts create mode 100644 sim/state/util.ts create mode 100644 sim/visuals/assets/Color Sensor.svg create mode 100644 sim/visuals/assets/ColorSensorsvg.ts create mode 100644 sim/visuals/assets/EV3.svg create mode 100644 sim/visuals/assets/EV3svg.ts create mode 100644 sim/visuals/assets/Large Motor.svg create mode 100644 sim/visuals/assets/LargeMotorsvg.ts create mode 100644 sim/visuals/assets/MediumMotor.svg create mode 100644 sim/visuals/assets/MediumMotorsvg.ts create mode 100644 sim/visuals/assets/Portsvg.ts create mode 100644 sim/visuals/assets/Touch sensor.svg create mode 100644 sim/visuals/assets/TouchSensorsvg.ts create mode 100644 sim/visuals/assets/gyro.svg create mode 100644 sim/visuals/assets/gyrosvg.ts create mode 100644 sim/visuals/assets/port.svg create mode 100644 sim/visuals/assets/ultra sonic.svg create mode 100644 sim/visuals/assets/ultrasonicsvg.ts delete mode 100644 sim/visuals/board.svg delete mode 100644 sim/visuals/boardsvg.ts create mode 100644 sim/visuals/controlView.ts create mode 100644 sim/visuals/controls/closeIcon.ts create mode 100644 sim/visuals/controls/colorGrid.ts create mode 100644 sim/visuals/controls/colorWheel.ts create mode 100644 sim/visuals/controls/distanceSlider.ts create mode 100644 sim/visuals/controls/rotationSlider.ts create mode 100644 sim/visuals/layoutView.ts create mode 100644 sim/visuals/nodes/brickView.ts create mode 100644 sim/visuals/nodes/colorSensorView.ts create mode 100644 sim/visuals/nodes/gyroSensorView.ts create mode 100644 sim/visuals/nodes/largeMotorView.ts create mode 100644 sim/visuals/nodes/mediumMotorView.ts create mode 100644 sim/visuals/nodes/portView.ts create mode 100644 sim/visuals/nodes/staticView.ts create mode 100644 sim/visuals/nodes/touchSensorView.ts create mode 100644 sim/visuals/nodes/ultrasonicView.ts delete mode 100644 sim/visuals/pincontrol.ts create mode 100644 sim/visuals/util.ts create mode 100644 sim/visuals/view.ts create mode 100644 sim/visuals/wireView.ts create mode 100644 svgicons/color.svg create mode 100644 svgicons/generateIcons.js create mode 100644 svgicons/gyro.svg create mode 100644 svgicons/touch.svg create mode 100644 svgicons/ultrasonic.svg diff --git a/docs/static/MC-LEGO-loader-eyes.gif b/docs/static/MC-LEGO-loader-eyes.gif new file mode 100644 index 0000000000000000000000000000000000000000..fc2fde336d8ea6cc37e1edf4981c2e77b289c8e1 GIT binary patch literal 70781 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

SGA!VHx;;m+*Tlj3yN^?;iF^Ty75@g)?tDSJ z+`-DSGVM1x{!ba5j9yb0_W6e;DxFy}mP#2msY?`kguSThabytYf7FM4^u&<$e(QET zakFB~j;9T&b#5II-L`nP86dkGB%Vh}jcsB~3QQG9WjY@W`o;VwLYF;4sNq?Hu2(S2 z@qq|ku|z}idH{Ps;Tsz*`^4DdvYC>n&BpA|6J3>2v)*g%=gSE>0W`BjCg5^*^u z3E&V76}~krecO*@iQgp6gkV`x)`0iUcEW&Yin2^=oq|jspgx{v^cC3SWnHFP18I5$ zNYjzkBPXIvf6_E&FF`p`z{}SxWJ_vTZaO}AA8t1dnfWqPM2QCN36Sd2*tN`$sRU-J z*3VxA60{Vxc01DH7wa0Hk7Yq;m+=U9Z9UrnRM7@>T1!%;VG0cGkKVal`YfTKJ0uro8Ihi9EdS@))z z84Y0+H%o^;Mp+gagl*P)@(}<45{-z1zTbTMitL>amtwVajdk98v@V(+Qf8vZ#${C?9KhfR#3lKKBhD z$k^_u7Uo(+qWEpb0}oFHCRa+^YeN~z#YCIX&#zla>nVE*X`7aC3R1?S z9lSK@xPcoRUZ+49r_36Z*Z+AM^2BWS{W|%TDRCqIVN#f;t@SE7slvJ5Ex%0%F(UuptEA*0{`h;OD;LhW3}yz0t;^| z{xA7hR(#oS?y&hr$sgL?S>Ae^wB-Y>%3Ei}tE?_2U@uI@D;&3o%&)F*ge+dF{G56q z8=n0wQo8x@U#{Ty2zeKBohhgD7yj>!u$<|S%7l``+EeE?5wMLI?u@nR ztyG87hbOc#q01x?Xt!MCgwdKLbX%epQl=es?lbWdnDDYsw-ZOzNBD=-7*F zcxL{vS@>Fs#`iG~Vl!4UH6kbU0Ccb_BRCM)IAq|^=7VdZtI?vxQ3LxcUonZevF~oJBIuMJcMuea(zSC56f%DL*Pj#XP-1s

yh)ZrH zcy+@OrImW7PBi8hVS13LKbzq$G0`aE793t7fi|(|l?0EE1C`APh*R+&3&h31-EJ)( z(*C(&MQx?ccl{)1q|2L+?z z8=O!Q5aIpjl{R~OPIVKtx4M9+EdjgP3}?@A9Ep-30xp9)7_jW^_tbEb+X?abeZMR1 z)W2=D=$p>9qT}$`FE{)LW4Yt#ZCgFJwJ_v9a5Qat6T0=Ev>a{viu_QT4v`Ptrdbh? z+bPQP5H&uW(2Lq+t8JJu2`kVQm+T28AN(1Aq0n<2+YQ`Tio3b}UL2f5$69%?&-|xbt?Mh0k&tL&k1&k<&YU z-}z>pB|*?u$aBpQFB3T3(t=RZHJTh$7jn$SJrV$9B6Ebn}hRXpOSsD(PPIinFqx<}jdk}7>Gy}=2+4v^*< zn|Zc3C(^nF!I$U7*TxhYWOt1m6W(wv`k>TJMzK7&S}MbE!Cja`^W-&uvt-d;^BUL5 zQ&`Y)^ZNY!S#)O|*jqDpp*gk#T6;*B{1{`G4&D{Hc<7Zi4|>A~Yw?h%b8MMd*^tU6 z+#*eNW_`YT_=>cpTpQi~gi7a->1(eK@CIem^zkHosv(W_7@QsaVIjU99ZRe>QaRXo zY1On2H&*%HG*uJkquO3eICqR;lMQM}AUYnK4i^Q~2KUKqE_?l&an55bGNt+68d(A( zWiN~3uH4S#u%;B@#00wUMmfA-7u6LI-be-6xFv%}P*zd`+tc%x%*S^Um_e(oYGotx z{(Q!5GN1Uk*C)hBPwekALz{$xQsf9)qJCXL0=n|>hR*8qTMn?jRYagTy+YaBcpEI_ zBG*KSdj1f88fqD#oXQ7sOz_XBMpWH{vt9%(nY5mo1^^X8O4nKzgQuy>4xan@%Oi!l z?iCDgyQ-D|x_% z=MEXk(&Dt6S}zgj&6T`|F8tdJwu&lryiIo9u(3kdK*NA1_=nw2K6p< zF+8YUlgj#zqo?}Q^S({xL$8?suf6mBr#kNczwOw^-W+>`jy*Dtee8YA?A@?AnFj~w z*pw(EvV|m}NM?x29%b(&TO{TCrmpMr{U5Ggf5YpC`|WYNKOYbFO@�#jMF6E(+2 zLrId=R3KAPf;ijQMa#fu@>v^pzM%McySl=xz)&znXhy6q1%TRfot_REEA--b+5ptf z*{@*5-FpNTgQ);Vi~hOiEq55vVz&zi($;YsIJ1Y90-NkAIp&Q|m61!|xU3W@jH0y1 zluF)LS+*+a9~Q_$xq=v4ZPCBoX(pBlH0psRanGk-VfV@Q)7jL>9u%n=Zf?iaeT?2F zh%VZ1Xu{*!nyvbXFjgaY0~`nk1T`)HBx50%&lme0TycYc{p0pq2 zU`f0X!?SbcDg8lI)VcE(=cfNd<8$8(_4!Fd#4tY|(sw}C?^eCm$H^Pkb*NycOVl>e zJv`F1VoCp=QfpxA!YwJ@dK1LFjFp&o# z6C*pYRegz;3(;&vA$2+U$a400%Orc%c;j-5R~yVXayZ54u}#qU+6{_59N7OgO``$_ zfTrn%e>%uN9ps-5^1rHs2>%DLC4ekj=WmwXFw!V_3E0IxKD+lW1nUHW>?d7+0DFxU z)+jrBpzs^a`_0&e@>_0NtNP;KEW1a0IfjSl{?5gE%kCYU>uG

|2J43p=o9QW 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 Lx?bBJgqZ&qf!Ye^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1eb7bf3d514fc8fcd5e86233366c75377ee775be GIT binary patch literal 2336 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 YykM^-FRpd)GeX`H4hwD>D{=t%2gCwdN&o-= literal 0 HcmV?d00001 diff --git a/docs/static/fonts/icons/iconfont.woff2 b/docs/static/fonts/icons/iconfont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a27e0d1f503a8decc940ab2be7e0d3aa4115878e GIT binary patch literal 1044 zcmV+v1nc{EPew8T0RR9100a~O3jhEB00|%f00YPX0RR9100000000000000000000 z0000SR0dW6gBl8k9ECvvHUcCA6blRh1Rw>376(fk^9K=?oWIDv7_gg*Ss5FdnWWK{ z2jBFVTAH^#d&0Wu2-kU8)_j2n3X z0x%b}7PJw{S2zOVFVMnAH}&*vSRE#l!MKUTVSiMw_-;HNOeWut zM*U&)T}}Lb`@ogKE{cfyw zIq~-oO@*JM3hK7iAq0yfc83PisEM(!FCv3;RjnG;M73+Nutb4j6TR&Nony zW@RA;G1e8WF*FC(#u!hlg{T%)p{{Q3YMVieQyNuOJ()C3Y>=w6ObCC?q|z5l(~{&e zWlDDGa<_b`-&*<;>rApeYf9%#afvIp`lZYLiG6DLELJZwlDBhhCYfbInVD<6DBSfr zTZY}f-GTSqD`5J*y0*Pprxuve>3W|jZ zxm24BnW&DU5t*rz6X^itoxErK(Cu{N(vKP0@=VzrRsSR6F*0#h2+3oh7mqkCmQxo) zF3VypA5x4llX_tK6CXAuklUIBtO*P*8hZP zRqLJ$Y;;)0J7uobI|TwIoD@aphCIotkJ+k5(K{;%A5VxkQ!s1Lgf(n>Zl|gWArN{c zII+xOm8QCFPa?2xIf0E%ew%m7T&s7Iy5-oDqUdliKFB4q5rD0<8O8k0N?zvU3F6HZ zn6)g^gf-aAGpou55QO%5UI|Xv8NRTRNH1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0= 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) { From 84c1079e505b85ab57a20b93effed9964ce0e1fe Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Mon, 18 Dec 2017 13:19:49 -0800 Subject: [PATCH 2/2] Fix Safari bug. --- sim/visuals/board.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sim/visuals/board.ts b/sim/visuals/board.ts index e6a2c589..d74b6359 100644 --- a/sim/visuals/board.ts +++ b/sim/visuals/board.ts @@ -398,7 +398,7 @@ namespace pxsim.visuals { let state = ev3board().screenState; const bBox = this.layoutView.getBrick().getScreenBBox(); - if (bBox.width == 0) return; + if (!bBox || bBox.width == 0) return; const scale = bBox.width / SCREEN_WIDTH; this.screenScaledWidth = bBox.width;