@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,970 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Chamfer } from '../../core/tools/chamfer.js';
4
+ import { Strings } from '../../core/lib/strings.js';
5
+ import { SingleSelection } from '../../core/lib/selectionManager.js';
6
+ import { expect, jest } from '@jest/globals';
7
+ import { withMockInput } from '../test-helpers/test-helpers.js';
8
+
9
+ const core = new Core();
10
+
11
+ // ─── register ────────────────────────────────────────────────────────────────
12
+
13
+ test('Chamfer.register returns correct command object', () => {
14
+ const reg = Chamfer.register();
15
+ expect(reg.command).toBe('Chamfer');
16
+ expect(reg.shortcut).toBe('CHA');
17
+ });
18
+
19
+ // ─── action: guard conditions ─────────────────────────────────────────────────
20
+
21
+ test('Chamfer.action does nothing when entities are not set', () => {
22
+ core.scene.clear();
23
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
24
+ const countBefore = core.scene.entities.count();
25
+
26
+ const chamfer = new Chamfer();
27
+ chamfer.action();
28
+
29
+ expect(core.scene.entities.count()).toBe(countBefore);
30
+ });
31
+
32
+ test('Chamfer.action notifies when first entity type is not supported', () => {
33
+ core.scene.clear();
34
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
35
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
36
+
37
+ const notifySpy = jest.spyOn(core, 'notify');
38
+
39
+ const chamfer = new Chamfer();
40
+ chamfer.firstPick.entity = core.scene.entities.get(0); // Circle
41
+ chamfer.secondPick.entity = core.scene.entities.get(1); // Line
42
+ chamfer.firstPick.clickPoint = new Point(0, 0);
43
+ chamfer.secondPick.clickPoint = new Point(5, 0);
44
+ chamfer.action();
45
+
46
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOCHAMFER));
47
+ notifySpy.mockRestore();
48
+ });
49
+
50
+ test('Chamfer.action notifies when second entity type is not supported', () => {
51
+ core.scene.clear();
52
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
53
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
54
+
55
+ const notifySpy = jest.spyOn(core, 'notify');
56
+
57
+ const chamfer = new Chamfer();
58
+ chamfer.firstPick.entity = core.scene.entities.get(0); // Line
59
+ chamfer.secondPick.entity = core.scene.entities.get(1); // Circle
60
+ chamfer.firstPick.clickPoint = new Point(5, 0);
61
+ chamfer.secondPick.clickPoint = new Point(0, 0);
62
+ chamfer.action();
63
+
64
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOCHAMFER));
65
+ notifySpy.mockRestore();
66
+ });
67
+
68
+ test('Chamfer.action notifies for parallel lines', () => {
69
+ core.scene.clear();
70
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
71
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
72
+
73
+ const notifySpy = jest.spyOn(core, 'notify');
74
+
75
+ const chamfer = new Chamfer();
76
+ chamfer.firstPick.entity = core.scene.entities.get(0);
77
+ chamfer.secondPick.entity = core.scene.entities.get(1);
78
+ chamfer.firstPick.clickPoint = new Point(5, 0);
79
+ chamfer.secondPick.clickPoint = new Point(5, 5);
80
+ chamfer.action();
81
+
82
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.PARALLELLINES));
83
+ notifySpy.mockRestore();
84
+ });
85
+
86
+ // ─── action: distance method ──────────────────────────────────────────────────
87
+
88
+ test('Chamfer.action dist=0 trimMode=true trims both lines to intersection', () => {
89
+ // Lines only touching when extended: horizontal (0,0)→(8,0), vertical (10,-10)→(10,-2)
90
+ // Infinite extensions meet at (10, 0).
91
+ core.scene.clear();
92
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(8, 0)] });
93
+ core.scene.addItem('Line', { points: [new Point(10, -10), new Point(10, -2)] });
94
+
95
+ core.scene.headers.chamferDistanceA = 0;
96
+ core.scene.headers.chamferDistanceB = 0;
97
+ core.scene.headers.chamferMode = false;
98
+ core.scene.headers.trimMode = true;
99
+
100
+ const chamfer = new Chamfer();
101
+ chamfer.firstPick.entity = core.scene.entities.get(0);
102
+ chamfer.secondPick.entity = core.scene.entities.get(1);
103
+ chamfer.firstPick.clickPoint = new Point(4, 0);
104
+ chamfer.secondPick.clickPoint = new Point(10, -6);
105
+ chamfer.action();
106
+
107
+ // No chamfer line added
108
+ expect(core.scene.entities.count()).toBe(2);
109
+
110
+ const line1 = core.scene.entities.get(0);
111
+ const line2 = core.scene.entities.get(1);
112
+
113
+ // Line 1 extended to intersection (10, 0)
114
+ expect(line1.points[1].x).toBeCloseTo(10);
115
+ expect(line1.points[1].y).toBeCloseTo(0);
116
+
117
+ // Line 2 extended to intersection (10, 0)
118
+ expect(line2.points[1].x).toBeCloseTo(10);
119
+ expect(line2.points[1].y).toBeCloseTo(0);
120
+ });
121
+
122
+ test('Chamfer.action dist=0 trimMode=false makes no changes', () => {
123
+ core.scene.clear();
124
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(8, 0)] });
125
+ core.scene.addItem('Line', { points: [new Point(10, -10), new Point(10, -2)] });
126
+
127
+ core.scene.headers.chamferDistanceA = 0;
128
+ core.scene.headers.chamferDistanceB = 0;
129
+ core.scene.headers.chamferMode = false;
130
+ core.scene.headers.trimMode = false;
131
+
132
+ const chamfer = new Chamfer();
133
+ chamfer.firstPick.entity = core.scene.entities.get(0);
134
+ chamfer.secondPick.entity = core.scene.entities.get(1);
135
+ chamfer.firstPick.clickPoint = new Point(4, 0);
136
+ chamfer.secondPick.clickPoint = new Point(10, -6);
137
+ chamfer.action();
138
+
139
+ expect(core.scene.entities.count()).toBe(2);
140
+ expect(core.scene.entities.get(0).points[1].x).toBeCloseTo(8);
141
+ expect(core.scene.entities.get(1).points[1].y).toBeCloseTo(-2);
142
+ });
143
+
144
+ test('Chamfer.action distance method trimMode=true adds chamfer line and trims', () => {
145
+ // Perpendicular L-corner at origin: horizontal (-10,0)→(0,0), vertical (0,0)→(0,10)
146
+ // Equal distances of 2 → chamfer endpoints at (-2,0) and (0,2)
147
+ core.scene.clear();
148
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
149
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
150
+
151
+ core.scene.headers.chamferDistanceA = 2;
152
+ core.scene.headers.chamferDistanceB = 2;
153
+ core.scene.headers.chamferMode = false;
154
+ core.scene.headers.trimMode = true;
155
+
156
+ const chamfer = new Chamfer();
157
+ chamfer.firstPick.entity = core.scene.entities.get(0);
158
+ chamfer.secondPick.entity = core.scene.entities.get(1);
159
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
160
+ chamfer.secondPick.clickPoint = new Point(0, 5);
161
+ chamfer.action();
162
+
163
+ expect(core.scene.entities.count()).toBe(3);
164
+
165
+ const line1 = core.scene.entities.get(0);
166
+ const line2 = core.scene.entities.get(1);
167
+ const chamferLine = core.scene.entities.get(2);
168
+
169
+ expect(chamferLine.type).toBe('Line');
170
+
171
+ // Chamfer line connects (-2,0) and (0,2)
172
+ expect(chamferLine.points[0].x).toBeCloseTo(-2);
173
+ expect(chamferLine.points[0].y).toBeCloseTo(0);
174
+ expect(chamferLine.points[1].x).toBeCloseTo(0);
175
+ expect(chamferLine.points[1].y).toBeCloseTo(2);
176
+
177
+ // Line 1 trimmed: kept end (-10,0), trimmed to (-2,0)
178
+ expect(line1.points[0].x).toBeCloseTo(-10);
179
+ expect(line1.points[0].y).toBeCloseTo(0);
180
+ expect(line1.points[1].x).toBeCloseTo(-2);
181
+ expect(line1.points[1].y).toBeCloseTo(0);
182
+
183
+ // Line 2 trimmed: kept end (0,10), trimmed to (0,2)
184
+ expect(line2.points[0].x).toBeCloseTo(0);
185
+ expect(line2.points[0].y).toBeCloseTo(10);
186
+ expect(line2.points[1].x).toBeCloseTo(0);
187
+ expect(line2.points[1].y).toBeCloseTo(2);
188
+ });
189
+
190
+ test('Chamfer.action distance method asymmetric distances', () => {
191
+ // L-corner at origin, distA=3 along horizontal, distB=1 along vertical
192
+ core.scene.clear();
193
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
194
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
195
+
196
+ core.scene.headers.chamferDistanceA = 3;
197
+ core.scene.headers.chamferDistanceB = 1;
198
+ core.scene.headers.chamferMode = false;
199
+ core.scene.headers.trimMode = true;
200
+
201
+ const chamfer = new Chamfer();
202
+ chamfer.firstPick.entity = core.scene.entities.get(0);
203
+ chamfer.secondPick.entity = core.scene.entities.get(1);
204
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
205
+ chamfer.secondPick.clickPoint = new Point(0, 5);
206
+ chamfer.action();
207
+
208
+ const chamferLine = core.scene.entities.get(2);
209
+ expect(chamferLine.type).toBe('Line');
210
+ expect(chamferLine.points[0].x).toBeCloseTo(-3);
211
+ expect(chamferLine.points[0].y).toBeCloseTo(0);
212
+ expect(chamferLine.points[1].x).toBeCloseTo(0);
213
+ expect(chamferLine.points[1].y).toBeCloseTo(1);
214
+ });
215
+
216
+ test('Chamfer.action distance method trimMode=false adds chamfer line only', () => {
217
+ core.scene.clear();
218
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
219
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
220
+
221
+ core.scene.headers.chamferDistanceA = 2;
222
+ core.scene.headers.chamferDistanceB = 2;
223
+ core.scene.headers.chamferMode = false;
224
+ core.scene.headers.trimMode = false;
225
+
226
+ const chamfer = new Chamfer();
227
+ chamfer.firstPick.entity = core.scene.entities.get(0);
228
+ chamfer.secondPick.entity = core.scene.entities.get(1);
229
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
230
+ chamfer.secondPick.clickPoint = new Point(0, 5);
231
+ chamfer.action();
232
+
233
+ // Chamfer line added, original lines unchanged
234
+ expect(core.scene.entities.count()).toBe(3);
235
+ expect(core.scene.entities.get(2).type).toBe('Line');
236
+
237
+ // Original lines still have their original endpoints
238
+ expect(core.scene.entities.get(0).points[1].x).toBeCloseTo(0);
239
+ expect(core.scene.entities.get(0).points[1].y).toBeCloseTo(0);
240
+ expect(core.scene.entities.get(1).points[0].x).toBeCloseTo(0);
241
+ expect(core.scene.entities.get(1).points[0].y).toBeCloseTo(0);
242
+ });
243
+
244
+ test('Chamfer.action notifies when distance is too large for the segments', () => {
245
+ // Lines only 5 units long from intersection — distance 10 won't fit
246
+ core.scene.clear();
247
+ core.scene.addItem('Line', { points: [new Point(-5, 0), new Point(0, 0)] });
248
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 5)] });
249
+
250
+ core.scene.headers.chamferDistanceA = 10;
251
+ core.scene.headers.chamferDistanceB = 10;
252
+ core.scene.headers.chamferMode = false;
253
+ core.scene.headers.trimMode = true;
254
+
255
+ const notifySpy = jest.spyOn(core, 'notify');
256
+
257
+ const chamfer = new Chamfer();
258
+ chamfer.firstPick.entity = core.scene.entities.get(0);
259
+ chamfer.secondPick.entity = core.scene.entities.get(1);
260
+ chamfer.firstPick.clickPoint = new Point(-2, 0);
261
+ chamfer.secondPick.clickPoint = new Point(0, 2);
262
+ chamfer.action();
263
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.MAXVALUE));
264
+ expect(core.scene.entities.count()).toBe(2);
265
+ notifySpy.mockRestore();
266
+ });
267
+
268
+ // ─── action: angle method ─────────────────────────────────────────────────────
269
+
270
+ test('Chamfer.action angle method 45° on perpendicular lines matches distance method', () => {
271
+ // L-corner at origin, distA=2, angle=45° on perpendicular lines.
272
+ // With perpendicular lines and 45°, the chamfer endpoint on line2 should also be 2 units
273
+ // from the intersection — matching a symmetric distance chamfer.
274
+ core.scene.clear();
275
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
276
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
277
+
278
+ core.scene.headers.chamferLength = 2;
279
+ core.scene.headers.chamferAngle = 45; // degrees
280
+ core.scene.headers.chamferMode = true;
281
+ core.scene.headers.trimMode = true;
282
+
283
+ const chamfer = new Chamfer();
284
+ chamfer.firstPick.entity = core.scene.entities.get(0);
285
+ chamfer.secondPick.entity = core.scene.entities.get(1);
286
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
287
+ chamfer.secondPick.clickPoint = new Point(0, 5);
288
+ chamfer.action();
289
+
290
+ expect(core.scene.entities.count()).toBe(3);
291
+ const chamferLine = core.scene.entities.get(2);
292
+ expect(chamferLine.type).toBe('Line');
293
+
294
+ // First chamfer point: 2 units along line1 from intersection → (-2, 0)
295
+ expect(chamferLine.points[0].x).toBeCloseTo(-2);
296
+ expect(chamferLine.points[0].y).toBeCloseTo(0);
297
+
298
+ // Second chamfer point: where chamfer ray meets line2 → (0, 2) for 45° on perpendicular lines
299
+ expect(chamferLine.points[1].x).toBeCloseTo(0);
300
+ expect(chamferLine.points[1].y).toBeCloseTo(2);
301
+ });
302
+
303
+ // ─── action: click at intersection ───────────────────────────────────────────
304
+
305
+ test('Chamfer.action notifies when click coincides with intersection', () => {
306
+ // L-corner at origin. Clicking firstClickPoint exactly at (0,0) makes
307
+ // firstClickDistance = 0, triggering the "Selected corner cannot be chamfered" guard.
308
+ core.scene.clear();
309
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
310
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
311
+
312
+ core.scene.headers.chamferDistanceA = 2;
313
+ core.scene.headers.chamferDistanceB = 2;
314
+ core.scene.headers.chamferMode = false;
315
+ core.scene.headers.trimMode = true;
316
+
317
+ const notifySpy = jest.spyOn(core, 'notify');
318
+
319
+ const chamfer = new Chamfer();
320
+ chamfer.firstPick.entity = core.scene.entities.get(0);
321
+ chamfer.secondPick.entity = core.scene.entities.get(1);
322
+ chamfer.firstPick.clickPoint = new Point(0, 0); // exactly at the intersection
323
+ chamfer.secondPick.clickPoint = new Point(0, 5);
324
+ chamfer.action();
325
+
326
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.SELECTION));
327
+ expect(core.scene.entities.count()).toBe(2);
328
+ notifySpy.mockRestore();
329
+ });
330
+
331
+ // ─── action: angle method edge cases ─────────────────────────────────────────
332
+
333
+ test('Chamfer.action angle method notifies INVALIDNUMBER when angle is zero', () => {
334
+ core.scene.clear();
335
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
336
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
337
+
338
+ core.scene.headers.chamferLength = 2;
339
+ core.scene.headers.chamferAngle = 0; // alpha = 0 * π/180 = 0, fails alpha <= 0 guard
340
+ core.scene.headers.chamferMode = true;
341
+ core.scene.headers.trimMode = true;
342
+
343
+ const notifySpy = jest.spyOn(core, 'notify');
344
+
345
+ const chamfer = new Chamfer();
346
+ chamfer.firstPick.entity = core.scene.entities.get(0);
347
+ chamfer.secondPick.entity = core.scene.entities.get(1);
348
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
349
+ chamfer.secondPick.clickPoint = new Point(0, 5);
350
+ chamfer.action();
351
+
352
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
353
+ expect(core.scene.entities.count()).toBe(2);
354
+ notifySpy.mockRestore();
355
+ });
356
+
357
+ test('Chamfer.action angle method notifies PARALLELLINES when chamfer direction is parallel to line2', () => {
358
+ // L-corner at origin, angle = 90°.
359
+ // firstClickUnit along line1 clicked from left: (-1, 0).
360
+ // rotAngle = π - π/2 = π/2.
361
+ // candidate1 = rotate(-1,0) by +π/2 = (0,-1); dot with secondClickUnit (0,1) = -1 < 0.
362
+ // candidate2 = rotate(-1,0) by -π/2 = (0, 1) → parallel to secondLineDirection (0,1) → cross = 0.
363
+ core.scene.clear();
364
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
365
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
366
+
367
+ core.scene.headers.chamferLength = 2;
368
+ core.scene.headers.chamferAngle = 90; // degrees
369
+ core.scene.headers.chamferMode = true;
370
+ core.scene.headers.trimMode = true;
371
+
372
+ const notifySpy = jest.spyOn(core, 'notify');
373
+
374
+ const chamfer = new Chamfer();
375
+ chamfer.firstPick.entity = core.scene.entities.get(0);
376
+ chamfer.secondPick.entity = core.scene.entities.get(1);
377
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
378
+ chamfer.secondPick.clickPoint = new Point(0, 5);
379
+ chamfer.action();
380
+
381
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.PARALLELLINES));
382
+ expect(core.scene.entities.count()).toBe(2);
383
+ notifySpy.mockRestore();
384
+ });
385
+
386
+ // ─── execute ──────────────────────────────────────────────────────────────────
387
+
388
+ test('Chamfer.execute returns early when first input is undefined', async () => {
389
+ core.scene.clear();
390
+ const chamfer = new Chamfer();
391
+ await withMockInput(core.scene, [], async () => {
392
+ await chamfer.execute();
393
+ });
394
+ // no error thrown — confirms early-return path
395
+ });
396
+
397
+ test('Chamfer.execute selects two lines and calls executeCommand', async () => {
398
+ core.scene.clear();
399
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
400
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
401
+
402
+ const executeCommandSpy = jest.fn();
403
+
404
+ await withMockInput(
405
+ core.scene,
406
+ [new SingleSelection(0, new Point(-5, 0)), new SingleSelection(1, new Point(0, 5))],
407
+ async () => {
408
+ const chamfer = new Chamfer();
409
+ await chamfer.execute();
410
+ expect(executeCommandSpy).toHaveBeenCalled();
411
+ },
412
+ { extraMethods: { executeCommand: executeCommandSpy } },
413
+ );
414
+ });
415
+
416
+ test('Chamfer.execute sets chamferDistanceA and chamferDistanceB via Distance option', async () => {
417
+ core.scene.headers.chamferDistanceA = 0;
418
+ core.scene.headers.chamferDistanceB = 0;
419
+
420
+ await withMockInput(
421
+ core.scene,
422
+ ['Distance', 4, 6, undefined],
423
+ async () => {
424
+ const chamfer = new Chamfer();
425
+ await chamfer.execute();
426
+ },
427
+ );
428
+
429
+ expect(core.scene.headers.chamferDistanceA).toBe(4);
430
+ expect(core.scene.headers.chamferDistanceB).toBe(6);
431
+ });
432
+
433
+ test('Chamfer.execute notifies INVALIDNUMBER for negative first distance', async () => {
434
+ const notifySpy = jest.spyOn(core, 'notify');
435
+
436
+ await withMockInput(
437
+ core.scene,
438
+ ['Distance', -2, undefined],
439
+ async () => {
440
+ const chamfer = new Chamfer();
441
+ await chamfer.execute();
442
+ },
443
+ );
444
+
445
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
446
+ notifySpy.mockRestore();
447
+ });
448
+
449
+ test('Chamfer.execute notifies INVALIDNUMBER for negative second distance', async () => {
450
+ const notifySpy = jest.spyOn(core, 'notify');
451
+
452
+ await withMockInput(
453
+ core.scene,
454
+ ['Distance', 3, -1, undefined],
455
+ async () => {
456
+ const chamfer = new Chamfer();
457
+ await chamfer.execute();
458
+ },
459
+ );
460
+
461
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
462
+ notifySpy.mockRestore();
463
+ });
464
+
465
+ test('Chamfer.execute sets chamferLength and chamferAngle via Angle option', async () => {
466
+ core.scene.headers.chamferLength = 0;
467
+ core.scene.headers.chamferAngle = 0;
468
+
469
+ await withMockInput(
470
+ core.scene,
471
+ ['Angle', 3, 60, undefined],
472
+ async () => {
473
+ const chamfer = new Chamfer();
474
+ await chamfer.execute();
475
+ },
476
+ );
477
+
478
+ expect(core.scene.headers.chamferLength).toBe(3);
479
+ expect(core.scene.headers.chamferAngle).toBe(60);
480
+ });
481
+
482
+ test('Chamfer.execute notifies INVALIDNUMBER for negative length in Angle option', async () => {
483
+ const notifySpy = jest.spyOn(core, 'notify');
484
+
485
+ await withMockInput(
486
+ core.scene,
487
+ ['Angle', -1, undefined],
488
+ async () => {
489
+ const chamfer = new Chamfer();
490
+ await chamfer.execute();
491
+ },
492
+ );
493
+
494
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
495
+ notifySpy.mockRestore();
496
+ });
497
+
498
+ test('Chamfer.execute notifies INVALIDNUMBER for out-of-range angle in Angle option', async () => {
499
+ const notifySpy = jest.spyOn(core, 'notify');
500
+
501
+ await withMockInput(
502
+ core.scene,
503
+ ['Angle', 3, 200, undefined], // 200° is >= 180, invalid
504
+ async () => {
505
+ const chamfer = new Chamfer();
506
+ await chamfer.execute();
507
+ },
508
+ );
509
+
510
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
511
+ notifySpy.mockRestore();
512
+ });
513
+
514
+ test('Chamfer.execute sets trimMode to false via Trim option', async () => {
515
+ core.scene.headers.trimMode = true;
516
+
517
+ await withMockInput(
518
+ core.scene,
519
+ ['Trim', 'No trim', undefined],
520
+ async () => {
521
+ const chamfer = new Chamfer();
522
+ await chamfer.execute();
523
+ },
524
+ );
525
+
526
+ expect(core.scene.headers.trimMode).toBe(false);
527
+ });
528
+
529
+ test('Chamfer.execute sets chamferMode to true via Method=Angle option', async () => {
530
+ core.scene.headers.chamferMode = false;
531
+
532
+ await withMockInput(
533
+ core.scene,
534
+ ['Method', 'Angle', undefined],
535
+ async () => {
536
+ const chamfer = new Chamfer();
537
+ await chamfer.execute();
538
+ },
539
+ );
540
+
541
+ expect(core.scene.headers.chamferMode).toBe(true);
542
+ });
543
+
544
+ test('Chamfer.execute re-prompts and notifies when first selected entity is not a Line', async () => {
545
+ core.scene.clear();
546
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
547
+
548
+ const notifySpy = jest.spyOn(core, 'notify');
549
+
550
+ await withMockInput(
551
+ core.scene,
552
+ [new SingleSelection(0, new Point(0, 0)), undefined],
553
+ async () => {
554
+ const chamfer = new Chamfer();
555
+ await chamfer.execute();
556
+ },
557
+ );
558
+
559
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOCHAMFER));
560
+ notifySpy.mockRestore();
561
+ });
562
+
563
+ test('Chamfer.execute re-prompts and notifies when second selected entity is not a Line', async () => {
564
+ core.scene.clear();
565
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
566
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
567
+
568
+ const notifySpy = jest.spyOn(core, 'notify');
569
+
570
+ await withMockInput(
571
+ core.scene,
572
+ [new SingleSelection(0, new Point(5, 0)), new SingleSelection(1, new Point(0, 0)), undefined],
573
+ async () => {
574
+ const chamfer = new Chamfer();
575
+ await chamfer.execute();
576
+ },
577
+ );
578
+
579
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOCHAMFER));
580
+ notifySpy.mockRestore();
581
+ });
582
+
583
+ // ─── action: polyline scenarios ───────────────────────────────────────────────
584
+
585
+ // Geometry shared across the Line + Lwpolyline tests:
586
+ // Line: (0,0)→(0,10) (vertical)
587
+ // Lwpolyline: [(-20,0), (-10,0), (0,0), (10,0)] (horizontal, 3 segments)
588
+ // Infinite extensions of the y=0 and x=0 lines meet at (0,0).
589
+ // With distance 2:
590
+ // polyChamfer = (-2, 0) (on the polyline segment y=0)
591
+ // lineChamfer = ( 0, 2) (on the external line x=0)
592
+
593
+ test('Chamfer.action dist>0 trimMode=true Line first + Lwpolyline second keepStart: chamfer embedded in polyline', () => {
594
+ // Click polyline near (-15,0) → segment 1. keepStart keeps the start portion.
595
+ core.scene.clear();
596
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
597
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
598
+
599
+ core.scene.headers.chamferDistanceA = 2;
600
+ core.scene.headers.chamferDistanceB = 2;
601
+ core.scene.headers.chamferMode = false;
602
+ core.scene.headers.trimMode = true;
603
+
604
+ const lineEntity = core.scene.entities.get(0);
605
+ const polyEntity = core.scene.entities.get(1);
606
+ const polyClickPoint = new Point(-15, 0);
607
+
608
+ const chamfer = new Chamfer();
609
+ chamfer.firstPick.entity = lineEntity;
610
+ chamfer.firstPick.segment = lineEntity;
611
+ chamfer.firstPick.segmentIndex = null;
612
+ chamfer.firstPick.clickPoint = new Point(0, 5);
613
+ chamfer.secondPick.entity = polyEntity;
614
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
615
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
616
+ chamfer.secondPick.clickPoint = polyClickPoint;
617
+ chamfer.action();
618
+
619
+ // Line is consumed into the polyline; no separate chamfer Line entity
620
+ expect(core.scene.entities.count()).toBe(1);
621
+
622
+ const poly = core.scene.entities.get(0);
623
+ // [(-20,0), polyChamfer(-2,0), lineChamfer(0,2), lineKeptEnd(0,10)]
624
+ expect(poly.points.length).toBe(4);
625
+ expect(poly.points[0].x).toBeCloseTo(-20);
626
+ expect(poly.points[0].y).toBeCloseTo(0);
627
+ expect(poly.points[1].x).toBeCloseTo(-2);
628
+ expect(poly.points[1].y).toBeCloseTo(0);
629
+ expect(poly.points[2].x).toBeCloseTo(0);
630
+ expect(poly.points[2].y).toBeCloseTo(2);
631
+ expect(poly.points[3].x).toBeCloseTo(0);
632
+ expect(poly.points[3].y).toBeCloseTo(10);
633
+ });
634
+
635
+ test('Chamfer.action dist>0 trimMode=true Lwpolyline first + Line second keepStart: same result with entities reversed', () => {
636
+ core.scene.clear();
637
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
638
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
639
+
640
+ core.scene.headers.chamferDistanceA = 2;
641
+ core.scene.headers.chamferDistanceB = 2;
642
+ core.scene.headers.chamferMode = false;
643
+ core.scene.headers.trimMode = true;
644
+
645
+ const polyEntity = core.scene.entities.get(0);
646
+ const lineEntity = core.scene.entities.get(1);
647
+ const polyClickPoint = new Point(-15, 0);
648
+
649
+ const chamfer = new Chamfer();
650
+ chamfer.firstPick.entity = polyEntity;
651
+ chamfer.firstPick.segment = polyEntity.getClosestSegment(polyClickPoint);
652
+ chamfer.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
653
+ chamfer.firstPick.clickPoint = polyClickPoint;
654
+ chamfer.secondPick.entity = lineEntity;
655
+ chamfer.secondPick.segment = lineEntity;
656
+ chamfer.secondPick.segmentIndex = null;
657
+ chamfer.secondPick.clickPoint = new Point(0, 5);
658
+ chamfer.action();
659
+
660
+ // Line is consumed; no separate chamfer Line entity
661
+ expect(core.scene.entities.count()).toBe(1);
662
+
663
+ const poly = core.scene.entities.get(0);
664
+ expect(poly.points.length).toBe(4);
665
+ expect(poly.points[0].x).toBeCloseTo(-20);
666
+ expect(poly.points[1].x).toBeCloseTo(-2);
667
+ expect(poly.points[1].y).toBeCloseTo(0);
668
+ expect(poly.points[2].x).toBeCloseTo(0);
669
+ expect(poly.points[2].y).toBeCloseTo(2);
670
+ expect(poly.points[3].x).toBeCloseTo(0);
671
+ expect(poly.points[3].y).toBeCloseTo(10);
672
+ });
673
+
674
+ test('Chamfer.action dist>0 trimMode=true Line + Lwpolyline keepEnd: keeps end portion of polyline', () => {
675
+ // Click polyline near (8,0) → segment 3 (right of intersection). keepEnd.
676
+ // polyChamfer = (2,0), lineChamfer = (0,2).
677
+ core.scene.clear();
678
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
679
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
680
+
681
+ core.scene.headers.chamferDistanceA = 2;
682
+ core.scene.headers.chamferDistanceB = 2;
683
+ core.scene.headers.chamferMode = false;
684
+ core.scene.headers.trimMode = true;
685
+
686
+ const lineEntity = core.scene.entities.get(0);
687
+ const polyEntity = core.scene.entities.get(1);
688
+ const polyClickPoint = new Point(8, 0);
689
+
690
+ const chamfer = new Chamfer();
691
+ chamfer.firstPick.entity = lineEntity;
692
+ chamfer.firstPick.segment = lineEntity;
693
+ chamfer.firstPick.segmentIndex = null;
694
+ chamfer.firstPick.clickPoint = new Point(0, 5);
695
+ chamfer.secondPick.entity = polyEntity;
696
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
697
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
698
+ chamfer.secondPick.clickPoint = polyClickPoint;
699
+ chamfer.action();
700
+
701
+ // Line consumed; end portion of polyline kept; no separate chamfer Line entity
702
+ expect(core.scene.entities.count()).toBe(1);
703
+
704
+ const poly = core.scene.entities.get(0);
705
+ // [lineKeptEnd(0,10), lineChamfer(0,2), polyChamfer(2,0), (10,0)]
706
+ expect(poly.points.length).toBe(4);
707
+ expect(poly.points[0].x).toBeCloseTo(0);
708
+ expect(poly.points[0].y).toBeCloseTo(10);
709
+ expect(poly.points[1].x).toBeCloseTo(0);
710
+ expect(poly.points[1].y).toBeCloseTo(2);
711
+ expect(poly.points[2].x).toBeCloseTo(2);
712
+ expect(poly.points[2].y).toBeCloseTo(0);
713
+ expect(poly.points[3].x).toBeCloseTo(10);
714
+ expect(poly.points[3].y).toBeCloseTo(0);
715
+ });
716
+
717
+ test('Chamfer.action dist>0 trimMode=true same Lwpolyline consecutive segments: chamfer points inserted', () => {
718
+ // L-shaped polyline; chamfer the inner corner. No separate chamfer Line entity.
719
+ // Segment 1: (-10,0)→(0,0), Segment 2: (0,0)→(0,10). Corner at (0,0).
720
+ // dist=2 → chamfer at (-2,0) and (0,2).
721
+ core.scene.clear();
722
+ core.scene.addItem('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
723
+
724
+ core.scene.headers.chamferDistanceA = 2;
725
+ core.scene.headers.chamferDistanceB = 2;
726
+ core.scene.headers.chamferMode = false;
727
+ core.scene.headers.trimMode = true;
728
+
729
+ const polyEntity = core.scene.entities.get(0);
730
+
731
+ const chamfer = new Chamfer();
732
+ chamfer.firstPick.entity = polyEntity;
733
+ chamfer.secondPick.entity = polyEntity;
734
+ chamfer.firstPick.segment = polyEntity.getClosestSegment(new Point(-5, 0));
735
+ chamfer.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(-5, 0));
736
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
737
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
738
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
739
+ chamfer.secondPick.clickPoint = new Point(0, 5);
740
+ chamfer.action();
741
+
742
+ // Chamfer segment is a straight polyline segment — no separate Line entity
743
+ expect(core.scene.entities.count()).toBe(1);
744
+
745
+ const poly = core.scene.entities.get(0);
746
+ // Corner vertex replaced: [(-10,0), (-2,0), (0,2), (0,10)]
747
+ expect(poly.points.length).toBe(4);
748
+ expect(poly.points[0].x).toBeCloseTo(-10);
749
+ expect(poly.points[0].y).toBeCloseTo(0);
750
+ expect(poly.points[1].x).toBeCloseTo(-2);
751
+ expect(poly.points[1].y).toBeCloseTo(0);
752
+ expect(poly.points[2].x).toBeCloseTo(0);
753
+ expect(poly.points[2].y).toBeCloseTo(2);
754
+ expect(poly.points[3].x).toBeCloseTo(0);
755
+ expect(poly.points[3].y).toBeCloseTo(10);
756
+ });
757
+
758
+ test('Chamfer.action dist>0 trimMode=true same Lwpolyline open ends: separate chamfer Line fills gap', () => {
759
+ // 4-point polyline: [(-5,0),(0,0),(1,0),(1,5)]. Segments 1 and 3 are the open ends.
760
+ // They are non-adjacent (diff=2) so the open-ends path is taken.
761
+ // Infinite lines of seg1 (y=0) and seg3 (x=1) meet at (1,0).
762
+ // dist=2, clicks at (-3,0) and (1,3):
763
+ // firstChamfer = (-1,0), secondChamfer = (1,2).
764
+ core.scene.clear();
765
+ core.scene.addItem('Lwpolyline', { points: [new Point(-5, 0), new Point(0, 0), new Point(1, 0), new Point(1, 5)] });
766
+
767
+ core.scene.headers.chamferDistanceA = 2;
768
+ core.scene.headers.chamferDistanceB = 2;
769
+ core.scene.headers.chamferMode = false;
770
+ core.scene.headers.trimMode = true;
771
+
772
+ const polyEntity = core.scene.entities.get(0);
773
+
774
+ const chamfer = new Chamfer();
775
+ chamfer.firstPick.entity = polyEntity;
776
+ chamfer.secondPick.entity = polyEntity;
777
+ chamfer.firstPick.segment = polyEntity.getClosestSegment(new Point(-3, 0)); // segment 1 (index 1)
778
+ chamfer.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(-3, 0));
779
+ chamfer.firstPick.clickPoint = new Point(-3, 0);
780
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(new Point(1, 3)); // segment 3 (last, index 3)
781
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(1, 3));
782
+ chamfer.secondPick.clickPoint = new Point(1, 3);
783
+ chamfer.action();
784
+
785
+ // Separate chamfer Line entity fills the gap between the trimmed open ends
786
+ expect(core.scene.entities.count()).toBe(2);
787
+
788
+ const chamferLine = core.scene.entities.get(1);
789
+ expect(chamferLine.type).toBe('Line');
790
+ // Chamfer line from firstChamfer(-1,0) to secondChamfer(1,2)
791
+ expect(chamferLine.points[0].x).toBeCloseTo(-1);
792
+ expect(chamferLine.points[0].y).toBeCloseTo(0);
793
+ expect(chamferLine.points[1].x).toBeCloseTo(1);
794
+ expect(chamferLine.points[1].y).toBeCloseTo(2);
795
+
796
+ const poly = core.scene.entities.get(0);
797
+ expect(poly.points.length).toBe(4);
798
+ // Start of polyline trimmed to first chamfer point: (-1,0)
799
+ expect(poly.points[0].x).toBeCloseTo(-1);
800
+ expect(poly.points[0].y).toBeCloseTo(0);
801
+ // End of polyline trimmed to second chamfer point: (1,2)
802
+ expect(poly.points[3].x).toBeCloseTo(1);
803
+ expect(poly.points[3].y).toBeCloseTo(2);
804
+ });
805
+
806
+ test('Chamfer.action dist>0 trimMode=true closed Lwpolyline seg3+closing-seg: corner-splice at shared vertex', () => {
807
+ // Square: [(0,0),(10,0),(10,10),(0,10)], closed (flag 1).
808
+ // Closing segment (idx=4) goes (0,10)→(0,0); seg 3 goes (10,10)→(0,10).
809
+ // Corner at (0,10) — chamfer dist=2 trims to (2,10) on seg3 and (0,8) on closing seg.
810
+ core.scene.clear();
811
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
812
+ const polyEntity = core.scene.entities.get(0);
813
+ polyEntity.flags.setFlagValue(1);
814
+
815
+ core.scene.headers.chamferDistanceA = 2;
816
+ core.scene.headers.chamferDistanceB = 2;
817
+ core.scene.headers.chamferMode = false;
818
+ core.scene.headers.trimMode = true;
819
+
820
+ const chamfer = new Chamfer();
821
+ chamfer.firstPick.entity = polyEntity;
822
+ chamfer.secondPick.entity = polyEntity;
823
+ // Click on segment 3 near (5,10) and on closing segment near (0,5)
824
+ chamfer.firstPick.segment = polyEntity.getClosestSegment(new Point(5, 10));
825
+ chamfer.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 10));
826
+ chamfer.firstPick.clickPoint = new Point(5, 10);
827
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
828
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
829
+ chamfer.secondPick.clickPoint = new Point(0, 5);
830
+ chamfer.action();
831
+
832
+ // Corner-splice: no extra entity, just the modified polyline
833
+ expect(core.scene.entities.count()).toBe(1);
834
+ const poly = core.scene.entities.get(0);
835
+ // Shared vertex (0,10) at index 3 replaced by two chamfer points → total 5 points
836
+ expect(poly.points.length).toBe(5);
837
+ // Other vertices unchanged
838
+ expect(poly.points[0].x).toBeCloseTo(0);
839
+ expect(poly.points[0].y).toBeCloseTo(0);
840
+ expect(poly.points[1].x).toBeCloseTo(10);
841
+ expect(poly.points[1].y).toBeCloseTo(0);
842
+ });
843
+
844
+ test('Chamfer.action dist>0 trimMode=true closed Lwpolyline closing-wrap (seg4+seg1): NOT treated as open-ends', () => {
845
+ // Same square, closed. Corner at (0,0) shared by closing seg (idx=4) and seg 1.
846
+ // Bug: old code had no closed-poly guard, so closing-wrap was treated as open-ends.
847
+ // Fix: closed poly must NOT enter the open-ends path → corner-splice at index 0.
848
+ core.scene.clear();
849
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
850
+ const polyEntity = core.scene.entities.get(0);
851
+ polyEntity.flags.setFlagValue(1);
852
+
853
+ core.scene.headers.chamferDistanceA = 2;
854
+ core.scene.headers.chamferDistanceB = 2;
855
+ core.scene.headers.chamferMode = false;
856
+ core.scene.headers.trimMode = true;
857
+
858
+ const chamfer = new Chamfer();
859
+ chamfer.firstPick.entity = polyEntity;
860
+ chamfer.secondPick.entity = polyEntity;
861
+ // closing segment near (0,5), seg 1 near (5,0)
862
+ chamfer.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
863
+ chamfer.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
864
+ chamfer.firstPick.clickPoint = new Point(0, 5);
865
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
866
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
867
+ chamfer.secondPick.clickPoint = new Point(5, 0);
868
+ chamfer.action();
869
+
870
+ // Must take corner-splice path (not open-ends): no extra chamfer Line entity
871
+ expect(core.scene.entities.count()).toBe(1);
872
+ const poly = core.scene.entities.get(0);
873
+ // Shared vertex (0,0) at index 0 replaced by two chamfer points → total 5 points
874
+ expect(poly.points.length).toBe(5);
875
+ // Endpoints must NOT have been moved to the intersection (open-ends symptom)
876
+ // The first two new points should be the chamfer points, not (0,0)/(0,0)
877
+ const allAtOrigin = poly.points[0].x === 0 && poly.points[0].y === 0 &&
878
+ poly.points[1].x === 0 && poly.points[1].y === 0;
879
+ expect(allAtOrigin).toBe(false);
880
+ });
881
+
882
+ test('Chamfer.action dist>0 trimMode=false closed Lwpolyline closing-wrap: standalone chamfer Line only', () => {
883
+ // Same square, closed, trimMode=false. No polyline mutation; only chamfer Line added.
884
+ core.scene.clear();
885
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
886
+ const polyEntity = core.scene.entities.get(0);
887
+ polyEntity.flags.setFlagValue(1);
888
+
889
+ core.scene.headers.chamferDistanceA = 2;
890
+ core.scene.headers.chamferDistanceB = 2;
891
+ core.scene.headers.chamferMode = false;
892
+ core.scene.headers.trimMode = false;
893
+
894
+ const chamfer = new Chamfer();
895
+ chamfer.firstPick.entity = polyEntity;
896
+ chamfer.secondPick.entity = polyEntity;
897
+ chamfer.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
898
+ chamfer.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
899
+ chamfer.firstPick.clickPoint = new Point(0, 5);
900
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
901
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
902
+ chamfer.secondPick.clickPoint = new Point(5, 0);
903
+ chamfer.action();
904
+
905
+ // trimMode=false: polyline unchanged, chamfer Line added
906
+ expect(core.scene.entities.count()).toBe(2);
907
+ expect(core.scene.entities.get(0).points.length).toBe(4); // polyline unchanged
908
+ expect(core.scene.entities.get(1).type).toBe('Line');
909
+ });
910
+
911
+ test('Chamfer.action dist>0 trimMode=true open Lwpolyline seg1+lastIdx still takes open-ends path', () => {
912
+ // Open 4-point polyline (no flags). Seg 1 and seg 3 (lastIdx=3) → open-ends path still fires.
913
+ // Regression guard: the closed-poly fix must not break the open-poly open-ends case.
914
+ core.scene.clear();
915
+ core.scene.addItem('Lwpolyline', { points: [new Point(-5, 0), new Point(0, 0), new Point(1, 0), new Point(1, 5)] });
916
+ const polyEntity = core.scene.entities.get(0);
917
+ // No flags set — open polyline
918
+
919
+ core.scene.headers.chamferDistanceA = 2;
920
+ core.scene.headers.chamferDistanceB = 2;
921
+ core.scene.headers.chamferMode = false;
922
+ core.scene.headers.trimMode = true;
923
+
924
+ const chamfer = new Chamfer();
925
+ chamfer.firstPick.entity = polyEntity;
926
+ chamfer.secondPick.entity = polyEntity;
927
+ chamfer.firstPick.segment = polyEntity.getClosestSegment(new Point(-3, 0));
928
+ chamfer.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(-3, 0));
929
+ chamfer.firstPick.clickPoint = new Point(-3, 0);
930
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(new Point(1, 3));
931
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(1, 3));
932
+ chamfer.secondPick.clickPoint = new Point(1, 3);
933
+ chamfer.action();
934
+
935
+ // Open-ends path: separate chamfer Line entity added
936
+ expect(core.scene.entities.count()).toBe(2);
937
+ expect(core.scene.entities.get(1).type).toBe('Line');
938
+ });
939
+
940
+ test('Chamfer.action dist>0 trimMode=false Line + Lwpolyline: standalone chamfer Line added, entities unchanged', () => {
941
+ core.scene.clear();
942
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
943
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
944
+
945
+ core.scene.headers.chamferDistanceA = 2;
946
+ core.scene.headers.chamferDistanceB = 2;
947
+ core.scene.headers.chamferMode = false;
948
+ core.scene.headers.trimMode = false;
949
+
950
+ const lineEntity = core.scene.entities.get(0);
951
+ const polyEntity = core.scene.entities.get(1);
952
+ const polyClickPoint = new Point(-15, 0);
953
+
954
+ const chamfer = new Chamfer();
955
+ chamfer.firstPick.entity = lineEntity;
956
+ chamfer.firstPick.segment = lineEntity;
957
+ chamfer.firstPick.segmentIndex = null;
958
+ chamfer.firstPick.clickPoint = new Point(0, 5);
959
+ chamfer.secondPick.entity = polyEntity;
960
+ chamfer.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
961
+ chamfer.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
962
+ chamfer.secondPick.clickPoint = polyClickPoint;
963
+ chamfer.action();
964
+
965
+ // Chamfer Line added as standalone entity; original line and polyline untouched
966
+ expect(core.scene.entities.count()).toBe(3);
967
+ expect(core.scene.entities.get(2).type).toBe('Line');
968
+ expect(core.scene.entities.get(0).points.length).toBe(2);
969
+ expect(core.scene.entities.get(1).points.length).toBe(4);
970
+ });