@design-core/core 51.2.0 → 52.2.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/core/dimensions/alignedDimension.js +3 -1
  2. package/core/dimensions/angularDimension.js +118 -36
  3. package/core/dimensions/circularDimensionBase.js +32 -12
  4. package/core/dimensions/diametricDimension.js +29 -1
  5. package/core/dimensions/dimension.js +58 -5
  6. package/core/dimensions/dimensionBase.js +107 -4
  7. package/core/dimensions/linearDimensionBase.js +75 -37
  8. package/core/dimensions/radialDimension.js +26 -1
  9. package/core/dimensions/rotatedDimension.js +6 -3
  10. package/core/entities/entity.js +9 -0
  11. package/core/lib/auxiliary/snapPoint.js +9 -0
  12. package/core/lib/canvas.js +22 -0
  13. package/core/lib/commandManager.js +4 -4
  14. package/core/lib/dxf/dxf.js +8 -2
  15. package/core/lib/inputManager.js +23 -3
  16. package/core/lib/renderers/pdfRenderer.js +17 -1
  17. package/core/lib/scene.js +2 -0
  18. package/core/lib/selectionManager.js +28 -0
  19. package/core/lib/strings.js +1 -0
  20. package/core/properties/dimType.js +14 -0
  21. package/core/properties/property.js +1 -0
  22. package/core/tables/dimStyle.js +32 -2
  23. package/core/tables/dimStyleManager.js +2 -0
  24. package/core/tables/styleManagerBase.js +17 -3
  25. package/core/tables/tableManagerBase.js +24 -2
  26. package/core/tools/gripStretch.js +97 -0
  27. package/core/tools/offset.js +7 -1
  28. package/core/tools/tool.js +10 -0
  29. package/cspell.json +3 -0
  30. package/package.json +1 -1
  31. package/test/dimensions/alignedDimension.test.js +165 -0
  32. package/test/dimensions/angularDimension.test.js +149 -0
  33. package/test/dimensions/diametricDimension.test.js +77 -4
  34. package/test/dimensions/dimension.test.js +130 -0
  35. package/test/dimensions/dimensionBase.test.js +97 -4
  36. package/test/dimensions/radialDimension.test.js +34 -0
  37. package/test/dimensions/rotatedDimension.test.js +80 -0
  38. package/test/integration/exportIntegration.reference.dxf +3 -9
  39. package/test/integration/exportIntegration.test.js +1 -0
  40. package/test/integration/userInputIntegration.reference.dxf +3 -9
  41. package/test/integration/userInputIntegration.test.js +1 -0
  42. package/test/lib/inputManager.test.js +316 -1
  43. package/test/lib/selectionManager.test.js +42 -0
  44. package/test/renderers/pdfRenderer.test.js +26 -0
  45. package/test/tables/dimStyleManager.test.js +90 -0
  46. package/test/tables/dimstyle.test.js +32 -0
  47. package/test/tables/tableManagerBase.test.js +167 -0
  48. package/test/tools/gripStretch.test.js +108 -0
  49. package/test/tools/offset.test.js +2 -0
  50. package/test/tools/purge.test.js +3 -1
  51. package/test/tools/tool.test.js +25 -0
@@ -21,7 +21,7 @@ export class AlignedDimension extends LinearDimensionBase {
21
21
  */
22
22
  constructor(data) {
23
23
  super(data);
24
- this.dimType.setDimType(1); // Aligned dimension
24
+ this.dimType.setBaseType(1); // Aligned dimension - preserve flags loaded from DXF group 70
25
25
  }
26
26
 
27
27
  /**
@@ -57,6 +57,8 @@ export class AlignedDimension extends LinearDimensionBase {
57
57
  this.points.push(pt14);
58
58
 
59
59
  const op2 = new PromptOptions(Strings.Input.DIMENSION, [Input.Type.POINT]);
60
+ // avoid polar/ortho tracking a line from pt14 while placing the dimension
61
+ DesignCore.Scene.inputManager.inputPoint = null;
60
62
  const pt11 = await DesignCore.Scene.inputManager.requestInput(op2);
61
63
  pt11.sequence = 11;
62
64
  this.points.push(pt11);
@@ -10,6 +10,7 @@ import { DXFFile } from '../lib/dxf/dxfFile.js';
10
10
  import { DimensionBase } from './dimensionBase.js';
11
11
  import { Intersection } from '../lib/intersect.js';
12
12
  import { Point } from '../entities/point.js';
13
+ import { SnapPoint } from '../lib/auxiliary/snapPoint.js';
13
14
 
14
15
  import { Text } from '../entities/text.js'; // used to debug the dimension points
15
16
 
@@ -27,7 +28,7 @@ export class AngularDimension extends DimensionBase {
27
28
  */
28
29
  constructor(data) {
29
30
  super(data);
30
- this.dimType.setDimType(2); // Angular dimension
31
+ this.dimType.setBaseType(2); // Angular dimension - preserve flags loaded from DXF group 70
31
32
  }
32
33
 
33
34
  /**
@@ -116,6 +117,8 @@ export class AngularDimension extends DimensionBase {
116
117
  }
117
118
 
118
119
  const op2 = new PromptOptions(Strings.Input.START, [Input.Type.POINT]);
120
+ // avoid polar/ortho tracking a line from the last defining point while placing the dimension
121
+ DesignCore.Scene.inputManager.inputPoint = null;
119
122
  const Pt11 = await DesignCore.Scene.inputManager.requestInput(op2);
120
123
 
121
124
  const tempLineOne = new Line({ points: [Pt15, Pt10] });
@@ -151,6 +154,32 @@ export class AngularDimension extends DimensionBase {
151
154
  }
152
155
  }
153
156
 
157
+ /**
158
+ * Get the grip points for interactive editing on the canvas.
159
+ * Pt16 (the dimension arc's radial position) is derived from Pt11 via getPointsFromSelection() -
160
+ * must be recomputed from the two unchanged defining lines, not left stale.
161
+ * @return {Array<{point: Point, apply: Function}>} grips
162
+ */
163
+ getGrips() {
164
+ const Pt15 = this.getPointBySequence(this.points, 15);
165
+ const Pt10 = this.getPointBySequence(this.points, 10);
166
+ const Pt13 = this.getPointBySequence(this.points, 13);
167
+ const Pt14 = this.getPointBySequence(this.points, 14);
168
+ const Pt11 = this.getPointBySequence(this.points, 11);
169
+
170
+ if (!Pt15 || !Pt10 || !Pt13 || !Pt14 || !Pt11) {
171
+ return [];
172
+ }
173
+
174
+ const apply = (newPoint) => {
175
+ const tempLineOne = new Line({ points: [Pt15, Pt10] });
176
+ const tempLineTwo = new Line({ points: [Pt13, Pt14] });
177
+ return AngularDimension.getPointsFromSelection([tempLineOne, tempLineTwo], newPoint);
178
+ };
179
+
180
+ return [{ point: Pt11, apply: apply }];
181
+ }
182
+
154
183
  /**
155
184
  * Get sequenced points from user selection
156
185
  * @param {any} entities
@@ -298,6 +327,79 @@ export class AngularDimension extends DimensionBase {
298
327
  return sortedPoints;
299
328
  }
300
329
 
330
+ /**
331
+ * Get the dimension arc end points (arrow positions) and the extension line extents
332
+ * @param {Point} Pt10 - sorted point 10
333
+ * @param {Point} Pt13 - sorted point 13
334
+ * @param {Point} Pt14 - sorted point 14
335
+ * @param {Point} Pt15 - sorted point 15
336
+ * @param {Point} Pt16 - dimension arc position
337
+ * @return {Object} {intersectPt, radius, arrow1pos, arrow2pos, line1Extents, line2Extents}
338
+ */
339
+ getDimensionArcGeometry(Pt10, Pt13, Pt14, Pt15, Pt16) {
340
+ const intersectPt = Intersection.intersectSegmentSegment(Pt15, Pt10, Pt13, Pt14, false, true).points[0];
341
+ const radius = intersectPt.distance(Pt16);
342
+
343
+ const quadOneStart = Pt14;
344
+ const quadOneEnd = Pt10;
345
+ const quadTwoStart = Pt14.rotate(intersectPt, Math.PI);
346
+ const quadTwoEnd = Pt10.rotate(intersectPt, Math.PI);
347
+ let arrow1pos = intersectPt;
348
+ let arrow2pos = intersectPt;
349
+ let line1Extents = Pt14;
350
+ let line2Extents = Pt10;
351
+ if (Pt16.isOnArc(quadOneStart, quadOneEnd, intersectPt, 1)) {
352
+ arrow1pos = intersectPt.project(intersectPt.angle(quadOneStart), radius);
353
+ arrow2pos = intersectPt.project(intersectPt.angle(quadOneEnd), radius);
354
+ line1Extents = Pt14;
355
+ line2Extents = Pt10;
356
+ } else if (Pt16.isOnArc(quadOneEnd, quadTwoStart, intersectPt, 1)) {
357
+ arrow1pos = intersectPt.project(intersectPt.angle(quadOneEnd), radius);
358
+ arrow2pos = intersectPt.project(intersectPt.angle(quadTwoStart), radius);
359
+ line1Extents = Pt10;
360
+ line2Extents = Pt13;
361
+ } else if (Pt16.isOnArc(quadTwoStart, quadTwoEnd, intersectPt, 1)) {
362
+ arrow1pos = intersectPt.project(intersectPt.angle(quadTwoStart), radius);
363
+ arrow2pos = intersectPt.project(intersectPt.angle(quadTwoEnd), radius);
364
+ line1Extents = Pt13;
365
+ line2Extents = Pt15;
366
+ } else if (Pt16.isOnArc(quadTwoEnd, quadOneStart, intersectPt, 1)) {
367
+ arrow1pos = intersectPt.project(intersectPt.angle(quadTwoEnd), radius);
368
+ arrow2pos = intersectPt.project(intersectPt.angle(quadOneStart), radius);
369
+ line1Extents = Pt15;
370
+ line2Extents = Pt14;
371
+ }
372
+
373
+ return { intersectPt, radius, arrow1pos, arrow2pos, line1Extents, line2Extents };
374
+ }
375
+
376
+ /**
377
+ * Get snap points
378
+ * @param {Point} mousePoint
379
+ * @param {number} delta
380
+ * @return {Array} - array of snap points
381
+ */
382
+ snaps(mousePoint, delta) {
383
+ if (!this.getPointBySequence(this.points, 16) || this.points.length < 6) {
384
+ return [];
385
+ }
386
+
387
+ const sortedPoints = this.sortDimensionPoints();
388
+ const Pt10 = this.getPointBySequence(sortedPoints, 10);
389
+ const Pt13 = this.getPointBySequence(sortedPoints, 13);
390
+ const Pt14 = this.getPointBySequence(sortedPoints, 14);
391
+ const Pt15 = this.getPointBySequence(sortedPoints, 15);
392
+ const Pt16 = this.getPointBySequence(this.points, 16);
393
+
394
+ const { radius, arrow1pos, arrow2pos } = this.getDimensionArcGeometry(Pt10, Pt13, Pt14, Pt15, Pt16);
395
+
396
+ if (radius === 0) {
397
+ return [];
398
+ }
399
+
400
+ return [arrow1pos, arrow2pos].map((point) => new SnapPoint(point, SnapPoint.Type.END));
401
+ }
402
+
301
403
  /**
302
404
  * Build the dimension
303
405
  * @param {Object} style
@@ -337,36 +439,7 @@ export class AngularDimension extends DimensionBase {
337
439
  return;
338
440
  }
339
441
 
340
- // Inline quadrant/arrow/line logic
341
- const quadOneStart = Pt14;
342
- const quadOneEnd = Pt10;
343
- const quadTwoStart = Pt14.rotate(intersectPt, Math.PI);
344
- const quadTwoEnd = Pt10.rotate(intersectPt, Math.PI);
345
- let arrow1pos = intersectPt;
346
- let arrow2pos = intersectPt;
347
- let line1Extents = Pt14;
348
- let line2Extents = Pt10;
349
- if (Pt16.isOnArc(quadOneStart, quadOneEnd, intersectPt, 1)) {
350
- arrow1pos = intersectPt.project(intersectPt.angle(quadOneStart), radius);
351
- arrow2pos = intersectPt.project(intersectPt.angle(quadOneEnd), radius);
352
- line1Extents = Pt14;
353
- line2Extents = Pt10;
354
- } else if (Pt16.isOnArc(quadOneEnd, quadTwoStart, intersectPt, 1)) {
355
- arrow1pos = intersectPt.project(intersectPt.angle(quadOneEnd), radius);
356
- arrow2pos = intersectPt.project(intersectPt.angle(quadTwoStart), radius);
357
- line1Extents = Pt10;
358
- line2Extents = Pt13;
359
- } else if (Pt16.isOnArc(quadTwoStart, quadTwoEnd, intersectPt, 1)) {
360
- arrow1pos = intersectPt.project(intersectPt.angle(quadTwoStart), radius);
361
- arrow2pos = intersectPt.project(intersectPt.angle(quadTwoEnd), radius);
362
- line1Extents = Pt13;
363
- line2Extents = Pt15;
364
- } else if (Pt16.isOnArc(quadTwoEnd, quadOneStart, intersectPt, 1)) {
365
- arrow1pos = intersectPt.project(intersectPt.angle(quadTwoEnd), radius);
366
- arrow2pos = intersectPt.project(intersectPt.angle(quadOneStart), radius);
367
- line1Extents = Pt15;
368
- line2Extents = Pt14;
369
- }
442
+ const { arrow1pos, arrow2pos, line1Extents, line2Extents } = this.getDimensionArcGeometry(Pt10, Pt13, Pt14, Pt15, Pt16);
370
443
 
371
444
  const line1Angle = intersectPt.angle(arrow1pos);
372
445
  const line2Angle = intersectPt.angle(arrow2pos);
@@ -389,7 +462,7 @@ export class AngularDimension extends DimensionBase {
389
462
  let textPosition = arcMiddle;
390
463
  let textRotation = this.getTextDirection(horizontalText ? 0 : arrow1pos.angle(arrow2pos));
391
464
  let textAboveDirection = textRotation + Math.PI / 2;
392
- let textAboveDistance = DIMTXT * 0.5 + DIMGAP;
465
+ const textAboveDistance = DIMTXT * 0.5 + DIMGAP;
393
466
  const outsideOffsetDirection = Pt11.distance(arrow1pos) < Pt11.distance(arrow2pos) ? -1 : 1;
394
467
  const textPositionOffsetAngle = arrowRadians * 3;
395
468
  const internalPositionAdjustmentAngle = dimension * 0.5 > textPositionOffsetAngle ? dimension * 0.5 - textPositionOffsetAngle : 0;
@@ -428,10 +501,19 @@ export class AngularDimension extends DimensionBase {
428
501
  break;
429
502
  }
430
503
 
431
- // Text vertical position
432
- const textAndDimlineAligned = this.alignedOrOpposite(textRotation, (intersectPt.angle(textPosition) + Math.PI / 2));
433
- const textIsOnExtensionLine = textPosition.perpendicular(intersectPt, arrow1pos).isSame(textPosition) || textPosition.perpendicular(intersectPt, arrow2pos).isSame(textPosition);
434
- if (!textAndDimlineAligned && !textIsOnExtensionLine) textAboveDistance = 0;
504
+ // A grip-dragged text position overrides the DIMJUST auto-placement above with Pt11 as
505
+ // the anchor - DIMTAD's vertical offset below still applies on top, matching AutoCAD.
506
+ // Rotation must be recomputed from Pt11's own tangent too, otherwise it's stuck at the
507
+ // DIMJUST-computed (pre-drag) rotation, which is wrong once the text has moved elsewhere
508
+ if (this.dimType.hasUserPositionedText()) {
509
+ textPosition = Pt11;
510
+ textRotation = this.getTextDirection(horizontalText ? 0 : (intersectPt.angle(Pt11) + Math.PI / 2));
511
+ textAboveDirection = textRotation + Math.PI / 2;
512
+ }
513
+
514
+ // DIMTAD applies unconditionally (matching CircularDimensionBase) - forced-horizontal text
515
+ // (DIMTIH/DIMTOH) rarely aligns with the arc's tangent, so gating the offset on that
516
+ // alignment suppressed DIMTAD for nearly every angular dimension
435
517
  switch (DIMTAD) {
436
518
  case 1:
437
519
  case 2:
@@ -5,7 +5,6 @@ import { PolylineBase } from '../entities/polylineBase.js';
5
5
  import { Line } from '../entities/line.js';
6
6
  import { Text } from '../entities/text.js';
7
7
  import { Point } from '../entities/point.js';
8
- import { Intersection } from '../lib/intersect.js';
9
8
  import { Input, PromptOptions } from '../lib/inputManager.js';
10
9
  import { Logging } from '../lib/logging.js';
11
10
  import { DimensionBase } from './dimensionBase.js';
@@ -70,6 +69,8 @@ export class CircularDimensionBase extends DimensionBase {
70
69
  }
71
70
 
72
71
  const op1 = new PromptOptions(Strings.Input.DIMENSION, [Input.Type.POINT]);
72
+ // avoid polar/ortho tracking a line from the last defining point while placing the dimension
73
+ DesignCore.Scene.inputManager.inputPoint = null;
73
74
  const Pt11 = await DesignCore.Scene.inputManager.requestInput(op1);
74
75
  Pt11.sequence = 11;
75
76
  this.points.push(Pt11);
@@ -130,15 +131,9 @@ export class CircularDimensionBase extends DimensionBase {
130
131
  // Ensure points are aligned Pt10 > Pt15 > Pt11
131
132
  // This resets the points to a known state to allow application of the dimstyle
132
133
  if (Pt15.isOnLine(Pt10, Pt11) === false) {
133
- // Find the intersection between Pt10 > Pt15 and a horizontal ray from Pt11
134
- const line2 = { start: Pt10, end: Pt15 };
135
- const line1 = { start: Pt11, end: new Point(Pt10.x, Pt11.y) };
136
- const intersect = Intersection.intersectSegmentSegment(line1.start, line1.end, line2.start, line2.end, false, true);
137
- // Reset Pt11 - This should be the same as the originally selected Pt11
138
- // Pt11 position can be changed depending on the dimstyle
139
- if (intersect.points[0]) {
140
- Pt11 = intersect.points[0];
141
- }
134
+ // Snap Pt11 onto the (extended) dimension line through Pt10/Pt15 via perpendicular
135
+ // projection - rotation-invariant, unlike a world-axis-aligned ray intersection
136
+ Pt11 = Pt11.perpendicular(Pt10, Pt15);
142
137
  }
143
138
 
144
139
  // set a minimum position for the text
@@ -216,13 +211,38 @@ export class CircularDimensionBase extends DimensionBase {
216
211
  if (DIMTAD !== 0 && textAndDimlineAligned) {
217
212
  lineLength = lineLength + approxTextWidth + DIMTXT; // Add text offset to line length
218
213
  }
219
- const startPoint = DIMTOFL ? Pt10 : Pt15;
220
- const endPoint = Pt15.project(Pt10.angle(Pt15), isInside ? -lineLength : lineLength);
214
+ const isDiametric = this.dimType.getBaseDimType() === 3;
215
+ // inside diametric dimensions start at Pt15 so the arrow stays connected to the text
216
+ const startPoint = isDiametric ? (isInside ? Pt15 : Pt10) : (DIMTOFL ? Pt10 : Pt15);
217
+ let endPoint = Pt15.project(Pt10.angle(Pt15), isInside ? -lineLength : lineLength);
218
+ const textOnLine = DIMTAD === 0 && textAndDimlineAligned;
219
+ const insideDiametric = isDiametric && isInside;
220
+
221
+ // text is not on the dimension line, so the line continues to the opposite arrow
222
+ if (insideDiametric && !textOnLine) {
223
+ endPoint = Pt10;
224
+ }
225
+
221
226
  const line = new Line({ points: [startPoint, endPoint] });
222
227
  const arrowDirection = isInside ? Pt15.angle(Pt10) : Pt10.angle(Pt15);
223
228
  const arrowHead = this.getArrowHead(Pt15, arrowDirection);
224
229
  entities.push(line, arrowHead);
225
230
 
231
+ if (insideDiametric && textOnLine) {
232
+ // opposite side segment, starting after the text and its gap
233
+ const dirToPt10 = Pt15.angle(Pt10);
234
+ const oppositeStart = textPosition.project(dirToPt10, approxTextWidth * 0.5 + DIMGAP);
235
+ if (Pt15.distance(oppositeStart) < Pt15.distance(Pt10)) {
236
+ entities.push(new Line({ points: [oppositeStart, Pt10] }));
237
+ }
238
+ }
239
+
240
+ if (isDiametric) {
241
+ // diametric dimensions have an arrow at both ends of the diameter
242
+ const oppositeDirection = isInside ? Pt10.angle(Pt15) : Pt15.angle(Pt10);
243
+ entities.push(this.getArrowHead(Pt10, oppositeDirection));
244
+ }
245
+
226
246
  // Get the dimension text using the value, position and rotation
227
247
  const text = this.getDimensionText(dimension, textPosition, textRotation);
228
248
  entities.push(text);
@@ -13,7 +13,7 @@ export class DiametricDimension extends CircularDimensionBase {
13
13
  */
14
14
  constructor(data) {
15
15
  super(data);
16
- this.dimType.setDimType(3); // Diametric dimension
16
+ this.dimType.setBaseType(3); // Diametric dimension - preserve flags loaded from DXF group 70
17
17
  }
18
18
 
19
19
  /**
@@ -52,6 +52,34 @@ export class DiametricDimension extends CircularDimensionBase {
52
52
  return points;
53
53
  }
54
54
 
55
+ /**
56
+ * Get the grip points for interactive editing on the canvas.
57
+ * Pt10/Pt15 are the two diametrically-opposite circle attach points (NOT the centre -
58
+ * unlike RadialDimension, getPointsFromSelection() reassigns both when Pt11 moves), derived
59
+ * from Pt11 via getPointsFromSelection() - must be recomputed from the unchanged centre/radius
60
+ * (reconstructed as their midpoint/half-distance), not left stale.
61
+ * @return {Array<{point: Point, apply: Function}>} grips
62
+ */
63
+ getGrips() {
64
+ const Pt10 = this.getPointBySequence(this.points, 10); // circle attach point
65
+ const Pt15 = this.getPointBySequence(this.points, 15); // opposite circle attach point
66
+ const Pt11 = this.getPointBySequence(this.points, 11); // text position
67
+
68
+ if (!Pt10 || !Pt15 || !Pt11) {
69
+ return [];
70
+ }
71
+
72
+ const centre = Pt10.midPoint(Pt15);
73
+ const radius = centre.distance(Pt10);
74
+
75
+ const apply = (newPoint) => {
76
+ const tempEntity = { points: [centre], getRadius: () => radius };
77
+ return this.constructor.getPointsFromSelection([tempEntity], newPoint);
78
+ };
79
+
80
+ return [{ point: Pt11, apply: apply }];
81
+ }
82
+
55
83
  /**
56
84
  * Write the entity to file in the dxf format
57
85
  * @param {DXFFile} file
@@ -54,7 +54,14 @@ export class Dimension extends DimensionBase {
54
54
  });
55
55
 
56
56
  if (data) {
57
- const entity = new this.dimensionMap[DimType.getBaseType(this.dimType.getBaseDimType())](data);
57
+ const DimensionType = this.dimensionMap[DimType.getBaseType(this.dimType.getBaseDimType())];
58
+
59
+ if (!DimensionType) {
60
+ // dimType 5 (Angular 3 point) and 6 (Ordinate) are valid DXF types but not yet implemented
61
+ throw new Error(`${this.type} - ${Strings.Error.INVALIDTYPE}: ${this.dimType.getBaseDimType()}`);
62
+ }
63
+
64
+ const entity = new DimensionType(data);
58
65
 
59
66
  // find the block linked to this dimension
60
67
  const linkedBlockIndex = DesignCore.Scene.entities.find('BLOCK', 'name', data[2]);
@@ -100,6 +107,8 @@ export class Dimension extends DimensionBase {
100
107
  this.dimType.setDimType(1);
101
108
  // select a second point to define the dimension
102
109
  const op1 = new PromptOptions(Strings.Input.END, [Input.Type.POINT]);
110
+ // avoid polar/ortho tracking a line from the first point while picking the second
111
+ DesignCore.Scene.inputManager.inputPoint = null;
103
112
  const pt14 = await DesignCore.Scene.inputManager.requestInput(op1);
104
113
  if (pt14 === undefined) return;
105
114
  // Create a temporary line using the selected points
@@ -172,6 +181,8 @@ export class Dimension extends DimensionBase {
172
181
  op2 = new PromptOptions(`${Strings.Input.DIMENSION}`, [Input.Type.POINT], options);
173
182
  }
174
183
 
184
+ // avoid polar/ortho tracking a line from the last defining point while placing the dimension
185
+ DesignCore.Scene.inputManager.inputPoint = null;
175
186
  const input2 = await DesignCore.Scene.inputManager.requestInput(op2);
176
187
  if (input2 === undefined) return;
177
188
 
@@ -187,10 +198,16 @@ export class Dimension extends DimensionBase {
187
198
  const Pt14 = this.selectedEntities[0].points[1];
188
199
  const linearDimTypeNumber = this.getLinearDimensionType(Pt13, Pt14, Pt11);
189
200
  this.dimType.setDimType(linearDimTypeNumber);
201
+
202
+ // Rotated dimensions use a fixed angle - choose horizontal or vertical based on the pick point
203
+ if (linearDimTypeNumber === 0) {
204
+ this.setProperty(Property.Names.LINEARDIMANGLE, this.getRotatedDimensionAngle(Pt13, Pt14, Pt11));
205
+ }
190
206
  }
191
207
 
192
208
  const dimensionType = this.dimensionMap[this.dimType.getBaseDimType()]; // TODO: use this.dimensionMap.name?
193
- this.points.push(...dimensionType.getPointsFromSelection(this.selectedEntities, Pt11));
209
+ const angle = this.getProperty(Property.Names.LINEARDIMANGLE);
210
+ this.points.push(...dimensionType.getPointsFromSelection(this.selectedEntities, Pt11, angle));
194
211
  }
195
212
  }
196
213
 
@@ -246,7 +263,15 @@ export class Dimension extends DimensionBase {
246
263
  }
247
264
  }
248
265
 
249
- DesignCore.Scene.inputManager.executeCommand(this);
266
+ // Dimension is a router entity with no buildDimension() of its own -
267
+ // commit an instance of the resolved subtype (e.g. RotatedDimension), not this
268
+ const DimensionType = this.dimensionMap[this.dimType.getBaseDimType()];
269
+ const entity = new DimensionType({
270
+ points: this.points,
271
+ dimensionStyle: this.getProperty(Property.Names.DIMENSIONSTYLE),
272
+ linearDimAngle: this.getProperty(Property.Names.LINEARDIMANGLE),
273
+ });
274
+ DesignCore.Scene.inputManager.executeCommand(entity);
250
275
  } catch (err) {
251
276
  Logging.instance.error(`${this.type} - ${err}`);
252
277
  }
@@ -276,6 +301,28 @@ export class Dimension extends DimensionBase {
276
301
  }
277
302
  }
278
303
 
304
+ /**
305
+ * Determine the fixed dimension line angle for a Rotated dimension (horizontal or vertical)
306
+ * @param {Point} Pt13 - start point
307
+ * @param {Point} Pt14 - end point
308
+ * @param {Point} Pt11 - pick point used to choose the axis
309
+ * @return {number} angle in degrees - 0 = horizontal, 90 = vertical
310
+ */
311
+ getRotatedDimensionAngle(Pt13, Pt14, Pt11) {
312
+ const dx = Pt14.x - Pt13.x;
313
+ const dy = Pt14.y - Pt13.y;
314
+
315
+ // axis-aligned edges only have one non-degenerate orientation
316
+ if (Utils.round(dx) === 0) return 90;
317
+ if (Utils.round(dy) === 0) return 0;
318
+
319
+ // Diagonal edge: pick the axis the pick point deviates from the edge the most along
320
+ const iX = (Math.abs(Pt11.x - Pt13.x) + Math.abs(Pt14.x - Pt11.x)) - Math.abs(dx);
321
+ const iY = (Math.abs(Pt11.y - Pt13.y) + Math.abs(Pt14.y - Pt11.y)) - Math.abs(dy);
322
+
323
+ return iX >= iY ? 90 : 0;
324
+ }
325
+
279
326
  /**
280
327
  * Preview the entity during creation
281
328
  */
@@ -286,20 +333,26 @@ export class Dimension extends DimensionBase {
286
333
  const Pt11 = DesignCore.Mouse.pointOnScene();
287
334
  Pt11.sequence = 11;
288
335
 
336
+ let angle;
289
337
  if (dimTypeNumber === 0 || dimTypeNumber === 1) {
290
338
  // for linear dimensions, determine if aligned or rotated based on mouse position
291
339
  const Pt13 = this.selectedEntities[0].points[0];
292
340
  const Pt14 = this.selectedEntities[0].points[1];
293
341
  dimTypeNumber = this.getLinearDimensionType(Pt13, Pt14, Pt11);
342
+
343
+ // Rotated dimensions use a fixed angle - choose horizontal or vertical based on the pick point
344
+ if (dimTypeNumber === 0) {
345
+ angle = this.getRotatedDimensionAngle(Pt13, Pt14, Pt11);
346
+ }
294
347
  }
295
348
  // get the dimension class
296
349
  const dimensionType = this.dimensionMap[dimTypeNumber];
297
350
  // get the dimension type as a string
298
351
  const dimensionTypeString = dimensionType.register().command;
299
352
  // get the points for the dimension
300
- const points = dimensionType.getPointsFromSelection(this.selectedEntities, Pt11);
353
+ const points = dimensionType.getPointsFromSelection(this.selectedEntities, Pt11, angle);
301
354
  // create the temporary dimension
302
- DesignCore.Scene.previewEntities.create(dimensionTypeString, { points: points, dimensionStyle: this.getProperty(Property.Names.DIMENSIONSTYLE) });
355
+ DesignCore.Scene.previewEntities.create(dimensionTypeString, { points: points, dimensionStyle: this.getProperty(Property.Names.DIMENSIONSTYLE), linearDimAngle: angle });
303
356
  }
304
357
  }
305
358
  }
@@ -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)) {