@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/CHANGELOG.md +11 -0
- package/dist/index.d.mts +39 -24
- package/dist/index.d.ts +39 -24
- package/dist/index.js +740 -534
- package/dist/index.mjs +744 -538
- package/package.json +2 -2
- package/src/coordinate.ts +57 -0
- package/src/dieline.ts +186 -129
- package/src/geometry.ts +19 -10
- package/src/hole.ts +88 -31
- package/src/image.ts +334 -365
- package/src/ruler.ts +295 -120
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pooder/kit",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
188
|
+
default: this.radius,
|
|
180
189
|
},
|
|
181
190
|
{
|
|
182
|
-
id: "dieline.
|
|
183
|
-
type: "
|
|
184
|
-
label: "
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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,
|
|
398
|
-
const cutH = Math.max(0,
|
|
399
|
-
const cutR =
|
|
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
|
-
|
|
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:
|
|
521
|
-
height:
|
|
522
|
-
radius:
|
|
573
|
+
width: visualWidth,
|
|
574
|
+
height: visualHeight,
|
|
575
|
+
radius: visualRadius,
|
|
523
576
|
x: cx,
|
|
524
577
|
y: cy,
|
|
525
|
-
holes: absoluteHoles,
|
|
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,
|
|
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
|
-
|
|
584
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
592
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
616
|
-
const
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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.
|
|
644
|
-
//
|
|
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
|
-
|
|
684
|
-
const tempBounds = tempPath.getBoundingRect();
|
|
754
|
+
clonedLayer.clipPath = clipPath;
|
|
685
755
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
697
|
-
const
|
|
760
|
+
// 4. Export
|
|
761
|
+
const dataUrl = clonedLayer.toDataURL({
|
|
698
762
|
format: "png",
|
|
699
|
-
multiplier: 2,
|
|
700
|
-
left:
|
|
701
|
-
top:
|
|
702
|
-
width:
|
|
703
|
-
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
|
-
|
|
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
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
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 *
|
|
92
|
-
y: hole.y *
|
|
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(
|
|
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 {
|
|
555
|
+
return {
|
|
556
|
+
x: bounds.x,
|
|
557
|
+
y: bounds.y,
|
|
558
|
+
width: bounds.width,
|
|
559
|
+
height: bounds.height,
|
|
560
|
+
};
|
|
552
561
|
}
|