@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.
Files changed (54) hide show
  1. package/core/entities/arc.js +39 -22
  2. package/core/entities/arctext.js +6 -4
  3. package/core/entities/basePolyline.js +26 -18
  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 +2 -3
  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 +2 -0
  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/circle.test.js +48 -22
  49. package/test/entities/insert.test.js +14 -4
  50. package/test/entities/line.test.js +53 -0
  51. package/test/entities/text.test.js +10 -1
  52. package/test/lib/input.test.js +117 -0
  53. package/test/lib/settings.test.js +5 -7
  54. package/test/lib/snapping.test.js +218 -12
@@ -6,13 +6,27 @@ import { Utils } from './utils.js';
6
6
  import { DesignCore } from '../designCore.js';
7
7
 
8
8
  /** SnapPoint Class */
9
- class SnapPoint {
9
+ export class SnapPoint {
10
+ static Type = Object.freeze({
11
+ NONE: 'none',
12
+ END: 'end',
13
+ MID: 'mid',
14
+ CENTRE: 'centre',
15
+ QUADRANT: 'quadrant',
16
+ NEAREST: 'nearest',
17
+ TANGENT: 'tangent',
18
+ NODE: 'node',
19
+ PERPENDICULAR: 'perpendicular',
20
+ });
21
+
10
22
  /**
11
23
  * Create SnapPoint
12
24
  * @param {Point} snapPoint
25
+ * @param {string} type - snap type from SnapPoint.Type
13
26
  */
14
- constructor(snapPoint) {
27
+ constructor(snapPoint, type = SnapPoint.Type.END) {
15
28
  this.snapPoint = snapPoint;
29
+ this.type = type;
16
30
  }
17
31
 
18
32
  /**
@@ -21,19 +35,74 @@ class SnapPoint {
21
35
  * @param {number} scale
22
36
  */
23
37
  draw(ctx, scale) {
24
- const snapColour = DesignCore.Settings.snapcolour;
25
- const radius = 4;
38
+ const snapColour = DesignCore.Settings.snaptrackingcolour;
39
+ const size = 6 / scale;
40
+ const x = this.snapPoint.x;
41
+ const y = this.snapPoint.y;
42
+ const lineWidth = 2.5 / scale;
26
43
 
27
44
  try { // HTML Canvas
28
- ctx.fillStyle = Colours.rgbToString(snapColour);
45
+ ctx.strokeStyle = Colours.rgbToString(snapColour);
46
+ ctx.lineWidth = lineWidth;
47
+ ctx.setLineDash([]);
29
48
  ctx.beginPath();
30
49
  } catch { // Cairo
31
50
  const rgbColour = Colours.rgbToScaledRGB(snapColour);
32
51
  ctx.setSourceRGB(rgbColour.r, rgbColour.g, rgbColour.b);
52
+ ctx.setLineWidth(lineWidth);
53
+ }
54
+
55
+ switch (this.type) {
56
+ case SnapPoint.Type.END: // square
57
+ ctx.moveTo(x - size, y - size);
58
+ ctx.lineTo(x + size, y - size);
59
+ ctx.lineTo(x + size, y + size);
60
+ ctx.lineTo(x - size, y + size);
61
+ ctx.closePath();
62
+ break;
63
+ case SnapPoint.Type.MID: // triangle
64
+ ctx.moveTo(x, y + size);
65
+ ctx.lineTo(x - size, y - size);
66
+ ctx.lineTo(x + size, y - size);
67
+ ctx.closePath();
68
+ break;
69
+ case SnapPoint.Type.QUADRANT: // diamond
70
+ ctx.moveTo(x, y - size);
71
+ ctx.lineTo(x + size, y);
72
+ ctx.lineTo(x, y + size);
73
+ ctx.lineTo(x - size, y);
74
+ ctx.closePath();
75
+ break;
76
+ case SnapPoint.Type.NEAREST: // hourglass (X with horizontal lines at top and bottom)
77
+ ctx.moveTo(x - size, y - size);
78
+ ctx.lineTo(x + size, y - size);
79
+ ctx.lineTo(x - size, y + size);
80
+ ctx.lineTo(x + size, y + size);
81
+ ctx.closePath();
82
+ break;
83
+ case SnapPoint.Type.TANGENT: // circle with a horizontal line over the top
84
+ ctx.arc(x, y, size, 0, 6.283);
85
+ ctx.moveTo(x - size, y + size * 1.5);
86
+ ctx.lineTo(x + size, y + size * 1.5);
87
+ break;
88
+ case SnapPoint.Type.NODE: // circle with an X inside
89
+ ctx.arc(x, y, size, 0, 6.283);
90
+ ctx.moveTo(x - size * 0.7, y - size * 0.7);
91
+ ctx.lineTo(x + size * 0.7, y + size * 0.7);
92
+ ctx.moveTo(x + size * 0.7, y - size * 0.7);
93
+ ctx.lineTo(x - size * 0.7, y + size * 0.7);
94
+ break;
95
+ case SnapPoint.Type.PERPENDICULAR: // right-angle corner (L-shape)
96
+ ctx.moveTo(x - size, y + size);
97
+ ctx.lineTo(x - size, y - size);
98
+ ctx.lineTo(x + size, y - size);
99
+ break;
100
+ default: // CENTRE - circle
101
+ ctx.arc(x, y, size, 0, 6.283);
102
+ break;
33
103
  }
34
104
 
35
- ctx.arc(this.snapPoint.x, this.snapPoint.y, radius / scale, 0, 6.283);
36
- ctx.fill();
105
+ ctx.stroke();
37
106
  }
38
107
  }
39
108
 
@@ -57,6 +126,7 @@ class TrackingLine {
57
126
  draw(ctx, scale) {
58
127
  const from = this.inputPoint;
59
128
  const dir = this.snapPoint.subtract(from);
129
+ const lineWidth = 2 / scale;
60
130
 
61
131
  if (dir.x === 0 && dir.y === 0) {
62
132
  return;
@@ -99,7 +169,7 @@ class TrackingLine {
99
169
  const start = from.add(dir.scale(tMin));
100
170
  const end = from.add(dir.scale(tMax));
101
171
 
102
- const lineColour = DesignCore.Settings.polarsnapcolour;
172
+ const lineColour = DesignCore.Settings.snaptrackingcolour;
103
173
  const dashSize = 4 / scale;
104
174
 
105
175
  ctx.save();
@@ -107,12 +177,12 @@ class TrackingLine {
107
177
  try { // HTML Canvas
108
178
  ctx.beginPath();
109
179
  ctx.strokeStyle = Colours.rgbToString(lineColour);
110
- ctx.lineWidth = 1 / scale;
180
+ ctx.lineWidth = lineWidth;
111
181
  ctx.setLineDash([dashSize, dashSize]);
112
182
  } catch { // Cairo
113
183
  const rgbColour = Colours.rgbToScaledRGB(lineColour);
114
184
  ctx.setSourceRGB(rgbColour.r, rgbColour.g, rgbColour.b);
115
- ctx.setLineWidth(1 / scale);
185
+ ctx.setLineWidth(lineWidth);
116
186
  ctx.setDash([dashSize, dashSize], 0);
117
187
  }
118
188
 
@@ -128,9 +198,32 @@ class TrackingLine {
128
198
  export class Snapping {
129
199
  /** Create snapping */
130
200
  constructor() {
201
+ // active is true when snapping is active (e.g. during a command)
202
+ this.active = false;
203
+ // snapOverride: null = use settings; SnapPoint.Type.NONE = suppress all snaps; any other type = snap only to that type
204
+ this.snapOverride = null;
205
+ }
206
+
207
+ /**
208
+ * Reset snapping state
209
+ */
210
+ reset() {
131
211
  this.active = false;
212
+ this.snapOverride = null;
213
+ }
214
+
215
+ /**
216
+ * Set the snap override type
217
+ * @param {string|null} type - a SnapPoint.Type value, or null to clear the override
218
+ */
219
+ setSnapOverride(type) {
220
+ if (type !== null && !Object.values(SnapPoint.Type).includes(type)) {
221
+ throw new Error(`Invalid snap override type: ${type}`);
222
+ }
223
+ this.snapOverride = type;
132
224
  }
133
225
 
226
+
134
227
  /**
135
228
  * Get snap point and draw to the scene
136
229
  * @return {Point} or undefined
@@ -149,14 +242,14 @@ export class Snapping {
149
242
 
150
243
  /**
151
244
  * Draw the snap point
152
- * @param {Point} snapPoint
245
+ * @param {SnapPoint} snapPoint
153
246
  */
154
247
  addSnapPoint(snapPoint) {
155
248
  // show the snap point
156
- DesignCore.Scene.auxiliaryEntities.add(new SnapPoint(snapPoint));
249
+ DesignCore.Scene.auxiliaryEntities.add(snapPoint);
157
250
 
158
251
  // Move the mouse to the closest snap point so if the mouse if clicked the snap point is used.
159
- DesignCore.Mouse.setPosFromScenePoint(snapPoint);
252
+ DesignCore.Mouse.setPosFromScenePoint(snapPoint.snapPoint);
160
253
  }
161
254
 
162
255
  /**
@@ -174,7 +267,8 @@ export class Snapping {
174
267
  */
175
268
  getSnapPoint() {
176
269
  let snapPoint;
177
- let delta = 25 / DesignCore.Canvas.getScale(); // find a more suitable starting value
270
+ const snapAperture = 25; // capture radius in pixels (screen space)
271
+ let delta = snapAperture / DesignCore.Canvas.getScale(); // convert to scene space
178
272
 
179
273
  for (let i = 0; i < DesignCore.Scene.entities.count(); i++) {
180
274
  const layer = DesignCore.LayerManager.getItemByName(DesignCore.Scene.entities.get(i).layer);
@@ -185,11 +279,12 @@ export class Snapping {
185
279
 
186
280
  const itemSnaps = DesignCore.Scene.entities.get(i).snaps(DesignCore.Mouse.pointOnScene(), delta); // get an array of snap point from the item
187
281
  if (itemSnaps) {
188
- for (let j = 0; j < itemSnaps.length; j++) {
189
- const length = itemSnaps[j].distance(DesignCore.Mouse.pointOnScene());
282
+ const filteredSnaps = this.snapOverride !== null ? itemSnaps.filter((s) => s.type === this.snapOverride) : itemSnaps.filter((s) => DesignCore.Settings[`${s.type}snap`]);
283
+ for (let j = 0; j < filteredSnaps.length; j++) {
284
+ const length = filteredSnaps[j].snapPoint.distance(DesignCore.Mouse.pointOnScene());
190
285
  if (length < delta) {
191
286
  delta = length;
192
- snapPoint = itemSnaps[j];
287
+ snapPoint = filteredSnaps[j];
193
288
  }
194
289
  }
195
290
  }
@@ -17,6 +17,8 @@ import { Utils } from '../lib/utils.js';
17
17
  * @extends Entity
18
18
  */
19
19
  export class Block extends Entity {
20
+ static type = 'Block';
21
+
20
22
  /**
21
23
  * Create a Block Entity
22
24
  * @param {Array} data
@@ -15,6 +15,8 @@ import { DesignCore } from '../designCore.js';
15
15
  * @extends ChamferFilletBase
16
16
  */
17
17
  export class Chamfer extends ChamferFilletBase {
18
+ static type = 'Chamfer';
19
+
18
20
  /**
19
21
  * Register the command
20
22
  * @return {Object}
@@ -15,6 +15,8 @@ import { DesignCore } from '../designCore.js';
15
15
  * @extends Tool
16
16
  */
17
17
  export class ChamferFilletBase extends Tool {
18
+ static type = 'ChamferFilletBase';
19
+
18
20
  /** Create a ChamferFilletBase command */
19
21
  constructor() {
20
22
  super();
@@ -13,6 +13,8 @@ import { DesignCore } from '../designCore.js';
13
13
  * @extends Tool
14
14
  */
15
15
  export class Copy extends Tool {
16
+ static type = 'Copy';
17
+
16
18
  /** Create a Copy command */
17
19
  constructor() {
18
20
  super();
@@ -10,6 +10,8 @@ import { DesignCore } from '../designCore.js';
10
10
  * @extends Tool
11
11
  */
12
12
  export class Copybase extends Tool {
13
+ static type = 'Copybase';
14
+
13
15
  /** Create a Copybase command */
14
16
  constructor() {
15
17
  super();
@@ -10,6 +10,8 @@ import { DesignCore } from '../designCore.js';
10
10
  * @extends Tool
11
11
  */
12
12
  export class Copyclip extends Tool {
13
+ static type = 'Copyclip';
14
+
13
15
  /** Create a Copyclip command */
14
16
  constructor() {
15
17
  super();
@@ -11,6 +11,8 @@ import { DesignCore } from '../designCore.js';
11
11
  * @extends Tool
12
12
  */
13
13
  export class Cutclip extends Tool {
14
+ static type = 'Cutclip';
15
+
14
16
  /** Create a Cutclip command */
15
17
  constructor() {
16
18
  super();
@@ -11,6 +11,8 @@ import { DesignCore } from '../designCore.js';
11
11
  * @extends Tool
12
12
  */
13
13
  export class Distance extends Tool {
14
+ static type = 'Distance';
15
+
14
16
  /** Create a Distance command */
15
17
  constructor() {
16
18
  super();
@@ -11,6 +11,8 @@ import { DesignCore } from '../designCore.js';
11
11
  * @extends Tool
12
12
  */
13
13
  export class Erase extends Tool {
14
+ static type = 'Erase';
15
+
14
16
  /** Create an Erase command */
15
17
  constructor() {
16
18
  super();
@@ -14,6 +14,8 @@ import { DesignCore } from '../designCore.js';
14
14
  * @extends Tool
15
15
  */
16
16
  export class Explode extends Tool {
17
+ static type = 'Explode';
18
+
17
19
  /** Create an Explode command */
18
20
  constructor() {
19
21
  super();
@@ -11,6 +11,8 @@ import { DesignCore } from '../designCore.js';
11
11
  * @extends Tool
12
12
  */
13
13
  export class Extend extends Tool {
14
+ static type = 'Extend';
15
+
14
16
  /** Create an Extend command */
15
17
  constructor() {
16
18
  super();
@@ -14,6 +14,8 @@ import { DesignCore } from '../designCore.js';
14
14
  * @extends ChamferFilletBase
15
15
  */
16
16
  export class Fillet extends ChamferFilletBase {
17
+ static type = 'Fillet';
18
+
17
19
  /**
18
20
  * Register the command
19
21
  * @return {Object}
@@ -10,6 +10,8 @@ import { DesignCore } from '../designCore.js';
10
10
  * @extends Tool
11
11
  */
12
12
  export class Identify extends Tool {
13
+ static type = 'Identify';
14
+
13
15
  /** Create an Identify command */
14
16
  constructor() {
15
17
  super();
@@ -12,6 +12,8 @@ import { DesignCore } from '../designCore.js';
12
12
  * @extends Tool
13
13
  */
14
14
  export class MatchProp extends Tool {
15
+ static type = 'MatchProp';
16
+
15
17
  /** Create a MatchProp command */
16
18
  constructor() {
17
19
  super();
@@ -13,6 +13,8 @@ import { DesignCore } from '../designCore.js';
13
13
  * @extends Tool
14
14
  */
15
15
  export class Mirror extends Tool {
16
+ static type = 'Mirror';
17
+
16
18
  /** Create a Mirror command */
17
19
  constructor() {
18
20
  super();
@@ -12,6 +12,8 @@ import { UpdateState } from '../lib/stateManager.js';
12
12
  * @extends Tool
13
13
  */
14
14
  export class Move extends Tool {
15
+ static type = 'Move';
16
+
15
17
  /** Create a Move command */
16
18
  constructor() {
17
19
  super();
@@ -14,6 +14,8 @@ import { DesignCore } from '../designCore.js';
14
14
  * @extends Tool
15
15
  */
16
16
  export class Offset extends Tool {
17
+ static type = 'Offset';
18
+
17
19
  /** Create an Offset command */
18
20
  constructor() {
19
21
  super();
package/core/tools/pan.js CHANGED
@@ -8,6 +8,8 @@ import { DesignCore } from '../designCore.js';
8
8
  * @extends Tool
9
9
  */
10
10
  export class Pan extends Tool {
11
+ static type = 'Pan';
12
+
11
13
  /** Create a Pan command */
12
14
  constructor() {
13
15
  super();
@@ -13,6 +13,8 @@ import { DesignCore } from '../designCore.js';
13
13
  * @extends Tool
14
14
  */
15
15
  export class Pasteclip extends Tool {
16
+ static type = 'Pasteclip';
17
+
16
18
  /** Create a Pasteclip command */
17
19
  constructor() {
18
20
  super();
@@ -10,6 +10,8 @@ import { DesignCore } from '../designCore.js';
10
10
  * @extends Tool
11
11
  */
12
12
  export class Purge extends Tool {
13
+ static type = 'Purge';
14
+
13
15
  /** Create a Purge command */
14
16
  constructor() {
15
17
  super();
@@ -13,6 +13,8 @@ import { DesignCore } from '../designCore.js';
13
13
  * @extends Tool
14
14
  */
15
15
  export class Rotate extends Tool {
16
+ static type = 'Rotate';
17
+
16
18
  /** Create a Rotate command */
17
19
  constructor() {
18
20
  super();
@@ -12,6 +12,8 @@ import { DesignCore } from '../designCore.js';
12
12
  * @extends Tool
13
13
  */
14
14
  export class Scale extends Tool {
15
+ static type = 'Scale';
16
+
15
17
  /** Create a Scale command */
16
18
  constructor() {
17
19
  super();
@@ -5,7 +5,7 @@ export class Tool {
5
5
  /** Create a Tool command */
6
6
  constructor() {
7
7
  Object.defineProperty(this, 'type', {
8
- value: this.constructor.name,
8
+ value: this.constructor.type ?? this.constructor.name,
9
9
  writable: true,
10
10
  });
11
11
 
@@ -11,6 +11,8 @@ import { DesignCore } from '../designCore.js';
11
11
  * @extends Tool
12
12
  */
13
13
  export class Trim extends Tool {
14
+ static type = 'Trim';
15
+
14
16
  /** Create a Trim command */
15
17
  constructor() {
16
18
  super();
@@ -12,6 +12,8 @@ import { DesignCore } from '../designCore.js';
12
12
  * @extends Tool
13
13
  */
14
14
  export class Zoom extends Tool {
15
+ static type = 'Zoom';
16
+
15
17
  /** Create a Zoom command */
16
18
  constructor() {
17
19
  super();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design-core/core",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
@@ -434,22 +434,16 @@ test('Arc.draw does not throw (ccw and cw)', () => {
434
434
  expect(() => arcCw.draw(ctx2, 1)).not.toThrow();
435
435
  });
436
436
 
437
- test('Arc.snaps returns end, centre and nearest snap points', () => {
437
+ test('Arc.snaps returns end and centre snap points', () => {
438
438
  const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: 1 });
439
439
 
440
- DesignCore.Settings.endsnap = true;
441
- DesignCore.Settings.centresnap = true;
442
- DesignCore.Settings.nearestsnap = false;
443
440
  const snaps = arc.snaps(new Point(5, 5), 10);
444
- expect(snaps.length).toBe(3); // start, end, centre
441
+ expect(snaps.filter((s) => s.type === 'end').length).toBe(2);
442
+ expect(snaps.filter((s) => s.type === 'centre').length).toBe(1);
445
443
 
446
- // nearestsnap: closestPoint returns [pnt, distance] (no third element),
447
- // so closest[2] === true is never satisfied and no snap is pushed
448
- DesignCore.Settings.endsnap = false;
449
- DesignCore.Settings.centresnap = false;
450
- DesignCore.Settings.nearestsnap = true;
444
+ // Arc.snaps nearest fires when mouse is on the arc surface
451
445
  const nearSnaps = arc.snaps(new Point(10, 0), 100);
452
- expect(nearSnaps.length).toBe(0);
446
+ expect(nearSnaps.filter((s) => s.type === 'nearest').length).toBe(1);
453
447
  });
454
448
 
455
449
  test('Arc.execute handles negative angle (clockwise)', async () => {
@@ -505,3 +499,58 @@ test('Arc.fromPolylinePoints precision drift is negligible', () => {
505
499
  expect(Math.abs(rebuilt.radius - arc.radius)).toBeLessThan(1e-10);
506
500
  });
507
501
 
502
+ test('Arc.snaps nearest does not fire when mouse is too far from arc', () => {
503
+ const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: 1 });
504
+ // Mouse far from arc surface; delta=1 so delta/10=0.1 — distance >> threshold
505
+ const snaps = arc.snaps(new Point(100, 100), 1);
506
+ expect(snaps.filter((s) => s.type === 'nearest').length).toBe(0);
507
+ });
508
+
509
+ test('Arc.snaps returns tangent snap when fromPoint is outside the arc', () => {
510
+ const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: 1 });
511
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
512
+ // fromPoint=(20,0): distanceToCenter=20 > radius=10
513
+ // tangentHalfAngle = acos(10/20) = π/3 (60°)
514
+ // tangentPoint at angle 0+60°=(5, 8.66) IS on CCW arc from 0° to 90°
515
+ // tangentPoint at angle 0-60°=(5,-8.66) is NOT on the arc
516
+ DesignCore.Scene.inputManager.inputPoint = new Point(20, 0);
517
+ const snaps = arc.snaps(new Point(5, 5), 100);
518
+ const tangentSnaps = snaps.filter((s) => s.type === 'tangent');
519
+ expect(tangentSnaps.length).toBe(1);
520
+ expect(tangentSnaps[0].snapPoint.x).toBeCloseTo(5);
521
+ expect(tangentSnaps[0].snapPoint.y).toBeCloseTo(8.6603, 3);
522
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
523
+ });
524
+
525
+ test('Arc.snaps returns no tangent when fromPoint is inside the arc radius', () => {
526
+ const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: 1 });
527
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
528
+ DesignCore.Scene.inputManager.inputPoint = new Point(1, 1); // distanceToCenter ≈ 1.41 < radius 10
529
+ const snaps = arc.snaps(new Point(5, 5), 100);
530
+ expect(snaps.filter((s) => s.type === 'tangent').length).toBe(0);
531
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
532
+ });
533
+
534
+ test('Arc.snaps returns perpendicular snap when radial point is on the arc', () => {
535
+ const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: 1 });
536
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
537
+ // fromPoint=(20,0): angleFromCentreToInput=0; perpendicularPoint=(10,0) IS on CCW arc
538
+ DesignCore.Scene.inputManager.inputPoint = new Point(20, 0);
539
+ const snaps = arc.snaps(new Point(5, 5), 100);
540
+ const perpSnaps = snaps.filter((s) => s.type === 'perpendicular');
541
+ expect(perpSnaps.length).toBe(1);
542
+ expect(perpSnaps[0].snapPoint.x).toBeCloseTo(10);
543
+ expect(perpSnaps[0].snapPoint.y).toBeCloseTo(0);
544
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
545
+ });
546
+
547
+ test('Arc.snaps returns no perpendicular when radial point is not on the arc', () => {
548
+ const arc = new Arc({ points: [new Point(0, 0), new Point(10, 0), new Point(0, 10)], direction: 1 });
549
+ const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;
550
+ // fromPoint=(0,-20): angleFromCentreToInput=-π/2; perpendicularPoint=(0,-10) NOT on CCW arc from 0° to 90°
551
+ DesignCore.Scene.inputManager.inputPoint = new Point(0, -20);
552
+ const snaps = arc.snaps(new Point(5, 5), 100);
553
+ expect(snaps.filter((s) => s.type === 'perpendicular').length).toBe(0);
554
+ DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
555
+ });
556
+
@@ -300,8 +300,9 @@ test('Test ArcText.snaps', () => {
300
300
  const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100 });
301
301
  const point = new Point(100, 100);
302
302
  const snaps = arcText.snaps(point, 1);
303
- expect(snaps[0].x).toBeCloseTo(-2.39222);
304
- expect(snaps[0].y).toBeCloseTo(101.22173);
303
+ const nodeSnaps = snaps.filter((s) => s.type === 'node');
304
+ expect(nodeSnaps[0].snapPoint.x).toBeCloseTo(-2.39222);
305
+ expect(nodeSnaps[0].snapPoint.y).toBeCloseTo(101.22173);
305
306
  });
306
307
 
307
308
  test('Test ArcText.closestPoint', () => {
@@ -327,3 +328,18 @@ test('Test ArcText.toPolylinePoints - returns correct points array', () => {
327
328
  expect(result[0].x).toBeCloseTo(-2.39222);
328
329
  expect(result[0].y).toBeCloseTo(101.22173);
329
330
  });
331
+
332
+ test('ArcText.snaps returns a centre snap at points[0]', () => {
333
+ const arcText = new ArcAlignedText({ points: [new Point(3, 7)], string: 'Hi', radius: 50 });
334
+ const centreSnaps = arcText.snaps(new Point(0, 0), 100).filter((s) => s.type === 'centre');
335
+ expect(centreSnaps.length).toBe(1);
336
+ expect(centreSnaps[0].snapPoint.x).toBe(3);
337
+ expect(centreSnaps[0].snapPoint.y).toBe(7);
338
+ });
339
+
340
+ test('ArcText.snaps returns node snaps only for non-space characters', () => {
341
+ // 'A B' has 3 characters: 'A', ' ', 'B' — only 'A' and 'B' produce node snaps
342
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'A B', radius: 100 });
343
+ const nodeSnaps = arcText.snaps(new Point(0, 0), 100).filter((s) => s.type === 'node');
344
+ expect(nodeSnaps.length).toBe(2);
345
+ });