@design-core/core 0.0.6 → 50.1.0

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