@pooder/kit 3.1.0 → 3.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pooder/kit",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Standard plugins for Pooder editor",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "paper": "^0.12.18",
21
21
  "fabric": "^7.0.0",
22
- "@pooder/core": "1.1.0"
22
+ "@pooder/core": "1.2.0"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsup src/index.ts --format cjs,esm --dts",
package/src/coordinate.ts CHANGED
@@ -8,7 +8,45 @@ export interface Size {
8
8
  height: number;
9
9
  }
10
10
 
11
+ export type Unit = "px" | "mm" | "cm" | "in";
12
+
13
+ export interface Layout {
14
+ scale: number;
15
+ offsetX: number;
16
+ offsetY: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+
11
21
  export class Coordinate {
22
+ /**
23
+ * Calculate layout to fit content within container while preserving aspect ratio.
24
+ */
25
+ static calculateLayout(
26
+ container: Size,
27
+ content: Size,
28
+ padding: number = 0,
29
+ ): Layout {
30
+ const availableWidth = Math.max(0, container.width - padding * 2);
31
+ const availableHeight = Math.max(0, container.height - padding * 2);
32
+
33
+ if (content.width === 0 || content.height === 0) {
34
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
35
+ }
36
+
37
+ const scaleX = availableWidth / content.width;
38
+ const scaleY = availableHeight / content.height;
39
+ const scale = Math.min(scaleX, scaleY);
40
+
41
+ const width = content.width * scale;
42
+ const height = content.height * scale;
43
+
44
+ const offsetX = (container.width - width) / 2;
45
+ const offsetY = (container.height - height) / 2;
46
+
47
+ return { scale, offsetX, offsetY, width, height };
48
+ }
49
+
12
50
  /**
13
51
  * Convert an absolute value to a normalized value (0-1).
14
52
  * @param value Absolute value (e.g., pixels)
@@ -46,4 +84,23 @@ export class Coordinate {
46
84
  y: this.toAbsolute(point.y, size.height),
47
85
  };
48
86
  }
87
+
88
+ static convertUnit(value: number, from: Unit, to: Unit): number {
89
+ if (from === to) return value;
90
+
91
+ // Base unit: mm
92
+ const toMM: Record<Unit, number> = {
93
+ px: 0.264583, // 1px = 0.264583mm (96 DPI)
94
+ mm: 1,
95
+ cm: 10,
96
+ in: 25.4
97
+ };
98
+
99
+ const mmValue = value * (from === 'px' ? toMM.px : toMM[from] || 1);
100
+
101
+ if (to === 'px') {
102
+ return mmValue / toMM.px;
103
+ }
104
+ return mmValue / (toMM[to] || 1);
105
+ }
49
106
  }
package/src/dieline.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  import { Path, Pattern } from "fabric";
9
9
  import CanvasService from "./CanvasService";
10
10
  import { ImageTracer } from "./tracer";
11
- import { Coordinate } from "./coordinate";
11
+ import { Coordinate, Unit } from "./coordinate";
12
12
  import {
13
13
  generateDielinePath,
14
14
  generateMaskPath,
@@ -20,6 +20,7 @@ import {
20
20
 
21
21
  export interface DielineGeometry {
22
22
  shape: "rect" | "circle" | "ellipse" | "custom";
23
+ unit: Unit;
23
24
  x: number;
24
25
  y: number;
25
26
  width: number;
@@ -27,6 +28,7 @@ export interface DielineGeometry {
27
28
  radius: number;
28
29
  offset: number;
29
30
  borderLength?: number;
31
+ scale?: number;
30
32
  pathData?: string;
31
33
  }
32
34
 
@@ -36,6 +38,7 @@ export class DielineTool implements Extension {
36
38
  name: "DielineTool",
37
39
  };
38
40
 
41
+ private unit: Unit = "mm";
39
42
  private shape: "rect" | "circle" | "ellipse" | "custom" = "rect";
40
43
  private width: number = 500;
41
44
  private height: number = 500;
@@ -48,7 +51,7 @@ export class DielineTool implements Extension {
48
51
  private holes: HoleData[] = [];
49
52
  // Position is stored as normalized coordinates (0-1)
50
53
  private position?: { x: number; y: number };
51
- private borderLength?: number;
54
+ private padding: number | string = 140;
52
55
  private pathData?: string;
53
56
 
54
57
  private canvasService?: CanvasService;
@@ -56,13 +59,14 @@ export class DielineTool implements Extension {
56
59
 
57
60
  constructor(
58
61
  options?: Partial<{
62
+ unit: Unit;
59
63
  shape: "rect" | "circle" | "ellipse" | "custom";
60
64
  width: number;
61
65
  height: number;
62
66
  radius: number;
63
67
  // Position is normalized (0-1)
64
68
  position: { x: number; y: number };
65
- borderLength: number;
69
+ padding: number | string;
66
70
  offset: number;
67
71
  style: "solid" | "dashed";
68
72
  insideColor: string;
@@ -88,14 +92,12 @@ export class DielineTool implements Extension {
88
92
  const configService = context.services.get<any>("ConfigurationService");
89
93
  if (configService) {
90
94
  // Load initial config
95
+ this.unit = configService.get("dieline.unit", this.unit);
91
96
  this.shape = configService.get("dieline.shape", this.shape);
92
97
  this.width = configService.get("dieline.width", this.width);
93
98
  this.height = configService.get("dieline.height", this.height);
94
99
  this.radius = configService.get("dieline.radius", this.radius);
95
- this.borderLength = configService.get(
96
- "dieline.borderLength",
97
- this.borderLength,
98
- );
100
+ this.padding = configService.get("dieline.padding", this.padding);
99
101
  this.offset = configService.get("dieline.offset", this.offset);
100
102
  this.style = configService.get("dieline.style", this.style);
101
103
  this.insideColor = configService.get(
@@ -141,6 +143,13 @@ export class DielineTool implements Extension {
141
143
  contribute() {
142
144
  return {
143
145
  [ContributionPointIds.CONFIGURATIONS]: [
146
+ {
147
+ id: "dieline.unit",
148
+ type: "select",
149
+ label: "Unit",
150
+ options: ["px", "mm", "cm", "in"],
151
+ default: this.unit,
152
+ },
144
153
  {
145
154
  id: "dieline.shape",
146
155
  type: "select",
@@ -176,15 +185,14 @@ export class DielineTool implements Extension {
176
185
  id: "dieline.position",
177
186
  type: "json",
178
187
  label: "Position (Normalized)",
179
- default: this.position,
188
+ default: this.radius,
180
189
  },
181
190
  {
182
- id: "dieline.borderLength",
183
- type: "number",
184
- label: "Margin",
185
- min: 0,
186
- max: 500,
187
- default: this.borderLength,
191
+ id: "dieline.padding",
192
+ type: "select",
193
+ label: "View Padding",
194
+ options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
195
+ default: this.padding,
188
196
  },
189
197
  {
190
198
  id: "dieline.offset",
@@ -255,8 +263,9 @@ export class DielineTool implements Extension {
255
263
  const newWidth = bounds.width * scale;
256
264
  const newHeight = bounds.height * scale;
257
265
 
258
- const configService =
259
- this.context?.services.get<any>("ConfigurationService");
266
+ const configService = this.context?.services.get<any>(
267
+ "ConfigurationService",
268
+ );
260
269
  if (configService) {
261
270
  configService.update("dieline.width", newWidth);
262
271
  configService.update("dieline.height", newHeight);
@@ -336,12 +345,30 @@ export class DielineTool implements Extension {
336
345
  return new Pattern({ source: canvas, repetition: "repeat" });
337
346
  }
338
347
 
348
+ private resolvePadding(
349
+ containerWidth: number,
350
+ containerHeight: number,
351
+ ): number {
352
+ if (typeof this.padding === "number") {
353
+ return this.padding;
354
+ }
355
+ if (typeof this.padding === "string") {
356
+ if (this.padding.endsWith("%")) {
357
+ const percent = parseFloat(this.padding) / 100;
358
+ return Math.min(containerWidth, containerHeight) * percent;
359
+ }
360
+ return parseFloat(this.padding) || 0;
361
+ }
362
+ return 0;
363
+ }
364
+
339
365
  public updateDieline(emitEvent: boolean = true) {
340
366
  if (!this.canvasService) return;
341
367
  const layer = this.getLayer();
342
368
  if (!layer) return;
343
369
 
344
370
  const {
371
+ unit,
345
372
  shape,
346
373
  radius,
347
374
  offset,
@@ -349,7 +376,6 @@ export class DielineTool implements Extension {
349
376
  insideColor,
350
377
  outsideColor,
351
378
  position,
352
- borderLength,
353
379
  showBleedLines,
354
380
  holes,
355
381
  } = this;
@@ -358,45 +384,72 @@ export class DielineTool implements Extension {
358
384
  const canvasW = this.canvasService.canvas.width || 800;
359
385
  const canvasH = this.canvasService.canvas.height || 600;
360
386
 
361
- let visualWidth = width;
362
- let visualHeight = height;
363
-
364
- if (borderLength && borderLength > 0) {
365
- visualWidth = Math.max(0, canvasW - borderLength * 2);
366
- visualHeight = Math.max(0, canvasH - borderLength * 2);
367
- }
368
-
369
- const cx = Coordinate.toAbsolute(position?.x ?? 0.5, canvasW);
370
- const cy = Coordinate.toAbsolute(position?.y ?? 0.5, canvasH);
387
+ // Calculate Layout based on Physical Dimensions and Canvas Size
388
+ // Add padding to avoid edge hugging
389
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
390
+ const layout = Coordinate.calculateLayout(
391
+ { width: canvasW, height: canvasH },
392
+ { width, height },
393
+ paddingPx,
394
+ );
395
+
396
+ const scale = layout.scale;
397
+ const cx = layout.offsetX + layout.width / 2;
398
+ const cy = layout.offsetY + layout.height / 2;
399
+
400
+ // Scaled dimensions for rendering (Pixels)
401
+ const visualWidth = layout.width;
402
+ const visualHeight = layout.height;
403
+ const visualRadius = radius * scale;
404
+ const visualOffset = offset * scale;
371
405
 
372
406
  // Clear existing objects
373
407
  layer.remove(...layer.getObjects());
374
408
 
375
- // Resolve Holes for Geometry Generation
409
+ // Resolve Holes for Geometry Generation (using visual coordinates)
376
410
  const geometryForHoles = {
377
411
  x: cx,
378
412
  y: cy,
379
413
  width: visualWidth,
380
414
  height: visualHeight,
415
+ // Pass scale/unit context if needed by resolveHolePosition (though currently unused there)
381
416
  };
382
417
 
383
418
  const absoluteHoles = (holes || []).map((h) => {
384
- const pos = resolveHolePosition(
385
- h,
386
- geometryForHoles,
387
- { width: canvasW, height: canvasH }
388
- );
419
+ // Scale hole radii and offsets: mm -> current unit -> pixels
420
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
421
+ const offsetScale = unitScale * scale;
422
+
423
+ // Apply scaling to offsets BEFORE resolving position
424
+ const hWithPixelOffsets = {
425
+ ...h,
426
+ offsetX: (h.offsetX || 0) * offsetScale,
427
+ offsetY: (h.offsetY || 0) * offsetScale,
428
+ };
429
+
430
+ const pos = resolveHolePosition(hWithPixelOffsets, geometryForHoles, {
431
+ width: canvasW,
432
+ height: canvasH,
433
+ });
434
+
389
435
  return {
390
436
  ...h,
391
437
  x: pos.x,
392
438
  y: pos.y,
439
+ // Scale hole radii: mm -> current unit -> pixels
440
+ innerRadius: h.innerRadius * offsetScale,
441
+ outerRadius: h.outerRadius * offsetScale,
442
+ // Store scaled offsets in the result for consistency, though pos is already resolved
443
+ offsetX: hWithPixelOffsets.offsetX,
444
+ offsetY: hWithPixelOffsets.offsetY,
393
445
  };
394
446
  });
395
447
 
396
448
  // 1. Draw Mask (Outside)
397
- const cutW = Math.max(0, width + offset * 2);
398
- const cutH = Math.max(0, height + offset * 2);
399
- const cutR = radius === 0 ? 0 : Math.max(0, radius + offset);
449
+ const cutW = Math.max(0, visualWidth + visualOffset * 2);
450
+ const cutH = Math.max(0, visualHeight + visualOffset * 2);
451
+ const cutR =
452
+ visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
400
453
 
401
454
  // Use Paper.js to generate the complex mask path
402
455
  const maskPathData = generateMaskPath({
@@ -458,15 +511,15 @@ export class DielineTool implements Extension {
458
511
  const bleedPathData = generateBleedZonePath(
459
512
  {
460
513
  shape,
461
- width,
462
- height,
463
- radius,
514
+ width: visualWidth,
515
+ height: visualHeight,
516
+ radius: visualRadius,
464
517
  x: cx,
465
518
  y: cy,
466
519
  holes: absoluteHoles,
467
520
  pathData: this.pathData,
468
521
  },
469
- offset,
522
+ visualOffset,
470
523
  );
471
524
 
472
525
  // Use solid red for hatch lines to match dieline, background is transparent
@@ -517,12 +570,12 @@ export class DielineTool implements Extension {
517
570
  // generateDielinePath expects holes to be in absolute coordinates (matching width/height scale)
518
571
  const borderPathData = generateDielinePath({
519
572
  shape,
520
- width: width,
521
- height: height,
522
- radius: radius,
573
+ width: visualWidth,
574
+ height: visualHeight,
575
+ radius: visualRadius,
523
576
  x: cx,
524
577
  y: cy,
525
- holes: absoluteHoles, // FIX: Use absoluteHoles instead of holes
578
+ holes: absoluteHoles,
526
579
  pathData: this.pathData,
527
580
  });
528
581
 
@@ -566,6 +619,13 @@ export class DielineTool implements Extension {
566
619
  // Emit change event so other tools (like HoleTool) can react
567
620
  // Only emit if requested (to avoid loops when updating non-geometry props like holes)
568
621
  if (emitEvent && this.context) {
622
+ // FIX: Ensure we use the exact same geometry values as used in rendering above.
623
+ // Although getGeometry() recalculates layout, it should be identical if props haven't changed.
624
+ // But to be absolutely safe and avoid micro-differences or race conditions, we can reuse calculated values.
625
+ // However, getGeometry is public API, so it's better if it's correct.
626
+ // Let's verify getGeometry logic matches updateDieline logic perfectly.
627
+ // Yes, both use Coordinate.calculateLayout with the same inputs.
628
+
569
629
  const geometry = this.getGeometry();
570
630
  if (geometry) {
571
631
  this.context.eventBus.emit("dieline:geometry:change", geometry);
@@ -575,141 +635,138 @@ export class DielineTool implements Extension {
575
635
 
576
636
  public getGeometry(): DielineGeometry | null {
577
637
  if (!this.canvasService) return null;
578
- const { shape, width, height, radius, position, borderLength, offset } =
579
- this;
638
+ const { unit, shape, width, height, radius, position, offset } = this;
580
639
  const canvasW = this.canvasService.canvas.width || 800;
581
640
  const canvasH = this.canvasService.canvas.height || 600;
582
641
 
583
- let visualWidth = width;
584
- let visualHeight = height;
642
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
643
+ const layout = Coordinate.calculateLayout(
644
+ { width: canvasW, height: canvasH },
645
+ { width, height },
646
+ paddingPx,
647
+ );
585
648
 
586
- if (borderLength && borderLength > 0) {
587
- visualWidth = Math.max(0, canvasW - borderLength * 2);
588
- visualHeight = Math.max(0, canvasH - borderLength * 2);
589
- }
649
+ const scale = layout.scale;
650
+ const cx = layout.offsetX + layout.width / 2;
651
+ const cy = layout.offsetY + layout.height / 2;
590
652
 
591
- const cx = Coordinate.toAbsolute(position?.x ?? 0.5, canvasW);
592
- const cy = Coordinate.toAbsolute(position?.y ?? 0.5, canvasH);
653
+ const visualWidth = layout.width;
654
+ const visualHeight = layout.height;
593
655
 
594
656
  return {
595
657
  shape,
658
+ unit,
596
659
  x: cx,
597
660
  y: cy,
598
661
  width: visualWidth,
599
662
  height: visualHeight,
600
- radius,
601
- offset,
602
- borderLength,
663
+ radius: radius * scale,
664
+ offset: offset * scale,
665
+ // Pass scale to help other tools (like HoleTool) convert units
666
+ scale,
603
667
  pathData: this.pathData,
604
- };
668
+ } as DielineGeometry;
605
669
  }
606
670
 
607
- public exportCutImage() {
671
+ public async exportCutImage() {
608
672
  if (!this.canvasService) return null;
609
- const canvas = this.canvasService.canvas;
673
+ const userLayer = this.canvasService.getLayer("user");
674
+
675
+ // Even if no user images, we might want to export the shape?
676
+ // But usually "Cut Image" implies the printed content.
677
+ // If empty, maybe just return null or empty string.
678
+ if (!userLayer) return null;
610
679
 
611
680
  // 1. Generate Path Data
612
681
  const { shape, width, height, radius, position, holes } = this;
613
- const canvasW = canvas.width || 800;
614
- const canvasH = canvas.height || 600;
615
- const cx = Coordinate.toAbsolute(position?.x ?? 0.5, canvasW);
616
- const cy = Coordinate.toAbsolute(position?.y ?? 0.5, canvasH);
682
+ const canvasW = this.canvasService.canvas.width || 800;
683
+ const canvasH = this.canvasService.canvas.height || 600;
684
+
685
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
686
+ const layout = Coordinate.calculateLayout(
687
+ { width: canvasW, height: canvasH },
688
+ { width, height },
689
+ paddingPx,
690
+ );
691
+ const scale = layout.scale;
692
+ const cx = layout.offsetX + layout.width / 2;
693
+ const cy = layout.offsetY + layout.height / 2;
694
+ const visualWidth = layout.width;
695
+ const visualHeight = layout.height;
696
+ const visualRadius = radius * scale;
617
697
 
618
698
  // Denormalize Holes for Export
619
699
  const absoluteHoles = (holes || []).map((h) => {
700
+ const unit = this.unit || "mm";
701
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
702
+
620
703
  const pos = resolveHolePosition(
621
- h,
622
- { x: cx, y: cy, width, height },
623
- { width: canvasW, height: canvasH }
704
+ {
705
+ ...h,
706
+ offsetX: (h.offsetX || 0) * unitScale * scale,
707
+ offsetY: (h.offsetY || 0) * unitScale * scale,
708
+ },
709
+ { x: cx, y: cy, width: visualWidth, height: visualHeight },
710
+ { width: canvasW, height: canvasH },
624
711
  );
712
+
625
713
  return {
626
714
  ...h,
627
715
  x: pos.x,
628
716
  y: pos.y,
717
+ innerRadius: h.innerRadius * unitScale * scale,
718
+ outerRadius: h.outerRadius * unitScale * scale,
719
+ offsetX: (h.offsetX || 0) * unitScale * scale,
720
+ offsetY: (h.offsetY || 0) * unitScale * scale,
629
721
  };
630
722
  });
631
723
 
632
724
  const pathData = generateDielinePath({
633
725
  shape,
634
- width,
635
- height,
636
- radius,
726
+ width: visualWidth,
727
+ height: visualHeight,
728
+ radius: visualRadius,
637
729
  x: cx,
638
730
  y: cy,
639
731
  holes: absoluteHoles,
640
732
  pathData: this.pathData,
641
733
  });
642
734
 
643
- // 2. Create Clip Path
644
- // @ts-ignore
735
+ // 2. Prepare for Export
736
+ // Clone the layer to not affect the stage
737
+ const clonedLayer = await userLayer.clone();
738
+
739
+ // Create Clip Path
740
+ // Note: In Fabric, clipPath is relative to the object center usually,
741
+ // but for a Group that is full-canvas (left=0, top=0), absolute coordinates should work
742
+ // if we configure it correctly.
743
+ // However, Fabric's clipPath handling can be tricky with Groups.
744
+ // Safest bet: Position the clipPath absolutely and ensuring group is absolute.
745
+
645
746
  const clipPath = new Path(pathData, {
646
- left: 0,
647
- top: 0,
648
747
  originX: "left",
649
748
  originY: "top",
650
- absolutePositioned: true,
651
- });
652
-
653
- // 3. Hide UI Layers
654
- const layer = this.getLayer();
655
- const wasVisible = layer?.visible ?? true;
656
- if (layer) layer.visible = false;
657
-
658
- // Hide hole markers
659
- const holeMarkers = canvas
660
- .getObjects()
661
- .filter((o: any) => o.data?.type === "hole-marker");
662
- holeMarkers.forEach((o) => (o.visible = false));
663
-
664
- // Hide Ruler Overlay
665
- const rulerLayer = canvas
666
- .getObjects()
667
- .find((obj: any) => obj.data?.id === "ruler-overlay");
668
- const rulerWasVisible = rulerLayer?.visible ?? true;
669
- if (rulerLayer) rulerLayer.visible = false;
670
-
671
- // 4. Apply Clip & Export
672
- const originalClip = canvas.clipPath;
673
- canvas.clipPath = clipPath;
674
-
675
- const bbox = clipPath.getBoundingRect();
676
-
677
- const clipPathCorrected = new Path(pathData, {
678
- absolutePositioned: true,
679
749
  left: 0,
680
750
  top: 0,
751
+ absolutePositioned: true, // Important for groups
681
752
  });
682
753
 
683
- const tempPath = new Path(pathData);
684
- const tempBounds = tempPath.getBoundingRect();
754
+ clonedLayer.clipPath = clipPath;
685
755
 
686
- clipPathCorrected.set({
687
- left: tempBounds.left,
688
- top: tempBounds.top,
689
- originX: "left",
690
- originY: "top",
691
- });
692
-
693
- // 4. Apply Clip & Export
694
- canvas.clipPath = clipPathCorrected;
756
+ // 3. Calculate Crop Area (The Dieline Bounds)
757
+ // We want to export only the area covered by the dieline
758
+ const bounds = clipPath.getBoundingRect();
695
759
 
696
- const exportBbox = clipPathCorrected.getBoundingRect();
697
- const dataURL = canvas.toDataURL({
760
+ // 4. Export
761
+ const dataUrl = clonedLayer.toDataURL({
698
762
  format: "png",
699
- multiplier: 2,
700
- left: exportBbox.left,
701
- top: exportBbox.top,
702
- width: exportBbox.width,
703
- height: exportBbox.height,
763
+ multiplier: 2, // Better quality
764
+ left: bounds.left,
765
+ top: bounds.top,
766
+ width: bounds.width,
767
+ height: bounds.height,
704
768
  });
705
769
 
706
- // 5. Restore
707
- canvas.clipPath = originalClip;
708
- if (layer) layer.visible = wasVisible;
709
- if (rulerLayer) rulerLayer.visible = rulerWasVisible;
710
- holeMarkers.forEach((o) => (o.visible = true));
711
- canvas.requestRenderAll();
712
-
713
- return dataURL;
770
+ return dataUrl;
714
771
  }
715
772
  }
package/src/geometry.ts CHANGED
@@ -24,7 +24,7 @@ export interface HoleData {
24
24
  export function resolveHolePosition(
25
25
  hole: HoleData,
26
26
  geometry: { x: number; y: number; width: number; height: number },
27
- canvasSize: { width: number; height: number }
27
+ canvasSize: { width: number; height: number },
28
28
  ): { x: number; y: number } {
29
29
  if (hole.anchor) {
30
30
  const { x, y, width, height } = geometry;
@@ -82,14 +82,13 @@ export function resolveHolePosition(
82
82
  y: by + (hole.offsetY || 0),
83
83
  };
84
84
  } else if (hole.x !== undefined && hole.y !== undefined) {
85
- // Legacy / Direct coordinates (Normalized)
86
- // We assume x/y are normalized to canvas size if no anchor is present
87
- // Or should we support absolute?
88
- // Current system uses normalized.
89
- // Coordinate.denormalizePoint logic:
85
+ // Legacy / Direct coordinates (Normalized relative to Dieline Geometry)
86
+ // Formula: absolute = normalized * width + (center - width/2)
87
+ // This handles padding correctly.
88
+ const { x, width, y, height } = geometry;
90
89
  return {
91
- x: hole.x * canvasSize.width,
92
- y: hole.y * canvasSize.height,
90
+ x: hole.x * width + (x - width / 2) + (hole.offsetX || 0),
91
+ y: hole.y * height + (y - height / 2) + (hole.offsetY || 0),
93
92
  };
94
93
  }
95
94
  return { x: 0, y: 0 };
@@ -358,7 +357,10 @@ function getDielineShape(options: GeometryOptions): paper.PathItem {
358
357
  cutsPath.remove();
359
358
  mainShape = temp;
360
359
  } catch (e) {
361
- console.error("Geometry: Failed to subtract cutsPath from mainShape", e);
360
+ console.error(
361
+ "Geometry: Failed to subtract cutsPath from mainShape",
362
+ e,
363
+ );
362
364
  }
363
365
  }
364
366
  }
@@ -541,6 +543,8 @@ export function getNearestPointOnDieline(
541
543
  }
542
544
 
543
545
  export function getPathBounds(pathData: string): {
546
+ x: number;
547
+ y: number;
544
548
  width: number;
545
549
  height: number;
546
550
  } {
@@ -548,5 +552,10 @@ export function getPathBounds(pathData: string): {
548
552
  path.pathData = pathData;
549
553
  const bounds = path.bounds;
550
554
  path.remove();
551
- return { width: bounds.width, height: bounds.height };
555
+ return {
556
+ x: bounds.x,
557
+ y: bounds.y,
558
+ width: bounds.width,
559
+ height: bounds.height,
560
+ };
552
561
  }