@design-core/core 0.0.6 → 50.1.0

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 (173) hide show
  1. package/.eslintrc.json +4 -0
  2. package/.github/workflows/DesignCoreQA.yml +3 -0
  3. package/TODO.md +49 -6
  4. package/core/core/core.js +0 -3
  5. package/core/designCore.js +1 -1
  6. package/core/dimensions/alignedDimension.js +12 -11
  7. package/core/dimensions/angularDimension.js +38 -37
  8. package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +22 -21
  9. package/core/dimensions/diametricDimension.js +14 -13
  10. package/core/dimensions/dimension.js +62 -55
  11. package/core/dimensions/{baseDimension.js → dimensionBase.js} +73 -107
  12. package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +15 -14
  13. package/core/dimensions/radialDimension.js +12 -11
  14. package/core/dimensions/rotatedDimension.js +20 -19
  15. package/core/entities/arc.js +123 -156
  16. package/core/entities/arctext.js +222 -184
  17. package/core/entities/circle.js +77 -99
  18. package/core/entities/entity.js +114 -93
  19. package/core/entities/hatch.js +346 -273
  20. package/core/entities/insert.js +68 -83
  21. package/core/entities/line.js +21 -142
  22. package/core/entities/lwpolyline.js +16 -11
  23. package/core/entities/point.js +19 -19
  24. package/core/entities/polyline.js +7 -7
  25. package/core/entities/{basePolyline.js → polylineBase.js} +47 -299
  26. package/core/entities/rectangle.js +3 -1
  27. package/core/entities/solid.js +6 -15
  28. package/core/entities/text.js +190 -201
  29. package/core/lib/auxiliary/rubberBand.js +32 -0
  30. package/core/lib/auxiliary/selectionWindow.js +63 -0
  31. package/core/lib/auxiliary/snapPoint.js +113 -0
  32. package/core/lib/auxiliary/trackingLine.js +76 -0
  33. package/core/lib/boundingBox.js +11 -11
  34. package/core/lib/canvas.js +232 -168
  35. package/core/lib/clipboard.js +9 -3
  36. package/core/lib/colour.js +16 -0
  37. package/core/lib/commandLine.js +1 -6
  38. package/core/lib/commandManager.js +18 -8
  39. package/core/lib/constants.js +28 -0
  40. package/core/lib/dxf/dxf.js +15 -14
  41. package/core/lib/dxf/dxfIterator.js +2 -2
  42. package/core/lib/entityManager.js +33 -26
  43. package/core/lib/inputManager.js +36 -12
  44. package/core/lib/intersect.js +25 -0
  45. package/core/lib/matrix.js +1 -1
  46. package/core/lib/patterns.js +9 -2
  47. package/core/lib/plotOptions.js +67 -0
  48. package/core/lib/polylineUtils.js +75 -0
  49. package/core/lib/renderers/cairoRenderer.js +371 -0
  50. package/core/lib/renderers/canvasRenderer.js +315 -0
  51. package/core/lib/renderers/pdfRenderer.js +366 -0
  52. package/core/lib/renderers/rendererBase.js +284 -0
  53. package/core/lib/renderers/svgRenderer.js +343 -0
  54. package/core/lib/scene.js +28 -6
  55. package/core/lib/selectionManager.js +20 -20
  56. package/core/lib/settings.js +4 -2
  57. package/core/lib/snapping.js +4 -191
  58. package/core/lib/stateManager.js +30 -1
  59. package/core/lib/utils.js +26 -7
  60. package/core/properties/entityProperties.js +104 -0
  61. package/core/properties/property.js +106 -2
  62. package/core/properties/propertyManager.js +60 -44
  63. package/core/tables/block.js +50 -81
  64. package/core/tables/dimStyle.js +5 -5
  65. package/core/tables/endblk.js +1 -1
  66. package/core/tables/layer.js +8 -0
  67. package/core/tables/ltypeManager.js +1 -1
  68. package/core/tables/styleManagerBase.js +3 -2
  69. package/core/tables/tableManagerBase.js +8 -3
  70. package/core/tables/vport.js +2 -2
  71. package/core/tools/chamfer.js +117 -143
  72. package/core/tools/chamferFilletBase.js +138 -37
  73. package/core/tools/copy.js +11 -11
  74. package/core/tools/copybase.js +3 -3
  75. package/core/tools/copyclip.js +3 -3
  76. package/core/tools/cornerEntity.js +2 -2
  77. package/core/tools/cutclip.js +3 -3
  78. package/core/tools/explode.js +7 -7
  79. package/core/tools/extend.js +138 -25
  80. package/core/tools/fillet.js +92 -189
  81. package/core/tools/matchProp.js +3 -12
  82. package/core/tools/mirror.js +15 -14
  83. package/core/tools/move.js +8 -8
  84. package/core/tools/offset.js +31 -33
  85. package/core/tools/pasteclip.js +1 -1
  86. package/core/tools/purge.js +10 -10
  87. package/core/tools/rotate.js +19 -16
  88. package/core/tools/scale.js +7 -7
  89. package/core/tools/trim.js +271 -29
  90. package/core/tools/zoom.js +1 -1
  91. package/cspell.json +321 -0
  92. package/jest.config.js +6 -0
  93. package/package.json +5 -3
  94. package/test/dimensions/alignedDimension.test.js +17 -10
  95. package/test/dimensions/angularDimension.test.js +13 -13
  96. package/test/dimensions/diametricDimension.test.js +15 -15
  97. package/test/dimensions/dimension.test.js +12 -12
  98. package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +73 -12
  99. package/test/dimensions/linearDimensionBase.test.js +62 -0
  100. package/test/dimensions/radialDimension.test.js +15 -15
  101. package/test/dimensions/rotatedDimension.test.js +18 -11
  102. package/test/entities/arc.test.js +83 -86
  103. package/test/entities/arctext.test.js +184 -57
  104. package/test/entities/circle.test.js +68 -96
  105. package/test/entities/entity.test.js +42 -8
  106. package/test/entities/hatch.test.js +569 -96
  107. package/test/entities/insert.test.js +99 -4
  108. package/test/entities/line.test.js +15 -41
  109. package/test/entities/lwpolyline.test.js +104 -0
  110. package/test/entities/point.test.js +3 -3
  111. package/test/entities/polyline.test.js +11 -0
  112. package/test/entities/{basePolyline.test.js → polylineBase.test.js} +192 -57
  113. package/test/entities/rectangle.test.js +11 -0
  114. package/test/entities/solid.test.js +1 -1
  115. package/test/entities/text.test.js +302 -77
  116. package/test/integration/exportIntegration.reference.dxf +9 -9
  117. package/test/integration/exportIntegration.test.js +5 -5
  118. package/test/integration/intersectionIntegration.test.js +4 -5
  119. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  120. package/test/integration/pdfRendererIntegration.test.js +61 -0
  121. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  122. package/test/integration/svgRendererIntegration.test.js +61 -0
  123. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  124. package/test/integration/userInputIntegration.test.js +239 -132
  125. package/test/lib/boundingBox.test.js +3 -3
  126. package/test/lib/canvas.test.js +744 -125
  127. package/test/lib/constants.test.js +27 -0
  128. package/test/lib/entityManager.test.js +4 -4
  129. package/test/lib/inputManager.test.js +81 -20
  130. package/test/lib/intersect.test.js +83 -0
  131. package/test/lib/plotOptions.test.js +179 -0
  132. package/test/lib/rubberBand.test.js +84 -0
  133. package/test/lib/scene.test.js +9 -9
  134. package/test/lib/selectionManager.test.js +3 -3
  135. package/test/lib/settings.test.js +5 -6
  136. package/test/lib/snapping.test.js +72 -130
  137. package/test/lib/stateManager.test.js +7 -7
  138. package/test/lib/utils.test.js +36 -2
  139. package/test/properties/entityProperties.test.js +132 -0
  140. package/test/properties/property.test.js +53 -0
  141. package/test/properties/propertyManager.test.js +25 -25
  142. package/test/renderers/canvasRenderer.test.js +317 -0
  143. package/test/renderers/pdfRenderer.test.js +271 -0
  144. package/test/renderers/rendererBase.test.js +213 -0
  145. package/test/renderers/svgRenderer.test.js +326 -0
  146. package/test/tables/block.test.js +92 -13
  147. package/test/tables/endblk.test.js +2 -2
  148. package/test/tables/layer.test.js +30 -0
  149. package/test/tables/layerManager.test.js +2 -2
  150. package/test/test-helpers/hatchGridRenderer.js +180 -0
  151. package/test/test-helpers/test-helpers.js +158 -6
  152. package/test/tools/chamfer.test.js +273 -51
  153. package/test/tools/chamferFilletBase.test.js +125 -36
  154. package/test/tools/copy.test.js +8 -8
  155. package/test/tools/copybase.test.js +2 -2
  156. package/test/tools/copyclip.test.js +2 -2
  157. package/test/tools/cornerEntity.test.js +8 -8
  158. package/test/tools/cutclip.test.js +2 -2
  159. package/test/tools/erase.test.js +6 -6
  160. package/test/tools/explode.test.js +5 -5
  161. package/test/tools/extend.test.js +373 -42
  162. package/test/tools/fillet.test.js +230 -49
  163. package/test/tools/matchProp.test.js +11 -11
  164. package/test/tools/mirror.test.js +39 -39
  165. package/test/tools/move.test.js +7 -7
  166. package/test/tools/offset.test.js +222 -55
  167. package/test/tools/pasteclip.test.js +5 -5
  168. package/test/tools/purge.test.js +4 -4
  169. package/test/tools/rotate.test.js +39 -9
  170. package/test/tools/scale.test.js +33 -33
  171. package/test/tools/trim.test.js +579 -53
  172. package/test/tools/zoom.test.js +12 -12
  173. package/core/lib/selectionWindow.js +0 -83
@@ -1,6 +1,7 @@
1
1
  import { Core } from '../../core/core/core.js';
2
2
  import { Point } from '../../core/entities/point.js';
3
3
  import { Chamfer } from '../../core/tools/chamfer.js';
4
+ import { DesignCore } from '../../core/designCore.js';
4
5
  import { Strings } from '../../core/lib/strings.js';
5
6
  import { SingleSelection } from '../../core/lib/selectionManager.js';
6
7
  import { expect, jest } from '@jest/globals';
@@ -20,7 +21,7 @@ test('Chamfer.register returns correct command object', () => {
20
21
 
21
22
  test('Chamfer.action does nothing when entities are not set', () => {
22
23
  core.scene.clear();
23
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
24
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
24
25
  const countBefore = core.scene.entities.count();
25
26
 
26
27
  const chamfer = new Chamfer();
@@ -31,8 +32,8 @@ test('Chamfer.action does nothing when entities are not set', () => {
31
32
 
32
33
  test('Chamfer.action notifies when first entity type is not supported', () => {
33
34
  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)] });
35
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
36
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
36
37
 
37
38
  const notifySpy = jest.spyOn(core, 'notify');
38
39
 
@@ -49,8 +50,8 @@ test('Chamfer.action notifies when first entity type is not supported', () => {
49
50
 
50
51
  test('Chamfer.action notifies when second entity type is not supported', () => {
51
52
  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)] });
53
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
54
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
54
55
 
55
56
  const notifySpy = jest.spyOn(core, 'notify');
56
57
 
@@ -67,8 +68,8 @@ test('Chamfer.action notifies when second entity type is not supported', () => {
67
68
 
68
69
  test('Chamfer.action notifies for parallel lines', () => {
69
70
  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)] });
71
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
72
+ core.scene.addEntity('Line', { points: [new Point(0, 5), new Point(10, 5)] });
72
73
 
73
74
  const notifySpy = jest.spyOn(core, 'notify');
74
75
 
@@ -89,8 +90,8 @@ test('Chamfer.action dist=0 trimMode=true trims both lines to intersection', ()
89
90
  // Lines only touching when extended: horizontal (0,0)→(8,0), vertical (10,-10)→(10,-2)
90
91
  // Infinite extensions meet at (10, 0).
91
92
  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)] });
93
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(8, 0)] });
94
+ core.scene.addEntity('Line', { points: [new Point(10, -10), new Point(10, -2)] });
94
95
 
95
96
  core.scene.headers.chamferDistanceA = 0;
96
97
  core.scene.headers.chamferDistanceB = 0;
@@ -121,8 +122,8 @@ test('Chamfer.action dist=0 trimMode=true trims both lines to intersection', ()
121
122
 
122
123
  test('Chamfer.action dist=0 trimMode=false makes no changes', () => {
123
124
  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)] });
125
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(8, 0)] });
126
+ core.scene.addEntity('Line', { points: [new Point(10, -10), new Point(10, -2)] });
126
127
 
127
128
  core.scene.headers.chamferDistanceA = 0;
128
129
  core.scene.headers.chamferDistanceB = 0;
@@ -145,8 +146,8 @@ test('Chamfer.action distance method trimMode=true adds chamfer line and trims',
145
146
  // Perpendicular L-corner at origin: horizontal (-10,0)→(0,0), vertical (0,0)→(0,10)
146
147
  // Equal distances of 2 → chamfer endpoints at (-2,0) and (0,2)
147
148
  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)] });
149
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
150
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
150
151
 
151
152
  core.scene.headers.chamferDistanceA = 2;
152
153
  core.scene.headers.chamferDistanceB = 2;
@@ -190,8 +191,8 @@ test('Chamfer.action distance method trimMode=true adds chamfer line and trims',
190
191
  test('Chamfer.action distance method asymmetric distances', () => {
191
192
  // L-corner at origin, distA=3 along horizontal, distB=1 along vertical
192
193
  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)] });
194
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
195
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
195
196
 
196
197
  core.scene.headers.chamferDistanceA = 3;
197
198
  core.scene.headers.chamferDistanceB = 1;
@@ -215,8 +216,8 @@ test('Chamfer.action distance method asymmetric distances', () => {
215
216
 
216
217
  test('Chamfer.action distance method trimMode=false adds chamfer line only', () => {
217
218
  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)] });
219
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
220
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
220
221
 
221
222
  core.scene.headers.chamferDistanceA = 2;
222
223
  core.scene.headers.chamferDistanceB = 2;
@@ -244,8 +245,8 @@ test('Chamfer.action distance method trimMode=false adds chamfer line only', ()
244
245
  test('Chamfer.action notifies when distance is too large for the segments', () => {
245
246
  // Lines only 5 units long from intersection — distance 10 won't fit
246
247
  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)] });
248
+ core.scene.addEntity('Line', { points: [new Point(-5, 0), new Point(0, 0)] });
249
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 5)] });
249
250
 
250
251
  core.scene.headers.chamferDistanceA = 10;
251
252
  core.scene.headers.chamferDistanceB = 10;
@@ -272,8 +273,8 @@ test('Chamfer.action angle method 45° on perpendicular lines matches distance m
272
273
  // With perpendicular lines and 45°, the chamfer endpoint on line2 should also be 2 units
273
274
  // from the intersection — matching a symmetric distance chamfer.
274
275
  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)] });
276
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
277
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
277
278
 
278
279
  core.scene.headers.chamferLength = 2;
279
280
  core.scene.headers.chamferAngle = 45; // degrees
@@ -306,8 +307,8 @@ test('Chamfer.action notifies when click coincides with intersection', () => {
306
307
  // L-corner at origin. Clicking firstClickPoint exactly at (0,0) makes
307
308
  // firstClickDistance = 0, triggering the "Selected corner cannot be chamfered" guard.
308
309
  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)] });
310
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
311
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
311
312
 
312
313
  core.scene.headers.chamferDistanceA = 2;
313
314
  core.scene.headers.chamferDistanceB = 2;
@@ -332,8 +333,8 @@ test('Chamfer.action notifies when click coincides with intersection', () => {
332
333
 
333
334
  test('Chamfer.action angle method notifies INVALIDNUMBER when angle is zero', () => {
334
335
  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)] });
336
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
337
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
337
338
 
338
339
  core.scene.headers.chamferLength = 2;
339
340
  core.scene.headers.chamferAngle = 0; // alpha = 0 * π/180 = 0, fails alpha <= 0 guard
@@ -361,8 +362,8 @@ test('Chamfer.action angle method notifies PARALLELLINES when chamfer direction
361
362
  // candidate1 = rotate(-1,0) by +π/2 = (0,-1); dot with secondClickUnit (0,1) = -1 < 0.
362
363
  // candidate2 = rotate(-1,0) by -π/2 = (0, 1) → parallel to secondLineDirection (0,1) → cross = 0.
363
364
  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)] });
365
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
366
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
366
367
 
367
368
  core.scene.headers.chamferLength = 2;
368
369
  core.scene.headers.chamferAngle = 90; // degrees
@@ -396,8 +397,8 @@ test('Chamfer.execute returns early when first input is undefined', async () =>
396
397
 
397
398
  test('Chamfer.execute selects two lines and calls executeCommand', async () => {
398
399
  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)] });
400
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
401
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
401
402
 
402
403
  const executeCommandSpy = jest.fn();
403
404
 
@@ -543,7 +544,7 @@ test('Chamfer.execute sets chamferMode to true via Method=Angle option', async (
543
544
 
544
545
  test('Chamfer.execute re-prompts and notifies when first selected entity is not a Line', async () => {
545
546
  core.scene.clear();
546
- core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
547
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
547
548
 
548
549
  const notifySpy = jest.spyOn(core, 'notify');
549
550
 
@@ -562,8 +563,8 @@ test('Chamfer.execute re-prompts and notifies when first selected entity is not
562
563
 
563
564
  test('Chamfer.execute re-prompts and notifies when second selected entity is not a Line', async () => {
564
565
  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)] });
566
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
567
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
567
568
 
568
569
  const notifySpy = jest.spyOn(core, 'notify');
569
570
 
@@ -593,8 +594,8 @@ test('Chamfer.execute re-prompts and notifies when second selected entity is not
593
594
  test('Chamfer.action dist>0 trimMode=true Line first + Lwpolyline second keepStart: chamfer embedded in polyline', () => {
594
595
  // Click polyline near (-15,0) → segment 1. keepStart keeps the start portion.
595
596
  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)] });
597
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
598
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
598
599
 
599
600
  core.scene.headers.chamferDistanceA = 2;
600
601
  core.scene.headers.chamferDistanceB = 2;
@@ -634,8 +635,8 @@ test('Chamfer.action dist>0 trimMode=true Line first + Lwpolyline second keepSta
634
635
 
635
636
  test('Chamfer.action dist>0 trimMode=true Lwpolyline first + Line second keepStart: same result with entities reversed', () => {
636
637
  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)] });
638
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
639
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
639
640
 
640
641
  core.scene.headers.chamferDistanceA = 2;
641
642
  core.scene.headers.chamferDistanceB = 2;
@@ -675,8 +676,8 @@ test('Chamfer.action dist>0 trimMode=true Line + Lwpolyline keepEnd: keeps end p
675
676
  // Click polyline near (8,0) → segment 3 (right of intersection). keepEnd.
676
677
  // polyChamfer = (2,0), lineChamfer = (0,2).
677
678
  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)] });
679
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
680
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
680
681
 
681
682
  core.scene.headers.chamferDistanceA = 2;
682
683
  core.scene.headers.chamferDistanceB = 2;
@@ -719,7 +720,7 @@ test('Chamfer.action dist>0 trimMode=true same Lwpolyline consecutive segments:
719
720
  // Segment 1: (-10,0)→(0,0), Segment 2: (0,0)→(0,10). Corner at (0,0).
720
721
  // dist=2 → chamfer at (-2,0) and (0,2).
721
722
  core.scene.clear();
722
- core.scene.addItem('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
723
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
723
724
 
724
725
  core.scene.headers.chamferDistanceA = 2;
725
726
  core.scene.headers.chamferDistanceB = 2;
@@ -762,7 +763,7 @@ test('Chamfer.action dist>0 trimMode=true same Lwpolyline open ends: separate ch
762
763
  // dist=2, clicks at (-3,0) and (1,3):
763
764
  // firstChamfer = (-1,0), secondChamfer = (1,2).
764
765
  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
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-5, 0), new Point(0, 0), new Point(1, 0), new Point(1, 5)] });
766
767
 
767
768
  core.scene.headers.chamferDistanceA = 2;
768
769
  core.scene.headers.chamferDistanceB = 2;
@@ -808,7 +809,7 @@ test('Chamfer.action dist>0 trimMode=true closed Lwpolyline seg3+closing-seg: co
808
809
  // Closing segment (idx=4) goes (0,10)→(0,0); seg 3 goes (10,10)→(0,10).
809
810
  // Corner at (0,10) — chamfer dist=2 trims to (2,10) on seg3 and (0,8) on closing seg.
810
811
  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
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
812
813
  const polyEntity = core.scene.entities.get(0);
813
814
  polyEntity.flags.setFlagValue(1);
814
815
 
@@ -846,7 +847,7 @@ test('Chamfer.action dist>0 trimMode=true closed Lwpolyline closing-wrap (seg4+s
846
847
  // Bug: old code had no closed-poly guard, so closing-wrap was treated as open-ends.
847
848
  // Fix: closed poly must NOT enter the open-ends path → corner-splice at index 0.
848
849
  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
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
850
851
  const polyEntity = core.scene.entities.get(0);
851
852
  polyEntity.flags.setFlagValue(1);
852
853
 
@@ -872,17 +873,26 @@ test('Chamfer.action dist>0 trimMode=true closed Lwpolyline closing-wrap (seg4+s
872
873
  const poly = core.scene.entities.get(0);
873
874
  // Shared vertex (0,0) at index 0 replaced by two chamfer points → total 5 points
874
875
  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);
876
+ // Verify the actual chamfer point coordinates and traversal order.
877
+ // The closing segment (0,10)→(0,0) is chamfered at (0,2),
878
+ // seg 1 (0,0)→(10,0) is chamfered at (2,0).
879
+ // After splice: [(0,2), (2,0), (10,0), (10,10), (0,10)]
880
+ expect(poly.points[0].x).toBeCloseTo(0);
881
+ expect(poly.points[0].y).toBeCloseTo(2);
882
+ expect(poly.points[1].x).toBeCloseTo(2);
883
+ expect(poly.points[1].y).toBeCloseTo(0);
884
+ expect(poly.points[2].x).toBeCloseTo(10);
885
+ expect(poly.points[2].y).toBeCloseTo(0);
886
+ expect(poly.points[3].x).toBeCloseTo(10);
887
+ expect(poly.points[3].y).toBeCloseTo(10);
888
+ expect(poly.points[4].x).toBeCloseTo(0);
889
+ expect(poly.points[4].y).toBeCloseTo(10);
880
890
  });
881
891
 
882
892
  test('Chamfer.action dist>0 trimMode=false closed Lwpolyline closing-wrap: standalone chamfer Line only', () => {
883
893
  // Same square, closed, trimMode=false. No polyline mutation; only chamfer Line added.
884
894
  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)] });
895
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
886
896
  const polyEntity = core.scene.entities.get(0);
887
897
  polyEntity.flags.setFlagValue(1);
888
898
 
@@ -912,7 +922,7 @@ test('Chamfer.action dist>0 trimMode=true open Lwpolyline seg1+lastIdx still tak
912
922
  // Open 4-point polyline (no flags). Seg 1 and seg 3 (lastIdx=3) → open-ends path still fires.
913
923
  // Regression guard: the closed-poly fix must not break the open-poly open-ends case.
914
924
  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)] });
925
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-5, 0), new Point(0, 0), new Point(1, 0), new Point(1, 5)] });
916
926
  const polyEntity = core.scene.entities.get(0);
917
927
  // No flags set — open polyline
918
928
 
@@ -939,8 +949,8 @@ test('Chamfer.action dist>0 trimMode=true open Lwpolyline seg1+lastIdx still tak
939
949
 
940
950
  test('Chamfer.action dist>0 trimMode=false Line + Lwpolyline: standalone chamfer Line added, entities unchanged', () => {
941
951
  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)] });
952
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
953
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
944
954
 
945
955
  core.scene.headers.chamferDistanceA = 2;
946
956
  core.scene.headers.chamferDistanceB = 2;
@@ -968,3 +978,215 @@ test('Chamfer.action dist>0 trimMode=false Line + Lwpolyline: standalone chamfer
968
978
  expect(core.scene.entities.get(0).points.length).toBe(2);
969
979
  expect(core.scene.entities.get(1).points.length).toBe(4);
970
980
  });
981
+
982
+ // ─── action: entity property inheritance ─────────────────────────────────────
983
+
984
+ test('Chamfer.action chamfer line inherits layer, lineWidth, lineType from first entity', () => {
985
+ // Perpendicular L-corner at origin; first entity on a non-default layer.
986
+ core.scene.clear();
987
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)], layer: 'walls', lineWidth: 5, lineType: 'DASHED' });
988
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
989
+
990
+ core.scene.headers.chamferDistanceA = 2;
991
+ core.scene.headers.chamferDistanceB = 2;
992
+ core.scene.headers.chamferMode = false;
993
+ core.scene.headers.trimMode = false;
994
+
995
+ const chamfer = new Chamfer();
996
+ chamfer.firstPick.entity = core.scene.entities.get(0);
997
+ chamfer.secondPick.entity = core.scene.entities.get(1);
998
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
999
+ chamfer.secondPick.clickPoint = new Point(0, 5);
1000
+ chamfer.action();
1001
+
1002
+ expect(core.scene.entities.count()).toBe(3);
1003
+ const chamferLine = core.scene.entities.get(2);
1004
+ expect(chamferLine.type).toBe('Line');
1005
+ expect(chamferLine.getProperty('layer')).toBe('walls');
1006
+ expect(chamferLine.getProperty('lineWidth')).toBe(5);
1007
+ expect(chamferLine.getProperty('lineType')).toBe('DASHED');
1008
+ });
1009
+
1010
+ test('Chamfer.preview does not throw', () => {
1011
+ const chamfer = new Chamfer();
1012
+ expect(() => chamfer.preview()).not.toThrow();
1013
+ });
1014
+
1015
+ test('Chamfer.preview returns early when findClosestItem returns undefined', () => {
1016
+ core.scene.clear();
1017
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1018
+ DesignCore.Scene.previewEntities.clear();
1019
+
1020
+ const chamfer = new Chamfer();
1021
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1022
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1023
+
1024
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
1025
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1026
+ chamfer.preview();
1027
+ findSpy.mockRestore();
1028
+
1029
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
1030
+ });
1031
+
1032
+ test('Chamfer.preview adds chamfer line to previewEntities in no-trim mode', () => {
1033
+ core.scene.clear();
1034
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1035
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1036
+ DesignCore.Scene.previewEntities.clear();
1037
+
1038
+ core.scene.headers.chamferDistanceA = 2;
1039
+ core.scene.headers.chamferDistanceB = 2;
1040
+ core.scene.headers.chamferMode = false;
1041
+ core.scene.headers.trimMode = false;
1042
+
1043
+ const chamfer = new Chamfer();
1044
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1045
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1046
+
1047
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1048
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1049
+ chamfer.preview();
1050
+ findSpy.mockRestore();
1051
+
1052
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
1053
+ });
1054
+
1055
+ test('Chamfer.preview skips chamfer line in no-trim mode when distances are both 0 (sharp corner)', () => {
1056
+ core.scene.clear();
1057
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1058
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1059
+ DesignCore.Scene.previewEntities.clear();
1060
+
1061
+ core.scene.headers.chamferDistanceA = 0;
1062
+ core.scene.headers.chamferDistanceB = 0;
1063
+ core.scene.headers.chamferMode = false;
1064
+ core.scene.headers.trimMode = false;
1065
+
1066
+ const chamfer = new Chamfer();
1067
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1068
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1069
+
1070
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1071
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1072
+ chamfer.preview();
1073
+ findSpy.mockRestore();
1074
+
1075
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
1076
+ });
1077
+
1078
+ test('Chamfer.preview adds 5 entities to previewEntities in trim mode', () => {
1079
+ core.scene.clear();
1080
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1081
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1082
+ DesignCore.Scene.previewEntities.clear();
1083
+
1084
+ core.scene.headers.chamferDistanceA = 2;
1085
+ core.scene.headers.chamferDistanceB = 2;
1086
+ core.scene.headers.chamferMode = false;
1087
+ core.scene.headers.trimMode = true;
1088
+
1089
+ const chamfer = new Chamfer();
1090
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1091
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1092
+
1093
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1094
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1095
+ chamfer.preview();
1096
+ findSpy.mockRestore();
1097
+
1098
+ // dulled firstSeg + dulled secondSeg + firstTrimLine + secondTrimLine + chamferLine = 5
1099
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(5);
1100
+ });
1101
+
1102
+ test('Chamfer.execute returns early when second-entity input is undefined (inner loop)', async () => {
1103
+ core.scene.clear();
1104
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1105
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1106
+
1107
+ const executeCommandSpy = jest.fn();
1108
+ await withMockInput(
1109
+ core.scene,
1110
+ [new SingleSelection(0, new Point(-5, 0))],
1111
+ async () => {
1112
+ const chamfer = new Chamfer();
1113
+ await chamfer.execute();
1114
+ expect(executeCommandSpy).not.toHaveBeenCalled();
1115
+ },
1116
+ { extraMethods: { executeCommand: executeCommandSpy } },
1117
+ );
1118
+ });
1119
+
1120
+ test('Chamfer.execute notifies NONCONSECUTIVESEGMENTS for non-adjacent polyline segments', async () => {
1121
+ // 5-point polyline → 4 segments. Pick segment 1 (near -15,0) then segment 3 (near 5,0).
1122
+ // Neither consecutive nor open-ends → NONCONSECUTIVESEGMENTS, then undefined to exit.
1123
+ core.scene.clear();
1124
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0), new Point(20, 0)] });
1125
+
1126
+ const notifySpy = jest.spyOn(core, 'notify');
1127
+ await withMockInput(
1128
+ core.scene,
1129
+ [new SingleSelection(0, new Point(-15, 0)), new SingleSelection(0, new Point(5, 0)), undefined],
1130
+ async () => {
1131
+ const chamfer = new Chamfer();
1132
+ await chamfer.execute();
1133
+ },
1134
+ );
1135
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NONCONSECUTIVESEGMENTS));
1136
+ notifySpy.mockRestore();
1137
+ });
1138
+
1139
+ test('Chamfer.execute Method→Distance sets chamferMode to false', async () => {
1140
+ core.scene.headers.chamferMode = true;
1141
+
1142
+ await withMockInput(
1143
+ core.scene,
1144
+ ['Method', 'Distance', undefined],
1145
+ async () => {
1146
+ const chamfer = new Chamfer();
1147
+ await chamfer.execute();
1148
+ },
1149
+ );
1150
+
1151
+ expect(core.scene.headers.chamferMode).toBe(false);
1152
+ });
1153
+
1154
+ test('Chamfer.execute Trim→Trim keeps trimMode=true', async () => {
1155
+ core.scene.headers.trimMode = true;
1156
+
1157
+ await withMockInput(
1158
+ core.scene,
1159
+ ['Trim', 'Trim', undefined],
1160
+ async () => {
1161
+ const chamfer = new Chamfer();
1162
+ await chamfer.execute();
1163
+ },
1164
+ );
1165
+
1166
+ expect(core.scene.headers.trimMode).toBe(true);
1167
+ });
1168
+
1169
+ test('Chamfer.action angle method angle=180 notifies INVALIDNUMBER', () => {
1170
+ // alpha = 180 * π/180 = π; checks alpha >= Math.PI → INVALIDNUMBER
1171
+ core.scene.clear();
1172
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1173
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1174
+
1175
+ core.scene.headers.chamferLength = 2;
1176
+ core.scene.headers.chamferAngle = 180;
1177
+ core.scene.headers.chamferMode = true;
1178
+ core.scene.headers.trimMode = true;
1179
+
1180
+ const notifySpy = jest.spyOn(core, 'notify');
1181
+
1182
+ const chamfer = new Chamfer();
1183
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1184
+ chamfer.secondPick.entity = core.scene.entities.get(1);
1185
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1186
+ chamfer.secondPick.clickPoint = new Point(0, 5);
1187
+ chamfer.action();
1188
+
1189
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
1190
+ expect(core.scene.entities.count()).toBe(2);
1191
+ notifySpy.mockRestore();
1192
+ });