@design-core/core 0.0.6 → 50.1.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/.eslintrc.json +4 -0
- package/.github/workflows/DesignCoreQA.yml +3 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/designCore.js +1 -1
- package/core/dimensions/alignedDimension.js +12 -11
- package/core/dimensions/angularDimension.js +38 -37
- package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +22 -21
- package/core/dimensions/diametricDimension.js +14 -13
- package/core/dimensions/dimension.js +62 -55
- package/core/dimensions/{baseDimension.js → dimensionBase.js} +73 -107
- package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +15 -14
- package/core/dimensions/radialDimension.js +12 -11
- package/core/dimensions/rotatedDimension.js +20 -19
- package/core/entities/arc.js +123 -156
- package/core/entities/arctext.js +222 -184
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +346 -273
- package/core/entities/insert.js +68 -83
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/point.js +19 -19
- package/core/entities/polyline.js +7 -7
- package/core/entities/{basePolyline.js → polylineBase.js} +47 -299
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +190 -201
- 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/boundingBox.js +11 -11
- package/core/lib/canvas.js +232 -168
- package/core/lib/clipboard.js +9 -3
- 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 +15 -14
- package/core/lib/dxf/dxfIterator.js +2 -2
- package/core/lib/entityManager.js +33 -26
- package/core/lib/inputManager.js +36 -12
- package/core/lib/intersect.js +25 -0
- package/core/lib/matrix.js +1 -1
- package/core/lib/patterns.js +9 -2
- 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 +28 -6
- package/core/lib/selectionManager.js +20 -20
- 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 +26 -7
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +60 -44
- package/core/tables/block.js +50 -81
- package/core/tables/dimStyle.js +5 -5
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/ltypeManager.js +1 -1
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tables/vport.js +2 -2
- package/core/tools/chamfer.js +117 -143
- package/core/tools/chamferFilletBase.js +138 -37
- package/core/tools/copy.js +11 -11
- package/core/tools/copybase.js +3 -3
- package/core/tools/copyclip.js +3 -3
- package/core/tools/cornerEntity.js +2 -2
- package/core/tools/cutclip.js +3 -3
- package/core/tools/explode.js +7 -7
- package/core/tools/extend.js +138 -25
- package/core/tools/fillet.js +92 -189
- package/core/tools/matchProp.js +3 -12
- package/core/tools/mirror.js +15 -14
- package/core/tools/move.js +8 -8
- package/core/tools/offset.js +31 -33
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/purge.js +10 -10
- package/core/tools/rotate.js +19 -16
- package/core/tools/scale.js +7 -7
- package/core/tools/trim.js +271 -29
- package/core/tools/zoom.js +1 -1
- package/cspell.json +321 -0
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +17 -10
- package/test/dimensions/angularDimension.test.js +13 -13
- package/test/dimensions/diametricDimension.test.js +15 -15
- package/test/dimensions/dimension.test.js +12 -12
- package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +73 -12
- package/test/dimensions/linearDimensionBase.test.js +62 -0
- package/test/dimensions/radialDimension.test.js +15 -15
- package/test/dimensions/rotatedDimension.test.js +18 -11
- package/test/entities/arc.test.js +83 -86
- package/test/entities/arctext.test.js +184 -57
- package/test/entities/circle.test.js +68 -96
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +569 -96
- package/test/entities/insert.test.js +99 -4
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/point.test.js +3 -3
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/{basePolyline.test.js → polylineBase.test.js} +192 -57
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +302 -77
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +5 -5
- 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/boundingBox.test.js +3 -3
- package/test/lib/canvas.test.js +744 -125
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +81 -20
- 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/scene.test.js +9 -9
- package/test/lib/selectionManager.test.js +3 -3
- package/test/lib/settings.test.js +5 -6
- package/test/lib/snapping.test.js +72 -130
- 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 +25 -25
- 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 +92 -13
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/tables/layerManager.test.js +2 -2
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +158 -6
- package/test/tools/chamfer.test.js +273 -51
- package/test/tools/chamferFilletBase.test.js +125 -36
- package/test/tools/copy.test.js +8 -8
- package/test/tools/copybase.test.js +2 -2
- package/test/tools/copyclip.test.js +2 -2
- package/test/tools/cornerEntity.test.js +8 -8
- package/test/tools/cutclip.test.js +2 -2
- package/test/tools/erase.test.js +6 -6
- package/test/tools/explode.test.js +5 -5
- package/test/tools/extend.test.js +373 -42
- package/test/tools/fillet.test.js +230 -49
- package/test/tools/matchProp.test.js +11 -11
- package/test/tools/mirror.test.js +39 -39
- package/test/tools/move.test.js +7 -7
- package/test/tools/offset.test.js +222 -55
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/purge.test.js +4 -4
- package/test/tools/rotate.test.js +39 -9
- package/test/tools/scale.test.js +33 -33
- package/test/tools/trim.test.js +579 -53
- package/test/tools/zoom.test.js +12 -12
- package/core/lib/selectionWindow.js +0 -83
package/test/tools/trim.test.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Point } from '../../core/entities/point.js';
|
|
|
3
3
|
import { Trim } from '../../core/tools/trim.js';
|
|
4
4
|
import { SingleSelection } from '../../core/lib/selectionManager.js';
|
|
5
5
|
import { Strings } from '../../core/lib/strings.js';
|
|
6
|
+
import { DesignCore } from '../../core/designCore.js';
|
|
6
7
|
import { expect, jest } from '@jest/globals';
|
|
7
8
|
import { withMockInput } from '../test-helpers/test-helpers.js';
|
|
8
9
|
|
|
@@ -23,9 +24,9 @@ test.each(inputScenarios)('Trim.execute handles $desc', async (scenario) => {
|
|
|
23
24
|
// clear all scene entities
|
|
24
25
|
core.scene.clear();
|
|
25
26
|
// create line
|
|
26
|
-
core.scene.
|
|
27
|
+
core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
|
|
27
28
|
// create circle
|
|
28
|
-
core.scene.
|
|
29
|
+
core.scene.addEntity('Circle', { points: [new Point(), new Point(10, 0)] });
|
|
29
30
|
// select line
|
|
30
31
|
core.scene.selectionManager.selectionSet.selectionSet.push(0);
|
|
31
32
|
|
|
@@ -34,7 +35,7 @@ test.each(inputScenarios)('Trim.execute handles $desc', async (scenario) => {
|
|
|
34
35
|
await trim.execute();
|
|
35
36
|
|
|
36
37
|
expect(trim.selectedBoundaryItems[0]).toEqual(core.scene.entities.get(0));
|
|
37
|
-
expect(trim.
|
|
38
|
+
expect(trim.selectedEntity).toBe(core.scene.entities.get(1));
|
|
38
39
|
expect(actionSpy).toHaveBeenCalled();
|
|
39
40
|
}, { extraMethods: { actionCommand: () => actionSpy() } });
|
|
40
41
|
});
|
|
@@ -59,12 +60,12 @@ test('Test Trim.action', () => {
|
|
|
59
60
|
// clear all scene entities
|
|
60
61
|
core.scene.clear();
|
|
61
62
|
// Add items to scene
|
|
62
|
-
core.scene.
|
|
63
|
-
core.scene.
|
|
63
|
+
core.scene.addEntity('Line', { points: [lineOneStart, lineOneEnd] });
|
|
64
|
+
core.scene.addEntity('Line', { points: [lineTwoStart, lineTwoEnd] });
|
|
64
65
|
// Select boundary item
|
|
65
66
|
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
66
67
|
// select item to trim
|
|
67
|
-
trim.
|
|
68
|
+
trim.selectedEntity = core.scene.entities.get(1);
|
|
68
69
|
// set mouse location - required for trim
|
|
69
70
|
core.mouse.setPosFromScenePoint(new Point(10, 50));
|
|
70
71
|
// Perform trim
|
|
@@ -90,12 +91,12 @@ test('Test Trim.action', () => {
|
|
|
90
91
|
// clear scene items
|
|
91
92
|
core.scene.clear();
|
|
92
93
|
// Add items to scene
|
|
93
|
-
core.scene.
|
|
94
|
-
core.scene.
|
|
94
|
+
core.scene.addEntity('Line', { points: [lineOneStart, lineOneEnd] });
|
|
95
|
+
core.scene.addEntity('Line', { points: [lineTwoStart, lineTwoEnd] });
|
|
95
96
|
// Select boundary item
|
|
96
97
|
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
97
98
|
// select item to trim
|
|
98
|
-
trim.
|
|
99
|
+
trim.selectedEntity = core.scene.entities.get(1);
|
|
99
100
|
// set mouse location - required for trim
|
|
100
101
|
core.mouse.setPosFromScenePoint(new Point(-10, 50));
|
|
101
102
|
// Perform trim
|
|
@@ -121,12 +122,12 @@ test('Test Trim.action', () => {
|
|
|
121
122
|
// clear scene items
|
|
122
123
|
core.scene.clear();
|
|
123
124
|
// Add items to scene
|
|
124
|
-
core.scene.
|
|
125
|
-
core.scene.
|
|
125
|
+
core.scene.addEntity('Line', { points: [lineOneStart, lineOneEnd] });
|
|
126
|
+
core.scene.addEntity('Line', { points: [lineTwoStart, lineTwoEnd] });
|
|
126
127
|
// Select boundary item
|
|
127
128
|
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
128
129
|
// select item to trim
|
|
129
|
-
trim.
|
|
130
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
130
131
|
// set mouse location - required for trim
|
|
131
132
|
core.mouse.setPosFromScenePoint(new Point(0, 60));
|
|
132
133
|
// Perform trim
|
|
@@ -154,12 +155,12 @@ test('Test Trim.action', () => {
|
|
|
154
155
|
// clear scene items
|
|
155
156
|
core.scene.clear();
|
|
156
157
|
// Add items to scene
|
|
157
|
-
core.scene.
|
|
158
|
-
core.scene.
|
|
158
|
+
core.scene.addEntity('Line', { points: [lineOneStart, lineOneEnd] });
|
|
159
|
+
core.scene.addEntity('Line', { points: [lineTwoStart, lineTwoEnd] });
|
|
159
160
|
// Select boundary item
|
|
160
161
|
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
161
162
|
// select item to trim
|
|
162
|
-
trim.
|
|
163
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
163
164
|
// set mouse location - required for trim
|
|
164
165
|
core.mouse.setPosFromScenePoint(new Point(0, 30));
|
|
165
166
|
// Perform trim
|
|
@@ -188,12 +189,12 @@ test('Test Trim.action', () => {
|
|
|
188
189
|
// clear scene items
|
|
189
190
|
core.scene.clear();
|
|
190
191
|
// Add items to scene
|
|
191
|
-
core.scene.
|
|
192
|
-
core.scene.
|
|
192
|
+
core.scene.addEntity('Line', { points: [lineOneStart, lineOneEnd] });
|
|
193
|
+
core.scene.addEntity('Line', { points: [crossingLineStart, crossingLineEnd] });
|
|
193
194
|
// Select boundary item
|
|
194
195
|
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
195
196
|
// select item to trim
|
|
196
|
-
trim.
|
|
197
|
+
trim.selectedEntity = core.scene.entities.get(1);
|
|
197
198
|
// set mouse location - required for trim
|
|
198
199
|
core.mouse.setPosFromScenePoint(new Point(25, 75));
|
|
199
200
|
// Perform trim
|
|
@@ -219,12 +220,12 @@ test('Test Trim.action', () => {
|
|
|
219
220
|
// clear scene items
|
|
220
221
|
core.scene.clear();
|
|
221
222
|
// Add items to scene
|
|
222
|
-
core.scene.
|
|
223
|
-
core.scene.
|
|
223
|
+
core.scene.addEntity('Line', { points: [lineOneStart, lineOneEnd] });
|
|
224
|
+
core.scene.addEntity('Line', { points: [crossingLineStart, crossingLineEnd] });
|
|
224
225
|
// Select boundary item
|
|
225
226
|
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
226
227
|
// select item to trim
|
|
227
|
-
trim.
|
|
228
|
+
trim.selectedEntity = core.scene.entities.get(1);
|
|
228
229
|
// set mouse location - required for trim
|
|
229
230
|
core.mouse.setPosFromScenePoint(new Point(-25, 25));
|
|
230
231
|
// Perform trim
|
|
@@ -252,11 +253,11 @@ test('Test Trim.action polyline - trim end segment', () => {
|
|
|
252
253
|
const trim = new Trim();
|
|
253
254
|
core.scene.clear();
|
|
254
255
|
|
|
255
|
-
core.scene.
|
|
256
|
-
core.scene.
|
|
256
|
+
core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0), new Point(100, 0)] });
|
|
257
|
+
core.scene.addEntity('Line', { points: [new Point(75, -50), new Point(75, 50)] });
|
|
257
258
|
|
|
258
259
|
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
259
|
-
trim.
|
|
260
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
260
261
|
core.mouse.setPosFromScenePoint(new Point(90, 0));
|
|
261
262
|
trim.action();
|
|
262
263
|
|
|
@@ -295,11 +296,11 @@ test('Test Trim.action polyline - trim start segment', () => {
|
|
|
295
296
|
const trim = new Trim();
|
|
296
297
|
core.scene.clear();
|
|
297
298
|
|
|
298
|
-
core.scene.
|
|
299
|
-
core.scene.
|
|
299
|
+
core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0), new Point(100, 0)] });
|
|
300
|
+
core.scene.addEntity('Line', { points: [new Point(25, -50), new Point(25, 50)] });
|
|
300
301
|
|
|
301
302
|
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
302
|
-
trim.
|
|
303
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
303
304
|
core.mouse.setPosFromScenePoint(new Point(10, 0));
|
|
304
305
|
trim.action();
|
|
305
306
|
|
|
@@ -333,12 +334,12 @@ test('Test Trim.action polyline - trim middle segment', () => {
|
|
|
333
334
|
const trim = new Trim();
|
|
334
335
|
core.scene.clear();
|
|
335
336
|
|
|
336
|
-
core.scene.
|
|
337
|
-
core.scene.
|
|
338
|
-
core.scene.
|
|
337
|
+
core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0), new Point(100, 0), new Point(150, 0)] });
|
|
338
|
+
core.scene.addEntity('Line', { points: [new Point(25, -50), new Point(25, 50)] });
|
|
339
|
+
core.scene.addEntity('Line', { points: [new Point(75, -50), new Point(75, 50)] });
|
|
339
340
|
|
|
340
341
|
trim.selectedBoundaryItems = [core.scene.entities.get(1), core.scene.entities.get(2)];
|
|
341
|
-
trim.
|
|
342
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
342
343
|
core.mouse.setPosFromScenePoint(new Point(50, 0));
|
|
343
344
|
trim.action();
|
|
344
345
|
|
|
@@ -375,7 +376,7 @@ test('Test Trim.action polyline - trim middle segment', () => {
|
|
|
375
376
|
test('Test Trim.action polyline - trim bulged arc segment', () => {
|
|
376
377
|
// Polyline: (0,0) with bulge=1 -> arc -> (100,0) -> straight -> (200,0)
|
|
377
378
|
// With bulge=1 (CCW) the semicircular arc goes through (50,-50)
|
|
378
|
-
// Arc
|
|
379
|
+
// Arc centre: (50,0), radius: 50
|
|
379
380
|
// Boundary: vertical line at x=50
|
|
380
381
|
// Intersection at (50,-50) — bottom of semicircle
|
|
381
382
|
// Mouse at (75,-40) — right of intersection on the arc
|
|
@@ -384,11 +385,11 @@ test('Test Trim.action polyline - trim bulged arc segment', () => {
|
|
|
384
385
|
const trim = new Trim();
|
|
385
386
|
core.scene.clear();
|
|
386
387
|
|
|
387
|
-
core.scene.
|
|
388
|
-
core.scene.
|
|
388
|
+
core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0, 1), new Point(100, 0), new Point(200, 0)] });
|
|
389
|
+
core.scene.addEntity('Line', { points: [new Point(50, -100), new Point(50, 100)] });
|
|
389
390
|
|
|
390
391
|
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
391
|
-
trim.
|
|
392
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
392
393
|
core.mouse.setPosFromScenePoint(new Point(75, -40));
|
|
393
394
|
trim.action();
|
|
394
395
|
|
|
@@ -431,10 +432,106 @@ test('Trim.preview does not throw', () => {
|
|
|
431
432
|
expect(() => trim.preview()).not.toThrow();
|
|
432
433
|
});
|
|
433
434
|
|
|
435
|
+
test('Trim.preview returns early when no boundary items set', () => {
|
|
436
|
+
core.scene.clear();
|
|
437
|
+
core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] });
|
|
438
|
+
DesignCore.Scene.previewEntities.clear();
|
|
439
|
+
|
|
440
|
+
const trim = new Trim();
|
|
441
|
+
// selectedBoundaryItems is empty by default
|
|
442
|
+
trim.preview();
|
|
443
|
+
|
|
444
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('Trim.preview returns early when findClosestItem returns undefined', () => {
|
|
448
|
+
core.scene.clear();
|
|
449
|
+
core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] });
|
|
450
|
+
core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
|
|
451
|
+
DesignCore.Scene.previewEntities.clear();
|
|
452
|
+
|
|
453
|
+
const trim = new Trim();
|
|
454
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
455
|
+
|
|
456
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
|
|
457
|
+
trim.preview();
|
|
458
|
+
findSpy.mockRestore();
|
|
459
|
+
|
|
460
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('Trim.preview returns early when hovered entity does not implement trim', () => {
|
|
464
|
+
// Text does not override Entity.trim() — preview must not emit a notification.
|
|
465
|
+
core.scene.clear();
|
|
466
|
+
core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary
|
|
467
|
+
core.scene.addEntity('Text', { points: [new Point(5, 0)], string: 'hello' }); // unsupported
|
|
468
|
+
DesignCore.Scene.previewEntities.clear();
|
|
469
|
+
|
|
470
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
471
|
+
const trim = new Trim();
|
|
472
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
473
|
+
|
|
474
|
+
const savedPointOnScene1 = DesignCore.Mouse.pointOnScene;
|
|
475
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
476
|
+
DesignCore.Mouse.pointOnScene = () => new Point(5, 0);
|
|
477
|
+
trim.preview();
|
|
478
|
+
findSpy.mockRestore();
|
|
479
|
+
notifySpy.mockRestore();
|
|
480
|
+
DesignCore.Mouse.pointOnScene = savedPointOnScene1;
|
|
481
|
+
|
|
482
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
483
|
+
expect(notifySpy).not.toHaveBeenCalled();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test('Trim.preview returns early when hovered entity has no intersections with boundary', () => {
|
|
487
|
+
// Two parallel horizontal lines — no intersection with the vertical boundary
|
|
488
|
+
core.scene.clear();
|
|
489
|
+
core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary (vertical)
|
|
490
|
+
core.scene.addEntity('Line', { points: [new Point(-10, 5), new Point(10, 5)] }); // hovered (horizontal, no intersection with boundary)
|
|
491
|
+
DesignCore.Scene.previewEntities.clear();
|
|
492
|
+
|
|
493
|
+
// Boundary is entity 0; hovered is entity 1 — they are parallel on the y-axis, so no intersection
|
|
494
|
+
core.scene.addEntity('Line', { points: [new Point(-10, 20), new Point(10, 20)] }); // extra line, no intersection
|
|
495
|
+
|
|
496
|
+
const trim = new Trim();
|
|
497
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(2)]; // horizontal boundary — parallel to hovered
|
|
498
|
+
|
|
499
|
+
const savedPointOnScene2 = DesignCore.Mouse.pointOnScene;
|
|
500
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
501
|
+
DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
|
|
502
|
+
trim.preview();
|
|
503
|
+
findSpy.mockRestore();
|
|
504
|
+
DesignCore.Mouse.pointOnScene = savedPointOnScene2;
|
|
505
|
+
|
|
506
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('Trim.preview populates previewEntities when entity can be trimmed', () => {
|
|
510
|
+
// Vertical boundary at x=0; horizontal line from (-10,0) to (10,0) is the hovered entity.
|
|
511
|
+
// Mouse at (5,0) → trims the right half, leaving the left half as a preview.
|
|
512
|
+
core.scene.clear();
|
|
513
|
+
core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary
|
|
514
|
+
core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(10, 0)] }); // hovered
|
|
515
|
+
DesignCore.Scene.previewEntities.clear();
|
|
516
|
+
|
|
517
|
+
const trim = new Trim();
|
|
518
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
519
|
+
|
|
520
|
+
const savedPointOnScene3 = DesignCore.Mouse.pointOnScene;
|
|
521
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
|
|
522
|
+
DesignCore.Mouse.pointOnScene = () => new Point(5, 0);
|
|
523
|
+
trim.preview();
|
|
524
|
+
findSpy.mockRestore();
|
|
525
|
+
DesignCore.Mouse.pointOnScene = savedPointOnScene3;
|
|
526
|
+
|
|
527
|
+
// dulled original + at least one trimmed survivor
|
|
528
|
+
expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(2);
|
|
529
|
+
});
|
|
530
|
+
|
|
434
531
|
test('Trim.execute returns early when boundary input is undefined', async () => {
|
|
435
532
|
core.scene.clear();
|
|
436
533
|
core.scene.selectionManager.reset();
|
|
437
|
-
core.scene.
|
|
534
|
+
core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
|
|
438
535
|
|
|
439
536
|
await withMockInput(core.scene, [undefined], async () => {
|
|
440
537
|
const trim = new Trim();
|
|
@@ -446,8 +543,8 @@ test('Trim.execute returns early when boundary input is undefined', async () =>
|
|
|
446
543
|
test('Trim.execute exits selection loop when selection input is undefined', async () => {
|
|
447
544
|
core.scene.clear();
|
|
448
545
|
core.scene.selectionManager.reset();
|
|
449
|
-
core.scene.
|
|
450
|
-
core.scene.
|
|
546
|
+
core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
|
|
547
|
+
core.scene.addEntity('Circle', { points: [new Point(), new Point(5, 0)] });
|
|
451
548
|
// preselect first item as boundary
|
|
452
549
|
core.scene.selectionManager.selectionSet.selectionSet.push(0);
|
|
453
550
|
|
|
@@ -463,9 +560,9 @@ test('Trim.execute exits selection loop when selection input is undefined', asyn
|
|
|
463
560
|
test('Trim.execute calls actionCommand once per trim selection', async () => {
|
|
464
561
|
core.scene.clear();
|
|
465
562
|
core.scene.selectionManager.reset();
|
|
466
|
-
core.scene.
|
|
467
|
-
core.scene.
|
|
468
|
-
core.scene.
|
|
563
|
+
core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] });
|
|
564
|
+
core.scene.addEntity('Circle', { points: [new Point(-5, 0), new Point(0, 0)] });
|
|
565
|
+
core.scene.addEntity('Circle', { points: [new Point(-5, 0), new Point(0, 0)] });
|
|
469
566
|
// preselect line as boundary
|
|
470
567
|
core.scene.selectionManager.selectionSet.selectionSet.push(0);
|
|
471
568
|
|
|
@@ -482,11 +579,11 @@ test('Trim.execute calls actionCommand once per trim selection', async () => {
|
|
|
482
579
|
);
|
|
483
580
|
});
|
|
484
581
|
|
|
485
|
-
test('Trim.action does nothing when
|
|
582
|
+
test('Trim.action does nothing when selectedEntity is null', () => {
|
|
486
583
|
core.scene.clear();
|
|
487
|
-
core.scene.
|
|
584
|
+
core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
|
|
488
585
|
const trim = new Trim();
|
|
489
|
-
trim.
|
|
586
|
+
trim.selectedEntity = null;
|
|
490
587
|
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
491
588
|
|
|
492
589
|
expect(() => trim.action()).not.toThrow();
|
|
@@ -494,41 +591,93 @@ test('Trim.action does nothing when selectedItem is null', () => {
|
|
|
494
591
|
|
|
495
592
|
test('Trim.action does nothing when selectedBoundaryItems is empty', () => {
|
|
496
593
|
core.scene.clear();
|
|
497
|
-
core.scene.
|
|
594
|
+
core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
|
|
498
595
|
const trim = new Trim();
|
|
499
|
-
trim.
|
|
596
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
500
597
|
trim.selectedBoundaryItems = [];
|
|
501
598
|
|
|
502
599
|
expect(() => trim.action()).not.toThrow();
|
|
503
|
-
expect(trim.
|
|
600
|
+
expect(trim.selectedEntity).toBeNull();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test('Trim.action notifies NOTRIM when selected entity lacks fromPolylinePoints', () => {
|
|
604
|
+
// Use a stub entity that has toPolylinePoints but no fromPolylinePoints.
|
|
605
|
+
// action() must notify and not throw.
|
|
606
|
+
core.scene.clear();
|
|
607
|
+
core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary
|
|
608
|
+
|
|
609
|
+
const trim = new Trim();
|
|
610
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
611
|
+
trim.selectedEntity = {
|
|
612
|
+
type: 'FakeEntity',
|
|
613
|
+
toPolylinePoints: () => [new Point(-5, 0), new Point(5, 0)],
|
|
614
|
+
// deliberately no fromPolylinePoints
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
618
|
+
trim.action();
|
|
619
|
+
|
|
620
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOTRIM));
|
|
621
|
+
expect(trim.selectedEntity).toBeNull();
|
|
622
|
+
notifySpy.mockRestore();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('Trim.preview silently skips entity that lacks fromPolylinePoints', () => {
|
|
626
|
+
// Stub entity implements toPolylinePoints but not fromPolylinePoints.
|
|
627
|
+
// preview() must return early without calling notify.
|
|
628
|
+
core.scene.clear();
|
|
629
|
+
core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] }); // boundary
|
|
630
|
+
DesignCore.Scene.previewEntities.clear();
|
|
631
|
+
|
|
632
|
+
const stubEntity = {
|
|
633
|
+
type: 'FakeEntity',
|
|
634
|
+
toPolylinePoints: () => [new Point(-5, 0), new Point(5, 0)],
|
|
635
|
+
// deliberately no fromPolylinePoints
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
639
|
+
const trim = new Trim();
|
|
640
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
641
|
+
|
|
642
|
+
const savedPointOnScene = DesignCore.Mouse.pointOnScene;
|
|
643
|
+
const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
|
|
644
|
+
jest.spyOn(core.scene.entities, 'get').mockReturnValue(stubEntity);
|
|
645
|
+
DesignCore.Mouse.pointOnScene = () => new Point(0, 0);
|
|
646
|
+
trim.preview();
|
|
647
|
+
findSpy.mockRestore();
|
|
648
|
+
jest.restoreAllMocks();
|
|
649
|
+
DesignCore.Mouse.pointOnScene = savedPointOnScene;
|
|
650
|
+
|
|
651
|
+
expect(DesignCore.Scene.previewEntities.count()).toBe(0);
|
|
652
|
+
expect(notifySpy).not.toHaveBeenCalled();
|
|
504
653
|
});
|
|
505
654
|
|
|
506
655
|
test('Trim.action notifies when no intersection found (parallel lines)', () => {
|
|
507
656
|
core.scene.clear();
|
|
508
657
|
// Two parallel vertical lines - no intersection
|
|
509
|
-
core.scene.
|
|
510
|
-
core.scene.
|
|
658
|
+
core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 100)] });
|
|
659
|
+
core.scene.addEntity('Line', { points: [new Point(10, 0), new Point(10, 100)] });
|
|
511
660
|
|
|
512
661
|
const trim = new Trim();
|
|
513
662
|
trim.selectedBoundaryItems = [core.scene.entities.get(0)];
|
|
514
|
-
trim.
|
|
663
|
+
trim.selectedEntity = core.scene.entities.get(1);
|
|
515
664
|
|
|
516
665
|
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
517
666
|
trim.action();
|
|
518
667
|
|
|
519
668
|
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOTRIM));
|
|
520
|
-
expect(trim.
|
|
669
|
+
expect(trim.selectedEntity).toBeNull();
|
|
521
670
|
notifySpy.mockRestore();
|
|
522
671
|
});
|
|
523
672
|
|
|
524
673
|
test('Trim.action notifies when boundary item equals selected item', () => {
|
|
525
674
|
core.scene.clear();
|
|
526
|
-
core.scene.
|
|
675
|
+
core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(100, 0)] });
|
|
527
676
|
const entity = core.scene.entities.get(0);
|
|
528
677
|
|
|
529
678
|
const trim = new Trim();
|
|
530
679
|
trim.selectedBoundaryItems = [entity];
|
|
531
|
-
trim.
|
|
680
|
+
trim.selectedEntity = entity;
|
|
532
681
|
|
|
533
682
|
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
534
683
|
trim.action();
|
|
@@ -536,3 +685,380 @@ test('Trim.action notifies when boundary item equals selected item', () => {
|
|
|
536
685
|
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOTRIM));
|
|
537
686
|
notifySpy.mockRestore();
|
|
538
687
|
});
|
|
688
|
+
|
|
689
|
+
test('Trim.action trims an arc', () => {
|
|
690
|
+
// Arc: centre (0,0), start (10,0), end (-10,0) — CCW upper semicircle through (0,10)
|
|
691
|
+
// Boundary: vertical line at x=0, intersects arc at (0,10)
|
|
692
|
+
// Mouse at (7,7) — in the right half, selects (10,0)→(0,10) segment for removal
|
|
693
|
+
// Expected: original arc removed, shorter arc from (0,10) to (-10,0) added
|
|
694
|
+
|
|
695
|
+
const trim = new Trim();
|
|
696
|
+
core.scene.clear();
|
|
697
|
+
|
|
698
|
+
core.scene.addEntity('Arc', { points: [new Point(0, 0), new Point(10, 0), new Point(-10, 0)] });
|
|
699
|
+
core.scene.addEntity('Line', { points: [new Point(0, -20), new Point(0, 20)] });
|
|
700
|
+
|
|
701
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
702
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
703
|
+
core.mouse.setPosFromScenePoint(new Point(7, 7));
|
|
704
|
+
trim.action();
|
|
705
|
+
|
|
706
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
707
|
+
|
|
708
|
+
let arc;
|
|
709
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
710
|
+
if (core.scene.entities.get(i).type === 'Arc') arc = core.scene.entities.get(i);
|
|
711
|
+
}
|
|
712
|
+
expect(arc).toBeDefined();
|
|
713
|
+
// Remaining arc runs from the intersection (0,10) to the original end (-10,0)
|
|
714
|
+
expect(arc.points[1].x).toBeCloseTo(0);
|
|
715
|
+
expect(arc.points[1].y).toBeCloseTo(10);
|
|
716
|
+
expect(arc.points[2].x).toBeCloseTo(-10);
|
|
717
|
+
expect(arc.points[2].y).toBeCloseTo(0);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('Trim.action trims a circle', () => {
|
|
721
|
+
// Circle: centre (0,0), radius 10
|
|
722
|
+
// Boundary: vertical line at x=0, intersects circle at (0,10) and (0,-10)
|
|
723
|
+
// Mouse at (8,0) — on the right side
|
|
724
|
+
// Expected: original circle removed, arc covering the right side added
|
|
725
|
+
|
|
726
|
+
const trim = new Trim();
|
|
727
|
+
core.scene.clear();
|
|
728
|
+
|
|
729
|
+
core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
730
|
+
core.scene.addEntity('Line', { points: [new Point(0, -20), new Point(0, 20)] });
|
|
731
|
+
|
|
732
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
733
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
734
|
+
core.mouse.setPosFromScenePoint(new Point(8, 0));
|
|
735
|
+
trim.action();
|
|
736
|
+
|
|
737
|
+
// circle removed, arc added, boundary line remains
|
|
738
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
739
|
+
|
|
740
|
+
let arc;
|
|
741
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
742
|
+
if (core.scene.entities.get(i).type === 'Arc') arc = core.scene.entities.get(i);
|
|
743
|
+
}
|
|
744
|
+
expect(arc).toBeDefined();
|
|
745
|
+
// Arc centre should match the original circle centre
|
|
746
|
+
expect(arc.points[0].x).toBeCloseTo(0, 10);
|
|
747
|
+
expect(arc.points[0].y).toBeCloseTo(0, 10);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test('Trim.action trims a closed polyline - produces open polyline with closed flag cleared', () => {
|
|
751
|
+
// Closed square: (0,0)→(100,0)→(100,100)→(0,100) with flag bit 1
|
|
752
|
+
// Boundary: vertical line at x=50
|
|
753
|
+
// Mouse at (75,50) — right of centre, selects the right side for removal
|
|
754
|
+
// Expected: one open polyline (50,100)→(0,100)→(0,0)→(50,0), flag bit 1 cleared
|
|
755
|
+
|
|
756
|
+
const trim = new Trim();
|
|
757
|
+
core.scene.clear();
|
|
758
|
+
|
|
759
|
+
// Five points where first === last → constructor pops last and sets flag=1
|
|
760
|
+
core.scene.addEntity('Lwpolyline', {
|
|
761
|
+
points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100), new Point(0, 0)],
|
|
762
|
+
});
|
|
763
|
+
core.scene.addEntity('Line', { points: [new Point(50, -10), new Point(50, 110)] });
|
|
764
|
+
|
|
765
|
+
const polyline = core.scene.entities.get(0);
|
|
766
|
+
expect(polyline.flags.hasFlag(1)).toBe(true); // confirm closed before trim
|
|
767
|
+
|
|
768
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
769
|
+
trim.selectedEntity = polyline;
|
|
770
|
+
core.mouse.setPosFromScenePoint(new Point(75, 50));
|
|
771
|
+
trim.action();
|
|
772
|
+
|
|
773
|
+
// Original closed polyline removed, one open polyline added, boundary line remains
|
|
774
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
775
|
+
|
|
776
|
+
let trimmed;
|
|
777
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
778
|
+
if (core.scene.entities.get(i).type === 'Polyline') trimmed = core.scene.entities.get(i);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
expect(trimmed).toBeDefined();
|
|
782
|
+
expect(trimmed.flags.hasFlag(1)).toBe(false); // closed flag removed
|
|
783
|
+
expect(trimmed.points.length).toBe(4);
|
|
784
|
+
// Joined path runs from one intersection, around the left side, to the other
|
|
785
|
+
expect(trimmed.points[0].x).toBeCloseTo(50);
|
|
786
|
+
expect(trimmed.points[3].x).toBeCloseTo(50);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test('Trim.action trims middle of a line - circle boundary creates two intersections on the same segment', () => {
|
|
790
|
+
// A circle boundary intersects a single-segment line at two points, both on
|
|
791
|
+
// segment index 1. The sort comparator falls through to the
|
|
792
|
+
// positionAlongSegment comparison (line 179 of trim.js) because the segment
|
|
793
|
+
// indices are equal.
|
|
794
|
+
// Circle: centre (50,0) radius 20 → intersects the x-axis at (30,0) and (70,0)
|
|
795
|
+
// Line: (0,0) → (100,0), mouse at (50,0) — inside the circle
|
|
796
|
+
// Expected: original line removed, two shorter lines (0→30) and (70→100) added
|
|
797
|
+
|
|
798
|
+
const trim = new Trim();
|
|
799
|
+
core.scene.clear();
|
|
800
|
+
|
|
801
|
+
core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(100, 0)] });
|
|
802
|
+
core.scene.addEntity('Circle', { points: [new Point(50, 0), new Point(70, 0)] }); // radius 20
|
|
803
|
+
|
|
804
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
805
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
806
|
+
core.mouse.setPosFromScenePoint(new Point(50, 0));
|
|
807
|
+
trim.action();
|
|
808
|
+
|
|
809
|
+
// original line removed; two shorter lines added; circle remains
|
|
810
|
+
expect(core.scene.entities.count()).toBe(3);
|
|
811
|
+
|
|
812
|
+
const lines = [];
|
|
813
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
814
|
+
if (core.scene.entities.get(i).type === 'Line') lines.push(core.scene.entities.get(i));
|
|
815
|
+
}
|
|
816
|
+
expect(lines.length).toBe(2);
|
|
817
|
+
lines.sort((a, b) => a.points[0].x - b.points[0].x);
|
|
818
|
+
|
|
819
|
+
expect(lines[0].points[0].x).toBeCloseTo(0);
|
|
820
|
+
expect(lines[0].points[1].x).toBeCloseTo(30);
|
|
821
|
+
expect(lines[1].points[0].x).toBeCloseTo(70);
|
|
822
|
+
expect(lines[1].points[1].x).toBeCloseTo(100);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
test('Trim.execute catches and logs errors thrown during input', async () => {
|
|
826
|
+
// Force an error inside execute() to cover the catch block (line 69 of trim.js)
|
|
827
|
+
core.scene.clear();
|
|
828
|
+
core.scene.selectionManager.reset();
|
|
829
|
+
|
|
830
|
+
const requestInputSpy = jest.spyOn(DesignCore.Scene.inputManager, 'requestInput')
|
|
831
|
+
.mockRejectedValue(new Error('mocked error'));
|
|
832
|
+
|
|
833
|
+
const trim = new Trim();
|
|
834
|
+
// execute() should resolve without re-throwing because the catch block handles it
|
|
835
|
+
await expect(trim.execute()).resolves.toBeUndefined();
|
|
836
|
+
|
|
837
|
+
requestInputSpy.mockRestore();
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test('Trim.action trims a circle near the seam (right side, mouse at angle 0)', () => {
|
|
841
|
+
// Circle: centre (0,0), radius 10.
|
|
842
|
+
// Circle.toPolylinePoints() always places the seam at angle 0 → vertex (10,0).
|
|
843
|
+
// The old code failed when the mouse was near that seam because both intersections
|
|
844
|
+
// appeared "after" the mouse in linear polyline order, leaving trimBefore=null.
|
|
845
|
+
//
|
|
846
|
+
// Boundary: vertical line at x=0, intersects at (0,10) [seg 2] and (0,-10) [seg 1].
|
|
847
|
+
// Mouse at (8,0) — on the RIGHT side, which is the forward arc from (0,-10) → (10,0) → (0,10)
|
|
848
|
+
// (crosses the seam vertex in the polyline chain but is a valid arc region).
|
|
849
|
+
// Expected: arc from (10,0) → (0,-10) covering the left half, trimming the seam-crossing right arc.
|
|
850
|
+
|
|
851
|
+
const trim = new Trim();
|
|
852
|
+
core.scene.clear();
|
|
853
|
+
|
|
854
|
+
core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
855
|
+
core.scene.addEntity('Line', { points: [new Point(0, -20), new Point(0, 20)] });
|
|
856
|
+
|
|
857
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
858
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
859
|
+
core.mouse.setPosFromScenePoint(new Point(8, 0));
|
|
860
|
+
trim.action();
|
|
861
|
+
|
|
862
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
863
|
+
|
|
864
|
+
let arc;
|
|
865
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
866
|
+
if (core.scene.entities.get(i).type === 'Arc') arc = core.scene.entities.get(i);
|
|
867
|
+
}
|
|
868
|
+
expect(arc).toBeDefined();
|
|
869
|
+
// Remaining arc is the left half: from (0,10) CCW through (-10,0) to (0,-10)
|
|
870
|
+
expect(arc.points[0].x).toBeCloseTo(0); // centre
|
|
871
|
+
expect(arc.points[0].y).toBeCloseTo(0);
|
|
872
|
+
expect(arc.points[1].x).toBeCloseTo(0); // start at (0,10)
|
|
873
|
+
expect(arc.points[1].y).toBeCloseTo(10);
|
|
874
|
+
expect(arc.points[2].x).toBeCloseTo(0); // end at (0,-10)
|
|
875
|
+
expect(arc.points[2].y).toBeCloseTo(-10);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test('Trim.action trims a closed polyline near the seam vertex', () => {
|
|
879
|
+
// Closed square drawn starting from (0,0): seam is at vertex (0,0).
|
|
880
|
+
// Boundary: horizontal line at y=50, intersects:
|
|
881
|
+
// left edge (closing segment (0,100)→(0,0)→...) at (0,50) — segment 4 (closing seg)
|
|
882
|
+
// right edge (100,0)→(100,100) at (100,50) — segment 2
|
|
883
|
+
//
|
|
884
|
+
// Mouse at (50,25) — in the BOTTOM half, which is the forward arc from intA=(100,50)
|
|
885
|
+
// through the bottom vertices to intB=(0,50). This forward arc crosses the seam vertex.
|
|
886
|
+
//
|
|
887
|
+
// Expected: open polyline (100,50)→(100,0)→(0,0)→(0,50), flag bit 1 cleared.
|
|
888
|
+
|
|
889
|
+
const trim = new Trim();
|
|
890
|
+
core.scene.clear();
|
|
891
|
+
|
|
892
|
+
core.scene.addEntity('Lwpolyline', {
|
|
893
|
+
points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100), new Point(0, 0)],
|
|
894
|
+
});
|
|
895
|
+
core.scene.addEntity('Line', { points: [new Point(-10, 50), new Point(110, 50)] });
|
|
896
|
+
|
|
897
|
+
const polyline = core.scene.entities.get(0);
|
|
898
|
+
expect(polyline.flags.hasFlag(1)).toBe(true);
|
|
899
|
+
|
|
900
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
901
|
+
trim.selectedEntity = polyline;
|
|
902
|
+
// Mouse in the BOTTOM half — forward arc between the two intersections, crosses the seam
|
|
903
|
+
core.mouse.setPosFromScenePoint(new Point(50, 25));
|
|
904
|
+
trim.action();
|
|
905
|
+
|
|
906
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
907
|
+
|
|
908
|
+
let trimmed;
|
|
909
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
910
|
+
if (core.scene.entities.get(i).type === 'Polyline') trimmed = core.scene.entities.get(i);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
expect(trimmed).toBeDefined();
|
|
914
|
+
expect(trimmed.flags.hasFlag(1)).toBe(false);
|
|
915
|
+
expect(trimmed.points.length).toBe(4);
|
|
916
|
+
// Forward arc: right edge from y=50 down to corner, across bottom, left edge up to y=50
|
|
917
|
+
expect(trimmed.points[0].x).toBeCloseTo(100);
|
|
918
|
+
expect(trimmed.points[0].y).toBeCloseTo(50);
|
|
919
|
+
expect(trimmed.points[3].x).toBeCloseTo(0);
|
|
920
|
+
expect(trimmed.points[3].y).toBeCloseTo(50);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
test('Trim.action trims an open polyline with two consecutive arc segments', () => {
|
|
924
|
+
// Polyline: (0,0,b=1) -> arc1 -> (100,0,b=1) -> arc2 -> (200,0)
|
|
925
|
+
// arc1 centre (50,0) radius 50, through (50,-50)
|
|
926
|
+
// arc2 centre (150,0) radius 50, through (150,-50)
|
|
927
|
+
// Boundary: vertical line at x=150, intersection at (150,-50) on arc2.
|
|
928
|
+
// Mouse at (120,-40): on arc2 BEFORE the intersection (in arc-travel order).
|
|
929
|
+
// Expected: keep right portion = (150,-50) -> (200,0); discard left side.
|
|
930
|
+
|
|
931
|
+
const trim = new Trim();
|
|
932
|
+
core.scene.clear();
|
|
933
|
+
|
|
934
|
+
core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0, 1), new Point(100, 0, 1), new Point(200, 0)] });
|
|
935
|
+
core.scene.addEntity('Line', { points: [new Point(150, -100), new Point(150, 100)] });
|
|
936
|
+
|
|
937
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
938
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
939
|
+
core.mouse.setPosFromScenePoint(new Point(120, -40));
|
|
940
|
+
trim.action();
|
|
941
|
+
|
|
942
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
943
|
+
|
|
944
|
+
let result;
|
|
945
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
946
|
+
if (core.scene.entities.get(i).type === 'Polyline') result = core.scene.entities.get(i);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
expect(result).toBeDefined();
|
|
950
|
+
expect(result.points.length).toBe(2);
|
|
951
|
+
expect(result.points[0].x).toBeCloseTo(150);
|
|
952
|
+
expect(result.points[0].y).toBeCloseTo(-50);
|
|
953
|
+
expect(result.points[1].x).toBeCloseTo(200);
|
|
954
|
+
expect(result.points[1].y).toBeCloseTo(0);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test('Trim.action trims an arc from the start (portionOne same-segment bulge)', () => {
|
|
958
|
+
// Arc: centre (0,0), start (10,0), end (-10,0) — CCW upper semicircle through (0,10).
|
|
959
|
+
// Boundary: vertical line at x=0, intersection at (0,10).
|
|
960
|
+
// Mouse at (-7,7) — LEFT of intersection, so trimBefore=(0,10), trimAfter=null.
|
|
961
|
+
// portionOne = startLoc(seg1,(10,0)) → trimBefore(seg1,(0,10)) — same-segment arc split.
|
|
962
|
+
// Expected: arc from (10,0) to (0,10), covering the right quarter.
|
|
963
|
+
|
|
964
|
+
const trim = new Trim();
|
|
965
|
+
core.scene.clear();
|
|
966
|
+
|
|
967
|
+
core.scene.addEntity('Arc', { points: [new Point(0, 0), new Point(10, 0), new Point(-10, 0)] });
|
|
968
|
+
core.scene.addEntity('Line', { points: [new Point(0, -20), new Point(0, 20)] });
|
|
969
|
+
|
|
970
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1)];
|
|
971
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
972
|
+
core.mouse.setPosFromScenePoint(new Point(-7, 7));
|
|
973
|
+
trim.action();
|
|
974
|
+
|
|
975
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
976
|
+
|
|
977
|
+
let arc;
|
|
978
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
979
|
+
if (core.scene.entities.get(i).type === 'Arc') arc = core.scene.entities.get(i);
|
|
980
|
+
}
|
|
981
|
+
expect(arc).toBeDefined();
|
|
982
|
+
// Kept portion runs from original start (10,0) to intersection (0,10)
|
|
983
|
+
expect(arc.points[1].x).toBeCloseTo(10);
|
|
984
|
+
expect(arc.points[1].y).toBeCloseTo(0);
|
|
985
|
+
expect(arc.points[2].x).toBeCloseTo(0);
|
|
986
|
+
expect(arc.points[2].y).toBeCloseTo(10);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
test('Trim.action trims the middle of an arc (two intersections on same arc segment)', () => {
|
|
990
|
+
// Arc: centre (0,0), start (10,0), end (-10,0) — CCW upper semicircle through (0,10).
|
|
991
|
+
// Two boundaries: vertical line at x=-5 and x=5.
|
|
992
|
+
// Intersections at (5,√75)≈(5,8.66) and (-5,√75)≈(-5,8.66), both on segment 1.
|
|
993
|
+
// Mouse at (0,9) — between both intersections at the top.
|
|
994
|
+
// Expected: two arcs: (10,0)→(5,8.66) and (-5,8.66)→(-10,0).
|
|
995
|
+
|
|
996
|
+
const trim = new Trim();
|
|
997
|
+
core.scene.clear();
|
|
998
|
+
|
|
999
|
+
core.scene.addEntity('Arc', { points: [new Point(0, 0), new Point(10, 0), new Point(-10, 0)] });
|
|
1000
|
+
core.scene.addEntity('Line', { points: [new Point(5, -20), new Point(5, 20)] });
|
|
1001
|
+
core.scene.addEntity('Line', { points: [new Point(-5, -20), new Point(-5, 20)] });
|
|
1002
|
+
|
|
1003
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1), core.scene.entities.get(2)];
|
|
1004
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
1005
|
+
core.mouse.setPosFromScenePoint(new Point(0, 9));
|
|
1006
|
+
trim.action();
|
|
1007
|
+
|
|
1008
|
+
// Original arc removed, two new arcs added, two boundary lines remain
|
|
1009
|
+
expect(core.scene.entities.count()).toBe(4);
|
|
1010
|
+
|
|
1011
|
+
const arcs = [];
|
|
1012
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
1013
|
+
if (core.scene.entities.get(i).type === 'Arc') arcs.push(core.scene.entities.get(i));
|
|
1014
|
+
}
|
|
1015
|
+
expect(arcs.length).toBe(2);
|
|
1016
|
+
arcs.sort((a, b) => a.points[1].x - b.points[1].x); // sort by start x
|
|
1017
|
+
|
|
1018
|
+
// Left arc: start near (-5,8.66), end at (-10,0)
|
|
1019
|
+
expect(arcs[0].points[1].x).toBeCloseTo(-5);
|
|
1020
|
+
expect(arcs[0].points[1].y).toBeCloseTo(Math.sqrt(75));
|
|
1021
|
+
expect(arcs[0].points[2].x).toBeCloseTo(-10);
|
|
1022
|
+
expect(arcs[0].points[2].y).toBeCloseTo(0);
|
|
1023
|
+
|
|
1024
|
+
// Right arc: start at (10,0), end near (5,8.66)
|
|
1025
|
+
expect(arcs[1].points[1].x).toBeCloseTo(10);
|
|
1026
|
+
expect(arcs[1].points[1].y).toBeCloseTo(0);
|
|
1027
|
+
expect(arcs[1].points[2].x).toBeCloseTo(5);
|
|
1028
|
+
expect(arcs[1].points[2].y).toBeCloseTo(Math.sqrt(75));
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test('Trim.action trims with multiple boundary items', () => {
|
|
1032
|
+
// Line: (0,0)→(200,0). Two separate boundary lines at x=50 and x=150.
|
|
1033
|
+
// Mouse at (100,0) — between both boundaries.
|
|
1034
|
+
// Expected: two lines: (0,0)→(50,0) and (150,0)→(200,0).
|
|
1035
|
+
|
|
1036
|
+
const trim = new Trim();
|
|
1037
|
+
core.scene.clear();
|
|
1038
|
+
|
|
1039
|
+
core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(200, 0)] });
|
|
1040
|
+
core.scene.addEntity('Line', { points: [new Point(50, -20), new Point(50, 20)] });
|
|
1041
|
+
core.scene.addEntity('Line', { points: [new Point(150, -20), new Point(150, 20)] });
|
|
1042
|
+
|
|
1043
|
+
trim.selectedBoundaryItems = [core.scene.entities.get(1), core.scene.entities.get(2)];
|
|
1044
|
+
trim.selectedEntity = core.scene.entities.get(0);
|
|
1045
|
+
core.mouse.setPosFromScenePoint(new Point(100, 0));
|
|
1046
|
+
trim.action();
|
|
1047
|
+
|
|
1048
|
+
// Original line removed, two new lines added, two boundary lines remain
|
|
1049
|
+
expect(core.scene.entities.count()).toBe(4);
|
|
1050
|
+
|
|
1051
|
+
const lines = [];
|
|
1052
|
+
for (let i = 0; i < core.scene.entities.count(); i++) {
|
|
1053
|
+
const e = core.scene.entities.get(i);
|
|
1054
|
+
// Identify result lines by both points being at y=0 (boundary lines are vertical)
|
|
1055
|
+
if (e.type === 'Line' && e.points[0].y === 0 && e.points[1].y === 0) lines.push(e);
|
|
1056
|
+
}
|
|
1057
|
+
expect(lines.length).toBe(2);
|
|
1058
|
+
lines.sort((a, b) => a.points[0].x - b.points[0].x);
|
|
1059
|
+
|
|
1060
|
+
expect(lines[0].points[0].x).toBeCloseTo(0);
|
|
1061
|
+
expect(lines[0].points[1].x).toBeCloseTo(50);
|
|
1062
|
+
expect(lines[1].points[0].x).toBeCloseTo(150);
|
|
1063
|
+
expect(lines[1].points[1].x).toBeCloseTo(200);
|
|
1064
|
+
});
|