@design-core/core 0.0.4 → 0.0.6

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 (56) hide show
  1. package/core/entities/arc.js +39 -22
  2. package/core/entities/arctext.js +6 -4
  3. package/core/entities/basePolyline.js +29 -20
  4. package/core/entities/circle.js +33 -15
  5. package/core/entities/entity.js +1 -1
  6. package/core/entities/hatch.js +2 -0
  7. package/core/entities/insert.js +9 -3
  8. package/core/entities/line.js +16 -11
  9. package/core/entities/lwpolyline.js +2 -0
  10. package/core/entities/point.js +3 -1
  11. package/core/entities/polyline.js +2 -0
  12. package/core/entities/rectangle.js +2 -0
  13. package/core/entities/solid.js +2 -0
  14. package/core/entities/text.js +5 -8
  15. package/core/lib/input.js +8 -4
  16. package/core/lib/inputManager.js +3 -5
  17. package/core/lib/selectionManager.js +4 -0
  18. package/core/lib/settings.js +9 -5
  19. package/core/lib/snapping.js +112 -17
  20. package/core/tables/block.js +2 -0
  21. package/core/tools/chamfer.js +2 -0
  22. package/core/tools/chamferFilletBase.js +2 -0
  23. package/core/tools/copy.js +2 -0
  24. package/core/tools/copybase.js +2 -0
  25. package/core/tools/copyclip.js +2 -0
  26. package/core/tools/cutclip.js +2 -0
  27. package/core/tools/distance.js +2 -0
  28. package/core/tools/erase.js +2 -0
  29. package/core/tools/explode.js +2 -0
  30. package/core/tools/extend.js +2 -0
  31. package/core/tools/fillet.js +5 -3
  32. package/core/tools/identify.js +2 -0
  33. package/core/tools/matchProp.js +2 -0
  34. package/core/tools/mirror.js +2 -0
  35. package/core/tools/move.js +2 -0
  36. package/core/tools/offset.js +2 -0
  37. package/core/tools/pan.js +2 -0
  38. package/core/tools/pasteclip.js +2 -0
  39. package/core/tools/purge.js +2 -0
  40. package/core/tools/rotate.js +2 -0
  41. package/core/tools/scale.js +2 -0
  42. package/core/tools/tool.js +1 -1
  43. package/core/tools/trim.js +2 -0
  44. package/core/tools/zoom.js +2 -0
  45. package/package.json +1 -1
  46. package/test/entities/arc.test.js +60 -11
  47. package/test/entities/arctext.test.js +18 -2
  48. package/test/entities/basePolyline.test.js +61 -0
  49. package/test/entities/circle.test.js +48 -22
  50. package/test/entities/insert.test.js +14 -4
  51. package/test/entities/line.test.js +53 -0
  52. package/test/entities/text.test.js +10 -1
  53. package/test/lib/input.test.js +117 -0
  54. package/test/lib/settings.test.js +5 -7
  55. package/test/lib/snapping.test.js +218 -12
  56. package/test/tools/fillet.test.js +115 -0
@@ -1,6 +1,6 @@
1
1
  import { jest } from '@jest/globals';
2
2
  import { Core } from '../../core/core/core.js';
3
- import { Snapping } from '../../core/lib/snapping.js';
3
+ import { Snapping, SnapPoint } from '../../core/lib/snapping.js';
4
4
  import { Point } from '../../core/entities/point.js';
5
5
  import { DesignCore } from '../../core/designCore.js';
6
6
 
@@ -24,16 +24,16 @@ test('Test Snapping.getSnapPoint', () => {
24
24
  core.mouse.mouseMoved(8, 8);
25
25
  // Get the snap point
26
26
  const snapPoint1 = snapping.getSnapPoint();
27
- expect(snapPoint1.x).toBe(10);
28
- expect(snapPoint1.y).toBe(10);
27
+ expect(snapPoint1.snapPoint.x).toBe(10);
28
+ expect(snapPoint1.snapPoint.y).toBe(10);
29
29
 
30
30
 
31
31
  // set the mouse position
32
32
  core.mouse.mouseMoved(101, 11);
33
33
  // Get the snap point
34
34
  const snapPoint2 = snapping.getSnapPoint();
35
- expect(snapPoint2.x).toBe(100);
36
- expect(snapPoint2.y).toBe(10);
35
+ expect(snapPoint2.snapPoint.x).toBe(100);
36
+ expect(snapPoint2.snapPoint.y).toBe(10);
37
37
 
38
38
  // set the mouse position
39
39
  core.mouse.mouseMoved(100, 100);
@@ -48,8 +48,8 @@ test('Test Snapping.snap active', () => {
48
48
  core.mouse.mouseMoved(8, 8);
49
49
  const result = snapping.snap();
50
50
  expect(result).toBeDefined();
51
- expect(result.x).toBe(10);
52
- expect(result.y).toBe(10);
51
+ expect(result.snapPoint.x).toBe(10);
52
+ expect(result.snapPoint.y).toBe(10);
53
53
  expect(DesignCore.Scene.auxiliaryEntities.count()).toBeGreaterThan(0);
54
54
  });
55
55
 
@@ -61,24 +61,29 @@ test('Test Snapping.snap inactive', () => {
61
61
 
62
62
  test('Test Snapping.addSnapPoint', () => {
63
63
  DesignCore.Scene.auxiliaryEntities.clear();
64
- snapping.addSnapPoint(new Point(10, 10));
64
+ snapping.addSnapPoint(new SnapPoint(new Point(10, 10)));
65
65
  expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(1);
66
66
  });
67
67
 
68
68
  test('Test SnapPoint.draw', () => {
69
69
  DesignCore.Scene.auxiliaryEntities.clear();
70
- snapping.addSnapPoint(new Point(0.5, 0.5));
70
+ snapping.addSnapPoint(new SnapPoint(new Point(0.5, 0.5), SnapPoint.Type.CENTRE));
71
71
  const snapPointEntity = DesignCore.Scene.auxiliaryEntities.get(0);
72
72
  const ctx = {
73
- fillStyle: '',
73
+ strokeStyle: '',
74
+ lineWidth: 0,
75
+ setLineDash: jest.fn(),
74
76
  beginPath: jest.fn(),
77
+ moveTo: jest.fn(),
78
+ lineTo: jest.fn(),
79
+ closePath: jest.fn(),
75
80
  arc: jest.fn(),
76
- fill: jest.fn(),
81
+ stroke: jest.fn(),
77
82
  };
78
83
  snapPointEntity.draw(ctx, 1);
79
84
  expect(ctx.beginPath).toHaveBeenCalled();
80
85
  expect(ctx.arc).toHaveBeenCalledWith(0.5, 0.5, expect.any(Number), 0, 6.283);
81
- expect(ctx.fill).toHaveBeenCalled();
86
+ expect(ctx.stroke).toHaveBeenCalled();
82
87
  });
83
88
 
84
89
  test('Test Snapping.addTrackingLine', () => {
@@ -193,3 +198,204 @@ test('Test Snapping.orthoSnap', () => {
193
198
  core.mouse.mouseMoved(100, -20);
194
199
  expect(snapping.orthoSnap(new Point(0, 0))).toBeUndefined();
195
200
  });
201
+
202
+ // ─── Context mock helpers ────────────────────────────────────────────────────
203
+
204
+ /** @return {object} mock HTML canvas context for SnapPoint.draw tests */
205
+ function makeSnapCtx() {
206
+ return {
207
+ strokeStyle: '',
208
+ lineWidth: 0,
209
+ setLineDash: jest.fn(),
210
+ beginPath: jest.fn(),
211
+ moveTo: jest.fn(),
212
+ lineTo: jest.fn(),
213
+ closePath: jest.fn(),
214
+ arc: jest.fn(),
215
+ stroke: jest.fn(),
216
+ };
217
+ }
218
+
219
+ /** @return {object} mock HTML canvas context for TrackingLine.draw tests */
220
+ function makeTrackingCtx() {
221
+ return {
222
+ save: jest.fn(),
223
+ restore: jest.fn(),
224
+ beginPath: jest.fn(),
225
+ strokeStyle: '',
226
+ lineWidth: 0,
227
+ setLineDash: jest.fn(),
228
+ moveTo: jest.fn(),
229
+ lineTo: jest.fn(),
230
+ stroke: jest.fn(),
231
+ };
232
+ }
233
+
234
+ // ─── Snapping.reset ──────────────────────────────────────────────────────────
235
+
236
+ test('Test Snapping.reset', () => {
237
+ snapping.active = true;
238
+ snapping.snapOverride = SnapPoint.Type.END;
239
+ snapping.reset();
240
+ expect(snapping.active).toBe(false);
241
+ expect(snapping.snapOverride).toBeNull();
242
+ });
243
+
244
+ // ─── Snapping.setSnapOverride ────────────────────────────────────────────────
245
+
246
+ test('Test Snapping.setSnapOverride valid type', () => {
247
+ snapping.setSnapOverride(SnapPoint.Type.END);
248
+ expect(snapping.snapOverride).toBe(SnapPoint.Type.END);
249
+ });
250
+
251
+ test('Test Snapping.setSnapOverride null clears override', () => {
252
+ snapping.setSnapOverride(SnapPoint.Type.END);
253
+ snapping.setSnapOverride(null);
254
+ expect(snapping.snapOverride).toBeNull();
255
+ });
256
+
257
+ test('Test Snapping.setSnapOverride NONE type', () => {
258
+ snapping.setSnapOverride(SnapPoint.Type.NONE);
259
+ expect(snapping.snapOverride).toBe(SnapPoint.Type.NONE);
260
+ snapping.setSnapOverride(null);
261
+ });
262
+
263
+ test('Test Snapping.setSnapOverride invalid type throws', () => {
264
+ expect(() => snapping.setSnapOverride('invalid')).toThrow('Invalid snap override type: invalid');
265
+ });
266
+
267
+ // ─── Snapping.getSnapPoint with snap override ────────────────────────────────
268
+
269
+ test('Test Snapping.getSnapPoint with END override returns end snap', () => {
270
+ snapping.setSnapOverride(SnapPoint.Type.END);
271
+ core.mouse.mouseMoved(8, 8);
272
+ const result = snapping.getSnapPoint();
273
+ expect(result).toBeDefined();
274
+ expect(result.type).toBe(SnapPoint.Type.END);
275
+ snapping.setSnapOverride(null);
276
+ });
277
+
278
+ test('Test Snapping.getSnapPoint with NONE override suppresses all snaps', () => {
279
+ snapping.setSnapOverride(SnapPoint.Type.NONE);
280
+ core.mouse.mouseMoved(8, 8);
281
+ const result = snapping.getSnapPoint();
282
+ expect(result).toBeUndefined();
283
+ snapping.setSnapOverride(null);
284
+ });
285
+
286
+ test('Test Snapping.getSnapPoint with non-matching override returns undefined', () => {
287
+ // Mouse near end of line at (10,10); mid-point is at (55,10), well outside aperture
288
+ snapping.setSnapOverride(SnapPoint.Type.MID);
289
+ core.mouse.mouseMoved(8, 8);
290
+ const result = snapping.getSnapPoint();
291
+ expect(result).toBeUndefined();
292
+ snapping.setSnapOverride(null);
293
+ });
294
+
295
+ // ─── SnapPoint.draw – all snap types ────────────────────────────────────────
296
+
297
+ test('Test SnapPoint.draw END type draws closed polygon without arc', () => {
298
+ const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.END);
299
+ const ctx = makeSnapCtx();
300
+ sp.draw(ctx, 1);
301
+ expect(ctx.beginPath).toHaveBeenCalled();
302
+ expect(ctx.closePath).toHaveBeenCalled();
303
+ expect(ctx.stroke).toHaveBeenCalled();
304
+ expect(ctx.arc).not.toHaveBeenCalled();
305
+ });
306
+
307
+ test('Test SnapPoint.draw MID type draws closed polygon without arc', () => {
308
+ const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.MID);
309
+ const ctx = makeSnapCtx();
310
+ sp.draw(ctx, 1);
311
+ expect(ctx.beginPath).toHaveBeenCalled();
312
+ expect(ctx.closePath).toHaveBeenCalled();
313
+ expect(ctx.stroke).toHaveBeenCalled();
314
+ expect(ctx.arc).not.toHaveBeenCalled();
315
+ });
316
+
317
+ test('Test SnapPoint.draw QUADRANT type draws closed polygon without arc', () => {
318
+ const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.QUADRANT);
319
+ const ctx = makeSnapCtx();
320
+ sp.draw(ctx, 1);
321
+ expect(ctx.beginPath).toHaveBeenCalled();
322
+ expect(ctx.closePath).toHaveBeenCalled();
323
+ expect(ctx.stroke).toHaveBeenCalled();
324
+ expect(ctx.arc).not.toHaveBeenCalled();
325
+ });
326
+
327
+ test('Test SnapPoint.draw NEAREST type draws closed polygon without arc', () => {
328
+ const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.NEAREST);
329
+ const ctx = makeSnapCtx();
330
+ sp.draw(ctx, 1);
331
+ expect(ctx.beginPath).toHaveBeenCalled();
332
+ expect(ctx.closePath).toHaveBeenCalled();
333
+ expect(ctx.stroke).toHaveBeenCalled();
334
+ expect(ctx.arc).not.toHaveBeenCalled();
335
+ });
336
+
337
+ test('Test SnapPoint.draw TANGENT type draws arc with tangent line', () => {
338
+ const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.TANGENT);
339
+ const ctx = makeSnapCtx();
340
+ sp.draw(ctx, 1);
341
+ expect(ctx.beginPath).toHaveBeenCalled();
342
+ expect(ctx.arc).toHaveBeenCalledWith(5, 5, expect.any(Number), 0, 6.283);
343
+ expect(ctx.stroke).toHaveBeenCalled();
344
+ expect(ctx.closePath).not.toHaveBeenCalled();
345
+ });
346
+
347
+ test('Test SnapPoint.draw NODE type draws arc with X inside', () => {
348
+ const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.NODE);
349
+ const ctx = makeSnapCtx();
350
+ sp.draw(ctx, 1);
351
+ expect(ctx.beginPath).toHaveBeenCalled();
352
+ expect(ctx.arc).toHaveBeenCalledWith(5, 5, expect.any(Number), 0, 6.283);
353
+ expect(ctx.stroke).toHaveBeenCalled();
354
+ expect(ctx.closePath).not.toHaveBeenCalled();
355
+ });
356
+
357
+ test('Test SnapPoint.draw PERPENDICULAR type draws L-shape without arc', () => {
358
+ const sp = new SnapPoint(new Point(5, 5), SnapPoint.Type.PERPENDICULAR);
359
+ const ctx = makeSnapCtx();
360
+ sp.draw(ctx, 1);
361
+ expect(ctx.beginPath).toHaveBeenCalled();
362
+ expect(ctx.stroke).toHaveBeenCalled();
363
+ expect(ctx.closePath).not.toHaveBeenCalled();
364
+ expect(ctx.arc).not.toHaveBeenCalled();
365
+ });
366
+
367
+ // ─── TrackingLine.draw – out-of-bounds cases ─────────────────────────────────
368
+
369
+ test('Test TrackingLine.draw vertical line outside x bounds', () => {
370
+ DesignCore.Scene.auxiliaryEntities.clear();
371
+ // dir.x === 0 and from.x = 2 > boundsMax.x = 1 → draw returns early
372
+ snapping.addTrackingLine(new Point(2, -0.5), new Point(2, 0.5));
373
+ const trackingLine = DesignCore.Scene.auxiliaryEntities.get(0);
374
+ const ctx = makeTrackingCtx();
375
+ trackingLine.draw(ctx, 1);
376
+ expect(ctx.moveTo).not.toHaveBeenCalled();
377
+ expect(ctx.stroke).not.toHaveBeenCalled();
378
+ });
379
+
380
+ test('Test TrackingLine.draw horizontal line outside y bounds', () => {
381
+ DesignCore.Scene.auxiliaryEntities.clear();
382
+ // dir.y === 0 and from.y = -5 < boundsMin.y = -1 → draw returns early
383
+ snapping.addTrackingLine(new Point(0.5, -5), new Point(0.8, -5));
384
+ const trackingLine = DesignCore.Scene.auxiliaryEntities.get(0);
385
+ const ctx = makeTrackingCtx();
386
+ trackingLine.draw(ctx, 1);
387
+ expect(ctx.moveTo).not.toHaveBeenCalled();
388
+ expect(ctx.stroke).not.toHaveBeenCalled();
389
+ });
390
+
391
+ test('Test TrackingLine.draw line does not intersect canvas bounds', () => {
392
+ DesignCore.Scene.auxiliaryEntities.clear();
393
+ // from = (2, -0.5), dir = (1, 1): x-clamp gives t ∈ [-2, -1], y-clamp gives t ∈ [-0.5, 0.5]
394
+ // tMax (-1) < tMin (-0.5) → draw returns early
395
+ snapping.addTrackingLine(new Point(2, -0.5), new Point(3, 0.5));
396
+ const trackingLine = DesignCore.Scene.auxiliaryEntities.get(0);
397
+ const ctx = makeTrackingCtx();
398
+ trackingLine.draw(ctx, 1);
399
+ expect(ctx.moveTo).not.toHaveBeenCalled();
400
+ expect(ctx.stroke).not.toHaveBeenCalled();
401
+ });
@@ -829,3 +829,118 @@ test('Fillet.action radius>0 trimMode=false Line + Lwpolyline: standalone arc ad
829
829
  expect(core.scene.entities.get(0).points.length).toBe(2);
830
830
  expect(core.scene.entities.get(1).points.length).toBe(4);
831
831
  });
832
+
833
+ // ─── action: closed polyline closing-wrap arc direction (regression) ──────────
834
+ //
835
+ // Square: P0=(0,0), P1=(10,0), P2=(10,10), P3=(0,10), closed flag=1.
836
+ // Closing segment (idx=4): P3=(0,10)→P0=(0,0) — i.e. the left edge, travelling downward.
837
+ // Segment 1: P0=(0,0)→P1=(10,0) — i.e. the bottom edge, travelling rightward.
838
+ // The shared corner is P0=(0,0).
839
+ //
840
+ // With radius 2 the fillet must sit inside the square (bottom-left interior corner).
841
+ // The arc centre is at (2,2).
842
+ // – lowerTangent = (0,2) on the closing segment (left edge, y=2)
843
+ // – upperTangent = (2,0) on segment 1 (bottom edge, x=2)
844
+ // – bulge on lowerTangent must be POSITIVE (CCW arc from (0,2) to (2,0) through ≈(0.59,0.59))
845
+ // (BasePolyline convention: –ve bulge = CW, +ve bulge = CCW)
846
+ //
847
+ // Before the fix the lowerTangent/upperTangent assignment was inverted, producing
848
+ // an arc that traversed the exterior of the square.
849
+
850
+ test('Fillet.action closing-wrap: lowerTangent is on closing segment (left edge)', () => {
851
+ core.scene.clear();
852
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
853
+ const polyEntity = core.scene.entities.get(0);
854
+ polyEntity.flags.setFlagValue(1);
855
+
856
+ core.scene.headers.filletRadius = 2;
857
+ core.scene.headers.trimMode = true;
858
+
859
+ // firstPick = closing segment (idx=4, left edge), secondPick = segment 1 (bottom edge)
860
+ const fillet = new Fillet();
861
+ fillet.firstPick.entity = polyEntity;
862
+ fillet.secondPick.entity = polyEntity;
863
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
864
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
865
+ fillet.firstPick.clickPoint = new Point(0, 5);
866
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
867
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
868
+ fillet.secondPick.clickPoint = new Point(5, 0);
869
+ fillet.action();
870
+
871
+ expect(core.scene.entities.count()).toBe(1);
872
+ const poly = core.scene.entities.get(0);
873
+ expect(poly.points.length).toBe(5);
874
+
875
+ // After the splice at cornerIdx=0:
876
+ // points[0] = lowerTangent (arcStartPoint), points[1] = upperTangent
877
+ // lowerTangent must be on the closing segment (left edge, x≈0)
878
+ expect(poly.points[0].x).toBeCloseTo(0);
879
+ expect(poly.points[0].y).toBeCloseTo(2);
880
+ // upperTangent must be on segment 1 (bottom edge, y≈0)
881
+ expect(poly.points[1].x).toBeCloseTo(2);
882
+ expect(poly.points[1].y).toBeCloseTo(0);
883
+ });
884
+
885
+ test('Fillet.action closing-wrap: arc bulge sign is positive (CCW, fillet inside square)', () => {
886
+ // Same setup. The CCW arc from (0,2) to (2,0) sweeps through (≈0.59, ≈0.59)
887
+ // staying inside the square — a positive bulge value.
888
+ // Before the fix the lowerTangent/upperTangent were swapped, producing an arc
889
+ // that traversed the exterior of the square.
890
+ core.scene.clear();
891
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
892
+ const polyEntity = core.scene.entities.get(0);
893
+ polyEntity.flags.setFlagValue(1);
894
+
895
+ core.scene.headers.filletRadius = 2;
896
+ core.scene.headers.trimMode = true;
897
+
898
+ const fillet = new Fillet();
899
+ fillet.firstPick.entity = polyEntity;
900
+ fillet.secondPick.entity = polyEntity;
901
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
902
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
903
+ fillet.firstPick.clickPoint = new Point(0, 5);
904
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
905
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
906
+ fillet.secondPick.clickPoint = new Point(5, 0);
907
+ fillet.action();
908
+
909
+ const poly = core.scene.entities.get(0);
910
+ // points[0] carries the bulge for the arc from lowerTangent to upperTangent
911
+ expect(poly.points[0].bulge).toBeGreaterThan(0);
912
+ });
913
+
914
+ test('Fillet.action closing-wrap reversed pick order: arc bulge sign and tangent positions are correct', () => {
915
+ // Swap firstPick/secondPick — should produce the same geometry.
916
+ core.scene.clear();
917
+ core.scene.addItem('Lwpolyline', { points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] });
918
+ const polyEntity = core.scene.entities.get(0);
919
+ polyEntity.flags.setFlagValue(1);
920
+
921
+ core.scene.headers.filletRadius = 2;
922
+ core.scene.headers.trimMode = true;
923
+
924
+ // firstPick = segment 1 (bottom edge), secondPick = closing segment (left edge, idx=4)
925
+ const fillet = new Fillet();
926
+ fillet.firstPick.entity = polyEntity;
927
+ fillet.secondPick.entity = polyEntity;
928
+ fillet.firstPick.segment = polyEntity.getClosestSegment(new Point(5, 0));
929
+ fillet.firstPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(5, 0));
930
+ fillet.firstPick.clickPoint = new Point(5, 0);
931
+ fillet.secondPick.segment = polyEntity.getClosestSegment(new Point(0, 5));
932
+ fillet.secondPick.segmentIndex = polyEntity.getClosestSegmentIndex(new Point(0, 5));
933
+ fillet.secondPick.clickPoint = new Point(0, 5);
934
+ fillet.action();
935
+
936
+ expect(core.scene.entities.count()).toBe(1);
937
+ const poly = core.scene.entities.get(0);
938
+ expect(poly.points.length).toBe(5);
939
+
940
+ // Regardless of pick order: lowerTangent at (0,2), upperTangent at (2,0), bulge > 0 (CCW)
941
+ expect(poly.points[0].x).toBeCloseTo(0);
942
+ expect(poly.points[0].y).toBeCloseTo(2);
943
+ expect(poly.points[1].x).toBeCloseTo(2);
944
+ expect(poly.points[1].y).toBeCloseTo(0);
945
+ expect(poly.points[0].bulge).toBeGreaterThan(0);
946
+ });