From cbe68b3199618d49be4831eb2ad0a76f5e682766 Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Thu, 28 Dec 2017 13:23:30 -0800 Subject: [PATCH] Add motor slider control --- sim/state/motornode.ts | 11 +- sim/visuals/board.ts | 17 ++- sim/visuals/controls/distanceSlider.ts | 7 +- sim/visuals/controls/motorSlider.ts | 199 +++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 sim/visuals/controls/motorSlider.ts diff --git a/sim/state/motornode.ts b/sim/state/motornode.ts index 5676eae6..9a35d31f 100644 --- a/sim/state/motornode.ts +++ b/sim/state/motornode.ts @@ -73,8 +73,17 @@ namespace pxsim { this.started = true; } + isInput() { + // TODO: figure out if a motor is in an input or output state + return true; + } + + setSpeedAsInput(speed: number) { + this.speed = speed; + } + updateState(elapsed: number) { - console.log(`motor: ${elapsed}ms - ${this.speed}% - ${this.angle}> - ${this.tacho}|`) + //console.log(`motor: ${elapsed}ms - ${this.speed}% - ${this.angle}> - ${this.tacho}|`) const interval = Math.min(20, elapsed); let t = 0; while(t < elapsed) { diff --git a/sim/visuals/board.ts b/sim/visuals/board.ts index e598839e..86931cf7 100644 --- a/sim/visuals/board.ts +++ b/sim/visuals/board.ts @@ -44,7 +44,7 @@ namespace pxsim.visuals { } .sim-text.number { font-family: Courier, Lato, Work Sans, PT Serif, Source Serif Pro; - font-weight: bold; + /*font-weight: bold;*/ } .sim-text.inverted { fill:#5A5A5A; @@ -62,6 +62,14 @@ namespace pxsim.visuals { fill: gray !important; cursor: pointer; } + + /* Motor slider */ + .sim-motor-btn { + cursor: pointer; + } + .sim-motor-btn:hover { + fill: gray !important; + } `; const EV3_WIDTH = 99.984346; @@ -213,9 +221,12 @@ namespace pxsim.visuals { } case NodeType.MediumMotor: case NodeType.LargeMotor: { - // TODO: figure out if the motor is in "input" or "output" mode const state = ev3board().getMotors()[port]; - view = new MotorReporterControl(this.element, this.defs, state, port); + if (state.isInput()) { + view = new MotorSliderControl(this.element, this.defs, state, port); + } else { + view = new MotorReporterControl(this.element, this.defs, state, port); + } break; } } diff --git a/sim/visuals/controls/distanceSlider.ts b/sim/visuals/controls/distanceSlider.ts index a4542ab6..7c5ee3f1 100644 --- a/sim/visuals/controls/distanceSlider.ts +++ b/sim/visuals/controls/distanceSlider.ts @@ -27,8 +27,8 @@ namespace pxsim.visuals { this.group = svg.elt("g") as SVGGElement; const reporterGroup = pxsim.svg.child(this.group, "g"); - reporterGroup.setAttribute("transform", `translate(31, 42)`); - this.reporter = pxsim.svg.child(reporterGroup, "text", { 'x': 0, 'y': '0', 'class': 'sim-text number large inverted' }) as SVGTextElement; + reporterGroup.setAttribute("transform", `translate(${this.getWidth() / 2}, 42)`); + this.reporter = pxsim.svg.child(reporterGroup, "text", { 'text-anchor': 'middle', 'x': 0, 'y': '0', 'class': 'sim-text number large inverted' }) as SVGTextElement; const sliderGroup = pxsim.svg.child(this.group, "g"); sliderGroup.setAttribute("transform", `translate(${this.getWidth() / 2 - this.getSliderWidth() / 2}, ${this.getReporterHeight()})`) @@ -60,12 +60,15 @@ namespace pxsim.visuals { }, ev => { captured = true; if ((ev as MouseEvent).clientY != undefined) { + dragSurface.setAttribute('cursor', '-webkit-grabbing'); this.updateSliderValue(pt, parent, ev as MouseEvent); } }, () => { captured = false; + dragSurface.setAttribute('cursor', '-webkit-grab'); }, () => { captured = false; + dragSurface.setAttribute('cursor', '-webkit-grab'); }) return this.group; diff --git a/sim/visuals/controls/motorSlider.ts b/sim/visuals/controls/motorSlider.ts new file mode 100644 index 00000000..3633bad0 --- /dev/null +++ b/sim/visuals/controls/motorSlider.ts @@ -0,0 +1,199 @@ + + +namespace pxsim.visuals { + + export class MotorSliderControl extends ControlView { + private group: SVGGElement; + private gradient: SVGLinearGradientElement; + private slider: SVGGElement; + + private reporter: SVGTextElement; + + private circleBar: SVGCircleElement; + private dial: SVGGElement; + + private static SLIDER_RADIUS = 100; + + private internalSpeed: number = 0; + + getInnerView(parent: SVGSVGElement, globalDefs: SVGDefsElement) { + this.group = svg.elt("g") as SVGGElement; + const sliderHeight = 250; + + const slider = pxsim.svg.child(this.group, 'g', { 'transform': 'translate(25,25)' }) + const outerCircle = pxsim.svg.child(slider, "circle", { + 'stroke-dasharray': '565.48', 'stroke-dashoffset': '0', + 'cx': 100, 'cy': 100, 'r': '90', 'style': `fill:transparent;`, + 'stroke': '#a8aaa8', 'stroke-width': '1rem' + }) as SVGCircleElement; + this.circleBar = pxsim.svg.child(slider, "circle", { + 'stroke-dasharray': '565.48', 'stroke-dashoffset': '0', + 'cx': 100, 'cy': 100, 'r': '90', 'style': `fill:transparent;`, + 'stroke': '#f12a21', 'stroke-width': '1rem', 'transform': 'rotate(-90 100 100)' + }) as SVGCircleElement; + + this.reporter = pxsim.svg.child(this.group, "text", { + 'x': this.getInnerWidth() / 2, 'y': sliderHeight / 2, + 'text-anchor': 'middle', 'alignment-baseline': 'middle', + 'style': 'font-size: 50px', + 'class': 'sim-text inverted number' + }) as SVGTextElement; + + this.dial = pxsim.svg.child(slider, "g", { 'cursor': '-webkit-grab' }) as SVGGElement; + const handleInner = pxsim.svg.child(this.dial, "g"); + pxsim.svg.child(handleInner, "circle", { 'cx': 0, 'cy': 0, 'r': 30, 'style': 'fill: #f12a21;' }); + pxsim.svg.child(handleInner, "circle", { 'cx': 0, 'cy': 0, 'r': 29.5, 'style': 'fill: none;stroke: #b32e29' }); + + // Add move buttons + const moveBtnRadius = 40; + const leftMoveG = pxsim.svg.child(this.group, 'g'); + const leftMove = pxsim.svg.child(leftMoveG, 'circle', { + 'cx': moveBtnRadius, 'cy': sliderHeight + moveBtnRadius, + 'r': moveBtnRadius, 'style': 'fill: #a8aaa8', 'class': 'sim-motor-btn' + }); + let leftMoveFrame: number; + touchEvents(leftMove, ev => { + // move + }, ev => { + if (leftMoveFrame) cancelAnimationFrame(leftMoveFrame); + let setSpeed = () => { + leftMoveFrame = requestAnimationFrame(() => { + this.state.setSpeedAsInput(-1 * this.internalSpeed); + setSpeed(); + }) + } + setSpeed(); + }, () => { + if (leftMoveFrame) cancelAnimationFrame(leftMoveFrame); + }, () => { + if (leftMoveFrame) cancelAnimationFrame(leftMoveFrame); + }) + + const rightMoveG = pxsim.svg.child(this.group, 'g'); + const rightMove = pxsim.svg.child(rightMoveG, 'circle', { + 'cx': this.getWidth() - moveBtnRadius, 'cy': sliderHeight + moveBtnRadius, + 'r': moveBtnRadius, 'style': 'fill: #a8aaa8', 'class': 'sim-motor-btn' + }); + let rightMoveFrame: number; + touchEvents(rightMove, ev => { + // move + }, ev => { + if (rightMoveFrame) cancelAnimationFrame(rightMoveFrame); + let setSpeed = () => { + rightMoveFrame = requestAnimationFrame(() => { + this.state.setSpeedAsInput(this.internalSpeed); + setSpeed(); + }) + } + setSpeed(); + }, () => { + if (rightMoveFrame) cancelAnimationFrame(rightMoveFrame); + }, () => { + if (rightMoveFrame) cancelAnimationFrame(rightMoveFrame); + }) + + this.updateInternalSpeed(); + + let pt = parent.createSVGPoint(); + let captured = false; + + const dragSurface = svg.child(this.group, "rect", { + x: 0, + y: 0, + width: this.getInnerWidth(), + height: sliderHeight, + opacity: 0, + cursor: '-webkit-grab' + }) + + touchEvents(dragSurface, ev => { + if (captured && (ev as MouseEvent).clientY != undefined) { + ev.preventDefault(); + this.updateSliderValue(pt, parent, ev as MouseEvent); + } + }, ev => { + captured = true; + if ((ev as MouseEvent).clientY != undefined) { + this.dial.setAttribute('cursor', '-webkit-grabbing'); + this.updateSliderValue(pt, parent, ev as MouseEvent); + } + }, () => { + captured = false; + this.dial.setAttribute('cursor', '-webkit-grab'); + }, () => { + captured = false; + this.dial.setAttribute('cursor', '-webkit-grab'); + }) + + return this.group; + } + + getInnerWidth() { + return 250; + } + + getInnerHeight() { + return 330; + } + + private lastPosition: number; + private prevVal: number; + private updateSliderValue(pt: SVGPoint, parent: SVGSVGElement, ev: MouseEvent) { + let cur = svg.cursorPoint(pt, parent, ev); + const coords = { + x: cur.x / this.scaleFactor - this.left / this.scaleFactor, + y: cur.y / this.scaleFactor - this.top / this.scaleFactor + }; + const radius = MotorSliderControl.SLIDER_RADIUS / 2; + const dx = coords.x - radius; + const dy = coords.y - radius; + const atan = Math.atan(-dy / dx); + let deg = Math.ceil(atan * (180 / Math.PI)); + + if (dx < 0) { + deg -= 270; + } else if (dy > 0) { + deg -= 450; + } else if (dx >= 0 && dy <= 0) { + deg = 90 - deg; + } + const value = Math.abs(Math.ceil((deg % 360) / 360 * this.getMax())); + + this.internalSpeed = value; + this.updateInternalSpeed(); + + this.prevVal = deg; + this.lastPosition = cur.x; + } + + private updateInternalSpeed() { + let speed = this.internalSpeed; + + // Update speed on circle bar + let c = Math.PI * (90 * 2); + speed = Math.abs(speed); + let pct = ((100 - speed) / 100) * c; + this.circleBar.setAttribute('stroke-dashoffset', `${pct}`); + + // Update reporter text + this.reporter.textContent = `${speed}`; + + // Update dial position + const deg = speed / this.getMax() * 360; // degrees + const radius = MotorSliderControl.SLIDER_RADIUS; + const dialRadius = 5; + const x = Math.ceil((radius - dialRadius) * Math.sin(deg * Math.PI / 180)) + radius; + const y = Math.ceil((radius - dialRadius) * -Math.cos(deg * Math.PI / 180)) + radius; + this.dial.setAttribute('transform', `translate(${x}, ${y})`); + } + + private getMin() { + return 0; + } + + private getMax() { + return 100; + } + } + +} \ No newline at end of file