@design-core/core 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/.eslintrc.json +14 -7
  2. package/.github/workflows/DesignCoreQA.yml +3 -1
  3. package/README.md +11 -4
  4. package/TODO.md +19 -0
  5. package/core/core/core.js +49 -42
  6. package/core/designCore.js +61 -2
  7. package/core/dimensions/alignedDimension.js +142 -0
  8. package/core/dimensions/angularDimension.js +550 -0
  9. package/core/dimensions/baseCircularDimension.js +244 -0
  10. package/core/dimensions/baseDimension.js +558 -0
  11. package/core/dimensions/baseLinearDimension.js +371 -0
  12. package/core/dimensions/diametricDimension.js +82 -0
  13. package/core/dimensions/dimension.js +298 -0
  14. package/core/dimensions/linearDimension.js +29 -0
  15. package/core/dimensions/radialDimension.js +78 -0
  16. package/core/dimensions/rotatedDimension.js +167 -0
  17. package/core/entities/arc.js +314 -60
  18. package/core/entities/arctext.js +642 -0
  19. package/core/entities/basePolyline.js +553 -94
  20. package/core/entities/circle.js +181 -54
  21. package/core/entities/entity.js +154 -74
  22. package/core/entities/hatch.js +807 -0
  23. package/core/entities/insert.js +200 -39
  24. package/core/entities/line.js +196 -134
  25. package/core/entities/lwpolyline.js +21 -44
  26. package/core/entities/point.js +194 -29
  27. package/core/entities/polyline.js +54 -7
  28. package/core/entities/rectangle.js +43 -13
  29. package/core/entities/solid.js +43 -6
  30. package/core/entities/text.js +324 -181
  31. package/core/lib/boundingBox.js +73 -13
  32. package/core/lib/canvas.js +256 -65
  33. package/core/lib/clipboard.js +198 -0
  34. package/core/lib/colour.js +144 -0
  35. package/core/lib/colours.js +367 -301
  36. package/core/lib/commandLine.js +16 -28
  37. package/core/lib/commandManager.js +115 -58
  38. package/core/lib/constants.js +23 -0
  39. package/core/lib/dxf/dxf.js +249 -34
  40. package/core/lib/dxf/dxfFile.js +23 -38
  41. package/core/lib/dxf/dxfIterator.js +17 -5
  42. package/core/lib/dxf/dxfRead.js +23 -7
  43. package/core/lib/dxf/dxfWrite.js +39 -129
  44. package/core/lib/dxf/handleManager.js +97 -0
  45. package/core/lib/dxf/sections/blocks.js +16 -2
  46. package/core/lib/dxf/sections/entities.js +17 -3
  47. package/core/lib/dxf/sections/header.js +10 -1
  48. package/core/lib/dxf/sections/objects.js +36 -0
  49. package/core/lib/dxf/sections/section.js +43 -5
  50. package/core/lib/dxf/sections/tables.js +17 -3
  51. package/core/lib/entityManager.js +178 -0
  52. package/core/lib/fileio.js +14 -4
  53. package/core/lib/headers.js +229 -0
  54. package/core/lib/input.js +46 -0
  55. package/core/lib/inputManager.js +199 -194
  56. package/core/lib/intersect.js +341 -468
  57. package/core/lib/logging.js +25 -1
  58. package/core/lib/matrix.js +6 -8
  59. package/core/lib/mouse.js +24 -79
  60. package/core/lib/patterns.js +690 -0
  61. package/core/lib/promptOptions.js +133 -0
  62. package/core/lib/scene.js +71 -174
  63. package/core/lib/selectionManager.js +74 -48
  64. package/core/lib/selectionWindow.js +24 -10
  65. package/core/lib/settings.js +18 -11
  66. package/core/lib/snapping.js +129 -21
  67. package/core/lib/stateManager.js +288 -0
  68. package/core/lib/strings.js +57 -5
  69. package/core/lib/utils.js +95 -11
  70. package/core/objects/dictionary.js +43 -0
  71. package/core/objects/dictionaryManager.js +80 -0
  72. package/core/properties/dimType.js +117 -0
  73. package/core/properties/flags.js +66 -0
  74. package/core/properties/property.js +26 -0
  75. package/core/properties/propertyManager.js +65 -19
  76. package/core/tables/appId.js +31 -0
  77. package/core/tables/appIdManager.js +44 -0
  78. package/core/tables/block.js +323 -0
  79. package/core/tables/blockManager.js +48 -0
  80. package/core/tables/blockRecord.js +29 -0
  81. package/core/tables/blockRecordManager.js +37 -0
  82. package/core/tables/dimStyle.js +627 -0
  83. package/core/tables/dimStyleManager.js +187 -0
  84. package/core/tables/endblk.js +28 -0
  85. package/core/tables/layer.js +219 -0
  86. package/core/tables/layerManager.js +47 -0
  87. package/core/tables/ltype.js +85 -0
  88. package/core/tables/ltypeManager.js +65 -0
  89. package/core/tables/style.js +145 -0
  90. package/core/tables/styleManager.js +55 -0
  91. package/core/tables/styleManagerBase.js +110 -0
  92. package/core/tables/tableManagerBase.js +354 -0
  93. package/core/tables/ucs.js +28 -0
  94. package/core/tables/ucsManager.js +44 -0
  95. package/core/tables/view.js +28 -0
  96. package/core/tables/viewManager.js +44 -0
  97. package/core/tables/vport.js +70 -0
  98. package/core/tables/vportManager.js +65 -0
  99. package/core/tools/chamfer.js +343 -0
  100. package/core/tools/chamferFilletBase.js +162 -0
  101. package/core/tools/copy.js +68 -40
  102. package/core/tools/copybase.js +65 -0
  103. package/core/tools/copyclip.js +59 -0
  104. package/core/tools/cornerEntity.js +138 -0
  105. package/core/tools/cutclip.js +72 -0
  106. package/core/tools/distance.js +37 -9
  107. package/core/tools/erase.js +43 -13
  108. package/core/tools/explode.js +117 -0
  109. package/core/tools/extend.js +71 -32
  110. package/core/tools/fillet.js +336 -0
  111. package/core/tools/identify.js +33 -6
  112. package/core/tools/matchProp.js +106 -0
  113. package/core/tools/mirror.js +141 -0
  114. package/core/tools/move.js +65 -32
  115. package/core/tools/offset.js +321 -0
  116. package/core/tools/pan.js +54 -0
  117. package/core/tools/pasteclip.js +93 -0
  118. package/core/tools/purge.js +90 -0
  119. package/core/tools/rotate.js +105 -70
  120. package/core/tools/scale.js +166 -0
  121. package/core/tools/tool.js +14 -4
  122. package/core/tools/trim.js +71 -32
  123. package/core/tools/zoom.js +152 -0
  124. package/package.json +3 -305
  125. package/test/dimensions/alignedDimension.test.js +211 -0
  126. package/test/dimensions/angularDimension.test.js +243 -0
  127. package/test/dimensions/baseDimension.test.js +239 -0
  128. package/test/dimensions/diametricDimension.test.js +203 -0
  129. package/test/dimensions/dimension.test.js +102 -0
  130. package/test/dimensions/radialDimension.test.js +203 -0
  131. package/test/dimensions/rotatedDimension.test.js +224 -0
  132. package/test/entities/arc.test.js +463 -41
  133. package/test/entities/arctext.test.js +329 -0
  134. package/test/entities/basePolyline.test.js +459 -10
  135. package/test/entities/circle.test.js +312 -7
  136. package/test/entities/entity.test.js +28 -9
  137. package/test/entities/hatch.test.js +606 -0
  138. package/test/entities/insert.test.js +90 -0
  139. package/test/entities/line.test.js +182 -6
  140. package/test/entities/point.test.js +375 -37
  141. package/test/entities/solid.test.js +156 -0
  142. package/test/entities/text.test.js +533 -7
  143. package/test/integration/exportIntegration.reference.dxf +1706 -0
  144. package/test/integration/exportIntegration.test.js +186 -0
  145. package/test/integration/importExportIntegration.test.js +126 -0
  146. package/test/integration/intersectionIntegration.test.js +880 -0
  147. package/test/integration/userInputIntegration.reference.dxf +1410 -0
  148. package/test/integration/userInputIntegration.test.js +269 -0
  149. package/test/lib/boundingBox.test.js +26 -2
  150. package/test/lib/canvas.test.js +613 -5
  151. package/test/lib/clipboard.test.js +70 -0
  152. package/test/lib/colour.test.js +114 -0
  153. package/test/lib/colours.test.js +63 -18
  154. package/test/lib/commandLine.test.js +159 -22
  155. package/test/lib/commandManager.test.js +4 -4
  156. package/test/lib/constants.test.js +9 -0
  157. package/test/lib/dxf/dxfFile.test.js +6 -2
  158. package/test/lib/dxf/dxfIterator.test.js +1 -1
  159. package/test/lib/dxf/dxfWrite.test.js +173 -0
  160. package/test/lib/dxf/handleManager.test.js +137 -0
  161. package/test/lib/dxf/sections/blocks.test.js +5 -5
  162. package/test/lib/dxf/sections/entities.test.js +4 -4
  163. package/test/lib/dxf/sections/header.test.js +18 -4
  164. package/test/lib/dxf/sections/objects.test.js +132 -0
  165. package/test/lib/dxf/sections/section.test.js +29 -29
  166. package/test/lib/dxf/sections/tables.test.js +3 -3
  167. package/test/lib/entityManager.test.js +163 -0
  168. package/test/lib/headers.test.js +233 -0
  169. package/test/lib/inputManager.test.js +1144 -5
  170. package/test/lib/intersect.test.js +454 -331
  171. package/test/lib/logging.test.js +1 -1
  172. package/test/lib/matrix.test.js +1 -1
  173. package/test/lib/mouse.test.js +327 -4
  174. package/test/lib/patterns.test.js +105 -0
  175. package/test/lib/promptOptions.test.js +179 -0
  176. package/test/lib/scene.test.js +16 -52
  177. package/test/lib/selectionManager.test.js +17 -5
  178. package/test/lib/settings.test.js +111 -5
  179. package/test/lib/snapping.test.js +142 -4
  180. package/test/lib/stateManager.test.js +155 -0
  181. package/test/lib/utils.test.js +81 -1
  182. package/test/objects/dictionary.test.js +81 -0
  183. package/test/objects/dictionaryManager.test.js +34 -0
  184. package/test/properties/dimType.test.js +62 -0
  185. package/test/properties/flags.test.js +164 -0
  186. package/test/properties/property.test.js +13 -0
  187. package/test/properties/propertyManager.test.js +10 -11
  188. package/test/tables/appId.test.js +46 -0
  189. package/test/tables/appIdManager.test.js +72 -0
  190. package/test/tables/block.test.js +157 -0
  191. package/test/tables/blockManager.test.js +52 -0
  192. package/test/tables/blockRecord.test.js +39 -0
  193. package/test/tables/blockRecordManager.test.js +60 -0
  194. package/test/tables/dimstyle.test.js +72 -0
  195. package/test/tables/endblk.test.js +30 -0
  196. package/test/tables/layer.test.js +95 -0
  197. package/test/tables/layerManager.test.js +172 -0
  198. package/test/{styles → tables}/ltype.test.js +7 -7
  199. package/test/{styles → tables}/ltypeManager.test.js +2 -2
  200. package/test/tables/style.test.js +44 -0
  201. package/test/tables/stylemanagerbase.test.js +220 -0
  202. package/test/tables/tableManagerBase.test.js +194 -0
  203. package/test/tables/ucs.test.js +37 -0
  204. package/test/tables/ucsManager.test.js +48 -0
  205. package/test/tables/view.test.js +37 -0
  206. package/test/tables/viewManager.test.js +48 -0
  207. package/test/tables/vport.test.js +117 -0
  208. package/test/tables/vportManager.test.js +54 -0
  209. package/test/test-helpers/test-helpers.js +66 -0
  210. package/test/tools/chamfer.test.js +970 -0
  211. package/test/tools/chamferFilletBase.test.js +446 -0
  212. package/test/tools/copy.test.js +22 -14
  213. package/test/tools/copybase.test.js +93 -0
  214. package/test/tools/copyclip.test.js +92 -0
  215. package/test/tools/cornerEntity.test.js +201 -0
  216. package/test/tools/cutclip.test.js +94 -0
  217. package/test/tools/distance.test.js +140 -18
  218. package/test/tools/erase.test.js +12 -12
  219. package/test/tools/explode.test.js +45 -0
  220. package/test/tools/extend.test.js +326 -44
  221. package/test/tools/fillet.test.js +831 -0
  222. package/test/tools/identify.test.js +115 -16
  223. package/test/tools/matchProp.test.js +84 -0
  224. package/test/tools/mirror.test.js +475 -0
  225. package/test/tools/move.test.js +75 -19
  226. package/test/tools/offset.test.js +748 -0
  227. package/test/tools/pan.test.js +72 -0
  228. package/test/tools/pasteclip.test.js +100 -0
  229. package/test/tools/purge.test.js +89 -0
  230. package/test/tools/rotate.test.js +89 -23
  231. package/test/tools/scale.test.js +529 -0
  232. package/test/tools/trim.test.js +419 -84
  233. package/test/tools/zoom.test.js +504 -0
  234. package/core/entities/alignedDimension.js +0 -279
  235. package/core/entities/angularDimension.js +0 -194
  236. package/core/entities/baseDimension.js +0 -227
  237. package/core/entities/block.js +0 -247
  238. package/core/entities/diametricDimension.js +0 -133
  239. package/core/entities/dimension.js +0 -202
  240. package/core/entities/radialDimension.js +0 -126
  241. package/core/styles/dimStyle.js +0 -719
  242. package/core/styles/dimStyleManager.js +0 -47
  243. package/core/styles/layer.js +0 -133
  244. package/core/styles/layerManager.js +0 -40
  245. package/core/styles/ltype.js +0 -86
  246. package/core/styles/ltypeManager.js +0 -50
  247. package/core/styles/style.js +0 -158
  248. package/core/styles/styleManager.js +0 -43
  249. package/core/styles/styleManagerBase.js +0 -299
  250. package/test/entities/block.test.js +0 -22
  251. package/test/styles/dimstyle.test.js +0 -19
  252. package/test/styles/layerManager.test.js +0 -150
  253. package/test/styles/style.test.js +0 -33
  254. package/test/styles/stylemanagerbase.test.js +0 -219
@@ -0,0 +1,92 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Copyclip } from '../../core/tools/copyclip.js';
4
+ import { DesignCore } from '../../core/designCore.js';
5
+
6
+ import { jest } from '@jest/globals';
7
+
8
+ // Initialize core (sets DesignCore.Core)
9
+ new Core();
10
+
11
+ describe('Copyclip Tool', () => {
12
+ beforeEach(() => {
13
+ // reset scene entities and selection
14
+ DesignCore.Scene.entities.clear();
15
+ DesignCore.Scene.selectionManager.reset();
16
+ // clear clipboard
17
+ DesignCore.Clipboard.Entities = [];
18
+ });
19
+
20
+ test('action copies selected entities and sets base point to bounding box bottom-left', () => {
21
+ // Create two line entities and add to scene
22
+ const line1 = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(10, 20), new Point(30, 40)] });
23
+ const line2 = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(-5, 100), new Point(0, 80)] });
24
+ DesignCore.Scene.entities.add(line1);
25
+ DesignCore.Scene.entities.add(line2);
26
+
27
+ // Select both
28
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
29
+ DesignCore.Scene.selectionManager.addToSelectionSet(1);
30
+ expect(DesignCore.Scene.selectionManager.selectedItems).toHaveLength(2);
31
+
32
+ const tool = new Copyclip();
33
+ tool.action();
34
+
35
+ // Clipboard now holds selected items (cloned)
36
+ expect(DesignCore.Clipboard.Entities).toHaveLength(2);
37
+ // BasePoint should be min x (-5), min y (20) across all points
38
+ expect(DesignCore.Clipboard.BasePoint.x).toBe(-5);
39
+ expect(DesignCore.Clipboard.BasePoint.y).toBe(20);
40
+ });
41
+
42
+ test('clipboard entities are clones (mutating original does not change clipboard)', () => {
43
+ const line = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(0, 0), new Point(10, 0)] });
44
+ DesignCore.Scene.entities.add(line);
45
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
46
+ const originalClonePoints = DesignCore.Scene.selectionManager.selectedItems[0].points.map((p) => ({ x: p.x, y: p.y }));
47
+
48
+ const tool = new Copyclip();
49
+ tool.action();
50
+ expect(DesignCore.Clipboard.Entities).toHaveLength(1);
51
+
52
+ // Mutate original entity
53
+ line.points[0].x = 999;
54
+ // Clipboard should remain with old coordinates
55
+ expect(DesignCore.Clipboard.Entities[0].points[0].x).toBe(originalClonePoints[0].x);
56
+ });
57
+
58
+ test('execute requests selection when none selected then calls executeCommand', async () => {
59
+ const tool = new Copyclip();
60
+ const requestSpy = jest.fn().mockResolvedValue(undefined);
61
+ const execSpy = jest.fn();
62
+ DesignCore.Scene.selectionManager.reset();
63
+
64
+ // Mock inputManager methods
65
+ DesignCore.Scene.inputManager.requestInput = requestSpy;
66
+ DesignCore.Scene.inputManager.executeCommand = execSpy;
67
+
68
+ await tool.execute();
69
+ expect(requestSpy).toHaveBeenCalledTimes(1);
70
+ expect(execSpy).toHaveBeenCalledTimes(1);
71
+ });
72
+
73
+ test('execute skips requestInput when selection exists', async () => {
74
+ // Prepare selection
75
+ const ent = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(1, 1), new Point(2, 2)] });
76
+ DesignCore.Scene.entities.add(ent);
77
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
78
+ expect(DesignCore.Scene.selectionManager.selectionSet.selectionSet.length).toBe(1);
79
+
80
+ const tool = new Copyclip();
81
+ const requestSpy = jest.fn().mockResolvedValue(undefined);
82
+ const execSpy = jest.fn();
83
+ DesignCore.Scene.inputManager.requestInput = requestSpy;
84
+ DesignCore.Scene.inputManager.executeCommand = execSpy;
85
+
86
+ await tool.execute();
87
+ expect(requestSpy).not.toHaveBeenCalled();
88
+ expect(execSpy).toHaveBeenCalledTimes(1);
89
+ });
90
+ });
91
+
92
+
@@ -0,0 +1,201 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Line } from '../../core/entities/line.js';
4
+ import { CornerEntity } from '../../core/tools/cornerEntity.js';
5
+ import { expect, jest } from '@jest/globals';
6
+
7
+ const core = new Core();
8
+
9
+ // ─── activeSeg ────────────────────────────────────────────────────────────────
10
+
11
+ test('CornerEntity.activeSeg returns entity when segment is null', () => {
12
+ const corner = new CornerEntity();
13
+ const entity = {};
14
+ corner.entity = entity;
15
+ expect(corner.activeSeg).toBe(entity);
16
+ });
17
+
18
+ test('CornerEntity.activeSeg returns segment when segment is set', () => {
19
+ const corner = new CornerEntity();
20
+ corner.entity = {};
21
+ const seg = {};
22
+ corner.segment = seg;
23
+ expect(corner.activeSeg).toBe(seg);
24
+ });
25
+
26
+ // ─── direction ────────────────────────────────────────────────────────────────
27
+
28
+ test('CornerEntity.direction returns lineEnd minus lineStart', () => {
29
+ const corner = new CornerEntity();
30
+ corner.lineStart = new Point(1, 2);
31
+ corner.lineEnd = new Point(4, 6);
32
+ const dir = corner.direction;
33
+ expect(dir.x).toBeCloseTo(3);
34
+ expect(dir.y).toBeCloseTo(4);
35
+ });
36
+
37
+ // ─── resolveEndpoints ─────────────────────────────────────────────────────────
38
+
39
+ test('CornerEntity.resolveEndpoints returns true and sets lineStart/lineEnd for a Line', () => {
40
+ core.scene.clear();
41
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
42
+ const line = core.scene.entities.get(0);
43
+
44
+ const corner = new CornerEntity();
45
+ corner.entity = line;
46
+
47
+ expect(corner.resolveEndpoints()).toBe(true);
48
+ expect(corner.lineStart).toBe(line.points[0]);
49
+ expect(corner.lineEnd).toBe(line.points[1]);
50
+ });
51
+
52
+ test('CornerEntity.resolveEndpoints returns false and leaves endpoints null for a non-Line', () => {
53
+ core.scene.clear();
54
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
55
+ const circle = core.scene.entities.get(0);
56
+
57
+ const corner = new CornerEntity();
58
+ corner.entity = circle;
59
+
60
+ expect(corner.resolveEndpoints()).toBe(false);
61
+ expect(corner.lineStart).toBeNull();
62
+ expect(corner.lineEnd).toBeNull();
63
+ });
64
+
65
+ // ─── setEntity ────────────────────────────────────────────────────────────────
66
+
67
+ test('CornerEntity.setEntity returns true and clears segment for a plain Line entity', () => {
68
+ core.scene.clear();
69
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
70
+ const line = core.scene.entities.get(0);
71
+
72
+ const corner = new CornerEntity();
73
+ expect(corner.setPick(line, new Point(5, 0), 'error')).toBe(true);
74
+ expect(corner.segment).toBeNull();
75
+ expect(corner.segmentIndex).toBeNull();
76
+ });
77
+
78
+ test('CornerEntity.setEntity sets segment and segmentIndex for a polyline straight segment', () => {
79
+ core.scene.clear();
80
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10)] });
81
+ const poly = core.scene.entities.get(0);
82
+
83
+ const corner = new CornerEntity();
84
+ expect(corner.setPick(poly, new Point(5, 0), 'error')).toBe(true); // near first (horizontal) segment
85
+ expect(corner.segment).toBeInstanceOf(Line);
86
+ expect(corner.segmentIndex).toBe(1);
87
+ });
88
+
89
+ test('CornerEntity.setEntity notifies and returns false for a polyline arc segment', () => {
90
+ core.scene.clear();
91
+ const notifySpy = jest.spyOn(core, 'notify');
92
+
93
+ // bulge on the start point makes the segment an arc
94
+ const arcStart = new Point(0, 0);
95
+ arcStart.bulge = 1;
96
+ core.scene.addItem('Lwpolyline', { points: [arcStart, new Point(10, 0)] });
97
+ const poly = core.scene.entities.get(0);
98
+
99
+ const errorMsg = 'arc segment error';
100
+ expect(new CornerEntity().setPick(poly, new Point(5, 2), errorMsg)).toBe(false);
101
+ expect(notifySpy).toHaveBeenCalledWith(errorMsg);
102
+ notifySpy.mockRestore();
103
+ });
104
+
105
+ // ─── click-side geometry getters ─────────────────────────────────────────────
106
+
107
+ test('CornerEntity click-side geometry methods return correct values', () => {
108
+ // Horizontal line (0,0)→(10,0), intersection at (5,0), click at (3,2)
109
+ // clickOnLine = perpendicular foot of (3,2) on the line = (3,0)
110
+ // clickDir = (3,0)−(5,0) = (−2,0)
111
+ // clickDistance = 2, clickUnit = (−1,0)
112
+ // lineKeptEnd = lineStart because clickDir points toward start
113
+ const corner = new CornerEntity();
114
+ corner.lineStart = new Point(0, 0);
115
+ corner.lineEnd = new Point(10, 0);
116
+ corner.clickPoint = new Point(3, 2);
117
+ const ip = new Point(5, 0);
118
+
119
+ expect(corner.clickDistance(ip)).toBeCloseTo(2);
120
+ expect(corner.clickDir(ip).x).toBeCloseTo(-2);
121
+ expect(corner.clickDir(ip).y).toBeCloseTo(0);
122
+ expect(corner.clickUnit(ip).x).toBeCloseTo(-1);
123
+ expect(corner.clickUnit(ip).y).toBeCloseTo(0);
124
+ expect(corner.lineKeptEnd(ip)).toBe(corner.lineStart);
125
+ });
126
+
127
+ test('CornerEntity.clickDistance is near zero when click projects exactly to intersection', () => {
128
+ // clickOnLine = (5,0) = intersectionPoint → clickDistance = 0 → ambiguous corner
129
+ const corner = new CornerEntity();
130
+ corner.lineStart = new Point(0, 0);
131
+ corner.lineEnd = new Point(10, 0);
132
+ corner.clickPoint = new Point(5, 0);
133
+
134
+ expect(corner.clickDistance(new Point(5, 0))).toBeCloseTo(0);
135
+ });
136
+
137
+ test('CornerEntity.lineKeptEnd is lineEnd when click is past intersection', () => {
138
+ // Click at (8,1): clickOnLine = (8,0), clickDir from (5,0) = (3,0) → points toward end
139
+ const corner = new CornerEntity();
140
+ corner.lineStart = new Point(0, 0);
141
+ corner.lineEnd = new Point(10, 0);
142
+ corner.clickPoint = new Point(8, 1);
143
+
144
+ expect(corner.lineKeptEnd(new Point(5, 0))).toBe(corner.lineEnd);
145
+ });
146
+
147
+ // ─── keepStart ───────────────────────────────────────────────────────────────
148
+
149
+ test('CornerEntity.keepStart returns true when segment start is on the click side', () => {
150
+ // Polyline: (0,0)→(10,0)→(10,10), click at (3,2), intersection=(10,0)
151
+ // clickOnLine=(3,0), clickDir=(−7,0) → points toward start side
152
+ core.scene.clear();
153
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10)] });
154
+ const poly = core.scene.entities.get(0);
155
+
156
+ const corner = new CornerEntity();
157
+ corner.entity = poly;
158
+ corner.segmentIndex = 1;
159
+ corner.lineStart = new Point(0, 0);
160
+ corner.lineEnd = new Point(10, 0);
161
+ corner.clickPoint = new Point(3, 2);
162
+
163
+ expect(corner.keepStart(new Point(10, 0))).toBe(true);
164
+ });
165
+
166
+ test('CornerEntity.keepStart returns false when segment end is on the click side', () => {
167
+ // Same polyline, click at (8,2), intersection=(0,0)
168
+ // clickOnLine=(8,0), clickDir=(8,0) → points toward end side
169
+ core.scene.clear();
170
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10)] });
171
+ const poly = core.scene.entities.get(0);
172
+
173
+ const corner = new CornerEntity();
174
+ corner.entity = poly;
175
+ corner.segmentIndex = 1;
176
+ corner.lineStart = new Point(0, 0);
177
+ corner.lineEnd = new Point(10, 0);
178
+ corner.clickPoint = new Point(8, 2);
179
+
180
+ expect(corner.keepStart(new Point(0, 0))).toBe(false);
181
+ });
182
+
183
+ test('CornerEntity.keepStart handles closing segment of closed polyline (segmentIndex === points.length)', () => {
184
+ // Closed polyline: (0,0)→(10,0)→(10,10), closing segment is index 3 (back to (0,0))
185
+ // segmentIndex === points.length wraps segEnd to points[0] = (0,0)
186
+ // lineStart=(10,10), lineEnd=(0,0), click at (9,9), intersection=(10,0)
187
+ // clickOnLine=(10,9.something...) but we just need keepStart not to throw
188
+ core.scene.clear();
189
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10)] });
190
+ const poly = core.scene.entities.get(0);
191
+
192
+ const corner = new CornerEntity();
193
+ corner.entity = poly;
194
+ corner.segmentIndex = poly.points.length; // closing segment
195
+ corner.lineStart = new Point(10, 10);
196
+ corner.lineEnd = new Point(0, 0);
197
+ corner.clickPoint = new Point(9, 9);
198
+
199
+ // Should not throw; segEnd wraps to points[0] = (0,0)
200
+ expect(() => corner.keepStart(new Point(10, 0))).not.toThrow();
201
+ });
@@ -0,0 +1,94 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Cutclip } from '../../core/tools/cutclip.js';
4
+ import { DesignCore } from '../../core/designCore.js';
5
+
6
+ import { jest } from '@jest/globals';
7
+
8
+ // Initialize core (sets DesignCore.Core)
9
+ new Core();
10
+
11
+ describe('Cutclip Tool', () => {
12
+ beforeEach(() => {
13
+ // reset scene entities and selection
14
+ DesignCore.Scene.entities.clear();
15
+ DesignCore.Scene.selectionManager.reset();
16
+ // clear clipboard
17
+ DesignCore.Clipboard.Entities = [];
18
+ });
19
+
20
+ test('action copies selected entities and sets base point to bounding box bottom-left', () => {
21
+ // Create two line entities and add to scene
22
+ const line1 = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(10, 20), new Point(30, 40)] });
23
+ const line2 = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(-5, 100), new Point(0, 80)] });
24
+ DesignCore.Scene.entities.add(line1);
25
+ DesignCore.Scene.entities.add(line2);
26
+
27
+ // Select both
28
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
29
+ DesignCore.Scene.selectionManager.addToSelectionSet(1);
30
+ expect(DesignCore.Scene.selectionManager.selectedItems).toHaveLength(2);
31
+
32
+ const tool = new Cutclip();
33
+ tool.action();
34
+
35
+ // Clipboard now holds selected items (cloned)
36
+ expect(DesignCore.Clipboard.Entities).toHaveLength(2);
37
+ // BasePoint should be min x (-5), min y (20) across all points
38
+ expect(DesignCore.Clipboard.BasePoint.x).toBe(-5);
39
+ expect(DesignCore.Clipboard.BasePoint.y).toBe(20);
40
+ // Scene entities should be removed
41
+ expect(DesignCore.Scene.entities.count()).toBe(0);
42
+ });
43
+
44
+ test('clipboard entities are clones (mutating original does not change clipboard)', () => {
45
+ const line = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(0, 0), new Point(10, 0)] });
46
+ DesignCore.Scene.entities.add(line);
47
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
48
+ const originalClonePoints = DesignCore.Scene.selectionManager.selectedItems[0].points.map((p) => ({ x: p.x, y: p.y }));
49
+
50
+ const tool = new Cutclip();
51
+ tool.action();
52
+ expect(DesignCore.Clipboard.Entities).toHaveLength(1);
53
+
54
+ // Mutate original entity
55
+ line.points[0].x = 999;
56
+ // Clipboard should remain with old coordinates
57
+ expect(DesignCore.Clipboard.Entities[0].points[0].x).toBe(originalClonePoints[0].x);
58
+ });
59
+
60
+ test('execute requests selection when none selected then calls executeCommand', async () => {
61
+ const tool = new Cutclip();
62
+ const requestSpy = jest.fn().mockResolvedValue(undefined);
63
+ const execSpy = jest.fn();
64
+ DesignCore.Scene.selectionManager.reset();
65
+
66
+ // Mock inputManager methods
67
+ DesignCore.Scene.inputManager.requestInput = requestSpy;
68
+ DesignCore.Scene.inputManager.executeCommand = execSpy;
69
+
70
+ await tool.execute();
71
+ expect(requestSpy).toHaveBeenCalledTimes(1);
72
+ expect(execSpy).toHaveBeenCalledTimes(1);
73
+ });
74
+
75
+ test('execute skips requestInput when selection exists', async () => {
76
+ // Prepare selection
77
+ const ent = DesignCore.CommandManager.createNew('Line', { layer: '0', points: [new Point(1, 1), new Point(2, 2)] });
78
+ DesignCore.Scene.entities.add(ent);
79
+ DesignCore.Scene.selectionManager.addToSelectionSet(0);
80
+ expect(DesignCore.Scene.selectionManager.selectionSet.selectionSet.length).toBe(1);
81
+
82
+ const tool = new Cutclip();
83
+ const requestSpy = jest.fn().mockResolvedValue(undefined);
84
+ const execSpy = jest.fn();
85
+ DesignCore.Scene.inputManager.requestInput = requestSpy;
86
+ DesignCore.Scene.inputManager.executeCommand = execSpy;
87
+
88
+ await tool.execute();
89
+ expect(requestSpy).not.toHaveBeenCalled();
90
+ expect(execSpy).toHaveBeenCalledTimes(1);
91
+ });
92
+ });
93
+
94
+
@@ -1,30 +1,152 @@
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 {Distance} from '../../core/tools/distance.js';
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Distance } from '../../core/tools/distance.js';
4
+ import { Strings } from '../../core/lib/strings.js';
5
+ import { expect, jest } from '@jest/globals';
6
+ import { withMockInput } from '../test-helpers/test-helpers.js';
5
7
 
6
8
  const core = new Core();
7
- let output;
8
9
 
9
- core.notify = (notification) => {
10
- output = notification;
11
- };
10
+ // ─── register ────────────────────────────────────────────────────────────────
12
11
 
13
- test('Test distance.action', () => {
14
- // Add items to scene
15
- DesignCore.Scene.addItem('Line', {points: [new Point(), new Point(0, 10)]});
12
+ test('Distance.register returns correct command object', () => {
13
+ const reg = Distance.register();
14
+ expect(reg.command).toBe('Distance');
15
+ expect(reg.shortcut).toBe('DI');
16
+ });
16
17
 
17
- const distance = new Distance();
18
+ // ─── action ───────────────────────────────────────────────────────────────────
18
19
 
19
- // set base point
20
- distance.points.push(new Point());
20
+ test('Distance.action vertical line: correct length, angle, and deltas', () => {
21
+ const notifySpy = jest.spyOn(core, 'notify');
21
22
 
22
- // set destination point
23
+ const distance = new Distance();
24
+ distance.points.push(new Point(0, 0));
23
25
  distance.points.push(new Point(0, 10));
26
+ distance.action();
27
+
28
+ expect(notifySpy).toHaveBeenCalledWith(`${Strings.Strings.LENGTH}: 10.0 Angle: 90.0${Strings.Symbol.DEGREE} ${Strings.Symbol.DELTA}X: 0.0 ${Strings.Symbol.DELTA}Y: 10.0`);
29
+ notifySpy.mockRestore();
30
+ });
31
+
32
+ test('Distance.action horizontal line: correct length, angle, and deltas', () => {
33
+ const notifySpy = jest.spyOn(core, 'notify');
34
+
35
+ const distance = new Distance();
36
+ distance.points.push(new Point(0, 0));
37
+ distance.points.push(new Point(10, 0));
38
+ distance.action();
39
+
40
+ expect(notifySpy).toHaveBeenCalledWith(`${Strings.Strings.LENGTH}: 10.0 Angle: 0.0${Strings.Symbol.DEGREE} ${Strings.Symbol.DELTA}X: 10.0 ${Strings.Symbol.DELTA}Y: 0.0`);
41
+ notifySpy.mockRestore();
42
+ });
43
+
44
+ test('Distance.action diagonal 3-4-5 triangle: correct length and angle', () => {
45
+ const notifySpy = jest.spyOn(core, 'notify');
24
46
 
25
- // Perform distance
47
+ const distance = new Distance();
48
+ distance.points.push(new Point(0, 0));
49
+ distance.points.push(new Point(3, 4));
26
50
  distance.action();
27
51
 
28
- expect(output).not.toBeUndefined();
29
- expect(output).toBe('Length: 10.0 ΔX: 0.0 ΔY: 10.0');
52
+ // length = 5, angle = atan2(4,3) ≈ 53.1°
53
+ expect(notifySpy).toHaveBeenCalledWith(`${Strings.Strings.LENGTH}: 5.0 Angle: 53.1${Strings.Symbol.DEGREE} ${Strings.Symbol.DELTA}X: 3.0 ${Strings.Symbol.DELTA}Y: 4.0`);
54
+ notifySpy.mockRestore();
55
+ });
56
+
57
+ test('Distance.action reversed horizontal line: negative delta and 180° angle', () => {
58
+ const notifySpy = jest.spyOn(core, 'notify');
59
+
60
+ const distance = new Distance();
61
+ distance.points.push(new Point(10, 0));
62
+ distance.points.push(new Point(0, 0));
63
+ distance.action();
64
+
65
+ // angle = atan2(0, -10) = 180°
66
+ expect(notifySpy).toHaveBeenCalledWith(`${Strings.Strings.LENGTH}: 10.0 Angle: 180.0${Strings.Symbol.DEGREE} ${Strings.Symbol.DELTA}X: -10.0 ${Strings.Symbol.DELTA}Y: 0.0`);
67
+ notifySpy.mockRestore();
68
+ });
69
+
70
+ test('Distance.action non-origin start point: deltas computed from actual coordinates', () => {
71
+ const notifySpy = jest.spyOn(core, 'notify');
72
+
73
+ const distance = new Distance();
74
+ distance.points.push(new Point(5, 5));
75
+ distance.points.push(new Point(5, 10));
76
+ distance.action();
77
+
78
+ // dx=0, dy=5, length=5, angle=90°
79
+ expect(notifySpy).toHaveBeenCalledWith(`${Strings.Strings.LENGTH}: 5.0 Angle: 90.0${Strings.Symbol.DEGREE} ${Strings.Symbol.DELTA}X: 0.0 ${Strings.Symbol.DELTA}Y: 5.0`);
80
+ notifySpy.mockRestore();
81
+ });
82
+
83
+ // ─── execute ──────────────────────────────────────────────────────────────────
84
+
85
+ test('Distance.execute collects two points and calls executeCommand', async () => {
86
+ const executeCommandSpy = jest.fn();
87
+
88
+ await withMockInput(
89
+ core.scene,
90
+ [new Point(0, 0), new Point(10, 0)],
91
+ async () => {
92
+ const distance = new Distance();
93
+ await distance.execute();
94
+ },
95
+ { extraMethods: { executeCommand: executeCommandSpy } },
96
+ );
97
+
98
+ expect(executeCommandSpy).toHaveBeenCalled();
99
+ });
100
+
101
+ test('Distance.execute returns early when first point is undefined', async () => {
102
+ const executeCommandSpy = jest.fn();
103
+
104
+ await withMockInput(
105
+ core.scene,
106
+ [],
107
+ async () => {
108
+ const distance = new Distance();
109
+ await distance.execute();
110
+ },
111
+ { extraMethods: { executeCommand: executeCommandSpy } },
112
+ );
113
+
114
+ expect(executeCommandSpy).not.toHaveBeenCalled();
115
+ });
116
+
117
+ test('Distance.execute returns early when second point is undefined', async () => {
118
+ const executeCommandSpy = jest.fn();
119
+
120
+ await withMockInput(
121
+ core.scene,
122
+ [new Point(0, 0)],
123
+ async () => {
124
+ const distance = new Distance();
125
+ await distance.execute();
126
+ },
127
+ { extraMethods: { executeCommand: executeCommandSpy } },
128
+ );
129
+
130
+ expect(executeCommandSpy).not.toHaveBeenCalled();
131
+ });
132
+
133
+ test('Distance.execute stores collected points for action', async () => {
134
+ let capturedDistance;
135
+ const executeCommandSpy = jest.fn();
136
+
137
+ await withMockInput(
138
+ core.scene,
139
+ [new Point(0, 0), new Point(0, 5)],
140
+ async () => {
141
+ capturedDistance = new Distance();
142
+ await capturedDistance.execute();
143
+ },
144
+ { extraMethods: { executeCommand: executeCommandSpy } },
145
+ );
146
+
147
+ expect(capturedDistance.points).toHaveLength(2);
148
+ expect(capturedDistance.points[0].x).toBe(0);
149
+ expect(capturedDistance.points[0].y).toBe(0);
150
+ expect(capturedDistance.points[1].x).toBe(0);
151
+ expect(capturedDistance.points[1].y).toBe(5);
30
152
  });
@@ -1,28 +1,28 @@
1
- import {Core} from '../../core/core/core.js';
2
- import {Point} from '../../core/entities/point.js';
3
- import {Erase} from '../../core/tools/erase.js';
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Erase } from '../../core/tools/erase.js';
4
4
 
5
5
  const core = new Core();
6
6
 
7
7
  test('Test Erase.action', () => {
8
8
  // Add items to scene
9
- core.scene.addItem('Line', {points: [new Point(10, 0), new Point(20, 0)]});
10
- core.scene.addItem('Circle', {points: [new Point(10, 0), new Point(20, 0)]});
11
- core.scene.addItem('Polyline', {points: [new Point(10, 0), new Point(20, 0)]});
12
- core.scene.addItem('Arc', {points: [new Point(10, 0), new Point(10, 10), new Point(0, 10)]});
13
- core.scene.addItem('Rectangle', {points: [new Point(10, 0), new Point(0, 10)]});
14
- core.scene.addItem('Text', {points: [new Point(10, 0), new Point(0, 10)], height: 10, rotation: 0, string: 'text test'});
9
+ core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
10
+ core.scene.addItem('Circle', { points: [new Point(10, 0), new Point(20, 0)] });
11
+ core.scene.addItem('Polyline', { points: [new Point(10, 0), new Point(20, 0)] });
12
+ core.scene.addItem('Arc', { points: [new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
13
+ core.scene.addItem('Rectangle', { points: [new Point(10, 0), new Point(0, 10)] });
14
+ core.scene.addItem('Text', { points: [new Point(10, 0), new Point(0, 10)], height: 10, rotation: 0, string: 'text test' });
15
15
 
16
16
  // Add items to selection set
17
- for (let i = 0; i < core.scene.items.length; i++) {
17
+ for (let i = 0; i < core.scene.entities.count(); i++) {
18
18
  core.scene.selectionManager.addToSelectionSet(i);
19
19
  }
20
20
 
21
- expect(core.scene.items.length).toBe(6);
21
+ expect(core.scene.entities.count()).toBe(6);
22
22
 
23
23
  // Perform Erase
24
24
  const erase = new Erase();
25
25
  erase.action();
26
26
 
27
- expect(core.scene.items.length).toBe(0);
27
+ expect(core.scene.entities.count()).toBe(0);
28
28
  });
@@ -0,0 +1,45 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Explode } from '../../core/tools/explode.js';
4
+ import { Line } from '../../core/entities/line.js';
5
+
6
+ const core = new Core();
7
+
8
+ test('Test Explode.action', () => {
9
+ const insertPoint = new Point(100, 100);
10
+ const insertIndex = core.scene.addItem('Insert', { points: [insertPoint] });
11
+ const insert = core.scene.entities.get(insertIndex);
12
+
13
+ for (let i = 0; i < 5; i++) {
14
+ const line = new Line({ points: [new Point(0, i * 10), new Point(100, i * 10)] });
15
+ insert.block.addItem(line);
16
+ }
17
+
18
+ expect(core.scene.entities.count()).toBe(1);
19
+
20
+ // Add items to selection set
21
+ for (let i = 0; i < core.scene.entities.count(); i++) {
22
+ core.scene.selectionManager.addToSelectionSet(i);
23
+ }
24
+
25
+ // Perform Explode
26
+ const explode = new Explode();
27
+ explode.action();
28
+
29
+ expect(core.scene.entities.count()).toBe(5);
30
+
31
+ // loop through all exploded items, check the type and
32
+ // check the points have been modified when exploded
33
+ const handles = new Set();
34
+ for (let i = 0; i < core.scene.entities.count(); i++) {
35
+ const item = core.scene.entities.get(i);
36
+ expect(item instanceof Line).toBe(true);
37
+ expect(item.points[0].x).toBe(0 + insertPoint.x);
38
+ expect(item.points[0].y).toBe(i * 10 + insertPoint.y);
39
+
40
+ // Verify each exploded entity has a unique handle
41
+ expect(item.handle).toBeDefined();
42
+ expect(handles.has(item.handle)).toBe(false);
43
+ handles.add(item.handle);
44
+ }
45
+ });