@pooder/kit 3.3.0 → 3.5.0

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/src/hole.ts DELETED
@@ -1,786 +0,0 @@
1
- import {
2
- Extension,
3
- ExtensionContext,
4
- ContributionPointIds,
5
- CommandContribution,
6
- ConfigurationContribution,
7
- ConfigurationService,
8
- } from "@pooder/core";
9
- import { Circle, Group, Point, Rect } from "fabric";
10
- import CanvasService from "./CanvasService";
11
- import { DielineGeometry } from "./dieline";
12
- import {
13
- getNearestPointOnDieline,
14
- HoleData,
15
- resolveHolePosition,
16
- } from "./geometry";
17
- import { Coordinate } from "./coordinate";
18
-
19
- export class HoleTool implements Extension {
20
- id = "pooder.kit.hole";
21
-
22
- public metadata = {
23
- name: "HoleTool",
24
- };
25
-
26
- private holes: HoleData[] = [];
27
- private constraintTarget: "original" | "bleed" = "bleed";
28
-
29
- private canvasService?: CanvasService;
30
- private context?: ExtensionContext;
31
- private isUpdatingConfig = false;
32
-
33
- private handleMoving: ((e: any) => void) | null = null;
34
- private handleModified: ((e: any) => void) | null = null;
35
- private handleDielineChange: ((geometry: DielineGeometry) => void) | null =
36
- null;
37
-
38
- // Cache geometry to enforce constraints during drag
39
- private currentGeometry: DielineGeometry | null = null;
40
-
41
- constructor(
42
- options?: Partial<{
43
- holes: HoleData[];
44
- constraintTarget: "original" | "bleed";
45
- }>
46
- ) {
47
- if (options) {
48
- Object.assign(this, options);
49
- }
50
- }
51
-
52
- activate(context: ExtensionContext) {
53
- this.context = context;
54
- this.canvasService = context.services.get<CanvasService>("CanvasService");
55
-
56
- if (!this.canvasService) {
57
- console.warn("CanvasService not found for HoleTool");
58
- return;
59
- }
60
-
61
- const configService = context.services.get<ConfigurationService>(
62
- "ConfigurationService"
63
- );
64
- if (configService) {
65
- // Load initial config
66
- this.constraintTarget = configService.get(
67
- "hole.constraintTarget",
68
- this.constraintTarget
69
- );
70
-
71
- // Load holes from dieline.holes (SSOT)
72
- this.holes = configService.get("dieline.holes", []);
73
-
74
- // Listen for changes
75
- configService.onAnyChange((e: { key: string; value: any }) => {
76
- if (this.isUpdatingConfig) return;
77
-
78
- if (e.key === "hole.constraintTarget") {
79
- this.constraintTarget = e.value;
80
- this.enforceConstraints();
81
- }
82
-
83
- // Listen for dieline.holes changes (e.g. from undo/redo or other sources)
84
- if (e.key === "dieline.holes") {
85
- this.holes = e.value || [];
86
- this.redraw();
87
- }
88
- });
89
- }
90
-
91
- this.setup();
92
- }
93
-
94
- deactivate(context: ExtensionContext) {
95
- this.teardown();
96
- this.canvasService = undefined;
97
- this.context = undefined;
98
- }
99
-
100
- contribute() {
101
- return {
102
- [ContributionPointIds.CONFIGURATIONS]: [
103
- {
104
- id: "hole.constraintTarget",
105
- type: "select",
106
- label: "Constraint Target",
107
- options: ["original", "bleed"],
108
- default: "bleed",
109
- },
110
- ] as ConfigurationContribution[],
111
- [ContributionPointIds.COMMANDS]: [
112
- {
113
- command: "resetHoles",
114
- title: "Reset Holes",
115
- handler: () => {
116
- if (!this.canvasService) return false;
117
- let defaultPos = { x: this.canvasService.canvas.width! / 2, y: 50 };
118
-
119
- if (this.currentGeometry) {
120
- const g = this.currentGeometry;
121
- const topCenter = { x: g.x, y: g.y - g.height / 2 };
122
- defaultPos = getNearestPointOnDieline(topCenter, {
123
- ...g,
124
- holes: [],
125
- } as any);
126
- }
127
-
128
- const { width, height } = this.canvasService.canvas;
129
- const normalizedHole = Coordinate.normalizePoint(defaultPos, {
130
- width: width || 800,
131
- height: height || 600,
132
- });
133
-
134
- const configService = this.context?.services.get<ConfigurationService>(
135
- "ConfigurationService"
136
- );
137
- if (configService) {
138
- configService.update("dieline.holes", [
139
- {
140
- x: normalizedHole.x,
141
- y: normalizedHole.y,
142
- innerRadius: 15,
143
- outerRadius: 25,
144
- },
145
- ]);
146
- }
147
- return true;
148
- },
149
- },
150
- {
151
- command: "addHole",
152
- title: "Add Hole",
153
- handler: (x: number, y: number) => {
154
- if (!this.canvasService) return false;
155
-
156
- // Normalize relative to Dieline Geometry if available
157
- let normalizedX = 0.5;
158
- let normalizedY = 0.5;
159
-
160
- if (this.currentGeometry) {
161
- const { x: gx, y: gy, width: gw, height: gh } = this.currentGeometry;
162
- const left = gx - gw / 2;
163
- const top = gy - gh / 2;
164
- normalizedX = gw > 0 ? (x - left) / gw : 0.5;
165
- normalizedY = gh > 0 ? (y - top) / gh : 0.5;
166
- } else {
167
- const { width, height } = this.canvasService.canvas;
168
- normalizedX = Coordinate.toNormalized(x, width || 800);
169
- normalizedY = Coordinate.toNormalized(y, height || 600);
170
- }
171
-
172
- const configService = this.context?.services.get<ConfigurationService>(
173
- "ConfigurationService"
174
- );
175
-
176
- if (configService) {
177
- const currentHoles = configService.get("dieline.holes", []) as HoleData[];
178
- // Use last hole's radii or default
179
- const lastHole = currentHoles[currentHoles.length - 1];
180
- const innerRadius = lastHole?.innerRadius ?? 15;
181
- const outerRadius = lastHole?.outerRadius ?? 25;
182
- const shape = lastHole?.shape ?? "circle";
183
-
184
- const newHole = {
185
- x: normalizedX,
186
- y: normalizedY,
187
- shape,
188
- innerRadius,
189
- outerRadius,
190
- };
191
- configService.update("dieline.holes", [...currentHoles, newHole]);
192
- }
193
- return true;
194
- },
195
- },
196
- {
197
- command: "clearHoles",
198
- title: "Clear Holes",
199
- handler: () => {
200
- const configService = this.context?.services.get<ConfigurationService>(
201
- "ConfigurationService"
202
- );
203
- if (configService) {
204
- configService.update("dieline.holes", []);
205
- }
206
- return true;
207
- },
208
- },
209
- ] as CommandContribution[],
210
- };
211
- }
212
-
213
- private setup() {
214
- if (!this.canvasService || !this.context) return;
215
- const canvas = this.canvasService.canvas;
216
-
217
- // 1. Listen for Dieline Geometry Changes
218
- if (!this.handleDielineChange) {
219
- this.handleDielineChange = (geometry: DielineGeometry) => {
220
- this.currentGeometry = geometry;
221
- this.redraw();
222
- const changed = this.enforceConstraints();
223
- // Only sync if constraints actually moved something
224
- if (changed) {
225
- this.syncHolesToDieline();
226
- }
227
- };
228
- this.context.eventBus.on(
229
- "dieline:geometry:change",
230
- this.handleDielineChange,
231
- );
232
- }
233
-
234
- // 2. Initial Fetch of Geometry
235
- // Assuming DielineTool registered 'getGeometry' command which is now available via CommandService
236
- // Since we don't have direct access to CommandService here (it was in activate),
237
- // we can get it from context.services
238
- const commandService = this.context.services.get<any>("CommandService");
239
- if (commandService) {
240
- try {
241
- const geometry = commandService.executeCommand("getGeometry");
242
- if (geometry) {
243
- // If executeCommand returns a promise, await it?
244
- // CommandService.executeCommand is async in previous definition.
245
- // But here we are in sync setup.
246
- // Let's assume we can handle the promise if needed, or if it returns value directly (if not async).
247
- // Checking CommandService implementation: executeCommand IS async.
248
- Promise.resolve(geometry).then((g) => {
249
- if (g) {
250
- this.currentGeometry = g as DielineGeometry;
251
- // Re-run setup logic dependent on geometry
252
- this.enforceConstraints();
253
- this.initializeHoles();
254
- }
255
- });
256
- }
257
- } catch (e) {
258
- // Command might not be ready
259
- }
260
- }
261
-
262
- // 3. Setup Canvas Interaction
263
- if (!this.handleMoving) {
264
- this.handleMoving = (e: any) => {
265
- const target = e.target;
266
- if (!target || target.data?.type !== "hole-marker") return;
267
-
268
- if (!this.currentGeometry) return;
269
-
270
- const index = target.data?.index ?? -1;
271
- const holeData = this.holes[index];
272
-
273
- // Calculate effective geometry based on constraint target
274
- const effectiveOffset =
275
- this.constraintTarget === "original"
276
- ? 0
277
- : this.currentGeometry.offset;
278
- const constraintGeometry = {
279
- ...this.currentGeometry,
280
- width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
281
- height: Math.max(
282
- 0,
283
- this.currentGeometry.height + effectiveOffset * 2
284
- ),
285
- radius: Math.max(0, this.currentGeometry.radius + effectiveOffset),
286
- };
287
-
288
- const p = new Point(target.left, target.top);
289
- const newPos = this.calculateConstrainedPosition(
290
- p,
291
- constraintGeometry,
292
- holeData?.innerRadius ?? 15,
293
- holeData?.outerRadius ?? 25
294
- );
295
-
296
- target.set({
297
- left: newPos.x,
298
- top: newPos.y,
299
- });
300
- };
301
- canvas.on("object:moving", this.handleMoving);
302
- }
303
-
304
- if (!this.handleModified) {
305
- this.handleModified = (e: any) => {
306
- const target = e.target;
307
- if (!target || target.data?.type !== "hole-marker") return;
308
-
309
- // Update state when hole is moved
310
- // Ensure final position is constrained (handles case where 'modified' reports unconstrained coords)
311
- const changed = this.enforceConstraints();
312
-
313
- // If enforceConstraints changed something, it already synced.
314
- // If not, we sync manually to save the move (which was valid).
315
- if (!changed) {
316
- this.syncHolesFromCanvas();
317
- }
318
- };
319
- canvas.on("object:modified", this.handleModified);
320
- }
321
-
322
- this.initializeHoles();
323
- }
324
-
325
- private initializeHoles() {
326
- if (!this.canvasService) return;
327
- this.redraw();
328
- this.syncHolesToDieline();
329
- }
330
-
331
- private teardown() {
332
- if (!this.canvasService) return;
333
- const canvas = this.canvasService.canvas;
334
-
335
- if (this.handleMoving) {
336
- canvas.off("object:moving", this.handleMoving);
337
- this.handleMoving = null;
338
- }
339
- if (this.handleModified) {
340
- canvas.off("object:modified", this.handleModified);
341
- this.handleModified = null;
342
- }
343
- if (this.handleDielineChange && this.context) {
344
- this.context.eventBus.off(
345
- "dieline:geometry:change",
346
- this.handleDielineChange,
347
- );
348
- this.handleDielineChange = null;
349
- }
350
-
351
- const objects = canvas
352
- .getObjects()
353
- .filter((obj: any) => obj.data?.type === "hole-marker");
354
- objects.forEach((obj) => canvas.remove(obj));
355
-
356
- // Clear holes from Dieline (visual only, state preserved in HoleTool options)
357
- if (this.context) {
358
- const commandService = this.context.services.get<any>("CommandService");
359
- if (commandService) {
360
- try {
361
- commandService.executeCommand("setHoles", []);
362
- } catch (e) {}
363
- }
364
- }
365
-
366
- this.canvasService.requestRenderAll();
367
- }
368
-
369
- private syncHolesFromCanvas() {
370
- if (!this.canvasService) return;
371
- const objects = this.canvasService.canvas
372
- .getObjects()
373
- .filter(
374
- (obj: any) =>
375
- obj.data?.type === "hole-marker" || obj.name === "hole-marker",
376
- );
377
-
378
- // If we have markers but no state, or mismatch, we should be careful.
379
- // However, if we just dragged one, we expect them to match.
380
- if (objects.length === 0 && this.holes.length > 0) {
381
- console.warn("HoleTool: No markers found on canvas to sync from");
382
- return;
383
- }
384
-
385
- // Sort objects by index to match this.holes order
386
- objects.sort(
387
- (a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0),
388
- );
389
-
390
- // Update holes based on canvas positions
391
- const newHoles = objects.map((obj, i) => {
392
- const original = this.holes[i];
393
- const newAbsX = obj.left!;
394
- const newAbsY = obj.top!;
395
-
396
- // Validate coordinates to prevent NaN issues
397
- if (isNaN(newAbsX) || isNaN(newAbsY)) {
398
- console.error("HoleTool: Invalid marker coordinates", {
399
- newAbsX,
400
- newAbsY,
401
- });
402
- return original;
403
- }
404
-
405
- // Get current scale to denormalize offsets
406
- const scale = this.currentGeometry?.scale || 1;
407
- const unit = this.currentGeometry?.unit || "mm";
408
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
409
-
410
- if (original && original.anchor && this.currentGeometry) {
411
- // Reverse calculate offset from anchor
412
- const { x, y, width, height } = this.currentGeometry;
413
- let bx = x;
414
- let by = y;
415
- const left = x - width / 2;
416
- const right = x + width / 2;
417
- const top = y - height / 2;
418
- const bottom = y + height / 2;
419
-
420
- switch (original.anchor) {
421
- case "top-left":
422
- bx = left;
423
- by = top;
424
- break;
425
- case "top-center":
426
- bx = x;
427
- by = top;
428
- break;
429
- case "top-right":
430
- bx = right;
431
- by = top;
432
- break;
433
- case "center-left":
434
- bx = left;
435
- by = y;
436
- break;
437
- case "center":
438
- bx = x;
439
- by = y;
440
- break;
441
- case "center-right":
442
- bx = right;
443
- by = y;
444
- break;
445
- case "bottom-left":
446
- bx = left;
447
- by = bottom;
448
- break;
449
- case "bottom-center":
450
- bx = x;
451
- by = bottom;
452
- break;
453
- case "bottom-right":
454
- bx = right;
455
- by = bottom;
456
- break;
457
- }
458
-
459
- return {
460
- ...original,
461
- // Denormalize offset back to physical units (mm)
462
- offsetX: (newAbsX - bx) / scale / unitScale,
463
- offsetY: (newAbsY - by) / scale / unitScale,
464
- // Clear direct coordinates if we use anchor
465
- x: undefined,
466
- y: undefined,
467
- // Ensure other properties are preserved
468
- innerRadius: original.innerRadius,
469
- outerRadius: original.outerRadius,
470
- shape: original.shape || "circle",
471
- };
472
- }
473
-
474
- // If no anchor, use normalized coordinates relative to Dieline Geometry
475
- let normalizedX = 0.5;
476
- let normalizedY = 0.5;
477
-
478
- if (this.currentGeometry) {
479
- const { x, y, width, height } = this.currentGeometry;
480
- const left = x - width / 2;
481
- const top = y - height / 2;
482
- normalizedX = width > 0 ? (newAbsX - left) / width : 0.5;
483
- normalizedY = height > 0 ? (newAbsY - top) / height : 0.5;
484
- } else {
485
- // Fallback to Canvas normalization
486
- const { width, height } = this.canvasService!.canvas;
487
- normalizedX = Coordinate.toNormalized(newAbsX, width || 800);
488
- normalizedY = Coordinate.toNormalized(newAbsY, height || 600);
489
- }
490
-
491
- return {
492
- ...original,
493
- x: normalizedX,
494
- y: normalizedY,
495
- // Clear offsets if we are using direct normalized coordinates
496
- offsetX: undefined,
497
- offsetY: undefined,
498
- // Ensure other properties are preserved
499
- innerRadius: original?.innerRadius ?? 15,
500
- outerRadius: original?.outerRadius ?? 25,
501
- shape: original?.shape || "circle",
502
- };
503
- });
504
-
505
- this.holes = newHoles;
506
- this.syncHolesToDieline();
507
- }
508
-
509
- private syncHolesToDieline() {
510
- if (!this.context || !this.canvasService) return;
511
-
512
- const configService = this.context.services.get<ConfigurationService>(
513
- "ConfigurationService"
514
- );
515
-
516
- if (configService) {
517
- this.isUpdatingConfig = true;
518
- try {
519
- configService.update("dieline.holes", this.holes);
520
- } finally {
521
- this.isUpdatingConfig = false;
522
- }
523
- }
524
- }
525
-
526
- private redraw() {
527
- if (!this.canvasService) return;
528
- const canvas = this.canvasService.canvas;
529
- const { width, height } = canvas;
530
-
531
- // Remove existing holes
532
- const existing = canvas
533
- .getObjects()
534
- .filter((obj: any) => obj.data?.type === "hole-marker");
535
- existing.forEach((obj) => canvas.remove(obj));
536
-
537
- const holes = this.holes;
538
-
539
- if (!holes || holes.length === 0) {
540
- this.canvasService.requestRenderAll();
541
- return;
542
- }
543
-
544
- // Resolve geometry if needed for anchors
545
- const geometry = this.currentGeometry || {
546
- x: (width || 800) / 2,
547
- y: (height || 600) / 2,
548
- width: width || 800,
549
- height: height || 600,
550
- scale: 1, // Default scale if no geometry loaded
551
- } as any;
552
-
553
- holes.forEach((hole, index) => {
554
- // Geometry scale is needed.
555
- const scale = geometry.scale || 1;
556
- const unit = geometry.unit || "mm";
557
- const unitScale = Coordinate.convertUnit(1, 'mm', unit);
558
-
559
- const visualInnerRadius = hole.innerRadius * unitScale * scale;
560
- const visualOuterRadius = hole.outerRadius * unitScale * scale;
561
-
562
- // Resolve position
563
- // Apply unit conversion and scale to offsets before resolving (mm -> px)
564
- const pos = resolveHolePosition(
565
- {
566
- ...hole,
567
- offsetX: (hole.offsetX || 0) * unitScale * scale,
568
- offsetY: (hole.offsetY || 0) * unitScale * scale,
569
- },
570
- geometry,
571
- { width: geometry.width, height: geometry.height } // Use geometry dims instead of canvas
572
- );
573
-
574
- const isSquare = hole.shape === "square";
575
-
576
- const innerMarker = isSquare
577
- ? new Rect({
578
- width: visualInnerRadius * 2,
579
- height: visualInnerRadius * 2,
580
- fill: "transparent",
581
- stroke: "red",
582
- strokeWidth: 2,
583
- originX: "center",
584
- originY: "center",
585
- })
586
- : new Circle({
587
- radius: visualInnerRadius,
588
- fill: "transparent",
589
- stroke: "red",
590
- strokeWidth: 2,
591
- originX: "center",
592
- originY: "center",
593
- });
594
-
595
- const outerMarker = isSquare
596
- ? new Rect({
597
- width: visualOuterRadius * 2,
598
- height: visualOuterRadius * 2,
599
- fill: "transparent",
600
- stroke: "#666",
601
- strokeWidth: 1,
602
- strokeDashArray: [5, 5],
603
- originX: "center",
604
- originY: "center",
605
- })
606
- : new Circle({
607
- radius: visualOuterRadius,
608
- fill: "transparent",
609
- stroke: "#666",
610
- strokeWidth: 1,
611
- strokeDashArray: [5, 5],
612
- originX: "center",
613
- originY: "center",
614
- });
615
-
616
- const holeGroup = new Group([outerMarker, innerMarker], {
617
- left: pos.x,
618
- top: pos.y,
619
- originX: "center",
620
- originY: "center",
621
- selectable: true,
622
- hasControls: false, // Don't allow resizing/rotating
623
- hasBorders: false,
624
- subTargetCheck: false,
625
- opacity: 0, // Default hidden
626
- hoverCursor: "move",
627
- data: { type: "hole-marker", index },
628
- } as any);
629
- (holeGroup as any).name = "hole-marker";
630
-
631
- // Auto-show/hide logic
632
- holeGroup.on("mouseover", () => {
633
- holeGroup.set("opacity", 1);
634
- canvas.requestRenderAll();
635
- });
636
- holeGroup.on("mouseout", () => {
637
- if (canvas.getActiveObject() !== holeGroup) {
638
- holeGroup.set("opacity", 0);
639
- canvas.requestRenderAll();
640
- }
641
- });
642
- holeGroup.on("selected", () => {
643
- holeGroup.set("opacity", 1);
644
- canvas.requestRenderAll();
645
- });
646
- holeGroup.on("deselected", () => {
647
- holeGroup.set("opacity", 0);
648
- canvas.requestRenderAll();
649
- });
650
-
651
- canvas.add(holeGroup);
652
- canvas.bringObjectToFront(holeGroup);
653
- });
654
-
655
- // Also bring all existing markers to front to be safe
656
- const markers = canvas.getObjects().filter((o: any) => o.data?.type === "hole-marker");
657
- markers.forEach(m => canvas.bringObjectToFront(m));
658
-
659
- this.canvasService.requestRenderAll();
660
- }
661
-
662
- public enforceConstraints(): boolean {
663
- const geometry = this.currentGeometry;
664
- if (!geometry || !this.canvasService) {
665
- return false;
666
- }
667
-
668
- const effectiveOffset =
669
- this.constraintTarget === "original" ? 0 : geometry.offset;
670
- const constraintGeometry = {
671
- ...geometry,
672
- width: Math.max(0, geometry.width + effectiveOffset * 2),
673
- height: Math.max(0, geometry.height + effectiveOffset * 2),
674
- radius: Math.max(0, geometry.radius + effectiveOffset),
675
- };
676
-
677
- // Get all hole markers
678
- const objects = this.canvasService.canvas
679
- .getObjects()
680
- .filter((obj: any) => obj.data?.type === "hole-marker");
681
-
682
- let changed = false;
683
- // Sort objects by index to maintain order in options.holes
684
- objects.sort(
685
- (a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0)
686
- );
687
-
688
- const newHoles: HoleData[] = [];
689
-
690
- objects.forEach((obj: any, i: number) => {
691
- const currentPos = new Point(obj.left, obj.top);
692
- // We need to pass the hole's radii to calculateConstrainedPosition
693
- const holeData = this.holes[i];
694
-
695
- // Scale radii for constraint calculation (since geometry is in pixels)
696
- // Geometry scale is needed.
697
- const scale = geometry.scale || 1;
698
- const unit = geometry.unit || "mm";
699
- const unitScale = Coordinate.convertUnit(1, 'mm', unit);
700
-
701
- const innerR = (holeData?.innerRadius ?? 15) * unitScale * scale;
702
- const outerR = (holeData?.outerRadius ?? 25) * unitScale * scale;
703
-
704
- const newPos = this.calculateConstrainedPosition(
705
- currentPos,
706
- constraintGeometry,
707
- innerR,
708
- outerR
709
- );
710
-
711
- if (currentPos.distanceFrom(newPos) > 0.1) {
712
- obj.set({
713
- left: newPos.x,
714
- top: newPos.y,
715
- });
716
- obj.setCoords();
717
- changed = true;
718
- }
719
-
720
- // Update data logic is handled in syncHolesFromCanvas which is called on modified
721
- // But here we are modifying programmatically.
722
- // We should probably just let the visual update happen, and then sync?
723
- // Or just push to newHoles list to verify change?
724
- });
725
-
726
- if (changed) {
727
- // If we moved things programmatically, we should update the state
728
- this.syncHolesFromCanvas();
729
- return true;
730
- }
731
- return false;
732
- }
733
-
734
- private calculateConstrainedPosition(
735
- p: Point,
736
- g: DielineGeometry,
737
- innerRadius: number,
738
- outerRadius: number
739
- ): Point {
740
- // Use Paper.js to get accurate nearest point
741
- const options = {
742
- ...g,
743
- holes: [], // We don't need holes for boundary calculation
744
- };
745
-
746
- const nearest = getNearestPointOnDieline(
747
- { x: p.x, y: p.y },
748
- options as any,
749
- );
750
-
751
- // Now constrain distance
752
- const nearestP = new Point(nearest.x, nearest.y);
753
- const dist = p.distanceFrom(nearestP);
754
-
755
- // Vector from nearest to current point
756
- const v = p.subtract(nearestP);
757
-
758
- // Vector from center to nearest point (approximate normal for convex shapes)
759
- const center = new Point(g.x, g.y);
760
-
761
- const distToCenter = p.distanceFrom(center);
762
- const nearestDistToCenter = nearestP.distanceFrom(center);
763
-
764
- let signedDist = dist;
765
- if (distToCenter < nearestDistToCenter) {
766
- signedDist = -dist; // Inside
767
- }
768
-
769
- // Clamp distance
770
- let clampedDist = signedDist;
771
- if (signedDist > 0) {
772
- clampedDist = Math.min(signedDist, innerRadius);
773
- } else {
774
- clampedDist = Math.max(signedDist, -outerRadius);
775
- }
776
-
777
- // Reconstruct point
778
- if (dist < 0.001) return nearestP;
779
-
780
- // We want the result to lie on the line connecting Nearest -> P
781
- const scale = Math.abs(clampedDist) / (dist || 1);
782
- const offset = v.scalarMultiply(scale);
783
-
784
- return nearestP.add(offset);
785
- }
786
- }