svghmi/widget_pathslider.ysl2
author Edouard Tisserant <edouard@beremiz.fr>
Fri, 30 Aug 2024 11:50:23 +0200
changeset 4008 f30573e98600
parent 3454 0b5ab53007a9
permissions -rw-r--r--
IDE: allow structures to be located.
// 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");
}