@design-core/core 0.0.5 → 0.0.7
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/.eslintrc.json +4 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/dimensions/alignedDimension.js +5 -4
- package/core/dimensions/angularDimension.js +6 -5
- package/core/dimensions/baseCircularDimension.js +3 -2
- package/core/dimensions/baseDimension.js +59 -93
- package/core/dimensions/baseLinearDimension.js +6 -5
- package/core/dimensions/diametricDimension.js +4 -3
- package/core/dimensions/dimension.js +21 -14
- package/core/dimensions/radialDimension.js +4 -3
- package/core/dimensions/rotatedDimension.js +12 -11
- package/core/entities/arc.js +112 -145
- package/core/entities/arctext.js +212 -174
- package/core/entities/basePolyline.js +33 -284
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +322 -249
- package/core/entities/insert.js +66 -81
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/polyline.js +4 -4
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +188 -199
- package/core/lib/auxiliary/rubberBand.js +32 -0
- package/core/lib/auxiliary/selectionWindow.js +63 -0
- package/core/lib/auxiliary/snapPoint.js +113 -0
- package/core/lib/auxiliary/trackingLine.js +76 -0
- package/core/lib/canvas.js +228 -166
- package/core/lib/clipboard.js +7 -1
- package/core/lib/colour.js +16 -0
- package/core/lib/commandLine.js +1 -6
- package/core/lib/commandManager.js +18 -8
- package/core/lib/constants.js +28 -0
- package/core/lib/dxf/dxf.js +4 -3
- package/core/lib/entityManager.js +18 -11
- package/core/lib/inputManager.js +36 -13
- package/core/lib/intersect.js +25 -0
- package/core/lib/patterns.js +8 -1
- package/core/lib/plotOptions.js +67 -0
- package/core/lib/polylineUtils.js +75 -0
- package/core/lib/renderers/cairoRenderer.js +371 -0
- package/core/lib/renderers/canvasRenderer.js +315 -0
- package/core/lib/renderers/pdfRenderer.js +366 -0
- package/core/lib/renderers/rendererBase.js +284 -0
- package/core/lib/renderers/svgRenderer.js +343 -0
- package/core/lib/scene.js +27 -5
- package/core/lib/selectionManager.js +2 -2
- package/core/lib/settings.js +4 -2
- package/core/lib/snapping.js +4 -191
- package/core/lib/stateManager.js +30 -1
- package/core/lib/utils.js +21 -2
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +29 -13
- package/core/tables/block.js +4 -37
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tools/chamfer.js +111 -137
- package/core/tools/chamferFilletBase.js +135 -34
- package/core/tools/copy.js +3 -3
- package/core/tools/explode.js +2 -2
- package/core/tools/extend.js +132 -19
- package/core/tools/fillet.js +86 -183
- package/core/tools/matchProp.js +2 -11
- package/core/tools/mirror.js +7 -6
- package/core/tools/move.js +3 -3
- package/core/tools/offset.js +3 -5
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/rotate.js +6 -3
- package/core/tools/scale.js +1 -1
- package/core/tools/trim.js +265 -23
- package/core/tools/zoom.js +1 -1
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +14 -7
- package/test/dimensions/angularDimension.test.js +6 -6
- package/test/dimensions/baseDimension.test.js +69 -8
- package/test/dimensions/baseLinearDimension.test.js +62 -0
- package/test/dimensions/diametricDimension.test.js +6 -6
- package/test/dimensions/radialDimension.test.js +6 -6
- package/test/dimensions/rotatedDimension.test.js +14 -7
- package/test/entities/arc.test.js +80 -83
- package/test/entities/arctext.test.js +180 -53
- package/test/entities/basePolyline.test.js +129 -2
- package/test/entities/circle.test.js +57 -85
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +543 -70
- package/test/entities/insert.test.js +96 -1
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +294 -69
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +2 -2
- package/test/integration/intersectionIntegration.test.js +4 -5
- package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
- package/test/integration/pdfRendererIntegration.test.js +61 -0
- package/test/integration/svgRendererIntegration.reference.svg +3343 -0
- package/test/integration/svgRendererIntegration.test.js +61 -0
- package/test/integration/userInputIntegration.reference.dxf +1165 -105
- package/test/integration/userInputIntegration.test.js +239 -132
- package/test/lib/canvas.test.js +742 -123
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +80 -19
- package/test/lib/intersect.test.js +83 -0
- package/test/lib/plotOptions.test.js +179 -0
- package/test/lib/rubberBand.test.js +84 -0
- package/test/lib/settings.test.js +5 -6
- package/test/lib/snapping.test.js +71 -129
- package/test/lib/stateManager.test.js +7 -7
- package/test/lib/utils.test.js +36 -2
- package/test/properties/entityProperties.test.js +132 -0
- package/test/properties/property.test.js +53 -0
- package/test/properties/propertyManager.test.js +3 -3
- package/test/renderers/canvasRenderer.test.js +317 -0
- package/test/renderers/pdfRenderer.test.js +271 -0
- package/test/renderers/rendererBase.test.js +213 -0
- package/test/renderers/svgRenderer.test.js +326 -0
- package/test/tables/block.test.js +31 -1
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +152 -0
- package/test/tools/chamfer.test.js +227 -5
- package/test/tools/chamferFilletBase.test.js +98 -9
- package/test/tools/copy.test.js +2 -2
- package/test/tools/explode.test.js +3 -3
- package/test/tools/extend.test.js +331 -0
- package/test/tools/fillet.test.js +296 -0
- package/test/tools/matchProp.test.js +5 -5
- package/test/tools/mirror.test.js +13 -13
- package/test/tools/offset.test.js +170 -3
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/rotate.test.js +32 -2
- package/test/tools/scale.test.js +5 -5
- package/test/tools/trim.test.js +526 -0
- package/test/tools/zoom.test.js +7 -7
- package/core/lib/selectionWindow.js +0 -83
package/core/tools/scale.js
CHANGED
|
@@ -122,7 +122,7 @@ export class Scale extends Tool {
|
|
|
122
122
|
if (this.points.length >= 1 && this.referencePoint === null) {
|
|
123
123
|
// Draw a line from base point to mouse
|
|
124
124
|
const points = [this.points[0], mousePoint];
|
|
125
|
-
DesignCore.Scene.
|
|
125
|
+
DesignCore.Scene.previewEntities.create('Line', { points: points });
|
|
126
126
|
|
|
127
127
|
const base = this.points[0];
|
|
128
128
|
const distance = base.distance(mousePoint);
|
package/core/tools/trim.js
CHANGED
|
@@ -3,6 +3,10 @@ import { Strings } from '../lib/strings.js';
|
|
|
3
3
|
import { Tool } from './tool.js';
|
|
4
4
|
import { Input, PromptOptions } from '../lib/inputManager.js';
|
|
5
5
|
import { Logging } from '../lib/logging.js';
|
|
6
|
+
import { AddState, RemoveState } from '../lib/stateManager.js';
|
|
7
|
+
import { Utils } from '../lib/utils.js';
|
|
8
|
+
import { Colour } from '../lib/colour.js';
|
|
9
|
+
import { PolylineUtils } from '../lib/polylineUtils.js';
|
|
6
10
|
|
|
7
11
|
import { DesignCore } from '../designCore.js';
|
|
8
12
|
|
|
@@ -38,10 +42,10 @@ export class Trim extends Tool {
|
|
|
38
42
|
*/
|
|
39
43
|
async execute() {
|
|
40
44
|
try {
|
|
41
|
-
const
|
|
45
|
+
const boundaryOp = new PromptOptions(Strings.Input.BOUNDARY, [Input.Type.SELECTIONSET]);
|
|
42
46
|
|
|
43
47
|
if (!DesignCore.Scene.selectionManager.selectionSet.selectionSet.length) {
|
|
44
|
-
const boundary = await DesignCore.Scene.inputManager.requestInput(
|
|
48
|
+
const boundary = await DesignCore.Scene.inputManager.requestInput(boundaryOp);
|
|
45
49
|
if (boundary === undefined) return;
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -51,9 +55,9 @@ export class Trim extends Tool {
|
|
|
51
55
|
this.selectedBoundaryItems.push(boundaryItem);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
const
|
|
58
|
+
const selectOp = new PromptOptions(Strings.Input.SELECT, [Input.Type.SINGLESELECTION]);
|
|
55
59
|
while (true) {
|
|
56
|
-
const selection = await DesignCore.Scene.inputManager.requestInput(
|
|
60
|
+
const selection = await DesignCore.Scene.inputManager.requestInput(selectOp);
|
|
57
61
|
if (selection === undefined) break;
|
|
58
62
|
this.selectedItem = DesignCore.Scene.entities.get(selection.selectedItemIndex);
|
|
59
63
|
DesignCore.Scene.selectionManager.removeLastSelection();
|
|
@@ -70,7 +74,29 @@ export class Trim extends Tool {
|
|
|
70
74
|
* Preview the command during execution
|
|
71
75
|
*/
|
|
72
76
|
preview() {
|
|
73
|
-
|
|
77
|
+
if (!this.selectedBoundaryItems.length) return;
|
|
78
|
+
|
|
79
|
+
const index = DesignCore.Scene.selectionManager.findClosestItem(DesignCore.Mouse.pointOnScene());
|
|
80
|
+
if (index === undefined) return;
|
|
81
|
+
|
|
82
|
+
const entity = DesignCore.Scene.entities.get(index);
|
|
83
|
+
if (typeof entity.toPolylinePoints !== 'function' || typeof entity.fromPolylinePoints !== 'function') return;
|
|
84
|
+
const intersectPoints = this.#collectIntersectPoints(entity);
|
|
85
|
+
if (!intersectPoints.length) return;
|
|
86
|
+
|
|
87
|
+
const stateChanges = this.#trimEntity(entity, intersectPoints);
|
|
88
|
+
if (!stateChanges?.length) return;
|
|
89
|
+
|
|
90
|
+
// Draw the full entity in a dulled colour, then draw survivors on top.
|
|
91
|
+
const dulledEntity = Utils.cloneObject(entity);
|
|
92
|
+
dulledEntity.setProperty('colour', Colour.blend(entity.getDrawColour(), DesignCore.Settings.canvasbackgroundcolour, DesignCore.Settings.previewBlendFactor));
|
|
93
|
+
DesignCore.Scene.previewEntities.add(dulledEntity);
|
|
94
|
+
|
|
95
|
+
for (const change of stateChanges) {
|
|
96
|
+
if (change instanceof AddState) {
|
|
97
|
+
DesignCore.Scene.previewEntities.add(change.entity);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
/**
|
|
@@ -78,26 +104,16 @@ export class Trim extends Tool {
|
|
|
78
104
|
*/
|
|
79
105
|
action() {
|
|
80
106
|
if (this.selectedItem && this.selectedBoundaryItems.length) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const intersect = Intersection.intersectPolylinePolyline(boundaryItem.toPolylinePoints(), this.selectedItem.toPolylinePoints());
|
|
87
|
-
if (intersect.points.length) {
|
|
88
|
-
for (let point = 0; point < intersect.points.length; point++) {
|
|
89
|
-
intersectPoints.push(intersect.points[point]);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
} catch {
|
|
93
|
-
Logging.instance.warn(`${this.constructor.name}: Error intersecting between ${boundaryItem.type} and ${this.selectedItem.type}`);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
107
|
+
if (typeof this.selectedItem.fromPolylinePoints !== 'function') {
|
|
108
|
+
DesignCore.Core.notify(`${this.type} - ${this.selectedItem.type} ${Strings.Message.NOTRIM}`);
|
|
109
|
+
this.selectedItem = null;
|
|
110
|
+
return;
|
|
97
111
|
}
|
|
98
112
|
|
|
113
|
+
const intersectPoints = this.#collectIntersectPoints(this.selectedItem);
|
|
114
|
+
|
|
99
115
|
if (intersectPoints.length) {
|
|
100
|
-
const stateChanges = this.selectedItem
|
|
116
|
+
const stateChanges = this.#trimEntity(this.selectedItem, intersectPoints);
|
|
101
117
|
if (stateChanges?.length) {
|
|
102
118
|
DesignCore.Scene.commit(stateChanges);
|
|
103
119
|
}
|
|
@@ -106,8 +122,234 @@ export class Trim extends Tool {
|
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
// reset selected item
|
|
110
125
|
this.selectedItem = null;
|
|
111
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Collect all intersection points between the boundary items and the given entity.
|
|
130
|
+
* Unsupported entity combinations are silently skipped.
|
|
131
|
+
* @param {Object} entity
|
|
132
|
+
* @return {Array}
|
|
133
|
+
*/
|
|
134
|
+
#collectIntersectPoints(entity) {
|
|
135
|
+
const intersectPoints = [];
|
|
136
|
+
for (const boundaryItem of this.selectedBoundaryItems) {
|
|
137
|
+
if (boundaryItem === entity) continue;
|
|
138
|
+
try {
|
|
139
|
+
const intersect = Intersection.intersectPolylinePolyline(boundaryItem.toPolylinePoints(), entity.toPolylinePoints());
|
|
140
|
+
intersectPoints.push(...intersect.points);
|
|
141
|
+
} catch {
|
|
142
|
+
// skip unsupported entity combinations
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return intersectPoints;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compute state changes to trim entity at the pair of intersection points
|
|
150
|
+
* surrounding the mouse position. Operates entirely in polyline-point space
|
|
151
|
+
* via entity.toPolylinePoints() / entity.fromPolylinePoints(), so it works
|
|
152
|
+
* for any entity that implements that protocol (Line, Arc, Circle,
|
|
153
|
+
* BasePolyline, and future entities such as Spline or Ellipse).
|
|
154
|
+
* @param {Object} entity
|
|
155
|
+
* @param {Array} intersectPoints
|
|
156
|
+
* @return {Array}
|
|
157
|
+
*/
|
|
158
|
+
#trimEntity(entity, intersectPoints) {
|
|
159
|
+
const polyPoints = entity.toPolylinePoints();
|
|
160
|
+
const mousePosition = DesignCore.Mouse.pointOnScene();
|
|
161
|
+
const isClosed = polyPoints[0].isSame(polyPoints.at(-1));
|
|
162
|
+
|
|
163
|
+
// Drop any intersection point that coincides with an existing vertex
|
|
164
|
+
const filtered = intersectPoints.filter((p) => !polyPoints.some((v) => v.isSame(p)));
|
|
165
|
+
if (!filtered.length) return [];
|
|
166
|
+
|
|
167
|
+
// Locate each intersection point within the polyline segments
|
|
168
|
+
const locatedIntersections = [];
|
|
169
|
+
for (const point of filtered) {
|
|
170
|
+
for (let i = 1; i < polyPoints.length; i++) {
|
|
171
|
+
if (PolylineUtils.isPointOnSegment(point, polyPoints[i - 1], polyPoints[i])) {
|
|
172
|
+
locatedIntersections.push({
|
|
173
|
+
segmentIndex: i,
|
|
174
|
+
point,
|
|
175
|
+
positionAlongSegment: PolylineUtils.positionOnSegment(polyPoints, point, i),
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!locatedIntersections.length) return [];
|
|
183
|
+
|
|
184
|
+
locatedIntersections.sort((a, b) => {
|
|
185
|
+
if (a.segmentIndex !== b.segmentIndex) return a.segmentIndex - b.segmentIndex;
|
|
186
|
+
return a.positionAlongSegment - b.positionAlongSegment;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Find which segment the mouse is closest to
|
|
190
|
+
let mouseSegmentIndex = 0;
|
|
191
|
+
let mouseClosest = null;
|
|
192
|
+
let minDistance = Infinity;
|
|
193
|
+
for (let i = 1; i < polyPoints.length; i++) {
|
|
194
|
+
const closest = PolylineUtils.closestPointOnSegment(polyPoints, mousePosition, i);
|
|
195
|
+
if (closest) {
|
|
196
|
+
const dist = mousePosition.distance(closest);
|
|
197
|
+
if (dist < minDistance) {
|
|
198
|
+
minDistance = dist;
|
|
199
|
+
mouseSegmentIndex = i;
|
|
200
|
+
mouseClosest = closest;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const mousePos = mouseClosest ?
|
|
206
|
+
PolylineUtils.positionOnSegment(polyPoints, mouseClosest, mouseSegmentIndex) :
|
|
207
|
+
0;
|
|
208
|
+
|
|
209
|
+
// Find the nearest intersections before and after the mouse position
|
|
210
|
+
let trimBefore = null;
|
|
211
|
+
let trimAfter = null;
|
|
212
|
+
for (const loc of locatedIntersections) {
|
|
213
|
+
if (loc.segmentIndex < mouseSegmentIndex) {
|
|
214
|
+
trimBefore = loc;
|
|
215
|
+
} else if (loc.segmentIndex === mouseSegmentIndex) {
|
|
216
|
+
if (loc.positionAlongSegment <= mousePos) {
|
|
217
|
+
trimBefore = loc;
|
|
218
|
+
} else if (!trimAfter) {
|
|
219
|
+
trimAfter = loc;
|
|
220
|
+
}
|
|
221
|
+
} else if (!trimAfter) {
|
|
222
|
+
trimAfter = loc;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const stateChanges = [];
|
|
227
|
+
|
|
228
|
+
if (isClosed) {
|
|
229
|
+
// Wrap cyclically: if trimBefore is null use the last intersection,
|
|
230
|
+
// if trimAfter is null use the first.
|
|
231
|
+
if (!trimBefore) trimBefore = locatedIntersections.at(-1);
|
|
232
|
+
if (!trimAfter) trimAfter = locatedIntersections[0];
|
|
233
|
+
|
|
234
|
+
// A single intersection point cannot divide a closed shape.
|
|
235
|
+
if (trimBefore === trimAfter) return [];
|
|
236
|
+
|
|
237
|
+
// Build the single kept open portion from trimAfter to trimBefore,
|
|
238
|
+
// going away from the mouse (i.e. not through the mouse segment).
|
|
239
|
+
let outputPoints;
|
|
240
|
+
if (trimAfter.segmentIndex < trimBefore.segmentIndex ||
|
|
241
|
+
(trimAfter.segmentIndex === trimBefore.segmentIndex &&
|
|
242
|
+
trimAfter.positionAlongSegment < trimBefore.positionAlongSegment)) {
|
|
243
|
+
// Forward walk: trimAfter → trimBefore without crossing the seam
|
|
244
|
+
outputPoints = this.#buildPortion(polyPoints, trimAfter, trimBefore);
|
|
245
|
+
} else {
|
|
246
|
+
// Wrapping walk: trimAfter → end → seam → start → trimBefore
|
|
247
|
+
outputPoints = this.#buildWrappingPortion(polyPoints, trimAfter, trimBefore);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (outputPoints && outputPoints.length >= 2) {
|
|
251
|
+
const output = Utils.cloneObject(entity).fromPolylinePoints(outputPoints);
|
|
252
|
+
output.flags?.removeValue(1);
|
|
253
|
+
stateChanges.push(new AddState(output));
|
|
254
|
+
stateChanges.push(new RemoveState(entity));
|
|
255
|
+
}
|
|
256
|
+
return stateChanges;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Open entity: build portionOne (start → trimBefore) and portionTwo (trimAfter → end)
|
|
260
|
+
const startLoc = { segmentIndex: 1, point: polyPoints[0] };
|
|
261
|
+
const endLoc = { segmentIndex: polyPoints.length - 1, point: polyPoints.at(-1) };
|
|
262
|
+
const portionOnePoints = trimBefore ? this.#buildPortion(polyPoints, startLoc, trimBefore) : null;
|
|
263
|
+
const portionTwoPoints = trimAfter ? this.#buildPortion(polyPoints, trimAfter, endLoc) : null;
|
|
264
|
+
|
|
265
|
+
if (portionOnePoints) {
|
|
266
|
+
const output = Utils.cloneObject(entity).fromPolylinePoints(portionOnePoints);
|
|
267
|
+
output.flags?.removeValue(1);
|
|
268
|
+
stateChanges.push(new AddState(output));
|
|
269
|
+
}
|
|
270
|
+
if (portionTwoPoints) {
|
|
271
|
+
const output = Utils.cloneObject(entity).fromPolylinePoints(portionTwoPoints);
|
|
272
|
+
output.flags?.removeValue(1);
|
|
273
|
+
stateChanges.push(new AddState(output));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (trimBefore || trimAfter) {
|
|
277
|
+
stateChanges.push(new RemoveState(entity));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return stateChanges;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Build a sub-polyline from fromLoc to toLoc going forward (no seam crossing).
|
|
285
|
+
* Both locations must appear in forward order along the polyline.
|
|
286
|
+
* @param {Array} polyPoints
|
|
287
|
+
* @param {Object} fromLoc - { segmentIndex, point, positionAlongSegment }
|
|
288
|
+
* @param {Object} toLoc - { segmentIndex, point, positionAlongSegment }
|
|
289
|
+
* @return {Array|null}
|
|
290
|
+
*/
|
|
291
|
+
#buildPortion(polyPoints, fromLoc, toLoc) {
|
|
292
|
+
const fromSegStart = polyPoints[fromLoc.segmentIndex - 1];
|
|
293
|
+
const fromSegEnd = polyPoints[fromLoc.segmentIndex];
|
|
294
|
+
|
|
295
|
+
// Same-segment case: compute the sub-arc bulge directly from the arc geometry
|
|
296
|
+
if (fromLoc.segmentIndex === toLoc.segmentIndex) {
|
|
297
|
+
const startPoint = fromLoc.point.clone();
|
|
298
|
+
if (fromSegStart.bulge !== 0 && fromSegStart.bulge !== undefined) {
|
|
299
|
+
const center = fromSegStart.bulgeCentrePoint(fromSegEnd);
|
|
300
|
+
const direction = fromSegStart.bulge > 0 ? 1 : -1;
|
|
301
|
+
const startAngle = center.angle(fromLoc.point);
|
|
302
|
+
const endAngle = center.angle(toLoc.point);
|
|
303
|
+
const includedAngle = ((endAngle - startAngle) * direction + 4 * Math.PI) % (2 * Math.PI);
|
|
304
|
+
startPoint.bulge = Math.tan(includedAngle / 4) * direction;
|
|
305
|
+
}
|
|
306
|
+
return [startPoint, toLoc.point.clone()];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const points = [];
|
|
310
|
+
|
|
311
|
+
// Start point: fromLoc.point with partial bulge covering the rest of its segment
|
|
312
|
+
const startPoint = fromLoc.point.clone();
|
|
313
|
+
if (fromSegStart.bulge !== 0 && fromSegStart.bulge !== undefined) {
|
|
314
|
+
startPoint.bulge = fromSegStart.partialBulge(fromSegEnd, fromLoc.point, true);
|
|
315
|
+
}
|
|
316
|
+
points.push(startPoint);
|
|
317
|
+
|
|
318
|
+
// Intermediate vertices between the two segments
|
|
319
|
+
for (let i = fromLoc.segmentIndex; i <= toLoc.segmentIndex - 2; i++) {
|
|
320
|
+
points.push(polyPoints[i].clone());
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Start of toLoc's segment with partial bulge trimmed to toLoc.point
|
|
324
|
+
const toSegStart = polyPoints[toLoc.segmentIndex - 1];
|
|
325
|
+
const toSegEnd = polyPoints[toLoc.segmentIndex];
|
|
326
|
+
const lastSegStart = toSegStart.clone();
|
|
327
|
+
if (toSegStart.bulge !== 0 && toSegStart.bulge !== undefined) {
|
|
328
|
+
lastSegStart.bulge = toSegStart.partialBulge(toSegEnd, toLoc.point);
|
|
329
|
+
}
|
|
330
|
+
if (!points.at(-1).isSame(lastSegStart)) points.push(lastSegStart);
|
|
331
|
+
|
|
332
|
+
const endPoint = toLoc.point.clone();
|
|
333
|
+
if (!points.at(-1).isSame(endPoint)) points.push(endPoint);
|
|
334
|
+
|
|
335
|
+
return points.length >= 2 ? points : null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Build a sub-polyline from fromLoc to toLoc crossing the seam (wrapping).
|
|
340
|
+
* Used for closed entities when the kept portion wraps through the closure point.
|
|
341
|
+
* Implemented as two forward #buildPortion calls joined at the seam.
|
|
342
|
+
* @param {Array} polyPoints
|
|
343
|
+
* @param {Object} fromLoc - { segmentIndex, point, positionAlongSegment }
|
|
344
|
+
* @param {Object} toLoc - { segmentIndex, point, positionAlongSegment }
|
|
345
|
+
* @return {Array|null}
|
|
346
|
+
*/
|
|
347
|
+
#buildWrappingPortion(polyPoints, fromLoc, toLoc) {
|
|
348
|
+
const firstHalf = this.#buildPortion(polyPoints, fromLoc, { segmentIndex: polyPoints.length - 1, point: polyPoints.at(-1) });
|
|
349
|
+
const secondHalf = this.#buildPortion(polyPoints, { segmentIndex: 1, point: polyPoints[0] }, toLoc);
|
|
350
|
+
if (!firstHalf || !secondHalf) return null;
|
|
351
|
+
firstHalf.pop(); // discard closure point before joining
|
|
352
|
+
return firstHalf.concat(secondHalf);
|
|
353
|
+
}
|
|
112
354
|
}
|
|
113
355
|
|
package/core/tools/zoom.js
CHANGED
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design-core/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"eslint": "^8.26.0",
|
|
10
10
|
"eslint-config-google": "^0.14.0",
|
|
11
11
|
"eslint-config-standard": "^17.0.0",
|
|
12
|
-
"eslint-plugin-import": "^2.
|
|
12
|
+
"eslint-plugin-import": "^2.32.0",
|
|
13
13
|
"eslint-plugin-n": "^15.3.0",
|
|
14
14
|
"eslint-plugin-promise": "^6.1.1",
|
|
15
15
|
"jest": "^29.7.0"
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"description": "2D CAD Library",
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./core/core/core.js",
|
|
20
|
-
"./designCore.js": "./core/designCore.js"
|
|
20
|
+
"./designCore.js": "./core/designCore.js",
|
|
21
|
+
"./property.js": "./core/properties/property.js",
|
|
22
|
+
"./canvasRenderer.js": "./core/lib/renderers/canvasRenderer.js"
|
|
21
23
|
},
|
|
22
24
|
"directories": {
|
|
23
25
|
"test": "test"
|
|
@@ -34,7 +34,7 @@ test.each(scenarios)('Dimension.execute handles $desc', async (scenario) => {
|
|
|
34
34
|
// check if the text value matches the expected dimension value
|
|
35
35
|
for (const entity of dimensionBlockEntities) {
|
|
36
36
|
if (entity.type === 'Text') {
|
|
37
|
-
expect(Number(entity.string)).toBeCloseTo(dimensionValue);
|
|
37
|
+
expect(Number(entity.getProperty('string'))).toBeCloseTo(dimensionValue);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}, { selectedItems });
|
|
@@ -104,23 +104,29 @@ describe('AlignedDimension.getPointsFromSelection', () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
test('AlignedDimension.preview runs without error and calls createTempItem', () => {
|
|
107
|
-
const
|
|
107
|
+
const origAddAux = DesignCore.Scene.auxiliaryEntities.add;
|
|
108
|
+
const origCreateTempItem = DesignCore.Scene.previewEntities.create;
|
|
108
109
|
const origPointOnScene = DesignCore.Mouse.pointOnScene;
|
|
109
110
|
// Manual mock for createTempItem
|
|
110
111
|
const createTempItemCalls = [];
|
|
111
|
-
DesignCore.Scene.
|
|
112
|
+
DesignCore.Scene.previewEntities.create = function(type, obj) {
|
|
112
113
|
createTempItemCalls.push([type, obj]);
|
|
113
114
|
};
|
|
115
|
+
// Manual mock for auxiliaryEntities.add
|
|
116
|
+
const addAuxCalls = [];
|
|
117
|
+
DesignCore.Scene.auxiliaryEntities.add = function(item) {
|
|
118
|
+
addAuxCalls.push(item);
|
|
119
|
+
};
|
|
114
120
|
// Manual mock for pointOnScene
|
|
115
121
|
DesignCore.Mouse.pointOnScene = function() {
|
|
116
122
|
return new Point(5, 5);
|
|
117
123
|
};
|
|
118
124
|
|
|
119
|
-
// Test with 1 point (should
|
|
125
|
+
// Test with 1 point (should add a RubberBand to auxiliaryEntities)
|
|
120
126
|
const dim1 = new AlignedDimension();
|
|
121
127
|
dim1.points = [new Point(0, 0)];
|
|
122
128
|
expect(() => dim1.preview()).not.toThrow();
|
|
123
|
-
expect(
|
|
129
|
+
expect(addAuxCalls.length).toBeGreaterThanOrEqual(1);
|
|
124
130
|
|
|
125
131
|
// Test with >1 point (should call createTempItem with this.type)
|
|
126
132
|
const dim2 = new AlignedDimension();
|
|
@@ -129,7 +135,8 @@ test('AlignedDimension.preview runs without error and calls createTempItem', ()
|
|
|
129
135
|
expect(createTempItemCalls.some((call) => call[0] === dim2.type)).toBe(true);
|
|
130
136
|
|
|
131
137
|
// Restore
|
|
132
|
-
DesignCore.Scene.
|
|
138
|
+
DesignCore.Scene.auxiliaryEntities.add = origAddAux;
|
|
139
|
+
DesignCore.Scene.previewEntities.create = origCreateTempItem;
|
|
133
140
|
DesignCore.Mouse.pointOnScene = origPointOnScene;
|
|
134
141
|
});
|
|
135
142
|
|
|
@@ -204,7 +211,7 @@ AcDbAlignedDimension
|
|
|
204
211
|
expect(file.contents).toEqual(dxfString);
|
|
205
212
|
|
|
206
213
|
// create new entity from entity data to ensure all props are loaded
|
|
207
|
-
const newDimension = new AlignedDimension(dimension);
|
|
214
|
+
const newDimension = new AlignedDimension({ handle: dimension.getProperty('handle'), points: dimension.points, ...dimension });
|
|
208
215
|
file = new File();
|
|
209
216
|
newDimension.dxf(file);
|
|
210
217
|
expect(file.contents).toEqual(dxfString);
|
|
@@ -69,9 +69,9 @@ test.each(scenarios)('Dimension.execute handles $desc', async (scenario) => {
|
|
|
69
69
|
for (const entity of dimensionBlockEntities) {
|
|
70
70
|
if (entity.type === 'Text') {
|
|
71
71
|
// remove all non-numeric characters except . and -
|
|
72
|
-
const numbersOnly = entity.string.replace(/[^0-9.-]+/g, '');
|
|
72
|
+
const numbersOnly = entity.getProperty('string').replace(/[^0-9.-]+/g, '');
|
|
73
73
|
expect(Number(numbersOnly)).toBeCloseTo(dimensionValue);
|
|
74
|
-
expect(entity.string).toContain('°');
|
|
74
|
+
expect(entity.getProperty('string')).toContain('°');
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}, { selectedItems });
|
|
@@ -100,11 +100,11 @@ test('getPointsFromSelection returns correct sequenced points', () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
test('AngularDimension.preview runs without error and calls createTempItem', () => {
|
|
103
|
-
const origCreateTempItem = DesignCore.Scene.
|
|
103
|
+
const origCreateTempItem = DesignCore.Scene.previewEntities.create;
|
|
104
104
|
const origPointOnScene = DesignCore.Mouse.pointOnScene;
|
|
105
105
|
// Manual mock for createTempItem
|
|
106
106
|
const createTempItemCalls = [];
|
|
107
|
-
DesignCore.Scene.
|
|
107
|
+
DesignCore.Scene.previewEntities.create = function(type, obj) {
|
|
108
108
|
createTempItemCalls.push([type, obj]);
|
|
109
109
|
};
|
|
110
110
|
// Manual mock for pointOnScene
|
|
@@ -138,7 +138,7 @@ test('AngularDimension.preview runs without error and calls createTempItem', ()
|
|
|
138
138
|
expect(createTempItemCalls.some((call) => call[0] === dim2.type)).toBe(true);
|
|
139
139
|
|
|
140
140
|
// Restore
|
|
141
|
-
DesignCore.Scene.
|
|
141
|
+
DesignCore.Scene.previewEntities.create = origCreateTempItem;
|
|
142
142
|
DesignCore.Mouse.pointOnScene = origPointOnScene;
|
|
143
143
|
});
|
|
144
144
|
|
|
@@ -234,7 +234,7 @@ AcDb2LineAngularDimension
|
|
|
234
234
|
expect(file.contents).toEqual(dxfString);
|
|
235
235
|
|
|
236
236
|
// create new entity from entity data to ensure all props are loaded
|
|
237
|
-
const newDimension = new AngularDimension(dimension);
|
|
237
|
+
const newDimension = new AngularDimension({ handle: dimension.getProperty('handle'), points: dimension.points, ...dimension });
|
|
238
238
|
file = new File();
|
|
239
239
|
newDimension.dxf(file);
|
|
240
240
|
expect(file.contents).toEqual(dxfString);
|
|
@@ -2,6 +2,8 @@ import { BaseDimension } from '../../core/dimensions/baseDimension.js';
|
|
|
2
2
|
import { Point } from '../../core/entities/point.js';
|
|
3
3
|
import { Core } from '../../core/core/core.js';
|
|
4
4
|
import { Text } from '../../core/entities/text.js';
|
|
5
|
+
import { Line } from '../../core/entities/line.js';
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
5
7
|
// import { DimType } from '../../core/properties/dimType.js';
|
|
6
8
|
|
|
7
9
|
// initialise core
|
|
@@ -16,11 +18,11 @@ describe('BaseDimension', () => {
|
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
test('constructor sets default properties', () => {
|
|
19
|
-
expect(baseDim.textOverride).toBe('');
|
|
20
|
-
expect(baseDim.blockName).toBe('');
|
|
21
|
-
expect(baseDim.dimensionStyle).toBe('STANDARD');
|
|
22
|
-
expect(baseDim.leaderLength).toBe(0);
|
|
23
|
-
expect(baseDim.linearDimAngle).toBe(0);
|
|
21
|
+
expect(baseDim.getProperty('textOverride')).toBe('');
|
|
22
|
+
expect(baseDim.getProperty('blockName')).toBe('');
|
|
23
|
+
expect(baseDim.getProperty('dimensionStyle')).toBe('STANDARD');
|
|
24
|
+
expect(baseDim.getProperty('leaderLength')).toBe(0);
|
|
25
|
+
expect(baseDim.getProperty('linearDimAngle')).toBe(0);
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
test('getDimensionValue formats values by dimension type', () => {
|
|
@@ -166,17 +168,17 @@ describe('BaseDimension', () => {
|
|
|
166
168
|
});
|
|
167
169
|
|
|
168
170
|
test('getDimensionValue uses textOverride', () => {
|
|
169
|
-
baseDim.textOverride
|
|
171
|
+
baseDim.setProperty('textOverride', 'foo <> bar');
|
|
170
172
|
expect(baseDim.getDimensionValue(10)).toBe('foo 10.0000 bar');
|
|
171
173
|
});
|
|
172
174
|
|
|
173
175
|
test('getDimensionText returns a Text object with correct values', () => {
|
|
174
176
|
const text = baseDim.getDimensionText(10, new Point(1, 2), Math.PI / 4);
|
|
175
177
|
expect(text).toBeInstanceOf(Text);
|
|
176
|
-
expect(text.height).toBe(0.18);
|
|
178
|
+
expect(text.getProperty('height')).toBe(0.18);
|
|
177
179
|
expect(text.points[0].x).toBe(1);
|
|
178
180
|
expect(text.points[0].y).toBe(2);
|
|
179
|
-
expect(typeof text.string).toBe('string');
|
|
181
|
+
expect(typeof text.getProperty('string')).toBe('string');
|
|
180
182
|
});
|
|
181
183
|
|
|
182
184
|
test('getTextDirection flips angle in lower half', () => {
|
|
@@ -236,4 +238,63 @@ describe('BaseDimension', () => {
|
|
|
236
238
|
baseDim.block = { touched: () => false };
|
|
237
239
|
expect(baseDim.touched({})).toBe(false);
|
|
238
240
|
});
|
|
241
|
+
|
|
242
|
+
describe('draw()', () => {
|
|
243
|
+
/**
|
|
244
|
+
* Return a BaseDimension with a stubbed buildDimension() that produces one Line.
|
|
245
|
+
* @return {BaseDimension}
|
|
246
|
+
*/
|
|
247
|
+
function makeDim() {
|
|
248
|
+
const dim = new BaseDimension({});
|
|
249
|
+
dim.points = [new Point(0, 0), new Point(10, 0)];
|
|
250
|
+
dim.buildDimension = jest.fn(() => {
|
|
251
|
+
return [new Line({ points: [new Point(0, 0), new Point(10, 0)] })];
|
|
252
|
+
});
|
|
253
|
+
return dim;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
test('draw() returns the block items array', () => {
|
|
257
|
+
const dim = makeDim();
|
|
258
|
+
const result = dim.draw({});
|
|
259
|
+
expect(result).toBe(dim.block.items);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('draw() calls refresh() (via buildDimension) when block is empty', () => {
|
|
263
|
+
const dim = makeDim();
|
|
264
|
+
expect(dim.block.items).toHaveLength(0);
|
|
265
|
+
dim.draw({});
|
|
266
|
+
expect(dim.buildDimension).toHaveBeenCalledTimes(1);
|
|
267
|
+
expect(dim.block.items.length).toBeGreaterThan(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('draw() does not call refresh() when block already has items', () => {
|
|
271
|
+
const dim = makeDim();
|
|
272
|
+
// pre-populate the block
|
|
273
|
+
dim.block.addItem(new Line({ points: [new Point(0, 0), new Point(1, 1)] }));
|
|
274
|
+
dim.draw({});
|
|
275
|
+
expect(dim.buildDimension).not.toHaveBeenCalled();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('draw() returns populated items after initial refresh', () => {
|
|
279
|
+
const dim = makeDim();
|
|
280
|
+
const items = dim.draw({});
|
|
281
|
+
expect(items).toBeInstanceOf(Array);
|
|
282
|
+
expect(items.length).toBeGreaterThan(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('draw() returns same array reference on repeated calls', () => {
|
|
286
|
+
const dim = makeDim();
|
|
287
|
+
const first = dim.draw({});
|
|
288
|
+
const second = dim.draw({});
|
|
289
|
+
expect(first).toBe(second);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('draw() block items have BYBLOCK colour after refresh', () => {
|
|
293
|
+
const dim = makeDim();
|
|
294
|
+
dim.draw({});
|
|
295
|
+
for (const item of dim.block.items) {
|
|
296
|
+
expect(item.entityColour.aci).toBe(0); // BYBLOCK = ACI 0
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
239
300
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AlignedDimension } from '../../core/dimensions/alignedDimension.js';
|
|
2
|
+
import { BaseLinearDimension } from '../../core/dimensions/baseLinearDimension.js';
|
|
3
|
+
import { Point } from '../../core/entities/point.js';
|
|
4
|
+
import { DesignCore } from '../../core/designCore.js';
|
|
5
|
+
import { Core } from '../../core/core/core.js';
|
|
6
|
+
|
|
7
|
+
// initialise core
|
|
8
|
+
new Core();
|
|
9
|
+
|
|
10
|
+
// ─── constructor ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
test('BaseLinearDimension can be instantiated', () => {
|
|
13
|
+
const dim = new BaseLinearDimension();
|
|
14
|
+
expect(dim).toBeInstanceOf(BaseLinearDimension);
|
|
15
|
+
expect(dim.points).toBeDefined();
|
|
16
|
+
expect(Array.isArray(dim.points)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ─── preview ─────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
test('BaseLinearDimension.preview - 0 points does nothing', () => {
|
|
22
|
+
const dim = new AlignedDimension();
|
|
23
|
+
DesignCore.Scene.auxiliaryEntities.clear();
|
|
24
|
+
DesignCore.Scene.previewEntities.clear();
|
|
25
|
+
|
|
26
|
+
expect(() => dim.preview()).not.toThrow();
|
|
27
|
+
expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
|
|
28
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('BaseLinearDimension.preview - 1 point adds RubberBand to auxiliaryEntities', () => {
|
|
32
|
+
const dim = new AlignedDimension();
|
|
33
|
+
dim.points = [new Point(0, 0)];
|
|
34
|
+
|
|
35
|
+
const origAdd = DesignCore.Scene.auxiliaryEntities.add;
|
|
36
|
+
const added = [];
|
|
37
|
+
DesignCore.Scene.auxiliaryEntities.add = (item) => added.push(item);
|
|
38
|
+
DesignCore.Scene.previewEntities.clear();
|
|
39
|
+
|
|
40
|
+
expect(() => dim.preview()).not.toThrow();
|
|
41
|
+
expect(added.length).toBeGreaterThanOrEqual(1);
|
|
42
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
43
|
+
|
|
44
|
+
DesignCore.Scene.auxiliaryEntities.add = origAdd;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('BaseLinearDimension.preview - >1 points creates temp entity', () => {
|
|
48
|
+
const dim = new AlignedDimension();
|
|
49
|
+
dim.points = [new Point(0, 0), new Point(10, 0)];
|
|
50
|
+
|
|
51
|
+
const origCreate = DesignCore.Scene.previewEntities.create;
|
|
52
|
+
const created = [];
|
|
53
|
+
DesignCore.Scene.previewEntities.create = (type, data) => created.push({ type, data });
|
|
54
|
+
DesignCore.Scene.auxiliaryEntities.clear();
|
|
55
|
+
|
|
56
|
+
expect(() => dim.preview()).not.toThrow();
|
|
57
|
+
expect(created.length).toBeGreaterThanOrEqual(1);
|
|
58
|
+
expect(created[0].type).toBe(dim.type);
|
|
59
|
+
expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
|
|
60
|
+
|
|
61
|
+
DesignCore.Scene.previewEntities.create = origCreate;
|
|
62
|
+
});
|