@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,27 +1,126 @@
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 {Identify} from '../../core/tools/identify.js';
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Identify } from '../../core/tools/identify.js';
4
+ import { expect, jest } from '@jest/globals';
5
+ import { withMockInput } from '../test-helpers/test-helpers.js';
5
6
 
6
7
  const core = new Core();
7
- let output;
8
8
 
9
- core.notify = (notification) => {
10
- output = notification;
11
- };
9
+ // ─── register ────────────────────────────────────────────────────────────────
12
10
 
13
- test('Test distance.action', () => {
14
- // Add items to scene
15
- DesignCore.Scene.addItem('Line', {points: [new Point(), new Point(0, 10)]});
11
+ test('Identify.register returns correct command object', () => {
12
+ const reg = Identify.register();
13
+ expect(reg.command).toBe('Identify');
14
+ expect(reg.shortcut).toBe('ID');
15
+ });
16
16
 
17
- const identify = new Identify();
17
+ // ─── action ───────────────────────────────────────────────────────────────────
18
+
19
+ test('Identify.action notifies formatted X and Y coordinates', () => {
20
+ const notifySpy = jest.spyOn(core, 'notify');
18
21
 
19
- // set base point
22
+ const identify = new Identify();
20
23
  identify.points.push(new Point(0, 10));
24
+ identify.action();
25
+
26
+ expect(notifySpy).toHaveBeenCalledWith('X:0.0 Y:10.0');
27
+ notifySpy.mockRestore();
28
+ });
29
+
30
+ test('Identify.action at origin formats both coordinates as zero', () => {
31
+ const notifySpy = jest.spyOn(core, 'notify');
32
+
33
+ const identify = new Identify();
34
+ identify.points.push(new Point(0, 0));
35
+ identify.action();
36
+
37
+ expect(notifySpy).toHaveBeenCalledWith('X:0.0 Y:0.0');
38
+ notifySpy.mockRestore();
39
+ });
40
+
41
+ test('Identify.action with negative coordinates', () => {
42
+ const notifySpy = jest.spyOn(core, 'notify');
43
+
44
+ const identify = new Identify();
45
+ identify.points.push(new Point(-5, -3));
46
+ identify.action();
47
+
48
+ expect(notifySpy).toHaveBeenCalledWith('X:-5.0 Y:-3.0');
49
+ notifySpy.mockRestore();
50
+ });
51
+
52
+ test('Identify.action with fractional coordinates rounded to 1 decimal place', () => {
53
+ const notifySpy = jest.spyOn(core, 'notify');
21
54
 
22
- // Perform identify
55
+ const identify = new Identify();
56
+ identify.points.push(new Point(1.5, 2.5));
57
+ identify.action();
58
+
59
+ expect(notifySpy).toHaveBeenCalledWith('X:1.5 Y:2.5');
60
+ notifySpy.mockRestore();
61
+ });
62
+
63
+ test('Identify.action uses the last point when multiple points are present', () => {
64
+ const notifySpy = jest.spyOn(core, 'notify');
65
+
66
+ const identify = new Identify();
67
+ identify.points.push(new Point(1, 2));
68
+ identify.points.push(new Point(7, 8));
23
69
  identify.action();
24
70
 
25
- expect(output).not.toBeUndefined();
26
- expect(output).toBe('X:0.0 Y:10.0');
71
+ expect(notifySpy).toHaveBeenCalledWith('X:7.0 Y:8.0');
72
+ notifySpy.mockRestore();
73
+ });
74
+
75
+ // ─── execute ──────────────────────────────────────────────────────────────────
76
+
77
+ test('Identify.execute collects one point and calls executeCommand', async () => {
78
+ const executeCommandSpy = jest.fn();
79
+
80
+ await withMockInput(
81
+ core.scene,
82
+ [new Point(3, 4)],
83
+ async () => {
84
+ const identify = new Identify();
85
+ await identify.execute();
86
+ },
87
+ { extraMethods: { executeCommand: executeCommandSpy } },
88
+ );
89
+
90
+ expect(executeCommandSpy).toHaveBeenCalled();
91
+ });
92
+
93
+ test('Identify.execute returns early when input is undefined', async () => {
94
+ const executeCommandSpy = jest.fn();
95
+
96
+ await withMockInput(
97
+ core.scene,
98
+ [],
99
+ async () => {
100
+ const identify = new Identify();
101
+ await identify.execute();
102
+ },
103
+ { extraMethods: { executeCommand: executeCommandSpy } },
104
+ );
105
+
106
+ expect(executeCommandSpy).not.toHaveBeenCalled();
107
+ });
108
+
109
+ test('Identify.execute stores the collected point for action', async () => {
110
+ let capturedIdentify;
111
+ const executeCommandSpy = jest.fn();
112
+
113
+ await withMockInput(
114
+ core.scene,
115
+ [new Point(9, 11)],
116
+ async () => {
117
+ capturedIdentify = new Identify();
118
+ await capturedIdentify.execute();
119
+ },
120
+ { extraMethods: { executeCommand: executeCommandSpy } },
121
+ );
122
+
123
+ expect(capturedIdentify.points).toHaveLength(1);
124
+ expect(capturedIdentify.points[0].x).toBe(9);
125
+ expect(capturedIdentify.points[0].y).toBe(11);
27
126
  });
@@ -0,0 +1,84 @@
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 { MatchProp } from '../../core/tools/matchProp.js';
5
+ import { Input } from '../../core/lib/inputManager.js';
6
+ import { jest } from '@jest/globals';
7
+
8
+ const core = new Core();
9
+
10
+ // Add items to scene
11
+ DesignCore.Scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)], layer: 'test' });
12
+ DesignCore.Scene.addItem('Circle', { points: [new Point(10, 0), new Point(20, 0)] });
13
+ DesignCore.Scene.addItem('Polyline', { points: [new Point(10, 0), new Point(20, 0)] });
14
+ DesignCore.Scene.addItem('Arc', { points: [new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
15
+ DesignCore.Scene.addItem('Rectangle', { points: [new Point(10, 0), new Point(0, 10)] });
16
+ DesignCore.Scene.addItem('Text', { points: [new Point(10, 0), new Point(0, 10)], height: 10, rotation: 0, string: 'text test' });
17
+
18
+ test('Test MatchProp.execute requests source and targets and completes', async () => {
19
+ const match = new MatchProp();
20
+
21
+ // Use existing items added at top of file
22
+ const src = core.scene.entities.get(0);
23
+ const tgt1 = core.scene.entities.get(1);
24
+ const tgt2 = core.scene.entities.get(2);
25
+
26
+ const im = core.scene.inputManager;
27
+
28
+ const promptCalls = [];
29
+ const reqSpy = jest
30
+ .spyOn(im, 'requestInput')
31
+ // First prompt: pick source entity (single selection)
32
+ .mockImplementationOnce((po) => {
33
+ promptCalls.push(po);
34
+ return Promise.resolve(src);
35
+ })
36
+ // Second prompt: pick target entities (selection set)
37
+ .mockImplementationOnce((po) => {
38
+ promptCalls.push(po);
39
+ return Promise.resolve([tgt1, tgt2]);
40
+ });
41
+
42
+ await expect(match.execute()).resolves.toBeUndefined();
43
+
44
+ // Validate input sequence
45
+ expect(reqSpy).toHaveBeenCalledTimes(2);
46
+ expect(promptCalls[0].types).toContain(Input.Type.SINGLESELECTION);
47
+ expect(promptCalls[1].types).toContain(Input.Type.SELECTIONSET);
48
+
49
+ reqSpy.mockRestore();
50
+ });
51
+
52
+
53
+ test('Test MatchProp.register', () => {
54
+ const command = MatchProp.register();
55
+ expect(command.command).toBe('MatchProp');
56
+ expect(command.shortcut).toBe('MA');
57
+ expect(command.type).toBeUndefined();
58
+ });
59
+
60
+ test('Test MatchProp.action matches properties from source to targets', () => {
61
+ const match = new MatchProp();
62
+
63
+ // Use existing items added at top of file
64
+ match.sourceIndex = 0; // Line with layer 'test'
65
+ match.destinationSetIndices = [1, 2, 3, 4, 5]; // Circle, Polyline, Arc, Rectangle, Text
66
+
67
+ match.action();
68
+
69
+ // Validate that layer property was matched to targets
70
+ for (let i = 0; i < match.destinationSetIndices.length; i++) {
71
+ const tgt = core.scene.entities.get(match.destinationSetIndices[i]);
72
+ expect(tgt.layer).toBe('test');
73
+ }
74
+
75
+ const circle = core.scene.entities.get(1);
76
+ expect(circle.radius).toBe(10); // original radius
77
+
78
+ const arc = core.scene.entities.get(3);
79
+ expect(arc.radius).toBe(10); // original radius
80
+
81
+ const text = core.scene.entities.get(5);
82
+ expect(text.height).toBe(10); // original height
83
+ expect(text.string).toBe('text test'); // original string
84
+ });
@@ -0,0 +1,475 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Mirror } from '../../core/tools/mirror.js';
4
+
5
+ const core = new Core();
6
+
7
+ const inputScenarios = [
8
+ {
9
+ desc: 'mirror across X axis keeping source',
10
+ points: [new Point(0, 5), new Point(10, 5)],
11
+ inputs: [new Point(0, 0), new Point(1, 0), 'No'],
12
+ expectedCount: 2,
13
+ checkIndex: 1,
14
+ expectedX: 0,
15
+ expectedY: -5,
16
+ },
17
+ {
18
+ desc: 'mirror across X axis erasing source',
19
+ points: [new Point(0, 5), new Point(10, 5)],
20
+ inputs: [new Point(0, 0), new Point(1, 0), 'Yes'],
21
+ expectedCount: 1,
22
+ checkIndex: 0,
23
+ expectedX: 0,
24
+ expectedY: -5,
25
+ },
26
+ {
27
+ desc: 'mirror across Y axis keeping source',
28
+ points: [new Point(5, 0), new Point(5, 10)],
29
+ inputs: [new Point(0, 0), new Point(0, 1), 'No'],
30
+ expectedCount: 2,
31
+ checkIndex: 1,
32
+ expectedX: -5,
33
+ expectedY: 0,
34
+ },
35
+ ];
36
+
37
+ test.each(inputScenarios)('Mirror.execute handles $desc', async (scenario) => {
38
+ const { points, inputs, expectedCount, checkIndex, expectedX, expectedY } = scenario;
39
+
40
+ core.scene.clear();
41
+ core.scene.selectionManager.reset();
42
+
43
+ core.scene.addItem('Line', { points });
44
+ core.scene.selectionManager.addToSelectionSet(0);
45
+
46
+ const origInputManager = core.scene.inputManager;
47
+ let callCount = 0;
48
+ core.scene.inputManager = {
49
+ requestInput: async () => {
50
+ if (callCount < inputs.length) {
51
+ return inputs[callCount++];
52
+ }
53
+ },
54
+ executeCommand: () => {},
55
+ reset: () => {},
56
+ };
57
+
58
+ const mirror = new Mirror();
59
+ await mirror.execute();
60
+ mirror.action();
61
+
62
+ expect(core.scene.entities.count()).toBe(expectedCount);
63
+
64
+ const entity = core.scene.entities.get(checkIndex);
65
+ expect(entity.points[0].x).toBeCloseTo(expectedX);
66
+ expect(entity.points[0].y).toBeCloseTo(expectedY);
67
+
68
+ core.scene.inputManager = origInputManager;
69
+ });
70
+
71
+ test('Mirror.execute - cancel at first point does not mirror', async () => {
72
+ core.scene.clear();
73
+ core.scene.selectionManager.reset();
74
+
75
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
76
+ core.scene.selectionManager.addToSelectionSet(0);
77
+
78
+ const origInputManager = core.scene.inputManager;
79
+ core.scene.inputManager = {
80
+ requestInput: async () => undefined,
81
+ executeCommand: () => {},
82
+ reset: () => {},
83
+ };
84
+
85
+ const mirror = new Mirror();
86
+ await mirror.execute();
87
+
88
+ expect(core.scene.entities.count()).toBe(1);
89
+ expect(core.scene.entities.get(0).points[0].y).toBeCloseTo(5);
90
+
91
+ core.scene.inputManager = origInputManager;
92
+ });
93
+
94
+ test('Mirror.execute - cancel at second point does not mirror', async () => {
95
+ core.scene.clear();
96
+ core.scene.selectionManager.reset();
97
+
98
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
99
+ core.scene.selectionManager.addToSelectionSet(0);
100
+
101
+ const origInputManager = core.scene.inputManager;
102
+ let callCount = 0;
103
+ const inputs = [new Point(0, 0)]; // provides pt1, then undefined on pt2
104
+ core.scene.inputManager = {
105
+ requestInput: async () => {
106
+ if (callCount < inputs.length) {
107
+ return inputs[callCount++];
108
+ }
109
+ },
110
+ executeCommand: () => {},
111
+ reset: () => {},
112
+ };
113
+
114
+ const mirror = new Mirror();
115
+ await mirror.execute();
116
+
117
+ expect(core.scene.entities.count()).toBe(1);
118
+ expect(core.scene.entities.get(0).points[0].y).toBeCloseTo(5);
119
+
120
+ core.scene.inputManager = origInputManager;
121
+ });
122
+
123
+ test('Mirror.execute - undefined erase prompt defaults to keeping source', async () => {
124
+ core.scene.clear();
125
+ core.scene.selectionManager.reset();
126
+
127
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
128
+ core.scene.selectionManager.addToSelectionSet(0);
129
+
130
+ const origInputManager = core.scene.inputManager;
131
+ let callCount = 0;
132
+ const inputs = [new Point(0, 0), new Point(1, 0)]; // pt1 + pt2, then undefined on erase
133
+ core.scene.inputManager = {
134
+ requestInput: async () => {
135
+ if (callCount < inputs.length) {
136
+ return inputs[callCount++];
137
+ }
138
+ },
139
+ executeCommand: () => {},
140
+ reset: () => {},
141
+ };
142
+
143
+ const mirror = new Mirror();
144
+ await mirror.execute();
145
+ mirror.action();
146
+
147
+ // eraseSource defaults to false — original kept and mirrored copy added
148
+ expect(mirror.eraseSource).toBe(false);
149
+ expect(core.scene.entities.count()).toBe(2);
150
+ expect(core.scene.entities.get(1).points[0].y).toBeCloseTo(-5);
151
+
152
+ core.scene.inputManager = origInputManager;
153
+ });
154
+
155
+ test('Mirror.execute - entity count unchanged when keeping source', async () => {
156
+ core.scene.clear();
157
+ core.scene.selectionManager.reset();
158
+
159
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
160
+ core.scene.addItem('Circle', { points: [new Point(0, 5), new Point(10, 5)] });
161
+
162
+ const entityCount = core.scene.entities.count();
163
+
164
+ for (let i = 0; i < entityCount; i++) {
165
+ core.scene.selectionManager.addToSelectionSet(i);
166
+ }
167
+
168
+ const origInputManager = core.scene.inputManager;
169
+ let callCount = 0;
170
+ const inputs = [new Point(0, 0), new Point(1, 0), 'No'];
171
+ core.scene.inputManager = {
172
+ requestInput: async () => {
173
+ if (callCount < inputs.length) {
174
+ return inputs[callCount++];
175
+ }
176
+ },
177
+ executeCommand: () => {},
178
+ reset: () => {},
179
+ };
180
+
181
+ const mirror = new Mirror();
182
+ await mirror.execute();
183
+ mirror.action();
184
+
185
+ expect(core.scene.entities.count()).toBe(entityCount * 2);
186
+
187
+ core.scene.inputManager = origInputManager;
188
+ });
189
+
190
+ test('Test Mirror.action - keep source (default)', () => {
191
+ core.scene.clear();
192
+ core.scene.selectionManager.reset();
193
+
194
+ // Add entities
195
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
196
+ core.scene.addItem('Circle', { points: [new Point(0, 5), new Point(10, 5)] });
197
+ core.scene.addItem('Polyline', { points: [new Point(0, 5), new Point(10, 5)] });
198
+ core.scene.addItem('Arc', { points: [new Point(0, 5), new Point(10, 5), new Point(5, 10)] });
199
+ core.scene.addItem('Rectangle', { points: [new Point(0, 5), new Point(10, 5)] });
200
+
201
+ const entityCount = core.scene.entities.count();
202
+
203
+ // Select all
204
+ for (let i = 0; i < entityCount; i++) {
205
+ core.scene.selectionManager.addToSelectionSet(i);
206
+ }
207
+
208
+ const mirror = new Mirror();
209
+
210
+ // Mirror line along the X axis (y=0)
211
+ mirror.points.push(new Point(0, 0));
212
+ mirror.points.push(new Point(1, 0));
213
+ mirror.eraseSource = false;
214
+
215
+ mirror.action();
216
+
217
+ // Originals kept + mirrored copies added
218
+ expect(core.scene.entities.count()).toBe(entityCount * 2);
219
+
220
+ // Originals unchanged
221
+ for (let i = 0; i < entityCount; i++) {
222
+ expect(core.scene.entities.get(i).points[0].y).toBeCloseTo(5);
223
+ }
224
+
225
+ // Mirrored copies have y negated
226
+ for (let i = entityCount; i < core.scene.entities.count(); i++) {
227
+ expect(core.scene.entities.get(i).points[0].y).toBeCloseTo(-5);
228
+ }
229
+
230
+ // Mirrored copies have unique handles
231
+ for (let i = 0; i < entityCount; i++) {
232
+ const original = core.scene.entities.get(i);
233
+ const copy = core.scene.entities.get(i + entityCount);
234
+ expect(copy.handle).toBeDefined();
235
+ expect(copy.handle).not.toBe(original.handle);
236
+ }
237
+ });
238
+
239
+ test('Test Mirror.action - erase source', () => {
240
+ core.scene.clear();
241
+ core.scene.selectionManager.reset();
242
+
243
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
244
+ core.scene.addItem('Circle', { points: [new Point(0, 5), new Point(10, 5)] });
245
+
246
+ const entityCount = core.scene.entities.count();
247
+
248
+ for (let i = 0; i < entityCount; i++) {
249
+ core.scene.selectionManager.addToSelectionSet(i);
250
+ }
251
+
252
+ const mirror = new Mirror();
253
+
254
+ // Mirror line along the X axis (y=0)
255
+ mirror.points.push(new Point(0, 0));
256
+ mirror.points.push(new Point(1, 0));
257
+ mirror.eraseSource = true;
258
+
259
+ mirror.action();
260
+
261
+ // No new entities — originals replaced in place
262
+ expect(core.scene.entities.count()).toBe(entityCount);
263
+
264
+ // All entities now have y negated
265
+ for (let i = 0; i < entityCount; i++) {
266
+ expect(core.scene.entities.get(i).points[0].y).toBeCloseTo(-5);
267
+ }
268
+ });
269
+
270
+ test('Test Mirror.action - mirror across Y axis', () => {
271
+ core.scene.clear();
272
+ core.scene.selectionManager.reset();
273
+
274
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(5, 10)] });
275
+
276
+ core.scene.selectionManager.addToSelectionSet(0);
277
+
278
+ const mirror = new Mirror();
279
+
280
+ // Mirror line along the Y axis (x=0)
281
+ mirror.points.push(new Point(0, 0));
282
+ mirror.points.push(new Point(0, 1));
283
+ mirror.eraseSource = false;
284
+
285
+ mirror.action();
286
+
287
+ const mirrored = core.scene.entities.get(1);
288
+ expect(mirrored.points[0].x).toBeCloseTo(-5);
289
+ expect(mirrored.points[0].y).toBeCloseTo(0);
290
+ });
291
+
292
+ test('Test Mirror.action - arc direction is reversed', () => {
293
+ core.scene.clear();
294
+ core.scene.selectionManager.reset();
295
+
296
+ // CCW arc (direction = 1)
297
+ core.scene.addItem('Arc', { points: [new Point(0, 5), new Point(10, 5), new Point(5, 10)], direction: 1 });
298
+
299
+ core.scene.selectionManager.addToSelectionSet(0);
300
+
301
+ const mirror = new Mirror();
302
+
303
+ // Mirror across X axis
304
+ mirror.points.push(new Point(0, 0));
305
+ mirror.points.push(new Point(1, 0));
306
+ mirror.eraseSource = false;
307
+
308
+ mirror.action();
309
+
310
+ const original = core.scene.entities.get(0);
311
+ const mirrored = core.scene.entities.get(1);
312
+
313
+ expect(original.direction).toBe(1);
314
+ expect(mirrored.direction).toBe(-1);
315
+ });
316
+
317
+ test('Test Mirror.getMirroredPoints', () => {
318
+ const mirror = new Mirror();
319
+ const pt1 = new Point(0, 0);
320
+ const pt2 = new Point(1, 0);
321
+
322
+ // Mirror two points across X axis
323
+ const points = [new Point(3, 4), new Point(-2, 7)];
324
+ const result = mirror.getMirroredPoints(points, pt1, pt2);
325
+
326
+ expect(result[0].x).toBeCloseTo(3);
327
+ expect(result[0].y).toBeCloseTo(-4);
328
+ expect(result[1].x).toBeCloseTo(-2);
329
+ expect(result[1].y).toBeCloseTo(-7);
330
+ });
331
+
332
+ test('Mirror.action - zero-length mirror line does not modify entities', () => {
333
+ core.scene.clear();
334
+ core.scene.selectionManager.reset();
335
+
336
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
337
+ core.scene.selectionManager.addToSelectionSet(0);
338
+
339
+ const mirror = new Mirror();
340
+ mirror.points.push(new Point(3, 3)); // pt1 === pt2 — degenerate mirror line
341
+ mirror.points.push(new Point(3, 3));
342
+ mirror.eraseSource = false;
343
+
344
+ mirror.action();
345
+
346
+ // No new entity should have been added
347
+ expect(core.scene.entities.count()).toBe(1);
348
+ // Original entity is unchanged
349
+ expect(core.scene.entities.get(0).points[0].y).toBeCloseTo(5);
350
+ });
351
+
352
+ test('Mirror.execute - requests selection set when none pre-selected', async () => {
353
+ core.scene.clear();
354
+ core.scene.selectionManager.reset();
355
+
356
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
357
+ // Intentionally do NOT add to selection set — triggers the SELECTIONSET prompt
358
+
359
+ const origInputManager = core.scene.inputManager;
360
+ let callCount = 0;
361
+ core.scene.inputManager = {
362
+ requestInput: async () => {
363
+ callCount++;
364
+ if (callCount === 1) {
365
+ // SELECTIONSET prompt: populate the selection manually and return
366
+ core.scene.selectionManager.addToSelectionSet(0);
367
+ return;
368
+ }
369
+ if (callCount === 2) return new Point(0, 0); // pt1
370
+ if (callCount === 3) return new Point(1, 0); // pt2
371
+ if (callCount === 4) return 'No'; // keep source
372
+ },
373
+ executeCommand: () => {},
374
+ reset: () => {},
375
+ };
376
+
377
+ const mirror = new Mirror();
378
+ await mirror.execute();
379
+ mirror.action();
380
+
381
+ expect(core.scene.entities.count()).toBe(2);
382
+ expect(core.scene.entities.get(1).points[0].y).toBeCloseTo(-5);
383
+
384
+ core.scene.inputManager = origInputManager;
385
+ });
386
+
387
+ test('Mirror.preview - no points set, adds no temp entities', () => {
388
+ core.scene.clear();
389
+ core.scene.selectionManager.reset();
390
+
391
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
392
+ core.scene.selectionManager.addToSelectionSet(0);
393
+
394
+ const mirror = new Mirror();
395
+ // No points pushed — guard condition should prevent any work
396
+ mirror.preview();
397
+
398
+ expect(core.scene.tempEntities.count()).toBe(0);
399
+ });
400
+
401
+ test('Mirror.preview - one point set, draws temp mirror line using mouse position', () => {
402
+ core.scene.clear();
403
+ core.scene.selectionManager.reset();
404
+
405
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
406
+ core.scene.selectionManager.addToSelectionSet(0);
407
+
408
+ const mirror = new Mirror();
409
+ mirror.points.push(new Point(3, 3)); // pt1 distinct from default mouse (0,0) scene position
410
+ mirror.preview();
411
+
412
+ // Temp mirror line should have been created
413
+ expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
414
+ });
415
+
416
+ test('Mirror.preview - two distinct points, draws mirror line and mirrors selected items', () => {
417
+ core.scene.clear();
418
+ core.scene.selectionManager.reset();
419
+
420
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
421
+ core.scene.selectionManager.addToSelectionSet(0);
422
+
423
+ const mirror = new Mirror();
424
+ mirror.points.push(new Point(0, 0)); // pt1
425
+ mirror.points.push(new Point(1, 0)); // pt2 — mirror across X axis
426
+ mirror.preview();
427
+
428
+ // Temp mirror line should exist
429
+ expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
430
+
431
+ // The selectedItems copy should have its points mirrored (y negated)
432
+ const previewItem = core.scene.selectionManager.selectedItems[0];
433
+ expect(previewItem.points[0].y).toBeCloseTo(-5);
434
+ });
435
+
436
+ test('Mirror.preview - identical points, draws line but skips mirroring selected items', () => {
437
+ core.scene.clear();
438
+ core.scene.selectionManager.reset();
439
+
440
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
441
+ core.scene.selectionManager.addToSelectionSet(0);
442
+
443
+ const mirror = new Mirror();
444
+ mirror.points.push(new Point(5, 5)); // pt1
445
+ mirror.points.push(new Point(5, 5)); // pt2 — same as pt1, zero-length mirror line
446
+ mirror.preview();
447
+
448
+ // Temp line is still drawn
449
+ expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
450
+
451
+ // Selected item points should be unchanged (mirroring skipped)
452
+ const previewItem = core.scene.selectionManager.selectedItems[0];
453
+ expect(previewItem.points[0].y).toBeCloseTo(5);
454
+ });
455
+
456
+ test('Mirror.preview - reverses direction for entities with a direction property', () => {
457
+ core.scene.clear();
458
+ core.scene.selectionManager.reset();
459
+
460
+ // Arc with CCW direction
461
+ core.scene.addItem('Arc', { points: [new Point(0, 5), new Point(10, 5), new Point(5, 10)], direction: 1 });
462
+ core.scene.selectionManager.addToSelectionSet(0);
463
+
464
+ const mirror = new Mirror();
465
+ mirror.points.push(new Point(0, 0)); // pt1
466
+ mirror.points.push(new Point(1, 0)); // pt2 — mirror across X axis
467
+ mirror.preview();
468
+
469
+ // Original entity direction is unchanged
470
+ expect(core.scene.entities.get(0).direction).toBe(1);
471
+
472
+ // Preview copy has direction reversed
473
+ const previewItem = core.scene.selectionManager.selectedItems[0];
474
+ expect(previewItem.direction).toBe(-1);
475
+ });