@pooder/kit 3.1.0 → 3.3.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 +17 -0
- package/dist/index.d.mts +40 -24
- package/dist/index.d.ts +40 -24
- package/dist/index.js +849 -557
- package/dist/index.mjs +854 -562
- package/package.json +2 -2
- package/src/coordinate.ts +57 -0
- package/src/dieline.ts +196 -129
- package/src/geometry.ts +56 -21
- package/src/hole.ts +163 -56
- package/src/image.ts +355 -363
- 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.3.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({
|
|
@@ -440,6 +493,8 @@ export class DielineTool implements Extension {
|
|
|
440
493
|
y: cy,
|
|
441
494
|
holes: absoluteHoles,
|
|
442
495
|
pathData: this.pathData,
|
|
496
|
+
canvasWidth: canvasW,
|
|
497
|
+
canvasHeight: canvasH,
|
|
443
498
|
});
|
|
444
499
|
|
|
445
500
|
const insideObj = new Path(productPathData, {
|
|
@@ -458,15 +513,17 @@ export class DielineTool implements Extension {
|
|
|
458
513
|
const bleedPathData = generateBleedZonePath(
|
|
459
514
|
{
|
|
460
515
|
shape,
|
|
461
|
-
width,
|
|
462
|
-
height,
|
|
463
|
-
radius,
|
|
516
|
+
width: visualWidth,
|
|
517
|
+
height: visualHeight,
|
|
518
|
+
radius: visualRadius,
|
|
464
519
|
x: cx,
|
|
465
520
|
y: cy,
|
|
466
521
|
holes: absoluteHoles,
|
|
467
522
|
pathData: this.pathData,
|
|
523
|
+
canvasWidth: canvasW,
|
|
524
|
+
canvasHeight: canvasH,
|
|
468
525
|
},
|
|
469
|
-
|
|
526
|
+
visualOffset,
|
|
470
527
|
);
|
|
471
528
|
|
|
472
529
|
// Use solid red for hatch lines to match dieline, background is transparent
|
|
@@ -496,6 +553,8 @@ export class DielineTool implements Extension {
|
|
|
496
553
|
y: cy,
|
|
497
554
|
holes: absoluteHoles,
|
|
498
555
|
pathData: this.pathData,
|
|
556
|
+
canvasWidth: canvasW,
|
|
557
|
+
canvasHeight: canvasH,
|
|
499
558
|
});
|
|
500
559
|
|
|
501
560
|
const offsetBorderObj = new Path(offsetPathData, {
|
|
@@ -517,13 +576,15 @@ export class DielineTool implements Extension {
|
|
|
517
576
|
// generateDielinePath expects holes to be in absolute coordinates (matching width/height scale)
|
|
518
577
|
const borderPathData = generateDielinePath({
|
|
519
578
|
shape,
|
|
520
|
-
width:
|
|
521
|
-
height:
|
|
522
|
-
radius:
|
|
579
|
+
width: visualWidth,
|
|
580
|
+
height: visualHeight,
|
|
581
|
+
radius: visualRadius,
|
|
523
582
|
x: cx,
|
|
524
583
|
y: cy,
|
|
525
|
-
holes: absoluteHoles,
|
|
584
|
+
holes: absoluteHoles,
|
|
526
585
|
pathData: this.pathData,
|
|
586
|
+
canvasWidth: canvasW,
|
|
587
|
+
canvasHeight: canvasH,
|
|
527
588
|
});
|
|
528
589
|
|
|
529
590
|
const borderObj = new Path(borderPathData, {
|
|
@@ -566,6 +627,13 @@ export class DielineTool implements Extension {
|
|
|
566
627
|
// Emit change event so other tools (like HoleTool) can react
|
|
567
628
|
// Only emit if requested (to avoid loops when updating non-geometry props like holes)
|
|
568
629
|
if (emitEvent && this.context) {
|
|
630
|
+
// FIX: Ensure we use the exact same geometry values as used in rendering above.
|
|
631
|
+
// Although getGeometry() recalculates layout, it should be identical if props haven't changed.
|
|
632
|
+
// But to be absolutely safe and avoid micro-differences or race conditions, we can reuse calculated values.
|
|
633
|
+
// However, getGeometry is public API, so it's better if it's correct.
|
|
634
|
+
// Let's verify getGeometry logic matches updateDieline logic perfectly.
|
|
635
|
+
// Yes, both use Coordinate.calculateLayout with the same inputs.
|
|
636
|
+
|
|
569
637
|
const geometry = this.getGeometry();
|
|
570
638
|
if (geometry) {
|
|
571
639
|
this.context.eventBus.emit("dieline:geometry:change", geometry);
|
|
@@ -575,141 +643,140 @@ export class DielineTool implements Extension {
|
|
|
575
643
|
|
|
576
644
|
public getGeometry(): DielineGeometry | null {
|
|
577
645
|
if (!this.canvasService) return null;
|
|
578
|
-
const { shape, width, height, radius, position,
|
|
579
|
-
this;
|
|
646
|
+
const { unit, shape, width, height, radius, position, offset } = this;
|
|
580
647
|
const canvasW = this.canvasService.canvas.width || 800;
|
|
581
648
|
const canvasH = this.canvasService.canvas.height || 600;
|
|
582
649
|
|
|
583
|
-
|
|
584
|
-
|
|
650
|
+
const paddingPx = this.resolvePadding(canvasW, canvasH);
|
|
651
|
+
const layout = Coordinate.calculateLayout(
|
|
652
|
+
{ width: canvasW, height: canvasH },
|
|
653
|
+
{ width, height },
|
|
654
|
+
paddingPx,
|
|
655
|
+
);
|
|
585
656
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
657
|
+
const scale = layout.scale;
|
|
658
|
+
const cx = layout.offsetX + layout.width / 2;
|
|
659
|
+
const cy = layout.offsetY + layout.height / 2;
|
|
590
660
|
|
|
591
|
-
const
|
|
592
|
-
const
|
|
661
|
+
const visualWidth = layout.width;
|
|
662
|
+
const visualHeight = layout.height;
|
|
593
663
|
|
|
594
664
|
return {
|
|
595
665
|
shape,
|
|
666
|
+
unit,
|
|
596
667
|
x: cx,
|
|
597
668
|
y: cy,
|
|
598
669
|
width: visualWidth,
|
|
599
670
|
height: visualHeight,
|
|
600
|
-
radius,
|
|
601
|
-
offset,
|
|
602
|
-
|
|
671
|
+
radius: radius * scale,
|
|
672
|
+
offset: offset * scale,
|
|
673
|
+
// Pass scale to help other tools (like HoleTool) convert units
|
|
674
|
+
scale,
|
|
603
675
|
pathData: this.pathData,
|
|
604
|
-
};
|
|
676
|
+
} as DielineGeometry;
|
|
605
677
|
}
|
|
606
678
|
|
|
607
|
-
public exportCutImage() {
|
|
679
|
+
public async exportCutImage() {
|
|
608
680
|
if (!this.canvasService) return null;
|
|
609
|
-
const
|
|
681
|
+
const userLayer = this.canvasService.getLayer("user");
|
|
682
|
+
|
|
683
|
+
// Even if no user images, we might want to export the shape?
|
|
684
|
+
// But usually "Cut Image" implies the printed content.
|
|
685
|
+
// If empty, maybe just return null or empty string.
|
|
686
|
+
if (!userLayer) return null;
|
|
610
687
|
|
|
611
688
|
// 1. Generate Path Data
|
|
612
689
|
const { shape, width, height, radius, position, holes } = this;
|
|
613
|
-
const canvasW = canvas.width || 800;
|
|
614
|
-
const canvasH = canvas.height || 600;
|
|
615
|
-
|
|
616
|
-
const
|
|
690
|
+
const canvasW = this.canvasService.canvas.width || 800;
|
|
691
|
+
const canvasH = this.canvasService.canvas.height || 600;
|
|
692
|
+
|
|
693
|
+
const paddingPx = this.resolvePadding(canvasW, canvasH);
|
|
694
|
+
const layout = Coordinate.calculateLayout(
|
|
695
|
+
{ width: canvasW, height: canvasH },
|
|
696
|
+
{ width, height },
|
|
697
|
+
paddingPx,
|
|
698
|
+
);
|
|
699
|
+
const scale = layout.scale;
|
|
700
|
+
const cx = layout.offsetX + layout.width / 2;
|
|
701
|
+
const cy = layout.offsetY + layout.height / 2;
|
|
702
|
+
const visualWidth = layout.width;
|
|
703
|
+
const visualHeight = layout.height;
|
|
704
|
+
const visualRadius = radius * scale;
|
|
617
705
|
|
|
618
706
|
// Denormalize Holes for Export
|
|
619
707
|
const absoluteHoles = (holes || []).map((h) => {
|
|
708
|
+
const unit = this.unit || "mm";
|
|
709
|
+
const unitScale = Coordinate.convertUnit(1, "mm", unit);
|
|
710
|
+
|
|
620
711
|
const pos = resolveHolePosition(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
712
|
+
{
|
|
713
|
+
...h,
|
|
714
|
+
offsetX: (h.offsetX || 0) * unitScale * scale,
|
|
715
|
+
offsetY: (h.offsetY || 0) * unitScale * scale,
|
|
716
|
+
},
|
|
717
|
+
{ x: cx, y: cy, width: visualWidth, height: visualHeight },
|
|
718
|
+
{ width: canvasW, height: canvasH },
|
|
624
719
|
);
|
|
720
|
+
|
|
625
721
|
return {
|
|
626
722
|
...h,
|
|
627
723
|
x: pos.x,
|
|
628
724
|
y: pos.y,
|
|
725
|
+
innerRadius: h.innerRadius * unitScale * scale,
|
|
726
|
+
outerRadius: h.outerRadius * unitScale * scale,
|
|
727
|
+
offsetX: (h.offsetX || 0) * unitScale * scale,
|
|
728
|
+
offsetY: (h.offsetY || 0) * unitScale * scale,
|
|
629
729
|
};
|
|
630
730
|
});
|
|
631
731
|
|
|
632
732
|
const pathData = generateDielinePath({
|
|
633
733
|
shape,
|
|
634
|
-
width,
|
|
635
|
-
height,
|
|
636
|
-
radius,
|
|
734
|
+
width: visualWidth,
|
|
735
|
+
height: visualHeight,
|
|
736
|
+
radius: visualRadius,
|
|
637
737
|
x: cx,
|
|
638
738
|
y: cy,
|
|
639
739
|
holes: absoluteHoles,
|
|
640
740
|
pathData: this.pathData,
|
|
741
|
+
canvasWidth: canvasW,
|
|
742
|
+
canvasHeight: canvasH,
|
|
641
743
|
});
|
|
642
744
|
|
|
643
|
-
// 2.
|
|
644
|
-
//
|
|
745
|
+
// 2. Prepare for Export
|
|
746
|
+
// Clone the layer to not affect the stage
|
|
747
|
+
const clonedLayer = await userLayer.clone();
|
|
748
|
+
|
|
749
|
+
// Create Clip Path
|
|
750
|
+
// Note: In Fabric, clipPath is relative to the object center usually,
|
|
751
|
+
// but for a Group that is full-canvas (left=0, top=0), absolute coordinates should work
|
|
752
|
+
// if we configure it correctly.
|
|
753
|
+
// However, Fabric's clipPath handling can be tricky with Groups.
|
|
754
|
+
// Safest bet: Position the clipPath absolutely and ensuring group is absolute.
|
|
755
|
+
|
|
645
756
|
const clipPath = new Path(pathData, {
|
|
646
|
-
left: 0,
|
|
647
|
-
top: 0,
|
|
648
757
|
originX: "left",
|
|
649
758
|
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
759
|
left: 0,
|
|
680
760
|
top: 0,
|
|
761
|
+
absolutePositioned: true, // Important for groups
|
|
681
762
|
});
|
|
682
763
|
|
|
683
|
-
|
|
684
|
-
const tempBounds = tempPath.getBoundingRect();
|
|
685
|
-
|
|
686
|
-
clipPathCorrected.set({
|
|
687
|
-
left: tempBounds.left,
|
|
688
|
-
top: tempBounds.top,
|
|
689
|
-
originX: "left",
|
|
690
|
-
originY: "top",
|
|
691
|
-
});
|
|
764
|
+
clonedLayer.clipPath = clipPath;
|
|
692
765
|
|
|
693
|
-
//
|
|
694
|
-
|
|
766
|
+
// 3. Calculate Crop Area (The Dieline Bounds)
|
|
767
|
+
// We want to export only the area covered by the dieline
|
|
768
|
+
const bounds = clipPath.getBoundingRect();
|
|
695
769
|
|
|
696
|
-
|
|
697
|
-
const
|
|
770
|
+
// 4. Export
|
|
771
|
+
const dataUrl = clonedLayer.toDataURL({
|
|
698
772
|
format: "png",
|
|
699
|
-
multiplier: 2,
|
|
700
|
-
left:
|
|
701
|
-
top:
|
|
702
|
-
width:
|
|
703
|
-
height:
|
|
773
|
+
multiplier: 2, // Better quality
|
|
774
|
+
left: bounds.left,
|
|
775
|
+
top: bounds.top,
|
|
776
|
+
width: bounds.width,
|
|
777
|
+
height: bounds.height,
|
|
704
778
|
});
|
|
705
779
|
|
|
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;
|
|
780
|
+
return dataUrl;
|
|
714
781
|
}
|
|
715
782
|
}
|