@teachinglab/omd 0.7.29 → 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.
@@ -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 = 3;
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 * 2);
393
- this.selectionBorder.setAttribute('height', bounds.height + padding * 2);
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 = 8;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.7.29",
3
+ "version": "0.7.31",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
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 offsetX = 10; // Left margin
73
- const offsetY = this.unitScale * this.verticalLeg + 10; // Bottom margin
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 + 20;
96
- this.height = this.unitScale * this.verticalLeg + 20;
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 offsetX = 10;
154
- const offsetY = triangleHeight + 10;
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 + 20;
173
- this.height = this.unitScale * this.triangleHeight + 20;
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 offsetX = 10;
226
- const offsetY = this.unitScale * this.rectHeight + 10;
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 + 20;
249
- this.height = this.unitScale * this.rectHeight + 20;
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
  }
@@ -264,6 +300,7 @@ export class omdEllipse extends jsvgGroup
264
300
  this.rectWidth = 10;
265
301
  this.rectHeight = 5;
266
302
  this.unitScale = 10;
303
+ this.showLabels = false;
267
304
 
268
305
  this.shapePath = new jsvgEllipse();
269
306
  this.shapePath.setWidthAndHeight( this.rectWidth*this.unitScale, this.rectHeight*this.unitScale );
@@ -272,6 +309,9 @@ export class omdEllipse extends jsvgGroup
272
309
  this.shapePath.setFillColor( omdColor.lightGray );
273
310
  this.addChild( this.shapePath );
274
311
 
312
+ this.labelsHolder = new jsvgGroup();
313
+ this.addChild( this.labelsHolder );
314
+
275
315
  this.updateLayout();
276
316
  }
277
317
 
@@ -286,6 +326,9 @@ export class omdEllipse extends jsvgGroup
286
326
  if ( typeof data.unitScale != "undefined" )
287
327
  this.unitScale = data.unitScale;
288
328
 
329
+ if ( typeof data.showLabels != "undefined" )
330
+ this.showLabels = data.showLabels;
331
+
289
332
  this.updateLayout();
290
333
  }
291
334
 
@@ -297,6 +340,19 @@ export class omdEllipse extends jsvgGroup
297
340
  this.shapePath.setWidthAndHeight( ellipseWidth, ellipseHeight );
298
341
  this.shapePath.setPosition( ellipseWidth * 0.5 + 10, ellipseHeight * 0.5 + 10 );
299
342
 
343
+ this.labelsHolder.removeAllChildren();
344
+ if ( this.showLabels )
345
+ {
346
+ const label = new jsvgTextLine();
347
+ label.setAlignment("center");
348
+ label.setFontFamily( "Albert Sans" );
349
+ label.setFontColor( "black" );
350
+ label.setFontSize( 12 );
351
+ label.setPosition( ellipseWidth * 0.5 + 10, ellipseHeight * 0.5 + 14 );
352
+ label.setText( `${this.rectWidth} × ${this.rectHeight}` );
353
+ this.labelsHolder.addChild( label );
354
+ }
355
+
300
356
  // Set dimensions and viewBox for API compatibility
301
357
  this.width = this.rectWidth * this.unitScale + 20;
302
358
  this.height = this.rectHeight * this.unitScale + 20;
@@ -315,6 +371,7 @@ export class omdCircle extends jsvgGroup
315
371
 
316
372
  this.radius = 5;
317
373
  this.unitScale = 10;
374
+ this.showLabels = false;
318
375
 
319
376
  this.shapePath = new jsvgEllipse();
320
377
  this.shapePath.setWidthAndHeight( this.radius*this.unitScale, this.radius*this.unitScale );
@@ -323,6 +380,9 @@ export class omdCircle extends jsvgGroup
323
380
  this.shapePath.setFillColor( omdColor.lightGray );
324
381
  this.addChild( this.shapePath );
325
382
 
383
+ this.labelsHolder = new jsvgGroup();
384
+ this.addChild( this.labelsHolder );
385
+
326
386
  this.updateLayout();
327
387
  }
328
388
 
@@ -334,15 +394,49 @@ export class omdCircle extends jsvgGroup
334
394
  if ( typeof data.unitScale != "undefined" )
335
395
  this.unitScale = data.unitScale;
336
396
 
397
+ if ( typeof data.showLabels != "undefined" )
398
+ this.showLabels = data.showLabels;
399
+
337
400
  this.updateLayout();
338
401
  }
339
402
 
340
403
  updateLayout()
341
404
  {
342
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;
343
409
 
344
410
  this.shapePath.setWidthAndHeight( diameter, diameter );
345
- this.shapePath.setPosition( this.radius * this.unitScale + 10, this.radius * this.unitScale + 10 );
411
+ this.shapePath.setPosition( centerX, centerY );
412
+
413
+ this.labelsHolder.removeAllChildren();
414
+ if ( this.showLabels )
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
+
426
+ const label = new jsvgTextLine();
427
+ label.setAlignment("center");
428
+ label.setFontFamily( "Albert Sans" );
429
+ label.setFontColor( "black" );
430
+ label.setFontSize( 12 );
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
+ );
437
+ label.setText( `r=${this.radius}` );
438
+ this.labelsHolder.addChild( label );
439
+ }
346
440
 
347
441
  // Set dimensions and viewBox for API compatibility
348
442
  this.width = 2.0 * this.radius * this.unitScale + 20;
@@ -400,6 +494,10 @@ export class omdRegularPolygon extends jsvgGroup
400
494
  updateLayout()
401
495
  {
402
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;
403
501
 
404
502
  var angleOffset = 0;
405
503
  if ( this.numberOfSides % 2 == 1 )
@@ -413,8 +511,8 @@ export class omdRegularPolygon extends jsvgGroup
413
511
  {
414
512
  var A = -2.0 * Math.PI / this.numberOfSides * i;
415
513
  A += angleOffset;
416
- var pX = Math.cos(A) * this.radius * this.unitScale;
417
- var pY = Math.sin(A) * this.radius * this.unitScale;
514
+ var pX = centerX + Math.cos(A) * radiusPx;
515
+ var pY = centerY + Math.sin(A) * radiusPx;
418
516
  this.shapePath.addPoint( pX, pY );
419
517
  }
420
518
 
@@ -432,9 +530,9 @@ export class omdRegularPolygon extends jsvgGroup
432
530
  }
433
531
 
434
532
  // Set dimensions and viewBox for API compatibility
435
- this.width = 2.0 * this.radius * this.unitScale + 20;
436
- this.height = 2.0 * this.radius * this.unitScale + 20;
437
- this.svgObject.setAttribute('viewBox', `-${this.width/2} -${this.height/2} ${this.width} ${this.height}`);
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}`);
438
536
  }
439
537
  }
440
538
 
@@ -449,76 +547,77 @@ export class omdShapeLabelSet extends jsvgGroup
449
547
 
450
548
  initializeWithShapePath( shapePath, labelTextArray=[] )
451
549
  {
452
- // this assumes counterclockwise path
550
+ const polygonPoints = getClosedPolygonVertices(shapePath);
551
+ const centroid = getPolygonCentroid(polygonPoints);
453
552
 
454
553
  // create lines
455
- for( var i=0; i<shapePath.points.length-1; i++ )
554
+ for (let i = 0; i < polygonPoints.length; i++)
456
555
  {
457
- if ( i >= labelTextArray.length || labelTextArray[i].length == 0 )
556
+ if ( i >= labelTextArray.length )
557
+ continue;
558
+
559
+ const labelValue = labelTextArray[i];
560
+ const labelString = String(labelValue ?? "");
561
+ if ( labelString.length === 0 )
458
562
  continue;
459
563
 
460
564
  // get points
461
- var point0 = shapePath.points[i];
462
- var x0 = point0.x;
463
- var y0 = point0.y;
565
+ const point0 = polygonPoints[i];
566
+ const x0 = point0.x;
567
+ const y0 = point0.y;
464
568
 
465
- var point1 = shapePath.points[i+1];
466
- var x1 = point1.x;
467
- var y1 = point1.y;
569
+ const point1 = polygonPoints[(i + 1) % polygonPoints.length];
570
+ const x1 = point1.x;
571
+ const y1 = point1.y;
468
572
 
469
573
  // get normalized vector
470
- var dX = x1 - x0;
471
- var dY = y1 - y0;
472
- var L = Math.sqrt( dX*dX + dY*dY );
574
+ let dX = x1 - x0;
575
+ let dY = y1 - y0;
576
+ const L = Math.sqrt( dX*dX + dY*dY );
473
577
  dX /= L;
474
578
  dY /= L;
475
579
 
476
- // get normal
477
- var normalX = -1.0 * dY;
478
- var normalY = dX;
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
+ }
479
592
 
480
593
  // make line
481
- var labelLine = new jsvgLine();
482
- var newLineX0 = x0 + normalX * 10.0;
483
- var newLineY0 = y0 + normalY * 10.0;
484
- var newLineX1 = x1 + normalX * 10.0;
485
- var newLineY1 = y1 + normalY * 10.0;
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;
486
599
  labelLine.setEndpoints( newLineX0, newLineY0, newLineX1, newLineY1 );
487
600
  this.addChild( labelLine );
488
601
 
489
- // center point
490
- var centerX = (x0 + x1) / 2.0;
491
- var centerY = (y0 + y1) / 2.0;
492
-
493
- var angle = Math.atan2( dY, dX );
494
- angle *= 180 / Math.PI;
495
- angle += 180.0;
496
- angle = angle % 360.0;
497
- var originalAngle = angle;
498
-
499
- // flip upside-down text
500
- var offset = 0;
501
- if ( angle > 90 && angle < 270 )
502
- {
503
- angle -= 180.0;
504
- offset += 10.0;
505
- }
602
+ let angle = Math.atan2( dY, dX ) * 180 / Math.PI;
603
+ if ( angle > 90 || angle < -90 )
604
+ angle += 180.0;
506
605
 
507
606
  // label text
508
- var textCenterX = centerX + normalX * (15.0+offset);
509
- var textCenterY = centerY + normalY * (15.0+offset);
510
- var labelText = new jsvgTextLine();
607
+ const textCenterX = centerX + normalX * 18.0;
608
+ const textCenterY = centerY + normalY * 18.0;
609
+ const labelText = new jsvgTextLine();
511
610
  labelText.setAlignment("center");
512
611
  labelText.setFontFamily( "Albert Sans" );
513
612
  labelText.setFontColor( "black" );
514
613
  labelText.setFontSize( 12 );
515
614
  labelText.setPosition( textCenterX, textCenterY );
516
615
  labelText.setRotation( angle );
616
+ labelText.svgObject.setAttribute('dominant-baseline', 'middle');
517
617
  this.addChild( labelText );
518
618
 
519
- labelText.setText( labelTextArray[i] );
619
+ labelText.setText( labelString );
520
620
  }
521
621
  }
522
622
 
523
623
  }
524
-