@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,831 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Fillet } from '../../core/tools/fillet.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
+ test('Fillet.register returns correct command object', () => {
12
+ const reg = Fillet.register();
13
+ expect(reg.command).toBe('Fillet');
14
+ expect(reg.shortcut).toBe('F');
15
+ });
16
+
17
+ test('Fillet.execute selects two entities and calls executeCommand', async () => {
18
+ core.scene.clear();
19
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
20
+ core.scene.addItem('Line', { points: [new Point(5, -5), new Point(5, 5)] });
21
+
22
+ const executeCommandSpy = jest.fn();
23
+
24
+ await withMockInput(
25
+ core.scene,
26
+ [new SingleSelection(0, new Point(2, 0)), new SingleSelection(1, new Point(5, 2))],
27
+ async () => {
28
+ const fillet = new Fillet();
29
+ await fillet.execute();
30
+ expect(executeCommandSpy).toHaveBeenCalled();
31
+ },
32
+ { extraMethods: { executeCommand: executeCommandSpy } },
33
+ );
34
+ });
35
+
36
+ test('Fillet.execute returns early when first input is undefined', async () => {
37
+ core.scene.clear();
38
+ const fillet = new Fillet();
39
+ await withMockInput(core.scene, [], async () => {
40
+ await fillet.execute(); // requestInput returns undefined → should not throw
41
+ });
42
+ // no assertion needed — just confirming no error is thrown
43
+ });
44
+
45
+ test('Fillet.action does nothing when entities are not set', () => {
46
+ core.scene.clear();
47
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
48
+ const entityCountBefore = core.scene.entities.count();
49
+
50
+ const fillet = new Fillet();
51
+ fillet.action(); // no firstEntity / secondEntity
52
+
53
+ expect(core.scene.entities.count()).toBe(entityCountBefore);
54
+ });
55
+
56
+ test('Fillet.action notifies when entity type is not supported', () => {
57
+ core.scene.clear();
58
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
59
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
60
+
61
+ const notifySpy = jest.spyOn(core, 'notify');
62
+
63
+ const fillet = new Fillet();
64
+ fillet.firstPick.entity = core.scene.entities.get(0); // Circle
65
+ fillet.secondPick.entity = core.scene.entities.get(1); // Line
66
+ fillet.firstPick.clickPoint = new Point(0, 0);
67
+ fillet.secondPick.clickPoint = new Point(5, 0);
68
+ fillet.action();
69
+
70
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOFILLET));
71
+ notifySpy.mockRestore();
72
+ });
73
+
74
+ test('Fillet.action notifies for parallel lines', () => {
75
+ core.scene.clear();
76
+ // Two horizontal parallel lines
77
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
78
+ core.scene.addItem('Line', { points: [new Point(0, 5), new Point(10, 5)] });
79
+
80
+ const notifySpy = jest.spyOn(core, 'notify');
81
+
82
+ const fillet = new Fillet();
83
+ fillet.firstPick.entity = core.scene.entities.get(0);
84
+ fillet.secondPick.entity = core.scene.entities.get(1);
85
+ fillet.firstPick.clickPoint = new Point(5, 0);
86
+ fillet.secondPick.clickPoint = new Point(5, 5);
87
+ fillet.action();
88
+
89
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.PARALLELLINES));
90
+ notifySpy.mockRestore();
91
+ });
92
+
93
+ test('Fillet.action radius=0 trimMode=true trims both lines to intersection', () => {
94
+ // Two lines meeting at 90° at (10, 0), but the segments don't overlap.
95
+ // Horizontal: (0,0)→(10,0) and Vertical: (10,0)→(10,10) — already share the corner.
96
+ // Use lines that only touch when extended:
97
+ // Horizontal: (0,0)→(8,0), Vertical: (10,-10)→(10,-2)
98
+ // Their infinite extensions meet at (10, 0).
99
+ core.scene.clear();
100
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(8, 0)] });
101
+ core.scene.addItem('Line', { points: [new Point(10, -10), new Point(10, -2)] });
102
+
103
+ core.scene.headers.filletRadius = 0;
104
+ core.scene.headers.trimMode = true;
105
+
106
+ const fillet = new Fillet();
107
+ fillet.firstPick.entity = core.scene.entities.get(0);
108
+ fillet.secondPick.entity = core.scene.entities.get(1);
109
+ fillet.firstPick.clickPoint = new Point(4, 0);
110
+ fillet.secondPick.clickPoint = new Point(10, -6);
111
+ fillet.action();
112
+
113
+ const line1 = core.scene.entities.get(0);
114
+ const line2 = core.scene.entities.get(1);
115
+
116
+ // Line 1 should now end at the intersection (10, 0)
117
+ expect(line1.points[0].x).toBeCloseTo(0);
118
+ expect(line1.points[0].y).toBeCloseTo(0);
119
+ expect(line1.points[1].x).toBeCloseTo(10);
120
+ expect(line1.points[1].y).toBeCloseTo(0);
121
+
122
+ // Line 2 should now end at the intersection (10, 0)
123
+ expect(line2.points[0].x).toBeCloseTo(10);
124
+ expect(line2.points[0].y).toBeCloseTo(-10);
125
+ expect(line2.points[1].x).toBeCloseTo(10);
126
+ expect(line2.points[1].y).toBeCloseTo(0);
127
+ });
128
+
129
+ test('Fillet.action radius=0 trimMode=false makes no changes', () => {
130
+ core.scene.clear();
131
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(8, 0)] });
132
+ core.scene.addItem('Line', { points: [new Point(10, -10), new Point(10, -2)] });
133
+
134
+ core.scene.headers.filletRadius = 0;
135
+ core.scene.headers.trimMode = false;
136
+
137
+ const fillet = new Fillet();
138
+ fillet.firstPick.entity = core.scene.entities.get(0);
139
+ fillet.secondPick.entity = core.scene.entities.get(1);
140
+ fillet.firstPick.clickPoint = new Point(4, 0);
141
+ fillet.secondPick.clickPoint = new Point(10, -6);
142
+ fillet.action();
143
+
144
+ // Entities unchanged — no new entities, same points
145
+ expect(core.scene.entities.count()).toBe(2);
146
+ expect(core.scene.entities.get(0).points[1].x).toBeCloseTo(8);
147
+ expect(core.scene.entities.get(1).points[1].y).toBeCloseTo(-2);
148
+ });
149
+
150
+ test('Fillet.action radius>0 trimMode=true adds arc and trims lines', () => {
151
+ // Perpendicular lines forming an L at (0,0):
152
+ // Horizontal: (-10,0)→(0,0) Vertical: (0,0)→(0,10)
153
+ // With radius 2, tangent points are at (-2,0) and (0,2)
154
+ core.scene.clear();
155
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
156
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
157
+
158
+ core.scene.headers.filletRadius = 2;
159
+ core.scene.headers.trimMode = true;
160
+
161
+ const fillet = new Fillet();
162
+ fillet.firstPick.entity = core.scene.entities.get(0);
163
+ fillet.secondPick.entity = core.scene.entities.get(1);
164
+ fillet.firstPick.clickPoint = new Point(-5, 0); // on the horizontal line, left of intersection
165
+ fillet.secondPick.clickPoint = new Point(0, 5); // on the vertical line, above intersection
166
+ fillet.action();
167
+
168
+ // An arc should have been added — scene now has 3 entities
169
+ expect(core.scene.entities.count()).toBe(3);
170
+
171
+ const line1 = core.scene.entities.get(0);
172
+ const line2 = core.scene.entities.get(1);
173
+ const arc = core.scene.entities.get(2);
174
+
175
+ expect(arc.type).toBe('Arc');
176
+
177
+ // Line 1 trimmed: kept end (-10,0), trimmed end at tangent (-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 end at tangent (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('Fillet.action radius>0 trimMode=false adds arc only, lines unchanged', () => {
191
+ core.scene.clear();
192
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
193
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
194
+
195
+ core.scene.headers.filletRadius = 2;
196
+ core.scene.headers.trimMode = false;
197
+
198
+ const fillet = new Fillet();
199
+ fillet.firstPick.entity = core.scene.entities.get(0);
200
+ fillet.secondPick.entity = core.scene.entities.get(1);
201
+ fillet.firstPick.clickPoint = new Point(-5, 0);
202
+ fillet.secondPick.clickPoint = new Point(0, 5);
203
+ fillet.action();
204
+
205
+ // Arc added, but lines not trimmed
206
+ expect(core.scene.entities.count()).toBe(3);
207
+ expect(core.scene.entities.get(2).type).toBe('Arc');
208
+
209
+ // Lines still have their original endpoints
210
+ expect(core.scene.entities.get(0).points[1].x).toBeCloseTo(0);
211
+ expect(core.scene.entities.get(0).points[1].y).toBeCloseTo(0);
212
+ expect(core.scene.entities.get(1).points[0].x).toBeCloseTo(0);
213
+ expect(core.scene.entities.get(1).points[0].y).toBeCloseTo(0);
214
+ });
215
+
216
+ test('Fillet.action notifies when radius is too large for the segments', () => {
217
+ // Lines only 5 units long from intersection — radius 10 won't fit
218
+ core.scene.clear();
219
+ core.scene.addItem('Line', { points: [new Point(-5, 0), new Point(0, 0)] });
220
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 5)] });
221
+
222
+ core.scene.headers.filletRadius = 10;
223
+ core.scene.headers.trimMode = true;
224
+
225
+ const notifySpy = jest.spyOn(core, 'notify');
226
+
227
+ const fillet = new Fillet();
228
+ fillet.firstPick.entity = core.scene.entities.get(0);
229
+ fillet.secondPick.entity = core.scene.entities.get(1);
230
+ fillet.firstPick.clickPoint = new Point(-2, 0);
231
+ fillet.secondPick.clickPoint = new Point(0, 2);
232
+ fillet.action();
233
+
234
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.RADIUSTOOLARGE));
235
+ // No arc added
236
+ expect(core.scene.entities.count()).toBe(2);
237
+ notifySpy.mockRestore();
238
+ });
239
+
240
+ test('Fillet.action arc centre is at correct position for 90° corner', () => {
241
+ // 90° corner at origin, radius 3 → arc centre at (-3, 3)
242
+ core.scene.clear();
243
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
244
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
245
+
246
+ core.scene.headers.filletRadius = 3;
247
+ core.scene.headers.trimMode = false;
248
+
249
+ const fillet = new Fillet();
250
+ fillet.firstPick.entity = core.scene.entities.get(0);
251
+ fillet.secondPick.entity = core.scene.entities.get(1);
252
+ fillet.firstPick.clickPoint = new Point(-5, 0);
253
+ fillet.secondPick.clickPoint = new Point(0, 5);
254
+ fillet.action();
255
+
256
+ const arc = core.scene.entities.get(2);
257
+ expect(arc.type).toBe('Arc');
258
+
259
+ // Arc centre point is points[0]
260
+ expect(arc.points[0].x).toBeCloseTo(-3);
261
+ expect(arc.points[0].y).toBeCloseTo(3);
262
+
263
+ // Tangent points
264
+ expect(arc.points[1].x).toBeCloseTo(-3);
265
+ expect(arc.points[1].y).toBeCloseTo(0);
266
+ expect(arc.points[2].x).toBeCloseTo(0);
267
+ expect(arc.points[2].y).toBeCloseTo(3);
268
+ });
269
+
270
+ test('Fillet.action selects correct corner when lines form a cross', () => {
271
+ // Full cross: horizontal (-10,0)→(10,0), vertical (0,-10)→(0,10)
272
+ // Click on the top-right quadrant → fillet should be in that quadrant
273
+ core.scene.clear();
274
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
275
+ core.scene.addItem('Line', { points: [new Point(0, -10), new Point(0, 10)] });
276
+
277
+ core.scene.headers.filletRadius = 2;
278
+ core.scene.headers.trimMode = false;
279
+
280
+ const fillet = new Fillet();
281
+ fillet.firstPick.entity = core.scene.entities.get(0);
282
+ fillet.secondPick.entity = core.scene.entities.get(1);
283
+ fillet.firstPick.clickPoint = new Point(5, 0); // right side of horizontal
284
+ fillet.secondPick.clickPoint = new Point(0, 5); // top of vertical
285
+ fillet.action();
286
+
287
+ const arc = core.scene.entities.get(2);
288
+ expect(arc.type).toBe('Arc');
289
+
290
+ // Arc centre should be in the top-right quadrant
291
+ expect(arc.points[0].x).toBeGreaterThan(0);
292
+ expect(arc.points[0].y).toBeGreaterThan(0);
293
+ });
294
+
295
+ // ─── action: second entity type guard ────────────────────────────────────────
296
+
297
+ test('Fillet.action notifies when second entity type is not supported', () => {
298
+ core.scene.clear();
299
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
300
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
301
+
302
+ const notifySpy = jest.spyOn(core, 'notify');
303
+
304
+ const fillet = new Fillet();
305
+ fillet.firstPick.entity = core.scene.entities.get(0); // Line
306
+ fillet.secondPick.entity = core.scene.entities.get(1); // Circle
307
+ fillet.firstPick.clickPoint = new Point(5, 0);
308
+ fillet.secondPick.clickPoint = new Point(0, 0);
309
+ fillet.action();
310
+
311
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOFILLET));
312
+ notifySpy.mockRestore();
313
+ });
314
+
315
+ // ─── action: click at intersection ───────────────────────────────────────────
316
+
317
+ test('Fillet.action notifies when click coincides with intersection', () => {
318
+ // L-corner at origin. Clicking firstClickPoint exactly at (0,0) makes
319
+ // firstClickDistance = 0, triggering the invalid-selection guard.
320
+ core.scene.clear();
321
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
322
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
323
+
324
+ core.scene.headers.filletRadius = 2;
325
+ core.scene.headers.trimMode = true;
326
+
327
+ const notifySpy = jest.spyOn(core, 'notify');
328
+
329
+ const fillet = new Fillet();
330
+ fillet.firstPick.entity = core.scene.entities.get(0);
331
+ fillet.secondPick.entity = core.scene.entities.get(1);
332
+ fillet.firstPick.clickPoint = new Point(0, 0); // exactly at the intersection
333
+ fillet.secondPick.clickPoint = new Point(0, 5);
334
+ fillet.action();
335
+
336
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.SELECTION));
337
+ expect(core.scene.entities.count()).toBe(2); // no arc added
338
+ notifySpy.mockRestore();
339
+ });
340
+
341
+ test('Fillet.action radius=0 notifies and makes no changes when click coincides with intersection', () => {
342
+ // Click at exactly the intersection point — clickDistance = 0.
343
+ // The guard must fire before applySharpTrim() runs.
344
+ core.scene.clear();
345
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
346
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
347
+
348
+ core.scene.headers.filletRadius = 0;
349
+ core.scene.headers.trimMode = true;
350
+
351
+ const notifySpy = jest.spyOn(core, 'notify');
352
+
353
+ const fillet = new Fillet();
354
+ fillet.firstPick.entity = core.scene.entities.get(0);
355
+ fillet.secondPick.entity = core.scene.entities.get(1);
356
+ fillet.firstPick.clickPoint = new Point(0, 0); // exactly at the intersection
357
+ fillet.secondPick.clickPoint = new Point(0, 5);
358
+ fillet.action();
359
+
360
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.SELECTION));
361
+ // lines must be unchanged — applySharpTrim must not have run
362
+ expect(core.scene.entities.get(0).points[1].x).toBeCloseTo(0);
363
+ expect(core.scene.entities.get(0).points[1].y).toBeCloseTo(0);
364
+ notifySpy.mockRestore();
365
+ });
366
+
367
+ // ─── execute: option branches ─────────────────────────────────────────────────
368
+
369
+ test('Fillet.execute sets filletRadius when Radius option is selected', async () => {
370
+ core.scene.headers.filletRadius = 0;
371
+
372
+ await withMockInput(
373
+ core.scene,
374
+ ['Radius', 5, undefined],
375
+ async () => {
376
+ const fillet = new Fillet();
377
+ await fillet.execute();
378
+ },
379
+ );
380
+
381
+ expect(core.scene.headers.filletRadius).toBe(5);
382
+ });
383
+
384
+ test('Fillet.execute notifies INVALIDNUMBER when negative radius is provided', async () => {
385
+ const notifySpy = jest.spyOn(core, 'notify');
386
+
387
+ await withMockInput(
388
+ core.scene,
389
+ ['Radius', -3, undefined],
390
+ async () => {
391
+ const fillet = new Fillet();
392
+ await fillet.execute();
393
+ },
394
+ );
395
+
396
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
397
+ notifySpy.mockRestore();
398
+ });
399
+
400
+ test('Fillet.execute sets trimMode to false when No trim option is selected', async () => {
401
+ core.scene.headers.trimMode = true;
402
+
403
+ await withMockInput(
404
+ core.scene,
405
+ ['Trim', 'No trim', undefined],
406
+ async () => {
407
+ const fillet = new Fillet();
408
+ await fillet.execute();
409
+ },
410
+ );
411
+
412
+ expect(core.scene.headers.trimMode).toBe(false);
413
+ });
414
+
415
+ test('Fillet.execute re-prompts and notifies when first selected entity is not a Line', async () => {
416
+ core.scene.clear();
417
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
418
+
419
+ const notifySpy = jest.spyOn(core, 'notify');
420
+
421
+ await withMockInput(
422
+ core.scene,
423
+ [new SingleSelection(0, new Point(0, 0)), undefined],
424
+ async () => {
425
+ const fillet = new Fillet();
426
+ await fillet.execute();
427
+ },
428
+ );
429
+
430
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOFILLET));
431
+ notifySpy.mockRestore();
432
+ });
433
+
434
+ test('Fillet.execute re-prompts and notifies when second selected entity is not a Line', async () => {
435
+ core.scene.clear();
436
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
437
+ core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
438
+
439
+ const notifySpy = jest.spyOn(core, 'notify');
440
+
441
+ await withMockInput(
442
+ core.scene,
443
+ [new SingleSelection(0, new Point(5, 0)), new SingleSelection(1, new Point(0, 0)), undefined],
444
+ async () => {
445
+ const fillet = new Fillet();
446
+ await fillet.execute();
447
+ },
448
+ );
449
+
450
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NOFILLET));
451
+ notifySpy.mockRestore();
452
+ });
453
+
454
+ // ─── action: polyline scenarios ───────────────────────────────────────────────
455
+
456
+ // Geometry shared across the Line + Lwpolyline tests:
457
+ // Line: (0,0)→(0,10) (vertical)
458
+ // Lwpolyline: [(-20,0), (-10,0), (0,0), (10,0)] (horizontal, 3 segments)
459
+ // Infinite extensions of the y=0 and x=0 lines meet at (0,0).
460
+ // With radius 2 and a 90° corner:
461
+ // arc centre = (-2, 2)
462
+ // polyTangent = (-2, 0) (tangent on the polyline segment y=0)
463
+ // lineTangent = ( 0, 2) (tangent on the external line x=0)
464
+
465
+ test('Fillet.action radius>0 trimMode=true Line first + Lwpolyline second keepStart: arc embedded as bulge', () => {
466
+ // Click polyline near (-15,0) → segment 1. keepStart keeps the start portion.
467
+ core.scene.clear();
468
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
469
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
470
+
471
+ core.scene.headers.filletRadius = 2;
472
+ core.scene.headers.trimMode = true;
473
+
474
+ const lineEntity = core.scene.entities.get(0);
475
+ const polyEntity = core.scene.entities.get(1);
476
+ const polyClickPoint = new Point(-15, 0);
477
+
478
+ const fillet = new Fillet();
479
+ fillet.firstPick.entity = lineEntity;
480
+ fillet.firstPick.segment = lineEntity;
481
+ fillet.firstPick.segmentIndex = null;
482
+ fillet.firstPick.clickPoint = new Point(0, 5);
483
+ fillet.secondPick.entity = polyEntity;
484
+ fillet.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
485
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
486
+ fillet.secondPick.clickPoint = polyClickPoint;
487
+ fillet.action();
488
+
489
+ // Line is consumed into the polyline; no separate Arc entity
490
+ expect(core.scene.entities.count()).toBe(1);
491
+
492
+ const poly = core.scene.entities.get(0);
493
+ // [(-20,0), polyTangent(-2,0)[bulge], lineTangent(0,2), lineKeptEnd(0,10)]
494
+ expect(poly.points.length).toBe(4);
495
+ expect(poly.points[0].x).toBeCloseTo(-20);
496
+ expect(poly.points[0].y).toBeCloseTo(0);
497
+ expect(poly.points[1].x).toBeCloseTo(-2);
498
+ expect(poly.points[1].y).toBeCloseTo(0);
499
+ expect(poly.points[1].bulge).not.toBe(0);
500
+ expect(poly.points[2].x).toBeCloseTo(0);
501
+ expect(poly.points[2].y).toBeCloseTo(2);
502
+ expect(poly.points[3].x).toBeCloseTo(0);
503
+ expect(poly.points[3].y).toBeCloseTo(10);
504
+ });
505
+
506
+ test('Fillet.action radius>0 trimMode=true Lwpolyline first + Line second keepStart: arc direction correct', () => {
507
+ // Same geometry but entity order reversed — tests the polyToLineDir arc direction fix.
508
+ core.scene.clear();
509
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
510
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
511
+
512
+ core.scene.headers.filletRadius = 2;
513
+ core.scene.headers.trimMode = true;
514
+
515
+ const polyEntity = core.scene.entities.get(0);
516
+ const lineEntity = core.scene.entities.get(1);
517
+ const polyClickPoint = new Point(-15, 0);
518
+
519
+ const fillet = new Fillet();
520
+ fillet.firstPick.entity = polyEntity;
521
+ fillet.firstPick.segment = polyEntity.getClosestSegment(polyClickPoint);
522
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
523
+ fillet.firstPick.clickPoint = polyClickPoint;
524
+ fillet.secondPick.entity = lineEntity;
525
+ fillet.secondPick.segment = lineEntity;
526
+ fillet.secondPick.segmentIndex = null;
527
+ fillet.secondPick.clickPoint = new Point(0, 5);
528
+ fillet.action();
529
+
530
+ // Line is consumed; no separate Arc entity
531
+ expect(core.scene.entities.count()).toBe(1);
532
+
533
+ const poly = core.scene.entities.get(0);
534
+ expect(poly.points.length).toBe(4);
535
+ expect(poly.points[0].x).toBeCloseTo(-20);
536
+ expect(poly.points[1].x).toBeCloseTo(-2);
537
+ expect(poly.points[1].y).toBeCloseTo(0);
538
+ // CCW arc: bulge should be positive
539
+ expect(poly.points[1].bulge).toBeGreaterThan(0);
540
+ expect(poly.points[2].x).toBeCloseTo(0);
541
+ expect(poly.points[2].y).toBeCloseTo(2);
542
+ expect(poly.points[3].x).toBeCloseTo(0);
543
+ expect(poly.points[3].y).toBeCloseTo(10);
544
+ });
545
+
546
+ test('Fillet.action radius>0 trimMode=true Line + Lwpolyline keepEnd: keeps end portion of polyline', () => {
547
+ // Click polyline near (8,0) → segment 3 (right of intersection). keepEnd.
548
+ // arc centre = (2,2), polyTangent = (2,0), lineTangent = (0,2).
549
+ core.scene.clear();
550
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
551
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
552
+
553
+ core.scene.headers.filletRadius = 2;
554
+ core.scene.headers.trimMode = true;
555
+
556
+ const lineEntity = core.scene.entities.get(0);
557
+ const polyEntity = core.scene.entities.get(1);
558
+ const polyClickPoint = new Point(8, 0);
559
+
560
+ const fillet = new Fillet();
561
+ fillet.firstPick.entity = lineEntity;
562
+ fillet.firstPick.segment = lineEntity;
563
+ fillet.firstPick.segmentIndex = null;
564
+ fillet.firstPick.clickPoint = new Point(0, 5);
565
+ fillet.secondPick.entity = polyEntity;
566
+ fillet.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
567
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
568
+ fillet.secondPick.clickPoint = polyClickPoint;
569
+ fillet.action();
570
+
571
+ // Line consumed; end portion kept; no separate Arc entity
572
+ expect(core.scene.entities.count()).toBe(1);
573
+
574
+ const poly = core.scene.entities.get(0);
575
+ // [lineKeptEnd(0,10), lineTangent(0,2)[bulge], polyTangent(2,0), (10,0)]
576
+ expect(poly.points.length).toBe(4);
577
+ expect(poly.points[0].x).toBeCloseTo(0);
578
+ expect(poly.points[0].y).toBeCloseTo(10);
579
+ expect(poly.points[1].x).toBeCloseTo(0);
580
+ expect(poly.points[1].y).toBeCloseTo(2);
581
+ expect(poly.points[1].bulge).not.toBe(0);
582
+ expect(poly.points[2].x).toBeCloseTo(2);
583
+ expect(poly.points[2].y).toBeCloseTo(0);
584
+ expect(poly.points[3].x).toBeCloseTo(10);
585
+ expect(poly.points[3].y).toBeCloseTo(0);
586
+ });
587
+
588
+ test('Fillet.action radius=0 trimMode=true Line + Lwpolyline keepStart: line consumed to sharp corner', () => {
589
+ // radius=0 → sharp intersection at (0,0), no arc.
590
+ core.scene.clear();
591
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
592
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
593
+
594
+ core.scene.headers.filletRadius = 0;
595
+ core.scene.headers.trimMode = true;
596
+
597
+ const lineEntity = core.scene.entities.get(0);
598
+ const polyEntity = core.scene.entities.get(1);
599
+ const polyClickPoint = new Point(-15, 0);
600
+
601
+ const fillet = new Fillet();
602
+ fillet.firstPick.entity = lineEntity;
603
+ fillet.firstPick.segment = lineEntity;
604
+ fillet.firstPick.segmentIndex = null;
605
+ fillet.firstPick.clickPoint = new Point(0, 5);
606
+ fillet.secondPick.entity = polyEntity;
607
+ fillet.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
608
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
609
+ fillet.secondPick.clickPoint = polyClickPoint;
610
+ fillet.action();
611
+
612
+ // Line removed; polyline updated with sharp corner; no arc entity
613
+ expect(core.scene.entities.count()).toBe(1);
614
+
615
+ const poly = core.scene.entities.get(0);
616
+ // [(-20,0), intersection(0,0), lineKeptEnd(0,10)]
617
+ expect(poly.points.length).toBe(3);
618
+ expect(poly.points[0].x).toBeCloseTo(-20);
619
+ expect(poly.points[0].y).toBeCloseTo(0);
620
+ expect(poly.points[1].x).toBeCloseTo(0);
621
+ expect(poly.points[1].y).toBeCloseTo(0);
622
+ expect(poly.points[2].x).toBeCloseTo(0);
623
+ expect(poly.points[2].y).toBeCloseTo(10);
624
+ });
625
+
626
+ test('Fillet.action radius>0 trimMode=true same Lwpolyline consecutive segments: arc inserted as bulge', () => {
627
+ // L-shaped polyline; fillet the inner corner. Arc encoded as a bulge — no separate Arc entity.
628
+ // Segment 1: (-10,0)→(0,0), Segment 2: (0,0)→(0,10). Corner at (0,0).
629
+ // radius=2 → arc centre (-2,2), tangents at (-2,0) and (0,2).
630
+ core.scene.clear();
631
+ core.scene.addItem('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
632
+
633
+ core.scene.headers.filletRadius = 2;
634
+ core.scene.headers.trimMode = true;
635
+
636
+ const polyEntity = core.scene.entities.get(0);
637
+
638
+ const fillet = new Fillet();
639
+ fillet.firstPick.entity = polyEntity;
640
+ fillet.secondPick.entity = polyEntity;
641
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(-5, 0));
642
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(-5, 0));
643
+ fillet.firstPick.clickPoint = new Point(-5, 0);
644
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
645
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
646
+ fillet.secondPick.clickPoint = new Point(0, 5);
647
+ fillet.action();
648
+
649
+ // Arc is embedded as a bulge — no separate Arc entity
650
+ expect(core.scene.entities.count()).toBe(1);
651
+
652
+ const poly = core.scene.entities.get(0);
653
+ // Corner vertex replaced: [(-10,0), (-2,0)[bulge], (0,2), (0,10)]
654
+ expect(poly.points.length).toBe(4);
655
+ expect(poly.points[0].x).toBeCloseTo(-10);
656
+ expect(poly.points[0].y).toBeCloseTo(0);
657
+ expect(poly.points[1].x).toBeCloseTo(-2);
658
+ expect(poly.points[1].y).toBeCloseTo(0);
659
+ expect(poly.points[1].bulge).not.toBe(0);
660
+ expect(poly.points[2].x).toBeCloseTo(0);
661
+ expect(poly.points[2].y).toBeCloseTo(2);
662
+ expect(poly.points[3].x).toBeCloseTo(0);
663
+ expect(poly.points[3].y).toBeCloseTo(10);
664
+ });
665
+
666
+ test('Fillet.action radius>0 trimMode=true same Lwpolyline open ends: separate Arc entity fills gap', () => {
667
+ // 4-point polyline: [(-5,0),(0,0),(1,0),(1,5)]. Segments 1 and 3 are the open ends.
668
+ // They are non-adjacent (diff=2) so the open-ends path is taken.
669
+ // Infinite lines of seg1 (y=0) and seg3 (x=1) meet at (1,0).
670
+ // radius=2, clicks at (-3,0) and (1,3):
671
+ // arc centre = (-1,2), firstTangent = (-1,0), secondTangent = (1,2).
672
+ core.scene.clear();
673
+ core.scene.addItem('Lwpolyline', { points: [new Point(-5, 0), new Point(0, 0), new Point(1, 0), new Point(1, 5)] });
674
+
675
+ core.scene.headers.filletRadius = 2;
676
+ core.scene.headers.trimMode = true;
677
+
678
+ const polyEntity = core.scene.entities.get(0);
679
+
680
+ const fillet = new Fillet();
681
+ fillet.firstPick.entity = polyEntity;
682
+ fillet.secondPick.entity = polyEntity;
683
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(-3, 0)); // segment 1 (index 1)
684
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(-3, 0));
685
+ fillet.firstPick.clickPoint = new Point(-3, 0);
686
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(1, 3)); // segment 3 (last, index 3)
687
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(1, 3));
688
+ fillet.secondPick.clickPoint = new Point(1, 3);
689
+ fillet.action();
690
+
691
+ // Separate Arc entity fills the gap between the trimmed open ends
692
+ expect(core.scene.entities.count()).toBe(2);
693
+
694
+ const arc = core.scene.entities.get(1);
695
+ expect(arc.type).toBe('Arc');
696
+ // Arc centre at (-1,2)
697
+ expect(arc.points[0].x).toBeCloseTo(-1);
698
+ expect(arc.points[0].y).toBeCloseTo(2);
699
+
700
+ const poly = core.scene.entities.get(0);
701
+ expect(poly.points.length).toBe(4);
702
+ // Start of polyline trimmed to tangent on segment 1: (-1,0)
703
+ expect(poly.points[0].x).toBeCloseTo(-1);
704
+ expect(poly.points[0].y).toBeCloseTo(0);
705
+ // End of polyline trimmed to tangent on segment 3: (1,2)
706
+ expect(poly.points[3].x).toBeCloseTo(1);
707
+ expect(poly.points[3].y).toBeCloseTo(2);
708
+ });
709
+
710
+ test('Fillet.action radius>0 trimMode=true closed Lwpolyline seg3+closing-seg: corner-splice at shared vertex', () => {
711
+ // Square: [(0,0),(10,0),(10,10),(0,10)], closed (flag 1).
712
+ // Closing segment (idx=4) goes (0,10)→(0,0); seg 3 goes (10,10)→(0,10).
713
+ // Corner at (0,10) — radius=2 replaces the shared vertex with arc bulge points.
714
+ core.scene.clear();
715
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
716
+ const polyEntity = core.scene.entities.get(0);
717
+ polyEntity.flags.setFlagValue(1);
718
+
719
+ core.scene.headers.filletRadius = 2;
720
+ core.scene.headers.trimMode = true;
721
+
722
+ const fillet = new Fillet();
723
+ fillet.firstPick.entity = polyEntity;
724
+ fillet.secondPick.entity = polyEntity;
725
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(5, 10));
726
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 10));
727
+ fillet.firstPick.clickPoint = new Point(5, 10);
728
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
729
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
730
+ fillet.secondPick.clickPoint = new Point(0, 5);
731
+ fillet.action();
732
+
733
+ // Corner-splice: no extra Arc entity, arc embedded as bulge
734
+ expect(core.scene.entities.count()).toBe(1);
735
+ const poly = core.scene.entities.get(0);
736
+ // Shared vertex (0,10) replaced by two tangent points → total 5 points
737
+ expect(poly.points.length).toBe(5);
738
+ // One of the new points should carry a non-zero bulge
739
+ const hasBulge = poly.points.some((p) => p.bulge !== 0);
740
+ expect(hasBulge).toBe(true);
741
+ });
742
+
743
+ test('Fillet.action radius>0 trimMode=true closed Lwpolyline closing-wrap (seg4+seg1): NOT treated as open-ends', () => {
744
+ // Corner at (0,0) shared by closing seg (idx=4) and seg 1.
745
+ // Bug: without the closed-poly guard, this was treated as open-ends → wrong path.
746
+ core.scene.clear();
747
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
748
+ const polyEntity = core.scene.entities.get(0);
749
+ polyEntity.flags.setFlagValue(1);
750
+
751
+ core.scene.headers.filletRadius = 2;
752
+ core.scene.headers.trimMode = true;
753
+
754
+ const fillet = new Fillet();
755
+ fillet.firstPick.entity = polyEntity;
756
+ fillet.secondPick.entity = polyEntity;
757
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
758
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
759
+ fillet.firstPick.clickPoint = new Point(0, 5);
760
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
761
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
762
+ fillet.secondPick.clickPoint = new Point(5, 0);
763
+ fillet.action();
764
+
765
+ // Must take corner-splice path: no extra Arc entity
766
+ expect(core.scene.entities.count()).toBe(1);
767
+ const poly = core.scene.entities.get(0);
768
+ // Shared vertex (0,0) replaced by two tangent points → total 5 points
769
+ expect(poly.points.length).toBe(5);
770
+ // Endpoints must NOT have been moved to the intersection (open-ends symptom)
771
+ const allAtOrigin = poly.points[0].x === 0 && poly.points[0].y === 0 &&
772
+ poly.points[poly.points.length - 1].x === 0 && poly.points[poly.points.length - 1].y === 0;
773
+ expect(allAtOrigin).toBe(false);
774
+ });
775
+
776
+ test('Fillet.action radius>0 trimMode=false closed Lwpolyline closing-wrap: standalone Arc only', () => {
777
+ // trimMode=false: no polyline mutation; only Arc entity added.
778
+ core.scene.clear();
779
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
780
+ const polyEntity = core.scene.entities.get(0);
781
+ polyEntity.flags.setFlagValue(1);
782
+
783
+ core.scene.headers.filletRadius = 2;
784
+ core.scene.headers.trimMode = false;
785
+
786
+ const fillet = new Fillet();
787
+ fillet.firstPick.entity = polyEntity;
788
+ fillet.secondPick.entity = polyEntity;
789
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
790
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
791
+ fillet.firstPick.clickPoint = new Point(0, 5);
792
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
793
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
794
+ fillet.secondPick.clickPoint = new Point(5, 0);
795
+ fillet.action();
796
+
797
+ // trimMode=false: polyline unchanged, arc added
798
+ expect(core.scene.entities.count()).toBe(2);
799
+ expect(core.scene.entities.get(0).points.length).toBe(4); // polyline unchanged
800
+ expect(core.scene.entities.get(1).type).toBe('Arc');
801
+ });
802
+
803
+ test('Fillet.action radius>0 trimMode=false Line + Lwpolyline: standalone arc added, entities unchanged', () => {
804
+ core.scene.clear();
805
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
806
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
807
+
808
+ core.scene.headers.filletRadius = 2;
809
+ core.scene.headers.trimMode = false;
810
+
811
+ const lineEntity = core.scene.entities.get(0);
812
+ const polyEntity = core.scene.entities.get(1);
813
+ const polyClickPoint = new Point(-15, 0);
814
+
815
+ const fillet = new Fillet();
816
+ fillet.firstPick.entity = lineEntity;
817
+ fillet.firstPick.segment = lineEntity;
818
+ fillet.firstPick.segmentIndex = null;
819
+ fillet.firstPick.clickPoint = new Point(0, 5);
820
+ fillet.secondPick.entity = polyEntity;
821
+ fillet.secondPick.segment = polyEntity.getClosestSegment(polyClickPoint);
822
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(polyClickPoint);
823
+ fillet.secondPick.clickPoint = polyClickPoint;
824
+ fillet.action();
825
+
826
+ // Arc added as standalone entity; original line and polyline untouched
827
+ expect(core.scene.entities.count()).toBe(3);
828
+ expect(core.scene.entities.get(2).type).toBe('Arc');
829
+ expect(core.scene.entities.get(0).points.length).toBe(2);
830
+ expect(core.scene.entities.get(1).points.length).toBe(4);
831
+ });