Initial sim implementation

This commit is contained in:
Sam El-Husseini
2017-12-18 13:04:17 -08:00
parent 6836852122
commit 6320379d02
88 changed files with 3949 additions and 3552 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,67 @@
/// <reference path="./staticView.ts" />
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])}`);
}
}
}
}
}

View File

@ -0,0 +1,10 @@
/// <reference path="./staticView.ts" />
namespace pxsim.visuals {
export class UltrasonicSensorView extends StaticModuleView implements LayoutElement {
constructor(port: number) {
super(ULTRASONIC_SVG, "ultrasonic", NodeType.UltrasonicSensor, port);
}
}
}