@teachinglab/omd 0.7.30 → 0.7.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/canvas/features/resizeHandleManager.js +16 -6
- package/canvas/tools/PointerTool.js +12 -5
- package/package.json +1 -1
- package/src/json-schemas.md +13 -4
- package/src/omdCoordinatePlane.js +388 -18
- package/src/omdShapes.js +120 -61
|
@@ -20,6 +20,7 @@ export class ResizeHandleManager {
|
|
|
20
20
|
this.selectionBorderColor = '#007bff';
|
|
21
21
|
this.selectionBorderWidth = 2;
|
|
22
22
|
this.selectionBorder = null;
|
|
23
|
+
this.selectionBorderPadding = { top: 8, right: 8, bottom: 8, left: 8 };
|
|
23
24
|
|
|
24
25
|
// Resize constraints
|
|
25
26
|
this.minSize = 20;
|
|
@@ -270,11 +271,19 @@ export class ResizeHandleManager {
|
|
|
270
271
|
* @param {string} [style.dasharray] - SVG stroke-dasharray value (e.g. '4,2' or 'none')
|
|
271
272
|
* @param {number} [style.cornerRadius]- rx/ry corner radius of the border rect
|
|
272
273
|
*/
|
|
273
|
-
setSelectionStyle({ color, width, dasharray, cornerRadius } = {}) {
|
|
274
|
+
setSelectionStyle({ color, width, dasharray, cornerRadius, padding, paddingX, paddingY, paddingTop, paddingRight, paddingBottom, paddingLeft } = {}) {
|
|
274
275
|
if (color !== undefined) this.selectionBorderColor = color;
|
|
275
276
|
if (width !== undefined) this.selectionBorderWidth = width;
|
|
276
277
|
if (dasharray !== undefined) this.selectionBorderDasharray = dasharray;
|
|
277
278
|
if (cornerRadius !== undefined) this.selectionBorderCornerRadius = cornerRadius;
|
|
279
|
+
const basePadding = padding ?? null;
|
|
280
|
+
const nextPadding = {
|
|
281
|
+
top: paddingTop ?? paddingY ?? basePadding ?? this.selectionBorderPadding.top,
|
|
282
|
+
right: paddingRight ?? paddingX ?? basePadding ?? this.selectionBorderPadding.right,
|
|
283
|
+
bottom: paddingBottom ?? paddingY ?? basePadding ?? this.selectionBorderPadding.bottom,
|
|
284
|
+
left: paddingLeft ?? paddingX ?? basePadding ?? this.selectionBorderPadding.left
|
|
285
|
+
};
|
|
286
|
+
this.selectionBorderPadding = nextPadding;
|
|
278
287
|
|
|
279
288
|
// Re-apply to live border if one exists
|
|
280
289
|
if (this.selectionBorder) {
|
|
@@ -286,6 +295,7 @@ export class ResizeHandleManager {
|
|
|
286
295
|
this.selectionBorder.setAttribute('rx', this.selectionBorderCornerRadius);
|
|
287
296
|
this.selectionBorder.setAttribute('ry', this.selectionBorderCornerRadius);
|
|
288
297
|
}
|
|
298
|
+
this._updateSelectionBorder();
|
|
289
299
|
}
|
|
290
300
|
}
|
|
291
301
|
|
|
@@ -385,12 +395,12 @@ export class ResizeHandleManager {
|
|
|
385
395
|
if (!this.selectionBorder || !this.selectedElement) return;
|
|
386
396
|
|
|
387
397
|
const bounds = this._getTransformedBounds();
|
|
388
|
-
const padding =
|
|
398
|
+
const padding = this.selectionBorderPadding;
|
|
389
399
|
|
|
390
|
-
this.selectionBorder.setAttribute('x', bounds.x - padding);
|
|
391
|
-
this.selectionBorder.setAttribute('y', bounds.y - padding);
|
|
392
|
-
this.selectionBorder.setAttribute('width', bounds.width + padding
|
|
393
|
-
this.selectionBorder.setAttribute('height', bounds.height + padding
|
|
400
|
+
this.selectionBorder.setAttribute('x', bounds.x - padding.left);
|
|
401
|
+
this.selectionBorder.setAttribute('y', bounds.y - padding.top);
|
|
402
|
+
this.selectionBorder.setAttribute('width', bounds.width + padding.left + padding.right);
|
|
403
|
+
this.selectionBorder.setAttribute('height', bounds.height + padding.top + padding.bottom);
|
|
394
404
|
}
|
|
395
405
|
|
|
396
406
|
/**
|
|
@@ -50,6 +50,13 @@ export class PointerTool extends Tool {
|
|
|
50
50
|
this.dragStartPoint = null;
|
|
51
51
|
this.potentialDeselect = null;
|
|
52
52
|
this.hasSeparatedForDrag = false;
|
|
53
|
+
|
|
54
|
+
this.selectionBoundsPadding = {
|
|
55
|
+
top: options.selectionBoundsPaddingTop ?? options.selectionBoundsPaddingY ?? options.selectionBoundsPadding ?? 14,
|
|
56
|
+
right: options.selectionBoundsPaddingRight ?? options.selectionBoundsPaddingX ?? options.selectionBoundsPadding ?? 14,
|
|
57
|
+
bottom: options.selectionBoundsPaddingBottom ?? options.selectionBoundsPaddingY ?? options.selectionBoundsPadding ?? 14,
|
|
58
|
+
left: options.selectionBoundsPaddingLeft ?? options.selectionBoundsPaddingX ?? options.selectionBoundsPadding ?? 14
|
|
59
|
+
};
|
|
53
60
|
|
|
54
61
|
// Initialize resize handle manager for OMD visuals
|
|
55
62
|
this.resizeHandleManager = new ResizeHandleManager(canvas);
|
|
@@ -595,12 +602,12 @@ export class PointerTool extends Tool {
|
|
|
595
602
|
if (!hasPoints) return null;
|
|
596
603
|
|
|
597
604
|
// Add padding to match the visual box
|
|
598
|
-
const padding =
|
|
605
|
+
const padding = this.selectionBoundsPadding;
|
|
599
606
|
return {
|
|
600
|
-
x: minX - padding,
|
|
601
|
-
y: minY - padding,
|
|
602
|
-
width: (maxX + padding) - (minX - padding),
|
|
603
|
-
height: (maxY + padding) - (minY - padding)
|
|
607
|
+
x: minX - padding.left,
|
|
608
|
+
y: minY - padding.top,
|
|
609
|
+
width: (maxX + padding.right) - (minX - padding.left),
|
|
610
|
+
height: (maxY + padding.bottom) - (minY - padding.top)
|
|
604
611
|
};
|
|
605
612
|
}
|
|
606
613
|
|
package/package.json
CHANGED
package/src/json-schemas.md
CHANGED
|
@@ -785,20 +785,29 @@ This document provides schemas and examples for the `loadFromJSON` method used i
|
|
|
785
785
|
"backgroundColor": "string",
|
|
786
786
|
"backgroundCornerRadius": "number",
|
|
787
787
|
"backgroundOpacity": "number",
|
|
788
|
-
"showBackground": "boolean"
|
|
788
|
+
"showBackground": "boolean",
|
|
789
|
+
"interactive": "boolean (optional) - when true, render sliders below the graph",
|
|
790
|
+
"variables": [
|
|
791
|
+
"array of strings or objects: { name, label, value, min, max, step, color, graphIndex }"
|
|
792
|
+
]
|
|
789
793
|
}
|
|
790
794
|
```
|
|
791
795
|
|
|
792
796
|
### Example
|
|
793
797
|
```json
|
|
794
798
|
{
|
|
799
|
+
"interactive": true,
|
|
800
|
+
"variables": [
|
|
801
|
+
{ "name": "a", "label": "a", "value": 2, "min": -5, "max": 5, "step": 0.1 },
|
|
802
|
+
{ "name": "b", "label": "b", "value": 1, "min": -5, "max": 5, "step": 0.1 }
|
|
803
|
+
],
|
|
795
804
|
"graphEquations": [
|
|
796
805
|
{
|
|
797
|
-
"equation": "y = x
|
|
798
|
-
"color": "
|
|
806
|
+
"equation": "y = a*x + b",
|
|
807
|
+
"color": "#0f766e",
|
|
799
808
|
"strokeWidth": 2,
|
|
800
809
|
"domain": { "min": -5, "max": 5 },
|
|
801
|
-
"label": "
|
|
810
|
+
"label": "y = a*x + b",
|
|
802
811
|
"labelAtX": 2,
|
|
803
812
|
"labelPosition": "above"
|
|
804
813
|
}
|
|
@@ -51,6 +51,13 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
51
51
|
this.backgroundOpacity = 1.0;
|
|
52
52
|
this.showBackground = true;
|
|
53
53
|
|
|
54
|
+
this.interactive = false;
|
|
55
|
+
this.variables = [];
|
|
56
|
+
this.basePaddingBottom = 30;
|
|
57
|
+
this.interactivePaddingBottom = 0;
|
|
58
|
+
this.renderedFunctionEntries = [];
|
|
59
|
+
this.sliderControlRows = [];
|
|
60
|
+
|
|
54
61
|
this.calculatePadding();
|
|
55
62
|
this.updateLayout();
|
|
56
63
|
}
|
|
@@ -85,6 +92,10 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
85
92
|
this.backgroundCornerRadius = (data.backgroundCornerRadius !== undefined) ? data.backgroundCornerRadius : this.backgroundCornerRadius;
|
|
86
93
|
this.backgroundOpacity = (data.backgroundOpacity !== undefined) ? data.backgroundOpacity : this.backgroundOpacity;
|
|
87
94
|
this.showBackground = (data.showBackground !== undefined) ? data.showBackground : this.showBackground;
|
|
95
|
+
this.interactive = data.interactive === true;
|
|
96
|
+
this.variables = this.normalizeInteractiveVariables(
|
|
97
|
+
data.variables || data.interactiveVariables || []
|
|
98
|
+
);
|
|
88
99
|
|
|
89
100
|
this.calculatePadding();
|
|
90
101
|
this.updateLayout();
|
|
@@ -153,6 +164,11 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
153
164
|
const shapesHolder = new jsvgGroup();
|
|
154
165
|
graphClipMask.addChild(shapesHolder);
|
|
155
166
|
this.addShapes(shapesHolder);
|
|
167
|
+
|
|
168
|
+
this.sliderControlRows = [];
|
|
169
|
+
if (this.shouldShowInteractiveControls()) {
|
|
170
|
+
this.createInteractiveControls(contentGroup.svgObject);
|
|
171
|
+
}
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
createAxisGrid(gridHolder, isXAxis) {
|
|
@@ -280,7 +296,11 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
280
296
|
|
|
281
297
|
calculatePadding() {
|
|
282
298
|
this.paddingLeft = this.yLabel ? 50 : 30;
|
|
283
|
-
this.
|
|
299
|
+
this.basePaddingBottom = this.xLabel ? 50 : 30;
|
|
300
|
+
this.interactivePaddingBottom = this.shouldShowInteractiveControls()
|
|
301
|
+
? this.getInteractiveControlHeight()
|
|
302
|
+
: 0;
|
|
303
|
+
this.paddingBottom = this.basePaddingBottom + this.interactivePaddingBottom;
|
|
284
304
|
this.paddingTop = 25;
|
|
285
305
|
this.paddingRight = 25;
|
|
286
306
|
}
|
|
@@ -374,17 +394,29 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
374
394
|
}
|
|
375
395
|
|
|
376
396
|
graphMultipleFunctions(holder) {
|
|
397
|
+
this.renderedFunctionEntries = [];
|
|
398
|
+
|
|
377
399
|
for (const functionConfig of this.graphEquations) {
|
|
378
400
|
try {
|
|
401
|
+
const compiledExpression = this.compileFunctionExpression(functionConfig.equation);
|
|
379
402
|
const path = new jsvgPath();
|
|
380
403
|
path.setStrokeColor(this.getAllowedColor(functionConfig.color));
|
|
381
404
|
path.setStrokeWidth(functionConfig.strokeWidth);
|
|
382
|
-
this.graphFunctionWithDomain(path, functionConfig.
|
|
405
|
+
this.graphFunctionWithDomain(path, functionConfig.domain, compiledExpression);
|
|
383
406
|
holder.addChild(path);
|
|
384
407
|
|
|
408
|
+
const entry = {
|
|
409
|
+
functionConfig,
|
|
410
|
+
compiledExpression,
|
|
411
|
+
path,
|
|
412
|
+
label: null
|
|
413
|
+
};
|
|
414
|
+
|
|
385
415
|
if (functionConfig.label) {
|
|
386
|
-
this.addFunctionLabel(holder, functionConfig);
|
|
416
|
+
entry.label = this.addFunctionLabel(holder, functionConfig, compiledExpression);
|
|
387
417
|
}
|
|
418
|
+
|
|
419
|
+
this.renderedFunctionEntries.push(entry);
|
|
388
420
|
} catch (e) {
|
|
389
421
|
console.warn(`Failed to graph equation: ${functionConfig.equation}`, e);
|
|
390
422
|
}
|
|
@@ -399,25 +431,28 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
399
431
|
return expression;
|
|
400
432
|
}
|
|
401
433
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const expression = this._extractExpression(functionString);
|
|
434
|
+
compileFunctionExpression(functionString) {
|
|
435
|
+
return math.compile(this._extractExpression(functionString));
|
|
436
|
+
}
|
|
406
437
|
|
|
407
|
-
|
|
438
|
+
graphFunctionWithDomain(pathObject, domain, compiledExpression) {
|
|
439
|
+
pathObject.clearPoints();
|
|
408
440
|
|
|
409
441
|
const leftLimit = (domain && typeof domain.min === 'number') ? domain.min : this.xMin;
|
|
410
442
|
const rightLimit = (domain && typeof domain.max === 'number') ? domain.max : this.xMax;
|
|
411
443
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
444
|
+
const pointCount = this.shouldShowInteractiveControls() ? 400 : 1000;
|
|
445
|
+
const span = rightLimit - leftLimit;
|
|
446
|
+
for (let i = 0; i <= pointCount; i++) {
|
|
447
|
+
const x = leftLimit + (span * i) / pointCount;
|
|
448
|
+
const y = compiledExpression.evaluate(this.getEvaluationScope({ x }));
|
|
449
|
+
if (!Number.isFinite(y)) continue;
|
|
415
450
|
pathObject.addPoint(this.toGraphPixelX(x), this.toGraphPixelY(y));
|
|
416
451
|
}
|
|
417
452
|
pathObject.updatePath();
|
|
418
453
|
}
|
|
419
454
|
|
|
420
|
-
addFunctionLabel(holder, functionConfig) {
|
|
455
|
+
addFunctionLabel(holder, functionConfig, compiledExpression) {
|
|
421
456
|
const labelText = String(functionConfig.label);
|
|
422
457
|
const fontSize = 12;
|
|
423
458
|
const padding = 6;
|
|
@@ -433,15 +468,18 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
433
468
|
label.setVerticalCentering();
|
|
434
469
|
label.setFontWeight(500);
|
|
435
470
|
|
|
471
|
+
this.updateFunctionLabelPosition(label, functionConfig, compiledExpression);
|
|
472
|
+
holder.addChild(label);
|
|
473
|
+
return label;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
updateFunctionLabelPosition(label, functionConfig, compiledExpression) {
|
|
436
477
|
const anchorX = (typeof functionConfig.labelAtX === 'number')
|
|
437
478
|
? functionConfig.labelAtX
|
|
438
479
|
: this.xMin + (this.xMax - this.xMin) * 0.1;
|
|
439
480
|
|
|
440
481
|
let anchorY = this.yMin + (this.yMax - this.yMin) * 0.1;
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
const compiled = math.compile(equationBody);
|
|
444
|
-
const yVal = compiled.evaluate({ x: anchorX });
|
|
482
|
+
const yVal = compiledExpression.evaluate(this.getEvaluationScope({ x: anchorX }));
|
|
445
483
|
anchorY = yVal;
|
|
446
484
|
|
|
447
485
|
const xPx = this.toGraphPixelX(anchorX);
|
|
@@ -469,7 +507,6 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
469
507
|
}
|
|
470
508
|
|
|
471
509
|
label.setPosition(finalX, finalY);
|
|
472
|
-
holder.addChild(label);
|
|
473
510
|
}
|
|
474
511
|
|
|
475
512
|
drawLineSegments(holder) {
|
|
@@ -530,6 +567,339 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
530
567
|
}
|
|
531
568
|
}
|
|
532
569
|
|
|
570
|
+
shouldShowInteractiveControls() {
|
|
571
|
+
return this.interactive === true && this.variables.length > 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
getInteractiveControlHeight() {
|
|
575
|
+
return 18 + this.variables.length * 38;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
normalizeInteractiveVariables(variables) {
|
|
579
|
+
if (!Array.isArray(variables)) return [];
|
|
580
|
+
|
|
581
|
+
return variables
|
|
582
|
+
.map((variable, index) => this.normalizeInteractiveVariable(variable, index))
|
|
583
|
+
.filter(Boolean);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
normalizeInteractiveVariable(variable, index) {
|
|
587
|
+
const source = (typeof variable === "string")
|
|
588
|
+
? { name: variable }
|
|
589
|
+
: (variable && typeof variable === "object" ? variable : null);
|
|
590
|
+
|
|
591
|
+
if (!source) return null;
|
|
592
|
+
|
|
593
|
+
const name = String(
|
|
594
|
+
source.name ??
|
|
595
|
+
source.variable ??
|
|
596
|
+
source.id ??
|
|
597
|
+
""
|
|
598
|
+
).trim();
|
|
599
|
+
|
|
600
|
+
if (!name) return null;
|
|
601
|
+
|
|
602
|
+
let min = Number.isFinite(source.min) ? source.min : -10;
|
|
603
|
+
let max = Number.isFinite(source.max) ? source.max : 10;
|
|
604
|
+
if (max < min) {
|
|
605
|
+
const tmp = min;
|
|
606
|
+
min = max;
|
|
607
|
+
max = tmp;
|
|
608
|
+
}
|
|
609
|
+
if (max === min) {
|
|
610
|
+
max = min + 1;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const step = (Number.isFinite(source.step) && source.step > 0) ? source.step : 0.1;
|
|
614
|
+
const fallbackValue = (min <= 0 && max >= 0) ? 0 : min;
|
|
615
|
+
const initialValue = this.clampInteractiveValue(
|
|
616
|
+
Number.isFinite(source.value) ? source.value : fallbackValue,
|
|
617
|
+
min,
|
|
618
|
+
max
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
name,
|
|
623
|
+
label: String(source.label ?? name),
|
|
624
|
+
min,
|
|
625
|
+
max,
|
|
626
|
+
step,
|
|
627
|
+
value: initialValue,
|
|
628
|
+
color: source.color ?? null,
|
|
629
|
+
graphIndex: Number.isInteger(source.graphIndex)
|
|
630
|
+
? source.graphIndex
|
|
631
|
+
: (Number.isInteger(source.equationIndex) ? source.equationIndex : null),
|
|
632
|
+
order: index
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
clampInteractiveValue(value, min, max) {
|
|
637
|
+
return Math.max(min, Math.min(max, value));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
snapInteractiveValue(value, variable) {
|
|
641
|
+
const step = variable.step || 0.1;
|
|
642
|
+
const snapped = variable.min + Math.round((value - variable.min) / step) * step;
|
|
643
|
+
const decimals = this.getStepPrecision(step);
|
|
644
|
+
const rounded = Number(snapped.toFixed(decimals));
|
|
645
|
+
return this.clampInteractiveValue(rounded, variable.min, variable.max);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
getStepPrecision(step) {
|
|
649
|
+
const stepText = String(step);
|
|
650
|
+
if (!stepText.includes(".")) return 0;
|
|
651
|
+
return stepText.split(".")[1].length;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
getInteractiveVariableValue(variable) {
|
|
655
|
+
if (!variable) return 0;
|
|
656
|
+
if (Number.isFinite(variable.currentValue)) return variable.currentValue;
|
|
657
|
+
if (Number.isFinite(variable.value)) return variable.value;
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
getEvaluationScope(extraScope = {}) {
|
|
662
|
+
const scope = { ...extraScope };
|
|
663
|
+
for (const variable of this.variables) {
|
|
664
|
+
scope[variable.name] = this.getInteractiveVariableValue(variable);
|
|
665
|
+
}
|
|
666
|
+
return scope;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
createInteractiveControls(parentSvg) {
|
|
670
|
+
const controlsGroup = this.createSvgElement("g", {
|
|
671
|
+
"data-omd-interactive-controls": "true"
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const graphBottom = this.paddingTop + this.graphHeight;
|
|
675
|
+
const startY = graphBottom + this.basePaddingBottom + 8;
|
|
676
|
+
const labelX = this.paddingLeft;
|
|
677
|
+
const valueX = this.width - this.paddingRight;
|
|
678
|
+
const trackStart = this.paddingLeft + 36;
|
|
679
|
+
const trackEnd = this.width - this.paddingRight - 56;
|
|
680
|
+
const trackWidth = Math.max(40, trackEnd - trackStart);
|
|
681
|
+
|
|
682
|
+
this.sliderControlRows = this.variables.map((variable, index) => {
|
|
683
|
+
const color = this.resolveInteractiveVariableColor(variable);
|
|
684
|
+
const rowY = startY + index * 38;
|
|
685
|
+
const rowGroup = this.createSvgElement("g", {
|
|
686
|
+
transform: `translate(0, ${rowY})`,
|
|
687
|
+
"data-omd-variable": variable.name
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const label = this.createSvgElement("text", {
|
|
691
|
+
x: labelX,
|
|
692
|
+
y: 4,
|
|
693
|
+
fill: color,
|
|
694
|
+
"font-size": 13,
|
|
695
|
+
"font-weight": 700,
|
|
696
|
+
"dominant-baseline": "middle"
|
|
697
|
+
});
|
|
698
|
+
label.textContent = variable.label;
|
|
699
|
+
|
|
700
|
+
const track = this.createSvgElement("line", {
|
|
701
|
+
x1: trackStart,
|
|
702
|
+
y1: 0,
|
|
703
|
+
x2: trackStart + trackWidth,
|
|
704
|
+
y2: 0,
|
|
705
|
+
stroke: "#d1d5db",
|
|
706
|
+
"stroke-width": 6,
|
|
707
|
+
"stroke-linecap": "round"
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const activeTrack = this.createSvgElement("line", {
|
|
711
|
+
x1: trackStart,
|
|
712
|
+
y1: 0,
|
|
713
|
+
x2: trackStart,
|
|
714
|
+
y2: 0,
|
|
715
|
+
stroke: color,
|
|
716
|
+
"stroke-width": 6,
|
|
717
|
+
"stroke-linecap": "round"
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const thumb = this.createSvgElement("circle", {
|
|
721
|
+
cx: trackStart,
|
|
722
|
+
cy: 0,
|
|
723
|
+
r: 8,
|
|
724
|
+
fill: color,
|
|
725
|
+
stroke: "#ffffff",
|
|
726
|
+
"stroke-width": 2
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const valueText = this.createSvgElement("text", {
|
|
730
|
+
x: valueX,
|
|
731
|
+
y: 4,
|
|
732
|
+
fill: color,
|
|
733
|
+
"font-size": 12,
|
|
734
|
+
"font-weight": 600,
|
|
735
|
+
"dominant-baseline": "middle",
|
|
736
|
+
"text-anchor": "end"
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const hitArea = this.createSvgElement("rect", {
|
|
740
|
+
x: trackStart - 10,
|
|
741
|
+
y: -14,
|
|
742
|
+
width: trackWidth + 20,
|
|
743
|
+
height: 28,
|
|
744
|
+
fill: "transparent",
|
|
745
|
+
"pointer-events": "all",
|
|
746
|
+
rx: 14
|
|
747
|
+
});
|
|
748
|
+
hitArea.style.cursor = "pointer";
|
|
749
|
+
|
|
750
|
+
rowGroup.appendChild(label);
|
|
751
|
+
rowGroup.appendChild(track);
|
|
752
|
+
rowGroup.appendChild(activeTrack);
|
|
753
|
+
rowGroup.appendChild(thumb);
|
|
754
|
+
rowGroup.appendChild(valueText);
|
|
755
|
+
rowGroup.appendChild(hitArea);
|
|
756
|
+
controlsGroup.appendChild(rowGroup);
|
|
757
|
+
|
|
758
|
+
const row = {
|
|
759
|
+
variable,
|
|
760
|
+
color,
|
|
761
|
+
label,
|
|
762
|
+
track,
|
|
763
|
+
activeTrack,
|
|
764
|
+
thumb,
|
|
765
|
+
valueText,
|
|
766
|
+
hitArea,
|
|
767
|
+
trackStart,
|
|
768
|
+
trackEnd: trackStart + trackWidth
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
this.attachSliderInteraction(row);
|
|
772
|
+
this.updateSliderRow(row);
|
|
773
|
+
return row;
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
parentSvg.appendChild(controlsGroup);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
attachSliderInteraction(row) {
|
|
780
|
+
const updateFromPointer = (event) => {
|
|
781
|
+
const localX = this.clientToLocalSvgX(event.clientX);
|
|
782
|
+
const ratio = (localX - row.trackStart) / (row.trackEnd - row.trackStart);
|
|
783
|
+
const unclampedValue = row.variable.min + ratio * (row.variable.max - row.variable.min);
|
|
784
|
+
const nextValue = this.snapInteractiveValue(unclampedValue, row.variable);
|
|
785
|
+
row.variable.currentValue = nextValue;
|
|
786
|
+
row.variable.value = nextValue;
|
|
787
|
+
this.refreshInteractiveElements();
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
row.hitArea.addEventListener("pointerdown", (event) => {
|
|
791
|
+
event.preventDefault();
|
|
792
|
+
event.stopPropagation();
|
|
793
|
+
|
|
794
|
+
updateFromPointer(event);
|
|
795
|
+
row.hitArea.setPointerCapture?.(event.pointerId);
|
|
796
|
+
|
|
797
|
+
const handleMove = (moveEvent) => {
|
|
798
|
+
moveEvent.preventDefault();
|
|
799
|
+
moveEvent.stopPropagation();
|
|
800
|
+
updateFromPointer(moveEvent);
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const stop = (endEvent) => {
|
|
804
|
+
endEvent?.preventDefault?.();
|
|
805
|
+
endEvent?.stopPropagation?.();
|
|
806
|
+
row.hitArea.removeEventListener("pointermove", handleMove);
|
|
807
|
+
row.hitArea.removeEventListener("pointerup", stop);
|
|
808
|
+
row.hitArea.removeEventListener("pointercancel", stop);
|
|
809
|
+
try {
|
|
810
|
+
row.hitArea.releasePointerCapture?.(event.pointerId);
|
|
811
|
+
} catch {}
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
row.hitArea.addEventListener("pointermove", handleMove);
|
|
815
|
+
row.hitArea.addEventListener("pointerup", stop);
|
|
816
|
+
row.hitArea.addEventListener("pointercancel", stop);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
refreshInteractiveElements() {
|
|
821
|
+
for (const entry of this.renderedFunctionEntries) {
|
|
822
|
+
try {
|
|
823
|
+
this.graphFunctionWithDomain(entry.path, entry.functionConfig.domain, entry.compiledExpression);
|
|
824
|
+
if (entry.label) {
|
|
825
|
+
this.updateFunctionLabelPosition(entry.label, entry.functionConfig, entry.compiledExpression);
|
|
826
|
+
}
|
|
827
|
+
} catch (error) {
|
|
828
|
+
console.warn(`Failed to refresh equation: ${entry.functionConfig?.equation}`, error);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
for (const row of this.sliderControlRows) {
|
|
833
|
+
this.updateSliderRow(row);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
updateSliderRow(row) {
|
|
838
|
+
const currentValue = this.getInteractiveVariableValue(row.variable);
|
|
839
|
+
const ratio = (currentValue - row.variable.min) / (row.variable.max - row.variable.min);
|
|
840
|
+
const clampedRatio = Math.max(0, Math.min(1, ratio));
|
|
841
|
+
const thumbX = row.trackStart + (row.trackEnd - row.trackStart) * clampedRatio;
|
|
842
|
+
|
|
843
|
+
row.activeTrack.setAttribute("x2", String(thumbX));
|
|
844
|
+
row.thumb.setAttribute("cx", String(thumbX));
|
|
845
|
+
row.valueText.textContent = this.formatInteractiveValue(currentValue, row.variable.step);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
formatInteractiveValue(value, step) {
|
|
849
|
+
const decimals = Math.min(4, Math.max(0, this.getStepPrecision(step)));
|
|
850
|
+
const rounded = Number(value.toFixed(decimals));
|
|
851
|
+
return `${rounded}`;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
resolveInteractiveVariableColor(variable) {
|
|
855
|
+
if (variable.color) {
|
|
856
|
+
return this.getAllowedColor(variable.color);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const graphIndex = variable.graphIndex;
|
|
860
|
+
if (Number.isInteger(graphIndex) && this.graphEquations[graphIndex]?.color) {
|
|
861
|
+
return this.getAllowedColor(this.graphEquations[graphIndex].color);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const matchedEquation = this.graphEquations.find((graphConfig) =>
|
|
865
|
+
this.equationContainsVariable(graphConfig?.equation, variable.name)
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
if (matchedEquation?.color) {
|
|
869
|
+
return this.getAllowedColor(matchedEquation.color);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return this.getAllowedColor("#2563eb");
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
equationContainsVariable(equation, variableName) {
|
|
876
|
+
if (!equation || !variableName) return false;
|
|
877
|
+
const escapedName = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
878
|
+
const variableRegex = new RegExp(`(^|[^A-Za-z0-9_])${escapedName}([^A-Za-z0-9_]|$)`);
|
|
879
|
+
return variableRegex.test(String(equation));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
createSvgElement(tagName, attributes = {}) {
|
|
883
|
+
const element = document.createElementNS("http://www.w3.org/2000/svg", tagName);
|
|
884
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
885
|
+
if (value !== undefined && value !== null) {
|
|
886
|
+
element.setAttribute(key, String(value));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return element;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
clientToLocalSvgX(clientX) {
|
|
893
|
+
const rect = this.svgObject.getBoundingClientRect();
|
|
894
|
+
const viewBox = (this.svgObject.getAttribute("viewBox") || "0 0 0 0")
|
|
895
|
+
.split(/\s+/)
|
|
896
|
+
.map(Number);
|
|
897
|
+
const [viewBoxX = 0, , viewBoxWidth = 0] = viewBox;
|
|
898
|
+
if (!rect.width || !viewBoxWidth) return 0;
|
|
899
|
+
|
|
900
|
+
return viewBoxX + ((clientX - rect.left) / rect.width) * viewBoxWidth;
|
|
901
|
+
}
|
|
902
|
+
|
|
533
903
|
// Background customization methods
|
|
534
904
|
setBackgroundColor(color) {
|
|
535
905
|
this.backgroundColor = color;
|
|
@@ -558,4 +928,4 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
558
928
|
if (options.show !== undefined) this.showBackground = options.show;
|
|
559
929
|
this.updateLayout();
|
|
560
930
|
}
|
|
561
|
-
}
|
|
931
|
+
}
|
package/src/omdShapes.js
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
import { omdColor } from "./omdColor.js";
|
|
3
3
|
import { jsvgGroup, jsvgPath, jsvgLine, jsvgTextLine, jsvgEllipse } from "@teachinglab/jsvg";
|
|
4
4
|
|
|
5
|
+
const DEFAULT_SHAPE_PADDING = 10;
|
|
6
|
+
const LABEL_SHAPE_PADDING = 38;
|
|
7
|
+
|
|
8
|
+
function getShapePadding(showLabels) {
|
|
9
|
+
return showLabels ? LABEL_SHAPE_PADDING : DEFAULT_SHAPE_PADDING;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getClosedPolygonVertices(shapePath) {
|
|
13
|
+
const points = Array.isArray(shapePath?.points) ? shapePath.points : [];
|
|
14
|
+
if (points.length <= 1) return points.slice();
|
|
15
|
+
|
|
16
|
+
const first = points[0];
|
|
17
|
+
const last = points[points.length - 1];
|
|
18
|
+
const isClosed = first.x === last.x && first.y === last.y;
|
|
19
|
+
return isClosed ? points.slice(0, -1) : points.slice();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getPolygonCentroid(points) {
|
|
23
|
+
if (!points.length) return { x: 0, y: 0 };
|
|
24
|
+
|
|
25
|
+
let sumX = 0;
|
|
26
|
+
let sumY = 0;
|
|
27
|
+
for (const point of points) {
|
|
28
|
+
sumX += point.x;
|
|
29
|
+
sumY += point.y;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
x: sumX / points.length,
|
|
34
|
+
y: sumY / points.length
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
5
38
|
export class omdRightTriangle extends jsvgGroup
|
|
6
39
|
{
|
|
7
40
|
constructor()
|
|
@@ -69,8 +102,9 @@ export class omdRightTriangle extends jsvgGroup
|
|
|
69
102
|
updateLayout()
|
|
70
103
|
{
|
|
71
104
|
// Center the triangle within the viewBox
|
|
72
|
-
const
|
|
73
|
-
const
|
|
105
|
+
const padding = getShapePadding(this.showLabels);
|
|
106
|
+
const offsetX = padding;
|
|
107
|
+
const offsetY = this.unitScale * this.verticalLeg + padding;
|
|
74
108
|
|
|
75
109
|
this.shapePath.clearPoints();
|
|
76
110
|
this.shapePath.addPoint(offsetX, offsetY);
|
|
@@ -92,8 +126,8 @@ export class omdRightTriangle extends jsvgGroup
|
|
|
92
126
|
}
|
|
93
127
|
|
|
94
128
|
// Set viewBox to center the shape
|
|
95
|
-
this.width = this.unitScale * this.horizontalLeg +
|
|
96
|
-
this.height = this.unitScale * this.verticalLeg +
|
|
129
|
+
this.width = this.unitScale * this.horizontalLeg + padding * 2;
|
|
130
|
+
this.height = this.unitScale * this.verticalLeg + padding * 2;
|
|
97
131
|
this.svgObject.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
|
|
98
132
|
}
|
|
99
133
|
}
|
|
@@ -150,8 +184,9 @@ export class omdIsoscelesTriangle extends jsvgGroup
|
|
|
150
184
|
|
|
151
185
|
const baseWidth = this.unitScale * this.triangleBase;
|
|
152
186
|
const triangleHeight = this.unitScale * this.triangleHeight;
|
|
153
|
-
const
|
|
154
|
-
const
|
|
187
|
+
const padding = getShapePadding(this.showLabels);
|
|
188
|
+
const offsetX = padding;
|
|
189
|
+
const offsetY = triangleHeight + padding;
|
|
155
190
|
|
|
156
191
|
this.shapePath.addPoint( offsetX, offsetY );
|
|
157
192
|
this.shapePath.addPoint( offsetX + baseWidth, offsetY );
|
|
@@ -169,8 +204,8 @@ export class omdIsoscelesTriangle extends jsvgGroup
|
|
|
169
204
|
}
|
|
170
205
|
|
|
171
206
|
// Set dimensions and viewBox for API compatibility
|
|
172
|
-
this.width = this.unitScale * this.triangleBase +
|
|
173
|
-
this.height = this.unitScale * this.triangleHeight +
|
|
207
|
+
this.width = this.unitScale * this.triangleBase + padding * 2;
|
|
208
|
+
this.height = this.unitScale * this.triangleHeight + padding * 2;
|
|
174
209
|
this.svgObject.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
|
|
175
210
|
}
|
|
176
211
|
}
|
|
@@ -222,8 +257,9 @@ export class omdRectangle extends jsvgGroup
|
|
|
222
257
|
updateLayout()
|
|
223
258
|
{
|
|
224
259
|
// Center the rectangle within the viewBox
|
|
225
|
-
const
|
|
226
|
-
const
|
|
260
|
+
const padding = getShapePadding(this.showLabels);
|
|
261
|
+
const offsetX = padding;
|
|
262
|
+
const offsetY = this.unitScale * this.rectHeight + padding;
|
|
227
263
|
|
|
228
264
|
this.shapePath.clearPoints();
|
|
229
265
|
this.shapePath.addPoint(offsetX, offsetY);
|
|
@@ -245,8 +281,8 @@ export class omdRectangle extends jsvgGroup
|
|
|
245
281
|
}
|
|
246
282
|
|
|
247
283
|
// Set viewBox
|
|
248
|
-
this.width = this.unitScale * this.rectWidth +
|
|
249
|
-
this.height = this.unitScale * this.rectHeight +
|
|
284
|
+
this.width = this.unitScale * this.rectWidth + padding * 2;
|
|
285
|
+
this.height = this.unitScale * this.rectHeight + padding * 2;
|
|
250
286
|
this.svgObject.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
|
|
251
287
|
}
|
|
252
288
|
}
|
|
@@ -367,19 +403,37 @@ export class omdCircle extends jsvgGroup
|
|
|
367
403
|
updateLayout()
|
|
368
404
|
{
|
|
369
405
|
const diameter = 2.0 * this.radius * this.unitScale;
|
|
406
|
+
const radiusPx = this.radius * this.unitScale;
|
|
407
|
+
const centerX = radiusPx + 10;
|
|
408
|
+
const centerY = radiusPx + 10;
|
|
370
409
|
|
|
371
410
|
this.shapePath.setWidthAndHeight( diameter, diameter );
|
|
372
|
-
this.shapePath.setPosition(
|
|
411
|
+
this.shapePath.setPosition( centerX, centerY );
|
|
373
412
|
|
|
374
413
|
this.labelsHolder.removeAllChildren();
|
|
375
414
|
if ( this.showLabels )
|
|
376
415
|
{
|
|
416
|
+
const angleRadians = -Math.PI / 6;
|
|
417
|
+
const endX = centerX + Math.cos(angleRadians) * radiusPx * 0.82;
|
|
418
|
+
const endY = centerY + Math.sin(angleRadians) * radiusPx * 0.82;
|
|
419
|
+
|
|
420
|
+
const radiusLine = new jsvgLine();
|
|
421
|
+
radiusLine.setStrokeColor("black");
|
|
422
|
+
radiusLine.setStrokeWidth(1.5);
|
|
423
|
+
radiusLine.setEndpoints(centerX, centerY, endX, endY);
|
|
424
|
+
this.labelsHolder.addChild(radiusLine);
|
|
425
|
+
|
|
377
426
|
const label = new jsvgTextLine();
|
|
378
427
|
label.setAlignment("center");
|
|
379
428
|
label.setFontFamily( "Albert Sans" );
|
|
380
429
|
label.setFontColor( "black" );
|
|
381
430
|
label.setFontSize( 12 );
|
|
382
|
-
label.
|
|
431
|
+
label.svgObject.setAttribute('dominant-baseline', 'middle');
|
|
432
|
+
label.setRotation(angleRadians * 180 / Math.PI);
|
|
433
|
+
label.setPosition(
|
|
434
|
+
(centerX + endX) * 0.5 + 3,
|
|
435
|
+
(centerY + endY) * 0.5 - 4
|
|
436
|
+
);
|
|
383
437
|
label.setText( `r=${this.radius}` );
|
|
384
438
|
this.labelsHolder.addChild( label );
|
|
385
439
|
}
|
|
@@ -440,6 +494,10 @@ export class omdRegularPolygon extends jsvgGroup
|
|
|
440
494
|
updateLayout()
|
|
441
495
|
{
|
|
442
496
|
this.shapePath.clearPoints();
|
|
497
|
+
const radiusPx = this.radius * this.unitScale;
|
|
498
|
+
const padding = this.showLabels ? 48 : 14;
|
|
499
|
+
const centerX = radiusPx + padding;
|
|
500
|
+
const centerY = radiusPx + padding;
|
|
443
501
|
|
|
444
502
|
var angleOffset = 0;
|
|
445
503
|
if ( this.numberOfSides % 2 == 1 )
|
|
@@ -453,8 +511,8 @@ export class omdRegularPolygon extends jsvgGroup
|
|
|
453
511
|
{
|
|
454
512
|
var A = -2.0 * Math.PI / this.numberOfSides * i;
|
|
455
513
|
A += angleOffset;
|
|
456
|
-
var pX = Math.cos(A) *
|
|
457
|
-
var pY = Math.sin(A) *
|
|
514
|
+
var pX = centerX + Math.cos(A) * radiusPx;
|
|
515
|
+
var pY = centerY + Math.sin(A) * radiusPx;
|
|
458
516
|
this.shapePath.addPoint( pX, pY );
|
|
459
517
|
}
|
|
460
518
|
|
|
@@ -472,9 +530,9 @@ export class omdRegularPolygon extends jsvgGroup
|
|
|
472
530
|
}
|
|
473
531
|
|
|
474
532
|
// Set dimensions and viewBox for API compatibility
|
|
475
|
-
this.width = 2.0 *
|
|
476
|
-
this.height = 2.0 *
|
|
477
|
-
this.svgObject.setAttribute('viewBox',
|
|
533
|
+
this.width = 2.0 * radiusPx + padding * 2;
|
|
534
|
+
this.height = 2.0 * radiusPx + padding * 2;
|
|
535
|
+
this.svgObject.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
|
|
478
536
|
}
|
|
479
537
|
}
|
|
480
538
|
|
|
@@ -489,76 +547,77 @@ export class omdShapeLabelSet extends jsvgGroup
|
|
|
489
547
|
|
|
490
548
|
initializeWithShapePath( shapePath, labelTextArray=[] )
|
|
491
549
|
{
|
|
492
|
-
|
|
550
|
+
const polygonPoints = getClosedPolygonVertices(shapePath);
|
|
551
|
+
const centroid = getPolygonCentroid(polygonPoints);
|
|
493
552
|
|
|
494
553
|
// create lines
|
|
495
|
-
for(
|
|
554
|
+
for (let i = 0; i < polygonPoints.length; i++)
|
|
496
555
|
{
|
|
497
|
-
if ( i >= labelTextArray.length
|
|
556
|
+
if ( i >= labelTextArray.length )
|
|
557
|
+
continue;
|
|
558
|
+
|
|
559
|
+
const labelValue = labelTextArray[i];
|
|
560
|
+
const labelString = String(labelValue ?? "");
|
|
561
|
+
if ( labelString.length === 0 )
|
|
498
562
|
continue;
|
|
499
563
|
|
|
500
564
|
// get points
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
565
|
+
const point0 = polygonPoints[i];
|
|
566
|
+
const x0 = point0.x;
|
|
567
|
+
const y0 = point0.y;
|
|
504
568
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
569
|
+
const point1 = polygonPoints[(i + 1) % polygonPoints.length];
|
|
570
|
+
const x1 = point1.x;
|
|
571
|
+
const y1 = point1.y;
|
|
508
572
|
|
|
509
573
|
// get normalized vector
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
574
|
+
let dX = x1 - x0;
|
|
575
|
+
let dY = y1 - y0;
|
|
576
|
+
const L = Math.sqrt( dX*dX + dY*dY );
|
|
513
577
|
dX /= L;
|
|
514
578
|
dY /= L;
|
|
515
579
|
|
|
516
|
-
// get normal
|
|
517
|
-
|
|
518
|
-
|
|
580
|
+
// get outward normal based on the polygon centroid rather than path winding
|
|
581
|
+
const centerX = (x0 + x1) / 2.0;
|
|
582
|
+
const centerY = (y0 + y1) / 2.0;
|
|
583
|
+
let normalX = -1.0 * dY;
|
|
584
|
+
let normalY = dX;
|
|
585
|
+
const toMidX = centerX - centroid.x;
|
|
586
|
+
const toMidY = centerY - centroid.y;
|
|
587
|
+
if ( toMidX * normalX + toMidY * normalY < 0 )
|
|
588
|
+
{
|
|
589
|
+
normalX *= -1.0;
|
|
590
|
+
normalY *= -1.0;
|
|
591
|
+
}
|
|
519
592
|
|
|
520
593
|
// make line
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
594
|
+
const labelLine = new jsvgLine();
|
|
595
|
+
const newLineX0 = x0 + normalX * 10.0;
|
|
596
|
+
const newLineY0 = y0 + normalY * 10.0;
|
|
597
|
+
const newLineX1 = x1 + normalX * 10.0;
|
|
598
|
+
const newLineY1 = y1 + normalY * 10.0;
|
|
526
599
|
labelLine.setEndpoints( newLineX0, newLineY0, newLineX1, newLineY1 );
|
|
527
600
|
this.addChild( labelLine );
|
|
528
601
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
var angle = Math.atan2( dY, dX );
|
|
534
|
-
angle *= 180 / Math.PI;
|
|
535
|
-
angle += 180.0;
|
|
536
|
-
angle = angle % 360.0;
|
|
537
|
-
var originalAngle = angle;
|
|
538
|
-
|
|
539
|
-
// flip upside-down text
|
|
540
|
-
var offset = 0;
|
|
541
|
-
if ( angle > 90 && angle < 270 )
|
|
542
|
-
{
|
|
543
|
-
angle -= 180.0;
|
|
544
|
-
offset += 10.0;
|
|
545
|
-
}
|
|
602
|
+
let angle = Math.atan2( dY, dX ) * 180 / Math.PI;
|
|
603
|
+
if ( angle > 90 || angle < -90 )
|
|
604
|
+
angle += 180.0;
|
|
546
605
|
|
|
547
606
|
// label text
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
607
|
+
const textCenterX = centerX + normalX * 18.0;
|
|
608
|
+
const textCenterY = centerY + normalY * 18.0;
|
|
609
|
+
const labelText = new jsvgTextLine();
|
|
551
610
|
labelText.setAlignment("center");
|
|
552
611
|
labelText.setFontFamily( "Albert Sans" );
|
|
553
612
|
labelText.setFontColor( "black" );
|
|
554
613
|
labelText.setFontSize( 12 );
|
|
555
614
|
labelText.setPosition( textCenterX, textCenterY );
|
|
556
615
|
labelText.setRotation( angle );
|
|
616
|
+
labelText.svgObject.setAttribute('dominant-baseline', 'middle');
|
|
557
617
|
this.addChild( labelText );
|
|
558
618
|
|
|
559
|
-
labelText.setText(
|
|
619
|
+
labelText.setText( labelString );
|
|
560
620
|
}
|
|
561
621
|
}
|
|
562
622
|
|
|
563
623
|
}
|
|
564
|
-
|