@pooder/kit 3.0.0 → 3.1.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
@@ -9,7 +9,11 @@ import {
9
9
  import { Circle, Group, Point } from "fabric";
10
10
  import CanvasService from "./CanvasService";
11
11
  import { DielineGeometry } from "./dieline";
12
- import { getNearestPointOnDieline, HoleData } from "./geometry";
12
+ import {
13
+ getNearestPointOnDieline,
14
+ HoleData,
15
+ resolveHolePosition,
16
+ } from "./geometry";
13
17
  import { Coordinate } from "./coordinate";
14
18
 
15
19
  export class HoleTool implements Extension {
@@ -19,10 +23,7 @@ export class HoleTool implements Extension {
19
23
  name: "HoleTool",
20
24
  };
21
25
 
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 holes: HoleData[] = [];
26
27
  private constraintTarget: "original" | "bleed" = "bleed";
27
28
 
28
29
  private canvasService?: CanvasService;
@@ -39,12 +40,9 @@ export class HoleTool implements Extension {
39
40
 
40
41
  constructor(
41
42
  options?: Partial<{
42
- innerRadius: number;
43
- outerRadius: number;
44
- style: "solid" | "dashed";
45
- holes: Array<{ x: number; y: number }>;
43
+ holes: HoleData[];
46
44
  constraintTarget: "original" | "bleed";
47
- }>,
45
+ }>
48
46
  ) {
49
47
  if (options) {
50
48
  Object.assign(this, options);
@@ -61,64 +59,31 @@ export class HoleTool implements Extension {
61
59
  }
62
60
 
63
61
  const configService = context.services.get<ConfigurationService>(
64
- "ConfigurationService",
62
+ "ConfigurationService"
65
63
  );
66
64
  if (configService) {
67
65
  // 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
66
  this.constraintTarget = configService.get(
78
67
  "hole.constraintTarget",
79
- this.constraintTarget,
68
+ this.constraintTarget
80
69
  );
81
70
 
82
71
  // 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
- }
94
-
72
+ this.holes = configService.get("dieline.holes", []);
73
+
95
74
  // Listen for changes
96
75
  configService.onAnyChange((e: { key: string; value: any }) => {
97
76
  if (this.isUpdatingConfig) return;
98
77
 
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
- }
78
+ if (e.key === "hole.constraintTarget") {
79
+ this.constraintTarget = e.value;
80
+ this.enforceConstraints();
107
81
  }
82
+
108
83
  // Listen for dieline.holes changes (e.g. from undo/redo or other sources)
109
84
  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
- }
85
+ this.holes = e.value || [];
86
+ this.redraw();
122
87
  }
123
88
  });
124
89
  }
@@ -135,29 +100,6 @@ export class HoleTool implements Extension {
135
100
  contribute() {
136
101
  return {
137
102
  [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
103
  {
162
104
  id: "hole.constraintTarget",
163
105
  type: "select",
@@ -183,13 +125,25 @@ export class HoleTool implements Extension {
183
125
  } as any);
184
126
  }
185
127
 
186
- this.innerRadius = 15;
187
- this.outerRadius = 25;
188
- this.style = "solid";
189
- this.holes = [defaultPos];
128
+ const { width, height } = this.canvasService.canvas;
129
+ const normalizedHole = Coordinate.normalizePoint(defaultPos, {
130
+ width: width || 800,
131
+ height: height || 600,
132
+ });
190
133
 
191
- this.redraw();
192
- this.syncHolesToDieline();
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
+ }
193
147
  return true;
194
148
  },
195
149
  },
@@ -197,10 +151,33 @@ export class HoleTool implements Extension {
197
151
  command: "addHole",
198
152
  title: "Add Hole",
199
153
  handler: (x: number, y: number) => {
200
- if (!this.holes) this.holes = [];
201
- this.holes.push({ x, y });
202
- this.redraw();
203
- this.syncHolesToDieline();
154
+ if (!this.canvasService) return false;
155
+ const { width, height } = this.canvasService.canvas;
156
+
157
+ const normalizedHole = Coordinate.normalizePoint(
158
+ { x, y },
159
+ { width: width || 800, height: height || 600 }
160
+ );
161
+
162
+ const configService = this.context?.services.get<ConfigurationService>(
163
+ "ConfigurationService"
164
+ );
165
+
166
+ if (configService) {
167
+ const currentHoles = configService.get("dieline.holes", []) as HoleData[];
168
+ // Use last hole's radii or default
169
+ const lastHole = currentHoles[currentHoles.length - 1];
170
+ const innerRadius = lastHole?.innerRadius ?? 15;
171
+ const outerRadius = lastHole?.outerRadius ?? 25;
172
+
173
+ const newHole = {
174
+ x: normalizedHole.x,
175
+ y: normalizedHole.y,
176
+ innerRadius,
177
+ outerRadius,
178
+ };
179
+ configService.update("dieline.holes", [...currentHoles, newHole]);
180
+ }
204
181
  return true;
205
182
  },
206
183
  },
@@ -208,9 +185,12 @@ export class HoleTool implements Extension {
208
185
  command: "clearHoles",
209
186
  title: "Clear Holes",
210
187
  handler: () => {
211
- this.holes = [];
212
- this.redraw();
213
- this.syncHolesToDieline();
188
+ const configService = this.context?.services.get<ConfigurationService>(
189
+ "ConfigurationService"
190
+ );
191
+ if (configService) {
192
+ configService.update("dieline.holes", []);
193
+ }
214
194
  return true;
215
195
  },
216
196
  },
@@ -274,6 +254,9 @@ export class HoleTool implements Extension {
274
254
 
275
255
  if (!this.currentGeometry) return;
276
256
 
257
+ const index = target.data?.index ?? -1;
258
+ const holeData = this.holes[index];
259
+
277
260
  // Calculate effective geometry based on constraint target
278
261
  const effectiveOffset =
279
262
  this.constraintTarget === "original"
@@ -284,13 +267,18 @@ export class HoleTool implements Extension {
284
267
  width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
285
268
  height: Math.max(
286
269
  0,
287
- this.currentGeometry.height + effectiveOffset * 2,
270
+ this.currentGeometry.height + effectiveOffset * 2
288
271
  ),
289
272
  radius: Math.max(0, this.currentGeometry.radius + effectiveOffset),
290
273
  };
291
274
 
292
275
  const p = new Point(target.left, target.top);
293
- const newPos = this.calculateConstrainedPosition(p, constraintGeometry);
276
+ const newPos = this.calculateConstrainedPosition(
277
+ p,
278
+ constraintGeometry,
279
+ holeData?.innerRadius ?? 15,
280
+ holeData?.outerRadius ?? 25
281
+ );
294
282
 
295
283
  target.set({
296
284
  left: newPos.x,
@@ -316,25 +304,6 @@ export class HoleTool implements Extension {
316
304
 
317
305
  private initializeHoles() {
318
306
  if (!this.canvasService) return;
319
- // Default hole if none exist
320
- if (!this.holes || this.holes.length === 0) {
321
- let defaultPos = { x: this.canvasService.canvas.width! / 2, y: 50 };
322
-
323
- if (this.currentGeometry) {
324
- const g = this.currentGeometry;
325
- // Default to Top-Center of Dieline shape
326
- const topCenter = { x: g.x, y: g.y - g.height / 2 };
327
- // Snap to exact shape edge
328
- const snapped = getNearestPointOnDieline(topCenter, {
329
- ...g,
330
- holes: [],
331
- } as any);
332
- defaultPos = snapped;
333
- }
334
-
335
- this.holes = [defaultPos];
336
- }
337
-
338
307
  this.redraw();
339
308
  this.syncHolesToDieline();
340
309
  }
@@ -383,38 +352,110 @@ export class HoleTool implements Extension {
383
352
  .getObjects()
384
353
  .filter((obj: any) => obj.data?.type === "hole-marker");
385
354
 
386
- const holes = objects.map((obj) => ({ x: obj.left!, y: obj.top! }));
387
- this.holes = holes;
355
+ // Sort objects by index
356
+ objects.sort(
357
+ (a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0)
358
+ );
359
+
360
+ // Update holes based on canvas positions
361
+ // We need to preserve original hole properties (radii, anchor)
362
+ // If a hole has an anchor, we update offsetX/Y instead of x/y
363
+ const newHoles = objects.map((obj, i) => {
364
+ const original = this.holes[i];
365
+ const newAbsX = obj.left!;
366
+ const newAbsY = obj.top!;
367
+
368
+ if (original && original.anchor && this.currentGeometry) {
369
+ // Reverse calculate offset from anchor
370
+ const { x, y, width, height } = this.currentGeometry;
371
+ let bx = x;
372
+ let by = y;
373
+ const left = x - width / 2;
374
+ const right = x + width / 2;
375
+ const top = y - height / 2;
376
+ const bottom = y + height / 2;
377
+
378
+ switch (original.anchor) {
379
+ case "top-left":
380
+ bx = left;
381
+ by = top;
382
+ break;
383
+ case "top-center":
384
+ bx = x;
385
+ by = top;
386
+ break;
387
+ case "top-right":
388
+ bx = right;
389
+ by = top;
390
+ break;
391
+ case "center-left":
392
+ bx = left;
393
+ by = y;
394
+ break;
395
+ case "center":
396
+ bx = x;
397
+ by = y;
398
+ break;
399
+ case "center-right":
400
+ bx = right;
401
+ by = y;
402
+ break;
403
+ case "bottom-left":
404
+ bx = left;
405
+ by = bottom;
406
+ break;
407
+ case "bottom-center":
408
+ bx = x;
409
+ by = bottom;
410
+ break;
411
+ case "bottom-right":
412
+ bx = right;
413
+ by = bottom;
414
+ break;
415
+ }
416
+
417
+ return {
418
+ ...original,
419
+ offsetX: newAbsX - bx,
420
+ offsetY: newAbsY - by,
421
+ // Clear direct coordinates if we use anchor
422
+ x: undefined,
423
+ y: undefined,
424
+ };
425
+ }
426
+
427
+ // If no anchor, use normalized coordinates
428
+ const { width, height } = this.canvasService!.canvas;
429
+ const p = Coordinate.normalizePoint(
430
+ { x: newAbsX, y: newAbsY },
431
+ { width: width || 800, height: height || 600 }
432
+ );
433
+
434
+ return {
435
+ ...original,
436
+ x: p.x,
437
+ y: p.y,
438
+ // Ensure radii are preserved
439
+ innerRadius: original?.innerRadius ?? 15,
440
+ outerRadius: original?.outerRadius ?? 25,
441
+ };
442
+ });
388
443
 
444
+ this.holes = newHoles;
389
445
  this.syncHolesToDieline();
390
446
  }
391
447
 
392
448
  private syncHolesToDieline() {
393
449
  if (!this.context || !this.canvasService) return;
394
450
 
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;
399
-
400
451
  const configService = this.context.services.get<ConfigurationService>(
401
- "ConfigurationService",
452
+ "ConfigurationService"
402
453
  );
403
454
 
404
455
  if (configService) {
405
456
  this.isUpdatingConfig = true;
406
457
  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);
458
+ configService.update("dieline.holes", this.holes);
418
459
  } finally {
419
460
  this.isUpdatingConfig = false;
420
461
  }
@@ -424,6 +465,7 @@ export class HoleTool implements Extension {
424
465
  private redraw() {
425
466
  if (!this.canvasService) return;
426
467
  const canvas = this.canvasService.canvas;
468
+ const { width, height } = canvas;
427
469
 
428
470
  // Remove existing holes
429
471
  const existing = canvas
@@ -431,16 +473,31 @@ export class HoleTool implements Extension {
431
473
  .filter((obj: any) => obj.data?.type === "hole-marker");
432
474
  existing.forEach((obj) => canvas.remove(obj));
433
475
 
434
- const { innerRadius, outerRadius, style, holes } = this;
476
+ const holes = this.holes;
435
477
 
436
478
  if (!holes || holes.length === 0) {
437
479
  this.canvasService.requestRenderAll();
438
480
  return;
439
481
  }
440
482
 
483
+ // Resolve geometry if needed for anchors
484
+ const geometry = this.currentGeometry || {
485
+ x: (width || 800) / 2,
486
+ y: (height || 600) / 2,
487
+ width: width || 800,
488
+ height: height || 600,
489
+ };
490
+
441
491
  holes.forEach((hole, index) => {
492
+ // Resolve position
493
+ const pos = resolveHolePosition(
494
+ hole,
495
+ geometry,
496
+ { width: width || 800, height: height || 600 }
497
+ );
498
+
442
499
  const innerCircle = new Circle({
443
- radius: innerRadius,
500
+ radius: hole.innerRadius,
444
501
  fill: "transparent",
445
502
  stroke: "red",
446
503
  strokeWidth: 2,
@@ -449,18 +506,18 @@ export class HoleTool implements Extension {
449
506
  });
450
507
 
451
508
  const outerCircle = new Circle({
452
- radius: outerRadius,
509
+ radius: hole.outerRadius,
453
510
  fill: "transparent",
454
511
  stroke: "#666",
455
512
  strokeWidth: 1,
456
- strokeDashArray: style === "dashed" ? [5, 5] : undefined,
513
+ strokeDashArray: [5, 5],
457
514
  originX: "center",
458
515
  originY: "center",
459
516
  });
460
517
 
461
518
  const holeGroup = new Group([outerCircle, innerCircle], {
462
- left: hole.x,
463
- top: hole.y,
519
+ left: pos.x,
520
+ top: pos.y,
464
521
  originX: "center",
465
522
  originY: "center",
466
523
  selectable: true,
@@ -494,12 +551,6 @@ export class HoleTool implements Extension {
494
551
  });
495
552
 
496
553
  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.
503
554
  canvas.bringObjectToFront(holeGroup);
504
555
  });
505
556
 
@@ -513,9 +564,6 @@ export class HoleTool implements Extension {
513
564
  public enforceConstraints(): boolean {
514
565
  const geometry = this.currentGeometry;
515
566
  if (!geometry || !this.canvasService) {
516
- console.log(
517
- "[HoleTool] Skipping enforceConstraints: No geometry or canvas service",
518
- );
519
567
  return false;
520
568
  }
521
569
 
@@ -533,29 +581,27 @@ export class HoleTool implements Extension {
533
581
  .getObjects()
534
582
  .filter((obj: any) => obj.data?.type === "hole-marker");
535
583
 
536
- console.log(
537
- `[HoleTool] Enforcing constraints on ${objects.length} markers`,
538
- );
539
-
540
584
  let changed = false;
541
585
  // Sort objects by index to maintain order in options.holes
542
586
  objects.sort(
543
- (a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0),
587
+ (a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0)
544
588
  );
545
589
 
546
- const newHoles: { x: number; y: number }[] = [];
590
+ const newHoles: HoleData[] = [];
547
591
 
548
- objects.forEach((obj: any) => {
592
+ objects.forEach((obj: any, i: number) => {
549
593
  const currentPos = new Point(obj.left, obj.top);
594
+ // We need to pass the hole's radii to calculateConstrainedPosition
595
+ const holeData = this.holes[i];
596
+
550
597
  const newPos = this.calculateConstrainedPosition(
551
598
  currentPos,
552
599
  constraintGeometry,
600
+ holeData?.innerRadius ?? 15,
601
+ holeData?.outerRadius ?? 25
553
602
  );
554
603
 
555
604
  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
605
  obj.set({
560
606
  left: newPos.x,
561
607
  top: newPos.y,
@@ -563,23 +609,28 @@ export class HoleTool implements Extension {
563
609
  obj.setCoords();
564
610
  changed = true;
565
611
  }
566
- newHoles.push({ x: obj.left, y: obj.top });
612
+
613
+ // Update data logic is handled in syncHolesFromCanvas which is called on modified
614
+ // But here we are modifying programmatically.
615
+ // We should probably just let the visual update happen, and then sync?
616
+ // Or just push to newHoles list to verify change?
567
617
  });
568
618
 
569
619
  if (changed) {
570
- this.holes = newHoles;
571
- this.canvasService.requestRenderAll();
572
- // We return true instead of syncing directly to avoid recursion
620
+ // If we moved things programmatically, we should update the state
621
+ this.syncHolesFromCanvas();
573
622
  return true;
574
623
  }
575
624
  return false;
576
625
  }
577
626
 
578
- private calculateConstrainedPosition(p: Point, g: DielineGeometry): Point {
627
+ private calculateConstrainedPosition(
628
+ p: Point,
629
+ g: DielineGeometry,
630
+ innerRadius: number,
631
+ outerRadius: number
632
+ ): Point {
579
633
  // Use Paper.js to get accurate nearest point
580
- // This handles ellipses, rects, and rounded rects correctly
581
-
582
- // Convert to holes format for geometry options
583
634
  const options = {
584
635
  ...g,
585
636
  holes: [], // We don't need holes for boundary calculation
@@ -599,7 +650,6 @@ export class HoleTool implements Extension {
599
650
 
600
651
  // Vector from center to nearest point (approximate normal for convex shapes)
601
652
  const center = new Point(g.x, g.y);
602
- const centerToNearest = nearestP.subtract(center);
603
653
 
604
654
  const distToCenter = p.distanceFrom(center);
605
655
  const nearestDistToCenter = nearestP.distanceFrom(center);
@@ -612,9 +662,9 @@ export class HoleTool implements Extension {
612
662
  // Clamp distance
613
663
  let clampedDist = signedDist;
614
664
  if (signedDist > 0) {
615
- clampedDist = Math.min(signedDist, this.innerRadius);
665
+ clampedDist = Math.min(signedDist, innerRadius);
616
666
  } else {
617
- clampedDist = Math.max(signedDist, -this.outerRadius);
667
+ clampedDist = Math.max(signedDist, -outerRadius);
618
668
  }
619
669
 
620
670
  // Reconstruct point