@pooder/kit 2.0.0 → 3.0.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 CHANGED
@@ -1,156 +1,303 @@
1
1
  import {
2
- Command,
3
- Editor,
4
- EditorState,
5
2
  Extension,
6
- OptionSchema,
7
- Circle,
8
- Group,
9
- Point,
3
+ ExtensionContext,
4
+ ContributionPointIds,
5
+ CommandContribution,
6
+ ConfigurationContribution,
7
+ ConfigurationService,
10
8
  } from "@pooder/core";
11
- import { DielineTool, DielineGeometry } from "./dieline";
12
- import { getNearestPointOnDieline } from "./geometry";
13
- import paper from "paper";
14
-
15
- export interface HoleToolOptions {
16
- innerRadius: number;
17
- outerRadius: number;
18
- style: "solid" | "dashed";
19
- holes?: Array<{ x: number; y: number }>;
20
- constraintTarget?: "original" | "bleed";
21
- }
9
+ import { Circle, Group, Point } from "fabric";
10
+ import CanvasService from "./CanvasService";
11
+ import { DielineGeometry } from "./dieline";
12
+ import { getNearestPointOnDieline, HoleData } from "./geometry";
13
+ import { Coordinate } from "./coordinate";
22
14
 
23
- export class HoleTool implements Extension<HoleToolOptions> {
24
- public name = "HoleTool";
25
- public options: HoleToolOptions = {
26
- innerRadius: 15,
27
- outerRadius: 25,
28
- style: "solid",
29
- holes: [],
30
- constraintTarget: "bleed",
31
- };
15
+ export class HoleTool implements Extension {
16
+ id = "pooder.kit.hole";
32
17
 
33
- public schema: Record<keyof HoleToolOptions, OptionSchema> = {
34
- innerRadius: {
35
- type: "number",
36
- min: 1,
37
- max: 100,
38
- label: "Inner Radius",
39
- },
40
- outerRadius: {
41
- type: "number",
42
- min: 1,
43
- max: 100,
44
- label: "Outer Radius",
45
- },
46
- style: {
47
- type: "select",
48
- options: ["solid", "dashed"],
49
- label: "Line Style",
50
- },
51
- constraintTarget: {
52
- type: "select",
53
- options: ["original", "bleed"],
54
- label: "Constraint Target",
55
- },
56
- holes: {
57
- type: "json",
58
- label: "Holes",
59
- } as any,
18
+ public metadata = {
19
+ name: "HoleTool",
60
20
  };
61
21
 
22
+ private innerRadius: number = 15;
23
+ private outerRadius: number = 25;
24
+ private style: "solid" | "dashed" = "solid";
25
+ private holes: Array<{ x: number; y: number }> = [];
26
+ private constraintTarget: "original" | "bleed" = "bleed";
27
+
28
+ private canvasService?: CanvasService;
29
+ private context?: ExtensionContext;
30
+ private isUpdatingConfig = false;
31
+
62
32
  private handleMoving: ((e: any) => void) | null = null;
63
33
  private handleModified: ((e: any) => void) | null = null;
64
-
65
- onMount(editor: Editor) {
66
- this.setup(editor);
34
+ private handleDielineChange: ((geometry: DielineGeometry) => void) | null =
35
+ null;
36
+
37
+ // Cache geometry to enforce constraints during drag
38
+ private currentGeometry: DielineGeometry | null = null;
39
+
40
+ constructor(
41
+ options?: Partial<{
42
+ innerRadius: number;
43
+ outerRadius: number;
44
+ style: "solid" | "dashed";
45
+ holes: Array<{ x: number; y: number }>;
46
+ constraintTarget: "original" | "bleed";
47
+ }>,
48
+ ) {
49
+ if (options) {
50
+ Object.assign(this, options);
51
+ }
67
52
  }
68
53
 
69
- onUnmount(editor: Editor) {
70
- this.teardown(editor);
71
- }
54
+ activate(context: ExtensionContext) {
55
+ this.context = context;
56
+ this.canvasService = context.services.get<CanvasService>("CanvasService");
72
57
 
73
- onDestroy(editor: Editor) {
74
- this.teardown(editor);
75
- }
58
+ if (!this.canvasService) {
59
+ console.warn("CanvasService not found for HoleTool");
60
+ return;
61
+ }
76
62
 
77
- private getDielineGeometry(editor: Editor): DielineGeometry | null {
78
- const dielineTool = editor.getExtension("DielineTool") as DielineTool;
79
- if (!dielineTool) return null;
63
+ const configService = context.services.get<ConfigurationService>(
64
+ "ConfigurationService",
65
+ );
66
+ if (configService) {
67
+ // Load initial config
68
+ this.innerRadius = configService.get(
69
+ "hole.innerRadius",
70
+ this.innerRadius,
71
+ );
72
+ this.outerRadius = configService.get(
73
+ "hole.outerRadius",
74
+ this.outerRadius,
75
+ );
76
+ this.style = configService.get("hole.style", this.style);
77
+ this.constraintTarget = configService.get(
78
+ "hole.constraintTarget",
79
+ this.constraintTarget,
80
+ );
81
+
82
+ // Load holes from dieline.holes (SSOT)
83
+ const dielineHoles = configService.get("dieline.holes", []);
84
+ if (this.canvasService) {
85
+ const { width, height } = this.canvasService.canvas;
86
+ this.holes = dielineHoles.map((h: any) => {
87
+ const p = Coordinate.denormalizePoint(h, {
88
+ width: width || 800,
89
+ height: height || 600,
90
+ });
91
+ return { x: p.x, y: p.y };
92
+ });
93
+ }
80
94
 
81
- const geometry = dielineTool.getGeometry(editor);
82
- if (!geometry) return null;
95
+ // Listen for changes
96
+ configService.onAnyChange((e: { key: string; value: any }) => {
97
+ if (this.isUpdatingConfig) return;
98
+
99
+ if (e.key.startsWith("hole.")) {
100
+ const prop = e.key.split(".")[1];
101
+ if (prop && prop in this) {
102
+ (this as any)[prop] = e.value;
103
+ this.redraw();
104
+ // Allow syncHolesToDieline to run to update dieline.holes
105
+ this.syncHolesToDieline();
106
+ }
107
+ }
108
+ // Listen for dieline.holes changes (e.g. from undo/redo or other sources)
109
+ if (e.key === "dieline.holes") {
110
+ const holes = e.value || [];
111
+ if (this.canvasService) {
112
+ const { width, height } = this.canvasService.canvas;
113
+ this.holes = holes.map((h: any) => {
114
+ const p = Coordinate.denormalizePoint(h, {
115
+ width: width || 800,
116
+ height: height || 600,
117
+ });
118
+ return { x: p.x, y: p.y };
119
+ });
120
+ this.redraw();
121
+ }
122
+ }
123
+ });
124
+ }
83
125
 
84
- const offset =
85
- this.options.constraintTarget === "original"
86
- ? 0
87
- : dielineTool.options.offset || 0;
126
+ this.setup();
127
+ }
88
128
 
89
- // Apply offset to geometry
129
+ deactivate(context: ExtensionContext) {
130
+ this.teardown();
131
+ this.canvasService = undefined;
132
+ this.context = undefined;
133
+ }
134
+
135
+ contribute() {
90
136
  return {
91
- ...geometry,
92
- width: Math.max(0, geometry.width + offset * 2),
93
- height: Math.max(0, geometry.height + offset * 2),
94
- radius: Math.max(0, geometry.radius + offset),
137
+ [ContributionPointIds.CONFIGURATIONS]: [
138
+ {
139
+ id: "hole.innerRadius",
140
+ type: "number",
141
+ label: "Inner Radius",
142
+ min: 1,
143
+ max: 100,
144
+ default: 15,
145
+ },
146
+ {
147
+ id: "hole.outerRadius",
148
+ type: "number",
149
+ label: "Outer Radius",
150
+ min: 1,
151
+ max: 100,
152
+ default: 25,
153
+ },
154
+ {
155
+ id: "hole.style",
156
+ type: "select",
157
+ label: "Line Style",
158
+ options: ["solid", "dashed"],
159
+ default: "solid",
160
+ },
161
+ {
162
+ id: "hole.constraintTarget",
163
+ type: "select",
164
+ label: "Constraint Target",
165
+ options: ["original", "bleed"],
166
+ default: "bleed",
167
+ },
168
+ ] as ConfigurationContribution[],
169
+ [ContributionPointIds.COMMANDS]: [
170
+ {
171
+ command: "resetHoles",
172
+ title: "Reset Holes",
173
+ handler: () => {
174
+ if (!this.canvasService) return false;
175
+ let defaultPos = { x: this.canvasService.canvas.width! / 2, y: 50 };
176
+
177
+ if (this.currentGeometry) {
178
+ const g = this.currentGeometry;
179
+ const topCenter = { x: g.x, y: g.y - g.height / 2 };
180
+ defaultPos = getNearestPointOnDieline(topCenter, {
181
+ ...g,
182
+ holes: [],
183
+ } as any);
184
+ }
185
+
186
+ this.innerRadius = 15;
187
+ this.outerRadius = 25;
188
+ this.style = "solid";
189
+ this.holes = [defaultPos];
190
+
191
+ this.redraw();
192
+ this.syncHolesToDieline();
193
+ return true;
194
+ },
195
+ },
196
+ {
197
+ command: "addHole",
198
+ title: "Add Hole",
199
+ handler: (x: number, y: number) => {
200
+ if (!this.holes) this.holes = [];
201
+ this.holes.push({ x, y });
202
+ this.redraw();
203
+ this.syncHolesToDieline();
204
+ return true;
205
+ },
206
+ },
207
+ {
208
+ command: "clearHoles",
209
+ title: "Clear Holes",
210
+ handler: () => {
211
+ this.holes = [];
212
+ this.redraw();
213
+ this.syncHolesToDieline();
214
+ return true;
215
+ },
216
+ },
217
+ ] as CommandContribution[],
95
218
  };
96
219
  }
97
220
 
98
- public enforceConstraints(editor: Editor) {
99
- const geometry = this.getDielineGeometry(editor);
100
- if (!geometry) return;
101
-
102
- // Get all hole markers
103
- const objects = editor.canvas
104
- .getObjects()
105
- .filter((obj: any) => obj.data?.type === "hole-marker");
106
-
107
- let changed = false;
108
- // Sort objects by index to maintain order in options.holes
109
- objects.sort(
110
- (a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0),
111
- );
112
-
113
- const newHoles: { x: number; y: number }[] = [];
114
-
115
- objects.forEach((obj: any) => {
116
- const currentPos = new Point(obj.left, obj.top);
117
- const newPos = this.calculateConstrainedPosition(currentPos, geometry);
221
+ private setup() {
222
+ if (!this.canvasService || !this.context) return;
223
+ const canvas = this.canvasService.canvas;
224
+
225
+ // 1. Listen for Dieline Geometry Changes
226
+ if (!this.handleDielineChange) {
227
+ this.handleDielineChange = (geometry: DielineGeometry) => {
228
+ this.currentGeometry = geometry;
229
+ const changed = this.enforceConstraints();
230
+ // Only sync if constraints actually moved something
231
+ if (changed) {
232
+ this.syncHolesToDieline();
233
+ }
234
+ };
235
+ this.context.eventBus.on(
236
+ "dieline:geometry:change",
237
+ this.handleDielineChange,
238
+ );
239
+ }
118
240
 
119
- if (currentPos.distanceFrom(newPos) > 0.1) {
120
- obj.set({
121
- left: newPos.x,
122
- top: newPos.y,
123
- });
124
- obj.setCoords();
125
- changed = true;
241
+ // 2. Initial Fetch of Geometry
242
+ // Assuming DielineTool registered 'getGeometry' command which is now available via CommandService
243
+ // Since we don't have direct access to CommandService here (it was in activate),
244
+ // we can get it from context.services
245
+ const commandService = this.context.services.get<any>("CommandService");
246
+ if (commandService) {
247
+ try {
248
+ const geometry = commandService.executeCommand("getGeometry");
249
+ if (geometry) {
250
+ // If executeCommand returns a promise, await it?
251
+ // CommandService.executeCommand is async in previous definition.
252
+ // But here we are in sync setup.
253
+ // Let's assume we can handle the promise if needed, or if it returns value directly (if not async).
254
+ // Checking CommandService implementation: executeCommand IS async.
255
+ Promise.resolve(geometry).then((g) => {
256
+ if (g) {
257
+ this.currentGeometry = g as DielineGeometry;
258
+ // Re-run setup logic dependent on geometry
259
+ this.enforceConstraints();
260
+ this.initializeHoles();
261
+ }
262
+ });
263
+ }
264
+ } catch (e) {
265
+ // Command might not be ready
126
266
  }
127
- newHoles.push({ x: obj.left, y: obj.top });
128
- });
129
-
130
- if (changed) {
131
- this.options.holes = newHoles;
132
- editor.canvas.requestRenderAll();
133
267
  }
134
- }
135
268
 
136
- private setup(editor: Editor) {
269
+ // 3. Setup Canvas Interaction
137
270
  if (!this.handleMoving) {
138
271
  this.handleMoving = (e: any) => {
139
272
  const target = e.target;
140
273
  if (!target || target.data?.type !== "hole-marker") return;
141
274
 
142
- const geometry = this.getDielineGeometry(editor);
143
- if (!geometry) return;
275
+ if (!this.currentGeometry) return;
276
+
277
+ // Calculate effective geometry based on constraint target
278
+ const effectiveOffset =
279
+ this.constraintTarget === "original"
280
+ ? 0
281
+ : this.currentGeometry.offset;
282
+ const constraintGeometry = {
283
+ ...this.currentGeometry,
284
+ width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
285
+ height: Math.max(
286
+ 0,
287
+ this.currentGeometry.height + effectiveOffset * 2,
288
+ ),
289
+ radius: Math.max(0, this.currentGeometry.radius + effectiveOffset),
290
+ };
144
291
 
145
292
  const p = new Point(target.left, target.top);
146
- const newPos = this.calculateConstrainedPosition(p, geometry);
293
+ const newPos = this.calculateConstrainedPosition(p, constraintGeometry);
147
294
 
148
295
  target.set({
149
296
  left: newPos.x,
150
297
  top: newPos.y,
151
298
  });
152
299
  };
153
- editor.canvas.on("object:moving", this.handleMoving);
300
+ canvas.on("object:moving", this.handleMoving);
154
301
  }
155
302
 
156
303
  if (!this.handleModified) {
@@ -159,18 +306,22 @@ export class HoleTool implements Extension<HoleToolOptions> {
159
306
  if (!target || target.data?.type !== "hole-marker") return;
160
307
 
161
308
  // Update state when hole is moved
162
- this.syncHolesFromCanvas(editor);
309
+ this.syncHolesFromCanvas();
163
310
  };
164
- editor.canvas.on("object:modified", this.handleModified);
311
+ canvas.on("object:modified", this.handleModified);
165
312
  }
166
313
 
167
- const opts = this.options;
314
+ this.initializeHoles();
315
+ }
316
+
317
+ private initializeHoles() {
318
+ if (!this.canvasService) return;
168
319
  // Default hole if none exist
169
- if (!opts.holes || opts.holes.length === 0) {
170
- let defaultPos = { x: editor.canvas.width! / 2, y: 50 };
320
+ if (!this.holes || this.holes.length === 0) {
321
+ let defaultPos = { x: this.canvasService.canvas.width! / 2, y: 50 };
171
322
 
172
- const g = this.getDielineGeometry(editor);
173
- if (g) {
323
+ if (this.currentGeometry) {
324
+ const g = this.currentGeometry;
174
325
  // Default to Top-Center of Dieline shape
175
326
  const topCenter = { x: g.x, y: g.y - g.height / 2 };
176
327
  // Snap to exact shape edge
@@ -181,138 +332,98 @@ export class HoleTool implements Extension<HoleToolOptions> {
181
332
  defaultPos = snapped;
182
333
  }
183
334
 
184
- opts.holes = [defaultPos];
335
+ this.holes = [defaultPos];
185
336
  }
186
337
 
187
- this.options = { ...opts };
188
- this.redraw(editor);
189
-
190
- // Ensure Dieline updates to reflect current holes (fusion effect)
191
- const dielineTool = editor.getExtension("DielineTool") as DielineTool;
192
- if (dielineTool && dielineTool.updateDieline) {
193
- dielineTool.updateDieline(editor);
194
- }
338
+ this.redraw();
339
+ this.syncHolesToDieline();
195
340
  }
196
341
 
197
- private teardown(editor: Editor) {
342
+ private teardown() {
343
+ if (!this.canvasService) return;
344
+ const canvas = this.canvasService.canvas;
345
+
198
346
  if (this.handleMoving) {
199
- editor.canvas.off("object:moving", this.handleMoving);
347
+ canvas.off("object:moving", this.handleMoving);
200
348
  this.handleMoving = null;
201
349
  }
202
350
  if (this.handleModified) {
203
- editor.canvas.off("object:modified", this.handleModified);
351
+ canvas.off("object:modified", this.handleModified);
204
352
  this.handleModified = null;
205
353
  }
354
+ if (this.handleDielineChange && this.context) {
355
+ this.context.eventBus.off(
356
+ "dieline:geometry:change",
357
+ this.handleDielineChange,
358
+ );
359
+ this.handleDielineChange = null;
360
+ }
206
361
 
207
- const objects = editor.canvas
362
+ const objects = canvas
208
363
  .getObjects()
209
364
  .filter((obj: any) => obj.data?.type === "hole-marker");
210
- objects.forEach((obj) => editor.canvas.remove(obj));
211
-
212
- editor.canvas.requestRenderAll();
213
- }
214
-
215
- onUpdate(editor: Editor, state: EditorState) {
216
- this.enforceConstraints(editor);
217
- this.redraw(editor);
218
-
219
- // Trigger Dieline update
220
- const dielineTool = editor.getExtension("DielineTool") as any;
221
- if (dielineTool && dielineTool.updateDieline) {
222
- dielineTool.updateDieline(editor);
365
+ objects.forEach((obj) => canvas.remove(obj));
366
+
367
+ // Clear holes from Dieline (visual only, state preserved in HoleTool options)
368
+ if (this.context) {
369
+ const commandService = this.context.services.get<any>("CommandService");
370
+ if (commandService) {
371
+ try {
372
+ commandService.executeCommand("setHoles", []);
373
+ } catch (e) {}
374
+ }
223
375
  }
224
- }
225
376
 
226
- commands: Record<string, Command> = {
227
- reset: {
228
- execute: (editor: Editor) => {
229
- let defaultPos = { x: editor.canvas.width! / 2, y: 50 };
230
-
231
- const g = this.getDielineGeometry(editor);
232
- if (g) {
233
- const topCenter = { x: g.x, y: g.y - g.height / 2 };
234
- defaultPos = getNearestPointOnDieline(topCenter, {
235
- ...g,
236
- holes: [],
237
- } as any);
238
- }
377
+ this.canvasService.requestRenderAll();
378
+ }
239
379
 
240
- this.options = {
241
- innerRadius: 15,
242
- outerRadius: 25,
243
- style: "solid",
244
- holes: [defaultPos],
245
- };
246
- this.redraw(editor);
380
+ private syncHolesFromCanvas() {
381
+ if (!this.canvasService) return;
382
+ const objects = this.canvasService.canvas
383
+ .getObjects()
384
+ .filter((obj: any) => obj.data?.type === "hole-marker");
247
385
 
248
- // Trigger Dieline update
249
- const dielineTool = editor.getExtension("DielineTool") as DielineTool;
250
- if (dielineTool && dielineTool.updateDieline) {
251
- dielineTool.updateDieline(editor);
252
- }
386
+ const holes = objects.map((obj) => ({ x: obj.left!, y: obj.top! }));
387
+ this.holes = holes;
253
388
 
254
- return true;
255
- },
256
- },
257
- addHole: {
258
- execute: (editor: Editor, x: number, y: number) => {
259
- if (!this.options.holes) this.options.holes = [];
260
- this.options.holes.push({ x, y });
261
- this.redraw(editor);
262
-
263
- // Trigger Dieline update
264
- const dielineTool = editor.getExtension("DielineTool") as any;
265
- if (dielineTool && dielineTool.updateDieline) {
266
- dielineTool.updateDieline(editor);
267
- }
389
+ this.syncHolesToDieline();
390
+ }
268
391
 
269
- return true;
270
- },
271
- schema: {
272
- x: {
273
- type: "number",
274
- label: "X Position",
275
- required: true,
276
- },
277
- y: {
278
- type: "number",
279
- label: "Y Position",
280
- required: true,
281
- },
282
- },
283
- },
284
- clearHoles: {
285
- execute: (editor: Editor) => {
286
- this.options.holes = [];
287
- this.redraw(editor);
288
-
289
- // Trigger Dieline update
290
- const dielineTool = editor.getExtension("DielineTool") as any;
291
- if (dielineTool && dielineTool.updateDieline) {
292
- dielineTool.updateDieline(editor);
293
- }
392
+ private syncHolesToDieline() {
393
+ if (!this.context || !this.canvasService) return;
294
394
 
295
- return true;
296
- },
297
- },
298
- };
395
+ const { holes, innerRadius, outerRadius } = this;
396
+ const currentHoles = holes || [];
397
+ const width = this.canvasService.canvas.width || 800;
398
+ const height = this.canvasService.canvas.height || 600;
299
399
 
300
- private syncHolesFromCanvas(editor: Editor) {
301
- const objects = editor.canvas
302
- .getObjects()
303
- .filter((obj: any) => obj.data?.type === "hole-marker");
304
- const holes = objects.map((obj) => ({ x: obj.left!, y: obj.top! }));
305
- this.options.holes = holes;
400
+ const configService = this.context.services.get<ConfigurationService>(
401
+ "ConfigurationService",
402
+ );
306
403
 
307
- // Trigger Dieline update for real-time fusion effect
308
- const dielineTool = editor.getExtension("DielineTool") as any;
309
- if (dielineTool && dielineTool.updateDieline) {
310
- dielineTool.updateDieline(editor);
404
+ if (configService) {
405
+ this.isUpdatingConfig = true;
406
+ try {
407
+ // Update dieline.holes (Normalized coordinates)
408
+ const normalizedHoles = currentHoles.map((h) => {
409
+ const p = Coordinate.normalizePoint(h, { width, height });
410
+ return {
411
+ x: p.x,
412
+ y: p.y,
413
+ innerRadius,
414
+ outerRadius,
415
+ };
416
+ });
417
+ configService.update("dieline.holes", normalizedHoles);
418
+ } finally {
419
+ this.isUpdatingConfig = false;
420
+ }
311
421
  }
312
422
  }
313
423
 
314
- private redraw(editor: Editor) {
315
- const canvas = editor.canvas;
424
+ private redraw() {
425
+ if (!this.canvasService) return;
426
+ const canvas = this.canvasService.canvas;
316
427
 
317
428
  // Remove existing holes
318
429
  const existing = canvas
@@ -320,10 +431,10 @@ export class HoleTool implements Extension<HoleToolOptions> {
320
431
  .filter((obj: any) => obj.data?.type === "hole-marker");
321
432
  existing.forEach((obj) => canvas.remove(obj));
322
433
 
323
- const { innerRadius, outerRadius, style, holes } = this.options;
434
+ const { innerRadius, outerRadius, style, holes } = this;
324
435
 
325
436
  if (!holes || holes.length === 0) {
326
- canvas.requestRenderAll();
437
+ this.canvasService.requestRenderAll();
327
438
  return;
328
439
  }
329
440
 
@@ -383,10 +494,85 @@ export class HoleTool implements Extension<HoleToolOptions> {
383
494
  });
384
495
 
385
496
  canvas.add(holeGroup);
497
+
498
+ // Ensure hole markers are always on top of Dieline layer
499
+ // Dieline layer uses bringObjectToFront, so we must be aggressive
500
+ // But we can't control when Dieline updates.
501
+ // Ideally, HoleTool should use a dedicated overlay layer above Dieline.
502
+ // For now, let's just bring to front.
386
503
  canvas.bringObjectToFront(holeGroup);
387
504
  });
388
505
 
389
- canvas.requestRenderAll();
506
+ // Also bring all existing markers to front to be safe
507
+ const markers = canvas.getObjects().filter((o: any) => o.data?.type === "hole-marker");
508
+ markers.forEach(m => canvas.bringObjectToFront(m));
509
+
510
+ this.canvasService.requestRenderAll();
511
+ }
512
+
513
+ public enforceConstraints(): boolean {
514
+ const geometry = this.currentGeometry;
515
+ if (!geometry || !this.canvasService) {
516
+ console.log(
517
+ "[HoleTool] Skipping enforceConstraints: No geometry or canvas service",
518
+ );
519
+ return false;
520
+ }
521
+
522
+ const effectiveOffset =
523
+ this.constraintTarget === "original" ? 0 : geometry.offset;
524
+ const constraintGeometry = {
525
+ ...geometry,
526
+ width: Math.max(0, geometry.width + effectiveOffset * 2),
527
+ height: Math.max(0, geometry.height + effectiveOffset * 2),
528
+ radius: Math.max(0, geometry.radius + effectiveOffset),
529
+ };
530
+
531
+ // Get all hole markers
532
+ const objects = this.canvasService.canvas
533
+ .getObjects()
534
+ .filter((obj: any) => obj.data?.type === "hole-marker");
535
+
536
+ console.log(
537
+ `[HoleTool] Enforcing constraints on ${objects.length} markers`,
538
+ );
539
+
540
+ let changed = false;
541
+ // Sort objects by index to maintain order in options.holes
542
+ objects.sort(
543
+ (a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0),
544
+ );
545
+
546
+ const newHoles: { x: number; y: number }[] = [];
547
+
548
+ objects.forEach((obj: any) => {
549
+ const currentPos = new Point(obj.left, obj.top);
550
+ const newPos = this.calculateConstrainedPosition(
551
+ currentPos,
552
+ constraintGeometry,
553
+ );
554
+
555
+ if (currentPos.distanceFrom(newPos) > 0.1) {
556
+ console.log(
557
+ `[HoleTool] Moving hole from (${currentPos.x}, ${currentPos.y}) to (${newPos.x}, ${newPos.y})`,
558
+ );
559
+ obj.set({
560
+ left: newPos.x,
561
+ top: newPos.y,
562
+ });
563
+ obj.setCoords();
564
+ changed = true;
565
+ }
566
+ newHoles.push({ x: obj.left, y: obj.top });
567
+ });
568
+
569
+ if (changed) {
570
+ this.holes = newHoles;
571
+ this.canvasService.requestRenderAll();
572
+ // We return true instead of syncing directly to avoid recursion
573
+ return true;
574
+ }
575
+ return false;
390
576
  }
391
577
 
392
578
  private calculateConstrainedPosition(p: Point, g: DielineGeometry): Point {
@@ -408,11 +594,6 @@ export class HoleTool implements Extension<HoleToolOptions> {
408
594
  const nearestP = new Point(nearest.x, nearest.y);
409
595
  const dist = p.distanceFrom(nearestP);
410
596
 
411
- // Determine if point is inside or outside
412
- // Simple heuristic: distance from center
413
- // Or using paper.js contains() if we had the full path object
414
- // For convex shapes, center distance works mostly, but let's use the vector direction
415
-
416
597
  // Vector from nearest to current point
417
598
  const v = p.subtract(nearestP);
418
599
 
@@ -420,24 +601,6 @@ export class HoleTool implements Extension<HoleToolOptions> {
420
601
  const center = new Point(g.x, g.y);
421
602
  const centerToNearest = nearestP.subtract(center);
422
603
 
423
- // Dot product to see if they align (outside) or oppose (inside)
424
- // If point is exactly on line, dist is 0.
425
-
426
- // However, we want to constrain the point to be within [innerRadius, -outerRadius] distance from the edge.
427
- // Actually, usually users want to snap to the edge or stay within a reasonable margin.
428
- // The previous logic clamped the distance.
429
-
430
- // Let's implement a simple snap-to-edge if close, otherwise allow free movement but clamp max distance?
431
- // Or reproduce the previous "slide along edge" behavior.
432
- // Previous behavior: "clampedDist = Math.min(dist, innerRadius); ... Math.max(dist, -outerRadius)"
433
- // This implies the hole center can be slightly inside or outside the main shape edge.
434
-
435
- // Let's determine sign of distance
436
- // We can use paper.js Shape.contains(point) to check if inside.
437
- // But getNearestPointOnDieline returns just coordinates.
438
-
439
- // Optimization: Let's assume for Dieline shapes (convex-ish),
440
- // if distance from center > distance of nearest from center, it's outside.
441
604
  const distToCenter = p.distanceFrom(center);
442
605
  const nearestDistToCenter = nearestP.distanceFrom(center);
443
606
 
@@ -449,43 +612,16 @@ export class HoleTool implements Extension<HoleToolOptions> {
449
612
  // Clamp distance
450
613
  let clampedDist = signedDist;
451
614
  if (signedDist > 0) {
452
- clampedDist = Math.min(signedDist, this.options.innerRadius);
615
+ clampedDist = Math.min(signedDist, this.innerRadius);
453
616
  } else {
454
- clampedDist = Math.max(signedDist, -this.options.outerRadius);
617
+ clampedDist = Math.max(signedDist, -this.outerRadius);
455
618
  }
456
619
 
457
620
  // Reconstruct point
458
- // If dist is very small, just use nearestP
459
621
  if (dist < 0.001) return nearestP;
460
622
 
461
- // Direction vector normalized
462
- const dir = v.scalarDivide(dist);
463
-
464
- // New point = nearest + dir * clampedDist
465
- // Note: if inside (signedDist < 0), v points towards center (roughly), dist is positive magnitude.
466
- // Wait, v = p - nearest.
467
- // If p is inside, p is closer to center. v points Inwards.
468
- // If we want clampedDist to be negative, we should probably stick to normal vectors.
469
-
470
- // Let's simplify:
471
- // Just place it at nearest point + offset vector.
472
- // Offset vector is 'v' scaled to clampedDist.
473
-
474
- // If p is inside, v points in. length is 'dist'.
475
- // We want length to be 'clampedDist' (magnitude).
476
- // Since clampedDist is negative for inside, we need to be careful with signs.
477
-
478
- // Actually simpler:
479
- // We want the result to lie on the line connecting Center -> P -> Nearest? No.
480
- // We want it on the line Nearest -> P.
481
-
482
- // Current distance is 'dist'.
483
- // Desired distance is abs(clampedDist).
484
- // If clampedDist sign matches signedDist sign, we just scale v.
485
-
623
+ // We want the result to lie on the line connecting Nearest -> P
486
624
  const scale = Math.abs(clampedDist) / (dist || 1);
487
-
488
- // If we are clamping, we just scale the vector from nearest.
489
625
  const offset = v.scalarMultiply(scale);
490
626
 
491
627
  return nearestP.add(offset);