From 180f32f25c613495769029c7ff8d6282d7377c8e Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Fri, 22 Dec 2017 14:00:23 -0800 Subject: [PATCH] Simulator refactoring to support better resizing of modules and controls --- sim/dalboard.ts | 30 +- sim/state/color.ts | 8 +- sim/state/motors.ts | 1 + sim/state/uart.ts | 2 +- sim/visuals/board.ts | 154 ++++----- sim/visuals/controlView.ts | 32 +- sim/visuals/controls/closeIcon.ts | 11 + sim/visuals/controls/colorGrid.ts | 2 +- sim/visuals/controls/colorWheel.ts | 2 +- sim/visuals/controls/distanceSlider.ts | 3 +- sim/visuals/controls/rotationSlider.ts | 3 +- sim/visuals/layoutView.ts | 300 +++++++++++------- sim/visuals/nodes/brickView.ts | 10 +- sim/visuals/nodes/colorSensorView.ts | 6 +- sim/visuals/nodes/gyroSensorView.ts | 4 +- sim/visuals/nodes/largeMotorView.ts | 4 +- sim/visuals/nodes/mediumMotorView.ts | 6 +- .../nodes/{staticView.ts => moduleView.ts} | 17 +- sim/visuals/nodes/portView.ts | 4 +- sim/visuals/nodes/touchSensorView.ts | 4 +- sim/visuals/nodes/ultrasonicView.ts | 4 +- sim/visuals/view.ts | 89 +++++- sim/visuals/wireView.ts | 13 +- 23 files changed, 408 insertions(+), 301 deletions(-) rename sim/visuals/nodes/{staticView.ts => moduleView.ts} (87%) diff --git a/sim/dalboard.ts b/sim/dalboard.ts index 9ad960b8..b542acb0 100644 --- a/sim/dalboard.ts +++ b/sim/dalboard.ts @@ -23,20 +23,6 @@ namespace pxsim { } export class EV3Board extends CoreBoard { - // state & update logic for component services - // neopixelState: CommonNeoPixelState; - buttonState: EV3ButtonState; - slideSwitchState: SlideSwitchState; - lightSensorState: AnalogSensorState; - thermometerState: AnalogSensorState; - thermometerUnitState: number; - microphoneState: AnalogSensorState; - edgeConnectorState: EdgeConnectorState; - capacitiveSensorState: CapacitiveSensorState; - accelerometerState: AccelerometerState; - touchButtonState: TouchButtonState; - irState: InfraredState; - view: SVGSVGElement; outputState: EV3OutputState; @@ -86,11 +72,6 @@ namespace pxsim { // TODO break; } - case "irpacket": { - let ev = msg; - this.irState.receive(new RefBuffer(ev.packet)); - break; - } } } @@ -120,6 +101,9 @@ namespace pxsim { document.body.innerHTML = ""; // clear children document.body.appendChild(this.view = viewHost.getView() as SVGSVGElement); + this.inputNodes = []; + this.outputNodes = []; + return Promise.resolve(); } @@ -127,14 +111,6 @@ namespace pxsim { return svg.toDataUri(new XMLSerializer().serializeToString(this.view)); } - //defaultNeopixelPin() { - // return this.edgeConnectorState.getPin(CPlayPinName.D8); - //} - - getDefaultPitchPin() { - return this.edgeConnectorState.getPin(CPlayPinName.D6); - } - getBrickNode() { return this.brickNode; } diff --git a/sim/state/color.ts b/sim/state/color.ts index 42ce61df..ad5f7ac5 100644 --- a/sim/state/color.ts +++ b/sim/state/color.ts @@ -20,7 +20,7 @@ namespace pxsim { export class ColorSensorNode extends UartSensorNode { id = NodeType.ColorSensor; - private color: number; + private color: number = 50; constructor(port: number) { super(port); @@ -31,10 +31,8 @@ namespace pxsim { } setColor(color: number) { - if (this.color != color) { - this.color = color; - this.setChangedState(); - } + this.color = color; + this.setChangedState(); } getValue() { diff --git a/sim/state/motors.ts b/sim/state/motors.ts index fcf71bcd..8abb3c75 100644 --- a/sim/state/motors.ts +++ b/sim/state/motors.ts @@ -48,6 +48,7 @@ namespace pxsim { stop() { // TODO: implement + this.setSpeed(0); } start() { diff --git a/sim/state/uart.ts b/sim/state/uart.ts index eeabdb9d..cc612b03 100644 --- a/sim/state/uart.ts +++ b/sim/state/uart.ts @@ -88,7 +88,7 @@ namespace pxsim { const inputNodes = ev3board().getInputNodes(); for (let port = 0; port < DAL.NUM_INPUTS; port++) { const node = inputNodes[port]; - if (node) { + if (node && node.isUart()) { // Actual const index = 0; //UartOff.Actual + port * 2; util.map16Bit(data, UartOff.Raw + DAL.MAX_DEVICE_DATALENGTH * 300 * port + DAL.MAX_DEVICE_DATALENGTH * index, Math.floor(node.getValue())) diff --git a/sim/visuals/board.ts b/sim/visuals/board.ts index e6239b92..ba68a607 100644 --- a/sim/visuals/board.ts +++ b/sim/visuals/board.ts @@ -99,16 +99,9 @@ namespace pxsim.visuals { 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; @@ -143,7 +136,12 @@ namespace pxsim.visuals { Runtime.messagePosted = (msg) => { switch (msg.type || "") { - case "status": if ((msg as pxsim.SimulatorStateMessage).state == "killed") this.kill(); break; + case "status": { + const state = (msg as pxsim.SimulatorStateMessage).state; + if (state == "killed") this.kill(); + if (state == "running") this.begin(); + break; + } } } } @@ -177,36 +175,6 @@ namespace pxsim.visuals { this.layoutView.updateTheme(theme); } - 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 getControlForNode(id: NodeType, port: number) { if (this.cachedControlNodes[id] && this.cachedControlNodes[id][port]) { return this.cachedControlNodes[id][port]; @@ -285,6 +253,10 @@ namespace pxsim.visuals { return undefined; } + private getCloseIconView() { + return new CloseIconControl(this.element, this.defs, new PortNode(-1), -1); + } + private buildDom() { this.wrapper = document.createElement('div'); this.wrapper.style.display = 'inline'; @@ -299,25 +271,10 @@ namespace pxsim.visuals { 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(); - }) - this.closeGroup.addView(this.closeIconView); - this.closeIconView.setVisible(false); - this.resize(); - //this.updateState(); // Add Screen canvas to board this.buildScreenCanvas(); @@ -329,6 +286,22 @@ namespace pxsim.visuals { window.addEventListener("resize", e => { this.resize(); }); + + setTimeout(() => { + this.resize(); + }, 200); + } + + public resize() { + if (!this.element) return; + const bounds = this.element.getBoundingClientRect(); + this.width = bounds.width; + this.height = bounds.height; + this.layoutView.layout(bounds.width, bounds.height); + + this.updateState(); + let state = ev3board().screenState; + this.updateScreenStep(state); } private buildScreenCanvas() { @@ -357,12 +330,27 @@ namespace pxsim.visuals { } private kill() { - if (this.lastAnimationId) cancelAnimationFrame(this.lastAnimationId); + this.running = false; + if (this.lastAnimationIds.length > 0) { + this.lastAnimationIds.forEach(animationId => { + cancelAnimationFrame(animationId); + }) + } } - private lastAnimationId: number; + private begin() { + this.running = true; + } + + private running: boolean = false; + private lastAnimationIds: number[] = []; public updateState() { - if (this.lastAnimationId) cancelAnimationFrame(this.lastAnimationId); + if (this.lastAnimationIds.length > 0) { + this.lastAnimationIds.forEach(animationId => { + cancelAnimationFrame(animationId); + }) + } + if (!this.running) return; const fps = GAME_LOOP_FPS; let now; let then = Date.now(); @@ -370,7 +358,8 @@ namespace pxsim.visuals { let delta; let that = this; function loop() { - that.lastAnimationId = requestAnimationFrame(loop); + const animationId = requestAnimationFrame(loop); + that.lastAnimationIds.push(animationId); now = Date.now(); delta = now - then; if (delta > interval) { @@ -382,67 +371,46 @@ namespace pxsim.visuals { } private updateStateStep(elapsed: number) { - const selected = this.layoutView.getSelected(); - let selectedChanged = false; const inputNodes = ev3board().getInputNodes(); inputNodes.forEach((node, index) => { node.updateState(elapsed); - if (!node.didChange()) return; const view = this.getDisplayViewForNode(node.id, index); + if (!node.didChange() && !view.didChange()) return; if (view) { - this.layoutView.setInput(index, view); + const control = view.getSelected() ? this.getControlForNode(node.id, index) : undefined; + const closeIcon = control ? this.getCloseIconView() : undefined; + this.layoutView.setInput(index, view, control, closeIcon); view.updateState(); - if (selected == view) selectedChanged = true; + if (control) control.updateState(); } }); const brickNode = ev3board().getBrickNode(); if (brickNode.didChange()) { - this.getDisplayViewForNode(ev3board().getBrickNode().id, -1).updateState(); + this.getDisplayViewForNode(brickNode.id, -1).updateState(); } const outputNodes = ev3board().getMotors(); outputNodes.forEach((node, index) => { node.updateState(elapsed); - if (!node.didChange()) return; const view = this.getDisplayViewForNode(node.id, index); + if (!node.didChange() && !view.didChange()) return; if (view) { - this.layoutView.setOutput(index, view); + const control = view.getSelected() ? this.getControlForNode(node.id, index) : undefined; + const closeIcon = control ? this.getCloseIconView() : undefined; + this.layoutView.setOutput(index, view, control, closeIcon); view.updateState(); - if (selected == view) selectedChanged = true; + if (control) control.updateState(); } }); - 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); - this.resize(); - } else if (!selected) { - this.controlGroup.clear(); - this.controlView = undefined; - this.selectedNode = undefined; - this.selectedPort = undefined; - this.closeIconView.setVisible(false); + let state = ev3board().screenState; + if (!state.didChange()) { + this.updateScreenStep(state); } - - if (selectedChanged && selected) { - this.controlView.updateState(); - } - - this.updateScreenStep(); } - private updateScreenStep() { - let state = ev3board().screenState; - if (!state.didChange()) return; - + private updateScreenStep(state: EV3ScreenState) { const bBox = this.layoutView.getBrick().getScreenBBox(); if (!bBox || bBox.width == 0) return; diff --git a/sim/visuals/controlView.ts b/sim/visuals/controlView.ts index 6d0fa6a9..6c9f1dd5 100644 --- a/sim/visuals/controlView.ts +++ b/sim/visuals/controlView.ts @@ -1,4 +1,4 @@ -/// +/// namespace pxsim.visuals { @@ -6,7 +6,7 @@ namespace pxsim.visuals { export const CONTROL_HEIGHT = 175; export abstract class ControlView extends SimView implements LayoutElement { - private background: SVGSVGElement; + protected content: SVGSVGElement; abstract getInnerView(parent: SVGSVGElement, globalDefs: SVGDefsElement): SVGElement; @@ -34,18 +34,30 @@ namespace pxsim.visuals { 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; + buildDom(): SVGElement { + this.content = svg.elt("svg", { viewBox: `0 0 ${this.getInnerWidth()} ${this.getInnerHeight()}`}) as SVGSVGElement; + this.content.appendChild(this.getInnerView(this.parent, this.globalDefs)); + return this.content; + } + + public resize(width: number, height: number) { + super.resize(width, height); + this.updateDimensions(width, height); + } + + private updateDimensions(width: number, height: number) { + if (this.content) { + const currentWidth = this.getInnerWidth(); + const currentHeight = this.getInnerHeight(); + const newHeight = currentHeight / currentWidth * width; + const newWidth = currentWidth / currentHeight * height; + this.content.setAttribute('width', `${width}`); + this.content.setAttribute('height', `${newHeight}`); + } } onComponentVisible() { } - - getWeight() { - return 0; - } } } \ No newline at end of file diff --git a/sim/visuals/controls/closeIcon.ts b/sim/visuals/controls/closeIcon.ts index 02b5307f..f16b1650 100644 --- a/sim/visuals/controls/closeIcon.ts +++ b/sim/visuals/controls/closeIcon.ts @@ -17,6 +17,17 @@ namespace pxsim.visuals { return this.closeGroup; } + + buildDom(): SVGElement { + this.content = svg.elt("svg", { width: "100%", height: "100%"}) as SVGSVGElement; + this.content.appendChild(this.getInnerView()); + return this.content; + } + + public resize(width: number, height: number) { + super.resize(width, height); + } + public getInnerHeight() { return 32; } diff --git a/sim/visuals/controls/colorGrid.ts b/sim/visuals/controls/colorGrid.ts index f8faca73..c1d6b6fc 100644 --- a/sim/visuals/controls/colorGrid.ts +++ b/sim/visuals/controls/colorGrid.ts @@ -7,7 +7,7 @@ namespace pxsim.visuals { getInnerView() { this.group = svg.elt("g") as SVGGElement; - this.group.setAttribute("transform", `translate(17, ${35 + this.getHeight() / 4}) scale(5)`) + this.group.setAttribute("transform", `translate(17, ${20 + this.getHeight() / 4}) scale(5)`) const colorIds = ['red', 'yellow', 'blue', 'green', 'black', 'grey']; const colors = ['#f12a21', '#ffd01b', '#006db3', '#00934b', '#000', '#6c2d00']; diff --git a/sim/visuals/controls/colorWheel.ts b/sim/visuals/controls/colorWheel.ts index 80ff4ccf..0d20763f 100644 --- a/sim/visuals/controls/colorWheel.ts +++ b/sim/visuals/controls/colorWheel.ts @@ -10,7 +10,7 @@ namespace pxsim.visuals { getInnerView(parent: SVGSVGElement) { this.defs = svg.child(this.element, "defs", {}); this.group = svg.elt("g") as SVGGElement; - this.group.setAttribute("transform", `translate(12, ${this.getHeight() / 2 - 15}) scale(2.5)`) + this.group.setAttribute("transform", `translate(12, ${this.getHeight() / 2 - 15}) scale(2)`) let gc = "gradient-color"; this.colorGradient = svg.linearGradient(this.defs, gc, true); diff --git a/sim/visuals/controls/distanceSlider.ts b/sim/visuals/controls/distanceSlider.ts index 033b8b0d..d3b28cce 100644 --- a/sim/visuals/controls/distanceSlider.ts +++ b/sim/visuals/controls/distanceSlider.ts @@ -100,7 +100,8 @@ namespace pxsim.visuals { 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 bBox = this.content.getBoundingClientRect(); + let t = Math.max(0, Math.min(1, (this.getTopPadding() + height + bBox.top / this.scaleFactor - cur.y / this.scaleFactor) / height)) const state = this.state; state.setDistance((1 - t) * (this.getMax())); diff --git a/sim/visuals/controls/rotationSlider.ts b/sim/visuals/controls/rotationSlider.ts index df5d8117..3858c53a 100644 --- a/sim/visuals/controls/rotationSlider.ts +++ b/sim/visuals/controls/rotationSlider.ts @@ -83,7 +83,8 @@ namespace pxsim.visuals { 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 bBox = this.content.getBoundingClientRect(); + let t = Math.max(0, Math.min(1, (width + bBox.left / this.scaleFactor - cur.x / this.scaleFactor) / width)) const state = this.state; state.setAngle((1 - t) * (100)); diff --git a/sim/visuals/layoutView.ts b/sim/visuals/layoutView.ts index a72cc5f0..5a70217e 100644 --- a/sim/visuals/layoutView.ts +++ b/sim/visuals/layoutView.ts @@ -1,5 +1,5 @@ /// -/// +/// /// namespace pxsim.visuals { @@ -9,59 +9,51 @@ namespace pxsim.visuals { 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_HEIGHT_RATIO = MODULE_AND_WIRING_HEIGHT_RATIO * 4 / 5; + export const WIRING_HEIGHT_RATIO = MODULE_AND_WIRING_HEIGHT_RATIO / 5; export const MODULE_INNER_PADDING_RATIO = 1 / 35; + export const MAX_MODULE_WIDTH = 100; + 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 inputContainers: ViewContainer[] = []; + private outputContainers: ViewContainer[] = []; + + private inputControls: View[] = []; + private outputControls: View[] = []; + + private inputCloseIcons: View[] = []; + private outputCloseIcons: View[] = []; + 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.outputContainers = [new ViewContainer(), new ViewContainer, new ViewContainer(), new ViewContainer()]; + this.inputContainers = [new ViewContainer(), new ViewContainer, new ViewContainer(), new ViewContainer()]; 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); } @@ -72,8 +64,7 @@ namespace pxsim.visuals { public layout(width: number, height: number) { this.hasDimensions = true; - this.width = width; - this.height = height; + this.resize(width, height); this.scrollGroup.setAttribute("width", width.toString()); this.scrollGroup.setAttribute("height", height.toString()); this.position(); @@ -81,6 +72,7 @@ namespace pxsim.visuals { public setBrick(brick: BrickView) { this.brick = brick; + this.brick.inject(this.scrollGroup); this.position(); } @@ -88,55 +80,112 @@ namespace pxsim.visuals { return this.brick; } - public setInput(port: number, child: LayoutElement) { - if (this.inputs[port]) { - // Remove current input - this.inputs[port].dispose(); + public setInput(port: number, view: LayoutElement, control?: View, closeIcon?: View) { + if (this.inputs[port] != view || this.inputControls[port] != control) { + if (this.inputs[port]) { + // Remove current input + this.inputs[port].dispose(); + } + this.inputs[port] = view; + if (this.inputControls[port]) { + this.inputControls[port].dispose(); + } + this.inputControls[port] = control; + this.inputCloseIcons[port] = closeIcon; + + this.inputContainers[port].clear(); + this.inputContainers[port].addView(view); + + if (control) this.inputContainers[port].addView(control); + + if (view.hasClick()) view.registerClick((ev: any) => { + view.setSelected(true); + runtime.queueDisplayUpdate(); + }, true); + + if (control && closeIcon) { + this.inputContainers[port].addView(closeIcon); + closeIcon.registerClick(() => { + // Clear selection + view.setSelected(false); + runtime.queueDisplayUpdate(); + }) + } } - this.inputs[port] = child; + this.position(); } - public setOutput(port: number, child: LayoutElement) { - if (this.outputs[port]) { - // Remove current input - this.outputs[port].dispose(); + public setOutput(port: number, view: LayoutElement, control?: View, closeIcon?: View) { + if (this.outputs[port] != view || this.outputControls[port] != control) { + if (this.outputs[port]) { + // Remove current output + this.outputs[port].dispose(); + } + this.outputs[port] = view; + if (this.outputControls[port]) { + this.outputControls[port].dispose(); + } + this.outputControls[port] = control; + this.outputCloseIcons[port] = closeIcon; + + this.outputContainers[port].clear(); + this.outputContainers[port].addView(view); + + if (control) this.outputContainers[port].addView(control); + + if (view.hasClick()) view.registerClick((ev: any) => { + view.setSelected(true); + runtime.queueDisplayUpdate(); + }, true) + + if (control && closeIcon) { + this.outputContainers[port].addView(closeIcon); + closeIcon.registerClick(() => { + // Clear selection + view.setSelected(false); + runtime.queueDisplayUpdate(); + }) + } } - 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) { + protected buildDom() { this.contentGroup = svg.elt("g") as SVGGElement; this.scrollGroup = svg.child(this.contentGroup, "g") as SVGGElement; + + // Inject all view containers + for (let i = 0; i < 4; i++) { + this.inputContainers[i].inject(this.scrollGroup); + this.outputContainers[i].inject(this.scrollGroup); + } + + this.inputs = []; + this.outputs = []; + this.inputControls = []; + this.outputControls = []; + + // Inject all wires + for (let port = 0; port < DAL.NUM_OUTPUTS; port++) { + this.outputWires[port].inject(this.scrollGroup); + } + for (let port = 0; port < DAL.NUM_INPUTS; port++) { + this.inputWires[port].inject(this.scrollGroup); + } + + // Inject all ports + this.setInput(0, new PortView(0, 'A')); + this.setInput(1, new PortView(1, 'B')); + this.setInput(2, new PortView(2, 'C')); + this.setInput(3, new PortView(3, 'D')); + + this.setOutput(0, new PortView(0, '1')); + this.setOutput(1, new PortView(1, '2')); + this.setOutput(2, new PortView(2, '3')); + this.setOutput(3, new PortView(3, '4')); + return this.contentGroup; } @@ -171,34 +220,47 @@ namespace pxsim.visuals { 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 modulePadding = this.getModulePadding(); const moduleSpacing = contentWidth / 4; - const moduleWidth = moduleSpacing - (modulePadding * 2); - let currentX = modulePadding; + const moduleWidth = this.getInnerModuleWidth(); + let currentX = this.getModulePadding(); 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); - }) + this.outputContainers[i].translate(currentX, currentY); + if (this.outputs[i]) { + const view = this.outputs[i]; + const outputPadding = this.getInnerModuleWidth() * view.getPaddingRatio(); + const desiredOutputWidth = this.getInnerModuleWidth() - outputPadding * 2; + const outputWidth = Math.min(desiredOutputWidth, MAX_MODULE_WIDTH); + const outputHeight = this.getModuleHeight(); + + // Translate and resize view + view.resize(outputWidth, outputHeight); + const viewHeight = view.getInnerHeight() / view.getInnerWidth() * outputWidth; + view.translate(outputPadding + ((desiredOutputWidth - outputWidth) / 2), outputHeight - viewHeight, true); + + // Resize control + const control = this.outputControls[i]; + if (control) { + control.resize(this.getInnerModuleWidth(), outputHeight); + + // Translate close icon + const closeIcon = this.outputCloseIcons[i]; + if (closeIcon) { + const closeIconWidth = closeIcon.getWidth(); + closeIcon.translate(this.getInnerModuleWidth() / 2 - closeIconWidth / 2, 0); + } + } + } currentX += moduleSpacing; }) @@ -206,7 +268,7 @@ namespace pxsim.visuals { currentY = moduleHeight; const wireBrickSpacing = brickWidth / 5; - const wiringYPadding = 10; + const wiringYPadding = 0; let wireStartX = 0; let wireEndX = brickPadding + wireBrickSpacing; let wireEndY = currentY + this.getWiringHeight() + wiringYPadding; @@ -214,7 +276,6 @@ namespace pxsim.visuals { // 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; @@ -225,22 +286,39 @@ namespace pxsim.visuals { currentY += this.getWiringHeight(); // Render the brick in the middle - this.brick.resize(brickWidth); + this.brick.resize(brickWidth, brickHeight); 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); - }) + this.inputContainers[i].translate(currentX, currentY); + if (this.inputs[i]) { + const view = this.inputs[i]; + const inputPadding = this.getInnerModuleWidth() * view.getPaddingRatio(); + const desiredInputWidth = this.getInnerModuleWidth() - inputPadding * 2; + const inputWidth = Math.min(desiredInputWidth, MAX_MODULE_WIDTH); + const inputHeight = this.getModuleHeight(); + + // Translate and resize view + view.resize(inputWidth, inputHeight); + view.translate(inputPadding + ((desiredInputWidth - inputWidth) / 2), 0, true); + + // Resize control + const control = this.inputControls[i]; + if (control) { + control.resize(this.getInnerModuleWidth(), inputHeight); + + // Translate and resize close icon + const closeIcon = this.inputCloseIcons[i]; + if (closeIcon) { + const closeIconWidth = closeIcon.getWidth(); + const closeIconHeight = closeIcon.getHeight(); + closeIcon.translate(this.getInnerModuleWidth() / 2 - closeIconWidth / 2, this.getModuleHeight() - closeIconHeight); + } + } + } currentX += moduleSpacing; }) @@ -251,7 +329,6 @@ namespace pxsim.visuals { // 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; @@ -259,27 +336,6 @@ namespace pxsim.visuals { } } - 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; } @@ -290,9 +346,21 @@ namespace pxsim.visuals { public getModuleBounds() { return { - width: this.width / 4, + width: (this.width || DEFAULT_WIDTH) / 4, height: this.getModuleHeight() } } + + public getModulePadding() { + return this.getModuleBounds().width / 35; + } + + public getInnerModuleWidth() { + return this.getModuleBounds().width - (this.getModulePadding() * 2); + } + + public getModuleHeight() { + return (this.height || DEFAULT_HEIGHT) * MODULE_HEIGHT_RATIO; + } } } \ No newline at end of file diff --git a/sim/visuals/nodes/brickView.ts b/sim/visuals/nodes/brickView.ts index 8619f053..6a732713 100644 --- a/sim/visuals/nodes/brickView.ts +++ b/sim/visuals/nodes/brickView.ts @@ -1,8 +1,8 @@ -/// +/// namespace pxsim.visuals { - export class BrickView extends StaticModuleView implements LayoutElement { + export class BrickView extends ModuleView implements LayoutElement { private static EV3_SCREEN_ID = "ev3_screen"; private static EV3_LIGHT_ID = "btn_color"; @@ -41,9 +41,9 @@ namespace pxsim.visuals { 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]); + // 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; diff --git a/sim/visuals/nodes/colorSensorView.ts b/sim/visuals/nodes/colorSensorView.ts index 19ee2ed7..6a1dc560 100644 --- a/sim/visuals/nodes/colorSensorView.ts +++ b/sim/visuals/nodes/colorSensorView.ts @@ -1,7 +1,7 @@ -/// +/// namespace pxsim.visuals { - export class ColorSensorView extends StaticModuleView implements LayoutElement { + export class ColorSensorView extends ModuleView implements LayoutElement { private control: ColorGridControl; @@ -10,7 +10,7 @@ namespace pxsim.visuals { } public getPaddingRatio() { - return 1 / 8; + return 1 / 6; } public updateState() { diff --git a/sim/visuals/nodes/gyroSensorView.ts b/sim/visuals/nodes/gyroSensorView.ts index c8280f8d..879e3d94 100644 --- a/sim/visuals/nodes/gyroSensorView.ts +++ b/sim/visuals/nodes/gyroSensorView.ts @@ -1,7 +1,7 @@ -/// +/// namespace pxsim.visuals { - export class GyroSensorView extends StaticModuleView implements LayoutElement { + export class GyroSensorView extends ModuleView implements LayoutElement { constructor(port: number) { super(GYRO_SVG, "gyro", NodeType.GyroSensor, port); diff --git a/sim/visuals/nodes/largeMotorView.ts b/sim/visuals/nodes/largeMotorView.ts index 42aeb2e8..6fb728fe 100644 --- a/sim/visuals/nodes/largeMotorView.ts +++ b/sim/visuals/nodes/largeMotorView.ts @@ -1,7 +1,7 @@ -/// +/// namespace pxsim.visuals { - export class LargeMotorView extends StaticModuleView implements LayoutElement { + export class LargeMotorView extends ModuleView implements LayoutElement { private static ROTATING_ECLIPSE_ID = "1eb2ae58-2419-47d4-86bf-4f26a7f0cf61"; diff --git a/sim/visuals/nodes/mediumMotorView.ts b/sim/visuals/nodes/mediumMotorView.ts index 546b7e4a..10371475 100644 --- a/sim/visuals/nodes/mediumMotorView.ts +++ b/sim/visuals/nodes/mediumMotorView.ts @@ -1,10 +1,10 @@ -/// +/// namespace pxsim.visuals { export const MOTOR_ROTATION_FPS = 32; - export class MediumMotorView extends StaticModuleView implements LayoutElement { + export class MediumMotorView extends ModuleView implements LayoutElement { private static ROTATING_ECLIPSE_ID = "Hole"; @@ -16,7 +16,7 @@ namespace pxsim.visuals { } public getPaddingRatio() { - return 1 / 10; + return 1 / 5; } updateState() { diff --git a/sim/visuals/nodes/staticView.ts b/sim/visuals/nodes/moduleView.ts similarity index 87% rename from sim/visuals/nodes/staticView.ts rename to sim/visuals/nodes/moduleView.ts index 1527c433..051f074d 100644 --- a/sim/visuals/nodes/staticView.ts +++ b/sim/visuals/nodes/moduleView.ts @@ -1,6 +1,6 @@ namespace pxsim.visuals { - export class StaticModuleView extends View implements LayoutElement { + export class ModuleView extends View implements LayoutElement { protected content: SVGSVGElement; protected controlShown: boolean; @@ -45,9 +45,8 @@ namespace pxsim.visuals { return 0.5; } - protected buildDom(width: number): SVGElement { + protected buildDom(): SVGElement { this.content = svg.parseString(this.xml); - this.updateDimensions(width); this.buildDomCore(); this.attachEvents(); if (this.hasClick()) @@ -82,15 +81,17 @@ namespace pxsim.visuals { public attachEvents() { } - public resize(width: number) { - this.updateDimensions(width); + public resize(width: number, height: number) { + super.resize(width, height); + this.updateDimensions(width, height); } - private updateDimensions(width: number) { + private updateDimensions(width: number, height: number) { if (this.content) { const currentWidth = this.getInnerWidth(); const currentHeight = this.getInnerHeight(); const newHeight = currentHeight / currentWidth * width; + const newWidth = currentWidth / currentHeight * height; this.content.setAttribute('width', `${width}`); this.content.setAttribute('height', `${newHeight}`); } @@ -101,13 +102,13 @@ namespace pxsim.visuals { } public setSelected(selected: boolean) { - this.selected = selected; + super.setSelected(selected); this.updateOpacity(); } protected updateOpacity() { if (this.rendered) { - const opacity = this.selected ? "0.5" : "1"; + const opacity = this.selected ? "0.2" : "1"; if (this.hasClick()) { this.setOpacity(opacity); if (this.selected) this.content.style.cursor = ""; diff --git a/sim/visuals/nodes/portView.ts b/sim/visuals/nodes/portView.ts index 5399713f..53908c75 100644 --- a/sim/visuals/nodes/portView.ts +++ b/sim/visuals/nodes/portView.ts @@ -1,8 +1,8 @@ -/// +/// namespace pxsim.visuals { - export class PortView extends StaticModuleView implements LayoutElement { + export class PortView extends ModuleView implements LayoutElement { constructor(port: NodeType, private label: string) { super(PORT_SVG, "port", NodeType.Port, port); diff --git a/sim/visuals/nodes/touchSensorView.ts b/sim/visuals/nodes/touchSensorView.ts index 0c623218..c86e88e9 100644 --- a/sim/visuals/nodes/touchSensorView.ts +++ b/sim/visuals/nodes/touchSensorView.ts @@ -1,7 +1,7 @@ -/// +/// namespace pxsim.visuals { - export class TouchSensorView extends StaticModuleView implements LayoutElement { + export class TouchSensorView extends ModuleView 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"]; diff --git a/sim/visuals/nodes/ultrasonicView.ts b/sim/visuals/nodes/ultrasonicView.ts index d370bd35..5ae9565b 100644 --- a/sim/visuals/nodes/ultrasonicView.ts +++ b/sim/visuals/nodes/ultrasonicView.ts @@ -1,7 +1,7 @@ -/// +/// namespace pxsim.visuals { - export class UltrasonicSensorView extends StaticModuleView implements LayoutElement { + export class UltrasonicSensorView extends ModuleView implements LayoutElement { constructor(port: number) { super(ULTRASONIC_SVG, "ultrasonic", NodeType.UltrasonicSensor, port); diff --git a/sim/visuals/view.ts b/sim/visuals/view.ts index 029f139d..7e46300c 100644 --- a/sim/visuals/view.ts +++ b/sim/visuals/view.ts @@ -3,14 +3,19 @@ namespace pxsim.visuals { protected element: SVGGElement; protected rendered = false; protected visible = false; + protected selected: boolean; protected width: number = 0; + protected height: number = 0; protected left: number = 0; protected top: number = 0; protected scaleFactor: number = 1; + private changed: boolean; + private hover: boolean = false; + protected theme: IBoardTheme; - protected abstract buildDom(width: number): SVGElement; + protected abstract buildDom(): SVGElement; public abstract getInnerWidth(): number; public abstract getInnerHeight(): number; @@ -83,9 +88,26 @@ namespace pxsim.visuals { } private onClickHandler: (ev: any) => void; - public registerClick(handler: (ev: any) => void) { + public registerClick(handler: (ev: any) => void, zoom?: boolean) { this.onClickHandler = handler; - this.getView().addEventListener(pointerEvents.up, this.onClickHandler); + if (zoom) { + this.getView().addEventListener(pointerEvents.up, (ev: any) => { + if (!this.getSelected()) { + this.onClickHandler(ev); + this.setHover(false); + } + }); + this.getView().addEventListener(pointerEvents.move, () => { + if (!this.getSelected()) { + this.setHover(true); + } + }); + this.getView().addEventListener(pointerEvents.leave, () => { + this.setHover(false); + }); + } else { + this.getView().addEventListener(pointerEvents.up, this.onClickHandler); + } } public dispose() { @@ -98,7 +120,7 @@ namespace pxsim.visuals { this.element = svg.elt("g") as SVGGElement; View.track(this); - const content = this.buildDom(this.width); + const content = this.buildDom(); if (content) { this.element.appendChild(content); } @@ -108,16 +130,30 @@ namespace pxsim.visuals { return this.element; } - public resize(width: number) { + public resize(width: number, height: number) { this.width = width; + this.height = height; } private updateTransform() { if (this.rendered) { - let transform = `translate(${this.left} ${this.top})`; + let left = this.left; + let top = this.top; + let scaleFactor = this.scaleFactor; + if (this.hover) { + const hoverScaleFactor = scaleFactor + 0.05; + // Scale around center of module + const centerX = this.getWidth() / 2; + const centerY = this.getHeight() / 2; + left = left - centerX * (hoverScaleFactor - 1); + top = top - centerY * (hoverScaleFactor - 1); + scaleFactor = hoverScaleFactor; + } - if (this.scaleFactor !== 1) { - transform += ` scale(${this.scaleFactor})`; + let transform = `translate(${left} ${top})`; + + if (scaleFactor !== 1) { + transform += ` scale(${scaleFactor})`; } this.element.setAttribute("transform", transform); @@ -149,6 +185,41 @@ namespace pxsim.visuals { delete View.allViews[id]; } } + + ///////// HOVERED STATE ///////////// + + public getHover() { + return this.hover; + } + + public setHover(hover: boolean) { + if (this.hover != hover) { + this.hover = hover; + this.updateTransform(); + } + } + + ///////// SELECTED STATE ///////////// + + public getSelected() { + return this.selected; + } + + public setSelected(selected: boolean) { + this.selected = selected; + this.setChangedState(); + } + + protected setChangedState() { + this.changed = true; + } + + public didChange() { + const res = this.changed; + this.changed = false; + return res; + } + } export abstract class SimView extends View implements LayoutElement { @@ -222,7 +293,7 @@ namespace pxsim.visuals { }); } - protected buildDom(width: number): SVGElement { + protected buildDom(): SVGElement { return undefined; } } diff --git a/sim/visuals/wireView.ts b/sim/visuals/wireView.ts index df1dd2a6..b37ac2ac 100644 --- a/sim/visuals/wireView.ts +++ b/sim/visuals/wireView.ts @@ -1,11 +1,10 @@ -/// +/// 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; @@ -30,13 +29,13 @@ namespace pxsim.visuals { this.updatePath(); } - buildDom(width: number): SVGElement { + buildDom(): 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' + 'stroke-width': '2px' }) as SVGPathElement; this.setSelected(true); return this.wire; @@ -45,8 +44,8 @@ namespace pxsim.visuals { 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; + const thirdHeight = height / 3; + const middleHeight = this.port == 1 || this.port == 2 ? thirdHeight : thirdHeight * 2; let d = `M${this.startX} ${this.startY}`; d += ` L${this.startX} ${this.startY + middleHeight}`; d += ` L${this.endX} ${this.startY + middleHeight}`; @@ -79,7 +78,7 @@ namespace pxsim.visuals { } public setSelected(selected: boolean) { - this.selected = selected; + super.setSelected(selected); this.updateOpacity(); }