@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
@@ -29,12 +29,12 @@ test.each(arcInputScenarios)('ArcText.execute handles $desc', async (scenario) =
29
29
  const arcText = new ArcAlignedText();
30
30
  await arcText.execute();
31
31
 
32
- expect(arcText.styleName).toBe('STANDARD');
33
- expect(arcText.height).toBe(expectedTextHeight);
34
- expect(arcText.string).toBe(expectedText);
32
+ expect(arcText.getProperty('styleName')).toBe('STANDARD');
33
+ expect(arcText.getProperty('height')).toBe(expectedTextHeight);
34
+ expect(arcText.getProperty('string')).toBe(expectedText);
35
35
 
36
36
  // arc props
37
- expect(arcText.radius).toBe(100);
37
+ expect(arcText.getProperty('radius')).toBe(100);
38
38
  expect(arcText.startAngle()).toBe(0);
39
39
  expect(arcText.endAngle()).toBeCloseTo(3.14159);
40
40
  }, { selectedItems });
@@ -52,7 +52,7 @@ test('ArcAlignedCharacter', () => {
52
52
  expect(ac.position.y).toBe(point.y);
53
53
  expect(ac.angle).toBeCloseTo(angle);
54
54
  expect(ac.baseline.x).toBe(10.5);
55
- expect(ac.baseline.y).toBe(19.7);
55
+ expect(ac.baseline.y).toBe(19.5);
56
56
  expect(ac.boundingBox).toBeInstanceOf(BoundingBox);
57
57
  });
58
58
 
@@ -115,14 +115,14 @@ test('ArcText getArcAlignedCharacters', () => {
115
115
  expect(chars.length).toBe(4);
116
116
  // First character
117
117
  expect(chars[0].character).toBe('T');
118
- expect(chars[0].position.x).toBeCloseTo(-2.39222);
119
- expect(chars[0].position.y).toBeCloseTo(101.22173);
120
- expect(chars[0].angle).toBeCloseTo(0.02362);
118
+ expect(chars[0].position.x).toBeCloseTo(-2.05483);
119
+ expect(chars[0].position.y).toBeCloseTo(101.22915);
120
+ expect(chars[0].angle).toBeCloseTo(0.02030);
121
121
  // Fourth character
122
122
  expect(chars.at(-1).character).toBe('t');
123
- expect(chars.at(-1).position.x).toBeCloseTo(2.39222);
124
- expect(chars.at(-1).position.y).toBeCloseTo(101.22173);
125
- expect(chars.at(-1).angle).toBeCloseTo(-0.02362);
123
+ expect(chars.at(-1).position.x).toBeCloseTo(2.05483);
124
+ expect(chars.at(-1).position.y).toBeCloseTo(101.22915);
125
+ expect(chars.at(-1).angle).toBeCloseTo(-0.02030);
126
126
 
127
127
 
128
128
  // //////// Text Alignment Tests //////////
@@ -133,29 +133,29 @@ test('ArcText getArcAlignedCharacters', () => {
133
133
  expect(chars.length).toBe(4);
134
134
  // First character - Reversed!
135
135
  expect(chars[0].character).toBe('t');
136
- expect(chars[0].position.x).toBeCloseTo(101.24722);
137
- expect(chars[0].position.y).toBeCloseTo(0.74997);
138
- expect(chars[0].angle).toBeCloseTo(-1.56338);
136
+ expect(chars[0].position.x).toBeCloseTo(101.23852);
137
+ expect(chars[0].position.y).toBeCloseTo(1.52491);
138
+ expect(chars[0].angle).toBeCloseTo(-1.55573);
139
139
  // Last character - Reversed!
140
140
  expect(chars.at(-1).character).toBe('T');
141
- expect(chars.at(-1).position.x).toBeCloseTo(101.09875);
142
- expect(chars.at(-1).position.y).toBeCloseTo(5.53213);
143
- expect(chars.at(-1).angle).toBeCloseTo(-1.51613);
141
+ expect(chars.at(-1).position.x).toBeCloseTo(101.09324);
142
+ expect(chars.at(-1).position.y).toBeCloseTo(5.63200);
143
+ expect(chars.at(-1).angle).toBeCloseTo(-1.51514);
144
144
 
145
- // Create ArcAlignedText - left aligned - textAlignment = 3
145
+ // Create ArcAlignedText - left aligned - textAlignment = 2
146
146
  const arcTextAlignLeft = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 2 });
147
147
  chars = arcTextAlignLeft.getArcAlignedCharacters();
148
148
  expect(chars.length).toBe(4);
149
149
  // First character
150
150
  expect(chars[0].character).toBe('T');
151
- expect(chars[0].position.x).toBeCloseTo(-101.247222);
152
- expect(chars[0].position.y).toBeCloseTo(0.749979);
153
- expect(chars[0].angle).toBeCloseTo(1.56338);
151
+ expect(chars[0].position.x).toBeCloseTo(-101.24664);
152
+ expect(chars[0].position.y).toBeCloseTo(0.82499);
153
+ expect(chars[0].angle).toBeCloseTo(1.56265);
154
154
  // Last character
155
155
  expect(chars.at(-1).character).toBe('t');
156
- expect(chars.at(-1).position.x).toBeCloseTo(-101.09875);
157
- expect(chars.at(-1).position.y).toBeCloseTo(5.53213);
158
- expect(chars.at(-1).angle).toBeCloseTo(1.51613);
156
+ expect(chars.at(-1).position.x).toBeCloseTo(-101.12976);
157
+ expect(chars.at(-1).position.y).toBeCloseTo(4.93298);
158
+ expect(chars.at(-1).angle).toBeCloseTo(1.52206);
159
159
 
160
160
  // Create ArcAlignedText - fit to arc - textAlignment = 1
161
161
  const arcTextFitToArc = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 1 });
@@ -163,14 +163,14 @@ test('ArcText getArcAlignedCharacters', () => {
163
163
  expect(chars.length).toBe(4);
164
164
  // First character
165
165
  expect(chars[0].character).toBe('T');
166
- expect(chars[0].position.x).toBeCloseTo(-101.247222);
167
- expect(chars[0].position.y).toBeCloseTo(0.74997);
168
- expect(chars[0].angle).toBeCloseTo(1.56338);
166
+ expect(chars[0].position.x).toBeCloseTo(-101.24664);
167
+ expect(chars[0].position.y).toBeCloseTo(0.82499);
168
+ expect(chars[0].angle).toBeCloseTo(1.56265);
169
169
  // Last character
170
170
  expect(chars.at(-1).character).toBe('t');
171
- expect(chars.at(-1).position.x).toBeCloseTo(101.24722);
172
- expect(chars.at(-1).position.y).toBeCloseTo(0.74997);
173
- expect(chars.at(-1).angle).toBeCloseTo(-1.56338);
171
+ expect(chars.at(-1).position.x).toBeCloseTo(101.23852);
172
+ expect(chars.at(-1).position.y).toBeCloseTo(1.52491);
173
+ expect(chars.at(-1).angle).toBeCloseTo(-1.55573);
174
174
 
175
175
  // //////// Arc Side Tests //////////
176
176
 
@@ -180,14 +180,14 @@ test('ArcText getArcAlignedCharacters', () => {
180
180
  expect(chars.length).toBe(4);
181
181
  // First character
182
182
  expect(chars[0].character).toBe('T');
183
- expect(chars[0].position.x).toBeCloseTo(-2.39222);
184
- expect(chars[0].position.y).toBeCloseTo(101.22173);
185
- expect(chars[0].angle).toBeCloseTo(0.02362);
183
+ expect(chars[0].position.x).toBeCloseTo(-2.05483);
184
+ expect(chars[0].position.y).toBeCloseTo(101.22915);
185
+ expect(chars[0].angle).toBeCloseTo(0.02030);
186
186
  // Fourth character
187
187
  expect(chars.at(-1).character).toBe('t');
188
- expect(chars.at(-1).position.x).toBeCloseTo(2.39222);
189
- expect(chars.at(-1).position.y).toBeCloseTo(101.22173);
190
- expect(chars.at(-1).angle).toBeCloseTo(-0.02362);
188
+ expect(chars.at(-1).position.x).toBeCloseTo(2.05483);
189
+ expect(chars.at(-1).position.y).toBeCloseTo(101.22915);
190
+ expect(chars.at(-1).angle).toBeCloseTo(-0.02030);
191
191
 
192
192
  // Create ArcAlignedText with string 'Test' and radius 100
193
193
  const arcTextConcave = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, arcSide: 2 });
@@ -195,14 +195,14 @@ test('ArcText getArcAlignedCharacters', () => {
195
195
  expect(chars.length).toBe(4);
196
196
  // First character
197
197
  expect(chars[0].character).toBe('T');
198
- expect(chars[0].position.x).toBeCloseTo(-2.39222);
199
- expect(chars[0].position.y).toBeCloseTo(98.72102);
200
- expect(chars[0].angle).toBeCloseTo(0.02422);
198
+ expect(chars[0].position.x).toBeCloseTo(-2.05482);
199
+ expect(chars[0].position.y).toBeCloseTo(98.72862);
200
+ expect(chars[0].angle).toBeCloseTo(0.02081);
201
201
  // Fourth character
202
202
  expect(chars.at(-1).character).toBe('t');
203
- expect(chars.at(-1).position.x).toBeCloseTo(2.39222);
204
- expect(chars.at(-1).position.y).toBeCloseTo(98.72102);
205
- expect(chars.at(-1).angle).toBeCloseTo(-0.02362);
203
+ expect(chars.at(-1).position.x).toBeCloseTo(2.05482);
204
+ expect(chars.at(-1).position.y).toBeCloseTo(98.72862);
205
+ expect(chars.at(-1).angle).toBeCloseTo(-0.02081);
206
206
  });
207
207
 
208
208
  test('ArcText.dxf', () => {
@@ -290,7 +290,21 @@ STANDARD
290
290
  expect(file.contents).toEqual(dxfString);
291
291
 
292
292
  // create new entity from entity data to ensure all props are loaded
293
- const newArcAlignedText = new ArcAlignedText(arctext);
293
+ const newArcAlignedText = new ArcAlignedText({
294
+ handle: arctext.getProperty('handle'),
295
+ points: arctext.points,
296
+ string: arctext.getProperty('string'),
297
+ styleName: arctext.getProperty('styleName'),
298
+ radius: arctext.getProperty('radius'),
299
+ height: arctext.getProperty('height'),
300
+ characterSpacing: arctext.getProperty('characterSpacing'),
301
+ offsetFromArc: arctext.getProperty('offsetFromArc'),
302
+ offsetFromRight: arctext.getProperty('offsetFromRight'),
303
+ offsetFromLeft: arctext.getProperty('offsetFromLeft'),
304
+ textOrientation: arctext.getProperty('textOrientation'),
305
+ textAlignment: arctext.getProperty('textAlignment'),
306
+ arcSide: arctext.getProperty('arcSide'),
307
+ });
294
308
  file = new File();
295
309
  newArcAlignedText.dxf(file);
296
310
  expect(file.contents).toEqual(dxfString);
@@ -301,32 +315,32 @@ test('Test ArcText.snaps', () => {
301
315
  const point = new Point(100, 100);
302
316
  const snaps = arcText.snaps(point, 1);
303
317
  const nodeSnaps = snaps.filter((s) => s.type === 'node');
304
- expect(nodeSnaps[0].snapPoint.x).toBeCloseTo(-2.39222);
305
- expect(nodeSnaps[0].snapPoint.y).toBeCloseTo(101.22173);
318
+ expect(nodeSnaps[0].snapPoint.x).toBeCloseTo(-2.05483);
319
+ expect(nodeSnaps[0].snapPoint.y).toBeCloseTo(101.22915);
306
320
  });
307
321
 
308
322
  test('Test ArcText.closestPoint', () => {
309
323
  const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100 });
310
324
  const point = new Point(100, 100);
311
325
  const closest = arcText.closestPoint(point);
312
- expect(closest[0].x).toBeCloseTo(2.39222);
313
- expect(closest[0].y).toBeCloseTo(101.22173);
326
+ expect(closest[0].x).toBeCloseTo(2.05483);
327
+ expect(closest[0].y).toBeCloseTo(101.22915);
314
328
  });
315
329
 
316
330
  test('Test ArcText.boundingBox', () => {
317
331
  const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100 });
318
- expect(arcText.boundingBox().xMin).toBeCloseTo(-3.14222);
319
- expect(arcText.boundingBox().xMax).toBeCloseTo(3.14222);
320
- expect(arcText.boundingBox().yMin).toBeCloseTo(99.97173);
321
- expect(arcText.boundingBox().yMax).toBeCloseTo(102.49685);
332
+ expect(arcText.boundingBox().xMin).toBeCloseTo(-2.81733);
333
+ expect(arcText.boundingBox().xMax).toBeCloseTo(2.46733);
334
+ expect(arcText.boundingBox().yMin).toBeCloseTo(99.97915);
335
+ expect(arcText.boundingBox().yMax).toBeCloseTo(102.49878);
322
336
  });
323
337
 
324
338
  test('Test ArcText.toPolylinePoints - returns correct points array', () => {
325
339
  const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100 });
326
340
  const result = arcText.toPolylinePoints();
327
341
  expect(result.length).toBe(4);
328
- expect(result[0].x).toBeCloseTo(-2.39222);
329
- expect(result[0].y).toBeCloseTo(101.22173);
342
+ expect(result[0].x).toBeCloseTo(-2.05483);
343
+ expect(result[0].y).toBeCloseTo(101.22915);
330
344
  });
331
345
 
332
346
  test('ArcText.snaps returns a centre snap at points[0]', () => {
@@ -343,3 +357,116 @@ test('ArcText.snaps returns node snaps only for non-space characters', () => {
343
357
  const nodeSnaps = arcText.snaps(new Point(0, 0), 100).filter((s) => s.type === 'node');
344
358
  expect(nodeSnaps.length).toBe(2);
345
359
  });
360
+
361
+ test('ArcAlignedCharacter respects explicit height and width', () => {
362
+ const ac = new ArcAlignedCharacter('A', new Point(0, 0), 0, 5, 3);
363
+ expect(ac.height).toBe(5);
364
+ expect(ac.width).toBe(3);
365
+ });
366
+
367
+ test('ArcText getArcAlignedCharacters empty string returns empty array', () => {
368
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: '', radius: 100 });
369
+ expect(arcText.getArcAlignedCharacters()).toHaveLength(0);
370
+ });
371
+
372
+ test('ArcText boundingBox empty string returns BoundingBox instance', () => {
373
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: '', radius: 100 });
374
+ expect(arcText.boundingBox()).toBeInstanceOf(BoundingBox);
375
+ });
376
+
377
+ test('ArcText toPolylinePoints empty string returns empty array', () => {
378
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: '', radius: 100 });
379
+ expect(arcText.toPolylinePoints()).toHaveLength(0);
380
+ });
381
+
382
+ test('ArcText snaps with empty string returns only centre snap', () => {
383
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: '', radius: 100 });
384
+ const snaps = arcText.snaps(new Point(0, 0), 1);
385
+ expect(snaps).toHaveLength(1);
386
+ expect(snaps[0].type).toBe('centre');
387
+ });
388
+
389
+ test('ArcText closestPoint returns distance 0.1 when within height * 0.35', () => {
390
+ // Single char 'T' center-aligned sits at (0, 101.25); height = 2.5, threshold = 0.875
391
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'T', radius: 100 });
392
+ const chars = arcText.getArcAlignedCharacters();
393
+ const charPos = chars[0].position;
394
+ // 0.5 units away is within the 0.875 threshold
395
+ const closePoint = new Point(charPos.x + 0.5, charPos.y);
396
+ const [, dist] = arcText.closestPoint(closePoint);
397
+ expect(dist).toBe(0.1);
398
+ });
399
+
400
+ test('ArcText getArcAlignedCharacters fit-to-arc single character places at midpoint', () => {
401
+ const t = new ArcAlignedText({ points: [new Point(0, 0)], radius: 10, string: 'X',
402
+ startAngle: 0, endAngle: 180, textAlignment: 1 });
403
+ const chars = t.getArcAlignedCharacters();
404
+ expect(chars).toHaveLength(1);
405
+ // midpoint of 0°–180° arc at radius 10 is 90° → point near (0, 10)
406
+ expect(chars[0].position.x).toBeCloseTo(0, 0);
407
+ expect(chars[0].position.y).toBeGreaterThan(0);
408
+ });
409
+
410
+ test('ArcText getArcAlignedCharacters single character is placed at arc midpoint', () => {
411
+ // Single char with center alignment: charOffsetAngles = [0], stringStartPoint = arcMidPoint
412
+ // arcMidAngle(0, PI) = PI/2 → project(PI/2, 101.25) = (0, 101.25), angle = PI/2 - PI/2 = 0
413
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'X', radius: 100 });
414
+ const chars = arcText.getArcAlignedCharacters();
415
+ expect(chars).toHaveLength(1);
416
+ expect(chars[0].character).toBe('X');
417
+ expect(chars[0].position.x).toBeCloseTo(0, 4);
418
+ expect(chars[0].position.y).toBeCloseTo(101.25, 4);
419
+ expect(chars[0].angle).toBeCloseTo(0, 4);
420
+ });
421
+
422
+ test('ArcText getArcAlignedCharacters textOrientation inward reverses character order', () => {
423
+ // textOrientation 2 reverses the string before placing characters
424
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textOrientation: 2 });
425
+ const chars = arcText.getArcAlignedCharacters();
426
+ expect(chars).toHaveLength(4);
427
+ expect(chars[0].character).toBe('t'); // reversed
428
+ expect(chars.at(-1).character).toBe('T'); // reversed
429
+ });
430
+
431
+ test('ArcText getArcAlignedCharacters offsetFromLeft shifts character positions', () => {
432
+ // offsetFromLeft moves the string start away from the arc end for left-aligned text
433
+ const base = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 2, offsetFromLeft: 0 });
434
+ const offset = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 2, offsetFromLeft: 10 });
435
+ const baseChars = base.getArcAlignedCharacters();
436
+ const offsetChars = offset.getArcAlignedCharacters();
437
+ expect(offsetChars[0].position.x).not.toBeCloseTo(baseChars[0].position.x, 1);
438
+ });
439
+
440
+ test('ArcText getArcAlignedCharacters offsetFromRight shifts character positions', () => {
441
+ // offsetFromRight moves the string start away from the arc start for right-aligned text
442
+ const base = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 3, offsetFromRight: 0 });
443
+ const offset = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 3, offsetFromRight: 10 });
444
+ const baseChars = base.getArcAlignedCharacters();
445
+ const offsetChars = offset.getArcAlignedCharacters();
446
+ expect(offsetChars[0].position.x).not.toBeCloseTo(baseChars[0].position.x, 1);
447
+ });
448
+
449
+ test('ArcAlignedText.toCharacters returns one entry per character with scene coords', () => {
450
+ const arc = new ArcAlignedText({ points: [new Point(0, 0)], string: 'AB', radius: 100 });
451
+ const chars = arc.toCharacters();
452
+ expect(chars).toHaveLength(2);
453
+ expect(chars[0].char).toBe('A');
454
+ expect(chars[1].char).toBe('B');
455
+ expect(typeof chars[0].x).toBe('number');
456
+ expect(typeof chars[0].y).toBe('number');
457
+ expect(typeof chars[0].rotation).toBe('number');
458
+ });
459
+
460
+ test('ArcAlignedText.draw calls renderer.drawText with all characters', () => {
461
+ const arc = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Hi', radius: 50 });
462
+ let calledChars;
463
+ const mockRenderer = {
464
+ drawText(characters) {
465
+ calledChars = characters;
466
+ },
467
+ };
468
+ arc.draw(mockRenderer);
469
+ expect(calledChars).toHaveLength(2);
470
+ expect(calledChars[0].char).toBe('H');
471
+ expect(calledChars[1].char).toBe('i');
472
+ });
@@ -46,6 +46,18 @@ test.each(inputScenarios)('Polyline.execute handles $desc', async (scenario) =>
46
46
  }, { extraMethods: { actionCommand: () => {} } });
47
47
  });
48
48
 
49
+ test('Test BasePolyline.draw calls renderer.drawShape with toPolylinePoints', () => {
50
+ const polyline = new Polyline({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 5)] });
51
+ let capturedPoints;
52
+ const mockRenderer = {
53
+ drawShape(points) {
54
+ capturedPoints = points;
55
+ },
56
+ };
57
+ polyline.draw(mockRenderer);
58
+ expect(capturedPoints).toEqual(polyline.toPolylinePoints());
59
+ });
60
+
49
61
  test('Test BasePolyline.getClosestSegment', () => {
50
62
  // Polyline with a line and an arc segment
51
63
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10)];
@@ -322,7 +334,7 @@ AcDbPolyline
322
334
  6
323
335
  ByLayer
324
336
  39
325
- 2
337
+ 1
326
338
  90
327
339
  3
328
340
  70
@@ -350,7 +362,7 @@ ByLayer
350
362
  expect(file.contents).toEqual(dxfString);
351
363
 
352
364
  // create new entity from entity data to ensure all props are loaded
353
- const newPolyline = new Polyline(polyline);
365
+ const newPolyline = new Polyline({ handle: polyline.getProperty('handle'), points: polyline.points });
354
366
  file = new File();
355
367
  newPolyline.dxf(file);
356
368
 
@@ -687,3 +699,57 @@ test('BasePolyline.closestPoint is consistent with getClosestSegmentIndex for cl
687
699
  expect(closest.x).toBeCloseTo(0);
688
700
  expect(closest.y).toBeCloseTo(5);
689
701
  });
702
+
703
+ // ─── preview ─────────────────────────────────────────────────────────────────
704
+
705
+ test('BasePolyline.preview - 0 points does nothing', () => {
706
+ const poly = new BasePolyline({});
707
+ DesignCore.Scene.auxiliaryEntities.clear();
708
+ DesignCore.Scene.previewEntities.clear();
709
+
710
+ expect(() => poly.preview()).not.toThrow();
711
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
712
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
713
+ });
714
+
715
+ test('BasePolyline.preview - LINE mode creates temp entity', () => {
716
+ const poly = new BasePolyline({});
717
+ poly.points.push(new Point(0, 0));
718
+
719
+ const origCreate = DesignCore.Scene.previewEntities.create;
720
+ const calls = [];
721
+ DesignCore.Scene.previewEntities.create = (type, data) => calls.push({ type, data });
722
+
723
+ DesignCore.Scene.auxiliaryEntities.clear();
724
+ poly.preview();
725
+
726
+ expect(calls.length).toBeGreaterThanOrEqual(1);
727
+ // LINE mode: no rubber-band auxiliary entity
728
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
729
+
730
+ DesignCore.Scene.previewEntities.create = origCreate;
731
+ });
732
+
733
+ test('BasePolyline.preview - ARC mode adds rubber band and creates arc temp entity', () => {
734
+ const poly = new BasePolyline({});
735
+ poly.points.push(new Point(0, 0));
736
+ poly.points.push(new Point(10, 0));
737
+ poly.inputMode = poly.modes.ARC;
738
+
739
+ const origAdd = DesignCore.Scene.auxiliaryEntities.add;
740
+ const origCreate = DesignCore.Scene.previewEntities.create;
741
+ const auxAdded = [];
742
+ const tempCreated = [];
743
+ DesignCore.Scene.auxiliaryEntities.add = (item) => auxAdded.push(item);
744
+ DesignCore.Scene.previewEntities.create = (type, data) => tempCreated.push({ type, data });
745
+
746
+ poly.preview();
747
+
748
+ // ARC mode: rubber-band drawn in auxiliaryEntities
749
+ expect(auxAdded.length).toBeGreaterThanOrEqual(1);
750
+ // ARC mode: arc preview drawn in previewEntities
751
+ expect(tempCreated.length).toBeGreaterThanOrEqual(1);
752
+
753
+ DesignCore.Scene.auxiliaryEntities.add = origAdd;
754
+ DesignCore.Scene.previewEntities.create = origCreate;
755
+ });
@@ -96,7 +96,7 @@ ByLayer
96
96
  30
97
97
  0.0
98
98
  39
99
- 2
99
+ 1
100
100
  40
101
101
  100
102
102
  `;
@@ -105,7 +105,7 @@ ByLayer
105
105
 
106
106
 
107
107
  // create new entity from entity data to ensure all props are loaded
108
- const newCircle = new Circle(circle);
108
+ const newCircle = new Circle({ handle: circle.getProperty('handle'), points: circle.points });
109
109
  file = new File();
110
110
  newCircle.dxf(file);
111
111
  expect(file.contents).toEqual(dxfString);
@@ -127,73 +127,6 @@ test('Test Circle.toPolylinePoints', () => {
127
127
  expect(circlePoints[2].bulge).toBe(0);
128
128
  });
129
129
 
130
- test('Test Circle.trim returns empty array when provided with empty intersection list', () => {
131
- const circle = new Circle({ points: [new Point(0, 0), new Point(100, 0)] });
132
-
133
- expect(circle.trim([])).toEqual([]);
134
- expect(circle.trim()).toEqual([]);
135
- });
136
-
137
- test('Test Circle.trim does not modify circle', () => {
138
- const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
139
-
140
- DesignCore.Mouse.pointOnScene = () => new Point(7.71, 7.71);
141
-
142
- // test a single intersection - not valid
143
- const intersections = [new Point(10, 0)];
144
- expect(circle.trim(intersections)).toEqual([]);
145
-
146
- // test two intersections - should return an arc
147
- intersections.push(new Point(-10, 0));
148
- let trimWithTwoPoints = circle.trim(intersections);
149
- let trimResult = trimWithTwoPoints[0].entity;
150
- expect(trimResult.type).toEqual('Arc');
151
- // Verify trimmed arc has a unique handle (not the circle's)
152
- expect(trimResult.handle).toBeUndefined();
153
- expect(trimResult.points[0].x).toEqual(0);
154
- expect(trimResult.points[0].y).toEqual(0);
155
-
156
- expect(trimResult.points[1].x).toEqual(10);
157
- expect(trimResult.points[1].y).toEqual(0);
158
-
159
- expect(trimResult.points[2].x).toEqual(-10);
160
- expect(trimResult.points[2].y).toEqual(0);
161
-
162
- expect(trimWithTwoPoints[1].entity).toEqual(circle);
163
-
164
- // test three intersections - should return an arc from first two points found
165
- intersections.push(new Point(0, 10));
166
- trimWithTwoPoints = circle.trim(intersections);
167
- trimResult = trimWithTwoPoints[0].entity;
168
- expect(trimResult.type).toEqual('Arc');
169
- expect(trimResult.points[0].x).toEqual(0);
170
- expect(trimResult.points[0].y).toEqual(0);
171
-
172
- expect(trimResult.points[1].x).toEqual(10);
173
- expect(trimResult.points[1].y).toEqual(0);
174
-
175
- expect(trimResult.points[2].x).toEqual(0);
176
- expect(trimResult.points[2].y).toEqual(10);
177
-
178
- expect(trimWithTwoPoints[1].entity).toEqual(circle);
179
-
180
- // test four intersections - should return an arc from first two points found
181
- intersections.push(new Point(0, -10));
182
- trimWithTwoPoints = circle.trim(intersections);
183
- trimResult = trimWithTwoPoints[0].entity;
184
- expect(trimResult.type).toEqual('Arc');
185
- expect(trimResult.points[0].x).toEqual(0);
186
- expect(trimResult.points[0].y).toEqual(0);
187
-
188
- expect(trimResult.points[1].x).toEqual(10);
189
- expect(trimResult.points[1].y).toEqual(0);
190
-
191
- expect(trimResult.points[2].x).toEqual(0);
192
- expect(trimResult.points[2].y).toEqual(10);
193
-
194
- expect(trimWithTwoPoints[1].entity).toEqual(circle);
195
- });
196
-
197
130
  test('Circle.execute creates circle from diameter input', async () => {
198
131
  const center = new Point(0, 0);
199
132
 
@@ -234,8 +167,8 @@ test('Circle.register returns command object', () => {
234
167
  expect(Circle.register()).toEqual({ command: 'Circle', shortcut: 'C', type: 'Entity' });
235
168
  });
236
169
 
237
- test('Circle constructor loads radius from DXF group code 40', () => {
238
- const circle = new Circle({ points: [new Point(0, 0)], 40: 50 });
170
+ test('Circle constructor loads radius from DXF group code 40 via fromDxf', () => {
171
+ const circle = new Circle(Circle.fromDxf({ points: [new Point(0, 0)], 40: 50 }));
239
172
  expect(circle.getRadius()).toBeCloseTo(50);
240
173
  expect(circle.points[1].x).toBeCloseTo(50);
241
174
  expect(circle.points[1].y).toBeCloseTo(0);
@@ -263,10 +196,10 @@ test('Circle.execute re-prompts on zero-distance point input', async () => {
263
196
  });
264
197
 
265
198
  test('Circle.preview does not throw with 0 or 1 points', () => {
266
- const origCreate = DesignCore.Scene.tempEntities.create;
199
+ const origCreate = DesignCore.Scene.previewEntities.create;
267
200
  const origMouse = DesignCore.Mouse.pointOnScene;
268
201
  const calls = [];
269
- DesignCore.Scene.tempEntities.create = (type, obj) => calls.push([type, obj]);
202
+ DesignCore.Scene.previewEntities.create = (type, obj) => calls.push([type, obj]);
270
203
  DesignCore.Mouse.pointOnScene = () => new Point(5, 5);
271
204
 
272
205
  const circle0 = new Circle({});
@@ -278,16 +211,20 @@ test('Circle.preview does not throw with 0 or 1 points', () => {
278
211
  expect(() => circle1.preview()).not.toThrow();
279
212
  expect(calls.some(([type]) => type === 'Circle')).toBe(true);
280
213
 
281
- DesignCore.Scene.tempEntities.create = origCreate;
214
+ DesignCore.Scene.previewEntities.create = origCreate;
282
215
  DesignCore.Mouse.pointOnScene = origMouse;
283
216
  });
284
217
 
285
- test('Circle.draw does not throw', () => {
218
+ test('Circle.draw calls renderer.drawShape with toPolylinePoints', () => {
286
219
  const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
287
- const ctx = { arc: jest.fn(), stroke: jest.fn() };
288
- expect(() => circle.draw(ctx, 1)).not.toThrow();
289
- expect(ctx.arc).toHaveBeenCalledTimes(1);
290
- expect(ctx.stroke).toHaveBeenCalledTimes(1);
220
+ let capturedPoints;
221
+ const mockRenderer = {
222
+ drawShape(points) {
223
+ capturedPoints = points;
224
+ },
225
+ };
226
+ circle.draw(mockRenderer);
227
+ expect(capturedPoints).toEqual(circle.toPolylinePoints());
291
228
  });
292
229
 
293
230
  test('Circle.snaps returns centre snap', () => {
@@ -326,21 +263,21 @@ test('Circle.snaps nearest snap does not fire when too far', () => {
326
263
  test('Circle.fromPolylinePoints round-trip preserves centre and radius', () => {
327
264
  const circle = new Circle({ points: [new Point(5, 10), new Point(5, 10).project(0, 8)] });
328
265
  const polyPts = circle.toPolylinePoints();
329
- const rebuilt = new Circle({});
330
- rebuilt.fromPolylinePoints(polyPts);
266
+ const rebuilt = new Circle({}).fromPolylinePoints(polyPts);
267
+ expect(rebuilt.type).toBe('Circle');
331
268
  expect(rebuilt.points[0].x).toBeCloseTo(5, 10);
332
269
  expect(rebuilt.points[0].y).toBeCloseTo(10, 10);
333
- expect(rebuilt.radius).toBeCloseTo(8, 10);
270
+ expect(rebuilt.getProperty('radius')).toBeCloseTo(8, 10);
334
271
  });
335
272
 
336
273
  test('Circle.fromPolylinePoints precision drift is negligible', () => {
337
274
  const circle = new Circle({ points: [new Point(123.456, -789.012), new Point(123.456, -789.012).project(1.234, 56.789)] });
338
275
  const polyPts = circle.toPolylinePoints();
339
- const rebuilt = new Circle({});
340
- rebuilt.fromPolylinePoints(polyPts);
276
+ const rebuilt = new Circle({}).fromPolylinePoints(polyPts);
277
+ expect(rebuilt.type).toBe('Circle');
341
278
  const drift = Math.hypot(rebuilt.points[0].x - circle.points[0].x, rebuilt.points[0].y - circle.points[0].y);
342
279
  expect(drift).toBeLessThan(1e-10);
343
- expect(Math.abs(rebuilt.radius - circle.radius)).toBeLessThan(1e-10);
280
+ expect(Math.abs(rebuilt.getProperty('radius') - circle.getProperty('radius'))).toBeLessThan(1e-10);
344
281
  });
345
282
 
346
283
  test('Circle.snaps returns two tangent snaps from outside', () => {
@@ -357,6 +294,41 @@ test('Circle.snaps returns two tangent snaps from outside', () => {
357
294
  DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
358
295
  });
359
296
 
297
+ describe('Circle.fromDxf', () => {
298
+ test('projects edge point from DXF radius code', () => {
299
+ const center = new Point(0, 0);
300
+ const result = Circle.fromDxf({ points: [center], 40: 10 });
301
+ expect(result.points.length).toBe(2);
302
+ expect(result.points[0]).toBe(center);
303
+ expect(result.points[1].x).toBeCloseTo(10);
304
+ expect(result.points[1].y).toBeCloseTo(0);
305
+ });
306
+
307
+ test('preserves all other data properties', () => {
308
+ const result = Circle.fromDxf({ points: [new Point(0, 0)], 40: 10, 8: 'MyLayer' });
309
+ expect(result[8]).toBe('MyLayer');
310
+ });
311
+
312
+ test('returns data unchanged when no center point', () => {
313
+ const data = { 40: 10 };
314
+ expect(Circle.fromDxf(data)).toBe(data);
315
+ });
316
+
317
+ test('returns data unchanged when no radius code', () => {
318
+ const data = { points: [new Point(0, 0)] };
319
+ expect(Circle.fromDxf(data)).toBe(data);
320
+ });
321
+
322
+ test('circle constructed via fromDxf matches circle constructed directly', () => {
323
+ const center = new Point(100, 100);
324
+ const fromDxfCircle = new Circle(Circle.fromDxf({ points: [center], 40: 50 }));
325
+ const directCircle = new Circle({ points: [center, new Point(150, 100)] });
326
+ expect(fromDxfCircle.getRadius()).toBeCloseTo(directCircle.getRadius());
327
+ expect(fromDxfCircle.points[0].x).toBeCloseTo(directCircle.points[0].x);
328
+ expect(fromDxfCircle.points[0].y).toBeCloseTo(directCircle.points[0].y);
329
+ });
330
+ });
331
+
360
332
  test('Circle.snaps returns no tangent when fromPoint is inside the circle', () => {
361
333
  const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
362
334
  const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;