@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 { Fillet } from '../../core/tools/fillet.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';
@@ -16,8 +17,8 @@ test('Fillet.register returns correct command object', () => {
16
17
 
17
18
  test('Fillet.execute selects two entities and calls executeCommand', async () => {
18
19
  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)] });
20
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
21
+ core.scene.addEntity('Line', { points: [new Point(5, -5), new Point(5, 5)] });
21
22
 
22
23
  const executeCommandSpy = jest.fn();
23
24
 
@@ -44,7 +45,7 @@ test('Fillet.execute returns early when first input is undefined', async () => {
44
45
 
45
46
  test('Fillet.action does nothing when entities are not set', () => {
46
47
  core.scene.clear();
47
- core.scene.addItem('Line', { points: [new Point(0, 0), new Point(10, 0)] });
48
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
48
49
  const entityCountBefore = core.scene.entities.count();
49
50
 
50
51
  const fillet = new Fillet();
@@ -55,8 +56,8 @@ test('Fillet.action does nothing when entities are not set', () => {
55
56
 
56
57
  test('Fillet.action notifies when entity type is not supported', () => {
57
58
  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)] });
59
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
60
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
60
61
 
61
62
  const notifySpy = jest.spyOn(core, 'notify');
62
63
 
@@ -74,8 +75,8 @@ test('Fillet.action notifies when entity type is not supported', () => {
74
75
  test('Fillet.action notifies for parallel lines', () => {
75
76
  core.scene.clear();
76
77
  // 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)] });
78
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
79
+ core.scene.addEntity('Line', { points: [new Point(0, 5), new Point(10, 5)] });
79
80
 
80
81
  const notifySpy = jest.spyOn(core, 'notify');
81
82
 
@@ -97,8 +98,8 @@ test('Fillet.action radius=0 trimMode=true trims both lines to intersection', ()
97
98
  // Horizontal: (0,0)→(8,0), Vertical: (10,-10)→(10,-2)
98
99
  // Their infinite extensions meet at (10, 0).
99
100
  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)] });
101
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(8, 0)] });
102
+ core.scene.addEntity('Line', { points: [new Point(10, -10), new Point(10, -2)] });
102
103
 
103
104
  core.scene.headers.filletRadius = 0;
104
105
  core.scene.headers.trimMode = true;
@@ -128,8 +129,8 @@ test('Fillet.action radius=0 trimMode=true trims both lines to intersection', ()
128
129
 
129
130
  test('Fillet.action radius=0 trimMode=false makes no changes', () => {
130
131
  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)] });
132
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(8, 0)] });
133
+ core.scene.addEntity('Line', { points: [new Point(10, -10), new Point(10, -2)] });
133
134
 
134
135
  core.scene.headers.filletRadius = 0;
135
136
  core.scene.headers.trimMode = false;
@@ -152,8 +153,8 @@ test('Fillet.action radius>0 trimMode=true adds arc and trims lines', () => {
152
153
  // Horizontal: (-10,0)→(0,0) Vertical: (0,0)→(0,10)
153
154
  // With radius 2, tangent points are at (-2,0) and (0,2)
154
155
  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)] });
156
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
157
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
157
158
 
158
159
  core.scene.headers.filletRadius = 2;
159
160
  core.scene.headers.trimMode = true;
@@ -189,8 +190,8 @@ test('Fillet.action radius>0 trimMode=true adds arc and trims lines', () => {
189
190
 
190
191
  test('Fillet.action radius>0 trimMode=false adds arc only, lines unchanged', () => {
191
192
  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)] });
193
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
194
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
194
195
 
195
196
  core.scene.headers.filletRadius = 2;
196
197
  core.scene.headers.trimMode = false;
@@ -216,8 +217,8 @@ test('Fillet.action radius>0 trimMode=false adds arc only, lines unchanged', ()
216
217
  test('Fillet.action notifies when radius is too large for the segments', () => {
217
218
  // Lines only 5 units long from intersection — radius 10 won't fit
218
219
  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)] });
220
+ core.scene.addEntity('Line', { points: [new Point(-5, 0), new Point(0, 0)] });
221
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 5)] });
221
222
 
222
223
  core.scene.headers.filletRadius = 10;
223
224
  core.scene.headers.trimMode = true;
@@ -240,8 +241,8 @@ test('Fillet.action notifies when radius is too large for the segments', () => {
240
241
  test('Fillet.action arc centre is at correct position for 90° corner', () => {
241
242
  // 90° corner at origin, radius 3 → arc centre at (-3, 3)
242
243
  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)] });
244
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
245
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
245
246
 
246
247
  core.scene.headers.filletRadius = 3;
247
248
  core.scene.headers.trimMode = false;
@@ -271,8 +272,8 @@ test('Fillet.action selects correct corner when lines form a cross', () => {
271
272
  // Full cross: horizontal (-10,0)→(10,0), vertical (0,-10)→(0,10)
272
273
  // Click on the top-right quadrant → fillet should be in that quadrant
273
274
  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)] });
275
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
276
+ core.scene.addEntity('Line', { points: [new Point(0, -10), new Point(0, 10)] });
276
277
 
277
278
  core.scene.headers.filletRadius = 2;
278
279
  core.scene.headers.trimMode = false;
@@ -296,8 +297,8 @@ test('Fillet.action selects correct corner when lines form a cross', () => {
296
297
 
297
298
  test('Fillet.action notifies when second entity type is not supported', () => {
298
299
  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)] });
300
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(10, 0)] });
301
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
301
302
 
302
303
  const notifySpy = jest.spyOn(core, 'notify');
303
304
 
@@ -318,8 +319,8 @@ test('Fillet.action notifies when click coincides with intersection', () => {
318
319
  // L-corner at origin. Clicking firstClickPoint exactly at (0,0) makes
319
320
  // firstClickDistance = 0, triggering the invalid-selection guard.
320
321
  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)] });
322
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
323
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
323
324
 
324
325
  core.scene.headers.filletRadius = 2;
325
326
  core.scene.headers.trimMode = true;
@@ -342,8 +343,8 @@ test('Fillet.action radius=0 notifies and makes no changes when click coincides
342
343
  // Click at exactly the intersection point — clickDistance = 0.
343
344
  // The guard must fire before applySharpTrim() runs.
344
345
  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)] });
346
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
347
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
347
348
 
348
349
  core.scene.headers.filletRadius = 0;
349
350
  core.scene.headers.trimMode = true;
@@ -414,7 +415,7 @@ test('Fillet.execute sets trimMode to false when No trim option is selected', as
414
415
 
415
416
  test('Fillet.execute re-prompts and notifies when first selected entity is not a Line', async () => {
416
417
  core.scene.clear();
417
- core.scene.addItem('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
418
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
418
419
 
419
420
  const notifySpy = jest.spyOn(core, 'notify');
420
421
 
@@ -433,8 +434,8 @@ test('Fillet.execute re-prompts and notifies when first selected entity is not a
433
434
 
434
435
  test('Fillet.execute re-prompts and notifies when second selected entity is not a Line', async () => {
435
436
  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)] });
437
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
438
+ core.scene.addEntity('Circle', { points: [new Point(0, 0), new Point(5, 0)] });
438
439
 
439
440
  const notifySpy = jest.spyOn(core, 'notify');
440
441
 
@@ -465,8 +466,8 @@ test('Fillet.execute re-prompts and notifies when second selected entity is not
465
466
  test('Fillet.action radius>0 trimMode=true Line first + Lwpolyline second keepStart: arc embedded as bulge', () => {
466
467
  // Click polyline near (-15,0) → segment 1. keepStart keeps the start portion.
467
468
  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)] });
469
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
470
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
470
471
 
471
472
  core.scene.headers.filletRadius = 2;
472
473
  core.scene.headers.trimMode = true;
@@ -506,8 +507,8 @@ test('Fillet.action radius>0 trimMode=true Line first + Lwpolyline second keepSt
506
507
  test('Fillet.action radius>0 trimMode=true Lwpolyline first + Line second keepStart: arc direction correct', () => {
507
508
  // Same geometry but entity order reversed — tests the polyToLineDir arc direction fix.
508
509
  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)] });
510
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
511
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
511
512
 
512
513
  core.scene.headers.filletRadius = 2;
513
514
  core.scene.headers.trimMode = true;
@@ -547,8 +548,8 @@ test('Fillet.action radius>0 trimMode=true Line + Lwpolyline keepEnd: keeps end
547
548
  // Click polyline near (8,0) → segment 3 (right of intersection). keepEnd.
548
549
  // arc centre = (2,2), polyTangent = (2,0), lineTangent = (0,2).
549
550
  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)] });
551
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
552
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
552
553
 
553
554
  core.scene.headers.filletRadius = 2;
554
555
  core.scene.headers.trimMode = true;
@@ -588,8 +589,8 @@ test('Fillet.action radius>0 trimMode=true Line + Lwpolyline keepEnd: keeps end
588
589
  test('Fillet.action radius=0 trimMode=true Line + Lwpolyline keepStart: line consumed to sharp corner', () => {
589
590
  // radius=0 → sharp intersection at (0,0), no arc.
590
591
  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)] });
592
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
593
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
593
594
 
594
595
  core.scene.headers.filletRadius = 0;
595
596
  core.scene.headers.trimMode = true;
@@ -628,7 +629,7 @@ test('Fillet.action radius>0 trimMode=true same Lwpolyline consecutive segments:
628
629
  // Segment 1: (-10,0)→(0,0), Segment 2: (0,0)→(0,10). Corner at (0,0).
629
630
  // radius=2 → arc centre (-2,2), tangents at (-2,0) and (0,2).
630
631
  core.scene.clear();
631
- core.scene.addItem('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
632
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-10, 0), new Point(0, 0), new Point(0, 10)] });
632
633
 
633
634
  core.scene.headers.filletRadius = 2;
634
635
  core.scene.headers.trimMode = true;
@@ -670,7 +671,7 @@ test('Fillet.action radius>0 trimMode=true same Lwpolyline open ends: separate A
670
671
  // radius=2, clicks at (-3,0) and (1,3):
671
672
  // arc centre = (-1,2), firstTangent = (-1,0), secondTangent = (1,2).
672
673
  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
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-5, 0), new Point(0, 0), new Point(1, 0), new Point(1, 5)] });
674
675
 
675
676
  core.scene.headers.filletRadius = 2;
676
677
  core.scene.headers.trimMode = true;
@@ -712,7 +713,7 @@ test('Fillet.action radius>0 trimMode=true closed Lwpolyline seg3+closing-seg: c
712
713
  // Closing segment (idx=4) goes (0,10)→(0,0); seg 3 goes (10,10)→(0,10).
713
714
  // Corner at (0,10) — radius=2 replaces the shared vertex with arc bulge points.
714
715
  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
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
716
717
  const polyEntity = core.scene.entities.get(0);
717
718
  polyEntity.flags.setFlagValue(1);
718
719
 
@@ -744,7 +745,7 @@ test('Fillet.action radius>0 trimMode=true closed Lwpolyline closing-wrap (seg4+
744
745
  // Corner at (0,0) shared by closing seg (idx=4) and seg 1.
745
746
  // Bug: without the closed-poly guard, this was treated as open-ends → wrong path.
746
747
  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
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
748
749
  const polyEntity = core.scene.entities.get(0);
749
750
  polyEntity.flags.setFlagValue(1);
750
751
 
@@ -776,7 +777,7 @@ test('Fillet.action radius>0 trimMode=true closed Lwpolyline closing-wrap (seg4+
776
777
  test('Fillet.action radius>0 trimMode=false closed Lwpolyline closing-wrap: standalone Arc only', () => {
777
778
  // trimMode=false: no polyline mutation; only Arc entity added.
778
779
  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
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
780
781
  const polyEntity = core.scene.entities.get(0);
781
782
  polyEntity.flags.setFlagValue(1);
782
783
 
@@ -802,8 +803,8 @@ test('Fillet.action radius>0 trimMode=false closed Lwpolyline closing-wrap: stan
802
803
 
803
804
  test('Fillet.action radius>0 trimMode=false Line + Lwpolyline: standalone arc added, entities unchanged', () => {
804
805
  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)] });
806
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
807
+ core.scene.addEntity('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0)] });
807
808
 
808
809
  core.scene.headers.filletRadius = 2;
809
810
  core.scene.headers.trimMode = false;
@@ -842,14 +843,14 @@ test('Fillet.action radius>0 trimMode=false Line + Lwpolyline: standalone arc ad
842
843
  // – lowerTangent = (0,2) on the closing segment (left edge, y=2)
843
844
  // – upperTangent = (2,0) on segment 1 (bottom edge, x=2)
844
845
  // – bulge on lowerTangent must be POSITIVE (CCW arc from (0,2) to (2,0) through ≈(0.59,0.59))
845
- // (BasePolyline convention: –ve bulge = CW, +ve bulge = CCW)
846
+ // (PolylineBase convention: –ve bulge = CW, +ve bulge = CCW)
846
847
  //
847
848
  // Before the fix the lowerTangent/upperTangent assignment was inverted, producing
848
849
  // an arc that traversed the exterior of the square.
849
850
 
850
851
  test('Fillet.action closing-wrap: lowerTangent is on closing segment (left edge)', () => {
851
852
  core.scene.clear();
852
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
853
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
853
854
  const polyEntity = core.scene.entities.get(0);
854
855
  polyEntity.flags.setFlagValue(1);
855
856
 
@@ -888,7 +889,7 @@ test('Fillet.action closing-wrap: arc bulge sign is positive (CCW, fillet inside
888
889
  // Before the fix the lowerTangent/upperTangent were swapped, producing an arc
889
890
  // that traversed the exterior of the square.
890
891
  core.scene.clear();
891
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
892
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
892
893
  const polyEntity = core.scene.entities.get(0);
893
894
  polyEntity.flags.setFlagValue(1);
894
895
 
@@ -914,7 +915,7 @@ test('Fillet.action closing-wrap: arc bulge sign is positive (CCW, fillet inside
914
915
  test('Fillet.action closing-wrap reversed pick order: arc bulge sign and tangent positions are correct', () => {
915
916
  // Swap firstPick/secondPick — should produce the same geometry.
916
917
  core.scene.clear();
917
- core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
918
+ core.scene.addEntity('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
918
919
  const polyEntity = core.scene.entities.get(0);
919
920
  polyEntity.flags.setFlagValue(1);
920
921
 
@@ -944,3 +945,183 @@ test('Fillet.action closing-wrap reversed pick order: arc bulge sign and tangent
944
945
  expect(poly.points[1].y).toBeCloseTo(0);
945
946
  expect(poly.points[0].bulge).toBeGreaterThan(0);
946
947
  });
948
+
949
+ // ─── action: entity property inheritance ─────────────────────────────────────
950
+
951
+ test('Fillet.action arc inherits layer, lineWidth, lineType from first entity', () => {
952
+ // Perpendicular L-corner at origin; first entity on a non-default layer.
953
+ core.scene.clear();
954
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)], layer: 'walls', lineWidth: 5, lineType: 'DASHED' });
955
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
956
+
957
+ core.scene.headers.filletRadius = 2;
958
+ core.scene.headers.trimMode = false;
959
+
960
+ const fillet = new Fillet();
961
+ fillet.firstPick.entity = core.scene.entities.get(0);
962
+ fillet.secondPick.entity = core.scene.entities.get(1);
963
+ fillet.firstPick.clickPoint = new Point(-5, 0);
964
+ fillet.secondPick.clickPoint = new Point(0, 5);
965
+ fillet.action();
966
+
967
+ expect(core.scene.entities.count()).toBe(3);
968
+ const arc = core.scene.entities.get(2);
969
+ expect(arc.type).toBe('Arc');
970
+ expect(arc.getProperty('layer')).toBe('walls');
971
+ expect(arc.getProperty('lineWidth')).toBe(5);
972
+ expect(arc.getProperty('lineType')).toBe('DASHED');
973
+ });
974
+
975
+ test('Fillet.preview does not throw', () => {
976
+ const fillet = new Fillet();
977
+ expect(() => fillet.preview()).not.toThrow();
978
+ });
979
+
980
+ test('Fillet.preview returns early when findClosestItem returns undefined', () => {
981
+ core.scene.clear();
982
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
983
+ DesignCore.Scene.previewEntities.clear();
984
+
985
+ const fillet = new Fillet();
986
+ fillet.firstPick.entity = core.scene.entities.get(0);
987
+ fillet.firstPick.clickPoint = new Point(-5, 0);
988
+
989
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
990
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
991
+ fillet.preview();
992
+ findSpy.mockRestore();
993
+
994
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
995
+ });
996
+
997
+ test('Fillet.preview adds fillet arc to previewEntities in no-trim mode', () => {
998
+ core.scene.clear();
999
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1000
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1001
+ DesignCore.Scene.previewEntities.clear();
1002
+
1003
+ core.scene.headers.filletRadius = 2;
1004
+ core.scene.headers.trimMode = false;
1005
+
1006
+ const fillet = new Fillet();
1007
+ fillet.firstPick.entity = core.scene.entities.get(0);
1008
+ fillet.firstPick.clickPoint = new Point(-5, 0);
1009
+
1010
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1011
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1012
+ fillet.preview();
1013
+ findSpy.mockRestore();
1014
+
1015
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
1016
+ });
1017
+
1018
+ test('Fillet.preview adds dulled segments and arc to previewEntities in trim mode', () => {
1019
+ core.scene.clear();
1020
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1021
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1022
+ DesignCore.Scene.previewEntities.clear();
1023
+
1024
+ core.scene.headers.filletRadius = 2;
1025
+ core.scene.headers.trimMode = true;
1026
+
1027
+ const fillet = new Fillet();
1028
+ fillet.firstPick.entity = core.scene.entities.get(0);
1029
+ fillet.firstPick.clickPoint = new Point(-5, 0);
1030
+
1031
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1032
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1033
+ fillet.preview();
1034
+ findSpy.mockRestore();
1035
+
1036
+ // dulled firstSeg + dulled secondSeg + firstTrimLine + secondTrimLine + arc = 5
1037
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(5);
1038
+ });
1039
+
1040
+ test('Fillet.execute returns early when second-entity input is undefined (inner loop)', async () => {
1041
+ core.scene.clear();
1042
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1043
+ core.scene.addEntity('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1044
+
1045
+ const executeCommandSpy = jest.fn();
1046
+ // Only one input: the first selection. Second requestInput returns undefined → inner loop returns.
1047
+ await withMockInput(
1048
+ core.scene,
1049
+ [new SingleSelection(0, new Point(-5, 0))],
1050
+ async () => {
1051
+ const fillet = new Fillet();
1052
+ await fillet.execute();
1053
+ expect(executeCommandSpy).not.toHaveBeenCalled();
1054
+ },
1055
+ { extraMethods: { executeCommand: executeCommandSpy } },
1056
+ );
1057
+ });
1058
+
1059
+ test('Fillet.execute notifies NONCONSECUTIVESEGMENTS for non-adjacent polyline segments', async () => {
1060
+ // 5-point polyline → 4 segments. Pick segment 1 (near -15,0) then segment 3 (near 5,0).
1061
+ // Neither consecutive nor open-ends → NONCONSECUTIVESEGMENTS, then undefined to exit.
1062
+ core.scene.clear();
1063
+ 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)] });
1064
+
1065
+ const notifySpy = jest.spyOn(core, 'notify');
1066
+ await withMockInput(
1067
+ core.scene,
1068
+ [new SingleSelection(0, new Point(-15, 0)), new SingleSelection(0, new Point(5, 0)), undefined],
1069
+ async () => {
1070
+ const fillet = new Fillet();
1071
+ await fillet.execute();
1072
+ },
1073
+ );
1074
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NONCONSECUTIVESEGMENTS));
1075
+ notifySpy.mockRestore();
1076
+ });
1077
+
1078
+ test('Fillet.execute Trim→Trim keeps trimMode=true', async () => {
1079
+ core.scene.headers.trimMode = true;
1080
+
1081
+ await withMockInput(
1082
+ core.scene,
1083
+ ['Trim', 'Trim', undefined],
1084
+ async () => {
1085
+ const fillet = new Fillet();
1086
+ await fillet.execute();
1087
+ },
1088
+ );
1089
+
1090
+ expect(core.scene.headers.trimMode).toBe(true);
1091
+ });
1092
+
1093
+ // ─── regression: bulge must not leak onto Line endpoints ─────────────────────
1094
+
1095
+ test('Fillet.action two fillets on the same line: trimmed endpoints have bulge = 0', () => {
1096
+ // Line A: (-10, 0) → (10, 0) horizontal
1097
+ // Line B: (-10, 2) → (-10, -2) vertical at left end
1098
+ // Line C: ( 10, 2) → ( 10, -2) vertical at right end
1099
+ // Fillet A–B at the left corner, then A–C at the right corner.
1100
+ // arc.toPolylinePoints() sets bulge on the start point;
1101
+ // that value must not bleed onto the Line endpoints.
1102
+ core.scene.clear();
1103
+ core.scene.addEntity('Line', { points: [new Point(-10, 0), new Point(10, 0)] });
1104
+ core.scene.addEntity('Line', { points: [new Point(-10, 2), new Point(-10, -2)] });
1105
+ core.scene.addEntity('Line', { points: [new Point(10, 2), new Point(10, -2)] });
1106
+
1107
+ core.scene.headers.filletRadius = 1;
1108
+ core.scene.headers.trimMode = true;
1109
+
1110
+ const fillet1 = new Fillet();
1111
+ fillet1.firstPick.entity = core.scene.entities.get(0);
1112
+ fillet1.secondPick.entity = core.scene.entities.get(1);
1113
+ fillet1.firstPick.clickPoint = new Point(-5, 0);
1114
+ fillet1.secondPick.clickPoint = new Point(-10, 1);
1115
+ fillet1.action();
1116
+
1117
+ const fillet2 = new Fillet();
1118
+ fillet2.firstPick.entity = core.scene.entities.get(0);
1119
+ fillet2.secondPick.entity = core.scene.entities.get(2);
1120
+ fillet2.firstPick.clickPoint = new Point(5, 0);
1121
+ fillet2.secondPick.clickPoint = new Point(10, 1);
1122
+ fillet2.action();
1123
+
1124
+ const line = core.scene.entities.get(0);
1125
+ expect(line.points[0].bulge).toBe(0);
1126
+ expect(line.points[1].bulge).toBe(0);
1127
+ });
@@ -8,12 +8,12 @@ import { jest } from '@jest/globals';
8
8
  const core = new Core();
9
9
 
10
10
  // Add items to scene
11
- DesignCore.Scene.addItem('Line', { points: [new Point(10, 0), new Point(20, 0)], layer: 'test' });
12
- DesignCore.Scene.addItem('Circle', { points: [new Point(10, 0), new Point(20, 0)] });
13
- DesignCore.Scene.addItem('Polyline', { points: [new Point(10, 0), new Point(20, 0)] });
14
- DesignCore.Scene.addItem('Arc', { points: [new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
15
- DesignCore.Scene.addItem('Rectangle', { points: [new Point(10, 0), new Point(0, 10)] });
16
- DesignCore.Scene.addItem('Text', { points: [new Point(10, 0), new Point(0, 10)], height: 10, rotation: 0, string: 'text test' });
11
+ DesignCore.Scene.addEntity('Line', { points: [new Point(10, 0), new Point(20, 0)], layer: 'test' });
12
+ DesignCore.Scene.addEntity('Circle', { points: [new Point(10, 0), new Point(20, 0)] });
13
+ DesignCore.Scene.addEntity('Polyline', { points: [new Point(10, 0), new Point(20, 0)] });
14
+ DesignCore.Scene.addEntity('Arc', { points: [new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
15
+ DesignCore.Scene.addEntity('Rectangle', { points: [new Point(10, 0), new Point(0, 10)] });
16
+ DesignCore.Scene.addEntity('Text', { points: [new Point(10, 0), new Point(0, 10)], height: 10, rotation: 0, string: 'text test' });
17
17
 
18
18
  test('Test MatchProp.execute requests source and targets and completes', async () => {
19
19
  const match = new MatchProp();
@@ -69,16 +69,16 @@ test('Test MatchProp.action matches properties from source to targets', () => {
69
69
  // Validate that layer property was matched to targets
70
70
  for (let i = 0; i < match.destinationSetIndices.length; i++) {
71
71
  const tgt = core.scene.entities.get(match.destinationSetIndices[i]);
72
- expect(tgt.layer).toBe('test');
72
+ expect(tgt.getProperty('layer')).toBe('test');
73
73
  }
74
74
 
75
75
  const circle = core.scene.entities.get(1);
76
- expect(circle.radius).toBe(10); // original radius
76
+ expect(circle.getProperty('radius')).toBe(10); // original radius
77
77
 
78
78
  const arc = core.scene.entities.get(3);
79
- expect(arc.radius).toBe(10); // original radius
79
+ expect(arc.getProperty('radius')).toBe(10); // original radius
80
80
 
81
81
  const text = core.scene.entities.get(5);
82
- expect(text.height).toBe(10); // original height
83
- expect(text.string).toBe('text test'); // original string
82
+ expect(text.getProperty('height')).toBe(10); // original height
83
+ expect(text.getProperty('string')).toBe('text test'); // original string
84
84
  });