Merge pull request #144 from Microsoft/motorslider
Add motor slider control
This commit is contained in:
		@@ -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, <brake> 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, <brake> 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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										167
									
								
								sim/visuals/controls/motorSlider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								sim/visuals/controls/motorSlider.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace pxsim.visuals {
 | 
			
		||||
 | 
			
		||||
    export class MotorSliderControl extends ControlView<MotorNode> {
 | 
			
		||||
        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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user