@pooder/kit 6.1.2 → 6.2.1

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 (30) hide show
  1. package/.test-dist/src/extensions/background/BackgroundTool.js +177 -5
  2. package/.test-dist/src/extensions/constraintUtils.js +44 -0
  3. package/.test-dist/src/extensions/dieline/DielineTool.js +52 -409
  4. package/.test-dist/src/extensions/dieline/featureResolution.js +29 -0
  5. package/.test-dist/src/extensions/dieline/model.js +83 -0
  6. package/.test-dist/src/extensions/dieline/renderBuilder.js +227 -0
  7. package/.test-dist/src/extensions/feature/FeatureTool.js +156 -45
  8. package/.test-dist/src/extensions/featureCoordinates.js +21 -0
  9. package/.test-dist/src/extensions/featurePlacement.js +46 -0
  10. package/.test-dist/src/extensions/image/ImageTool.js +281 -25
  11. package/.test-dist/src/extensions/ruler/RulerTool.js +24 -1
  12. package/.test-dist/src/shared/constants/layers.js +3 -1
  13. package/.test-dist/tests/run.js +25 -0
  14. package/CHANGELOG.md +12 -0
  15. package/dist/index.d.mts +47 -13
  16. package/dist/index.d.ts +47 -13
  17. package/dist/index.js +1325 -977
  18. package/dist/index.mjs +1311 -966
  19. package/package.json +1 -1
  20. package/src/extensions/background/BackgroundTool.ts +264 -4
  21. package/src/extensions/dieline/DielineTool.ts +67 -548
  22. package/src/extensions/dieline/model.ts +165 -1
  23. package/src/extensions/dieline/renderBuilder.ts +301 -0
  24. package/src/extensions/feature/FeatureTool.ts +190 -47
  25. package/src/extensions/featureCoordinates.ts +35 -0
  26. package/src/extensions/featurePlacement.ts +118 -0
  27. package/src/extensions/image/ImageTool.ts +139 -157
  28. package/src/extensions/ruler/RulerTool.ts +24 -2
  29. package/src/shared/constants/layers.ts +2 -0
  30. package/tests/run.ts +37 -0
@@ -6,19 +6,8 @@ import {
6
6
  } from "@pooder/core";
7
7
  import { Canvas as FabricCanvas, Path, Pattern } from "fabric";
8
8
  import { CanvasService, RenderEffectSpec, RenderObjectSpec } from "../../services";
9
- import { parseLengthToMm } from "../../units";
10
- import {
11
- DEFAULT_DIELINE_SHAPE,
12
- DEFAULT_DIELINE_SHAPE_STYLE,
13
- normalizeShapeStyle,
14
- normalizeDielineShape,
15
- } from "../dielineShape";
16
- import type { DielineShape, DielineShapeStyle } from "../dielineShape";
17
- import {
18
- generateDielinePath,
19
- generateBleedZonePath,
20
- DielineFeature,
21
- } from "../geometry";
9
+ import { generateDielinePath } from "../geometry";
10
+ import { normalizeShapeStyle, normalizeDielineShape } from "../dielineShape";
22
11
  import {
23
12
  buildSceneGeometry,
24
13
  computeSceneLayout,
@@ -30,49 +19,17 @@ import {
30
19
  } from "../../shared/constants/layers";
31
20
  import { createDielineCommands } from "./commands";
32
21
  import { createDielineConfigurations } from "./config";
33
-
34
- export interface DielineGeometry {
35
- shape: DielineShape;
36
- shapeStyle: DielineShapeStyle;
37
- unit: "px";
38
- x: number;
39
- y: number;
40
- width: number;
41
- height: number;
42
- radius: number;
43
- offset: number;
44
- borderLength?: number;
45
- scale?: number;
46
- strokeWidth?: number;
47
- pathData?: string;
48
- customSourceWidthPx?: number;
49
- customSourceHeightPx?: number;
50
- }
51
-
52
- export interface LineStyle {
53
- width: number;
54
- color: string;
55
- dashLength: number;
56
- style: "solid" | "dashed" | "hidden";
57
- }
58
-
59
- export interface DielineState {
60
- shape: DielineShape;
61
- shapeStyle: DielineShapeStyle;
62
- width: number;
63
- height: number;
64
- radius: number;
65
- offset: number;
66
- padding: number | string;
67
- mainLine: LineStyle;
68
- offsetLine: LineStyle;
69
- insideColor: string;
70
- showBleedLines: boolean;
71
- features: DielineFeature[];
72
- pathData?: string;
73
- customSourceWidthPx?: number;
74
- customSourceHeightPx?: number;
75
- }
22
+ import {
23
+ createDefaultDielineState,
24
+ DielineGeometry,
25
+ DielineState,
26
+ readDielineState,
27
+ } from "./model";
28
+ import { buildDielineRenderBundle } from "./renderBuilder";
29
+ import {
30
+ projectPlacedFeatures,
31
+ resolveFeaturePlacements,
32
+ } from "../featurePlacement";
76
33
 
77
34
  export class DielineTool implements Extension {
78
35
  id = "pooder.kit.dieline";
@@ -80,30 +37,7 @@ export class DielineTool implements Extension {
80
37
  name: "DielineTool",
81
38
  };
82
39
 
83
- private state: DielineState = {
84
- shape: DEFAULT_DIELINE_SHAPE,
85
- shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
86
- width: 500,
87
- height: 500,
88
- radius: 0,
89
- offset: 0,
90
- padding: 140,
91
- mainLine: {
92
- width: 2.7,
93
- color: "#FF0000",
94
- dashLength: 5,
95
- style: "solid",
96
- },
97
- offsetLine: {
98
- width: 2.7,
99
- color: "#FF0000",
100
- dashLength: 5,
101
- style: "solid",
102
- },
103
- insideColor: "rgba(0,0,0,0)",
104
- showBleedLines: true,
105
- features: [],
106
- };
40
+ private state: DielineState = createDefaultDielineState();
107
41
 
108
42
  private canvasService?: CanvasService;
109
43
  private context?: ExtensionContext;
@@ -174,166 +108,12 @@ export class DielineTool implements Extension {
174
108
  "ConfigurationService",
175
109
  );
176
110
  if (configService) {
177
- // Load initial config
178
- const s = this.state;
179
- const sizeState = readSizeState(configService);
180
- s.shape = normalizeDielineShape(
181
- configService.get("dieline.shape", s.shape),
182
- s.shape,
183
- );
184
- s.shapeStyle = normalizeShapeStyle(
185
- configService.get("dieline.shapeStyle", s.shapeStyle),
186
- s.shapeStyle,
187
- );
188
- s.width = sizeState.actualWidthMm;
189
- s.height = sizeState.actualHeightMm;
190
- s.radius = parseLengthToMm(
191
- configService.get("dieline.radius", s.radius),
192
- "mm",
193
- );
194
- s.padding = sizeState.viewPadding;
195
- s.offset =
196
- sizeState.cutMode === "outset"
197
- ? sizeState.cutMarginMm
198
- : sizeState.cutMode === "inset"
199
- ? -sizeState.cutMarginMm
200
- : 0;
201
-
202
- // Main Line
203
- s.mainLine.width = configService.get(
204
- "dieline.strokeWidth",
205
- s.mainLine.width,
206
- );
207
- s.mainLine.color = configService.get(
208
- "dieline.strokeColor",
209
- s.mainLine.color,
210
- );
211
- s.mainLine.dashLength = configService.get(
212
- "dieline.dashLength",
213
- s.mainLine.dashLength,
214
- );
215
- s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
216
-
217
- // Offset Line
218
- s.offsetLine.width = configService.get(
219
- "dieline.offsetStrokeWidth",
220
- s.offsetLine.width,
221
- );
222
- s.offsetLine.color = configService.get(
223
- "dieline.offsetStrokeColor",
224
- s.offsetLine.color,
225
- );
226
- s.offsetLine.dashLength = configService.get(
227
- "dieline.offsetDashLength",
228
- s.offsetLine.dashLength,
229
- );
230
- s.offsetLine.style = configService.get(
231
- "dieline.offsetStyle",
232
- s.offsetLine.style,
233
- );
234
-
235
- s.insideColor = configService.get("dieline.insideColor", s.insideColor);
236
- s.showBleedLines = configService.get(
237
- "dieline.showBleedLines",
238
- s.showBleedLines,
239
- );
240
- s.features = configService.get("dieline.features", s.features);
241
- s.pathData = configService.get("dieline.pathData", s.pathData);
242
- const sourceWidth = Number(
243
- configService.get("dieline.customSourceWidthPx", 0),
244
- );
245
- const sourceHeight = Number(
246
- configService.get("dieline.customSourceHeightPx", 0),
247
- );
248
- s.customSourceWidthPx =
249
- Number.isFinite(sourceWidth) && sourceWidth > 0
250
- ? sourceWidth
251
- : undefined;
252
- s.customSourceHeightPx =
253
- Number.isFinite(sourceHeight) && sourceHeight > 0
254
- ? sourceHeight
255
- : undefined;
111
+ Object.assign(this.state, readDielineState(configService, this.state));
256
112
 
257
113
  // Listen for changes
258
114
  configService.onAnyChange((e: { key: string; value: any }) => {
259
- if (e.key.startsWith("size.")) {
260
- const nextSize = readSizeState(configService);
261
- s.width = nextSize.actualWidthMm;
262
- s.height = nextSize.actualHeightMm;
263
- s.padding = nextSize.viewPadding;
264
- s.offset =
265
- nextSize.cutMode === "outset"
266
- ? nextSize.cutMarginMm
267
- : nextSize.cutMode === "inset"
268
- ? -nextSize.cutMarginMm
269
- : 0;
270
- this.updateDieline();
271
- return;
272
- }
273
-
274
- if (e.key.startsWith("dieline.")) {
275
- switch (e.key) {
276
- case "dieline.shape":
277
- s.shape = normalizeDielineShape(e.value, s.shape);
278
- break;
279
- case "dieline.shapeStyle":
280
- s.shapeStyle = normalizeShapeStyle(e.value, s.shapeStyle);
281
- break;
282
- case "dieline.radius":
283
- s.radius = parseLengthToMm(e.value, "mm");
284
- break;
285
-
286
- case "dieline.strokeWidth":
287
- s.mainLine.width = e.value;
288
- break;
289
- case "dieline.strokeColor":
290
- s.mainLine.color = e.value;
291
- break;
292
- case "dieline.dashLength":
293
- s.mainLine.dashLength = e.value;
294
- break;
295
- case "dieline.style":
296
- s.mainLine.style = e.value;
297
- break;
298
-
299
- case "dieline.offsetStrokeWidth":
300
- s.offsetLine.width = e.value;
301
- break;
302
- case "dieline.offsetStrokeColor":
303
- s.offsetLine.color = e.value;
304
- break;
305
- case "dieline.offsetDashLength":
306
- s.offsetLine.dashLength = e.value;
307
- break;
308
- case "dieline.offsetStyle":
309
- s.offsetLine.style = e.value;
310
- break;
311
-
312
- case "dieline.insideColor":
313
- s.insideColor = e.value;
314
- break;
315
- case "dieline.showBleedLines":
316
- s.showBleedLines = e.value;
317
- break;
318
- case "dieline.features":
319
- s.features = e.value;
320
- break;
321
- case "dieline.pathData":
322
- s.pathData = e.value;
323
- break;
324
- case "dieline.customSourceWidthPx":
325
- s.customSourceWidthPx =
326
- Number.isFinite(Number(e.value)) && Number(e.value) > 0
327
- ? Number(e.value)
328
- : undefined;
329
- break;
330
- case "dieline.customSourceHeightPx":
331
- s.customSourceHeightPx =
332
- Number.isFinite(Number(e.value)) && Number(e.value) > 0
333
- ? Number(e.value)
334
- : undefined;
335
- break;
336
- }
115
+ if (e.key.startsWith("size.") || e.key.startsWith("dieline.")) {
116
+ Object.assign(this.state, readDielineState(configService, this.state));
337
117
  this.updateDieline();
338
118
  }
339
119
  });
@@ -413,316 +193,39 @@ export class DielineTool implements Extension {
413
193
  return Array.isArray(items) && items.length > 0;
414
194
  }
415
195
 
416
- private syncSizeState(configService: ConfigurationService) {
417
- const sizeState = readSizeState(configService);
418
- this.state.width = sizeState.actualWidthMm;
419
- this.state.height = sizeState.actualHeightMm;
420
- this.state.padding = sizeState.viewPadding;
421
- this.state.offset =
422
- sizeState.cutMode === "outset"
423
- ? sizeState.cutMarginMm
424
- : sizeState.cutMode === "inset"
425
- ? -sizeState.cutMarginMm
426
- : 0;
427
- }
428
-
429
196
  private buildDielineSpecs(
430
197
  sceneLayout: NonNullable<ReturnType<typeof computeSceneLayout>>,
431
198
  ): RenderObjectSpec[] {
432
- const {
433
- shape,
434
- shapeStyle,
435
- radius,
436
- mainLine,
437
- offsetLine,
438
- insideColor,
439
- showBleedLines,
440
- features,
441
- } = this.state;
442
199
  const hasImages = this.hasImageItems();
443
-
444
- const canvasW =
445
- sceneLayout.canvasWidth || this.canvasService?.canvas.width || 800;
446
- const canvasH =
447
- sceneLayout.canvasHeight || this.canvasService?.canvas.height || 600;
448
- const scale = sceneLayout.scale;
449
- const cx = sceneLayout.trimRect.centerX;
450
- const cy = sceneLayout.trimRect.centerY;
451
-
452
- const visualWidth = sceneLayout.trimRect.width;
453
- const visualHeight = sceneLayout.trimRect.height;
454
- const visualRadius = radius * scale;
455
- const cutW = sceneLayout.cutRect.width;
456
- const cutH = sceneLayout.cutRect.height;
457
- const visualOffset = (cutW - visualWidth) / 2;
458
- const cutR =
459
- visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
460
-
461
- const absoluteFeatures = (features || []).map((f) => ({
462
- ...f,
463
- x: f.x,
464
- y: f.y,
465
- width: (f.width || 0) * scale,
466
- height: (f.height || 0) * scale,
467
- radius: (f.radius || 0) * scale,
468
- }));
469
- const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
470
-
471
- const specs: RenderObjectSpec[] = [];
472
-
473
- if (
474
- insideColor &&
475
- insideColor !== "transparent" &&
476
- insideColor !== "rgba(0,0,0,0)" &&
477
- !hasImages
478
- ) {
479
- const productPathData = generateDielinePath({
480
- shape,
481
- width: cutW,
482
- height: cutH,
483
- radius: cutR,
484
- x: cx,
485
- y: cy,
486
- features: cutFeatures,
487
- shapeStyle,
488
- pathData: this.state.pathData,
489
- customSourceWidthPx: this.state.customSourceWidthPx,
490
- customSourceHeightPx: this.state.customSourceHeightPx,
491
- canvasWidth: canvasW,
492
- canvasHeight: canvasH,
493
- });
494
-
495
- specs.push({
496
- id: "dieline.inside",
497
- type: "path",
498
- space: "screen",
499
- data: { id: "dieline.inside", type: "dieline" },
500
- props: {
501
- pathData: productPathData,
502
- fill: insideColor,
503
- stroke: null,
504
- selectable: false,
505
- evented: false,
506
- originX: "left",
507
- originY: "top",
508
- },
509
- });
510
- }
511
-
512
- if (Math.abs(visualOffset) > 0.0001) {
513
- const bleedPathData = generateBleedZonePath(
514
- {
515
- shape,
516
- width: visualWidth,
517
- height: visualHeight,
518
- radius: visualRadius,
519
- x: cx,
520
- y: cy,
521
- features: cutFeatures,
522
- shapeStyle,
523
- pathData: this.state.pathData,
524
- customSourceWidthPx: this.state.customSourceWidthPx,
525
- customSourceHeightPx: this.state.customSourceHeightPx,
526
- canvasWidth: canvasW,
527
- canvasHeight: canvasH,
528
- },
529
- {
530
- shape,
531
- width: cutW,
532
- height: cutH,
533
- radius: cutR,
534
- x: cx,
535
- y: cy,
536
- features: cutFeatures,
537
- shapeStyle,
538
- pathData: this.state.pathData,
539
- customSourceWidthPx: this.state.customSourceWidthPx,
540
- customSourceHeightPx: this.state.customSourceHeightPx,
541
- canvasWidth: canvasW,
542
- canvasHeight: canvasH,
543
- },
544
- visualOffset,
545
- );
546
-
547
- if (showBleedLines !== false) {
548
- const pattern = this.createHatchPattern(mainLine.color);
549
- if (pattern) {
550
- specs.push({
551
- id: "dieline.bleed-zone",
552
- type: "path",
553
- space: "screen",
554
- data: { id: "dieline.bleed-zone", type: "dieline" },
555
- props: {
556
- pathData: bleedPathData,
557
- fill: pattern,
558
- stroke: null,
559
- selectable: false,
560
- evented: false,
561
- objectCaching: false,
562
- originX: "left",
563
- originY: "top",
564
- },
565
- });
566
- }
567
- }
568
-
569
- const offsetPathData = generateDielinePath({
570
- shape,
571
- width: cutW,
572
- height: cutH,
573
- radius: cutR,
574
- x: cx,
575
- y: cy,
576
- features: cutFeatures,
577
- shapeStyle,
578
- pathData: this.state.pathData,
579
- customSourceWidthPx: this.state.customSourceWidthPx,
580
- customSourceHeightPx: this.state.customSourceHeightPx,
581
- canvasWidth: canvasW,
582
- canvasHeight: canvasH,
583
- });
584
-
585
- specs.push({
586
- id: "dieline.offset-border",
587
- type: "path",
588
- space: "screen",
589
- data: { id: "dieline.offset-border", type: "dieline" },
590
- props: {
591
- pathData: offsetPathData,
592
- fill: null,
593
- stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
594
- strokeWidth: offsetLine.width,
595
- strokeDashArray:
596
- offsetLine.style === "dashed"
597
- ? [offsetLine.dashLength, offsetLine.dashLength]
598
- : undefined,
599
- selectable: false,
600
- evented: false,
601
- originX: "left",
602
- originY: "top",
603
- },
604
- });
605
- }
606
-
607
- const borderPathData = generateDielinePath({
608
- shape,
609
- width: visualWidth,
610
- height: visualHeight,
611
- radius: visualRadius,
612
- x: cx,
613
- y: cy,
614
- features: absoluteFeatures,
615
- shapeStyle,
616
- pathData: this.state.pathData,
617
- customSourceWidthPx: this.state.customSourceWidthPx,
618
- customSourceHeightPx: this.state.customSourceHeightPx,
619
- canvasWidth: canvasW,
620
- canvasHeight: canvasH,
621
- });
622
-
623
- specs.push({
624
- id: "dieline.border",
625
- type: "path",
626
- space: "screen",
627
- data: { id: "dieline.border", type: "dieline" },
628
- props: {
629
- pathData: borderPathData,
630
- fill: "transparent",
631
- stroke: mainLine.style === "hidden" ? null : mainLine.color,
632
- strokeWidth: mainLine.width,
633
- strokeDashArray:
634
- mainLine.style === "dashed"
635
- ? [mainLine.dashLength, mainLine.dashLength]
636
- : undefined,
637
- selectable: false,
638
- evented: false,
639
- originX: "left",
640
- originY: "top",
641
- },
642
- });
643
-
644
- return specs;
200
+ return buildDielineRenderBundle({
201
+ state: this.state,
202
+ sceneLayout,
203
+ canvasWidth: sceneLayout.canvasWidth || this.canvasService?.canvas.width || 800,
204
+ canvasHeight:
205
+ sceneLayout.canvasHeight || this.canvasService?.canvas.height || 600,
206
+ hasImages,
207
+ createHatchPattern: (color) => this.createHatchPattern(color),
208
+ includeImageClipEffect: false,
209
+ }).specs;
645
210
  }
646
211
 
647
212
  private buildImageClipEffects(
648
213
  sceneLayout: NonNullable<ReturnType<typeof computeSceneLayout>>,
649
214
  ): RenderEffectSpec[] {
650
- const { shape, shapeStyle, radius, features } = this.state;
651
-
652
- const canvasW =
653
- sceneLayout.canvasWidth || this.canvasService?.canvas.width || 800;
654
- const canvasH =
655
- sceneLayout.canvasHeight || this.canvasService?.canvas.height || 600;
656
- const scale = sceneLayout.scale;
657
- const cx = sceneLayout.trimRect.centerX;
658
- const cy = sceneLayout.trimRect.centerY;
659
-
660
- const visualWidth = sceneLayout.trimRect.width;
661
- const visualRadius = radius * scale;
662
- const cutW = sceneLayout.cutRect.width;
663
- const cutH = sceneLayout.cutRect.height;
664
- const visualOffset = (cutW - visualWidth) / 2;
665
- const cutR =
666
- visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
667
-
668
- const absoluteFeatures = (features || []).map((f) => ({
669
- ...f,
670
- x: f.x,
671
- y: f.y,
672
- width: (f.width || 0) * scale,
673
- height: (f.height || 0) * scale,
674
- radius: (f.radius || 0) * scale,
675
- }));
676
- const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
677
-
678
- const clipPathData = generateDielinePath({
679
- shape,
680
- width: cutW,
681
- height: cutH,
682
- radius: cutR,
683
- x: cx,
684
- y: cy,
685
- features: cutFeatures,
686
- shapeStyle,
687
- pathData: this.state.pathData,
688
- customSourceWidthPx: this.state.customSourceWidthPx,
689
- customSourceHeightPx: this.state.customSourceHeightPx,
690
- canvasWidth: canvasW,
691
- canvasHeight: canvasH,
692
- });
693
- if (!clipPathData) return [];
694
-
695
- return [
696
- {
697
- type: "clipPath",
698
- id: "dieline.clip.image",
699
- visibility: {
700
- op: "not",
701
- expr: { op: "anySessionActive" },
702
- },
703
- targetPassIds: [IMAGE_OBJECT_LAYER_ID],
704
- source: {
705
- id: "dieline.effect.clip-path",
706
- type: "path",
707
- space: "screen",
708
- data: {
709
- id: "dieline.effect.clip-path",
710
- type: "dieline-effect",
711
- effect: "clipPath",
712
- },
713
- props: {
714
- pathData: clipPathData,
715
- fill: "#000000",
716
- stroke: null,
717
- originX: "left",
718
- originY: "top",
719
- selectable: false,
720
- evented: false,
721
- excludeFromExport: true,
722
- },
723
- },
215
+ return buildDielineRenderBundle({
216
+ state: this.state,
217
+ sceneLayout,
218
+ canvasWidth: sceneLayout.canvasWidth || this.canvasService?.canvas.width || 800,
219
+ canvasHeight:
220
+ sceneLayout.canvasHeight || this.canvasService?.canvas.height || 600,
221
+ hasImages: this.hasImageItems(),
222
+ includeImageClipEffect: true,
223
+ clipTargetPassIds: [IMAGE_OBJECT_LAYER_ID],
224
+ clipVisibility: {
225
+ op: "not",
226
+ expr: { op: "anySessionActive" },
724
227
  },
725
- ];
228
+ }).effects;
726
229
  }
727
230
 
728
231
  public updateDieline(_emitEvent: boolean = true) {
@@ -735,7 +238,7 @@ export class DielineTool implements Extension {
735
238
  if (!configService) return;
736
239
  const seq = ++this.renderSeq;
737
240
 
738
- this.syncSizeState(configService);
241
+ Object.assign(this.state, readDielineState(configService, this.state));
739
242
  const sceneLayout = computeSceneLayout(
740
243
  this.canvasService,
741
244
  readSizeState(configService),
@@ -794,7 +297,7 @@ export class DielineTool implements Extension {
794
297
  return null;
795
298
  }
796
299
 
797
- this.syncSizeState(configService);
300
+ this.state = readDielineState(configService, this.state);
798
301
  const sceneLayout = computeSceneLayout(
799
302
  this.canvasService,
800
303
  readSizeState(configService),
@@ -821,15 +324,31 @@ export class DielineTool implements Extension {
821
324
  const cutR =
822
325
  visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
823
326
 
824
- const absoluteFeatures = (features || []).map((f) => ({
825
- ...f,
826
- x: f.x,
827
- y: f.y,
828
- width: (f.width || 0) * scale,
829
- height: (f.height || 0) * scale,
830
- radius: (f.radius || 0) * scale,
831
- }));
832
- const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
327
+ const placements = resolveFeaturePlacements(features || [], {
328
+ shape,
329
+ shapeStyle,
330
+ pathData,
331
+ customSourceWidthPx: this.state.customSourceWidthPx,
332
+ customSourceHeightPx: this.state.customSourceHeightPx,
333
+ canvasWidth: canvasW,
334
+ canvasHeight: canvasH,
335
+ x: cx,
336
+ y: cy,
337
+ width: sceneLayout.trimRect.width,
338
+ height: sceneLayout.trimRect.height,
339
+ radius: visualRadius,
340
+ scale,
341
+ });
342
+ const cutFeatures = projectPlacedFeatures(
343
+ placements.filter((placement) => !placement.feature.skipCut),
344
+ {
345
+ x: cx,
346
+ y: cy,
347
+ width: cutW,
348
+ height: cutH,
349
+ },
350
+ scale,
351
+ );
833
352
 
834
353
  const generatedPathData = generateDielinePath({
835
354
  shape,