@pooder/kit 3.3.0 → 3.5.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
+ EdgeFeature,
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: EdgeFeature[];
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,51 +497,38 @@ 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
+ const originalFeatures = absoluteFeatures.filter(
516
+ (f) => !f.target || f.target === "original" || f.target === "both",
517
+ );
518
+ const offsetFeatures = absoluteFeatures.filter(
519
+ (f) => f.target === "offset" || f.target === "both",
520
+ );
521
+
448
522
  // 1. Draw Mask (Outside)
449
523
  const cutW = Math.max(0, visualWidth + visualOffset * 2);
450
524
  const cutH = Math.max(0, visualHeight + visualOffset * 2);
451
525
  const cutR =
452
526
  visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
453
527
 
528
+ // If no bleed offset, mask should match the original dieline (including its features)
529
+ // If bleed offset exists (positive or negative), mask matches bleed line (which only includes offset features)
530
+ const maskFeatures = visualOffset !== 0 ? offsetFeatures : originalFeatures;
531
+
454
532
  // Use Paper.js to generate the complex mask path
455
533
  const maskPathData = generateMaskPath({
456
534
  canvasWidth: canvasW,
@@ -461,8 +539,8 @@ export class DielineTool implements Extension {
461
539
  radius: cutR,
462
540
  x: cx,
463
541
  y: cy,
464
- holes: absoluteHoles,
465
- pathData: this.pathData,
542
+ features: maskFeatures,
543
+ pathData: this.state.pathData,
466
544
  });
467
545
 
468
546
  const mask = new Path(maskPathData, {
@@ -477,13 +555,13 @@ export class DielineTool implements Extension {
477
555
  });
478
556
  layer.add(mask);
479
557
 
480
- // 2. Draw Inside Fill (Dieline Shape itself, merged with holes if needed)
558
+ // 2. Draw Inside Fill (Dieline Shape itself, merged with features if needed)
481
559
  if (
482
560
  insideColor &&
483
561
  insideColor !== "transparent" &&
484
562
  insideColor !== "rgba(0,0,0,0)"
485
563
  ) {
486
- // Generate path for the product shape (Paper) = Dieline - Holes
564
+ // Generate path for the product shape (Paper) = Dieline +/- Features
487
565
  const productPathData = generateDielinePath({
488
566
  shape,
489
567
  width: cutW,
@@ -491,8 +569,8 @@ export class DielineTool implements Extension {
491
569
  radius: cutR,
492
570
  x: cx,
493
571
  y: cy,
494
- holes: absoluteHoles,
495
- pathData: this.pathData,
572
+ features: maskFeatures, // Use same features as mask for consistency
573
+ pathData: this.state.pathData,
496
574
  canvasWidth: canvasW,
497
575
  canvasHeight: canvasH,
498
576
  });
@@ -518,8 +596,20 @@ export class DielineTool implements Extension {
518
596
  radius: visualRadius,
519
597
  x: cx,
520
598
  y: cy,
521
- holes: absoluteHoles,
522
- pathData: this.pathData,
599
+ features: originalFeatures,
600
+ pathData: this.state.pathData,
601
+ canvasWidth: canvasW,
602
+ canvasHeight: canvasH,
603
+ },
604
+ {
605
+ shape,
606
+ width: cutW,
607
+ height: cutH,
608
+ radius: cutR,
609
+ x: cx,
610
+ y: cy,
611
+ features: offsetFeatures,
612
+ pathData: this.state.pathData,
523
613
  canvasWidth: canvasW,
524
614
  canvasHeight: canvasH,
525
615
  },
@@ -528,7 +618,7 @@ export class DielineTool implements Extension {
528
618
 
529
619
  // Use solid red for hatch lines to match dieline, background is transparent
530
620
  if (showBleedLines !== false) {
531
- const pattern = this.createHatchPattern("red");
621
+ const pattern = this.createHatchPattern(mainLine.color);
532
622
  if (pattern) {
533
623
  const bleedObj = new Path(bleedPathData, {
534
624
  fill: pattern,
@@ -551,17 +641,20 @@ export class DielineTool implements Extension {
551
641
  radius: cutR,
552
642
  x: cx,
553
643
  y: cy,
554
- holes: absoluteHoles,
555
- pathData: this.pathData,
644
+ features: offsetFeatures,
645
+ pathData: this.state.pathData,
556
646
  canvasWidth: canvasW,
557
647
  canvasHeight: canvasH,
558
648
  });
559
649
 
560
650
  const offsetBorderObj = new Path(offsetPathData, {
561
651
  fill: null,
562
- stroke: "#666", // Grey
563
- strokeWidth: 1,
564
- strokeDashArray: [4, 4], // Dashed
652
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
653
+ strokeWidth: offsetLine.width,
654
+ strokeDashArray:
655
+ offsetLine.style === "dashed"
656
+ ? [offsetLine.dashLength, offsetLine.dashLength]
657
+ : undefined,
565
658
  selectable: false,
566
659
  evented: false,
567
660
  originX: "left",
@@ -571,9 +664,6 @@ export class DielineTool implements Extension {
571
664
  }
572
665
 
573
666
  // 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
667
  const borderPathData = generateDielinePath({
578
668
  shape,
579
669
  width: visualWidth,
@@ -581,17 +671,18 @@ export class DielineTool implements Extension {
581
671
  radius: visualRadius,
582
672
  x: cx,
583
673
  y: cy,
584
- holes: absoluteHoles,
585
- pathData: this.pathData,
674
+ features: originalFeatures,
675
+ pathData: this.state.pathData,
586
676
  canvasWidth: canvasW,
587
677
  canvasHeight: canvasH,
588
678
  });
589
679
 
590
680
  const borderObj = new Path(borderPathData, {
591
681
  fill: "transparent",
592
- stroke: "red",
593
- strokeWidth: 1,
594
- strokeDashArray: style === "dashed" ? [5, 5] : undefined,
682
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
683
+ strokeWidth: mainLine.width,
684
+ strokeDashArray:
685
+ mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : undefined,
595
686
  selectable: false,
596
687
  evented: false,
597
688
  originX: "left",
@@ -624,16 +715,8 @@ export class DielineTool implements Extension {
624
715
  layer.dirty = true;
625
716
  this.canvasService.requestRenderAll();
626
717
 
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)
718
+ // Emit change event so other tools (like FeatureTool) can react
629
719
  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
720
  const geometry = this.getGeometry();
638
721
  if (geometry) {
639
722
  this.context.eventBus.emit("dieline:geometry:change", geometry);
@@ -643,7 +726,7 @@ export class DielineTool implements Extension {
643
726
 
644
727
  public getGeometry(): DielineGeometry | null {
645
728
  if (!this.canvasService) return null;
646
- const { unit, shape, width, height, radius, position, offset } = this;
729
+ const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
647
730
  const canvasW = this.canvasService.canvas.width || 800;
648
731
  const canvasH = this.canvasService.canvas.height || 600;
649
732
 
@@ -670,9 +753,10 @@ export class DielineTool implements Extension {
670
753
  height: visualHeight,
671
754
  radius: radius * scale,
672
755
  offset: offset * scale,
673
- // Pass scale to help other tools (like HoleTool) convert units
756
+ // Pass scale to help other tools (like FeatureTool) convert units
674
757
  scale,
675
- pathData: this.pathData,
758
+ strokeWidth: mainLine.width,
759
+ pathData,
676
760
  } as DielineGeometry;
677
761
  }
678
762
 
@@ -680,13 +764,10 @@ export class DielineTool implements Extension {
680
764
  if (!this.canvasService) return null;
681
765
  const userLayer = this.canvasService.getLayer("user");
682
766
 
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
767
  if (!userLayer) return null;
687
768
 
688
769
  // 1. Generate Path Data
689
- const { shape, width, height, radius, position, holes } = this;
770
+ const { shape, width, height, radius, features, unit, pathData } = this.state;
690
771
  const canvasW = this.canvasService.canvas.width || 800;
691
772
  const canvasH = this.canvasService.canvas.height || 600;
692
773
 
@@ -703,74 +784,57 @@ export class DielineTool implements Extension {
703
784
  const visualHeight = layout.height;
704
785
  const visualRadius = radius * scale;
705
786
 
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
- );
787
+ // Scale Features
788
+ const absoluteFeatures = (features || []).map((f) => {
789
+ const featureScale = scale;
720
790
 
721
791
  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,
792
+ ...f,
793
+ x: f.x,
794
+ y: f.y,
795
+ width: (f.width || 0) * featureScale,
796
+ height: (f.height || 0) * featureScale,
797
+ radius: (f.radius || 0) * featureScale,
729
798
  };
730
799
  });
731
800
 
732
- const pathData = generateDielinePath({
801
+ const originalFeatures = absoluteFeatures.filter(
802
+ (f) => !f.target || f.target === "original" || f.target === "both",
803
+ );
804
+
805
+ const generatedPathData = generateDielinePath({
733
806
  shape,
734
807
  width: visualWidth,
735
808
  height: visualHeight,
736
809
  radius: visualRadius,
737
810
  x: cx,
738
811
  y: cy,
739
- holes: absoluteHoles,
740
- pathData: this.pathData,
812
+ features: originalFeatures,
813
+ pathData,
741
814
  canvasWidth: canvasW,
742
815
  canvasHeight: canvasH,
743
816
  });
744
817
 
745
818
  // 2. Prepare for Export
746
- // Clone the layer to not affect the stage
747
819
  const clonedLayer = await userLayer.clone();
748
820
 
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, {
821
+ const clipPath = new Path(generatedPathData, {
757
822
  originX: "left",
758
823
  originY: "top",
759
824
  left: 0,
760
825
  top: 0,
761
- absolutePositioned: true, // Important for groups
826
+ absolutePositioned: true,
762
827
  });
763
828
 
764
829
  clonedLayer.clipPath = clipPath;
765
830
 
766
831
  // 3. Calculate Crop Area (The Dieline Bounds)
767
- // We want to export only the area covered by the dieline
768
832
  const bounds = clipPath.getBoundingRect();
769
833
 
770
834
  // 4. Export
771
835
  const dataUrl = clonedLayer.toDataURL({
772
836
  format: "png",
773
- multiplier: 2, // Better quality
837
+ multiplier: 2,
774
838
  left: bounds.left,
775
839
  top: bounds.top,
776
840
  width: bounds.width,