@design-core/core 51.1.0 → 52.0.0

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.
Files changed (51) hide show
  1. package/.github/workflows/publish.yml +40 -0
  2. package/README.md +14 -1
  3. package/core/dimensions/alignedDimension.js +3 -1
  4. package/core/dimensions/angularDimension.js +118 -36
  5. package/core/dimensions/circularDimensionBase.js +32 -12
  6. package/core/dimensions/diametricDimension.js +29 -1
  7. package/core/dimensions/dimension.js +58 -5
  8. package/core/dimensions/dimensionBase.js +107 -4
  9. package/core/dimensions/linearDimensionBase.js +75 -37
  10. package/core/dimensions/radialDimension.js +26 -1
  11. package/core/dimensions/rotatedDimension.js +6 -3
  12. package/core/entities/entity.js +9 -0
  13. package/core/lib/auxiliary/snapPoint.js +9 -0
  14. package/core/lib/canvas.js +26 -1
  15. package/core/lib/commandManager.js +9 -4
  16. package/core/lib/dxf/dxf.js +8 -2
  17. package/core/lib/inputManager.js +24 -3
  18. package/core/lib/mouse.js +10 -0
  19. package/core/lib/plotOptions.js +17 -0
  20. package/core/lib/renderers/pdfRenderer.js +17 -1
  21. package/core/lib/scene.js +2 -0
  22. package/core/lib/selectionManager.js +28 -0
  23. package/core/lib/strings.js +3 -0
  24. package/core/properties/dimType.js +14 -0
  25. package/core/properties/property.js +1 -0
  26. package/core/tables/dimStyle.js +32 -2
  27. package/core/tools/gripStretch.js +97 -0
  28. package/core/tools/offset.js +7 -1
  29. package/core/tools/tool.js +10 -0
  30. package/core/tools/windowPick.js +75 -0
  31. package/cspell.json +5 -0
  32. package/package.json +7 -2
  33. package/test/dimensions/alignedDimension.test.js +165 -0
  34. package/test/dimensions/angularDimension.test.js +149 -0
  35. package/test/dimensions/diametricDimension.test.js +76 -4
  36. package/test/dimensions/dimension.test.js +130 -0
  37. package/test/dimensions/dimensionBase.test.js +97 -4
  38. package/test/dimensions/radialDimension.test.js +33 -0
  39. package/test/dimensions/rotatedDimension.test.js +80 -0
  40. package/test/integration/exportIntegration.reference.dxf +3 -9
  41. package/test/integration/plotPageSizeIntegration.test.js +113 -0
  42. package/test/integration/userInputIntegration.reference.dxf +3 -9
  43. package/test/lib/inputManager.test.js +316 -1
  44. package/test/lib/plotOptions.test.js +58 -1
  45. package/test/lib/selectionManager.test.js +42 -0
  46. package/test/renderers/pdfRenderer.test.js +26 -0
  47. package/test/tables/dimstyle.test.js +32 -0
  48. package/test/tools/gripStretch.test.js +108 -0
  49. package/test/tools/offset.test.js +2 -0
  50. package/test/tools/tool.test.js +25 -0
  51. package/test/tools/windowPick.test.js +129 -0
@@ -84,6 +84,17 @@ export class DimensionBase extends Entity {
84
84
  writable: true,
85
85
  });
86
86
 
87
+ // DXF Groupcode 70 bit 128 - user-positioned text flag, exposed as a property so a grip-drop
88
+ // commit can include it explicitly in the UpdateState snapshot, alongside points, for undo/redo
89
+ this.properties.add(Property.Names.USERPOSITIONEDTEXT, {
90
+ type: Property.Type.BOOLEAN,
91
+ visible: false,
92
+ get: (entity) => entity.dimType.hasUserPositionedText(),
93
+ set: (entity, value) => {
94
+ entity.dimType.userPositionedText = value;
95
+ },
96
+ });
97
+
87
98
  if (data) {
88
99
  if (data.hasOwnProperty('41')) {
89
100
  // DXF Groupcode 41 - Line Spacing Factor
@@ -350,6 +361,30 @@ export class DimensionBase extends Entity {
350
361
  return DesignCore.DimStyleManager.getItemByName(this.getProperty(Property.Names.DIMENSIONSTYLE));
351
362
  }
352
363
 
364
+ /**
365
+ * Get the grip points for interactive editing on the canvas.
366
+ * Only the text position (Pt11) is exposed - it's never a dependency for any other point.
367
+ * @return {Array<{point: Point, apply: Function}>} grips
368
+ */
369
+ getGrips() {
370
+ const Pt11 = this.getPointBySequence(this.points, 11);
371
+
372
+ if (!Pt11) {
373
+ return [];
374
+ }
375
+
376
+ const apply = (newPoint) => {
377
+ return this.points.map((point) => {
378
+ if (point.sequence === 11) {
379
+ return new Point(newPoint.x, newPoint.y, point.bulge, point.sequence);
380
+ }
381
+ return point;
382
+ });
383
+ };
384
+
385
+ return [{ point: Pt11, apply: apply }];
386
+ }
387
+
353
388
  /**
354
389
  * Get the point for the sequence number
355
390
  * @param {Array} points - array of points to search
@@ -430,15 +465,24 @@ export class DimensionBase extends Entity {
430
465
  return centreMark;
431
466
  }
432
467
 
468
+ /**
469
+ * Refresh the block geometry if it hasn't been built yet.
470
+ * Needed because block delegates (draw, boundingBox, within, touched, closestPoint)
471
+ * can run before the first draw() call (e.g. a window-select immediately after creation).
472
+ */
473
+ ensureRefreshed() {
474
+ if (this.block.entities.length === 0) {
475
+ this.refresh();
476
+ }
477
+ }
478
+
433
479
  /**
434
480
  * Draw the entity
435
481
  * @param {Object} renderer
436
482
  * @return {Array} block entities for the canvas to render recursively
437
483
  */
438
484
  draw(renderer) {
439
- if (this.block.entities.length === 0) {
440
- this.refresh();
441
- }
485
+ this.ensureRefreshed();
442
486
 
443
487
  return this.block.entities;
444
488
  }
@@ -460,6 +504,7 @@ export class DimensionBase extends Entity {
460
504
  * @return {Array} - [Point, distance]
461
505
  */
462
506
  closestPoint(P) {
507
+ this.ensureRefreshed();
463
508
  return this.block.closestPoint(P);
464
509
  }
465
510
 
@@ -468,6 +513,7 @@ export class DimensionBase extends Entity {
468
513
  * @return {BoundingBox}
469
514
  */
470
515
  boundingBox() {
516
+ this.ensureRefreshed();
471
517
  return this.block.boundingBox();
472
518
  }
473
519
 
@@ -477,6 +523,7 @@ export class DimensionBase extends Entity {
477
523
  * @return {boolean} true if within
478
524
  */
479
525
  within(selection) {
526
+ this.ensureRefreshed();
480
527
  return this.block.within(selection);
481
528
  }
482
529
 
@@ -486,6 +533,7 @@ export class DimensionBase extends Entity {
486
533
  * @return {boolean} true if touched
487
534
  */
488
535
  touched(selection) {
536
+ this.ensureRefreshed();
489
537
  return this.block.touched(selection);
490
538
  }
491
539
 
@@ -493,7 +541,22 @@ export class DimensionBase extends Entity {
493
541
  * Refresh the dimension geometry
494
542
  */
495
543
  refresh() {
496
- const entities = this.buildDimension();
544
+ // buildDimension() can itself call setProperty() (e.g. LINEARDIMANGLE) which would
545
+ // otherwise re-enter refresh() recursively - skip while already refreshing
546
+ if (this.refreshing) {
547
+ return;
548
+ }
549
+
550
+ this.refreshing = true;
551
+ let entities;
552
+ try {
553
+ entities = this.buildDimension();
554
+ } catch (err) {
555
+ // geometry can't be built yet (e.g. incomplete points during interactive creation) - skip this refresh
556
+ return;
557
+ } finally {
558
+ this.refreshing = false;
559
+ }
497
560
 
498
561
  if (entities) {
499
562
  this.block.clearEntities();
@@ -513,7 +576,47 @@ export class DimensionBase extends Entity {
513
576
  */
514
577
  setProperty(property, value) {
515
578
  if (this.properties.has(property)) {
579
+ if (property === Property.Names.POINTS) {
580
+ // Dragging the Pt11 grip independently of the anchor points (Pt13/Pt14) - as opposed
581
+ // to a rigid whole-dimension Move/Rotate, which moves them together - means the user
582
+ // has explicitly repositioned the text; record that via the DXF bit 128 flag so
583
+ // buildDimension() uses Pt11 directly instead of auto-placing it (DIMJUST/DIMTAD)
584
+ const oldPt11 = this.getPointBySequence(this.points, 11);
585
+ const oldPt13 = this.getPointBySequence(this.points, 13);
586
+ const oldPt14 = this.getPointBySequence(this.points, 14);
587
+ const newPt11 = this.getPointBySequence(value, 11);
588
+ const newPt13 = this.getPointBySequence(value, 13);
589
+ const newPt14 = this.getPointBySequence(value, 14);
590
+
591
+ if (oldPt11 && newPt11 && oldPt13 && newPt13 && oldPt14 && newPt14) {
592
+ const pt11Moved = !oldPt11.isSame(newPt11);
593
+ const anchorsUnchanged = oldPt13.isSame(newPt13) && oldPt14.isSame(newPt14);
594
+ if (pt11Moved && anchorsUnchanged) {
595
+ this.dimType.userPositionedText = true;
596
+ }
597
+ }
598
+ }
599
+
600
+ if (property === Property.Names.POINTS && this.dimType.getBaseDimType() === 0) {
601
+ // Rotated dimension: LINEARDIMANGLE is a fixed angle independent of the points,
602
+ // so a rigid rotation of the points (e.g. via the Rotate tool) must also rotate
603
+ // this stored angle by the same amount - mirrors Hatch's points -> angle handling
604
+ const oldPt13 = this.getPointBySequence(this.points, 13);
605
+ const oldPt14 = this.getPointBySequence(this.points, 14);
606
+ const newPt13 = this.getPointBySequence(value, 13);
607
+ const newPt14 = this.getPointBySequence(value, 14);
608
+
609
+ if (oldPt13 && oldPt14 && newPt13 && newPt14) {
610
+ const theta = newPt13.angle(newPt14) - oldPt13.angle(oldPt14);
611
+ if (theta !== 0) {
612
+ this.setProperty(Property.Names.LINEARDIMANGLE, this.getProperty(Property.Names.LINEARDIMANGLE) + Utils.radians2degrees(theta));
613
+ }
614
+ }
615
+ }
516
616
  super.setProperty(property, value);
617
+ // geometry is derived from properties (points, style, angle, etc.) - rebuild it,
618
+ // e.g. so tools like Rotate/Move that update 'points' are reflected visually
619
+ this.refresh();
517
620
  return;
518
621
  }
519
622
  if (this.hasOwnProperty(property)) {
@@ -4,12 +4,12 @@ import { Text } from '../entities/text.js';
4
4
  import { Input, PromptOptions } from '../lib/inputManager.js';
5
5
  import { Logging } from '../lib/logging.js';
6
6
  import { DimensionBase } from './dimensionBase.js';
7
- import { Point } from '../entities/point.js';
8
7
 
9
8
  import { DesignCore } from '../designCore.js';
10
9
  import { Property } from '../properties/property.js';
11
10
  import { Utils } from '../lib/utils.js';
12
11
  import { RubberBand } from '../lib/auxiliary/rubberBand.js';
12
+ import { SnapPoint } from '../lib/auxiliary/snapPoint.js';
13
13
 
14
14
  /**
15
15
  * Aligned Dimension Entity Class
@@ -45,6 +45,8 @@ export class LinearDimensionBase extends DimensionBase {
45
45
  this.points.push(pt14);
46
46
 
47
47
  const op2 = new PromptOptions(Strings.Input.DIMENSION, [Input.Type.POINT]);
48
+ // avoid polar/ortho tracking a line from pt14 while placing the dimension
49
+ DesignCore.Scene.inputManager.inputPoint = null;
48
50
  const pt11 = await DesignCore.Scene.inputManager.requestInput(op2);
49
51
  pt11.sequence = 11;
50
52
  this.points.push(pt11);
@@ -76,6 +78,70 @@ export class LinearDimensionBase extends DimensionBase {
76
78
  }
77
79
  }
78
80
 
81
+ /**
82
+ * Get the grip points for interactive editing on the canvas.
83
+ * Pt10 (the dimension line's far end) is derived from Pt11 via getPointsFromSelection() -
84
+ * it must be recomputed, not left stale, when the Pt11 grip moves.
85
+ * @return {Array<{point: Point, apply: Function}>} grips
86
+ */
87
+ getGrips() {
88
+ const Pt13 = this.getPointBySequence(this.points, 13);
89
+ const Pt14 = this.getPointBySequence(this.points, 14);
90
+ const Pt11 = this.getPointBySequence(this.points, 11);
91
+
92
+ if (!Pt13 || !Pt14 || !Pt11) {
93
+ return [];
94
+ }
95
+
96
+ const apply = (newPoint) => {
97
+ const tempLine = new Line({ points: [Pt13, Pt14] });
98
+ const angle = this.getProperty(Property.Names.LINEARDIMANGLE);
99
+ return this.constructor.getPointsFromSelection([tempLine], newPoint, angle);
100
+ };
101
+
102
+ return [{ point: Pt11, apply: apply }];
103
+ }
104
+
105
+ /**
106
+ * Get the points where the extension lines meet the dimension line
107
+ * @return {Array<Point>} [Pt13e, Pt14e]
108
+ */
109
+ getDimensionLineEndPoints() {
110
+ const Pt10 = this.getPointBySequence(this.points, 10);
111
+ const Pt13 = this.getPointBySequence(this.points, 13);
112
+ const Pt14 = this.getPointBySequence(this.points, 14);
113
+
114
+ if (this.dimType.getBaseDimType() === 0) {
115
+ // Rotated dimension: the dimension line direction is a fixed, user-specified angle
116
+ // (LINEARDIMANGLE) independent of the measured points - project the extension
117
+ // line origin onto that fixed line, so it stays correct at any rotation, not just 0/90/180/270
118
+ const dimLineAngleRad = Utils.degrees2radians(this.getProperty(Property.Names.LINEARDIMANGLE));
119
+ const linePt2 = Pt10.project(dimLineAngleRad, 1);
120
+ return [Pt13.perpendicular(Pt10, linePt2), Pt10];
121
+ }
122
+
123
+ // Aligned dimension: the dimension line is parallel to the measured edge (Pt13 -> Pt14),
124
+ // derived dynamically from the points rather than a fixed angle
125
+ return [Pt13.project(Pt14.angle(Pt10), Pt14.distance(Pt10)), Pt10];
126
+ }
127
+
128
+ /**
129
+ * Get snap points
130
+ * @param {Point} mousePoint
131
+ * @param {number} delta
132
+ * @return {Array} - array of snap points
133
+ */
134
+ snaps(mousePoint, delta) {
135
+ const Pt10 = this.getPointBySequence(this.points, 10);
136
+ const Pt13 = this.getPointBySequence(this.points, 13);
137
+ const Pt14 = this.getPointBySequence(this.points, 14);
138
+
139
+ if (!Pt10 || !Pt13 || !Pt14) {
140
+ return [];
141
+ }
142
+
143
+ return this.getDimensionLineEndPoints().map((point) => new SnapPoint(point, SnapPoint.Type.END));
144
+ }
79
145
 
80
146
  /**
81
147
  * Build the dimension
@@ -86,7 +152,6 @@ export class LinearDimensionBase extends DimensionBase {
86
152
  let dimension = 0;
87
153
  const entities = [];
88
154
 
89
- const Pt10 = this.getPointBySequence(this.points, 10);
90
155
  const Pt13 = this.getPointBySequence(this.points, 13);
91
156
  const Pt14 = this.getPointBySequence(this.points, 14);
92
157
  const Pt11 = this.getPointBySequence(this.points, 11);
@@ -113,38 +178,8 @@ export class LinearDimensionBase extends DimensionBase {
113
178
  return;
114
179
  }
115
180
 
116
- let Pt13e = new Point();
117
- const Pt14e = Pt10;
118
-
119
- // generate the x and y delta values
120
- const dx = Pt14.x - Pt13.x;
121
- const dy = Pt14.y - Pt13.y;
122
-
123
- // check if Line Pt13 -> P14 is perpendicular to Line Pt14 -> Pt10
124
- const m1 = (Pt14.y - Pt13.y) / (Pt14.x - Pt13.x);
125
- const m2 = (Pt14.y - Pt10.y) / (Pt14.x - Pt10.x);
126
- const perpendicular = Number((m1 * m2).toFixed(1));
127
-
128
- const isAligned = perpendicular !== -1.0 ? false : true;
129
-
130
- if (isAligned || Utils.round(dx) === 0 || Utils.round(dy) === 0) {
131
- Pt13e = Pt13.project(Pt14.angle(Pt10), Pt14.distance(Pt10));
132
- dimension = Pt13.distance(Pt14);
133
- } else {
134
- // Not perpendicular to the selected line: get the primary axis (x or y) and calculate the dimension
135
- const iX = ((Math.abs(Pt10.x - Pt13.x) + Math.abs(Pt14.x - Pt10.x)) - Math.abs(dx));
136
- const iY = ((Math.abs(Pt10.y - Pt13.y) + Math.abs(Pt14.y - Pt10.y)) - Math.abs(dy));
137
-
138
- if (iX >= iY && dy !== 0) {
139
- Pt13e.x = Pt10.x;
140
- Pt13e.y = Pt13.y;
141
- dimension = dy;
142
- } else if (iX < iY && dx !== 0) {
143
- Pt13e.x = Pt13.x;
144
- Pt13e.y = Pt10.y;
145
- dimension = dx;
146
- }
147
- }
181
+ const [Pt13e, Pt14e] = this.getDimensionLineEndPoints();
182
+ dimension = this.dimType.getBaseDimType() === 0 ? Pt13e.distance(Pt14e) : Pt13.distance(Pt14);
148
183
 
149
184
  // ensure dimension is positive
150
185
  dimension = Math.abs(dimension);
@@ -155,9 +190,6 @@ export class LinearDimensionBase extends DimensionBase {
155
190
  const reverseDimLineAngle = Pt14e.angle(Pt13e);
156
191
  const midPoint = Pt13e.midPoint(Pt14e);
157
192
 
158
- // set the dimension line angle. This is required for rotated dimension dxf definition
159
- this.setProperty(Property.Names.LINEARDIMANGLE, Utils.radians2degrees(dimLineAngle));
160
-
161
193
  // Approximate text width based on height using the formatted dimension value i.e with units, precision and symbols
162
194
  const approxTextWidth = Text.getApproximateWidth(this.getDimensionValue(dimension), DIMTXT);
163
195
  const approxTextHalfWidth = approxTextWidth * 0.5;
@@ -221,6 +253,12 @@ export class LinearDimensionBase extends DimensionBase {
221
253
  break;
222
254
  }
223
255
 
256
+ // A grip-dragged text position overrides the DIMJUST auto-placement above with Pt11 as
257
+ // the anchor - DIMTAD's vertical offset below still applies on top, matching AutoCAD
258
+ if (this.dimType.hasUserPositionedText()) {
259
+ textPosition = Pt11;
260
+ }
261
+
224
262
  // Check if the text is aligned with the dimension line
225
263
  const textAndDimlineAligned = this.alignedOrOpposite(textRotation, dimLineAngle);
226
264
 
@@ -14,7 +14,7 @@ export class RadialDimension extends CircularDimensionBase {
14
14
  */
15
15
  constructor(data) {
16
16
  super(data);
17
- this.dimType.setDimType(4); // Radial dimension
17
+ this.dimType.setBaseType(4); // Radial dimension - preserve flags loaded from DXF group 70
18
18
  }
19
19
 
20
20
  /**
@@ -48,6 +48,31 @@ export class RadialDimension extends CircularDimensionBase {
48
48
  return points;
49
49
  }
50
50
 
51
+ /**
52
+ * Get the grip points for interactive editing on the canvas.
53
+ * Pt15 (the circle attach point) is derived from Pt11 via getPointsFromSelection() -
54
+ * must be recomputed from the unchanged centre (Pt10) and radius, not left stale.
55
+ * @return {Array<{point: Point, apply: Function}>} grips
56
+ */
57
+ getGrips() {
58
+ const Pt10 = this.getPointBySequence(this.points, 10); // centre
59
+ const Pt15 = this.getPointBySequence(this.points, 15); // point on circle
60
+ const Pt11 = this.getPointBySequence(this.points, 11); // text position
61
+
62
+ if (!Pt10 || !Pt15 || !Pt11) {
63
+ return [];
64
+ }
65
+
66
+ const radius = Pt10.distance(Pt15);
67
+
68
+ const apply = (newPoint) => {
69
+ const tempEntity = { points: [Pt10], getRadius: () => radius };
70
+ return this.constructor.getPointsFromSelection([tempEntity], newPoint);
71
+ };
72
+
73
+ return [{ point: Pt11, apply: apply }];
74
+ }
75
+
51
76
  /**
52
77
  * Write the entity to file in the dxf format
53
78
  * @param {DXFFile} file
@@ -22,7 +22,7 @@ export class RotatedDimension extends LinearDimensionBase {
22
22
  */
23
23
  constructor(data) {
24
24
  super(data);
25
- this.dimType.setDimType(0); // Rotated dimension
25
+ this.dimType.setBaseType(0); // Rotated dimension - preserve flags loaded from DXF group 70
26
26
  }
27
27
 
28
28
  /**
@@ -62,6 +62,8 @@ export class RotatedDimension extends LinearDimensionBase {
62
62
  this.points.push(pt14);
63
63
 
64
64
  const op2 = new PromptOptions(Strings.Input.DIMENSION, [Input.Type.POINT]);
65
+ // avoid polar/ortho tracking a line from pt14 while placing the dimension
66
+ DesignCore.Scene.inputManager.inputPoint = null;
65
67
  const pt11 = await DesignCore.Scene.inputManager.requestInput(op2);
66
68
  pt11.sequence = 11;
67
69
  this.points.push(pt11);
@@ -126,8 +128,9 @@ export class RotatedDimension extends LinearDimensionBase {
126
128
  const pt11 = DesignCore.Mouse.pointOnScene();
127
129
  pt11.sequence = 11;
128
130
  const tempLine = new Line({ points: [this.points[0], this.points[1]] });
129
- const points = RotatedDimension.getPointsFromSelection([tempLine], pt11, this.getProperty(Property.Names.LINEARDIMANGLE));
130
- DesignCore.Scene.previewEntities.create(this.type, { points: points, dimensionStyle: this.getProperty(Property.Names.DIMENSIONSTYLE) });
131
+ const angle = this.getProperty(Property.Names.LINEARDIMANGLE);
132
+ const points = RotatedDimension.getPointsFromSelection([tempLine], pt11, angle);
133
+ DesignCore.Scene.previewEntities.create(this.type, { points: points, dimensionStyle: this.getProperty(Property.Names.DIMENSIONSTYLE), linearDimAngle: angle });
131
134
  }
132
135
  }
133
136
 
@@ -109,6 +109,15 @@ export class Entity {
109
109
  this.properties.set(Property.Names.POINTS, value, this);
110
110
  }
111
111
 
112
+ /**
113
+ * Get the grip points for interactive editing on the canvas.
114
+ * Overridden by entities that expose editable grips (e.g. DimensionBase).
115
+ * @return {Array<{point: Point, apply: Function}>} grips - apply(newPoint) returns a new points array
116
+ */
117
+ getGrips() {
118
+ return [];
119
+ }
120
+
112
121
  /**
113
122
  * Normalise entity-level DXF group codes before construction.
114
123
  * Subclasses override this for geometric transformation; they receive data
@@ -13,6 +13,7 @@ export class SnapPoint {
13
13
  TANGENT: 'tangent',
14
14
  NODE: 'node',
15
15
  PERPENDICULAR: 'perpendicular',
16
+ GRIP: 'grip',
16
17
  });
17
18
 
18
19
  /**
@@ -57,6 +58,14 @@ export class SnapPoint {
57
58
  new Point(x - size, y + size),
58
59
  ], { closed: true });
59
60
  break;
61
+ case SnapPoint.Type.GRIP: // filled square
62
+ renderer.drawShape([
63
+ new Point(x - size, y - size),
64
+ new Point(x + size, y - size),
65
+ new Point(x + size, y + size),
66
+ new Point(x - size, y + size),
67
+ ], { closed: true, fill: true });
68
+ break;
60
69
  case SnapPoint.Type.MID: // triangle
61
70
  renderer.drawShape([
62
71
  new Point(x, y + size),
@@ -5,6 +5,7 @@ import { Point } from '../entities/point.js';
5
5
  import { Input } from './input.js';
6
6
  import { Logging } from './logging.js';
7
7
  import { PlotOptions } from './plotOptions.js';
8
+ import { SnapPoint } from './auxiliary/snapPoint.js';
8
9
  // import { RendererBase } from './renderers/rendererBase.js';
9
10
 
10
11
  import { DesignCore } from '../designCore.js';
@@ -305,7 +306,9 @@ export class Canvas {
305
306
  */
306
307
  exportTo(renderer, plotOptions) {
307
308
  let area;
308
- if (plotOptions.plotArea === PlotOptions.Area.DISPLAY) {
309
+ if (plotOptions.plotArea === PlotOptions.Area.WINDOW) {
310
+ area = new BoundingBox(plotOptions.windowArea.point1, plotOptions.windowArea.point2);
311
+ } else if (plotOptions.plotArea === PlotOptions.Area.DISPLAY) {
309
312
  const viewport = this.getSceneOffset();
310
313
  area = new BoundingBox(new Point(viewport.xmin, viewport.ymin), new Point(viewport.xmax, viewport.ymax));
311
314
  } else {
@@ -318,6 +321,7 @@ export class Canvas {
318
321
  pageWidth: plotOptions.pageWidth,
319
322
  pageHeight: plotOptions.pageHeight,
320
323
  plotScale: plotOptions.plotScale,
324
+ margin: plotOptions.margin,
321
325
  });
322
326
  if (!matrix) return false;
323
327
 
@@ -396,6 +400,11 @@ export class Canvas {
396
400
  const selectedEntities = DesignCore.Scene.selectionManager.selectedEntities;
397
401
  this.#paintEntities(selectedEntities, renderer, { colour: selectionHaloColour, lineWidthDelta: selectionLineWidthDelta });
398
402
 
403
+ // Paint grips for selected entities - only when idle (no active command)
404
+ if (DesignCore.Scene.inputManager.activeCommand === undefined) {
405
+ this.#paintGrips(selectedEntities, renderer, scale);
406
+ }
407
+
399
408
  // Paint the auxiliary scene items
400
409
  // auxiliary items include things like the selection window, snap points etc
401
410
  // these items have their own draw routine
@@ -459,6 +468,22 @@ export class Canvas {
459
468
  }
460
469
  }
461
470
 
471
+ /**
472
+ * Draw grip markers for every grip exposed by the given entities.
473
+ * @param {Array} entities - selected entities to draw grips for
474
+ * @param {Object} renderer - renderer instance
475
+ * @param {number} scale - current canvas scale
476
+ */
477
+ #paintGrips(entities, renderer, scale) {
478
+ for (const entity of entities) {
479
+ for (const grip of entity.getGrips()) {
480
+ renderer.save();
481
+ new SnapPoint(grip.point, SnapPoint.Type.GRIP).draw(renderer, scale);
482
+ renderer.restore();
483
+ }
484
+ }
485
+ }
486
+
462
487
  /**
463
488
  * Set the scene context on the renderer before drawing an entity.
464
489
  * @param {Object} item - entity to draw
@@ -12,7 +12,7 @@ import { Text } from '../entities/text.js';
12
12
  import { Block } from '../tables/block.js';
13
13
  import { Insert } from '../entities/insert.js';
14
14
  import { Hatch } from '../entities/hatch.js';
15
- /*
15
+
16
16
  import { Dimension } from '../dimensions/dimension.js';
17
17
  import { AlignedDimension } from '../dimensions/alignedDimension.js';
18
18
  import { AngularDimension } from '../dimensions/angularDimension.js';
@@ -20,7 +20,7 @@ import { LinearDimension } from '../dimensions/linearDimension.js';
20
20
  import { DiametricDimension } from '../dimensions/diametricDimension.js';
21
21
  import { RadialDimension } from '../dimensions/radialDimension.js';
22
22
  import { RotatedDimension } from '../dimensions/rotatedDimension.js';
23
- */
23
+
24
24
 
25
25
  // import { Point } from "../entities/point.js"
26
26
 
@@ -43,6 +43,9 @@ import { Purge } from '../tools/purge.js';
43
43
  import { Pan } from '../tools/pan.js';
44
44
  import { Zoom } from '../tools/zoom.js';
45
45
 
46
+ // Internal tools (not shown in any toolbar, invoked programmatically)
47
+ import { WindowPick } from '../tools/windowPick.js';
48
+
46
49
  // Clipboard tools
47
50
  import { Copybase } from '../tools/copybase.js';
48
51
  import { Copyclip } from '../tools/copyclip.js';
@@ -67,7 +70,7 @@ const classes = {
67
70
  // Spline,
68
71
  ArcAlignedText,
69
72
  Text,
70
- /*
73
+
71
74
  Dimension,
72
75
  AngularDimension,
73
76
  AlignedDimension,
@@ -75,7 +78,7 @@ const classes = {
75
78
  LinearDimension,
76
79
  RadialDimension,
77
80
  RotatedDimension,
78
- */
81
+
79
82
  Block,
80
83
  Insert,
81
84
  Hatch,
@@ -99,6 +102,8 @@ const classes = {
99
102
  Zoom,
100
103
  Pan,
101
104
 
105
+ WindowPick,
106
+
102
107
  Copybase,
103
108
  Copyclip,
104
109
  Cutclip,
@@ -388,8 +388,14 @@ export class DXF {
388
388
 
389
389
  const command = entity[0];
390
390
  if (DesignCore.CommandManager.isCommand(command)) {
391
- const newEntity = DesignCore.CommandManager.createNew(command, entity);
392
- DesignCore.Scene.entities.add(newEntity);
391
+ try {
392
+ const newEntity = DesignCore.CommandManager.createNew(command, entity);
393
+ DesignCore.Scene.entities.add(newEntity);
394
+ } catch (err) {
395
+ // a recognised command can still fail to build (e.g. unsupported dimension subtype) - skip it, don't abort the rest of the file
396
+ Logging.instance.warn(`${Strings.Message.UNKNOWNCOMMAND} ${command}: ${err}`);
397
+ this.unsupportedElements = true;
398
+ }
393
399
  } else {
394
400
  Logging.instance.warn(`${Strings.Message.UNKNOWNCOMMAND} ${command}`);
395
401
  this.unsupportedElements = true;
@@ -1,4 +1,5 @@
1
1
  import { Tool } from '../tools/tool.js';
2
+ import { GripStretch } from '../tools/gripStretch.js';
2
3
  import { Snapping } from './snapping.js';
3
4
 
4
5
  import { DesignCore } from '../designCore.js';
@@ -39,6 +40,7 @@ export class InputManager {
39
40
  }
40
41
 
41
42
  this.promptOption = undefined;
43
+ DesignCore.Mouse.reset();
42
44
  DesignCore.Scene.reset();
43
45
  DesignCore.Canvas.setCursorForInputTypes([]);
44
46
  }
@@ -130,7 +132,9 @@ export class InputManager {
130
132
  * @param {string} command - command name or shortcut
131
133
  */
132
134
  startCommand(command) {
133
- if (this.activeCommand !== undefined) {
135
+ if (this.activeCommand instanceof Tool) {
136
+ this.activeCommand.cancel();
137
+ } else if (this.activeCommand !== undefined) {
134
138
  this.reset();
135
139
  }
136
140
 
@@ -195,6 +199,17 @@ export class InputManager {
195
199
  * @param {Point} point
196
200
  */
197
201
  onLeftClick(point) {
202
+ // Grip hit-test takes priority over normal entity selection when idle, so clicking a grip
203
+ // starts GripStretch (which requests the stretch point itself) instead of reselecting it
204
+ if (this.activeCommand === undefined) {
205
+ const grabbedGrip = DesignCore.Scene.selectionManager.findGripUnderMouse(DesignCore.Mouse.pointOnScene());
206
+ if (grabbedGrip) {
207
+ this.activeCommand = new GripStretch(grabbedGrip.entityIndex, grabbedGrip.grip);
208
+ this.activeCommand.execute();
209
+ return;
210
+ }
211
+ }
212
+
198
213
  // left clicks are used for selection of entities and points
199
214
  // Selection of entities can only occur if there is no active snap available
200
215
  // Selection of entities can only occur if the mouse is over an entity
@@ -281,8 +296,14 @@ export class InputManager {
281
296
  this.activeCommand.preview();
282
297
  }
283
298
 
284
- // Only show hover glow when no command preview is occupying previewEntities
285
- if (!snapped && DesignCore.Scene.previewEntities.count() === 0) {
299
+ // a command preview can coexist with an active selection prompt (e.g. a dimension
300
+ // previewing a point-based result while still accepting a second entity selection)
301
+ const canSelectEntity = this.promptOption?.types.includes(Input.Type.SINGLESELECTION) ||
302
+ this.promptOption?.types.includes(Input.Type.SELECTIONSET);
303
+
304
+ // Only show hover glow when no command preview is occupying previewEntities,
305
+ // unless the active prompt also accepts an entity selection
306
+ if (!snapped && (DesignCore.Scene.previewEntities.count() === 0 || canSelectEntity)) {
286
307
  this.highlightEntityUnderMouse();
287
308
  }
288
309
 
package/core/lib/mouse.js CHANGED
@@ -15,6 +15,16 @@ export class Mouse {
15
15
  this.lastButton = 0; // last button for double click check
16
16
  }
17
17
 
18
+ /**
19
+ * Clear button state - guards against a stuck button when a mouseUp
20
+ * never reaches the canvas (e.g. focus stolen by another window mid-click)
21
+ */
22
+ reset() {
23
+ this.buttonOneDown = false;
24
+ this.buttonTwoDown = false;
25
+ this.buttonThreeDown = false;
26
+ }
27
+
18
28
  /**
19
29
  * Returns the point on the design canvas
20
30
  * @return {Point}