@pooder/kit 3.0.1 → 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 +17 -0
- package/dist/index.d.mts +46 -36
- package/dist/index.d.ts +46 -36
- package/dist/index.js +958 -696
- package/dist/index.mjs +962 -700
- package/package.json +2 -2
- package/src/coordinate.ts +57 -0
- package/src/dieline.ts +206 -214
- package/src/geometry.ts +101 -4
- package/src/hole.ts +277 -170
- package/src/image.ts +334 -365
- package/src/ruler.ts +295 -120
package/src/dieline.ts
CHANGED
|
@@ -8,17 +8,19 @@ 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,
|
|
15
15
|
generateBleedZonePath,
|
|
16
16
|
getPathBounds,
|
|
17
17
|
HoleData,
|
|
18
|
+
resolveHolePosition,
|
|
18
19
|
} from "./geometry";
|
|
19
20
|
|
|
20
21
|
export interface DielineGeometry {
|
|
21
22
|
shape: "rect" | "circle" | "ellipse" | "custom";
|
|
23
|
+
unit: Unit;
|
|
22
24
|
x: number;
|
|
23
25
|
y: number;
|
|
24
26
|
width: number;
|
|
@@ -26,6 +28,7 @@ export interface DielineGeometry {
|
|
|
26
28
|
radius: number;
|
|
27
29
|
offset: number;
|
|
28
30
|
borderLength?: number;
|
|
31
|
+
scale?: number;
|
|
29
32
|
pathData?: string;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -35,6 +38,7 @@ export class DielineTool implements Extension {
|
|
|
35
38
|
name: "DielineTool",
|
|
36
39
|
};
|
|
37
40
|
|
|
41
|
+
private unit: Unit = "mm";
|
|
38
42
|
private shape: "rect" | "circle" | "ellipse" | "custom" = "rect";
|
|
39
43
|
private width: number = 500;
|
|
40
44
|
private height: number = 500;
|
|
@@ -47,7 +51,7 @@ export class DielineTool implements Extension {
|
|
|
47
51
|
private holes: HoleData[] = [];
|
|
48
52
|
// Position is stored as normalized coordinates (0-1)
|
|
49
53
|
private position?: { x: number; y: number };
|
|
50
|
-
private
|
|
54
|
+
private padding: number | string = 140;
|
|
51
55
|
private pathData?: string;
|
|
52
56
|
|
|
53
57
|
private canvasService?: CanvasService;
|
|
@@ -55,13 +59,14 @@ export class DielineTool implements Extension {
|
|
|
55
59
|
|
|
56
60
|
constructor(
|
|
57
61
|
options?: Partial<{
|
|
62
|
+
unit: Unit;
|
|
58
63
|
shape: "rect" | "circle" | "ellipse" | "custom";
|
|
59
64
|
width: number;
|
|
60
65
|
height: number;
|
|
61
66
|
radius: number;
|
|
62
67
|
// Position is normalized (0-1)
|
|
63
68
|
position: { x: number; y: number };
|
|
64
|
-
|
|
69
|
+
padding: number | string;
|
|
65
70
|
offset: number;
|
|
66
71
|
style: "solid" | "dashed";
|
|
67
72
|
insideColor: string;
|
|
@@ -87,14 +92,12 @@ export class DielineTool implements Extension {
|
|
|
87
92
|
const configService = context.services.get<any>("ConfigurationService");
|
|
88
93
|
if (configService) {
|
|
89
94
|
// Load initial config
|
|
95
|
+
this.unit = configService.get("dieline.unit", this.unit);
|
|
90
96
|
this.shape = configService.get("dieline.shape", this.shape);
|
|
91
97
|
this.width = configService.get("dieline.width", this.width);
|
|
92
98
|
this.height = configService.get("dieline.height", this.height);
|
|
93
99
|
this.radius = configService.get("dieline.radius", this.radius);
|
|
94
|
-
this.
|
|
95
|
-
"dieline.borderLength",
|
|
96
|
-
this.borderLength,
|
|
97
|
-
);
|
|
100
|
+
this.padding = configService.get("dieline.padding", this.padding);
|
|
98
101
|
this.offset = configService.get("dieline.offset", this.offset);
|
|
99
102
|
this.style = configService.get("dieline.style", this.style);
|
|
100
103
|
this.insideColor = configService.get(
|
|
@@ -140,6 +143,13 @@ export class DielineTool implements Extension {
|
|
|
140
143
|
contribute() {
|
|
141
144
|
return {
|
|
142
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
|
+
},
|
|
143
153
|
{
|
|
144
154
|
id: "dieline.shape",
|
|
145
155
|
type: "select",
|
|
@@ -175,15 +185,14 @@ export class DielineTool implements Extension {
|
|
|
175
185
|
id: "dieline.position",
|
|
176
186
|
type: "json",
|
|
177
187
|
label: "Position (Normalized)",
|
|
178
|
-
default: this.
|
|
188
|
+
default: this.radius,
|
|
179
189
|
},
|
|
180
190
|
{
|
|
181
|
-
id: "dieline.
|
|
182
|
-
type: "
|
|
183
|
-
label: "
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
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,
|
|
187
196
|
},
|
|
188
197
|
{
|
|
189
198
|
id: "dieline.offset",
|
|
@@ -226,65 +235,6 @@ export class DielineTool implements Extension {
|
|
|
226
235
|
},
|
|
227
236
|
] as ConfigurationContribution[],
|
|
228
237
|
[ContributionPointIds.COMMANDS]: [
|
|
229
|
-
{
|
|
230
|
-
command: "reset",
|
|
231
|
-
title: "Reset Dieline",
|
|
232
|
-
handler: () => {
|
|
233
|
-
this.shape = "rect";
|
|
234
|
-
this.width = 300;
|
|
235
|
-
this.height = 300;
|
|
236
|
-
this.radius = 0;
|
|
237
|
-
this.offset = 0;
|
|
238
|
-
this.style = "solid";
|
|
239
|
-
this.insideColor = "rgba(0,0,0,0)";
|
|
240
|
-
this.outsideColor = "#ffffff";
|
|
241
|
-
this.showBleedLines = true;
|
|
242
|
-
this.holes = [];
|
|
243
|
-
this.pathData = undefined;
|
|
244
|
-
this.updateDieline();
|
|
245
|
-
return true;
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
command: "setDimensions",
|
|
250
|
-
title: "Set Dimensions",
|
|
251
|
-
handler: (width: number, height: number) => {
|
|
252
|
-
if (this.width === width && this.height === height) return true;
|
|
253
|
-
this.width = width;
|
|
254
|
-
this.height = height;
|
|
255
|
-
this.updateDieline();
|
|
256
|
-
return true;
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
command: "setShape",
|
|
261
|
-
title: "Set Shape",
|
|
262
|
-
handler: (shape: "rect" | "circle" | "ellipse" | "custom") => {
|
|
263
|
-
if (this.shape === shape) return true;
|
|
264
|
-
this.shape = shape;
|
|
265
|
-
this.updateDieline();
|
|
266
|
-
return true;
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
command: "setBleed",
|
|
271
|
-
title: "Set Bleed",
|
|
272
|
-
handler: (bleed: number) => {
|
|
273
|
-
if (this.offset === bleed) return true;
|
|
274
|
-
this.offset = bleed;
|
|
275
|
-
this.updateDieline();
|
|
276
|
-
return true;
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
command: "setHoles",
|
|
281
|
-
title: "Set Holes",
|
|
282
|
-
handler: (holes: HoleData[]) => {
|
|
283
|
-
this.holes = holes;
|
|
284
|
-
this.updateDieline(false);
|
|
285
|
-
return true;
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
238
|
{
|
|
289
239
|
command: "getGeometry",
|
|
290
240
|
title: "Get Geometry",
|
|
@@ -304,39 +254,25 @@ export class DielineTool implements Extension {
|
|
|
304
254
|
title: "Detect Edge from Image",
|
|
305
255
|
handler: async (imageUrl: string, options?: any) => {
|
|
306
256
|
try {
|
|
307
|
-
// Pass current dimensions if we want to scale immediately?
|
|
308
|
-
// But wait, the user said "It should be scaled according to width and height".
|
|
309
|
-
// If the user already set width/height on the tool, we should respect it?
|
|
310
|
-
// Or should we set width/height based on the image aspect ratio?
|
|
311
|
-
// Usually for a new trace, we might want to respect the IMAGE aspect ratio but fit into current width/height?
|
|
312
|
-
// Or just replace width/height with image dimensions?
|
|
313
|
-
// Let's assume we want to keep the current "box" size but fit the shape inside?
|
|
314
|
-
// Or if options has width/height use that.
|
|
315
|
-
|
|
316
|
-
// Let's first trace to get the natural shape (and its aspect ratio)
|
|
317
|
-
// Then we can decide how to update this.width/this.height.
|
|
318
|
-
|
|
319
257
|
const pathData = await ImageTracer.trace(imageUrl, options);
|
|
320
|
-
|
|
321
|
-
// We need to set width/height from the path bounds to avoid distortion
|
|
322
258
|
const bounds = getPathBounds(pathData);
|
|
323
259
|
|
|
324
|
-
// If we want to scale the path to specific dimensions, we can do it via ImageTracer options.scaleToWidth/Height
|
|
325
|
-
// But here we got the raw path.
|
|
326
|
-
// Let's update the TOOL's dimensions to match the detected shape's aspect ratio,
|
|
327
|
-
// while keeping the size reasonable (e.g. max dimension 300 or current size).
|
|
328
|
-
|
|
329
|
-
// If current tool size is default 300x300, we might want to resize tool to match image ratio.
|
|
330
260
|
const currentMax = Math.max(this.width, this.height);
|
|
331
261
|
const scale = currentMax / Math.max(bounds.width, bounds.height);
|
|
332
262
|
|
|
333
|
-
|
|
334
|
-
|
|
263
|
+
const newWidth = bounds.width * scale;
|
|
264
|
+
const newHeight = bounds.height * scale;
|
|
335
265
|
|
|
336
|
-
|
|
337
|
-
|
|
266
|
+
const configService = this.context?.services.get<any>(
|
|
267
|
+
"ConfigurationService",
|
|
268
|
+
);
|
|
269
|
+
if (configService) {
|
|
270
|
+
configService.update("dieline.width", newWidth);
|
|
271
|
+
configService.update("dieline.height", newHeight);
|
|
272
|
+
configService.update("dieline.shape", "custom");
|
|
273
|
+
configService.update("dieline.pathData", pathData);
|
|
274
|
+
}
|
|
338
275
|
|
|
339
|
-
this.updateDieline();
|
|
340
276
|
return pathData;
|
|
341
277
|
} catch (e) {
|
|
342
278
|
console.error("Edge detection failed", e);
|
|
@@ -409,12 +345,30 @@ export class DielineTool implements Extension {
|
|
|
409
345
|
return new Pattern({ source: canvas, repetition: "repeat" });
|
|
410
346
|
}
|
|
411
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
|
+
|
|
412
365
|
public updateDieline(emitEvent: boolean = true) {
|
|
413
366
|
if (!this.canvasService) return;
|
|
414
367
|
const layer = this.getLayer();
|
|
415
368
|
if (!layer) return;
|
|
416
369
|
|
|
417
370
|
const {
|
|
371
|
+
unit,
|
|
418
372
|
shape,
|
|
419
373
|
radius,
|
|
420
374
|
offset,
|
|
@@ -422,7 +376,6 @@ export class DielineTool implements Extension {
|
|
|
422
376
|
insideColor,
|
|
423
377
|
outsideColor,
|
|
424
378
|
position,
|
|
425
|
-
borderLength,
|
|
426
379
|
showBleedLines,
|
|
427
380
|
holes,
|
|
428
381
|
} = this;
|
|
@@ -431,38 +384,72 @@ export class DielineTool implements Extension {
|
|
|
431
384
|
const canvasW = this.canvasService.canvas.width || 800;
|
|
432
385
|
const canvasH = this.canvasService.canvas.height || 600;
|
|
433
386
|
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
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;
|
|
445
405
|
|
|
446
406
|
// Clear existing objects
|
|
447
407
|
layer.remove(...layer.getObjects());
|
|
448
408
|
|
|
449
|
-
//
|
|
409
|
+
// Resolve Holes for Geometry Generation (using visual coordinates)
|
|
410
|
+
const geometryForHoles = {
|
|
411
|
+
x: cx,
|
|
412
|
+
y: cy,
|
|
413
|
+
width: visualWidth,
|
|
414
|
+
height: visualHeight,
|
|
415
|
+
// Pass scale/unit context if needed by resolveHolePosition (though currently unused there)
|
|
416
|
+
};
|
|
417
|
+
|
|
450
418
|
const absoluteHoles = (holes || []).map((h) => {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
+
|
|
455
435
|
return {
|
|
456
436
|
...h,
|
|
457
|
-
x:
|
|
458
|
-
y:
|
|
437
|
+
x: pos.x,
|
|
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,
|
|
459
445
|
};
|
|
460
446
|
});
|
|
461
447
|
|
|
462
448
|
// 1. Draw Mask (Outside)
|
|
463
|
-
const cutW = Math.max(0,
|
|
464
|
-
const cutH = Math.max(0,
|
|
465
|
-
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);
|
|
466
453
|
|
|
467
454
|
// Use Paper.js to generate the complex mask path
|
|
468
455
|
const maskPathData = generateMaskPath({
|
|
@@ -524,15 +511,15 @@ export class DielineTool implements Extension {
|
|
|
524
511
|
const bleedPathData = generateBleedZonePath(
|
|
525
512
|
{
|
|
526
513
|
shape,
|
|
527
|
-
width,
|
|
528
|
-
height,
|
|
529
|
-
radius,
|
|
514
|
+
width: visualWidth,
|
|
515
|
+
height: visualHeight,
|
|
516
|
+
radius: visualRadius,
|
|
530
517
|
x: cx,
|
|
531
518
|
y: cy,
|
|
532
519
|
holes: absoluteHoles,
|
|
533
520
|
pathData: this.pathData,
|
|
534
521
|
},
|
|
535
|
-
|
|
522
|
+
visualOffset,
|
|
536
523
|
);
|
|
537
524
|
|
|
538
525
|
// Use solid red for hatch lines to match dieline, background is transparent
|
|
@@ -583,12 +570,12 @@ export class DielineTool implements Extension {
|
|
|
583
570
|
// generateDielinePath expects holes to be in absolute coordinates (matching width/height scale)
|
|
584
571
|
const borderPathData = generateDielinePath({
|
|
585
572
|
shape,
|
|
586
|
-
width:
|
|
587
|
-
height:
|
|
588
|
-
radius:
|
|
573
|
+
width: visualWidth,
|
|
574
|
+
height: visualHeight,
|
|
575
|
+
radius: visualRadius,
|
|
589
576
|
x: cx,
|
|
590
577
|
y: cy,
|
|
591
|
-
holes: absoluteHoles,
|
|
578
|
+
holes: absoluteHoles,
|
|
592
579
|
pathData: this.pathData,
|
|
593
580
|
});
|
|
594
581
|
|
|
@@ -632,6 +619,13 @@ export class DielineTool implements Extension {
|
|
|
632
619
|
// Emit change event so other tools (like HoleTool) can react
|
|
633
620
|
// Only emit if requested (to avoid loops when updating non-geometry props like holes)
|
|
634
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
|
+
|
|
635
629
|
const geometry = this.getGeometry();
|
|
636
630
|
if (geometry) {
|
|
637
631
|
this.context.eventBus.emit("dieline:geometry:change", geometry);
|
|
@@ -641,140 +635,138 @@ export class DielineTool implements Extension {
|
|
|
641
635
|
|
|
642
636
|
public getGeometry(): DielineGeometry | null {
|
|
643
637
|
if (!this.canvasService) return null;
|
|
644
|
-
const { shape, width, height, radius, position,
|
|
645
|
-
this;
|
|
638
|
+
const { unit, shape, width, height, radius, position, offset } = this;
|
|
646
639
|
const canvasW = this.canvasService.canvas.width || 800;
|
|
647
640
|
const canvasH = this.canvasService.canvas.height || 600;
|
|
648
641
|
|
|
649
|
-
|
|
650
|
-
|
|
642
|
+
const paddingPx = this.resolvePadding(canvasW, canvasH);
|
|
643
|
+
const layout = Coordinate.calculateLayout(
|
|
644
|
+
{ width: canvasW, height: canvasH },
|
|
645
|
+
{ width, height },
|
|
646
|
+
paddingPx,
|
|
647
|
+
);
|
|
651
648
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
649
|
+
const scale = layout.scale;
|
|
650
|
+
const cx = layout.offsetX + layout.width / 2;
|
|
651
|
+
const cy = layout.offsetY + layout.height / 2;
|
|
656
652
|
|
|
657
|
-
const
|
|
658
|
-
const
|
|
653
|
+
const visualWidth = layout.width;
|
|
654
|
+
const visualHeight = layout.height;
|
|
659
655
|
|
|
660
656
|
return {
|
|
661
657
|
shape,
|
|
658
|
+
unit,
|
|
662
659
|
x: cx,
|
|
663
660
|
y: cy,
|
|
664
661
|
width: visualWidth,
|
|
665
662
|
height: visualHeight,
|
|
666
|
-
radius,
|
|
667
|
-
offset,
|
|
668
|
-
|
|
663
|
+
radius: radius * scale,
|
|
664
|
+
offset: offset * scale,
|
|
665
|
+
// Pass scale to help other tools (like HoleTool) convert units
|
|
666
|
+
scale,
|
|
669
667
|
pathData: this.pathData,
|
|
670
|
-
};
|
|
668
|
+
} as DielineGeometry;
|
|
671
669
|
}
|
|
672
670
|
|
|
673
|
-
public exportCutImage() {
|
|
671
|
+
public async exportCutImage() {
|
|
674
672
|
if (!this.canvasService) return null;
|
|
675
|
-
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;
|
|
676
679
|
|
|
677
680
|
// 1. Generate Path Data
|
|
678
681
|
const { shape, width, height, radius, position, holes } = this;
|
|
679
|
-
const canvasW = canvas.width || 800;
|
|
680
|
-
const canvasH = canvas.height || 600;
|
|
681
|
-
|
|
682
|
-
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;
|
|
683
697
|
|
|
684
698
|
// Denormalize Holes for Export
|
|
685
699
|
const absoluteHoles = (holes || []).map((h) => {
|
|
686
|
-
const
|
|
687
|
-
|
|
700
|
+
const unit = this.unit || "mm";
|
|
701
|
+
const unitScale = Coordinate.convertUnit(1, "mm", unit);
|
|
702
|
+
|
|
703
|
+
const pos = resolveHolePosition(
|
|
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 },
|
|
688
710
|
{ width: canvasW, height: canvasH },
|
|
689
711
|
);
|
|
712
|
+
|
|
690
713
|
return {
|
|
691
714
|
...h,
|
|
692
|
-
x:
|
|
693
|
-
y:
|
|
715
|
+
x: pos.x,
|
|
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,
|
|
694
721
|
};
|
|
695
722
|
});
|
|
696
723
|
|
|
697
724
|
const pathData = generateDielinePath({
|
|
698
725
|
shape,
|
|
699
|
-
width,
|
|
700
|
-
height,
|
|
701
|
-
radius,
|
|
726
|
+
width: visualWidth,
|
|
727
|
+
height: visualHeight,
|
|
728
|
+
radius: visualRadius,
|
|
702
729
|
x: cx,
|
|
703
730
|
y: cy,
|
|
704
731
|
holes: absoluteHoles,
|
|
705
732
|
pathData: this.pathData,
|
|
706
733
|
});
|
|
707
734
|
|
|
708
|
-
// 2.
|
|
709
|
-
//
|
|
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
|
+
|
|
710
746
|
const clipPath = new Path(pathData, {
|
|
711
|
-
left: 0,
|
|
712
|
-
top: 0,
|
|
713
747
|
originX: "left",
|
|
714
748
|
originY: "top",
|
|
715
|
-
absolutePositioned: true,
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
// 3. Hide UI Layers
|
|
719
|
-
const layer = this.getLayer();
|
|
720
|
-
const wasVisible = layer?.visible ?? true;
|
|
721
|
-
if (layer) layer.visible = false;
|
|
722
|
-
|
|
723
|
-
// Hide hole markers
|
|
724
|
-
const holeMarkers = canvas
|
|
725
|
-
.getObjects()
|
|
726
|
-
.filter((o: any) => o.data?.type === "hole-marker");
|
|
727
|
-
holeMarkers.forEach((o) => (o.visible = false));
|
|
728
|
-
|
|
729
|
-
// Hide Ruler Overlay
|
|
730
|
-
const rulerLayer = canvas
|
|
731
|
-
.getObjects()
|
|
732
|
-
.find((obj: any) => obj.data?.id === "ruler-overlay");
|
|
733
|
-
const rulerWasVisible = rulerLayer?.visible ?? true;
|
|
734
|
-
if (rulerLayer) rulerLayer.visible = false;
|
|
735
|
-
|
|
736
|
-
// 4. Apply Clip & Export
|
|
737
|
-
const originalClip = canvas.clipPath;
|
|
738
|
-
canvas.clipPath = clipPath;
|
|
739
|
-
|
|
740
|
-
const bbox = clipPath.getBoundingRect();
|
|
741
|
-
|
|
742
|
-
const clipPathCorrected = new Path(pathData, {
|
|
743
|
-
absolutePositioned: true,
|
|
744
749
|
left: 0,
|
|
745
750
|
top: 0,
|
|
751
|
+
absolutePositioned: true, // Important for groups
|
|
746
752
|
});
|
|
747
753
|
|
|
748
|
-
|
|
749
|
-
const tempBounds = tempPath.getBoundingRect();
|
|
754
|
+
clonedLayer.clipPath = clipPath;
|
|
750
755
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
originX: "left",
|
|
755
|
-
originY: "top",
|
|
756
|
-
});
|
|
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();
|
|
757
759
|
|
|
758
|
-
// 4.
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const exportBbox = clipPathCorrected.getBoundingRect();
|
|
762
|
-
const dataURL = canvas.toDataURL({
|
|
760
|
+
// 4. Export
|
|
761
|
+
const dataUrl = clonedLayer.toDataURL({
|
|
763
762
|
format: "png",
|
|
764
|
-
multiplier: 2,
|
|
765
|
-
left:
|
|
766
|
-
top:
|
|
767
|
-
width:
|
|
768
|
-
height:
|
|
763
|
+
multiplier: 2, // Better quality
|
|
764
|
+
left: bounds.left,
|
|
765
|
+
top: bounds.top,
|
|
766
|
+
width: bounds.width,
|
|
767
|
+
height: bounds.height,
|
|
769
768
|
});
|
|
770
769
|
|
|
771
|
-
|
|
772
|
-
canvas.clipPath = originalClip;
|
|
773
|
-
if (layer) layer.visible = wasVisible;
|
|
774
|
-
if (rulerLayer) rulerLayer.visible = rulerWasVisible;
|
|
775
|
-
holeMarkers.forEach((o) => (o.visible = true));
|
|
776
|
-
canvas.requestRenderAll();
|
|
777
|
-
|
|
778
|
-
return dataURL;
|
|
770
|
+
return dataUrl;
|
|
779
771
|
}
|
|
780
772
|
}
|