@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
|
@@ -307,9 +307,24 @@ export class TableManagerBase {
|
|
|
307
307
|
* @param {string} newItemName
|
|
308
308
|
*/
|
|
309
309
|
updateSceneItem(oldItemName, newItemName) {
|
|
310
|
+
const target = typeof oldItemName === 'string' ? oldItemName.toUpperCase() : undefined;
|
|
311
|
+
if (target === undefined) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
310
315
|
for (let i = 0; i < DesignCore.Scene.entities.count(); i++) {
|
|
311
|
-
|
|
312
|
-
|
|
316
|
+
const entity = DesignCore.Scene.entities.get(i);
|
|
317
|
+
const isProperty = entity.properties?.has(this.itemProperty) ?? false;
|
|
318
|
+
const value = isProperty ? entity.getProperty(this.itemProperty) : entity[this.itemProperty];
|
|
319
|
+
|
|
320
|
+
if (typeof value !== 'string' || value.toUpperCase() !== target) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (isProperty) {
|
|
325
|
+
entity.setProperty(this.itemProperty, newItemName);
|
|
326
|
+
} else {
|
|
327
|
+
entity[this.itemProperty] = newItemName;
|
|
313
328
|
}
|
|
314
329
|
}
|
|
315
330
|
}
|
|
@@ -344,7 +359,14 @@ export class TableManagerBase {
|
|
|
344
359
|
/** Purge unused items */
|
|
345
360
|
purge() {
|
|
346
361
|
const itemsToPurge = [];
|
|
362
|
+
// only style managers have a current style
|
|
363
|
+
const currentStyle = this.getCstyle?.()?.toUpperCase();
|
|
364
|
+
|
|
347
365
|
this.items.forEach((item, index) => {
|
|
366
|
+
if (item.name.toUpperCase() === currentStyle) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
348
370
|
const searchTerm = this.itemProperty === 'block' ? item : item.name;
|
|
349
371
|
const items = DesignCore.Scene.entities.find('ANY', this.itemProperty, searchTerm);
|
|
350
372
|
if (items.length === 0) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Tool } from './tool.js';
|
|
2
|
+
import { Strings } from '../lib/strings.js';
|
|
3
|
+
import { Input } from '../lib/input.js';
|
|
4
|
+
import { PromptOptions } from '../lib/promptOptions.js';
|
|
5
|
+
import { Logging } from '../lib/logging.js';
|
|
6
|
+
import { UpdateState } from '../lib/stateManager.js';
|
|
7
|
+
|
|
8
|
+
import { DesignCore } from '../designCore.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GripStretch Command Class
|
|
12
|
+
* Not shown in any toolbar and never started via the command line - only InputManager creates it.
|
|
13
|
+
* @extends Tool
|
|
14
|
+
*/
|
|
15
|
+
export class GripStretch extends Tool {
|
|
16
|
+
static type = 'GripStretch';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a GripStretch command
|
|
20
|
+
* @param {number} entityIndex - index of the entity in the selection set that owns the grip
|
|
21
|
+
* @param {Object} grip - { point, apply } descriptor returned by Entity.getGrips()
|
|
22
|
+
*/
|
|
23
|
+
constructor(entityIndex, grip) {
|
|
24
|
+
super();
|
|
25
|
+
this.entityIndex = entityIndex;
|
|
26
|
+
this.grip = grip;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Execute method
|
|
31
|
+
* requests the stretch point, then commits it - keeping the entity selected afterwards
|
|
32
|
+
* (with its grips still visible), unlike most commands which deselect on completion
|
|
33
|
+
*/
|
|
34
|
+
async execute() {
|
|
35
|
+
try {
|
|
36
|
+
const op = new PromptOptions(Strings.Input.STRETCHPOINT, [Input.Type.POINT]);
|
|
37
|
+
const newPoint = await DesignCore.Scene.inputManager.requestInput(op);
|
|
38
|
+
if (newPoint === undefined) return;
|
|
39
|
+
this.points.push(newPoint);
|
|
40
|
+
|
|
41
|
+
const inputManager = DesignCore.Scene.inputManager;
|
|
42
|
+
inputManager.actionCommand();
|
|
43
|
+
|
|
44
|
+
// finish without the standard executeCommand()/reset() combo, which would deselect the
|
|
45
|
+
// entity via Scene.reset() - grip editing must leave it selected (grips still visible)
|
|
46
|
+
inputManager.activeCommand = undefined;
|
|
47
|
+
inputManager.promptOption = undefined;
|
|
48
|
+
// reset() normally clears this - without it the next command's first point (or the next
|
|
49
|
+
// grip drag's tracking line) would stay anchored to this drop, not start fresh
|
|
50
|
+
inputManager.inputPoint = null;
|
|
51
|
+
DesignCore.CommandLine.resetPrompt();
|
|
52
|
+
DesignCore.Scene.selectionManager.reloadSelectedEntities();
|
|
53
|
+
DesignCore.Canvas.requestPaint();
|
|
54
|
+
} catch (err) {
|
|
55
|
+
Logging.instance.error(`${this.type} - ${err}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Preview the live grip position while dragging
|
|
61
|
+
*/
|
|
62
|
+
preview() {
|
|
63
|
+
const newPoints = this.grip.apply(DesignCore.Mouse.pointOnScene());
|
|
64
|
+
const previewIndex = DesignCore.Scene.selectionManager.selectionSet.selectionSet.indexOf(this.entityIndex);
|
|
65
|
+
if (previewIndex !== -1) {
|
|
66
|
+
DesignCore.Scene.selectionManager.selectedEntities[previewIndex].setProperty('points', newPoints);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Perform the command - commits the grip's new (dropped) position
|
|
72
|
+
*/
|
|
73
|
+
action() {
|
|
74
|
+
const entity = DesignCore.Scene.entities.get(this.entityIndex);
|
|
75
|
+
const newPoints = this.grip.apply(this.points[0]);
|
|
76
|
+
const properties = { points: newPoints };
|
|
77
|
+
// all grips currently implemented reposition dimension text - record the flag
|
|
78
|
+
// explicitly so its pre-drag value is captured by the UpdateState undo snapshot too
|
|
79
|
+
if (entity.dimType) {
|
|
80
|
+
properties.userPositionedText = true;
|
|
81
|
+
}
|
|
82
|
+
DesignCore.Scene.commit([new UpdateState(entity, properties)]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Cancel the in-progress stretch - unlike Tool's default (full reset), this must not deselect
|
|
87
|
+
* the entity, and must resolve the suspended execute()'s pending requestInput() promise so it
|
|
88
|
+
* doesn't stay forever awaiting a prompt nobody can respond to or cancel again
|
|
89
|
+
*/
|
|
90
|
+
cancel() {
|
|
91
|
+
const inputManager = DesignCore.Scene.inputManager;
|
|
92
|
+
inputManager.promptOption?.cancel();
|
|
93
|
+
inputManager.promptOption = undefined;
|
|
94
|
+
inputManager.activeCommand = undefined;
|
|
95
|
+
DesignCore.Scene.selectionManager.reloadSelectedEntities();
|
|
96
|
+
}
|
|
97
|
+
}
|
package/core/tools/offset.js
CHANGED
|
@@ -151,7 +151,13 @@ export class Offset extends Tool {
|
|
|
151
151
|
const isClosed = polyPts.length >= 3 && polyPts[0].isSame(polyPts.at(-1));
|
|
152
152
|
const points = isClosed ? polyPts.slice(0, -1) : polyPts;
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
const offsetPoints = this.getOffsetPolylinePoints(points, isClosed, sidePoint, distance);
|
|
155
|
+
if (offsetPoints && isClosed) {
|
|
156
|
+
// restore the closing point so closed entities (e.g. Circle) are rebuilt as closed, not as an Arc
|
|
157
|
+
offsetPoints.push(new Point(offsetPoints[0].x, offsetPoints[0].y));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return offsetPoints;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
/**
|
package/core/tools/tool.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DesignCore } from '../designCore.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Tool Class
|
|
3
5
|
*/
|
|
@@ -28,5 +30,13 @@ export class Tool {
|
|
|
28
30
|
action() {
|
|
29
31
|
// action function to be overridden by implementation
|
|
30
32
|
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cancel this command while it is active - overridden by implementations that need to
|
|
36
|
+
* clean up differently to the default full reset (e.g. GripStretch, which must not deselect)
|
|
37
|
+
*/
|
|
38
|
+
cancel() {
|
|
39
|
+
DesignCore.Scene.inputManager.reset();
|
|
40
|
+
};
|
|
31
41
|
}
|
|
32
42
|
|
package/cspell.json
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"dimstyles",
|
|
46
46
|
"dimline",
|
|
47
47
|
"DIMENSIONSTYLE",
|
|
48
|
+
"USERPOSITIONEDTEXT",
|
|
48
49
|
"DIMADEC",
|
|
49
50
|
"DIMALI",
|
|
50
51
|
"DIMALT",
|
|
@@ -127,6 +128,7 @@
|
|
|
127
128
|
"DIMTSZ",
|
|
128
129
|
"DIMTVP",
|
|
129
130
|
"DIMTXSTY",
|
|
131
|
+
"DIMTXSTYISHANDLE",
|
|
130
132
|
"DIMTXT",
|
|
131
133
|
"DIMTXTDIRECTION",
|
|
132
134
|
"DIMTZIN",
|
|
@@ -197,6 +199,7 @@
|
|
|
197
199
|
"SCALEFACTOR",
|
|
198
200
|
"SECONDCORNER",
|
|
199
201
|
"selectedchild",
|
|
202
|
+
"STRETCHPOINT",
|
|
200
203
|
"SELECTIONSET",
|
|
201
204
|
"SEQEND",
|
|
202
205
|
"SINGLESELECTION",
|
package/package.json
CHANGED
|
@@ -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
|
+
});
|