@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,748 @@
|
|
|
1
|
+
import { Core } from '../../core/core/core.js';
|
|
2
|
+
import { Point } from '../../core/entities/point.js';
|
|
3
|
+
import { Offset } from '../../core/tools/offset.js';
|
|
4
|
+
import { Line } from '../../core/entities/line.js';
|
|
5
|
+
import { Arc } from '../../core/entities/arc.js';
|
|
6
|
+
import { Circle } from '../../core/entities/circle.js';
|
|
7
|
+
import { expect, jest } from '@jest/globals';
|
|
8
|
+
|
|
9
|
+
const core = new Core();
|
|
10
|
+
|
|
11
|
+
const inputScenarios = [
|
|
12
|
+
{
|
|
13
|
+
desc: 'offset line to the left by distance 5',
|
|
14
|
+
entityType: 'Line',
|
|
15
|
+
entityData: { points: [new Point(0, 0), new Point(10, 0)] },
|
|
16
|
+
inputs: [5, { selectedItemIndex: 0 }, new Point(5, 5)],
|
|
17
|
+
checkIndex: 1,
|
|
18
|
+
expectedPoints: [new Point(0, 5), new Point(10, 5)],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
desc: 'offset line to the right by distance 5',
|
|
22
|
+
entityType: 'Line',
|
|
23
|
+
entityData: { points: [new Point(0, 0), new Point(10, 0)] },
|
|
24
|
+
inputs: [5, { selectedItemIndex: 0 }, new Point(5, -5)],
|
|
25
|
+
checkIndex: 1,
|
|
26
|
+
expectedPoints: [new Point(0, -5), new Point(10, -5)],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
desc: 'offset circle outward by distance 5',
|
|
30
|
+
entityType: 'Circle',
|
|
31
|
+
entityData: { points: [new Point(0, 0), new Point(10, 0)] },
|
|
32
|
+
inputs: [5, { selectedItemIndex: 0 }, new Point(20, 0)],
|
|
33
|
+
checkIndex: 1,
|
|
34
|
+
checkRadius: 15,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
desc: 'offset circle inward by distance 3',
|
|
38
|
+
entityType: 'Circle',
|
|
39
|
+
entityData: { points: [new Point(0, 0), new Point(10, 0)] },
|
|
40
|
+
inputs: [3, { selectedItemIndex: 0 }, new Point(5, 0)],
|
|
41
|
+
checkIndex: 1,
|
|
42
|
+
checkRadius: 7,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
desc: 'offset arc outward by distance 5',
|
|
46
|
+
entityType: 'Arc',
|
|
47
|
+
entityData: { points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] },
|
|
48
|
+
inputs: [5, { selectedItemIndex: 0 }, new Point(20, 0)],
|
|
49
|
+
checkIndex: 1,
|
|
50
|
+
checkRadius: 15,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
test.each(inputScenarios)('Offset.execute handles $desc', async (scenario) => {
|
|
55
|
+
const { entityType, entityData, inputs, checkIndex, expectedPoints, checkRadius } = scenario;
|
|
56
|
+
|
|
57
|
+
core.scene.clear();
|
|
58
|
+
core.scene.selectionManager.reset();
|
|
59
|
+
|
|
60
|
+
core.scene.addItem(entityType, entityData);
|
|
61
|
+
|
|
62
|
+
const origInputManager = core.scene.inputManager;
|
|
63
|
+
let callCount = 0;
|
|
64
|
+
core.scene.inputManager = {
|
|
65
|
+
requestInput: async () => {
|
|
66
|
+
if (callCount < inputs.length) {
|
|
67
|
+
return inputs[callCount++];
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
actionCommand: () => {
|
|
71
|
+
offset.action();
|
|
72
|
+
},
|
|
73
|
+
executeCommand: () => {},
|
|
74
|
+
reset: () => {},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const offset = new Offset();
|
|
78
|
+
await offset.execute();
|
|
79
|
+
|
|
80
|
+
const entity = core.scene.entities.get(checkIndex);
|
|
81
|
+
expect(entity).toBeDefined();
|
|
82
|
+
|
|
83
|
+
if (expectedPoints) {
|
|
84
|
+
expect(entity.points[0].x).toBeCloseTo(expectedPoints[0].x);
|
|
85
|
+
expect(entity.points[0].y).toBeCloseTo(expectedPoints[0].y);
|
|
86
|
+
expect(entity.points[1].x).toBeCloseTo(expectedPoints[1].x);
|
|
87
|
+
expect(entity.points[1].y).toBeCloseTo(expectedPoints[1].y);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (checkRadius !== undefined) {
|
|
91
|
+
const radius = entity.points[0].distance(entity.points[1]);
|
|
92
|
+
expect(radius).toBeCloseTo(checkRadius);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
core.scene.inputManager = origInputManager;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('Offset.execute - cancel at distance prompt does not offset', async () => {
|
|
99
|
+
core.scene.clear();
|
|
100
|
+
core.scene.selectionManager.reset();
|
|
101
|
+
|
|
102
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
103
|
+
|
|
104
|
+
const origInputManager = core.scene.inputManager;
|
|
105
|
+
core.scene.inputManager = {
|
|
106
|
+
requestInput: async () => undefined,
|
|
107
|
+
executeCommand: () => {},
|
|
108
|
+
reset: () => {},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const offset = new Offset();
|
|
112
|
+
await offset.execute();
|
|
113
|
+
|
|
114
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
115
|
+
|
|
116
|
+
core.scene.inputManager = origInputManager;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('Offset.execute - cancel at entity selection does not offset', async () => {
|
|
120
|
+
core.scene.clear();
|
|
121
|
+
core.scene.selectionManager.reset();
|
|
122
|
+
|
|
123
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
124
|
+
|
|
125
|
+
const origInputManager = core.scene.inputManager;
|
|
126
|
+
let callCount = 0;
|
|
127
|
+
const inputs = [5]; // provides distance, then undefined on selection
|
|
128
|
+
core.scene.inputManager = {
|
|
129
|
+
requestInput: async () => {
|
|
130
|
+
if (callCount < inputs.length) {
|
|
131
|
+
return inputs[callCount++];
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
executeCommand: () => {},
|
|
135
|
+
reset: () => {},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const offset = new Offset();
|
|
139
|
+
await offset.execute();
|
|
140
|
+
|
|
141
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
142
|
+
|
|
143
|
+
core.scene.inputManager = origInputManager;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('Offset.execute - cancel at side point does not offset', async () => {
|
|
147
|
+
core.scene.clear();
|
|
148
|
+
core.scene.selectionManager.reset();
|
|
149
|
+
|
|
150
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
151
|
+
|
|
152
|
+
const origInputManager = core.scene.inputManager;
|
|
153
|
+
let callCount = 0;
|
|
154
|
+
const inputs = [5, { selectedItemIndex: 0 }]; // distance + selection, then undefined
|
|
155
|
+
core.scene.inputManager = {
|
|
156
|
+
requestInput: async () => {
|
|
157
|
+
if (callCount < inputs.length) {
|
|
158
|
+
return inputs[callCount++];
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
executeCommand: () => {},
|
|
162
|
+
reset: () => {},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const offset = new Offset();
|
|
166
|
+
await offset.execute();
|
|
167
|
+
|
|
168
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
169
|
+
// selectedItem must be cleared so a stale action() call cannot create an unintended entity
|
|
170
|
+
expect(offset.selectedItem).toBeNull();
|
|
171
|
+
|
|
172
|
+
core.scene.inputManager = origInputManager;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Offset.execute - entity count increases by one per offset', async () => {
|
|
176
|
+
core.scene.clear();
|
|
177
|
+
core.scene.selectionManager.reset();
|
|
178
|
+
|
|
179
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
180
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
181
|
+
|
|
182
|
+
const entityCount = core.scene.entities.count();
|
|
183
|
+
|
|
184
|
+
const origInputManager = core.scene.inputManager;
|
|
185
|
+
let callCount = 0;
|
|
186
|
+
const inputs = [5, { selectedItemIndex: 0 }, new Point(5, 5)];
|
|
187
|
+
core.scene.inputManager = {
|
|
188
|
+
requestInput: async () => {
|
|
189
|
+
if (callCount < inputs.length) {
|
|
190
|
+
return inputs[callCount++];
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
actionCommand: () => {
|
|
194
|
+
offset.action();
|
|
195
|
+
},
|
|
196
|
+
executeCommand: () => {},
|
|
197
|
+
reset: () => {},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const offset = new Offset();
|
|
201
|
+
await offset.execute();
|
|
202
|
+
|
|
203
|
+
expect(core.scene.entities.count()).toBe(entityCount + 1);
|
|
204
|
+
|
|
205
|
+
core.scene.inputManager = origInputManager;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('Test Offset.action - offset line perpendicular', () => {
|
|
209
|
+
core.scene.clear();
|
|
210
|
+
core.scene.selectionManager.reset();
|
|
211
|
+
|
|
212
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
213
|
+
|
|
214
|
+
const offset = new Offset();
|
|
215
|
+
offset.selectedItem = core.scene.entities.get(0);
|
|
216
|
+
core.scene.headers.offsetDistance = 5;
|
|
217
|
+
offset.points = [new Point(5, 5)]; // side point above line
|
|
218
|
+
|
|
219
|
+
offset.action();
|
|
220
|
+
|
|
221
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
222
|
+
|
|
223
|
+
const newLine = core.scene.entities.get(1);
|
|
224
|
+
expect(newLine.points[0].x).toBeCloseTo(0);
|
|
225
|
+
expect(newLine.points[0].y).toBeCloseTo(5);
|
|
226
|
+
expect(newLine.points[1].x).toBeCloseTo(10);
|
|
227
|
+
expect(newLine.points[1].y).toBeCloseTo(5);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('Test Offset.action - offset vertical line', () => {
|
|
231
|
+
core.scene.clear();
|
|
232
|
+
core.scene.selectionManager.reset();
|
|
233
|
+
|
|
234
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
|
|
235
|
+
|
|
236
|
+
const offset = new Offset();
|
|
237
|
+
offset.selectedItem = core.scene.entities.get(0);
|
|
238
|
+
core.scene.headers.offsetDistance = 3;
|
|
239
|
+
offset.points = [new Point(5, 5)]; // side point to the right
|
|
240
|
+
|
|
241
|
+
offset.action();
|
|
242
|
+
|
|
243
|
+
const newLine = core.scene.entities.get(1);
|
|
244
|
+
expect(newLine.points[0].x).toBeCloseTo(3);
|
|
245
|
+
expect(newLine.points[0].y).toBeCloseTo(0);
|
|
246
|
+
expect(newLine.points[1].x).toBeCloseTo(3);
|
|
247
|
+
expect(newLine.points[1].y).toBeCloseTo(10);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('Test Offset.action - offset circle outward', () => {
|
|
251
|
+
core.scene.clear();
|
|
252
|
+
core.scene.selectionManager.reset();
|
|
253
|
+
|
|
254
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
255
|
+
|
|
256
|
+
const offset = new Offset();
|
|
257
|
+
offset.selectedItem = core.scene.entities.get(0);
|
|
258
|
+
core.scene.headers.offsetDistance = 5;
|
|
259
|
+
offset.points = [new Point(20, 0)]; // outside the circle
|
|
260
|
+
|
|
261
|
+
offset.action();
|
|
262
|
+
|
|
263
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
264
|
+
|
|
265
|
+
const newCircle = core.scene.entities.get(1);
|
|
266
|
+
// Centre should remain the same
|
|
267
|
+
expect(newCircle.points[0].x).toBeCloseTo(0);
|
|
268
|
+
expect(newCircle.points[0].y).toBeCloseTo(0);
|
|
269
|
+
// New radius should be 15
|
|
270
|
+
expect(newCircle.radius).toBeCloseTo(15);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('Test Offset.action - offset circle inward', () => {
|
|
274
|
+
core.scene.clear();
|
|
275
|
+
core.scene.selectionManager.reset();
|
|
276
|
+
|
|
277
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
278
|
+
|
|
279
|
+
const offset = new Offset();
|
|
280
|
+
offset.selectedItem = core.scene.entities.get(0);
|
|
281
|
+
core.scene.headers.offsetDistance = 3;
|
|
282
|
+
offset.points = [new Point(5, 0)]; // inside the circle
|
|
283
|
+
|
|
284
|
+
offset.action();
|
|
285
|
+
|
|
286
|
+
const newCircle = core.scene.entities.get(1);
|
|
287
|
+
expect(newCircle.points[0].x).toBeCloseTo(0);
|
|
288
|
+
expect(newCircle.points[0].y).toBeCloseTo(0);
|
|
289
|
+
expect(newCircle.radius).toBeCloseTo(7);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('Test Offset.action - offset circle inward returns null when radius would be zero', () => {
|
|
293
|
+
core.scene.clear();
|
|
294
|
+
core.scene.selectionManager.reset();
|
|
295
|
+
|
|
296
|
+
core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
297
|
+
|
|
298
|
+
const offset = new Offset();
|
|
299
|
+
offset.selectedItem = core.scene.entities.get(0);
|
|
300
|
+
core.scene.headers.offsetDistance = 15; // larger than radius
|
|
301
|
+
offset.points = [new Point(5, 0)]; // inside
|
|
302
|
+
|
|
303
|
+
offset.action();
|
|
304
|
+
|
|
305
|
+
// No new entity should be created
|
|
306
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('Test Offset.action - offset arc outward preserves angles', () => {
|
|
310
|
+
core.scene.clear();
|
|
311
|
+
core.scene.selectionManager.reset();
|
|
312
|
+
|
|
313
|
+
const center = new Point(0, 0);
|
|
314
|
+
const start = center.project(0, 10); // radius 10
|
|
315
|
+
const end = center.project(Math.PI / 2, 10);
|
|
316
|
+
|
|
317
|
+
core.scene.addItem('Arc', { points: [center, start, end] });
|
|
318
|
+
|
|
319
|
+
const offset = new Offset();
|
|
320
|
+
offset.selectedItem = core.scene.entities.get(0);
|
|
321
|
+
core.scene.headers.offsetDistance = 5;
|
|
322
|
+
offset.points = [new Point(20, 0)]; // outside
|
|
323
|
+
|
|
324
|
+
offset.action();
|
|
325
|
+
|
|
326
|
+
const newArc = core.scene.entities.get(1);
|
|
327
|
+
// New radius should be 15
|
|
328
|
+
const newRadius = newArc.points[0].distance(newArc.points[1]);
|
|
329
|
+
expect(newRadius).toBeCloseTo(15);
|
|
330
|
+
|
|
331
|
+
// Start and end angles should be preserved
|
|
332
|
+
const originalStartAngle = center.angle(start);
|
|
333
|
+
const originalEndAngle = center.angle(end);
|
|
334
|
+
const newStartAngle = newArc.points[0].angle(newArc.points[1]);
|
|
335
|
+
const newEndAngle = newArc.points[0].angle(newArc.points[2]);
|
|
336
|
+
expect(newStartAngle).toBeCloseTo(originalStartAngle);
|
|
337
|
+
expect(newEndAngle).toBeCloseTo(originalEndAngle);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('Test Offset.action - unsupported entity type does not create new entity', () => {
|
|
341
|
+
core.scene.clear();
|
|
342
|
+
core.scene.selectionManager.reset();
|
|
343
|
+
|
|
344
|
+
core.scene.addItem('Rectangle', { points: [new Point(0, 0), new Point(10, 10)] });
|
|
345
|
+
|
|
346
|
+
const offset = new Offset();
|
|
347
|
+
offset.selectedItem = core.scene.entities.get(0);
|
|
348
|
+
core.scene.headers.offsetDistance = 5;
|
|
349
|
+
offset.points = [new Point(20, 20)];
|
|
350
|
+
|
|
351
|
+
offset.action();
|
|
352
|
+
|
|
353
|
+
// No new entity should be created for unsupported types
|
|
354
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('Test Offset.getOffsetPoints - diagonal line via polyline path', () => {
|
|
358
|
+
const offset = new Offset();
|
|
359
|
+
|
|
360
|
+
const entity = new Line({ points: [new Point(0, 0), new Point(10, 10)] });
|
|
361
|
+
const sidePoint = new Point(0, 10); // above-left
|
|
362
|
+
const result = offset.getOffsetPoints(entity, sidePoint, 5);
|
|
363
|
+
|
|
364
|
+
expect(result).not.toBeNull();
|
|
365
|
+
// Reconstruct and check the offset line
|
|
366
|
+
const rebuilt = new Line({});
|
|
367
|
+
rebuilt.fromPolylinePoints(result);
|
|
368
|
+
|
|
369
|
+
// For a 45° line, the normal offset of 5 should shift by ~3.535 in each axis
|
|
370
|
+
const expected = 5 / Math.sqrt(2);
|
|
371
|
+
expect(rebuilt.points[0].x).toBeCloseTo(-expected);
|
|
372
|
+
expect(rebuilt.points[0].y).toBeCloseTo(expected);
|
|
373
|
+
expect(rebuilt.points[1].x).toBeCloseTo(10 - expected);
|
|
374
|
+
expect(rebuilt.points[1].y).toBeCloseTo(10 + expected);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('Test Offset.getThroughDistance - line', () => {
|
|
378
|
+
const offset = new Offset();
|
|
379
|
+
const entity = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
|
|
380
|
+
|
|
381
|
+
const throughPoint = new Point(5, 7);
|
|
382
|
+
const result = offset.getThroughDistance(entity, throughPoint);
|
|
383
|
+
expect(result).toBeCloseTo(7);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('Test Offset.getThroughDistance - circle', () => {
|
|
387
|
+
const offset = new Offset();
|
|
388
|
+
const entity = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
|
|
389
|
+
|
|
390
|
+
const throughPoint = new Point(15, 0);
|
|
391
|
+
const result = offset.getThroughDistance(entity, throughPoint);
|
|
392
|
+
expect(result).toBeCloseTo(5);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ─── getThroughDistance ───────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
test('Test Offset.getThroughDistance - arc uses same radial distance as circle', () => {
|
|
398
|
+
const offset = new Offset();
|
|
399
|
+
// Arc: center (0,0), radius 10
|
|
400
|
+
const entity = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] });
|
|
401
|
+
|
|
402
|
+
// Point inside the arc radius
|
|
403
|
+
const throughPoint = new Point(4, 0);
|
|
404
|
+
const result = offset.getThroughDistance(entity, throughPoint);
|
|
405
|
+
expect(result).toBeCloseTo(6); // |4 - 10|
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('Test Offset.getThroughDistance - polyline uses closestPoint distance', () => {
|
|
409
|
+
core.scene.clear();
|
|
410
|
+
core.scene.selectionManager.reset();
|
|
411
|
+
core.scene.addItem('Polyline', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
412
|
+
const entity = core.scene.entities.get(0);
|
|
413
|
+
|
|
414
|
+
const offset = new Offset();
|
|
415
|
+
const throughPoint = new Point(5, 3);
|
|
416
|
+
const result = offset.getThroughDistance(entity, throughPoint);
|
|
417
|
+
expect(result).toBeCloseTo(3);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('Test Offset.getThroughDistance - unsupported entity returns 0', () => {
|
|
421
|
+
const offset = new Offset();
|
|
422
|
+
const entity = { points: [] };
|
|
423
|
+
expect(offset.getThroughDistance(entity, new Point(5, 5))).toBe(0);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// ─── action edge cases ────────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
test('Test Offset.action - returns early when selectedItem is null', () => {
|
|
429
|
+
core.scene.clear();
|
|
430
|
+
core.scene.selectionManager.reset();
|
|
431
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
432
|
+
|
|
433
|
+
const offset = new Offset();
|
|
434
|
+
offset.selectedItem = null;
|
|
435
|
+
core.scene.headers.offsetDistance = 5;
|
|
436
|
+
offset.action();
|
|
437
|
+
|
|
438
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('Test Offset.action - returns early when offsetDistance is 0', () => {
|
|
442
|
+
core.scene.clear();
|
|
443
|
+
core.scene.selectionManager.reset();
|
|
444
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
445
|
+
|
|
446
|
+
const offset = new Offset();
|
|
447
|
+
offset.selectedItem = core.scene.entities.get(0);
|
|
448
|
+
core.scene.headers.offsetDistance = 0;
|
|
449
|
+
offset.points = [new Point(5, 5)];
|
|
450
|
+
offset.action();
|
|
451
|
+
|
|
452
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// ─── getOffsetPoints - arc ────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
test('Test Offset.getOffsetPoints - arc inward offset shrinks radius', () => {
|
|
458
|
+
const offset = new Offset();
|
|
459
|
+
const entity = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] });
|
|
460
|
+
|
|
461
|
+
// sidePoint inside the arc (between center and circumference)
|
|
462
|
+
const result = offset.getOffsetPoints(entity, new Point(5, 0), 3);
|
|
463
|
+
|
|
464
|
+
expect(result).not.toBeNull();
|
|
465
|
+
const rebuilt = new Arc({});
|
|
466
|
+
rebuilt.fromPolylinePoints(result);
|
|
467
|
+
expect(rebuilt.radius).toBeCloseTo(7);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('Test Offset.getOffsetPoints - arc returns null when inward offset collapses radius', () => {
|
|
471
|
+
const offset = new Offset();
|
|
472
|
+
const entity = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] });
|
|
473
|
+
|
|
474
|
+
// distance (15) > radius (10) when going inward
|
|
475
|
+
const result = offset.getOffsetPoints(entity, new Point(5, 0), 15);
|
|
476
|
+
|
|
477
|
+
expect(result).toBeNull();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// ─── getOffsetPolylinePoints ──────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
test('Test Offset.getOffsetPolylinePoints - returns null for fewer than 2 points', () => {
|
|
483
|
+
const offset = new Offset();
|
|
484
|
+
expect(offset.getOffsetPolylinePoints([new Point(0, 0)], false, new Point(0, 5), 1)).toBeNull();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('Test Offset.getOffsetPolylinePoints - open L-shape offset produces 3 points', () => {
|
|
488
|
+
const offset = new Offset();
|
|
489
|
+
// L-shape: (0,0) → (10,0) → (10,10)
|
|
490
|
+
const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10)];
|
|
491
|
+
|
|
492
|
+
// sidePoint above the bottom segment → offset upward/left
|
|
493
|
+
const result = offset.getOffsetPolylinePoints(points, false, new Point(5, 2), 1);
|
|
494
|
+
|
|
495
|
+
expect(result).not.toBeNull();
|
|
496
|
+
expect(result.length).toBe(3);
|
|
497
|
+
// First point: start of bottom segment offset upward
|
|
498
|
+
expect(result[0].x).toBeCloseTo(0);
|
|
499
|
+
expect(result[0].y).toBeCloseTo(1);
|
|
500
|
+
// Last point: end of right segment offset left
|
|
501
|
+
expect(result[2].x).toBeCloseTo(9);
|
|
502
|
+
expect(result[2].y).toBeCloseTo(10);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('Test Offset.getOffsetPolylinePoints - open polyline offset to other side', () => {
|
|
506
|
+
const offset = new Offset();
|
|
507
|
+
const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10)];
|
|
508
|
+
|
|
509
|
+
// sidePoint below the bottom segment → offset downward/right
|
|
510
|
+
const result = offset.getOffsetPolylinePoints(points, false, new Point(5, -2), 1);
|
|
511
|
+
|
|
512
|
+
expect(result).not.toBeNull();
|
|
513
|
+
expect(result.length).toBe(3);
|
|
514
|
+
expect(result[0].y).toBeCloseTo(-1);
|
|
515
|
+
expect(result[2].x).toBeCloseTo(11);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('Test Offset.getOffsetPolylinePoints - closed rectangle offset outward', () => {
|
|
519
|
+
const offset = new Offset();
|
|
520
|
+
// Closed rectangle: (0,0) → (10,0) → (10,10) → (0,10), closed
|
|
521
|
+
const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
|
|
522
|
+
|
|
523
|
+
// sidePoint outside the left edge
|
|
524
|
+
const result = offset.getOffsetPolylinePoints(points, true, new Point(-5, 5), 1);
|
|
525
|
+
|
|
526
|
+
expect(result).not.toBeNull();
|
|
527
|
+
expect(result.length).toBe(4); // closed: junction per segment, same count as point count
|
|
528
|
+
// All result corners should be further from the origin than the originals
|
|
529
|
+
const xs = result.map((p) => p.x);
|
|
530
|
+
const ys = result.map((p) => p.y);
|
|
531
|
+
expect(Math.min(...xs)).toBeCloseTo(-1);
|
|
532
|
+
expect(Math.max(...xs)).toBeCloseTo(11);
|
|
533
|
+
expect(Math.min(...ys)).toBeCloseTo(-1);
|
|
534
|
+
expect(Math.max(...ys)).toBeCloseTo(11);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('Test Offset.getOffsetPolylinePoints - arc segment (bulge) offset outward grows radius', () => {
|
|
538
|
+
const offset = new Offset();
|
|
539
|
+
// CW semicircle (bulge=-1) from (0,0) to (10,0): traces the TOP half through (5,5)
|
|
540
|
+
// center=(5,0), radius=5
|
|
541
|
+
const start = new Point(0, 0, -1); // bulge=-1 → CW, arc goes upward
|
|
542
|
+
const end = new Point(10, 0);
|
|
543
|
+
const points = [start, end];
|
|
544
|
+
|
|
545
|
+
// sidePoint above the arc (outside), offset outward by 1 → new radius = 6
|
|
546
|
+
const result = offset.getOffsetPolylinePoints(points, false, new Point(5, 10), 1);
|
|
547
|
+
|
|
548
|
+
expect(result).not.toBeNull();
|
|
549
|
+
expect(result.length).toBe(2);
|
|
550
|
+
const arcCenter = new Point(5, 0);
|
|
551
|
+
const newRadius = arcCenter.distance(result[0]);
|
|
552
|
+
expect(newRadius).toBeCloseTo(6);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// ─── findJunction ─────────────────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
test('Offset.findJunction - line/line returns intersection point', () => {
|
|
558
|
+
const offset = new Offset();
|
|
559
|
+
// Two offset line segments that would meet at (5, 2)
|
|
560
|
+
const seg = { A: new Point(0, 2), B: new Point(10, 2), bulge: 0 };
|
|
561
|
+
const nextSeg = { A: new Point(5, 0), B: new Point(5, 10), bulge: 0 };
|
|
562
|
+
const result = offset.findJunction(seg, nextSeg);
|
|
563
|
+
expect(result.x).toBeCloseTo(5);
|
|
564
|
+
expect(result.y).toBeCloseTo(2);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test('Offset.findJunction - parallel line/line falls back to midpoint', () => {
|
|
568
|
+
const offset = new Offset();
|
|
569
|
+
// Parallel lines — no intersection, midpoint between seg.B and nextSeg.A
|
|
570
|
+
const seg = { A: new Point(0, 1), B: new Point(4, 1), bulge: 0 };
|
|
571
|
+
const nextSeg = { A: new Point(6, 1), B: new Point(10, 1), bulge: 0 };
|
|
572
|
+
const result = offset.findJunction(seg, nextSeg);
|
|
573
|
+
expect(result.x).toBeCloseTo(5);
|
|
574
|
+
expect(result.y).toBeCloseTo(1);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('Offset.findJunction - arc/line returns circle-line intersection', () => {
|
|
578
|
+
const offset = new Offset();
|
|
579
|
+
// Arc: A=(-5,0) B=(5,0) bulge=1 → center (0,0), radius 5 via bulgeCentrePoint
|
|
580
|
+
// Line: vertical at x=3 → circle intersects at (3,4) and (3,-4)
|
|
581
|
+
const seg = { A: new Point(-5, 0), B: new Point(5, 0), bulge: 1 };
|
|
582
|
+
const nextSeg = { A: new Point(3, 5), B: new Point(3, 10), bulge: 0 };
|
|
583
|
+
const result = offset.findJunction(seg, nextSeg);
|
|
584
|
+
// midpoint of seg.B(5,0) and nextSeg.A(3,5) = (4,2.5) → closest intersection is (3,4)
|
|
585
|
+
expect(result.x).toBeCloseTo(3);
|
|
586
|
+
expect(result.y).toBeCloseTo(4);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('Offset.findJunction - line/arc returns line-circle intersection', () => {
|
|
590
|
+
const offset = new Offset();
|
|
591
|
+
// Line: y=0. Arc: A=(10,3) B=(0,3) bulge=1 → center (5,3), radius 5 via bulgeCentrePoint
|
|
592
|
+
// Circle intersects y=0 at x=1 and x=9
|
|
593
|
+
const seg = { A: new Point(0, 0), B: new Point(6, 0), bulge: 0 };
|
|
594
|
+
const nextSeg = { A: new Point(10, 3), B: new Point(0, 3), bulge: 1 };
|
|
595
|
+
const result = offset.findJunction(seg, nextSeg);
|
|
596
|
+
// midpoint of seg.B(6,0) and nextSeg.A(10,3) = (8,1.5) → closest intersection is (9,0)
|
|
597
|
+
expect(result.x).toBeCloseTo(9);
|
|
598
|
+
expect(result.y).toBeCloseTo(0);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('Offset.findJunction - arc/arc returns circle-circle intersection', () => {
|
|
602
|
+
const offset = new Offset();
|
|
603
|
+
// Circle 1: A=(-4,0) B=(4,0) bulge=1 → center (0,0), radius 4 via bulgeCentrePoint
|
|
604
|
+
// Circle 2: A=(0,4) B=(6,4) bulge=1 → center (3,4), radius 3 via bulgeCentrePoint
|
|
605
|
+
// Intersections: (0,4) and (96/25, 28/25)
|
|
606
|
+
const seg = { A: new Point(-4, 0), B: new Point(4, 0), bulge: 1 };
|
|
607
|
+
const nextSeg = { A: new Point(0, 4), B: new Point(6, 4), bulge: 1 };
|
|
608
|
+
const result = offset.findJunction(seg, nextSeg);
|
|
609
|
+
// midpoint of seg.B(4,0) and nextSeg.A(0,4) = (2,2) → closest is (96/25, 28/25) ≈ (3.84, 1.12)
|
|
610
|
+
expect(result.x).toBeCloseTo(3.84);
|
|
611
|
+
expect(result.y).toBeCloseTo(1.12);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('Offset.findJunction - non-intersecting arc/arc falls back to midpoint', () => {
|
|
615
|
+
const offset = new Offset();
|
|
616
|
+
// Two circles too far apart to intersect
|
|
617
|
+
// A=(0,2) B=(4,2) bulge=1 → center (2,2), radius 2 via bulgeCentrePoint
|
|
618
|
+
// A=(16,4) B=(20,4) bulge=1 → center (18,4), radius 2 via bulgeCentrePoint
|
|
619
|
+
const seg = { A: new Point(0, 2), B: new Point(4, 2), bulge: 1 };
|
|
620
|
+
const nextSeg = { A: new Point(16, 4), B: new Point(20, 4), bulge: 1 };
|
|
621
|
+
const result = offset.findJunction(seg, nextSeg);
|
|
622
|
+
expect(result.x).toBeCloseTo(10);
|
|
623
|
+
expect(result.y).toBeCloseTo(3);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// ─── preselection ───────────────────────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
test('Offset.execute - single preselection skips entity selection prompt', async () => {
|
|
629
|
+
core.scene.clear();
|
|
630
|
+
core.scene.selectionManager.reset();
|
|
631
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
632
|
+
// Preselect the entity
|
|
633
|
+
core.scene.selectionManager.addToSelectionSet(0);
|
|
634
|
+
|
|
635
|
+
const origInputManager = core.scene.inputManager;
|
|
636
|
+
let callCount = 0;
|
|
637
|
+
// Only distance and side-point inputs — no selection prompt should be requested
|
|
638
|
+
const inputs = [5, new Point(5, 5)];
|
|
639
|
+
const offset = new Offset();
|
|
640
|
+
core.scene.inputManager = {
|
|
641
|
+
requestInput: async () => {
|
|
642
|
+
if (callCount < inputs.length) return inputs[callCount++];
|
|
643
|
+
},
|
|
644
|
+
actionCommand: () => {
|
|
645
|
+
offset.action();
|
|
646
|
+
},
|
|
647
|
+
executeCommand: () => {},
|
|
648
|
+
reset: () => {},
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
await offset.execute();
|
|
652
|
+
|
|
653
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
654
|
+
core.scene.inputManager = origInputManager;
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test('Offset.execute - multi-preselection clears selection and prompts', async () => {
|
|
658
|
+
core.scene.clear();
|
|
659
|
+
core.scene.selectionManager.reset();
|
|
660
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
661
|
+
core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
|
|
662
|
+
// Preselect two entities
|
|
663
|
+
core.scene.selectionManager.addToSelectionSet(0);
|
|
664
|
+
core.scene.selectionManager.addToSelectionSet(1);
|
|
665
|
+
|
|
666
|
+
const origInputManager = core.scene.inputManager;
|
|
667
|
+
let callCount = 0;
|
|
668
|
+
// After clearing, distance + selection + side-point
|
|
669
|
+
const inputs = [5, { selectedItemIndex: 0 }, new Point(5, 5)];
|
|
670
|
+
const offset = new Offset();
|
|
671
|
+
core.scene.inputManager = {
|
|
672
|
+
requestInput: async () => {
|
|
673
|
+
if (callCount < inputs.length) return inputs[callCount++];
|
|
674
|
+
},
|
|
675
|
+
actionCommand: () => {
|
|
676
|
+
offset.action();
|
|
677
|
+
},
|
|
678
|
+
executeCommand: () => {},
|
|
679
|
+
reset: () => {},
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
await offset.execute();
|
|
683
|
+
|
|
684
|
+
expect(core.scene.entities.count()).toBe(3);
|
|
685
|
+
core.scene.inputManager = origInputManager;
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// ─── execute edge cases ───────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
test('Offset.execute - distance <= 0 notifies and does not offset', async () => {
|
|
691
|
+
core.scene.clear();
|
|
692
|
+
core.scene.selectionManager.reset();
|
|
693
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
694
|
+
|
|
695
|
+
const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
|
|
696
|
+
|
|
697
|
+
const origInputManager = core.scene.inputManager;
|
|
698
|
+
core.scene.inputManager = {
|
|
699
|
+
requestInput: async () => -5,
|
|
700
|
+
executeCommand: () => {},
|
|
701
|
+
reset: () => {},
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const offset = new Offset();
|
|
705
|
+
await offset.execute();
|
|
706
|
+
|
|
707
|
+
expect(notifySpy).toHaveBeenCalled();
|
|
708
|
+
expect(core.scene.entities.count()).toBe(1);
|
|
709
|
+
|
|
710
|
+
notifySpy.mockRestore();
|
|
711
|
+
core.scene.inputManager = origInputManager;
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test('Offset.execute - Through mode offsets entity to the through point distance', async () => {
|
|
715
|
+
core.scene.clear();
|
|
716
|
+
core.scene.selectionManager.reset();
|
|
717
|
+
core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
|
|
718
|
+
|
|
719
|
+
const origInputManager = core.scene.inputManager;
|
|
720
|
+
let callCount = 0;
|
|
721
|
+
// 'Through' → select entity at index 0 → through point at (5, 7) → undefined exits loop
|
|
722
|
+
const inputs = ['Through', { selectedItemIndex: 0 }, new Point(5, 7)];
|
|
723
|
+
|
|
724
|
+
const offset = new Offset();
|
|
725
|
+
core.scene.inputManager = {
|
|
726
|
+
requestInput: async () => {
|
|
727
|
+
if (callCount < inputs.length) return inputs[callCount++];
|
|
728
|
+
},
|
|
729
|
+
actionCommand: () => {
|
|
730
|
+
offset.action();
|
|
731
|
+
},
|
|
732
|
+
executeCommand: () => {},
|
|
733
|
+
reset: () => {},
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
await offset.execute();
|
|
737
|
+
|
|
738
|
+
// getThroughDistance(line, (5,7)) = perpendicular distance = 7
|
|
739
|
+
expect(core.scene.headers.offsetDistance).toBeCloseTo(7);
|
|
740
|
+
// Offset entity should have been created
|
|
741
|
+
expect(core.scene.entities.count()).toBe(2);
|
|
742
|
+
// The new line should be at y ≈ 7
|
|
743
|
+
const newLine = core.scene.entities.get(1);
|
|
744
|
+
expect(newLine.points[0].y).toBeCloseTo(7);
|
|
745
|
+
expect(newLine.points[1].y).toBeCloseTo(7);
|
|
746
|
+
|
|
747
|
+
core.scene.inputManager = origInputManager;
|
|
748
|
+
});
|