@design-core/core 0.0.3 → 0.0.5

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 (55) hide show
  1. package/core/core/core.js +3 -0
  2. package/core/entities/arc.js +39 -22
  3. package/core/entities/arctext.js +6 -4
  4. package/core/entities/basePolyline.js +26 -18
  5. package/core/entities/circle.js +33 -15
  6. package/core/entities/entity.js +1 -1
  7. package/core/entities/hatch.js +2 -0
  8. package/core/entities/insert.js +9 -3
  9. package/core/entities/line.js +16 -11
  10. package/core/entities/lwpolyline.js +2 -0
  11. package/core/entities/point.js +3 -1
  12. package/core/entities/polyline.js +2 -0
  13. package/core/entities/rectangle.js +2 -0
  14. package/core/entities/solid.js +2 -0
  15. package/core/entities/text.js +5 -8
  16. package/core/lib/input.js +8 -4
  17. package/core/lib/inputManager.js +2 -3
  18. package/core/lib/selectionManager.js +4 -0
  19. package/core/lib/settings.js +9 -5
  20. package/core/lib/snapping.js +112 -17
  21. package/core/tables/block.js +2 -0
  22. package/core/tools/chamfer.js +2 -0
  23. package/core/tools/chamferFilletBase.js +2 -0
  24. package/core/tools/copy.js +2 -0
  25. package/core/tools/copybase.js +2 -0
  26. package/core/tools/copyclip.js +2 -0
  27. package/core/tools/cutclip.js +2 -0
  28. package/core/tools/distance.js +2 -0
  29. package/core/tools/erase.js +2 -0
  30. package/core/tools/explode.js +2 -0
  31. package/core/tools/extend.js +2 -0
  32. package/core/tools/fillet.js +2 -0
  33. package/core/tools/identify.js +2 -0
  34. package/core/tools/matchProp.js +2 -0
  35. package/core/tools/mirror.js +2 -0
  36. package/core/tools/move.js +2 -0
  37. package/core/tools/offset.js +2 -0
  38. package/core/tools/pan.js +2 -0
  39. package/core/tools/pasteclip.js +2 -0
  40. package/core/tools/purge.js +2 -0
  41. package/core/tools/rotate.js +2 -0
  42. package/core/tools/scale.js +2 -0
  43. package/core/tools/tool.js +1 -1
  44. package/core/tools/trim.js +2 -0
  45. package/core/tools/zoom.js +2 -0
  46. package/package.json +1 -1
  47. package/test/entities/arc.test.js +60 -11
  48. package/test/entities/arctext.test.js +18 -2
  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
@@ -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');
@@ -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
+ });