@pooder/kit 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.test-dist/src/extensions/background/BackgroundTool.js +524 -0
  2. package/.test-dist/src/extensions/background/index.js +17 -0
  3. package/.test-dist/src/extensions/background.js +1 -1
  4. package/.test-dist/src/extensions/dieline/DielineTool.js +748 -0
  5. package/.test-dist/src/extensions/dieline/commands.js +127 -0
  6. package/.test-dist/src/extensions/dieline/config.js +107 -0
  7. package/.test-dist/src/extensions/dieline/index.js +21 -0
  8. package/.test-dist/src/extensions/dieline/model.js +2 -0
  9. package/.test-dist/src/extensions/dieline/renderer.js +2 -0
  10. package/.test-dist/src/extensions/dieline.js +4 -0
  11. package/.test-dist/src/extensions/feature/FeatureTool.js +914 -0
  12. package/.test-dist/src/extensions/feature/index.js +17 -0
  13. package/.test-dist/src/extensions/film/FilmTool.js +207 -0
  14. package/.test-dist/src/extensions/film/index.js +17 -0
  15. package/.test-dist/src/extensions/image/ImageTool.js +1499 -0
  16. package/.test-dist/src/extensions/image/commands.js +162 -0
  17. package/.test-dist/src/extensions/image/config.js +129 -0
  18. package/.test-dist/src/extensions/image/index.js +21 -0
  19. package/.test-dist/src/extensions/image/model.js +2 -0
  20. package/.test-dist/src/extensions/image/renderer.js +5 -0
  21. package/.test-dist/src/extensions/image.js +182 -7
  22. package/.test-dist/src/extensions/mirror/MirrorTool.js +104 -0
  23. package/.test-dist/src/extensions/mirror/index.js +17 -0
  24. package/.test-dist/src/extensions/ruler/RulerTool.js +442 -0
  25. package/.test-dist/src/extensions/ruler/index.js +17 -0
  26. package/.test-dist/src/extensions/sceneLayout.js +2 -93
  27. package/.test-dist/src/extensions/sceneLayoutModel.js +15 -200
  28. package/.test-dist/src/extensions/size/SizeTool.js +332 -0
  29. package/.test-dist/src/extensions/size/index.js +17 -0
  30. package/.test-dist/src/extensions/white-ink/WhiteInkTool.js +1003 -0
  31. package/.test-dist/src/extensions/white-ink/commands.js +148 -0
  32. package/.test-dist/src/extensions/white-ink/config.js +31 -0
  33. package/.test-dist/src/extensions/white-ink/index.js +21 -0
  34. package/.test-dist/src/extensions/white-ink/model.js +2 -0
  35. package/.test-dist/src/extensions/white-ink/renderer.js +5 -0
  36. package/.test-dist/src/services/CanvasService.js +34 -13
  37. package/.test-dist/src/services/SceneLayoutService.js +96 -0
  38. package/.test-dist/src/services/index.js +1 -0
  39. package/.test-dist/src/services/visibility.js +3 -0
  40. package/.test-dist/src/shared/constants/layers.js +25 -0
  41. package/.test-dist/src/shared/imaging/sourceSizeCache.js +82 -0
  42. package/.test-dist/src/shared/index.js +22 -0
  43. package/.test-dist/src/shared/runtime/sessionState.js +74 -0
  44. package/.test-dist/src/shared/runtime/subscriptions.js +30 -0
  45. package/.test-dist/src/shared/scene/frame.js +34 -0
  46. package/.test-dist/src/shared/scene/sceneLayoutModel.js +202 -0
  47. package/.test-dist/tests/run.js +118 -0
  48. package/CHANGELOG.md +12 -0
  49. package/dist/index.d.mts +403 -366
  50. package/dist/index.d.ts +403 -366
  51. package/dist/index.js +5172 -4752
  52. package/dist/index.mjs +1410 -2027
  53. package/dist/tracer-PO7CRBYY.mjs +1016 -0
  54. package/package.json +1 -1
  55. package/src/extensions/{background.ts → background/BackgroundTool.ts} +33 -50
  56. package/src/extensions/background/index.ts +1 -0
  57. package/src/extensions/{dieline.ts → dieline/DielineTool.ts} +18 -218
  58. package/src/extensions/dieline/commands.ts +109 -0
  59. package/src/extensions/dieline/config.ts +106 -0
  60. package/src/extensions/dieline/index.ts +5 -0
  61. package/src/extensions/dieline/model.ts +1 -0
  62. package/src/extensions/dieline/renderer.ts +1 -0
  63. package/src/extensions/{feature.ts → feature/FeatureTool.ts} +27 -21
  64. package/src/extensions/feature/index.ts +1 -0
  65. package/src/extensions/{film.ts → film/FilmTool.ts} +36 -48
  66. package/src/extensions/film/index.ts +1 -0
  67. package/src/extensions/{image.ts → image/ImageTool.ts} +289 -335
  68. package/src/extensions/image/commands.ts +176 -0
  69. package/src/extensions/image/config.ts +128 -0
  70. package/src/extensions/image/index.ts +5 -0
  71. package/src/extensions/image/model.ts +1 -0
  72. package/src/extensions/image/renderer.ts +1 -0
  73. package/src/extensions/{mirror.ts → mirror/MirrorTool.ts} +1 -1
  74. package/src/extensions/mirror/index.ts +1 -0
  75. package/src/extensions/{ruler.ts → ruler/RulerTool.ts} +4 -5
  76. package/src/extensions/ruler/index.ts +1 -0
  77. package/src/extensions/sceneLayout.ts +1 -140
  78. package/src/extensions/sceneLayoutModel.ts +1 -364
  79. package/src/extensions/{size.ts → size/SizeTool.ts} +7 -6
  80. package/src/extensions/size/index.ts +1 -0
  81. package/src/extensions/{white-ink.ts → white-ink/WhiteInkTool.ts} +130 -317
  82. package/src/extensions/white-ink/commands.ts +157 -0
  83. package/src/extensions/white-ink/config.ts +30 -0
  84. package/src/extensions/white-ink/index.ts +5 -0
  85. package/src/extensions/white-ink/model.ts +1 -0
  86. package/src/extensions/white-ink/renderer.ts +1 -0
  87. package/src/services/CanvasService.ts +43 -12
  88. package/src/services/SceneLayoutService.ts +139 -0
  89. package/src/services/index.ts +1 -0
  90. package/src/services/renderSpec.ts +2 -0
  91. package/src/services/visibility.ts +5 -0
  92. package/src/shared/constants/layers.ts +23 -0
  93. package/src/shared/imaging/sourceSizeCache.ts +103 -0
  94. package/src/shared/index.ts +6 -0
  95. package/src/shared/runtime/sessionState.ts +105 -0
  96. package/src/shared/runtime/subscriptions.ts +45 -0
  97. package/src/shared/scene/frame.ts +46 -0
  98. package/src/shared/scene/sceneLayoutModel.ts +367 -0
  99. package/tests/run.ts +151 -0
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // src/extensions/background.ts
1
+ // src/extensions/background/BackgroundTool.ts
2
2
  import {
3
3
  ContributionPointIds
4
4
  } from "@pooder/core";
@@ -157,7 +157,7 @@ function getHeartShapeParams(style) {
157
157
  };
158
158
  }
159
159
 
160
- // src/extensions/sceneLayoutModel.ts
160
+ // src/shared/scene/sceneLayoutModel.ts
161
161
  var DEFAULT_SIZE_STATE = {
162
162
  unit: "mm",
163
163
  actualWidthMm: 500,
@@ -392,8 +392,117 @@ function buildSceneGeometry(configService, layout) {
392
392
  };
393
393
  }
394
394
 
395
- // src/extensions/background.ts
395
+ // src/shared/constants/layers.ts
396
396
  var BACKGROUND_LAYER_ID = "background";
397
+ var IMAGE_OBJECT_LAYER_ID = "image.user";
398
+ var IMAGE_OVERLAY_LAYER_ID = "image-overlay";
399
+ var WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
400
+ var WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
401
+ var WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
402
+ var DIELINE_LAYER_ID = "dieline-overlay";
403
+ var FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
404
+ var RULER_LAYER_ID = "ruler-overlay";
405
+ var FILM_LAYER_ID = "overlay";
406
+
407
+ // src/shared/imaging/sourceSizeCache.ts
408
+ function normalizeSourceSize(size) {
409
+ const width = Number(size.width || 0);
410
+ const height = Number(size.height || 0);
411
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
412
+ if (width <= 0 || height <= 0) return null;
413
+ return { width, height };
414
+ }
415
+ function getCoverScale(frame, source) {
416
+ const frameWidth = Math.max(1, Number(frame.width || 0));
417
+ const frameHeight = Math.max(1, Number(frame.height || 0));
418
+ const sourceWidth = Math.max(1, Number(source.width || 0));
419
+ const sourceHeight = Math.max(1, Number(source.height || 0));
420
+ return Math.max(frameWidth / sourceWidth, frameHeight / sourceHeight);
421
+ }
422
+ function createSourceSizeCache(loadSize) {
423
+ const sizesBySrc = /* @__PURE__ */ new Map();
424
+ const pendingBySrc = /* @__PURE__ */ new Map();
425
+ const rememberSourceSize = (src, size) => {
426
+ const normalized = normalizeSourceSize(size);
427
+ if (!src || !normalized) return null;
428
+ sizesBySrc.set(src, normalized);
429
+ return normalized;
430
+ };
431
+ const getSourceSize = (src) => {
432
+ if (!src) return null;
433
+ const cached = sizesBySrc.get(src);
434
+ if (!cached) return null;
435
+ return { width: cached.width, height: cached.height };
436
+ };
437
+ const ensureImageSize = async (src) => {
438
+ if (!src) return null;
439
+ const cached = sizesBySrc.get(src);
440
+ if (cached) return { width: cached.width, height: cached.height };
441
+ const pending = pendingBySrc.get(src);
442
+ if (pending) {
443
+ return pending;
444
+ }
445
+ const task = loadSize(src);
446
+ pendingBySrc.set(src, task);
447
+ try {
448
+ const size = await task;
449
+ if (size) {
450
+ rememberSourceSize(src, size);
451
+ }
452
+ return size;
453
+ } finally {
454
+ if (pendingBySrc.get(src) === task) {
455
+ pendingBySrc.delete(src);
456
+ }
457
+ }
458
+ };
459
+ const deleteSourceSize = (src) => {
460
+ if (!src) return;
461
+ sizesBySrc.delete(src);
462
+ pendingBySrc.delete(src);
463
+ };
464
+ const clear = () => {
465
+ sizesBySrc.clear();
466
+ pendingBySrc.clear();
467
+ };
468
+ return {
469
+ ensureImageSize,
470
+ rememberSourceSize,
471
+ getSourceSize,
472
+ deleteSourceSize,
473
+ clear
474
+ };
475
+ }
476
+
477
+ // src/shared/runtime/subscriptions.ts
478
+ var SubscriptionBag = class {
479
+ constructor() {
480
+ this.disposables = [];
481
+ }
482
+ add(disposable) {
483
+ if (disposable) {
484
+ this.disposables.push(disposable);
485
+ }
486
+ return disposable;
487
+ }
488
+ on(eventBus, event, handler) {
489
+ eventBus.on(event, handler);
490
+ this.disposables.push({
491
+ dispose: () => eventBus.off(event, handler)
492
+ });
493
+ }
494
+ onConfigChange(configService, handler) {
495
+ this.add(configService.onAnyChange(handler));
496
+ }
497
+ disposeAll() {
498
+ while (this.disposables.length > 0) {
499
+ const disposable = this.disposables.pop();
500
+ disposable == null ? void 0 : disposable.dispose();
501
+ }
502
+ }
503
+ };
504
+
505
+ // src/extensions/background/BackgroundTool.ts
397
506
  var BACKGROUND_CONFIG_KEY = "background.config";
398
507
  var DEFAULT_WIDTH = 800;
399
508
  var DEFAULT_HEIGHT = 600;
@@ -409,7 +518,7 @@ var DEFAULT_BACKGROUND_CONFIG = {
409
518
  order: 0,
410
519
  enabled: true,
411
520
  exportable: false,
412
- color: "#fff"
521
+ color: "#eee"
413
522
  }
414
523
  ]
415
524
  };
@@ -520,10 +629,12 @@ var BackgroundTool = class {
520
629
  };
521
630
  this.config = cloneConfig(DEFAULT_BACKGROUND_CONFIG);
522
631
  this.specs = [];
632
+ this.subscriptions = new SubscriptionBag();
523
633
  this.renderSeq = 0;
524
634
  this.latestSceneLayout = null;
525
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
526
- this.pendingSizeBySrc = /* @__PURE__ */ new Map();
635
+ this.sourceSizeCache = createSourceSizeCache(
636
+ (src) => this.loadImageSize(src)
637
+ );
527
638
  this.onCanvasResized = () => {
528
639
  this.latestSceneLayout = null;
529
640
  this.updateBackground();
@@ -537,7 +648,8 @@ var BackgroundTool = class {
537
648
  }
538
649
  }
539
650
  activate(context) {
540
- var _a, _b;
651
+ var _a;
652
+ this.subscriptions.disposeAll();
541
653
  this.canvasService = context.services.get("CanvasService");
542
654
  if (!this.canvasService) {
543
655
  console.warn("CanvasService not found for BackgroundTool");
@@ -553,8 +665,8 @@ var BackgroundTool = class {
553
665
  DEFAULT_BACKGROUND_CONFIG
554
666
  )
555
667
  );
556
- (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
557
- this.configChangeDisposable = this.configService.onAnyChange(
668
+ this.subscriptions.onConfigChange(
669
+ this.configService,
558
670
  (e) => {
559
671
  if (e.key === BACKGROUND_CONFIG_KEY) {
560
672
  this.config = normalizeConfig(e.value);
@@ -568,7 +680,7 @@ var BackgroundTool = class {
568
680
  }
569
681
  );
570
682
  }
571
- (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
683
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
572
684
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
573
685
  this.id,
574
686
  () => ({
@@ -583,20 +695,26 @@ var BackgroundTool = class {
583
695
  }),
584
696
  { priority: 0 }
585
697
  );
586
- context.eventBus.on("canvas:resized", this.onCanvasResized);
587
- context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
698
+ this.subscriptions.on(
699
+ context.eventBus,
700
+ "canvas:resized",
701
+ this.onCanvasResized
702
+ );
703
+ this.subscriptions.on(
704
+ context.eventBus,
705
+ "scene:layout:change",
706
+ this.onSceneLayoutChanged
707
+ );
588
708
  this.updateBackground();
589
709
  }
590
710
  deactivate(context) {
591
- var _a, _b;
592
- context.eventBus.off("canvas:resized", this.onCanvasResized);
593
- context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
711
+ var _a;
712
+ this.subscriptions.disposeAll();
594
713
  this.renderSeq += 1;
595
714
  this.specs = [];
596
715
  this.latestSceneLayout = null;
597
- (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
598
- this.configChangeDisposable = void 0;
599
- (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
716
+ this.sourceSizeCache.clear();
717
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
600
718
  this.renderProducerDisposable = void 0;
601
719
  if (!this.canvasService) return;
602
720
  void this.canvasService.flushRenderFromProducers();
@@ -804,7 +922,7 @@ var BackgroundTool = class {
804
922
  buildImageLayerSpec(layer) {
805
923
  const src = String(layer.src || "").trim();
806
924
  if (!src) return [];
807
- const sourceSize = this.sourceSizeBySrc.get(src);
925
+ const sourceSize = this.sourceSizeCache.getSourceSize(src);
808
926
  if (!sourceSize) return [];
809
927
  const rect = this.resolveAnchorRect(layer.anchor);
810
928
  const placement = this.resolveImagePlacement(rect, sourceSize, layer.fit);
@@ -863,24 +981,6 @@ var BackgroundTool = class {
863
981
  });
864
982
  return Array.from(urls);
865
983
  }
866
- async ensureImageSize(src) {
867
- if (!src) return null;
868
- const cached = this.sourceSizeBySrc.get(src);
869
- if (cached) return cached;
870
- const pending = this.pendingSizeBySrc.get(src);
871
- if (pending) {
872
- return pending;
873
- }
874
- const task = this.loadImageSize(src);
875
- this.pendingSizeBySrc.set(src, task);
876
- try {
877
- return await task;
878
- } finally {
879
- if (this.pendingSizeBySrc.get(src) === task) {
880
- this.pendingSizeBySrc.delete(src);
881
- }
882
- }
883
- }
884
984
  async loadImageSize(src) {
885
985
  try {
886
986
  const image = await FabricImage.fromURL(src, {
@@ -889,9 +989,7 @@ var BackgroundTool = class {
889
989
  const width = Number((image == null ? void 0 : image.width) || 0);
890
990
  const height = Number((image == null ? void 0 : image.height) || 0);
891
991
  if (width > 0 && height > 0) {
892
- const size = { width, height };
893
- this.sourceSizeBySrc.set(src, size);
894
- return size;
992
+ return { width, height };
895
993
  }
896
994
  } catch (error) {
897
995
  console.error("[BackgroundTool] Failed to load image", src, error);
@@ -907,7 +1005,9 @@ var BackgroundTool = class {
907
1005
  const currentConfig = cloneConfig(this.config);
908
1006
  const activeUrls = this.collectActiveImageUrls(currentConfig);
909
1007
  if (activeUrls.length > 0) {
910
- await Promise.all(activeUrls.map((url) => this.ensureImageSize(url)));
1008
+ await Promise.all(
1009
+ activeUrls.map((url) => this.sourceSizeCache.ensureImageSize(url))
1010
+ );
911
1011
  if (seq !== this.renderSeq) return;
912
1012
  }
913
1013
  this.specs = this.buildBackgroundSpecs(currentConfig);
@@ -917,15 +1017,17 @@ var BackgroundTool = class {
917
1017
  }
918
1018
  };
919
1019
 
920
- // src/extensions/image.ts
1020
+ // src/extensions/image/ImageTool.ts
921
1021
  import {
922
1022
  ContributionPointIds as ContributionPointIds2
923
1023
  } from "@pooder/core";
924
1024
  import {
925
1025
  Canvas as FabricCanvas,
1026
+ Control,
926
1027
  Image as FabricImage2,
927
1028
  Pattern,
928
- Point
1029
+ Point,
1030
+ controlsUtils
929
1031
  } from "fabric";
930
1032
 
931
1033
  // src/extensions/geometry.ts
@@ -1547,9 +1649,379 @@ function getPathBounds(pathData) {
1547
1649
  };
1548
1650
  }
1549
1651
 
1550
- // src/extensions/image.ts
1551
- var IMAGE_OBJECT_LAYER_ID = "image.user";
1552
- var IMAGE_OVERLAY_LAYER_ID = "image-overlay";
1652
+ // src/shared/scene/frame.ts
1653
+ function emptyFrameRect() {
1654
+ return { left: 0, top: 0, width: 0, height: 0 };
1655
+ }
1656
+ function resolveCutFrameRect(canvasService, configService) {
1657
+ if (!canvasService || !configService) {
1658
+ return emptyFrameRect();
1659
+ }
1660
+ const sizeState = readSizeState(configService);
1661
+ const layout = computeSceneLayout(canvasService, sizeState);
1662
+ if (!layout) {
1663
+ return emptyFrameRect();
1664
+ }
1665
+ return canvasService.toSceneRect({
1666
+ left: layout.cutRect.left,
1667
+ top: layout.cutRect.top,
1668
+ width: layout.cutRect.width,
1669
+ height: layout.cutRect.height
1670
+ });
1671
+ }
1672
+ function toLayoutSceneRect(rect) {
1673
+ return {
1674
+ left: rect.left,
1675
+ top: rect.top,
1676
+ width: rect.width,
1677
+ height: rect.height,
1678
+ space: "scene"
1679
+ };
1680
+ }
1681
+
1682
+ // src/shared/runtime/sessionState.ts
1683
+ function cloneWithJson(value) {
1684
+ return JSON.parse(JSON.stringify(value));
1685
+ }
1686
+ function applyCommittedSnapshot(session, nextCommitted, options) {
1687
+ const clone = options.clone;
1688
+ session.committed = clone(nextCommitted);
1689
+ const shouldPreserveDirtyWorking = options.toolActive && options.preserveDirtyWorking !== false && session.hasWorkingChanges;
1690
+ if (!shouldPreserveDirtyWorking) {
1691
+ session.working = clone(session.committed);
1692
+ session.hasWorkingChanges = false;
1693
+ }
1694
+ }
1695
+ function runDeferredConfigUpdate(state, action, cooldownMs = 0) {
1696
+ state.isUpdatingConfig = true;
1697
+ action();
1698
+ if (cooldownMs <= 0) {
1699
+ state.isUpdatingConfig = false;
1700
+ return;
1701
+ }
1702
+ setTimeout(() => {
1703
+ state.isUpdatingConfig = false;
1704
+ }, cooldownMs);
1705
+ }
1706
+
1707
+ // src/extensions/image/commands.ts
1708
+ function createImageCommands(tool) {
1709
+ return [
1710
+ {
1711
+ command: "addImage",
1712
+ id: "addImage",
1713
+ title: "Add Image",
1714
+ handler: async (url, options) => {
1715
+ const result = await tool.upsertImageEntry(url, {
1716
+ mode: "add",
1717
+ addOptions: options
1718
+ });
1719
+ return result.id;
1720
+ }
1721
+ },
1722
+ {
1723
+ command: "upsertImage",
1724
+ id: "upsertImage",
1725
+ title: "Upsert Image",
1726
+ handler: async (url, options = {}) => {
1727
+ return await tool.upsertImageEntry(url, options);
1728
+ }
1729
+ },
1730
+ {
1731
+ command: "getWorkingImages",
1732
+ id: "getWorkingImages",
1733
+ title: "Get Working Images",
1734
+ handler: () => {
1735
+ return tool.cloneItems(tool.workingItems);
1736
+ }
1737
+ },
1738
+ {
1739
+ command: "setWorkingImage",
1740
+ id: "setWorkingImage",
1741
+ title: "Set Working Image",
1742
+ handler: (id, updates) => {
1743
+ tool.updateImageInWorking(id, updates);
1744
+ }
1745
+ },
1746
+ {
1747
+ command: "resetWorkingImages",
1748
+ id: "resetWorkingImages",
1749
+ title: "Reset Working Images",
1750
+ handler: () => {
1751
+ tool.workingItems = tool.cloneItems(tool.items);
1752
+ tool.hasWorkingChanges = false;
1753
+ tool.updateImages();
1754
+ tool.emitWorkingChange();
1755
+ }
1756
+ },
1757
+ {
1758
+ command: "completeImages",
1759
+ id: "completeImages",
1760
+ title: "Complete Images",
1761
+ handler: async () => {
1762
+ return await tool.commitWorkingImagesAsCropped();
1763
+ }
1764
+ },
1765
+ {
1766
+ command: "exportUserCroppedImage",
1767
+ id: "exportUserCroppedImage",
1768
+ title: "Export User Cropped Image",
1769
+ handler: async (options = {}) => {
1770
+ return await tool.exportUserCroppedImage(options);
1771
+ }
1772
+ },
1773
+ {
1774
+ command: "fitImageToArea",
1775
+ id: "fitImageToArea",
1776
+ title: "Fit Image to Area",
1777
+ handler: async (id, area) => {
1778
+ await tool.fitImageToArea(id, area);
1779
+ }
1780
+ },
1781
+ {
1782
+ command: "fitImageToDefaultArea",
1783
+ id: "fitImageToDefaultArea",
1784
+ title: "Fit Image to Default Area",
1785
+ handler: async (id) => {
1786
+ await tool.fitImageToDefaultArea(id);
1787
+ }
1788
+ },
1789
+ {
1790
+ command: "focusImage",
1791
+ id: "focusImage",
1792
+ title: "Focus Image",
1793
+ handler: (id, options = {}) => {
1794
+ return tool.setImageFocus(id, options);
1795
+ }
1796
+ },
1797
+ {
1798
+ command: "removeImage",
1799
+ id: "removeImage",
1800
+ title: "Remove Image",
1801
+ handler: (id) => {
1802
+ const removed = tool.items.find((item) => item.id === id);
1803
+ const next = tool.items.filter((item) => item.id !== id);
1804
+ if (next.length !== tool.items.length) {
1805
+ tool.purgeSourceSizeCacheForItem(removed);
1806
+ if (tool.focusedImageId === id) {
1807
+ tool.setImageFocus(null, {
1808
+ syncCanvasSelection: true,
1809
+ skipRender: true
1810
+ });
1811
+ }
1812
+ tool.updateConfig(next);
1813
+ }
1814
+ }
1815
+ },
1816
+ {
1817
+ command: "updateImage",
1818
+ id: "updateImage",
1819
+ title: "Update Image",
1820
+ handler: async (id, updates, options = {}) => {
1821
+ await tool.updateImage(id, updates, options);
1822
+ }
1823
+ },
1824
+ {
1825
+ command: "clearImages",
1826
+ id: "clearImages",
1827
+ title: "Clear Images",
1828
+ handler: () => {
1829
+ tool.sourceSizeCache.clear();
1830
+ tool.setImageFocus(null, {
1831
+ syncCanvasSelection: true,
1832
+ skipRender: true
1833
+ });
1834
+ tool.updateConfig([]);
1835
+ }
1836
+ },
1837
+ {
1838
+ command: "bringToFront",
1839
+ id: "bringToFront",
1840
+ title: "Bring Image to Front",
1841
+ handler: (id) => {
1842
+ const index = tool.items.findIndex((item) => item.id === id);
1843
+ if (index !== -1 && index < tool.items.length - 1) {
1844
+ const next = [...tool.items];
1845
+ const [item] = next.splice(index, 1);
1846
+ next.push(item);
1847
+ tool.updateConfig(next);
1848
+ }
1849
+ }
1850
+ },
1851
+ {
1852
+ command: "sendToBack",
1853
+ id: "sendToBack",
1854
+ title: "Send Image to Back",
1855
+ handler: (id) => {
1856
+ const index = tool.items.findIndex((item) => item.id === id);
1857
+ if (index > 0) {
1858
+ const next = [...tool.items];
1859
+ const [item] = next.splice(index, 1);
1860
+ next.unshift(item);
1861
+ tool.updateConfig(next);
1862
+ }
1863
+ }
1864
+ }
1865
+ ];
1866
+ }
1867
+
1868
+ // src/extensions/image/config.ts
1869
+ function createImageConfigurations() {
1870
+ return [
1871
+ {
1872
+ id: "image.items",
1873
+ type: "array",
1874
+ label: "Images",
1875
+ default: []
1876
+ },
1877
+ {
1878
+ id: "image.debug",
1879
+ type: "boolean",
1880
+ label: "Image Debug Log",
1881
+ default: false
1882
+ },
1883
+ {
1884
+ id: "image.control.cornerSize",
1885
+ type: "number",
1886
+ label: "Image Control Corner Size",
1887
+ min: 4,
1888
+ max: 64,
1889
+ step: 1,
1890
+ default: 14
1891
+ },
1892
+ {
1893
+ id: "image.control.touchCornerSize",
1894
+ type: "number",
1895
+ label: "Image Control Touch Corner Size",
1896
+ min: 8,
1897
+ max: 96,
1898
+ step: 1,
1899
+ default: 24
1900
+ },
1901
+ {
1902
+ id: "image.control.cornerStyle",
1903
+ type: "select",
1904
+ label: "Image Control Corner Style",
1905
+ options: ["circle", "rect"],
1906
+ default: "circle"
1907
+ },
1908
+ {
1909
+ id: "image.control.cornerColor",
1910
+ type: "color",
1911
+ label: "Image Control Corner Color",
1912
+ default: "#ffffff"
1913
+ },
1914
+ {
1915
+ id: "image.control.cornerStrokeColor",
1916
+ type: "color",
1917
+ label: "Image Control Corner Stroke Color",
1918
+ default: "#1677ff"
1919
+ },
1920
+ {
1921
+ id: "image.control.transparentCorners",
1922
+ type: "boolean",
1923
+ label: "Image Control Transparent Corners",
1924
+ default: false
1925
+ },
1926
+ {
1927
+ id: "image.control.borderColor",
1928
+ type: "color",
1929
+ label: "Image Control Border Color",
1930
+ default: "#1677ff"
1931
+ },
1932
+ {
1933
+ id: "image.control.borderScaleFactor",
1934
+ type: "number",
1935
+ label: "Image Control Border Width",
1936
+ min: 0.5,
1937
+ max: 8,
1938
+ step: 0.1,
1939
+ default: 1.5
1940
+ },
1941
+ {
1942
+ id: "image.control.padding",
1943
+ type: "number",
1944
+ label: "Image Control Padding",
1945
+ min: 0,
1946
+ max: 64,
1947
+ step: 1,
1948
+ default: 0
1949
+ },
1950
+ {
1951
+ id: "image.frame.strokeColor",
1952
+ type: "color",
1953
+ label: "Image Frame Stroke Color",
1954
+ default: "#808080"
1955
+ },
1956
+ {
1957
+ id: "image.frame.strokeWidth",
1958
+ type: "number",
1959
+ label: "Image Frame Stroke Width",
1960
+ min: 0,
1961
+ max: 20,
1962
+ step: 0.5,
1963
+ default: 2
1964
+ },
1965
+ {
1966
+ id: "image.frame.strokeStyle",
1967
+ type: "select",
1968
+ label: "Image Frame Stroke Style",
1969
+ options: ["solid", "dashed", "hidden"],
1970
+ default: "dashed"
1971
+ },
1972
+ {
1973
+ id: "image.frame.dashLength",
1974
+ type: "number",
1975
+ label: "Image Frame Dash Length",
1976
+ min: 1,
1977
+ max: 40,
1978
+ step: 1,
1979
+ default: 8
1980
+ },
1981
+ {
1982
+ id: "image.frame.innerBackground",
1983
+ type: "color",
1984
+ label: "Image Frame Inner Background",
1985
+ default: "rgba(0,0,0,0)"
1986
+ },
1987
+ {
1988
+ id: "image.frame.outerBackground",
1989
+ type: "color",
1990
+ label: "Image Frame Outer Background",
1991
+ default: "#f5f5f5"
1992
+ }
1993
+ ];
1994
+ }
1995
+
1996
+ // src/extensions/image/ImageTool.ts
1997
+ var IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
1998
+ "rotate",
1999
+ "scale"
2000
+ ];
2001
+ var IMAGE_CONTROL_DESCRIPTORS = [
2002
+ {
2003
+ key: "tl",
2004
+ capability: "rotate",
2005
+ create: () => new Control({
2006
+ x: -0.5,
2007
+ y: -0.5,
2008
+ actionName: "rotate",
2009
+ actionHandler: controlsUtils.rotationWithSnapping,
2010
+ cursorStyleHandler: controlsUtils.rotationStyleHandler
2011
+ })
2012
+ },
2013
+ {
2014
+ key: "br",
2015
+ capability: "scale",
2016
+ create: () => new Control({
2017
+ x: 0.5,
2018
+ y: 0.5,
2019
+ actionName: "scale",
2020
+ actionHandler: controlsUtils.scalingEqually,
2021
+ cursorStyleHandler: controlsUtils.scaleCursorStyleHandler
2022
+ })
2023
+ }
2024
+ ];
1553
2025
  var ImageTool = class {
1554
2026
  constructor() {
1555
2027
  this.id = "pooder.kit.image";
@@ -1560,7 +2032,9 @@ var ImageTool = class {
1560
2032
  this.workingItems = [];
1561
2033
  this.hasWorkingChanges = false;
1562
2034
  this.loadResolvers = /* @__PURE__ */ new Map();
1563
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
2035
+ this.sourceSizeCache = createSourceSizeCache(
2036
+ (src) => this.loadImageSize(src)
2037
+ );
1564
2038
  this.isUpdatingConfig = false;
1565
2039
  this.isToolActive = false;
1566
2040
  this.isImageSelectionActive = false;
@@ -1568,6 +2042,8 @@ var ImageTool = class {
1568
2042
  this.renderSeq = 0;
1569
2043
  this.imageSpecs = [];
1570
2044
  this.overlaySpecs = [];
2045
+ this.subscriptions = new SubscriptionBag();
2046
+ this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
1571
2047
  this.onToolActivated = (event) => {
1572
2048
  const before = this.isToolActive;
1573
2049
  this.syncToolActiveFromWorkbench(event.id);
@@ -1664,6 +2140,7 @@ var ImageTool = class {
1664
2140
  }
1665
2141
  activate(context) {
1666
2142
  var _a;
2143
+ this.subscriptions.disposeAll();
1667
2144
  this.context = context;
1668
2145
  this.canvasService = context.services.get("CanvasService");
1669
2146
  if (!this.canvasService) {
@@ -1705,37 +2182,55 @@ var ImageTool = class {
1705
2182
  }),
1706
2183
  { priority: 300 }
1707
2184
  );
1708
- context.eventBus.on("tool:activated", this.onToolActivated);
1709
- context.eventBus.on("object:modified", this.onObjectModified);
1710
- context.eventBus.on("selection:created", this.onSelectionChanged);
1711
- context.eventBus.on("selection:updated", this.onSelectionChanged);
1712
- context.eventBus.on("selection:cleared", this.onSelectionCleared);
1713
- context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
1714
- context.eventBus.on("scene:geometry:change", this.onSceneGeometryChanged);
2185
+ this.subscriptions.on(context.eventBus, "tool:activated", this.onToolActivated);
2186
+ this.subscriptions.on(context.eventBus, "object:modified", this.onObjectModified);
2187
+ this.subscriptions.on(
2188
+ context.eventBus,
2189
+ "selection:created",
2190
+ this.onSelectionChanged
2191
+ );
2192
+ this.subscriptions.on(
2193
+ context.eventBus,
2194
+ "selection:updated",
2195
+ this.onSelectionChanged
2196
+ );
2197
+ this.subscriptions.on(
2198
+ context.eventBus,
2199
+ "selection:cleared",
2200
+ this.onSelectionCleared
2201
+ );
2202
+ this.subscriptions.on(
2203
+ context.eventBus,
2204
+ "scene:layout:change",
2205
+ this.onSceneLayoutChanged
2206
+ );
2207
+ this.subscriptions.on(
2208
+ context.eventBus,
2209
+ "scene:geometry:change",
2210
+ this.onSceneGeometryChanged
2211
+ );
1715
2212
  const configService = context.services.get(
1716
2213
  "ConfigurationService"
1717
2214
  );
1718
2215
  if (configService) {
1719
- this.items = this.normalizeItems(
1720
- configService.get("image.items", []) || []
1721
- );
1722
- this.workingItems = this.cloneItems(this.items);
1723
- this.hasWorkingChanges = false;
1724
- configService.onAnyChange((e) => {
1725
- if (this.isUpdatingConfig) return;
1726
- if (e.key === "image.items") {
1727
- this.items = this.normalizeItems(e.value || []);
1728
- if (!this.isToolActive || !this.hasWorkingChanges) {
1729
- this.workingItems = this.cloneItems(this.items);
1730
- this.hasWorkingChanges = false;
2216
+ this.applyCommittedItems(configService.get("image.items", []) || []);
2217
+ this.subscriptions.onConfigChange(
2218
+ configService,
2219
+ (e) => {
2220
+ if (this.isUpdatingConfig) return;
2221
+ if (e.key === "image.items") {
2222
+ this.applyCommittedItems(e.value || []);
2223
+ this.updateImages();
2224
+ return;
2225
+ }
2226
+ if (e.key.startsWith("size.") || e.key.startsWith("image.frame.") || e.key.startsWith("image.control.")) {
2227
+ if (e.key.startsWith("image.control.")) {
2228
+ this.imageControlsByCapabilityKey.clear();
2229
+ }
2230
+ this.updateImages();
1731
2231
  }
1732
- this.updateImages();
1733
- return;
1734
- }
1735
- if (e.key.startsWith("size.") || e.key.startsWith("image.frame.")) {
1736
- this.updateImages();
1737
2232
  }
1738
- });
2233
+ );
1739
2234
  }
1740
2235
  const toolSessionService = context.services.get("ToolSessionService");
1741
2236
  this.dirtyTrackerDisposable = toolSessionService == null ? void 0 : toolSessionService.registerDirtyTracker(
@@ -1746,20 +2241,16 @@ var ImageTool = class {
1746
2241
  }
1747
2242
  deactivate(context) {
1748
2243
  var _a, _b;
1749
- context.eventBus.off("tool:activated", this.onToolActivated);
1750
- context.eventBus.off("object:modified", this.onObjectModified);
1751
- context.eventBus.off("selection:created", this.onSelectionChanged);
1752
- context.eventBus.off("selection:updated", this.onSelectionChanged);
1753
- context.eventBus.off("selection:cleared", this.onSelectionCleared);
1754
- context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
1755
- context.eventBus.off("scene:geometry:change", this.onSceneGeometryChanged);
2244
+ this.subscriptions.disposeAll();
1756
2245
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
1757
2246
  this.dirtyTrackerDisposable = void 0;
1758
2247
  this.cropShapeHatchPattern = void 0;
1759
2248
  this.cropShapeHatchPatternColor = void 0;
1760
2249
  this.cropShapeHatchPatternKey = void 0;
2250
+ this.sourceSizeCache.clear();
1761
2251
  this.imageSpecs = [];
1762
2252
  this.overlaySpecs = [];
2253
+ this.imageControlsByCapabilityKey.clear();
1763
2254
  this.clearRenderedImages();
1764
2255
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
1765
2256
  this.renderProducerDisposable = void 0;
@@ -1782,6 +2273,90 @@ var ImageTool = class {
1782
2273
  isImageEditingVisible() {
1783
2274
  return this.isToolActive || this.isImageSelectionActive || !!this.focusedImageId;
1784
2275
  }
2276
+ getEnabledImageControlCapabilities() {
2277
+ return IMAGE_DEFAULT_CONTROL_CAPABILITIES;
2278
+ }
2279
+ getImageControls(capabilities) {
2280
+ const normalized = [...new Set(capabilities)].sort();
2281
+ const cacheKey = normalized.join("|");
2282
+ const cached = this.imageControlsByCapabilityKey.get(cacheKey);
2283
+ if (cached) {
2284
+ return cached;
2285
+ }
2286
+ const enabled = new Set(normalized);
2287
+ const controls = {};
2288
+ IMAGE_CONTROL_DESCRIPTORS.forEach((descriptor) => {
2289
+ if (!enabled.has(descriptor.capability)) return;
2290
+ controls[descriptor.key] = descriptor.create();
2291
+ });
2292
+ this.imageControlsByCapabilityKey.set(cacheKey, controls);
2293
+ return controls;
2294
+ }
2295
+ getImageControlVisualConfig() {
2296
+ var _a, _b, _c, _d;
2297
+ const cornerSizeRaw = Number(
2298
+ (_a = this.getConfig("image.control.cornerSize", 14)) != null ? _a : 14
2299
+ );
2300
+ const touchCornerSizeRaw = Number(
2301
+ (_b = this.getConfig("image.control.touchCornerSize", 24)) != null ? _b : 24
2302
+ );
2303
+ const borderScaleFactorRaw = Number(
2304
+ (_c = this.getConfig("image.control.borderScaleFactor", 1.5)) != null ? _c : 1.5
2305
+ );
2306
+ const paddingRaw = Number(
2307
+ (_d = this.getConfig("image.control.padding", 0)) != null ? _d : 0
2308
+ );
2309
+ const cornerStyleRaw = this.getConfig(
2310
+ "image.control.cornerStyle",
2311
+ "circle"
2312
+ ) || "circle";
2313
+ const cornerStyle = cornerStyleRaw === "rect" ? "rect" : "circle";
2314
+ return {
2315
+ cornerSize: Number.isFinite(cornerSizeRaw) ? Math.max(4, Math.min(64, cornerSizeRaw)) : 14,
2316
+ touchCornerSize: Number.isFinite(touchCornerSizeRaw) ? Math.max(8, Math.min(96, touchCornerSizeRaw)) : 24,
2317
+ cornerStyle,
2318
+ cornerColor: this.getConfig("image.control.cornerColor", "#ffffff") || "#ffffff",
2319
+ cornerStrokeColor: this.getConfig("image.control.cornerStrokeColor", "#1677ff") || "#1677ff",
2320
+ transparentCorners: !!this.getConfig(
2321
+ "image.control.transparentCorners",
2322
+ false
2323
+ ),
2324
+ borderColor: this.getConfig("image.control.borderColor", "#1677ff") || "#1677ff",
2325
+ borderScaleFactor: Number.isFinite(borderScaleFactorRaw) ? Math.max(0.5, Math.min(8, borderScaleFactorRaw)) : 1.5,
2326
+ padding: Number.isFinite(paddingRaw) ? Math.max(0, Math.min(64, paddingRaw)) : 0
2327
+ };
2328
+ }
2329
+ applyImageObjectInteractionState(obj) {
2330
+ var _a;
2331
+ if (!obj) return;
2332
+ const visible = this.isImageEditingVisible();
2333
+ const visual = this.getImageControlVisualConfig();
2334
+ obj.set({
2335
+ selectable: visible,
2336
+ evented: visible,
2337
+ hasControls: visible,
2338
+ hasBorders: visible,
2339
+ lockScalingFlip: true,
2340
+ cornerSize: visual.cornerSize,
2341
+ touchCornerSize: visual.touchCornerSize,
2342
+ cornerStyle: visual.cornerStyle,
2343
+ cornerColor: visual.cornerColor,
2344
+ cornerStrokeColor: visual.cornerStrokeColor,
2345
+ transparentCorners: visual.transparentCorners,
2346
+ borderColor: visual.borderColor,
2347
+ borderScaleFactor: visual.borderScaleFactor,
2348
+ padding: visual.padding
2349
+ });
2350
+ obj.controls = this.getImageControls(
2351
+ this.getEnabledImageControlCapabilities()
2352
+ );
2353
+ (_a = obj.setCoords) == null ? void 0 : _a.call(obj);
2354
+ }
2355
+ refreshImageObjectInteractionState() {
2356
+ this.getImageObjects().forEach(
2357
+ (obj) => this.applyImageObjectInteractionState(obj)
2358
+ );
2359
+ }
1785
2360
  isDebugEnabled() {
1786
2361
  return !!this.getConfig("image.debug", false);
1787
2362
  }
@@ -1811,205 +2386,8 @@ var ImageTool = class {
1811
2386
  }
1812
2387
  }
1813
2388
  ],
1814
- [ContributionPointIds2.CONFIGURATIONS]: [
1815
- {
1816
- id: "image.items",
1817
- type: "array",
1818
- label: "Images",
1819
- default: []
1820
- },
1821
- {
1822
- id: "image.debug",
1823
- type: "boolean",
1824
- label: "Image Debug Log",
1825
- default: false
1826
- },
1827
- {
1828
- id: "image.frame.strokeColor",
1829
- type: "color",
1830
- label: "Image Frame Stroke Color",
1831
- default: "#808080"
1832
- },
1833
- {
1834
- id: "image.frame.strokeWidth",
1835
- type: "number",
1836
- label: "Image Frame Stroke Width",
1837
- min: 0,
1838
- max: 20,
1839
- step: 0.5,
1840
- default: 2
1841
- },
1842
- {
1843
- id: "image.frame.strokeStyle",
1844
- type: "select",
1845
- label: "Image Frame Stroke Style",
1846
- options: ["solid", "dashed", "hidden"],
1847
- default: "dashed"
1848
- },
1849
- {
1850
- id: "image.frame.dashLength",
1851
- type: "number",
1852
- label: "Image Frame Dash Length",
1853
- min: 1,
1854
- max: 40,
1855
- step: 1,
1856
- default: 8
1857
- },
1858
- {
1859
- id: "image.frame.innerBackground",
1860
- type: "color",
1861
- label: "Image Frame Inner Background",
1862
- default: "rgba(0,0,0,0)"
1863
- },
1864
- {
1865
- id: "image.frame.outerBackground",
1866
- type: "color",
1867
- label: "Image Frame Outer Background",
1868
- default: "#f5f5f5"
1869
- }
1870
- ],
1871
- [ContributionPointIds2.COMMANDS]: [
1872
- {
1873
- command: "addImage",
1874
- title: "Add Image",
1875
- handler: async (url, options) => {
1876
- const result = await this.upsertImageEntry(url, {
1877
- mode: "add",
1878
- addOptions: options
1879
- });
1880
- return result.id;
1881
- }
1882
- },
1883
- {
1884
- command: "upsertImage",
1885
- title: "Upsert Image",
1886
- handler: async (url, options = {}) => {
1887
- return await this.upsertImageEntry(url, options);
1888
- }
1889
- },
1890
- {
1891
- command: "getWorkingImages",
1892
- title: "Get Working Images",
1893
- handler: () => {
1894
- return this.cloneItems(this.workingItems);
1895
- }
1896
- },
1897
- {
1898
- command: "setWorkingImage",
1899
- title: "Set Working Image",
1900
- handler: (id, updates) => {
1901
- this.updateImageInWorking(id, updates);
1902
- }
1903
- },
1904
- {
1905
- command: "resetWorkingImages",
1906
- title: "Reset Working Images",
1907
- handler: () => {
1908
- this.workingItems = this.cloneItems(this.items);
1909
- this.hasWorkingChanges = false;
1910
- this.updateImages();
1911
- this.emitWorkingChange();
1912
- }
1913
- },
1914
- {
1915
- command: "completeImages",
1916
- title: "Complete Images",
1917
- handler: async () => {
1918
- return await this.commitWorkingImagesAsCropped();
1919
- }
1920
- },
1921
- {
1922
- command: "exportUserCroppedImage",
1923
- title: "Export User Cropped Image",
1924
- handler: async (options = {}) => {
1925
- return await this.exportUserCroppedImage(options);
1926
- }
1927
- },
1928
- {
1929
- command: "fitImageToArea",
1930
- title: "Fit Image to Area",
1931
- handler: async (id, area) => {
1932
- await this.fitImageToArea(id, area);
1933
- }
1934
- },
1935
- {
1936
- command: "fitImageToDefaultArea",
1937
- title: "Fit Image to Default Area",
1938
- handler: async (id) => {
1939
- await this.fitImageToDefaultArea(id);
1940
- }
1941
- },
1942
- {
1943
- command: "focusImage",
1944
- title: "Focus Image",
1945
- handler: (id, options = {}) => {
1946
- return this.setImageFocus(id, options);
1947
- }
1948
- },
1949
- {
1950
- command: "removeImage",
1951
- title: "Remove Image",
1952
- handler: (id) => {
1953
- const removed = this.items.find((item) => item.id === id);
1954
- const next = this.items.filter((item) => item.id !== id);
1955
- if (next.length !== this.items.length) {
1956
- this.purgeSourceSizeCacheForItem(removed);
1957
- if (this.focusedImageId === id) {
1958
- this.setImageFocus(null, {
1959
- syncCanvasSelection: true,
1960
- skipRender: true
1961
- });
1962
- }
1963
- this.updateConfig(next);
1964
- }
1965
- }
1966
- },
1967
- {
1968
- command: "updateImage",
1969
- title: "Update Image",
1970
- handler: async (id, updates, options = {}) => {
1971
- await this.updateImage(id, updates, options);
1972
- }
1973
- },
1974
- {
1975
- command: "clearImages",
1976
- title: "Clear Images",
1977
- handler: () => {
1978
- this.sourceSizeBySrc.clear();
1979
- this.setImageFocus(null, {
1980
- syncCanvasSelection: true,
1981
- skipRender: true
1982
- });
1983
- this.updateConfig([]);
1984
- }
1985
- },
1986
- {
1987
- command: "bringToFront",
1988
- title: "Bring Image to Front",
1989
- handler: (id) => {
1990
- const index = this.items.findIndex((item) => item.id === id);
1991
- if (index !== -1 && index < this.items.length - 1) {
1992
- const next = [...this.items];
1993
- const [item] = next.splice(index, 1);
1994
- next.push(item);
1995
- this.updateConfig(next);
1996
- }
1997
- }
1998
- },
1999
- {
2000
- command: "sendToBack",
2001
- title: "Send Image to Back",
2002
- handler: (id) => {
2003
- const index = this.items.findIndex((item) => item.id === id);
2004
- if (index > 0) {
2005
- const next = [...this.items];
2006
- const [item] = next.splice(index, 1);
2007
- next.unshift(item);
2008
- this.updateConfig(next);
2009
- }
2010
- }
2011
- }
2012
- ]
2389
+ [ContributionPointIds2.CONFIGURATIONS]: createImageConfigurations(),
2390
+ [ContributionPointIds2.COMMANDS]: createImageCommands(this)
2013
2391
  };
2014
2392
  }
2015
2393
  normalizeItem(item) {
@@ -2061,12 +2439,7 @@ var ImageTool = class {
2061
2439
  } else {
2062
2440
  const obj = this.getImageObject(id);
2063
2441
  if (obj) {
2064
- obj.set({
2065
- selectable: true,
2066
- evented: true,
2067
- hasControls: true,
2068
- hasBorders: true
2069
- });
2442
+ this.applyImageObjectInteractionState(obj);
2070
2443
  canvas.setActiveObject(obj);
2071
2444
  }
2072
2445
  }
@@ -2140,47 +2513,45 @@ var ImageTool = class {
2140
2513
  if (!configService) return fallback;
2141
2514
  return (_a = configService.get(key, fallback)) != null ? _a : fallback;
2142
2515
  }
2516
+ applyCommittedItems(nextItems) {
2517
+ const session = {
2518
+ committed: this.items,
2519
+ working: this.workingItems,
2520
+ hasWorkingChanges: this.hasWorkingChanges
2521
+ };
2522
+ applyCommittedSnapshot(session, this.normalizeItems(nextItems), {
2523
+ clone: (items) => this.cloneItems(items),
2524
+ toolActive: this.isToolActive,
2525
+ preserveDirtyWorking: true
2526
+ });
2527
+ this.items = session.committed;
2528
+ this.workingItems = session.working;
2529
+ this.hasWorkingChanges = session.hasWorkingChanges;
2530
+ }
2143
2531
  updateConfig(newItems, skipCanvasUpdate = false) {
2144
2532
  if (!this.context) return;
2145
- this.isUpdatingConfig = true;
2146
- this.items = this.normalizeItems(newItems);
2147
- if (!this.isToolActive || !this.hasWorkingChanges) {
2148
- this.workingItems = this.cloneItems(this.items);
2149
- this.hasWorkingChanges = false;
2150
- }
2151
- const configService = this.context.services.get(
2152
- "ConfigurationService"
2533
+ this.applyCommittedItems(newItems);
2534
+ runDeferredConfigUpdate(
2535
+ this,
2536
+ () => {
2537
+ var _a;
2538
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2539
+ "ConfigurationService"
2540
+ );
2541
+ configService == null ? void 0 : configService.update("image.items", this.items);
2542
+ if (!skipCanvasUpdate) {
2543
+ this.updateImages();
2544
+ }
2545
+ },
2546
+ 50
2153
2547
  );
2154
- configService == null ? void 0 : configService.update("image.items", this.items);
2155
- if (!skipCanvasUpdate) {
2156
- this.updateImages();
2157
- }
2158
- setTimeout(() => {
2159
- this.isUpdatingConfig = false;
2160
- }, 50);
2161
2548
  }
2162
2549
  getFrameRect() {
2163
2550
  var _a;
2164
- if (!this.canvasService) {
2165
- return { left: 0, top: 0, width: 0, height: 0 };
2166
- }
2167
2551
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2168
2552
  "ConfigurationService"
2169
2553
  );
2170
- if (!configService) {
2171
- return { left: 0, top: 0, width: 0, height: 0 };
2172
- }
2173
- const sizeState = readSizeState(configService);
2174
- const layout = computeSceneLayout(this.canvasService, sizeState);
2175
- if (!layout) {
2176
- return { left: 0, top: 0, width: 0, height: 0 };
2177
- }
2178
- return this.canvasService.toSceneRect({
2179
- left: layout.cutRect.left,
2180
- top: layout.cutRect.top,
2181
- width: layout.cutRect.width,
2182
- height: layout.cutRect.height
2183
- });
2554
+ return resolveCutFrameRect(this.canvasService, configService);
2184
2555
  }
2185
2556
  getFrameRectScreen(frame) {
2186
2557
  if (!this.canvasService) {
@@ -2189,13 +2560,7 @@ var ImageTool = class {
2189
2560
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
2190
2561
  }
2191
2562
  toLayoutSceneRect(rect) {
2192
- return {
2193
- left: rect.left,
2194
- top: rect.top,
2195
- width: rect.width,
2196
- height: rect.height,
2197
- space: "scene"
2198
- };
2563
+ return toLayoutSceneRect(rect);
2199
2564
  }
2200
2565
  async resolveDefaultFitArea() {
2201
2566
  if (!this.canvasService) return null;
@@ -2253,31 +2618,31 @@ var ImageTool = class {
2253
2618
  const sources = [item.url, item.sourceUrl, item.committedUrl].filter(
2254
2619
  (value) => typeof value === "string" && value.length > 0
2255
2620
  );
2256
- sources.forEach((src) => this.sourceSizeBySrc.delete(src));
2621
+ sources.forEach((src) => this.sourceSizeCache.deleteSourceSize(src));
2257
2622
  }
2258
2623
  rememberSourceSize(src, obj) {
2259
2624
  const width = Number((obj == null ? void 0 : obj.width) || 0);
2260
2625
  const height = Number((obj == null ? void 0 : obj.height) || 0);
2261
2626
  if (src && width > 0 && height > 0) {
2262
- this.sourceSizeBySrc.set(src, { width, height });
2627
+ this.sourceSizeCache.rememberSourceSize(src, { width, height });
2263
2628
  }
2264
2629
  }
2265
2630
  getSourceSize(src, obj) {
2266
- const cached = src ? this.sourceSizeBySrc.get(src) : void 0;
2631
+ const cached = src ? this.sourceSizeCache.getSourceSize(src) : void 0;
2267
2632
  if (cached) return cached;
2268
2633
  const width = Number((obj == null ? void 0 : obj.width) || 0);
2269
2634
  const height = Number((obj == null ? void 0 : obj.height) || 0);
2270
2635
  if (src && width > 0 && height > 0) {
2271
2636
  const size = { width, height };
2272
- this.sourceSizeBySrc.set(src, size);
2637
+ this.sourceSizeCache.rememberSourceSize(src, size);
2273
2638
  return size;
2274
2639
  }
2275
2640
  return { width: 1, height: 1 };
2276
2641
  }
2277
2642
  async ensureSourceSize(src) {
2278
- if (!src) return null;
2279
- const cached = this.sourceSizeBySrc.get(src);
2280
- if (cached) return cached;
2643
+ return this.sourceSizeCache.ensureImageSize(src);
2644
+ }
2645
+ async loadImageSize(src) {
2281
2646
  try {
2282
2647
  const image = await FabricImage2.fromURL(src, {
2283
2648
  crossOrigin: "anonymous"
@@ -2285,9 +2650,7 @@ var ImageTool = class {
2285
2650
  const width = Number((image == null ? void 0 : image.width) || 0);
2286
2651
  const height = Number((image == null ? void 0 : image.height) || 0);
2287
2652
  if (width > 0 && height > 0) {
2288
- const size = { width, height };
2289
- this.sourceSizeBySrc.set(src, size);
2290
- return size;
2653
+ return { width, height };
2291
2654
  }
2292
2655
  } catch (error) {
2293
2656
  this.debug("image:size:load-failed", {
@@ -2298,11 +2661,7 @@ var ImageTool = class {
2298
2661
  return null;
2299
2662
  }
2300
2663
  getCoverScale(frame, size) {
2301
- const sw = Math.max(1, size.width);
2302
- const sh = Math.max(1, size.height);
2303
- const fw = Math.max(1, frame.width);
2304
- const fh = Math.max(1, frame.height);
2305
- return Math.max(fw / sw, fh / sh);
2664
+ return getCoverScale(frame, size);
2306
2665
  }
2307
2666
  getFrameVisualConfig() {
2308
2667
  var _a, _b;
@@ -2863,6 +3222,7 @@ var ImageTool = class {
2863
3222
  this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2864
3223
  await this.canvasService.flushRenderFromProducers();
2865
3224
  if (seq !== this.renderSeq) return;
3225
+ this.refreshImageObjectInteractionState();
2866
3226
  renderItems.forEach((item) => {
2867
3227
  if (!this.getImageObject(item.id)) return;
2868
3228
  const resolver = this.loadResolvers.get(item.id);
@@ -3053,7 +3413,7 @@ var ImageTool = class {
3053
3413
  })
3054
3414
  );
3055
3415
  if (previousCommitted && previousCommitted !== url) {
3056
- this.sourceSizeBySrc.delete(previousCommitted);
3416
+ this.sourceSizeCache.deleteSourceSize(previousCommitted);
3057
3417
  }
3058
3418
  }
3059
3419
  this.hasWorkingChanges = false;
@@ -3149,7 +3509,7 @@ var ImageTool = class {
3149
3509
  }
3150
3510
  };
3151
3511
 
3152
- // src/extensions/size.ts
3512
+ // src/extensions/size/SizeTool.ts
3153
3513
  import {
3154
3514
  ContributionPointIds as ContributionPointIds3
3155
3515
  } from "@pooder/core";
@@ -3369,1140 +3729,324 @@ var SizeTool = class {
3369
3729
  const anchor = changed === "height" ? nextHeightMm : changed === "width" ? nextWidthMm : (_b = providedWidthMm != null ? providedWidthMm : providedHeightMm) != null ? _b : nextWidthMm;
3370
3730
  nextWidthMm = anchor;
3371
3731
  nextHeightMm = anchor;
3372
- } else if (state.constraintMode === "lockAspect") {
3373
- const ratio = Math.max(1e-4, state.aspectRatio);
3374
- if (changed === "height") {
3375
- nextWidthMm = nextHeightMm * ratio;
3376
- } else {
3377
- nextHeightMm = nextWidthMm / ratio;
3378
- }
3379
- }
3380
- nextWidthMm = sanitizeMmValue(nextWidthMm, limits);
3381
- nextHeightMm = sanitizeMmValue(nextHeightMm, limits);
3382
- if (state.constraintMode === "equal") {
3383
- const value = Math.max(nextWidthMm, nextHeightMm);
3384
- nextWidthMm = value;
3385
- nextHeightMm = value;
3386
- } else if (state.constraintMode === "lockAspect") {
3387
- const ratio = Math.max(1e-4, state.aspectRatio);
3388
- if (changed === "height") {
3389
- nextWidthMm = sanitizeMmValue(nextHeightMm * ratio, limits);
3390
- } else {
3391
- nextHeightMm = sanitizeMmValue(nextWidthMm / ratio, limits);
3392
- }
3393
- }
3394
- configService.update("size.actualWidthMm", nextWidthMm);
3395
- configService.update("size.actualHeightMm", nextHeightMm);
3396
- configService.update("size.unit", inputUnit);
3397
- this.emitStateChanged();
3398
- return this.getStateForUI();
3399
- }
3400
- setConstraintMode(modeRaw) {
3401
- const configService = this.getConfigService();
3402
- if (!configService) return null;
3403
- const state = readSizeState(configService);
3404
- const mode = normalizeConstraintMode(modeRaw);
3405
- configService.update("size.constraintMode", mode);
3406
- if (mode === "lockAspect") {
3407
- const ratio = state.actualWidthMm / Math.max(1e-3, state.actualHeightMm);
3408
- configService.update("size.aspectRatio", ratio);
3409
- }
3410
- if (mode === "equal") {
3411
- const value = sanitizeMmValue(
3412
- Math.max(state.actualWidthMm, state.actualHeightMm),
3413
- {
3414
- minMm: state.minMm,
3415
- maxMm: state.maxMm,
3416
- stepMm: state.stepMm
3417
- }
3418
- );
3419
- configService.update("size.actualWidthMm", value);
3420
- configService.update("size.actualHeightMm", value);
3421
- configService.update("size.aspectRatio", 1);
3422
- }
3423
- this.emitStateChanged();
3424
- return this.getStateForUI();
3425
- }
3426
- setUnit(unitRaw) {
3427
- const configService = this.getConfigService();
3428
- if (!configService) return null;
3429
- const unit = normalizeUnit(unitRaw);
3430
- configService.update("size.unit", unit);
3431
- this.emitStateChanged();
3432
- return this.getStateForUI();
3433
- }
3434
- setCut(cutModeRaw, cutMarginMm = 0) {
3435
- const configService = this.getConfigService();
3436
- if (!configService) return null;
3437
- const cutMode = normalizeCutMode(cutModeRaw);
3438
- const margin = Math.max(0, Number(cutMarginMm) || 0);
3439
- configService.update("size.cutMode", cutMode);
3440
- configService.update("size.cutMarginMm", margin);
3441
- this.emitStateChanged();
3442
- return this.getStateForUI();
3443
- }
3444
- getSelectedImageSize(id) {
3445
- var _a, _b, _c;
3446
- const configService = this.getConfigService();
3447
- if (!configService || !this.canvasService) return null;
3448
- const sizeState = readSizeState(configService);
3449
- const layout = computeSceneLayout(this.canvasService, sizeState);
3450
- if (!layout || layout.scale <= 0) return null;
3451
- const all = this.canvasService.canvas.getObjects();
3452
- const active = this.canvasService.canvas.getActiveObject();
3453
- const activeId = ((_a = active == null ? void 0 : active.data) == null ? void 0 : _a.layerId) === "image.user" ? (_b = active == null ? void 0 : active.data) == null ? void 0 : _b.id : null;
3454
- const targetId = id || activeId;
3455
- const target = all.find(
3456
- (obj) => {
3457
- var _a2, _b2;
3458
- return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === "image.user" && ((_b2 = obj == null ? void 0 : obj.data) == null ? void 0 : _b2.id) === targetId;
3459
- }
3460
- ) || all.find((obj) => {
3461
- var _a2;
3462
- return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === "image.user";
3463
- });
3464
- if (!target) return null;
3465
- const objectWidthPx = Math.abs((target.width || 0) * (target.scaleX || 1));
3466
- const objectHeightPx = Math.abs(
3467
- (target.height || 0) * (target.scaleY || 1)
3468
- );
3469
- if (objectWidthPx <= 0 || objectHeightPx <= 0) return null;
3470
- const widthMm = objectWidthPx / layout.scale;
3471
- const heightMm = objectHeightPx / layout.scale;
3472
- return {
3473
- id: ((_c = target == null ? void 0 : target.data) == null ? void 0 : _c.id) || null,
3474
- widthMm,
3475
- heightMm,
3476
- width: fromMm(widthMm, sizeState.unit),
3477
- height: fromMm(heightMm, sizeState.unit),
3478
- unit: sizeState.unit
3479
- };
3480
- }
3481
- };
3482
-
3483
- // src/extensions/dieline.ts
3484
- import {
3485
- ContributionPointIds as ContributionPointIds4
3486
- } from "@pooder/core";
3487
- import { Canvas as FabricCanvas2, Path, Pattern as Pattern2 } from "fabric";
3488
-
3489
- // src/extensions/tracer.ts
3490
- import paper2 from "paper";
3491
-
3492
- // src/extensions/maskOps.ts
3493
- function createMask(imageData, options) {
3494
- const { width, height, data } = imageData;
3495
- const {
3496
- threshold,
3497
- padding,
3498
- paddedWidth,
3499
- paddedHeight,
3500
- maskMode = "auto",
3501
- whiteThreshold = 240,
3502
- alphaOpaqueCutoff = 250
3503
- } = options;
3504
- const resolvedMode = maskMode === "auto" ? inferMaskMode(imageData, alphaOpaqueCutoff) : maskMode;
3505
- const mask = new Uint8Array(paddedWidth * paddedHeight);
3506
- for (let y = 0; y < height; y++) {
3507
- for (let x = 0; x < width; x++) {
3508
- const srcIdx = (y * width + x) * 4;
3509
- const r = data[srcIdx];
3510
- const g = data[srcIdx + 1];
3511
- const b = data[srcIdx + 2];
3512
- const a = data[srcIdx + 3];
3513
- const destIdx = (y + padding) * paddedWidth + (x + padding);
3514
- if (resolvedMode === "alpha") {
3515
- if (a > threshold) mask[destIdx] = 1;
3516
- } else {
3517
- if (a > threshold && !(r > whiteThreshold && g > whiteThreshold && b > whiteThreshold)) {
3518
- mask[destIdx] = 1;
3519
- }
3520
- }
3521
- }
3522
- }
3523
- return mask;
3524
- }
3525
- function inferMaskMode(imageData, alphaOpaqueCutoff) {
3526
- const analysis = analyzeAlpha(imageData, alphaOpaqueCutoff);
3527
- if (analysis.minAlpha === 255) return "whitebg";
3528
- if (analysis.veryTransparentRatio >= 5e-4) return "alpha";
3529
- if (analysis.belowOpaqueRatio >= 0.01) return "alpha";
3530
- return "whitebg";
3531
- }
3532
- function analyzeAlpha(imageData, alphaOpaqueCutoff) {
3533
- const { data } = imageData;
3534
- const total = data.length / 4;
3535
- let belowOpaque = 0;
3536
- let veryTransparent = 0;
3537
- let minAlpha = 255;
3538
- for (let i = 3; i < data.length; i += 4) {
3539
- const a = data[i];
3540
- if (a < minAlpha) minAlpha = a;
3541
- if (a < alphaOpaqueCutoff) belowOpaque++;
3542
- if (a < 32) veryTransparent++;
3543
- }
3544
- return {
3545
- total,
3546
- minAlpha,
3547
- belowOpaqueRatio: belowOpaque / total,
3548
- veryTransparentRatio: veryTransparent / total
3549
- };
3550
- }
3551
- function circularMorphology(mask, width, height, radius, op) {
3552
- const r = Math.max(0, Math.floor(radius));
3553
- if (r <= 0) {
3554
- return mask.slice();
3555
- }
3556
- const dilateDisk = (m, radiusPx) => {
3557
- const horizontalDist = new Int32Array(width * height);
3558
- for (let y = 0; y < height; y++) {
3559
- let lastSolid = -radiusPx * 2;
3560
- for (let x = 0; x < width; x++) {
3561
- if (m[y * width + x]) lastSolid = x;
3562
- horizontalDist[y * width + x] = x - lastSolid;
3563
- }
3564
- lastSolid = width + radiusPx * 2;
3565
- for (let x = width - 1; x >= 0; x--) {
3566
- if (m[y * width + x]) lastSolid = x;
3567
- horizontalDist[y * width + x] = Math.min(
3568
- horizontalDist[y * width + x],
3569
- lastSolid - x
3570
- );
3571
- }
3572
- }
3573
- const result = new Uint8Array(width * height);
3574
- const r2 = radiusPx * radiusPx;
3575
- for (let x = 0; x < width; x++) {
3576
- for (let y = 0; y < height; y++) {
3577
- let found = false;
3578
- const minY = Math.max(0, y - radiusPx);
3579
- const maxY = Math.min(height - 1, y + radiusPx);
3580
- for (let dy = minY; dy <= maxY; dy++) {
3581
- const dY = dy - y;
3582
- const hDist = horizontalDist[dy * width + x];
3583
- if (hDist * hDist + dY * dY <= r2) {
3584
- found = true;
3585
- break;
3586
- }
3587
- }
3588
- if (found) result[y * width + x] = 1;
3589
- }
3590
- }
3591
- return result;
3592
- };
3593
- const erodeDiamond = (m, radiusPx) => {
3594
- if (radiusPx <= 0) return m.slice();
3595
- let current = m;
3596
- for (let step = 0; step < radiusPx; step++) {
3597
- const next = new Uint8Array(width * height);
3598
- for (let y = 1; y < height - 1; y++) {
3599
- const row = y * width;
3600
- for (let x = 1; x < width - 1; x++) {
3601
- const idx = row + x;
3602
- if (current[idx] && current[idx - 1] && current[idx + 1] && current[idx - width] && current[idx + width]) {
3603
- next[idx] = 1;
3604
- }
3605
- }
3606
- }
3607
- current = next;
3608
- }
3609
- return current;
3610
- };
3611
- const restoreBridgePixels = (source, eroded) => {
3612
- const restored = eroded.slice();
3613
- for (let y = 1; y < height - 1; y++) {
3614
- const row = y * width;
3615
- for (let x = 1; x < width - 1; x++) {
3616
- const idx = row + x;
3617
- if (!source[idx] || restored[idx]) continue;
3618
- const up = source[idx - width] === 1;
3619
- const down = source[idx + width] === 1;
3620
- const left = source[idx - 1] === 1;
3621
- const right = source[idx + 1] === 1;
3622
- const upLeft = source[idx - width - 1] === 1;
3623
- const upRight = source[idx - width + 1] === 1;
3624
- const downLeft = source[idx + width - 1] === 1;
3625
- const downRight = source[idx + width + 1] === 1;
3626
- const keepsBridge = left && right || up && down || upLeft && downRight || upRight && downLeft;
3627
- if (keepsBridge) {
3628
- restored[idx] = 1;
3629
- }
3630
- }
3631
- }
3632
- return restored;
3633
- };
3634
- const erodePreservingBridges = (m, radiusPx) => {
3635
- const eroded = erodeDiamond(m, radiusPx);
3636
- return restoreBridgePixels(m, eroded);
3637
- };
3638
- switch (op) {
3639
- case "dilate":
3640
- return dilateDisk(mask, r);
3641
- case "erode":
3642
- return erodePreservingBridges(mask, r);
3643
- case "closing": {
3644
- const erodeRadius = Math.max(1, Math.floor(r * 0.65));
3645
- return erodePreservingBridges(dilateDisk(mask, r), erodeRadius);
3646
- }
3647
- case "opening":
3648
- return dilateDisk(erodePreservingBridges(mask, r), r);
3649
- default:
3650
- return mask;
3651
- }
3652
- }
3653
- function fillHoles(mask, width, height) {
3654
- const background = new Uint8Array(width * height);
3655
- const queue = [];
3656
- for (let x = 0; x < width; x++) {
3657
- if (mask[x] === 0) {
3658
- background[x] = 1;
3659
- queue.push(x);
3660
- }
3661
- const lastRowIdx = (height - 1) * width + x;
3662
- if (mask[lastRowIdx] === 0) {
3663
- background[lastRowIdx] = 1;
3664
- queue.push(lastRowIdx);
3665
- }
3666
- }
3667
- for (let y = 1; y < height - 1; y++) {
3668
- const leftIdx = y * width;
3669
- const rightIdx = y * width + (width - 1);
3670
- if (mask[leftIdx] === 0) {
3671
- background[leftIdx] = 1;
3672
- queue.push(leftIdx);
3673
- }
3674
- if (mask[rightIdx] === 0) {
3675
- background[rightIdx] = 1;
3676
- queue.push(rightIdx);
3677
- }
3678
- }
3679
- let head = 0;
3680
- while (head < queue.length) {
3681
- const idx = queue[head++];
3682
- const x = idx % width;
3683
- const y = (idx - x) / width;
3684
- const up = y > 0 ? idx - width : -1;
3685
- const down = y < height - 1 ? idx + width : -1;
3686
- const left = x > 0 ? idx - 1 : -1;
3687
- const right = x < width - 1 ? idx + 1 : -1;
3688
- if (up >= 0 && mask[up] === 0 && background[up] === 0) {
3689
- background[up] = 1;
3690
- queue.push(up);
3691
- }
3692
- if (down >= 0 && mask[down] === 0 && background[down] === 0) {
3693
- background[down] = 1;
3694
- queue.push(down);
3695
- }
3696
- if (left >= 0 && mask[left] === 0 && background[left] === 0) {
3697
- background[left] = 1;
3698
- queue.push(left);
3699
- }
3700
- if (right >= 0 && mask[right] === 0 && background[right] === 0) {
3701
- background[right] = 1;
3702
- queue.push(right);
3703
- }
3704
- }
3705
- const filledMask = new Uint8Array(width * height);
3706
- for (let i = 0; i < width * height; i++) {
3707
- filledMask[i] = background[i] === 0 ? 1 : 0;
3708
- }
3709
- return filledMask;
3710
- }
3711
- function polygonSignedArea(points) {
3712
- if (points.length < 3) return 0;
3713
- let sum = 0;
3714
- for (let i = 0; i < points.length; i++) {
3715
- const a = points[i];
3716
- const b = points[(i + 1) % points.length];
3717
- sum += a.x * b.y - b.x * a.y;
3718
- }
3719
- return sum / 2;
3720
- }
3721
-
3722
- // src/extensions/tracer.ts
3723
- var ImageTracer = class {
3724
- /**
3725
- * Main entry point: Traces an image URL to an SVG path string.
3726
- * @param imageUrl The URL or Base64 string of the image.
3727
- * @param options Configuration options.
3728
- */
3729
- static async trace(imageUrl, options = {}) {
3730
- const { pathData } = await this.traceWithBounds(imageUrl, options);
3731
- return pathData;
3732
- }
3733
- static async traceWithBounds(imageUrl, options = {}) {
3734
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
3735
- const img = await this.loadImage(imageUrl);
3736
- const width = img.width;
3737
- const height = img.height;
3738
- if (width <= 0 || height <= 0) {
3739
- const w = (_a = options.scaleToWidth) != null ? _a : 0;
3740
- const h = (_b = options.scaleToHeight) != null ? _b : 0;
3741
- return {
3742
- pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
3743
- baseBounds: { x: 0, y: 0, width: w, height: h },
3744
- bounds: { x: 0, y: 0, width: w, height: h }
3745
- };
3746
- }
3747
- const debug = options.debug === true;
3748
- const debugLog = (message, payload) => {
3749
- if (!debug) return;
3750
- if (payload) {
3751
- console.info(`[ImageTracer] ${message}`, payload);
3752
- return;
3753
- }
3754
- console.info(`[ImageTracer] ${message}`);
3755
- };
3756
- const canvas = document.createElement("canvas");
3757
- canvas.width = width;
3758
- canvas.height = height;
3759
- const ctx = canvas.getContext("2d");
3760
- if (!ctx) throw new Error("Could not get 2D context");
3761
- ctx.drawImage(img, 0, 0);
3762
- const imageData = ctx.getImageData(0, 0, width, height);
3763
- const threshold = (_c = options.threshold) != null ? _c : 10;
3764
- const expand = Math.max(0, Math.floor((_d = options.expand) != null ? _d : 0));
3765
- const simplifyTolerance = (_e = options.simplifyTolerance) != null ? _e : 2.5;
3766
- const useSmoothing = options.smoothing !== false;
3767
- const componentMode = "all";
3768
- const minComponentArea = 0;
3769
- const maxDim = Math.max(width, height);
3770
- const maskMode = "auto";
3771
- const whiteThreshold = 240;
3772
- const alphaOpaqueCutoff = 250;
3773
- const preprocessDilateRadius = Math.max(
3774
- 2,
3775
- Math.floor(Math.max(maxDim * 0.012, expand * 0.35))
3776
- );
3777
- const preprocessErodeRadius = Math.max(
3778
- 1,
3779
- Math.floor(preprocessDilateRadius * 0.65)
3780
- );
3781
- const smoothDilateRadius = Math.max(
3782
- 1,
3783
- Math.floor(preprocessDilateRadius * 0.25)
3784
- );
3785
- const smoothErodeRadius = Math.max(1, Math.floor(smoothDilateRadius * 0.8));
3786
- const connectStartDilateRadius = Math.max(
3787
- 1,
3788
- Math.floor(Math.max(maxDim * 6e-3, expand * 0.2))
3789
- );
3790
- const connectMaxDilateRadius = Math.max(
3791
- connectStartDilateRadius,
3792
- Math.floor(Math.max(maxDim * 0.2, expand * 2.5))
3793
- );
3794
- const connectErodeRatio = 0.65;
3795
- debugLog("traceWithBounds:start", {
3796
- width,
3797
- height,
3798
- threshold,
3799
- expand,
3800
- simplifyTolerance,
3801
- smoothing: useSmoothing,
3802
- strategy: {
3803
- maskMode,
3804
- whiteThreshold,
3805
- alphaOpaqueCutoff,
3806
- fillHoles: true,
3807
- preprocessDilateRadius,
3808
- preprocessErodeRadius,
3809
- smoothDilateRadius,
3810
- smoothErodeRadius,
3811
- connectEnabled: true,
3812
- connectStartDilateRadius,
3813
- connectMaxDilateRadius,
3814
- connectErodeRatio
3815
- }
3816
- });
3817
- const padding = Math.max(
3818
- preprocessDilateRadius,
3819
- smoothDilateRadius,
3820
- connectMaxDilateRadius,
3821
- expand
3822
- ) + 2;
3823
- const paddedWidth = width + padding * 2;
3824
- const paddedHeight = height + padding * 2;
3825
- const summarizeMaskContours = (m) => {
3826
- const summary = this.summarizeAllContours(
3827
- m,
3828
- paddedWidth,
3829
- paddedHeight,
3830
- minComponentArea
3831
- );
3832
- return {
3833
- rawContourCount: summary.rawCount,
3834
- selectedContourCount: summary.selectedCount
3835
- };
3836
- };
3837
- let mask = createMask(imageData, {
3838
- threshold,
3839
- padding,
3840
- paddedWidth,
3841
- paddedHeight,
3842
- maskMode,
3843
- whiteThreshold,
3844
- alphaOpaqueCutoff
3845
- });
3846
- if (debug) {
3847
- debugLog(
3848
- "traceWithBounds:mask:after-create",
3849
- summarizeMaskContours(mask)
3850
- );
3851
- }
3852
- mask = circularMorphology(
3853
- mask,
3854
- paddedWidth,
3855
- paddedHeight,
3856
- preprocessDilateRadius,
3857
- "dilate"
3858
- );
3859
- mask = fillHoles(mask, paddedWidth, paddedHeight);
3860
- mask = circularMorphology(
3861
- mask,
3862
- paddedWidth,
3863
- paddedHeight,
3864
- preprocessErodeRadius,
3865
- "erode"
3866
- );
3867
- mask = fillHoles(mask, paddedWidth, paddedHeight);
3868
- if (debug) {
3869
- debugLog("traceWithBounds:mask:after-preprocess", {
3870
- dilateRadius: preprocessDilateRadius,
3871
- erodeRadius: preprocessErodeRadius,
3872
- ...summarizeMaskContours(mask)
3873
- });
3874
- }
3875
- mask = circularMorphology(
3876
- mask,
3877
- paddedWidth,
3878
- paddedHeight,
3879
- smoothDilateRadius,
3880
- "dilate"
3881
- );
3882
- mask = fillHoles(mask, paddedWidth, paddedHeight);
3883
- mask = circularMorphology(
3884
- mask,
3885
- paddedWidth,
3886
- paddedHeight,
3887
- smoothErodeRadius,
3888
- "erode"
3889
- );
3890
- mask = fillHoles(mask, paddedWidth, paddedHeight);
3891
- if (debug) {
3892
- debugLog("traceWithBounds:mask:after-smooth", {
3893
- dilateRadius: smoothDilateRadius,
3894
- erodeRadius: smoothErodeRadius,
3895
- ...summarizeMaskContours(mask)
3896
- });
3897
- }
3898
- const beforeConnectSummary = summarizeMaskContours(mask);
3899
- if (beforeConnectSummary.selectedContourCount <= 1) {
3900
- debugLog("traceWithBounds:mask:connect-skipped", {
3901
- reason: "already-single-component",
3902
- before: beforeConnectSummary
3903
- });
3904
- } else {
3905
- const connectResult = this.findForceConnectResult(
3906
- mask,
3907
- paddedWidth,
3908
- paddedHeight,
3909
- minComponentArea,
3910
- connectStartDilateRadius,
3911
- connectMaxDilateRadius,
3912
- connectErodeRatio
3913
- );
3914
- if (debug) {
3915
- debugLog("traceWithBounds:mask:after-connect", {
3916
- before: beforeConnectSummary,
3917
- appliedDilateRadius: connectResult.appliedDilateRadius,
3918
- appliedErodeRadius: connectResult.appliedErodeRadius,
3919
- reachedSingleComponent: connectResult.reachedSingleComponent,
3920
- after: {
3921
- rawContourCount: connectResult.rawContourCount,
3922
- selectedContourCount: connectResult.selectedContourCount
3923
- }
3924
- });
3925
- }
3926
- mask = connectResult.mask;
3927
- }
3928
- if (debug) {
3929
- const afterConnectSummary = summarizeMaskContours(mask);
3930
- if (afterConnectSummary.selectedContourCount > 1) {
3931
- debugLog("traceWithBounds:mask:connect-warning", {
3932
- reason: "still-multi-component-after-connect-search",
3933
- summary: afterConnectSummary
3934
- });
3935
- }
3936
- }
3937
- const baseMask = mask;
3938
- const baseContoursRaw = this.traceAllContours(
3939
- baseMask,
3940
- paddedWidth,
3941
- paddedHeight
3942
- );
3943
- const baseContours = this.selectContours(
3944
- baseContoursRaw,
3945
- componentMode,
3946
- minComponentArea
3947
- );
3948
- if (!baseContours.length) {
3949
- const w = (_f = options.scaleToWidth) != null ? _f : width;
3950
- const h = (_g = options.scaleToHeight) != null ? _g : height;
3951
- debugLog("fallback:no-base-contour", { width: w, height: h });
3952
- return {
3953
- pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
3954
- baseBounds: { x: 0, y: 0, width: w, height: h },
3955
- bounds: { x: 0, y: 0, width: w, height: h }
3956
- };
3957
- }
3958
- const baseUnpaddedContours = baseContours.map(
3959
- (contour) => contour.map((p) => ({
3960
- x: p.x - padding,
3961
- y: p.y - padding
3962
- }))
3963
- ).filter((contour) => contour.length > 2);
3964
- if (!baseUnpaddedContours.length) {
3965
- const w = (_h = options.scaleToWidth) != null ? _h : width;
3966
- const h = (_i = options.scaleToHeight) != null ? _i : height;
3967
- debugLog("fallback:empty-base-contours", { width: w, height: h });
3968
- return {
3969
- pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
3970
- baseBounds: { x: 0, y: 0, width: w, height: h },
3971
- bounds: { x: 0, y: 0, width: w, height: h }
3972
- };
3973
- }
3974
- let baseBounds = this.boundsFromPoints(
3975
- this.flattenContours(baseUnpaddedContours)
3976
- );
3977
- let maskExpanded = baseMask;
3978
- if (expand > 0) {
3979
- maskExpanded = circularMorphology(
3980
- baseMask,
3981
- paddedWidth,
3982
- paddedHeight,
3983
- expand,
3984
- "dilate"
3985
- );
3986
- }
3987
- const expandedContoursRaw = this.traceAllContours(
3988
- maskExpanded,
3989
- paddedWidth,
3990
- paddedHeight
3991
- );
3992
- const expandedContours = this.selectContours(
3993
- expandedContoursRaw,
3994
- componentMode,
3995
- minComponentArea
3996
- );
3997
- if (!expandedContours.length) {
3998
- debugLog("fallback:no-expanded-contour", {
3999
- baseBounds,
4000
- width,
4001
- height,
4002
- expand
4003
- });
4004
- return {
4005
- pathData: `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`,
4006
- baseBounds,
4007
- bounds: baseBounds
4008
- };
4009
- }
4010
- const expandedUnpaddedContours = expandedContours.map(
4011
- (contour) => contour.map((p) => ({
4012
- x: p.x - padding,
4013
- y: p.y - padding
4014
- }))
4015
- ).filter((contour) => contour.length > 2);
4016
- if (!expandedUnpaddedContours.length) {
4017
- debugLog("fallback:empty-expanded-contours", {
4018
- baseBounds,
4019
- width,
4020
- height,
4021
- expand
4022
- });
4023
- return {
4024
- pathData: `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`,
4025
- baseBounds,
4026
- bounds: baseBounds
4027
- };
4028
- }
4029
- let globalBounds = this.boundsFromPoints(
4030
- this.flattenContours(expandedUnpaddedContours)
4031
- );
4032
- let finalContours = expandedUnpaddedContours;
4033
- if (options.scaleToWidth && options.scaleToHeight) {
4034
- finalContours = this.scaleContours(
4035
- expandedUnpaddedContours,
4036
- options.scaleToWidth,
4037
- options.scaleToHeight,
4038
- globalBounds
4039
- );
4040
- globalBounds = this.boundsFromPoints(this.flattenContours(finalContours));
4041
- const baseScaledContours = this.scaleContours(
4042
- baseUnpaddedContours,
4043
- options.scaleToWidth,
4044
- options.scaleToHeight,
4045
- baseBounds
4046
- );
4047
- baseBounds = this.boundsFromPoints(
4048
- this.flattenContours(baseScaledContours)
4049
- );
4050
- }
4051
- if (expand > 0) {
4052
- const expectedExpandedBounds = {
4053
- x: baseBounds.x - expand,
4054
- y: baseBounds.y - expand,
4055
- width: baseBounds.width + expand * 2,
4056
- height: baseBounds.height + expand * 2
4057
- };
4058
- if (expectedExpandedBounds.width > 0 && expectedExpandedBounds.height > 0 && globalBounds.width > 0 && globalBounds.height > 0) {
4059
- const shouldNormalizeExpandBounds = Math.abs(globalBounds.x - expectedExpandedBounds.x) > 1 || Math.abs(globalBounds.y - expectedExpandedBounds.y) > 1 || Math.abs(globalBounds.width - expectedExpandedBounds.width) > 1 || Math.abs(globalBounds.height - expectedExpandedBounds.height) > 1;
4060
- if (shouldNormalizeExpandBounds) {
4061
- const beforeNormalize = globalBounds;
4062
- finalContours = this.translateContours(
4063
- this.scaleContours(
4064
- finalContours,
4065
- expectedExpandedBounds.width,
4066
- expectedExpandedBounds.height,
4067
- globalBounds
4068
- ),
4069
- expectedExpandedBounds.x,
4070
- expectedExpandedBounds.y
4071
- );
4072
- globalBounds = this.boundsFromPoints(
4073
- this.flattenContours(finalContours)
4074
- );
4075
- debugLog("traceWithBounds:expand-normalized", {
4076
- expand,
4077
- expectedExpandedBounds,
4078
- beforeNormalize,
4079
- afterNormalize: globalBounds
4080
- });
4081
- }
4082
- }
4083
- }
4084
- debugLog("traceWithBounds:contours", {
4085
- baseContourCount: baseContoursRaw.length,
4086
- baseSelectedCount: baseContours.length,
4087
- expandedContourCount: expandedContoursRaw.length,
4088
- expandedSelectedCount: expandedContours.length,
4089
- baseBounds,
4090
- expandedBounds: globalBounds,
4091
- expandedDeltaX: globalBounds.width - baseBounds.width,
4092
- expandedDeltaY: globalBounds.height - baseBounds.height,
4093
- expandedMayOverflowImageBounds: expand > 0,
4094
- useSmoothing,
4095
- componentMode
4096
- });
4097
- if (useSmoothing) {
4098
- return {
4099
- pathData: this.contoursToSVGPaper(finalContours, simplifyTolerance),
4100
- baseBounds,
4101
- bounds: globalBounds
4102
- };
4103
- } else {
4104
- const simplifiedContours = finalContours.map((points) => this.douglasPeucker(points, simplifyTolerance)).filter((points) => points.length > 2);
4105
- const pathData = this.contoursToSVG(simplifiedContours) || this.contoursToSVG(finalContours);
4106
- return {
4107
- pathData,
4108
- baseBounds,
4109
- bounds: globalBounds
4110
- };
4111
- }
4112
- }
4113
- static pickPrimaryContour(contours) {
4114
- if (contours.length === 0) return null;
4115
- return contours.reduce((best, cur) => {
4116
- if (!best) return cur;
4117
- const bestArea = Math.abs(polygonSignedArea(best));
4118
- const curArea = Math.abs(polygonSignedArea(cur));
4119
- if (curArea !== bestArea) return curArea > bestArea ? cur : best;
4120
- return cur.length > best.length ? cur : best;
4121
- }, contours[0]);
4122
- }
4123
- static flattenContours(contours) {
4124
- return contours.flatMap((contour) => contour);
4125
- }
4126
- static contourCentroid(points) {
4127
- if (!points.length) return { x: 0, y: 0 };
4128
- const sum = points.reduce(
4129
- (acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }),
4130
- { x: 0, y: 0 }
4131
- );
4132
- return {
4133
- x: sum.x / points.length,
4134
- y: sum.y / points.length
4135
- };
4136
- }
4137
- static pointInPolygon(point, polygon) {
4138
- let inside = false;
4139
- const { x, y } = point;
4140
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
4141
- const xi = polygon[i].x;
4142
- const yi = polygon[i].y;
4143
- const xj = polygon[j].x;
4144
- const yj = polygon[j].y;
4145
- const intersects = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi || Number.EPSILON) + xi;
4146
- if (intersects) inside = !inside;
4147
- }
4148
- return inside;
4149
- }
4150
- static keepOutermostContours(contours) {
4151
- if (contours.length <= 1) return contours;
4152
- const sorted = [...contours].sort(
4153
- (a, b) => Math.abs(polygonSignedArea(b)) - Math.abs(polygonSignedArea(a))
4154
- );
4155
- const selected = [];
4156
- for (const contour of sorted) {
4157
- const centroid = this.contourCentroid(contour);
4158
- const isNested = selected.some(
4159
- (outer) => this.pointInPolygon(centroid, outer)
4160
- );
4161
- if (!isNested) {
4162
- selected.push(contour);
4163
- }
4164
- }
4165
- return selected;
4166
- }
4167
- static summarizeAllContours(mask, width, height, minComponentArea) {
4168
- const raw = this.traceAllContours(mask, width, height);
4169
- const selected = this.selectContours(raw, "all", minComponentArea);
4170
- return {
4171
- rawCount: raw.length,
4172
- selectedCount: selected.length
4173
- };
4174
- }
4175
- static findForceConnectResult(sourceMask, width, height, minComponentArea, startDilateRadius, maxDilateRadius, erodeRatio) {
4176
- const initial = this.summarizeAllContours(
4177
- sourceMask,
4178
- width,
4179
- height,
4180
- minComponentArea
4181
- );
4182
- if (initial.selectedCount <= 1) {
4183
- return {
4184
- mask: sourceMask,
4185
- appliedDilateRadius: 0,
4186
- appliedErodeRadius: 0,
4187
- reachedSingleComponent: true,
4188
- rawContourCount: initial.rawCount,
4189
- selectedContourCount: initial.selectedCount
4190
- };
4191
- }
4192
- const normalizedStart = Math.max(1, Math.floor(startDilateRadius));
4193
- const normalizedMax = Math.max(
4194
- normalizedStart,
4195
- Math.floor(maxDilateRadius)
4196
- );
4197
- const normalizedErodeRatio = Math.max(0, erodeRatio);
4198
- const evaluate = (dilateRadius) => {
4199
- const erodeRadius = Math.max(
4200
- 1,
4201
- Math.floor(dilateRadius * normalizedErodeRatio)
4202
- );
4203
- let mask = sourceMask;
4204
- mask = circularMorphology(mask, width, height, dilateRadius, "dilate");
4205
- mask = fillHoles(mask, width, height);
4206
- mask = circularMorphology(mask, width, height, erodeRadius, "erode");
4207
- mask = fillHoles(mask, width, height);
4208
- const summary = this.summarizeAllContours(
4209
- mask,
4210
- width,
4211
- height,
4212
- minComponentArea
4213
- );
4214
- return {
4215
- dilateRadius,
4216
- erodeRadius,
4217
- mask,
4218
- rawCount: summary.rawCount,
4219
- selectedCount: summary.selectedCount
4220
- };
4221
- };
4222
- let low = normalizedStart - 1;
4223
- let high = normalizedStart;
4224
- let highResult = evaluate(high);
4225
- while (high < normalizedMax && highResult.selectedCount > 1) {
4226
- low = high;
4227
- high = Math.min(
4228
- normalizedMax,
4229
- Math.max(high + 1, Math.floor(high * 1.6))
4230
- );
4231
- highResult = evaluate(high);
4232
- }
4233
- if (highResult.selectedCount > 1) {
4234
- return {
4235
- mask: highResult.mask,
4236
- appliedDilateRadius: highResult.dilateRadius,
4237
- appliedErodeRadius: highResult.erodeRadius,
4238
- reachedSingleComponent: false,
4239
- rawContourCount: highResult.rawCount,
4240
- selectedContourCount: highResult.selectedCount
4241
- };
3732
+ } else if (state.constraintMode === "lockAspect") {
3733
+ const ratio = Math.max(1e-4, state.aspectRatio);
3734
+ if (changed === "height") {
3735
+ nextWidthMm = nextHeightMm * ratio;
3736
+ } else {
3737
+ nextHeightMm = nextWidthMm / ratio;
3738
+ }
4242
3739
  }
4243
- let best = highResult;
4244
- while (low + 1 < high) {
4245
- const mid = Math.floor((low + high) / 2);
4246
- const midResult = evaluate(mid);
4247
- if (midResult.selectedCount <= 1) {
4248
- best = midResult;
4249
- high = mid;
3740
+ nextWidthMm = sanitizeMmValue(nextWidthMm, limits);
3741
+ nextHeightMm = sanitizeMmValue(nextHeightMm, limits);
3742
+ if (state.constraintMode === "equal") {
3743
+ const value = Math.max(nextWidthMm, nextHeightMm);
3744
+ nextWidthMm = value;
3745
+ nextHeightMm = value;
3746
+ } else if (state.constraintMode === "lockAspect") {
3747
+ const ratio = Math.max(1e-4, state.aspectRatio);
3748
+ if (changed === "height") {
3749
+ nextWidthMm = sanitizeMmValue(nextHeightMm * ratio, limits);
4250
3750
  } else {
4251
- low = mid;
3751
+ nextHeightMm = sanitizeMmValue(nextWidthMm / ratio, limits);
4252
3752
  }
4253
3753
  }
4254
- return {
4255
- mask: best.mask,
4256
- appliedDilateRadius: best.dilateRadius,
4257
- appliedErodeRadius: best.erodeRadius,
4258
- reachedSingleComponent: true,
4259
- rawContourCount: best.rawCount,
4260
- selectedContourCount: best.selectedCount
4261
- };
3754
+ configService.update("size.actualWidthMm", nextWidthMm);
3755
+ configService.update("size.actualHeightMm", nextHeightMm);
3756
+ configService.update("size.unit", inputUnit);
3757
+ this.emitStateChanged();
3758
+ return this.getStateForUI();
4262
3759
  }
4263
- static selectContours(contours, mode, minComponentArea) {
4264
- if (!contours.length) return [];
4265
- if (mode === "largest") {
4266
- const primary2 = this.pickPrimaryContour(contours);
4267
- return primary2 ? [primary2] : [];
3760
+ setConstraintMode(modeRaw) {
3761
+ const configService = this.getConfigService();
3762
+ if (!configService) return null;
3763
+ const state = readSizeState(configService);
3764
+ const mode = normalizeConstraintMode(modeRaw);
3765
+ configService.update("size.constraintMode", mode);
3766
+ if (mode === "lockAspect") {
3767
+ const ratio = state.actualWidthMm / Math.max(1e-3, state.actualHeightMm);
3768
+ configService.update("size.aspectRatio", ratio);
4268
3769
  }
4269
- const threshold = Math.max(0, minComponentArea);
4270
- if (threshold <= 0) {
4271
- return this.keepOutermostContours(contours);
3770
+ if (mode === "equal") {
3771
+ const value = sanitizeMmValue(
3772
+ Math.max(state.actualWidthMm, state.actualHeightMm),
3773
+ {
3774
+ minMm: state.minMm,
3775
+ maxMm: state.maxMm,
3776
+ stepMm: state.stepMm
3777
+ }
3778
+ );
3779
+ configService.update("size.actualWidthMm", value);
3780
+ configService.update("size.actualHeightMm", value);
3781
+ configService.update("size.aspectRatio", 1);
4272
3782
  }
4273
- const filtered = contours.filter(
4274
- (contour) => Math.abs(polygonSignedArea(contour)) >= threshold
3783
+ this.emitStateChanged();
3784
+ return this.getStateForUI();
3785
+ }
3786
+ setUnit(unitRaw) {
3787
+ const configService = this.getConfigService();
3788
+ if (!configService) return null;
3789
+ const unit = normalizeUnit(unitRaw);
3790
+ configService.update("size.unit", unit);
3791
+ this.emitStateChanged();
3792
+ return this.getStateForUI();
3793
+ }
3794
+ setCut(cutModeRaw, cutMarginMm = 0) {
3795
+ const configService = this.getConfigService();
3796
+ if (!configService) return null;
3797
+ const cutMode = normalizeCutMode(cutModeRaw);
3798
+ const margin = Math.max(0, Number(cutMarginMm) || 0);
3799
+ configService.update("size.cutMode", cutMode);
3800
+ configService.update("size.cutMarginMm", margin);
3801
+ this.emitStateChanged();
3802
+ return this.getStateForUI();
3803
+ }
3804
+ getSelectedImageSize(id) {
3805
+ var _a, _b, _c;
3806
+ const configService = this.getConfigService();
3807
+ if (!configService || !this.canvasService) return null;
3808
+ const sizeState = readSizeState(configService);
3809
+ const layout = computeSceneLayout(this.canvasService, sizeState);
3810
+ if (!layout || layout.scale <= 0) return null;
3811
+ const all = this.canvasService.canvas.getObjects();
3812
+ const active = this.canvasService.canvas.getActiveObject();
3813
+ const activeId = ((_a = active == null ? void 0 : active.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID ? (_b = active == null ? void 0 : active.data) == null ? void 0 : _b.id : null;
3814
+ const targetId = id || activeId;
3815
+ const target = all.find(
3816
+ (obj) => {
3817
+ var _a2, _b2;
3818
+ return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === IMAGE_OBJECT_LAYER_ID && ((_b2 = obj == null ? void 0 : obj.data) == null ? void 0 : _b2.id) === targetId;
3819
+ }
3820
+ ) || all.find((obj) => {
3821
+ var _a2;
3822
+ return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === IMAGE_OBJECT_LAYER_ID;
3823
+ });
3824
+ if (!target) return null;
3825
+ const objectWidthPx = Math.abs((target.width || 0) * (target.scaleX || 1));
3826
+ const objectHeightPx = Math.abs(
3827
+ (target.height || 0) * (target.scaleY || 1)
4275
3828
  );
4276
- if (filtered.length > 0) {
4277
- return this.keepOutermostContours(filtered);
4278
- }
4279
- const primary = this.pickPrimaryContour(contours);
4280
- return primary ? [primary] : [];
4281
- }
4282
- static boundsFromPoints(points) {
4283
- let minX = Infinity;
4284
- let minY = Infinity;
4285
- let maxX = -Infinity;
4286
- let maxY = -Infinity;
4287
- for (const p of points) {
4288
- if (p.x < minX) minX = p.x;
4289
- if (p.y < minY) minY = p.y;
4290
- if (p.x > maxX) maxX = p.x;
4291
- if (p.y > maxY) maxY = p.y;
4292
- }
4293
- if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
4294
- return { x: 0, y: 0, width: 0, height: 0 };
4295
- }
3829
+ if (objectWidthPx <= 0 || objectHeightPx <= 0) return null;
3830
+ const widthMm = objectWidthPx / layout.scale;
3831
+ const heightMm = objectHeightPx / layout.scale;
4296
3832
  return {
4297
- x: minX,
4298
- y: minY,
4299
- width: maxX - minX,
4300
- height: maxY - minY
3833
+ id: ((_c = target == null ? void 0 : target.data) == null ? void 0 : _c.id) || null,
3834
+ widthMm,
3835
+ heightMm,
3836
+ width: fromMm(widthMm, sizeState.unit),
3837
+ height: fromMm(heightMm, sizeState.unit),
3838
+ unit: sizeState.unit
4301
3839
  };
4302
3840
  }
4303
- /**
4304
- * Traces all contours in the mask with optimized start-point detection
4305
- */
4306
- static traceAllContours(mask, width, height) {
4307
- const visited = new Uint8Array(width * height);
4308
- const allContours = [];
4309
- for (let y = 0; y < height; y++) {
4310
- for (let x = 0; x < width; x++) {
4311
- const idx = y * width + x;
4312
- if (mask[idx] && !visited[idx]) {
4313
- const isLeftEdge = x === 0 || mask[idx - 1] === 0;
4314
- if (isLeftEdge) {
4315
- const contour = this.marchingSquares(
4316
- mask,
4317
- visited,
4318
- x,
4319
- y,
4320
- width,
4321
- height
4322
- );
4323
- if (contour.length > 2) {
4324
- allContours.push(contour);
3841
+ };
3842
+
3843
+ // src/extensions/dieline/DielineTool.ts
3844
+ import {
3845
+ ContributionPointIds as ContributionPointIds4
3846
+ } from "@pooder/core";
3847
+ import { Canvas as FabricCanvas2, Path, Pattern as Pattern2 } from "fabric";
3848
+
3849
+ // src/extensions/dieline/commands.ts
3850
+ function createDielineCommands(tool, state) {
3851
+ return [
3852
+ {
3853
+ command: "updateFeaturePosition",
3854
+ id: "updateFeaturePosition",
3855
+ title: "Update Feature Position",
3856
+ handler: (groupId, x, y) => {
3857
+ var _a;
3858
+ const configService = (_a = tool.context) == null ? void 0 : _a.services.get("ConfigurationService");
3859
+ if (!configService) return;
3860
+ const features = configService.get("dieline.features") || [];
3861
+ let changed = false;
3862
+ const newFeatures = features.map((f) => {
3863
+ if (f.groupId === groupId) {
3864
+ if (f.x !== x || f.y !== y) {
3865
+ changed = true;
3866
+ return { ...f, x, y };
4325
3867
  }
4326
3868
  }
3869
+ return f;
3870
+ });
3871
+ if (changed) {
3872
+ configService.update("dieline.features", newFeatures);
4327
3873
  }
4328
3874
  }
4329
- }
4330
- return allContours;
4331
- }
4332
- static loadImage(url) {
4333
- return new Promise((resolve, reject) => {
4334
- const img = new Image();
4335
- img.crossOrigin = "Anonymous";
4336
- img.onload = () => resolve(img);
4337
- img.onerror = (e) => reject(e);
4338
- img.src = url;
4339
- });
4340
- }
4341
- /**
4342
- * Moore-Neighbor Tracing Algorithm
4343
- * More robust for irregular shapes than simple Marching Squares walker.
4344
- */
4345
- static marchingSquares(mask, visited, startX, startY, width, height) {
4346
- const isSolid = (x, y) => {
4347
- if (x < 0 || x >= width || y < 0 || y >= height) return false;
4348
- return mask[y * width + x] === 1;
4349
- };
4350
- const points = [];
4351
- let cx = startX;
4352
- let cy = startY;
4353
- const neighbors = [
4354
- { x: 0, y: -1 },
4355
- { x: 1, y: -1 },
4356
- { x: 1, y: 0 },
4357
- { x: 1, y: 1 },
4358
- { x: 0, y: 1 },
4359
- { x: -1, y: 1 },
4360
- { x: -1, y: 0 },
4361
- { x: -1, y: -1 }
4362
- ];
4363
- let backtrack = 6;
4364
- const maxSteps = width * height * 3;
4365
- let steps = 0;
4366
- do {
4367
- points.push({ x: cx, y: cy });
4368
- visited[cy * width + cx] = 1;
4369
- let found = false;
4370
- for (let i = 0; i < 8; i++) {
4371
- const idx = (backtrack + 1 + i) % 8;
4372
- const nx = cx + neighbors[idx].x;
4373
- const ny = cy + neighbors[idx].y;
4374
- if (isSolid(nx, ny)) {
4375
- cx = nx;
4376
- cy = ny;
4377
- backtrack = (idx + 4 + 1) % 8;
4378
- found = true;
4379
- break;
4380
- }
4381
- }
4382
- if (!found) break;
4383
- steps++;
4384
- } while ((cx !== startX || cy !== startY) && steps < maxSteps);
4385
- return points;
4386
- }
4387
- /**
4388
- * Douglas-Peucker Line Simplification
4389
- */
4390
- static douglasPeucker(points, tolerance) {
4391
- if (points.length <= 2) return points;
4392
- const sqTolerance = tolerance * tolerance;
4393
- let maxSqDist = 0;
4394
- let index = 0;
4395
- const first = points[0];
4396
- const last = points[points.length - 1];
4397
- for (let i = 1; i < points.length - 1; i++) {
4398
- const sqDist = this.getSqSegDist(points[i], first, last);
4399
- if (sqDist > maxSqDist) {
4400
- index = i;
4401
- maxSqDist = sqDist;
3875
+ },
3876
+ {
3877
+ command: "exportCutImage",
3878
+ id: "exportCutImage",
3879
+ title: "Export Cut Image",
3880
+ handler: (options) => {
3881
+ return tool.exportCutImage(options);
4402
3882
  }
4403
- }
4404
- if (maxSqDist > sqTolerance) {
4405
- const left = this.douglasPeucker(points.slice(0, index + 1), tolerance);
4406
- const right = this.douglasPeucker(points.slice(index), tolerance);
4407
- return left.slice(0, left.length - 1).concat(right);
4408
- } else {
4409
- return [first, last];
4410
- }
4411
- }
4412
- static getSqSegDist(p, p1, p2) {
4413
- let x = p1.x;
4414
- let y = p1.y;
4415
- let dx = p2.x - x;
4416
- let dy = p2.y - y;
4417
- if (dx !== 0 || dy !== 0) {
4418
- const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
4419
- if (t > 1) {
4420
- x = p2.x;
4421
- y = p2.y;
4422
- } else if (t > 0) {
4423
- x += dx * t;
4424
- y += dy * t;
3883
+ },
3884
+ {
3885
+ command: "detectEdge",
3886
+ id: "detectEdge",
3887
+ title: "Detect Edge from Image",
3888
+ handler: async (imageUrl, options) => {
3889
+ var _a, _b, _c;
3890
+ try {
3891
+ const detectOptions = options || {};
3892
+ const debug = detectOptions.debug === true;
3893
+ const tracerOptions = {
3894
+ expand: (_a = detectOptions.expand) != null ? _a : 0,
3895
+ smoothing: (_b = detectOptions.smoothing) != null ? _b : true,
3896
+ simplifyTolerance: (_c = detectOptions.simplifyTolerance) != null ? _c : 2,
3897
+ threshold: detectOptions.threshold,
3898
+ debug
3899
+ };
3900
+ const loadImage = (url) => {
3901
+ return new Promise((resolve, reject) => {
3902
+ const img2 = new Image();
3903
+ img2.crossOrigin = "Anonymous";
3904
+ img2.onload = () => resolve(img2);
3905
+ img2.onerror = (e) => reject(e);
3906
+ img2.src = url;
3907
+ });
3908
+ };
3909
+ const [img, traced] = await Promise.all([
3910
+ loadImage(imageUrl),
3911
+ import("./tracer-PO7CRBYY.mjs").then(
3912
+ ({ ImageTracer }) => ImageTracer.traceWithBounds(imageUrl, tracerOptions)
3913
+ )
3914
+ ]);
3915
+ const { pathData, baseBounds, bounds } = traced;
3916
+ if (debug) {
3917
+ console.info("[DielineTool] detectEdge", {
3918
+ imageWidth: img.width,
3919
+ imageHeight: img.height,
3920
+ baseBounds,
3921
+ expandedBounds: bounds,
3922
+ currentDielineWidth: state.width,
3923
+ currentDielineHeight: state.height,
3924
+ options: tracerOptions,
3925
+ strategy: "single-connected-silhouette"
3926
+ });
3927
+ }
3928
+ return {
3929
+ pathData,
3930
+ rawBounds: bounds,
3931
+ baseBounds,
3932
+ imageWidth: img.width,
3933
+ imageHeight: img.height
3934
+ };
3935
+ } catch (e) {
3936
+ console.error("Edge detection failed", e);
3937
+ throw e;
3938
+ }
4425
3939
  }
4426
3940
  }
4427
- dx = p.x - x;
4428
- dy = p.y - y;
4429
- return dx * dx + dy * dy;
4430
- }
4431
- static scalePoints(points, targetWidth, targetHeight, bounds) {
4432
- if (points.length === 0) return points;
4433
- if (bounds.width === 0 || bounds.height === 0) return points;
4434
- const scaleX = targetWidth / bounds.width;
4435
- const scaleY = targetHeight / bounds.height;
4436
- return points.map((p) => ({
4437
- x: (p.x - bounds.x) * scaleX,
4438
- y: (p.y - bounds.y) * scaleY
4439
- }));
4440
- }
4441
- static scaleContours(contours, targetWidth, targetHeight, bounds) {
4442
- return contours.map(
4443
- (points) => this.scalePoints(points, targetWidth, targetHeight, bounds)
4444
- );
4445
- }
4446
- static translateContours(contours, offsetX, offsetY) {
4447
- return contours.map(
4448
- (points) => points.map((p) => ({
4449
- x: p.x + offsetX,
4450
- y: p.y + offsetY
4451
- }))
4452
- );
4453
- }
4454
- static pointsToSVG(points) {
4455
- if (points.length === 0) return "";
4456
- const head = points[0];
4457
- const tail = points.slice(1);
4458
- return `M ${head.x} ${head.y} ` + tail.map((p) => `L ${p.x} ${p.y}`).join(" ") + " Z";
4459
- }
4460
- static contoursToSVG(contours) {
4461
- return contours.filter((points) => points.length > 2).map((points) => this.pointsToSVG(points)).join(" ").trim();
4462
- }
4463
- static ensurePaper() {
4464
- if (!paper2.project) {
4465
- paper2.setup(new paper2.Size(100, 100));
4466
- }
4467
- }
4468
- static pointsToSVGPaper(points, tolerance) {
4469
- if (points.length < 3) return this.pointsToSVG(points);
4470
- this.ensurePaper();
4471
- const path = new paper2.Path({
4472
- segments: points.map((p) => [p.x, p.y]),
4473
- closed: true
4474
- });
4475
- path.simplify(tolerance);
4476
- const data = path.pathData;
4477
- path.remove();
4478
- return data;
4479
- }
4480
- static contoursToSVGPaper(contours, tolerance) {
4481
- const normalizedContours = contours.filter((points) => points.length > 2);
4482
- if (!normalizedContours.length) return "";
4483
- if (normalizedContours.length === 1) {
4484
- return this.pointsToSVGPaper(normalizedContours[0], tolerance);
4485
- }
4486
- this.ensurePaper();
4487
- const compound = new paper2.CompoundPath({ insert: false });
4488
- for (const points of normalizedContours) {
4489
- const child = new paper2.Path({
4490
- segments: points.map((p) => [p.x, p.y]),
4491
- closed: true,
4492
- insert: false
4493
- });
4494
- child.simplify(tolerance);
4495
- compound.addChild(child);
3941
+ ];
3942
+ }
3943
+
3944
+ // src/extensions/dieline/config.ts
3945
+ function createDielineConfigurations(state) {
3946
+ return [
3947
+ {
3948
+ id: "dieline.shape",
3949
+ type: "select",
3950
+ label: "Shape",
3951
+ options: Array.from(DIELINE_SHAPES),
3952
+ default: state.shape
3953
+ },
3954
+ {
3955
+ id: "dieline.radius",
3956
+ type: "number",
3957
+ label: "Corner Radius (mm)",
3958
+ min: 0,
3959
+ max: 500,
3960
+ default: state.radius
3961
+ },
3962
+ {
3963
+ id: "dieline.shapeStyle",
3964
+ type: "json",
3965
+ label: "Shape Style",
3966
+ default: state.shapeStyle
3967
+ },
3968
+ {
3969
+ id: "dieline.showBleedLines",
3970
+ type: "boolean",
3971
+ label: "Show Bleed Lines",
3972
+ default: state.showBleedLines
3973
+ },
3974
+ {
3975
+ id: "dieline.strokeWidth",
3976
+ type: "number",
3977
+ label: "Line Width",
3978
+ min: 0.1,
3979
+ max: 10,
3980
+ step: 0.1,
3981
+ default: state.mainLine.width
3982
+ },
3983
+ {
3984
+ id: "dieline.strokeColor",
3985
+ type: "color",
3986
+ label: "Line Color",
3987
+ default: state.mainLine.color
3988
+ },
3989
+ {
3990
+ id: "dieline.dashLength",
3991
+ type: "number",
3992
+ label: "Dash Length",
3993
+ min: 1,
3994
+ max: 50,
3995
+ default: state.mainLine.dashLength
3996
+ },
3997
+ {
3998
+ id: "dieline.style",
3999
+ type: "select",
4000
+ label: "Line Style",
4001
+ options: ["solid", "dashed", "hidden"],
4002
+ default: state.mainLine.style
4003
+ },
4004
+ {
4005
+ id: "dieline.offsetStrokeWidth",
4006
+ type: "number",
4007
+ label: "Offset Line Width",
4008
+ min: 0.1,
4009
+ max: 10,
4010
+ step: 0.1,
4011
+ default: state.offsetLine.width
4012
+ },
4013
+ {
4014
+ id: "dieline.offsetStrokeColor",
4015
+ type: "color",
4016
+ label: "Offset Line Color",
4017
+ default: state.offsetLine.color
4018
+ },
4019
+ {
4020
+ id: "dieline.offsetDashLength",
4021
+ type: "number",
4022
+ label: "Offset Dash Length",
4023
+ min: 1,
4024
+ max: 50,
4025
+ default: state.offsetLine.dashLength
4026
+ },
4027
+ {
4028
+ id: "dieline.offsetStyle",
4029
+ type: "select",
4030
+ label: "Offset Line Style",
4031
+ options: ["solid", "dashed", "hidden"],
4032
+ default: state.offsetLine.style
4033
+ },
4034
+ {
4035
+ id: "dieline.insideColor",
4036
+ type: "color",
4037
+ label: "Inside Color",
4038
+ default: state.insideColor
4039
+ },
4040
+ {
4041
+ id: "dieline.features",
4042
+ type: "json",
4043
+ label: "Edge Features",
4044
+ default: state.features
4496
4045
  }
4497
- const data = compound.pathData || this.contoursToSVG(normalizedContours);
4498
- compound.remove();
4499
- return data;
4500
- }
4501
- };
4046
+ ];
4047
+ }
4502
4048
 
4503
- // src/extensions/dieline.ts
4504
- var IMAGE_OBJECT_LAYER_ID2 = "image.user";
4505
- var DIELINE_LAYER_ID = "dieline-overlay";
4049
+ // src/extensions/dieline/DielineTool.ts
4506
4050
  var DielineTool = class {
4507
4051
  constructor(options) {
4508
4052
  this.id = "pooder.kit.dieline";
@@ -4743,7 +4287,6 @@ var DielineTool = class {
4743
4287
  this.context = void 0;
4744
4288
  }
4745
4289
  contribute() {
4746
- const s = this.state;
4747
4290
  return {
4748
4291
  [ContributionPointIds4.TOOLS]: [
4749
4292
  {
@@ -4756,195 +4299,8 @@ var DielineTool = class {
4756
4299
  }
4757
4300
  }
4758
4301
  ],
4759
- [ContributionPointIds4.CONFIGURATIONS]: [
4760
- {
4761
- id: "dieline.shape",
4762
- type: "select",
4763
- label: "Shape",
4764
- options: Array.from(DIELINE_SHAPES),
4765
- default: s.shape
4766
- },
4767
- {
4768
- id: "dieline.radius",
4769
- type: "number",
4770
- label: "Corner Radius (mm)",
4771
- min: 0,
4772
- max: 500,
4773
- default: s.radius
4774
- },
4775
- {
4776
- id: "dieline.shapeStyle",
4777
- type: "json",
4778
- label: "Shape Style",
4779
- default: s.shapeStyle
4780
- },
4781
- {
4782
- id: "dieline.showBleedLines",
4783
- type: "boolean",
4784
- label: "Show Bleed Lines",
4785
- default: s.showBleedLines
4786
- },
4787
- {
4788
- id: "dieline.strokeWidth",
4789
- type: "number",
4790
- label: "Line Width",
4791
- min: 0.1,
4792
- max: 10,
4793
- step: 0.1,
4794
- default: s.mainLine.width
4795
- },
4796
- {
4797
- id: "dieline.strokeColor",
4798
- type: "color",
4799
- label: "Line Color",
4800
- default: s.mainLine.color
4801
- },
4802
- {
4803
- id: "dieline.dashLength",
4804
- type: "number",
4805
- label: "Dash Length",
4806
- min: 1,
4807
- max: 50,
4808
- default: s.mainLine.dashLength
4809
- },
4810
- {
4811
- id: "dieline.style",
4812
- type: "select",
4813
- label: "Line Style",
4814
- options: ["solid", "dashed", "hidden"],
4815
- default: s.mainLine.style
4816
- },
4817
- {
4818
- id: "dieline.offsetStrokeWidth",
4819
- type: "number",
4820
- label: "Offset Line Width",
4821
- min: 0.1,
4822
- max: 10,
4823
- step: 0.1,
4824
- default: s.offsetLine.width
4825
- },
4826
- {
4827
- id: "dieline.offsetStrokeColor",
4828
- type: "color",
4829
- label: "Offset Line Color",
4830
- default: s.offsetLine.color
4831
- },
4832
- {
4833
- id: "dieline.offsetDashLength",
4834
- type: "number",
4835
- label: "Offset Dash Length",
4836
- min: 1,
4837
- max: 50,
4838
- default: s.offsetLine.dashLength
4839
- },
4840
- {
4841
- id: "dieline.offsetStyle",
4842
- type: "select",
4843
- label: "Offset Line Style",
4844
- options: ["solid", "dashed", "hidden"],
4845
- default: s.offsetLine.style
4846
- },
4847
- {
4848
- id: "dieline.insideColor",
4849
- type: "color",
4850
- label: "Inside Color",
4851
- default: s.insideColor
4852
- },
4853
- {
4854
- id: "dieline.features",
4855
- type: "json",
4856
- label: "Edge Features",
4857
- default: s.features
4858
- }
4859
- ],
4860
- [ContributionPointIds4.COMMANDS]: [
4861
- {
4862
- command: "updateFeaturePosition",
4863
- title: "Update Feature Position",
4864
- handler: (groupId, x, y) => {
4865
- var _a;
4866
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
4867
- "ConfigurationService"
4868
- );
4869
- if (!configService) return;
4870
- const features = configService.get("dieline.features") || [];
4871
- let changed = false;
4872
- const newFeatures = features.map((f) => {
4873
- if (f.groupId === groupId) {
4874
- if (f.x !== x || f.y !== y) {
4875
- changed = true;
4876
- return { ...f, x, y };
4877
- }
4878
- }
4879
- return f;
4880
- });
4881
- if (changed) {
4882
- configService.update("dieline.features", newFeatures);
4883
- }
4884
- }
4885
- },
4886
- {
4887
- command: "exportCutImage",
4888
- title: "Export Cut Image",
4889
- handler: (options) => {
4890
- return this.exportCutImage(options);
4891
- }
4892
- },
4893
- {
4894
- command: "detectEdge",
4895
- title: "Detect Edge from Image",
4896
- handler: async (imageUrl, options) => {
4897
- var _a, _b, _c;
4898
- try {
4899
- const detectOptions = options || {};
4900
- const debug = detectOptions.debug === true;
4901
- const tracerOptions = {
4902
- expand: (_a = detectOptions.expand) != null ? _a : 0,
4903
- smoothing: (_b = detectOptions.smoothing) != null ? _b : true,
4904
- simplifyTolerance: (_c = detectOptions.simplifyTolerance) != null ? _c : 2,
4905
- threshold: detectOptions.threshold,
4906
- debug
4907
- };
4908
- const loadImage = (url) => {
4909
- return new Promise((resolve, reject) => {
4910
- const img2 = new Image();
4911
- img2.crossOrigin = "Anonymous";
4912
- img2.onload = () => resolve(img2);
4913
- img2.onerror = (e) => reject(e);
4914
- img2.src = url;
4915
- });
4916
- };
4917
- const [img, traced] = await Promise.all([
4918
- loadImage(imageUrl),
4919
- ImageTracer.traceWithBounds(imageUrl, tracerOptions)
4920
- ]);
4921
- const { pathData, baseBounds, bounds } = traced;
4922
- if (debug) {
4923
- console.info("[DielineTool] detectEdge", {
4924
- imageWidth: img.width,
4925
- imageHeight: img.height,
4926
- baseBounds,
4927
- expandedBounds: bounds,
4928
- currentDielineWidth: s.width,
4929
- currentDielineHeight: s.height,
4930
- options: tracerOptions,
4931
- strategy: "single-connected-silhouette"
4932
- });
4933
- }
4934
- return {
4935
- pathData,
4936
- rawBounds: bounds,
4937
- baseBounds,
4938
- imageWidth: img.width,
4939
- imageHeight: img.height
4940
- };
4941
- } catch (e) {
4942
- console.error("Edge detection failed", e);
4943
- throw e;
4944
- }
4945
- }
4946
- }
4947
- ]
4302
+ [ContributionPointIds4.CONFIGURATIONS]: createDielineConfigurations(this.state),
4303
+ [ContributionPointIds4.COMMANDS]: createDielineCommands(this, this.state)
4948
4304
  };
4949
4305
  }
4950
4306
  createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
@@ -5218,7 +4574,11 @@ var DielineTool = class {
5218
4574
  {
5219
4575
  type: "clipPath",
5220
4576
  id: "dieline.clip.image",
5221
- targetPassIds: [IMAGE_OBJECT_LAYER_ID2],
4577
+ visibility: {
4578
+ op: "not",
4579
+ expr: { op: "anySessionActive" }
4580
+ },
4581
+ targetPassIds: [IMAGE_OBJECT_LAYER_ID],
5222
4582
  source: {
5223
4583
  id: "dieline.effect.clip-path",
5224
4584
  type: "path",
@@ -5383,7 +4743,7 @@ var DielineTool = class {
5383
4743
  const exportBounds = pathBounds;
5384
4744
  const sourceImages = this.canvasService.canvas.getObjects().filter((obj) => {
5385
4745
  var _a2;
5386
- return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === IMAGE_OBJECT_LAYER_ID2;
4746
+ return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === IMAGE_OBJECT_LAYER_ID;
5387
4747
  });
5388
4748
  if (!sourceImages.length) {
5389
4749
  console.warn(
@@ -5455,7 +4815,7 @@ var DielineTool = class {
5455
4815
  }
5456
4816
  };
5457
4817
 
5458
- // src/extensions/feature.ts
4818
+ // src/extensions/feature/FeatureTool.ts
5459
4819
  import {
5460
4820
  ContributionPointIds as ContributionPointIds5
5461
4821
  } from "@pooder/core";
@@ -5657,8 +5017,7 @@ function completeFeaturesStrict(features, context, update) {
5657
5017
  return { ok: true };
5658
5018
  }
5659
5019
 
5660
- // src/extensions/feature.ts
5661
- var FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
5020
+ // src/extensions/feature/FeatureTool.ts
5662
5021
  var FEATURE_STROKE_WIDTH = 2;
5663
5022
  var DEFAULT_RECT_SIZE = 10;
5664
5023
  var DEFAULT_CIRCLE_RADIUS = 5;
@@ -5676,6 +5035,7 @@ var FeatureTool = class {
5676
5035
  this.hasWorkingChanges = false;
5677
5036
  this.specs = [];
5678
5037
  this.renderSeq = 0;
5038
+ this.subscriptions = new SubscriptionBag();
5679
5039
  this.handleMoving = null;
5680
5040
  this.handleModified = null;
5681
5041
  this.handleSceneGeometryChange = null;
@@ -5693,6 +5053,7 @@ var FeatureTool = class {
5693
5053
  }
5694
5054
  activate(context) {
5695
5055
  var _a;
5056
+ this.subscriptions.disposeAll();
5696
5057
  this.context = context;
5697
5058
  this.canvasService = context.services.get("CanvasService");
5698
5059
  if (!this.canvasService) {
@@ -5721,29 +5082,32 @@ var FeatureTool = class {
5721
5082
  const features = configService.get("dieline.features", []) || [];
5722
5083
  this.workingFeatures = this.cloneFeatures(features);
5723
5084
  this.hasWorkingChanges = false;
5724
- configService.onAnyChange((e) => {
5725
- if (this.isUpdatingConfig) return;
5726
- if (e.key === "dieline.features") {
5727
- if (this.isFeatureSessionActive) return;
5728
- const next = e.value || [];
5729
- this.workingFeatures = this.cloneFeatures(next);
5730
- this.hasWorkingChanges = false;
5731
- this.redraw();
5732
- this.emitWorkingChange();
5085
+ this.subscriptions.onConfigChange(
5086
+ configService,
5087
+ (e) => {
5088
+ if (this.isUpdatingConfig) return;
5089
+ if (e.key === "dieline.features") {
5090
+ if (this.isFeatureSessionActive) return;
5091
+ const next = e.value || [];
5092
+ this.workingFeatures = this.cloneFeatures(next);
5093
+ this.hasWorkingChanges = false;
5094
+ this.redraw();
5095
+ this.emitWorkingChange();
5096
+ }
5733
5097
  }
5734
- });
5098
+ );
5735
5099
  }
5736
5100
  const toolSessionService = context.services.get("ToolSessionService");
5737
5101
  this.dirtyTrackerDisposable = toolSessionService == null ? void 0 : toolSessionService.registerDirtyTracker(
5738
5102
  this.id,
5739
5103
  () => this.hasWorkingChanges
5740
5104
  );
5741
- context.eventBus.on("tool:activated", this.onToolActivated);
5105
+ this.subscriptions.on(context.eventBus, "tool:activated", this.onToolActivated);
5742
5106
  this.setup();
5743
5107
  }
5744
5108
  deactivate(context) {
5745
5109
  var _a;
5746
- context.eventBus.off("tool:activated", this.onToolActivated);
5110
+ this.subscriptions.disposeAll();
5747
5111
  this.restoreSessionFeaturesToConfig();
5748
5112
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
5749
5113
  this.dirtyTrackerDisposable = void 0;
@@ -5886,7 +5250,7 @@ var FeatureTool = class {
5886
5250
  };
5887
5251
  }
5888
5252
  cloneFeatures(features) {
5889
- return JSON.parse(JSON.stringify(features || []));
5253
+ return cloneWithJson(features || []);
5890
5254
  }
5891
5255
  getConfigService() {
5892
5256
  var _a;
@@ -6570,12 +5934,11 @@ var FeatureTool = class {
6570
5934
  }
6571
5935
  };
6572
5936
 
6573
- // src/extensions/film.ts
5937
+ // src/extensions/film/FilmTool.ts
6574
5938
  import {
6575
5939
  ContributionPointIds as ContributionPointIds6
6576
5940
  } from "@pooder/core";
6577
5941
  import { FabricImage as FabricImage3 } from "fabric";
6578
- var FILM_LAYER_ID = "overlay";
6579
5942
  var FILM_IMAGE_ID = "film-image";
6580
5943
  var DEFAULT_WIDTH2 = 800;
6581
5944
  var DEFAULT_HEIGHT2 = 600;
@@ -6590,8 +5953,10 @@ var FilmTool = class {
6590
5953
  this.specs = [];
6591
5954
  this.renderSeq = 0;
6592
5955
  this.renderImageUrl = "";
6593
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
6594
- this.pendingSizeBySrc = /* @__PURE__ */ new Map();
5956
+ this.sourceSizeCache = createSourceSizeCache(
5957
+ (src) => this.loadImageSize(src)
5958
+ );
5959
+ this.subscriptions = new SubscriptionBag();
6595
5960
  this.onCanvasResized = () => {
6596
5961
  this.updateFilm();
6597
5962
  };
@@ -6601,6 +5966,7 @@ var FilmTool = class {
6601
5966
  }
6602
5967
  activate(context) {
6603
5968
  var _a;
5969
+ this.subscriptions.disposeAll();
6604
5970
  this.canvasService = context.services.get("CanvasService");
6605
5971
  if (!this.canvasService) {
6606
5972
  console.warn("CanvasService not found for FilmTool");
@@ -6627,28 +5993,32 @@ var FilmTool = class {
6627
5993
  if (configService) {
6628
5994
  this.url = configService.get("film.url", this.url);
6629
5995
  this.opacity = configService.get("film.opacity", this.opacity);
6630
- configService.onAnyChange((e) => {
6631
- if (e.key.startsWith("film.")) {
6632
- const prop = e.key.split(".")[1];
6633
- console.log(
6634
- `[FilmTool] Config change detected: ${e.key} -> ${e.value}`
6635
- );
6636
- if (prop && prop in this) {
6637
- this[prop] = e.value;
6638
- this.updateFilm();
5996
+ this.subscriptions.onConfigChange(
5997
+ configService,
5998
+ (e) => {
5999
+ if (e.key.startsWith("film.")) {
6000
+ const prop = e.key.split(".")[1];
6001
+ console.log(
6002
+ `[FilmTool] Config change detected: ${e.key} -> ${e.value}`
6003
+ );
6004
+ if (prop && prop in this) {
6005
+ this[prop] = e.value;
6006
+ this.updateFilm();
6007
+ }
6639
6008
  }
6640
6009
  }
6641
- });
6010
+ );
6642
6011
  }
6643
- context.eventBus.on("canvas:resized", this.onCanvasResized);
6012
+ this.subscriptions.on(context.eventBus, "canvas:resized", this.onCanvasResized);
6644
6013
  this.updateFilm();
6645
6014
  }
6646
6015
  deactivate(context) {
6647
6016
  var _a;
6648
- context.eventBus.off("canvas:resized", this.onCanvasResized);
6017
+ this.subscriptions.disposeAll();
6649
6018
  this.renderSeq += 1;
6650
6019
  this.specs = [];
6651
6020
  this.renderImageUrl = "";
6021
+ this.sourceSizeCache.clear();
6652
6022
  (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6653
6023
  this.renderProducerDisposable = void 0;
6654
6024
  if (!this.canvasService) return;
@@ -6707,10 +6077,13 @@ var FilmTool = class {
6707
6077
  return [];
6708
6078
  }
6709
6079
  const { width, height } = this.getViewportSize();
6710
- const sourceSize = this.sourceSizeBySrc.get(imageUrl);
6080
+ const sourceSize = this.sourceSizeCache.getSourceSize(imageUrl);
6711
6081
  const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
6712
6082
  const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
6713
- const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
6083
+ const coverScale = getCoverScale(
6084
+ { width, height },
6085
+ { width: sourceWidth, height: sourceHeight }
6086
+ );
6714
6087
  return [
6715
6088
  {
6716
6089
  id: FILM_IMAGE_ID,
@@ -6737,24 +6110,6 @@ var FilmTool = class {
6737
6110
  }
6738
6111
  ];
6739
6112
  }
6740
- async ensureImageSize(src) {
6741
- if (!src) return null;
6742
- const cached = this.sourceSizeBySrc.get(src);
6743
- if (cached) return cached;
6744
- const pending = this.pendingSizeBySrc.get(src);
6745
- if (pending) {
6746
- return pending;
6747
- }
6748
- const task = this.loadImageSize(src);
6749
- this.pendingSizeBySrc.set(src, task);
6750
- try {
6751
- return await task;
6752
- } finally {
6753
- if (this.pendingSizeBySrc.get(src) === task) {
6754
- this.pendingSizeBySrc.delete(src);
6755
- }
6756
- }
6757
- }
6758
6113
  async loadImageSize(src) {
6759
6114
  try {
6760
6115
  const image = await FabricImage3.fromURL(src, {
@@ -6763,9 +6118,7 @@ var FilmTool = class {
6763
6118
  const width = Number((image == null ? void 0 : image.width) || 0);
6764
6119
  const height = Number((image == null ? void 0 : image.height) || 0);
6765
6120
  if (width > 0 && height > 0) {
6766
- const size = { width, height };
6767
- this.sourceSizeBySrc.set(src, size);
6768
- return size;
6121
+ return { width, height };
6769
6122
  }
6770
6123
  } catch (error) {
6771
6124
  console.error("[FilmTool] Failed to load film image", src, error);
@@ -6782,7 +6135,7 @@ var FilmTool = class {
6782
6135
  if (!nextUrl) {
6783
6136
  this.renderImageUrl = "";
6784
6137
  } else if (nextUrl !== this.renderImageUrl) {
6785
- const loaded = await this.ensureImageSize(nextUrl);
6138
+ const loaded = await this.sourceSizeCache.ensureImageSize(nextUrl);
6786
6139
  if (seq !== this.renderSeq) return;
6787
6140
  if (loaded) {
6788
6141
  this.renderImageUrl = nextUrl;
@@ -6795,7 +6148,7 @@ var FilmTool = class {
6795
6148
  }
6796
6149
  };
6797
6150
 
6798
- // src/extensions/mirror.ts
6151
+ // src/extensions/mirror/MirrorTool.ts
6799
6152
  import {
6800
6153
  ContributionPointIds as ContributionPointIds7
6801
6154
  } from "@pooder/core";
@@ -6887,11 +6240,10 @@ var MirrorTool = class {
6887
6240
  }
6888
6241
  };
6889
6242
 
6890
- // src/extensions/ruler.ts
6243
+ // src/extensions/ruler/RulerTool.ts
6891
6244
  import {
6892
6245
  ContributionPointIds as ContributionPointIds8
6893
6246
  } from "@pooder/core";
6894
- var RULER_LAYER_ID = "ruler-overlay";
6895
6247
  var EXTENSION_LINE_LENGTH = 5;
6896
6248
  var MIN_ARROW_SIZE = 4;
6897
6249
  var THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
@@ -7453,17 +6805,197 @@ var RulerTool = class {
7453
6805
  }
7454
6806
  };
7455
6807
 
7456
- // src/extensions/white-ink.ts
6808
+ // src/extensions/white-ink/WhiteInkTool.ts
7457
6809
  import {
7458
6810
  ContributionPointIds as ContributionPointIds9
7459
6811
  } from "@pooder/core";
7460
- var WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
7461
- var WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
7462
- var WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
7463
- var IMAGE_OBJECT_LAYER_ID3 = "image.user";
7464
- var WHITE_INK_DEBUG_KEY = "whiteInk.debug";
6812
+
6813
+ // src/extensions/white-ink/commands.ts
7465
6814
  var WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
7466
6815
  var WHITE_INK_DEFAULT_OPACITY = 0.85;
6816
+ function createWhiteInkCommands(tool) {
6817
+ return [
6818
+ {
6819
+ command: "addWhiteInk",
6820
+ id: "addWhiteInk",
6821
+ title: "Add White Ink",
6822
+ handler: async (url, options) => {
6823
+ return await tool.addWhiteInkEntry(url, options);
6824
+ }
6825
+ },
6826
+ {
6827
+ command: "upsertWhiteInk",
6828
+ id: "upsertWhiteInk",
6829
+ title: "Upsert White Ink",
6830
+ handler: async (url, options = {}) => {
6831
+ return await tool.upsertWhiteInkEntry(url, options);
6832
+ }
6833
+ },
6834
+ {
6835
+ command: "getWhiteInks",
6836
+ id: "getWhiteInks",
6837
+ title: "Get White Inks",
6838
+ handler: () => tool.cloneItems(tool.items)
6839
+ },
6840
+ {
6841
+ command: "getWhiteInkSettings",
6842
+ id: "getWhiteInkSettings",
6843
+ title: "Get White Ink Settings",
6844
+ handler: () => {
6845
+ const first = tool.getEffectiveWhiteInkItem(tool.items);
6846
+ const primarySource = tool.getPrimaryImageSource();
6847
+ const sourceUrl = tool.resolveSourceUrl(first) || primarySource;
6848
+ return {
6849
+ id: (first == null ? void 0 : first.id) || null,
6850
+ url: sourceUrl,
6851
+ sourceUrl,
6852
+ opacity: WHITE_INK_DEFAULT_OPACITY,
6853
+ printWithWhiteInk: tool.printWithWhiteInk,
6854
+ previewImageVisible: tool.previewImageVisible
6855
+ };
6856
+ }
6857
+ },
6858
+ {
6859
+ command: "setWhiteInkPrintEnabled",
6860
+ id: "setWhiteInkPrintEnabled",
6861
+ title: "Set White Ink Preview Enabled",
6862
+ handler: (enabled) => {
6863
+ var _a;
6864
+ tool.printWithWhiteInk = !!enabled;
6865
+ const configService = (_a = tool.context) == null ? void 0 : _a.services.get("ConfigurationService");
6866
+ configService == null ? void 0 : configService.update("whiteInk.printWithWhiteInk", tool.printWithWhiteInk);
6867
+ tool.updateWhiteInks();
6868
+ return { ok: true };
6869
+ }
6870
+ },
6871
+ {
6872
+ command: "setWhiteInkPreviewImageVisible",
6873
+ id: "setWhiteInkPreviewImageVisible",
6874
+ title: "Set White Ink Cover Visible",
6875
+ handler: (visible) => {
6876
+ var _a;
6877
+ tool.previewImageVisible = !!visible;
6878
+ const configService = (_a = tool.context) == null ? void 0 : _a.services.get("ConfigurationService");
6879
+ configService == null ? void 0 : configService.update(
6880
+ WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
6881
+ tool.previewImageVisible
6882
+ );
6883
+ tool.updateWhiteInks();
6884
+ return { ok: true };
6885
+ }
6886
+ },
6887
+ {
6888
+ command: "getWorkingWhiteInks",
6889
+ id: "getWorkingWhiteInks",
6890
+ title: "Get Working White Inks",
6891
+ handler: () => tool.cloneItems(tool.workingItems)
6892
+ },
6893
+ {
6894
+ command: "setWorkingWhiteInk",
6895
+ id: "setWorkingWhiteInk",
6896
+ title: "Set Working White Ink",
6897
+ handler: (id, updates) => {
6898
+ tool.updateWhiteInkInWorking(id, updates);
6899
+ }
6900
+ },
6901
+ {
6902
+ command: "updateWhiteInk",
6903
+ id: "updateWhiteInk",
6904
+ title: "Update White Ink",
6905
+ handler: async (id, updates, options = {}) => {
6906
+ await tool.updateWhiteInkItem(id, updates, options);
6907
+ }
6908
+ },
6909
+ {
6910
+ command: "removeWhiteInk",
6911
+ id: "removeWhiteInk",
6912
+ title: "Remove White Ink",
6913
+ handler: (id) => {
6914
+ tool.removeWhiteInk(id);
6915
+ }
6916
+ },
6917
+ {
6918
+ command: "clearWhiteInks",
6919
+ id: "clearWhiteInks",
6920
+ title: "Clear White Inks",
6921
+ handler: () => {
6922
+ tool.clearWhiteInks();
6923
+ }
6924
+ },
6925
+ {
6926
+ command: "resetWorkingWhiteInks",
6927
+ id: "resetWorkingWhiteInks",
6928
+ title: "Reset Working White Inks",
6929
+ handler: () => {
6930
+ tool.workingItems = tool.cloneItems(tool.items);
6931
+ tool.hasWorkingChanges = false;
6932
+ tool.updateWhiteInks();
6933
+ }
6934
+ },
6935
+ {
6936
+ command: "completeWhiteInks",
6937
+ id: "completeWhiteInks",
6938
+ title: "Complete White Inks",
6939
+ handler: async () => {
6940
+ return await tool.completeWhiteInks();
6941
+ }
6942
+ },
6943
+ {
6944
+ command: "setWhiteInkImage",
6945
+ id: "setWhiteInkImage",
6946
+ title: "Set White Ink Image",
6947
+ handler: async (url) => {
6948
+ if (!url) {
6949
+ tool.clearWhiteInks();
6950
+ return { ok: true };
6951
+ }
6952
+ const targetId = tool.resolveReplaceTargetId(null);
6953
+ const upsertResult = await tool.upsertWhiteInkEntry(url, {
6954
+ id: targetId || void 0,
6955
+ mode: targetId ? "replace" : "add",
6956
+ createIfMissing: true,
6957
+ addOptions: {}
6958
+ });
6959
+ return { ok: true, id: upsertResult.id };
6960
+ }
6961
+ }
6962
+ ];
6963
+ }
6964
+
6965
+ // src/extensions/white-ink/config.ts
6966
+ function createWhiteInkConfigurations() {
6967
+ return [
6968
+ {
6969
+ id: "whiteInk.items",
6970
+ type: "array",
6971
+ label: "White Ink Images",
6972
+ default: []
6973
+ },
6974
+ {
6975
+ id: "whiteInk.printWithWhiteInk",
6976
+ type: "boolean",
6977
+ label: "Preview White Ink",
6978
+ default: true
6979
+ },
6980
+ {
6981
+ id: "whiteInk.previewImageVisible",
6982
+ type: "boolean",
6983
+ label: "Show Cover During White Ink Preview",
6984
+ default: true
6985
+ },
6986
+ {
6987
+ id: "whiteInk.debug",
6988
+ type: "boolean",
6989
+ label: "White Ink Debug Log",
6990
+ default: false
6991
+ }
6992
+ ];
6993
+ }
6994
+
6995
+ // src/extensions/white-ink/WhiteInkTool.ts
6996
+ var WHITE_INK_DEBUG_KEY = "whiteInk.debug";
6997
+ var WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY2 = "whiteInk.previewImageVisible";
6998
+ var WHITE_INK_DEFAULT_OPACITY2 = 0.85;
7467
6999
  var WHITE_INK_AUTO_ITEM_ID = "white-ink-auto";
7468
7000
  var WHITE_INK_COVER_OPACITY_FACTOR = 0.45;
7469
7001
  var WHITE_INK_COVER_OPACITY_MIN = 0.15;
@@ -7479,7 +7011,9 @@ var WhiteInkTool = class {
7479
7011
  this.items = [];
7480
7012
  this.workingItems = [];
7481
7013
  this.hasWorkingChanges = false;
7482
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
7014
+ this.sourceSizeCache = createSourceSizeCache(
7015
+ (src) => this.loadImageSize(src)
7016
+ );
7483
7017
  this.previewMaskBySource = /* @__PURE__ */ new Map();
7484
7018
  this.pendingPreviewMaskBySource = /* @__PURE__ */ new Map();
7485
7019
  this.isUpdatingConfig = false;
@@ -7490,6 +7024,7 @@ var WhiteInkTool = class {
7490
7024
  this.whiteSpecs = [];
7491
7025
  this.coverSpecs = [];
7492
7026
  this.overlaySpecs = [];
7027
+ this.subscriptions = new SubscriptionBag();
7493
7028
  this.onToolActivated = (event) => {
7494
7029
  const before = this.isToolActive;
7495
7030
  this.syncToolActiveFromWorkbench(event.id);
@@ -7507,19 +7042,19 @@ var WhiteInkTool = class {
7507
7042
  this.onObjectAdded = (e) => {
7508
7043
  var _a, _b;
7509
7044
  const layerId = (_b = (_a = e == null ? void 0 : e.target) == null ? void 0 : _a.data) == null ? void 0 : _b.layerId;
7510
- if (layerId !== IMAGE_OBJECT_LAYER_ID3) return;
7045
+ if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
7511
7046
  this.updateWhiteInks();
7512
7047
  };
7513
7048
  this.onObjectModified = (e) => {
7514
7049
  var _a, _b;
7515
7050
  const layerId = (_b = (_a = e == null ? void 0 : e.target) == null ? void 0 : _a.data) == null ? void 0 : _b.layerId;
7516
- if (layerId !== IMAGE_OBJECT_LAYER_ID3) return;
7051
+ if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
7517
7052
  this.updateWhiteInks();
7518
7053
  };
7519
7054
  this.onObjectRemoved = (e) => {
7520
7055
  var _a, _b;
7521
7056
  const layerId = (_b = (_a = e == null ? void 0 : e.target) == null ? void 0 : _a.data) == null ? void 0 : _b.layerId;
7522
- if (layerId !== IMAGE_OBJECT_LAYER_ID3) return;
7057
+ if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
7523
7058
  this.updateWhiteInks();
7524
7059
  };
7525
7060
  this.onImageWorkingChanged = () => {
@@ -7528,6 +7063,7 @@ var WhiteInkTool = class {
7528
7063
  }
7529
7064
  activate(context) {
7530
7065
  var _a;
7066
+ this.subscriptions.disposeAll();
7531
7067
  this.context = context;
7532
7068
  this.canvasService = context.services.get("CanvasService");
7533
7069
  if (!this.canvasService) {
@@ -7561,62 +7097,73 @@ var WhiteInkTool = class {
7561
7097
  }),
7562
7098
  { priority: 260 }
7563
7099
  );
7564
- context.eventBus.on("tool:activated", this.onToolActivated);
7565
- context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
7566
- context.eventBus.on("object:added", this.onObjectAdded);
7567
- context.eventBus.on("object:modified", this.onObjectModified);
7568
- context.eventBus.on("object:removed", this.onObjectRemoved);
7569
- context.eventBus.on("image:working:change", this.onImageWorkingChanged);
7100
+ this.subscriptions.on(context.eventBus, "tool:activated", this.onToolActivated);
7101
+ this.subscriptions.on(
7102
+ context.eventBus,
7103
+ "scene:layout:change",
7104
+ this.onSceneLayoutChanged
7105
+ );
7106
+ this.subscriptions.on(context.eventBus, "object:added", this.onObjectAdded);
7107
+ this.subscriptions.on(
7108
+ context.eventBus,
7109
+ "object:modified",
7110
+ this.onObjectModified
7111
+ );
7112
+ this.subscriptions.on(
7113
+ context.eventBus,
7114
+ "object:removed",
7115
+ this.onObjectRemoved
7116
+ );
7117
+ this.subscriptions.on(
7118
+ context.eventBus,
7119
+ "image:working:change",
7120
+ this.onImageWorkingChanged
7121
+ );
7570
7122
  const configService = context.services.get(
7571
7123
  "ConfigurationService"
7572
7124
  );
7573
7125
  if (configService) {
7574
- this.items = this.normalizeItems(
7575
- configService.get("whiteInk.items", []) || []
7576
- );
7577
- this.workingItems = this.cloneItems(this.items);
7578
- this.hasWorkingChanges = false;
7126
+ this.applyCommittedItems(configService.get("whiteInk.items", []) || []);
7579
7127
  this.printWithWhiteInk = !!configService.get(
7580
7128
  "whiteInk.printWithWhiteInk",
7581
7129
  true
7582
7130
  );
7583
7131
  this.previewImageVisible = !!configService.get(
7584
- WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
7132
+ WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY2,
7585
7133
  true
7586
7134
  );
7587
7135
  this.migrateLegacyConfigIfNeeded(configService);
7588
- configService.onAnyChange((e) => {
7589
- if (this.isUpdatingConfig) return;
7590
- if (e.key === "whiteInk.items") {
7591
- this.items = this.normalizeItems(e.value || []);
7592
- if (!this.isToolActive || !this.hasWorkingChanges) {
7593
- this.workingItems = this.cloneItems(this.items);
7594
- this.hasWorkingChanges = false;
7136
+ this.subscriptions.onConfigChange(
7137
+ configService,
7138
+ (e) => {
7139
+ if (this.isUpdatingConfig) return;
7140
+ if (e.key === "whiteInk.items") {
7141
+ this.applyCommittedItems(e.value || []);
7142
+ this.updateWhiteInks();
7143
+ return;
7144
+ }
7145
+ if (e.key === "whiteInk.printWithWhiteInk") {
7146
+ this.printWithWhiteInk = !!e.value;
7147
+ this.updateWhiteInks();
7148
+ return;
7149
+ }
7150
+ if (e.key === WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY2) {
7151
+ this.previewImageVisible = !!e.value;
7152
+ this.updateWhiteInks();
7153
+ return;
7154
+ }
7155
+ if (e.key === "image.items") {
7156
+ this.updateWhiteInks();
7157
+ return;
7158
+ }
7159
+ if (e.key === WHITE_INK_DEBUG_KEY) {
7160
+ return;
7161
+ }
7162
+ if (e.key.startsWith("size.")) {
7163
+ this.updateWhiteInks();
7595
7164
  }
7596
- this.updateWhiteInks();
7597
- return;
7598
- }
7599
- if (e.key === "whiteInk.printWithWhiteInk") {
7600
- this.printWithWhiteInk = !!e.value;
7601
- this.updateWhiteInks();
7602
- return;
7603
- }
7604
- if (e.key === WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY) {
7605
- this.previewImageVisible = !!e.value;
7606
- this.updateWhiteInks();
7607
- return;
7608
- }
7609
- if (e.key === "image.items") {
7610
- this.updateWhiteInks();
7611
- return;
7612
- }
7613
- if (e.key === WHITE_INK_DEBUG_KEY) {
7614
- return;
7615
- }
7616
- if (e.key.startsWith("size.")) {
7617
- this.updateWhiteInks();
7618
7165
  }
7619
- });
7166
+ );
7620
7167
  }
7621
7168
  const toolSessionService = context.services.get("ToolSessionService");
7622
7169
  this.dirtyTrackerDisposable = toolSessionService == null ? void 0 : toolSessionService.registerDirtyTracker(
@@ -7627,14 +7174,10 @@ var WhiteInkTool = class {
7627
7174
  }
7628
7175
  deactivate(context) {
7629
7176
  var _a, _b;
7630
- context.eventBus.off("tool:activated", this.onToolActivated);
7631
- context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
7632
- context.eventBus.off("object:added", this.onObjectAdded);
7633
- context.eventBus.off("object:modified", this.onObjectModified);
7634
- context.eventBus.off("object:removed", this.onObjectRemoved);
7635
- context.eventBus.off("image:working:change", this.onImageWorkingChanged);
7177
+ this.subscriptions.disposeAll();
7636
7178
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
7637
7179
  this.dirtyTrackerDisposable = void 0;
7180
+ this.sourceSizeCache.clear();
7638
7181
  this.clearRenderedWhiteInks();
7639
7182
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7640
7183
  this.renderProducerDisposable = void 0;
@@ -7662,171 +7205,8 @@ var WhiteInkTool = class {
7662
7205
  }
7663
7206
  }
7664
7207
  ],
7665
- [ContributionPointIds9.CONFIGURATIONS]: [
7666
- {
7667
- id: "whiteInk.items",
7668
- type: "array",
7669
- label: "White Ink Images",
7670
- default: []
7671
- },
7672
- {
7673
- id: "whiteInk.printWithWhiteInk",
7674
- type: "boolean",
7675
- label: "Preview White Ink",
7676
- default: true
7677
- },
7678
- {
7679
- id: WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
7680
- type: "boolean",
7681
- label: "Show Cover During White Ink Preview",
7682
- default: true
7683
- },
7684
- {
7685
- id: WHITE_INK_DEBUG_KEY,
7686
- type: "boolean",
7687
- label: "White Ink Debug Log",
7688
- default: false
7689
- }
7690
- ],
7691
- [ContributionPointIds9.COMMANDS]: [
7692
- {
7693
- command: "addWhiteInk",
7694
- title: "Add White Ink",
7695
- handler: async (url, options) => {
7696
- return await this.addWhiteInkEntry(url, options);
7697
- }
7698
- },
7699
- {
7700
- command: "upsertWhiteInk",
7701
- title: "Upsert White Ink",
7702
- handler: async (url, options = {}) => {
7703
- return await this.upsertWhiteInkEntry(url, options);
7704
- }
7705
- },
7706
- {
7707
- command: "getWhiteInks",
7708
- title: "Get White Inks",
7709
- handler: () => this.cloneItems(this.items)
7710
- },
7711
- {
7712
- command: "getWhiteInkSettings",
7713
- title: "Get White Ink Settings",
7714
- handler: () => {
7715
- const first = this.getEffectiveWhiteInkItem(this.items);
7716
- const primarySource = this.getPrimaryImageSource();
7717
- const sourceUrl = this.resolveSourceUrl(first) || primarySource;
7718
- return {
7719
- id: (first == null ? void 0 : first.id) || null,
7720
- url: sourceUrl,
7721
- sourceUrl,
7722
- opacity: WHITE_INK_DEFAULT_OPACITY,
7723
- printWithWhiteInk: this.printWithWhiteInk,
7724
- previewImageVisible: this.previewImageVisible
7725
- };
7726
- }
7727
- },
7728
- {
7729
- command: "setWhiteInkPrintEnabled",
7730
- title: "Set White Ink Preview Enabled",
7731
- handler: (enabled) => {
7732
- var _a;
7733
- this.printWithWhiteInk = !!enabled;
7734
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
7735
- "ConfigurationService"
7736
- );
7737
- configService == null ? void 0 : configService.update(
7738
- "whiteInk.printWithWhiteInk",
7739
- this.printWithWhiteInk
7740
- );
7741
- this.updateWhiteInks();
7742
- return { ok: true };
7743
- }
7744
- },
7745
- {
7746
- command: "setWhiteInkPreviewImageVisible",
7747
- title: "Set White Ink Cover Visible",
7748
- handler: (visible) => {
7749
- var _a;
7750
- this.previewImageVisible = !!visible;
7751
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
7752
- "ConfigurationService"
7753
- );
7754
- configService == null ? void 0 : configService.update(
7755
- WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
7756
- this.previewImageVisible
7757
- );
7758
- this.updateWhiteInks();
7759
- return { ok: true };
7760
- }
7761
- },
7762
- {
7763
- command: "getWorkingWhiteInks",
7764
- title: "Get Working White Inks",
7765
- handler: () => this.cloneItems(this.workingItems)
7766
- },
7767
- {
7768
- command: "setWorkingWhiteInk",
7769
- title: "Set Working White Ink",
7770
- handler: (id, updates) => {
7771
- this.updateWhiteInkInWorking(id, updates);
7772
- }
7773
- },
7774
- {
7775
- command: "updateWhiteInk",
7776
- title: "Update White Ink",
7777
- handler: async (id, updates, options = {}) => {
7778
- await this.updateWhiteInkItem(id, updates, options);
7779
- }
7780
- },
7781
- {
7782
- command: "removeWhiteInk",
7783
- title: "Remove White Ink",
7784
- handler: (id) => {
7785
- this.removeWhiteInk(id);
7786
- }
7787
- },
7788
- {
7789
- command: "clearWhiteInks",
7790
- title: "Clear White Inks",
7791
- handler: () => {
7792
- this.clearWhiteInks();
7793
- }
7794
- },
7795
- {
7796
- command: "resetWorkingWhiteInks",
7797
- title: "Reset Working White Inks",
7798
- handler: () => {
7799
- this.workingItems = this.cloneItems(this.items);
7800
- this.hasWorkingChanges = false;
7801
- this.updateWhiteInks();
7802
- }
7803
- },
7804
- {
7805
- command: "completeWhiteInks",
7806
- title: "Complete White Inks",
7807
- handler: async () => {
7808
- return await this.completeWhiteInks();
7809
- }
7810
- },
7811
- {
7812
- command: "setWhiteInkImage",
7813
- title: "Set White Ink Image",
7814
- handler: async (url) => {
7815
- if (!url) {
7816
- this.clearWhiteInks();
7817
- return { ok: true };
7818
- }
7819
- const targetId = this.resolveReplaceTargetId(null);
7820
- const upsertResult = await this.upsertWhiteInkEntry(url, {
7821
- id: targetId || void 0,
7822
- mode: targetId ? "replace" : "add",
7823
- createIfMissing: true,
7824
- addOptions: {}
7825
- });
7826
- return { ok: true, id: upsertResult.id };
7827
- }
7828
- }
7829
- ]
7208
+ [ContributionPointIds9.CONFIGURATIONS]: createWhiteInkConfigurations(),
7209
+ [ContributionPointIds9.COMMANDS]: createWhiteInkCommands(this)
7830
7210
  };
7831
7211
  }
7832
7212
  migrateLegacyConfigIfNeeded(configService) {
@@ -7836,15 +7216,12 @@ var WhiteInkTool = class {
7836
7216
  const item = this.normalizeItem({
7837
7217
  id: this.generateId(),
7838
7218
  sourceUrl: legacyMask,
7839
- opacity: WHITE_INK_DEFAULT_OPACITY
7219
+ opacity: WHITE_INK_DEFAULT_OPACITY2
7220
+ });
7221
+ this.applyCommittedItems([item]);
7222
+ runDeferredConfigUpdate(this, () => {
7223
+ configService.update("whiteInk.items", this.items);
7840
7224
  });
7841
- this.items = [item];
7842
- this.workingItems = this.cloneItems(this.items);
7843
- this.isUpdatingConfig = true;
7844
- configService.update("whiteInk.items", this.items);
7845
- setTimeout(() => {
7846
- this.isUpdatingConfig = false;
7847
- }, 0);
7848
7225
  }
7849
7226
  syncToolActiveFromWorkbench(fallbackId) {
7850
7227
  var _a;
@@ -7886,7 +7263,7 @@ var WhiteInkTool = class {
7886
7263
  id: String(item.id || this.generateId()),
7887
7264
  sourceUrl,
7888
7265
  url: sourceUrl,
7889
- opacity: WHITE_INK_DEFAULT_OPACITY
7266
+ opacity: WHITE_INK_DEFAULT_OPACITY2
7890
7267
  };
7891
7268
  }
7892
7269
  normalizeItems(items) {
@@ -7905,7 +7282,7 @@ var WhiteInkTool = class {
7905
7282
  }
7906
7283
  return {
7907
7284
  id: WHITE_INK_AUTO_ITEM_ID,
7908
- opacity: WHITE_INK_DEFAULT_OPACITY
7285
+ opacity: WHITE_INK_DEFAULT_OPACITY2
7909
7286
  };
7910
7287
  }
7911
7288
  generateId() {
@@ -7928,31 +7305,45 @@ var WhiteInkTool = class {
7928
7305
  }
7929
7306
  return null;
7930
7307
  }
7308
+ applyCommittedItems(nextItems) {
7309
+ const session = {
7310
+ committed: this.items,
7311
+ working: this.workingItems,
7312
+ hasWorkingChanges: this.hasWorkingChanges
7313
+ };
7314
+ applyCommittedSnapshot(session, this.normalizeItems(nextItems), {
7315
+ clone: (items) => this.cloneItems(items),
7316
+ toolActive: this.isToolActive,
7317
+ preserveDirtyWorking: true
7318
+ });
7319
+ this.items = session.committed;
7320
+ this.workingItems = session.working;
7321
+ this.hasWorkingChanges = session.hasWorkingChanges;
7322
+ }
7931
7323
  updateConfig(newItems, skipCanvasUpdate = false) {
7932
7324
  if (!this.context) return;
7933
- this.isUpdatingConfig = true;
7934
- this.items = this.normalizeItems(newItems);
7935
- if (!this.isToolActive || !this.hasWorkingChanges) {
7936
- this.workingItems = this.cloneItems(this.items);
7937
- this.hasWorkingChanges = false;
7938
- }
7939
- const configService = this.context.services.get(
7940
- "ConfigurationService"
7325
+ this.applyCommittedItems(newItems);
7326
+ runDeferredConfigUpdate(
7327
+ this,
7328
+ () => {
7329
+ var _a;
7330
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
7331
+ "ConfigurationService"
7332
+ );
7333
+ configService == null ? void 0 : configService.update("whiteInk.items", this.items);
7334
+ if (!skipCanvasUpdate) {
7335
+ this.updateWhiteInks();
7336
+ }
7337
+ },
7338
+ 50
7941
7339
  );
7942
- configService == null ? void 0 : configService.update("whiteInk.items", this.items);
7943
- if (!skipCanvasUpdate) {
7944
- this.updateWhiteInks();
7945
- }
7946
- setTimeout(() => {
7947
- this.isUpdatingConfig = false;
7948
- }, 50);
7949
7340
  }
7950
7341
  async addWhiteInkEntry(url, options) {
7951
7342
  const id = this.generateId();
7952
7343
  const item = this.normalizeItem({
7953
7344
  id,
7954
7345
  sourceUrl: url,
7955
- opacity: WHITE_INK_DEFAULT_OPACITY,
7346
+ opacity: WHITE_INK_DEFAULT_OPACITY2,
7956
7347
  ...options
7957
7348
  });
7958
7349
  const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
@@ -8037,7 +7428,7 @@ var WhiteInkTool = class {
8037
7428
  this.updateConfig(next);
8038
7429
  }
8039
7430
  clearWhiteInks() {
8040
- this.sourceSizeBySrc.clear();
7431
+ this.sourceSizeCache.clear();
8041
7432
  this.previewMaskBySource.clear();
8042
7433
  this.pendingPreviewMaskBySource.clear();
8043
7434
  this.updateConfig([]);
@@ -8049,41 +7440,19 @@ var WhiteInkTool = class {
8049
7440
  }
8050
7441
  getFrameRect() {
8051
7442
  var _a;
8052
- if (!this.canvasService) {
8053
- return { left: 0, top: 0, width: 0, height: 0 };
8054
- }
8055
7443
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
8056
7444
  "ConfigurationService"
8057
7445
  );
8058
- if (!configService) {
8059
- return { left: 0, top: 0, width: 0, height: 0 };
8060
- }
8061
- const sizeState = readSizeState(configService);
8062
- const layout = computeSceneLayout(this.canvasService, sizeState);
8063
- if (!layout) {
8064
- return { left: 0, top: 0, width: 0, height: 0 };
8065
- }
8066
- return this.canvasService.toSceneRect({
8067
- left: layout.cutRect.left,
8068
- top: layout.cutRect.top,
8069
- width: layout.cutRect.width,
8070
- height: layout.cutRect.height
8071
- });
7446
+ return resolveCutFrameRect(this.canvasService, configService);
8072
7447
  }
8073
7448
  toLayoutSceneRect(rect) {
8074
- return {
8075
- left: rect.left,
8076
- top: rect.top,
8077
- width: rect.width,
8078
- height: rect.height,
8079
- space: "scene"
8080
- };
7449
+ return toLayoutSceneRect(rect);
8081
7450
  }
8082
7451
  getImageObjects() {
8083
7452
  if (!this.canvasService) return [];
8084
7453
  return this.canvasService.canvas.getObjects().filter((obj) => {
8085
7454
  var _a;
8086
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3;
7455
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID;
8087
7456
  });
8088
7457
  }
8089
7458
  getPrimaryImageObject() {
@@ -8155,21 +7524,16 @@ var WhiteInkTool = class {
8155
7524
  return snapshot.src === placement.committedUrl;
8156
7525
  }
8157
7526
  getCoverScale(frame, source) {
8158
- const frameW = Math.max(1, frame.width);
8159
- const frameH = Math.max(1, frame.height);
8160
- const sourceW = Math.max(1, source.width);
8161
- const sourceH = Math.max(1, source.height);
8162
- return Math.max(frameW / sourceW, frameH / sourceH);
7527
+ return getCoverScale(frame, source);
8163
7528
  }
8164
7529
  async ensureSourceSize(sourceUrl) {
8165
- if (!sourceUrl) return null;
8166
- const cached = this.getSourceSize(sourceUrl);
8167
- if (cached) return cached;
7530
+ return this.sourceSizeCache.ensureImageSize(sourceUrl);
7531
+ }
7532
+ async loadImageSize(sourceUrl) {
8168
7533
  try {
8169
7534
  const image = await this.loadImageElement(sourceUrl);
8170
7535
  const size = this.getElementSize(image);
8171
7536
  if (!size) return null;
8172
- this.rememberSourceSize(sourceUrl, size);
8173
7537
  return {
8174
7538
  width: size.width,
8175
7539
  height: size.height
@@ -8214,22 +7578,10 @@ var WhiteInkTool = class {
8214
7578
  return (obj == null ? void 0 : obj._element) || (obj == null ? void 0 : obj._originalElement) || null;
8215
7579
  }
8216
7580
  rememberSourceSize(src, size) {
8217
- if (!src) return;
8218
- if (!Number.isFinite(size.width) || !Number.isFinite(size.height)) return;
8219
- if (size.width <= 0 || size.height <= 0) return;
8220
- this.sourceSizeBySrc.set(src, {
8221
- width: size.width,
8222
- height: size.height
8223
- });
7581
+ this.sourceSizeCache.rememberSourceSize(src, size);
8224
7582
  }
8225
7583
  getSourceSize(src) {
8226
- if (!src) return null;
8227
- const cached = this.sourceSizeBySrc.get(src);
8228
- if (!cached) return null;
8229
- return {
8230
- width: cached.width,
8231
- height: cached.height
8232
- };
7584
+ return this.sourceSizeCache.getSourceSize(src);
8233
7585
  }
8234
7586
  computeWhiteScaleAdjust(baseSource, whiteSource) {
8235
7587
  if (!baseSource || !whiteSource || baseSource === whiteSource) {
@@ -8249,7 +7601,7 @@ var WhiteInkTool = class {
8249
7601
  };
8250
7602
  }
8251
7603
  computeCoverOpacity() {
8252
- const raw = WHITE_INK_DEFAULT_OPACITY * WHITE_INK_COVER_OPACITY_FACTOR;
7604
+ const raw = WHITE_INK_DEFAULT_OPACITY2 * WHITE_INK_COVER_OPACITY_FACTOR;
8253
7605
  return Math.max(
8254
7606
  WHITE_INK_COVER_OPACITY_MIN,
8255
7607
  Math.min(WHITE_INK_COVER_OPACITY_MAX, raw)
@@ -8513,7 +7865,7 @@ var WhiteInkTool = class {
8513
7865
  purgeSourceCaches(item) {
8514
7866
  const sourceUrl = this.resolveSourceUrl(item);
8515
7867
  if (!sourceUrl) return;
8516
- this.sourceSizeBySrc.delete(sourceUrl);
7868
+ this.sourceSizeCache.deleteSourceSize(sourceUrl);
8517
7869
  const prefix = `${sourceUrl}::`;
8518
7870
  Array.from(this.previewMaskBySource.keys()).forEach((cacheKey) => {
8519
7871
  if (cacheKey.startsWith(prefix)) {
@@ -8552,7 +7904,7 @@ var WhiteInkTool = class {
8552
7904
  "white-ink.main",
8553
7905
  snapshot,
8554
7906
  sources.whiteSrc,
8555
- WHITE_INK_DEFAULT_OPACITY,
7907
+ WHITE_INK_DEFAULT_OPACITY2,
8556
7908
  WHITE_INK_OBJECT_LAYER_ID,
8557
7909
  "white-ink",
8558
7910
  sources.whiteScaleAdjustX,
@@ -8660,7 +8012,7 @@ var WhiteInkTool = class {
8660
8012
  }
8661
8013
  };
8662
8014
 
8663
- // src/extensions/sceneLayout.ts
8015
+ // src/services/SceneLayoutService.ts
8664
8016
  import {
8665
8017
  COMMAND_SERVICE,
8666
8018
  CONFIGURATION_SERVICE
@@ -8673,6 +8025,7 @@ var SceneLayoutService = class {
8673
8025
  constructor() {
8674
8026
  this.lastLayout = null;
8675
8027
  this.lastGeometry = null;
8028
+ this.subscriptions = new SubscriptionBag();
8676
8029
  this.commandDisposables = [];
8677
8030
  this.onCanvasResized = () => {
8678
8031
  this.refresh();
@@ -8708,16 +8061,13 @@ var SceneLayoutService = class {
8708
8061
  () => this.getGeometry()
8709
8062
  )
8710
8063
  );
8711
- this.onConfigChange = configService.onAnyChange(this.onConfigChanged);
8712
- context.eventBus.on("canvas:resized", this.onCanvasResized);
8064
+ this.subscriptions.disposeAll();
8065
+ this.subscriptions.onConfigChange(configService, this.onConfigChanged);
8066
+ this.subscriptions.on(context.eventBus, "canvas:resized", this.onCanvasResized);
8713
8067
  this.refresh();
8714
8068
  }
8715
8069
  dispose(context) {
8716
- var _a, _b;
8717
- const activeContext = (_a = this.context) != null ? _a : context;
8718
- activeContext.eventBus.off("canvas:resized", this.onCanvasResized);
8719
- (_b = this.onConfigChange) == null ? void 0 : _b.dispose();
8720
- this.onConfigChange = void 0;
8070
+ this.subscriptions.disposeAll();
8721
8071
  this.commandDisposables.forEach((item) => item.dispose());
8722
8072
  this.commandDisposables = [];
8723
8073
  this.context = void 0;
@@ -8871,6 +8221,9 @@ function evaluateVisibilityExpr(expr, context) {
8871
8221
  if (!toolId) return false;
8872
8222
  return context.isSessionActive ? context.isSessionActive(toolId) : false;
8873
8223
  }
8224
+ if (expr.op === "anySessionActive") {
8225
+ return context.hasAnyActiveSession ? context.hasAnyActiveSession() : false;
8226
+ }
8874
8227
  if (expr.op === "layerExists") {
8875
8228
  return layerState(context, expr.layerId).exists === true;
8876
8229
  }
@@ -8904,6 +8257,7 @@ var CanvasService = class {
8904
8257
  this.visibilityRefreshScheduled = false;
8905
8258
  this.managedProducerPassIds = /* @__PURE__ */ new Set();
8906
8259
  this.managedPassMetas = /* @__PURE__ */ new Map();
8260
+ this.managedPassEffects = [];
8907
8261
  this.canvasForwardersBound = false;
8908
8262
  this.forwardSelectionCreated = (e) => {
8909
8263
  var _a;
@@ -8931,9 +8285,11 @@ var CanvasService = class {
8931
8285
  };
8932
8286
  this.onToolActivated = () => {
8933
8287
  this.applyManagedPassVisibility();
8288
+ void this.applyManagedPassEffects(void 0, { render: true });
8934
8289
  };
8935
8290
  this.onToolSessionChanged = () => {
8936
8291
  this.applyManagedPassVisibility();
8292
+ void this.applyManagedPassEffects(void 0, { render: true });
8937
8293
  };
8938
8294
  this.onCanvasObjectChanged = () => {
8939
8295
  if (this.producerApplyInProgress) return;
@@ -8998,6 +8354,7 @@ var CanvasService = class {
8998
8354
  this.renderProducers.clear();
8999
8355
  this.managedProducerPassIds.clear();
9000
8356
  this.managedPassMetas.clear();
8357
+ this.managedPassEffects = [];
9001
8358
  this.context = void 0;
9002
8359
  this.workbenchService = void 0;
9003
8360
  this.toolSessionService = void 0;
@@ -9104,6 +8461,7 @@ var CanvasService = class {
9104
8461
  return {
9105
8462
  type: "clipPath",
9106
8463
  key,
8464
+ visibility: effect.visibility,
9107
8465
  source: {
9108
8466
  ...source,
9109
8467
  id: sourceId
@@ -9211,22 +8569,30 @@ var CanvasService = class {
9211
8569
  });
9212
8570
  return state;
9213
8571
  }
9214
- applyManagedPassVisibility(options = {}) {
8572
+ isSessionActive(toolId) {
8573
+ if (!this.toolSessionService) return false;
8574
+ return this.toolSessionService.getState(toolId).status === "active";
8575
+ }
8576
+ hasAnyActiveSession() {
8577
+ var _a, _b;
8578
+ return (_b = (_a = this.toolSessionService) == null ? void 0 : _a.hasAnyActiveSession()) != null ? _b : false;
8579
+ }
8580
+ buildVisibilityEvalContext(layers) {
9215
8581
  var _a, _b;
8582
+ return {
8583
+ activeToolId: (_b = (_a = this.workbenchService) == null ? void 0 : _a.activeToolId) != null ? _b : null,
8584
+ isSessionActive: (toolId) => this.isSessionActive(toolId),
8585
+ hasAnyActiveSession: () => this.hasAnyActiveSession(),
8586
+ layers
8587
+ };
8588
+ }
8589
+ applyManagedPassVisibility(options = {}) {
9216
8590
  if (!this.managedPassMetas.size) return false;
9217
8591
  const layers = this.getPassRuntimeState();
9218
- const activeToolId = (_b = (_a = this.workbenchService) == null ? void 0 : _a.activeToolId) != null ? _b : null;
9219
- const isSessionActive = (toolId) => {
9220
- if (!this.toolSessionService) return false;
9221
- return this.toolSessionService.getState(toolId).status === "active";
9222
- };
8592
+ const context = this.buildVisibilityEvalContext(layers);
9223
8593
  let changed = false;
9224
8594
  this.managedPassMetas.forEach((meta) => {
9225
- const visible = evaluateVisibilityExpr(meta.visibility, {
9226
- activeToolId,
9227
- isSessionActive,
9228
- layers
9229
- });
8595
+ const visible = evaluateVisibilityExpr(meta.visibility, context);
9230
8596
  changed = this.setPassVisibility(meta.id, visible) || changed;
9231
8597
  });
9232
8598
  if (changed && options.render !== false) {
@@ -9294,18 +8660,24 @@ var CanvasService = class {
9294
8660
  }
9295
8661
  this.managedProducerPassIds = nextPassIds;
9296
8662
  this.managedPassMetas = nextManagedPassMetas;
8663
+ this.managedPassEffects = nextEffects;
9297
8664
  this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
9298
- await this.applyManagedPassEffects(nextEffects);
8665
+ await this.applyManagedPassEffects(nextEffects, { render: false });
9299
8666
  this.applyManagedPassVisibility({ render: false });
9300
8667
  } finally {
9301
8668
  this.producerApplyInProgress = false;
9302
8669
  }
9303
8670
  this.requestRenderAll();
9304
8671
  }
9305
- async applyManagedPassEffects(effects) {
8672
+ async applyManagedPassEffects(effects = this.managedPassEffects, options = {}) {
9306
8673
  const effectTargetMap = /* @__PURE__ */ new Map();
8674
+ const layers = this.getPassRuntimeState();
8675
+ const visibilityContext = this.buildVisibilityEvalContext(layers);
9307
8676
  for (const effect of effects) {
9308
8677
  if (effect.type !== "clipPath") continue;
8678
+ if (!evaluateVisibilityExpr(effect.visibility, visibilityContext)) {
8679
+ continue;
8680
+ }
9309
8681
  effect.targetPassIds.forEach((targetPassId) => {
9310
8682
  this.getPassCanvasObjects(targetPassId).forEach((obj) => {
9311
8683
  effectTargetMap.set(obj, effect);
@@ -9337,6 +8709,9 @@ var CanvasService = class {
9337
8709
  targetEffect.key
9338
8710
  );
9339
8711
  }
8712
+ if (options.render !== false) {
8713
+ this.requestRenderAll();
8714
+ }
9340
8715
  }
9341
8716
  getObject(id, passId) {
9342
8717
  const normalizedId = String(id || "").trim();
@@ -9873,5 +9248,13 @@ export {
9873
9248
  SizeTool,
9874
9249
  ViewportSystem,
9875
9250
  WhiteInkTool,
9251
+ getCoverScale as computeImageCoverScale,
9252
+ getCoverScale as computeWhiteInkCoverScale,
9253
+ createDielineCommands,
9254
+ createDielineConfigurations,
9255
+ createImageCommands,
9256
+ createImageConfigurations,
9257
+ createWhiteInkCommands,
9258
+ createWhiteInkConfigurations,
9876
9259
  evaluateVisibilityExpr
9877
9260
  };