@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,529 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Scale } from '../../core/tools/scale.js';
4
+
5
+ const core = new Core();
6
+
7
+ const inputScenarios = [
8
+ {
9
+ desc: 'numeric factor 2 from origin',
10
+ points: [new Point(10, 0), new Point(20, 0)],
11
+ inputs: [new Point(0, 0), 2],
12
+ result: [new Point(20, 0), new Point(40, 0)],
13
+ },
14
+ {
15
+ desc: 'numeric factor 0.5 from origin',
16
+ points: [new Point(10, 0), new Point(20, 0)],
17
+ inputs: [new Point(0, 0), 0.5],
18
+ result: [new Point(5, 0), new Point(10, 0)],
19
+ },
20
+ {
21
+ desc: 'point input uses distance from base as scale factor',
22
+ points: [new Point(1, 0), new Point(2, 0)],
23
+ inputs: [new Point(0, 0), new Point(3, 4)], // distance = 5 → factor = 5
24
+ result: [new Point(5, 0), new Point(10, 0)],
25
+ },
26
+ {
27
+ desc: 'reference workflow with numeric ref and new length',
28
+ points: [new Point(10, 0), new Point(20, 0)],
29
+ inputs: [new Point(0, 0), 'Reference', 10, 30], // factor = 30/10 = 3
30
+ result: [new Point(30, 0), new Point(60, 0)],
31
+ },
32
+ {
33
+ desc: 'reference workflow with point-based ref length',
34
+ points: [new Point(5, 0), new Point(10, 0)],
35
+ inputs: [new Point(0, 0), 'Reference', new Point(0, 0), new Point(10, 0), 20], // ref=10, new=20 → factor=2
36
+ result: [new Point(10, 0), new Point(20, 0)],
37
+ },
38
+ ];
39
+
40
+ test.each(inputScenarios)('Scale.execute handles $desc', async (scenario) => {
41
+ const { points, inputs, result } = scenario;
42
+
43
+ core.scene.clear();
44
+ core.scene.selectionManager.reset();
45
+
46
+ core.scene.addItem('Line', { points });
47
+ core.scene.selectionManager.addToSelectionSet(0);
48
+
49
+ const origInputManager = core.scene.inputManager;
50
+ let callCount = 0;
51
+ core.scene.inputManager = {
52
+ requestInput: async () => {
53
+ if (callCount < inputs.length) {
54
+ return inputs[callCount++];
55
+ }
56
+ },
57
+ executeCommand: () => {},
58
+ reset: () => {},
59
+ };
60
+
61
+ const scale = new Scale();
62
+ await scale.execute();
63
+
64
+ scale.action();
65
+
66
+ const line = core.scene.entities.get(0);
67
+ expect(line.points[0].x).toBeCloseTo(result[0].x);
68
+ expect(line.points[0].y).toBeCloseTo(result[0].y);
69
+ expect(line.points[1].x).toBeCloseTo(result[1].x);
70
+ expect(line.points[1].y).toBeCloseTo(result[1].y);
71
+
72
+ core.scene.inputManager = origInputManager;
73
+ });
74
+
75
+ test('Scale.execute - cancel at base point does not scale', async () => {
76
+ core.scene.clear();
77
+ core.scene.selectionManager.reset();
78
+
79
+ core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
80
+ core.scene.selectionManager.addToSelectionSet(0);
81
+
82
+ const origInputManager = core.scene.inputManager;
83
+ core.scene.inputManager = {
84
+ requestInput: async () => undefined,
85
+ executeCommand: () => {},
86
+ reset: () => {},
87
+ };
88
+
89
+ const scale = new Scale();
90
+ await scale.execute();
91
+
92
+ const line = core.scene.entities.get(0);
93
+ expect(line.points[0].x).toBeCloseTo(10);
94
+ expect(line.points[1].x).toBeCloseTo(20);
95
+
96
+ core.scene.inputManager = origInputManager;
97
+ });
98
+
99
+ test('Scale.execute - cancel at factor prompt does not scale', async () => {
100
+ core.scene.clear();
101
+ core.scene.selectionManager.reset();
102
+
103
+ core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
104
+ core.scene.selectionManager.addToSelectionSet(0);
105
+
106
+ const origInputManager = core.scene.inputManager;
107
+ let callCount = 0;
108
+ const inputs = [new Point(0, 0)]; // provides base point, then undefined on factor
109
+ core.scene.inputManager = {
110
+ requestInput: async () => {
111
+ if (callCount < inputs.length) {
112
+ return inputs[callCount++];
113
+ }
114
+ },
115
+ executeCommand: () => {},
116
+ reset: () => {},
117
+ };
118
+
119
+ const scale = new Scale();
120
+ await scale.execute();
121
+
122
+ const line = core.scene.entities.get(0);
123
+ expect(line.points[0].x).toBeCloseTo(10);
124
+ expect(line.points[1].x).toBeCloseTo(20);
125
+
126
+ core.scene.inputManager = origInputManager;
127
+ });
128
+
129
+ test('Scale.execute - entity count unchanged after execute', async () => {
130
+ core.scene.clear();
131
+ core.scene.selectionManager.reset();
132
+
133
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(10, 0)] });
134
+ core.scene.addItem('Circle', { points: [new Point(5, 0), new Point(10, 0)] });
135
+
136
+ const entityCount = core.scene.entities.count();
137
+
138
+ for (let i = 0; i < entityCount; i++) {
139
+ core.scene.selectionManager.addToSelectionSet(i);
140
+ }
141
+
142
+ const origInputManager = core.scene.inputManager;
143
+ let callCount = 0;
144
+ const inputs = [new Point(0, 0), 3];
145
+ core.scene.inputManager = {
146
+ requestInput: async () => {
147
+ if (callCount < inputs.length) {
148
+ return inputs[callCount++];
149
+ }
150
+ },
151
+ executeCommand: () => {},
152
+ reset: () => {},
153
+ };
154
+
155
+ const scale = new Scale();
156
+ await scale.execute();
157
+ scale.action();
158
+
159
+ expect(core.scene.entities.count()).toBe(entityCount);
160
+
161
+ core.scene.inputManager = origInputManager;
162
+ });
163
+
164
+ test('Test Scale.action - scale by factor 2 from origin', () => {
165
+ core.scene.clear();
166
+ core.scene.selectionManager.reset();
167
+
168
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(10, 0)] });
169
+ core.scene.addItem('Circle', { points: [new Point(5, 0), new Point(10, 0)] });
170
+ core.scene.addItem('Polyline', { points: [new Point(5, 0), new Point(10, 0)] });
171
+ core.scene.addItem('Arc', { points: [new Point(5, 0), new Point(10, 0), new Point(5, 5)] });
172
+ core.scene.addItem('Rectangle', { points: [new Point(5, 0), new Point(10, 0)] });
173
+
174
+ for (let i = 0; i < core.scene.entities.count(); i++) {
175
+ core.scene.selectionManager.addToSelectionSet(i);
176
+ }
177
+
178
+ const scale = new Scale();
179
+ scale.points.push(new Point(0, 0)); // base point at origin
180
+ scale.scaleFactor = 2;
181
+
182
+ scale.action();
183
+
184
+ for (let i = 0; i < core.scene.entities.count(); i++) {
185
+ expect(core.scene.entities.get(i).points[0].x).toBeCloseTo(10);
186
+ expect(core.scene.entities.get(i).points[0].y).toBeCloseTo(0);
187
+ expect(core.scene.entities.get(i).points[1].x).toBeCloseTo(20);
188
+ expect(core.scene.entities.get(i).points[1].y).toBeCloseTo(0);
189
+ }
190
+ });
191
+
192
+ test('Test Scale.action - scale by factor 0.5 from origin', () => {
193
+ core.scene.clear();
194
+ core.scene.selectionManager.reset();
195
+
196
+ core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
197
+
198
+ core.scene.selectionManager.addToSelectionSet(0);
199
+
200
+ const scale = new Scale();
201
+ scale.points.push(new Point(0, 0));
202
+ scale.scaleFactor = 0.5;
203
+
204
+ scale.action();
205
+
206
+ const line = core.scene.entities.get(0);
207
+ expect(line.points[0].x).toBeCloseTo(5);
208
+ expect(line.points[0].y).toBeCloseTo(0);
209
+ expect(line.points[1].x).toBeCloseTo(10);
210
+ expect(line.points[1].y).toBeCloseTo(0);
211
+ });
212
+
213
+ test('Test Scale.action - scale from offset base point', () => {
214
+ core.scene.clear();
215
+ core.scene.selectionManager.reset();
216
+
217
+ core.scene.addItem('Line', { points: [new Point(10, 10), new Point(20, 10)] });
218
+
219
+ core.scene.selectionManager.addToSelectionSet(0);
220
+
221
+ const scale = new Scale();
222
+ scale.points.push(new Point(10, 10)); // base at first point
223
+ scale.scaleFactor = 2;
224
+
225
+ scale.action();
226
+
227
+ const line = core.scene.entities.get(0);
228
+ // base point stays fixed, second point doubles away from it
229
+ expect(line.points[0].x).toBeCloseTo(10);
230
+ expect(line.points[0].y).toBeCloseTo(10);
231
+ expect(line.points[1].x).toBeCloseTo(30);
232
+ expect(line.points[1].y).toBeCloseTo(10);
233
+ });
234
+
235
+ test('Test Scale.action - entity count unchanged (in-place update)', () => {
236
+ core.scene.clear();
237
+ core.scene.selectionManager.reset();
238
+
239
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(10, 0)] });
240
+ core.scene.addItem('Circle', { points: [new Point(5, 0), new Point(10, 0)] });
241
+
242
+ const entityCount = core.scene.entities.count();
243
+
244
+ for (let i = 0; i < entityCount; i++) {
245
+ core.scene.selectionManager.addToSelectionSet(i);
246
+ }
247
+
248
+ const scale = new Scale();
249
+ scale.points.push(new Point(0, 0));
250
+ scale.scaleFactor = 3;
251
+
252
+ scale.action();
253
+
254
+ // Scale is in-place — no new entities added
255
+ expect(core.scene.entities.count()).toBe(entityCount);
256
+ });
257
+
258
+ test('Test Scale.action - scale factor 1 leaves points unchanged', () => {
259
+ core.scene.clear();
260
+ core.scene.selectionManager.reset();
261
+
262
+ core.scene.addItem('Line', { points: [new Point(10, 20), new Point(30, 40)] });
263
+
264
+ core.scene.selectionManager.addToSelectionSet(0);
265
+
266
+ const scale = new Scale();
267
+ scale.points.push(new Point(0, 0));
268
+ scale.scaleFactor = 1;
269
+
270
+ scale.action();
271
+
272
+ const line = core.scene.entities.get(0);
273
+ expect(line.points[0].x).toBeCloseTo(10);
274
+ expect(line.points[0].y).toBeCloseTo(20);
275
+ expect(line.points[1].x).toBeCloseTo(30);
276
+ expect(line.points[1].y).toBeCloseTo(40);
277
+ });
278
+
279
+ test('Test Scale.action - scale 2D point (non-axis-aligned)', () => {
280
+ core.scene.clear();
281
+ core.scene.selectionManager.reset();
282
+
283
+ core.scene.addItem('Line', { points: [new Point(5, 5), new Point(10, 10)] });
284
+
285
+ core.scene.selectionManager.addToSelectionSet(0);
286
+
287
+ const scale = new Scale();
288
+ scale.points.push(new Point(0, 0));
289
+ scale.scaleFactor = 2;
290
+
291
+ scale.action();
292
+
293
+ const line = core.scene.entities.get(0);
294
+ expect(line.points[0].x).toBeCloseTo(10);
295
+ expect(line.points[0].y).toBeCloseTo(10);
296
+ expect(line.points[1].x).toBeCloseTo(20);
297
+ expect(line.points[1].y).toBeCloseTo(20);
298
+ });
299
+
300
+ test('Test Scale.action - empty selection does not throw', () => {
301
+ core.scene.clear();
302
+ core.scene.selectionManager.reset();
303
+
304
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(10, 0)] });
305
+ // deliberately do not add anything to the selection set
306
+
307
+ const scale = new Scale();
308
+ scale.points.push(new Point(0, 0));
309
+ scale.scaleFactor = 2;
310
+
311
+ expect(() => scale.action()).not.toThrow();
312
+
313
+ // Entity should be unmodified
314
+ const line = core.scene.entities.get(0);
315
+ expect(line.points[0].x).toBeCloseTo(5);
316
+ expect(line.points[1].x).toBeCloseTo(10);
317
+ });
318
+
319
+ test('Test Scale.getScaledPoints - from origin', () => {
320
+ const scale = new Scale();
321
+ const base = new Point(0, 0);
322
+
323
+ const points = [new Point(5, 0), new Point(0, 5)];
324
+ const result = scale.getScaledPoints(points, base, 2);
325
+
326
+ expect(result[0].x).toBeCloseTo(10);
327
+ expect(result[0].y).toBeCloseTo(0);
328
+ expect(result[1].x).toBeCloseTo(0);
329
+ expect(result[1].y).toBeCloseTo(10);
330
+ });
331
+
332
+ test('Test Scale.getScaledPoints - from offset base', () => {
333
+ const scale = new Scale();
334
+ const base = new Point(10, 10);
335
+
336
+ const points = [new Point(20, 10), new Point(10, 20)];
337
+ const result = scale.getScaledPoints(points, base, 3);
338
+
339
+ // (20-10)*3 + 10 = 40, (10-10)*3 + 10 = 10
340
+ expect(result[0].x).toBeCloseTo(40);
341
+ expect(result[0].y).toBeCloseTo(10);
342
+ // (10-10)*3 + 10 = 10, (20-10)*3 + 10 = 40
343
+ expect(result[1].x).toBeCloseTo(10);
344
+ expect(result[1].y).toBeCloseTo(40);
345
+ });
346
+
347
+ test('Test Scale.getScaledPoints - preserves bulge and sequence', () => {
348
+ const scale = new Scale();
349
+ const base = new Point(0, 0);
350
+
351
+ const points = [new Point(5, 0, 0.5, 1), new Point(10, 0, -0.25, 2)];
352
+ const result = scale.getScaledPoints(points, base, 2);
353
+
354
+ expect(result[0].bulge).toBe(0.5);
355
+ expect(result[0].sequence).toBe(1);
356
+ expect(result[1].bulge).toBe(-0.25);
357
+ expect(result[1].sequence).toBe(2);
358
+ });
359
+
360
+ test('Scale.execute - requests selection set when none pre-selected', async () => {
361
+ core.scene.clear();
362
+ core.scene.selectionManager.reset();
363
+
364
+ core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
365
+ // Intentionally do NOT add to selection set — triggers the SELECTIONSET prompt
366
+
367
+ const origInputManager = core.scene.inputManager;
368
+ let callCount = 0;
369
+ core.scene.inputManager = {
370
+ requestInput: async () => {
371
+ callCount++;
372
+ if (callCount === 1) {
373
+ // SELECTIONSET prompt: populate the selection manually and return
374
+ core.scene.selectionManager.addToSelectionSet(0);
375
+ return;
376
+ }
377
+ if (callCount === 2) return new Point(0, 0); // base point
378
+ if (callCount === 3) return 2; // scale factor
379
+ },
380
+ executeCommand: () => {},
381
+ reset: () => {},
382
+ };
383
+
384
+ const scale = new Scale();
385
+ await scale.execute();
386
+ scale.action();
387
+
388
+ const line = core.scene.entities.get(0);
389
+ expect(line.points[0].x).toBeCloseTo(20);
390
+ expect(line.points[1].x).toBeCloseTo(40);
391
+
392
+ core.scene.inputManager = origInputManager;
393
+ });
394
+
395
+ test('Scale.execute - reference with zero length returns without scaling', async () => {
396
+ core.scene.clear();
397
+ core.scene.selectionManager.reset();
398
+
399
+ core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
400
+ core.scene.selectionManager.addToSelectionSet(0);
401
+
402
+ const origInputManager = core.scene.inputManager;
403
+ let callCount = 0;
404
+ // base point, then 'Reference', then ref length = 0
405
+ const inputs = [new Point(0, 0), 'Reference', 0];
406
+ core.scene.inputManager = {
407
+ requestInput: async () => {
408
+ if (callCount < inputs.length) {
409
+ return inputs[callCount++];
410
+ }
411
+ },
412
+ executeCommand: () => {},
413
+ reset: () => {},
414
+ };
415
+
416
+ const scale = new Scale();
417
+ await scale.execute();
418
+ // action() should not be called — execute returned early, scaleFactor unchanged (1)
419
+ scale.action();
420
+
421
+ // Entity should be unchanged (scaleFactor stayed at 1 but no second point pushed,
422
+ // so points array only has base — action uses scaleFactor=1)
423
+ const line = core.scene.entities.get(0);
424
+ expect(line.points[0].x).toBeCloseTo(10);
425
+ expect(line.points[1].x).toBeCloseTo(20);
426
+
427
+ core.scene.inputManager = origInputManager;
428
+ });
429
+
430
+ test('Scale.preview - no points set, adds no temp entities', () => {
431
+ core.scene.clear();
432
+ core.scene.selectionManager.reset();
433
+
434
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(10, 0)] });
435
+ core.scene.selectionManager.addToSelectionSet(0);
436
+
437
+ const scale = new Scale();
438
+ // No points — guard condition prevents any work
439
+ scale.preview();
440
+
441
+ expect(core.scene.tempEntities.count()).toBe(0);
442
+ });
443
+
444
+ test('Scale.preview - referencePoint set, suppresses preview', () => {
445
+ core.scene.clear();
446
+ core.scene.selectionManager.reset();
447
+
448
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(10, 0)] });
449
+ core.scene.selectionManager.addToSelectionSet(0);
450
+
451
+ const scale = new Scale();
452
+ scale.points.push(new Point(0, 0));
453
+ scale.referencePoint = new Point(0, 0); // non-null blocks preview
454
+
455
+ scale.preview();
456
+
457
+ expect(core.scene.tempEntities.count()).toBe(0);
458
+ });
459
+
460
+ test('Scale.preview - mouse on base point (distance 0), draws line but skips scaling', () => {
461
+ core.scene.clear();
462
+ core.scene.selectionManager.reset();
463
+
464
+ core.scene.addItem('Line', { points: [new Point(5, 0), new Point(10, 0)] });
465
+ core.scene.selectionManager.addToSelectionSet(0);
466
+
467
+ const scale = new Scale();
468
+ // Push base point identical to default mouse scene position (0,0)
469
+ scale.points.push(new Point(0, 0));
470
+ scale.referencePoint = null;
471
+
472
+ scale.preview();
473
+
474
+ // Temp line is still created
475
+ expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
476
+
477
+ // Selected item points should be unchanged (scaling skipped)
478
+ const previewItem = core.scene.selectionManager.selectedItems[0];
479
+ expect(previewItem.points[0].x).toBeCloseTo(5);
480
+ });
481
+
482
+ test('Scale.preview - one point set, draws temp line and scales selected items', () => {
483
+ core.scene.clear();
484
+ core.scene.selectionManager.reset();
485
+
486
+ core.scene.addItem('Line', { points: [new Point(1, 0), new Point(2, 0)] });
487
+ core.scene.selectionManager.addToSelectionSet(0);
488
+
489
+ const scale = new Scale();
490
+ // Base point away from the default mouse scene position so distance > 0
491
+ scale.points.push(new Point(100, 100));
492
+ scale.referencePoint = null;
493
+
494
+ scale.preview();
495
+
496
+ // Temp line should be created
497
+ expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
498
+
499
+ // Preview item should have been scaled (x differs from original 1.0)
500
+ const previewItem = core.scene.selectionManager.selectedItems[0];
501
+ expect(previewItem.points[0].x).not.toBeCloseTo(1);
502
+ });
503
+
504
+ test('Scale.preview - reference mode uses referenceLength to compute factor', () => {
505
+ core.scene.clear();
506
+ core.scene.selectionManager.reset();
507
+
508
+ // Line at x=10..20, y=0
509
+ core.scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)] });
510
+ core.scene.selectionManager.addToSelectionSet(0);
511
+
512
+ const scale = new Scale();
513
+ scale.points.push(new Point(0, 0)); // base at origin
514
+ scale.referencePoint = null; // reference collection done — preview active
515
+ scale.referenceLength = 10; // established reference length
516
+
517
+ // The default mouse scene position will give some distance from (0,0).
518
+ // We can't control the mouse, so just verify the factor applied is
519
+ // distance/referenceLength rather than raw distance: reload the selectedItems
520
+ // before and after with a known referenceLength to confirm scaling occurred.
521
+ scale.preview();
522
+
523
+ expect(core.scene.tempEntities.count()).toBeGreaterThanOrEqual(1);
524
+
525
+ // The preview item's first point should differ from the original (10,0)
526
+ // because the factor (distance/10) is applied
527
+ const previewItem = core.scene.selectionManager.selectedItems[0];
528
+ expect(previewItem.points[0].x).not.toBeCloseTo(10);
529
+ });