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