@teachinglab/omd 0.7.30 → 0.7.31
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/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/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
|
-
|