Simulator refactoring to support better resizing of modules and controls

This commit is contained in:
Sam El-Husseini 2017-12-22 14:00:23 -08:00
parent 300a2c1476
commit 180f32f25c
23 changed files with 408 additions and 301 deletions

View File

@ -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 = <SimulatorInfraredPacketMessage>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;
}

View File

@ -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() {

View File

@ -48,6 +48,7 @@ namespace pxsim {
stop() {
// TODO: implement
this.setSpeed(0);
}
start() {

View File

@ -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()))

View File

@ -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;

View File

@ -1,4 +1,4 @@
/// <reference path="./nodes/staticView.ts" />
/// <reference path="./nodes/moduleView.ts" />
namespace pxsim.visuals {
@ -6,7 +6,7 @@ namespace pxsim.visuals {
export const CONTROL_HEIGHT = 175;
export abstract class ControlView<T extends BaseNode> extends SimView<T> 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;
}
}
}

View File

@ -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;
}

View File

@ -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'];

View File

@ -10,7 +10,7 @@ namespace pxsim.visuals {
getInnerView(parent: SVGSVGElement) {
this.defs = <SVGDefsElement>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);

View File

@ -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()));

View File

@ -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));

View File

@ -1,5 +1,5 @@
/// <reference path="./view.ts" />
/// <reference path="./nodes/staticView.ts" />
/// <reference path="./nodes/moduleView.ts" />
/// <reference path="./nodes/portView.ts" />
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<boolean> = {};
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;
}
}
}

View File

@ -1,8 +1,8 @@
/// <reference path="./staticView.ts" />
/// <reference path="./moduleView.ts" />
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;

View File

@ -1,7 +1,7 @@
/// <reference path="./staticView.ts" />
/// <reference path="./moduleView.ts" />
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() {

View File

@ -1,7 +1,7 @@
/// <reference path="./staticView.ts" />
/// <reference path="./moduleView.ts" />
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);

View File

@ -1,7 +1,7 @@
/// <reference path="./staticView.ts" />
/// <reference path="./moduleView.ts" />
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";

View File

@ -1,10 +1,10 @@
/// <reference path="./staticView.ts" />
/// <reference path="./moduleView.ts" />
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() {

View File

@ -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 = "";

View File

@ -1,8 +1,8 @@
/// <reference path="./staticView.ts" />
/// <reference path="./moduleView.ts" />
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);

View File

@ -1,7 +1,7 @@
/// <reference path="./staticView.ts" />
/// <reference path="./moduleView.ts" />
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"];

View File

@ -1,7 +1,7 @@
/// <reference path="./staticView.ts" />
/// <reference path="./moduleView.ts" />
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);

View File

@ -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<T extends BaseNode> extends View implements LayoutElement {
@ -222,7 +293,7 @@ namespace pxsim.visuals {
});
}
protected buildDom(width: number): SVGElement {
protected buildDom(): SVGElement {
return undefined;
}
}

View File

@ -1,11 +1,10 @@
/// <reference path="./nodes/staticView.ts" />
/// <reference path="./nodes/moduleView.ts" />
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();
}