@pooder/kit 4.1.0 → 4.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.
@@ -0,0 +1,758 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DielineTool = void 0;
4
+ const core_1 = require("@pooder/core");
5
+ const fabric_1 = require("fabric");
6
+ const tracer_1 = require("./tracer");
7
+ const units_1 = require("./units");
8
+ const geometry_1 = require("./geometry");
9
+ class DielineTool {
10
+ constructor(options) {
11
+ this.id = "pooder.kit.dieline";
12
+ this.metadata = {
13
+ name: "DielineTool",
14
+ };
15
+ this.state = {
16
+ displayUnit: "mm",
17
+ shape: "rect",
18
+ width: 500,
19
+ height: 500,
20
+ radius: 0,
21
+ offset: 0,
22
+ padding: 140,
23
+ mainLine: {
24
+ width: 2.7,
25
+ color: "#FF0000",
26
+ dashLength: 5,
27
+ style: "solid",
28
+ },
29
+ offsetLine: {
30
+ width: 2.7,
31
+ color: "#FF0000",
32
+ dashLength: 5,
33
+ style: "solid",
34
+ },
35
+ insideColor: "rgba(0,0,0,0)",
36
+ outsideColor: "#ffffff",
37
+ showBleedLines: true,
38
+ features: [],
39
+ };
40
+ if (options) {
41
+ // Deep merge for styles to avoid overwriting defaults with partial objects
42
+ if (options.mainLine) {
43
+ Object.assign(this.state.mainLine, options.mainLine);
44
+ delete options.mainLine;
45
+ }
46
+ if (options.offsetLine) {
47
+ Object.assign(this.state.offsetLine, options.offsetLine);
48
+ delete options.offsetLine;
49
+ }
50
+ Object.assign(this.state, options);
51
+ }
52
+ }
53
+ activate(context) {
54
+ this.context = context;
55
+ this.canvasService = context.services.get("CanvasService");
56
+ if (!this.canvasService) {
57
+ console.warn("CanvasService not found for DielineTool");
58
+ return;
59
+ }
60
+ const configService = context.services.get("ConfigurationService");
61
+ if (configService) {
62
+ // Load initial config
63
+ const s = this.state;
64
+ s.displayUnit = configService.get("dieline.displayUnit", s.displayUnit);
65
+ s.shape = configService.get("dieline.shape", s.shape);
66
+ s.width = (0, units_1.parseLengthToMm)(configService.get("dieline.width", s.width), "mm");
67
+ s.height = (0, units_1.parseLengthToMm)(configService.get("dieline.height", s.height), "mm");
68
+ s.radius = (0, units_1.parseLengthToMm)(configService.get("dieline.radius", s.radius), "mm");
69
+ s.padding = configService.get("dieline.padding", s.padding);
70
+ s.offset = (0, units_1.parseLengthToMm)(configService.get("dieline.offset", s.offset), "mm");
71
+ // Main Line
72
+ s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
73
+ s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
74
+ s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
75
+ s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
76
+ // Offset Line
77
+ s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
78
+ s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
79
+ s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
80
+ s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
81
+ s.insideColor = configService.get("dieline.insideColor", s.insideColor);
82
+ s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
83
+ s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
84
+ s.features = configService.get("dieline.features", s.features);
85
+ s.pathData = configService.get("dieline.pathData", s.pathData);
86
+ // Listen for changes
87
+ configService.onAnyChange((e) => {
88
+ if (e.key.startsWith("dieline.")) {
89
+ switch (e.key) {
90
+ case "dieline.displayUnit":
91
+ s.displayUnit = e.value;
92
+ break;
93
+ case "dieline.shape":
94
+ s.shape = e.value;
95
+ break;
96
+ case "dieline.width":
97
+ s.width = (0, units_1.parseLengthToMm)(e.value, "mm");
98
+ break;
99
+ case "dieline.height":
100
+ s.height = (0, units_1.parseLengthToMm)(e.value, "mm");
101
+ break;
102
+ case "dieline.radius":
103
+ s.radius = (0, units_1.parseLengthToMm)(e.value, "mm");
104
+ break;
105
+ case "dieline.padding":
106
+ s.padding = e.value;
107
+ break;
108
+ case "dieline.offset":
109
+ s.offset = (0, units_1.parseLengthToMm)(e.value, "mm");
110
+ break;
111
+ case "dieline.strokeWidth":
112
+ s.mainLine.width = e.value;
113
+ break;
114
+ case "dieline.strokeColor":
115
+ s.mainLine.color = e.value;
116
+ break;
117
+ case "dieline.dashLength":
118
+ s.mainLine.dashLength = e.value;
119
+ break;
120
+ case "dieline.style":
121
+ s.mainLine.style = e.value;
122
+ break;
123
+ case "dieline.offsetStrokeWidth":
124
+ s.offsetLine.width = e.value;
125
+ break;
126
+ case "dieline.offsetStrokeColor":
127
+ s.offsetLine.color = e.value;
128
+ break;
129
+ case "dieline.offsetDashLength":
130
+ s.offsetLine.dashLength = e.value;
131
+ break;
132
+ case "dieline.offsetStyle":
133
+ s.offsetLine.style = e.value;
134
+ break;
135
+ case "dieline.insideColor":
136
+ s.insideColor = e.value;
137
+ break;
138
+ case "dieline.outsideColor":
139
+ s.outsideColor = e.value;
140
+ break;
141
+ case "dieline.showBleedLines":
142
+ s.showBleedLines = e.value;
143
+ break;
144
+ case "dieline.features":
145
+ s.features = e.value;
146
+ break;
147
+ case "dieline.pathData":
148
+ s.pathData = e.value;
149
+ break;
150
+ }
151
+ this.updateDieline();
152
+ }
153
+ });
154
+ }
155
+ this.createLayer();
156
+ this.updateDieline();
157
+ }
158
+ deactivate(context) {
159
+ this.destroyLayer();
160
+ this.canvasService = undefined;
161
+ this.context = undefined;
162
+ }
163
+ contribute() {
164
+ const s = this.state;
165
+ return {
166
+ [core_1.ContributionPointIds.CONFIGURATIONS]: [
167
+ {
168
+ id: "dieline.displayUnit",
169
+ type: "select",
170
+ label: "Display Unit",
171
+ options: ["mm", "cm", "in"],
172
+ default: s.displayUnit,
173
+ },
174
+ {
175
+ id: "dieline.shape",
176
+ type: "select",
177
+ label: "Shape",
178
+ options: ["rect", "circle", "ellipse", "custom"],
179
+ default: s.shape,
180
+ },
181
+ {
182
+ id: "dieline.width",
183
+ type: "number",
184
+ label: "Width (mm)",
185
+ min: 10,
186
+ max: 2000,
187
+ default: s.width,
188
+ },
189
+ {
190
+ id: "dieline.height",
191
+ type: "number",
192
+ label: "Height (mm)",
193
+ min: 10,
194
+ max: 2000,
195
+ default: s.height,
196
+ },
197
+ {
198
+ id: "dieline.radius",
199
+ type: "number",
200
+ label: "Corner Radius (mm)",
201
+ min: 0,
202
+ max: 500,
203
+ default: s.radius,
204
+ },
205
+ {
206
+ id: "dieline.padding",
207
+ type: "select",
208
+ label: "View Padding",
209
+ options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
210
+ default: s.padding,
211
+ },
212
+ {
213
+ id: "dieline.offset",
214
+ type: "number",
215
+ label: "Bleed Offset (mm)",
216
+ min: -100,
217
+ max: 100,
218
+ default: s.offset,
219
+ },
220
+ {
221
+ id: "dieline.showBleedLines",
222
+ type: "boolean",
223
+ label: "Show Bleed Lines",
224
+ default: s.showBleedLines,
225
+ },
226
+ {
227
+ id: "dieline.strokeWidth",
228
+ type: "number",
229
+ label: "Line Width",
230
+ min: 0.1,
231
+ max: 10,
232
+ step: 0.1,
233
+ default: s.mainLine.width,
234
+ },
235
+ {
236
+ id: "dieline.strokeColor",
237
+ type: "color",
238
+ label: "Line Color",
239
+ default: s.mainLine.color,
240
+ },
241
+ {
242
+ id: "dieline.dashLength",
243
+ type: "number",
244
+ label: "Dash Length",
245
+ min: 1,
246
+ max: 50,
247
+ default: s.mainLine.dashLength,
248
+ },
249
+ {
250
+ id: "dieline.style",
251
+ type: "select",
252
+ label: "Line Style",
253
+ options: ["solid", "dashed", "hidden"],
254
+ default: s.mainLine.style,
255
+ },
256
+ {
257
+ id: "dieline.offsetStrokeWidth",
258
+ type: "number",
259
+ label: "Offset Line Width",
260
+ min: 0.1,
261
+ max: 10,
262
+ step: 0.1,
263
+ default: s.offsetLine.width,
264
+ },
265
+ {
266
+ id: "dieline.offsetStrokeColor",
267
+ type: "color",
268
+ label: "Offset Line Color",
269
+ default: s.offsetLine.color,
270
+ },
271
+ {
272
+ id: "dieline.offsetDashLength",
273
+ type: "number",
274
+ label: "Offset Dash Length",
275
+ min: 1,
276
+ max: 50,
277
+ default: s.offsetLine.dashLength,
278
+ },
279
+ {
280
+ id: "dieline.offsetStyle",
281
+ type: "select",
282
+ label: "Offset Line Style",
283
+ options: ["solid", "dashed", "hidden"],
284
+ default: s.offsetLine.style,
285
+ },
286
+ {
287
+ id: "dieline.insideColor",
288
+ type: "color",
289
+ label: "Inside Color",
290
+ default: s.insideColor,
291
+ },
292
+ {
293
+ id: "dieline.outsideColor",
294
+ type: "color",
295
+ label: "Outside Color",
296
+ default: s.outsideColor,
297
+ },
298
+ {
299
+ id: "dieline.features",
300
+ type: "json",
301
+ label: "Edge Features",
302
+ default: s.features,
303
+ },
304
+ ],
305
+ [core_1.ContributionPointIds.COMMANDS]: [
306
+ {
307
+ command: "updateFeaturePosition",
308
+ title: "Update Feature Position",
309
+ handler: (groupId, x, y) => {
310
+ const configService = this.context?.services.get("ConfigurationService");
311
+ if (!configService)
312
+ return;
313
+ const features = configService.get("dieline.features") || [];
314
+ let changed = false;
315
+ const newFeatures = features.map((f) => {
316
+ if (f.groupId === groupId) {
317
+ if (f.x !== x || f.y !== y) {
318
+ changed = true;
319
+ return { ...f, x, y };
320
+ }
321
+ }
322
+ return f;
323
+ });
324
+ if (changed) {
325
+ configService.update("dieline.features", newFeatures);
326
+ }
327
+ },
328
+ },
329
+ {
330
+ command: "getGeometry",
331
+ title: "Get Geometry",
332
+ handler: () => {
333
+ return this.getGeometry();
334
+ },
335
+ },
336
+ {
337
+ command: "exportCutImage",
338
+ title: "Export Cut Image",
339
+ handler: () => {
340
+ return this.exportCutImage();
341
+ },
342
+ },
343
+ {
344
+ command: "detectEdge",
345
+ title: "Detect Edge from Image",
346
+ handler: async (imageUrl, options) => {
347
+ try {
348
+ const pathData = await tracer_1.ImageTracer.trace(imageUrl, options);
349
+ const bounds = (0, geometry_1.getPathBounds)(pathData);
350
+ const currentMax = Math.max(s.width, s.height);
351
+ const scale = currentMax / Math.max(bounds.width, bounds.height);
352
+ const newWidth = bounds.width * scale;
353
+ const newHeight = bounds.height * scale;
354
+ return {
355
+ pathData,
356
+ width: newWidth,
357
+ height: newHeight,
358
+ };
359
+ }
360
+ catch (e) {
361
+ console.error("Edge detection failed", e);
362
+ throw e;
363
+ }
364
+ },
365
+ },
366
+ ],
367
+ };
368
+ }
369
+ getLayer() {
370
+ return this.canvasService?.getLayer("dieline-overlay");
371
+ }
372
+ createLayer() {
373
+ if (!this.canvasService)
374
+ return;
375
+ const width = this.canvasService.canvas.width || 800;
376
+ const height = this.canvasService.canvas.height || 600;
377
+ const layer = this.canvasService.createLayer("dieline-overlay", {
378
+ width,
379
+ height,
380
+ selectable: false,
381
+ evented: false,
382
+ });
383
+ this.canvasService.canvas.bringObjectToFront(layer);
384
+ // Ensure above user layer
385
+ const userLayer = this.canvasService.getLayer("user");
386
+ if (userLayer) {
387
+ const userIndex = this.canvasService.canvas
388
+ .getObjects()
389
+ .indexOf(userLayer);
390
+ this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
391
+ }
392
+ }
393
+ destroyLayer() {
394
+ if (!this.canvasService)
395
+ return;
396
+ const layer = this.getLayer();
397
+ if (layer) {
398
+ this.canvasService.canvas.remove(layer);
399
+ }
400
+ }
401
+ createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
402
+ if (typeof document === "undefined") {
403
+ return undefined;
404
+ }
405
+ const size = 20;
406
+ const canvas = document.createElement("canvas");
407
+ canvas.width = size;
408
+ canvas.height = size;
409
+ const ctx = canvas.getContext("2d");
410
+ if (ctx) {
411
+ // Transparent background
412
+ ctx.clearRect(0, 0, size, size);
413
+ // Draw diagonal /
414
+ ctx.strokeStyle = color;
415
+ ctx.lineWidth = 1;
416
+ ctx.beginPath();
417
+ ctx.moveTo(0, size);
418
+ ctx.lineTo(size, 0);
419
+ ctx.stroke();
420
+ }
421
+ // @ts-ignore
422
+ return new fabric_1.Pattern({ source: canvas, repetition: "repeat" });
423
+ }
424
+ resolvePadding(containerWidth, containerHeight) {
425
+ if (typeof this.state.padding === "number") {
426
+ return this.state.padding;
427
+ }
428
+ if (typeof this.state.padding === "string") {
429
+ if (this.state.padding.endsWith("%")) {
430
+ const percent = parseFloat(this.state.padding) / 100;
431
+ return Math.min(containerWidth, containerHeight) * percent;
432
+ }
433
+ return parseFloat(this.state.padding) || 0;
434
+ }
435
+ return 0;
436
+ }
437
+ updateDieline(emitEvent = true) {
438
+ if (!this.canvasService)
439
+ return;
440
+ const layer = this.getLayer();
441
+ if (!layer)
442
+ return;
443
+ const { displayUnit, shape, radius, offset, mainLine, offsetLine, insideColor, outsideColor, showBleedLines, features, } = this.state;
444
+ const { width, height } = this.state;
445
+ const canvasW = this.canvasService.canvas.width || 800;
446
+ const canvasH = this.canvasService.canvas.height || 600;
447
+ // Calculate Layout based on Physical Dimensions and Canvas Size
448
+ // Add padding to avoid edge hugging
449
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
450
+ // Update Viewport System
451
+ this.canvasService.viewport.setPadding(paddingPx);
452
+ this.canvasService.viewport.updatePhysical(width, height);
453
+ const layout = this.canvasService.viewport.layout;
454
+ const scale = layout.scale;
455
+ const cx = layout.offsetX + layout.width / 2;
456
+ const cy = layout.offsetY + layout.height / 2;
457
+ // Scaled dimensions for rendering (Pixels)
458
+ const visualWidth = layout.width;
459
+ const visualHeight = layout.height;
460
+ const visualRadius = radius * scale;
461
+ const visualOffset = offset * scale;
462
+ // Clear existing objects
463
+ layer.remove(...layer.getObjects());
464
+ // Scale Features for Geometry Generation
465
+ const absoluteFeatures = (features || []).map((f) => {
466
+ const featureScale = scale;
467
+ return {
468
+ ...f,
469
+ x: f.x,
470
+ y: f.y,
471
+ width: (f.width || 0) * featureScale,
472
+ height: (f.height || 0) * featureScale,
473
+ radius: (f.radius || 0) * featureScale,
474
+ };
475
+ });
476
+ // Split features into Cut (Physical) and Visual (All)
477
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
478
+ // 1. Draw Mask (Outside)
479
+ const cutW = Math.max(0, visualWidth + visualOffset * 2);
480
+ const cutH = Math.max(0, visualHeight + visualOffset * 2);
481
+ const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
482
+ // Use Paper.js to generate the complex mask path
483
+ const maskPathData = (0, geometry_1.generateMaskPath)({
484
+ canvasWidth: canvasW,
485
+ canvasHeight: canvasH,
486
+ shape,
487
+ width: cutW,
488
+ height: cutH,
489
+ radius: cutR,
490
+ x: cx,
491
+ y: cy,
492
+ features: cutFeatures,
493
+ pathData: this.state.pathData,
494
+ });
495
+ const mask = new fabric_1.Path(maskPathData, {
496
+ fill: outsideColor,
497
+ stroke: null,
498
+ selectable: false,
499
+ evented: false,
500
+ originX: "left",
501
+ originY: "top",
502
+ left: 0,
503
+ top: 0,
504
+ });
505
+ layer.add(mask);
506
+ // 2. Draw Inside Fill (Dieline Shape itself, merged with features if needed)
507
+ if (insideColor &&
508
+ insideColor !== "transparent" &&
509
+ insideColor !== "rgba(0,0,0,0)") {
510
+ // Generate path for the product shape (Paper) = Dieline +/- Features
511
+ const productPathData = (0, geometry_1.generateDielinePath)({
512
+ shape,
513
+ width: cutW,
514
+ height: cutH,
515
+ radius: cutR,
516
+ x: cx,
517
+ y: cy,
518
+ features: cutFeatures, // Use same features as mask for consistency
519
+ pathData: this.state.pathData,
520
+ canvasWidth: canvasW,
521
+ canvasHeight: canvasH,
522
+ });
523
+ const insideObj = new fabric_1.Path(productPathData, {
524
+ fill: insideColor,
525
+ stroke: null,
526
+ selectable: false,
527
+ evented: false,
528
+ originX: "left", // paper.js paths are absolute
529
+ originY: "top",
530
+ });
531
+ layer.add(insideObj);
532
+ }
533
+ // 3. Draw Bleed Zone (Hatch Fill) and Offset Border
534
+ if (offset !== 0) {
535
+ const bleedPathData = (0, geometry_1.generateBleedZonePath)({
536
+ shape,
537
+ width: visualWidth,
538
+ height: visualHeight,
539
+ radius: visualRadius,
540
+ x: cx,
541
+ y: cy,
542
+ features: cutFeatures,
543
+ pathData: this.state.pathData,
544
+ canvasWidth: canvasW,
545
+ canvasHeight: canvasH,
546
+ }, {
547
+ shape,
548
+ width: cutW,
549
+ height: cutH,
550
+ radius: cutR,
551
+ x: cx,
552
+ y: cy,
553
+ features: cutFeatures,
554
+ pathData: this.state.pathData,
555
+ canvasWidth: canvasW,
556
+ canvasHeight: canvasH,
557
+ }, visualOffset);
558
+ // Use solid red for hatch lines to match dieline, background is transparent
559
+ if (showBleedLines !== false) {
560
+ const pattern = this.createHatchPattern(mainLine.color);
561
+ if (pattern) {
562
+ const bleedObj = new fabric_1.Path(bleedPathData, {
563
+ fill: pattern,
564
+ stroke: null,
565
+ selectable: false,
566
+ evented: false,
567
+ objectCaching: false,
568
+ originX: "left",
569
+ originY: "top",
570
+ });
571
+ layer.add(bleedObj);
572
+ }
573
+ }
574
+ // Offset Dieline Border
575
+ const offsetPathData = (0, geometry_1.generateDielinePath)({
576
+ shape,
577
+ width: cutW,
578
+ height: cutH,
579
+ radius: cutR,
580
+ x: cx,
581
+ y: cy,
582
+ features: cutFeatures,
583
+ pathData: this.state.pathData,
584
+ canvasWidth: canvasW,
585
+ canvasHeight: canvasH,
586
+ });
587
+ const offsetBorderObj = new fabric_1.Path(offsetPathData, {
588
+ fill: null,
589
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
590
+ strokeWidth: offsetLine.width,
591
+ strokeDashArray: offsetLine.style === "dashed"
592
+ ? [offsetLine.dashLength, offsetLine.dashLength]
593
+ : undefined,
594
+ selectable: false,
595
+ evented: false,
596
+ originX: "left",
597
+ originY: "top",
598
+ });
599
+ layer.add(offsetBorderObj);
600
+ }
601
+ // 4. Draw Dieline (Visual Border)
602
+ const borderPathData = (0, geometry_1.generateDielinePath)({
603
+ shape,
604
+ width: visualWidth,
605
+ height: visualHeight,
606
+ radius: visualRadius,
607
+ x: cx,
608
+ y: cy,
609
+ features: absoluteFeatures,
610
+ pathData: this.state.pathData,
611
+ canvasWidth: canvasW,
612
+ canvasHeight: canvasH,
613
+ });
614
+ const borderObj = new fabric_1.Path(borderPathData, {
615
+ fill: "transparent",
616
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
617
+ strokeWidth: mainLine.width,
618
+ strokeDashArray: mainLine.style === "dashed"
619
+ ? [mainLine.dashLength, mainLine.dashLength]
620
+ : undefined,
621
+ selectable: false,
622
+ evented: false,
623
+ originX: "left",
624
+ originY: "top",
625
+ });
626
+ layer.add(borderObj);
627
+ // Enforce z-index: Dieline > User
628
+ const userLayer = this.canvasService.getLayer("user");
629
+ if (layer && userLayer) {
630
+ const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
631
+ const userIndex = this.canvasService.canvas
632
+ .getObjects()
633
+ .indexOf(userLayer);
634
+ if (layerIndex < userIndex) {
635
+ this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
636
+ }
637
+ }
638
+ else {
639
+ // If no user layer, just bring to front (safe default)
640
+ this.canvasService.canvas.bringObjectToFront(layer);
641
+ }
642
+ // Ensure Ruler is above Dieline if it exists
643
+ const rulerLayer = this.canvasService.getLayer("ruler-overlay");
644
+ if (rulerLayer) {
645
+ this.canvasService.canvas.bringObjectToFront(rulerLayer);
646
+ }
647
+ layer.dirty = true;
648
+ this.canvasService.requestRenderAll();
649
+ // Emit change event so other tools (like FeatureTool) can react
650
+ if (emitEvent && this.context) {
651
+ const geometry = this.getGeometry();
652
+ if (geometry) {
653
+ this.context.eventBus.emit("dieline:geometry:change", geometry);
654
+ }
655
+ }
656
+ }
657
+ getGeometry() {
658
+ if (!this.canvasService)
659
+ return null;
660
+ const { displayUnit, shape, width, height, radius, offset, mainLine, pathData, } = this.state;
661
+ const canvasW = this.canvasService.canvas.width || 800;
662
+ const canvasH = this.canvasService.canvas.height || 600;
663
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
664
+ // Update Viewport System (Ensure it's up to date)
665
+ this.canvasService.viewport.setPadding(paddingPx);
666
+ this.canvasService.viewport.updatePhysical(width, height);
667
+ const layout = this.canvasService.viewport.layout;
668
+ const scale = layout.scale;
669
+ const cx = layout.offsetX + layout.width / 2;
670
+ const cy = layout.offsetY + layout.height / 2;
671
+ const visualWidth = layout.width;
672
+ const visualHeight = layout.height;
673
+ return {
674
+ shape,
675
+ unit: "mm",
676
+ displayUnit,
677
+ x: cx,
678
+ y: cy,
679
+ width: visualWidth,
680
+ height: visualHeight,
681
+ radius: radius * scale,
682
+ offset: offset * scale,
683
+ scale,
684
+ strokeWidth: mainLine.width,
685
+ pathData,
686
+ };
687
+ }
688
+ async exportCutImage() {
689
+ if (!this.canvasService)
690
+ return null;
691
+ const userLayer = this.canvasService.getLayer("user");
692
+ if (!userLayer)
693
+ return null;
694
+ // 1. Generate Path Data
695
+ const { shape, width, height, radius, features, pathData } = this.state;
696
+ const canvasW = this.canvasService.canvas.width || 800;
697
+ const canvasH = this.canvasService.canvas.height || 600;
698
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
699
+ // Update Viewport System
700
+ this.canvasService.viewport.setPadding(paddingPx);
701
+ this.canvasService.viewport.updatePhysical(width, height);
702
+ const layout = this.canvasService.viewport.layout;
703
+ const scale = layout.scale;
704
+ const cx = layout.offsetX + layout.width / 2;
705
+ const cy = layout.offsetY + layout.height / 2;
706
+ const visualWidth = layout.width;
707
+ const visualHeight = layout.height;
708
+ const visualRadius = radius * scale;
709
+ // Scale Features
710
+ const absoluteFeatures = (features || []).map((f) => {
711
+ const featureScale = scale;
712
+ return {
713
+ ...f,
714
+ x: f.x,
715
+ y: f.y,
716
+ width: (f.width || 0) * featureScale,
717
+ height: (f.height || 0) * featureScale,
718
+ radius: (f.radius || 0) * featureScale,
719
+ };
720
+ });
721
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
722
+ const generatedPathData = (0, geometry_1.generateDielinePath)({
723
+ shape,
724
+ width: visualWidth,
725
+ height: visualHeight,
726
+ radius: visualRadius,
727
+ x: cx,
728
+ y: cy,
729
+ features: cutFeatures,
730
+ pathData,
731
+ canvasWidth: canvasW,
732
+ canvasHeight: canvasH,
733
+ });
734
+ // 2. Prepare for Export
735
+ const clonedLayer = await userLayer.clone();
736
+ const clipPath = new fabric_1.Path(generatedPathData, {
737
+ originX: "left",
738
+ originY: "top",
739
+ left: 0,
740
+ top: 0,
741
+ absolutePositioned: true,
742
+ });
743
+ clonedLayer.clipPath = clipPath;
744
+ // 3. Calculate Crop Area (The Dieline Bounds)
745
+ const bounds = clipPath.getBoundingRect();
746
+ // 4. Export
747
+ const dataUrl = clonedLayer.toDataURL({
748
+ format: "png",
749
+ multiplier: 2,
750
+ left: bounds.left,
751
+ top: bounds.top,
752
+ width: bounds.width,
753
+ height: bounds.height,
754
+ });
755
+ return dataUrl;
756
+ }
757
+ }
758
+ exports.DielineTool = DielineTool;