@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.
Files changed (254) hide show
  1. package/.eslintrc.json +14 -7
  2. package/.github/workflows/DesignCoreQA.yml +3 -1
  3. package/README.md +11 -4
  4. package/TODO.md +19 -0
  5. package/core/core/core.js +49 -42
  6. package/core/designCore.js +61 -2
  7. package/core/dimensions/alignedDimension.js +142 -0
  8. package/core/dimensions/angularDimension.js +550 -0
  9. package/core/dimensions/baseCircularDimension.js +244 -0
  10. package/core/dimensions/baseDimension.js +558 -0
  11. package/core/dimensions/baseLinearDimension.js +371 -0
  12. package/core/dimensions/diametricDimension.js +82 -0
  13. package/core/dimensions/dimension.js +298 -0
  14. package/core/dimensions/linearDimension.js +29 -0
  15. package/core/dimensions/radialDimension.js +78 -0
  16. package/core/dimensions/rotatedDimension.js +167 -0
  17. package/core/entities/arc.js +314 -60
  18. package/core/entities/arctext.js +642 -0
  19. package/core/entities/basePolyline.js +553 -94
  20. package/core/entities/circle.js +181 -54
  21. package/core/entities/entity.js +154 -74
  22. package/core/entities/hatch.js +807 -0
  23. package/core/entities/insert.js +200 -39
  24. package/core/entities/line.js +196 -134
  25. package/core/entities/lwpolyline.js +21 -44
  26. package/core/entities/point.js +194 -29
  27. package/core/entities/polyline.js +54 -7
  28. package/core/entities/rectangle.js +43 -13
  29. package/core/entities/solid.js +43 -6
  30. package/core/entities/text.js +324 -181
  31. package/core/lib/boundingBox.js +73 -13
  32. package/core/lib/canvas.js +256 -65
  33. package/core/lib/clipboard.js +198 -0
  34. package/core/lib/colour.js +144 -0
  35. package/core/lib/colours.js +367 -301
  36. package/core/lib/commandLine.js +16 -28
  37. package/core/lib/commandManager.js +115 -58
  38. package/core/lib/constants.js +23 -0
  39. package/core/lib/dxf/dxf.js +249 -34
  40. package/core/lib/dxf/dxfFile.js +23 -38
  41. package/core/lib/dxf/dxfIterator.js +17 -5
  42. package/core/lib/dxf/dxfRead.js +23 -7
  43. package/core/lib/dxf/dxfWrite.js +39 -129
  44. package/core/lib/dxf/handleManager.js +97 -0
  45. package/core/lib/dxf/sections/blocks.js +16 -2
  46. package/core/lib/dxf/sections/entities.js +17 -3
  47. package/core/lib/dxf/sections/header.js +10 -1
  48. package/core/lib/dxf/sections/objects.js +36 -0
  49. package/core/lib/dxf/sections/section.js +43 -5
  50. package/core/lib/dxf/sections/tables.js +17 -3
  51. package/core/lib/entityManager.js +178 -0
  52. package/core/lib/fileio.js +14 -4
  53. package/core/lib/headers.js +229 -0
  54. package/core/lib/input.js +46 -0
  55. package/core/lib/inputManager.js +199 -194
  56. package/core/lib/intersect.js +341 -468
  57. package/core/lib/logging.js +25 -1
  58. package/core/lib/matrix.js +6 -8
  59. package/core/lib/mouse.js +24 -79
  60. package/core/lib/patterns.js +690 -0
  61. package/core/lib/promptOptions.js +133 -0
  62. package/core/lib/scene.js +71 -174
  63. package/core/lib/selectionManager.js +74 -48
  64. package/core/lib/selectionWindow.js +24 -10
  65. package/core/lib/settings.js +18 -11
  66. package/core/lib/snapping.js +129 -21
  67. package/core/lib/stateManager.js +288 -0
  68. package/core/lib/strings.js +57 -5
  69. package/core/lib/utils.js +95 -11
  70. package/core/objects/dictionary.js +43 -0
  71. package/core/objects/dictionaryManager.js +80 -0
  72. package/core/properties/dimType.js +117 -0
  73. package/core/properties/flags.js +66 -0
  74. package/core/properties/property.js +26 -0
  75. package/core/properties/propertyManager.js +65 -19
  76. package/core/tables/appId.js +31 -0
  77. package/core/tables/appIdManager.js +44 -0
  78. package/core/tables/block.js +323 -0
  79. package/core/tables/blockManager.js +48 -0
  80. package/core/tables/blockRecord.js +29 -0
  81. package/core/tables/blockRecordManager.js +37 -0
  82. package/core/tables/dimStyle.js +627 -0
  83. package/core/tables/dimStyleManager.js +187 -0
  84. package/core/tables/endblk.js +28 -0
  85. package/core/tables/layer.js +219 -0
  86. package/core/tables/layerManager.js +47 -0
  87. package/core/tables/ltype.js +85 -0
  88. package/core/tables/ltypeManager.js +65 -0
  89. package/core/tables/style.js +145 -0
  90. package/core/tables/styleManager.js +55 -0
  91. package/core/tables/styleManagerBase.js +110 -0
  92. package/core/tables/tableManagerBase.js +354 -0
  93. package/core/tables/ucs.js +28 -0
  94. package/core/tables/ucsManager.js +44 -0
  95. package/core/tables/view.js +28 -0
  96. package/core/tables/viewManager.js +44 -0
  97. package/core/tables/vport.js +70 -0
  98. package/core/tables/vportManager.js +65 -0
  99. package/core/tools/chamfer.js +343 -0
  100. package/core/tools/chamferFilletBase.js +162 -0
  101. package/core/tools/copy.js +68 -40
  102. package/core/tools/copybase.js +65 -0
  103. package/core/tools/copyclip.js +59 -0
  104. package/core/tools/cornerEntity.js +138 -0
  105. package/core/tools/cutclip.js +72 -0
  106. package/core/tools/distance.js +37 -9
  107. package/core/tools/erase.js +43 -13
  108. package/core/tools/explode.js +117 -0
  109. package/core/tools/extend.js +71 -32
  110. package/core/tools/fillet.js +336 -0
  111. package/core/tools/identify.js +33 -6
  112. package/core/tools/matchProp.js +106 -0
  113. package/core/tools/mirror.js +141 -0
  114. package/core/tools/move.js +65 -32
  115. package/core/tools/offset.js +321 -0
  116. package/core/tools/pan.js +54 -0
  117. package/core/tools/pasteclip.js +93 -0
  118. package/core/tools/purge.js +90 -0
  119. package/core/tools/rotate.js +105 -70
  120. package/core/tools/scale.js +166 -0
  121. package/core/tools/tool.js +14 -4
  122. package/core/tools/trim.js +71 -32
  123. package/core/tools/zoom.js +152 -0
  124. package/package.json +3 -305
  125. package/test/dimensions/alignedDimension.test.js +211 -0
  126. package/test/dimensions/angularDimension.test.js +243 -0
  127. package/test/dimensions/baseDimension.test.js +239 -0
  128. package/test/dimensions/diametricDimension.test.js +203 -0
  129. package/test/dimensions/dimension.test.js +102 -0
  130. package/test/dimensions/radialDimension.test.js +203 -0
  131. package/test/dimensions/rotatedDimension.test.js +224 -0
  132. package/test/entities/arc.test.js +463 -41
  133. package/test/entities/arctext.test.js +329 -0
  134. package/test/entities/basePolyline.test.js +459 -10
  135. package/test/entities/circle.test.js +312 -7
  136. package/test/entities/entity.test.js +28 -9
  137. package/test/entities/hatch.test.js +606 -0
  138. package/test/entities/insert.test.js +90 -0
  139. package/test/entities/line.test.js +182 -6
  140. package/test/entities/point.test.js +375 -37
  141. package/test/entities/solid.test.js +156 -0
  142. package/test/entities/text.test.js +533 -7
  143. package/test/integration/exportIntegration.reference.dxf +1706 -0
  144. package/test/integration/exportIntegration.test.js +186 -0
  145. package/test/integration/importExportIntegration.test.js +126 -0
  146. package/test/integration/intersectionIntegration.test.js +880 -0
  147. package/test/integration/userInputIntegration.reference.dxf +1410 -0
  148. package/test/integration/userInputIntegration.test.js +269 -0
  149. package/test/lib/boundingBox.test.js +26 -2
  150. package/test/lib/canvas.test.js +613 -5
  151. package/test/lib/clipboard.test.js +70 -0
  152. package/test/lib/colour.test.js +114 -0
  153. package/test/lib/colours.test.js +63 -18
  154. package/test/lib/commandLine.test.js +159 -22
  155. package/test/lib/commandManager.test.js +4 -4
  156. package/test/lib/constants.test.js +9 -0
  157. package/test/lib/dxf/dxfFile.test.js +6 -2
  158. package/test/lib/dxf/dxfIterator.test.js +1 -1
  159. package/test/lib/dxf/dxfWrite.test.js +173 -0
  160. package/test/lib/dxf/handleManager.test.js +137 -0
  161. package/test/lib/dxf/sections/blocks.test.js +5 -5
  162. package/test/lib/dxf/sections/entities.test.js +4 -4
  163. package/test/lib/dxf/sections/header.test.js +18 -4
  164. package/test/lib/dxf/sections/objects.test.js +132 -0
  165. package/test/lib/dxf/sections/section.test.js +29 -29
  166. package/test/lib/dxf/sections/tables.test.js +3 -3
  167. package/test/lib/entityManager.test.js +163 -0
  168. package/test/lib/headers.test.js +233 -0
  169. package/test/lib/inputManager.test.js +1144 -5
  170. package/test/lib/intersect.test.js +454 -331
  171. package/test/lib/logging.test.js +1 -1
  172. package/test/lib/matrix.test.js +1 -1
  173. package/test/lib/mouse.test.js +327 -4
  174. package/test/lib/patterns.test.js +105 -0
  175. package/test/lib/promptOptions.test.js +179 -0
  176. package/test/lib/scene.test.js +16 -52
  177. package/test/lib/selectionManager.test.js +17 -5
  178. package/test/lib/settings.test.js +111 -5
  179. package/test/lib/snapping.test.js +142 -4
  180. package/test/lib/stateManager.test.js +155 -0
  181. package/test/lib/utils.test.js +81 -1
  182. package/test/objects/dictionary.test.js +81 -0
  183. package/test/objects/dictionaryManager.test.js +34 -0
  184. package/test/properties/dimType.test.js +62 -0
  185. package/test/properties/flags.test.js +164 -0
  186. package/test/properties/property.test.js +13 -0
  187. package/test/properties/propertyManager.test.js +10 -11
  188. package/test/tables/appId.test.js +46 -0
  189. package/test/tables/appIdManager.test.js +72 -0
  190. package/test/tables/block.test.js +157 -0
  191. package/test/tables/blockManager.test.js +52 -0
  192. package/test/tables/blockRecord.test.js +39 -0
  193. package/test/tables/blockRecordManager.test.js +60 -0
  194. package/test/tables/dimstyle.test.js +72 -0
  195. package/test/tables/endblk.test.js +30 -0
  196. package/test/tables/layer.test.js +95 -0
  197. package/test/tables/layerManager.test.js +172 -0
  198. package/test/{styles → tables}/ltype.test.js +7 -7
  199. package/test/{styles → tables}/ltypeManager.test.js +2 -2
  200. package/test/tables/style.test.js +44 -0
  201. package/test/tables/stylemanagerbase.test.js +220 -0
  202. package/test/tables/tableManagerBase.test.js +194 -0
  203. package/test/tables/ucs.test.js +37 -0
  204. package/test/tables/ucsManager.test.js +48 -0
  205. package/test/tables/view.test.js +37 -0
  206. package/test/tables/viewManager.test.js +48 -0
  207. package/test/tables/vport.test.js +117 -0
  208. package/test/tables/vportManager.test.js +54 -0
  209. package/test/test-helpers/test-helpers.js +66 -0
  210. package/test/tools/chamfer.test.js +970 -0
  211. package/test/tools/chamferFilletBase.test.js +446 -0
  212. package/test/tools/copy.test.js +22 -14
  213. package/test/tools/copybase.test.js +93 -0
  214. package/test/tools/copyclip.test.js +92 -0
  215. package/test/tools/cornerEntity.test.js +201 -0
  216. package/test/tools/cutclip.test.js +94 -0
  217. package/test/tools/distance.test.js +140 -18
  218. package/test/tools/erase.test.js +12 -12
  219. package/test/tools/explode.test.js +45 -0
  220. package/test/tools/extend.test.js +326 -44
  221. package/test/tools/fillet.test.js +831 -0
  222. package/test/tools/identify.test.js +115 -16
  223. package/test/tools/matchProp.test.js +84 -0
  224. package/test/tools/mirror.test.js +475 -0
  225. package/test/tools/move.test.js +75 -19
  226. package/test/tools/offset.test.js +748 -0
  227. package/test/tools/pan.test.js +72 -0
  228. package/test/tools/pasteclip.test.js +100 -0
  229. package/test/tools/purge.test.js +89 -0
  230. package/test/tools/rotate.test.js +89 -23
  231. package/test/tools/scale.test.js +529 -0
  232. package/test/tools/trim.test.js +419 -84
  233. package/test/tools/zoom.test.js +504 -0
  234. package/core/entities/alignedDimension.js +0 -279
  235. package/core/entities/angularDimension.js +0 -194
  236. package/core/entities/baseDimension.js +0 -227
  237. package/core/entities/block.js +0 -247
  238. package/core/entities/diametricDimension.js +0 -133
  239. package/core/entities/dimension.js +0 -202
  240. package/core/entities/radialDimension.js +0 -126
  241. package/core/styles/dimStyle.js +0 -719
  242. package/core/styles/dimStyleManager.js +0 -47
  243. package/core/styles/layer.js +0 -133
  244. package/core/styles/layerManager.js +0 -40
  245. package/core/styles/ltype.js +0 -86
  246. package/core/styles/ltypeManager.js +0 -50
  247. package/core/styles/style.js +0 -158
  248. package/core/styles/styleManager.js +0 -43
  249. package/core/styles/styleManagerBase.js +0 -299
  250. package/test/entities/block.test.js +0 -22
  251. package/test/styles/dimstyle.test.js +0 -19
  252. package/test/styles/layerManager.test.js +0 -150
  253. package/test/styles/style.test.js +0 -33
  254. package/test/styles/stylemanagerbase.test.js +0 -219
@@ -0,0 +1,446 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { DesignCore } from '../../core/designCore.js';
3
+ import { Point } from '../../core/entities/point.js';
4
+ import { Chamfer } from '../../core/tools/chamfer.js';
5
+ import { Strings } from '../../core/lib/strings.js';
6
+ import { expect, jest } from '@jest/globals';
7
+
8
+ const core = new Core();
9
+
10
+ // ─── resolveCornerGeometry ────────────────────────────────────────────────────
11
+
12
+ test('ChamferFilletBase.resolveCornerGeometry returns false and notifies when first segment is not a Line', () => {
13
+ core.scene.clear();
14
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
15
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
16
+
17
+ const notifySpy = jest.spyOn(core, 'notify');
18
+ const chamfer = new Chamfer();
19
+ chamfer.firstPick.entity = core.scene.entities.get(0); // Circle
20
+ chamfer.secondPick.entity = core.scene.entities.get(1);
21
+ chamfer.firstPick.clickPoint = new Point(0, 0);
22
+ chamfer.secondPick.clickPoint = new Point(0, 5);
23
+
24
+ expect(chamfer.resolveCornerGeometry('test-msg')).toBe(false);
25
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining('test-msg'));
26
+ notifySpy.mockRestore();
27
+ });
28
+
29
+ test('ChamferFilletBase.resolveCornerGeometry returns false and notifies when second segment is not a Line', () => {
30
+ core.scene.clear();
31
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
32
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
33
+
34
+ const notifySpy = jest.spyOn(core, 'notify');
35
+ const chamfer = new Chamfer();
36
+ chamfer.firstPick.entity = core.scene.entities.get(0);
37
+ chamfer.secondPick.entity = core.scene.entities.get(1); // Circle
38
+ chamfer.firstPick.clickPoint = new Point(5, 0);
39
+ chamfer.secondPick.clickPoint = new Point(0, 0);
40
+
41
+ expect(chamfer.resolveCornerGeometry('test-msg')).toBe(false);
42
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining('test-msg'));
43
+ notifySpy.mockRestore();
44
+ });
45
+
46
+ test('ChamferFilletBase.resolveCornerGeometry returns false and notifies for parallel lines', () => {
47
+ core.scene.clear();
48
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
49
+ core.scene.addItem('Line', { points: [new Point(0, 2), new Point(10, 2)] });
50
+
51
+ const notifySpy = jest.spyOn(core, 'notify');
52
+ const chamfer = new Chamfer();
53
+ chamfer.firstPick.entity = core.scene.entities.get(0);
54
+ chamfer.secondPick.entity = core.scene.entities.get(1);
55
+ chamfer.firstPick.clickPoint = new Point(5, 0);
56
+ chamfer.secondPick.clickPoint = new Point(5, 2);
57
+
58
+ expect(chamfer.resolveCornerGeometry('test-msg')).toBe(false);
59
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.PARALLELLINES));
60
+ notifySpy.mockRestore();
61
+ });
62
+
63
+ test('ChamferFilletBase.resolveCornerGeometry returns false for collinear overlapping lines', () => {
64
+ core.scene.clear();
65
+ // Two segments on the same line that overlap
66
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
67
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(15, 0)] });
68
+
69
+ const notifySpy = jest.spyOn(core, 'notify');
70
+ const chamfer = new Chamfer();
71
+ chamfer.firstPick.entity = core.scene.entities.get(0);
72
+ chamfer.secondPick.entity = core.scene.entities.get(1);
73
+ chamfer.firstPick.clickPoint = new Point(3, 0);
74
+ chamfer.secondPick.clickPoint = new Point(12, 0);
75
+
76
+ expect(chamfer.resolveCornerGeometry('test-msg')).toBe(false);
77
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.PARALLELLINES));
78
+ notifySpy.mockRestore();
79
+ });
80
+
81
+ test('ChamferFilletBase.resolveCornerGeometry returns false for collinear endpoint-sharing lines', () => {
82
+ core.scene.clear();
83
+ // Two collinear segments sharing an endpoint (180° angle)
84
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(5, 0)] });
85
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(10, 0)] });
86
+
87
+ const notifySpy = jest.spyOn(core, 'notify');
88
+ const chamfer = new Chamfer();
89
+ chamfer.firstPick.entity = core.scene.entities.get(0);
90
+ chamfer.secondPick.entity = core.scene.entities.get(1);
91
+ chamfer.firstPick.clickPoint = new Point(2, 0);
92
+ chamfer.secondPick.clickPoint = new Point(8, 0);
93
+
94
+ expect(chamfer.resolveCornerGeometry('test-msg')).toBe(false);
95
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.PARALLELLINES));
96
+ notifySpy.mockRestore();
97
+ });
98
+
99
+ test('ChamferFilletBase.resolveCornerGeometry returns true and computes intersection for L-corner', () => {
100
+ // Horizontal (-10,0)→(0,0) and vertical (0,0)→(0,10); infinite lines meet at (0,0)
101
+ core.scene.clear();
102
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
103
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
104
+
105
+ const chamfer = new Chamfer();
106
+ chamfer.firstPick.entity = core.scene.entities.get(0);
107
+ chamfer.secondPick.entity = core.scene.entities.get(1);
108
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
109
+ chamfer.secondPick.clickPoint = new Point(0, 5);
110
+
111
+ expect(chamfer.resolveCornerGeometry('test-msg')).toBe(true);
112
+ expect(chamfer.intersectionPoint.x).toBeCloseTo(0);
113
+ expect(chamfer.intersectionPoint.y).toBeCloseTo(0);
114
+ // Click on line1 is to the left of intersection → clickUnit points left
115
+ expect(chamfer.firstPick.clickUnit(chamfer.intersectionPoint).x).toBeCloseTo(-1);
116
+ expect(chamfer.firstPick.clickUnit(chamfer.intersectionPoint).y).toBeCloseTo(0);
117
+ expect(chamfer.firstPick.lineKeptEnd(chamfer.intersectionPoint)).toBe(chamfer.firstPick.entity.points[0]); // (-10,0)
118
+ // Click on line2 is above intersection → clickUnit points up
119
+ expect(chamfer.secondPick.clickUnit(chamfer.intersectionPoint).x).toBeCloseTo(0);
120
+ expect(chamfer.secondPick.clickUnit(chamfer.intersectionPoint).y).toBeCloseTo(1);
121
+ expect(chamfer.secondPick.lineKeptEnd(chamfer.intersectionPoint)).toBe(chamfer.secondPick.entity.points[1]); // (0,10)
122
+ });
123
+
124
+ test('ChamferFilletBase.resolveCornerGeometry uses polyline activeSeg for first entity', () => {
125
+ // Polyline [(-10,0),(0,0),(0,10)], click near segment 1 (y=0).
126
+ // Paired with vertical line x=5; lines meet at (5,0).
127
+ core.scene.clear();
128
+ core.scene.addItem('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
129
+ core.scene.addItem('Line', { points: [new Point(5, -5), new Point(5, 5)] });
130
+
131
+ const polyEntity = core.scene.entities.get(0);
132
+ const lineEntity = core.scene.entities.get(1);
133
+ const polyClickPoint = new Point(-5, 0);
134
+
135
+ const chamfer = new Chamfer();
136
+ chamfer.firstPick.entity = polyEntity;
137
+ chamfer.firstPick.clickPoint = polyClickPoint;
138
+ chamfer.firstPick.segment = polyEntity.getClosestSegment(polyClickPoint);
139
+ chamfer.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
140
+ chamfer.secondPick.entity = lineEntity;
141
+ chamfer.secondPick.clickPoint = new Point(5, 2);
142
+
143
+ expect(chamfer.resolveCornerGeometry('test-msg')).toBe(true);
144
+ expect(chamfer.intersectionPoint.x).toBeCloseTo(5);
145
+ expect(chamfer.intersectionPoint.y).toBeCloseTo(0);
146
+ });
147
+
148
+ // ─── applySharpTrim: Line + Line ──────────────────────────────────────────────
149
+
150
+ test('ChamferFilletBase.applySharpTrim trims two Lines to the intersection point', () => {
151
+ // Line1: (0,0)→(8,0); Line2: (10,-10)→(10,-2). Infinite lines meet at (10,0).
152
+ // keepEnd for Line1 = (0,0); keepEnd for Line2 = (10,-10).
153
+ core.scene.clear();
154
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(8, 0)] });
155
+ core.scene.addItem('Line', { points: [new Point(10, -10), new Point(10, -2)] });
156
+ const lineA = core.scene.entities.get(0);
157
+ const lineB = core.scene.entities.get(1);
158
+
159
+ const chamfer = new Chamfer();
160
+ chamfer.firstPick.entity = lineA;
161
+ chamfer.firstPick.lineStart = lineA.points[0]; // (0,0)
162
+ chamfer.firstPick.lineEnd = lineA.points[1]; // (8,0)
163
+ chamfer.firstPick.clickPoint = new Point(3, 2); // projects to (3,0) → lineKeptEnd = (0,0)
164
+ chamfer.secondPick.entity = lineB;
165
+ chamfer.secondPick.lineStart = lineB.points[0]; // (10,-10)
166
+ chamfer.secondPick.lineEnd = lineB.points[1]; // (10,-2)
167
+ chamfer.secondPick.clickPoint = new Point(8, -7); // projects to (10,-7) → lineKeptEnd = (10,-10)
168
+ chamfer.intersectionPoint = new Point(10, 0);
169
+
170
+ const stateChanges = chamfer.applySharpTrim();
171
+ DesignCore.Scene.commit(stateChanges);
172
+
173
+ const updatedA = core.scene.entities.get(0);
174
+ const updatedB = core.scene.entities.get(1);
175
+ expect(updatedA.points[0].x).toBeCloseTo(0);
176
+ expect(updatedA.points[0].y).toBeCloseTo(0);
177
+ expect(updatedA.points[1].x).toBeCloseTo(10);
178
+ expect(updatedA.points[1].y).toBeCloseTo(0);
179
+ expect(updatedB.points[0].x).toBeCloseTo(10);
180
+ expect(updatedB.points[0].y).toBeCloseTo(-10);
181
+ expect(updatedB.points[1].x).toBeCloseTo(10);
182
+ expect(updatedB.points[1].y).toBeCloseTo(0);
183
+ });
184
+
185
+ // ─── applySharpTrim: Poly + Poly, same entity ─────────────────────────────────
186
+
187
+ test('ChamferFilletBase.applySharpTrim poly+poly consecutive segments: replaces corner vertex with intersection', () => {
188
+ // L-shaped polyline [(-10,0),(0,0),(0,10)]; segments 1 and 2 share vertex (0,0).
189
+ // Intersection of infinite lines is also (0,0), so splice replaces vertex with itself.
190
+ core.scene.clear();
191
+ core.scene.addItem('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
192
+ const polyEntity = core.scene.entities.get(0);
193
+
194
+ const chamfer = new Chamfer();
195
+ chamfer.firstPick.entity = polyEntity;
196
+ chamfer.secondPick.entity = polyEntity;
197
+ chamfer.firstPick.segmentIndex = 1;
198
+ chamfer.secondPick.segmentIndex = 2;
199
+ chamfer.intersectionPoint = new Point(0, 0);
200
+
201
+ const stateChanges = chamfer.applySharpTrim();
202
+ DesignCore.Scene.commit(stateChanges);
203
+
204
+ const poly = core.scene.entities.get(0);
205
+ expect(poly.points.length).toBe(3);
206
+ expect(poly.points[1].x).toBeCloseTo(0);
207
+ expect(poly.points[1].y).toBeCloseTo(0);
208
+ });
209
+
210
+ test('ChamferFilletBase.applySharpTrim poly+poly open ends: moves both endpoints to intersection', () => {
211
+ // 4-vertex polyline: [(-5,2),(0,2),(3,4),(3,8)].
212
+ // Seg 1 is on y=2, seg 3 (lastIdx=3) is on x=3 → infinite lines meet at (3,2).
213
+ // Both open endpoints (index 0 and 3) are moved to (3,2).
214
+ core.scene.clear();
215
+ core.scene.addItem('Lwpolyline', { points: [new Point(-5, 2), new Point(0, 2), new Point(3, 4), new Point(3, 8)] });
216
+ const polyEntity = core.scene.entities.get(0);
217
+
218
+ const chamfer = new Chamfer();
219
+ chamfer.firstPick.entity = polyEntity;
220
+ chamfer.secondPick.entity = polyEntity;
221
+ chamfer.firstPick.segmentIndex = 1; // open start
222
+ chamfer.secondPick.segmentIndex = 3; // open end (lastIdx = 3)
223
+ chamfer.intersectionPoint = new Point(3, 2);
224
+
225
+ const stateChanges = chamfer.applySharpTrim();
226
+ DesignCore.Scene.commit(stateChanges);
227
+
228
+ const poly = core.scene.entities.get(0);
229
+ expect(poly.points.length).toBe(4);
230
+ // Both endpoints trimmed to intersection
231
+ expect(poly.points[0].x).toBeCloseTo(3);
232
+ expect(poly.points[0].y).toBeCloseTo(2);
233
+ expect(poly.points[3].x).toBeCloseTo(3);
234
+ expect(poly.points[3].y).toBeCloseTo(2);
235
+ // Middle points unchanged
236
+ expect(poly.points[1].x).toBeCloseTo(0);
237
+ expect(poly.points[1].y).toBeCloseTo(2);
238
+ expect(poly.points[2].x).toBeCloseTo(3);
239
+ expect(poly.points[2].y).toBeCloseTo(4);
240
+ });
241
+
242
+ test('ChamferFilletBase.applySharpTrim poly+poly closed polyline seg1+lastIdx: corner-splice path, not open-ends', () => {
243
+ // Closed 4-vertex polyline: [(-10,0),(0,0),(0,10),(close)].
244
+ // flags bit 1 set → polyline is closed.
245
+ // segmentIndex 1 and lastIdx 3 would look like open-ends by index alone, but the
246
+ // closed-polyline guard must send this to the corner-splice path instead.
247
+ // The shared corner is at index min(1,3)=1, so corner vertex (0,0) is replaced with
248
+ // the intersection point (0,0) — a no-op on coordinates but proves the splice path ran.
249
+ core.scene.clear();
250
+ core.scene.addItem('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
251
+ const polyEntity = core.scene.entities.get(0);
252
+ polyEntity.flags.setFlagValue(1); // mark as closed
253
+
254
+ const chamfer = new Chamfer();
255
+ chamfer.firstPick.entity = polyEntity;
256
+ chamfer.secondPick.entity = polyEntity;
257
+ chamfer.firstPick.segmentIndex = 1;
258
+ chamfer.secondPick.segmentIndex = polyEntity.points.length - 1; // lastIdx
259
+ chamfer.intersectionPoint = new Point(0, 0);
260
+
261
+ const stateChanges = chamfer.applySharpTrim();
262
+ DesignCore.Scene.commit(stateChanges);
263
+
264
+ // Corner-splice: replaces vertex at cornerIdx=1 with intersectionPoint — point count unchanged
265
+ const poly = core.scene.entities.get(0);
266
+ expect(poly.points.length).toBe(3);
267
+ // endpoints NOT moved (open-ends path would have moved points[0] and points[lastIdx])
268
+ expect(poly.points[0].x).toBeCloseTo(-10);
269
+ expect(poly.points[0].y).toBeCloseTo(0);
270
+ });
271
+
272
+ test('ChamferFilletBase.applySharpTrim closed poly last-regular-seg + closing-seg: replaces last vertex', () => {
273
+ // Square: [( 0,0),(10,0),(10,10),(0,10)], closed.
274
+ // Segments: 1=(0,0)→(10,0), 2=(10,0)→(10,10), 3=(10,10)→(0,10), 4(closing)=(0,10)→(0,0).
275
+ // Corner at (0,10) shared by seg 3 and seg 4. segDiff=1 → corner-splice path.
276
+ // cornerIdx = min(3,4) = 3 → replaces points[3]=(0,10) with intersectionPoint (0,10).
277
+ core.scene.clear();
278
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
279
+ const polyEntity = core.scene.entities.get(0);
280
+ polyEntity.flags.setFlagValue(1);
281
+
282
+ const chamfer = new Chamfer();
283
+ chamfer.firstPick.entity = polyEntity;
284
+ chamfer.secondPick.entity = polyEntity;
285
+ chamfer.firstPick.segmentIndex = 3;
286
+ chamfer.secondPick.segmentIndex = 4; // closing segment (points.length)
287
+ chamfer.intersectionPoint = new Point(0, 10);
288
+
289
+ const stateChanges = chamfer.applySharpTrim();
290
+ DesignCore.Scene.commit(stateChanges);
291
+
292
+ const poly = core.scene.entities.get(0);
293
+ expect(poly.points.length).toBe(4);
294
+ expect(poly.points[3].x).toBeCloseTo(0);
295
+ expect(poly.points[3].y).toBeCloseTo(10);
296
+ // Other vertices not affected
297
+ expect(poly.points[0].x).toBeCloseTo(0);
298
+ expect(poly.points[0].y).toBeCloseTo(0);
299
+ });
300
+
301
+ test('ChamferFilletBase.applySharpTrim closed poly closing-seg + seg-1: replaces first vertex (index 0)', () => {
302
+ // Same square. Corner at (0,0) shared by closing seg 4 and seg 1.
303
+ // segDiff = |4-1| = 3 (not 1), closing-wrap detected → cornerIdx = 0.
304
+ // Replaces points[0]=(0,0) with intersectionPoint (0,0).
305
+ core.scene.clear();
306
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
307
+ const polyEntity = core.scene.entities.get(0);
308
+ polyEntity.flags.setFlagValue(1);
309
+
310
+ const chamfer = new Chamfer();
311
+ chamfer.firstPick.entity = polyEntity;
312
+ chamfer.secondPick.entity = polyEntity;
313
+ chamfer.firstPick.segmentIndex = 4; // closing segment
314
+ chamfer.secondPick.segmentIndex = 1;
315
+ chamfer.intersectionPoint = new Point(0, 0);
316
+
317
+ const stateChanges = chamfer.applySharpTrim();
318
+ DesignCore.Scene.commit(stateChanges);
319
+
320
+ const poly = core.scene.entities.get(0);
321
+ expect(poly.points.length).toBe(4);
322
+ expect(poly.points[0].x).toBeCloseTo(0);
323
+ expect(poly.points[0].y).toBeCloseTo(0);
324
+ // Other vertices not affected
325
+ expect(poly.points[1].x).toBeCloseTo(10);
326
+ expect(poly.points[1].y).toBeCloseTo(0);
327
+ // endpoints not moved (open-ends path must not fire)
328
+ expect(poly.points[3].x).toBeCloseTo(0);
329
+ expect(poly.points[3].y).toBeCloseTo(10);
330
+ });
331
+
332
+ // ─── applySharpTrim: Line + Poly ─────────────────────────────────────────────
333
+
334
+ test('ChamferFilletBase.applySharpTrim first=Line second=Poly keepStart: line consumed, poly start portion kept', () => {
335
+ // Line: (0,0)→(0,10); Poly: [(-20,0),(-10,0),(0,0),(10,0)].
336
+ // second=Poly, segmentIndex=2 (segment (-10,0)→(0,0)), click left of intersection → keepStart.
337
+ // Result: Poly becomes [(-20,0),(-10,0),(0,0),(0,10)]; Line removed.
338
+ core.scene.clear();
339
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
340
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
341
+ const lineEntity = core.scene.entities.get(0);
342
+ const polyEntity = core.scene.entities.get(1);
343
+
344
+ const chamfer = new Chamfer();
345
+ chamfer.firstPick.entity = lineEntity;
346
+ chamfer.firstPick.lineStart = lineEntity.points[0]; // (0,0)
347
+ chamfer.firstPick.lineEnd = lineEntity.points[1]; // (0,10)
348
+ chamfer.firstPick.clickPoint = new Point(-1, 5); // projects to (0,5) → lineKeptEnd = (0,10)
349
+ chamfer.secondPick.entity = polyEntity;
350
+ chamfer.secondPick.segmentIndex = 2;
351
+ chamfer.secondPick.lineStart = new Point(-10, 0); // polyEntity.points[1]
352
+ chamfer.secondPick.lineEnd = new Point(0, 0); // polyEntity.points[2]
353
+ chamfer.secondPick.clickPoint = new Point(-5, 2); // projects to (-5,0) → clickDir toward start
354
+ chamfer.intersectionPoint = new Point(0, 0);
355
+
356
+
357
+ const stateChanges = chamfer.applySharpTrim();
358
+ DesignCore.Scene.commit(stateChanges);
359
+
360
+ expect(core.scene.entities.count()).toBe(1);
361
+ const poly = core.scene.entities.get(0);
362
+ expect(poly.points.length).toBe(4);
363
+ expect(poly.points[0].x).toBeCloseTo(-20);
364
+ expect(poly.points[0].y).toBeCloseTo(0);
365
+ expect(poly.points[1].x).toBeCloseTo(-10);
366
+ expect(poly.points[1].y).toBeCloseTo(0);
367
+ expect(poly.points[2].x).toBeCloseTo(0);
368
+ expect(poly.points[2].y).toBeCloseTo(0);
369
+ expect(poly.points[3].x).toBeCloseTo(0);
370
+ expect(poly.points[3].y).toBeCloseTo(10);
371
+ });
372
+
373
+ test('ChamferFilletBase.applySharpTrim first=Line second=Poly keepEnd: line consumed, poly end portion kept', () => {
374
+ // Same setup, but segmentIndex=3. click right of intersection → keepEnd.
375
+ // Result: Poly becomes [(0,10),(0,0),(10,0)]; Line removed.
376
+ core.scene.clear();
377
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
378
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
379
+ const lineEntity = core.scene.entities.get(0);
380
+ const polyEntity = core.scene.entities.get(1);
381
+
382
+ const chamfer = new Chamfer();
383
+ chamfer.firstPick.entity = lineEntity;
384
+ chamfer.firstPick.lineStart = lineEntity.points[0]; // (0,0)
385
+ chamfer.firstPick.lineEnd = lineEntity.points[1]; // (0,10)
386
+ chamfer.firstPick.clickPoint = new Point(-1, 5); // projects to (0,5) → lineKeptEnd = (0,10)
387
+ chamfer.secondPick.entity = polyEntity;
388
+ chamfer.secondPick.segmentIndex = 3;
389
+ chamfer.secondPick.lineStart = new Point(0, 0); // polyEntity.points[2]
390
+ chamfer.secondPick.lineEnd = new Point(10, 0); // polyEntity.points[3]
391
+ chamfer.secondPick.clickPoint = new Point(6, 2); // projects to (6,0) → clickDir toward end
392
+ chamfer.intersectionPoint = new Point(0, 0);
393
+
394
+
395
+ const stateChanges = chamfer.applySharpTrim();
396
+ DesignCore.Scene.commit(stateChanges);
397
+
398
+ expect(core.scene.entities.count()).toBe(1);
399
+ const poly = core.scene.entities.get(0);
400
+ expect(poly.points.length).toBe(3);
401
+ expect(poly.points[0].x).toBeCloseTo(0);
402
+ expect(poly.points[0].y).toBeCloseTo(10);
403
+ expect(poly.points[1].x).toBeCloseTo(0);
404
+ expect(poly.points[1].y).toBeCloseTo(0);
405
+ expect(poly.points[2].x).toBeCloseTo(10);
406
+ expect(poly.points[2].y).toBeCloseTo(0);
407
+ });
408
+
409
+ // ─── applySharpTrim: Poly + Line ─────────────────────────────────────────────
410
+
411
+ test('ChamferFilletBase.applySharpTrim first=Poly second=Line: poly/line ordering assigned correctly', () => {
412
+ // Identical geometry to the keepStart test but with first=Poly and second=Line.
413
+ // firstIsPolyline=true → [poly, line] = [this.firstPick, this.secondPick].
414
+ // Result should be the same: Poly becomes [(-20,0),(-10,0),(0,0),(0,10)]; Line removed.
415
+ core.scene.clear();
416
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
417
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
418
+ const polyEntity = core.scene.entities.get(0);
419
+ const lineEntity = core.scene.entities.get(1);
420
+
421
+ const chamfer = new Chamfer();
422
+ chamfer.firstPick.entity = polyEntity;
423
+ chamfer.firstPick.segmentIndex = 2;
424
+ chamfer.firstPick.lineStart = new Point(-10, 0); // polyEntity.points[1]
425
+ chamfer.firstPick.lineEnd = new Point(0, 0); // polyEntity.points[2]
426
+ chamfer.firstPick.clickPoint = new Point(-5, 2); // projects to (-5,0) → clickDir toward start
427
+ chamfer.secondPick.entity = lineEntity;
428
+ chamfer.secondPick.lineStart = lineEntity.points[0]; // (0,0)
429
+ chamfer.secondPick.lineEnd = lineEntity.points[1]; // (0,10)
430
+ chamfer.secondPick.clickPoint = new Point(-1, 5); // projects to (0,5) → lineKeptEnd = (0,10)
431
+ chamfer.intersectionPoint = new Point(0, 0);
432
+
433
+
434
+ const stateChanges = chamfer.applySharpTrim();
435
+ DesignCore.Scene.commit(stateChanges);
436
+
437
+ expect(core.scene.entities.count()).toBe(1);
438
+ const poly = core.scene.entities.get(0);
439
+ expect(poly.points.length).toBe(4);
440
+ expect(poly.points[0].x).toBeCloseTo(-20);
441
+ expect(poly.points[1].x).toBeCloseTo(-10);
442
+ expect(poly.points[2].x).toBeCloseTo(0);
443
+ expect(poly.points[2].y).toBeCloseTo(0);
444
+ expect(poly.points[3].x).toBeCloseTo(0);
445
+ expect(poly.points[3].y).toBeCloseTo(10);
446
+ });
@@ -1,20 +1,20 @@
1
- import {Core} from '../../core/core/core.js';
2
- import {Point} from '../../core/entities/point.js';
3
- import {Copy} from '../../core/tools/copy.js';
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Copy } from '../../core/tools/copy.js';
4
4
 
5
5
  const core = new Core();
6
6
 
7
7
  test('Test Copy.action', () => {
8
8
  // Add items to scene
9
- core.scene.addItem('Line', {points: [new Point(), new Point(0, 10)]});
10
- core.scene.addItem('Circle', {points: [new Point(), new Point(0, 10)]});
11
- core.scene.addItem('Polyline', {points: [new Point(), new Point(0, 10)]});
12
- core.scene.addItem('Arc', {points: [new Point(), new Point(0, 10), new Point(10, 0)]});
13
- core.scene.addItem('Rectangle', {points: [new Point(), new Point(0, 10)]});
14
- core.scene.addItem('Text', {points: [new Point(), new Point(0, 10)], height: 10, rotation: 0, string: 'text test'});
9
+ core.scene.addItem('Line', { points: [new Point(), new Point(0, 10)] });
10
+ core.scene.addItem('Circle', { points: [new Point(), new Point(0, 10)] });
11
+ core.scene.addItem('Polyline', { points: [new Point(), new Point(0, 10)] });
12
+ core.scene.addItem('Arc', { points: [new Point(), new Point(0, 10), new Point(10, 0)] });
13
+ core.scene.addItem('Rectangle', { points: [new Point(), new Point(0, 10)] });
14
+ core.scene.addItem('Text', { points: [new Point(), new Point(0, 10)], height: 10, rotation: 0, string: 'text test' });
15
15
 
16
16
  // Add items to selection set
17
- for (let i = 0; i < core.scene.items.length; i++) {
17
+ for (let i = 0; i < core.scene.entities.count(); i++) {
18
18
  core.scene.selectionManager.addToSelectionSet(i);
19
19
  }
20
20
 
@@ -33,10 +33,18 @@ test('Test Copy.action', () => {
33
33
  // Perform Copy
34
34
  copy.action();
35
35
 
36
- expect(core.scene.items.length).toBe(12);
36
+ expect(core.scene.entities.count()).toBe(12);
37
37
 
38
- for (let i = 6; i < core.scene.items.length; i++) {
39
- expect(core.scene.items[i].points[0].x).toBe(10);
40
- expect(core.scene.items[i].points[0].y).toBe(0);
38
+ for (let i = 6; i < core.scene.entities.count(); i++) {
39
+ expect(core.scene.entities.get(i).points[0].x).toBe(10);
40
+ expect(core.scene.entities.get(i).points[0].y).toBe(0);
41
+ }
42
+
43
+ // Verify copied entities have unique handles
44
+ for (let i = 0; i < 6; i++) {
45
+ const original = core.scene.entities.get(i);
46
+ const copy = core.scene.entities.get(i + 6);
47
+ expect(copy.handle).toBeDefined();
48
+ expect(copy.handle).not.toBe(original.handle);
41
49
  }
42
50
  });
@@ -0,0 +1,93 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Copybase } from '../../core/tools/copybase.js';
4
+ import { DesignCore } from '../../core/designCore.js';
5
+
6
+ import { jest } from '@jest/globals';
7
+
8
+ // Initialize core (sets DesignCore.Core)
9
+ new Core();
10
+
11
+ describe('Copybase Tool', () => {
12
+ beforeEach(() => {
13
+ // reset scene entities and selection
14
+ DesignCore.Scene.entities.clear();
15
+ DesignCore.Scene.selectionManager.reset();
16
+ // clear clipboard
17
+ DesignCore.Clipboard.Entities = [];
18
+ });
19
+
20
+ test('action copies selected entities and sets base point to user-provided point', () => {
21
+ // Create two line entities and add to scene
22
+ const line1 = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(10, 20), new Point(30, 40)] });
23
+ const line2 = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(-5, 100), new Point(0, 80)] });
24
+ DesignCore.Scene.entities.add(line1);
25
+ DesignCore.Scene.entities.add(line2);
26
+
27
+ // Select both
28
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
29
+ DesignCore.Scene.selectionManager.addToSelectionSet(1);
30
+ expect(DesignCore.Scene.selectionManager.selectedItems).toHaveLength(2);
31
+
32
+ const tool = new Copybase();
33
+ tool.points.push(new Point(10, 11)); // set base point
34
+ tool.action();
35
+
36
+ // Clipboard now holds selected items (cloned)
37
+ expect(DesignCore.Clipboard.Entities).toHaveLength(2);
38
+ // BasePoint should be min x (-5), min y (20) across all points
39
+ expect(DesignCore.Clipboard.BasePoint.x).toBe(10);
40
+ expect(DesignCore.Clipboard.BasePoint.y).toBe(11);
41
+ });
42
+
43
+ test('clipboard entities are clones (mutating original does not change clipboard)', () => {
44
+ const line = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(0, 0), new Point(10, 0)] });
45
+ DesignCore.Scene.entities.add(line);
46
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
47
+ const originalClonePoints = DesignCore.Scene.selectionManager.selectedItems[0].points.map((p) => ({ x: p.x, y: p.y }));
48
+
49
+ const tool = new Copybase();
50
+ tool.action();
51
+ expect(DesignCore.Clipboard.Entities).toHaveLength(1);
52
+
53
+ // Mutate original entity
54
+ line.points[0].x = 999;
55
+ // Clipboard should remain with old coordinates
56
+ expect(DesignCore.Clipboard.Entities[0].points[0].x).toBe(originalClonePoints[0].x);
57
+ });
58
+
59
+ test('execute requests selection when none selected then calls executeCommand', async () => {
60
+ const tool = new Copybase();
61
+ const requestSpy = jest.fn().mockResolvedValue(undefined);
62
+ const execSpy = jest.fn();
63
+ DesignCore.Scene.selectionManager.reset();
64
+
65
+ // Mock inputManager methods
66
+ DesignCore.Scene.inputManager.requestInput = requestSpy;
67
+ DesignCore.Scene.inputManager.executeCommand = execSpy;
68
+
69
+ await tool.execute();
70
+ expect(requestSpy).toHaveBeenCalledTimes(2);
71
+ expect(execSpy).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ test('execute skips requestInput when selection exists', async () => {
75
+ // Prepare selection
76
+ const ent = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(1, 1), new Point(2, 2)] });
77
+ DesignCore.Scene.entities.add(ent);
78
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
79
+ expect(DesignCore.Scene.selectionManager.selectionSet.selectionSet.length).toBe(1);
80
+
81
+ const tool = new Copybase();
82
+ const requestSpy = jest.fn().mockResolvedValue(undefined);
83
+ const execSpy = jest.fn();
84
+ DesignCore.Scene.inputManager.requestInput = requestSpy;
85
+ DesignCore.Scene.inputManager.executeCommand = execSpy;
86
+
87
+ await tool.execute();
88
+ expect(requestSpy).toHaveBeenCalledTimes(1);
89
+ expect(execSpy).toHaveBeenCalledTimes(1);
90
+ });
91
+ });
92
+
93
+