@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
@@ -626,3 +626,64 @@ test('BasePolyline.fromPolylinePoints round-trip strips closure point for closed
626
626
  expect(rebuilt.points[3].x).toBe(0);
627
627
  expect(rebuilt.points[3].y).toBe(10);
628
628
  });
629
+
630
+ // ─── closestPoint: closed polyline closing segment ───────────────────────────
631
+
632
+ test('BasePolyline.closestPoint finds closest point on closing segment of closed polyline', () => {
633
+ // Square: P0=(0,0), P1=(10,0), P2=(10,10), P3=(0,10), closed flag set.
634
+ // Closing segment is P3=(0,10) → P0=(0,0).
635
+ // A test point on the left edge (0, 5) must resolve to (0, 5) on the closing segment.
636
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
637
+ const poly = new BasePolyline({ points });
638
+ poly.flags.setFlagValue(1);
639
+
640
+ const [closest, dist] = poly.closestPoint(new Point(0, 5));
641
+ expect(closest.x).toBeCloseTo(0);
642
+ expect(closest.y).toBeCloseTo(5);
643
+ expect(dist).toBeCloseTo(0);
644
+ });
645
+
646
+ test('BasePolyline.closestPoint closing segment is preferred over other segments', () => {
647
+ // The test point (-1, 5) is clearly nearest the closing segment (left edge),
648
+ // not the bottom, right, or top edges.
649
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
650
+ const poly = new BasePolyline({ points });
651
+ poly.flags.setFlagValue(1);
652
+
653
+ const [closest, dist] = poly.closestPoint(new Point(-1, 5));
654
+ expect(closest.x).toBeCloseTo(0);
655
+ expect(closest.y).toBeCloseTo(5);
656
+ expect(dist).toBeCloseTo(1);
657
+ });
658
+
659
+ test('BasePolyline.closestPoint for open polyline does not include closing segment', () => {
660
+ // For point (-1, 1) on the open square:
661
+ // – Nearest on seg P0→P1 (y=0): foot (-1,0) clips to (0,0), dist = √2 ≈ 1.41
662
+ // – Nearest on seg P2→P3 (y=10): foot (-1,10) clips to (0,10), dist ≈ 9.06
663
+ // – If the closing segment existed (x=0, y∈[0,10]): foot (0,1), dist = 1 ← closer!
664
+ // An open polyline must NOT check the closing segment, so it returns (0,0).
665
+ const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
666
+ const poly = new BasePolyline({ points });
667
+ // flag NOT set — open polyline
668
+
669
+ const [closest] = poly.closestPoint(new Point(-1, 1));
670
+ expect(closest.x).toBeCloseTo(0);
671
+ expect(closest.y).toBeCloseTo(0);
672
+ });
673
+
674
+ test('BasePolyline.closestPoint is consistent with getClosestSegmentIndex for closed polyline', () => {
675
+ // For a closed square, both methods should agree that the closing segment
676
+ // is nearest for a point on the left edge.
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
+ poly.flags.setFlagValue(1);
680
+
681
+ const testPoint = new Point(-2, 5);
682
+ const [closest] = poly.closestPoint(testPoint);
683
+ const segIdx = poly.getClosestSegmentIndex(testPoint);
684
+
685
+ // Closing segment index == points.length == 4
686
+ expect(segIdx).toBe(points.length);
687
+ expect(closest.x).toBeCloseTo(0);
688
+ expect(closest.y).toBeCloseTo(5);
689
+ });
@@ -292,45 +292,35 @@ test('Circle.draw does not throw', () => {
292
292
 
293
293
  test('Circle.snaps returns centre snap', () => {
294
294
  const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
295
- DesignCore.Settings.centresnap = true;
296
- DesignCore.Settings.quadrantsnap = false;
297
- DesignCore.Settings.nearestsnap = false;
298
295
  const snaps = circle.snaps(new Point(5, 0), 100);
299
- expect(snaps).toHaveLength(1);
300
- expect(snaps[0].x).toBe(0);
301
- expect(snaps[0].y).toBe(0);
296
+ const centreSnaps = snaps.filter((s) => s.type === 'centre');
297
+ expect(centreSnaps).toHaveLength(1);
298
+ expect(centreSnaps[0].snapPoint.x).toBe(0);
299
+ expect(centreSnaps[0].snapPoint.y).toBe(0);
302
300
  });
303
301
 
304
302
  test('Circle.snaps returns four quadrant snaps', () => {
305
303
  const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
306
- DesignCore.Settings.centresnap = false;
307
- DesignCore.Settings.quadrantsnap = true;
308
- DesignCore.Settings.nearestsnap = false;
309
304
  const snaps = circle.snaps(new Point(5, 0), 100);
310
- expect(snaps).toHaveLength(4);
311
- expect(snaps.some((p) => p.x === 10 && p.y === 0)).toBe(true);// 0°
312
- expect(snaps.some((p) => p.x === 0 && p.y === 10)).toBe(true);// 90°
313
- expect(snaps.some((p) => p.x === -10 && p.y === 0)).toBe(true);// 180°
314
- expect(snaps.some((p) => p.x === 0 && p.y === -10)).toBe(true);// 270°
305
+ const quadrantSnaps = snaps.filter((s) => s.type === 'quadrant');
306
+ expect(quadrantSnaps).toHaveLength(4);
307
+ expect(quadrantSnaps.some((p) => p.snapPoint.x === 10 && p.snapPoint.y === 0)).toBe(true);// 0°
308
+ expect(quadrantSnaps.some((p) => p.snapPoint.x === 0 && p.snapPoint.y === 10)).toBe(true);// 90°
309
+ expect(quadrantSnaps.some((p) => p.snapPoint.x === -10 && p.snapPoint.y === 0)).toBe(true);// 180°
310
+ expect(quadrantSnaps.some((p) => p.snapPoint.x === 0 && p.snapPoint.y === -10)).toBe(true);// 270°
315
311
  });
316
312
 
317
313
  test('Circle.snaps nearest snap fires when close enough', () => {
318
314
  const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
319
- DesignCore.Settings.centresnap = false;
320
- DesignCore.Settings.quadrantsnap = false;
321
- DesignCore.Settings.nearestsnap = true;
322
315
  // Mouse on the circle surface with large delta
323
316
  const snaps = circle.snaps(new Point(10, 0), 1000);
324
- expect(snaps.length).toBeGreaterThanOrEqual(1);
317
+ expect(snaps.filter((s) => s.type === 'nearest').length).toBeGreaterThanOrEqual(1);
325
318
  });
326
319
 
327
320
  test('Circle.snaps nearest snap does not fire when too far', () => {
328
321
  const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
329
- DesignCore.Settings.centresnap = false;
330
- DesignCore.Settings.quadrantsnap = false;
331
- DesignCore.Settings.nearestsnap = true;
332
322
  const snaps = circle.snaps(new Point(100, 100), 1);
333
- expect(snaps.length).toBe(0);
323
+ expect(snaps.filter((s) => s.type === 'nearest').length).toBe(0);
334
324
  });
335
325
 
336
326
  test('Circle.fromPolylinePoints round-trip preserves centre and radius', () => {
@@ -352,3 +342,39 @@ test('Circle.fromPolylinePoints precision drift is negligible', () => {
352
342
  expect(drift).toBeLessThan(1e-10);
353
343
  expect(Math.abs(rebuilt.radius - circle.radius)).toBeLessThan(1e-10);
354
344
  });
345
+
346
+ test('Circle.snaps returns two tangent snaps from outside', () => {
347
+ const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
348
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
349
+ // fromPoint=(20,0): distanceToCenter=20 > radius=10
350
+ // tangentHalfAngle = acos(10/20) = π/3; both tangent points at (5, ±8.66) are on the full circle
351
+ DesignCore.Scene.inputManager.inputPoint = new Point(20, 0);
352
+ const snaps = circle.snaps(new Point(5, 0), 100);
353
+ const tangentSnaps = snaps.filter((s) => s.type === 'tangent');
354
+ expect(tangentSnaps.length).toBe(2);
355
+ expect(tangentSnaps.some((s) => s.snapPoint.x > 4.9 && s.snapPoint.y > 8.5)).toBe(true);
356
+ expect(tangentSnaps.some((s) => s.snapPoint.x > 4.9 && s.snapPoint.y < -8.5)).toBe(true);
357
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
358
+ });
359
+
360
+ test('Circle.snaps returns no tangent when fromPoint is inside the circle', () => {
361
+ const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
362
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
363
+ DesignCore.Scene.inputManager.inputPoint = new Point(1, 1); // distanceToCenter ≈ 1.41 < radius 10
364
+ const snaps = circle.snaps(new Point(5, 0), 100);
365
+ expect(snaps.filter((s) => s.type === 'tangent').length).toBe(0);
366
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
367
+ });
368
+
369
+ test('Circle.snaps returns perpendicular snap from outside', () => {
370
+ const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
371
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
372
+ // fromPoint=(20,0): angleFromCentreToInput=0; perpendicularPoint=(10,0)
373
+ DesignCore.Scene.inputManager.inputPoint = new Point(20, 0);
374
+ const snaps = circle.snaps(new Point(5, 0), 100);
375
+ const perpSnaps = snaps.filter((s) => s.type === 'perpendicular');
376
+ expect(perpSnaps.length).toBe(1);
377
+ expect(perpSnaps[0].snapPoint.x).toBeCloseTo(10);
378
+ expect(perpSnaps[0].snapPoint.y).toBeCloseTo(0);
379
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
380
+ });
@@ -19,13 +19,15 @@ rotatedInsert.block.addItem(line);
19
19
  test('Test Insert.snaps', () => {
20
20
  const point = new Point(100, 100);
21
21
  const snaps = insert.snaps(point, 1);
22
- expect(snaps[0].x).toBeCloseTo(101);
23
- expect(snaps[0].y).toBeCloseTo(102);
22
+ const endSnaps = snaps.filter((s) => s.type === 'end');
23
+ expect(endSnaps[0].snapPoint.x).toBeCloseTo(101);
24
+ expect(endSnaps[0].snapPoint.y).toBeCloseTo(102);
24
25
 
25
26
  // Test snaps for a rotated block
26
27
  const rotatedSnaps = rotatedInsert.snaps(point);
27
- expect(rotatedSnaps[0].x).toBeCloseTo(-0.7071);
28
- expect(rotatedSnaps[0].y).toBeCloseTo(143.5426);
28
+ const rotatedEndSnaps = rotatedSnaps.filter((s) => s.type === 'end');
29
+ expect(rotatedEndSnaps[0].snapPoint.x).toBeCloseTo(-0.7071);
30
+ expect(rotatedEndSnaps[0].snapPoint.y).toBeCloseTo(143.5426);
29
31
  });
30
32
 
31
33
  test('Test Insert.closestPoint', () => {
@@ -88,3 +90,11 @@ AcDbBlockReference
88
90
  newInsert.dxf(file);
89
91
  expect(file.contents).toEqual(dxfString);
90
92
  });
93
+
94
+ test('Insert.snaps returns a node snap at points[0]', () => {
95
+ const nodeInsert = new Insert({ points: [new Point(5, 10)] });
96
+ const nodeSnaps = nodeInsert.snaps(new Point(0, 0), 100).filter((s) => s.type === 'node');
97
+ expect(nodeSnaps.length).toBe(1);
98
+ expect(nodeSnaps[0].snapPoint.x).toBe(5);
99
+ expect(nodeSnaps[0].snapPoint.y).toBe(10);
100
+ });
@@ -201,3 +201,56 @@ test('Line.fromPolylinePoints round-trip preserves points exactly', () => {
201
201
  expect(rebuilt.points[1].x).toBe(15);
202
202
  expect(rebuilt.points[1].y).toBe(-4);
203
203
  });
204
+
205
+ test('Line.snaps returns two end snaps at both endpoints', () => {
206
+ const line = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
207
+ const endSnaps = line.snaps(new Point(5, 0), 100).filter((s) => s.type === 'end');
208
+ expect(endSnaps.length).toBe(2);
209
+ expect(endSnaps.some((s) => s.snapPoint.x === 0 && s.snapPoint.y === 0)).toBe(true);
210
+ expect(endSnaps.some((s) => s.snapPoint.x === 10 && s.snapPoint.y === 0)).toBe(true);
211
+ });
212
+
213
+ test('Line.snaps returns mid snap at midpoint', () => {
214
+ const line = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
215
+ const midSnaps = line.snaps(new Point(5, 0), 100).filter((s) => s.type === 'mid');
216
+ expect(midSnaps.length).toBe(1);
217
+ expect(midSnaps[0].snapPoint.x).toBe(5);
218
+ expect(midSnaps[0].snapPoint.y).toBe(0);
219
+ });
220
+
221
+ test('Line.snaps nearest fires when mouse is close to the line', () => {
222
+ const line = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
223
+ // mouse at (5, 0.5): distance to line = 0.5; delta=10 so delta/10=1; 0.5 < 1 → fires
224
+ const snaps = line.snaps(new Point(5, 0.5), 10);
225
+ expect(snaps.filter((s) => s.type === 'nearest').length).toBe(1);
226
+ });
227
+
228
+ test('Line.snaps nearest does not fire when mouse is too far from the line', () => {
229
+ const line = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
230
+ // mouse at (5, 20): distance to line = 20; delta=1 so delta/10=0.1; 20 > 0.1 → no fire
231
+ const snaps = line.snaps(new Point(5, 20), 1);
232
+ expect(snaps.filter((s) => s.type === 'nearest').length).toBe(0);
233
+ });
234
+
235
+ test('Line.snaps perpendicular fires when foot falls on the segment', () => {
236
+ const line = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
237
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
238
+ // fromPoint=(5,5): foot of perpendicular = (5,0), which IS on the segment
239
+ DesignCore.Scene.inputManager.inputPoint = new Point(5, 5);
240
+ const snaps = line.snaps(new Point(5, 0), 100);
241
+ const perpSnaps = snaps.filter((s) => s.type === 'perpendicular');
242
+ expect(perpSnaps.length).toBe(1);
243
+ expect(perpSnaps[0].snapPoint.x).toBeCloseTo(5);
244
+ expect(perpSnaps[0].snapPoint.y).toBeCloseTo(0);
245
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
246
+ });
247
+
248
+ test('Line.snaps perpendicular does not fire when foot falls off the segment', () => {
249
+ const line = new Line({ points: [new Point(0, 0), new Point(10, 0)] });
250
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
251
+ // fromPoint=(20,5): foot = (20,0), which is BEYOND the segment end at (10,0)
252
+ DesignCore.Scene.inputManager.inputPoint = new Point(20, 5);
253
+ const snaps = line.snaps(new Point(5, 0), 100);
254
+ expect(snaps.filter((s) => s.type === 'perpendicular').length).toBe(0);
255
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
256
+ });
@@ -535,7 +535,8 @@ test('Text snaps returns all snap points', () => {
535
535
  const t = new Text({ points: [new Point(1, 2)] });
536
536
  t.boundingRect = { width: 10, height: 10 };
537
537
  const snaps = t.snaps(new Point(0, 0), 1);
538
- expect(snaps.length).toBe(5);
538
+ expect(snaps.length).toBe(1);
539
+ expect(snaps[0].type).toBe('node');
539
540
  });
540
541
 
541
542
  test('Text closestPoint returns correct distance', () => {
@@ -630,3 +631,11 @@ test('Text.execute does not create entity for empty string', async () => {
630
631
 
631
632
  expect(executeCommandCalled).toBe(false);
632
633
  });
634
+
635
+ test('Text.snaps node snap position matches points[0]', () => {
636
+ const t = new Text({ points: [new Point(5, 10)] });
637
+ const nodeSnaps = t.snaps(new Point(0, 0), 100).filter((s) => s.type === 'node');
638
+ expect(nodeSnaps.length).toBe(1);
639
+ expect(nodeSnaps[0].snapPoint.x).toBe(5);
640
+ expect(nodeSnaps[0].snapPoint.y).toBe(10);
641
+ });
@@ -0,0 +1,117 @@
1
+ import { Input, MouseDown, MouseUp } from '../../core/lib/inputManager.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { SingleSelection, SelectionSet } from '../../core/lib/selectionManager.js';
4
+ import { expect } from '@jest/globals';
5
+
6
+ // Stub classes that simulate what terser does in a production build:
7
+ // constructor.name is mangled to a single letter but static type is preserved.
8
+
9
+ /** Stub simulating a minified class resolved as Point. */
10
+ class MangledPoint {
11
+ static type = 'Point';
12
+ }
13
+
14
+ /** Stub simulating a minified class resolved as SingleSelection. */
15
+ class MangledSingleSelection {
16
+ static type = 'SingleSelection';
17
+ }
18
+
19
+ /** Stub simulating a minified class resolved as SelectionSet. */
20
+ class MangledSelectionSet {
21
+ static type = 'SelectionSet';
22
+ }
23
+
24
+ /** Stub simulating a minified class resolved as MouseDown. */
25
+ class MangledMouseDown {
26
+ static type = 'MouseDown';
27
+ }
28
+
29
+ /** Stub simulating a minified class resolved as MouseUp. */
30
+ class MangledMouseUp {
31
+ static type = 'MouseUp';
32
+ }
33
+
34
+ /** Stub with no static type and an unrecognised constructor.name. */
35
+ class UnknownStub {}
36
+
37
+ // ─── Input.Type ───────────────────────────────────────────────────────────────
38
+
39
+ test('Input.Type contains all expected type entries', () => {
40
+ const keys = Object.keys(Input.Type);
41
+ expect(keys).toContain('POINT');
42
+ expect(keys).toContain('SELECTIONSET');
43
+ expect(keys).toContain('SINGLESELECTION');
44
+ expect(keys).toContain('NUMBER');
45
+ expect(keys).toContain('STRING');
46
+ expect(keys).toContain('DYNAMIC');
47
+ expect(keys).toContain('MOUSEDOWN');
48
+ expect(keys).toContain('MOUSEUP');
49
+ });
50
+
51
+ // ─── Input.getType — undefined ────────────────────────────────────────────────
52
+
53
+ test('Input.getType returns undefined for undefined input', () => {
54
+ expect(Input.getType(undefined)).toBeUndefined();
55
+ });
56
+
57
+ // ─── Input.getType — constructor.name path (dev / unminified) ─────────────────
58
+
59
+ test('Input.getType resolves Point via constructor.name', () => {
60
+ expect(Input.getType(new Point(1, 2))).toBe(Input.Type.POINT);
61
+ });
62
+
63
+ test('Input.getType resolves SingleSelection via constructor.name', () => {
64
+ expect(Input.getType(new SingleSelection(0, new Point(0, 0)))).toBe(Input.Type.SINGLESELECTION);
65
+ });
66
+
67
+ test('Input.getType resolves SelectionSet via constructor.name', () => {
68
+ expect(Input.getType(new SelectionSet())).toBe(Input.Type.SELECTIONSET);
69
+ });
70
+
71
+ test('Input.getType resolves MouseDown via constructor.name', () => {
72
+ expect(Input.getType(new MouseDown())).toBe(Input.Type.MOUSEDOWN);
73
+ });
74
+
75
+ test('Input.getType resolves MouseUp via constructor.name', () => {
76
+ expect(Input.getType(new MouseUp())).toBe(Input.Type.MOUSEUP);
77
+ });
78
+
79
+ test('Input.getType resolves Number via constructor.name', () => {
80
+ expect(Input.getType(42)).toBe(Input.Type.NUMBER);
81
+ });
82
+
83
+ test('Input.getType resolves String via constructor.name', () => {
84
+ expect(Input.getType('hello')).toBe(Input.Type.STRING);
85
+ });
86
+
87
+ // ─── Input.getType — static type path (minified / production) ─────────────────
88
+
89
+ test('Input.getType resolves via static type when constructor.name is mangled (Point)', () => {
90
+ expect(Input.getType(new MangledPoint())).toBe(Input.Type.POINT);
91
+ });
92
+
93
+ test('Input.getType resolves via static type when constructor.name is mangled (SingleSelection)', () => {
94
+ expect(Input.getType(new MangledSingleSelection())).toBe(Input.Type.SINGLESELECTION);
95
+ });
96
+
97
+ test('Input.getType resolves via static type when constructor.name is mangled (SelectionSet)', () => {
98
+ expect(Input.getType(new MangledSelectionSet())).toBe(Input.Type.SELECTIONSET);
99
+ });
100
+
101
+ test('Input.getType resolves via static type when constructor.name is mangled (MouseDown)', () => {
102
+ expect(Input.getType(new MangledMouseDown())).toBe(Input.Type.MOUSEDOWN);
103
+ });
104
+
105
+ test('Input.getType resolves via static type when constructor.name is mangled (MouseUp)', () => {
106
+ expect(Input.getType(new MangledMouseUp())).toBe(Input.Type.MOUSEUP);
107
+ });
108
+
109
+ // ─── Input.getType — no match ─────────────────────────────────────────────────
110
+
111
+ test('Input.getType returns undefined when neither static type nor constructor.name matches', () => {
112
+ expect(Input.getType(new UnknownStub())).toBeUndefined();
113
+ });
114
+
115
+ test('Input.getType returns undefined for a plain object', () => {
116
+ expect(Input.getType({})).toBeUndefined();
117
+ });
@@ -41,10 +41,8 @@ test('Settings constructor - colour defaults', () => {
41
41
  const fresh = new (settings.constructor)();
42
42
  expect(fresh.canvasbackgroundcolour).toEqual({ r: 30, g: 30, b: 30 });
43
43
  expect(fresh.selecteditemscolour).toEqual({ r: 0, g: 255, b: 0 });
44
- expect(fresh.snapcolour).toEqual({ r: 255, g: 0, b: 0 });
44
+ expect(fresh.snaptrackingcolour).toEqual({ r: 55, g: 180, b: 75 });
45
45
  expect(fresh.gridcolour).toEqual({ r: 119, g: 118, b: 123 });
46
- expect(fresh.helpergeometrycolour).toEqual({ r: 0, g: 195, b: 255 });
47
- expect(fresh.polarsnapcolour).toEqual({ r: 55, g: 180, b: 75 });
48
46
  expect(fresh.selectionWindow).toEqual({ r: 0, g: 255, b: 0 });
49
47
  expect(fresh.crossingWindow).toEqual({ r: 0, g: 0, b: 255 });
50
48
  });
@@ -84,9 +82,9 @@ test('setSetting with false value does not trigger polar/ortho exclusion', () =>
84
82
  });
85
83
 
86
84
  test('setSetting updates arbitrary colour settings', () => {
87
- settings.setSetting('polarsnapcolour', { r: 1, g: 2, b: 3 });
88
- expect(settings.polarsnapcolour).toEqual({ r: 1, g: 2, b: 3 });
89
- settings.setSetting('polarsnapcolour', { r: 55, g: 180, b: 75 });
85
+ settings.setSetting('snaptrackingcolour', { r: 1, g: 2, b: 3 });
86
+ expect(settings.snaptrackingcolour).toEqual({ r: 1, g: 2, b: 3 });
87
+ settings.setSetting('snaptrackingcolour', { r: 55, g: 180, b: 75 });
90
88
  });
91
89
 
92
90
  test('setSetting updates polarangle', () => {
@@ -118,7 +116,7 @@ test('getSetting returns current value after setSetting', () => {
118
116
  });
119
117
 
120
118
  test('getSetting returns correct type for colour objects', () => {
121
- const colour = settings.getSetting('snapcolour');
119
+ const colour = settings.getSetting('snaptrackingcolour');
122
120
  expect(colour).toHaveProperty('r');
123
121
  expect(colour).toHaveProperty('g');
124
122
  expect(colour).toHaveProperty('b');