@pooder/kit 3.4.0 → 4.0.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/src/dieline.ts CHANGED
@@ -14,8 +14,7 @@ import {
14
14
  generateMaskPath,
15
15
  generateBleedZonePath,
16
16
  getPathBounds,
17
- HoleData,
18
- resolveHolePosition,
17
+ DielineFeature,
19
18
  } from "./geometry";
20
19
 
21
20
  export interface DielineGeometry {
@@ -29,6 +28,31 @@ export interface DielineGeometry {
29
28
  offset: number;
30
29
  borderLength?: number;
31
30
  scale?: number;
31
+ strokeWidth?: number;
32
+ pathData?: string;
33
+ }
34
+
35
+ export interface LineStyle {
36
+ width: number;
37
+ color: string;
38
+ dashLength: number;
39
+ style: "solid" | "dashed" | "hidden";
40
+ }
41
+
42
+ export interface DielineState {
43
+ unit: Unit;
44
+ shape: "rect" | "circle" | "ellipse" | "custom";
45
+ width: number;
46
+ height: number;
47
+ radius: number;
48
+ offset: number;
49
+ padding: number | string;
50
+ mainLine: LineStyle;
51
+ offsetLine: LineStyle;
52
+ insideColor: string;
53
+ outsideColor: string;
54
+ showBleedLines: boolean;
55
+ features: DielineFeature[];
32
56
  pathData?: string;
33
57
  }
34
58
 
@@ -38,46 +62,47 @@ export class DielineTool implements Extension {
38
62
  name: "DielineTool",
39
63
  };
40
64
 
41
- private unit: Unit = "mm";
42
- private shape: "rect" | "circle" | "ellipse" | "custom" = "rect";
43
- private width: number = 500;
44
- private height: number = 500;
45
- private radius: number = 0;
46
- private offset: number = 0;
47
- private style: "solid" | "dashed" = "solid";
48
- private insideColor: string = "rgba(0,0,0,0)";
49
- private outsideColor: string = "#ffffff";
50
- private showBleedLines: boolean = true;
51
- private holes: HoleData[] = [];
52
- // Position is stored as normalized coordinates (0-1)
53
- private position?: { x: number; y: number };
54
- private padding: number | string = 140;
55
- private pathData?: string;
65
+ private state: DielineState = {
66
+ unit: "mm",
67
+ shape: "rect",
68
+ width: 500,
69
+ height: 500,
70
+ radius: 0,
71
+ offset: 0,
72
+ padding: 140,
73
+ mainLine: {
74
+ width: 2.7,
75
+ color: "#FF0000",
76
+ dashLength: 5,
77
+ style: "solid",
78
+ },
79
+ offsetLine: {
80
+ width: 2.7,
81
+ color: "#FF0000",
82
+ dashLength: 5,
83
+ style: "solid",
84
+ },
85
+ insideColor: "rgba(0,0,0,0)",
86
+ outsideColor: "#ffffff",
87
+ showBleedLines: true,
88
+ features: [],
89
+ };
56
90
 
57
91
  private canvasService?: CanvasService;
58
92
  private context?: ExtensionContext;
59
93
 
60
- constructor(
61
- options?: Partial<{
62
- unit: Unit;
63
- shape: "rect" | "circle" | "ellipse" | "custom";
64
- width: number;
65
- height: number;
66
- radius: number;
67
- // Position is normalized (0-1)
68
- position: { x: number; y: number };
69
- padding: number | string;
70
- offset: number;
71
- style: "solid" | "dashed";
72
- insideColor: string;
73
- outsideColor: string;
74
- showBleedLines: boolean;
75
- holes: HoleData[];
76
- pathData: string;
77
- }>,
78
- ) {
94
+ constructor(options?: Partial<DielineState>) {
79
95
  if (options) {
80
- Object.assign(this, options);
96
+ // Deep merge for styles to avoid overwriting defaults with partial objects
97
+ if (options.mainLine) {
98
+ Object.assign(this.state.mainLine, options.mainLine);
99
+ delete options.mainLine;
100
+ }
101
+ if (options.offsetLine) {
102
+ Object.assign(this.state.offsetLine, options.offsetLine);
103
+ delete options.offsetLine;
104
+ }
105
+ Object.assign(this.state, options);
81
106
  }
82
107
  }
83
108
 
@@ -92,40 +117,64 @@ export class DielineTool implements Extension {
92
117
  const configService = context.services.get<any>("ConfigurationService");
93
118
  if (configService) {
94
119
  // Load initial config
95
- this.unit = configService.get("dieline.unit", this.unit);
96
- this.shape = configService.get("dieline.shape", this.shape);
97
- this.width = configService.get("dieline.width", this.width);
98
- this.height = configService.get("dieline.height", this.height);
99
- this.radius = configService.get("dieline.radius", this.radius);
100
- this.padding = configService.get("dieline.padding", this.padding);
101
- this.offset = configService.get("dieline.offset", this.offset);
102
- this.style = configService.get("dieline.style", this.style);
103
- this.insideColor = configService.get(
104
- "dieline.insideColor",
105
- this.insideColor,
106
- );
107
- this.outsideColor = configService.get(
108
- "dieline.outsideColor",
109
- this.outsideColor,
110
- );
111
- this.showBleedLines = configService.get(
112
- "dieline.showBleedLines",
113
- this.showBleedLines,
114
- );
115
- this.holes = configService.get("dieline.holes", this.holes);
116
- this.pathData = configService.get("dieline.pathData", this.pathData);
120
+ const s = this.state;
121
+ s.unit = configService.get("dieline.unit", s.unit);
122
+ s.shape = configService.get("dieline.shape", s.shape);
123
+ s.width = configService.get("dieline.width", s.width);
124
+ s.height = configService.get("dieline.height", s.height);
125
+ s.radius = configService.get("dieline.radius", s.radius);
126
+ s.padding = configService.get("dieline.padding", s.padding);
127
+ s.offset = configService.get("dieline.offset", s.offset);
128
+
129
+ // Main Line
130
+ s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
131
+ s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
132
+ s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
133
+ s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
134
+
135
+ // Offset Line
136
+ s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
137
+ s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
138
+ s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
139
+ s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
140
+
141
+ s.insideColor = configService.get("dieline.insideColor", s.insideColor);
142
+ s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
143
+ s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
144
+ s.features = configService.get("dieline.features", s.features);
145
+ s.pathData = configService.get("dieline.pathData", s.pathData);
117
146
 
118
147
  // Listen for changes
119
148
  configService.onAnyChange((e: { key: string; value: any }) => {
120
149
  if (e.key.startsWith("dieline.")) {
121
- const prop = e.key.split(".")[1];
122
- console.log(
123
- `[DielineTool] Config change detected: ${e.key} -> ${e.value}`,
124
- );
125
- if (prop && prop in this) {
126
- (this as any)[prop] = e.value;
127
- this.updateDieline();
150
+ console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
151
+
152
+ switch (e.key) {
153
+ case "dieline.unit": s.unit = e.value; break;
154
+ case "dieline.shape": s.shape = e.value; break;
155
+ case "dieline.width": s.width = e.value; break;
156
+ case "dieline.height": s.height = e.value; break;
157
+ case "dieline.radius": s.radius = e.value; break;
158
+ case "dieline.padding": s.padding = e.value; break;
159
+ case "dieline.offset": s.offset = e.value; break;
160
+
161
+ case "dieline.strokeWidth": s.mainLine.width = e.value; break;
162
+ case "dieline.strokeColor": s.mainLine.color = e.value; break;
163
+ case "dieline.dashLength": s.mainLine.dashLength = e.value; break;
164
+ case "dieline.style": s.mainLine.style = e.value; break;
165
+
166
+ case "dieline.offsetStrokeWidth": s.offsetLine.width = e.value; break;
167
+ case "dieline.offsetStrokeColor": s.offsetLine.color = e.value; break;
168
+ case "dieline.offsetDashLength": s.offsetLine.dashLength = e.value; break;
169
+ case "dieline.offsetStyle": s.offsetLine.style = e.value; break;
170
+
171
+ case "dieline.insideColor": s.insideColor = e.value; break;
172
+ case "dieline.outsideColor": s.outsideColor = e.value; break;
173
+ case "dieline.showBleedLines": s.showBleedLines = e.value; break;
174
+ case "dieline.features": s.features = e.value; break;
175
+ case "dieline.pathData": s.pathData = e.value; break;
128
176
  }
177
+ this.updateDieline();
129
178
  }
130
179
  });
131
180
  }
@@ -141,6 +190,7 @@ export class DielineTool implements Extension {
141
190
  }
142
191
 
143
192
  contribute() {
193
+ const s = this.state;
144
194
  return {
145
195
  [ContributionPointIds.CONFIGURATIONS]: [
146
196
  {
@@ -148,14 +198,14 @@ export class DielineTool implements Extension {
148
198
  type: "select",
149
199
  label: "Unit",
150
200
  options: ["px", "mm", "cm", "in"],
151
- default: this.unit,
201
+ default: s.unit,
152
202
  },
153
203
  {
154
204
  id: "dieline.shape",
155
205
  type: "select",
156
206
  label: "Shape",
157
207
  options: ["rect", "circle", "ellipse", "custom"],
158
- default: this.shape,
208
+ default: s.shape,
159
209
  },
160
210
  {
161
211
  id: "dieline.width",
@@ -163,7 +213,7 @@ export class DielineTool implements Extension {
163
213
  label: "Width",
164
214
  min: 10,
165
215
  max: 2000,
166
- default: this.width,
216
+ default: s.width,
167
217
  },
168
218
  {
169
219
  id: "dieline.height",
@@ -171,7 +221,7 @@ export class DielineTool implements Extension {
171
221
  label: "Height",
172
222
  min: 10,
173
223
  max: 2000,
174
- default: this.height,
224
+ default: s.height,
175
225
  },
176
226
  {
177
227
  id: "dieline.radius",
@@ -179,20 +229,14 @@ export class DielineTool implements Extension {
179
229
  label: "Corner Radius",
180
230
  min: 0,
181
231
  max: 500,
182
- default: this.radius,
183
- },
184
- {
185
- id: "dieline.position",
186
- type: "json",
187
- label: "Position (Normalized)",
188
- default: this.radius,
232
+ default: s.radius,
189
233
  },
190
234
  {
191
235
  id: "dieline.padding",
192
236
  type: "select",
193
237
  label: "View Padding",
194
238
  options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
195
- default: this.padding,
239
+ default: s.padding,
196
240
  },
197
241
  {
198
242
  id: "dieline.offset",
@@ -200,38 +244,91 @@ export class DielineTool implements Extension {
200
244
  label: "Bleed Offset",
201
245
  min: -100,
202
246
  max: 100,
203
- default: this.offset,
247
+ default: s.offset,
204
248
  },
205
249
  {
206
250
  id: "dieline.showBleedLines",
207
251
  type: "boolean",
208
252
  label: "Show Bleed Lines",
209
- default: this.showBleedLines,
253
+ default: s.showBleedLines,
254
+ },
255
+ {
256
+ id: "dieline.strokeWidth",
257
+ type: "number",
258
+ label: "Line Width",
259
+ min: 0.1,
260
+ max: 10,
261
+ step: 0.1,
262
+ default: s.mainLine.width,
263
+ },
264
+ {
265
+ id: "dieline.strokeColor",
266
+ type: "color",
267
+ label: "Line Color",
268
+ default: s.mainLine.color,
269
+ },
270
+ {
271
+ id: "dieline.dashLength",
272
+ type: "number",
273
+ label: "Dash Length",
274
+ min: 1,
275
+ max: 50,
276
+ default: s.mainLine.dashLength,
210
277
  },
211
278
  {
212
279
  id: "dieline.style",
213
280
  type: "select",
214
281
  label: "Line Style",
215
- options: ["solid", "dashed"],
216
- default: this.style,
282
+ options: ["solid", "dashed", "hidden"],
283
+ default: s.mainLine.style,
284
+ },
285
+ {
286
+ id: "dieline.offsetStrokeWidth",
287
+ type: "number",
288
+ label: "Offset Line Width",
289
+ min: 0.1,
290
+ max: 10,
291
+ step: 0.1,
292
+ default: s.offsetLine.width,
293
+ },
294
+ {
295
+ id: "dieline.offsetStrokeColor",
296
+ type: "color",
297
+ label: "Offset Line Color",
298
+ default: s.offsetLine.color,
299
+ },
300
+ {
301
+ id: "dieline.offsetDashLength",
302
+ type: "number",
303
+ label: "Offset Dash Length",
304
+ min: 1,
305
+ max: 50,
306
+ default: s.offsetLine.dashLength,
307
+ },
308
+ {
309
+ id: "dieline.offsetStyle",
310
+ type: "select",
311
+ label: "Offset Line Style",
312
+ options: ["solid", "dashed", "hidden"],
313
+ default: s.offsetLine.style,
217
314
  },
218
315
  {
219
316
  id: "dieline.insideColor",
220
317
  type: "color",
221
318
  label: "Inside Color",
222
- default: this.insideColor,
319
+ default: s.insideColor,
223
320
  },
224
321
  {
225
322
  id: "dieline.outsideColor",
226
323
  type: "color",
227
324
  label: "Outside Color",
228
- default: this.outsideColor,
325
+ default: s.outsideColor,
229
326
  },
230
327
  {
231
- id: "dieline.holes",
328
+ id: "dieline.features",
232
329
  type: "json",
233
- label: "Holes",
234
- default: this.holes,
330
+ label: "Edge Features",
331
+ default: s.features,
235
332
  },
236
333
  ] as ConfigurationContribution[],
237
334
  [ContributionPointIds.COMMANDS]: [
@@ -257,23 +354,17 @@ export class DielineTool implements Extension {
257
354
  const pathData = await ImageTracer.trace(imageUrl, options);
258
355
  const bounds = getPathBounds(pathData);
259
356
 
260
- const currentMax = Math.max(this.width, this.height);
357
+ const currentMax = Math.max(s.width, s.height);
261
358
  const scale = currentMax / Math.max(bounds.width, bounds.height);
262
359
 
263
360
  const newWidth = bounds.width * scale;
264
361
  const newHeight = bounds.height * scale;
265
362
 
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
- }
275
-
276
- return pathData;
363
+ return {
364
+ pathData,
365
+ width: newWidth,
366
+ height: newHeight,
367
+ };
277
368
  } catch (e) {
278
369
  console.error("Edge detection failed", e);
279
370
  throw e;
@@ -349,15 +440,15 @@ export class DielineTool implements Extension {
349
440
  containerWidth: number,
350
441
  containerHeight: number,
351
442
  ): number {
352
- if (typeof this.padding === "number") {
353
- return this.padding;
443
+ if (typeof this.state.padding === "number") {
444
+ return this.state.padding;
354
445
  }
355
- if (typeof this.padding === "string") {
356
- if (this.padding.endsWith("%")) {
357
- const percent = parseFloat(this.padding) / 100;
446
+ if (typeof this.state.padding === "string") {
447
+ if (this.state.padding.endsWith("%")) {
448
+ const percent = parseFloat(this.state.padding) / 100;
358
449
  return Math.min(containerWidth, containerHeight) * percent;
359
450
  }
360
- return parseFloat(this.padding) || 0;
451
+ return parseFloat(this.state.padding) || 0;
361
452
  }
362
453
  return 0;
363
454
  }
@@ -372,14 +463,14 @@ export class DielineTool implements Extension {
372
463
  shape,
373
464
  radius,
374
465
  offset,
375
- style,
466
+ mainLine,
467
+ offsetLine,
376
468
  insideColor,
377
469
  outsideColor,
378
- position,
379
470
  showBleedLines,
380
- holes,
381
- } = this;
382
- let { width, height } = this;
471
+ features,
472
+ } = this.state;
473
+ let { width, height } = this.state;
383
474
 
384
475
  const canvasW = this.canvasService.canvas.width || 800;
385
476
  const canvasH = this.canvasService.canvas.height || 600;
@@ -406,45 +497,24 @@ export class DielineTool implements Extension {
406
497
  // Clear existing objects
407
498
  layer.remove(...layer.getObjects());
408
499
 
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
-
418
- const absoluteHoles = (holes || []).map((h) => {
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
- });
500
+ // Scale Features for Geometry Generation
501
+ const absoluteFeatures = (features || []).map((f) => {
502
+ // Scale current unit -> pixels (features share the same unit as the dieline)
503
+ const featureScale = scale;
434
504
 
435
505
  return {
436
- ...h,
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,
506
+ ...f,
507
+ x: f.x,
508
+ y: f.y,
509
+ width: (f.width || 0) * featureScale,
510
+ height: (f.height || 0) * featureScale,
511
+ radius: (f.radius || 0) * featureScale,
445
512
  };
446
513
  });
447
514
 
515
+ // Split features into Cut (Physical) and Visual (All)
516
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
517
+
448
518
  // 1. Draw Mask (Outside)
449
519
  const cutW = Math.max(0, visualWidth + visualOffset * 2);
450
520
  const cutH = Math.max(0, visualHeight + visualOffset * 2);
@@ -461,8 +531,8 @@ export class DielineTool implements Extension {
461
531
  radius: cutR,
462
532
  x: cx,
463
533
  y: cy,
464
- holes: absoluteHoles,
465
- pathData: this.pathData,
534
+ features: cutFeatures,
535
+ pathData: this.state.pathData,
466
536
  });
467
537
 
468
538
  const mask = new Path(maskPathData, {
@@ -477,13 +547,13 @@ export class DielineTool implements Extension {
477
547
  });
478
548
  layer.add(mask);
479
549
 
480
- // 2. Draw Inside Fill (Dieline Shape itself, merged with holes if needed)
550
+ // 2. Draw Inside Fill (Dieline Shape itself, merged with features if needed)
481
551
  if (
482
552
  insideColor &&
483
553
  insideColor !== "transparent" &&
484
554
  insideColor !== "rgba(0,0,0,0)"
485
555
  ) {
486
- // Generate path for the product shape (Paper) = Dieline - Holes
556
+ // Generate path for the product shape (Paper) = Dieline +/- Features
487
557
  const productPathData = generateDielinePath({
488
558
  shape,
489
559
  width: cutW,
@@ -491,8 +561,8 @@ export class DielineTool implements Extension {
491
561
  radius: cutR,
492
562
  x: cx,
493
563
  y: cy,
494
- holes: absoluteHoles,
495
- pathData: this.pathData,
564
+ features: cutFeatures, // Use same features as mask for consistency
565
+ pathData: this.state.pathData,
496
566
  canvasWidth: canvasW,
497
567
  canvasHeight: canvasH,
498
568
  });
@@ -518,8 +588,20 @@ export class DielineTool implements Extension {
518
588
  radius: visualRadius,
519
589
  x: cx,
520
590
  y: cy,
521
- holes: absoluteHoles,
522
- pathData: this.pathData,
591
+ features: cutFeatures,
592
+ pathData: this.state.pathData,
593
+ canvasWidth: canvasW,
594
+ canvasHeight: canvasH,
595
+ },
596
+ {
597
+ shape,
598
+ width: cutW,
599
+ height: cutH,
600
+ radius: cutR,
601
+ x: cx,
602
+ y: cy,
603
+ features: cutFeatures,
604
+ pathData: this.state.pathData,
523
605
  canvasWidth: canvasW,
524
606
  canvasHeight: canvasH,
525
607
  },
@@ -528,7 +610,7 @@ export class DielineTool implements Extension {
528
610
 
529
611
  // Use solid red for hatch lines to match dieline, background is transparent
530
612
  if (showBleedLines !== false) {
531
- const pattern = this.createHatchPattern("red");
613
+ const pattern = this.createHatchPattern(mainLine.color);
532
614
  if (pattern) {
533
615
  const bleedObj = new Path(bleedPathData, {
534
616
  fill: pattern,
@@ -551,17 +633,20 @@ export class DielineTool implements Extension {
551
633
  radius: cutR,
552
634
  x: cx,
553
635
  y: cy,
554
- holes: absoluteHoles,
555
- pathData: this.pathData,
636
+ features: cutFeatures,
637
+ pathData: this.state.pathData,
556
638
  canvasWidth: canvasW,
557
639
  canvasHeight: canvasH,
558
640
  });
559
641
 
560
642
  const offsetBorderObj = new Path(offsetPathData, {
561
643
  fill: null,
562
- stroke: "#666", // Grey
563
- strokeWidth: 1,
564
- strokeDashArray: [4, 4], // Dashed
644
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
645
+ strokeWidth: offsetLine.width,
646
+ strokeDashArray:
647
+ offsetLine.style === "dashed"
648
+ ? [offsetLine.dashLength, offsetLine.dashLength]
649
+ : undefined,
565
650
  selectable: false,
566
651
  evented: false,
567
652
  originX: "left",
@@ -571,9 +656,6 @@ export class DielineTool implements Extension {
571
656
  }
572
657
 
573
658
  // 4. Draw Dieline (Visual Border)
574
- // This should outline the product shape AND the holes.
575
- // NOTE: We need to use absoluteHoles (denormalized) here, NOT holes (normalized 0-1)
576
- // generateDielinePath expects holes to be in absolute coordinates (matching width/height scale)
577
659
  const borderPathData = generateDielinePath({
578
660
  shape,
579
661
  width: visualWidth,
@@ -581,17 +663,18 @@ export class DielineTool implements Extension {
581
663
  radius: visualRadius,
582
664
  x: cx,
583
665
  y: cy,
584
- holes: absoluteHoles,
585
- pathData: this.pathData,
666
+ features: absoluteFeatures,
667
+ pathData: this.state.pathData,
586
668
  canvasWidth: canvasW,
587
669
  canvasHeight: canvasH,
588
670
  });
589
671
 
590
672
  const borderObj = new Path(borderPathData, {
591
673
  fill: "transparent",
592
- stroke: "red",
593
- strokeWidth: 1,
594
- strokeDashArray: style === "dashed" ? [5, 5] : undefined,
674
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
675
+ strokeWidth: mainLine.width,
676
+ strokeDashArray:
677
+ mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : undefined,
595
678
  selectable: false,
596
679
  evented: false,
597
680
  originX: "left",
@@ -624,16 +707,8 @@ export class DielineTool implements Extension {
624
707
  layer.dirty = true;
625
708
  this.canvasService.requestRenderAll();
626
709
 
627
- // Emit change event so other tools (like HoleTool) can react
628
- // Only emit if requested (to avoid loops when updating non-geometry props like holes)
710
+ // Emit change event so other tools (like FeatureTool) can react
629
711
  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
-
637
712
  const geometry = this.getGeometry();
638
713
  if (geometry) {
639
714
  this.context.eventBus.emit("dieline:geometry:change", geometry);
@@ -643,7 +718,7 @@ export class DielineTool implements Extension {
643
718
 
644
719
  public getGeometry(): DielineGeometry | null {
645
720
  if (!this.canvasService) return null;
646
- const { unit, shape, width, height, radius, position, offset } = this;
721
+ const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
647
722
  const canvasW = this.canvasService.canvas.width || 800;
648
723
  const canvasH = this.canvasService.canvas.height || 600;
649
724
 
@@ -670,9 +745,10 @@ export class DielineTool implements Extension {
670
745
  height: visualHeight,
671
746
  radius: radius * scale,
672
747
  offset: offset * scale,
673
- // Pass scale to help other tools (like HoleTool) convert units
748
+ // Pass scale to help other tools (like FeatureTool) convert units
674
749
  scale,
675
- pathData: this.pathData,
750
+ strokeWidth: mainLine.width,
751
+ pathData,
676
752
  } as DielineGeometry;
677
753
  }
678
754
 
@@ -680,13 +756,10 @@ export class DielineTool implements Extension {
680
756
  if (!this.canvasService) return null;
681
757
  const userLayer = this.canvasService.getLayer("user");
682
758
 
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
759
  if (!userLayer) return null;
687
760
 
688
761
  // 1. Generate Path Data
689
- const { shape, width, height, radius, position, holes } = this;
762
+ const { shape, width, height, radius, features, unit, pathData } = this.state;
690
763
  const canvasW = this.canvasService.canvas.width || 800;
691
764
  const canvasH = this.canvasService.canvas.height || 600;
692
765
 
@@ -703,74 +776,55 @@ export class DielineTool implements Extension {
703
776
  const visualHeight = layout.height;
704
777
  const visualRadius = radius * scale;
705
778
 
706
- // Denormalize Holes for Export
707
- const absoluteHoles = (holes || []).map((h) => {
708
- const unit = this.unit || "mm";
709
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
710
-
711
- const pos = resolveHolePosition(
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 },
719
- );
779
+ // Scale Features
780
+ const absoluteFeatures = (features || []).map((f) => {
781
+ const featureScale = scale;
720
782
 
721
783
  return {
722
- ...h,
723
- x: pos.x,
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,
784
+ ...f,
785
+ x: f.x,
786
+ y: f.y,
787
+ width: (f.width || 0) * featureScale,
788
+ height: (f.height || 0) * featureScale,
789
+ radius: (f.radius || 0) * featureScale,
729
790
  };
730
791
  });
731
792
 
732
- const pathData = generateDielinePath({
793
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
794
+
795
+ const generatedPathData = generateDielinePath({
733
796
  shape,
734
797
  width: visualWidth,
735
798
  height: visualHeight,
736
799
  radius: visualRadius,
737
800
  x: cx,
738
801
  y: cy,
739
- holes: absoluteHoles,
740
- pathData: this.pathData,
802
+ features: cutFeatures,
803
+ pathData,
741
804
  canvasWidth: canvasW,
742
805
  canvasHeight: canvasH,
743
806
  });
744
807
 
745
808
  // 2. Prepare for Export
746
- // Clone the layer to not affect the stage
747
809
  const clonedLayer = await userLayer.clone();
748
810
 
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
-
756
- const clipPath = new Path(pathData, {
811
+ const clipPath = new Path(generatedPathData, {
757
812
  originX: "left",
758
813
  originY: "top",
759
814
  left: 0,
760
815
  top: 0,
761
- absolutePositioned: true, // Important for groups
816
+ absolutePositioned: true,
762
817
  });
763
818
 
764
819
  clonedLayer.clipPath = clipPath;
765
820
 
766
821
  // 3. Calculate Crop Area (The Dieline Bounds)
767
- // We want to export only the area covered by the dieline
768
822
  const bounds = clipPath.getBoundingRect();
769
823
 
770
824
  // 4. Export
771
825
  const dataUrl = clonedLayer.toDataURL({
772
826
  format: "png",
773
- multiplier: 2, // Better quality
827
+ multiplier: 2,
774
828
  left: bounds.left,
775
829
  top: bounds.top,
776
830
  width: bounds.width,