@design-core/core 0.0.6 → 0.0.7

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 (145) hide show
  1. package/.eslintrc.json +4 -0
  2. package/TODO.md +49 -6
  3. package/core/core/core.js +0 -3
  4. package/core/dimensions/alignedDimension.js +5 -4
  5. package/core/dimensions/angularDimension.js +6 -5
  6. package/core/dimensions/baseCircularDimension.js +3 -2
  7. package/core/dimensions/baseDimension.js +59 -93
  8. package/core/dimensions/baseLinearDimension.js +6 -5
  9. package/core/dimensions/diametricDimension.js +4 -3
  10. package/core/dimensions/dimension.js +21 -14
  11. package/core/dimensions/radialDimension.js +4 -3
  12. package/core/dimensions/rotatedDimension.js +12 -11
  13. package/core/entities/arc.js +112 -145
  14. package/core/entities/arctext.js +212 -174
  15. package/core/entities/basePolyline.js +30 -282
  16. package/core/entities/circle.js +77 -99
  17. package/core/entities/entity.js +114 -93
  18. package/core/entities/hatch.js +322 -249
  19. package/core/entities/insert.js +66 -81
  20. package/core/entities/line.js +21 -142
  21. package/core/entities/lwpolyline.js +16 -11
  22. package/core/entities/polyline.js +4 -4
  23. package/core/entities/rectangle.js +3 -1
  24. package/core/entities/solid.js +6 -15
  25. package/core/entities/text.js +188 -199
  26. package/core/lib/auxiliary/rubberBand.js +32 -0
  27. package/core/lib/auxiliary/selectionWindow.js +63 -0
  28. package/core/lib/auxiliary/snapPoint.js +113 -0
  29. package/core/lib/auxiliary/trackingLine.js +76 -0
  30. package/core/lib/canvas.js +228 -166
  31. package/core/lib/clipboard.js +7 -1
  32. package/core/lib/colour.js +16 -0
  33. package/core/lib/commandLine.js +1 -6
  34. package/core/lib/commandManager.js +18 -8
  35. package/core/lib/constants.js +28 -0
  36. package/core/lib/dxf/dxf.js +4 -3
  37. package/core/lib/entityManager.js +18 -11
  38. package/core/lib/inputManager.js +35 -11
  39. package/core/lib/intersect.js +25 -0
  40. package/core/lib/patterns.js +8 -1
  41. package/core/lib/plotOptions.js +67 -0
  42. package/core/lib/polylineUtils.js +75 -0
  43. package/core/lib/renderers/cairoRenderer.js +371 -0
  44. package/core/lib/renderers/canvasRenderer.js +315 -0
  45. package/core/lib/renderers/pdfRenderer.js +366 -0
  46. package/core/lib/renderers/rendererBase.js +284 -0
  47. package/core/lib/renderers/svgRenderer.js +343 -0
  48. package/core/lib/scene.js +27 -5
  49. package/core/lib/selectionManager.js +2 -2
  50. package/core/lib/settings.js +4 -2
  51. package/core/lib/snapping.js +4 -191
  52. package/core/lib/stateManager.js +30 -1
  53. package/core/lib/utils.js +21 -2
  54. package/core/properties/entityProperties.js +104 -0
  55. package/core/properties/property.js +106 -2
  56. package/core/properties/propertyManager.js +29 -13
  57. package/core/tables/block.js +4 -37
  58. package/core/tables/endblk.js +1 -1
  59. package/core/tables/layer.js +8 -0
  60. package/core/tables/styleManagerBase.js +3 -2
  61. package/core/tables/tableManagerBase.js +8 -3
  62. package/core/tools/chamfer.js +111 -137
  63. package/core/tools/chamferFilletBase.js +135 -34
  64. package/core/tools/copy.js +3 -3
  65. package/core/tools/explode.js +2 -2
  66. package/core/tools/extend.js +132 -19
  67. package/core/tools/fillet.js +86 -183
  68. package/core/tools/matchProp.js +2 -11
  69. package/core/tools/mirror.js +7 -6
  70. package/core/tools/move.js +3 -3
  71. package/core/tools/offset.js +3 -5
  72. package/core/tools/pasteclip.js +1 -1
  73. package/core/tools/rotate.js +6 -3
  74. package/core/tools/scale.js +1 -1
  75. package/core/tools/trim.js +265 -23
  76. package/core/tools/zoom.js +1 -1
  77. package/jest.config.js +6 -0
  78. package/package.json +5 -3
  79. package/test/dimensions/alignedDimension.test.js +14 -7
  80. package/test/dimensions/angularDimension.test.js +6 -6
  81. package/test/dimensions/baseDimension.test.js +69 -8
  82. package/test/dimensions/baseLinearDimension.test.js +62 -0
  83. package/test/dimensions/diametricDimension.test.js +6 -6
  84. package/test/dimensions/radialDimension.test.js +6 -6
  85. package/test/dimensions/rotatedDimension.test.js +14 -7
  86. package/test/entities/arc.test.js +80 -83
  87. package/test/entities/arctext.test.js +180 -53
  88. package/test/entities/basePolyline.test.js +68 -2
  89. package/test/entities/circle.test.js +57 -85
  90. package/test/entities/entity.test.js +42 -8
  91. package/test/entities/hatch.test.js +543 -70
  92. package/test/entities/insert.test.js +96 -1
  93. package/test/entities/line.test.js +15 -41
  94. package/test/entities/lwpolyline.test.js +104 -0
  95. package/test/entities/polyline.test.js +11 -0
  96. package/test/entities/rectangle.test.js +11 -0
  97. package/test/entities/solid.test.js +1 -1
  98. package/test/entities/text.test.js +294 -69
  99. package/test/integration/exportIntegration.reference.dxf +9 -9
  100. package/test/integration/exportIntegration.test.js +2 -2
  101. package/test/integration/intersectionIntegration.test.js +4 -5
  102. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  103. package/test/integration/pdfRendererIntegration.test.js +61 -0
  104. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  105. package/test/integration/svgRendererIntegration.test.js +61 -0
  106. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  107. package/test/integration/userInputIntegration.test.js +239 -132
  108. package/test/lib/canvas.test.js +742 -123
  109. package/test/lib/constants.test.js +27 -0
  110. package/test/lib/entityManager.test.js +4 -4
  111. package/test/lib/inputManager.test.js +80 -19
  112. package/test/lib/intersect.test.js +83 -0
  113. package/test/lib/plotOptions.test.js +179 -0
  114. package/test/lib/rubberBand.test.js +84 -0
  115. package/test/lib/settings.test.js +5 -6
  116. package/test/lib/snapping.test.js +71 -129
  117. package/test/lib/stateManager.test.js +7 -7
  118. package/test/lib/utils.test.js +36 -2
  119. package/test/properties/entityProperties.test.js +132 -0
  120. package/test/properties/property.test.js +53 -0
  121. package/test/properties/propertyManager.test.js +3 -3
  122. package/test/renderers/canvasRenderer.test.js +317 -0
  123. package/test/renderers/pdfRenderer.test.js +271 -0
  124. package/test/renderers/rendererBase.test.js +213 -0
  125. package/test/renderers/svgRenderer.test.js +326 -0
  126. package/test/tables/block.test.js +31 -1
  127. package/test/tables/endblk.test.js +2 -2
  128. package/test/tables/layer.test.js +30 -0
  129. package/test/test-helpers/hatchGridRenderer.js +180 -0
  130. package/test/test-helpers/test-helpers.js +152 -0
  131. package/test/tools/chamfer.test.js +227 -5
  132. package/test/tools/chamferFilletBase.test.js +98 -9
  133. package/test/tools/copy.test.js +2 -2
  134. package/test/tools/explode.test.js +3 -3
  135. package/test/tools/extend.test.js +331 -0
  136. package/test/tools/fillet.test.js +181 -0
  137. package/test/tools/matchProp.test.js +5 -5
  138. package/test/tools/mirror.test.js +13 -13
  139. package/test/tools/offset.test.js +170 -3
  140. package/test/tools/pasteclip.test.js +5 -5
  141. package/test/tools/rotate.test.js +32 -2
  142. package/test/tools/scale.test.js +5 -5
  143. package/test/tools/trim.test.js +526 -0
  144. package/test/tools/zoom.test.js +7 -7
  145. package/core/lib/selectionWindow.js +0 -83
@@ -61,9 +61,9 @@ test.each(scenarios)('DiametricDimension.execute handles $desc', async (scenario
61
61
  for (const entity of dimensionBlockEntities) {
62
62
  if (entity.type === 'Text') {
63
63
  // remove all none numberic 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('Ø');
66
+ expect(entity.getProperty('string')).toContain('Ø');
67
67
  }
68
68
  }
69
69
  }, { selectedItems });
@@ -90,13 +90,13 @@ test('getPointsFromSelection returns correct sequenced points', () => {
90
90
  });
91
91
 
92
92
  test('DiametricDimension.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('DiametricDimension.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 @@ AcDbDiametricDimension
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 DiametricDimension(dimension);
199
+ const newDimension = new DiametricDimension({ 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);
@@ -61,9 +61,9 @@ 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
69
  }, { selectedItems });
@@ -90,13 +90,13 @@ test('getPointsFromSelection returns correct sequenced points', () => {
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);
@@ -40,7 +40,7 @@ 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
46
  }, { selectedItems });
@@ -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(() => {
@@ -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
453
  const centerDrift = Math.hypot(rebuilt.points[0].x - arc.points[0].x, rebuilt.points[0].y - arc.points[0].y);
498
454
  expect(centerDrift).toBeLessThan(1e-10);
499
- expect(Math.abs(rebuilt.radius - arc.radius)).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 center = new Point(0, 0);
493
+ const result = Arc.fromDxf({ points: [center], 40: 10, 50: 0, 51: 90 });
494
+ expect(result.points.length).toBe(3);
495
+ expect(result.points[0]).toBe(center);
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 center 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 center = new Point(100, 100);
523
+ const fromDxfArc = new Arc(Arc.fromDxf({ points: [center], 40: 50, 50: 0, 51: 90 }));
524
+ const directArc = new Arc({ points: [center, 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;