@pooder/kit 5.3.1 → 6.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.
Files changed (65) hide show
  1. package/.test-dist/src/extensions/background.js +475 -131
  2. package/.test-dist/src/extensions/dieline.js +283 -180
  3. package/.test-dist/src/extensions/dielineShape.js +66 -0
  4. package/.test-dist/src/extensions/feature.js +388 -303
  5. package/.test-dist/src/extensions/film.js +133 -74
  6. package/.test-dist/src/extensions/geometry.js +120 -56
  7. package/.test-dist/src/extensions/image.js +296 -212
  8. package/.test-dist/src/extensions/index.js +1 -3
  9. package/.test-dist/src/extensions/maskOps.js +75 -20
  10. package/.test-dist/src/extensions/ruler.js +312 -215
  11. package/.test-dist/src/extensions/sceneLayoutModel.js +9 -3
  12. package/.test-dist/src/extensions/sceneVisibility.js +3 -10
  13. package/.test-dist/src/extensions/tracer.js +229 -58
  14. package/.test-dist/src/extensions/white-ink.js +139 -129
  15. package/.test-dist/src/services/CanvasService.js +888 -126
  16. package/.test-dist/src/services/index.js +1 -0
  17. package/.test-dist/src/services/visibility.js +54 -0
  18. package/.test-dist/tests/run.js +58 -4
  19. package/CHANGELOG.md +12 -0
  20. package/dist/index.d.mts +377 -82
  21. package/dist/index.d.ts +377 -82
  22. package/dist/index.js +3920 -2178
  23. package/dist/index.mjs +3992 -2247
  24. package/package.json +1 -1
  25. package/src/extensions/background.ts +631 -145
  26. package/src/extensions/dieline.ts +280 -187
  27. package/src/extensions/dielineShape.ts +109 -0
  28. package/src/extensions/feature.ts +485 -366
  29. package/src/extensions/film.ts +152 -76
  30. package/src/extensions/geometry.ts +203 -104
  31. package/src/extensions/image.ts +319 -238
  32. package/src/extensions/index.ts +0 -1
  33. package/src/extensions/ruler.ts +481 -268
  34. package/src/extensions/sceneLayoutModel.ts +18 -6
  35. package/src/extensions/white-ink.ts +157 -171
  36. package/src/services/CanvasService.ts +1126 -140
  37. package/src/services/index.ts +1 -0
  38. package/src/services/renderSpec.ts +69 -4
  39. package/src/services/visibility.ts +78 -0
  40. package/tests/run.ts +139 -4
  41. package/.test-dist/src/CanvasService.js +0 -249
  42. package/.test-dist/src/ViewportSystem.js +0 -75
  43. package/.test-dist/src/background.js +0 -203
  44. package/.test-dist/src/bridgeSelection.js +0 -20
  45. package/.test-dist/src/constraints.js +0 -237
  46. package/.test-dist/src/dieline.js +0 -818
  47. package/.test-dist/src/edgeScale.js +0 -12
  48. package/.test-dist/src/feature.js +0 -826
  49. package/.test-dist/src/featureComplete.js +0 -32
  50. package/.test-dist/src/film.js +0 -167
  51. package/.test-dist/src/geometry.js +0 -506
  52. package/.test-dist/src/image.js +0 -1250
  53. package/.test-dist/src/maskOps.js +0 -270
  54. package/.test-dist/src/mirror.js +0 -104
  55. package/.test-dist/src/renderSpec.js +0 -2
  56. package/.test-dist/src/ruler.js +0 -343
  57. package/.test-dist/src/sceneLayout.js +0 -99
  58. package/.test-dist/src/sceneLayoutModel.js +0 -196
  59. package/.test-dist/src/sceneView.js +0 -40
  60. package/.test-dist/src/sceneVisibility.js +0 -42
  61. package/.test-dist/src/size.js +0 -332
  62. package/.test-dist/src/tracer.js +0 -544
  63. package/.test-dist/src/white-ink.js +0 -829
  64. package/.test-dist/src/wrappedOffsets.js +0 -33
  65. package/src/extensions/sceneVisibility.ts +0 -71
@@ -7,13 +7,19 @@ import {
7
7
  ConfigurationService,
8
8
  } from "@pooder/core";
9
9
  import { Canvas as FabricCanvas, Path, Pattern } from "fabric";
10
- import { CanvasService } from "../services";
10
+ import { CanvasService, RenderEffectSpec, RenderObjectSpec } from "../services";
11
11
  import { ImageTracer } from "./tracer";
12
- import { Unit } from "../coordinate";
13
12
  import { parseLengthToMm } from "../units";
13
+ import {
14
+ DEFAULT_DIELINE_SHAPE,
15
+ DEFAULT_DIELINE_SHAPE_STYLE,
16
+ DIELINE_SHAPES,
17
+ normalizeShapeStyle,
18
+ normalizeDielineShape,
19
+ } from "./dielineShape";
20
+ import type { DielineShape, DielineShapeStyle } from "./dielineShape";
14
21
  import {
15
22
  generateDielinePath,
16
- generateMaskPath,
17
23
  generateBleedZonePath,
18
24
  DielineFeature,
19
25
  } from "./geometry";
@@ -24,9 +30,9 @@ import {
24
30
  } from "./sceneLayoutModel";
25
31
 
26
32
  export interface DielineGeometry {
27
- shape: "rect" | "circle" | "ellipse" | "custom";
28
- unit: "mm";
29
- displayUnit: Unit;
33
+ shape: DielineShape;
34
+ shapeStyle: DielineShapeStyle;
35
+ unit: "px";
30
36
  x: number;
31
37
  y: number;
32
38
  width: number;
@@ -49,8 +55,8 @@ export interface LineStyle {
49
55
  }
50
56
 
51
57
  export interface DielineState {
52
- displayUnit: Unit;
53
- shape: "rect" | "circle" | "ellipse" | "custom";
58
+ shape: DielineShape;
59
+ shapeStyle: DielineShapeStyle;
54
60
  width: number;
55
61
  height: number;
56
62
  radius: number;
@@ -59,7 +65,6 @@ export interface DielineState {
59
65
  mainLine: LineStyle;
60
66
  offsetLine: LineStyle;
61
67
  insideColor: string;
62
- outsideColor: string;
63
68
  showBleedLines: boolean;
64
69
  features: DielineFeature[];
65
70
  pathData?: string;
@@ -68,6 +73,7 @@ export interface DielineState {
68
73
  }
69
74
 
70
75
  const IMAGE_OBJECT_LAYER_ID = "image.user";
76
+ const DIELINE_LAYER_ID = "dieline-overlay";
71
77
 
72
78
  export class DielineTool implements Extension {
73
79
  id = "pooder.kit.dieline";
@@ -76,8 +82,8 @@ export class DielineTool implements Extension {
76
82
  };
77
83
 
78
84
  private state: DielineState = {
79
- displayUnit: "mm",
80
- shape: "rect",
85
+ shape: DEFAULT_DIELINE_SHAPE,
86
+ shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
81
87
  width: 500,
82
88
  height: 500,
83
89
  radius: 0,
@@ -96,13 +102,16 @@ export class DielineTool implements Extension {
96
102
  style: "solid",
97
103
  },
98
104
  insideColor: "rgba(0,0,0,0)",
99
- outsideColor: "#ffffff",
100
105
  showBleedLines: true,
101
106
  features: [],
102
107
  };
103
108
 
104
109
  private canvasService?: CanvasService;
105
110
  private context?: ExtensionContext;
111
+ private specs: RenderObjectSpec[] = [];
112
+ private effects: RenderEffectSpec[] = [];
113
+ private renderSeq = 0;
114
+ private renderProducerDisposable?: { dispose: () => void };
106
115
  private onCanvasResized = () => {
107
116
  this.updateDieline();
108
117
  };
@@ -118,7 +127,15 @@ export class DielineTool implements Extension {
118
127
  Object.assign(this.state.offsetLine, options.offsetLine);
119
128
  delete options.offsetLine;
120
129
  }
130
+ if (options.shapeStyle) {
131
+ this.state.shapeStyle = normalizeShapeStyle(
132
+ options.shapeStyle,
133
+ this.state.shapeStyle,
134
+ );
135
+ delete options.shapeStyle;
136
+ }
121
137
  Object.assign(this.state, options);
138
+ this.state.shape = normalizeDielineShape(options.shape, this.state.shape);
122
139
  }
123
140
  }
124
141
 
@@ -129,6 +146,30 @@ export class DielineTool implements Extension {
129
146
  console.warn("CanvasService not found for DielineTool");
130
147
  return;
131
148
  }
149
+ this.renderProducerDisposable?.dispose();
150
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
151
+ this.id,
152
+ () => ({
153
+ passes: [
154
+ {
155
+ id: DIELINE_LAYER_ID,
156
+ stack: 700,
157
+ order: 0,
158
+ replace: true,
159
+ visibility: {
160
+ op: "not",
161
+ expr: {
162
+ op: "activeToolIn",
163
+ ids: ["pooder.kit.image", "pooder.kit.white-ink"],
164
+ },
165
+ },
166
+ effects: this.effects,
167
+ objects: this.specs,
168
+ },
169
+ ],
170
+ }),
171
+ { priority: 250 },
172
+ );
132
173
 
133
174
  const configService = context.services.get<ConfigurationService>(
134
175
  "ConfigurationService",
@@ -137,8 +178,14 @@ export class DielineTool implements Extension {
137
178
  // Load initial config
138
179
  const s = this.state;
139
180
  const sizeState = readSizeState(configService);
140
- s.displayUnit = sizeState.unit;
141
- s.shape = configService.get("dieline.shape", s.shape);
181
+ s.shape = normalizeDielineShape(
182
+ configService.get("dieline.shape", s.shape),
183
+ s.shape,
184
+ );
185
+ s.shapeStyle = normalizeShapeStyle(
186
+ configService.get("dieline.shapeStyle", s.shapeStyle),
187
+ s.shapeStyle,
188
+ );
142
189
  s.width = sizeState.actualWidthMm;
143
190
  s.height = sizeState.actualHeightMm;
144
191
  s.radius = parseLengthToMm(
@@ -187,10 +234,6 @@ export class DielineTool implements Extension {
187
234
  );
188
235
 
189
236
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
190
- s.outsideColor = configService.get(
191
- "dieline.outsideColor",
192
- s.outsideColor,
193
- );
194
237
  s.showBleedLines = configService.get(
195
238
  "dieline.showBleedLines",
196
239
  s.showBleedLines,
@@ -216,7 +259,6 @@ export class DielineTool implements Extension {
216
259
  configService.onAnyChange((e: { key: string; value: any }) => {
217
260
  if (e.key.startsWith("size.")) {
218
261
  const nextSize = readSizeState(configService);
219
- s.displayUnit = nextSize.unit;
220
262
  s.width = nextSize.actualWidthMm;
221
263
  s.height = nextSize.actualHeightMm;
222
264
  s.padding = nextSize.viewPadding;
@@ -233,7 +275,10 @@ export class DielineTool implements Extension {
233
275
  if (e.key.startsWith("dieline.")) {
234
276
  switch (e.key) {
235
277
  case "dieline.shape":
236
- s.shape = e.value;
278
+ s.shape = normalizeDielineShape(e.value, s.shape);
279
+ break;
280
+ case "dieline.shapeStyle":
281
+ s.shapeStyle = normalizeShapeStyle(e.value, s.shapeStyle);
237
282
  break;
238
283
  case "dieline.radius":
239
284
  s.radius = parseLengthToMm(e.value, "mm");
@@ -268,9 +313,6 @@ export class DielineTool implements Extension {
268
313
  case "dieline.insideColor":
269
314
  s.insideColor = e.value;
270
315
  break;
271
- case "dieline.outsideColor":
272
- s.outsideColor = e.value;
273
- break;
274
316
  case "dieline.showBleedLines":
275
317
  s.showBleedLines = e.value;
276
318
  break;
@@ -299,13 +341,19 @@ export class DielineTool implements Extension {
299
341
  }
300
342
 
301
343
  context.eventBus.on("canvas:resized", this.onCanvasResized);
302
- this.createLayer();
303
344
  this.updateDieline();
304
345
  }
305
346
 
306
347
  deactivate(context: ExtensionContext) {
307
348
  context.eventBus.off("canvas:resized", this.onCanvasResized);
308
- this.destroyLayer();
349
+ this.renderSeq += 1;
350
+ this.specs = [];
351
+ this.effects = [];
352
+ this.renderProducerDisposable?.dispose();
353
+ this.renderProducerDisposable = undefined;
354
+ if (this.canvasService) {
355
+ void this.canvasService.flushRenderFromProducers();
356
+ }
309
357
  this.canvasService = undefined;
310
358
  this.context = undefined;
311
359
  }
@@ -329,7 +377,7 @@ export class DielineTool implements Extension {
329
377
  id: "dieline.shape",
330
378
  type: "select",
331
379
  label: "Shape",
332
- options: ["rect", "circle", "ellipse", "custom"],
380
+ options: Array.from(DIELINE_SHAPES),
333
381
  default: s.shape,
334
382
  },
335
383
  {
@@ -340,6 +388,12 @@ export class DielineTool implements Extension {
340
388
  max: 500,
341
389
  default: s.radius,
342
390
  },
391
+ {
392
+ id: "dieline.shapeStyle",
393
+ type: "json",
394
+ label: "Shape Style",
395
+ default: s.shapeStyle,
396
+ },
343
397
  {
344
398
  id: "dieline.showBleedLines",
345
399
  type: "boolean",
@@ -412,12 +466,6 @@ export class DielineTool implements Extension {
412
466
  label: "Inside Color",
413
467
  default: s.insideColor,
414
468
  },
415
- {
416
- id: "dieline.outsideColor",
417
- type: "color",
418
- label: "Outside Color",
419
- default: s.outsideColor,
420
- },
421
469
  {
422
470
  id: "dieline.features",
423
471
  type: "json",
@@ -531,42 +579,6 @@ export class DielineTool implements Extension {
531
579
  };
532
580
  }
533
581
 
534
- private getLayer() {
535
- return this.canvasService?.getLayer("dieline-overlay");
536
- }
537
-
538
- private createLayer() {
539
- if (!this.canvasService) return;
540
- const width = this.canvasService.canvas.width || 800;
541
- const height = this.canvasService.canvas.height || 600;
542
-
543
- const layer = this.canvasService.createLayer("dieline-overlay", {
544
- width,
545
- height,
546
- selectable: false,
547
- evented: false,
548
- });
549
-
550
- this.canvasService.canvas.bringObjectToFront(layer);
551
-
552
- // Ensure above user layer
553
- const userLayer = this.canvasService.getLayer("user");
554
- if (userLayer) {
555
- const userIndex = this.canvasService.canvas
556
- .getObjects()
557
- .indexOf(userLayer);
558
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
559
- }
560
- }
561
-
562
- private destroyLayer() {
563
- if (!this.canvasService) return;
564
- const layer = this.getLayer();
565
- if (layer) {
566
- this.canvasService.canvas.remove(layer);
567
- }
568
- }
569
-
570
582
  private createHatchPattern(color: string = "rgba(0, 0, 0, 0.3)") {
571
583
  if (typeof document === "undefined") {
572
584
  return undefined;
@@ -598,9 +610,15 @@ export class DielineTool implements Extension {
598
610
  );
599
611
  }
600
612
 
613
+ private hasImageItems(): boolean {
614
+ const configService = this.getConfigService();
615
+ if (!configService) return false;
616
+ const items = configService.get("image.items", []) as unknown;
617
+ return Array.isArray(items) && items.length > 0;
618
+ }
619
+
601
620
  private syncSizeState(configService: ConfigurationService) {
602
621
  const sizeState = readSizeState(configService);
603
- this.state.displayUnit = sizeState.unit;
604
622
  this.state.width = sizeState.actualWidthMm;
605
623
  this.state.height = sizeState.actualHeightMm;
606
624
  this.state.padding = sizeState.viewPadding;
@@ -609,47 +627,28 @@ export class DielineTool implements Extension {
609
627
  ? sizeState.cutMarginMm
610
628
  : sizeState.cutMode === "inset"
611
629
  ? -sizeState.cutMarginMm
612
- : 0;
613
- }
614
-
615
- private bringFeatureMarkersToFront() {
616
- if (!this.canvasService) return;
617
- const canvas = this.canvasService.canvas;
618
- canvas
619
- .getObjects()
620
- .filter((obj: any) => obj?.data?.type === "feature-marker")
621
- .forEach((obj: any) => canvas.bringObjectToFront(obj));
630
+ : 0;
622
631
  }
623
632
 
624
- public updateDieline(_emitEvent: boolean = true) {
625
- if (!this.canvasService) return;
626
- const layer = this.getLayer();
627
- if (!layer) return;
628
- const configService = this.getConfigService();
629
- if (!configService) return;
630
-
631
- this.syncSizeState(configService);
632
- const sceneLayout = computeSceneLayout(
633
- this.canvasService,
634
- readSizeState(configService),
635
- );
636
- if (!sceneLayout) return;
637
-
633
+ private buildDielineSpecs(
634
+ sceneLayout: NonNullable<ReturnType<typeof computeSceneLayout>>,
635
+ ): RenderObjectSpec[] {
638
636
  const {
639
637
  shape,
638
+ shapeStyle,
640
639
  radius,
641
640
  mainLine,
642
641
  offsetLine,
643
642
  insideColor,
644
- outsideColor,
645
643
  showBleedLines,
646
644
  features,
647
645
  } = this.state;
646
+ const hasImages = this.hasImageItems();
648
647
 
649
648
  const canvasW =
650
- sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
649
+ sceneLayout.canvasWidth || this.canvasService?.canvas.width || 800;
651
650
  const canvasH =
652
- sceneLayout.canvasHeight || this.canvasService.canvas.height || 600;
651
+ sceneLayout.canvasHeight || this.canvasService?.canvas.height || 600;
653
652
  const scale = sceneLayout.scale;
654
653
  const cx = sceneLayout.trimRect.centerX;
655
654
  const cy = sceneLayout.trimRect.centerY;
@@ -663,8 +662,6 @@ export class DielineTool implements Extension {
663
662
  const cutR =
664
663
  visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
665
664
 
666
- layer.remove(...layer.getObjects());
667
-
668
665
  const absoluteFeatures = (features || []).map((f) => ({
669
666
  ...f,
670
667
  x: f.x,
@@ -675,36 +672,13 @@ export class DielineTool implements Extension {
675
672
  }));
676
673
  const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
677
674
 
678
- const maskPathData = generateMaskPath({
679
- canvasWidth: canvasW,
680
- canvasHeight: canvasH,
681
- shape,
682
- width: cutW,
683
- height: cutH,
684
- radius: cutR,
685
- x: cx,
686
- y: cy,
687
- features: cutFeatures,
688
- pathData: this.state.pathData,
689
- customSourceWidthPx: this.state.customSourceWidthPx,
690
- customSourceHeightPx: this.state.customSourceHeightPx,
691
- });
692
- const mask = new Path(maskPathData, {
693
- fill: outsideColor,
694
- stroke: null,
695
- selectable: false,
696
- evented: false,
697
- originX: "left" as const,
698
- originY: "top" as const,
699
- left: 0,
700
- top: 0,
701
- });
702
- layer.add(mask);
675
+ const specs: RenderObjectSpec[] = [];
703
676
 
704
677
  if (
705
678
  insideColor &&
706
679
  insideColor !== "transparent" &&
707
- insideColor !== "rgba(0,0,0,0)"
680
+ insideColor !== "rgba(0,0,0,0)" &&
681
+ !hasImages
708
682
  ) {
709
683
  const productPathData = generateDielinePath({
710
684
  shape,
@@ -714,6 +688,7 @@ export class DielineTool implements Extension {
714
688
  x: cx,
715
689
  y: cy,
716
690
  features: cutFeatures,
691
+ shapeStyle,
717
692
  pathData: this.state.pathData,
718
693
  customSourceWidthPx: this.state.customSourceWidthPx,
719
694
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -721,15 +696,21 @@ export class DielineTool implements Extension {
721
696
  canvasHeight: canvasH,
722
697
  });
723
698
 
724
- const insideObj = new Path(productPathData, {
725
- fill: insideColor,
726
- stroke: null,
727
- selectable: false,
728
- evented: false,
729
- originX: "left",
730
- originY: "top",
699
+ specs.push({
700
+ id: "dieline.inside",
701
+ type: "path",
702
+ space: "screen",
703
+ data: { id: "dieline.inside", type: "dieline" },
704
+ props: {
705
+ pathData: productPathData,
706
+ fill: insideColor,
707
+ stroke: null,
708
+ selectable: false,
709
+ evented: false,
710
+ originX: "left",
711
+ originY: "top",
712
+ },
731
713
  });
732
- layer.add(insideObj);
733
714
  }
734
715
 
735
716
  if (Math.abs(visualOffset) > 0.0001) {
@@ -742,6 +723,7 @@ export class DielineTool implements Extension {
742
723
  x: cx,
743
724
  y: cy,
744
725
  features: cutFeatures,
726
+ shapeStyle,
745
727
  pathData: this.state.pathData,
746
728
  customSourceWidthPx: this.state.customSourceWidthPx,
747
729
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -756,6 +738,7 @@ export class DielineTool implements Extension {
756
738
  x: cx,
757
739
  y: cy,
758
740
  features: cutFeatures,
741
+ shapeStyle,
759
742
  pathData: this.state.pathData,
760
743
  customSourceWidthPx: this.state.customSourceWidthPx,
761
744
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -768,16 +751,22 @@ export class DielineTool implements Extension {
768
751
  if (showBleedLines !== false) {
769
752
  const pattern = this.createHatchPattern(mainLine.color);
770
753
  if (pattern) {
771
- const bleedObj = new Path(bleedPathData, {
772
- fill: pattern,
773
- stroke: null,
774
- selectable: false,
775
- evented: false,
776
- objectCaching: false,
777
- originX: "left",
778
- originY: "top",
754
+ specs.push({
755
+ id: "dieline.bleed-zone",
756
+ type: "path",
757
+ space: "screen",
758
+ data: { id: "dieline.bleed-zone", type: "dieline" },
759
+ props: {
760
+ pathData: bleedPathData,
761
+ fill: pattern,
762
+ stroke: null,
763
+ selectable: false,
764
+ evented: false,
765
+ objectCaching: false,
766
+ originX: "left",
767
+ originY: "top",
768
+ },
779
769
  });
780
- layer.add(bleedObj);
781
770
  }
782
771
  }
783
772
 
@@ -789,6 +778,7 @@ export class DielineTool implements Extension {
789
778
  x: cx,
790
779
  y: cy,
791
780
  features: cutFeatures,
781
+ shapeStyle,
792
782
  pathData: this.state.pathData,
793
783
  customSourceWidthPx: this.state.customSourceWidthPx,
794
784
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -796,20 +786,26 @@ export class DielineTool implements Extension {
796
786
  canvasHeight: canvasH,
797
787
  });
798
788
 
799
- const offsetBorderObj = new Path(offsetPathData, {
800
- fill: null,
801
- stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
802
- strokeWidth: offsetLine.width,
803
- strokeDashArray:
804
- offsetLine.style === "dashed"
805
- ? [offsetLine.dashLength, offsetLine.dashLength]
806
- : undefined,
807
- selectable: false,
808
- evented: false,
809
- originX: "left",
810
- originY: "top",
789
+ specs.push({
790
+ id: "dieline.offset-border",
791
+ type: "path",
792
+ space: "screen",
793
+ data: { id: "dieline.offset-border", type: "dieline" },
794
+ props: {
795
+ pathData: offsetPathData,
796
+ fill: null,
797
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
798
+ strokeWidth: offsetLine.width,
799
+ strokeDashArray:
800
+ offsetLine.style === "dashed"
801
+ ? [offsetLine.dashLength, offsetLine.dashLength]
802
+ : undefined,
803
+ selectable: false,
804
+ evented: false,
805
+ originX: "left",
806
+ originY: "top",
807
+ },
811
808
  });
812
- layer.add(offsetBorderObj);
813
809
  }
814
810
 
815
811
  const borderPathData = generateDielinePath({
@@ -820,49 +816,145 @@ export class DielineTool implements Extension {
820
816
  x: cx,
821
817
  y: cy,
822
818
  features: absoluteFeatures,
819
+ shapeStyle,
823
820
  pathData: this.state.pathData,
824
821
  customSourceWidthPx: this.state.customSourceWidthPx,
825
822
  customSourceHeightPx: this.state.customSourceHeightPx,
826
823
  canvasWidth: canvasW,
827
824
  canvasHeight: canvasH,
828
825
  });
829
- const borderObj = new Path(borderPathData, {
830
- fill: "transparent",
831
- stroke: mainLine.style === "hidden" ? null : mainLine.color,
832
- strokeWidth: mainLine.width,
833
- strokeDashArray:
834
- mainLine.style === "dashed"
835
- ? [mainLine.dashLength, mainLine.dashLength]
836
- : undefined,
837
- selectable: false,
838
- evented: false,
839
- originX: "left",
840
- originY: "top",
826
+
827
+ specs.push({
828
+ id: "dieline.border",
829
+ type: "path",
830
+ space: "screen",
831
+ data: { id: "dieline.border", type: "dieline" },
832
+ props: {
833
+ pathData: borderPathData,
834
+ fill: "transparent",
835
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
836
+ strokeWidth: mainLine.width,
837
+ strokeDashArray:
838
+ mainLine.style === "dashed"
839
+ ? [mainLine.dashLength, mainLine.dashLength]
840
+ : undefined,
841
+ selectable: false,
842
+ evented: false,
843
+ originX: "left",
844
+ originY: "top",
845
+ },
841
846
  });
842
- layer.add(borderObj);
843
-
844
- const userLayer = this.canvasService.getLayer("user");
845
- if (layer && userLayer) {
846
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
847
- const userIndex = this.canvasService.canvas
848
- .getObjects()
849
- .indexOf(userLayer);
850
- if (layerIndex < userIndex) {
851
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
852
- }
853
- } else {
854
- this.canvasService.canvas.bringObjectToFront(layer);
855
- }
856
847
 
857
- // Feature tool markers can extend outside trim. Keep them above dieline mask.
858
- this.bringFeatureMarkersToFront();
848
+ return specs;
849
+ }
850
+
851
+ private buildImageClipEffects(
852
+ sceneLayout: NonNullable<ReturnType<typeof computeSceneLayout>>,
853
+ ): RenderEffectSpec[] {
854
+ const { shape, shapeStyle, radius, features } = this.state;
855
+
856
+ const canvasW =
857
+ sceneLayout.canvasWidth || this.canvasService?.canvas.width || 800;
858
+ const canvasH =
859
+ sceneLayout.canvasHeight || this.canvasService?.canvas.height || 600;
860
+ const scale = sceneLayout.scale;
861
+ const cx = sceneLayout.trimRect.centerX;
862
+ const cy = sceneLayout.trimRect.centerY;
863
+
864
+ const visualWidth = sceneLayout.trimRect.width;
865
+ const visualRadius = radius * scale;
866
+ const cutW = sceneLayout.cutRect.width;
867
+ const cutH = sceneLayout.cutRect.height;
868
+ const visualOffset = (cutW - visualWidth) / 2;
869
+ const cutR =
870
+ visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
859
871
 
860
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
861
- if (rulerLayer) {
862
- this.canvasService.canvas.bringObjectToFront(rulerLayer);
872
+ const absoluteFeatures = (features || []).map((f) => ({
873
+ ...f,
874
+ x: f.x,
875
+ y: f.y,
876
+ width: (f.width || 0) * scale,
877
+ height: (f.height || 0) * scale,
878
+ radius: (f.radius || 0) * scale,
879
+ }));
880
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
881
+
882
+ const clipPathData = generateDielinePath({
883
+ shape,
884
+ width: cutW,
885
+ height: cutH,
886
+ radius: cutR,
887
+ x: cx,
888
+ y: cy,
889
+ features: cutFeatures,
890
+ shapeStyle,
891
+ pathData: this.state.pathData,
892
+ customSourceWidthPx: this.state.customSourceWidthPx,
893
+ customSourceHeightPx: this.state.customSourceHeightPx,
894
+ canvasWidth: canvasW,
895
+ canvasHeight: canvasH,
896
+ });
897
+ if (!clipPathData) return [];
898
+
899
+ return [
900
+ {
901
+ type: "clipPath",
902
+ id: "dieline.clip.image",
903
+ targetPassIds: [IMAGE_OBJECT_LAYER_ID],
904
+ source: {
905
+ id: "dieline.effect.clip-path",
906
+ type: "path",
907
+ space: "screen",
908
+ data: {
909
+ id: "dieline.effect.clip-path",
910
+ type: "dieline-effect",
911
+ effect: "clipPath",
912
+ },
913
+ props: {
914
+ pathData: clipPathData,
915
+ fill: "#000000",
916
+ stroke: null,
917
+ originX: "left",
918
+ originY: "top",
919
+ selectable: false,
920
+ evented: false,
921
+ excludeFromExport: true,
922
+ },
923
+ },
924
+ },
925
+ ];
926
+ }
927
+
928
+ public updateDieline(_emitEvent: boolean = true) {
929
+ void this.updateDielineAsync();
930
+ }
931
+
932
+ private async updateDielineAsync() {
933
+ if (!this.canvasService) return;
934
+ const configService = this.getConfigService();
935
+ if (!configService) return;
936
+ const seq = ++this.renderSeq;
937
+
938
+ this.syncSizeState(configService);
939
+ const sceneLayout = computeSceneLayout(
940
+ this.canvasService,
941
+ readSizeState(configService),
942
+ );
943
+ if (!sceneLayout) {
944
+ if (seq !== this.renderSeq) return;
945
+ this.specs = [];
946
+ this.effects = [];
947
+ await this.canvasService.flushRenderFromProducers();
948
+ return;
863
949
  }
864
950
 
865
- layer.dirty = true;
951
+ const nextSpecs = this.buildDielineSpecs(sceneLayout);
952
+ const nextEffects = this.buildImageClipEffects(sceneLayout);
953
+ if (seq !== this.renderSeq) return;
954
+ this.specs = nextSpecs;
955
+ this.effects = nextEffects;
956
+ await this.canvasService.flushRenderFromProducers();
957
+ if (seq !== this.renderSeq) return;
866
958
  this.canvasService.requestRenderAll();
867
959
  }
868
960
 
@@ -914,7 +1006,7 @@ export class DielineTool implements Extension {
914
1006
  return null;
915
1007
  }
916
1008
 
917
- const { shape, radius, features, pathData } = this.state;
1009
+ const { shape, shapeStyle, radius, features, pathData } = this.state;
918
1010
  const canvasW =
919
1011
  sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
920
1012
  const canvasH =
@@ -947,6 +1039,7 @@ export class DielineTool implements Extension {
947
1039
  x: cx,
948
1040
  y: cy,
949
1041
  features: cutFeatures,
1042
+ shapeStyle,
950
1043
  pathData,
951
1044
  customSourceWidthPx: this.state.customSourceWidthPx,
952
1045
  customSourceHeightPx: this.state.customSourceHeightPx,