@design-core/core 0.0.5 → 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 +33 -284
  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 +36 -13
  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 +129 -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 +296 -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
 
@@ -626,3 +638,118 @@ test('BasePolyline.fromPolylinePoints round-trip strips closure point for closed
626
638
  expect(rebuilt.points[3].x).toBe(0);
627
639
  expect(rebuilt.points[3].y).toBe(10);
628
640
  });
641
+
642
+ // ─── closestPoint: closed polyline closing segment ───────────────────────────
643
+
644
+ test('BasePolyline.closestPoint finds closest point on closing segment of closed polyline', () => {
645
+ // Square: P0=(0,0), P1=(10,0), P2=(10,10), P3=(0,10), closed flag set.
646
+ // Closing segment is P3=(0,10) → P0=(0,0).
647
+ // A test point on the left edge (0, 5) must resolve to (0, 5) on the closing segment.
648
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
649
+ const poly = new BasePolyline({ points });
650
+ poly.flags.setFlagValue(1);
651
+
652
+ const [closest, dist] = poly.closestPoint(new Point(0, 5));
653
+ expect(closest.x).toBeCloseTo(0);
654
+ expect(closest.y).toBeCloseTo(5);
655
+ expect(dist).toBeCloseTo(0);
656
+ });
657
+
658
+ test('BasePolyline.closestPoint closing segment is preferred over other segments', () => {
659
+ // The test point (-1, 5) is clearly nearest the closing segment (left edge),
660
+ // not the bottom, right, or top edges.
661
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
662
+ const poly = new BasePolyline({ points });
663
+ poly.flags.setFlagValue(1);
664
+
665
+ const [closest, dist] = poly.closestPoint(new Point(-1, 5));
666
+ expect(closest.x).toBeCloseTo(0);
667
+ expect(closest.y).toBeCloseTo(5);
668
+ expect(dist).toBeCloseTo(1);
669
+ });
670
+
671
+ test('BasePolyline.closestPoint for open polyline does not include closing segment', () => {
672
+ // For point (-1, 1) on the open square:
673
+ // – Nearest on seg P0→P1 (y=0): foot (-1,0) clips to (0,0), dist = √2 ≈ 1.41
674
+ // – Nearest on seg P2→P3 (y=10): foot (-1,10) clips to (0,10), dist ≈ 9.06
675
+ // – If the closing segment existed (x=0, y∈[0,10]): foot (0,1), dist = 1 ← closer!
676
+ // An open polyline must NOT check the closing segment, so it returns (0,0).
677
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
678
+ const poly = new BasePolyline({ points });
679
+ // flag NOT set — open polyline
680
+
681
+ const [closest] = poly.closestPoint(new Point(-1, 1));
682
+ expect(closest.x).toBeCloseTo(0);
683
+ expect(closest.y).toBeCloseTo(0);
684
+ });
685
+
686
+ test('BasePolyline.closestPoint is consistent with getClosestSegmentIndex for closed polyline', () => {
687
+ // For a closed square, both methods should agree that the closing segment
688
+ // is nearest for a point on the left edge.
689
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
690
+ const poly = new BasePolyline({ points });
691
+ poly.flags.setFlagValue(1);
692
+
693
+ const testPoint = new Point(-2, 5);
694
+ const [closest] = poly.closestPoint(testPoint);
695
+ const segIdx = poly.getClosestSegmentIndex(testPoint);
696
+
697
+ // Closing segment index == points.length == 4
698
+ expect(segIdx).toBe(points.length);
699
+ expect(closest.x).toBeCloseTo(0);
700
+ expect(closest.y).toBeCloseTo(5);
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
+ });