@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
@@ -1,8 +1,1147 @@
1
1
 
2
- // import {InputManager} from '../../lib/inputManager.js';
3
- // import {Core} from '../../core.js';
2
+ import { Core } from '../../core/core/core.js';
3
+ import { DesignCore } from '../../core/designCore.js';
4
+ import { Line } from '../../core/entities/line.js';
5
+ import { Point } from '../../core/entities/point.js';
6
+ import { PromptOptions } from '../../core/lib/inputManager.js';
7
+ import { Input, MouseDown, MouseUp } from '../../core/lib/inputManager.js';
8
+ import { SingleSelection } from '../../core/lib/selectionManager.js';
9
+ import { expect, jest } from '@jest/globals';
4
10
 
5
- // const core = new Core();
6
- // const inputManager = new InputManager();
11
+ const core = new Core();
12
+ const inputManager = core.scene.inputManager;
7
13
 
8
- test.todo('Test input manager');
14
+ afterEach(() => {
15
+ // Cancel any in-flight prompt and clear command/snapping state so tests
16
+ // cannot leak into one another regardless of pass/fail or input path taken.
17
+ inputManager.reset();
18
+ // Restore all spies unconditionally so mocks don't bleed across tests.
19
+ jest.restoreAllMocks();
20
+ });
21
+
22
+ test('requestInput resolves when PromptOptions.respond is called', async () => {
23
+ const po = new PromptOptions('Enter text', [Input.Type.STRING]);
24
+ const p = inputManager.requestInput(po);
25
+
26
+ // resolve via PromptOptions.respond
27
+ po.respond('hello');
28
+
29
+ await expect(p).resolves.toBe('hello');
30
+ });
31
+
32
+ test('snapping is activated for POINT prompt and deactivated after respond', async () => {
33
+ const po = new PromptOptions('Pick a point', [Input.Type.POINT]);
34
+ const p = inputManager.requestInput(po);
35
+
36
+ // snapping should be active while awaiting a POINT
37
+ expect(inputManager.snapping.active).toBeTruthy();
38
+
39
+ // respond with a point to resolve the promise
40
+ po.respond(new Point(1, 2));
41
+
42
+ await p;
43
+
44
+ // reset input manager for cleanup
45
+ inputManager.reset();
46
+
47
+ // snapping should be turned off after input is processed
48
+ expect(inputManager.snapping.active).toBeFalsy();
49
+ });
50
+
51
+ test('reset clears activeCommand, promptOption and deactivates snapping', () => {
52
+ inputManager.activeCommand = new Line();
53
+ inputManager.promptOption = new PromptOptions('Test Prompt', [Input.Type.POINT]);
54
+ inputManager.snapping.active = true;
55
+
56
+ expect(inputManager.activeCommand).toBeDefined();
57
+ expect(inputManager.promptOption).toBeDefined();
58
+ expect(inputManager.snapping.active).toBeTruthy();
59
+
60
+ inputManager.reset();
61
+
62
+ expect(inputManager.activeCommand).toBeUndefined();
63
+ expect(inputManager.promptOption).toBeUndefined();
64
+ expect(inputManager.snapping.active).toBeFalsy();
65
+ });
66
+
67
+ test('mouseMoved draws selection window when mouse button one is down and no promptOption', () => {
68
+ // ensure no active promptOption
69
+ inputManager.promptOption = undefined;
70
+
71
+ // spy on selectionManager.drawSelectionWindow
72
+ const spy = jest.spyOn(core.scene.selectionManager, 'drawSelectionWindow').mockImplementation(() => {});
73
+
74
+ // simulate mouse down state
75
+ core.scene.inputManager.mouseDownCanvasPoint = new Point(0, 0);
76
+ core.mouse.buttonOneDown = true;
77
+
78
+ inputManager.mouseMoved();
79
+
80
+ expect(spy).toHaveBeenCalled();
81
+
82
+ spy.mockRestore();
83
+ core.mouse.buttonOneDown = false;
84
+ });
85
+
86
+ test('test onCommand with no activeCommand or promptOption', async () => {
87
+ // Test Unknown Command
88
+ inputManager.reset();
89
+ inputManager.onCommand('TEST_COMMAND');
90
+ // No activeCommand or promptOption should remain undefined
91
+ expect(inputManager.activeCommand).toBeUndefined();
92
+ expect(inputManager.promptOption).toBeUndefined();
93
+
94
+ // Test Known Entity Command
95
+ inputManager.reset();
96
+ inputManager.onCommand('Line');
97
+ // No activeCommand or promptOption should remain undefined
98
+ expect(inputManager.activeCommand).toBeDefined();
99
+ expect(inputManager.promptOption).toBeDefined();
100
+
101
+ // Test Known Tool Command
102
+ inputManager.reset();
103
+ inputManager.onCommand('Rotate');
104
+ // No activeCommand or promptOption should remain undefined
105
+ expect(inputManager.activeCommand).toBeDefined();
106
+ expect(inputManager.promptOption).toBeDefined();
107
+
108
+ const po = new PromptOptions('Point or Number', [Input.Type.POINT, Input.Type.NUMBER], ['One', 'Two', 'Three']);
109
+
110
+ const numericalInput = core.scene.inputManager.requestInput(po);
111
+ expect(inputManager.promptOption).toBe(po);
112
+ inputManager.onCommand(42); // numerical input
113
+ await expect(numericalInput).resolves.toBe(42);
114
+
115
+ const pointInput = core.scene.inputManager.requestInput(po);
116
+ const point = new Point(5, 10);
117
+ inputManager.onCommand(point); // point input
118
+ await expect(pointInput).resolves.toBe(point);
119
+
120
+ const optionInputOne = core.scene.inputManager.requestInput(po);
121
+ inputManager.onCommand('O'); // option input
122
+ await expect(optionInputOne).resolves.toBe('One');
123
+
124
+ const optionInputTwo = core.scene.inputManager.requestInput(po);
125
+ inputManager.onCommand('T'); // option input
126
+ await expect(optionInputTwo).resolves.toBe('Two');
127
+
128
+ const optionInputThree = core.scene.inputManager.requestInput(po);
129
+ inputManager.onCommand('Th'); // option input
130
+ await expect(optionInputThree).resolves.toBe('Three');
131
+
132
+ const stringInput = core.scene.inputManager.requestInput(po);
133
+ inputManager.onCommand('Some String'); // string input
134
+ inputManager.onCommand('Another String'); // string input
135
+ inputManager.onCommand(123); // numerical input to finally resolve
136
+ await expect(stringInput).resolves.toBe(123);
137
+ });
138
+
139
+ test('Test Input.getType', () => {
140
+ expect(Input.getType(100)).toBe(Input.Type.NUMBER);
141
+ expect(Input.getType('text')).toBe(Input.Type.STRING);
142
+ });
143
+
144
+ test('Test tool switching - click circle then rectangle', () => {
145
+ // Reset input manager
146
+ inputManager.reset();
147
+
148
+ // Initially no active command
149
+ expect(inputManager.activeCommand).toBeUndefined();
150
+
151
+ // Click circle tool (shortcut 'C')
152
+ inputManager.onCommand('C');
153
+ expect(inputManager.activeCommand).not.toBeUndefined();
154
+ expect(inputManager.activeCommand.constructor.name).toBe('Circle');
155
+
156
+ // Click rectangle tool (shortcut 'REC') - should switch tools
157
+ inputManager.onCommand('REC');
158
+ expect(inputManager.activeCommand).not.toBeUndefined();
159
+ expect(inputManager.activeCommand.constructor.name).toBe('Rectangle');
160
+
161
+ // Reset for cleanup
162
+ inputManager.reset();
163
+ });
164
+
165
+ test('Test tool switching - click rectangle then circle', () => {
166
+ // Reset input manager
167
+ inputManager.reset();
168
+
169
+ // Initially no active command
170
+ expect(inputManager.activeCommand).toBeUndefined();
171
+
172
+ // Click rectangle tool (shortcut 'REC')
173
+ inputManager.onCommand('REC');
174
+ expect(inputManager.activeCommand).not.toBeUndefined();
175
+ expect(inputManager.activeCommand.constructor.name).toBe('Rectangle');
176
+
177
+ // Click circle tool (shortcut 'C') - should switch tools
178
+ inputManager.onCommand('C');
179
+ expect(inputManager.activeCommand).not.toBeUndefined();
180
+ expect(inputManager.activeCommand.constructor.name).toBe('Circle');
181
+
182
+ // Reset for cleanup
183
+ inputManager.reset();
184
+ });
185
+
186
+ test('Test tool switching with invalid command', () => {
187
+ // Reset input manager
188
+ inputManager.reset();
189
+
190
+ // Start with circle tool
191
+ inputManager.onCommand('C');
192
+ expect(inputManager.activeCommand.constructor.name).toBe('Circle');
193
+
194
+ // Try to switch to invalid command - should not change active command
195
+ const originalCommand = inputManager.activeCommand;
196
+ inputManager.onCommand('INVALID');
197
+ expect(inputManager.activeCommand).toBe(originalCommand);
198
+
199
+ // Reset for cleanup
200
+ inputManager.reset();
201
+ });
202
+
203
+ // ─── onCommandButton ──────────────────────────────────────────────────────────
204
+
205
+ test('onCommandButton with valid shortcut - starts command, ignores active prompt', async () => {
206
+ inputManager.reset();
207
+ // Simulate an active command with a STRING prompt waiting
208
+ inputManager.activeCommand = new Line();
209
+ const po = new PromptOptions('Enter text', [Input.Type.STRING]);
210
+ const p = inputManager.requestInput(po);
211
+
212
+ // clicking a toolbar button while STRING prompt is active should switch commands
213
+ inputManager.onCommandButton('L');
214
+ expect(inputManager.activeCommand).toBeDefined();
215
+ expect(inputManager.activeCommand.constructor.name).toBe('Line');
216
+
217
+ // the STRING promise should have been cancelled (resolved with undefined)
218
+ await expect(p).resolves.toBeUndefined();
219
+ inputManager.reset();
220
+ });
221
+
222
+ test('onCommandButton with invalid command - does nothing', () => {
223
+ inputManager.reset();
224
+ inputManager.onCommand('Line');
225
+ const original = inputManager.activeCommand;
226
+ inputManager.onCommandButton('NOTACOMMAND');
227
+ expect(inputManager.activeCommand).toBe(original);
228
+ inputManager.reset();
229
+ });
230
+
231
+ test('onCommandButton with no active command - starts command', () => {
232
+ inputManager.reset();
233
+ expect(inputManager.activeCommand).toBeUndefined();
234
+ inputManager.onCommandButton('REC');
235
+ expect(inputManager.activeCommand).toBeDefined();
236
+ expect(inputManager.activeCommand.constructor.name).toBe('Rectangle');
237
+ inputManager.reset();
238
+ });
239
+
240
+ // ─── onCommand - command shortcut while non-accepting prompt is active ─────────
241
+
242
+ test('onCommand with valid shortcut while POINT prompt active - switches command without notifying', () => {
243
+ inputManager.reset();
244
+ inputManager.onCommand('Line');
245
+ expect(inputManager.promptOption.types).toContain(Input.Type.POINT);
246
+
247
+ const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
248
+ inputManager.onCommand('C');
249
+ expect(notifySpy).not.toHaveBeenCalled();
250
+ expect(inputManager.activeCommand.constructor.name).toBe('Circle');
251
+ notifySpy.mockRestore();
252
+ inputManager.reset();
253
+ });
254
+
255
+ test('onCommand with valid shortcut while STRING prompt active - sends string to prompt', async () => {
256
+ inputManager.reset();
257
+ // Simulate an active command with a STRING prompt waiting
258
+ inputManager.activeCommand = new Line();
259
+ const po = new PromptOptions('Enter text', [Input.Type.STRING]);
260
+ const p = inputManager.requestInput(po);
261
+
262
+ // 'L' is the Line shortcut but a STRING prompt accepts any string - should go to prompt
263
+ inputManager.onCommand('L');
264
+ const result = await p;
265
+ expect(result).toBe('L');
266
+ // activeCommand is still the original Line stub, not a newly-started Line
267
+ expect(inputManager.activeCommand.constructor.name).toBe('Line');
268
+ inputManager.reset();
269
+ });
270
+
271
+ // ─── Input ────────────────────────────────────────────────────────────────────
272
+
273
+ test('Input.getType(undefined) returns undefined', () => {
274
+ expect(Input.getType(undefined)).toBeUndefined();
275
+ });
276
+
277
+ test('Input.getType returns Number for numeric values regardless of prompt', () => {
278
+ const po = new PromptOptions('Enter distance', [Input.Type.DYNAMIC]);
279
+ inputManager.requestInput(po);
280
+ expect(Input.getType(42)).toBe(Input.Type.NUMBER);
281
+ po.cancel();
282
+ inputManager.reset();
283
+ });
284
+
285
+ // ─── onEnterPressed ───────────────────────────────────────────────────────────
286
+
287
+ test('onEnterPressed with promptOption and defaultValue resolves with defaultValue', async () => {
288
+ inputManager.reset();
289
+ inputManager.activeCommand = new Line();
290
+ const po = new PromptOptions('Enter distance', [Input.Type.NUMBER], [], 5);
291
+ const p = inputManager.requestInput(po);
292
+ inputManager.onEnterPressed();
293
+ await expect(p).resolves.toBe(5);
294
+ inputManager.reset();
295
+ });
296
+
297
+ test('onEnterPressed with no activeCommand and empty lastCommand - does nothing', () => {
298
+ inputManager.reset();
299
+ core.commandLine.lastCommand = [];
300
+ expect(() => inputManager.onEnterPressed()).not.toThrow();
301
+ expect(inputManager.activeCommand).toBeUndefined();
302
+ });
303
+
304
+ test('onEnterPressed with no activeCommand and lastCommand history - repeats last command', () => {
305
+ inputManager.reset();
306
+ core.commandLine.lastCommand = ['Line'];
307
+ inputManager.onEnterPressed();
308
+ expect(inputManager.activeCommand).toBeDefined();
309
+ expect(inputManager.activeCommand.constructor.name).toBe('Line');
310
+ inputManager.reset();
311
+ });
312
+
313
+ test('onEnterPressed with activeCommand but no promptOption - resets', () => {
314
+ inputManager.reset();
315
+ inputManager.activeCommand = new Line();
316
+ inputManager.promptOption = undefined;
317
+ inputManager.onEnterPressed();
318
+ expect(inputManager.activeCommand).toBeUndefined();
319
+ });
320
+
321
+ test('onEnterPressed with SELECTIONSET prompt and unaccepted selectionSet - accepts and responds', async () => {
322
+ inputManager.reset();
323
+ inputManager.activeCommand = new Line();
324
+ const po = new PromptOptions('Select entities', [Input.Type.SELECTIONSET]);
325
+ const p = inputManager.requestInput(po);
326
+ core.scene.selectionManager.selectionSet.accepted = false;
327
+ inputManager.onEnterPressed();
328
+ const result = await p;
329
+ expect(result).toBeDefined();
330
+ inputManager.reset();
331
+ });
332
+
333
+ // ─── onEscapePressed ──────────────────────────────────────────────────────────
334
+
335
+ test('onEscapePressed resets the input manager', () => {
336
+ inputManager.onCommand('Line');
337
+ expect(inputManager.activeCommand).toBeDefined();
338
+ inputManager.onEscapePressed();
339
+ expect(inputManager.activeCommand).toBeUndefined();
340
+ expect(inputManager.promptOption).toBeUndefined();
341
+ });
342
+
343
+ // ─── onLeftClick ──────────────────────────────────────────────────────────────
344
+
345
+ test('onLeftClick with POINT prompt and no nearby entity - responds with the given point', () => {
346
+ inputManager.reset();
347
+ inputManager.onCommand('Line');
348
+ expect(inputManager.promptOption.types).toContain(Input.Type.POINT);
349
+ const respondSpy = jest.spyOn(inputManager.promptOption, 'respond');
350
+ const point = new Point(5, 5);
351
+ inputManager.onLeftClick(point);
352
+ expect(respondSpy).toHaveBeenCalledWith(point);
353
+ respondSpy.mockRestore();
354
+ inputManager.reset();
355
+ });
356
+
357
+ test('onLeftClick with entity found and no active command - delegates to onSelection', () => {
358
+ inputManager.reset();
359
+ const mockSelection = { selectedItemIndex: 0 };
360
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
361
+ jest.spyOn(core.scene.selectionManager, 'singleSelect').mockReturnValue(mockSelection);
362
+ const onSelectionSpy = jest.spyOn(inputManager, 'onSelection').mockImplementation(() => {});
363
+ inputManager.onLeftClick(new Point(0, 0));
364
+ expect(onSelectionSpy).toHaveBeenCalledWith(mockSelection);
365
+ jest.restoreAllMocks();
366
+ });
367
+
368
+ // ─── onSelection ──────────────────────────────────────────────────────────────
369
+
370
+ test('onSelection with SINGLESELECTION prompt - resolves promptOption with the selection', async () => {
371
+ inputManager.reset();
372
+ inputManager.activeCommand = new Line();
373
+ const po = new PromptOptions('Select entity', [Input.Type.SINGLESELECTION]);
374
+ const p = inputManager.requestInput(po);
375
+ const selection = new SingleSelection(0, new Point(0, 0));
376
+ inputManager.onSelection(selection);
377
+ await expect(p).resolves.toBe(selection);
378
+ inputManager.reset();
379
+ });
380
+
381
+ test('onSelection without active command - adds item index to selection set', () => {
382
+ inputManager.reset();
383
+ const addToSelectionSetSpy = jest.spyOn(core.scene.selectionManager, 'addToSelectionSet').mockImplementation(() => {});
384
+ const selection = new SingleSelection(7, new Point(0, 0));
385
+ inputManager.onSelection(selection);
386
+ expect(addToSelectionSetSpy).toHaveBeenCalledWith(7);
387
+ addToSelectionSetSpy.mockRestore();
388
+ });
389
+
390
+ // ─── mouseDown ────────────────────────────────────────────────────────────────
391
+
392
+ test('mouseDown case 0 calls singleSelect', () => {
393
+ const singleSelectSpy = jest.spyOn(inputManager, 'singleSelect').mockImplementation(() => {});
394
+ inputManager.mouseDown(0);
395
+ expect(singleSelectSpy).toHaveBeenCalled();
396
+ singleSelectSpy.mockRestore();
397
+ });
398
+
399
+ test('mouseDown case 0 with MOUSEDOWN prompt - resolves the prompt', async () => {
400
+ inputManager.reset();
401
+ const singleSelectSpy = jest.spyOn(inputManager, 'singleSelect').mockImplementation(() => {});
402
+ const po = new PromptOptions('', [Input.Type.MOUSEDOWN]);
403
+ const p = inputManager.requestInput(po);
404
+ inputManager.mouseDown(0);
405
+ await expect(p).resolves.toBeInstanceOf(MouseDown);
406
+ singleSelectSpy.mockRestore();
407
+ inputManager.reset();
408
+ });
409
+
410
+ // ─── mouseUp ──────────────────────────────────────────────────────────────────
411
+
412
+ test('mouseUp case 2 triggers onEnterPressed and resets active command', () => {
413
+ inputManager.onCommand('Line');
414
+ expect(inputManager.activeCommand).toBeDefined();
415
+ inputManager.mouseUp(2);
416
+ expect(inputManager.activeCommand).toBeUndefined();
417
+ });
418
+
419
+ test('mouseUp case 0 with MOUSEUP prompt - resolves the prompt and returns early', async () => {
420
+ inputManager.reset();
421
+ const windowSelectSpy = jest.spyOn(inputManager, 'windowSelect').mockImplementation(() => {});
422
+ const po = new PromptOptions('', [Input.Type.MOUSEUP]);
423
+ const p = inputManager.requestInput(po);
424
+ inputManager.mouseUp(0);
425
+ await expect(p).resolves.toBeInstanceOf(MouseUp);
426
+ expect(windowSelectSpy).not.toHaveBeenCalled();
427
+ windowSelectSpy.mockRestore();
428
+ inputManager.reset();
429
+ });
430
+
431
+ test('mouseUp case 0 without SELECTIONSET - returns early without calling windowSelect', () => {
432
+ inputManager.reset();
433
+ const windowSelectSpy = jest.spyOn(inputManager, 'windowSelect').mockImplementation(() => {});
434
+ const po = new PromptOptions('Enter point', [Input.Type.POINT]);
435
+ inputManager.requestInput(po);
436
+ inputManager.mouseUp(0);
437
+ expect(windowSelectSpy).not.toHaveBeenCalled();
438
+ windowSelectSpy.mockRestore();
439
+ inputManager.reset();
440
+ });
441
+
442
+ test('mouseUp case 0 with SELECTIONSET and mouse moved - calls windowSelect', () => {
443
+ inputManager.reset();
444
+ const windowSelectSpy = jest.spyOn(inputManager, 'windowSelect').mockImplementation(() => {});
445
+ const po = new PromptOptions('', [Input.Type.SELECTIONSET]);
446
+ inputManager.requestInput(po);
447
+ core.mouse.mouseDownCanvasPoint = new Point(0, 0);
448
+ core.mouse.x = 100;
449
+ core.mouse.y = 100;
450
+ inputManager.mouseUp(0);
451
+ expect(windowSelectSpy).toHaveBeenCalled();
452
+ windowSelectSpy.mockRestore();
453
+ core.mouse.x = 0;
454
+ core.mouse.y = 0;
455
+ inputManager.reset();
456
+ });
457
+
458
+ // ─── windowSelect ─────────────────────────────────────────────────────────────
459
+
460
+ test('windowSelect delegates to selectionManager.windowSelect', () => {
461
+ const spy = jest.spyOn(core.scene.selectionManager, 'windowSelect').mockImplementation(() => {});
462
+ inputManager.windowSelect();
463
+ expect(spy).toHaveBeenCalled();
464
+ spy.mockRestore();
465
+ });
466
+
467
+ // ─── singleSelect ─────────────────────────────────────────────────────────────
468
+
469
+ test('singleSelect calls onLeftClick with the current mouse scene position', () => {
470
+ const onLeftClickSpy = jest.spyOn(inputManager, 'onLeftClick').mockImplementation(() => {});
471
+ core.mouse.x = 10;
472
+ core.mouse.y = 20;
473
+ inputManager.singleSelect();
474
+ expect(onLeftClickSpy).toHaveBeenCalled();
475
+ expect(onLeftClickSpy.mock.calls[0][0]).toBeInstanceOf(Point);
476
+ onLeftClickSpy.mockRestore();
477
+ });
478
+
479
+ // ─── setPrompt ────────────────────────────────────────────────────────────────
480
+
481
+ test('setPrompt with no active command - returns without error', () => {
482
+ inputManager.reset();
483
+ expect(() => inputManager.setPrompt('anything')).not.toThrow();
484
+ });
485
+
486
+ test('setPrompt with an active command - sends formatted prompt to commandLine', () => {
487
+ inputManager.reset();
488
+ inputManager.onCommand('Line');
489
+ const setPromptSpy = jest.spyOn(core.commandLine, 'setPrompt');
490
+ inputManager.setPrompt('Pick start point');
491
+ expect(setPromptSpy).toHaveBeenCalledWith('Line- Pick start point');
492
+ setPromptSpy.mockRestore();
493
+ inputManager.reset();
494
+ });
495
+
496
+ // ─── actionCommand / executeCommand ───────────────────────────────────────────
497
+
498
+ test('actionCommand with a Tool as activeCommand - calls its action method', () => {
499
+ inputManager.reset();
500
+ inputManager.onCommand('Rotate');
501
+ expect(inputManager.activeCommand).toBeDefined();
502
+ const actionSpy = jest.spyOn(inputManager.activeCommand, 'action').mockImplementation(() => {});
503
+ inputManager.actionCommand();
504
+ expect(actionSpy).toHaveBeenCalled();
505
+ actionSpy.mockRestore();
506
+ inputManager.reset();
507
+ });
508
+
509
+ test('executeCommand with an entity item - adds item to scene then resets', () => {
510
+ inputManager.reset();
511
+ inputManager.onCommand('Line');
512
+ const addItemSpy = jest.spyOn(core.scene, 'addItem').mockReturnValue(1);
513
+ const line = new Line();
514
+ inputManager.executeCommand(line);
515
+ expect(addItemSpy).toHaveBeenCalled();
516
+ expect(inputManager.activeCommand).toBeUndefined();
517
+ addItemSpy.mockRestore();
518
+ });
519
+
520
+ // ─── requestInput validation ──────────────────────────────────────────────────
521
+
522
+ test('requestInput throws on undefined Input.Type', () => {
523
+ const po = new PromptOptions('bad', [undefined]);
524
+ expect(() => inputManager.requestInput(po)).toThrow('Undefined Input.Type');
525
+ });
526
+
527
+ test('requestInput throws on invalid Input.Type', () => {
528
+ const po = new PromptOptions('bad', ['NotAType']);
529
+ expect(() => inputManager.requestInput(po)).toThrow('Invalid input type');
530
+ });
531
+
532
+ // ─── requestInput DYNAMIC conversion ─────────────────────────────────────────
533
+
534
+ test('requestInput with DYNAMIC type converts number to projected point', async () => {
535
+ inputManager.reset();
536
+ inputManager.inputPoint = new Point(0, 0);
537
+ // set mouse position to create a known angle
538
+ core.mouse.x = 100;
539
+ core.mouse.y = 0;
540
+
541
+ const po = new PromptOptions('Distance', [Input.Type.POINT, Input.Type.DYNAMIC]);
542
+ const p = inputManager.requestInput(po);
543
+ po.resolve(50);
544
+ const result = await p;
545
+
546
+ // result should be a Point, not the raw number
547
+ expect(result).toBeInstanceOf(Point);
548
+ inputManager.reset();
549
+ core.mouse.x = 0;
550
+ core.mouse.y = 0;
551
+ });
552
+
553
+ test('requestInput with non-DYNAMIC type passes number through unchanged', async () => {
554
+ inputManager.reset();
555
+ const po = new PromptOptions('Value', [Input.Type.NUMBER]);
556
+ const p = inputManager.requestInput(po);
557
+ po.resolve(42);
558
+ const result = await p;
559
+ expect(result).toBe(42);
560
+ inputManager.reset();
561
+ });
562
+
563
+ // ─── requestInput inputPoint tracking ─────────────────────────────────────────
564
+
565
+ test('requestInput updates inputPoint when input is a Point', async () => {
566
+ inputManager.reset();
567
+ inputManager.inputPoint = new Point(0, 0);
568
+ const po = new PromptOptions('Point', [Input.Type.POINT]);
569
+ const p = inputManager.requestInput(po);
570
+ const pt = new Point(99, 77);
571
+ po.resolve(pt);
572
+ await p;
573
+ expect(inputManager.inputPoint.x).toBe(99);
574
+ expect(inputManager.inputPoint.y).toBe(77);
575
+ inputManager.reset();
576
+ });
577
+
578
+ test('requestInput does not update inputPoint for non-Point input', async () => {
579
+ inputManager.reset();
580
+ inputManager.inputPoint = new Point(5, 5);
581
+ const po = new PromptOptions('Text', [Input.Type.STRING]);
582
+ const p = inputManager.requestInput(po);
583
+ po.resolve('hello');
584
+ await p;
585
+ expect(inputManager.inputPoint.x).toBe(5);
586
+ expect(inputManager.inputPoint.y).toBe(5);
587
+ inputManager.reset();
588
+ });
589
+
590
+ test('requestInput returns undefined when cancelled', async () => {
591
+ inputManager.reset();
592
+ inputManager.inputPoint = new Point(5, 5);
593
+ const po = new PromptOptions('Point', [Input.Type.POINT]);
594
+ const p = inputManager.requestInput(po);
595
+ po.cancel();
596
+ const result = await p;
597
+ expect(result).toBeUndefined();
598
+ // inputPoint should not be changed
599
+ expect(inputManager.inputPoint.x).toBe(5);
600
+ inputManager.reset();
601
+ });
602
+
603
+ // ─── Input.getType for all types ──────────────────────────────────────────────
604
+
605
+ test('Input.getType returns correct type for Point', () => {
606
+ expect(Input.getType(new Point(1, 2))).toBe(Input.Type.POINT);
607
+ });
608
+
609
+ test('Input.getType returns correct type for SingleSelection', () => {
610
+ expect(Input.getType(new SingleSelection(0, new Point()))).toBe(Input.Type.SINGLESELECTION);
611
+ });
612
+
613
+ // ─── handlePromptInput ────────────────────────────────────────────────────────
614
+
615
+ test('handlePromptInput returns false when promptOption is null', () => {
616
+ inputManager.reset();
617
+ inputManager.promptOption = undefined;
618
+ expect(inputManager.handlePromptInput('anything')).toBe(false);
619
+ });
620
+
621
+ test('handlePromptInput returns false when input is rejected by respond', () => {
622
+ inputManager.reset();
623
+ const notifySpy = jest.spyOn(core, 'notify').mockImplementation(() => {});
624
+ inputManager.activeCommand = new Line();
625
+ const po = new PromptOptions('Enter point', [Input.Type.POINT]);
626
+ inputManager.requestInput(po);
627
+
628
+ // string input should not match POINT type
629
+ const result = inputManager.handlePromptInput('not a point');
630
+ expect(result).toBe(false);
631
+
632
+ notifySpy.mockRestore();
633
+ inputManager.reset();
634
+ });
635
+
636
+ // ─── setPrompt edge cases ─────────────────────────────────────────────────────
637
+
638
+ test('setPrompt with empty string omits separator', () => {
639
+ inputManager.reset();
640
+ inputManager.onCommand('Line');
641
+ const setPromptSpy = jest.spyOn(core.commandLine, 'setPrompt');
642
+ inputManager.setPrompt('');
643
+ expect(setPromptSpy).toHaveBeenCalledWith('Line');
644
+ setPromptSpy.mockRestore();
645
+ inputManager.reset();
646
+ });
647
+
648
+ // ─── highlightEntityUnderMouse ────────────────────────────────────────────────
649
+
650
+ test('highlightEntityUnderMouse returns false when activeCommand has non-selection prompt', () => {
651
+ inputManager.reset();
652
+ inputManager.onCommand('Line');
653
+ // Line requests POINT, not SINGLESELECTION/SELECTIONSET
654
+ expect(inputManager.highlightEntityUnderMouse()).toBe(false);
655
+ inputManager.reset();
656
+ });
657
+
658
+ test('highlightEntityUnderMouse returns false when no entity is near', () => {
659
+ inputManager.reset();
660
+ // no active command — allows highlighting, but nothing nearby
661
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
662
+ expect(inputManager.highlightEntityUnderMouse()).toBe(false);
663
+ jest.restoreAllMocks();
664
+ });
665
+
666
+ test('highlightEntityUnderMouse returns true when entity found and no active command', () => {
667
+ inputManager.reset();
668
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
669
+ // mock entities.get to return a cloneable object
670
+ const mockLine = new Line();
671
+ jest.spyOn(core.scene.entities, 'get').mockReturnValue(mockLine);
672
+ expect(inputManager.highlightEntityUnderMouse()).toBe(true);
673
+ jest.restoreAllMocks();
674
+ });
675
+
676
+ // ─── onLeftClick edge cases ───────────────────────────────────────────────────
677
+
678
+ test('onLeftClick with no promptOption and no nearby entity - does nothing', () => {
679
+ inputManager.reset();
680
+ // no active command, no entity in range
681
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
682
+ const onSelectionSpy = jest.spyOn(inputManager, 'onSelection');
683
+ inputManager.onLeftClick(new Point(0, 0));
684
+ expect(onSelectionSpy).not.toHaveBeenCalled();
685
+ jest.restoreAllMocks();
686
+ });
687
+
688
+ // ─── mouseDown/mouseUp for other buttons ──────────────────────────────────────
689
+
690
+ test('mouseDown with middle button does not call singleSelect', () => {
691
+ const spy = jest.spyOn(inputManager, 'singleSelect').mockImplementation(() => {});
692
+ inputManager.mouseDown(1);
693
+ expect(spy).not.toHaveBeenCalled();
694
+ spy.mockRestore();
695
+ });
696
+
697
+ test('mouseUp with middle button does not trigger any action', () => {
698
+ const enterSpy = jest.spyOn(inputManager, 'onEnterPressed').mockImplementation(() => {});
699
+ const windowSpy = jest.spyOn(inputManager, 'windowSelect').mockImplementation(() => {});
700
+ inputManager.mouseUp(1);
701
+ expect(enterSpy).not.toHaveBeenCalled();
702
+ expect(windowSpy).not.toHaveBeenCalled();
703
+ enterSpy.mockRestore();
704
+ windowSpy.mockRestore();
705
+ });
706
+
707
+
708
+ // ─── onCommand edge cases ─────────────────────────────────────────────────────
709
+
710
+ test('onCommand with active command and no promptOption - does not handle via prompt', () => {
711
+ inputManager.reset();
712
+ inputManager.activeCommand = new Line();
713
+ inputManager.promptOption = undefined;
714
+ // should not throw even though there's no promptOption
715
+ expect(() => inputManager.onCommand('INVALID')).not.toThrow();
716
+ });
717
+
718
+ // ─── actionCommand edge cases ─────────────────────────────────────────────────
719
+
720
+ test('actionCommand with undefined item and non-Tool activeCommand - does nothing', () => {
721
+ inputManager.reset();
722
+ inputManager.onCommand('Line');
723
+ // calling with no item should not throw
724
+ expect(() => inputManager.actionCommand(undefined)).not.toThrow();
725
+ inputManager.reset();
726
+ });
727
+
728
+ test('actionCommand returns the item index when adding an entity', () => {
729
+ inputManager.reset();
730
+ inputManager.onCommand('Line');
731
+ const addItemSpy = jest.spyOn(core.scene, 'addItem').mockReturnValue(42);
732
+ const line = new Line();
733
+ const index = inputManager.actionCommand(line);
734
+ expect(index).toBe(42);
735
+ addItemSpy.mockRestore();
736
+ inputManager.reset();
737
+ });
738
+
739
+ test('actionCommand passes index parameter to addItem', () => {
740
+ inputManager.reset();
741
+ inputManager.onCommand('Line');
742
+ const addItemSpy = jest.spyOn(core.scene, 'addItem').mockReturnValue(5);
743
+ const line = new Line();
744
+ inputManager.actionCommand(line, 5);
745
+ expect(addItemSpy).toHaveBeenCalledWith(line.type, expect.any(Object), 5);
746
+ addItemSpy.mockRestore();
747
+ inputManager.reset();
748
+ });
749
+
750
+ // ─── Input.Type completeness ──────────────────────────────────────────────────
751
+
752
+ test('Input.Type contains all expected types', () => {
753
+ expect(Input.Type.POINT).toBeDefined();
754
+ expect(Input.Type.NUMBER).toBeDefined();
755
+ expect(Input.Type.STRING).toBeDefined();
756
+ expect(Input.Type.DYNAMIC).toBeDefined();
757
+ expect(Input.Type.SINGLESELECTION).toBeDefined();
758
+ expect(Input.Type.SELECTIONSET).toBeDefined();
759
+ expect(Input.Type.MOUSEDOWN).toBeDefined();
760
+ expect(Input.Type.MOUSEUP).toBeDefined();
761
+ });
762
+
763
+ // ─── reset cancels active promptOption ────────────────────────────────────────
764
+
765
+ test('reset calls cancel on the active promptOption', () => {
766
+ inputManager.reset();
767
+ const po = new PromptOptions('Test', [Input.Type.POINT]);
768
+ inputManager.requestInput(po);
769
+ const cancelSpy = jest.spyOn(po, 'cancel');
770
+ inputManager.reset();
771
+ expect(cancelSpy).toHaveBeenCalled();
772
+ });
773
+
774
+ // ─── mouseMoved – activeCommand.preview() ────────────────────────────────────
775
+
776
+ test('mouseMoved calls activeCommand.preview when command is active and no entity nearby', () => {
777
+ inputManager.reset();
778
+ inputManager.onCommand('Line');
779
+ const previewSpy = jest.spyOn(inputManager.activeCommand, 'preview').mockImplementation(() => {});
780
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
781
+ core.mouse.buttonOneDown = false;
782
+ inputManager.mouseMoved();
783
+ expect(previewSpy).toHaveBeenCalled();
784
+ jest.restoreAllMocks();
785
+ inputManager.reset();
786
+ });
787
+
788
+ test('mouseMoved does not call preview when entity is being selected', () => {
789
+ inputManager.reset();
790
+ // with SINGLESELECTION prompt, highlightEntityUnderMouse will run and return true (selecting = true)
791
+ inputManager.activeCommand = new Line();
792
+ const po = new PromptOptions('', [Input.Type.SINGLESELECTION]);
793
+ inputManager.requestInput(po);
794
+ const previewSpy = jest.spyOn(inputManager.activeCommand, 'preview');
795
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
796
+ jest.spyOn(core.scene.entities, 'get').mockReturnValue(new Line());
797
+ core.mouse.buttonOneDown = false;
798
+ inputManager.mouseMoved();
799
+ expect(previewSpy).not.toHaveBeenCalled();
800
+ jest.restoreAllMocks();
801
+ po.cancel();
802
+ inputManager.reset();
803
+ });
804
+
805
+ test('mouseMoved draws selection window with SELECTIONSET prompt and button down', () => {
806
+ inputManager.reset();
807
+ inputManager.onCommand('Line');
808
+ const po = new PromptOptions('', [Input.Type.SELECTIONSET]);
809
+ inputManager.requestInput(po);
810
+ const drawSpy = jest.spyOn(core.scene.selectionManager, 'drawSelectionWindow').mockImplementation(() => {});
811
+ core.mouse.buttonOneDown = true;
812
+ inputManager.mouseMoved();
813
+ expect(drawSpy).toHaveBeenCalled();
814
+ drawSpy.mockRestore();
815
+ core.mouse.buttonOneDown = false;
816
+ po.cancel();
817
+ inputManager.reset();
818
+ });
819
+
820
+ test('mouseMoved does not draw selection window when button is not down', () => {
821
+ inputManager.reset();
822
+ const drawSpy = jest.spyOn(core.scene.selectionManager, 'drawSelectionWindow').mockImplementation(() => {});
823
+ core.mouse.buttonOneDown = false;
824
+ inputManager.mouseMoved();
825
+ expect(drawSpy).not.toHaveBeenCalled();
826
+ drawSpy.mockRestore();
827
+ });
828
+
829
+ // ─── mouseMoved – snapping path ───────────────────────────────────────────────
830
+
831
+ test('mouseMoved skips highlightEntityUnderMouse when snapping is active and snap returns a point', () => {
832
+ inputManager.reset();
833
+ inputManager.onCommand('Line');
834
+ // snapping is activated by POINT prompt - requestInput already activated it
835
+ expect(inputManager.snapping.active).toBe(true);
836
+ const highlightSpy = jest.spyOn(inputManager, 'highlightEntityUnderMouse');
837
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(new Point(1, 1));
838
+ core.mouse.buttonOneDown = false;
839
+ inputManager.mouseMoved();
840
+ expect(highlightSpy).not.toHaveBeenCalled();
841
+ jest.restoreAllMocks();
842
+ inputManager.reset();
843
+ });
844
+
845
+ // ─── mouseDown – no MOUSEDOWN prompt ─────────────────────────────────────────
846
+
847
+ test('mouseDown case 0 without MOUSEDOWN prompt does not attempt to resolve a prompt', () => {
848
+ inputManager.reset();
849
+ const singleSelectSpy = jest.spyOn(inputManager, 'singleSelect').mockImplementation(() => {});
850
+ const po = new PromptOptions('', [Input.Type.POINT]);
851
+ inputManager.requestInput(po);
852
+ const respondSpy = jest.spyOn(po, 'respond');
853
+ inputManager.mouseDown(0);
854
+ expect(respondSpy).not.toHaveBeenCalled();
855
+ singleSelectSpy.mockRestore();
856
+ po.cancel();
857
+ inputManager.reset();
858
+ });
859
+
860
+ test('mouseDown with MOUSEDOWN prompt but unresolved promise - does not throw', () => {
861
+ inputManager.reset();
862
+ const singleSelectSpy = jest.spyOn(inputManager, 'singleSelect').mockImplementation(() => {});
863
+ // Assign promptOption directly without requestInput, so resolve is undefined
864
+ const po = new PromptOptions('', [Input.Type.MOUSEDOWN]);
865
+ inputManager.promptOption = po;
866
+ expect(() => inputManager.mouseDown(0)).not.toThrow();
867
+ singleSelectSpy.mockRestore();
868
+ inputManager.reset();
869
+ });
870
+
871
+ // ─── mouseUp – no prompt, no active command ───────────────────────────────────
872
+
873
+ test('mouseUp case 0 with no prompt and no active command and mouse moved - calls windowSelect', () => {
874
+ inputManager.reset();
875
+ const windowSelectSpy = jest.spyOn(inputManager, 'windowSelect').mockImplementation(() => {});
876
+ core.mouse.mouseDownCanvasPoint = new Point(0, 0);
877
+ core.mouse.x = 50;
878
+ core.mouse.y = 50;
879
+ inputManager.mouseUp(0);
880
+ expect(windowSelectSpy).toHaveBeenCalled();
881
+ windowSelectSpy.mockRestore();
882
+ core.mouse.x = 0;
883
+ core.mouse.y = 0;
884
+ });
885
+
886
+ test('mouseUp case 0 with no prompt and no active command and mouse not moved - does not call windowSelect', () => {
887
+ inputManager.reset();
888
+ const windowSelectSpy = jest.spyOn(inputManager, 'windowSelect').mockImplementation(() => {});
889
+ core.mouse.mouseDownCanvasPoint = new Point(0, 0);
890
+ core.mouse.x = 0;
891
+ core.mouse.y = 0;
892
+ inputManager.mouseUp(0);
893
+ expect(windowSelectSpy).not.toHaveBeenCalled();
894
+ windowSelectSpy.mockRestore();
895
+ });
896
+
897
+ // ─── highlightEntityUnderMouse – selection prompts ───────────────────────────
898
+
899
+ test('highlightEntityUnderMouse returns true with SINGLESELECTION prompt and entity found', () => {
900
+ inputManager.reset();
901
+ inputManager.activeCommand = new Line();
902
+ const po = new PromptOptions('', [Input.Type.SINGLESELECTION]);
903
+ inputManager.requestInput(po);
904
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
905
+ jest.spyOn(core.scene.entities, 'get').mockReturnValue(new Line());
906
+ expect(inputManager.highlightEntityUnderMouse()).toBe(true);
907
+ jest.restoreAllMocks();
908
+ po.cancel();
909
+ inputManager.reset();
910
+ });
911
+
912
+ test('highlightEntityUnderMouse returns true with SELECTIONSET prompt and entity found', () => {
913
+ inputManager.reset();
914
+ inputManager.activeCommand = new Line();
915
+ const po = new PromptOptions('', [Input.Type.SELECTIONSET]);
916
+ inputManager.requestInput(po);
917
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
918
+ jest.spyOn(core.scene.entities, 'get').mockReturnValue(new Line());
919
+ expect(inputManager.highlightEntityUnderMouse()).toBe(true);
920
+ jest.restoreAllMocks();
921
+ po.cancel();
922
+ inputManager.reset();
923
+ });
924
+
925
+ // ─── onLeftClick – snap prevents entity selection ────────────────────────────
926
+
927
+ test('onLeftClick with active POINT prompt and snap returns a point - responds with the point', () => {
928
+ inputManager.reset();
929
+ inputManager.onCommand('Line');
930
+ const respondSpy = jest.spyOn(inputManager.promptOption, 'respond');
931
+ // snap returns a snapped point (non-null), so entity selection is blocked
932
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(new Point(5, 5));
933
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
934
+ const clickPoint = new Point(5, 5);
935
+ inputManager.onLeftClick(clickPoint);
936
+ // entity select is blocked by snap, so falls through to point respond
937
+ expect(respondSpy).toHaveBeenCalledWith(clickPoint);
938
+ jest.restoreAllMocks();
939
+ inputManager.reset();
940
+ });
941
+
942
+ test('onLeftClick with active command and non-selection prompt - does not select entity', () => {
943
+ inputManager.reset();
944
+ inputManager.onCommand('Line');
945
+ // Line uses POINT prompt - entity selection should be suppressed
946
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(null);
947
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
948
+ const onSelectionSpy = jest.spyOn(inputManager, 'onSelection');
949
+ inputManager.onLeftClick(new Point(1, 1));
950
+ expect(onSelectionSpy).not.toHaveBeenCalled();
951
+ jest.restoreAllMocks();
952
+ inputManager.reset();
953
+ });
954
+
955
+ test('onLeftClick with SINGLESELECTION prompt and entity nearby - selects the entity', () => {
956
+ inputManager.reset();
957
+ inputManager.activeCommand = new Line();
958
+ const po = new PromptOptions('', [Input.Type.SINGLESELECTION]);
959
+ inputManager.requestInput(po);
960
+ const mockSelection = new SingleSelection(3, new Point(0, 0));
961
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(null);
962
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(3);
963
+ jest.spyOn(core.scene.selectionManager, 'singleSelect').mockReturnValue(mockSelection);
964
+ const onSelectionSpy = jest.spyOn(inputManager, 'onSelection');
965
+ inputManager.onLeftClick(new Point(0, 0));
966
+ expect(onSelectionSpy).toHaveBeenCalledWith(mockSelection);
967
+ jest.restoreAllMocks();
968
+ po.cancel();
969
+ inputManager.reset();
970
+ });
971
+
972
+ // ─── onEnterPressed – SELECTIONSET already accepted ──────────────────────────
973
+
974
+ test('onEnterPressed with SELECTIONSET prompt already accepted - resets', () => {
975
+ inputManager.reset();
976
+ inputManager.activeCommand = new Line();
977
+ const po = new PromptOptions('', [Input.Type.SELECTIONSET]);
978
+ inputManager.requestInput(po);
979
+ core.scene.selectionManager.selectionSet.accepted = true;
980
+ inputManager.onEnterPressed();
981
+ expect(inputManager.activeCommand).toBeUndefined();
982
+ po.cancel();
983
+ });
984
+
985
+ // ─── handlePromptInput – returns true on valid input ─────────────────────────
986
+
987
+ test('handlePromptInput returns true on valid input', () => {
988
+ inputManager.reset();
989
+ inputManager.onCommand('Line');
990
+ const result = inputManager.handlePromptInput(new Point(1, 2));
991
+ expect(result).toBe(true);
992
+ inputManager.reset();
993
+ });
994
+
995
+ // ─── reset – clears inputPoint ────────────────────────────────────────────────
996
+
997
+ test('reset sets inputPoint to null', () => {
998
+ inputManager.inputPoint = new Point(10, 20);
999
+ inputManager.reset();
1000
+ expect(inputManager.inputPoint).toBeNull();
1001
+ });
1002
+
1003
+ // ─── mouseMoved – auxiliaryEntities cleared at start ─────────────────────────
1004
+
1005
+ test('mouseMoved clears auxiliaryEntities and tempEntities at the start', () => {
1006
+ // pre-populate both collections
1007
+ const addTrackLineSpy = jest.spyOn(inputManager.snapping, 'addTrackingLine').mockImplementation(() => {});
1008
+ DesignCore.Scene.auxiliaryEntities.add({ draw: () => {} });
1009
+ DesignCore.Scene.tempEntities.add({ draw: () => {} });
1010
+ inputManager.mouseMoved();
1011
+ // both should have been cleared (count re-started from 0 before any new adds)
1012
+ // After mouseMoved the tracking line may or may not have been re-added, but the
1013
+ // clear happened — verify by checking they were empty after the initial clear
1014
+ // (we can confirm this indirectly: no crash and spies didn't see stale data)
1015
+ expect(() => inputManager.mouseMoved()).not.toThrow();
1016
+ addTrackLineSpy.mockRestore();
1017
+ });
1018
+
1019
+ // ─── mouseMoved – polar tracking line ────────────────────────────────────────
1020
+
1021
+ test('mouseMoved adds a tracking line when polar is active and inputPoint is set', () => {
1022
+ inputManager.reset();
1023
+ inputManager.onCommand('Line');
1024
+ // provide an initial input point so the tracking line branch can fire
1025
+ inputManager.inputPoint = new Point(0, 0);
1026
+ expect(inputManager.snapping.active).toBe(true);
1027
+
1028
+ // no entity snap
1029
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(undefined);
1030
+ // polar is true by default; stub polarSnap to return a definite point
1031
+ const trackPoint = new Point(100, 0);
1032
+ jest.spyOn(inputManager.snapping, 'polarSnap').mockReturnValue(trackPoint);
1033
+ const addTrackLineSpy = jest.spyOn(inputManager.snapping, 'addTrackingLine');
1034
+
1035
+ core.mouse.buttonOneDown = false;
1036
+ core.mouse.buttonTwoDown = false;
1037
+ core.mouse.buttonThreeDown = false;
1038
+ inputManager.mouseMoved();
1039
+
1040
+ expect(addTrackLineSpy).toHaveBeenCalledWith(inputManager.inputPoint, trackPoint);
1041
+ jest.restoreAllMocks();
1042
+ inputManager.reset();
1043
+ });
1044
+
1045
+ test('mouseMoved adds a tracking line when ortho is active and inputPoint is set', () => {
1046
+ inputManager.reset();
1047
+ inputManager.onCommand('Line');
1048
+ inputManager.inputPoint = new Point(0, 0);
1049
+
1050
+ // Switch to ortho mode
1051
+ const savedPolar = DesignCore.Settings.polar;
1052
+ const savedOrtho = DesignCore.Settings.ortho;
1053
+ DesignCore.Settings.polar = false;
1054
+ DesignCore.Settings.ortho = true;
1055
+
1056
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(undefined);
1057
+ const trackPoint = new Point(0, 100);
1058
+ jest.spyOn(inputManager.snapping, 'orthoSnap').mockReturnValue(trackPoint);
1059
+ const addTrackLineSpy = jest.spyOn(inputManager.snapping, 'addTrackingLine');
1060
+
1061
+ core.mouse.buttonOneDown = false;
1062
+ core.mouse.buttonTwoDown = false;
1063
+ core.mouse.buttonThreeDown = false;
1064
+ inputManager.mouseMoved();
1065
+
1066
+ expect(addTrackLineSpy).toHaveBeenCalledWith(inputManager.inputPoint, trackPoint);
1067
+
1068
+ DesignCore.Settings.polar = savedPolar;
1069
+ DesignCore.Settings.ortho = savedOrtho;
1070
+ jest.restoreAllMocks();
1071
+ inputManager.reset();
1072
+ });
1073
+
1074
+ test('mouseMoved does not add tracking line when inputPoint is null', () => {
1075
+ inputManager.reset();
1076
+ inputManager.onCommand('Line');
1077
+ // inputPoint is null after reset
1078
+ expect(inputManager.inputPoint).toBeNull();
1079
+
1080
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(undefined);
1081
+ const addTrackLineSpy = jest.spyOn(inputManager.snapping, 'addTrackingLine');
1082
+
1083
+ core.mouse.buttonOneDown = false;
1084
+ inputManager.mouseMoved();
1085
+
1086
+ expect(addTrackLineSpy).not.toHaveBeenCalled();
1087
+ jest.restoreAllMocks();
1088
+ inputManager.reset();
1089
+ });
1090
+
1091
+ test('mouseMoved does not add tracking line when a mouse button is down', () => {
1092
+ inputManager.reset();
1093
+ inputManager.onCommand('Line');
1094
+ inputManager.inputPoint = new Point(0, 0);
1095
+
1096
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(undefined);
1097
+ jest.spyOn(inputManager.snapping, 'polarSnap').mockReturnValue(new Point(100, 0));
1098
+ const addTrackLineSpy = jest.spyOn(inputManager.snapping, 'addTrackingLine');
1099
+
1100
+ core.mouse.buttonOneDown = true;
1101
+ inputManager.mouseMoved();
1102
+
1103
+ expect(addTrackLineSpy).not.toHaveBeenCalled();
1104
+ core.mouse.buttonOneDown = false;
1105
+ jest.restoreAllMocks();
1106
+ inputManager.reset();
1107
+ });
1108
+
1109
+ test('mouseMoved does not add tracking line when entity snap is active', () => {
1110
+ inputManager.reset();
1111
+ inputManager.onCommand('Line');
1112
+ inputManager.inputPoint = new Point(0, 0);
1113
+
1114
+ // snap returns a point → entity snap is active
1115
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(new Point(10, 10));
1116
+ const addTrackLineSpy = jest.spyOn(inputManager.snapping, 'addTrackingLine');
1117
+
1118
+ core.mouse.buttonOneDown = false;
1119
+ inputManager.mouseMoved();
1120
+
1121
+ expect(addTrackLineSpy).not.toHaveBeenCalled();
1122
+ jest.restoreAllMocks();
1123
+ inputManager.reset();
1124
+ });
1125
+
1126
+ // ─── highlightEntityUnderMouse – button pressed ───────────────────────────────
1127
+
1128
+ test('highlightEntityUnderMouse returns false when buttonOneDown is true', () => {
1129
+ inputManager.reset();
1130
+ core.mouse.buttonOneDown = true;
1131
+ expect(inputManager.highlightEntityUnderMouse()).toBe(false);
1132
+ core.mouse.buttonOneDown = false;
1133
+ });
1134
+
1135
+ test('highlightEntityUnderMouse returns false when buttonTwoDown is true', () => {
1136
+ inputManager.reset();
1137
+ core.mouse.buttonTwoDown = true;
1138
+ expect(inputManager.highlightEntityUnderMouse()).toBe(false);
1139
+ core.mouse.buttonTwoDown = false;
1140
+ });
1141
+
1142
+ test('highlightEntityUnderMouse returns false when buttonThreeDown is true', () => {
1143
+ inputManager.reset();
1144
+ core.mouse.buttonThreeDown = true;
1145
+ expect(inputManager.highlightEntityUnderMouse()).toBe(false);
1146
+ core.mouse.buttonThreeDown = false;
1147
+ });