@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
@@ -3,6 +3,8 @@ import { Point } from '../../core/entities/point.js';
3
3
  import { DesignCore } from '../../core/designCore.js';
4
4
  import { Core } from '../../core/core/core.js';
5
5
  import { Line } from '../../core/entities/line.js';
6
+ import { DxfIterator } from '../../core/lib/dxf/dxfIterator.js';
7
+ import { Entities } from '../../core/lib/dxf/sections/entities.js';
6
8
  import { File, withMockInput } from '../test-helpers/test-helpers.js';
7
9
 
8
10
  // initialise core
@@ -103,6 +105,154 @@ describe('AlignedDimension.getPointsFromSelection', () => {
103
105
  });
104
106
  });
105
107
 
108
+ // Regression test: dragging the Pt11 (text) grip must recompute Pt10 (the dimension line's
109
+ // far end) from the unchanged Pt13/Pt14 - previously Pt10 was left frozen at its creation-time
110
+ // value, so moving Pt11 alone produced no visible change (text falls back to the midpoint,
111
+ // and the dimension line/value are driven entirely by the stale Pt10)
112
+ test('getGrips recomputes Pt10 (not stale) when the Pt11 grip moves', () => {
113
+ const pt13 = new Point(0, 0);
114
+ const pt14 = new Point(10, 0);
115
+ const points = AlignedDimension.getPointsFromSelection([new Line({ points: [pt13, pt14] })], new Point(5, 5));
116
+ const dim = new AlignedDimension({ points });
117
+
118
+ const originalPt10 = dim.getPointBySequence(dim.points, 10);
119
+ expect(originalPt10.x).toBeCloseTo(10);
120
+ expect(originalPt10.y).toBeCloseTo(5);
121
+
122
+ const grips = dim.getGrips();
123
+ expect(grips).toHaveLength(1);
124
+ expect(grips[0].point.x).toBeCloseTo(5);
125
+ expect(grips[0].point.y).toBeCloseTo(5);
126
+
127
+ const newPoints = grips[0].apply(new Point(5, 20));
128
+ const newPt11 = newPoints.find((p) => p.sequence === 11);
129
+ const newPt10 = newPoints.find((p) => p.sequence === 10);
130
+
131
+ expect(newPt11.x).toBeCloseTo(5);
132
+ expect(newPt11.y).toBeCloseTo(20);
133
+ // Pt10 must track the moved Pt11, not stay frozen at its original position
134
+ expect(newPt10.y).not.toBeCloseTo(originalPt10.y);
135
+ expect(newPt10.x).toBeCloseTo(10);
136
+ expect(newPt10.y).toBeCloseTo(20);
137
+
138
+ // the measured value (driven by Pt13/Pt14, unchanged) must still be correct
139
+ const rebuilt = new AlignedDimension({ points: newPoints });
140
+ const text = rebuilt.buildDimension().find((e) => e.type === 'Text');
141
+ expect(Number(text.getProperty('string'))).toBeCloseTo(10);
142
+ });
143
+
144
+ // Regression test: committing a grip-dragged Pt11 (the InputManager drop path - setProperty('points', ...))
145
+ // must actually move the rendered text, not just the stored point. DIMJUST=0 (the default) hardcodes
146
+ // textPosition to the midpoint when text fits inside the extension lines, ignoring Pt11 entirely -
147
+ // unless the dimension is flagged as user-positioned (DXF bit 128), which setProperty() must now detect
148
+ test('dragging the Pt11 grip via setProperty moves the rendered text, not just the stored point', () => {
149
+ const pt13 = new Point(0, 0);
150
+ const pt14 = new Point(10, 0);
151
+ const points = AlignedDimension.getPointsFromSelection([new Line({ points: [pt13, pt14] })], new Point(5, 1));
152
+ const dim = new AlignedDimension({ points });
153
+
154
+ // sanity check: with the default DIMJUST=0 and short text fitting inside, text starts at the midpoint
155
+ const textBefore = dim.buildDimension().find((e) => e.type === 'Text');
156
+ expect(textBefore.points[0].x).toBeCloseTo(5);
157
+
158
+ expect(dim.dimType.hasUserPositionedText()).toBe(false);
159
+
160
+ const grip = dim.getGrips()[0];
161
+ const newPoints = grip.apply(new Point(5, 30));
162
+
163
+ // exactly what InputManager's drop handler does: entity.setProperty('points', newPoints)
164
+ dim.setProperty('points', newPoints);
165
+
166
+ expect(dim.dimType.hasUserPositionedText()).toBe(true);
167
+
168
+ const textAfter = dim.buildDimension().find((e) => e.type === 'Text');
169
+ expect(textAfter.points[0].x).toBeCloseTo(5);
170
+ expect(textAfter.points[0].y).toBeCloseTo(30);
171
+ });
172
+
173
+ // Regression test: DIMTAD's vertical offset must still apply on top of a user-positioned
174
+ // (grip-dragged) text anchor - user-positioning only replaces the DIMJUST auto-anchor, it
175
+ // doesn't bypass DIMTAD entirely
176
+ test('DIMTAD vertical offset still applies on top of a user-positioned text', () => {
177
+ const pt13 = new Point(0, 0);
178
+ const pt14 = new Point(10, 0);
179
+ const points = AlignedDimension.getPointsFromSelection([new Line({ points: [pt13, pt14] })], new Point(5, 5));
180
+ const dim = new AlignedDimension({ points });
181
+
182
+ const realStyle = dim.getDimensionStyle();
183
+ const DIMTXT = realStyle.getValue('DIMTXT');
184
+ const DIMGAP = realStyle.getValue('DIMGAP');
185
+ // DIMTAD = 4: places the dimension text below the dimension line
186
+ dim.getDimensionStyle = () => ({
187
+ getValue: (prop) => (prop === 'DIMTAD' ? 4 : realStyle.getValue(prop)),
188
+ });
189
+
190
+ const grip = dim.getGrips()[0];
191
+ const newPoints = grip.apply(new Point(5, 30));
192
+ dim.setProperty('points', newPoints);
193
+
194
+ expect(dim.dimType.hasUserPositionedText()).toBe(true);
195
+
196
+ const text = dim.buildDimension().find((e) => e.type === 'Text');
197
+ // anchored at the user-positioned Pt11 (x=5, y=30), then offset below by DIMTAD=4
198
+ expect(text.points[0].x).toBeCloseTo(5);
199
+ expect(text.points[0].y).toBeCloseTo(30 - (DIMTXT * 0.5 + DIMGAP));
200
+ });
201
+
202
+ // Regression test: userPositionedText (DXF group 70 bit 128) must survive a real write -> parse ->
203
+ // reconstruct DXF round-trip. AlignedDimension's constructor used to call dimType.setDimType(1) after
204
+ // super(data), which clobbered the flags super(data) had just loaded from group 70, reverting a
205
+ // grip-edited dimension back to automatic text placement when the file was reopened
206
+ test('grip-edited dimension preserves userPositionedText through a DXF round-trip', () => {
207
+ const pt13 = new Point(0, 0);
208
+ const pt14 = new Point(10, 0);
209
+ const points = AlignedDimension.getPointsFromSelection([new Line({ points: [pt13, pt14] })], new Point(5, 5));
210
+ const dim = new AlignedDimension({ points, handle: '1' });
211
+
212
+ // grip-drag Pt11 away from the DIMJUST-computed anchor
213
+ const grip = dim.getGrips()[0];
214
+ const newPoints = grip.apply(new Point(5, 30));
215
+ dim.setProperty('points', newPoints);
216
+ expect(dim.dimType.hasUserPositionedText()).toBe(true);
217
+
218
+ // write the grip-edited dimension to DXF text
219
+ const file = new File();
220
+ dim.dxf(file);
221
+
222
+ // parse the written text back through the real DXF entity reader (not an object-spread shortcut)
223
+ const iterator = new DxfIterator();
224
+ iterator.loadFile(`0\nENTITIES\n${file.contents}0\nENDSEC\n0\nEOF`);
225
+ const [rawEntity] = new Entities().read(iterator);
226
+ rawEntity.points = rawEntity.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence));
227
+
228
+ // reconstruct through the same command dispatch path used when loading a DXF file
229
+ const reloaded = DesignCore.CommandManager.createNew(rawEntity[0], rawEntity);
230
+
231
+ expect(reloaded).toBeInstanceOf(AlignedDimension);
232
+ expect(reloaded.dimType.hasUserPositionedText()).toBe(true);
233
+
234
+ const text = reloaded.buildDimension().find((e) => e.type === 'Text');
235
+ expect(text.points[0].x).toBeCloseTo(5);
236
+ expect(text.points[0].y).toBeCloseTo(30);
237
+ });
238
+
239
+ // Regression test: a rigid whole-dimension Move (all points translated together, including Pt11)
240
+ // must NOT be mistaken for a grip-drag - auto text placement should keep working normally
241
+ test('a rigid Move (all points translated together) does not set userPositionedText', () => {
242
+ const pt13 = new Point(0, 0);
243
+ const pt14 = new Point(10, 0);
244
+ const points = AlignedDimension.getPointsFromSelection([new Line({ points: [pt13, pt14] })], new Point(5, 5));
245
+ const dim = new AlignedDimension({ points });
246
+
247
+ const delta = new Point(100, 100);
248
+ const movedPoints = dim.points.map((p) => new Point(p.x + delta.x, p.y + delta.y, p.bulge, p.sequence));
249
+
250
+ dim.setProperty('points', movedPoints);
251
+
252
+ expect(dim.dimType.hasUserPositionedText()).toBe(false);
253
+ });
254
+
255
+
106
256
  test('AlignedDimension.preview runs without error and calls createTempItem', () => {
107
257
  const origAddAux = DesignCore.Scene.auxiliaryEntities.add;
108
258
  const origCreateTempItem = DesignCore.Scene.previewEntities.create;
@@ -216,3 +366,18 @@ AcDbAlignedDimension
216
366
  newDimension.dxf(file);
217
367
  expect(file.contents).toEqual(dxfString);
218
368
  });
369
+
370
+ test('snaps returns END snaps at the dimension line endpoints', () => {
371
+ const points = AlignedDimension.getPointsFromSelection([new Line({ points: [new Point(0, 0), new Point(10, 0)] })], new Point(5, 5));
372
+ const dim = new AlignedDimension({ points });
373
+ const snaps = dim.snaps(new Point(), 1);
374
+
375
+ expect(snaps.length).toBe(2);
376
+ expect(snaps.every((snap) => snap.type === 'end')).toBe(true);
377
+ expect(snaps[0].snapPoint.isSame(new Point(0, 5))).toBe(true);
378
+ expect(snaps[1].snapPoint.isSame(new Point(10, 5))).toBe(true);
379
+ });
380
+
381
+ test('snaps returns no snaps when the dimension has no points', () => {
382
+ expect(new AlignedDimension().snaps(new Point(), 1)).toEqual([]);
383
+ });
@@ -6,6 +6,7 @@ import { Polyline } from '../../core/entities/polyline.js';
6
6
  import { Core } from '../../core/core/core.js';
7
7
  import { DesignCore } from '../../core/designCore.js';
8
8
  import { SingleSelection } from '../../core/lib/selectionManager.js';
9
+ import { Utils } from '../../core/lib/utils.js';
9
10
 
10
11
  import { File, withMockInput } from '../test-helpers/test-helpers.js';
11
12
 
@@ -77,6 +78,29 @@ test.each(scenarios)('Dimension.execute handles $desc', async (scenario) => {
77
78
  }, { selectedEntities });
78
79
  });
79
80
 
81
+ // Regression test: rigidly rotating an AngularDimension (points, as the Rotate tool
82
+ // does) must not change the measured angle - buildDimension() derives everything from
83
+ // relative angles between the points, not world-axis alignment, so this should already
84
+ // be rotation-invariant (confirms it, mirroring the RotatedDimension/DiametricDimension checks)
85
+ test.each([0, 30, 45, 90, 137, 200])('measured angle is rotation-invariant at %s degrees', (rotationDeg) => {
86
+ const line1 = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
87
+ const line2 = new Line({ points: [new Point(0, 0), new Point(10, 10)] }); // 45 degree angle
88
+ const textPos = new Point(5, 5);
89
+
90
+ const points = AngularDimension.getPointsFromSelection([line1, line2], textPos);
91
+ const dim = new AngularDimension({ points });
92
+
93
+ const theta = Utils.degrees2radians(rotationDeg);
94
+ const centre = new Point(0, 0);
95
+ const rotatedPoints = dim.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(centre, theta));
96
+ const rotatedDim = new AngularDimension({ points: rotatedPoints });
97
+
98
+ const entities = rotatedDim.buildDimension();
99
+ const text = entities.find((e) => e.type === 'Text');
100
+ expect(text).toBeDefined();
101
+ expect(Number(text.getProperty('string').replace(/[^0-9.-]+/g, ''))).toBeCloseTo(45);
102
+ });
103
+
80
104
  test('constructor sets default properties', () => {
81
105
  const dim = new AngularDimension();
82
106
  expect(dim).toBeInstanceOf(AngularDimension);
@@ -99,6 +123,114 @@ test('getPointsFromSelection returns correct sequenced points', () => {
99
123
  expect(result.length).toBeGreaterThanOrEqual(4);
100
124
  });
101
125
 
126
+ // Regression test: dragging the Pt11 (text) grip must recompute Pt16 (the dimension arc's
127
+ // radial position) from the unchanged defining lines - previously Pt16 was a frozen copy of
128
+ // Pt11 taken at creation time, so the arc stayed at its original radius when only Pt11 moved
129
+ test('getGrips recomputes Pt16 (not stale) when the Pt11 grip moves', () => {
130
+ const lineOne = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
131
+ const lineTwo = new Line({ points: [new Point(0, 0), new Point(10, 10)] });
132
+ const points = AngularDimension.getPointsFromSelection([lineOne, lineTwo], new Point(5, 5));
133
+ const dim = new AngularDimension({ points });
134
+
135
+ const originalPt16 = dim.getPointBySequence(dim.points, 16);
136
+
137
+ const grips = dim.getGrips();
138
+ expect(grips).toHaveLength(1);
139
+ expect(grips[0].point.x).toBeCloseTo(5);
140
+ expect(grips[0].point.y).toBeCloseTo(5);
141
+
142
+ const newPoints = grips[0].apply(new Point(20, 20));
143
+ const newPt11 = newPoints.find((p) => p.sequence === 11);
144
+ const newPt16 = newPoints.find((p) => p.sequence === 16);
145
+
146
+ expect(newPt11.x).toBeCloseTo(20);
147
+ expect(newPt11.y).toBeCloseTo(20);
148
+ // Pt16 must track the moved Pt11 (further out along the angle), not stay at its original radius
149
+ expect(newPt16.x).not.toBeCloseTo(originalPt16.x);
150
+ expect(newPt16.y).not.toBeCloseTo(originalPt16.y);
151
+
152
+ // the measured angle (driven by the two defining lines, unchanged) must still be correct
153
+ const rebuilt = new AngularDimension({ points: newPoints });
154
+ const text = rebuilt.buildDimension().find((e) => e.type === 'Text');
155
+ expect(Number(text.getProperty('string').replace(/[^0-9.-]+/g, ''))).toBeCloseTo(45);
156
+ });
157
+
158
+ // Regression test: committing a grip-dragged Pt11 (the InputManager drop path - setProperty('points', ...))
159
+ // must actually move the rendered text, not just the stored point - the default auto-placement
160
+ // (arcMiddle) ignores Pt11 entirely unless the dimension is flagged as user-positioned (DXF bit 128)
161
+ test('dragging the Pt11 grip via setProperty moves the rendered text, not just the stored point', () => {
162
+ const lineOne = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
163
+ const lineTwo = new Line({ points: [new Point(0, 0), new Point(10, 10)] });
164
+ const points = AngularDimension.getPointsFromSelection([lineOne, lineTwo], new Point(5, 5));
165
+ const dim = new AngularDimension({ points });
166
+
167
+ expect(dim.dimType.hasUserPositionedText()).toBe(false);
168
+
169
+ const grip = dim.getGrips()[0];
170
+ const newPoints = grip.apply(new Point(20, 20));
171
+
172
+ // exactly what InputManager's drop handler does: entity.setProperty('points', newPoints)
173
+ dim.setProperty('points', newPoints);
174
+
175
+ expect(dim.dimType.hasUserPositionedText()).toBe(true);
176
+
177
+ const text = dim.buildDimension().find((e) => e.type === 'Text');
178
+ expect(text.points[0].x).toBeCloseTo(20);
179
+ expect(text.points[0].y).toBeCloseTo(20);
180
+ });
181
+
182
+ // Regression test: DIMTAD's vertical offset must still apply on top of a user-positioned
183
+ // (grip-dragged) text anchor - user-positioning only replaces the DIMJUST auto-anchor, it
184
+ // doesn't bypass DIMTAD entirely
185
+ test('DIMTAD vertical offset still applies on top of a user-positioned text', () => {
186
+ const lineOne = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
187
+ const lineTwo = new Line({ points: [new Point(0, 0), new Point(10, 10)] });
188
+ const points = AngularDimension.getPointsFromSelection([lineOne, lineTwo], new Point(5, 5));
189
+ const dim = new AngularDimension({ points });
190
+
191
+ const realStyle = dim.getDimensionStyle();
192
+ // DIMTAD = 4: places the dimension text below the dimension line
193
+ dim.getDimensionStyle = () => ({
194
+ getValue: (prop) => (prop === 'DIMTAD' ? 4 : realStyle.getValue(prop)),
195
+ });
196
+
197
+ const grip = dim.getGrips()[0];
198
+ const newPoints = grip.apply(new Point(20, 20));
199
+ dim.setProperty('points', newPoints);
200
+
201
+ expect(dim.dimType.hasUserPositionedText()).toBe(true);
202
+
203
+ const text = dim.buildDimension().find((e) => e.type === 'Text');
204
+ // DIMTAD=4 must offset the text away from the pure user-positioned anchor (20,20)
205
+ const distanceFromAnchor = text.points[0].distance(new Point(20, 20));
206
+ expect(distanceFromAnchor).toBeGreaterThan(0.01);
207
+ });
208
+
209
+ // Regression test: a grip-dragged text's rotation must follow the tangent at its new
210
+ // (dragged) position, not stay frozen at the pre-drag DIMJUST-computed rotation
211
+ test('text rotation follows the dragged position, not the pre-drag rotation', () => {
212
+ const lineOne = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
213
+ const lineTwo = new Line({ points: [new Point(0, 0), new Point(10, 10)] });
214
+ const points = AngularDimension.getPointsFromSelection([lineOne, lineTwo], new Point(5, 5));
215
+ const dim = new AngularDimension({ points });
216
+
217
+ const realStyle = dim.getDimensionStyle();
218
+ // DIMTIH/DIMTOH = 0: text follows the dimension line's direction rather than staying horizontal
219
+ dim.getDimensionStyle = () => ({
220
+ getValue: (prop) => ((prop === 'DIMTIH' || prop === 'DIMTOH') ? 0 : realStyle.getValue(prop)),
221
+ });
222
+
223
+ const grip = dim.getGrips()[0];
224
+ const dragTo = new Point(-20, 5);
225
+ const newPoints = grip.apply(dragTo);
226
+ dim.setProperty('points', newPoints);
227
+
228
+ const text = dim.buildDimension().find((e) => e.type === 'Text');
229
+ // expected rotation is the tangent at the dragged point, not the original chord-based angle
230
+ const expectedRotation = Utils.radians2degrees(dim.getTextDirection(new Point(0, 0).angle(dragTo) + Math.PI / 2)) % 360;
231
+ expect(text.getProperty('rotation')).toBeCloseTo(expectedRotation);
232
+ });
233
+
102
234
  test('AngularDimension.preview runs without error and calls createTempItem', () => {
103
235
  const origCreateTempItem = DesignCore.Scene.previewEntities.create;
104
236
  const origPointOnScene = DesignCore.Mouse.pointOnScene;
@@ -241,3 +373,20 @@ AcDb2LineAngularDimension
241
373
  });
242
374
 
243
375
 
376
+ test('snaps returns END snaps at the dimension arc end points', () => {
377
+ const lineOne = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
378
+ const lineTwo = new Line({ points: [new Point(0, 0), new Point(0, 10)] });
379
+ const points = AngularDimension.getPointsFromSelection([lineOne, lineTwo], new Point(5, 5));
380
+ const dim = new AngularDimension({ points });
381
+ const snaps = dim.snaps(new Point(), 1);
382
+ const radius = Math.hypot(5, 5);
383
+
384
+ expect(snaps.length).toBe(2);
385
+ expect(snaps.every((snap) => snap.type === 'end')).toBe(true);
386
+ expect(snaps.some((snap) => snap.snapPoint.isSame(new Point(radius, 0)))).toBe(true);
387
+ expect(snaps.some((snap) => snap.snapPoint.isSame(new Point(0, radius)))).toBe(true);
388
+ });
389
+
390
+ test('snaps returns no snaps when the dimension has no points', () => {
391
+ expect(new AngularDimension().snaps(new Point(), 1)).toEqual([]);
392
+ });
@@ -7,6 +7,7 @@ import { DesignCore } from '../../core/designCore.js';
7
7
  import { SingleSelection } from '../../core/lib/selectionManager.js';
8
8
  import { Circle } from '../../core/entities/circle.js';
9
9
  import { Polyline } from '../../core/entities/polyline.js';
10
+ import { Utils } from '../../core/lib/utils.js';
10
11
 
11
12
 
12
13
  // initialise core
@@ -20,28 +21,28 @@ const scenarios = [
20
21
  selectedEntities: [new Circle({ points: [new Point(0, 0), new Point(10, 0)] })],
21
22
  expectedDimType: 3,
22
23
  dimensionValue: 20,
23
- dimensionEntities: 6,
24
+ dimensionEntities: 7,
24
25
  },
25
26
  { desc: 'Diametric dimension from circle selection - Text inside circle',
26
27
  input: [new SingleSelection(0, new Point()), new Point(0, 0)],
27
28
  selectedEntities: [new Circle({ points: [new Point(0, 0), new Point(10, 0)] })],
28
29
  expectedDimType: 3,
29
30
  dimensionValue: 20,
30
- dimensionEntities: 5,
31
+ dimensionEntities: 8,
31
32
  },
32
33
  { desc: 'Diametric dimension from arc selection',
33
34
  input: [new SingleSelection(0, new Point()), new Point(20, 10)],
34
35
  selectedEntities: [new Arc({ points: [new Point(0, 0), new Point(20, 0), new Point(20, 20)] })],
35
36
  expectedDimType: 3,
36
37
  dimensionValue: 40,
37
- dimensionEntities: 6,
38
+ dimensionEntities: 7,
38
39
  },
39
40
  { desc: 'Diametric dimension from polyline selection',
40
41
  input: [new SingleSelection(0, new Point(30, 10)), new Point(30, 10)],
41
42
  selectedEntities: [new Polyline({ points: [new Point(0, 0), new Point(20, 0, 1), new Point(20, 20)] })],
42
43
  expectedDimType: 3,
43
44
  dimensionValue: 20,
44
- dimensionEntities: 6,
45
+ dimensionEntities: 7,
45
46
  },
46
47
  ];
47
48
 
@@ -69,6 +70,29 @@ test.each(scenarios)('DiametricDimension.execute handles $desc', async (scenario
69
70
  }, { selectedEntities });
70
71
  });
71
72
 
73
+ // Regression test: Pt11 (text position) not lying on the Pt10-Pt15 segment used to be
74
+ // snapped back onto the dimension line via a world-horizontal ray intersection, which
75
+ // silently fails (no intersection) whenever the dimension line itself is near-horizontal -
76
+ // perpendicular projection has no such degenerate case at any rotation angle
77
+ test.each([0, 30, 45, 90, 135, 180, 270])('off-segment text stays connected at %s degree dimension line rotation', (rotationDeg) => {
78
+ const centre = new Point(0, 0);
79
+ const circle = new Circle({ points: [centre, new Point(10, 0)] });
80
+ // text placed off to the side, not on the Pt10-Pt15 line
81
+ const textPos = new Point(20, 15);
82
+
83
+ const theta = Utils.degrees2radians(rotationDeg);
84
+ const points = DiametricDimension.getPointsFromSelection([circle], textPos)
85
+ .map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(centre, theta));
86
+
87
+ const dim = new DiametricDimension({ points });
88
+ const entities = dim.buildDimension();
89
+
90
+ expect(entities.length).toBeGreaterThan(0);
91
+ const text = entities.find((e) => e.type === 'Text');
92
+ expect(text).toBeDefined();
93
+ expect(Number(text.getProperty('string').replace(/[^0-9.-]+/g, ''))).toBeCloseTo(20);
94
+ });
95
+
72
96
  test('constructor sets default properties', () => {
73
97
  const dim = new DiametricDimension();
74
98
  expect(dim).toBeInstanceOf(DiametricDimension);
@@ -89,6 +113,39 @@ test('getPointsFromSelection returns correct sequenced points', () => {
89
113
  expect(result[2].sequence).toBe(11); // text position
90
114
  });
91
115
 
116
+ // Regression test: dragging the Pt11 (text) grip must recompute Pt10/Pt15 (the circle attach
117
+ // points) from the unchanged centre/radius - previously they were left frozen at their
118
+ // creation-time angle, so the leader line tracked the new Pt11 but attached to a stale point
119
+ test('getGrips recomputes Pt10/Pt15 (not stale) when the Pt11 grip moves', () => {
120
+ const centre = new Point(0, 0);
121
+ const item = { points: [centre], getRadius: () => 10 };
122
+ const points = DiametricDimension.getPointsFromSelection([item], new Point(10, 0));
123
+ const dim = new DiametricDimension({ points });
124
+
125
+ const originalPt15 = dim.getPointBySequence(dim.points, 15);
126
+
127
+ const grips = dim.getGrips();
128
+ expect(grips).toHaveLength(1);
129
+ expect(grips[0].point.x).toBeCloseTo(10);
130
+ expect(grips[0].point.y).toBeCloseTo(0);
131
+
132
+ const newPoints = grips[0].apply(new Point(0, -10));
133
+ const newPt11 = newPoints.find((p) => p.sequence === 11);
134
+ const newPt15 = newPoints.find((p) => p.sequence === 15);
135
+
136
+ expect(newPt11.x).toBeCloseTo(0);
137
+ expect(newPt11.y).toBeCloseTo(-10);
138
+ // Pt15 must track the moved Pt11 around the circle, not stay frozen at its original angle
139
+ expect(newPt15.x).not.toBeCloseTo(originalPt15.x);
140
+ expect(newPt15.y).not.toBeCloseTo(originalPt15.y);
141
+ expect(centre.distance(newPt15)).toBeCloseTo(10);
142
+
143
+ // the measured value (the diameter, driven by radius, unchanged) must still be correct
144
+ const rebuilt = new DiametricDimension({ points: newPoints });
145
+ const text = rebuilt.buildDimension().find((e) => e.type === 'Text');
146
+ expect(Number(text.getProperty('string').replace(/[^0-9.-]+/g, ''))).toBeCloseTo(20);
147
+ });
148
+
92
149
  test('DiametricDimension.preview runs without error and calls createTempItem', () => {
93
150
  const origCreateTempItem = DesignCore.Scene.previewEntities.create;
94
151
  const origPointOnScene = DesignCore.Mouse.pointOnScene;
@@ -201,3 +258,18 @@ AcDbDiametricDimension
201
258
  newDimension.dxf(file);
202
259
  expect(file.contents).toEqual(dxfString);
203
260
  });
261
+
262
+ test('inside text keeps the Pt15 arrow connected to the text and adds an opposite segment', () => {
263
+ const dim = new DiametricDimension({
264
+ points: [new Point(-10, 0, 0, 10), new Point(10, 0, 0, 15), new Point(2, 0, 0, 11)],
265
+ });
266
+ const lines = dim.buildDimension().filter((e) => e.type === 'Line');
267
+ const segments = lines.map((l) => l.points.map((p) => p.x).sort((a, b) => a - b));
268
+
269
+ // Pt15 to text position
270
+ expect(segments.some(([a, b]) => a === 2 && b === 10)).toBe(true);
271
+ // opposite side segment ends at Pt10 and leaves a gap for the text
272
+ const toPt10 = segments.find(([a]) => a === -10);
273
+ expect(toPt10).toBeDefined();
274
+ expect(toPt10[1]).toBeLessThan(2);
275
+ });
@@ -92,6 +92,136 @@ test('get linear dimension type', () => {
92
92
  expect(dim.getLinearDimensionType(new Point(0, 0), new Point(10, 10), new Point(12.5, 8.5))).toBe(0);
93
93
  });
94
94
 
95
+ test('get rotated dimension angle', () => {
96
+ const dim = new Dimension();
97
+ // vertical edge - only a vertical dimension is non-degenerate
98
+ expect(dim.getRotatedDimensionAngle(new Point(0, 0), new Point(0, 100), new Point(50, 50))).toBe(90);
99
+ // horizontal edge - only a horizontal dimension is non-degenerate
100
+ expect(dim.getRotatedDimensionAngle(new Point(0, 0), new Point(100, 0), new Point(50, 50))).toBe(0);
101
+ // diagonal edge, pick point deviates mostly in x - vertical dimension
102
+ expect(dim.getRotatedDimensionAngle(new Point(0, 0), new Point(100, 100), new Point(300, 50))).toBe(90);
103
+ // diagonal edge, pick point deviates mostly in y - horizontal dimension
104
+ expect(dim.getRotatedDimensionAngle(new Point(0, 0), new Point(100, 100), new Point(50, 300))).toBe(0);
105
+ });
106
+
107
+ test.each([
108
+ { desc: 'vertical', line: new Line({ points: [new Point(0, 0), new Point(0, 100)] }), textPos: new Point(50, 50), expectedAngle: 90 },
109
+ { desc: 'horizontal', line: new Line({ points: [new Point(0, 0), new Point(100, 0)] }), textPos: new Point(50, 50), expectedAngle: 0 },
110
+ ])('Dimension.execute commits a non-degenerate Rotated dimension for a $desc edge', async (scenario) => {
111
+ const { line, textPos, expectedAngle } = scenario;
112
+ let committed;
113
+
114
+ await withMockInput(DesignCore.Scene, [new SingleSelection(0, new Point()), textPos], async () => {
115
+ const dim = new Dimension();
116
+ await dim.execute();
117
+ }, {
118
+ selectedEntities: [line],
119
+ extraMethods: { executeCommand: (entity) => {
120
+ committed = entity;
121
+ } },
122
+ });
123
+
124
+ expect(committed.getProperty('linearDimAngle')).toBe(expectedAngle);
125
+ const box = committed.boundingBox();
126
+ expect(box.xLength).toBeGreaterThan(0);
127
+ expect(box.yLength).toBeGreaterThan(0);
128
+ });
129
+
130
+ test.each([
131
+ { desc: 'pick far to the right picks a vertical dimension', line: new Line({ points: [new Point(0, 0), new Point(100, 100)] }), textPos: new Point(300, 50), expectedAngle: 90 },
132
+ { desc: 'pick far above picks a horizontal dimension', line: new Line({ points: [new Point(0, 0), new Point(100, 100)] }), textPos: new Point(50, 300), expectedAngle: 0 },
133
+ ])('Dimension.execute allows $desc for a diagonal edge', async (scenario) => {
134
+ const { line, textPos, expectedAngle } = scenario;
135
+ let committed;
136
+
137
+ await withMockInput(DesignCore.Scene, [new SingleSelection(0, new Point()), textPos], async () => {
138
+ const dim = new Dimension();
139
+ await dim.execute();
140
+ }, {
141
+ selectedEntities: [line],
142
+ extraMethods: { executeCommand: (entity) => {
143
+ committed = entity;
144
+ } },
145
+ });
146
+
147
+ expect(committed.dimType.getBaseDimType()).toBe(0);
148
+ expect(committed.getProperty('linearDimAngle')).toBe(expectedAngle);
149
+ const box = committed.boundingBox();
150
+ expect(box.xLength).toBeGreaterThan(0);
151
+ expect(box.yLength).toBeGreaterThan(0);
152
+ });
153
+
154
+ test('Dimension.execute still allows an Aligned dimension for a diagonal edge', async () => {
155
+ const line = new Line({ points: [new Point(0, 0), new Point(100, 100)] });
156
+ let committed;
157
+
158
+ await withMockInput(DesignCore.Scene, [new SingleSelection(0, new Point()), new Point(60, 40)], async () => {
159
+ const dim = new Dimension();
160
+ await dim.execute();
161
+ }, {
162
+ selectedEntities: [line],
163
+ extraMethods: { executeCommand: (entity) => {
164
+ committed = entity;
165
+ } },
166
+ });
167
+
168
+ expect(committed.dimType.getBaseDimType()).toBe(1);
169
+ });
170
+
171
+ test('Dimension.execute does not show a polar tracking line while picking pt14 or the text position', async () => {
172
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
173
+ const hoverAt = (point) => {
174
+ DesignCore.Mouse.setPosFromScenePoint(point);
175
+ DesignCore.Canvas.mouseMoved();
176
+ };
177
+ const clickAt = (point) => {
178
+ DesignCore.Mouse.setPosFromScenePoint(point);
179
+ DesignCore.Canvas.mouseDown(0);
180
+ DesignCore.Canvas.mouseUp(0);
181
+ };
182
+
183
+ const prevEndSnap = DesignCore.Settings.endsnap;
184
+ const prevMidSnap = DesignCore.Settings.midsnap;
185
+ const prevCentreSnap = DesignCore.Settings.centresnap;
186
+ const prevPolar = DesignCore.Settings.polar;
187
+ DesignCore.Settings.endsnap = false;
188
+ DesignCore.Settings.midsnap = false;
189
+ DesignCore.Settings.centresnap = false;
190
+ DesignCore.Settings.polar = true;
191
+
192
+ try {
193
+ const dim = new Dimension();
194
+ DesignCore.Scene.inputManager.activeCommand = dim;
195
+ const execPromise = dim.execute();
196
+ await flush();
197
+
198
+ // pick pt13 directly (point path, not selecting an existing entity)
199
+ clickAt(new Point(0, 0));
200
+ await flush();
201
+
202
+ // hover to pick pt14 - should not draw a polar tracking line from pt13
203
+ hoverAt(new Point(100, 1));
204
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
205
+
206
+ clickAt(new Point(100, 0));
207
+ await flush();
208
+
209
+ // hover to pick the text position - should not draw a polar tracking line from pt14
210
+ hoverAt(new Point(50, 50));
211
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
212
+
213
+ clickAt(new Point(50, 50));
214
+ await flush();
215
+ await execPromise;
216
+ } finally {
217
+ DesignCore.Settings.endsnap = prevEndSnap;
218
+ DesignCore.Settings.midsnap = prevMidSnap;
219
+ DesignCore.Settings.centresnap = prevCentreSnap;
220
+ DesignCore.Settings.polar = prevPolar;
221
+ DesignCore.Scene.inputManager.reset();
222
+ }
223
+ });
224
+
95
225
 
96
226
  test('preview calls createTempItem with correct args', () => {
97
227
  const dim = new Dimension({ 70: 1 });