SVGHMI: fix parsing of floating point and negative min and max limits in widget label syntax.
// 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");
}