@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.
- package/core/dimensions/alignedDimension.js +3 -1
- package/core/dimensions/angularDimension.js +118 -36
- package/core/dimensions/circularDimensionBase.js +32 -12
- package/core/dimensions/diametricDimension.js +29 -1
- package/core/dimensions/dimension.js +58 -5
- package/core/dimensions/dimensionBase.js +107 -4
- package/core/dimensions/linearDimensionBase.js +75 -37
- package/core/dimensions/radialDimension.js +26 -1
- package/core/dimensions/rotatedDimension.js +6 -3
- package/core/entities/entity.js +9 -0
- package/core/lib/auxiliary/snapPoint.js +9 -0
- package/core/lib/canvas.js +22 -0
- package/core/lib/commandManager.js +4 -4
- package/core/lib/dxf/dxf.js +8 -2
- package/core/lib/inputManager.js +23 -3
- package/core/lib/renderers/pdfRenderer.js +17 -1
- package/core/lib/scene.js +2 -0
- package/core/lib/selectionManager.js +28 -0
- package/core/lib/strings.js +1 -0
- package/core/properties/dimType.js +14 -0
- package/core/properties/property.js +1 -0
- package/core/tables/dimStyle.js +32 -2
- package/core/tables/dimStyleManager.js +2 -0
- package/core/tables/styleManagerBase.js +17 -3
- package/core/tables/tableManagerBase.js +24 -2
- package/core/tools/gripStretch.js +97 -0
- package/core/tools/offset.js +7 -1
- package/core/tools/tool.js +10 -0
- package/cspell.json +3 -0
- package/package.json +1 -1
- package/test/dimensions/alignedDimension.test.js +165 -0
- package/test/dimensions/angularDimension.test.js +149 -0
- package/test/dimensions/diametricDimension.test.js +77 -4
- package/test/dimensions/dimension.test.js +130 -0
- package/test/dimensions/dimensionBase.test.js +97 -4
- package/test/dimensions/radialDimension.test.js +34 -0
- package/test/dimensions/rotatedDimension.test.js +80 -0
- package/test/integration/exportIntegration.reference.dxf +3 -9
- package/test/integration/exportIntegration.test.js +1 -0
- package/test/integration/userInputIntegration.reference.dxf +3 -9
- package/test/integration/userInputIntegration.test.js +1 -0
- package/test/lib/inputManager.test.js +316 -1
- package/test/lib/selectionManager.test.js +42 -0
- package/test/renderers/pdfRenderer.test.js +26 -0
- package/test/tables/dimStyleManager.test.js +90 -0
- package/test/tables/dimstyle.test.js +32 -0
- package/test/tables/tableManagerBase.test.js +167 -0
- package/test/tools/gripStretch.test.js +108 -0
- package/test/tools/offset.test.js +2 -0
- package/test/tools/purge.test.js +3 -1
- 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
825
|
-
341
|
|
826
|
-
ClosedFilled
|
|
820
|
+
19
|
|
827
821
|
371
|
|
828
822
|
-2
|
|
829
823
|
372
|
|
@@ -405,9 +405,7 @@ CloseFilled
|
|
|
405
405
|
289
|
|
406
406
|
3
|
|
407
407
|
340
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|