@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
@@ -17,28 +17,28 @@ const scenarios = [
17
17
 
18
18
  { desc: 'Radial dimension from circle selection - Text outside circle',
19
19
  input: [new SingleSelection(0, new Point()), new Point(20, 0)],
20
- selectedItems: [new Circle({ points: [new Point(0, 0), new Point(10, 0)] })],
20
+ selectedEntities: [new Circle({ points: [new Point(0, 0), new Point(10, 0)] })],
21
21
  expectedDimType: 4,
22
22
  dimensionValue: 10,
23
23
  dimensionEntities: 6,
24
24
  },
25
25
  { desc: 'Radial dimension from circle selection - Text inside circle',
26
26
  input: [new SingleSelection(0, new Point()), new Point(0, 0)],
27
- selectedItems: [new Circle({ points: [new Point(0, 0), new Point(10, 0)] })],
27
+ selectedEntities: [new Circle({ points: [new Point(0, 0), new Point(10, 0)] })],
28
28
  expectedDimType: 4,
29
29
  dimensionValue: 10,
30
30
  dimensionEntities: 5,
31
31
  },
32
32
  { desc: 'Radial dimension from arc selection',
33
33
  input: [new SingleSelection(0, new Point()), new Point(20, 10)],
34
- selectedItems: [new Arc({ points: [new Point(0, 0), new Point(20, 0), new Point(20, 20)] })],
34
+ selectedEntities: [new Arc({ points: [new Point(0, 0), new Point(20, 0), new Point(20, 20)] })],
35
35
  expectedDimType: 4,
36
36
  dimensionValue: 20,
37
37
  dimensionEntities: 6,
38
38
  },
39
39
  { desc: 'Radial dimension from polyline selection',
40
40
  input: [new SingleSelection(0, new Point(30, 10)), new Point(30, 10)],
41
- selectedItems: [new Polyline({ points: [new Point(0, 0), new Point(20, 0, 1), new Point(20, 20)] })],
41
+ selectedEntities: [new Polyline({ points: [new Point(0, 0), new Point(20, 0, 1), new Point(20, 20)] })],
42
42
  expectedDimType: 4,
43
43
  dimensionValue: 10,
44
44
  dimensionEntities: 6,
@@ -46,7 +46,7 @@ const scenarios = [
46
46
  ];
47
47
 
48
48
  test.each(scenarios)('RadialDimension.execute handles $desc', async (scenario) => {
49
- const { input, selectedItems, expectedDimType, dimensionValue, dimensionEntities } = scenario;
49
+ const { input, selectedEntities, expectedDimType, dimensionValue, dimensionEntities } = scenario;
50
50
 
51
51
  await withMockInput(DesignCore.Scene, input, async () => {
52
52
  const dim = new RadialDimension();
@@ -61,12 +61,12 @@ test.each(scenarios)('RadialDimension.execute handles $desc', async (scenario) =
61
61
  for (const entity of dimensionBlockEntities) {
62
62
  if (entity.type === 'Text') {
63
63
  // remove all non-numeric characters except . and -
64
- const numbersOnly = entity.string.replace(/[^0-9.-]+/g, '');
64
+ const numbersOnly = entity.getProperty('string').replace(/[^0-9.-]+/g, '');
65
65
  expect(Number(numbersOnly)).toBeCloseTo(dimensionValue);
66
- expect(entity.string).toContain('R');
66
+ expect(entity.getProperty('string')).toContain('R');
67
67
  }
68
68
  }
69
- }, { selectedItems });
69
+ }, { selectedEntities });
70
70
  });
71
71
 
72
72
  test('constructor sets default properties', () => {
@@ -77,26 +77,26 @@ test('constructor sets default properties', () => {
77
77
 
78
78
 
79
79
  test('getPointsFromSelection returns correct sequenced points', () => {
80
- const center = new Point(0, 0);
80
+ const centre = new Point(0, 0);
81
81
  const textPos = new Point(10, 0);
82
82
  // Mock item with getRadius
83
- const item = { points: [center], getRadius: () => 10 };
83
+ const item = { points: [centre], getRadius: () => 10 };
84
84
  const result = RadialDimension.getPointsFromSelection([item], textPos);
85
85
  expect(Array.isArray(result)).toBe(true);
86
86
  expect(result.length).toBe(3);
87
- expect(result[0].sequence).toBe(10); // center
87
+ expect(result[0].sequence).toBe(10); // centre
88
88
  expect(result[1].sequence).toBe(15); // radius point
89
89
  expect(result[2].sequence).toBe(11); // text position
90
90
  });
91
91
 
92
92
  test('RadialDimension.preview runs without error and calls createTempItem', () => {
93
- const origCreateTempItem = DesignCore.Scene.tempEntities.create;
93
+ const origCreateTempItem = DesignCore.Scene.previewEntities.create;
94
94
  const origPointOnScene = DesignCore.Mouse.pointOnScene;
95
95
  const origSeletionSet = DesignCore.Scene.selectionManager.selectionSet.selectionSet.length;
96
96
 
97
97
  // Manual mock for createTempItem
98
98
  const createTempItemCalls = [];
99
- DesignCore.Scene.tempEntities.create = function(type, obj) {
99
+ DesignCore.Scene.previewEntities.create = function(type, obj) {
100
100
  createTempItemCalls.push([type, obj]);
101
101
  };
102
102
  // Manual mock for pointOnScene
@@ -130,7 +130,7 @@ test('RadialDimension.preview runs without error and calls createTempItem', () =
130
130
  expect(createTempItemCalls.some((call) => call[0] === dim2.type)).toBe(true);
131
131
 
132
132
  // Restore
133
- DesignCore.Scene.tempEntities.create = origCreateTempItem;
133
+ DesignCore.Scene.previewEntities.create = origCreateTempItem;
134
134
  DesignCore.Mouse.pointOnScene = origPointOnScene;
135
135
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.length = origSeletionSet;
136
136
  });
@@ -196,7 +196,7 @@ AcDbRadialDimension
196
196
  expect(file.contents).toEqual(dxfString);
197
197
 
198
198
  // create new entity from entity data to ensure all props are loaded
199
- const newDimension = new RadialDimension(dimension);
199
+ const newDimension = new RadialDimension({ handle: dimension.getProperty('handle'), points: dimension.points, ...dimension });
200
200
  file = new File();
201
201
  newDimension.dxf(file);
202
202
  expect(file.contents).toEqual(dxfString);
@@ -13,20 +13,20 @@ const scenarios = [
13
13
 
14
14
  { desc: 'Rotated dimension from point selection (angle 0)',
15
15
  input: [0, new Point(), new Point(10, 0), new Point(5, 5)],
16
- selectedItems: [],
16
+ selectedEntities: [],
17
17
  expectedDimType: 0,
18
18
  dimensionValue: 10,
19
19
  },
20
20
  { desc: 'Rotated dimension from point selection (angle 90)',
21
21
  input: [90, new Point(), new Point(0, 10), new Point(5, 5)],
22
- selectedItems: [],
22
+ selectedEntities: [],
23
23
  expectedDimType: 0,
24
24
  dimensionValue: 10,
25
25
  },
26
26
  ];
27
27
 
28
28
  test.each(scenarios)('Dimension.execute handles $desc', async (scenario) => {
29
- const { input, selectedItems, expectedDimType, dimensionValue } = scenario;
29
+ const { input, selectedEntities, expectedDimType, dimensionValue } = scenario;
30
30
 
31
31
  await withMockInput(DesignCore.Scene, input, async () => {
32
32
  const dim = new RotatedDimension();
@@ -40,10 +40,10 @@ test.each(scenarios)('Dimension.execute handles $desc', async (scenario) => {
40
40
  // check if the text value matches the expected dimension value
41
41
  for (const entity of dimensionBlockEntities) {
42
42
  if (entity.type === 'Text') {
43
- expect(Number(entity.string)).toBeCloseTo(dimensionValue);
43
+ expect(Number(entity.getProperty('string'))).toBeCloseTo(dimensionValue);
44
44
  }
45
45
  }
46
- }, { selectedItems });
46
+ }, { selectedEntities });
47
47
  });
48
48
 
49
49
  test('constructor sets default properties', () => {
@@ -113,23 +113,29 @@ describe('RotatedDimension.getPointsFromSelection', () => {
113
113
  });
114
114
 
115
115
  test('RotatedDimension.preview runs without error and calls createTempItem', () => {
116
- const origCreateTempItem = DesignCore.Scene.tempEntities.create;
116
+ const origAddAux = DesignCore.Scene.auxiliaryEntities.add;
117
+ const origCreateTempItem = DesignCore.Scene.previewEntities.create;
117
118
  const origPointOnScene = DesignCore.Mouse.pointOnScene;
118
119
  // Manual mock for createTempItem
119
120
  const createTempItemCalls = [];
120
- DesignCore.Scene.tempEntities.create = function(type, obj) {
121
+ DesignCore.Scene.previewEntities.create = function(type, obj) {
121
122
  createTempItemCalls.push([type, obj]);
122
123
  };
124
+ // Manual mock for auxiliaryEntities.add
125
+ const addAuxCalls = [];
126
+ DesignCore.Scene.auxiliaryEntities.add = function(item) {
127
+ addAuxCalls.push(item);
128
+ };
123
129
  // Manual mock for pointOnScene
124
130
  DesignCore.Mouse.pointOnScene = function() {
125
131
  return new Point(5, 5);
126
132
  };
127
133
 
128
- // Test with 1 point (should call createTempItem with 'Line')
134
+ // Test with 1 point (should add a RubberBand to auxiliaryEntities)
129
135
  const dim1 = new RotatedDimension();
130
136
  dim1.points = [new Point(0, 0)];
131
137
  expect(() => dim1.preview()).not.toThrow();
132
- expect(createTempItemCalls.some((call) => call[0] === 'Line')).toBe(true);
138
+ expect(addAuxCalls.length).toBeGreaterThanOrEqual(1);
133
139
 
134
140
  // Test with >1 point (should call createTempItem with this.type)
135
141
  const dim2 = new RotatedDimension();
@@ -138,7 +144,8 @@ test('RotatedDimension.preview runs without error and calls createTempItem', ()
138
144
  expect(createTempItemCalls.some((call) => call[0] === dim2.type)).toBe(true);
139
145
 
140
146
  // Restore
141
- DesignCore.Scene.tempEntities.create = origCreateTempItem;
147
+ DesignCore.Scene.auxiliaryEntities.add = origAddAux;
148
+ DesignCore.Scene.previewEntities.create = origCreateTempItem;
142
149
  DesignCore.Mouse.pointOnScene = origPointOnScene;
143
150
  });
144
151
 
@@ -217,7 +224,7 @@ AcDbRotatedDimension
217
224
  expect(file.contents).toEqual(dxfString);
218
225
 
219
226
  // create new entity from entity data to ensure all props are loaded
220
- const newDimension = new RotatedDimension(dimension);
227
+ const newDimension = new RotatedDimension({ handle: dimension.getProperty('handle'), points: dimension.points, ...dimension });
221
228
  file = new File();
222
229
  newDimension.dxf(file);
223
230
  expect(file.contents).toEqual(dxfString);
@@ -1,6 +1,15 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Arc } from '../../core/entities/arc.js';
3
+ import { Point } from '../../core/entities/point.js';
4
+ import { DesignCore } from '../../core/designCore.js';
5
+
6
+ import { File, withMockInput } from '../test-helpers/test-helpers.js';
1
7
  import { jest } from '@jest/globals';
2
8
  import { Strings } from '../../core/lib/strings.js';
3
9
 
10
+ // initialise core
11
+ new Core();
12
+
4
13
  describe('Arc.execute input validation and notifications', () => {
5
14
  let notifySpy;
6
15
  beforeEach(() => {
@@ -49,7 +58,7 @@ describe('Arc.execute input validation and notifications', () => {
49
58
  test('Notifies on invalid point input (duplicate)', async () => {
50
59
  const input1 = new Point(0, 0);
51
60
  const input2 = new Point(10, 0);
52
- const input3 = new Point(0, 0); // Same as center
61
+ const input3 = new Point(0, 0); // Same as centre
53
62
  await withMockInput(DesignCore.Scene, [input1, input2, input3, new Point(10, 10)], async () => {
54
63
  const arc = new Arc({});
55
64
  await arc.execute();
@@ -69,16 +78,6 @@ describe('Arc.execute input validation and notifications', () => {
69
78
  });
70
79
  });
71
80
  });
72
- import { Core } from '../../core/core/core.js';
73
- import { Arc } from '../../core/entities/arc.js';
74
- import { Point } from '../../core/entities/point.js';
75
- import { DesignCore } from '../../core/designCore.js';
76
- import { AddState, RemoveState } from '../../core/lib/stateManager.js';
77
-
78
- import { File, withMockInput } from '../test-helpers/test-helpers.js';
79
-
80
- // initialise core
81
- new Core();
82
81
 
83
82
  const arcInputScenarios = [
84
83
  {
@@ -95,7 +94,7 @@ const arcInputScenarios = [
95
94
  input1: new Point(0, 0),
96
95
  input2: new Point(10, 0),
97
96
  input3: new Point(10, 10),
98
- expectedPt2: (input1, input2, input3) => input3,
97
+ expectedPt2: (input1, input2, input3) => input1.project(input1.angle(input3), input1.distance(input2)),
99
98
  expectedStart: 0,
100
99
  expectedEnd: Math.PI / 4,
101
100
  },
@@ -274,7 +273,7 @@ ByLayer
274
273
  30
275
274
  0.0
276
275
  39
277
- 2
276
+ 1
278
277
  40
279
278
  100
280
279
  100
@@ -288,7 +287,7 @@ AcDbArc
288
287
  expect(file.contents).toEqual(dxfString);
289
288
 
290
289
  // create new entity from entity data to ensure all props are loaded
291
- const newArc = new Arc(arc);
290
+ const newArc = new Arc({ handle: arc.getProperty('handle'), points: arc.points, direction: arc.getProperty('direction') });
292
291
  file = new File();
293
292
  newArc.dxf(file);
294
293
  expect(file.contents).toEqual(dxfString);
@@ -327,58 +326,14 @@ test('Arc constructor handles missing/invalid data', () => {
327
326
  expect(() => new Arc({ points: [null, undefined, {}] })).not.toThrow();
328
327
  });
329
328
 
330
- test('Arc.trim returns empty for no intersections', () => {
331
- const arc = new Arc({ points: [new Point(100, 100), new Point(200, 100), new Point(170.71, 170.71)] });
332
- expect(arc.trim([])).toEqual([]);
333
- expect(arc.trim()).toEqual([]);
334
- });
335
-
336
-
337
- test('Arc.trim returns remove + add states when trimming between two intersections', () => {
338
- const arc = new Arc({ points: [new Point(), new Point(10, 0), new Point(-10, 0)], handle: 'B2' });
339
- // Intersection points on the arc.
340
- const i1 = new Point(7.71, 7.71);
341
- const i2 = new Point(-7.71, 7.71);
342
- const intersections = [i1, i2];
343
-
344
- // set the mouse position to mid point of the arc
345
- DesignCore.Mouse.pointOnScene = () => new Point(0, 10);
346
-
347
- const changes = arc.trim(intersections);
348
-
349
- expect(Array.isArray(changes)).toBe(true);
350
- expect(changes.length).toBeGreaterThanOrEqual(2);
351
-
352
- expect(changes[0]).toBeInstanceOf(AddState);
353
- expect(changes[1]).toBeInstanceOf(AddState);
354
- expect(changes[2]).toBeInstanceOf(RemoveState);
355
-
356
- // Verify trimmed arcs have unique handles (not the original)
357
- expect(changes[0].entity.handle).toBeUndefined();
358
- expect(changes[1].entity.handle).toBeUndefined();
359
-
360
- expect(changes[0].entity.points[0].x).toBe(0);
361
- expect(changes[0].entity.points[0].y).toBe(0);
362
-
363
- expect(changes[0].entity.points[1].x).toBeCloseTo(10);
364
- expect(changes[0].entity.points[1].y).toBeCloseTo(0);
365
- expect(changes[0].entity.points[2].x).toBeCloseTo(7.71);
366
- expect(changes[0].entity.points[2].y).toBeCloseTo(7.71);
367
-
368
- expect(changes[1].entity.points[1].x).toBeCloseTo(-7.71);
369
- expect(changes[1].entity.points[1].y).toBeCloseTo(7.71);
370
- expect(changes[1].entity.points[2].x).toBe(-10);
371
- expect(changes[1].entity.points[2].y).toBe(0);
372
- });
373
-
374
329
  test('Arc.getRadius returns the arc radius', () => {
375
330
  const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] });
376
331
  expect(arc.getRadius()).toBe(10);
377
332
  });
378
333
 
379
- test('Arc constructor loads from DXF group codes', () => {
380
- const arc = new Arc({ points: [new Point(100, 100)], 40: 50, 50: 0, 51: 90 });
381
- expect(arc.radius).toBe(50);
334
+ test('Arc constructor loads from DXF group codes via fromDxf', () => {
335
+ const arc = new Arc(Arc.fromDxf({ points: [new Point(100, 100)], 40: 50, 50: 0, 51: 90 }));
336
+ expect(arc.getProperty('radius')).toBe(50);
382
337
  expect(arc.points[1].x).toBeCloseTo(150);
383
338
  expect(arc.points[1].y).toBeCloseTo(100);
384
339
  expect(arc.points[2].x).toBeCloseTo(100);
@@ -387,7 +342,7 @@ test('Arc constructor loads from DXF group codes', () => {
387
342
 
388
343
  test('Arc constructor loads direction from data', () => {
389
344
  const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: -1 });
390
- expect(arc.direction).toBe(-1);
345
+ expect(arc.getProperty('direction')).toBe(-1);
391
346
  });
392
347
 
393
348
  test('Arc.toPolylinePoints handles a full circle (360 degrees)', () => {
@@ -401,9 +356,9 @@ test('Arc.toPolylinePoints handles a full circle (360 degrees)', () => {
401
356
  });
402
357
 
403
358
  test('Arc.preview does not throw with 1, 2 or 3 points', () => {
404
- const origCreate = DesignCore.Scene.tempEntities.create;
359
+ const origCreate = DesignCore.Scene.previewEntities.create;
405
360
  const origMouse = DesignCore.Mouse.pointOnScene;
406
- DesignCore.Scene.tempEntities.create = () => {};
361
+ DesignCore.Scene.previewEntities.create = () => {};
407
362
  DesignCore.Mouse.pointOnScene = () => new Point(5, 5);
408
363
 
409
364
  const arc1 = new Arc({});
@@ -418,20 +373,30 @@ test('Arc.preview does not throw with 1, 2 or 3 points', () => {
418
373
  arc3.points = [new Point(0, 0), new Point(10, 0), new Point(0, 10)];
419
374
  expect(() => arc3.preview()).not.toThrow();
420
375
 
421
- DesignCore.Scene.tempEntities.create = origCreate;
376
+ DesignCore.Scene.previewEntities.create = origCreate;
422
377
  DesignCore.Mouse.pointOnScene = origMouse;
423
378
  });
424
379
 
425
- test('Arc.draw does not throw (ccw and cw)', () => {
426
- const ctx = { arc: jest.fn(), stroke: jest.fn() };
380
+ test('Arc.draw calls renderer.drawShape with toPolylinePoints', () => {
427
381
  const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: 1 });
428
- expect(() => arc.draw(ctx, 1)).not.toThrow();
429
- expect(ctx.arc).toHaveBeenCalledTimes(1);
430
- expect(ctx.stroke).toHaveBeenCalledTimes(1);
382
+ let capturedPoints;
383
+ const mockRenderer = {
384
+ drawShape(points) {
385
+ capturedPoints = points;
386
+ },
387
+ };
388
+ arc.draw(mockRenderer);
389
+ expect(capturedPoints).toEqual(arc.toPolylinePoints());
390
+ });
431
391
 
432
- const ctx2 = { arc: jest.fn(), stroke: jest.fn() };
392
+ test('Arc.draw (cw) calls renderer.drawShape with toPolylinePoints', () => {
433
393
  const arcCw = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: -1 });
434
- expect(() => arcCw.draw(ctx2, 1)).not.toThrow();
394
+ let capturedPoints;
395
+ const mockRenderer = { drawShape(points) {
396
+ capturedPoints = points;
397
+ } };
398
+ arcCw.draw(mockRenderer);
399
+ expect(capturedPoints).toEqual(arcCw.toPolylinePoints());
435
400
  });
436
401
 
437
402
  test('Arc.snaps returns end and centre snap points', () => {
@@ -454,7 +419,7 @@ test('Arc.execute handles negative angle (clockwise)', async () => {
454
419
  const arc = new Arc({});
455
420
  await arc.execute();
456
421
  expect(arc.points.length).toBe(3);
457
- expect(arc.direction).toBe(-1);
422
+ expect(arc.getProperty('direction')).toBe(-1);
458
423
  });
459
424
  });
460
425
 
@@ -468,35 +433,26 @@ test('Arc.execute notifies on angle > 360', async () => {
468
433
  notifySpy.mockRestore();
469
434
  });
470
435
 
471
- test('Arc.trim returns empty when only intersection is at an endpoint', () => {
472
- const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(-10, 0)] });
473
- // intersection exactly at the start point — filtered out, nothing to trim
474
- const changes = arc.trim([new Point(10, 0)]);
475
- expect(changes).toEqual([]);
476
- });
477
-
478
436
  test('Arc.fromPolylinePoints round-trip preserves centre, radius and angles', () => {
479
437
  const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)] });
480
- arc.radius = 10;
481
438
  const polyPts = arc.toPolylinePoints();
482
439
  const rebuilt = new Arc({});
483
440
  rebuilt.fromPolylinePoints(polyPts);
484
441
  expect(rebuilt.points[0].x).toBeCloseTo(0, 10);
485
442
  expect(rebuilt.points[0].y).toBeCloseTo(0, 10);
486
- expect(rebuilt.radius).toBeCloseTo(10, 10);
443
+ expect(rebuilt.getProperty('radius')).toBeCloseTo(10, 10);
487
444
  expect(rebuilt.startAngle()).toBeCloseTo(arc.startAngle(), 10);
488
445
  expect(rebuilt.endAngle()).toBeCloseTo(arc.endAngle(), 10);
489
446
  });
490
447
 
491
448
  test('Arc.fromPolylinePoints precision drift is negligible', () => {
492
449
  const arc = new Arc({ points: [new Point(50, 50), new Point(50, 50).project(0.7, 30), new Point(50, 50).project(2.1, 30)] });
493
- arc.radius = 30;
494
450
  const polyPts = arc.toPolylinePoints();
495
451
  const rebuilt = new Arc({});
496
452
  rebuilt.fromPolylinePoints(polyPts);
497
- const centerDrift = Math.hypot(rebuilt.points[0].x - arc.points[0].x, rebuilt.points[0].y - arc.points[0].y);
498
- expect(centerDrift).toBeLessThan(1e-10);
499
- expect(Math.abs(rebuilt.radius - arc.radius)).toBeLessThan(1e-10);
453
+ const centreDrift = Math.hypot(rebuilt.points[0].x - arc.points[0].x, rebuilt.points[0].y - arc.points[0].y);
454
+ expect(centreDrift).toBeLessThan(1e-10);
455
+ expect(Math.abs(rebuilt.getProperty('radius') - arc.getProperty('radius'))).toBeLessThan(1e-10);
500
456
  });
501
457
 
502
458
  test('Arc.snaps nearest does not fire when mouse is too far from arc', () => {
@@ -531,6 +487,47 @@ test('Arc.snaps returns no tangent when fromPoint is inside the arc radius', ()
531
487
  DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
532
488
  });
533
489
 
490
+ describe('Arc.fromDxf', () => {
491
+ test('projects start and end points from DXF angle codes', () => {
492
+ const centre = new Point(0, 0);
493
+ const result = Arc.fromDxf({ points: [centre], 40: 10, 50: 0, 51: 90 });
494
+ expect(result.points.length).toBe(3);
495
+ expect(result.points[0]).toBe(centre);
496
+ expect(result.points[1].x).toBeCloseTo(10); // project(0°, 10)
497
+ expect(result.points[1].y).toBeCloseTo(0);
498
+ expect(result.points[2].x).toBeCloseTo(0); // project(90°, 10)
499
+ expect(result.points[2].y).toBeCloseTo(10);
500
+ });
501
+
502
+ test('uses direction from data[73], defaults to 1', () => {
503
+ const result1 = Arc.fromDxf({ points: [new Point(0, 0)], 40: 10, 50: 0, 51: 90, 73: -1 });
504
+ expect(result1.direction).toBe(-1);
505
+
506
+ const result2 = Arc.fromDxf({ points: [new Point(0, 0)], 40: 10, 50: 0, 51: 90 });
507
+ expect(result2.direction).toBe(1);
508
+ });
509
+
510
+ test('preserves all other data properties', () => {
511
+ const result = Arc.fromDxf({ points: [new Point(0, 0)], 40: 10, 50: 0, 51: 90, 8: 'MyLayer', 6: 'ByLayer' });
512
+ expect(result[8]).toBe('MyLayer');
513
+ expect(result[6]).toBe('ByLayer');
514
+ });
515
+
516
+ test('returns data unchanged when no centre point', () => {
517
+ const data = { 40: 10, 50: 0, 51: 90 };
518
+ expect(Arc.fromDxf(data)).toBe(data);
519
+ });
520
+
521
+ test('arc constructed via fromDxf matches arc constructed directly', () => {
522
+ const centre = new Point(100, 100);
523
+ const fromDxfArc = new Arc(Arc.fromDxf({ points: [centre], 40: 50, 50: 0, 51: 90 }));
524
+ const directArc = new Arc({ points: [centre, new Point(150, 100), new Point(100, 150)] });
525
+ expect(fromDxfArc.getProperty('radius')).toBeCloseTo(directArc.getProperty('radius'));
526
+ expect(fromDxfArc.startAngle()).toBeCloseTo(directArc.startAngle());
527
+ expect(fromDxfArc.endAngle()).toBeCloseTo(directArc.endAngle());
528
+ });
529
+ });
530
+
534
531
  test('Arc.snaps returns perpendicular snap when radial point is on the arc', () => {
535
532
  const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: 1 });
536
533
  const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;