@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
@@ -7,10 +7,12 @@ 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
13
14
  new Core();
15
+ DesignCore.DimStyleManager.setCstyle('STANDARD');
14
16
 
15
17
  // Test cases for user input
16
18
  const scenarios = [
@@ -20,28 +22,28 @@ const scenarios = [
20
22
  selectedEntities: [new Circle({ points: [new Point(0, 0), new Point(10, 0)] })],
21
23
  expectedDimType: 3,
22
24
  dimensionValue: 20,
23
- dimensionEntities: 6,
25
+ dimensionEntities: 7,
24
26
  },
25
27
  { desc: 'Diametric dimension from circle selection - Text inside circle',
26
28
  input: [new SingleSelection(0, new Point()), new Point(0, 0)],
27
29
  selectedEntities: [new Circle({ points: [new Point(0, 0), new Point(10, 0)] })],
28
30
  expectedDimType: 3,
29
31
  dimensionValue: 20,
30
- dimensionEntities: 5,
32
+ dimensionEntities: 8,
31
33
  },
32
34
  { desc: 'Diametric dimension from arc selection',
33
35
  input: [new SingleSelection(0, new Point()), new Point(20, 10)],
34
36
  selectedEntities: [new Arc({ points: [new Point(0, 0), new Point(20, 0), new Point(20, 20)] })],
35
37
  expectedDimType: 3,
36
38
  dimensionValue: 40,
37
- dimensionEntities: 6,
39
+ dimensionEntities: 7,
38
40
  },
39
41
  { desc: 'Diametric dimension from polyline selection',
40
42
  input: [new SingleSelection(0, new Point(30, 10)), new Point(30, 10)],
41
43
  selectedEntities: [new Polyline({ points: [new Point(0, 0), new Point(20, 0, 1), new Point(20, 20)] })],
42
44
  expectedDimType: 3,
43
45
  dimensionValue: 20,
44
- dimensionEntities: 6,
46
+ dimensionEntities: 7,
45
47
  },
46
48
  ];
47
49
 
@@ -69,6 +71,29 @@ test.each(scenarios)('DiametricDimension.execute handles $desc', async (scenario
69
71
  }, { selectedEntities });
70
72
  });
71
73
 
74
+ // Regression test: Pt11 (text position) not lying on the Pt10-Pt15 segment used to be
75
+ // snapped back onto the dimension line via a world-horizontal ray intersection, which
76
+ // silently fails (no intersection) whenever the dimension line itself is near-horizontal -
77
+ // perpendicular projection has no such degenerate case at any rotation angle
78
+ test.each([0, 30, 45, 90, 135, 180, 270])('off-segment text stays connected at %s degree dimension line rotation', (rotationDeg) => {
79
+ const centre = new Point(0, 0);
80
+ const circle = new Circle({ points: [centre, new Point(10, 0)] });
81
+ // text placed off to the side, not on the Pt10-Pt15 line
82
+ const textPos = new Point(20, 15);
83
+
84
+ const theta = Utils.degrees2radians(rotationDeg);
85
+ const points = DiametricDimension.getPointsFromSelection([circle], textPos)
86
+ .map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(centre, theta));
87
+
88
+ const dim = new DiametricDimension({ points });
89
+ const entities = dim.buildDimension();
90
+
91
+ expect(entities.length).toBeGreaterThan(0);
92
+ const text = entities.find((e) => e.type === 'Text');
93
+ expect(text).toBeDefined();
94
+ expect(Number(text.getProperty('string').replace(/[^0-9.-]+/g, ''))).toBeCloseTo(20);
95
+ });
96
+
72
97
  test('constructor sets default properties', () => {
73
98
  const dim = new DiametricDimension();
74
99
  expect(dim).toBeInstanceOf(DiametricDimension);
@@ -89,6 +114,39 @@ test('getPointsFromSelection returns correct sequenced points', () => {
89
114
  expect(result[2].sequence).toBe(11); // text position
90
115
  });
91
116
 
117
+ // Regression test: dragging the Pt11 (text) grip must recompute Pt10/Pt15 (the circle attach
118
+ // points) from the unchanged centre/radius - previously they were left frozen at their
119
+ // creation-time angle, so the leader line tracked the new Pt11 but attached to a stale point
120
+ test('getGrips recomputes Pt10/Pt15 (not stale) when the Pt11 grip moves', () => {
121
+ const centre = new Point(0, 0);
122
+ const item = { points: [centre], getRadius: () => 10 };
123
+ const points = DiametricDimension.getPointsFromSelection([item], new Point(10, 0));
124
+ const dim = new DiametricDimension({ points });
125
+
126
+ const originalPt15 = dim.getPointBySequence(dim.points, 15);
127
+
128
+ const grips = dim.getGrips();
129
+ expect(grips).toHaveLength(1);
130
+ expect(grips[0].point.x).toBeCloseTo(10);
131
+ expect(grips[0].point.y).toBeCloseTo(0);
132
+
133
+ const newPoints = grips[0].apply(new Point(0, -10));
134
+ const newPt11 = newPoints.find((p) => p.sequence === 11);
135
+ const newPt15 = newPoints.find((p) => p.sequence === 15);
136
+
137
+ expect(newPt11.x).toBeCloseTo(0);
138
+ expect(newPt11.y).toBeCloseTo(-10);
139
+ // Pt15 must track the moved Pt11 around the circle, not stay frozen at its original angle
140
+ expect(newPt15.x).not.toBeCloseTo(originalPt15.x);
141
+ expect(newPt15.y).not.toBeCloseTo(originalPt15.y);
142
+ expect(centre.distance(newPt15)).toBeCloseTo(10);
143
+
144
+ // the measured value (the diameter, driven by radius, unchanged) must still be correct
145
+ const rebuilt = new DiametricDimension({ points: newPoints });
146
+ const text = rebuilt.buildDimension().find((e) => e.type === 'Text');
147
+ expect(Number(text.getProperty('string').replace(/[^0-9.-]+/g, ''))).toBeCloseTo(20);
148
+ });
149
+
92
150
  test('DiametricDimension.preview runs without error and calls createTempItem', () => {
93
151
  const origCreateTempItem = DesignCore.Scene.previewEntities.create;
94
152
  const origPointOnScene = DesignCore.Mouse.pointOnScene;
@@ -201,3 +259,18 @@ AcDbDiametricDimension
201
259
  newDimension.dxf(file);
202
260
  expect(file.contents).toEqual(dxfString);
203
261
  });
262
+
263
+ test('inside text keeps the Pt15 arrow connected to the text and adds an opposite segment', () => {
264
+ const dim = new DiametricDimension({
265
+ points: [new Point(-10, 0, 0, 10), new Point(10, 0, 0, 15), new Point(2, 0, 0, 11)],
266
+ });
267
+ const lines = dim.buildDimension().filter((e) => e.type === 'Line');
268
+ const segments = lines.map((l) => l.points.map((p) => p.x).sort((a, b) => a - b));
269
+
270
+ // Pt15 to text position
271
+ expect(segments.some(([a, b]) => a === 2 && b === 10)).toBe(true);
272
+ // opposite side segment ends at Pt10 and leaves a gap for the text
273
+ const toPt10 = segments.find(([a]) => a === -10);
274
+ expect(toPt10).toBeDefined();
275
+ expect(toPt10[1]).toBeLessThan(2);
276
+ });
@@ -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 });
@@ -219,26 +219,119 @@ describe('DimensionBase', () => {
219
219
  expect(baseDim.snaps(new Point(0, 0), 1)).toEqual([]);
220
220
  });
221
221
 
222
+ describe('getGrips', () => {
223
+ test('returns no grips when there is no sequence 11 point', () => {
224
+ baseDim.points = [Object.assign(new Point(1, 2), { sequence: 13 })];
225
+ expect(baseDim.getGrips()).toEqual([]);
226
+ });
227
+
228
+ test('returns the text position (Pt11) grip', () => {
229
+ baseDim.points = [
230
+ Object.assign(new Point(1, 2), { sequence: 13 }),
231
+ Object.assign(new Point(5, 6), { sequence: 11 }),
232
+ ];
233
+ const grips = baseDim.getGrips();
234
+ expect(grips.length).toBe(1);
235
+ expect(grips[0].point.x).toBe(5);
236
+ expect(grips[0].point.y).toBe(6);
237
+ });
238
+
239
+ test('apply() replaces only the Pt11 point, preserving others and the sequence tag', () => {
240
+ baseDim.points = [
241
+ Object.assign(new Point(1, 2), { sequence: 13 }),
242
+ Object.assign(new Point(5, 6), { sequence: 11 }),
243
+ ];
244
+ const grip = baseDim.getGrips()[0];
245
+ const newPoints = grip.apply(new Point(9, 9));
246
+
247
+ expect(newPoints.length).toBe(2);
248
+ expect(newPoints[0].x).toBe(1);
249
+ expect(newPoints[0].y).toBe(2);
250
+ expect(newPoints[0].sequence).toBe(13);
251
+ expect(newPoints[1].x).toBe(9);
252
+ expect(newPoints[1].y).toBe(9);
253
+ expect(newPoints[1].sequence).toBe(11);
254
+ });
255
+ });
256
+
222
257
  test('closestPoint delegates to block', () => {
223
- baseDim.block = { closestPoint: () => 'foo' };
258
+ baseDim.block = { entities: ['dummy'], closestPoint: () => 'foo' };
224
259
  expect(baseDim.closestPoint(new Point(0, 0))).toBe('foo');
225
260
  });
226
261
 
227
262
  test('boundingBox delegates to block', () => {
228
- baseDim.block = { boundingBox: () => 'box' };
263
+ baseDim.block = { entities: ['dummy'], boundingBox: () => 'box' };
229
264
  expect(baseDim.boundingBox()).toBe('box');
230
265
  });
231
266
 
232
267
  test('within delegates to block', () => {
233
- baseDim.block = { within: () => true };
268
+ baseDim.block = { entities: ['dummy'], within: () => true };
234
269
  expect(baseDim.within({})).toBe(true);
235
270
  });
236
271
 
237
272
  test('touched delegates to block', () => {
238
- baseDim.block = { touched: () => false };
273
+ baseDim.block = { entities: ['dummy'], touched: () => false };
239
274
  expect(baseDim.touched({})).toBe(false);
240
275
  });
241
276
 
277
+ /**
278
+ * Return a DimensionBase with a stubbed buildDimension() that produces one Line,
279
+ * with an empty (unrefreshed) block - mirrors a freshly created dimension that
280
+ * hasn't been drawn yet.
281
+ * @return {DimensionBase}
282
+ */
283
+ function makeUnrefreshedDim() {
284
+ const dim = new DimensionBase({});
285
+ dim.buildDimension = jest.fn(() => {
286
+ return [new Line({ points: [new Point(0, 0), new Point(10, 0)] })];
287
+ });
288
+ return dim;
289
+ }
290
+
291
+ test('boundingBox refreshes an empty block before delegating (fixes crash on unpainted dimension)', () => {
292
+ const dim = makeUnrefreshedDim();
293
+ expect(dim.block.entities).toHaveLength(0);
294
+ const box = dim.boundingBox();
295
+ expect(dim.buildDimension).toHaveBeenCalledTimes(1);
296
+ expect(box).toBeDefined();
297
+ });
298
+
299
+ test('within refreshes an empty block before delegating (fixes window-select crash before first draw)', () => {
300
+ const dim = makeUnrefreshedDim();
301
+ expect(dim.block.entities).toHaveLength(0);
302
+ const result = dim.within({ min: new Point(-100, -100), max: new Point(100, 100) });
303
+ expect(dim.buildDimension).toHaveBeenCalledTimes(1);
304
+ expect(result).toBe(true);
305
+ });
306
+
307
+ test('touched refreshes an empty block before delegating', () => {
308
+ const dim = makeUnrefreshedDim();
309
+ expect(dim.block.entities).toHaveLength(0);
310
+ dim.touched({ min: new Point(-100, -100), max: new Point(100, 100) });
311
+ expect(dim.buildDimension).toHaveBeenCalledTimes(1);
312
+ });
313
+
314
+ test('repeated delegate calls only refresh once the block is populated', () => {
315
+ const dim = makeUnrefreshedDim();
316
+ dim.boundingBox();
317
+ dim.within({ min: new Point(-100, -100), max: new Point(100, 100) });
318
+ dim.touched({ min: new Point(-100, -100), max: new Point(100, 100) });
319
+ dim.closestPoint(new Point(0, 0));
320
+ dim.draw({});
321
+ expect(dim.buildDimension).toHaveBeenCalledTimes(1);
322
+ });
323
+
324
+ test('setProperty("points", ...) refreshes geometry (fixes Rotate tool not updating dimension display)', () => {
325
+ const dim = makeUnrefreshedDim();
326
+ dim.draw({}); // populate block.entities once so we can detect a rebuild
327
+ const before = dim.block.entities;
328
+
329
+ dim.setProperty('points', [new Point(5, 5), new Point(15, 5)]);
330
+
331
+ expect(dim.buildDimension).toHaveBeenCalledTimes(2);
332
+ expect(dim.block.entities).not.toBe(before);
333
+ });
334
+
242
335
  describe('draw()', () => {
243
336
  /**
244
337
  * Return a DimensionBase with a stubbed buildDimension() that produces one Line.
@@ -11,6 +11,7 @@ import { Polyline } from '../../core/entities/polyline.js';
11
11
 
12
12
  // initialise core
13
13
  new Core();
14
+ DesignCore.DimStyleManager.setCstyle('STANDARD');
14
15
 
15
16
  // Test cases for user input
16
17
  const scenarios = [
@@ -89,6 +90,39 @@ test('getPointsFromSelection returns correct sequenced points', () => {
89
90
  expect(result[2].sequence).toBe(11); // text position
90
91
  });
91
92
 
93
+ // Regression test: dragging the Pt11 (text) grip must recompute Pt15 (the circle attach
94
+ // point) from the unchanged centre/radius - previously it was left frozen at its
95
+ // creation-time angle, so the leader line tracked the new Pt11 but attached to a stale point
96
+ test('getGrips recomputes Pt15 (not stale) when the Pt11 grip moves', () => {
97
+ const centre = new Point(0, 0);
98
+ const item = { points: [centre], getRadius: () => 10 };
99
+ const points = RadialDimension.getPointsFromSelection([item], new Point(10, 0));
100
+ const dim = new RadialDimension({ points });
101
+
102
+ const originalPt15 = dim.getPointBySequence(dim.points, 15);
103
+
104
+ const grips = dim.getGrips();
105
+ expect(grips).toHaveLength(1);
106
+ expect(grips[0].point.x).toBeCloseTo(10);
107
+ expect(grips[0].point.y).toBeCloseTo(0);
108
+
109
+ const newPoints = grips[0].apply(new Point(0, -10));
110
+ const newPt11 = newPoints.find((p) => p.sequence === 11);
111
+ const newPt15 = newPoints.find((p) => p.sequence === 15);
112
+
113
+ expect(newPt11.x).toBeCloseTo(0);
114
+ expect(newPt11.y).toBeCloseTo(-10);
115
+ // Pt15 must track the moved Pt11 around the circle, not stay frozen at its original angle
116
+ expect(newPt15.x).not.toBeCloseTo(originalPt15.x);
117
+ expect(newPt15.y).not.toBeCloseTo(originalPt15.y);
118
+ expect(centre.distance(newPt15)).toBeCloseTo(10);
119
+
120
+ // the measured value (the radius, unchanged) must still be correct
121
+ const rebuilt = new RadialDimension({ points: newPoints });
122
+ const text = rebuilt.buildDimension().find((e) => e.type === 'Text');
123
+ expect(Number(text.getProperty('string').replace(/[^0-9.-]+/g, ''))).toBeCloseTo(10);
124
+ });
125
+
92
126
  test('RadialDimension.preview runs without error and calls createTempItem', () => {
93
127
  const origCreateTempItem = DesignCore.Scene.previewEntities.create;
94
128
  const origPointOnScene = DesignCore.Mouse.pointOnScene;
@@ -3,6 +3,7 @@ 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 { Utils } from '../../core/lib/utils.js';
6
7
  import { File, withMockInput } from '../test-helpers/test-helpers.js';
7
8
 
8
9
  // initialise core
@@ -229,3 +230,82 @@ AcDbRotatedDimension
229
230
  newDimension.dxf(file);
230
231
  expect(file.contents).toEqual(dxfString);
231
232
  });
233
+
234
+ // Regression test: rigidly rotating a RotatedDimension (points + linearDimAngle, as the
235
+ // Rotate tool does) must not change the measured value - the dimension line direction is
236
+ // a fixed angle relative to the geometry, not derived from world-axis alignment
237
+ test.each([0, 30, 45, 90, 137, 200])('measured value is rotation-invariant at %s degrees', (rotationDeg) => {
238
+ const pt13 = new Point(0, 0);
239
+ const pt14 = new Point(30, 40); // 3-4-5 triangle x10, diagonal edge
240
+ const pt11 = new Point(60, 20);
241
+
242
+ const angleDeg = 90; // vertical dimension line - measures the Y-extent (40)
243
+ const points = RotatedDimension.getPointsFromSelection([new Line({ points: [pt13, pt14] })], pt11, angleDeg);
244
+ const dim = new RotatedDimension({ points, linearDimAngle: angleDeg });
245
+
246
+ const theta = Utils.degrees2radians(rotationDeg);
247
+ const centre = new Point(0, 0);
248
+ const rotatedPoints = dim.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(centre, theta));
249
+ const rotated = new RotatedDimension({ points: rotatedPoints, linearDimAngle: angleDeg + rotationDeg });
250
+
251
+ const entities = rotated.buildDimension();
252
+ const text = entities.find((e) => e.type === 'Text');
253
+ expect(Number(text.getProperty('string'))).toBeCloseTo(40);
254
+ });
255
+
256
+ // Regression test: the Rotate tool only calls entity.setProperty('points', rotatedPoints) -
257
+ // it never touches linearDimAngle directly, so DimensionBase.setProperty() must detect
258
+ // the implied rotation from the points themselves and keep linearDimAngle in sync
259
+ test.each([30, 45, 90, 137, 200])('setProperty(points) alone keeps the measured value correct after a %s degree rotation', (rotationDeg) => {
260
+ const pt13 = new Point(0, 0);
261
+ const pt14 = new Point(30, 40);
262
+ const pt11 = new Point(60, 20);
263
+
264
+ const angleDeg = 90;
265
+ const points = RotatedDimension.getPointsFromSelection([new Line({ points: [pt13, pt14] })], pt11, angleDeg);
266
+ const dim = new RotatedDimension({ points, linearDimAngle: angleDeg });
267
+
268
+ const theta = Utils.degrees2radians(rotationDeg);
269
+ const centre = new Point(0, 0);
270
+ const rotatedPoints = dim.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(centre, theta));
271
+
272
+ // exactly what Rotate.action()/preview() call - nothing else
273
+ dim.setProperty('points', rotatedPoints);
274
+
275
+ expect(dim.getProperty('linearDimAngle')).toBeCloseTo(angleDeg + rotationDeg);
276
+ const text = dim.buildDimension().find((e) => e.type === 'Text');
277
+ expect(Number(text.getProperty('string'))).toBeCloseTo(40);
278
+ });
279
+
280
+ // Regression test: dragging the Pt11 (text) grip must recompute Pt10 (the dimension line's
281
+ // far end) from the unchanged Pt13/Pt14 - Pt10 is derived from Pt11 at creation time via
282
+ // getPointsFromSelection() and must not be left stale when only Pt11 moves
283
+ test('getGrips recomputes Pt10 (not stale) when the Pt11 grip moves', () => {
284
+ const pt13 = new Point(0, 0);
285
+ const pt14 = new Point(10, 0);
286
+ const points = RotatedDimension.getPointsFromSelection([new Line({ points: [pt13, pt14] })], new Point(5, 5), 0);
287
+ const dim = new RotatedDimension({ points, linearDimAngle: 0 });
288
+
289
+ const originalPt10 = dim.getPointBySequence(dim.points, 10);
290
+
291
+ const grips = dim.getGrips();
292
+ expect(grips).toHaveLength(1);
293
+ expect(grips[0].point.x).toBeCloseTo(5);
294
+ expect(grips[0].point.y).toBeCloseTo(5);
295
+
296
+ const newPoints = grips[0].apply(new Point(5, 20));
297
+ const newPt11 = newPoints.find((p) => p.sequence === 11);
298
+ const newPt10 = newPoints.find((p) => p.sequence === 10);
299
+
300
+ expect(newPt11.x).toBeCloseTo(5);
301
+ expect(newPt11.y).toBeCloseTo(20);
302
+ // Pt10 must track the moved Pt11, not stay frozen at its original position
303
+ expect(newPt10.y).not.toBeCloseTo(originalPt10.y);
304
+ expect(newPt10.y).toBeCloseTo(20);
305
+
306
+ // the measured value (driven by Pt13/Pt14, unchanged) must still be correct
307
+ const rebuilt = new RotatedDimension({ points: newPoints, linearDimAngle: 0 });
308
+ const text = rebuilt.buildDimension().find((e) => e.type === 'Text');
309
+ expect(Number(text.getProperty('string'))).toBeCloseTo(10);
310
+ });
311
+
@@ -545,9 +545,7 @@ CloseFilled
545
545
  289
546
546
  3
547
547
  340
548
- Standard
549
- 341
550
- ClosedFilled
548
+ 19
551
549
  371
552
550
  -2
553
551
  372
@@ -683,9 +681,7 @@ CloseFilled
683
681
  289
684
682
  3
685
683
  340
686
- Standard
687
- 341
688
- ClosedFilled
684
+ 19
689
685
  371
690
686
  -2
691
687
  372
@@ -821,9 +817,7 @@ CloseFilled
821
817
  289
822
818
  3
823
819
  340
824
- Standard
825
- 341
826
- ClosedFilled
820
+ 19
827
821
  371
828
822
  -2
829
823
  372
@@ -24,6 +24,7 @@ let core;
24
24
 
25
25
  beforeAll(() => {
26
26
  core = new Core();
27
+ core.dimStyleManager.setCstyle('STANDARD');
27
28
 
28
29
  // Entities are arranged in a 3x3 grid of 100x100 cells:
29
30
  //
@@ -405,9 +405,7 @@ CloseFilled
405
405
  289
406
406
  3
407
407
  340
408
- Standard
409
- 341
410
- ClosedFilled
408
+ 19
411
409
  371
412
410
  -2
413
411
  372
@@ -543,9 +541,7 @@ CloseFilled
543
541
  289
544
542
  3
545
543
  340
546
- Standard
547
- 341
548
- ClosedFilled
544
+ 19
549
545
  371
550
546
  -2
551
547
  372
@@ -681,9 +677,7 @@ CloseFilled
681
677
  289
682
678
  3
683
679
  340
684
- Standard
685
- 341
686
- ClosedFilled
680
+ 19
687
681
  371
688
682
  -2
689
683
  372
@@ -95,6 +95,7 @@ async function executeToolWithInputs(tool, selectionIndices, inputs) {
95
95
 
96
96
  beforeAll(async () => {
97
97
  core = new Core();
98
+ core.dimStyleManager.setCstyle('STANDARD');
98
99
 
99
100
  // =====================================================================
100
101
  // Phase 1: Showcase entities (idx 0–7)