@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
@@ -2,6 +2,8 @@ import { Core } from '../../core/core/core.js';
2
2
  import { Point } from '../../core/entities/point.js';
3
3
  import { Extend } from '../../core/tools/extend.js';
4
4
  import { SingleSelection } from '../../core/lib/selectionManager.js';
5
+ import { Strings } from '../../core/lib/strings.js';
6
+ import { DesignCore } from '../../core/designCore.js';
5
7
  import { expect, jest } from '@jest/globals';
6
8
  import { withMockInput } from '../test-helpers/test-helpers.js';
7
9
 
@@ -22,9 +24,9 @@ test.each(inputScenarios)('Trim.execute handles $desc', async (scenario) => {
22
24
  // clear all scene entities
23
25
  core.scene.clear();
24
26
  // create line
25
- core.scene.addItem('Line', { points: [new Point(), new Point(10, 0)] });
27
+ core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
26
28
  // create circle
27
- core.scene.addItem('Circle', { points: [new Point(), new Point(10, 0)] });
29
+ core.scene.addEntity('Circle', { points: [new Point(), new Point(10, 0)] });
28
30
  // select line
29
31
  core.scene.selectionManager.selectionSet.selectionSet.push(0);
30
32
 
@@ -33,7 +35,7 @@ test.each(inputScenarios)('Trim.execute handles $desc', async (scenario) => {
33
35
  await extend.execute();
34
36
 
35
37
  expect(extend.selectedBoundaryItems[0]).toEqual(core.scene.entities.get(0));
36
- expect(extend.selectedItem).toBe(core.scene.entities.get(1));
38
+ expect(extend.selectedEntity).toBe(core.scene.entities.get(1));
37
39
  expect(actionSpy).toHaveBeenCalled();
38
40
  }, { extraMethods: { actionCommand: () => actionSpy() } });
39
41
  });
@@ -61,12 +63,12 @@ test('Test Extend.action', () => {
61
63
  // clear all scene entities
62
64
  core.scene.clear();
63
65
  // Add items to scene
64
- core.scene.addItem('Line', { points: [lineOneStart, lineOneEnd] });
65
- core.scene.addItem('Line', { points: [lineTwoStart, lineTwoEnd] });
66
+ core.scene.addEntity('Line', { points: [lineOneStart, lineOneEnd] });
67
+ core.scene.addEntity('Line', { points: [lineTwoStart, lineTwoEnd] });
66
68
  // Select boundary item
67
69
  extend.selectedBoundaryItems = [core.scene.entities.get(0)];
68
70
  // select item to trim
69
- extend.selectedItem = core.scene.entities.get(1);
71
+ extend.selectedEntity = core.scene.entities.get(1);
70
72
  // set mouse location - required for Extend
71
73
  core.mouse.setPosFromScenePoint(new Point(40, 50));
72
74
  // Perform Extend
@@ -93,12 +95,12 @@ test('Test Extend.action', () => {
93
95
  // clear scene items
94
96
  core.scene.clear();
95
97
  // Add items to scene
96
- core.scene.addItem('Line', { points: [lineTwoStart, lineTwoEnd] });
97
- core.scene.addItem('Line', { points: [lineThreeStart, lineThreeEnd] });
98
+ core.scene.addEntity('Line', { points: [lineTwoStart, lineTwoEnd] });
99
+ core.scene.addEntity('Line', { points: [lineThreeStart, lineThreeEnd] });
98
100
  // Select boundary item
99
101
  extend.selectedBoundaryItems = [core.scene.entities.get(0)];
100
102
  // select item to extend
101
- extend.selectedItem = core.scene.entities.get(1);
103
+ extend.selectedEntity = core.scene.entities.get(1);
102
104
  // set mouse location - required for Extend
103
105
  core.mouse.setPosFromScenePoint(new Point(0, 20));
104
106
  // Perform Extend
@@ -124,12 +126,12 @@ test('Test Extend.action', () => {
124
126
  // clear scene items
125
127
  core.scene.clear();
126
128
  // Add items to scene
127
- core.scene.addItem('Line', { points: [lineOneStart, lineOneEnd] });
128
- core.scene.addItem('Line', { points: [crossingLineStart, crossingLineEnd] });
129
+ core.scene.addEntity('Line', { points: [lineOneStart, lineOneEnd] });
130
+ core.scene.addEntity('Line', { points: [crossingLineStart, crossingLineEnd] });
129
131
  // Select boundary item
130
132
  extend.selectedBoundaryItems = [core.scene.entities.get(0)];
131
133
  // select item to extend
132
- extend.selectedItem = core.scene.entities.get(1);
134
+ extend.selectedEntity = core.scene.entities.get(1);
133
135
  // set mouse location - required for Extend
134
136
  core.mouse.setPosFromScenePoint(new Point(35, 35));
135
137
  // Perform Extend
@@ -156,8 +158,8 @@ test('Test Extend.action line - endpoint coincident with intersection', () => {
156
158
  const extend = new Extend();
157
159
  core.scene.clear();
158
160
 
159
- core.scene.addItem('Line', { points: [new Point(0, 50), new Point(25, 50)] });
160
- core.scene.addItem('Lwpolyline', { points: [
161
+ core.scene.addEntity('Line', { points: [new Point(0, 50), new Point(25, 50)] });
162
+ core.scene.addEntity('Lwpolyline', { points: [
161
163
  new Point(50, 0), new Point(50, 100), new Point(100, 100),
162
164
  new Point(100, 0), new Point(150, 0), new Point(150, 100),
163
165
  new Point(200, 100), new Point(200, 0),
@@ -169,7 +171,7 @@ test('Test Extend.action line - endpoint coincident with intersection', () => {
169
171
  const expected = [50, 100, 150, 200];
170
172
  for (const ex of expected) {
171
173
  extend.selectedBoundaryItems = [boundary];
172
- extend.selectedItem = line;
174
+ extend.selectedEntity = line;
173
175
  core.mouse.setPosFromScenePoint(new Point(line.points[1].x - 5, 50));
174
176
  extend.action();
175
177
  expect(line.points[1].x).toBe(ex);
@@ -186,11 +188,11 @@ test('Test Extend.action polyline - endpoint coincident with intersection', () =
186
188
  const extend = new Extend();
187
189
  core.scene.clear();
188
190
 
189
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(50, 50)] });
190
- core.scene.addItem('Line', { points: [new Point(100, 0), new Point(100, 100)] });
191
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(50, 50)] });
192
+ core.scene.addEntity('Line', { points: [new Point(100, 0), new Point(100, 100)] });
191
193
 
192
194
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
193
- extend.selectedItem = core.scene.entities.get(0);
195
+ extend.selectedEntity = core.scene.entities.get(0);
194
196
  core.mouse.setPosFromScenePoint(new Point(45, 45));
195
197
  extend.action();
196
198
 
@@ -208,11 +210,11 @@ test('Test Extend.action polyline - 2-point polyline extend start', () => {
208
210
  const extend = new Extend();
209
211
  core.scene.clear();
210
212
 
211
- core.scene.addItem('Lwpolyline', { points: [new Point(50, 0), new Point(100, 0)] });
212
- core.scene.addItem('Line', { points: [new Point(0, -50), new Point(0, 50)] });
213
+ core.scene.addEntity('Lwpolyline', { points: [new Point(50, 0), new Point(100, 0)] });
214
+ core.scene.addEntity('Line', { points: [new Point(0, -50), new Point(0, 50)] });
213
215
 
214
216
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
215
- extend.selectedItem = core.scene.entities.get(0);
217
+ extend.selectedEntity = core.scene.entities.get(0);
216
218
  core.mouse.setPosFromScenePoint(new Point(55, 0));
217
219
  extend.action();
218
220
 
@@ -232,11 +234,11 @@ test('Test Extend.action polyline - extend end segment', () => {
232
234
  const extend = new Extend();
233
235
  core.scene.clear();
234
236
 
235
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0), new Point(75, 0)] });
236
- core.scene.addItem('Line', { points: [new Point(100, -50), new Point(100, 50)] });
237
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0), new Point(75, 0)] });
238
+ core.scene.addEntity('Line', { points: [new Point(100, -50), new Point(100, 50)] });
237
239
 
238
240
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
239
- extend.selectedItem = core.scene.entities.get(0);
241
+ extend.selectedEntity = core.scene.entities.get(0);
240
242
  core.mouse.setPosFromScenePoint(new Point(80, 0));
241
243
  extend.action();
242
244
 
@@ -259,11 +261,11 @@ test('Test Extend.action polyline - extend start segment', () => {
259
261
  const extend = new Extend();
260
262
  core.scene.clear();
261
263
 
262
- core.scene.addItem('Lwpolyline', { points: [new Point(25, 0), new Point(50, 0), new Point(100, 0)] });
263
- core.scene.addItem('Line', { points: [new Point(0, -50), new Point(0, 50)] });
264
+ core.scene.addEntity('Lwpolyline', { points: [new Point(25, 0), new Point(50, 0), new Point(100, 0)] });
265
+ core.scene.addEntity('Line', { points: [new Point(0, -50), new Point(0, 50)] });
264
266
 
265
267
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
266
- extend.selectedItem = core.scene.entities.get(0);
268
+ extend.selectedEntity = core.scene.entities.get(0);
267
269
  core.mouse.setPosFromScenePoint(new Point(20, 0));
268
270
  extend.action();
269
271
 
@@ -286,11 +288,11 @@ test('Test Extend.action polyline - reject arc end segment', () => {
286
288
  const extend = new Extend();
287
289
  core.scene.clear();
288
290
 
289
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0, 1), new Point(100, 0)] });
290
- core.scene.addItem('Line', { points: [new Point(125, -50), new Point(125, 50)] });
291
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0, 1), new Point(100, 0)] });
292
+ core.scene.addEntity('Line', { points: [new Point(125, -50), new Point(125, 50)] });
291
293
 
292
294
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
293
- extend.selectedItem = core.scene.entities.get(0);
295
+ extend.selectedEntity = core.scene.entities.get(0);
294
296
  core.mouse.setPosFromScenePoint(new Point(100, 0));
295
297
  extend.action();
296
298
 
@@ -314,11 +316,11 @@ test('Test Extend.action polyline - reject intersection behind endpoint', () =>
314
316
  const extend = new Extend();
315
317
  core.scene.clear();
316
318
 
317
- core.scene.addItem('Lwpolyline', { points: [new Point(50, 0), new Point(75, 0), new Point(100, 0)] });
318
- core.scene.addItem('Line', { points: [new Point(25, -50), new Point(25, 50)] });
319
+ core.scene.addEntity('Lwpolyline', { points: [new Point(50, 0), new Point(75, 0), new Point(100, 0)] });
320
+ core.scene.addEntity('Line', { points: [new Point(25, -50), new Point(25, 50)] });
319
321
 
320
322
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
321
- extend.selectedItem = core.scene.entities.get(0);
323
+ extend.selectedEntity = core.scene.entities.get(0);
322
324
  core.mouse.setPosFromScenePoint(new Point(95, 0));
323
325
  extend.action();
324
326
 
@@ -337,11 +339,11 @@ test('Test Extend.action polyline - reject shortening intersection', () => {
337
339
  const extend = new Extend();
338
340
  core.scene.clear();
339
341
 
340
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0), new Point(100, 0)] });
341
- core.scene.addItem('Line', { points: [new Point(75, -50), new Point(75, 50)] });
342
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0), new Point(100, 0)] });
343
+ core.scene.addEntity('Line', { points: [new Point(75, -50), new Point(75, 50)] });
342
344
 
343
345
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
344
- extend.selectedItem = core.scene.entities.get(0);
346
+ extend.selectedEntity = core.scene.entities.get(0);
345
347
  core.mouse.setPosFromScenePoint(new Point(95, 0));
346
348
  extend.action();
347
349
 
@@ -360,11 +362,11 @@ test('Test Extend.action polyline - extend with arc start segment', () => {
360
362
  const extend = new Extend();
361
363
  core.scene.clear();
362
364
 
363
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0, 1), new Point(50, 0), new Point(100, 0)] });
364
- core.scene.addItem('Line', { points: [new Point(125, -50), new Point(125, 50)] });
365
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0, 1), new Point(50, 0), new Point(100, 0)] });
366
+ core.scene.addEntity('Line', { points: [new Point(125, -50), new Point(125, 50)] });
365
367
 
366
368
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
367
- extend.selectedItem = core.scene.entities.get(0);
369
+ extend.selectedEntity = core.scene.entities.get(0);
368
370
  core.mouse.setPosFromScenePoint(new Point(110, 0));
369
371
  extend.action();
370
372
 
@@ -381,11 +383,11 @@ test('Test Extend.action polyline - no intersections', () => {
381
383
  const extend = new Extend();
382
384
  core.scene.clear();
383
385
 
384
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0)] });
385
- core.scene.addItem('Line', { points: [new Point(0, 50), new Point(100, 50)] });
386
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(50, 0)] });
387
+ core.scene.addEntity('Line', { points: [new Point(0, 50), new Point(100, 50)] });
386
388
 
387
389
  extend.selectedBoundaryItems = [core.scene.entities.get(1)];
388
- extend.selectedItem = core.scene.entities.get(0);
390
+ extend.selectedEntity = core.scene.entities.get(0);
389
391
  core.mouse.setPosFromScenePoint(new Point(45, 0));
390
392
  extend.action();
391
393
 
@@ -393,3 +395,332 @@ test('Test Extend.action polyline - no intersections', () => {
393
395
  expect(polyline.points[0].x).toBe(0);
394
396
  expect(polyline.points[1].x).toBe(50);
395
397
  });
398
+
399
+ test('Extend.register returns correct metadata', () => {
400
+ const reg = Extend.register();
401
+ expect(reg.command).toBe('Extend');
402
+ expect(reg.shortcut).toBe('EX');
403
+ expect(reg.type).toBe('Tool');
404
+ });
405
+
406
+ test('Extend.preview does not throw', () => {
407
+ const extend = new Extend();
408
+ expect(() => extend.preview()).not.toThrow();
409
+ });
410
+
411
+ test('Extend.preview returns early when no boundary items set', () => {
412
+ core.scene.clear();
413
+ core.scene.addEntity('Line', { points: [new Point(100, -10), new Point(100, 10)] });
414
+ DesignCore.Scene.previewEntities.clear();
415
+
416
+ const extend = new Extend();
417
+ // selectedBoundaryItems is empty by default
418
+ extend.preview();
419
+
420
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
421
+ });
422
+
423
+ test('Extend.preview returns early when findClosestItem returns undefined', () => {
424
+ core.scene.clear();
425
+ core.scene.addEntity('Line', { points: [new Point(100, -10), new Point(100, 10)] });
426
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(50, 0)] });
427
+ DesignCore.Scene.previewEntities.clear();
428
+
429
+ const extend = new Extend();
430
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
431
+
432
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
433
+ extend.preview();
434
+ findSpy.mockRestore();
435
+
436
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
437
+ });
438
+
439
+ test('Extend.preview returns early when hovered entity does not implement extend', () => {
440
+ // Circle does not override Entity.extend() — preview must not emit a notification.
441
+ core.scene.clear();
442
+ core.scene.addEntity('Line', { points: [new Point(100, -10), new Point(100, 10)] }); // boundary
443
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] }); // unsupported
444
+ DesignCore.Scene.previewEntities.clear();
445
+
446
+ const notifySpy = jest.spyOn(core, 'notify');
447
+ const extend = new Extend();
448
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
449
+
450
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
451
+ DesignCore.Mouse.pointOnScene = () => new Point(5, 0);
452
+ extend.preview();
453
+ findSpy.mockRestore();
454
+ notifySpy.mockRestore();
455
+
456
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
457
+ expect(notifySpy).not.toHaveBeenCalled();
458
+ });
459
+
460
+ test('Extend.preview returns early when hovered entity has no valid extension', () => {
461
+ // Hovered line is parallel to the boundary — no intersection ahead of its endpoint.
462
+ core.scene.clear();
463
+ core.scene.addEntity('Line', { points: [new Point(0, 50), new Point(100, 50)] }); // boundary (horizontal)
464
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(50, 0)] }); // hovered (parallel, y=0)
465
+ DesignCore.Scene.previewEntities.clear();
466
+
467
+ const extend = new Extend();
468
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
469
+
470
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
471
+ DesignCore.Mouse.pointOnScene = () => new Point(45, 0);
472
+ extend.preview();
473
+ findSpy.mockRestore();
474
+
475
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
476
+ });
477
+
478
+ test('Extend.preview populates previewEntities when entity can be extended', () => {
479
+ // Vertical boundary at x=100; short horizontal line from (0,0) to (50,0).
480
+ // Mouse near end (45,0) → end extends to (100,0).
481
+ core.scene.clear();
482
+ core.scene.addEntity('Line', { points: [new Point(100, -10), new Point(100, 10)] }); // boundary
483
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(50, 0)] }); // hovered
484
+ DesignCore.Scene.previewEntities.clear();
485
+
486
+ const extend = new Extend();
487
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
488
+
489
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
490
+ DesignCore.Mouse.pointOnScene = () => new Point(45, 0);
491
+ extend.preview();
492
+ findSpy.mockRestore();
493
+
494
+ // Preview of the extended line should be added
495
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
496
+ // The previewed line should reach the boundary
497
+ const preview = DesignCore.Scene.previewEntities.get(DesignCore.Scene.previewEntities.count() - 1);
498
+ expect(preview.points[1].x).toBeCloseTo(100);
499
+ expect(preview.points[1].y).toBeCloseTo(0);
500
+ });
501
+
502
+ test('Extend.execute returns early when boundary input is undefined', async () => {
503
+ core.scene.clear();
504
+ core.scene.selectionManager.reset();
505
+ core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
506
+
507
+ await withMockInput(core.scene, [undefined], async () => {
508
+ const extend = new Extend();
509
+ await extend.execute();
510
+ expect(extend.selectedBoundaryItems).toHaveLength(0);
511
+ });
512
+ });
513
+
514
+ test('Extend.execute exits selection loop when selection input is undefined', async () => {
515
+ core.scene.clear();
516
+ core.scene.selectionManager.reset();
517
+ core.scene.addEntity('Line', { points: [new Point(100, -10), new Point(100, 10)] });
518
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(50, 0)] });
519
+ core.scene.selectionManager.selectionSet.selectionSet.push(0);
520
+
521
+ const executeCommandSpy = jest.fn();
522
+ await withMockInput(core.scene, [undefined], async () => {
523
+ const extend = new Extend();
524
+ await extend.execute();
525
+ expect(extend.selectedBoundaryItems).toHaveLength(1);
526
+ expect(executeCommandSpy).toHaveBeenCalled();
527
+ }, { extraMethods: { executeCommand: () => executeCommandSpy() } });
528
+ });
529
+
530
+ test('Extend.execute calls actionCommand once per extend selection', async () => {
531
+ core.scene.clear();
532
+ core.scene.selectionManager.reset();
533
+ core.scene.addEntity('Line', { points: [new Point(100, -10), new Point(100, 10)] });
534
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(50, 0)] });
535
+ core.scene.addEntity('Line', { points: [new Point(0, 5), new Point(50, 5)] });
536
+ core.scene.selectionManager.selectionSet.selectionSet.push(0);
537
+
538
+ const actionSpy = jest.fn();
539
+ await withMockInput(
540
+ core.scene,
541
+ [new SingleSelection(1, new Point(45, 0)), new SingleSelection(2, new Point(45, 5)), undefined],
542
+ async () => {
543
+ const extend = new Extend();
544
+ await extend.execute();
545
+ expect(actionSpy).toHaveBeenCalledTimes(2);
546
+ },
547
+ { extraMethods: { actionCommand: () => actionSpy() } },
548
+ );
549
+ });
550
+
551
+ test('Extend.action does nothing when selectedEntity is null', () => {
552
+ core.scene.clear();
553
+ core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
554
+ const extend = new Extend();
555
+ extend.selectedEntity = null;
556
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
557
+
558
+ expect(() => extend.action()).not.toThrow();
559
+ });
560
+
561
+ test('Extend.action does nothing when selectedBoundaryItems is empty', () => {
562
+ core.scene.clear();
563
+ core.scene.addEntity('Line', { points: [new Point(), new Point(10, 0)] });
564
+ const extend = new Extend();
565
+ extend.selectedEntity = core.scene.entities.get(0);
566
+ extend.selectedBoundaryItems = [];
567
+
568
+ expect(() => extend.action()).not.toThrow();
569
+ expect(extend.selectedEntity).toBeNull();
570
+ });
571
+
572
+ test('Extend.action notifies NOEXTEND when selected entity lacks fromPolylinePoints', () => {
573
+ // Use a stub entity that has toPolylinePoints but no fromPolylinePoints.
574
+ // action() must notify and not throw.
575
+ core.scene.clear();
576
+ core.scene.addEntity('Line', { points: [new Point(100, -10), new Point(100, 10)] }); // boundary
577
+
578
+ const extend = new Extend();
579
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
580
+ extend.selectedEntity = {
581
+ type: 'FakeEntity',
582
+ toPolylinePoints: () => [new Point(0, 0), new Point(50, 0)],
583
+ // deliberately no fromPolylinePoints
584
+ };
585
+ core.mouse.setPosFromScenePoint(new Point(45, 0));
586
+
587
+ const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
588
+ extend.action();
589
+
590
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
591
+ expect(extend.selectedEntity).toBeNull();
592
+ notifySpy.mockRestore();
593
+ });
594
+
595
+ test('Extend.action notifies NOEXTEND when selected entity is a closed polyline', () => {
596
+ // Closed polylines have no open endpoints — extend must notify and leave the entity unchanged.
597
+ core.scene.clear();
598
+ core.scene.addEntity('Line', { points: [new Point(200, -10), new Point(200, 10)] }); // boundary
599
+ // Closed square: flag bit 1 set
600
+ core.scene.addEntity('Lwpolyline', {
601
+ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)],
602
+ flags: 1,
603
+ });
604
+
605
+ const extend = new Extend();
606
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
607
+ extend.selectedEntity = core.scene.entities.get(1);
608
+ core.mouse.setPosFromScenePoint(new Point(95, 0));
609
+
610
+ const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
611
+ extend.action();
612
+
613
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
614
+ expect(extend.selectedEntity).toBeNull();
615
+ notifySpy.mockRestore();
616
+ });
617
+
618
+ test('Extend.preview silently skips entity that lacks fromPolylinePoints', () => {
619
+ // Stub entity implements toPolylinePoints but not fromPolylinePoints.
620
+ // preview() must return early without calling notify.
621
+ core.scene.clear();
622
+ core.scene.addEntity('Line', { points: [new Point(100, -10), new Point(100, 10)] }); // boundary
623
+ DesignCore.Scene.previewEntities.clear();
624
+
625
+ const stubEntity = {
626
+ type: 'FakeEntity',
627
+ toPolylinePoints: () => [new Point(0, 0), new Point(50, 0)],
628
+ // deliberately no fromPolylinePoints
629
+ };
630
+
631
+ const notifySpy = jest.spyOn(core, 'notify');
632
+ const extend = new Extend();
633
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
634
+
635
+ const savedPointOnScene = DesignCore.Mouse.pointOnScene;
636
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
637
+ jest.spyOn(core.scene.entities, 'get').mockReturnValue(stubEntity);
638
+ DesignCore.Mouse.pointOnScene = () => new Point(45, 0);
639
+ extend.preview();
640
+ findSpy.mockRestore();
641
+ jest.restoreAllMocks();
642
+ DesignCore.Mouse.pointOnScene = savedPointOnScene;
643
+
644
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
645
+ expect(notifySpy).not.toHaveBeenCalled();
646
+ });
647
+
648
+ test('Extend.action notifies when no intersection found (parallel lines)', () => {
649
+ core.scene.clear();
650
+ // Two parallel horizontal lines — no intersection possible
651
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(100, 0)] });
652
+ core.scene.addEntity('Line', { points: [new Point(0, 50), new Point(50, 50)] });
653
+
654
+ const extend = new Extend();
655
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
656
+ extend.selectedEntity = core.scene.entities.get(1);
657
+ core.mouse.setPosFromScenePoint(new Point(40, 50));
658
+
659
+ const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
660
+ extend.action();
661
+
662
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
663
+ expect(extend.selectedEntity).toBeNull();
664
+ notifySpy.mockRestore();
665
+ });
666
+
667
+ test('Extend.action notifies when boundary item equals selected item', () => {
668
+ core.scene.clear();
669
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(50, 0)] });
670
+ const entity = core.scene.entities.get(0);
671
+
672
+ const extend = new Extend();
673
+ extend.selectedBoundaryItems = [entity];
674
+ extend.selectedEntity = entity;
675
+ core.mouse.setPosFromScenePoint(new Point(40, 0));
676
+
677
+ const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
678
+ extend.action();
679
+
680
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
681
+ notifySpy.mockRestore();
682
+ });
683
+
684
+ test('Extend.action extends the start of a line', () => {
685
+ // Line from (50,50) to (100,50) — horizontal
686
+ // Boundary: vertical line at x=0
687
+ // Mouse near the start (50,50) → extend start back to x=0
688
+ core.scene.clear();
689
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 100)] });
690
+ core.scene.addEntity('Line', { points: [new Point(50, 50), new Point(100, 50)] });
691
+
692
+ const extend = new Extend();
693
+ extend.selectedBoundaryItems = [core.scene.entities.get(0)];
694
+ extend.selectedEntity = core.scene.entities.get(1);
695
+ core.mouse.setPosFromScenePoint(new Point(55, 50));
696
+ extend.action();
697
+
698
+ expect(core.scene.entities.get(1).points[0].x).toBeCloseTo(0);
699
+ expect(core.scene.entities.get(1).points[0].y).toBeCloseTo(50);
700
+ expect(core.scene.entities.get(1).points[1].x).toBe(100);
701
+ expect(core.scene.entities.get(1).points[1].y).toBe(50);
702
+ });
703
+
704
+ test('Extend.action notifies NOEXTEND when end segment of an Arc is curved', () => {
705
+ // A standalone Arc always has a non-zero bulge on its single segment.
706
+ // #extendEntity must reject the extension and notify because endSegmentBulge !== 0.
707
+ core.scene.clear();
708
+ core.scene.addEntity('Arc', {
709
+ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)],
710
+ direction: 1,
711
+ });
712
+ // Vertical boundary at x=5 — intersects the CCW quarter-arc
713
+ core.scene.addEntity('Line', { points: [new Point(5, -20), new Point(5, 20)] });
714
+
715
+ const extend = new Extend();
716
+ extend.selectedBoundaryItems = [core.scene.entities.get(1)];
717
+ extend.selectedEntity = core.scene.entities.get(0);
718
+ core.mouse.setPosFromScenePoint(new Point(9, 1)); // near arc start (10,0)
719
+
720
+ const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
721
+ extend.action();
722
+
723
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOEXTEND));
724
+ expect(extend.selectedEntity).toBeNull();
725
+ notifySpy.mockRestore();
726
+ });