diff --git a/sim/state/motornode.ts b/sim/state/motornode.ts index be502448..1abab33b 100644 --- a/sim/state/motornode.ts +++ b/sim/state/motornode.ts @@ -17,6 +17,8 @@ namespace pxsim { private speedCmdTime: number; private _synchedMotor: MotorNode; // non-null if synchronized + private manualSpeed: number = 0; + constructor(port: number, large: boolean) { super(port); this.setLarge(large); @@ -42,7 +44,7 @@ namespace pxsim { setSpeedCmd(cmd: DAL, values: number[]) { if (this.speedCmd != cmd || JSON.stringify(this.speedCmdValues) != JSON.stringify(values)) - this.setChangedState(); + this.setChangedState(); // new command TODO: values this.speedCmd = cmd; this.speedCmdValues = values; @@ -96,6 +98,17 @@ namespace pxsim { this.started = true; } + manualMotorDown() { + } + + manualMotorMove(speed: number) { + this.manualSpeed = speed; + } + + manualMotorUp() { + this.manualSpeed = undefined; + } + updateState(elapsed: number) { //console.log(`motor: ${elapsed}ms - ${this.speed}% - ${this.angle}> - ${this.tacho}|`) const interval = Math.min(20, elapsed); @@ -109,79 +122,84 @@ namespace pxsim { } private updateStateStep(elapsed: number) { - // compute new speed - switch (this.speedCmd) { - case DAL.opOutputSpeed: - case DAL.opOutputPower: - // assume power == speed - // TODO: PID - this.speed = this.speedCmdValues[0]; - break; - case DAL.opOutputTimeSpeed: - case DAL.opOutputTimePower: - case DAL.opOutputStepPower: - case DAL.opOutputStepSpeed: { - // ramp up, run, ramp down, using time - const speed = this.speedCmdValues[0]; - const step1 = this.speedCmdValues[1]; - const step2 = this.speedCmdValues[2]; - const step3 = this.speedCmdValues[3]; - const brake = this.speedCmdValues[4]; - const dstep = (this.speedCmd == DAL.opOutputTimePower || this.speedCmd == DAL.opOutputTimeSpeed) - ? pxsim.U.now() - this.speedCmdTime - : this.tacho - this.speedCmdTacho; - if (dstep < step1) // rampup - this.speed = speed * dstep / step1; - else if (dstep < step1 + step2) // run - this.speed = speed; - else if (dstep < step1 + step2 + step3) - this.speed = speed * (step1 + step2 + step3 - dstep) / (step1 + step2 + step3); - else { - if (brake) this.speed = 0; - this.clearSpeedCmd(); - } - break; - } - case DAL.opOutputStepSync: - case DAL.opOutputTimeSync: { - const otherMotor = this._synchedMotor; - if (otherMotor.port < this.port) // handled in other motor code + if (!this.manualSpeed) { + // compute new speed + switch (this.speedCmd) { + case DAL.opOutputSpeed: + case DAL.opOutputPower: + // assume power == speed + // TODO: PID + this.speed = this.speedCmdValues[0]; + break; + case DAL.opOutputTimeSpeed: + case DAL.opOutputTimePower: + case DAL.opOutputStepPower: + case DAL.opOutputStepSpeed: { + // ramp up, run, ramp down, using time + const speed = this.speedCmdValues[0]; + const step1 = this.speedCmdValues[1]; + const step2 = this.speedCmdValues[2]; + const step3 = this.speedCmdValues[3]; + const brake = this.speedCmdValues[4]; + const dstep = (this.speedCmd == DAL.opOutputTimePower || this.speedCmd == DAL.opOutputTimeSpeed) + ? pxsim.U.now() - this.speedCmdTime + : this.tacho - this.speedCmdTacho; + if (dstep < step1) // rampup + this.speed = speed * dstep / step1; + else if (dstep < step1 + step2) // run + this.speed = speed; + else if (dstep < step1 + step2 + step3) + this.speed = speed * (step1 + step2 + step3 - dstep) / (step1 + step2 + step3); + else { + if (brake) this.speed = 0; + this.clearSpeedCmd(); + } break; - - const speed = this.speedCmdValues[0]; - const turnRatio = this.speedCmdValues[1]; - const stepsOrTime = this.speedCmdValues[2]; - const brake = this.speedCmdValues[3]; - const dstep = this.speedCmd == DAL.opOutputTimeSync - ? pxsim.U.now() - this.speedCmdTime - : this.tacho - this.speedCmdTacho; - // 0 is special case, run infinite - if (!stepsOrTime || dstep < stepsOrTime) - this.speed = speed; - else { - if (brake) this.speed = 0; - this.clearSpeedCmd(); } + case DAL.opOutputStepSync: + case DAL.opOutputTimeSync: { + const otherMotor = this._synchedMotor; + if (otherMotor.port < this.port) // handled in other motor code + break; - // turn ratio is a bit weird to interpret - // see https://communities.theiet.org/blogs/698/1706 - if (turnRatio < 0) { - otherMotor.speed = speed; - this.speed *= (100 + turnRatio) / 100; - } else { - otherMotor.speed = this.speed * (100 - turnRatio) / 100; + const speed = this.speedCmdValues[0]; + const turnRatio = this.speedCmdValues[1]; + const stepsOrTime = this.speedCmdValues[2]; + const brake = this.speedCmdValues[3]; + const dstep = this.speedCmd == DAL.opOutputTimeSync + ? pxsim.U.now() - this.speedCmdTime + : this.tacho - this.speedCmdTacho; + // 0 is special case, run infinite + if (!stepsOrTime || dstep < stepsOrTime) + this.speed = speed; + else { + if (brake) this.speed = 0; + this.clearSpeedCmd(); + } + + // turn ratio is a bit weird to interpret + // see https://communities.theiet.org/blogs/698/1706 + if (turnRatio < 0) { + otherMotor.speed = speed; + this.speed *= (100 + turnRatio) / 100; + } else { + otherMotor.speed = this.speed * (100 - turnRatio) / 100; + } + + // clamp + this.speed = Math.max(-100, Math.min(100, this.speed >> 0)); + otherMotor.speed = Math.max(-100, Math.min(100, otherMotor.speed >> 0));; + + // stop other motor if needed + if (!this._synchedMotor) + otherMotor.clearSpeedCmd(); + break; } - - // clamp - this.speed = Math.max(-100, Math.min(100, this.speed >> 0)); - otherMotor.speed = Math.max(-100, Math.min(100, otherMotor.speed >> 0));; - - // stop other motor if needed - if (!this._synchedMotor) - otherMotor.clearSpeedCmd(); - break; } } + else { + this.speed = this.manualSpeed; + } this.speed = Math.round(this.speed); // integer only // compute delta angle diff --git a/sim/visuals/board.ts b/sim/visuals/board.ts index 58afc8d6..d3be5c8a 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; @@ -71,6 +71,15 @@ namespace pxsim.visuals { fill: gray !important; cursor: pointer; } + + /* Motor slider */ + .sim-motor-btn { + cursor: pointer; + } + .sim-motor-btn:hover .btn { + stroke-width: 2px; + stroke: black !important; + } `; const EV3_WIDTH = 99.984346; @@ -222,9 +231,8 @@ 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); + view = new MotorSliderControl(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..2e30731e --- /dev/null +++ b/sim/visuals/controls/motorSlider.ts @@ -0,0 +1,167 @@ + + +namespace pxsim.visuals { + + export class MotorSliderControl extends ControlView { + private group: SVGGElement; + private gradient: SVGLinearGradientElement; + private slider: SVGGElement; + + private reporter: SVGTextElement; + + 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 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.reporter = pxsim.svg.child(this.group, "text", { + 'x': this.getInnerWidth() / 2, 'y': this.getInnerHeight() / 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' }); + + this.updateDial(); + + let pt = parent.createSVGPoint(); + let captured = false; + + const dragSurface = svg.child(this.group, "rect", { + x: 0, + y: 0, + width: this.getInnerWidth(), + height: this.getInnerHeight(), + opacity: 0, + cursor: '-webkit-grab' + }) + + touchEvents(dragSurface, ev => { + if (captured && (ev as MouseEvent).clientY != undefined) { + ev.preventDefault(); + this.updateSliderValue(pt, parent, ev as MouseEvent); + this.handleSliderDown(); + } + }, ev => { + captured = true; + if ((ev as MouseEvent).clientY != undefined) { + this.updateSliderValue(pt, parent, ev as MouseEvent); + this.handleSliderMove(); + } + }, () => { + captured = false; + this.handleSliderUp(); + }, () => { + captured = false; + this.handleSliderUp(); + }) + + return this.group; + } + + getInnerWidth() { + return 250; + } + + getInnerHeight() { + return 250; + } + + 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.updateDial(); + + this.prevVal = deg; + this.lastPosition = cur.x; + } + + private handleSliderDown() { + const state = this.state; + state.manualMotorDown(); + } + + private handleSliderMove() { + this.dial.setAttribute('cursor', '-webkit-grabbing'); + const state = this.state; + state.manualMotorMove(this.internalSpeed); + } + + private handleSliderUp() { + this.dial.setAttribute('cursor', '-webkit-grab'); + const state = this.state; + state.manualMotorUp(); + + this.internalSpeed = 0; + this.updateDial(); + } + + private updateDial() { + let speed = this.internalSpeed; + + // 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})`); + } + + updateState() { + if (!this.visible) { + return; + } + const node = this.state; + const speed = node.getSpeed(); + + // Update reporter + this.reporter.textContent = `${speed}`; + } + + private getMin() { + return 0; + } + + private getMax() { + return 100; + } + } + +} \ No newline at end of file