@design-core/core 0.0.4 → 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.
- package/core/entities/arc.js +39 -22
- package/core/entities/arctext.js +6 -4
- package/core/entities/basePolyline.js +26 -18
- package/core/entities/circle.js +33 -15
- package/core/entities/entity.js +1 -1
- package/core/entities/hatch.js +2 -0
- package/core/entities/insert.js +9 -3
- package/core/entities/line.js +16 -11
- package/core/entities/lwpolyline.js +2 -0
- package/core/entities/point.js +3 -1
- package/core/entities/polyline.js +2 -0
- package/core/entities/rectangle.js +2 -0
- package/core/entities/solid.js +2 -0
- package/core/entities/text.js +5 -8
- package/core/lib/input.js +8 -4
- package/core/lib/inputManager.js +2 -3
- package/core/lib/selectionManager.js +4 -0
- package/core/lib/settings.js +9 -5
- package/core/lib/snapping.js +112 -17
- package/core/tables/block.js +2 -0
- package/core/tools/chamfer.js +2 -0
- package/core/tools/chamferFilletBase.js +2 -0
- package/core/tools/copy.js +2 -0
- package/core/tools/copybase.js +2 -0
- package/core/tools/copyclip.js +2 -0
- package/core/tools/cutclip.js +2 -0
- package/core/tools/distance.js +2 -0
- package/core/tools/erase.js +2 -0
- package/core/tools/explode.js +2 -0
- package/core/tools/extend.js +2 -0
- package/core/tools/fillet.js +2 -0
- package/core/tools/identify.js +2 -0
- package/core/tools/matchProp.js +2 -0
- package/core/tools/mirror.js +2 -0
- package/core/tools/move.js +2 -0
- package/core/tools/offset.js +2 -0
- package/core/tools/pan.js +2 -0
- package/core/tools/pasteclip.js +2 -0
- package/core/tools/purge.js +2 -0
- package/core/tools/rotate.js +2 -0
- package/core/tools/scale.js +2 -0
- package/core/tools/tool.js +1 -1
- package/core/tools/trim.js +2 -0
- package/core/tools/zoom.js +2 -0
- package/package.json +1 -1
- package/test/entities/arc.test.js +60 -11
- package/test/entities/arctext.test.js +18 -2
- package/test/entities/circle.test.js +48 -22
- package/test/entities/insert.test.js +14 -4
- package/test/entities/line.test.js +53 -0
- package/test/entities/text.test.js +10 -1
- package/test/lib/input.test.js +117 -0
- package/test/lib/settings.test.js +5 -7
- 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
|
-
|
|
300
|
-
expect(
|
|
301
|
-
expect(
|
|
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
|
-
|
|
311
|
-
expect(
|
|
312
|
-
expect(
|
|
313
|
-
expect(
|
|
314
|
-
expect(
|
|
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
|
-
|
|
23
|
-
expect(
|
|
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
|
-
|
|
28
|
-
expect(
|
|
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(
|
|
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.
|
|
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('
|
|
88
|
-
expect(settings.
|
|
89
|
-
settings.setSetting('
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
});
|