edouard@3356: // widget_pathslider.ysl2
edouard@3356: widget_desc("PathSlider") {
edouard@3356:     longdesc
Edouard@3454:     ||
Edouard@3454:     PathSlider -
edouard@3356:     ||
edouard@3356: 
edouard@3356:     shortdesc > Slide an SVG element along a path by dragging it
edouard@3356: 
edouard@3356:     path name="value" accepts="HMI_INT,HMI_REAL" > value
edouard@3356:     path name="min" count="optional" accepts="HMI_INT,HMI_REAL" > min
edouard@3356:     path name="max" count="optional" accepts="HMI_INT,HMI_REAL" > max
Edouard@3454: 
edouard@3356:     arg name="min" count="optional" accepts="int,real" > minimum value
edouard@3356:     arg name="max" count="optional" accepts="int,real" > maximum value
edouard@3356: }
edouard@3356: 
edouard@3356: widget_class("PathSlider") {
edouard@3356:     ||
edouard@3356:         frequency = 10;
edouard@3356:         position = undefined;
edouard@3356:         min = 0;
edouard@3356:         max = 100;
edouard@3356:         scannedPoints = [];
edouard@3356:         pathLength = undefined;
edouard@3356:         precision = undefined;
edouard@3356:         origPt = undefined;
Edouard@3454: 
edouard@3356: 
edouard@3356:         scanPath() {
edouard@3356:           this.pathLength = this.path_elt.getTotalLength();
edouard@3356:           this.precision = Math.floor(this.pathLength / 10);
edouard@3356: 
edouard@3356:           // save linear scan for coarse approximation
edouard@3356:           for (var scanLength = 0; scanLength <= this.pathLength; scanLength += this.precision) {
edouard@3356:             this.scannedPoints.push([this.path_elt.getPointAtLength(scanLength), scanLength]);
edouard@3356:           }
edouard@3356:           [this.origPt,] = this.scannedPoints[0];
edouard@3356:         }
edouard@3356: 
edouard@3356:         closestPoint(point) {
edouard@3356:           var bestPoint,
edouard@3356:               bestLength,
edouard@3356:               bestDistance = Infinity,
edouard@3356:               scanDistance;
edouard@3356: 
edouard@3356:           // use linear scan for coarse approximation
edouard@3356:           for (let [scanPoint, scanLength] of this.scannedPoints){
edouard@3356:             if ((scanDistance = distance2(scanPoint)) < bestDistance) {
edouard@3356:               bestPoint = scanPoint,
edouard@3356:               bestLength = scanLength,
edouard@3356:               bestDistance = scanDistance;
edouard@3356:             }
edouard@3356:           }
edouard@3356: 
edouard@3356:           // binary search for more precise estimate
edouard@3356:           let precision = this.precision / 2;
edouard@3356:           while (precision > 0.5) {
edouard@3356:             var beforePoint,
edouard@3356:                 afterPoint,
edouard@3356:                 beforeLength,
edouard@3356:                 afterLength,
edouard@3356:                 beforeDistance,
edouard@3356:                 afterDistance;
edouard@3356:             if ((beforeLength = bestLength - precision) >= 0 &&
edouard@3356:                 (beforeDistance = distance2(beforePoint = this.path_elt.getPointAtLength(beforeLength))) < bestDistance) {
edouard@3356:               bestPoint = beforePoint,
edouard@3356:               bestLength = beforeLength,
edouard@3356:               bestDistance = beforeDistance;
Edouard@3454:             } else if ((afterLength = bestLength + precision) <= this.pathLength &&
edouard@3356:                        (afterDistance = distance2(afterPoint = this.path_elt.getPointAtLength(afterLength))) < bestDistance) {
edouard@3356:               bestPoint = afterPoint,
edouard@3356:               bestLength = afterLength,
edouard@3356:               bestDistance = afterDistance;
edouard@3356:             }
edouard@3356:             precision /= 2;
edouard@3356:           }
edouard@3356: 
edouard@3356:           return [bestPoint, bestLength];
edouard@3356: 
edouard@3356:           function distance2(p) {
edouard@3356:             var dx = p.x - point.x,
edouard@3356:                 dy = p.y - point.y;
edouard@3356:             return dx * dx + dy * dy;
edouard@3356:           }
edouard@3356:         }
edouard@3356: 
edouard@3356:         dispatch(value,oldval, index) {
edouard@3356:             switch(index) {
edouard@3356:                 case 0:
edouard@3356:                     this.position = value;
edouard@3356:                     break;
edouard@3356:                 case 1:
edouard@3356:                     this.min = value;
edouard@3356:                     break;
edouard@3356:                 case 2:
edouard@3356:                     this.max = value;
edouard@3356:                     break;
edouard@3356:             }
edouard@3356: 
edouard@3356:             this.request_animate();
edouard@3356:         }
edouard@3356: 
edouard@3356:         get_current_point(){
edouard@3356:             let currLength = this.pathLength * (this.position - this.min) / (this.max - this.min)
edouard@3356:             return this.path_elt.getPointAtLength(currLength);
edouard@3356:         }
edouard@3356: 
edouard@3356:         animate(){
edouard@3356:             if(this.position == undefined)
edouard@3356:                 return;
edouard@3356: 
edouard@3356:             let currPt = this.get_current_point();
edouard@3356:             this.cursor_transform.setTranslate(currPt.x - this.origPt.x, currPt.y - this.origPt.y);
edouard@3356:         }
edouard@3356: 
edouard@3356:         init() {
edouard@3356:             if(this.args.length == 2)
edouard@3356:                 [this.min, this.max]=this.args;
edouard@3356: 
edouard@3356:             this.scanPath();
edouard@3356: 
edouard@3356:             this.cursor_transform = svg_root.createSVGTransform();
edouard@3356: 
edouard@3356:             this.cursor_elt.transform.baseVal.appendItem(this.cursor_transform);
edouard@3356: 
edouard@3356:             this.cursor_elt.onpointerdown = (e) => this.on_cursor_down(e);
edouard@3356: 
edouard@3356:             this.bound_drag = this.drag.bind(this);
edouard@3356:             this.bound_drop = this.drop.bind(this);
edouard@3356:         }
edouard@3356: 
edouard@3356:         start_dragging_from_event(e){
edouard@3356:             let clientPoint = new DOMPoint(e.clientX, e.clientY);
edouard@3356:             let point = clientPoint.matrixTransform(this.invctm);
edouard@3356:             let currPt = this.get_current_point();
edouard@3356:             this.draggingOffset = new DOMPoint(point.x - currPt.x , point.y - currPt.y);
edouard@3356:         }
edouard@3356: 
edouard@3356:         apply_position_from_event(e){
edouard@3356:             let clientPoint = new DOMPoint(e.clientX, e.clientY);
edouard@3356:             let rawPoint = clientPoint.matrixTransform(this.invctm);
edouard@3356:             let point = new DOMPoint(rawPoint.x - this.draggingOffset.x , rawPoint.y - this.draggingOffset.y);
edouard@3356:             let [closestPoint, closestLength] = this.closestPoint(point);
edouard@3356:             let new_position = this.min + (this.max - this.min) * closestLength / this.pathLength;
edouard@3356:             this.position = Math.round(Math.max(Math.min(new_position, this.max), this.min));
edouard@3356:             this.apply_hmi_value(0, this.position);
edouard@3356:         }
edouard@3356: 
edouard@3356:         on_cursor_down(e){
edouard@3356:             // get scrollbar -> root transform
edouard@3356:             let ctm = this.path_elt.getCTM();
edouard@3356:             // root -> path transform
edouard@3356:             this.invctm = ctm.inverse();
edouard@3356:             this.start_dragging_from_event(e);
edouard@3356:             svg_root.addEventListener("pointerup", this.bound_drop, true);
edouard@3356:             svg_root.addEventListener("pointermove", this.bound_drag, true);
edouard@3356:         }
edouard@3356: 
edouard@3356:         drop(e) {
edouard@3356:             svg_root.removeEventListener("pointerup", this.bound_drop, true);
edouard@3356:             svg_root.removeEventListener("pointermove", this.bound_drag, true);
edouard@3356:         }
edouard@3356: 
edouard@3356:         drag(e) {
edouard@3356:             this.apply_position_from_event(e);
edouard@3356:         }
edouard@3356:     ||
edouard@3356: }
edouard@3356: 
edouard@3356: widget_defs("PathSlider") {
edouard@3356:     labels("cursor path");
edouard@3356: }