@design-core/core 0.0.1 → 0.0.3
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 +14 -7
- package/.github/workflows/DesignCoreQA.yml +3 -1
- package/README.md +11 -4
- package/TODO.md +19 -0
- package/core/core/core.js +49 -42
- package/core/designCore.js +61 -2
- package/core/dimensions/alignedDimension.js +142 -0
- package/core/dimensions/angularDimension.js +550 -0
- package/core/dimensions/baseCircularDimension.js +244 -0
- package/core/dimensions/baseDimension.js +558 -0
- package/core/dimensions/baseLinearDimension.js +371 -0
- package/core/dimensions/diametricDimension.js +82 -0
- package/core/dimensions/dimension.js +298 -0
- package/core/dimensions/linearDimension.js +29 -0
- package/core/dimensions/radialDimension.js +78 -0
- package/core/dimensions/rotatedDimension.js +167 -0
- package/core/entities/arc.js +314 -60
- package/core/entities/arctext.js +642 -0
- package/core/entities/basePolyline.js +553 -94
- package/core/entities/circle.js +181 -54
- package/core/entities/entity.js +154 -74
- package/core/entities/hatch.js +807 -0
- package/core/entities/insert.js +200 -39
- package/core/entities/line.js +196 -134
- package/core/entities/lwpolyline.js +21 -44
- package/core/entities/point.js +194 -29
- package/core/entities/polyline.js +54 -7
- package/core/entities/rectangle.js +43 -13
- package/core/entities/solid.js +43 -6
- package/core/entities/text.js +324 -181
- package/core/lib/boundingBox.js +73 -13
- package/core/lib/canvas.js +256 -65
- package/core/lib/clipboard.js +198 -0
- package/core/lib/colour.js +144 -0
- package/core/lib/colours.js +367 -301
- package/core/lib/commandLine.js +16 -28
- package/core/lib/commandManager.js +115 -58
- package/core/lib/constants.js +23 -0
- package/core/lib/dxf/dxf.js +249 -34
- package/core/lib/dxf/dxfFile.js +23 -38
- package/core/lib/dxf/dxfIterator.js +17 -5
- package/core/lib/dxf/dxfRead.js +23 -7
- package/core/lib/dxf/dxfWrite.js +39 -129
- package/core/lib/dxf/handleManager.js +97 -0
- package/core/lib/dxf/sections/blocks.js +16 -2
- package/core/lib/dxf/sections/entities.js +17 -3
- package/core/lib/dxf/sections/header.js +10 -1
- package/core/lib/dxf/sections/objects.js +36 -0
- package/core/lib/dxf/sections/section.js +43 -5
- package/core/lib/dxf/sections/tables.js +17 -3
- package/core/lib/entityManager.js +178 -0
- package/core/lib/fileio.js +14 -4
- package/core/lib/headers.js +229 -0
- package/core/lib/input.js +46 -0
- package/core/lib/inputManager.js +199 -194
- package/core/lib/intersect.js +341 -468
- package/core/lib/logging.js +25 -1
- package/core/lib/matrix.js +6 -8
- package/core/lib/mouse.js +24 -79
- package/core/lib/patterns.js +690 -0
- package/core/lib/promptOptions.js +133 -0
- package/core/lib/scene.js +71 -174
- package/core/lib/selectionManager.js +74 -48
- package/core/lib/selectionWindow.js +24 -10
- package/core/lib/settings.js +18 -11
- package/core/lib/snapping.js +129 -21
- package/core/lib/stateManager.js +288 -0
- package/core/lib/strings.js +57 -5
- package/core/lib/utils.js +95 -11
- package/core/objects/dictionary.js +43 -0
- package/core/objects/dictionaryManager.js +80 -0
- package/core/properties/dimType.js +117 -0
- package/core/properties/flags.js +66 -0
- package/core/properties/property.js +26 -0
- package/core/properties/propertyManager.js +65 -19
- package/core/tables/appId.js +31 -0
- package/core/tables/appIdManager.js +44 -0
- package/core/tables/block.js +323 -0
- package/core/tables/blockManager.js +48 -0
- package/core/tables/blockRecord.js +29 -0
- package/core/tables/blockRecordManager.js +37 -0
- package/core/tables/dimStyle.js +627 -0
- package/core/tables/dimStyleManager.js +187 -0
- package/core/tables/endblk.js +28 -0
- package/core/tables/layer.js +219 -0
- package/core/tables/layerManager.js +47 -0
- package/core/tables/ltype.js +85 -0
- package/core/tables/ltypeManager.js +65 -0
- package/core/tables/style.js +145 -0
- package/core/tables/styleManager.js +55 -0
- package/core/tables/styleManagerBase.js +110 -0
- package/core/tables/tableManagerBase.js +354 -0
- package/core/tables/ucs.js +28 -0
- package/core/tables/ucsManager.js +44 -0
- package/core/tables/view.js +28 -0
- package/core/tables/viewManager.js +44 -0
- package/core/tables/vport.js +70 -0
- package/core/tables/vportManager.js +65 -0
- package/core/tools/chamfer.js +343 -0
- package/core/tools/chamferFilletBase.js +162 -0
- package/core/tools/copy.js +68 -40
- package/core/tools/copybase.js +65 -0
- package/core/tools/copyclip.js +59 -0
- package/core/tools/cornerEntity.js +138 -0
- package/core/tools/cutclip.js +72 -0
- package/core/tools/distance.js +37 -9
- package/core/tools/erase.js +43 -13
- package/core/tools/explode.js +117 -0
- package/core/tools/extend.js +71 -32
- package/core/tools/fillet.js +336 -0
- package/core/tools/identify.js +33 -6
- package/core/tools/matchProp.js +106 -0
- package/core/tools/mirror.js +141 -0
- package/core/tools/move.js +65 -32
- package/core/tools/offset.js +321 -0
- package/core/tools/pan.js +54 -0
- package/core/tools/pasteclip.js +93 -0
- package/core/tools/purge.js +90 -0
- package/core/tools/rotate.js +105 -70
- package/core/tools/scale.js +166 -0
- package/core/tools/tool.js +14 -4
- package/core/tools/trim.js +71 -32
- package/core/tools/zoom.js +152 -0
- package/package.json +3 -305
- package/test/dimensions/alignedDimension.test.js +211 -0
- package/test/dimensions/angularDimension.test.js +243 -0
- package/test/dimensions/baseDimension.test.js +239 -0
- package/test/dimensions/diametricDimension.test.js +203 -0
- package/test/dimensions/dimension.test.js +102 -0
- package/test/dimensions/radialDimension.test.js +203 -0
- package/test/dimensions/rotatedDimension.test.js +224 -0
- package/test/entities/arc.test.js +463 -41
- package/test/entities/arctext.test.js +329 -0
- package/test/entities/basePolyline.test.js +459 -10
- package/test/entities/circle.test.js +312 -7
- package/test/entities/entity.test.js +28 -9
- package/test/entities/hatch.test.js +606 -0
- package/test/entities/insert.test.js +90 -0
- package/test/entities/line.test.js +182 -6
- package/test/entities/point.test.js +375 -37
- package/test/entities/solid.test.js +156 -0
- package/test/entities/text.test.js +533 -7
- package/test/integration/exportIntegration.reference.dxf +1706 -0
- package/test/integration/exportIntegration.test.js +186 -0
- package/test/integration/importExportIntegration.test.js +126 -0
- package/test/integration/intersectionIntegration.test.js +880 -0
- package/test/integration/userInputIntegration.reference.dxf +1410 -0
- package/test/integration/userInputIntegration.test.js +269 -0
- package/test/lib/boundingBox.test.js +26 -2
- package/test/lib/canvas.test.js +613 -5
- package/test/lib/clipboard.test.js +70 -0
- package/test/lib/colour.test.js +114 -0
- package/test/lib/colours.test.js +63 -18
- package/test/lib/commandLine.test.js +159 -22
- package/test/lib/commandManager.test.js +4 -4
- package/test/lib/constants.test.js +9 -0
- package/test/lib/dxf/dxfFile.test.js +6 -2
- package/test/lib/dxf/dxfIterator.test.js +1 -1
- package/test/lib/dxf/dxfWrite.test.js +173 -0
- package/test/lib/dxf/handleManager.test.js +137 -0
- package/test/lib/dxf/sections/blocks.test.js +5 -5
- package/test/lib/dxf/sections/entities.test.js +4 -4
- package/test/lib/dxf/sections/header.test.js +18 -4
- package/test/lib/dxf/sections/objects.test.js +132 -0
- package/test/lib/dxf/sections/section.test.js +29 -29
- package/test/lib/dxf/sections/tables.test.js +3 -3
- package/test/lib/entityManager.test.js +163 -0
- package/test/lib/headers.test.js +233 -0
- package/test/lib/inputManager.test.js +1144 -5
- package/test/lib/intersect.test.js +454 -331
- package/test/lib/logging.test.js +1 -1
- package/test/lib/matrix.test.js +1 -1
- package/test/lib/mouse.test.js +327 -4
- package/test/lib/patterns.test.js +105 -0
- package/test/lib/promptOptions.test.js +179 -0
- package/test/lib/scene.test.js +16 -52
- package/test/lib/selectionManager.test.js +17 -5
- package/test/lib/settings.test.js +111 -5
- package/test/lib/snapping.test.js +142 -4
- package/test/lib/stateManager.test.js +155 -0
- package/test/lib/utils.test.js +81 -1
- package/test/objects/dictionary.test.js +81 -0
- package/test/objects/dictionaryManager.test.js +34 -0
- package/test/properties/dimType.test.js +62 -0
- package/test/properties/flags.test.js +164 -0
- package/test/properties/property.test.js +13 -0
- package/test/properties/propertyManager.test.js +10 -11
- package/test/tables/appId.test.js +46 -0
- package/test/tables/appIdManager.test.js +72 -0
- package/test/tables/block.test.js +157 -0
- package/test/tables/blockManager.test.js +52 -0
- package/test/tables/blockRecord.test.js +39 -0
- package/test/tables/blockRecordManager.test.js +60 -0
- package/test/tables/dimstyle.test.js +72 -0
- package/test/tables/endblk.test.js +30 -0
- package/test/tables/layer.test.js +95 -0
- package/test/tables/layerManager.test.js +172 -0
- package/test/{styles → tables}/ltype.test.js +7 -7
- package/test/{styles → tables}/ltypeManager.test.js +2 -2
- package/test/tables/style.test.js +44 -0
- package/test/tables/stylemanagerbase.test.js +220 -0
- package/test/tables/tableManagerBase.test.js +194 -0
- package/test/tables/ucs.test.js +37 -0
- package/test/tables/ucsManager.test.js +48 -0
- package/test/tables/view.test.js +37 -0
- package/test/tables/viewManager.test.js +48 -0
- package/test/tables/vport.test.js +117 -0
- package/test/tables/vportManager.test.js +54 -0
- package/test/test-helpers/test-helpers.js +66 -0
- package/test/tools/chamfer.test.js +970 -0
- package/test/tools/chamferFilletBase.test.js +446 -0
- package/test/tools/copy.test.js +22 -14
- package/test/tools/copybase.test.js +93 -0
- package/test/tools/copyclip.test.js +92 -0
- package/test/tools/cornerEntity.test.js +201 -0
- package/test/tools/cutclip.test.js +94 -0
- package/test/tools/distance.test.js +140 -18
- package/test/tools/erase.test.js +12 -12
- package/test/tools/explode.test.js +45 -0
- package/test/tools/extend.test.js +326 -44
- package/test/tools/fillet.test.js +831 -0
- package/test/tools/identify.test.js +115 -16
- package/test/tools/matchProp.test.js +84 -0
- package/test/tools/mirror.test.js +475 -0
- package/test/tools/move.test.js +75 -19
- package/test/tools/offset.test.js +748 -0
- package/test/tools/pan.test.js +72 -0
- package/test/tools/pasteclip.test.js +100 -0
- package/test/tools/purge.test.js +89 -0
- package/test/tools/rotate.test.js +89 -23
- package/test/tools/scale.test.js +529 -0
- package/test/tools/trim.test.js +419 -84
- package/test/tools/zoom.test.js +504 -0
- package/core/entities/alignedDimension.js +0 -279
- package/core/entities/angularDimension.js +0 -194
- package/core/entities/baseDimension.js +0 -227
- package/core/entities/block.js +0 -247
- package/core/entities/diametricDimension.js +0 -133
- package/core/entities/dimension.js +0 -202
- package/core/entities/radialDimension.js +0 -126
- package/core/styles/dimStyle.js +0 -719
- package/core/styles/dimStyleManager.js +0 -47
- package/core/styles/layer.js +0 -133
- package/core/styles/layerManager.js +0 -40
- package/core/styles/ltype.js +0 -86
- package/core/styles/ltypeManager.js +0 -50
- package/core/styles/style.js +0 -158
- package/core/styles/styleManager.js +0 -43
- package/core/styles/styleManagerBase.js +0 -299
- package/test/entities/block.test.js +0 -22
- package/test/styles/dimstyle.test.js +0 -19
- package/test/styles/layerManager.test.js +0 -150
- package/test/styles/style.test.js +0 -33
- package/test/styles/stylemanagerbase.test.js +0 -219
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
import { Core } from '../../core/core/core.js';
|
|
2
|
+
import { Point } from '../../core/entities/point.js';
|
|
3
|
+
import { Fillet } from '../../core/tools/fillet.js';
|
|
4
|
+
import { Strings } from '../../core/lib/strings.js';
|
|
5
|
+
import { SingleSelection } from '../../core/lib/selectionManager.js';
|
|
6
|
+
import { expect, jest } from '@jest/globals';
|
|
7
|
+
import { withMockInput } from '../test-helpers/test-helpers.js';
|
|
8
|
+
|
|
9
|
+
const core = new Core();
|
|
10
|
+
|
|
11
|
+
test('Fillet.register returns correct command object', () => {
|
|
12
|
+
const reg = Fillet.register();
|
|
13
|
+
expect(reg.command).toBe('Fillet');
|
|
14
|
+
expect(reg.shortcut).toBe('F');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('Fillet.execute selects two entities and calls executeCommand', async () => {
|
|
18
|
+
core.scene.clear();
|
|
19
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
20
|
+
core.scene.addItem('Line', { points: [new Point(5, -5), new Point(5, 5)] });
|
|
21
|
+
|
|
22
|
+
const executeCommandSpy = jest.fn();
|
|
23
|
+
|
|
24
|
+
await withMockInput(
|
|
25
|
+
core.scene,
|
|
26
|
+
[new SingleSelection(0, new Point(2, 0)), new SingleSelection(1, new Point(5, 2))],
|
|
27
|
+
async () => {
|
|
28
|
+
const fillet = new Fillet();
|
|
29
|
+
await fillet.execute();
|
|
30
|
+
expect(executeCommandSpy).toHaveBeenCalled();
|
|
31
|
+
},
|
|
32
|
+
{ extraMethods: { executeCommand: executeCommandSpy } },
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('Fillet.execute returns early when first input is undefined', async () => {
|
|
37
|
+
core.scene.clear();
|
|
38
|
+
const fillet = new Fillet();
|
|
39
|
+
await withMockInput(core.scene, [], async () => {
|
|
40
|
+
await fillet.execute(); // requestInput returns undefined → should not throw
|
|
41
|
+
});
|
|
42
|
+
// no assertion needed — just confirming no error is thrown
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Fillet.action does nothing when entities are not set', () => {
|
|
46
|
+
core.scene.clear();
|
|
47
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
48
|
+
const entityCountBefore = core.scene.entities.count();
|
|
49
|
+
|
|
50
|
+
const fillet = new Fillet();
|
|
51
|
+
fillet.action(); // no firstEntity / secondEntity
|
|
52
|
+
|
|
53
|
+
expect(core.scene.entities.count()).toBe(entityCountBefore);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Fillet.action notifies when entity type is not supported', () => {
|
|
57
|
+
core.scene.clear();
|
|
58
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
|
|
59
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
60
|
+
|
|
61
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
62
|
+
|
|
63
|
+
const fillet = new Fillet();
|
|
64
|
+
fillet.firstPick.entity = core.scene.entities.get(0); // Circle
|
|
65
|
+
fillet.secondPick.entity = core.scene.entities.get(1); // Line
|
|
66
|
+
fillet.firstPick.clickPoint = new Point(0, 0);
|
|
67
|
+
fillet.secondPick.clickPoint = new Point(5, 0);
|
|
68
|
+
fillet.action();
|
|
69
|
+
|
|
70
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOFILLET));
|
|
71
|
+
notifySpy.mockRestore();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('Fillet.action notifies for parallel lines', () => {
|
|
75
|
+
core.scene.clear();
|
|
76
|
+
// Two horizontal parallel lines
|
|
77
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
78
|
+
core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
|
|
79
|
+
|
|
80
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
81
|
+
|
|
82
|
+
const fillet = new Fillet();
|
|
83
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
84
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
85
|
+
fillet.firstPick.clickPoint = new Point(5, 0);
|
|
86
|
+
fillet.secondPick.clickPoint = new Point(5, 5);
|
|
87
|
+
fillet.action();
|
|
88
|
+
|
|
89
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.PARALLELLINES));
|
|
90
|
+
notifySpy.mockRestore();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('Fillet.action radius=0 trimMode=true trims both lines to intersection', () => {
|
|
94
|
+
// Two lines meeting at 90° at (10, 0), but the segments don't overlap.
|
|
95
|
+
// Horizontal: (0,0)→(10,0) and Vertical: (10,0)→(10,10) — already share the corner.
|
|
96
|
+
// Use lines that only touch when extended:
|
|
97
|
+
// Horizontal: (0,0)→(8,0), Vertical: (10,-10)→(10,-2)
|
|
98
|
+
// Their infinite extensions meet at (10, 0).
|
|
99
|
+
core.scene.clear();
|
|
100
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(8, 0)] });
|
|
101
|
+
core.scene.addItem('Line', { points: [new Point(10, -10), new Point(10, -2)] });
|
|
102
|
+
|
|
103
|
+
core.scene.headers.filletRadius = 0;
|
|
104
|
+
core.scene.headers.trimMode = true;
|
|
105
|
+
|
|
106
|
+
const fillet = new Fillet();
|
|
107
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
108
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
109
|
+
fillet.firstPick.clickPoint = new Point(4, 0);
|
|
110
|
+
fillet.secondPick.clickPoint = new Point(10, -6);
|
|
111
|
+
fillet.action();
|
|
112
|
+
|
|
113
|
+
const line1 = core.scene.entities.get(0);
|
|
114
|
+
const line2 = core.scene.entities.get(1);
|
|
115
|
+
|
|
116
|
+
// Line 1 should now end at the intersection (10, 0)
|
|
117
|
+
expect(line1.points[0].x).toBeCloseTo(0);
|
|
118
|
+
expect(line1.points[0].y).toBeCloseTo(0);
|
|
119
|
+
expect(line1.points[1].x).toBeCloseTo(10);
|
|
120
|
+
expect(line1.points[1].y).toBeCloseTo(0);
|
|
121
|
+
|
|
122
|
+
// Line 2 should now end at the intersection (10, 0)
|
|
123
|
+
expect(line2.points[0].x).toBeCloseTo(10);
|
|
124
|
+
expect(line2.points[0].y).toBeCloseTo(-10);
|
|
125
|
+
expect(line2.points[1].x).toBeCloseTo(10);
|
|
126
|
+
expect(line2.points[1].y).toBeCloseTo(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('Fillet.action radius=0 trimMode=false makes no changes', () => {
|
|
130
|
+
core.scene.clear();
|
|
131
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(8, 0)] });
|
|
132
|
+
core.scene.addItem('Line', { points: [new Point(10, -10), new Point(10, -2)] });
|
|
133
|
+
|
|
134
|
+
core.scene.headers.filletRadius = 0;
|
|
135
|
+
core.scene.headers.trimMode = false;
|
|
136
|
+
|
|
137
|
+
const fillet = new Fillet();
|
|
138
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
139
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
140
|
+
fillet.firstPick.clickPoint = new Point(4, 0);
|
|
141
|
+
fillet.secondPick.clickPoint = new Point(10, -6);
|
|
142
|
+
fillet.action();
|
|
143
|
+
|
|
144
|
+
// Entities unchanged — no new entities, same points
|
|
145
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
146
|
+
expect(core.scene.entities.get(0).points[1].x).toBeCloseTo(8);
|
|
147
|
+
expect(core.scene.entities.get(1).points[1].y).toBeCloseTo(-2);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('Fillet.action radius>0 trimMode=true adds arc and trims lines', () => {
|
|
151
|
+
// Perpendicular lines forming an L at (0,0):
|
|
152
|
+
// Horizontal: (-10,0)→(0,0) Vertical: (0,0)→(0,10)
|
|
153
|
+
// With radius 2, tangent points are at (-2,0) and (0,2)
|
|
154
|
+
core.scene.clear();
|
|
155
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
|
|
156
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
157
|
+
|
|
158
|
+
core.scene.headers.filletRadius = 2;
|
|
159
|
+
core.scene.headers.trimMode = true;
|
|
160
|
+
|
|
161
|
+
const fillet = new Fillet();
|
|
162
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
163
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
164
|
+
fillet.firstPick.clickPoint = new Point(-5, 0); // on the horizontal line, left of intersection
|
|
165
|
+
fillet.secondPick.clickPoint = new Point(0, 5); // on the vertical line, above intersection
|
|
166
|
+
fillet.action();
|
|
167
|
+
|
|
168
|
+
// An arc should have been added — scene now has 3 entities
|
|
169
|
+
expect(core.scene.entities.count()).toBe(3);
|
|
170
|
+
|
|
171
|
+
const line1 = core.scene.entities.get(0);
|
|
172
|
+
const line2 = core.scene.entities.get(1);
|
|
173
|
+
const arc = core.scene.entities.get(2);
|
|
174
|
+
|
|
175
|
+
expect(arc.type).toBe('Arc');
|
|
176
|
+
|
|
177
|
+
// Line 1 trimmed: kept end (-10,0), trimmed end at tangent (-2, 0)
|
|
178
|
+
expect(line1.points[0].x).toBeCloseTo(-10);
|
|
179
|
+
expect(line1.points[0].y).toBeCloseTo(0);
|
|
180
|
+
expect(line1.points[1].x).toBeCloseTo(-2);
|
|
181
|
+
expect(line1.points[1].y).toBeCloseTo(0);
|
|
182
|
+
|
|
183
|
+
// Line 2 trimmed: kept end (0,10), trimmed end at tangent (0, 2)
|
|
184
|
+
expect(line2.points[0].x).toBeCloseTo(0);
|
|
185
|
+
expect(line2.points[0].y).toBeCloseTo(10);
|
|
186
|
+
expect(line2.points[1].x).toBeCloseTo(0);
|
|
187
|
+
expect(line2.points[1].y).toBeCloseTo(2);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('Fillet.action radius>0 trimMode=false adds arc only, lines unchanged', () => {
|
|
191
|
+
core.scene.clear();
|
|
192
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
|
|
193
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
194
|
+
|
|
195
|
+
core.scene.headers.filletRadius = 2;
|
|
196
|
+
core.scene.headers.trimMode = false;
|
|
197
|
+
|
|
198
|
+
const fillet = new Fillet();
|
|
199
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
200
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
201
|
+
fillet.firstPick.clickPoint = new Point(-5, 0);
|
|
202
|
+
fillet.secondPick.clickPoint = new Point(0, 5);
|
|
203
|
+
fillet.action();
|
|
204
|
+
|
|
205
|
+
// Arc added, but lines not trimmed
|
|
206
|
+
expect(core.scene.entities.count()).toBe(3);
|
|
207
|
+
expect(core.scene.entities.get(2).type).toBe('Arc');
|
|
208
|
+
|
|
209
|
+
// Lines still have their original endpoints
|
|
210
|
+
expect(core.scene.entities.get(0).points[1].x).toBeCloseTo(0);
|
|
211
|
+
expect(core.scene.entities.get(0).points[1].y).toBeCloseTo(0);
|
|
212
|
+
expect(core.scene.entities.get(1).points[0].x).toBeCloseTo(0);
|
|
213
|
+
expect(core.scene.entities.get(1).points[0].y).toBeCloseTo(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('Fillet.action notifies when radius is too large for the segments', () => {
|
|
217
|
+
// Lines only 5 units long from intersection — radius 10 won't fit
|
|
218
|
+
core.scene.clear();
|
|
219
|
+
core.scene.addItem('Line', { points: [new Point(-5, 0), new Point(0, 0)] });
|
|
220
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 5)] });
|
|
221
|
+
|
|
222
|
+
core.scene.headers.filletRadius = 10;
|
|
223
|
+
core.scene.headers.trimMode = true;
|
|
224
|
+
|
|
225
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
226
|
+
|
|
227
|
+
const fillet = new Fillet();
|
|
228
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
229
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
230
|
+
fillet.firstPick.clickPoint = new Point(-2, 0);
|
|
231
|
+
fillet.secondPick.clickPoint = new Point(0, 2);
|
|
232
|
+
fillet.action();
|
|
233
|
+
|
|
234
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.RADIUSTOOLARGE));
|
|
235
|
+
// No arc added
|
|
236
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
237
|
+
notifySpy.mockRestore();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('Fillet.action arc centre is at correct position for 90° corner', () => {
|
|
241
|
+
// 90° corner at origin, radius 3 → arc centre at (-3, 3)
|
|
242
|
+
core.scene.clear();
|
|
243
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
|
|
244
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
245
|
+
|
|
246
|
+
core.scene.headers.filletRadius = 3;
|
|
247
|
+
core.scene.headers.trimMode = false;
|
|
248
|
+
|
|
249
|
+
const fillet = new Fillet();
|
|
250
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
251
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
252
|
+
fillet.firstPick.clickPoint = new Point(-5, 0);
|
|
253
|
+
fillet.secondPick.clickPoint = new Point(0, 5);
|
|
254
|
+
fillet.action();
|
|
255
|
+
|
|
256
|
+
const arc = core.scene.entities.get(2);
|
|
257
|
+
expect(arc.type).toBe('Arc');
|
|
258
|
+
|
|
259
|
+
// Arc centre point is points[0]
|
|
260
|
+
expect(arc.points[0].x).toBeCloseTo(-3);
|
|
261
|
+
expect(arc.points[0].y).toBeCloseTo(3);
|
|
262
|
+
|
|
263
|
+
// Tangent points
|
|
264
|
+
expect(arc.points[1].x).toBeCloseTo(-3);
|
|
265
|
+
expect(arc.points[1].y).toBeCloseTo(0);
|
|
266
|
+
expect(arc.points[2].x).toBeCloseTo(0);
|
|
267
|
+
expect(arc.points[2].y).toBeCloseTo(3);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('Fillet.action selects correct corner when lines form a cross', () => {
|
|
271
|
+
// Full cross: horizontal (-10,0)→(10,0), vertical (0,-10)→(0,10)
|
|
272
|
+
// Click on the top-right quadrant → fillet should be in that quadrant
|
|
273
|
+
core.scene.clear();
|
|
274
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
|
|
275
|
+
core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] });
|
|
276
|
+
|
|
277
|
+
core.scene.headers.filletRadius = 2;
|
|
278
|
+
core.scene.headers.trimMode = false;
|
|
279
|
+
|
|
280
|
+
const fillet = new Fillet();
|
|
281
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
282
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
283
|
+
fillet.firstPick.clickPoint = new Point(5, 0); // right side of horizontal
|
|
284
|
+
fillet.secondPick.clickPoint = new Point(0, 5); // top of vertical
|
|
285
|
+
fillet.action();
|
|
286
|
+
|
|
287
|
+
const arc = core.scene.entities.get(2);
|
|
288
|
+
expect(arc.type).toBe('Arc');
|
|
289
|
+
|
|
290
|
+
// Arc centre should be in the top-right quadrant
|
|
291
|
+
expect(arc.points[0].x).toBeGreaterThan(0);
|
|
292
|
+
expect(arc.points[0].y).toBeGreaterThan(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ─── action: second entity type guard ────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
test('Fillet.action notifies when second entity type is not supported', () => {
|
|
298
|
+
core.scene.clear();
|
|
299
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
300
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
|
|
301
|
+
|
|
302
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
303
|
+
|
|
304
|
+
const fillet = new Fillet();
|
|
305
|
+
fillet.firstPick.entity = core.scene.entities.get(0); // Line
|
|
306
|
+
fillet.secondPick.entity = core.scene.entities.get(1); // Circle
|
|
307
|
+
fillet.firstPick.clickPoint = new Point(5, 0);
|
|
308
|
+
fillet.secondPick.clickPoint = new Point(0, 0);
|
|
309
|
+
fillet.action();
|
|
310
|
+
|
|
311
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOFILLET));
|
|
312
|
+
notifySpy.mockRestore();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ─── action: click at intersection ───────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
test('Fillet.action notifies when click coincides with intersection', () => {
|
|
318
|
+
// L-corner at origin. Clicking firstClickPoint exactly at (0,0) makes
|
|
319
|
+
// firstClickDistance = 0, triggering the invalid-selection guard.
|
|
320
|
+
core.scene.clear();
|
|
321
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
|
|
322
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
323
|
+
|
|
324
|
+
core.scene.headers.filletRadius = 2;
|
|
325
|
+
core.scene.headers.trimMode = true;
|
|
326
|
+
|
|
327
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
328
|
+
|
|
329
|
+
const fillet = new Fillet();
|
|
330
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
331
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
332
|
+
fillet.firstPick.clickPoint = new Point(0, 0); // exactly at the intersection
|
|
333
|
+
fillet.secondPick.clickPoint = new Point(0, 5);
|
|
334
|
+
fillet.action();
|
|
335
|
+
|
|
336
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.SELECTION));
|
|
337
|
+
expect(core.scene.entities.count()).toBe(2); // no arc added
|
|
338
|
+
notifySpy.mockRestore();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('Fillet.action radius=0 notifies and makes no changes when click coincides with intersection', () => {
|
|
342
|
+
// Click at exactly the intersection point — clickDistance = 0.
|
|
343
|
+
// The guard must fire before applySharpTrim() runs.
|
|
344
|
+
core.scene.clear();
|
|
345
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
|
|
346
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
347
|
+
|
|
348
|
+
core.scene.headers.filletRadius = 0;
|
|
349
|
+
core.scene.headers.trimMode = true;
|
|
350
|
+
|
|
351
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
352
|
+
|
|
353
|
+
const fillet = new Fillet();
|
|
354
|
+
fillet.firstPick.entity = core.scene.entities.get(0);
|
|
355
|
+
fillet.secondPick.entity = core.scene.entities.get(1);
|
|
356
|
+
fillet.firstPick.clickPoint = new Point(0, 0); // exactly at the intersection
|
|
357
|
+
fillet.secondPick.clickPoint = new Point(0, 5);
|
|
358
|
+
fillet.action();
|
|
359
|
+
|
|
360
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.SELECTION));
|
|
361
|
+
// lines must be unchanged — applySharpTrim must not have run
|
|
362
|
+
expect(core.scene.entities.get(0).points[1].x).toBeCloseTo(0);
|
|
363
|
+
expect(core.scene.entities.get(0).points[1].y).toBeCloseTo(0);
|
|
364
|
+
notifySpy.mockRestore();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ─── execute: option branches ─────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
test('Fillet.execute sets filletRadius when Radius option is selected', async () => {
|
|
370
|
+
core.scene.headers.filletRadius = 0;
|
|
371
|
+
|
|
372
|
+
await withMockInput(
|
|
373
|
+
core.scene,
|
|
374
|
+
['Radius', 5, undefined],
|
|
375
|
+
async () => {
|
|
376
|
+
const fillet = new Fillet();
|
|
377
|
+
await fillet.execute();
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
expect(core.scene.headers.filletRadius).toBe(5);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('Fillet.execute notifies INVALIDNUMBER when negative radius is provided', async () => {
|
|
385
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
386
|
+
|
|
387
|
+
await withMockInput(
|
|
388
|
+
core.scene,
|
|
389
|
+
['Radius', -3, undefined],
|
|
390
|
+
async () => {
|
|
391
|
+
const fillet = new Fillet();
|
|
392
|
+
await fillet.execute();
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
|
|
397
|
+
notifySpy.mockRestore();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test('Fillet.execute sets trimMode to false when No trim option is selected', async () => {
|
|
401
|
+
core.scene.headers.trimMode = true;
|
|
402
|
+
|
|
403
|
+
await withMockInput(
|
|
404
|
+
core.scene,
|
|
405
|
+
['Trim', 'No trim', undefined],
|
|
406
|
+
async () => {
|
|
407
|
+
const fillet = new Fillet();
|
|
408
|
+
await fillet.execute();
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
expect(core.scene.headers.trimMode).toBe(false);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('Fillet.execute re-prompts and notifies when first selected entity is not a Line', async () => {
|
|
416
|
+
core.scene.clear();
|
|
417
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
|
|
418
|
+
|
|
419
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
420
|
+
|
|
421
|
+
await withMockInput(
|
|
422
|
+
core.scene,
|
|
423
|
+
[new SingleSelection(0, new Point(0, 0)), undefined],
|
|
424
|
+
async () => {
|
|
425
|
+
const fillet = new Fillet();
|
|
426
|
+
await fillet.execute();
|
|
427
|
+
},
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOFILLET));
|
|
431
|
+
notifySpy.mockRestore();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('Fillet.execute re-prompts and notifies when second selected entity is not a Line', async () => {
|
|
435
|
+
core.scene.clear();
|
|
436
|
+
core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
|
|
437
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
|
|
438
|
+
|
|
439
|
+
const notifySpy = jest.spyOn(core, 'notify');
|
|
440
|
+
|
|
441
|
+
await withMockInput(
|
|
442
|
+
core.scene,
|
|
443
|
+
[new SingleSelection(0, new Point(5, 0)), new SingleSelection(1, new Point(0, 0)), undefined],
|
|
444
|
+
async () => {
|
|
445
|
+
const fillet = new Fillet();
|
|
446
|
+
await fillet.execute();
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOFILLET));
|
|
451
|
+
notifySpy.mockRestore();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ─── action: polyline scenarios ───────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
// Geometry shared across the Line + Lwpolyline tests:
|
|
457
|
+
// Line: (0,0)→(0,10) (vertical)
|
|
458
|
+
// Lwpolyline: [(-20,0), (-10,0), (0,0), (10,0)] (horizontal, 3 segments)
|
|
459
|
+
// Infinite extensions of the y=0 and x=0 lines meet at (0,0).
|
|
460
|
+
// With radius 2 and a 90° corner:
|
|
461
|
+
// arc centre = (-2, 2)
|
|
462
|
+
// polyTangent = (-2, 0) (tangent on the polyline segment y=0)
|
|
463
|
+
// lineTangent = ( 0, 2) (tangent on the external line x=0)
|
|
464
|
+
|
|
465
|
+
test('Fillet.action radius>0 trimMode=true Line first + Lwpolyline second keepStart: arc embedded as bulge', () => {
|
|
466
|
+
// Click polyline near (-15,0) → segment 1. keepStart keeps the start portion.
|
|
467
|
+
core.scene.clear();
|
|
468
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
469
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
|
|
470
|
+
|
|
471
|
+
core.scene.headers.filletRadius = 2;
|
|
472
|
+
core.scene.headers.trimMode = true;
|
|
473
|
+
|
|
474
|
+
const lineEntity = core.scene.entities.get(0);
|
|
475
|
+
const polyEntity = core.scene.entities.get(1);
|
|
476
|
+
const polyClickPoint = new Point(-15, 0);
|
|
477
|
+
|
|
478
|
+
const fillet = new Fillet();
|
|
479
|
+
fillet.firstPick.entity = lineEntity;
|
|
480
|
+
fillet.firstPick.segment = lineEntity;
|
|
481
|
+
fillet.firstPick.segmentIndex = null;
|
|
482
|
+
fillet.firstPick.clickPoint = new Point(0, 5);
|
|
483
|
+
fillet.secondPick.entity = polyEntity;
|
|
484
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
|
|
485
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
|
|
486
|
+
fillet.secondPick.clickPoint = polyClickPoint;
|
|
487
|
+
fillet.action();
|
|
488
|
+
|
|
489
|
+
// Line is consumed into the polyline; no separate Arc entity
|
|
490
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
491
|
+
|
|
492
|
+
const poly = core.scene.entities.get(0);
|
|
493
|
+
// [(-20,0), polyTangent(-2,0)[bulge], lineTangent(0,2), lineKeptEnd(0,10)]
|
|
494
|
+
expect(poly.points.length).toBe(4);
|
|
495
|
+
expect(poly.points[0].x).toBeCloseTo(-20);
|
|
496
|
+
expect(poly.points[0].y).toBeCloseTo(0);
|
|
497
|
+
expect(poly.points[1].x).toBeCloseTo(-2);
|
|
498
|
+
expect(poly.points[1].y).toBeCloseTo(0);
|
|
499
|
+
expect(poly.points[1].bulge).not.toBe(0);
|
|
500
|
+
expect(poly.points[2].x).toBeCloseTo(0);
|
|
501
|
+
expect(poly.points[2].y).toBeCloseTo(2);
|
|
502
|
+
expect(poly.points[3].x).toBeCloseTo(0);
|
|
503
|
+
expect(poly.points[3].y).toBeCloseTo(10);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('Fillet.action radius>0 trimMode=true Lwpolyline first + Line second keepStart: arc direction correct', () => {
|
|
507
|
+
// Same geometry but entity order reversed — tests the polyToLineDir arc direction fix.
|
|
508
|
+
core.scene.clear();
|
|
509
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
|
|
510
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
511
|
+
|
|
512
|
+
core.scene.headers.filletRadius = 2;
|
|
513
|
+
core.scene.headers.trimMode = true;
|
|
514
|
+
|
|
515
|
+
const polyEntity = core.scene.entities.get(0);
|
|
516
|
+
const lineEntity = core.scene.entities.get(1);
|
|
517
|
+
const polyClickPoint = new Point(-15, 0);
|
|
518
|
+
|
|
519
|
+
const fillet = new Fillet();
|
|
520
|
+
fillet.firstPick.entity = polyEntity;
|
|
521
|
+
fillet.firstPick.segment = polyEntity.getClosestSegment(polyClickPoint);
|
|
522
|
+
fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
|
|
523
|
+
fillet.firstPick.clickPoint = polyClickPoint;
|
|
524
|
+
fillet.secondPick.entity = lineEntity;
|
|
525
|
+
fillet.secondPick.segment = lineEntity;
|
|
526
|
+
fillet.secondPick.segmentIndex = null;
|
|
527
|
+
fillet.secondPick.clickPoint = new Point(0, 5);
|
|
528
|
+
fillet.action();
|
|
529
|
+
|
|
530
|
+
// Line is consumed; no separate Arc entity
|
|
531
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
532
|
+
|
|
533
|
+
const poly = core.scene.entities.get(0);
|
|
534
|
+
expect(poly.points.length).toBe(4);
|
|
535
|
+
expect(poly.points[0].x).toBeCloseTo(-20);
|
|
536
|
+
expect(poly.points[1].x).toBeCloseTo(-2);
|
|
537
|
+
expect(poly.points[1].y).toBeCloseTo(0);
|
|
538
|
+
// CCW arc: bulge should be positive
|
|
539
|
+
expect(poly.points[1].bulge).toBeGreaterThan(0);
|
|
540
|
+
expect(poly.points[2].x).toBeCloseTo(0);
|
|
541
|
+
expect(poly.points[2].y).toBeCloseTo(2);
|
|
542
|
+
expect(poly.points[3].x).toBeCloseTo(0);
|
|
543
|
+
expect(poly.points[3].y).toBeCloseTo(10);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test('Fillet.action radius>0 trimMode=true Line + Lwpolyline keepEnd: keeps end portion of polyline', () => {
|
|
547
|
+
// Click polyline near (8,0) → segment 3 (right of intersection). keepEnd.
|
|
548
|
+
// arc centre = (2,2), polyTangent = (2,0), lineTangent = (0,2).
|
|
549
|
+
core.scene.clear();
|
|
550
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
551
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
|
|
552
|
+
|
|
553
|
+
core.scene.headers.filletRadius = 2;
|
|
554
|
+
core.scene.headers.trimMode = true;
|
|
555
|
+
|
|
556
|
+
const lineEntity = core.scene.entities.get(0);
|
|
557
|
+
const polyEntity = core.scene.entities.get(1);
|
|
558
|
+
const polyClickPoint = new Point(8, 0);
|
|
559
|
+
|
|
560
|
+
const fillet = new Fillet();
|
|
561
|
+
fillet.firstPick.entity = lineEntity;
|
|
562
|
+
fillet.firstPick.segment = lineEntity;
|
|
563
|
+
fillet.firstPick.segmentIndex = null;
|
|
564
|
+
fillet.firstPick.clickPoint = new Point(0, 5);
|
|
565
|
+
fillet.secondPick.entity = polyEntity;
|
|
566
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
|
|
567
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
|
|
568
|
+
fillet.secondPick.clickPoint = polyClickPoint;
|
|
569
|
+
fillet.action();
|
|
570
|
+
|
|
571
|
+
// Line consumed; end portion kept; no separate Arc entity
|
|
572
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
573
|
+
|
|
574
|
+
const poly = core.scene.entities.get(0);
|
|
575
|
+
// [lineKeptEnd(0,10), lineTangent(0,2)[bulge], polyTangent(2,0), (10,0)]
|
|
576
|
+
expect(poly.points.length).toBe(4);
|
|
577
|
+
expect(poly.points[0].x).toBeCloseTo(0);
|
|
578
|
+
expect(poly.points[0].y).toBeCloseTo(10);
|
|
579
|
+
expect(poly.points[1].x).toBeCloseTo(0);
|
|
580
|
+
expect(poly.points[1].y).toBeCloseTo(2);
|
|
581
|
+
expect(poly.points[1].bulge).not.toBe(0);
|
|
582
|
+
expect(poly.points[2].x).toBeCloseTo(2);
|
|
583
|
+
expect(poly.points[2].y).toBeCloseTo(0);
|
|
584
|
+
expect(poly.points[3].x).toBeCloseTo(10);
|
|
585
|
+
expect(poly.points[3].y).toBeCloseTo(0);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('Fillet.action radius=0 trimMode=true Line + Lwpolyline keepStart: line consumed to sharp corner', () => {
|
|
589
|
+
// radius=0 → sharp intersection at (0,0), no arc.
|
|
590
|
+
core.scene.clear();
|
|
591
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
592
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
|
|
593
|
+
|
|
594
|
+
core.scene.headers.filletRadius = 0;
|
|
595
|
+
core.scene.headers.trimMode = true;
|
|
596
|
+
|
|
597
|
+
const lineEntity = core.scene.entities.get(0);
|
|
598
|
+
const polyEntity = core.scene.entities.get(1);
|
|
599
|
+
const polyClickPoint = new Point(-15, 0);
|
|
600
|
+
|
|
601
|
+
const fillet = new Fillet();
|
|
602
|
+
fillet.firstPick.entity = lineEntity;
|
|
603
|
+
fillet.firstPick.segment = lineEntity;
|
|
604
|
+
fillet.firstPick.segmentIndex = null;
|
|
605
|
+
fillet.firstPick.clickPoint = new Point(0, 5);
|
|
606
|
+
fillet.secondPick.entity = polyEntity;
|
|
607
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
|
|
608
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
|
|
609
|
+
fillet.secondPick.clickPoint = polyClickPoint;
|
|
610
|
+
fillet.action();
|
|
611
|
+
|
|
612
|
+
// Line removed; polyline updated with sharp corner; no arc entity
|
|
613
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
614
|
+
|
|
615
|
+
const poly = core.scene.entities.get(0);
|
|
616
|
+
// [(-20,0), intersection(0,0), lineKeptEnd(0,10)]
|
|
617
|
+
expect(poly.points.length).toBe(3);
|
|
618
|
+
expect(poly.points[0].x).toBeCloseTo(-20);
|
|
619
|
+
expect(poly.points[0].y).toBeCloseTo(0);
|
|
620
|
+
expect(poly.points[1].x).toBeCloseTo(0);
|
|
621
|
+
expect(poly.points[1].y).toBeCloseTo(0);
|
|
622
|
+
expect(poly.points[2].x).toBeCloseTo(0);
|
|
623
|
+
expect(poly.points[2].y).toBeCloseTo(10);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test('Fillet.action radius>0 trimMode=true same Lwpolyline consecutive segments: arc inserted as bulge', () => {
|
|
627
|
+
// L-shaped polyline; fillet the inner corner. Arc encoded as a bulge — no separate Arc entity.
|
|
628
|
+
// Segment 1: (-10,0)→(0,0), Segment 2: (0,0)→(0,10). Corner at (0,0).
|
|
629
|
+
// radius=2 → arc centre (-2,2), tangents at (-2,0) and (0,2).
|
|
630
|
+
core.scene.clear();
|
|
631
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
|
|
632
|
+
|
|
633
|
+
core.scene.headers.filletRadius = 2;
|
|
634
|
+
core.scene.headers.trimMode = true;
|
|
635
|
+
|
|
636
|
+
const polyEntity = core.scene.entities.get(0);
|
|
637
|
+
|
|
638
|
+
const fillet = new Fillet();
|
|
639
|
+
fillet.firstPick.entity = polyEntity;
|
|
640
|
+
fillet.secondPick.entity = polyEntity;
|
|
641
|
+
fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(-5, 0));
|
|
642
|
+
fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(-5, 0));
|
|
643
|
+
fillet.firstPick.clickPoint = new Point(-5, 0);
|
|
644
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
|
|
645
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
|
|
646
|
+
fillet.secondPick.clickPoint = new Point(0, 5);
|
|
647
|
+
fillet.action();
|
|
648
|
+
|
|
649
|
+
// Arc is embedded as a bulge — no separate Arc entity
|
|
650
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
651
|
+
|
|
652
|
+
const poly = core.scene.entities.get(0);
|
|
653
|
+
// Corner vertex replaced: [(-10,0), (-2,0)[bulge], (0,2), (0,10)]
|
|
654
|
+
expect(poly.points.length).toBe(4);
|
|
655
|
+
expect(poly.points[0].x).toBeCloseTo(-10);
|
|
656
|
+
expect(poly.points[0].y).toBeCloseTo(0);
|
|
657
|
+
expect(poly.points[1].x).toBeCloseTo(-2);
|
|
658
|
+
expect(poly.points[1].y).toBeCloseTo(0);
|
|
659
|
+
expect(poly.points[1].bulge).not.toBe(0);
|
|
660
|
+
expect(poly.points[2].x).toBeCloseTo(0);
|
|
661
|
+
expect(poly.points[2].y).toBeCloseTo(2);
|
|
662
|
+
expect(poly.points[3].x).toBeCloseTo(0);
|
|
663
|
+
expect(poly.points[3].y).toBeCloseTo(10);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('Fillet.action radius>0 trimMode=true same Lwpolyline open ends: separate Arc entity fills gap', () => {
|
|
667
|
+
// 4-point polyline: [(-5,0),(0,0),(1,0),(1,5)]. Segments 1 and 3 are the open ends.
|
|
668
|
+
// They are non-adjacent (diff=2) so the open-ends path is taken.
|
|
669
|
+
// Infinite lines of seg1 (y=0) and seg3 (x=1) meet at (1,0).
|
|
670
|
+
// radius=2, clicks at (-3,0) and (1,3):
|
|
671
|
+
// arc centre = (-1,2), firstTangent = (-1,0), secondTangent = (1,2).
|
|
672
|
+
core.scene.clear();
|
|
673
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(-5, 0), new Point(0, 0), new Point(1, 0), new Point(1, 5)] });
|
|
674
|
+
|
|
675
|
+
core.scene.headers.filletRadius = 2;
|
|
676
|
+
core.scene.headers.trimMode = true;
|
|
677
|
+
|
|
678
|
+
const polyEntity = core.scene.entities.get(0);
|
|
679
|
+
|
|
680
|
+
const fillet = new Fillet();
|
|
681
|
+
fillet.firstPick.entity = polyEntity;
|
|
682
|
+
fillet.secondPick.entity = polyEntity;
|
|
683
|
+
fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(-3, 0)); // segment 1 (index 1)
|
|
684
|
+
fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(-3, 0));
|
|
685
|
+
fillet.firstPick.clickPoint = new Point(-3, 0);
|
|
686
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(1, 3)); // segment 3 (last, index 3)
|
|
687
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(1, 3));
|
|
688
|
+
fillet.secondPick.clickPoint = new Point(1, 3);
|
|
689
|
+
fillet.action();
|
|
690
|
+
|
|
691
|
+
// Separate Arc entity fills the gap between the trimmed open ends
|
|
692
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
693
|
+
|
|
694
|
+
const arc = core.scene.entities.get(1);
|
|
695
|
+
expect(arc.type).toBe('Arc');
|
|
696
|
+
// Arc centre at (-1,2)
|
|
697
|
+
expect(arc.points[0].x).toBeCloseTo(-1);
|
|
698
|
+
expect(arc.points[0].y).toBeCloseTo(2);
|
|
699
|
+
|
|
700
|
+
const poly = core.scene.entities.get(0);
|
|
701
|
+
expect(poly.points.length).toBe(4);
|
|
702
|
+
// Start of polyline trimmed to tangent on segment 1: (-1,0)
|
|
703
|
+
expect(poly.points[0].x).toBeCloseTo(-1);
|
|
704
|
+
expect(poly.points[0].y).toBeCloseTo(0);
|
|
705
|
+
// End of polyline trimmed to tangent on segment 3: (1,2)
|
|
706
|
+
expect(poly.points[3].x).toBeCloseTo(1);
|
|
707
|
+
expect(poly.points[3].y).toBeCloseTo(2);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
test('Fillet.action radius>0 trimMode=true closed Lwpolyline seg3+closing-seg: corner-splice at shared vertex', () => {
|
|
711
|
+
// Square: [(0,0),(10,0),(10,10),(0,10)], closed (flag 1).
|
|
712
|
+
// Closing segment (idx=4) goes (0,10)→(0,0); seg 3 goes (10,10)→(0,10).
|
|
713
|
+
// Corner at (0,10) — radius=2 replaces the shared vertex with arc bulge points.
|
|
714
|
+
core.scene.clear();
|
|
715
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
|
|
716
|
+
const polyEntity = core.scene.entities.get(0);
|
|
717
|
+
polyEntity.flags.setFlagValue(1);
|
|
718
|
+
|
|
719
|
+
core.scene.headers.filletRadius = 2;
|
|
720
|
+
core.scene.headers.trimMode = true;
|
|
721
|
+
|
|
722
|
+
const fillet = new Fillet();
|
|
723
|
+
fillet.firstPick.entity = polyEntity;
|
|
724
|
+
fillet.secondPick.entity = polyEntity;
|
|
725
|
+
fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(5, 10));
|
|
726
|
+
fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 10));
|
|
727
|
+
fillet.firstPick.clickPoint = new Point(5, 10);
|
|
728
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
|
|
729
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
|
|
730
|
+
fillet.secondPick.clickPoint = new Point(0, 5);
|
|
731
|
+
fillet.action();
|
|
732
|
+
|
|
733
|
+
// Corner-splice: no extra Arc entity, arc embedded as bulge
|
|
734
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
735
|
+
const poly = core.scene.entities.get(0);
|
|
736
|
+
// Shared vertex (0,10) replaced by two tangent points → total 5 points
|
|
737
|
+
expect(poly.points.length).toBe(5);
|
|
738
|
+
// One of the new points should carry a non-zero bulge
|
|
739
|
+
const hasBulge = poly.points.some((p) => p.bulge !== 0);
|
|
740
|
+
expect(hasBulge).toBe(true);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test('Fillet.action radius>0 trimMode=true closed Lwpolyline closing-wrap (seg4+seg1): NOT treated as open-ends', () => {
|
|
744
|
+
// Corner at (0,0) shared by closing seg (idx=4) and seg 1.
|
|
745
|
+
// Bug: without the closed-poly guard, this was treated as open-ends → wrong path.
|
|
746
|
+
core.scene.clear();
|
|
747
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
|
|
748
|
+
const polyEntity = core.scene.entities.get(0);
|
|
749
|
+
polyEntity.flags.setFlagValue(1);
|
|
750
|
+
|
|
751
|
+
core.scene.headers.filletRadius = 2;
|
|
752
|
+
core.scene.headers.trimMode = true;
|
|
753
|
+
|
|
754
|
+
const fillet = new Fillet();
|
|
755
|
+
fillet.firstPick.entity = polyEntity;
|
|
756
|
+
fillet.secondPick.entity = polyEntity;
|
|
757
|
+
fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
|
|
758
|
+
fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
|
|
759
|
+
fillet.firstPick.clickPoint = new Point(0, 5);
|
|
760
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
|
|
761
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
|
|
762
|
+
fillet.secondPick.clickPoint = new Point(5, 0);
|
|
763
|
+
fillet.action();
|
|
764
|
+
|
|
765
|
+
// Must take corner-splice path: no extra Arc entity
|
|
766
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
767
|
+
const poly = core.scene.entities.get(0);
|
|
768
|
+
// Shared vertex (0,0) replaced by two tangent points → total 5 points
|
|
769
|
+
expect(poly.points.length).toBe(5);
|
|
770
|
+
// Endpoints must NOT have been moved to the intersection (open-ends symptom)
|
|
771
|
+
const allAtOrigin = poly.points[0].x === 0 && poly.points[0].y === 0 &&
|
|
772
|
+
poly.points[poly.points.length - 1].x === 0 && poly.points[poly.points.length - 1].y === 0;
|
|
773
|
+
expect(allAtOrigin).toBe(false);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test('Fillet.action radius>0 trimMode=false closed Lwpolyline closing-wrap: standalone Arc only', () => {
|
|
777
|
+
// trimMode=false: no polyline mutation; only Arc entity added.
|
|
778
|
+
core.scene.clear();
|
|
779
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
|
|
780
|
+
const polyEntity = core.scene.entities.get(0);
|
|
781
|
+
polyEntity.flags.setFlagValue(1);
|
|
782
|
+
|
|
783
|
+
core.scene.headers.filletRadius = 2;
|
|
784
|
+
core.scene.headers.trimMode = false;
|
|
785
|
+
|
|
786
|
+
const fillet = new Fillet();
|
|
787
|
+
fillet.firstPick.entity = polyEntity;
|
|
788
|
+
fillet.secondPick.entity = polyEntity;
|
|
789
|
+
fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
|
|
790
|
+
fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
|
|
791
|
+
fillet.firstPick.clickPoint = new Point(0, 5);
|
|
792
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
|
|
793
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
|
|
794
|
+
fillet.secondPick.clickPoint = new Point(5, 0);
|
|
795
|
+
fillet.action();
|
|
796
|
+
|
|
797
|
+
// trimMode=false: polyline unchanged, arc added
|
|
798
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
799
|
+
expect(core.scene.entities.get(0).points.length).toBe(4); // polyline unchanged
|
|
800
|
+
expect(core.scene.entities.get(1).type).toBe('Arc');
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test('Fillet.action radius>0 trimMode=false Line + Lwpolyline: standalone arc added, entities unchanged', () => {
|
|
804
|
+
core.scene.clear();
|
|
805
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
806
|
+
core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
|
|
807
|
+
|
|
808
|
+
core.scene.headers.filletRadius = 2;
|
|
809
|
+
core.scene.headers.trimMode = false;
|
|
810
|
+
|
|
811
|
+
const lineEntity = core.scene.entities.get(0);
|
|
812
|
+
const polyEntity = core.scene.entities.get(1);
|
|
813
|
+
const polyClickPoint = new Point(-15, 0);
|
|
814
|
+
|
|
815
|
+
const fillet = new Fillet();
|
|
816
|
+
fillet.firstPick.entity = lineEntity;
|
|
817
|
+
fillet.firstPick.segment = lineEntity;
|
|
818
|
+
fillet.firstPick.segmentIndex = null;
|
|
819
|
+
fillet.firstPick.clickPoint = new Point(0, 5);
|
|
820
|
+
fillet.secondPick.entity = polyEntity;
|
|
821
|
+
fillet.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
|
|
822
|
+
fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
|
|
823
|
+
fillet.secondPick.clickPoint = polyClickPoint;
|
|
824
|
+
fillet.action();
|
|
825
|
+
|
|
826
|
+
// Arc added as standalone entity; original line and polyline untouched
|
|
827
|
+
expect(core.scene.entities.count()).toBe(3);
|
|
828
|
+
expect(core.scene.entities.get(2).type).toBe('Arc');
|
|
829
|
+
expect(core.scene.entities.get(0).points.length).toBe(2);
|
|
830
|
+
expect(core.scene.entities.get(1).points.length).toBe(4);
|
|
831
|
+
});
|