@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
package/dist/index.mjs CHANGED
@@ -400,6 +400,7 @@ var WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
400
400
  var WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
401
401
  var WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
402
402
  var DIELINE_LAYER_ID = "dieline-overlay";
403
+ var FEATURE_DIELINE_LAYER_ID = "feature-dieline-overlay";
403
404
  var FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
404
405
  var RULER_LAYER_ID = "ruler-overlay";
405
406
  var FILM_LAYER_ID = "overlay";
@@ -541,6 +542,18 @@ function normalizeFitMode2(value, fallback) {
541
542
  }
542
543
  return fallback;
543
544
  }
545
+ function normalizeRegionUnit(value, fallback) {
546
+ if (value === "px" || value === "normalized") {
547
+ return value;
548
+ }
549
+ return fallback;
550
+ }
551
+ function normalizeRegistrationFrame(value, fallback) {
552
+ if (value === "trim" || value === "cut" || value === "bleed" || value === "focus" || value === "viewport") {
553
+ return value;
554
+ }
555
+ return fallback;
556
+ }
544
557
  function normalizeAnchor(value, fallback) {
545
558
  if (typeof value !== "string") return fallback;
546
559
  const trimmed = value.trim();
@@ -551,6 +564,63 @@ function normalizeOrder(value, fallback) {
551
564
  if (!Number.isFinite(numeric)) return fallback;
552
565
  return numeric;
553
566
  }
567
+ function normalizeRegionValue(value, fallback) {
568
+ const numeric = Number(value);
569
+ return Number.isFinite(numeric) ? numeric : fallback;
570
+ }
571
+ function normalizeRegistrationRegion(raw, fallback) {
572
+ if (!raw || typeof raw !== "object") {
573
+ return fallback ? { ...fallback } : void 0;
574
+ }
575
+ const input = raw;
576
+ const base = fallback || {
577
+ left: 0,
578
+ top: 0,
579
+ width: 1,
580
+ height: 1,
581
+ unit: "normalized"
582
+ };
583
+ return {
584
+ left: normalizeRegionValue(input.left, base.left),
585
+ top: normalizeRegionValue(input.top, base.top),
586
+ width: normalizeRegionValue(input.width, base.width),
587
+ height: normalizeRegionValue(input.height, base.height),
588
+ unit: normalizeRegionUnit(input.unit, base.unit)
589
+ };
590
+ }
591
+ function normalizeRegistration(raw, fallback) {
592
+ if (!raw || typeof raw !== "object") {
593
+ return fallback ? {
594
+ sourceRegion: fallback.sourceRegion ? { ...fallback.sourceRegion } : void 0,
595
+ targetFrame: fallback.targetFrame,
596
+ fit: fallback.fit
597
+ } : void 0;
598
+ }
599
+ const input = raw;
600
+ const normalized = {
601
+ sourceRegion: normalizeRegistrationRegion(
602
+ input.sourceRegion,
603
+ fallback == null ? void 0 : fallback.sourceRegion
604
+ ),
605
+ targetFrame: normalizeRegistrationFrame(
606
+ input.targetFrame,
607
+ (fallback == null ? void 0 : fallback.targetFrame) || "trim"
608
+ ),
609
+ fit: normalizeFitMode2(input.fit, (fallback == null ? void 0 : fallback.fit) || "stretch")
610
+ };
611
+ if (!normalized.sourceRegion) {
612
+ return void 0;
613
+ }
614
+ return normalized;
615
+ }
616
+ function cloneRegistration(registration) {
617
+ if (!registration) return void 0;
618
+ return {
619
+ sourceRegion: registration.sourceRegion ? { ...registration.sourceRegion } : void 0,
620
+ targetFrame: registration.targetFrame,
621
+ fit: registration.fit
622
+ };
623
+ }
554
624
  function normalizeLayer(raw, index, fallback) {
555
625
  const fallbackLayer = fallback || {
556
626
  id: `layer-${index + 1}`,
@@ -564,7 +634,10 @@ function normalizeLayer(raw, index, fallback) {
564
634
  src: ""
565
635
  };
566
636
  if (!raw || typeof raw !== "object") {
567
- return { ...fallbackLayer };
637
+ return {
638
+ ...fallbackLayer,
639
+ registration: cloneRegistration(fallbackLayer.registration)
640
+ };
568
641
  }
569
642
  const input = raw;
570
643
  const kind = normalizeLayerKind(input.kind, fallbackLayer.kind);
@@ -578,7 +651,8 @@ function normalizeLayer(raw, index, fallback) {
578
651
  enabled: typeof input.enabled === "boolean" ? input.enabled : fallbackLayer.enabled,
579
652
  exportable: typeof input.exportable === "boolean" ? input.exportable : fallbackLayer.exportable,
580
653
  color: kind === "color" ? typeof input.color === "string" ? input.color : typeof fallbackLayer.color === "string" ? fallbackLayer.color : "#ffffff" : void 0,
581
- src: kind === "image" ? typeof input.src === "string" ? input.src.trim() : typeof fallbackLayer.src === "string" ? fallbackLayer.src : "" : void 0
654
+ src: kind === "image" ? typeof input.src === "string" ? input.src.trim() : typeof fallbackLayer.src === "string" ? fallbackLayer.src : "" : void 0,
655
+ registration: kind === "image" ? normalizeRegistration(input.registration, fallbackLayer.registration) : void 0
582
656
  };
583
657
  }
584
658
  function normalizeConfig(raw) {
@@ -608,7 +682,10 @@ function normalizeConfig(raw) {
608
682
  function cloneConfig(config) {
609
683
  return {
610
684
  version: config.version,
611
- layers: (config.layers || []).map((layer) => ({ ...layer }))
685
+ layers: (config.layers || []).map((layer) => ({
686
+ ...layer,
687
+ registration: cloneRegistration(layer.registration)
688
+ }))
612
689
  };
613
690
  }
614
691
  function mergeConfig(base, patch) {
@@ -859,6 +936,41 @@ var BackgroundTool = class {
859
936
  height: layout.trimRect.height
860
937
  };
861
938
  }
939
+ resolveTargetFrameRect(frame) {
940
+ if (frame === "viewport") {
941
+ return this.getViewportRect();
942
+ }
943
+ const layout = this.resolveSceneLayout();
944
+ if (!layout) {
945
+ return frame === "focus" ? this.getViewportRect() : null;
946
+ }
947
+ switch (frame) {
948
+ case "trim":
949
+ case "focus":
950
+ return {
951
+ left: layout.trimRect.left,
952
+ top: layout.trimRect.top,
953
+ width: layout.trimRect.width,
954
+ height: layout.trimRect.height
955
+ };
956
+ case "cut":
957
+ return {
958
+ left: layout.cutRect.left,
959
+ top: layout.cutRect.top,
960
+ width: layout.cutRect.width,
961
+ height: layout.cutRect.height
962
+ };
963
+ case "bleed":
964
+ return {
965
+ left: layout.bleedRect.left,
966
+ top: layout.bleedRect.top,
967
+ width: layout.bleedRect.width,
968
+ height: layout.bleedRect.height
969
+ };
970
+ default:
971
+ return null;
972
+ }
973
+ }
862
974
  resolveAnchorRect(anchor) {
863
975
  if (anchor === "focus") {
864
976
  return this.resolveFocusRect() || this.getViewportRect();
@@ -891,6 +1003,53 @@ var BackgroundTool = class {
891
1003
  scaleY: scale
892
1004
  };
893
1005
  }
1006
+ resolveRegistrationRegion(region, sourceSize) {
1007
+ const sourceWidth = Math.max(1, Number(sourceSize.width || 0));
1008
+ const sourceHeight = Math.max(1, Number(sourceSize.height || 0));
1009
+ const width = region.unit === "normalized" ? region.width * sourceWidth : region.width;
1010
+ const height = region.unit === "normalized" ? region.height * sourceHeight : region.height;
1011
+ const left = region.unit === "normalized" ? region.left * sourceWidth : region.left;
1012
+ const top = region.unit === "normalized" ? region.top * sourceHeight : region.top;
1013
+ if (!Number.isFinite(left) || !Number.isFinite(top) || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
1014
+ return null;
1015
+ }
1016
+ return { left, top, width, height };
1017
+ }
1018
+ resolveRegistrationPlacement(layer, sourceSize) {
1019
+ const registration = layer.registration;
1020
+ if (!(registration == null ? void 0 : registration.sourceRegion)) return null;
1021
+ const targetRect = this.resolveTargetFrameRect(
1022
+ registration.targetFrame || "trim"
1023
+ );
1024
+ if (!targetRect) return null;
1025
+ const sourceRegion = this.resolveRegistrationRegion(
1026
+ registration.sourceRegion,
1027
+ sourceSize
1028
+ );
1029
+ if (!sourceRegion) return null;
1030
+ const fit = registration.fit || "stretch";
1031
+ const baseScaleX = targetRect.width / sourceRegion.width;
1032
+ const baseScaleY = targetRect.height / sourceRegion.height;
1033
+ if (fit === "stretch") {
1034
+ return {
1035
+ left: targetRect.left - sourceRegion.left * baseScaleX,
1036
+ top: targetRect.top - sourceRegion.top * baseScaleY,
1037
+ scaleX: baseScaleX,
1038
+ scaleY: baseScaleY
1039
+ };
1040
+ }
1041
+ const uniformScale = fit === "contain" ? Math.min(baseScaleX, baseScaleY) : Math.max(baseScaleX, baseScaleY);
1042
+ const alignedWidth = sourceRegion.width * uniformScale;
1043
+ const alignedHeight = sourceRegion.height * uniformScale;
1044
+ const offsetLeft = targetRect.left + (targetRect.width - alignedWidth) / 2;
1045
+ const offsetTop = targetRect.top + (targetRect.height - alignedHeight) / 2;
1046
+ return {
1047
+ left: offsetLeft - sourceRegion.left * uniformScale,
1048
+ top: offsetTop - sourceRegion.top * uniformScale,
1049
+ scaleX: uniformScale,
1050
+ scaleY: uniformScale
1051
+ };
1052
+ }
894
1053
  buildColorLayerSpec(layer) {
895
1054
  const rect = this.resolveAnchorRect(layer.anchor);
896
1055
  return {
@@ -924,8 +1083,11 @@ var BackgroundTool = class {
924
1083
  if (!src) return [];
925
1084
  const sourceSize = this.sourceSizeCache.getSourceSize(src);
926
1085
  if (!sourceSize) return [];
927
- const rect = this.resolveAnchorRect(layer.anchor);
928
- const placement = this.resolveImagePlacement(rect, sourceSize, layer.fit);
1086
+ const placement = this.resolveRegistrationPlacement(layer, sourceSize) || this.resolveImagePlacement(
1087
+ this.resolveAnchorRect(layer.anchor),
1088
+ sourceSize,
1089
+ layer.fit
1090
+ );
929
1091
  return [
930
1092
  {
931
1093
  id: `background.layer.${layer.id}.image`,
@@ -1025,7 +1187,6 @@ import {
1025
1187
  Canvas as FabricCanvas,
1026
1188
  Control,
1027
1189
  Image as FabricImage2,
1028
- Path as FabricPath,
1029
1190
  Pattern,
1030
1191
  Point,
1031
1192
  controlsUtils
@@ -2000,8 +2161,6 @@ var IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
2000
2161
  "scale"
2001
2162
  ];
2002
2163
  var IMAGE_MOVE_SNAP_THRESHOLD_PX = 6;
2003
- var IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX = 10;
2004
- var IMAGE_SNAP_GUIDE_LAYER_ID = "image.snapGuide";
2005
2164
  var IMAGE_CONTROL_DESCRIPTORS = [
2006
2165
  {
2007
2166
  key: "tl",
@@ -2048,13 +2207,15 @@ var ImageTool = class {
2048
2207
  this.overlaySpecs = [];
2049
2208
  this.activeSnapX = null;
2050
2209
  this.activeSnapY = null;
2210
+ this.movingImageId = null;
2211
+ this.hasRenderedSnapGuides = false;
2051
2212
  this.subscriptions = new SubscriptionBag();
2052
2213
  this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
2053
2214
  this.onToolActivated = (event) => {
2054
2215
  const before = this.isToolActive;
2055
2216
  this.syncToolActiveFromWorkbench(event.id);
2056
2217
  if (!this.isToolActive) {
2057
- this.clearSnapGuides();
2218
+ this.endMoveSnapInteraction();
2058
2219
  this.setImageFocus(null, {
2059
2220
  syncCanvasSelection: true,
2060
2221
  skipRender: true
@@ -2105,7 +2266,7 @@ var ImageTool = class {
2105
2266
  this.updateImages();
2106
2267
  };
2107
2268
  this.onSelectionCleared = () => {
2108
- this.clearSnapGuides();
2269
+ this.endMoveSnapInteraction();
2109
2270
  this.setImageFocus(null, {
2110
2271
  syncCanvasSelection: false,
2111
2272
  skipRender: true
@@ -2114,7 +2275,8 @@ var ImageTool = class {
2114
2275
  this.updateImages();
2115
2276
  };
2116
2277
  this.onSceneLayoutChanged = () => {
2117
- this.updateSnapGuideVisuals();
2278
+ var _a;
2279
+ (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2118
2280
  this.updateImages();
2119
2281
  };
2120
2282
  this.onSceneGeometryChanged = () => {
@@ -2127,11 +2289,12 @@ var ImageTool = class {
2127
2289
  const id = (_a = target == null ? void 0 : target.data) == null ? void 0 : _a.id;
2128
2290
  const layerId = (_b = target == null ? void 0 : target.data) == null ? void 0 : _b.layerId;
2129
2291
  if (typeof id !== "string" || layerId !== IMAGE_OBJECT_LAYER_ID) return;
2292
+ if (this.movingImageId === id) {
2293
+ this.applyMoveSnapToTarget(target);
2294
+ }
2130
2295
  const frame = this.getFrameRect();
2296
+ this.endMoveSnapInteraction();
2131
2297
  if (!frame.width || !frame.height) return;
2132
- const matches = this.computeMoveSnapMatches(target, frame);
2133
- this.applySnapMatchesToTarget(target, matches);
2134
- this.clearSnapGuides();
2135
2298
  const center = target.getCenterPoint ? target.getCenterPoint() : new Point((_c = target.left) != null ? _c : 0, (_d = target.top) != null ? _d : 0);
2136
2299
  const centerScene = this.canvasService ? this.canvasService.toScenePoint({ x: center.x, y: center.y }) : { x: center.x, y: center.y };
2137
2300
  const objectScale = Number.isFinite(target == null ? void 0 : target.scaleX) ? target.scaleX : 1;
@@ -2272,7 +2435,7 @@ var ImageTool = class {
2272
2435
  this.imageSpecs = [];
2273
2436
  this.overlaySpecs = [];
2274
2437
  this.imageControlsByCapabilityKey.clear();
2275
- this.clearSnapGuides();
2438
+ this.endMoveSnapInteraction();
2276
2439
  this.unbindCanvasInteractionHandlers();
2277
2440
  this.clearRenderedImages();
2278
2441
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
@@ -2285,21 +2448,61 @@ var ImageTool = class {
2285
2448
  }
2286
2449
  bindCanvasInteractionHandlers() {
2287
2450
  if (!this.canvasService || this.canvasObjectMovingHandler) return;
2451
+ this.canvasMouseUpHandler = (e) => {
2452
+ var _a;
2453
+ const target = this.getActiveImageTarget(e == null ? void 0 : e.target);
2454
+ if (target && typeof ((_a = target == null ? void 0 : target.data) == null ? void 0 : _a.id) === "string" && target.data.id === this.movingImageId) {
2455
+ this.applyMoveSnapToTarget(target);
2456
+ }
2457
+ this.endMoveSnapInteraction();
2458
+ };
2288
2459
  this.canvasObjectMovingHandler = (e) => {
2289
2460
  this.handleCanvasObjectMoving(e);
2290
2461
  };
2462
+ this.canvasBeforeRenderHandler = () => {
2463
+ this.handleCanvasBeforeRender();
2464
+ };
2465
+ this.canvasAfterRenderHandler = () => {
2466
+ this.handleCanvasAfterRender();
2467
+ };
2468
+ this.canvasService.canvas.on("mouse:up", this.canvasMouseUpHandler);
2291
2469
  this.canvasService.canvas.on(
2292
2470
  "object:moving",
2293
2471
  this.canvasObjectMovingHandler
2294
2472
  );
2473
+ this.canvasService.canvas.on(
2474
+ "before:render",
2475
+ this.canvasBeforeRenderHandler
2476
+ );
2477
+ this.canvasService.canvas.on("after:render", this.canvasAfterRenderHandler);
2295
2478
  }
2296
2479
  unbindCanvasInteractionHandlers() {
2297
- if (!this.canvasService || !this.canvasObjectMovingHandler) return;
2298
- this.canvasService.canvas.off(
2299
- "object:moving",
2300
- this.canvasObjectMovingHandler
2301
- );
2480
+ if (!this.canvasService) return;
2481
+ if (this.canvasMouseUpHandler) {
2482
+ this.canvasService.canvas.off("mouse:up", this.canvasMouseUpHandler);
2483
+ }
2484
+ if (this.canvasObjectMovingHandler) {
2485
+ this.canvasService.canvas.off(
2486
+ "object:moving",
2487
+ this.canvasObjectMovingHandler
2488
+ );
2489
+ }
2490
+ if (this.canvasBeforeRenderHandler) {
2491
+ this.canvasService.canvas.off(
2492
+ "before:render",
2493
+ this.canvasBeforeRenderHandler
2494
+ );
2495
+ }
2496
+ if (this.canvasAfterRenderHandler) {
2497
+ this.canvasService.canvas.off(
2498
+ "after:render",
2499
+ this.canvasAfterRenderHandler
2500
+ );
2501
+ }
2502
+ this.canvasMouseUpHandler = void 0;
2302
2503
  this.canvasObjectMovingHandler = void 0;
2504
+ this.canvasBeforeRenderHandler = void 0;
2505
+ this.canvasAfterRenderHandler = void 0;
2303
2506
  }
2304
2507
  getActiveImageTarget(target) {
2305
2508
  var _a, _b;
@@ -2328,20 +2531,11 @@ var ImageTool = class {
2328
2531
  if (!this.canvasService) return px;
2329
2532
  return this.canvasService.toSceneLength(px);
2330
2533
  }
2331
- pickSnapMatch(candidates, previous) {
2534
+ pickSnapMatch(candidates) {
2332
2535
  if (!candidates.length) return null;
2333
2536
  const snapThreshold = this.getSnapThresholdScene(
2334
2537
  IMAGE_MOVE_SNAP_THRESHOLD_PX
2335
2538
  );
2336
- const releaseThreshold = this.getSnapThresholdScene(
2337
- IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX
2338
- );
2339
- if (previous) {
2340
- const sticky = candidates.find((candidate) => {
2341
- return candidate.lineId === previous.lineId && Math.abs(candidate.deltaScene) <= releaseThreshold;
2342
- });
2343
- if (sticky) return sticky;
2344
- }
2345
2539
  let best = null;
2346
2540
  candidates.forEach((candidate) => {
2347
2541
  if (Math.abs(candidate.deltaScene) > snapThreshold) return;
@@ -2351,8 +2545,7 @@ var ImageTool = class {
2351
2545
  });
2352
2546
  return best;
2353
2547
  }
2354
- computeMoveSnapMatches(target, frame) {
2355
- const bounds = this.getTargetBoundsScene(target);
2548
+ computeMoveSnapMatches(bounds, frame) {
2356
2549
  if (!bounds || frame.width <= 0 || frame.height <= 0) {
2357
2550
  return { x: null, y: null };
2358
2551
  }
@@ -2403,8 +2596,8 @@ var ImageTool = class {
2403
2596
  }
2404
2597
  ];
2405
2598
  return {
2406
- x: this.pickSnapMatch(xCandidates, this.activeSnapX),
2407
- y: this.pickSnapMatch(yCandidates, this.activeSnapY)
2599
+ x: this.pickSnapMatch(xCandidates),
2600
+ y: this.pickSnapMatch(yCandidates)
2408
2601
  };
2409
2602
  }
2410
2603
  areSnapMatchesEqual(a, b) {
@@ -2413,136 +2606,123 @@ var ImageTool = class {
2413
2606
  return a.lineId === b.lineId && a.axis === b.axis && a.kind === b.kind;
2414
2607
  }
2415
2608
  updateSnapMatchState(nextX, nextY) {
2609
+ var _a;
2416
2610
  const changed = !this.areSnapMatchesEqual(this.activeSnapX, nextX) || !this.areSnapMatchesEqual(this.activeSnapY, nextY);
2417
2611
  this.activeSnapX = nextX;
2418
2612
  this.activeSnapY = nextY;
2419
2613
  if (changed) {
2420
- this.updateSnapGuideVisuals();
2614
+ (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2421
2615
  }
2422
2616
  }
2423
- clearSnapGuides() {
2617
+ clearSnapPreview() {
2424
2618
  var _a;
2425
2619
  this.activeSnapX = null;
2426
2620
  this.activeSnapY = null;
2427
- this.removeSnapGuideObject("x");
2428
- this.removeSnapGuideObject("y");
2621
+ this.hasRenderedSnapGuides = false;
2429
2622
  (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2430
2623
  }
2431
- removeSnapGuideObject(axis) {
2624
+ endMoveSnapInteraction() {
2625
+ this.movingImageId = null;
2626
+ this.clearSnapPreview();
2627
+ }
2628
+ applyMoveSnapToTarget(target) {
2629
+ var _a, _b, _c, _d;
2630
+ if (!this.canvasService) {
2631
+ return { x: null, y: null };
2632
+ }
2633
+ const frame = this.getFrameRect();
2634
+ if (frame.width <= 0 || frame.height <= 0) {
2635
+ return { x: null, y: null };
2636
+ }
2637
+ const bounds = this.getTargetBoundsScene(target);
2638
+ const matches = this.computeMoveSnapMatches(bounds, frame);
2639
+ const deltaScreenX = this.canvasService.toScreenLength(
2640
+ (_b = (_a = matches.x) == null ? void 0 : _a.deltaScene) != null ? _b : 0
2641
+ );
2642
+ const deltaScreenY = this.canvasService.toScreenLength(
2643
+ (_d = (_c = matches.y) == null ? void 0 : _c.deltaScene) != null ? _d : 0
2644
+ );
2645
+ if (deltaScreenX || deltaScreenY) {
2646
+ target.set({
2647
+ left: Number(target.left || 0) + deltaScreenX,
2648
+ top: Number(target.top || 0) + deltaScreenY
2649
+ });
2650
+ target.setCoords();
2651
+ }
2652
+ return matches;
2653
+ }
2654
+ handleCanvasBeforeRender() {
2432
2655
  if (!this.canvasService) return;
2433
- const canvas = this.canvasService.canvas;
2434
- const current = axis === "x" ? this.snapGuideXObject : this.snapGuideYObject;
2435
- if (!current) return;
2436
- canvas.remove(current);
2437
- if (axis === "x") {
2438
- this.snapGuideXObject = void 0;
2656
+ if (!this.hasRenderedSnapGuides && !this.activeSnapX && !this.activeSnapY) {
2439
2657
  return;
2440
2658
  }
2441
- this.snapGuideYObject = void 0;
2659
+ this.canvasService.canvas.clearContext(
2660
+ this.canvasService.canvas.contextTop
2661
+ );
2662
+ this.hasRenderedSnapGuides = false;
2442
2663
  }
2443
- createOrUpdateSnapGuideObject(axis, pathData) {
2664
+ drawSnapGuideLine(from, to) {
2444
2665
  if (!this.canvasService) return;
2445
- const canvas = this.canvasService.canvas;
2666
+ const ctx = this.canvasService.canvas.contextTop;
2667
+ if (!ctx) return;
2446
2668
  const color = this.getConfig("image.control.borderColor", "#1677ff") || "#1677ff";
2447
- const strokeWidth = 1;
2448
- this.removeSnapGuideObject(axis);
2449
- const created = new FabricPath(pathData, {
2450
- originX: "left",
2451
- originY: "top",
2452
- fill: "rgba(0,0,0,0)",
2453
- stroke: color,
2454
- strokeWidth,
2455
- selectable: false,
2456
- evented: false,
2457
- excludeFromExport: true,
2458
- objectCaching: false,
2459
- data: {
2460
- id: `${IMAGE_SNAP_GUIDE_LAYER_ID}.${axis}`,
2461
- layerId: IMAGE_SNAP_GUIDE_LAYER_ID,
2462
- type: "image-snap-guide"
2463
- }
2464
- });
2465
- created.setCoords();
2466
- canvas.add(created);
2467
- canvas.bringObjectToFront(created);
2468
- if (axis === "x") {
2469
- this.snapGuideXObject = created;
2470
- return;
2471
- }
2472
- this.snapGuideYObject = created;
2669
+ ctx.save();
2670
+ ctx.strokeStyle = color;
2671
+ ctx.lineWidth = 1;
2672
+ ctx.beginPath();
2673
+ ctx.moveTo(from.x, from.y);
2674
+ ctx.lineTo(to.x, to.y);
2675
+ ctx.stroke();
2676
+ ctx.restore();
2473
2677
  }
2474
- updateSnapGuideVisuals() {
2678
+ handleCanvasAfterRender() {
2475
2679
  if (!this.canvasService || !this.isImageEditingVisible()) {
2476
- this.removeSnapGuideObject("x");
2477
- this.removeSnapGuideObject("y");
2478
2680
  return;
2479
2681
  }
2480
2682
  const frame = this.getFrameRect();
2481
2683
  if (frame.width <= 0 || frame.height <= 0) {
2482
- this.removeSnapGuideObject("x");
2483
- this.removeSnapGuideObject("y");
2484
2684
  return;
2485
2685
  }
2486
2686
  const frameScreen = this.getFrameRectScreen(frame);
2687
+ let drew = false;
2487
2688
  if (this.activeSnapX) {
2488
2689
  const x = this.canvasService.toScreenPoint({
2489
2690
  x: this.activeSnapX.lineScene,
2490
2691
  y: frame.top
2491
2692
  }).x;
2492
- this.createOrUpdateSnapGuideObject(
2493
- "x",
2494
- `M ${x} ${frameScreen.top} L ${x} ${frameScreen.top + frameScreen.height}`
2693
+ this.drawSnapGuideLine(
2694
+ { x, y: frameScreen.top },
2695
+ { x, y: frameScreen.top + frameScreen.height }
2495
2696
  );
2496
- } else {
2497
- this.removeSnapGuideObject("x");
2697
+ drew = true;
2498
2698
  }
2499
2699
  if (this.activeSnapY) {
2500
2700
  const y = this.canvasService.toScreenPoint({
2501
2701
  x: frame.left,
2502
2702
  y: this.activeSnapY.lineScene
2503
2703
  }).y;
2504
- this.createOrUpdateSnapGuideObject(
2505
- "y",
2506
- `M ${frameScreen.left} ${y} L ${frameScreen.left + frameScreen.width} ${y}`
2704
+ this.drawSnapGuideLine(
2705
+ { x: frameScreen.left, y },
2706
+ { x: frameScreen.left + frameScreen.width, y }
2507
2707
  );
2508
- } else {
2509
- this.removeSnapGuideObject("y");
2708
+ drew = true;
2510
2709
  }
2511
- this.canvasService.requestRenderAll();
2710
+ this.hasRenderedSnapGuides = drew;
2512
2711
  }
2513
2712
  handleCanvasObjectMoving(e) {
2514
- var _a, _b, _c, _d;
2713
+ var _a;
2515
2714
  const target = this.getActiveImageTarget(e == null ? void 0 : e.target);
2516
2715
  if (!target || !this.canvasService) return;
2716
+ this.movingImageId = typeof ((_a = target == null ? void 0 : target.data) == null ? void 0 : _a.id) === "string" ? target.data.id : null;
2517
2717
  const frame = this.getFrameRect();
2518
2718
  if (frame.width <= 0 || frame.height <= 0) {
2519
- this.clearSnapGuides();
2719
+ this.endMoveSnapInteraction();
2520
2720
  return;
2521
2721
  }
2522
- const matches = this.computeMoveSnapMatches(target, frame);
2523
- const deltaX = (_b = (_a = matches.x) == null ? void 0 : _a.deltaScene) != null ? _b : 0;
2524
- const deltaY = (_d = (_c = matches.y) == null ? void 0 : _c.deltaScene) != null ? _d : 0;
2525
- if (deltaX || deltaY) {
2526
- target.set({
2527
- left: Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
2528
- top: Number(target.top || 0) + this.canvasService.toScreenLength(deltaY)
2529
- });
2530
- target.setCoords();
2531
- }
2722
+ const rawBounds = this.getTargetBoundsScene(target);
2723
+ const matches = this.computeMoveSnapMatches(rawBounds, frame);
2532
2724
  this.updateSnapMatchState(matches.x, matches.y);
2533
2725
  }
2534
- applySnapMatchesToTarget(target, matches) {
2535
- var _a, _b, _c, _d;
2536
- if (!this.canvasService || !target) return;
2537
- const deltaX = (_b = (_a = matches.x) == null ? void 0 : _a.deltaScene) != null ? _b : 0;
2538
- const deltaY = (_d = (_c = matches.y) == null ? void 0 : _c.deltaScene) != null ? _d : 0;
2539
- if (!deltaX && !deltaY) return;
2540
- target.set({
2541
- left: Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
2542
- top: Number(target.top || 0) + this.canvasService.toScreenLength(deltaY)
2543
- });
2544
- target.setCoords();
2545
- }
2546
2726
  syncToolActiveFromWorkbench(fallbackId) {
2547
2727
  var _a;
2548
2728
  const wb = (_a = this.context) == null ? void 0 : _a.services.get("WorkbenchService");
@@ -3166,33 +3346,9 @@ var ImageTool = class {
3166
3346
  originY: "top",
3167
3347
  fill: hatchFill,
3168
3348
  opacity: patternFill ? 1 : 0.8,
3169
- stroke: null,
3170
- fillRule: "evenodd",
3171
- selectable: false,
3172
- evented: false,
3173
- excludeFromExport: true,
3174
- objectCaching: false
3175
- }
3176
- },
3177
- {
3178
- id: "image.cropShapePath",
3179
- type: "path",
3180
- data: { id: "image.cropShapePath", zIndex: 6 },
3181
- layout: {
3182
- reference: "custom",
3183
- referenceRect: frameRect,
3184
- alignX: "start",
3185
- alignY: "start",
3186
- offsetX: shapeBounds.x,
3187
- offsetY: shapeBounds.y
3188
- },
3189
- props: {
3190
- pathData: shapePathData,
3191
- originX: "left",
3192
- originY: "top",
3193
- fill: "rgba(0,0,0,0)",
3194
3349
  stroke: "rgba(255, 0, 0, 0.9)",
3195
3350
  strokeWidth: (_b = (_a = this.canvasService) == null ? void 0 : _a.toSceneLength(1)) != null ? _b : 1,
3351
+ fillRule: "evenodd",
3196
3352
  selectable: false,
3197
3353
  evented: false,
3198
3354
  excludeFromExport: true,
@@ -3529,7 +3685,6 @@ var ImageTool = class {
3529
3685
  isImageSelectionActive: this.isImageSelectionActive,
3530
3686
  focusedImageId: this.focusedImageId
3531
3687
  });
3532
- this.updateSnapGuideVisuals();
3533
3688
  this.canvasService.requestRenderAll();
3534
3689
  }
3535
3690
  clampNormalized(value) {
@@ -4330,225 +4485,668 @@ function createDielineConfigurations(state) {
4330
4485
  ];
4331
4486
  }
4332
4487
 
4333
- // src/extensions/dieline/DielineTool.ts
4334
- var DielineTool = class {
4335
- constructor(options) {
4336
- this.id = "pooder.kit.dieline";
4337
- this.metadata = {
4338
- name: "DielineTool"
4339
- };
4340
- this.state = {
4341
- shape: DEFAULT_DIELINE_SHAPE,
4342
- shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
4343
- width: 500,
4344
- height: 500,
4345
- radius: 0,
4346
- offset: 0,
4347
- padding: 140,
4348
- mainLine: {
4349
- width: 2.7,
4350
- color: "#FF0000",
4351
- dashLength: 5,
4352
- style: "solid"
4353
- },
4354
- offsetLine: {
4355
- width: 2.7,
4356
- color: "#FF0000",
4357
- dashLength: 5,
4358
- style: "solid"
4359
- },
4360
- insideColor: "rgba(0,0,0,0)",
4361
- showBleedLines: true,
4362
- features: []
4363
- };
4364
- this.specs = [];
4365
- this.effects = [];
4366
- this.renderSeq = 0;
4367
- this.onCanvasResized = () => {
4368
- this.updateDieline();
4369
- };
4370
- if (options) {
4371
- if (options.mainLine) {
4372
- Object.assign(this.state.mainLine, options.mainLine);
4373
- delete options.mainLine;
4374
- }
4375
- if (options.offsetLine) {
4376
- Object.assign(this.state.offsetLine, options.offsetLine);
4377
- delete options.offsetLine;
4378
- }
4379
- if (options.shapeStyle) {
4380
- this.state.shapeStyle = normalizeShapeStyle(
4381
- options.shapeStyle,
4382
- this.state.shapeStyle
4383
- );
4384
- delete options.shapeStyle;
4385
- }
4386
- Object.assign(this.state, options);
4387
- this.state.shape = normalizeDielineShape(options.shape, this.state.shape);
4488
+ // src/extensions/dieline/model.ts
4489
+ function createDefaultDielineState() {
4490
+ return {
4491
+ shape: DEFAULT_DIELINE_SHAPE,
4492
+ shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
4493
+ width: 500,
4494
+ height: 500,
4495
+ radius: 0,
4496
+ offset: 0,
4497
+ padding: 140,
4498
+ mainLine: {
4499
+ width: 2.7,
4500
+ color: "#FF0000",
4501
+ dashLength: 5,
4502
+ style: "solid"
4503
+ },
4504
+ offsetLine: {
4505
+ width: 2.7,
4506
+ color: "#FF0000",
4507
+ dashLength: 5,
4508
+ style: "solid"
4509
+ },
4510
+ insideColor: "rgba(0,0,0,0)",
4511
+ showBleedLines: true,
4512
+ features: []
4513
+ };
4514
+ }
4515
+ function readDielineState(configService, fallback) {
4516
+ const base = createDefaultDielineState();
4517
+ if (fallback) {
4518
+ Object.assign(base, fallback);
4519
+ if (fallback.mainLine) {
4520
+ base.mainLine = { ...base.mainLine, ...fallback.mainLine };
4388
4521
  }
4389
- }
4390
- activate(context) {
4391
- var _a;
4392
- this.context = context;
4393
- this.canvasService = context.services.get("CanvasService");
4394
- if (!this.canvasService) {
4395
- console.warn("CanvasService not found for DielineTool");
4396
- return;
4522
+ if (fallback.offsetLine) {
4523
+ base.offsetLine = { ...base.offsetLine, ...fallback.offsetLine };
4397
4524
  }
4398
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4399
- this.renderProducerDisposable = this.canvasService.registerRenderProducer(
4400
- this.id,
4401
- () => ({
4402
- passes: [
4403
- {
4404
- id: DIELINE_LAYER_ID,
4405
- stack: 700,
4406
- order: 0,
4407
- replace: true,
4408
- visibility: {
4409
- op: "not",
4410
- expr: {
4411
- op: "activeToolIn",
4412
- ids: ["pooder.kit.image", "pooder.kit.white-ink"]
4413
- }
4414
- },
4415
- effects: this.effects,
4416
- objects: this.specs
4417
- }
4418
- ]
4419
- }),
4420
- { priority: 250 }
4421
- );
4422
- const configService = context.services.get(
4423
- "ConfigurationService"
4424
- );
4425
- if (configService) {
4426
- const s = this.state;
4427
- const sizeState = readSizeState(configService);
4428
- s.shape = normalizeDielineShape(
4429
- configService.get("dieline.shape", s.shape),
4430
- s.shape
4431
- );
4432
- s.shapeStyle = normalizeShapeStyle(
4433
- configService.get("dieline.shapeStyle", s.shapeStyle),
4434
- s.shapeStyle
4435
- );
4436
- s.width = sizeState.actualWidthMm;
4437
- s.height = sizeState.actualHeightMm;
4438
- s.radius = parseLengthToMm(
4439
- configService.get("dieline.radius", s.radius),
4440
- "mm"
4441
- );
4442
- s.padding = sizeState.viewPadding;
4443
- s.offset = sizeState.cutMode === "outset" ? sizeState.cutMarginMm : sizeState.cutMode === "inset" ? -sizeState.cutMarginMm : 0;
4444
- s.mainLine.width = configService.get(
4445
- "dieline.strokeWidth",
4446
- s.mainLine.width
4447
- );
4448
- s.mainLine.color = configService.get(
4449
- "dieline.strokeColor",
4450
- s.mainLine.color
4451
- );
4452
- s.mainLine.dashLength = configService.get(
4525
+ if (fallback.shapeStyle) {
4526
+ base.shapeStyle = normalizeShapeStyle(fallback.shapeStyle, base.shapeStyle);
4527
+ }
4528
+ }
4529
+ const sizeState = readSizeState(configService);
4530
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
4531
+ const sourceHeight = Number(
4532
+ configService.get("dieline.customSourceHeightPx", 0)
4533
+ );
4534
+ return {
4535
+ ...base,
4536
+ shape: normalizeDielineShape(
4537
+ configService.get("dieline.shape", base.shape),
4538
+ base.shape
4539
+ ),
4540
+ shapeStyle: normalizeShapeStyle(
4541
+ configService.get("dieline.shapeStyle", base.shapeStyle),
4542
+ base.shapeStyle
4543
+ ),
4544
+ width: sizeState.actualWidthMm,
4545
+ height: sizeState.actualHeightMm,
4546
+ radius: parseLengthToMm(configService.get("dieline.radius", base.radius), "mm"),
4547
+ padding: sizeState.viewPadding,
4548
+ offset: sizeState.cutMode === "outset" ? sizeState.cutMarginMm : sizeState.cutMode === "inset" ? -sizeState.cutMarginMm : 0,
4549
+ mainLine: {
4550
+ width: configService.get("dieline.strokeWidth", base.mainLine.width),
4551
+ color: configService.get("dieline.strokeColor", base.mainLine.color),
4552
+ dashLength: configService.get(
4453
4553
  "dieline.dashLength",
4454
- s.mainLine.dashLength
4455
- );
4456
- s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
4457
- s.offsetLine.width = configService.get(
4554
+ base.mainLine.dashLength
4555
+ ),
4556
+ style: configService.get("dieline.style", base.mainLine.style)
4557
+ },
4558
+ offsetLine: {
4559
+ width: configService.get(
4458
4560
  "dieline.offsetStrokeWidth",
4459
- s.offsetLine.width
4460
- );
4461
- s.offsetLine.color = configService.get(
4561
+ base.offsetLine.width
4562
+ ),
4563
+ color: configService.get(
4462
4564
  "dieline.offsetStrokeColor",
4463
- s.offsetLine.color
4464
- );
4465
- s.offsetLine.dashLength = configService.get(
4565
+ base.offsetLine.color
4566
+ ),
4567
+ dashLength: configService.get(
4466
4568
  "dieline.offsetDashLength",
4467
- s.offsetLine.dashLength
4468
- );
4469
- s.offsetLine.style = configService.get(
4470
- "dieline.offsetStyle",
4471
- s.offsetLine.style
4472
- );
4473
- s.insideColor = configService.get("dieline.insideColor", s.insideColor);
4474
- s.showBleedLines = configService.get(
4475
- "dieline.showBleedLines",
4476
- s.showBleedLines
4477
- );
4478
- s.features = configService.get("dieline.features", s.features);
4479
- s.pathData = configService.get("dieline.pathData", s.pathData);
4480
- const sourceWidth = Number(
4481
- configService.get("dieline.customSourceWidthPx", 0)
4482
- );
4483
- const sourceHeight = Number(
4484
- configService.get("dieline.customSourceHeightPx", 0)
4485
- );
4486
- s.customSourceWidthPx = Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0;
4487
- s.customSourceHeightPx = Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0;
4488
- configService.onAnyChange((e) => {
4489
- if (e.key.startsWith("size.")) {
4490
- const nextSize = readSizeState(configService);
4491
- s.width = nextSize.actualWidthMm;
4492
- s.height = nextSize.actualHeightMm;
4493
- s.padding = nextSize.viewPadding;
4494
- s.offset = nextSize.cutMode === "outset" ? nextSize.cutMarginMm : nextSize.cutMode === "inset" ? -nextSize.cutMarginMm : 0;
4495
- this.updateDieline();
4496
- return;
4569
+ base.offsetLine.dashLength
4570
+ ),
4571
+ style: configService.get("dieline.offsetStyle", base.offsetLine.style)
4572
+ },
4573
+ insideColor: configService.get("dieline.insideColor", base.insideColor),
4574
+ showBleedLines: configService.get(
4575
+ "dieline.showBleedLines",
4576
+ base.showBleedLines
4577
+ ),
4578
+ features: configService.get("dieline.features", base.features),
4579
+ pathData: configService.get("dieline.pathData", base.pathData),
4580
+ customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
4581
+ customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
4582
+ };
4583
+ }
4584
+
4585
+ // src/extensions/constraints.ts
4586
+ var ConstraintRegistry = class {
4587
+ static register(type, handler) {
4588
+ this.handlers.set(type, handler);
4589
+ }
4590
+ static apply(x, y, feature, context, constraints) {
4591
+ const list = constraints || feature.constraints;
4592
+ if (!list || list.length === 0) {
4593
+ return { x, y };
4594
+ }
4595
+ let currentX = x;
4596
+ let currentY = y;
4597
+ for (const constraint of list) {
4598
+ const handler = this.handlers.get(constraint.type);
4599
+ if (handler) {
4600
+ const result = handler(currentX, currentY, feature, context, constraint.params || {});
4601
+ currentX = result.x;
4602
+ currentY = result.y;
4603
+ }
4604
+ }
4605
+ return { x: currentX, y: currentY };
4606
+ }
4607
+ };
4608
+ ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
4609
+ var pathConstraint = (x, y, feature, context, params) => {
4610
+ const { dielineWidth, dielineHeight, geometry } = context;
4611
+ if (!geometry) return { x, y };
4612
+ const minX = geometry.x - geometry.width / 2;
4613
+ const minY = geometry.y - geometry.height / 2;
4614
+ const absX = minX + x * geometry.width;
4615
+ const absY = minY + y * geometry.height;
4616
+ const nearest = getNearestPointOnDieline(
4617
+ { x: absX, y: absY },
4618
+ geometry
4619
+ );
4620
+ let finalX = nearest.x;
4621
+ let finalY = nearest.y;
4622
+ const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
4623
+ if (hasOffsetParams && nearest.normal) {
4624
+ const dx = absX - nearest.x;
4625
+ const dy = absY - nearest.y;
4626
+ const nx2 = nearest.normal.x;
4627
+ const ny2 = nearest.normal.y;
4628
+ const dist = dx * nx2 + dy * ny2;
4629
+ const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
4630
+ const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
4631
+ const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
4632
+ const minOffset = rawMin * scale;
4633
+ const maxOffset = rawMax * scale;
4634
+ const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
4635
+ finalX = nearest.x + nx2 * clampedDist;
4636
+ finalY = nearest.y + ny2 * clampedDist;
4637
+ }
4638
+ const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
4639
+ const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
4640
+ return { x: nx, y: ny };
4641
+ };
4642
+ var edgeConstraint = (x, y, feature, context, params) => {
4643
+ const { dielineWidth, dielineHeight } = context;
4644
+ const allowedEdges = params.allowedEdges || [
4645
+ "top",
4646
+ "bottom",
4647
+ "left",
4648
+ "right"
4649
+ ];
4650
+ const confine = params.confine || false;
4651
+ const offset = params.offset || 0;
4652
+ const distances = [];
4653
+ if (allowedEdges.includes("top"))
4654
+ distances.push({ edge: "top", dist: y * dielineHeight });
4655
+ if (allowedEdges.includes("bottom"))
4656
+ distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
4657
+ if (allowedEdges.includes("left"))
4658
+ distances.push({ edge: "left", dist: x * dielineWidth });
4659
+ if (allowedEdges.includes("right"))
4660
+ distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
4661
+ if (distances.length === 0) return { x, y };
4662
+ distances.sort((a, b) => a.dist - b.dist);
4663
+ const nearest = distances[0].edge;
4664
+ let newX = x;
4665
+ let newY = y;
4666
+ const fw = feature.width || 0;
4667
+ const fh = feature.height || 0;
4668
+ switch (nearest) {
4669
+ case "top":
4670
+ newY = 0 + offset / dielineHeight;
4671
+ if (confine) {
4672
+ const minX = fw / 2 / dielineWidth;
4673
+ const maxX = 1 - minX;
4674
+ newX = Math.max(minX, Math.min(newX, maxX));
4675
+ }
4676
+ break;
4677
+ case "bottom":
4678
+ newY = 1 - offset / dielineHeight;
4679
+ if (confine) {
4680
+ const minX = fw / 2 / dielineWidth;
4681
+ const maxX = 1 - minX;
4682
+ newX = Math.max(minX, Math.min(newX, maxX));
4683
+ }
4684
+ break;
4685
+ case "left":
4686
+ newX = 0 + offset / dielineWidth;
4687
+ if (confine) {
4688
+ const minY = fh / 2 / dielineHeight;
4689
+ const maxY = 1 - minY;
4690
+ newY = Math.max(minY, Math.min(newY, maxY));
4691
+ }
4692
+ break;
4693
+ case "right":
4694
+ newX = 1 - offset / dielineWidth;
4695
+ if (confine) {
4696
+ const minY = fh / 2 / dielineHeight;
4697
+ const maxY = 1 - minY;
4698
+ newY = Math.max(minY, Math.min(newY, maxY));
4699
+ }
4700
+ break;
4701
+ }
4702
+ return { x: newX, y: newY };
4703
+ };
4704
+ var internalConstraint = (x, y, feature, context, params) => {
4705
+ const { dielineWidth, dielineHeight } = context;
4706
+ const margin = params.margin || 0;
4707
+ const fw = feature.width || 0;
4708
+ const fh = feature.height || 0;
4709
+ const minX = (margin + fw / 2) / dielineWidth;
4710
+ const maxX = 1 - (margin + fw / 2) / dielineWidth;
4711
+ const minY = (margin + fh / 2) / dielineHeight;
4712
+ const maxY = 1 - (margin + fh / 2) / dielineHeight;
4713
+ const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
4714
+ const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
4715
+ return { x: clampedX, y: clampedY };
4716
+ };
4717
+ var tangentBottomConstraint = (x, y, feature, context, params) => {
4718
+ const { dielineWidth, dielineHeight } = context;
4719
+ const gap = params.gap || 0;
4720
+ const confineX = params.confineX !== false;
4721
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
4722
+ const newY = 1 + (extentY + gap) / dielineHeight;
4723
+ let newX = x;
4724
+ if (confineX) {
4725
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
4726
+ const minX = extentX / dielineWidth;
4727
+ const maxX = 1 - extentX / dielineWidth;
4728
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
4729
+ }
4730
+ return { x: newX, y: newY };
4731
+ };
4732
+ var lowestTangentConstraint = (x, y, feature, context, params) => {
4733
+ const { dielineWidth, dielineHeight, geometry } = context;
4734
+ if (!geometry) return { x, y };
4735
+ const lowest = getLowestPointOnDieline(geometry);
4736
+ const minY = geometry.y - geometry.height / 2;
4737
+ const normY = (lowest.y - minY) / geometry.height;
4738
+ const gap = params.gap || 0;
4739
+ const confineX = params.confineX !== false;
4740
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
4741
+ const newY = normY + (extentY + gap) / dielineHeight;
4742
+ let newX = x;
4743
+ if (confineX) {
4744
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
4745
+ const minX = extentX / dielineWidth;
4746
+ const maxX = 1 - extentX / dielineWidth;
4747
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
4748
+ }
4749
+ return { x: newX, y: newY };
4750
+ };
4751
+ ConstraintRegistry.register("path", pathConstraint);
4752
+ ConstraintRegistry.register("edge", edgeConstraint);
4753
+ ConstraintRegistry.register("internal", internalConstraint);
4754
+ ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
4755
+ ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
4756
+
4757
+ // src/extensions/featureCoordinates.ts
4758
+ function resolveFeaturePosition2(feature, geometry) {
4759
+ const { x, y, width, height } = geometry;
4760
+ const left = x - width / 2;
4761
+ const top = y - height / 2;
4762
+ return {
4763
+ x: left + feature.x * width,
4764
+ y: top + feature.y * height
4765
+ };
4766
+ }
4767
+ function normalizePointInGeometry(point, geometry) {
4768
+ const left = geometry.x - geometry.width / 2;
4769
+ const top = geometry.y - geometry.height / 2;
4770
+ return {
4771
+ x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
4772
+ y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5
4773
+ };
4774
+ }
4775
+
4776
+ // src/extensions/featurePlacement.ts
4777
+ function scaleFeatureForRender(feature, scale, x, y) {
4778
+ return {
4779
+ ...feature,
4780
+ x,
4781
+ y,
4782
+ width: feature.width !== void 0 ? feature.width * scale : void 0,
4783
+ height: feature.height !== void 0 ? feature.height * scale : void 0,
4784
+ radius: feature.radius !== void 0 ? feature.radius * scale : void 0
4785
+ };
4786
+ }
4787
+ function resolveFeaturePlacements(features, geometry) {
4788
+ const dielineWidth = geometry.scale > 0 ? geometry.width / geometry.scale : geometry.width;
4789
+ const dielineHeight = geometry.scale > 0 ? geometry.height / geometry.scale : geometry.height;
4790
+ return (features || []).map((feature) => {
4791
+ var _a;
4792
+ const activeConstraints = (_a = feature.constraints) == null ? void 0 : _a.filter(
4793
+ (constraint) => !constraint.validateOnly
4794
+ );
4795
+ const constrained = ConstraintRegistry.apply(
4796
+ feature.x,
4797
+ feature.y,
4798
+ feature,
4799
+ {
4800
+ dielineWidth,
4801
+ dielineHeight,
4802
+ geometry
4803
+ },
4804
+ activeConstraints
4805
+ );
4806
+ const center = resolveFeaturePosition2(
4807
+ {
4808
+ ...feature,
4809
+ x: constrained.x,
4810
+ y: constrained.y
4811
+ },
4812
+ geometry
4813
+ );
4814
+ return {
4815
+ feature,
4816
+ normalizedX: constrained.x,
4817
+ normalizedY: constrained.y,
4818
+ centerX: center.x,
4819
+ centerY: center.y
4820
+ };
4821
+ });
4822
+ }
4823
+ function projectPlacedFeatures(placements, geometry, scale) {
4824
+ return placements.map((placement) => {
4825
+ const normalized = normalizePointInGeometry(
4826
+ { x: placement.centerX, y: placement.centerY },
4827
+ geometry
4828
+ );
4829
+ return scaleFeatureForRender(
4830
+ placement.feature,
4831
+ scale,
4832
+ normalized.x,
4833
+ normalized.y
4834
+ );
4835
+ });
4836
+ }
4837
+
4838
+ // src/extensions/dieline/renderBuilder.ts
4839
+ var DEFAULT_IDS = {
4840
+ inside: "dieline.inside",
4841
+ bleedZone: "dieline.bleed-zone",
4842
+ offsetBorder: "dieline.offset-border",
4843
+ border: "dieline.border",
4844
+ clip: "dieline.clip.image",
4845
+ clipSource: "dieline.effect.clip-path"
4846
+ };
4847
+ function buildDielineRenderBundle(options) {
4848
+ const ids = { ...DEFAULT_IDS, ...options.ids || {} };
4849
+ const {
4850
+ state,
4851
+ sceneLayout,
4852
+ canvasWidth,
4853
+ canvasHeight,
4854
+ hasImages,
4855
+ createHatchPattern,
4856
+ includeImageClipEffect = true,
4857
+ clipTargetPassIds = [IMAGE_OBJECT_LAYER_ID],
4858
+ clipVisibility
4859
+ } = options;
4860
+ const { shape, shapeStyle, radius, mainLine, offsetLine, insideColor } = state;
4861
+ const scale = sceneLayout.scale;
4862
+ const cx = sceneLayout.trimRect.centerX;
4863
+ const cy = sceneLayout.trimRect.centerY;
4864
+ const visualWidth = sceneLayout.trimRect.width;
4865
+ const visualHeight = sceneLayout.trimRect.height;
4866
+ const visualRadius = radius * scale;
4867
+ const cutW = sceneLayout.cutRect.width;
4868
+ const cutH = sceneLayout.cutRect.height;
4869
+ const visualOffset = (cutW - visualWidth) / 2;
4870
+ const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4871
+ const placements = resolveFeaturePlacements(state.features || [], {
4872
+ shape,
4873
+ shapeStyle,
4874
+ pathData: state.pathData,
4875
+ customSourceWidthPx: state.customSourceWidthPx,
4876
+ customSourceHeightPx: state.customSourceHeightPx,
4877
+ canvasWidth,
4878
+ canvasHeight,
4879
+ x: cx,
4880
+ y: cy,
4881
+ width: visualWidth,
4882
+ height: visualHeight,
4883
+ radius: visualRadius,
4884
+ scale
4885
+ });
4886
+ const absoluteFeatures = projectPlacedFeatures(
4887
+ placements,
4888
+ {
4889
+ x: cx,
4890
+ y: cy,
4891
+ width: visualWidth,
4892
+ height: visualHeight
4893
+ },
4894
+ scale
4895
+ );
4896
+ const cutFeatures = projectPlacedFeatures(
4897
+ placements.filter((placement) => !placement.feature.skipCut),
4898
+ {
4899
+ x: cx,
4900
+ y: cy,
4901
+ width: cutW,
4902
+ height: cutH
4903
+ },
4904
+ scale
4905
+ );
4906
+ const common = {
4907
+ shape,
4908
+ shapeStyle,
4909
+ pathData: state.pathData,
4910
+ customSourceWidthPx: state.customSourceWidthPx,
4911
+ customSourceHeightPx: state.customSourceHeightPx,
4912
+ canvasWidth,
4913
+ canvasHeight
4914
+ };
4915
+ const specs = [];
4916
+ if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
4917
+ specs.push({
4918
+ id: ids.inside,
4919
+ type: "path",
4920
+ space: "screen",
4921
+ data: { id: ids.inside, type: "dieline" },
4922
+ props: {
4923
+ pathData: generateDielinePath({
4924
+ ...common,
4925
+ width: cutW,
4926
+ height: cutH,
4927
+ radius: cutR,
4928
+ x: cx,
4929
+ y: cy,
4930
+ features: cutFeatures
4931
+ }),
4932
+ fill: insideColor,
4933
+ stroke: null,
4934
+ selectable: false,
4935
+ evented: false,
4936
+ originX: "left",
4937
+ originY: "top"
4938
+ }
4939
+ });
4940
+ }
4941
+ if (Math.abs(visualOffset) > 1e-4) {
4942
+ const trimPathInput = {
4943
+ ...common,
4944
+ width: visualWidth,
4945
+ height: visualHeight,
4946
+ radius: visualRadius,
4947
+ x: cx,
4948
+ y: cy,
4949
+ features: cutFeatures
4950
+ };
4951
+ const cutPathInput = {
4952
+ ...common,
4953
+ width: cutW,
4954
+ height: cutH,
4955
+ radius: cutR,
4956
+ x: cx,
4957
+ y: cy,
4958
+ features: cutFeatures
4959
+ };
4960
+ if (state.showBleedLines !== false) {
4961
+ const pattern = createHatchPattern == null ? void 0 : createHatchPattern(mainLine.color);
4962
+ if (pattern) {
4963
+ specs.push({
4964
+ id: ids.bleedZone,
4965
+ type: "path",
4966
+ space: "screen",
4967
+ data: { id: ids.bleedZone, type: "dieline" },
4968
+ props: {
4969
+ pathData: generateBleedZonePath(
4970
+ trimPathInput,
4971
+ cutPathInput,
4972
+ visualOffset
4973
+ ),
4974
+ fill: pattern,
4975
+ stroke: null,
4976
+ selectable: false,
4977
+ evented: false,
4978
+ objectCaching: false,
4979
+ originX: "left",
4980
+ originY: "top"
4981
+ }
4982
+ });
4983
+ }
4984
+ }
4985
+ specs.push({
4986
+ id: ids.offsetBorder,
4987
+ type: "path",
4988
+ space: "screen",
4989
+ data: { id: ids.offsetBorder, type: "dieline" },
4990
+ props: {
4991
+ pathData: generateDielinePath(cutPathInput),
4992
+ fill: null,
4993
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
4994
+ strokeWidth: offsetLine.width,
4995
+ strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
4996
+ selectable: false,
4997
+ evented: false,
4998
+ originX: "left",
4999
+ originY: "top"
5000
+ }
5001
+ });
5002
+ }
5003
+ specs.push({
5004
+ id: ids.border,
5005
+ type: "path",
5006
+ space: "screen",
5007
+ data: { id: ids.border, type: "dieline" },
5008
+ props: {
5009
+ pathData: generateDielinePath({
5010
+ ...common,
5011
+ width: visualWidth,
5012
+ height: visualHeight,
5013
+ radius: visualRadius,
5014
+ x: cx,
5015
+ y: cy,
5016
+ features: absoluteFeatures
5017
+ }),
5018
+ fill: "transparent",
5019
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
5020
+ strokeWidth: mainLine.width,
5021
+ strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
5022
+ selectable: false,
5023
+ evented: false,
5024
+ originX: "left",
5025
+ originY: "top"
5026
+ }
5027
+ });
5028
+ if (!includeImageClipEffect) {
5029
+ return { specs, effects: [] };
5030
+ }
5031
+ const clipPathData = generateDielinePath({
5032
+ ...common,
5033
+ width: cutW,
5034
+ height: cutH,
5035
+ radius: cutR,
5036
+ x: cx,
5037
+ y: cy,
5038
+ features: cutFeatures
5039
+ });
5040
+ if (!clipPathData) {
5041
+ return { specs, effects: [] };
5042
+ }
5043
+ return {
5044
+ specs,
5045
+ effects: [
5046
+ {
5047
+ type: "clipPath",
5048
+ id: ids.clip,
5049
+ visibility: clipVisibility,
5050
+ targetPassIds: clipTargetPassIds,
5051
+ source: {
5052
+ id: ids.clipSource,
5053
+ type: "path",
5054
+ space: "screen",
5055
+ data: {
5056
+ id: ids.clipSource,
5057
+ type: "dieline-effect",
5058
+ effect: "clipPath"
5059
+ },
5060
+ props: {
5061
+ pathData: clipPathData,
5062
+ fill: "#000000",
5063
+ stroke: null,
5064
+ originX: "left",
5065
+ originY: "top",
5066
+ selectable: false,
5067
+ evented: false,
5068
+ excludeFromExport: true
5069
+ }
4497
5070
  }
4498
- if (e.key.startsWith("dieline.")) {
4499
- switch (e.key) {
4500
- case "dieline.shape":
4501
- s.shape = normalizeDielineShape(e.value, s.shape);
4502
- break;
4503
- case "dieline.shapeStyle":
4504
- s.shapeStyle = normalizeShapeStyle(e.value, s.shapeStyle);
4505
- break;
4506
- case "dieline.radius":
4507
- s.radius = parseLengthToMm(e.value, "mm");
4508
- break;
4509
- case "dieline.strokeWidth":
4510
- s.mainLine.width = e.value;
4511
- break;
4512
- case "dieline.strokeColor":
4513
- s.mainLine.color = e.value;
4514
- break;
4515
- case "dieline.dashLength":
4516
- s.mainLine.dashLength = e.value;
4517
- break;
4518
- case "dieline.style":
4519
- s.mainLine.style = e.value;
4520
- break;
4521
- case "dieline.offsetStrokeWidth":
4522
- s.offsetLine.width = e.value;
4523
- break;
4524
- case "dieline.offsetStrokeColor":
4525
- s.offsetLine.color = e.value;
4526
- break;
4527
- case "dieline.offsetDashLength":
4528
- s.offsetLine.dashLength = e.value;
4529
- break;
4530
- case "dieline.offsetStyle":
4531
- s.offsetLine.style = e.value;
4532
- break;
4533
- case "dieline.insideColor":
4534
- s.insideColor = e.value;
4535
- break;
4536
- case "dieline.showBleedLines":
4537
- s.showBleedLines = e.value;
4538
- break;
4539
- case "dieline.features":
4540
- s.features = e.value;
4541
- break;
4542
- case "dieline.pathData":
4543
- s.pathData = e.value;
4544
- break;
4545
- case "dieline.customSourceWidthPx":
4546
- s.customSourceWidthPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
4547
- break;
4548
- case "dieline.customSourceHeightPx":
4549
- s.customSourceHeightPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
4550
- break;
5071
+ }
5072
+ ]
5073
+ };
5074
+ }
5075
+
5076
+ // src/extensions/dieline/DielineTool.ts
5077
+ var DielineTool = class {
5078
+ constructor(options) {
5079
+ this.id = "pooder.kit.dieline";
5080
+ this.metadata = {
5081
+ name: "DielineTool"
5082
+ };
5083
+ this.state = createDefaultDielineState();
5084
+ this.specs = [];
5085
+ this.effects = [];
5086
+ this.renderSeq = 0;
5087
+ this.onCanvasResized = () => {
5088
+ this.updateDieline();
5089
+ };
5090
+ if (options) {
5091
+ if (options.mainLine) {
5092
+ Object.assign(this.state.mainLine, options.mainLine);
5093
+ delete options.mainLine;
5094
+ }
5095
+ if (options.offsetLine) {
5096
+ Object.assign(this.state.offsetLine, options.offsetLine);
5097
+ delete options.offsetLine;
5098
+ }
5099
+ if (options.shapeStyle) {
5100
+ this.state.shapeStyle = normalizeShapeStyle(
5101
+ options.shapeStyle,
5102
+ this.state.shapeStyle
5103
+ );
5104
+ delete options.shapeStyle;
5105
+ }
5106
+ Object.assign(this.state, options);
5107
+ this.state.shape = normalizeDielineShape(options.shape, this.state.shape);
5108
+ }
5109
+ }
5110
+ activate(context) {
5111
+ var _a;
5112
+ this.context = context;
5113
+ this.canvasService = context.services.get("CanvasService");
5114
+ if (!this.canvasService) {
5115
+ console.warn("CanvasService not found for DielineTool");
5116
+ return;
5117
+ }
5118
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5119
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5120
+ this.id,
5121
+ () => ({
5122
+ passes: [
5123
+ {
5124
+ id: DIELINE_LAYER_ID,
5125
+ stack: 700,
5126
+ order: 0,
5127
+ replace: true,
5128
+ visibility: {
5129
+ op: "not",
5130
+ expr: {
5131
+ op: "activeToolIn",
5132
+ ids: ["pooder.kit.image", "pooder.kit.white-ink"]
5133
+ }
5134
+ },
5135
+ effects: this.effects,
5136
+ objects: this.specs
4551
5137
  }
5138
+ ]
5139
+ }),
5140
+ { priority: 250 }
5141
+ );
5142
+ const configService = context.services.get(
5143
+ "ConfigurationService"
5144
+ );
5145
+ if (configService) {
5146
+ Object.assign(this.state, readDielineState(configService, this.state));
5147
+ configService.onAnyChange((e) => {
5148
+ if (e.key.startsWith("size.") || e.key.startsWith("dieline.")) {
5149
+ Object.assign(this.state, readDielineState(configService, this.state));
4552
5150
  this.updateDieline();
4553
5151
  }
4554
5152
  });
@@ -4598,293 +5196,55 @@ var DielineTool = class {
4598
5196
  const ctx = canvas.getContext("2d");
4599
5197
  if (ctx) {
4600
5198
  ctx.clearRect(0, 0, size, size);
4601
- ctx.strokeStyle = color;
4602
- ctx.lineWidth = 1;
4603
- ctx.beginPath();
4604
- ctx.moveTo(0, size);
4605
- ctx.lineTo(size, 0);
4606
- ctx.stroke();
4607
- }
4608
- return new Pattern2({ source: canvas, repetition: "repeat" });
4609
- }
4610
- getConfigService() {
4611
- var _a;
4612
- return (_a = this.context) == null ? void 0 : _a.services.get(
4613
- "ConfigurationService"
4614
- );
4615
- }
4616
- hasImageItems() {
4617
- const configService = this.getConfigService();
4618
- if (!configService) return false;
4619
- const items = configService.get("image.items", []);
4620
- return Array.isArray(items) && items.length > 0;
4621
- }
4622
- syncSizeState(configService) {
4623
- const sizeState = readSizeState(configService);
4624
- this.state.width = sizeState.actualWidthMm;
4625
- this.state.height = sizeState.actualHeightMm;
4626
- this.state.padding = sizeState.viewPadding;
4627
- this.state.offset = sizeState.cutMode === "outset" ? sizeState.cutMarginMm : sizeState.cutMode === "inset" ? -sizeState.cutMarginMm : 0;
4628
- }
4629
- buildDielineSpecs(sceneLayout) {
4630
- var _a, _b;
4631
- const {
4632
- shape,
4633
- shapeStyle,
4634
- radius,
4635
- mainLine,
4636
- offsetLine,
4637
- insideColor,
4638
- showBleedLines,
4639
- features
4640
- } = this.state;
4641
- const hasImages = this.hasImageItems();
4642
- const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4643
- const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4644
- const scale = sceneLayout.scale;
4645
- const cx = sceneLayout.trimRect.centerX;
4646
- const cy = sceneLayout.trimRect.centerY;
4647
- const visualWidth = sceneLayout.trimRect.width;
4648
- const visualHeight = sceneLayout.trimRect.height;
4649
- const visualRadius = radius * scale;
4650
- const cutW = sceneLayout.cutRect.width;
4651
- const cutH = sceneLayout.cutRect.height;
4652
- const visualOffset = (cutW - visualWidth) / 2;
4653
- const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4654
- const absoluteFeatures = (features || []).map((f) => ({
4655
- ...f,
4656
- x: f.x,
4657
- y: f.y,
4658
- width: (f.width || 0) * scale,
4659
- height: (f.height || 0) * scale,
4660
- radius: (f.radius || 0) * scale
4661
- }));
4662
- const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
4663
- const specs = [];
4664
- if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
4665
- const productPathData = generateDielinePath({
4666
- shape,
4667
- width: cutW,
4668
- height: cutH,
4669
- radius: cutR,
4670
- x: cx,
4671
- y: cy,
4672
- features: cutFeatures,
4673
- shapeStyle,
4674
- pathData: this.state.pathData,
4675
- customSourceWidthPx: this.state.customSourceWidthPx,
4676
- customSourceHeightPx: this.state.customSourceHeightPx,
4677
- canvasWidth: canvasW,
4678
- canvasHeight: canvasH
4679
- });
4680
- specs.push({
4681
- id: "dieline.inside",
4682
- type: "path",
4683
- space: "screen",
4684
- data: { id: "dieline.inside", type: "dieline" },
4685
- props: {
4686
- pathData: productPathData,
4687
- fill: insideColor,
4688
- stroke: null,
4689
- selectable: false,
4690
- evented: false,
4691
- originX: "left",
4692
- originY: "top"
4693
- }
4694
- });
4695
- }
4696
- if (Math.abs(visualOffset) > 1e-4) {
4697
- const bleedPathData = generateBleedZonePath(
4698
- {
4699
- shape,
4700
- width: visualWidth,
4701
- height: visualHeight,
4702
- radius: visualRadius,
4703
- x: cx,
4704
- y: cy,
4705
- features: cutFeatures,
4706
- shapeStyle,
4707
- pathData: this.state.pathData,
4708
- customSourceWidthPx: this.state.customSourceWidthPx,
4709
- customSourceHeightPx: this.state.customSourceHeightPx,
4710
- canvasWidth: canvasW,
4711
- canvasHeight: canvasH
4712
- },
4713
- {
4714
- shape,
4715
- width: cutW,
4716
- height: cutH,
4717
- radius: cutR,
4718
- x: cx,
4719
- y: cy,
4720
- features: cutFeatures,
4721
- shapeStyle,
4722
- pathData: this.state.pathData,
4723
- customSourceWidthPx: this.state.customSourceWidthPx,
4724
- customSourceHeightPx: this.state.customSourceHeightPx,
4725
- canvasWidth: canvasW,
4726
- canvasHeight: canvasH
4727
- },
4728
- visualOffset
4729
- );
4730
- if (showBleedLines !== false) {
4731
- const pattern = this.createHatchPattern(mainLine.color);
4732
- if (pattern) {
4733
- specs.push({
4734
- id: "dieline.bleed-zone",
4735
- type: "path",
4736
- space: "screen",
4737
- data: { id: "dieline.bleed-zone", type: "dieline" },
4738
- props: {
4739
- pathData: bleedPathData,
4740
- fill: pattern,
4741
- stroke: null,
4742
- selectable: false,
4743
- evented: false,
4744
- objectCaching: false,
4745
- originX: "left",
4746
- originY: "top"
4747
- }
4748
- });
4749
- }
4750
- }
4751
- const offsetPathData = generateDielinePath({
4752
- shape,
4753
- width: cutW,
4754
- height: cutH,
4755
- radius: cutR,
4756
- x: cx,
4757
- y: cy,
4758
- features: cutFeatures,
4759
- shapeStyle,
4760
- pathData: this.state.pathData,
4761
- customSourceWidthPx: this.state.customSourceWidthPx,
4762
- customSourceHeightPx: this.state.customSourceHeightPx,
4763
- canvasWidth: canvasW,
4764
- canvasHeight: canvasH
4765
- });
4766
- specs.push({
4767
- id: "dieline.offset-border",
4768
- type: "path",
4769
- space: "screen",
4770
- data: { id: "dieline.offset-border", type: "dieline" },
4771
- props: {
4772
- pathData: offsetPathData,
4773
- fill: null,
4774
- stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
4775
- strokeWidth: offsetLine.width,
4776
- strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
4777
- selectable: false,
4778
- evented: false,
4779
- originX: "left",
4780
- originY: "top"
4781
- }
4782
- });
4783
- }
4784
- const borderPathData = generateDielinePath({
4785
- shape,
4786
- width: visualWidth,
4787
- height: visualHeight,
4788
- radius: visualRadius,
4789
- x: cx,
4790
- y: cy,
4791
- features: absoluteFeatures,
4792
- shapeStyle,
4793
- pathData: this.state.pathData,
4794
- customSourceWidthPx: this.state.customSourceWidthPx,
4795
- customSourceHeightPx: this.state.customSourceHeightPx,
4796
- canvasWidth: canvasW,
4797
- canvasHeight: canvasH
4798
- });
4799
- specs.push({
4800
- id: "dieline.border",
4801
- type: "path",
4802
- space: "screen",
4803
- data: { id: "dieline.border", type: "dieline" },
4804
- props: {
4805
- pathData: borderPathData,
4806
- fill: "transparent",
4807
- stroke: mainLine.style === "hidden" ? null : mainLine.color,
4808
- strokeWidth: mainLine.width,
4809
- strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
4810
- selectable: false,
4811
- evented: false,
4812
- originX: "left",
4813
- originY: "top"
4814
- }
4815
- });
4816
- return specs;
4817
- }
4818
- buildImageClipEffects(sceneLayout) {
4819
- var _a, _b;
4820
- const { shape, shapeStyle, radius, features } = this.state;
4821
- const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4822
- const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4823
- const scale = sceneLayout.scale;
4824
- const cx = sceneLayout.trimRect.centerX;
4825
- const cy = sceneLayout.trimRect.centerY;
4826
- const visualWidth = sceneLayout.trimRect.width;
4827
- const visualRadius = radius * scale;
4828
- const cutW = sceneLayout.cutRect.width;
4829
- const cutH = sceneLayout.cutRect.height;
4830
- const visualOffset = (cutW - visualWidth) / 2;
4831
- const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4832
- const absoluteFeatures = (features || []).map((f) => ({
4833
- ...f,
4834
- x: f.x,
4835
- y: f.y,
4836
- width: (f.width || 0) * scale,
4837
- height: (f.height || 0) * scale,
4838
- radius: (f.radius || 0) * scale
4839
- }));
4840
- const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
4841
- const clipPathData = generateDielinePath({
4842
- shape,
4843
- width: cutW,
4844
- height: cutH,
4845
- radius: cutR,
4846
- x: cx,
4847
- y: cy,
4848
- features: cutFeatures,
4849
- shapeStyle,
4850
- pathData: this.state.pathData,
4851
- customSourceWidthPx: this.state.customSourceWidthPx,
4852
- customSourceHeightPx: this.state.customSourceHeightPx,
4853
- canvasWidth: canvasW,
4854
- canvasHeight: canvasH
4855
- });
4856
- if (!clipPathData) return [];
4857
- return [
4858
- {
4859
- type: "clipPath",
4860
- id: "dieline.clip.image",
4861
- visibility: {
4862
- op: "not",
4863
- expr: { op: "anySessionActive" }
4864
- },
4865
- targetPassIds: [IMAGE_OBJECT_LAYER_ID],
4866
- source: {
4867
- id: "dieline.effect.clip-path",
4868
- type: "path",
4869
- space: "screen",
4870
- data: {
4871
- id: "dieline.effect.clip-path",
4872
- type: "dieline-effect",
4873
- effect: "clipPath"
4874
- },
4875
- props: {
4876
- pathData: clipPathData,
4877
- fill: "#000000",
4878
- stroke: null,
4879
- originX: "left",
4880
- originY: "top",
4881
- selectable: false,
4882
- evented: false,
4883
- excludeFromExport: true
4884
- }
4885
- }
5199
+ ctx.strokeStyle = color;
5200
+ ctx.lineWidth = 1;
5201
+ ctx.beginPath();
5202
+ ctx.moveTo(0, size);
5203
+ ctx.lineTo(size, 0);
5204
+ ctx.stroke();
5205
+ }
5206
+ return new Pattern2({ source: canvas, repetition: "repeat" });
5207
+ }
5208
+ getConfigService() {
5209
+ var _a;
5210
+ return (_a = this.context) == null ? void 0 : _a.services.get(
5211
+ "ConfigurationService"
5212
+ );
5213
+ }
5214
+ hasImageItems() {
5215
+ const configService = this.getConfigService();
5216
+ if (!configService) return false;
5217
+ const items = configService.get("image.items", []);
5218
+ return Array.isArray(items) && items.length > 0;
5219
+ }
5220
+ buildDielineSpecs(sceneLayout) {
5221
+ var _a, _b;
5222
+ const hasImages = this.hasImageItems();
5223
+ return buildDielineRenderBundle({
5224
+ state: this.state,
5225
+ sceneLayout,
5226
+ canvasWidth: sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800,
5227
+ canvasHeight: sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600,
5228
+ hasImages,
5229
+ createHatchPattern: (color) => this.createHatchPattern(color),
5230
+ includeImageClipEffect: false
5231
+ }).specs;
5232
+ }
5233
+ buildImageClipEffects(sceneLayout) {
5234
+ var _a, _b;
5235
+ return buildDielineRenderBundle({
5236
+ state: this.state,
5237
+ sceneLayout,
5238
+ canvasWidth: sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800,
5239
+ canvasHeight: sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600,
5240
+ hasImages: this.hasImageItems(),
5241
+ includeImageClipEffect: true,
5242
+ clipTargetPassIds: [IMAGE_OBJECT_LAYER_ID],
5243
+ clipVisibility: {
5244
+ op: "not",
5245
+ expr: { op: "anySessionActive" }
4886
5246
  }
4887
- ];
5247
+ }).effects;
4888
5248
  }
4889
5249
  updateDieline(_emitEvent = true) {
4890
5250
  void this.updateDielineAsync();
@@ -4894,7 +5254,7 @@ var DielineTool = class {
4894
5254
  const configService = this.getConfigService();
4895
5255
  if (!configService) return;
4896
5256
  const seq = ++this.renderSeq;
4897
- this.syncSizeState(configService);
5257
+ Object.assign(this.state, readDielineState(configService, this.state));
4898
5258
  const sceneLayout = computeSceneLayout(
4899
5259
  this.canvasService,
4900
5260
  readSizeState(configService)
@@ -4949,7 +5309,7 @@ var DielineTool = class {
4949
5309
  );
4950
5310
  return null;
4951
5311
  }
4952
- this.syncSizeState(configService);
5312
+ this.state = readDielineState(configService, this.state);
4953
5313
  const sceneLayout = computeSceneLayout(
4954
5314
  this.canvasService,
4955
5315
  readSizeState(configService)
@@ -4971,15 +5331,31 @@ var DielineTool = class {
4971
5331
  const visualRadius = radius * scale;
4972
5332
  const visualOffset = (cutW - sceneLayout.trimRect.width) / 2;
4973
5333
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4974
- const absoluteFeatures = (features || []).map((f) => ({
4975
- ...f,
4976
- x: f.x,
4977
- y: f.y,
4978
- width: (f.width || 0) * scale,
4979
- height: (f.height || 0) * scale,
4980
- radius: (f.radius || 0) * scale
4981
- }));
4982
- const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
5334
+ const placements = resolveFeaturePlacements(features || [], {
5335
+ shape,
5336
+ shapeStyle,
5337
+ pathData,
5338
+ customSourceWidthPx: this.state.customSourceWidthPx,
5339
+ customSourceHeightPx: this.state.customSourceHeightPx,
5340
+ canvasWidth: canvasW,
5341
+ canvasHeight: canvasH,
5342
+ x: cx,
5343
+ y: cy,
5344
+ width: sceneLayout.trimRect.width,
5345
+ height: sceneLayout.trimRect.height,
5346
+ radius: visualRadius,
5347
+ scale
5348
+ });
5349
+ const cutFeatures = projectPlacedFeatures(
5350
+ placements.filter((placement) => !placement.feature.skipCut),
5351
+ {
5352
+ x: cx,
5353
+ y: cy,
5354
+ width: cutW,
5355
+ height: cutH
5356
+ },
5357
+ scale
5358
+ );
4983
5359
  const generatedPathData = generateDielinePath({
4984
5360
  shape,
4985
5361
  width: cutW,
@@ -4998,283 +5374,112 @@ var DielineTool = class {
4998
5374
  const clipPath = new Path(generatedPathData, {
4999
5375
  originX: "center",
5000
5376
  originY: "center",
5001
- left: cx,
5002
- top: cy,
5003
- absolutePositioned: true
5004
- });
5005
- const pathOffsetX = Number((_a = clipPath == null ? void 0 : clipPath.pathOffset) == null ? void 0 : _a.x);
5006
- const pathOffsetY = Number((_b = clipPath == null ? void 0 : clipPath.pathOffset) == null ? void 0 : _b.y);
5007
- const centerX = Number.isFinite(pathOffsetX) ? pathOffsetX : cx;
5008
- const centerY = Number.isFinite(pathOffsetY) ? pathOffsetY : cy;
5009
- clipPath.set({
5010
- originX: "center",
5011
- originY: "center",
5012
- left: centerX,
5013
- top: centerY,
5014
- absolutePositioned: true
5015
- });
5016
- clipPath.setCoords();
5017
- const pathBounds = clipPath.getBoundingRect();
5018
- if (!Number.isFinite(pathBounds.left) || !Number.isFinite(pathBounds.top) || !Number.isFinite(pathBounds.width) || !Number.isFinite(pathBounds.height) || pathBounds.width <= 0 || pathBounds.height <= 0) {
5019
- console.warn(
5020
- "[DielineTool] exportCutImage returned null: invalid-cut-bounds",
5021
- {
5022
- bounds: pathBounds
5023
- }
5024
- );
5025
- return null;
5026
- }
5027
- const exportBounds = pathBounds;
5028
- const sourceImages = this.canvasService.canvas.getObjects().filter((obj) => {
5029
- var _a2;
5030
- return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === IMAGE_OBJECT_LAYER_ID;
5031
- });
5032
- if (!sourceImages.length) {
5033
- console.warn(
5034
- "[DielineTool] exportCutImage returned null: no-image-objects-on-canvas"
5035
- );
5036
- return null;
5037
- }
5038
- const sourceCanvasWidth = Number(
5039
- this.canvasService.canvas.width || sceneLayout.canvasWidth || canvasW
5040
- );
5041
- const sourceCanvasHeight = Number(
5042
- this.canvasService.canvas.height || sceneLayout.canvasHeight || canvasH
5043
- );
5044
- const el = document.createElement("canvas");
5045
- const exportCanvas = new FabricCanvas2(el, {
5046
- renderOnAddRemove: false,
5047
- selection: false,
5048
- enableRetinaScaling: false,
5049
- preserveObjectStacking: true
5050
- });
5051
- exportCanvas.setDimensions({
5052
- width: Math.max(1, sourceCanvasWidth),
5053
- height: Math.max(1, sourceCanvasHeight)
5054
- });
5055
- try {
5056
- for (const source of sourceImages) {
5057
- const clone = await source.clone();
5058
- clone.set({
5059
- selectable: false,
5060
- evented: false
5061
- });
5062
- clone.setCoords();
5063
- exportCanvas.add(clone);
5064
- }
5065
- exportCanvas.clipPath = clipPath;
5066
- exportCanvas.renderAll();
5067
- const dataUrl = exportCanvas.toDataURL({
5068
- format: "png",
5069
- multiplier: 2,
5070
- left: exportBounds.left,
5071
- top: exportBounds.top,
5072
- width: exportBounds.width,
5073
- height: exportBounds.height
5074
- });
5075
- if (debug) {
5076
- console.info("[DielineTool] exportCutImage success", {
5077
- sourceCount: sourceImages.length,
5078
- bounds: exportBounds,
5079
- rawPathBounds: pathBounds,
5080
- pathOffset: {
5081
- x: Number.isFinite(pathOffsetX) ? pathOffsetX : null,
5082
- y: Number.isFinite(pathOffsetY) ? pathOffsetY : null
5083
- },
5084
- clipPathCenter: {
5085
- x: centerX,
5086
- y: centerY
5087
- },
5088
- cutRect: sceneLayout.cutRect,
5089
- canvasSize: {
5090
- width: Math.max(1, sourceCanvasWidth),
5091
- height: Math.max(1, sourceCanvasHeight)
5092
- }
5093
- });
5094
- }
5095
- return dataUrl;
5096
- } finally {
5097
- exportCanvas.dispose();
5098
- }
5099
- }
5100
- };
5101
-
5102
- // src/extensions/feature/FeatureTool.ts
5103
- import {
5104
- ContributionPointIds as ContributionPointIds5
5105
- } from "@pooder/core";
5106
-
5107
- // src/extensions/constraints.ts
5108
- var ConstraintRegistry = class {
5109
- static register(type, handler) {
5110
- this.handlers.set(type, handler);
5111
- }
5112
- static apply(x, y, feature, context, constraints) {
5113
- const list = constraints || feature.constraints;
5114
- if (!list || list.length === 0) {
5115
- return { x, y };
5116
- }
5117
- let currentX = x;
5118
- let currentY = y;
5119
- for (const constraint of list) {
5120
- const handler = this.handlers.get(constraint.type);
5121
- if (handler) {
5122
- const result = handler(currentX, currentY, feature, context, constraint.params || {});
5123
- currentX = result.x;
5124
- currentY = result.y;
5125
- }
5126
- }
5127
- return { x: currentX, y: currentY };
5128
- }
5129
- };
5130
- ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
5131
- var pathConstraint = (x, y, feature, context, params) => {
5132
- const { dielineWidth, dielineHeight, geometry } = context;
5133
- if (!geometry) return { x, y };
5134
- const minX = geometry.x - geometry.width / 2;
5135
- const minY = geometry.y - geometry.height / 2;
5136
- const absX = minX + x * geometry.width;
5137
- const absY = minY + y * geometry.height;
5138
- const nearest = getNearestPointOnDieline(
5139
- { x: absX, y: absY },
5140
- geometry
5141
- );
5142
- let finalX = nearest.x;
5143
- let finalY = nearest.y;
5144
- const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
5145
- if (hasOffsetParams && nearest.normal) {
5146
- const dx = absX - nearest.x;
5147
- const dy = absY - nearest.y;
5148
- const nx2 = nearest.normal.x;
5149
- const ny2 = nearest.normal.y;
5150
- const dist = dx * nx2 + dy * ny2;
5151
- const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
5152
- const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
5153
- const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
5154
- const minOffset = rawMin * scale;
5155
- const maxOffset = rawMax * scale;
5156
- const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
5157
- finalX = nearest.x + nx2 * clampedDist;
5158
- finalY = nearest.y + ny2 * clampedDist;
5159
- }
5160
- const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
5161
- const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
5162
- return { x: nx, y: ny };
5163
- };
5164
- var edgeConstraint = (x, y, feature, context, params) => {
5165
- const { dielineWidth, dielineHeight } = context;
5166
- const allowedEdges = params.allowedEdges || [
5167
- "top",
5168
- "bottom",
5169
- "left",
5170
- "right"
5171
- ];
5172
- const confine = params.confine || false;
5173
- const offset = params.offset || 0;
5174
- const distances = [];
5175
- if (allowedEdges.includes("top"))
5176
- distances.push({ edge: "top", dist: y * dielineHeight });
5177
- if (allowedEdges.includes("bottom"))
5178
- distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
5179
- if (allowedEdges.includes("left"))
5180
- distances.push({ edge: "left", dist: x * dielineWidth });
5181
- if (allowedEdges.includes("right"))
5182
- distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
5183
- if (distances.length === 0) return { x, y };
5184
- distances.sort((a, b) => a.dist - b.dist);
5185
- const nearest = distances[0].edge;
5186
- let newX = x;
5187
- let newY = y;
5188
- const fw = feature.width || 0;
5189
- const fh = feature.height || 0;
5190
- switch (nearest) {
5191
- case "top":
5192
- newY = 0 + offset / dielineHeight;
5193
- if (confine) {
5194
- const minX = fw / 2 / dielineWidth;
5195
- const maxX = 1 - minX;
5196
- newX = Math.max(minX, Math.min(newX, maxX));
5197
- }
5198
- break;
5199
- case "bottom":
5200
- newY = 1 - offset / dielineHeight;
5201
- if (confine) {
5202
- const minX = fw / 2 / dielineWidth;
5203
- const maxX = 1 - minX;
5204
- newX = Math.max(minX, Math.min(newX, maxX));
5205
- }
5206
- break;
5207
- case "left":
5208
- newX = 0 + offset / dielineWidth;
5209
- if (confine) {
5210
- const minY = fh / 2 / dielineHeight;
5211
- const maxY = 1 - minY;
5212
- newY = Math.max(minY, Math.min(newY, maxY));
5377
+ left: cx,
5378
+ top: cy,
5379
+ absolutePositioned: true
5380
+ });
5381
+ const pathOffsetX = Number((_a = clipPath == null ? void 0 : clipPath.pathOffset) == null ? void 0 : _a.x);
5382
+ const pathOffsetY = Number((_b = clipPath == null ? void 0 : clipPath.pathOffset) == null ? void 0 : _b.y);
5383
+ const centerX = Number.isFinite(pathOffsetX) ? pathOffsetX : cx;
5384
+ const centerY = Number.isFinite(pathOffsetY) ? pathOffsetY : cy;
5385
+ clipPath.set({
5386
+ originX: "center",
5387
+ originY: "center",
5388
+ left: centerX,
5389
+ top: centerY,
5390
+ absolutePositioned: true
5391
+ });
5392
+ clipPath.setCoords();
5393
+ const pathBounds = clipPath.getBoundingRect();
5394
+ if (!Number.isFinite(pathBounds.left) || !Number.isFinite(pathBounds.top) || !Number.isFinite(pathBounds.width) || !Number.isFinite(pathBounds.height) || pathBounds.width <= 0 || pathBounds.height <= 0) {
5395
+ console.warn(
5396
+ "[DielineTool] exportCutImage returned null: invalid-cut-bounds",
5397
+ {
5398
+ bounds: pathBounds
5399
+ }
5400
+ );
5401
+ return null;
5402
+ }
5403
+ const exportBounds = pathBounds;
5404
+ const sourceImages = this.canvasService.canvas.getObjects().filter((obj) => {
5405
+ var _a2;
5406
+ return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === IMAGE_OBJECT_LAYER_ID;
5407
+ });
5408
+ if (!sourceImages.length) {
5409
+ console.warn(
5410
+ "[DielineTool] exportCutImage returned null: no-image-objects-on-canvas"
5411
+ );
5412
+ return null;
5413
+ }
5414
+ const sourceCanvasWidth = Number(
5415
+ this.canvasService.canvas.width || sceneLayout.canvasWidth || canvasW
5416
+ );
5417
+ const sourceCanvasHeight = Number(
5418
+ this.canvasService.canvas.height || sceneLayout.canvasHeight || canvasH
5419
+ );
5420
+ const el = document.createElement("canvas");
5421
+ const exportCanvas = new FabricCanvas2(el, {
5422
+ renderOnAddRemove: false,
5423
+ selection: false,
5424
+ enableRetinaScaling: false,
5425
+ preserveObjectStacking: true
5426
+ });
5427
+ exportCanvas.setDimensions({
5428
+ width: Math.max(1, sourceCanvasWidth),
5429
+ height: Math.max(1, sourceCanvasHeight)
5430
+ });
5431
+ try {
5432
+ for (const source of sourceImages) {
5433
+ const clone = await source.clone();
5434
+ clone.set({
5435
+ selectable: false,
5436
+ evented: false
5437
+ });
5438
+ clone.setCoords();
5439
+ exportCanvas.add(clone);
5213
5440
  }
5214
- break;
5215
- case "right":
5216
- newX = 1 - offset / dielineWidth;
5217
- if (confine) {
5218
- const minY = fh / 2 / dielineHeight;
5219
- const maxY = 1 - minY;
5220
- newY = Math.max(minY, Math.min(newY, maxY));
5441
+ exportCanvas.clipPath = clipPath;
5442
+ exportCanvas.renderAll();
5443
+ const dataUrl = exportCanvas.toDataURL({
5444
+ format: "png",
5445
+ multiplier: 2,
5446
+ left: exportBounds.left,
5447
+ top: exportBounds.top,
5448
+ width: exportBounds.width,
5449
+ height: exportBounds.height
5450
+ });
5451
+ if (debug) {
5452
+ console.info("[DielineTool] exportCutImage success", {
5453
+ sourceCount: sourceImages.length,
5454
+ bounds: exportBounds,
5455
+ rawPathBounds: pathBounds,
5456
+ pathOffset: {
5457
+ x: Number.isFinite(pathOffsetX) ? pathOffsetX : null,
5458
+ y: Number.isFinite(pathOffsetY) ? pathOffsetY : null
5459
+ },
5460
+ clipPathCenter: {
5461
+ x: centerX,
5462
+ y: centerY
5463
+ },
5464
+ cutRect: sceneLayout.cutRect,
5465
+ canvasSize: {
5466
+ width: Math.max(1, sourceCanvasWidth),
5467
+ height: Math.max(1, sourceCanvasHeight)
5468
+ }
5469
+ });
5221
5470
  }
5222
- break;
5223
- }
5224
- return { x: newX, y: newY };
5225
- };
5226
- var internalConstraint = (x, y, feature, context, params) => {
5227
- const { dielineWidth, dielineHeight } = context;
5228
- const margin = params.margin || 0;
5229
- const fw = feature.width || 0;
5230
- const fh = feature.height || 0;
5231
- const minX = (margin + fw / 2) / dielineWidth;
5232
- const maxX = 1 - (margin + fw / 2) / dielineWidth;
5233
- const minY = (margin + fh / 2) / dielineHeight;
5234
- const maxY = 1 - (margin + fh / 2) / dielineHeight;
5235
- const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
5236
- const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
5237
- return { x: clampedX, y: clampedY };
5238
- };
5239
- var tangentBottomConstraint = (x, y, feature, context, params) => {
5240
- const { dielineWidth, dielineHeight } = context;
5241
- const gap = params.gap || 0;
5242
- const confineX = params.confineX !== false;
5243
- const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
5244
- const newY = 1 + (extentY + gap) / dielineHeight;
5245
- let newX = x;
5246
- if (confineX) {
5247
- const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
5248
- const minX = extentX / dielineWidth;
5249
- const maxX = 1 - extentX / dielineWidth;
5250
- newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
5251
- }
5252
- return { x: newX, y: newY };
5253
- };
5254
- var lowestTangentConstraint = (x, y, feature, context, params) => {
5255
- const { dielineWidth, dielineHeight, geometry } = context;
5256
- if (!geometry) return { x, y };
5257
- const lowest = getLowestPointOnDieline(geometry);
5258
- const minY = geometry.y - geometry.height / 2;
5259
- const normY = (lowest.y - minY) / geometry.height;
5260
- const gap = params.gap || 0;
5261
- const confineX = params.confineX !== false;
5262
- const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
5263
- const newY = normY + (extentY + gap) / dielineHeight;
5264
- let newX = x;
5265
- if (confineX) {
5266
- const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
5267
- const minX = extentX / dielineWidth;
5268
- const maxX = 1 - extentX / dielineWidth;
5269
- newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
5471
+ return dataUrl;
5472
+ } finally {
5473
+ exportCanvas.dispose();
5474
+ }
5270
5475
  }
5271
- return { x: newX, y: newY };
5272
5476
  };
5273
- ConstraintRegistry.register("path", pathConstraint);
5274
- ConstraintRegistry.register("edge", edgeConstraint);
5275
- ConstraintRegistry.register("internal", internalConstraint);
5276
- ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
5277
- ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
5477
+
5478
+ // src/extensions/feature/FeatureTool.ts
5479
+ import {
5480
+ ContributionPointIds as ContributionPointIds5
5481
+ } from "@pooder/core";
5482
+ import { Pattern as Pattern3 } from "fabric";
5278
5483
 
5279
5484
  // src/extensions/featureComplete.ts
5280
5485
  function validateFeaturesStrict(features, context) {
@@ -5317,7 +5522,9 @@ var FeatureTool = class {
5317
5522
  this.isFeatureSessionActive = false;
5318
5523
  this.sessionOriginalFeatures = null;
5319
5524
  this.hasWorkingChanges = false;
5320
- this.specs = [];
5525
+ this.markerSpecs = [];
5526
+ this.sessionDielineSpecs = [];
5527
+ this.sessionDielineEffects = [];
5321
5528
  this.renderSeq = 0;
5322
5529
  this.subscriptions = new SubscriptionBag();
5323
5530
  this.handleMoving = null;
@@ -5327,7 +5534,7 @@ var FeatureTool = class {
5327
5534
  this.onToolActivated = (event) => {
5328
5535
  this.isToolActive = event.id === this.id;
5329
5536
  if (!this.isToolActive) {
5330
- this.restoreSessionFeaturesToConfig();
5537
+ this.suspendFeatureSession();
5331
5538
  }
5332
5539
  this.updateVisibility();
5333
5540
  };
@@ -5347,16 +5554,38 @@ var FeatureTool = class {
5347
5554
  (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5348
5555
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5349
5556
  this.id,
5350
- () => ({
5351
- passes: [
5557
+ () => {
5558
+ const passes = [
5352
5559
  {
5353
5560
  id: FEATURE_OVERLAY_LAYER_ID,
5354
5561
  stack: 880,
5355
5562
  order: 0,
5356
- objects: this.specs
5563
+ replace: true,
5564
+ objects: this.markerSpecs
5357
5565
  }
5358
- ]
5359
- }),
5566
+ ];
5567
+ if (this.isSessionVisible()) {
5568
+ passes.push(
5569
+ {
5570
+ id: DIELINE_LAYER_ID,
5571
+ stack: 700,
5572
+ order: 0,
5573
+ replace: false,
5574
+ visibility: { op: "const", value: false },
5575
+ objects: []
5576
+ },
5577
+ {
5578
+ id: FEATURE_DIELINE_LAYER_ID,
5579
+ stack: 705,
5580
+ order: 0,
5581
+ replace: true,
5582
+ effects: this.sessionDielineEffects,
5583
+ objects: this.sessionDielineSpecs
5584
+ }
5585
+ );
5586
+ }
5587
+ return { passes };
5588
+ },
5360
5589
  { priority: 350 }
5361
5590
  );
5362
5591
  const configService = context.services.get(
@@ -5371,12 +5600,22 @@ var FeatureTool = class {
5371
5600
  (e) => {
5372
5601
  if (this.isUpdatingConfig) return;
5373
5602
  if (e.key === "dieline.features") {
5374
- if (this.isFeatureSessionActive) return;
5603
+ if (this.isFeatureSessionActive && this.hasFeatureSessionDraft()) {
5604
+ return;
5605
+ }
5606
+ if (this.hasFeatureSessionDraft()) {
5607
+ this.clearFeatureSessionState();
5608
+ }
5375
5609
  const next = e.value || [];
5376
5610
  this.workingFeatures = this.cloneFeatures(next);
5377
5611
  this.hasWorkingChanges = false;
5378
5612
  this.redraw();
5379
5613
  this.emitWorkingChange();
5614
+ return;
5615
+ }
5616
+ if (e.key.startsWith("size.") || e.key.startsWith("dieline.")) {
5617
+ void this.refreshGeometry();
5618
+ this.redraw({ enforceConstraints: true });
5380
5619
  }
5381
5620
  }
5382
5621
  );
@@ -5392,7 +5631,8 @@ var FeatureTool = class {
5392
5631
  deactivate(context) {
5393
5632
  var _a;
5394
5633
  this.subscriptions.disposeAll();
5395
- this.restoreSessionFeaturesToConfig();
5634
+ this.restoreCommittedFeaturesToConfig();
5635
+ this.clearFeatureSessionState();
5396
5636
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
5397
5637
  this.dirtyTrackerDisposable = void 0;
5398
5638
  this.teardown();
@@ -5402,6 +5642,9 @@ var FeatureTool = class {
5402
5642
  updateVisibility() {
5403
5643
  this.redraw();
5404
5644
  }
5645
+ isSessionVisible() {
5646
+ return this.isToolActive && this.isFeatureSessionActive;
5647
+ }
5405
5648
  contribute() {
5406
5649
  return {
5407
5650
  [ContributionPointIds5.TOOLS]: [
@@ -5428,15 +5671,16 @@ var FeatureTool = class {
5428
5671
  if (this.isFeatureSessionActive) {
5429
5672
  return { ok: true };
5430
5673
  }
5431
- const original = this.getCommittedFeatures();
5432
- this.sessionOriginalFeatures = this.cloneFeatures(original);
5674
+ if (!this.hasFeatureSessionDraft()) {
5675
+ const original = this.getCommittedFeatures();
5676
+ this.sessionOriginalFeatures = this.cloneFeatures(original);
5677
+ this.setWorkingFeatures(this.cloneFeatures(original));
5678
+ this.hasWorkingChanges = false;
5679
+ }
5433
5680
  this.isFeatureSessionActive = true;
5434
5681
  await this.refreshGeometry();
5435
- this.setWorkingFeatures(this.cloneFeatures(original));
5436
- this.hasWorkingChanges = false;
5437
5682
  this.redraw();
5438
5683
  this.emitWorkingChange();
5439
- this.updateCommittedFeatures([]);
5440
5684
  return { ok: true };
5441
5685
  }
5442
5686
  },
@@ -5472,25 +5716,6 @@ var FeatureTool = class {
5472
5716
  return true;
5473
5717
  }
5474
5718
  },
5475
- {
5476
- command: "getWorkingFeatures",
5477
- title: "Get Working Features",
5478
- handler: () => {
5479
- return this.cloneFeatures(this.workingFeatures);
5480
- }
5481
- },
5482
- {
5483
- command: "setWorkingFeatures",
5484
- title: "Set Working Features",
5485
- handler: async (features) => {
5486
- await this.refreshGeometry();
5487
- this.setWorkingFeatures(this.cloneFeatures(features || []));
5488
- this.hasWorkingChanges = true;
5489
- this.redraw();
5490
- this.emitWorkingChange();
5491
- return { ok: true };
5492
- }
5493
- },
5494
5719
  {
5495
5720
  command: "rollbackFeatureSession",
5496
5721
  title: "Rollback Feature Session",
@@ -5557,17 +5782,24 @@ var FeatureTool = class {
5557
5782
  this.isUpdatingConfig = false;
5558
5783
  }
5559
5784
  }
5785
+ hasFeatureSessionDraft() {
5786
+ return Array.isArray(this.sessionOriginalFeatures);
5787
+ }
5560
5788
  clearFeatureSessionState() {
5561
5789
  this.isFeatureSessionActive = false;
5562
5790
  this.sessionOriginalFeatures = null;
5563
5791
  }
5564
- restoreSessionFeaturesToConfig() {
5565
- if (!this.isFeatureSessionActive) return;
5792
+ restoreCommittedFeaturesToConfig() {
5793
+ if (!this.hasFeatureSessionDraft()) return;
5566
5794
  const original = this.cloneFeatures(
5567
5795
  this.sessionOriginalFeatures || this.getCommittedFeatures()
5568
5796
  );
5569
5797
  this.updateCommittedFeatures(original);
5570
- this.clearFeatureSessionState();
5798
+ }
5799
+ suspendFeatureSession() {
5800
+ if (!this.isFeatureSessionActive) return;
5801
+ this.restoreCommittedFeaturesToConfig();
5802
+ this.isFeatureSessionActive = false;
5571
5803
  }
5572
5804
  emitWorkingChange() {
5573
5805
  var _a;
@@ -5589,7 +5821,7 @@ var FeatureTool = class {
5589
5821
  }
5590
5822
  async resetWorkingFeaturesFromSource() {
5591
5823
  const next = this.cloneFeatures(
5592
- this.isFeatureSessionActive && this.sessionOriginalFeatures ? this.sessionOriginalFeatures : this.getCommittedFeatures()
5824
+ this.sessionOriginalFeatures || this.getCommittedFeatures()
5593
5825
  );
5594
5826
  await this.refreshGeometry();
5595
5827
  this.setWorkingFeatures(next);
@@ -5813,11 +6045,35 @@ var FeatureTool = class {
5813
6045
  this.handleSceneGeometryChange = null;
5814
6046
  }
5815
6047
  this.renderSeq += 1;
5816
- this.specs = [];
6048
+ this.markerSpecs = [];
6049
+ this.sessionDielineSpecs = [];
6050
+ this.sessionDielineEffects = [];
5817
6051
  (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5818
6052
  this.renderProducerDisposable = void 0;
5819
6053
  void this.canvasService.flushRenderFromProducers();
5820
6054
  }
6055
+ createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
6056
+ if (typeof document === "undefined") {
6057
+ return void 0;
6058
+ }
6059
+ const size = 20;
6060
+ const canvas = document.createElement("canvas");
6061
+ canvas.width = size;
6062
+ canvas.height = size;
6063
+ const ctx = canvas.getContext("2d");
6064
+ if (ctx) {
6065
+ ctx.clearRect(0, 0, size, size);
6066
+ ctx.strokeStyle = color;
6067
+ ctx.lineWidth = 1;
6068
+ ctx.beginPath();
6069
+ ctx.moveTo(0, size);
6070
+ ctx.lineTo(size, 0);
6071
+ ctx.stroke();
6072
+ }
6073
+ return new Pattern3({
6074
+ source: canvas
6075
+ });
6076
+ }
5821
6077
  getDraggableMarkerTarget(target) {
5822
6078
  var _a, _b;
5823
6079
  if (!this.isFeatureSessionActive || !this.isToolActive) return null;
@@ -5893,6 +6149,7 @@ var FeatureTool = class {
5893
6149
  next[index] = updatedFeature;
5894
6150
  this.setWorkingFeatures(next);
5895
6151
  this.hasWorkingChanges = true;
6152
+ this.redraw();
5896
6153
  this.emitWorkingChange();
5897
6154
  }
5898
6155
  syncGroupFromCanvas(target) {
@@ -5931,6 +6188,7 @@ var FeatureTool = class {
5931
6188
  if (!changed) return;
5932
6189
  this.setWorkingFeatures(next);
5933
6190
  this.hasWorkingChanges = true;
6191
+ this.redraw();
5934
6192
  this.emitWorkingChange();
5935
6193
  }
5936
6194
  redraw(options = {}) {
@@ -5939,7 +6197,10 @@ var FeatureTool = class {
5939
6197
  async redrawAsync(options = {}) {
5940
6198
  if (!this.canvasService) return;
5941
6199
  const seq = ++this.renderSeq;
5942
- this.specs = this.buildFeatureSpecs();
6200
+ this.markerSpecs = this.buildMarkerSpecs();
6201
+ const sessionRender = this.buildSessionDielineRender();
6202
+ this.sessionDielineSpecs = sessionRender.specs;
6203
+ this.sessionDielineEffects = sessionRender.effects;
5943
6204
  if (seq !== this.renderSeq) return;
5944
6205
  await this.canvasService.flushRenderFromProducers();
5945
6206
  if (seq !== this.renderSeq) return;
@@ -5947,15 +6208,77 @@ var FeatureTool = class {
5947
6208
  this.enforceConstraints();
5948
6209
  }
5949
6210
  }
5950
- buildFeatureSpecs() {
6211
+ buildSessionDielineRender() {
6212
+ if (!this.isSessionVisible() || !this.canvasService) {
6213
+ return { specs: [], effects: [] };
6214
+ }
6215
+ const configService = this.getConfigService();
6216
+ if (!configService) {
6217
+ return { specs: [], effects: [] };
6218
+ }
6219
+ const sceneLayout = computeSceneLayout(
6220
+ this.canvasService,
6221
+ readSizeState(configService)
6222
+ );
6223
+ if (!sceneLayout) {
6224
+ return { specs: [], effects: [] };
6225
+ }
6226
+ const state = readDielineState(configService);
6227
+ state.features = this.cloneFeatures(this.workingFeatures);
6228
+ return buildDielineRenderBundle({
6229
+ state,
6230
+ sceneLayout,
6231
+ canvasWidth: sceneLayout.canvasWidth || this.canvasService.canvas.width || 800,
6232
+ canvasHeight: sceneLayout.canvasHeight || this.canvasService.canvas.height || 600,
6233
+ hasImages: this.hasImageItems(),
6234
+ createHatchPattern: (color) => this.createHatchPattern(color),
6235
+ clipTargetPassIds: [IMAGE_OBJECT_LAYER_ID],
6236
+ clipVisibility: { op: "const", value: true },
6237
+ ids: {
6238
+ inside: "feature.session.dieline.inside",
6239
+ bleedZone: "feature.session.dieline.bleed-zone",
6240
+ offsetBorder: "feature.session.dieline.offset-border",
6241
+ border: "feature.session.dieline.border",
6242
+ clip: "feature.session.dieline.clip.image",
6243
+ clipSource: "feature.session.dieline.effect.clip-path"
6244
+ }
6245
+ });
6246
+ }
6247
+ hasImageItems() {
6248
+ const configService = this.getConfigService();
6249
+ if (!configService) return false;
6250
+ const items = configService.get("image.items", []);
6251
+ return Array.isArray(items) && items.length > 0;
6252
+ }
6253
+ buildMarkerSpecs() {
5951
6254
  if (!this.isFeatureSessionActive || !this.currentGeometry || this.workingFeatures.length === 0) {
5952
6255
  return [];
5953
6256
  }
5954
6257
  const groups = /* @__PURE__ */ new Map();
5955
6258
  const singles = [];
5956
- this.workingFeatures.forEach((feature, index) => {
6259
+ const placements = resolveFeaturePlacements(
6260
+ this.workingFeatures,
6261
+ {
6262
+ shape: this.currentGeometry.shape,
6263
+ shapeStyle: this.currentGeometry.shapeStyle,
6264
+ pathData: this.currentGeometry.pathData,
6265
+ customSourceWidthPx: this.currentGeometry.customSourceWidthPx,
6266
+ customSourceHeightPx: this.currentGeometry.customSourceHeightPx,
6267
+ x: this.currentGeometry.x,
6268
+ y: this.currentGeometry.y,
6269
+ width: this.currentGeometry.width,
6270
+ height: this.currentGeometry.height,
6271
+ radius: this.currentGeometry.radius,
6272
+ scale: this.currentGeometry.scale || 1
6273
+ }
6274
+ );
6275
+ placements.forEach((placement, index) => {
6276
+ const feature = placement.feature;
5957
6277
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5958
- const position = resolveFeaturePosition(feature, geometry);
6278
+ const position = {
6279
+ x: placement.centerX,
6280
+ y: placement.centerY
6281
+ };
5959
6282
  const scale = geometry.scale || 1;
5960
6283
  const marker = {
5961
6284
  feature,
@@ -6532,11 +6855,12 @@ var EXTENSION_LINE_LENGTH = 5;
6532
6855
  var MIN_ARROW_SIZE = 4;
6533
6856
  var THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
6534
6857
  var DEFAULT_THICKNESS = 20;
6535
- var DEFAULT_GAP = 45;
6858
+ var DEFAULT_GAP = 65;
6536
6859
  var DEFAULT_FONT_SIZE = 10;
6537
6860
  var DEFAULT_BACKGROUND_COLOR = "#f0f0f0";
6538
6861
  var DEFAULT_TEXT_COLOR = "#333333";
6539
6862
  var DEFAULT_LINE_COLOR = "#999999";
6863
+ var RULER_DEBUG_KEY = "ruler.debug";
6540
6864
  var RULER_THICKNESS_MIN = 10;
6541
6865
  var RULER_THICKNESS_MAX = 100;
6542
6866
  var RULER_GAP_MIN = 0;
@@ -6555,6 +6879,7 @@ var RulerTool = class {
6555
6879
  this.textColor = DEFAULT_TEXT_COLOR;
6556
6880
  this.lineColor = DEFAULT_LINE_COLOR;
6557
6881
  this.fontSize = DEFAULT_FONT_SIZE;
6882
+ this.debugEnabled = false;
6558
6883
  this.renderSeq = 0;
6559
6884
  this.numericProps = /* @__PURE__ */ new Set(["thickness", "gap", "fontSize"]);
6560
6885
  this.specs = [];
@@ -6603,7 +6928,14 @@ var RulerTool = class {
6603
6928
  this.syncConfig(configService);
6604
6929
  configService.onAnyChange((e) => {
6605
6930
  let shouldUpdate = false;
6606
- if (e.key.startsWith("ruler.")) {
6931
+ if (e.key === RULER_DEBUG_KEY) {
6932
+ this.debugEnabled = e.value === true;
6933
+ this.log("config:update", {
6934
+ key: e.key,
6935
+ raw: e.value,
6936
+ normalized: this.debugEnabled
6937
+ });
6938
+ } else if (e.key.startsWith("ruler.")) {
6607
6939
  const prop = e.key.split(".")[1];
6608
6940
  if (prop && prop in this) {
6609
6941
  if (this.numericProps.has(prop)) {
@@ -6690,6 +7022,12 @@ var RulerTool = class {
6690
7022
  min: RULER_FONT_SIZE_MIN,
6691
7023
  max: RULER_FONT_SIZE_MAX,
6692
7024
  default: DEFAULT_FONT_SIZE
7025
+ },
7026
+ {
7027
+ id: RULER_DEBUG_KEY,
7028
+ type: "boolean",
7029
+ label: "Ruler Debug Log",
7030
+ default: false
6693
7031
  }
6694
7032
  ],
6695
7033
  [ContributionPointIds8.COMMANDS]: [
@@ -6726,7 +7064,11 @@ var RulerTool = class {
6726
7064
  ]
6727
7065
  };
6728
7066
  }
7067
+ isDebugEnabled() {
7068
+ return this.debugEnabled;
7069
+ }
6729
7070
  log(step, payload) {
7071
+ if (!this.isDebugEnabled()) return;
6730
7072
  if (payload) {
6731
7073
  console.debug(`[RulerTool] ${step}`, payload);
6732
7074
  return;
@@ -6755,6 +7097,7 @@ var RulerTool = class {
6755
7097
  configService.get("ruler.fontSize", this.fontSize),
6756
7098
  DEFAULT_FONT_SIZE
6757
7099
  );
7100
+ this.debugEnabled = configService.get(RULER_DEBUG_KEY, this.debugEnabled) === true;
6758
7101
  this.log("config:loaded", {
6759
7102
  thickness: this.thickness,
6760
7103
  gap: this.gap,
@@ -9534,11 +9877,13 @@ export {
9534
9877
  WhiteInkTool,
9535
9878
  getCoverScale as computeImageCoverScale,
9536
9879
  getCoverScale as computeWhiteInkCoverScale,
9880
+ createDefaultDielineState,
9537
9881
  createDielineCommands,
9538
9882
  createDielineConfigurations,
9539
9883
  createImageCommands,
9540
9884
  createImageConfigurations,
9541
9885
  createWhiteInkCommands,
9542
9886
  createWhiteInkConfigurations,
9543
- evaluateVisibilityExpr
9887
+ evaluateVisibilityExpr,
9888
+ readDielineState
9544
9889
  };