@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
@@ -0,0 +1,40 @@
1
+ # Publishes @design-core/core to npm when a semver tag (e.g. v51.1.0) is pushed.
2
+ # Requires an NPM_TOKEN repository secret (an "Automation" type npm access token).
3
+ name: Publish to npm
4
+
5
+ on:
6
+ push:
7
+ tags:
8
+ - 'v*.*.*'
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+
17
+ - name: Use Node.js
18
+ uses: actions/setup-node@v3
19
+ with:
20
+ node-version: 18.x
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - run: npm ci
24
+ - run: npm test -- --coverage
25
+ - run: npx eslint . --ext .js
26
+
27
+ # Fail fast if the tag and package.json version have drifted apart
28
+ - name: Verify tag matches package.json version
29
+ run: |
30
+ PKG_VERSION=$(node -p "require('./package.json').version")
31
+ TAG_VERSION=${GITHUB_REF_NAME#v}
32
+ if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
33
+ echo "Tag v$TAG_VERSION does not match package.json version $PKG_VERSION"
34
+ exit 1
35
+ fi
36
+
37
+ - name: Publish to npm
38
+ run: npm publish --access public
39
+ env:
40
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -14,10 +14,23 @@ Design-Core uses the industry standard Drawing Exchange Format (DXF) to read and
14
14
  * Design-Web: https://github.com/dubstar-04/Design-Web
15
15
  * HTML Canvas: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
16
16
  * Cairo: https://www.cairographics.org/
17
+ * npm package: https://www.npmjs.com/package/@design-core/core
17
18
 
18
19
 
19
20
  ## Publishing
20
21
 
21
- npm login
22
+ Releases are published to npm automatically by the [Publish to npm](.github/workflows/publish.yml) workflow whenever a `v*.*.*` tag is pushed:
23
+
24
+ ```bash
25
+ npm version patch # or minor / major, bumps package.json and creates a git tag
26
+ git push --follow-tags
27
+ ```
28
+
29
+ The workflow runs the test suite and linter, verifies the tag matches `package.json`'s version, then publishes.
22
30
 
31
+ To publish manually instead:
32
+
33
+ ```bash
34
+ npm login
23
35
  npm publish --access public
36
+ ```
@@ -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
  }