@pooder/kit 6.0.1 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) 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/dieline/DielineTool.js +748 -0
  4. package/.test-dist/src/extensions/dieline/commands.js +127 -0
  5. package/.test-dist/src/extensions/dieline/config.js +107 -0
  6. package/.test-dist/src/extensions/dieline/index.js +21 -0
  7. package/.test-dist/src/extensions/dieline/model.js +2 -0
  8. package/.test-dist/src/extensions/dieline/renderer.js +2 -0
  9. package/.test-dist/src/extensions/feature/FeatureTool.js +914 -0
  10. package/.test-dist/src/extensions/feature/index.js +17 -0
  11. package/.test-dist/src/extensions/film/FilmTool.js +207 -0
  12. package/.test-dist/src/extensions/film/index.js +17 -0
  13. package/.test-dist/src/extensions/image/ImageTool.js +1499 -0
  14. package/.test-dist/src/extensions/image/commands.js +162 -0
  15. package/.test-dist/src/extensions/image/config.js +129 -0
  16. package/.test-dist/src/extensions/image/index.js +21 -0
  17. package/.test-dist/src/extensions/image/model.js +2 -0
  18. package/.test-dist/src/extensions/image/renderer.js +5 -0
  19. package/.test-dist/src/extensions/mirror/MirrorTool.js +104 -0
  20. package/.test-dist/src/extensions/mirror/index.js +17 -0
  21. package/.test-dist/src/extensions/ruler/RulerTool.js +442 -0
  22. package/.test-dist/src/extensions/ruler/index.js +17 -0
  23. package/.test-dist/src/extensions/sceneLayout.js +2 -93
  24. package/.test-dist/src/extensions/sceneLayoutModel.js +15 -200
  25. package/.test-dist/src/extensions/size/SizeTool.js +332 -0
  26. package/.test-dist/src/extensions/size/index.js +17 -0
  27. package/.test-dist/src/extensions/white-ink/WhiteInkTool.js +1003 -0
  28. package/.test-dist/src/extensions/white-ink/commands.js +148 -0
  29. package/.test-dist/src/extensions/white-ink/config.js +31 -0
  30. package/.test-dist/src/extensions/white-ink/index.js +21 -0
  31. package/.test-dist/src/extensions/white-ink/model.js +2 -0
  32. package/.test-dist/src/extensions/white-ink/renderer.js +5 -0
  33. package/.test-dist/src/services/SceneLayoutService.js +96 -0
  34. package/.test-dist/src/services/index.js +1 -0
  35. package/.test-dist/src/shared/constants/layers.js +25 -0
  36. package/.test-dist/src/shared/imaging/sourceSizeCache.js +82 -0
  37. package/.test-dist/src/shared/index.js +22 -0
  38. package/.test-dist/src/shared/runtime/sessionState.js +74 -0
  39. package/.test-dist/src/shared/runtime/subscriptions.js +30 -0
  40. package/.test-dist/src/shared/scene/frame.js +34 -0
  41. package/.test-dist/src/shared/scene/sceneLayoutModel.js +202 -0
  42. package/.test-dist/tests/run.js +116 -0
  43. package/CHANGELOG.md +14 -0
  44. package/dist/index.d.mts +390 -367
  45. package/dist/index.d.ts +390 -367
  46. package/dist/index.js +5138 -4927
  47. package/dist/index.mjs +1149 -1977
  48. package/dist/tracer-PO7CRBYY.mjs +1016 -0
  49. package/package.json +2 -2
  50. package/src/extensions/{background.ts → background/BackgroundTool.ts} +33 -50
  51. package/src/extensions/background/index.ts +1 -0
  52. package/src/extensions/{dieline.ts → dieline/DielineTool.ts} +14 -218
  53. package/src/extensions/dieline/commands.ts +109 -0
  54. package/src/extensions/dieline/config.ts +106 -0
  55. package/src/extensions/dieline/index.ts +5 -0
  56. package/src/extensions/dieline/model.ts +1 -0
  57. package/src/extensions/dieline/renderer.ts +1 -0
  58. package/src/extensions/{feature.ts → feature/FeatureTool.ts} +27 -21
  59. package/src/extensions/feature/index.ts +1 -0
  60. package/src/extensions/{film.ts → film/FilmTool.ts} +36 -48
  61. package/src/extensions/film/index.ts +1 -0
  62. package/src/extensions/{image.ts → image/ImageTool.ts} +123 -402
  63. package/src/extensions/image/commands.ts +176 -0
  64. package/src/extensions/image/config.ts +128 -0
  65. package/src/extensions/image/index.ts +5 -0
  66. package/src/extensions/image/model.ts +1 -0
  67. package/src/extensions/image/renderer.ts +1 -0
  68. package/src/extensions/{mirror.ts → mirror/MirrorTool.ts} +1 -1
  69. package/src/extensions/mirror/index.ts +1 -0
  70. package/src/extensions/{ruler.ts → ruler/RulerTool.ts} +4 -5
  71. package/src/extensions/ruler/index.ts +1 -0
  72. package/src/extensions/sceneLayout.ts +1 -140
  73. package/src/extensions/sceneLayoutModel.ts +1 -364
  74. package/src/extensions/{size.ts → size/SizeTool.ts} +7 -6
  75. package/src/extensions/size/index.ts +1 -0
  76. package/src/extensions/{white-ink.ts → white-ink/WhiteInkTool.ts} +130 -317
  77. package/src/extensions/white-ink/commands.ts +157 -0
  78. package/src/extensions/white-ink/config.ts +30 -0
  79. package/src/extensions/white-ink/index.ts +5 -0
  80. package/src/extensions/white-ink/model.ts +1 -0
  81. package/src/extensions/white-ink/renderer.ts +1 -0
  82. package/src/services/SceneLayoutService.ts +139 -0
  83. package/src/services/index.ts +1 -0
  84. package/src/shared/constants/layers.ts +23 -0
  85. package/src/shared/imaging/sourceSizeCache.ts +103 -0
  86. package/src/shared/index.ts +6 -0
  87. package/src/shared/runtime/sessionState.ts +105 -0
  88. package/src/shared/runtime/subscriptions.ts +45 -0
  89. package/src/shared/scene/frame.ts +46 -0
  90. package/src/shared/scene/sceneLayoutModel.ts +367 -0
  91. package/tests/run.ts +146 -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,7 +1017,7 @@ 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";
@@ -1549,9 +1649,351 @@ function getPathBounds(pathData) {
1549
1649
  };
1550
1650
  }
1551
1651
 
1552
- // src/extensions/image.ts
1553
- var IMAGE_OBJECT_LAYER_ID = "image.user";
1554
- 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
1555
1997
  var IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
1556
1998
  "rotate",
1557
1999
  "scale"
@@ -1590,7 +2032,9 @@ var ImageTool = class {
1590
2032
  this.workingItems = [];
1591
2033
  this.hasWorkingChanges = false;
1592
2034
  this.loadResolvers = /* @__PURE__ */ new Map();
1593
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
2035
+ this.sourceSizeCache = createSourceSizeCache(
2036
+ (src) => this.loadImageSize(src)
2037
+ );
1594
2038
  this.isUpdatingConfig = false;
1595
2039
  this.isToolActive = false;
1596
2040
  this.isImageSelectionActive = false;
@@ -1598,6 +2042,7 @@ var ImageTool = class {
1598
2042
  this.renderSeq = 0;
1599
2043
  this.imageSpecs = [];
1600
2044
  this.overlaySpecs = [];
2045
+ this.subscriptions = new SubscriptionBag();
1601
2046
  this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
1602
2047
  this.onToolActivated = (event) => {
1603
2048
  const before = this.isToolActive;
@@ -1695,6 +2140,7 @@ var ImageTool = class {
1695
2140
  }
1696
2141
  activate(context) {
1697
2142
  var _a;
2143
+ this.subscriptions.disposeAll();
1698
2144
  this.context = context;
1699
2145
  this.canvasService = context.services.get("CanvasService");
1700
2146
  if (!this.canvasService) {
@@ -1736,40 +2182,55 @@ var ImageTool = class {
1736
2182
  }),
1737
2183
  { priority: 300 }
1738
2184
  );
1739
- context.eventBus.on("tool:activated", this.onToolActivated);
1740
- context.eventBus.on("object:modified", this.onObjectModified);
1741
- context.eventBus.on("selection:created", this.onSelectionChanged);
1742
- context.eventBus.on("selection:updated", this.onSelectionChanged);
1743
- context.eventBus.on("selection:cleared", this.onSelectionCleared);
1744
- context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
1745
- 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
+ );
1746
2212
  const configService = context.services.get(
1747
2213
  "ConfigurationService"
1748
2214
  );
1749
2215
  if (configService) {
1750
- this.items = this.normalizeItems(
1751
- configService.get("image.items", []) || []
1752
- );
1753
- this.workingItems = this.cloneItems(this.items);
1754
- this.hasWorkingChanges = false;
1755
- configService.onAnyChange((e) => {
1756
- if (this.isUpdatingConfig) return;
1757
- if (e.key === "image.items") {
1758
- this.items = this.normalizeItems(e.value || []);
1759
- if (!this.isToolActive || !this.hasWorkingChanges) {
1760
- this.workingItems = this.cloneItems(this.items);
1761
- 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;
1762
2225
  }
1763
- this.updateImages();
1764
- return;
1765
- }
1766
- if (e.key.startsWith("size.") || e.key.startsWith("image.frame.") || e.key.startsWith("image.control.")) {
1767
- if (e.key.startsWith("image.control.")) {
1768
- this.imageControlsByCapabilityKey.clear();
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();
1769
2231
  }
1770
- this.updateImages();
1771
2232
  }
1772
- });
2233
+ );
1773
2234
  }
1774
2235
  const toolSessionService = context.services.get("ToolSessionService");
1775
2236
  this.dirtyTrackerDisposable = toolSessionService == null ? void 0 : toolSessionService.registerDirtyTracker(
@@ -1780,18 +2241,13 @@ var ImageTool = class {
1780
2241
  }
1781
2242
  deactivate(context) {
1782
2243
  var _a, _b;
1783
- context.eventBus.off("tool:activated", this.onToolActivated);
1784
- context.eventBus.off("object:modified", this.onObjectModified);
1785
- context.eventBus.off("selection:created", this.onSelectionChanged);
1786
- context.eventBus.off("selection:updated", this.onSelectionChanged);
1787
- context.eventBus.off("selection:cleared", this.onSelectionCleared);
1788
- context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
1789
- context.eventBus.off("scene:geometry:change", this.onSceneGeometryChanged);
2244
+ this.subscriptions.disposeAll();
1790
2245
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
1791
2246
  this.dirtyTrackerDisposable = void 0;
1792
2247
  this.cropShapeHatchPattern = void 0;
1793
2248
  this.cropShapeHatchPatternColor = void 0;
1794
2249
  this.cropShapeHatchPatternKey = void 0;
2250
+ this.sourceSizeCache.clear();
1795
2251
  this.imageSpecs = [];
1796
2252
  this.overlaySpecs = [];
1797
2253
  this.imageControlsByCapabilityKey.clear();
@@ -1930,272 +2386,8 @@ var ImageTool = class {
1930
2386
  }
1931
2387
  }
1932
2388
  ],
1933
- [ContributionPointIds2.CONFIGURATIONS]: [
1934
- {
1935
- id: "image.items",
1936
- type: "array",
1937
- label: "Images",
1938
- default: []
1939
- },
1940
- {
1941
- id: "image.debug",
1942
- type: "boolean",
1943
- label: "Image Debug Log",
1944
- default: false
1945
- },
1946
- {
1947
- id: "image.control.cornerSize",
1948
- type: "number",
1949
- label: "Image Control Corner Size",
1950
- min: 4,
1951
- max: 64,
1952
- step: 1,
1953
- default: 14
1954
- },
1955
- {
1956
- id: "image.control.touchCornerSize",
1957
- type: "number",
1958
- label: "Image Control Touch Corner Size",
1959
- min: 8,
1960
- max: 96,
1961
- step: 1,
1962
- default: 24
1963
- },
1964
- {
1965
- id: "image.control.cornerStyle",
1966
- type: "select",
1967
- label: "Image Control Corner Style",
1968
- options: ["circle", "rect"],
1969
- default: "circle"
1970
- },
1971
- {
1972
- id: "image.control.cornerColor",
1973
- type: "color",
1974
- label: "Image Control Corner Color",
1975
- default: "#ffffff"
1976
- },
1977
- {
1978
- id: "image.control.cornerStrokeColor",
1979
- type: "color",
1980
- label: "Image Control Corner Stroke Color",
1981
- default: "#1677ff"
1982
- },
1983
- {
1984
- id: "image.control.transparentCorners",
1985
- type: "boolean",
1986
- label: "Image Control Transparent Corners",
1987
- default: false
1988
- },
1989
- {
1990
- id: "image.control.borderColor",
1991
- type: "color",
1992
- label: "Image Control Border Color",
1993
- default: "#1677ff"
1994
- },
1995
- {
1996
- id: "image.control.borderScaleFactor",
1997
- type: "number",
1998
- label: "Image Control Border Width",
1999
- min: 0.5,
2000
- max: 8,
2001
- step: 0.1,
2002
- default: 1.5
2003
- },
2004
- {
2005
- id: "image.control.padding",
2006
- type: "number",
2007
- label: "Image Control Padding",
2008
- min: 0,
2009
- max: 64,
2010
- step: 1,
2011
- default: 0
2012
- },
2013
- {
2014
- id: "image.frame.strokeColor",
2015
- type: "color",
2016
- label: "Image Frame Stroke Color",
2017
- default: "#808080"
2018
- },
2019
- {
2020
- id: "image.frame.strokeWidth",
2021
- type: "number",
2022
- label: "Image Frame Stroke Width",
2023
- min: 0,
2024
- max: 20,
2025
- step: 0.5,
2026
- default: 2
2027
- },
2028
- {
2029
- id: "image.frame.strokeStyle",
2030
- type: "select",
2031
- label: "Image Frame Stroke Style",
2032
- options: ["solid", "dashed", "hidden"],
2033
- default: "dashed"
2034
- },
2035
- {
2036
- id: "image.frame.dashLength",
2037
- type: "number",
2038
- label: "Image Frame Dash Length",
2039
- min: 1,
2040
- max: 40,
2041
- step: 1,
2042
- default: 8
2043
- },
2044
- {
2045
- id: "image.frame.innerBackground",
2046
- type: "color",
2047
- label: "Image Frame Inner Background",
2048
- default: "rgba(0,0,0,0)"
2049
- },
2050
- {
2051
- id: "image.frame.outerBackground",
2052
- type: "color",
2053
- label: "Image Frame Outer Background",
2054
- default: "#f5f5f5"
2055
- }
2056
- ],
2057
- [ContributionPointIds2.COMMANDS]: [
2058
- {
2059
- command: "addImage",
2060
- title: "Add Image",
2061
- handler: async (url, options) => {
2062
- const result = await this.upsertImageEntry(url, {
2063
- mode: "add",
2064
- addOptions: options
2065
- });
2066
- return result.id;
2067
- }
2068
- },
2069
- {
2070
- command: "upsertImage",
2071
- title: "Upsert Image",
2072
- handler: async (url, options = {}) => {
2073
- return await this.upsertImageEntry(url, options);
2074
- }
2075
- },
2076
- {
2077
- command: "getWorkingImages",
2078
- title: "Get Working Images",
2079
- handler: () => {
2080
- return this.cloneItems(this.workingItems);
2081
- }
2082
- },
2083
- {
2084
- command: "setWorkingImage",
2085
- title: "Set Working Image",
2086
- handler: (id, updates) => {
2087
- this.updateImageInWorking(id, updates);
2088
- }
2089
- },
2090
- {
2091
- command: "resetWorkingImages",
2092
- title: "Reset Working Images",
2093
- handler: () => {
2094
- this.workingItems = this.cloneItems(this.items);
2095
- this.hasWorkingChanges = false;
2096
- this.updateImages();
2097
- this.emitWorkingChange();
2098
- }
2099
- },
2100
- {
2101
- command: "completeImages",
2102
- title: "Complete Images",
2103
- handler: async () => {
2104
- return await this.commitWorkingImagesAsCropped();
2105
- }
2106
- },
2107
- {
2108
- command: "exportUserCroppedImage",
2109
- title: "Export User Cropped Image",
2110
- handler: async (options = {}) => {
2111
- return await this.exportUserCroppedImage(options);
2112
- }
2113
- },
2114
- {
2115
- command: "fitImageToArea",
2116
- title: "Fit Image to Area",
2117
- handler: async (id, area) => {
2118
- await this.fitImageToArea(id, area);
2119
- }
2120
- },
2121
- {
2122
- command: "fitImageToDefaultArea",
2123
- title: "Fit Image to Default Area",
2124
- handler: async (id) => {
2125
- await this.fitImageToDefaultArea(id);
2126
- }
2127
- },
2128
- {
2129
- command: "focusImage",
2130
- title: "Focus Image",
2131
- handler: (id, options = {}) => {
2132
- return this.setImageFocus(id, options);
2133
- }
2134
- },
2135
- {
2136
- command: "removeImage",
2137
- title: "Remove Image",
2138
- handler: (id) => {
2139
- const removed = this.items.find((item) => item.id === id);
2140
- const next = this.items.filter((item) => item.id !== id);
2141
- if (next.length !== this.items.length) {
2142
- this.purgeSourceSizeCacheForItem(removed);
2143
- if (this.focusedImageId === id) {
2144
- this.setImageFocus(null, {
2145
- syncCanvasSelection: true,
2146
- skipRender: true
2147
- });
2148
- }
2149
- this.updateConfig(next);
2150
- }
2151
- }
2152
- },
2153
- {
2154
- command: "updateImage",
2155
- title: "Update Image",
2156
- handler: async (id, updates, options = {}) => {
2157
- await this.updateImage(id, updates, options);
2158
- }
2159
- },
2160
- {
2161
- command: "clearImages",
2162
- title: "Clear Images",
2163
- handler: () => {
2164
- this.sourceSizeBySrc.clear();
2165
- this.setImageFocus(null, {
2166
- syncCanvasSelection: true,
2167
- skipRender: true
2168
- });
2169
- this.updateConfig([]);
2170
- }
2171
- },
2172
- {
2173
- command: "bringToFront",
2174
- title: "Bring Image to Front",
2175
- handler: (id) => {
2176
- const index = this.items.findIndex((item) => item.id === id);
2177
- if (index !== -1 && index < this.items.length - 1) {
2178
- const next = [...this.items];
2179
- const [item] = next.splice(index, 1);
2180
- next.push(item);
2181
- this.updateConfig(next);
2182
- }
2183
- }
2184
- },
2185
- {
2186
- command: "sendToBack",
2187
- title: "Send Image to Back",
2188
- handler: (id) => {
2189
- const index = this.items.findIndex((item) => item.id === id);
2190
- if (index > 0) {
2191
- const next = [...this.items];
2192
- const [item] = next.splice(index, 1);
2193
- next.unshift(item);
2194
- this.updateConfig(next);
2195
- }
2196
- }
2197
- }
2198
- ]
2389
+ [ContributionPointIds2.CONFIGURATIONS]: createImageConfigurations(),
2390
+ [ContributionPointIds2.COMMANDS]: createImageCommands(this)
2199
2391
  };
2200
2392
  }
2201
2393
  normalizeItem(item) {
@@ -2321,47 +2513,45 @@ var ImageTool = class {
2321
2513
  if (!configService) return fallback;
2322
2514
  return (_a = configService.get(key, fallback)) != null ? _a : fallback;
2323
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
+ }
2324
2531
  updateConfig(newItems, skipCanvasUpdate = false) {
2325
2532
  if (!this.context) return;
2326
- this.isUpdatingConfig = true;
2327
- this.items = this.normalizeItems(newItems);
2328
- if (!this.isToolActive || !this.hasWorkingChanges) {
2329
- this.workingItems = this.cloneItems(this.items);
2330
- this.hasWorkingChanges = false;
2331
- }
2332
- const configService = this.context.services.get(
2333
- "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
2334
2547
  );
2335
- configService == null ? void 0 : configService.update("image.items", this.items);
2336
- if (!skipCanvasUpdate) {
2337
- this.updateImages();
2338
- }
2339
- setTimeout(() => {
2340
- this.isUpdatingConfig = false;
2341
- }, 50);
2342
2548
  }
2343
2549
  getFrameRect() {
2344
2550
  var _a;
2345
- if (!this.canvasService) {
2346
- return { left: 0, top: 0, width: 0, height: 0 };
2347
- }
2348
2551
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2349
2552
  "ConfigurationService"
2350
2553
  );
2351
- if (!configService) {
2352
- return { left: 0, top: 0, width: 0, height: 0 };
2353
- }
2354
- const sizeState = readSizeState(configService);
2355
- const layout = computeSceneLayout(this.canvasService, sizeState);
2356
- if (!layout) {
2357
- return { left: 0, top: 0, width: 0, height: 0 };
2358
- }
2359
- return this.canvasService.toSceneRect({
2360
- left: layout.cutRect.left,
2361
- top: layout.cutRect.top,
2362
- width: layout.cutRect.width,
2363
- height: layout.cutRect.height
2364
- });
2554
+ return resolveCutFrameRect(this.canvasService, configService);
2365
2555
  }
2366
2556
  getFrameRectScreen(frame) {
2367
2557
  if (!this.canvasService) {
@@ -2370,13 +2560,7 @@ var ImageTool = class {
2370
2560
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
2371
2561
  }
2372
2562
  toLayoutSceneRect(rect) {
2373
- return {
2374
- left: rect.left,
2375
- top: rect.top,
2376
- width: rect.width,
2377
- height: rect.height,
2378
- space: "scene"
2379
- };
2563
+ return toLayoutSceneRect(rect);
2380
2564
  }
2381
2565
  async resolveDefaultFitArea() {
2382
2566
  if (!this.canvasService) return null;
@@ -2434,31 +2618,31 @@ var ImageTool = class {
2434
2618
  const sources = [item.url, item.sourceUrl, item.committedUrl].filter(
2435
2619
  (value) => typeof value === "string" && value.length > 0
2436
2620
  );
2437
- sources.forEach((src) => this.sourceSizeBySrc.delete(src));
2621
+ sources.forEach((src) => this.sourceSizeCache.deleteSourceSize(src));
2438
2622
  }
2439
2623
  rememberSourceSize(src, obj) {
2440
2624
  const width = Number((obj == null ? void 0 : obj.width) || 0);
2441
2625
  const height = Number((obj == null ? void 0 : obj.height) || 0);
2442
2626
  if (src && width > 0 && height > 0) {
2443
- this.sourceSizeBySrc.set(src, { width, height });
2627
+ this.sourceSizeCache.rememberSourceSize(src, { width, height });
2444
2628
  }
2445
2629
  }
2446
2630
  getSourceSize(src, obj) {
2447
- const cached = src ? this.sourceSizeBySrc.get(src) : void 0;
2631
+ const cached = src ? this.sourceSizeCache.getSourceSize(src) : void 0;
2448
2632
  if (cached) return cached;
2449
2633
  const width = Number((obj == null ? void 0 : obj.width) || 0);
2450
2634
  const height = Number((obj == null ? void 0 : obj.height) || 0);
2451
2635
  if (src && width > 0 && height > 0) {
2452
2636
  const size = { width, height };
2453
- this.sourceSizeBySrc.set(src, size);
2637
+ this.sourceSizeCache.rememberSourceSize(src, size);
2454
2638
  return size;
2455
2639
  }
2456
2640
  return { width: 1, height: 1 };
2457
2641
  }
2458
2642
  async ensureSourceSize(src) {
2459
- if (!src) return null;
2460
- const cached = this.sourceSizeBySrc.get(src);
2461
- if (cached) return cached;
2643
+ return this.sourceSizeCache.ensureImageSize(src);
2644
+ }
2645
+ async loadImageSize(src) {
2462
2646
  try {
2463
2647
  const image = await FabricImage2.fromURL(src, {
2464
2648
  crossOrigin: "anonymous"
@@ -2466,9 +2650,7 @@ var ImageTool = class {
2466
2650
  const width = Number((image == null ? void 0 : image.width) || 0);
2467
2651
  const height = Number((image == null ? void 0 : image.height) || 0);
2468
2652
  if (width > 0 && height > 0) {
2469
- const size = { width, height };
2470
- this.sourceSizeBySrc.set(src, size);
2471
- return size;
2653
+ return { width, height };
2472
2654
  }
2473
2655
  } catch (error) {
2474
2656
  this.debug("image:size:load-failed", {
@@ -2479,11 +2661,7 @@ var ImageTool = class {
2479
2661
  return null;
2480
2662
  }
2481
2663
  getCoverScale(frame, size) {
2482
- const sw = Math.max(1, size.width);
2483
- const sh = Math.max(1, size.height);
2484
- const fw = Math.max(1, frame.width);
2485
- const fh = Math.max(1, frame.height);
2486
- return Math.max(fw / sw, fh / sh);
2664
+ return getCoverScale(frame, size);
2487
2665
  }
2488
2666
  getFrameVisualConfig() {
2489
2667
  var _a, _b;
@@ -3235,7 +3413,7 @@ var ImageTool = class {
3235
3413
  })
3236
3414
  );
3237
3415
  if (previousCommitted && previousCommitted !== url) {
3238
- this.sourceSizeBySrc.delete(previousCommitted);
3416
+ this.sourceSizeCache.deleteSourceSize(previousCommitted);
3239
3417
  }
3240
3418
  }
3241
3419
  this.hasWorkingChanges = false;
@@ -3331,7 +3509,7 @@ var ImageTool = class {
3331
3509
  }
3332
3510
  };
3333
3511
 
3334
- // src/extensions/size.ts
3512
+ // src/extensions/size/SizeTool.ts
3335
3513
  import {
3336
3514
  ContributionPointIds as ContributionPointIds3
3337
3515
  } from "@pooder/core";
@@ -3632,16 +3810,16 @@ var SizeTool = class {
3632
3810
  if (!layout || layout.scale <= 0) return null;
3633
3811
  const all = this.canvasService.canvas.getObjects();
3634
3812
  const active = this.canvasService.canvas.getActiveObject();
3635
- 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;
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;
3636
3814
  const targetId = id || activeId;
3637
3815
  const target = all.find(
3638
3816
  (obj) => {
3639
3817
  var _a2, _b2;
3640
- 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;
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;
3641
3819
  }
3642
3820
  ) || all.find((obj) => {
3643
3821
  var _a2;
3644
- return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === "image.user";
3822
+ return ((_a2 = obj == null ? void 0 : obj.data) == null ? void 0 : _a2.layerId) === IMAGE_OBJECT_LAYER_ID;
3645
3823
  });
3646
3824
  if (!target) return null;
3647
3825
  const objectWidthPx = Math.abs((target.width || 0) * (target.scaleX || 1));
@@ -3662,1029 +3840,213 @@ var SizeTool = class {
3662
3840
  }
3663
3841
  };
3664
3842
 
3665
- // src/extensions/dieline.ts
3843
+ // src/extensions/dieline/DielineTool.ts
3666
3844
  import {
3667
3845
  ContributionPointIds as ContributionPointIds4
3668
3846
  } from "@pooder/core";
3669
3847
  import { Canvas as FabricCanvas2, Path, Pattern as Pattern2 } from "fabric";
3670
3848
 
3671
- // src/extensions/tracer.ts
3672
- import paper2 from "paper";
3673
-
3674
- // src/extensions/maskOps.ts
3675
- function createMask(imageData, options) {
3676
- const { width, height, data } = imageData;
3677
- const {
3678
- threshold,
3679
- padding,
3680
- paddedWidth,
3681
- paddedHeight,
3682
- maskMode = "auto",
3683
- whiteThreshold = 240,
3684
- alphaOpaqueCutoff = 250
3685
- } = options;
3686
- const resolvedMode = maskMode === "auto" ? inferMaskMode(imageData, alphaOpaqueCutoff) : maskMode;
3687
- const mask = new Uint8Array(paddedWidth * paddedHeight);
3688
- for (let y = 0; y < height; y++) {
3689
- for (let x = 0; x < width; x++) {
3690
- const srcIdx = (y * width + x) * 4;
3691
- const r = data[srcIdx];
3692
- const g = data[srcIdx + 1];
3693
- const b = data[srcIdx + 2];
3694
- const a = data[srcIdx + 3];
3695
- const destIdx = (y + padding) * paddedWidth + (x + padding);
3696
- if (resolvedMode === "alpha") {
3697
- if (a > threshold) mask[destIdx] = 1;
3698
- } else {
3699
- if (a > threshold && !(r > whiteThreshold && g > whiteThreshold && b > whiteThreshold)) {
3700
- mask[destIdx] = 1;
3701
- }
3702
- }
3703
- }
3704
- }
3705
- return mask;
3706
- }
3707
- function inferMaskMode(imageData, alphaOpaqueCutoff) {
3708
- const analysis = analyzeAlpha(imageData, alphaOpaqueCutoff);
3709
- if (analysis.minAlpha === 255) return "whitebg";
3710
- if (analysis.veryTransparentRatio >= 5e-4) return "alpha";
3711
- if (analysis.belowOpaqueRatio >= 0.01) return "alpha";
3712
- return "whitebg";
3713
- }
3714
- function analyzeAlpha(imageData, alphaOpaqueCutoff) {
3715
- const { data } = imageData;
3716
- const total = data.length / 4;
3717
- let belowOpaque = 0;
3718
- let veryTransparent = 0;
3719
- let minAlpha = 255;
3720
- for (let i = 3; i < data.length; i += 4) {
3721
- const a = data[i];
3722
- if (a < minAlpha) minAlpha = a;
3723
- if (a < alphaOpaqueCutoff) belowOpaque++;
3724
- if (a < 32) veryTransparent++;
3725
- }
3726
- return {
3727
- total,
3728
- minAlpha,
3729
- belowOpaqueRatio: belowOpaque / total,
3730
- veryTransparentRatio: veryTransparent / total
3731
- };
3732
- }
3733
- function circularMorphology(mask, width, height, radius, op) {
3734
- const r = Math.max(0, Math.floor(radius));
3735
- if (r <= 0) {
3736
- return mask.slice();
3737
- }
3738
- const dilateDisk = (m, radiusPx) => {
3739
- const horizontalDist = new Int32Array(width * height);
3740
- for (let y = 0; y < height; y++) {
3741
- let lastSolid = -radiusPx * 2;
3742
- for (let x = 0; x < width; x++) {
3743
- if (m[y * width + x]) lastSolid = x;
3744
- horizontalDist[y * width + x] = x - lastSolid;
3745
- }
3746
- lastSolid = width + radiusPx * 2;
3747
- for (let x = width - 1; x >= 0; x--) {
3748
- if (m[y * width + x]) lastSolid = x;
3749
- horizontalDist[y * width + x] = Math.min(
3750
- horizontalDist[y * width + x],
3751
- lastSolid - x
3752
- );
3753
- }
3754
- }
3755
- const result = new Uint8Array(width * height);
3756
- const r2 = radiusPx * radiusPx;
3757
- for (let x = 0; x < width; x++) {
3758
- for (let y = 0; y < height; y++) {
3759
- let found = false;
3760
- const minY = Math.max(0, y - radiusPx);
3761
- const maxY = Math.min(height - 1, y + radiusPx);
3762
- for (let dy = minY; dy <= maxY; dy++) {
3763
- const dY = dy - y;
3764
- const hDist = horizontalDist[dy * width + x];
3765
- if (hDist * hDist + dY * dY <= r2) {
3766
- found = true;
3767
- break;
3768
- }
3769
- }
3770
- if (found) result[y * width + x] = 1;
3771
- }
3772
- }
3773
- return result;
3774
- };
3775
- const erodeDiamond = (m, radiusPx) => {
3776
- if (radiusPx <= 0) return m.slice();
3777
- let current = m;
3778
- for (let step = 0; step < radiusPx; step++) {
3779
- const next = new Uint8Array(width * height);
3780
- for (let y = 1; y < height - 1; y++) {
3781
- const row = y * width;
3782
- for (let x = 1; x < width - 1; x++) {
3783
- const idx = row + x;
3784
- if (current[idx] && current[idx - 1] && current[idx + 1] && current[idx - width] && current[idx + width]) {
3785
- next[idx] = 1;
3786
- }
3787
- }
3788
- }
3789
- current = next;
3790
- }
3791
- return current;
3792
- };
3793
- const restoreBridgePixels = (source, eroded) => {
3794
- const restored = eroded.slice();
3795
- for (let y = 1; y < height - 1; y++) {
3796
- const row = y * width;
3797
- for (let x = 1; x < width - 1; x++) {
3798
- const idx = row + x;
3799
- if (!source[idx] || restored[idx]) continue;
3800
- const up = source[idx - width] === 1;
3801
- const down = source[idx + width] === 1;
3802
- const left = source[idx - 1] === 1;
3803
- const right = source[idx + 1] === 1;
3804
- const upLeft = source[idx - width - 1] === 1;
3805
- const upRight = source[idx - width + 1] === 1;
3806
- const downLeft = source[idx + width - 1] === 1;
3807
- const downRight = source[idx + width + 1] === 1;
3808
- const keepsBridge = left && right || up && down || upLeft && downRight || upRight && downLeft;
3809
- if (keepsBridge) {
3810
- restored[idx] = 1;
3811
- }
3812
- }
3813
- }
3814
- return restored;
3815
- };
3816
- const erodePreservingBridges = (m, radiusPx) => {
3817
- const eroded = erodeDiamond(m, radiusPx);
3818
- return restoreBridgePixels(m, eroded);
3819
- };
3820
- switch (op) {
3821
- case "dilate":
3822
- return dilateDisk(mask, r);
3823
- case "erode":
3824
- return erodePreservingBridges(mask, r);
3825
- case "closing": {
3826
- const erodeRadius = Math.max(1, Math.floor(r * 0.65));
3827
- return erodePreservingBridges(dilateDisk(mask, r), erodeRadius);
3828
- }
3829
- case "opening":
3830
- return dilateDisk(erodePreservingBridges(mask, r), r);
3831
- default:
3832
- return mask;
3833
- }
3834
- }
3835
- function fillHoles(mask, width, height) {
3836
- const background = new Uint8Array(width * height);
3837
- const queue = [];
3838
- for (let x = 0; x < width; x++) {
3839
- if (mask[x] === 0) {
3840
- background[x] = 1;
3841
- queue.push(x);
3842
- }
3843
- const lastRowIdx = (height - 1) * width + x;
3844
- if (mask[lastRowIdx] === 0) {
3845
- background[lastRowIdx] = 1;
3846
- queue.push(lastRowIdx);
3847
- }
3848
- }
3849
- for (let y = 1; y < height - 1; y++) {
3850
- const leftIdx = y * width;
3851
- const rightIdx = y * width + (width - 1);
3852
- if (mask[leftIdx] === 0) {
3853
- background[leftIdx] = 1;
3854
- queue.push(leftIdx);
3855
- }
3856
- if (mask[rightIdx] === 0) {
3857
- background[rightIdx] = 1;
3858
- queue.push(rightIdx);
3859
- }
3860
- }
3861
- let head = 0;
3862
- while (head < queue.length) {
3863
- const idx = queue[head++];
3864
- const x = idx % width;
3865
- const y = (idx - x) / width;
3866
- const up = y > 0 ? idx - width : -1;
3867
- const down = y < height - 1 ? idx + width : -1;
3868
- const left = x > 0 ? idx - 1 : -1;
3869
- const right = x < width - 1 ? idx + 1 : -1;
3870
- if (up >= 0 && mask[up] === 0 && background[up] === 0) {
3871
- background[up] = 1;
3872
- queue.push(up);
3873
- }
3874
- if (down >= 0 && mask[down] === 0 && background[down] === 0) {
3875
- background[down] = 1;
3876
- queue.push(down);
3877
- }
3878
- if (left >= 0 && mask[left] === 0 && background[left] === 0) {
3879
- background[left] = 1;
3880
- queue.push(left);
3881
- }
3882
- if (right >= 0 && mask[right] === 0 && background[right] === 0) {
3883
- background[right] = 1;
3884
- queue.push(right);
3885
- }
3886
- }
3887
- const filledMask = new Uint8Array(width * height);
3888
- for (let i = 0; i < width * height; i++) {
3889
- filledMask[i] = background[i] === 0 ? 1 : 0;
3890
- }
3891
- return filledMask;
3892
- }
3893
- function polygonSignedArea(points) {
3894
- if (points.length < 3) return 0;
3895
- let sum = 0;
3896
- for (let i = 0; i < points.length; i++) {
3897
- const a = points[i];
3898
- const b = points[(i + 1) % points.length];
3899
- sum += a.x * b.y - b.x * a.y;
3900
- }
3901
- return sum / 2;
3902
- }
3903
-
3904
- // src/extensions/tracer.ts
3905
- var ImageTracer = class {
3906
- /**
3907
- * Main entry point: Traces an image URL to an SVG path string.
3908
- * @param imageUrl The URL or Base64 string of the image.
3909
- * @param options Configuration options.
3910
- */
3911
- static async trace(imageUrl, options = {}) {
3912
- const { pathData } = await this.traceWithBounds(imageUrl, options);
3913
- return pathData;
3914
- }
3915
- static async traceWithBounds(imageUrl, options = {}) {
3916
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
3917
- const img = await this.loadImage(imageUrl);
3918
- const width = img.width;
3919
- const height = img.height;
3920
- if (width <= 0 || height <= 0) {
3921
- const w = (_a = options.scaleToWidth) != null ? _a : 0;
3922
- const h = (_b = options.scaleToHeight) != null ? _b : 0;
3923
- return {
3924
- pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
3925
- baseBounds: { x: 0, y: 0, width: w, height: h },
3926
- bounds: { x: 0, y: 0, width: w, height: h }
3927
- };
3928
- }
3929
- const debug = options.debug === true;
3930
- const debugLog = (message, payload) => {
3931
- if (!debug) return;
3932
- if (payload) {
3933
- console.info(`[ImageTracer] ${message}`, payload);
3934
- return;
3935
- }
3936
- console.info(`[ImageTracer] ${message}`);
3937
- };
3938
- const canvas = document.createElement("canvas");
3939
- canvas.width = width;
3940
- canvas.height = height;
3941
- const ctx = canvas.getContext("2d");
3942
- if (!ctx) throw new Error("Could not get 2D context");
3943
- ctx.drawImage(img, 0, 0);
3944
- const imageData = ctx.getImageData(0, 0, width, height);
3945
- const threshold = (_c = options.threshold) != null ? _c : 10;
3946
- const expand = Math.max(0, Math.floor((_d = options.expand) != null ? _d : 0));
3947
- const simplifyTolerance = (_e = options.simplifyTolerance) != null ? _e : 2.5;
3948
- const useSmoothing = options.smoothing !== false;
3949
- const componentMode = "all";
3950
- const minComponentArea = 0;
3951
- const maxDim = Math.max(width, height);
3952
- const maskMode = "auto";
3953
- const whiteThreshold = 240;
3954
- const alphaOpaqueCutoff = 250;
3955
- const preprocessDilateRadius = Math.max(
3956
- 2,
3957
- Math.floor(Math.max(maxDim * 0.012, expand * 0.35))
3958
- );
3959
- const preprocessErodeRadius = Math.max(
3960
- 1,
3961
- Math.floor(preprocessDilateRadius * 0.65)
3962
- );
3963
- const smoothDilateRadius = Math.max(
3964
- 1,
3965
- Math.floor(preprocessDilateRadius * 0.25)
3966
- );
3967
- const smoothErodeRadius = Math.max(1, Math.floor(smoothDilateRadius * 0.8));
3968
- const connectStartDilateRadius = Math.max(
3969
- 1,
3970
- Math.floor(Math.max(maxDim * 6e-3, expand * 0.2))
3971
- );
3972
- const connectMaxDilateRadius = Math.max(
3973
- connectStartDilateRadius,
3974
- Math.floor(Math.max(maxDim * 0.2, expand * 2.5))
3975
- );
3976
- const connectErodeRatio = 0.65;
3977
- debugLog("traceWithBounds:start", {
3978
- width,
3979
- height,
3980
- threshold,
3981
- expand,
3982
- simplifyTolerance,
3983
- smoothing: useSmoothing,
3984
- strategy: {
3985
- maskMode,
3986
- whiteThreshold,
3987
- alphaOpaqueCutoff,
3988
- fillHoles: true,
3989
- preprocessDilateRadius,
3990
- preprocessErodeRadius,
3991
- smoothDilateRadius,
3992
- smoothErodeRadius,
3993
- connectEnabled: true,
3994
- connectStartDilateRadius,
3995
- connectMaxDilateRadius,
3996
- connectErodeRatio
3997
- }
3998
- });
3999
- const padding = Math.max(
4000
- preprocessDilateRadius,
4001
- smoothDilateRadius,
4002
- connectMaxDilateRadius,
4003
- expand
4004
- ) + 2;
4005
- const paddedWidth = width + padding * 2;
4006
- const paddedHeight = height + padding * 2;
4007
- const summarizeMaskContours = (m) => {
4008
- const summary = this.summarizeAllContours(
4009
- m,
4010
- paddedWidth,
4011
- paddedHeight,
4012
- minComponentArea
4013
- );
4014
- return {
4015
- rawContourCount: summary.rawCount,
4016
- selectedContourCount: summary.selectedCount
4017
- };
4018
- };
4019
- let mask = createMask(imageData, {
4020
- threshold,
4021
- padding,
4022
- paddedWidth,
4023
- paddedHeight,
4024
- maskMode,
4025
- whiteThreshold,
4026
- alphaOpaqueCutoff
4027
- });
4028
- if (debug) {
4029
- debugLog(
4030
- "traceWithBounds:mask:after-create",
4031
- summarizeMaskContours(mask)
4032
- );
4033
- }
4034
- mask = circularMorphology(
4035
- mask,
4036
- paddedWidth,
4037
- paddedHeight,
4038
- preprocessDilateRadius,
4039
- "dilate"
4040
- );
4041
- mask = fillHoles(mask, paddedWidth, paddedHeight);
4042
- mask = circularMorphology(
4043
- mask,
4044
- paddedWidth,
4045
- paddedHeight,
4046
- preprocessErodeRadius,
4047
- "erode"
4048
- );
4049
- mask = fillHoles(mask, paddedWidth, paddedHeight);
4050
- if (debug) {
4051
- debugLog("traceWithBounds:mask:after-preprocess", {
4052
- dilateRadius: preprocessDilateRadius,
4053
- erodeRadius: preprocessErodeRadius,
4054
- ...summarizeMaskContours(mask)
4055
- });
4056
- }
4057
- mask = circularMorphology(
4058
- mask,
4059
- paddedWidth,
4060
- paddedHeight,
4061
- smoothDilateRadius,
4062
- "dilate"
4063
- );
4064
- mask = fillHoles(mask, paddedWidth, paddedHeight);
4065
- mask = circularMorphology(
4066
- mask,
4067
- paddedWidth,
4068
- paddedHeight,
4069
- smoothErodeRadius,
4070
- "erode"
4071
- );
4072
- mask = fillHoles(mask, paddedWidth, paddedHeight);
4073
- if (debug) {
4074
- debugLog("traceWithBounds:mask:after-smooth", {
4075
- dilateRadius: smoothDilateRadius,
4076
- erodeRadius: smoothErodeRadius,
4077
- ...summarizeMaskContours(mask)
4078
- });
4079
- }
4080
- const beforeConnectSummary = summarizeMaskContours(mask);
4081
- if (beforeConnectSummary.selectedContourCount <= 1) {
4082
- debugLog("traceWithBounds:mask:connect-skipped", {
4083
- reason: "already-single-component",
4084
- before: beforeConnectSummary
4085
- });
4086
- } else {
4087
- const connectResult = this.findForceConnectResult(
4088
- mask,
4089
- paddedWidth,
4090
- paddedHeight,
4091
- minComponentArea,
4092
- connectStartDilateRadius,
4093
- connectMaxDilateRadius,
4094
- connectErodeRatio
4095
- );
4096
- if (debug) {
4097
- debugLog("traceWithBounds:mask:after-connect", {
4098
- before: beforeConnectSummary,
4099
- appliedDilateRadius: connectResult.appliedDilateRadius,
4100
- appliedErodeRadius: connectResult.appliedErodeRadius,
4101
- reachedSingleComponent: connectResult.reachedSingleComponent,
4102
- after: {
4103
- rawContourCount: connectResult.rawContourCount,
4104
- selectedContourCount: connectResult.selectedContourCount
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 };
3867
+ }
4105
3868
  }
3869
+ return f;
4106
3870
  });
4107
- }
4108
- mask = connectResult.mask;
4109
- }
4110
- if (debug) {
4111
- const afterConnectSummary = summarizeMaskContours(mask);
4112
- if (afterConnectSummary.selectedContourCount > 1) {
4113
- debugLog("traceWithBounds:mask:connect-warning", {
4114
- reason: "still-multi-component-after-connect-search",
4115
- summary: afterConnectSummary
4116
- });
4117
- }
4118
- }
4119
- const baseMask = mask;
4120
- const baseContoursRaw = this.traceAllContours(
4121
- baseMask,
4122
- paddedWidth,
4123
- paddedHeight
4124
- );
4125
- const baseContours = this.selectContours(
4126
- baseContoursRaw,
4127
- componentMode,
4128
- minComponentArea
4129
- );
4130
- if (!baseContours.length) {
4131
- const w = (_f = options.scaleToWidth) != null ? _f : width;
4132
- const h = (_g = options.scaleToHeight) != null ? _g : height;
4133
- debugLog("fallback:no-base-contour", { width: w, height: h });
4134
- return {
4135
- pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
4136
- baseBounds: { x: 0, y: 0, width: w, height: h },
4137
- bounds: { x: 0, y: 0, width: w, height: h }
4138
- };
4139
- }
4140
- const baseUnpaddedContours = baseContours.map(
4141
- (contour) => contour.map((p) => ({
4142
- x: p.x - padding,
4143
- y: p.y - padding
4144
- }))
4145
- ).filter((contour) => contour.length > 2);
4146
- if (!baseUnpaddedContours.length) {
4147
- const w = (_h = options.scaleToWidth) != null ? _h : width;
4148
- const h = (_i = options.scaleToHeight) != null ? _i : height;
4149
- debugLog("fallback:empty-base-contours", { width: w, height: h });
4150
- return {
4151
- pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
4152
- baseBounds: { x: 0, y: 0, width: w, height: h },
4153
- bounds: { x: 0, y: 0, width: w, height: h }
4154
- };
4155
- }
4156
- let baseBounds = this.boundsFromPoints(
4157
- this.flattenContours(baseUnpaddedContours)
4158
- );
4159
- let maskExpanded = baseMask;
4160
- if (expand > 0) {
4161
- maskExpanded = circularMorphology(
4162
- baseMask,
4163
- paddedWidth,
4164
- paddedHeight,
4165
- expand,
4166
- "dilate"
4167
- );
4168
- }
4169
- const expandedContoursRaw = this.traceAllContours(
4170
- maskExpanded,
4171
- paddedWidth,
4172
- paddedHeight
4173
- );
4174
- const expandedContours = this.selectContours(
4175
- expandedContoursRaw,
4176
- componentMode,
4177
- minComponentArea
4178
- );
4179
- if (!expandedContours.length) {
4180
- debugLog("fallback:no-expanded-contour", {
4181
- baseBounds,
4182
- width,
4183
- height,
4184
- expand
4185
- });
4186
- return {
4187
- pathData: `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`,
4188
- baseBounds,
4189
- bounds: baseBounds
4190
- };
4191
- }
4192
- const expandedUnpaddedContours = expandedContours.map(
4193
- (contour) => contour.map((p) => ({
4194
- x: p.x - padding,
4195
- y: p.y - padding
4196
- }))
4197
- ).filter((contour) => contour.length > 2);
4198
- if (!expandedUnpaddedContours.length) {
4199
- debugLog("fallback:empty-expanded-contours", {
4200
- baseBounds,
4201
- width,
4202
- height,
4203
- expand
4204
- });
4205
- return {
4206
- pathData: `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`,
4207
- baseBounds,
4208
- bounds: baseBounds
4209
- };
4210
- }
4211
- let globalBounds = this.boundsFromPoints(
4212
- this.flattenContours(expandedUnpaddedContours)
4213
- );
4214
- let finalContours = expandedUnpaddedContours;
4215
- if (options.scaleToWidth && options.scaleToHeight) {
4216
- finalContours = this.scaleContours(
4217
- expandedUnpaddedContours,
4218
- options.scaleToWidth,
4219
- options.scaleToHeight,
4220
- globalBounds
4221
- );
4222
- globalBounds = this.boundsFromPoints(this.flattenContours(finalContours));
4223
- const baseScaledContours = this.scaleContours(
4224
- baseUnpaddedContours,
4225
- options.scaleToWidth,
4226
- options.scaleToHeight,
4227
- baseBounds
4228
- );
4229
- baseBounds = this.boundsFromPoints(
4230
- this.flattenContours(baseScaledContours)
4231
- );
4232
- }
4233
- if (expand > 0) {
4234
- const expectedExpandedBounds = {
4235
- x: baseBounds.x - expand,
4236
- y: baseBounds.y - expand,
4237
- width: baseBounds.width + expand * 2,
4238
- height: baseBounds.height + expand * 2
4239
- };
4240
- if (expectedExpandedBounds.width > 0 && expectedExpandedBounds.height > 0 && globalBounds.width > 0 && globalBounds.height > 0) {
4241
- 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;
4242
- if (shouldNormalizeExpandBounds) {
4243
- const beforeNormalize = globalBounds;
4244
- finalContours = this.translateContours(
4245
- this.scaleContours(
4246
- finalContours,
4247
- expectedExpandedBounds.width,
4248
- expectedExpandedBounds.height,
4249
- globalBounds
4250
- ),
4251
- expectedExpandedBounds.x,
4252
- expectedExpandedBounds.y
4253
- );
4254
- globalBounds = this.boundsFromPoints(
4255
- this.flattenContours(finalContours)
4256
- );
4257
- debugLog("traceWithBounds:expand-normalized", {
4258
- expand,
4259
- expectedExpandedBounds,
4260
- beforeNormalize,
4261
- afterNormalize: globalBounds
4262
- });
3871
+ if (changed) {
3872
+ configService.update("dieline.features", newFeatures);
4263
3873
  }
4264
3874
  }
4265
- }
4266
- debugLog("traceWithBounds:contours", {
4267
- baseContourCount: baseContoursRaw.length,
4268
- baseSelectedCount: baseContours.length,
4269
- expandedContourCount: expandedContoursRaw.length,
4270
- expandedSelectedCount: expandedContours.length,
4271
- baseBounds,
4272
- expandedBounds: globalBounds,
4273
- expandedDeltaX: globalBounds.width - baseBounds.width,
4274
- expandedDeltaY: globalBounds.height - baseBounds.height,
4275
- expandedMayOverflowImageBounds: expand > 0,
4276
- useSmoothing,
4277
- componentMode
4278
- });
4279
- if (useSmoothing) {
4280
- return {
4281
- pathData: this.contoursToSVGPaper(finalContours, simplifyTolerance),
4282
- baseBounds,
4283
- bounds: globalBounds
4284
- };
4285
- } else {
4286
- const simplifiedContours = finalContours.map((points) => this.douglasPeucker(points, simplifyTolerance)).filter((points) => points.length > 2);
4287
- const pathData = this.contoursToSVG(simplifiedContours) || this.contoursToSVG(finalContours);
4288
- return {
4289
- pathData,
4290
- baseBounds,
4291
- bounds: globalBounds
4292
- };
4293
- }
4294
- }
4295
- static pickPrimaryContour(contours) {
4296
- if (contours.length === 0) return null;
4297
- return contours.reduce((best, cur) => {
4298
- if (!best) return cur;
4299
- const bestArea = Math.abs(polygonSignedArea(best));
4300
- const curArea = Math.abs(polygonSignedArea(cur));
4301
- if (curArea !== bestArea) return curArea > bestArea ? cur : best;
4302
- return cur.length > best.length ? cur : best;
4303
- }, contours[0]);
4304
- }
4305
- static flattenContours(contours) {
4306
- return contours.flatMap((contour) => contour);
4307
- }
4308
- static contourCentroid(points) {
4309
- if (!points.length) return { x: 0, y: 0 };
4310
- const sum = points.reduce(
4311
- (acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }),
4312
- { x: 0, y: 0 }
4313
- );
4314
- return {
4315
- x: sum.x / points.length,
4316
- y: sum.y / points.length
4317
- };
4318
- }
4319
- static pointInPolygon(point, polygon) {
4320
- let inside = false;
4321
- const { x, y } = point;
4322
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
4323
- const xi = polygon[i].x;
4324
- const yi = polygon[i].y;
4325
- const xj = polygon[j].x;
4326
- const yj = polygon[j].y;
4327
- const intersects = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi || Number.EPSILON) + xi;
4328
- if (intersects) inside = !inside;
4329
- }
4330
- return inside;
4331
- }
4332
- static keepOutermostContours(contours) {
4333
- if (contours.length <= 1) return contours;
4334
- const sorted = [...contours].sort(
4335
- (a, b) => Math.abs(polygonSignedArea(b)) - Math.abs(polygonSignedArea(a))
4336
- );
4337
- const selected = [];
4338
- for (const contour of sorted) {
4339
- const centroid = this.contourCentroid(contour);
4340
- const isNested = selected.some(
4341
- (outer) => this.pointInPolygon(centroid, outer)
4342
- );
4343
- if (!isNested) {
4344
- selected.push(contour);
4345
- }
4346
- }
4347
- return selected;
4348
- }
4349
- static summarizeAllContours(mask, width, height, minComponentArea) {
4350
- const raw = this.traceAllContours(mask, width, height);
4351
- const selected = this.selectContours(raw, "all", minComponentArea);
4352
- return {
4353
- rawCount: raw.length,
4354
- selectedCount: selected.length
4355
- };
4356
- }
4357
- static findForceConnectResult(sourceMask, width, height, minComponentArea, startDilateRadius, maxDilateRadius, erodeRatio) {
4358
- const initial = this.summarizeAllContours(
4359
- sourceMask,
4360
- width,
4361
- height,
4362
- minComponentArea
4363
- );
4364
- if (initial.selectedCount <= 1) {
4365
- return {
4366
- mask: sourceMask,
4367
- appliedDilateRadius: 0,
4368
- appliedErodeRadius: 0,
4369
- reachedSingleComponent: true,
4370
- rawContourCount: initial.rawCount,
4371
- selectedContourCount: initial.selectedCount
4372
- };
4373
- }
4374
- const normalizedStart = Math.max(1, Math.floor(startDilateRadius));
4375
- const normalizedMax = Math.max(
4376
- normalizedStart,
4377
- Math.floor(maxDilateRadius)
4378
- );
4379
- const normalizedErodeRatio = Math.max(0, erodeRatio);
4380
- const evaluate = (dilateRadius) => {
4381
- const erodeRadius = Math.max(
4382
- 1,
4383
- Math.floor(dilateRadius * normalizedErodeRatio)
4384
- );
4385
- let mask = sourceMask;
4386
- mask = circularMorphology(mask, width, height, dilateRadius, "dilate");
4387
- mask = fillHoles(mask, width, height);
4388
- mask = circularMorphology(mask, width, height, erodeRadius, "erode");
4389
- mask = fillHoles(mask, width, height);
4390
- const summary = this.summarizeAllContours(
4391
- mask,
4392
- width,
4393
- height,
4394
- minComponentArea
4395
- );
4396
- return {
4397
- dilateRadius,
4398
- erodeRadius,
4399
- mask,
4400
- rawCount: summary.rawCount,
4401
- selectedCount: summary.selectedCount
4402
- };
4403
- };
4404
- let low = normalizedStart - 1;
4405
- let high = normalizedStart;
4406
- let highResult = evaluate(high);
4407
- while (high < normalizedMax && highResult.selectedCount > 1) {
4408
- low = high;
4409
- high = Math.min(
4410
- normalizedMax,
4411
- Math.max(high + 1, Math.floor(high * 1.6))
4412
- );
4413
- highResult = evaluate(high);
4414
- }
4415
- if (highResult.selectedCount > 1) {
4416
- return {
4417
- mask: highResult.mask,
4418
- appliedDilateRadius: highResult.dilateRadius,
4419
- appliedErodeRadius: highResult.erodeRadius,
4420
- reachedSingleComponent: false,
4421
- rawContourCount: highResult.rawCount,
4422
- selectedContourCount: highResult.selectedCount
4423
- };
4424
- }
4425
- let best = highResult;
4426
- while (low + 1 < high) {
4427
- const mid = Math.floor((low + high) / 2);
4428
- const midResult = evaluate(mid);
4429
- if (midResult.selectedCount <= 1) {
4430
- best = midResult;
4431
- high = mid;
4432
- } else {
4433
- low = mid;
3875
+ },
3876
+ {
3877
+ command: "exportCutImage",
3878
+ id: "exportCutImage",
3879
+ title: "Export Cut Image",
3880
+ handler: (options) => {
3881
+ return tool.exportCutImage(options);
4434
3882
  }
4435
- }
4436
- return {
4437
- mask: best.mask,
4438
- appliedDilateRadius: best.dilateRadius,
4439
- appliedErodeRadius: best.erodeRadius,
4440
- reachedSingleComponent: true,
4441
- rawContourCount: best.rawCount,
4442
- selectedContourCount: best.selectedCount
4443
- };
4444
- }
4445
- static selectContours(contours, mode, minComponentArea) {
4446
- if (!contours.length) return [];
4447
- if (mode === "largest") {
4448
- const primary2 = this.pickPrimaryContour(contours);
4449
- return primary2 ? [primary2] : [];
4450
- }
4451
- const threshold = Math.max(0, minComponentArea);
4452
- if (threshold <= 0) {
4453
- return this.keepOutermostContours(contours);
4454
- }
4455
- const filtered = contours.filter(
4456
- (contour) => Math.abs(polygonSignedArea(contour)) >= threshold
4457
- );
4458
- if (filtered.length > 0) {
4459
- return this.keepOutermostContours(filtered);
4460
- }
4461
- const primary = this.pickPrimaryContour(contours);
4462
- return primary ? [primary] : [];
4463
- }
4464
- static boundsFromPoints(points) {
4465
- let minX = Infinity;
4466
- let minY = Infinity;
4467
- let maxX = -Infinity;
4468
- let maxY = -Infinity;
4469
- for (const p of points) {
4470
- if (p.x < minX) minX = p.x;
4471
- if (p.y < minY) minY = p.y;
4472
- if (p.x > maxX) maxX = p.x;
4473
- if (p.y > maxY) maxY = p.y;
4474
- }
4475
- if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
4476
- return { x: 0, y: 0, width: 0, height: 0 };
4477
- }
4478
- return {
4479
- x: minX,
4480
- y: minY,
4481
- width: maxX - minX,
4482
- height: maxY - minY
4483
- };
4484
- }
4485
- /**
4486
- * Traces all contours in the mask with optimized start-point detection
4487
- */
4488
- static traceAllContours(mask, width, height) {
4489
- const visited = new Uint8Array(width * height);
4490
- const allContours = [];
4491
- for (let y = 0; y < height; y++) {
4492
- for (let x = 0; x < width; x++) {
4493
- const idx = y * width + x;
4494
- if (mask[idx] && !visited[idx]) {
4495
- const isLeftEdge = x === 0 || mask[idx - 1] === 0;
4496
- if (isLeftEdge) {
4497
- const contour = this.marchingSquares(
4498
- mask,
4499
- visited,
4500
- x,
4501
- y,
4502
- width,
4503
- height
4504
- );
4505
- if (contour.length > 2) {
4506
- allContours.push(contour);
4507
- }
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
+ });
4508
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;
4509
3938
  }
4510
3939
  }
4511
3940
  }
4512
- return allContours;
4513
- }
4514
- static loadImage(url) {
4515
- return new Promise((resolve, reject) => {
4516
- const img = new Image();
4517
- img.crossOrigin = "Anonymous";
4518
- img.onload = () => resolve(img);
4519
- img.onerror = (e) => reject(e);
4520
- img.src = url;
4521
- });
4522
- }
4523
- /**
4524
- * Moore-Neighbor Tracing Algorithm
4525
- * More robust for irregular shapes than simple Marching Squares walker.
4526
- */
4527
- static marchingSquares(mask, visited, startX, startY, width, height) {
4528
- const isSolid = (x, y) => {
4529
- if (x < 0 || x >= width || y < 0 || y >= height) return false;
4530
- return mask[y * width + x] === 1;
4531
- };
4532
- const points = [];
4533
- let cx = startX;
4534
- let cy = startY;
4535
- const neighbors = [
4536
- { x: 0, y: -1 },
4537
- { x: 1, y: -1 },
4538
- { x: 1, y: 0 },
4539
- { x: 1, y: 1 },
4540
- { x: 0, y: 1 },
4541
- { x: -1, y: 1 },
4542
- { x: -1, y: 0 },
4543
- { x: -1, y: -1 }
4544
- ];
4545
- let backtrack = 6;
4546
- const maxSteps = width * height * 3;
4547
- let steps = 0;
4548
- do {
4549
- points.push({ x: cx, y: cy });
4550
- visited[cy * width + cx] = 1;
4551
- let found = false;
4552
- for (let i = 0; i < 8; i++) {
4553
- const idx = (backtrack + 1 + i) % 8;
4554
- const nx = cx + neighbors[idx].x;
4555
- const ny = cy + neighbors[idx].y;
4556
- if (isSolid(nx, ny)) {
4557
- cx = nx;
4558
- cy = ny;
4559
- backtrack = (idx + 4 + 1) % 8;
4560
- found = true;
4561
- break;
4562
- }
4563
- }
4564
- if (!found) break;
4565
- steps++;
4566
- } while ((cx !== startX || cy !== startY) && steps < maxSteps);
4567
- return points;
4568
- }
4569
- /**
4570
- * Douglas-Peucker Line Simplification
4571
- */
4572
- static douglasPeucker(points, tolerance) {
4573
- if (points.length <= 2) return points;
4574
- const sqTolerance = tolerance * tolerance;
4575
- let maxSqDist = 0;
4576
- let index = 0;
4577
- const first = points[0];
4578
- const last = points[points.length - 1];
4579
- for (let i = 1; i < points.length - 1; i++) {
4580
- const sqDist = this.getSqSegDist(points[i], first, last);
4581
- if (sqDist > maxSqDist) {
4582
- index = i;
4583
- maxSqDist = sqDist;
4584
- }
4585
- }
4586
- if (maxSqDist > sqTolerance) {
4587
- const left = this.douglasPeucker(points.slice(0, index + 1), tolerance);
4588
- const right = this.douglasPeucker(points.slice(index), tolerance);
4589
- return left.slice(0, left.length - 1).concat(right);
4590
- } else {
4591
- return [first, last];
4592
- }
4593
- }
4594
- static getSqSegDist(p, p1, p2) {
4595
- let x = p1.x;
4596
- let y = p1.y;
4597
- let dx = p2.x - x;
4598
- let dy = p2.y - y;
4599
- if (dx !== 0 || dy !== 0) {
4600
- const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
4601
- if (t > 1) {
4602
- x = p2.x;
4603
- y = p2.y;
4604
- } else if (t > 0) {
4605
- x += dx * t;
4606
- y += dy * t;
4607
- }
4608
- }
4609
- dx = p.x - x;
4610
- dy = p.y - y;
4611
- return dx * dx + dy * dy;
4612
- }
4613
- static scalePoints(points, targetWidth, targetHeight, bounds) {
4614
- if (points.length === 0) return points;
4615
- if (bounds.width === 0 || bounds.height === 0) return points;
4616
- const scaleX = targetWidth / bounds.width;
4617
- const scaleY = targetHeight / bounds.height;
4618
- return points.map((p) => ({
4619
- x: (p.x - bounds.x) * scaleX,
4620
- y: (p.y - bounds.y) * scaleY
4621
- }));
4622
- }
4623
- static scaleContours(contours, targetWidth, targetHeight, bounds) {
4624
- return contours.map(
4625
- (points) => this.scalePoints(points, targetWidth, targetHeight, bounds)
4626
- );
4627
- }
4628
- static translateContours(contours, offsetX, offsetY) {
4629
- return contours.map(
4630
- (points) => points.map((p) => ({
4631
- x: p.x + offsetX,
4632
- y: p.y + offsetY
4633
- }))
4634
- );
4635
- }
4636
- static pointsToSVG(points) {
4637
- if (points.length === 0) return "";
4638
- const head = points[0];
4639
- const tail = points.slice(1);
4640
- return `M ${head.x} ${head.y} ` + tail.map((p) => `L ${p.x} ${p.y}`).join(" ") + " Z";
4641
- }
4642
- static contoursToSVG(contours) {
4643
- return contours.filter((points) => points.length > 2).map((points) => this.pointsToSVG(points)).join(" ").trim();
4644
- }
4645
- static ensurePaper() {
4646
- if (!paper2.project) {
4647
- paper2.setup(new paper2.Size(100, 100));
4648
- }
4649
- }
4650
- static pointsToSVGPaper(points, tolerance) {
4651
- if (points.length < 3) return this.pointsToSVG(points);
4652
- this.ensurePaper();
4653
- const path = new paper2.Path({
4654
- segments: points.map((p) => [p.x, p.y]),
4655
- closed: true
4656
- });
4657
- path.simplify(tolerance);
4658
- const data = path.pathData;
4659
- path.remove();
4660
- return data;
4661
- }
4662
- static contoursToSVGPaper(contours, tolerance) {
4663
- const normalizedContours = contours.filter((points) => points.length > 2);
4664
- if (!normalizedContours.length) return "";
4665
- if (normalizedContours.length === 1) {
4666
- return this.pointsToSVGPaper(normalizedContours[0], tolerance);
4667
- }
4668
- this.ensurePaper();
4669
- const compound = new paper2.CompoundPath({ insert: false });
4670
- for (const points of normalizedContours) {
4671
- const child = new paper2.Path({
4672
- segments: points.map((p) => [p.x, p.y]),
4673
- closed: true,
4674
- insert: false
4675
- });
4676
- child.simplify(tolerance);
4677
- 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
4678
4045
  }
4679
- const data = compound.pathData || this.contoursToSVG(normalizedContours);
4680
- compound.remove();
4681
- return data;
4682
- }
4683
- };
4046
+ ];
4047
+ }
4684
4048
 
4685
- // src/extensions/dieline.ts
4686
- var IMAGE_OBJECT_LAYER_ID2 = "image.user";
4687
- var DIELINE_LAYER_ID = "dieline-overlay";
4049
+ // src/extensions/dieline/DielineTool.ts
4688
4050
  var DielineTool = class {
4689
4051
  constructor(options) {
4690
4052
  this.id = "pooder.kit.dieline";
@@ -4925,208 +4287,20 @@ var DielineTool = class {
4925
4287
  this.context = void 0;
4926
4288
  }
4927
4289
  contribute() {
4928
- const s = this.state;
4929
4290
  return {
4930
4291
  [ContributionPointIds4.TOOLS]: [
4931
4292
  {
4932
4293
  id: this.id,
4933
4294
  name: "Dieline",
4934
- interaction: "session",
4935
- session: {
4936
- autoBegin: false,
4937
- leavePolicy: "block"
4938
- }
4939
- }
4940
- ],
4941
- [ContributionPointIds4.CONFIGURATIONS]: [
4942
- {
4943
- id: "dieline.shape",
4944
- type: "select",
4945
- label: "Shape",
4946
- options: Array.from(DIELINE_SHAPES),
4947
- default: s.shape
4948
- },
4949
- {
4950
- id: "dieline.radius",
4951
- type: "number",
4952
- label: "Corner Radius (mm)",
4953
- min: 0,
4954
- max: 500,
4955
- default: s.radius
4956
- },
4957
- {
4958
- id: "dieline.shapeStyle",
4959
- type: "json",
4960
- label: "Shape Style",
4961
- default: s.shapeStyle
4962
- },
4963
- {
4964
- id: "dieline.showBleedLines",
4965
- type: "boolean",
4966
- label: "Show Bleed Lines",
4967
- default: s.showBleedLines
4968
- },
4969
- {
4970
- id: "dieline.strokeWidth",
4971
- type: "number",
4972
- label: "Line Width",
4973
- min: 0.1,
4974
- max: 10,
4975
- step: 0.1,
4976
- default: s.mainLine.width
4977
- },
4978
- {
4979
- id: "dieline.strokeColor",
4980
- type: "color",
4981
- label: "Line Color",
4982
- default: s.mainLine.color
4983
- },
4984
- {
4985
- id: "dieline.dashLength",
4986
- type: "number",
4987
- label: "Dash Length",
4988
- min: 1,
4989
- max: 50,
4990
- default: s.mainLine.dashLength
4991
- },
4992
- {
4993
- id: "dieline.style",
4994
- type: "select",
4995
- label: "Line Style",
4996
- options: ["solid", "dashed", "hidden"],
4997
- default: s.mainLine.style
4998
- },
4999
- {
5000
- id: "dieline.offsetStrokeWidth",
5001
- type: "number",
5002
- label: "Offset Line Width",
5003
- min: 0.1,
5004
- max: 10,
5005
- step: 0.1,
5006
- default: s.offsetLine.width
5007
- },
5008
- {
5009
- id: "dieline.offsetStrokeColor",
5010
- type: "color",
5011
- label: "Offset Line Color",
5012
- default: s.offsetLine.color
5013
- },
5014
- {
5015
- id: "dieline.offsetDashLength",
5016
- type: "number",
5017
- label: "Offset Dash Length",
5018
- min: 1,
5019
- max: 50,
5020
- default: s.offsetLine.dashLength
5021
- },
5022
- {
5023
- id: "dieline.offsetStyle",
5024
- type: "select",
5025
- label: "Offset Line Style",
5026
- options: ["solid", "dashed", "hidden"],
5027
- default: s.offsetLine.style
5028
- },
5029
- {
5030
- id: "dieline.insideColor",
5031
- type: "color",
5032
- label: "Inside Color",
5033
- default: s.insideColor
5034
- },
5035
- {
5036
- id: "dieline.features",
5037
- type: "json",
5038
- label: "Edge Features",
5039
- default: s.features
5040
- }
5041
- ],
5042
- [ContributionPointIds4.COMMANDS]: [
5043
- {
5044
- command: "updateFeaturePosition",
5045
- title: "Update Feature Position",
5046
- handler: (groupId, x, y) => {
5047
- var _a;
5048
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
5049
- "ConfigurationService"
5050
- );
5051
- if (!configService) return;
5052
- const features = configService.get("dieline.features") || [];
5053
- let changed = false;
5054
- const newFeatures = features.map((f) => {
5055
- if (f.groupId === groupId) {
5056
- if (f.x !== x || f.y !== y) {
5057
- changed = true;
5058
- return { ...f, x, y };
5059
- }
5060
- }
5061
- return f;
5062
- });
5063
- if (changed) {
5064
- configService.update("dieline.features", newFeatures);
5065
- }
5066
- }
5067
- },
5068
- {
5069
- command: "exportCutImage",
5070
- title: "Export Cut Image",
5071
- handler: (options) => {
5072
- return this.exportCutImage(options);
5073
- }
5074
- },
5075
- {
5076
- command: "detectEdge",
5077
- title: "Detect Edge from Image",
5078
- handler: async (imageUrl, options) => {
5079
- var _a, _b, _c;
5080
- try {
5081
- const detectOptions = options || {};
5082
- const debug = detectOptions.debug === true;
5083
- const tracerOptions = {
5084
- expand: (_a = detectOptions.expand) != null ? _a : 0,
5085
- smoothing: (_b = detectOptions.smoothing) != null ? _b : true,
5086
- simplifyTolerance: (_c = detectOptions.simplifyTolerance) != null ? _c : 2,
5087
- threshold: detectOptions.threshold,
5088
- debug
5089
- };
5090
- const loadImage = (url) => {
5091
- return new Promise((resolve, reject) => {
5092
- const img2 = new Image();
5093
- img2.crossOrigin = "Anonymous";
5094
- img2.onload = () => resolve(img2);
5095
- img2.onerror = (e) => reject(e);
5096
- img2.src = url;
5097
- });
5098
- };
5099
- const [img, traced] = await Promise.all([
5100
- loadImage(imageUrl),
5101
- ImageTracer.traceWithBounds(imageUrl, tracerOptions)
5102
- ]);
5103
- const { pathData, baseBounds, bounds } = traced;
5104
- if (debug) {
5105
- console.info("[DielineTool] detectEdge", {
5106
- imageWidth: img.width,
5107
- imageHeight: img.height,
5108
- baseBounds,
5109
- expandedBounds: bounds,
5110
- currentDielineWidth: s.width,
5111
- currentDielineHeight: s.height,
5112
- options: tracerOptions,
5113
- strategy: "single-connected-silhouette"
5114
- });
5115
- }
5116
- return {
5117
- pathData,
5118
- rawBounds: bounds,
5119
- baseBounds,
5120
- imageWidth: img.width,
5121
- imageHeight: img.height
5122
- };
5123
- } catch (e) {
5124
- console.error("Edge detection failed", e);
5125
- throw e;
5126
- }
4295
+ interaction: "session",
4296
+ session: {
4297
+ autoBegin: false,
4298
+ leavePolicy: "block"
5127
4299
  }
5128
4300
  }
5129
- ]
4301
+ ],
4302
+ [ContributionPointIds4.CONFIGURATIONS]: createDielineConfigurations(this.state),
4303
+ [ContributionPointIds4.COMMANDS]: createDielineCommands(this, this.state)
5130
4304
  };
5131
4305
  }
5132
4306
  createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
@@ -5404,7 +4578,7 @@ var DielineTool = class {
5404
4578
  op: "not",
5405
4579
  expr: { op: "anySessionActive" }
5406
4580
  },
5407
- targetPassIds: [IMAGE_OBJECT_LAYER_ID2],
4581
+ targetPassIds: [IMAGE_OBJECT_LAYER_ID],
5408
4582
  source: {
5409
4583
  id: "dieline.effect.clip-path",
5410
4584
  type: "path",
@@ -5569,7 +4743,7 @@ var DielineTool = class {
5569
4743
  const exportBounds = pathBounds;
5570
4744
  const sourceImages = this.canvasService.canvas.getObjects().filter((obj) => {
5571
4745
  var _a2;
5572
- 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;
5573
4747
  });
5574
4748
  if (!sourceImages.length) {
5575
4749
  console.warn(
@@ -5641,7 +4815,7 @@ var DielineTool = class {
5641
4815
  }
5642
4816
  };
5643
4817
 
5644
- // src/extensions/feature.ts
4818
+ // src/extensions/feature/FeatureTool.ts
5645
4819
  import {
5646
4820
  ContributionPointIds as ContributionPointIds5
5647
4821
  } from "@pooder/core";
@@ -5843,8 +5017,7 @@ function completeFeaturesStrict(features, context, update) {
5843
5017
  return { ok: true };
5844
5018
  }
5845
5019
 
5846
- // src/extensions/feature.ts
5847
- var FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
5020
+ // src/extensions/feature/FeatureTool.ts
5848
5021
  var FEATURE_STROKE_WIDTH = 2;
5849
5022
  var DEFAULT_RECT_SIZE = 10;
5850
5023
  var DEFAULT_CIRCLE_RADIUS = 5;
@@ -5862,6 +5035,7 @@ var FeatureTool = class {
5862
5035
  this.hasWorkingChanges = false;
5863
5036
  this.specs = [];
5864
5037
  this.renderSeq = 0;
5038
+ this.subscriptions = new SubscriptionBag();
5865
5039
  this.handleMoving = null;
5866
5040
  this.handleModified = null;
5867
5041
  this.handleSceneGeometryChange = null;
@@ -5879,6 +5053,7 @@ var FeatureTool = class {
5879
5053
  }
5880
5054
  activate(context) {
5881
5055
  var _a;
5056
+ this.subscriptions.disposeAll();
5882
5057
  this.context = context;
5883
5058
  this.canvasService = context.services.get("CanvasService");
5884
5059
  if (!this.canvasService) {
@@ -5907,29 +5082,32 @@ var FeatureTool = class {
5907
5082
  const features = configService.get("dieline.features", []) || [];
5908
5083
  this.workingFeatures = this.cloneFeatures(features);
5909
5084
  this.hasWorkingChanges = false;
5910
- configService.onAnyChange((e) => {
5911
- if (this.isUpdatingConfig) return;
5912
- if (e.key === "dieline.features") {
5913
- if (this.isFeatureSessionActive) return;
5914
- const next = e.value || [];
5915
- this.workingFeatures = this.cloneFeatures(next);
5916
- this.hasWorkingChanges = false;
5917
- this.redraw();
5918
- 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
+ }
5919
5097
  }
5920
- });
5098
+ );
5921
5099
  }
5922
5100
  const toolSessionService = context.services.get("ToolSessionService");
5923
5101
  this.dirtyTrackerDisposable = toolSessionService == null ? void 0 : toolSessionService.registerDirtyTracker(
5924
5102
  this.id,
5925
5103
  () => this.hasWorkingChanges
5926
5104
  );
5927
- context.eventBus.on("tool:activated", this.onToolActivated);
5105
+ this.subscriptions.on(context.eventBus, "tool:activated", this.onToolActivated);
5928
5106
  this.setup();
5929
5107
  }
5930
5108
  deactivate(context) {
5931
5109
  var _a;
5932
- context.eventBus.off("tool:activated", this.onToolActivated);
5110
+ this.subscriptions.disposeAll();
5933
5111
  this.restoreSessionFeaturesToConfig();
5934
5112
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
5935
5113
  this.dirtyTrackerDisposable = void 0;
@@ -6072,7 +5250,7 @@ var FeatureTool = class {
6072
5250
  };
6073
5251
  }
6074
5252
  cloneFeatures(features) {
6075
- return JSON.parse(JSON.stringify(features || []));
5253
+ return cloneWithJson(features || []);
6076
5254
  }
6077
5255
  getConfigService() {
6078
5256
  var _a;
@@ -6756,12 +5934,11 @@ var FeatureTool = class {
6756
5934
  }
6757
5935
  };
6758
5936
 
6759
- // src/extensions/film.ts
5937
+ // src/extensions/film/FilmTool.ts
6760
5938
  import {
6761
5939
  ContributionPointIds as ContributionPointIds6
6762
5940
  } from "@pooder/core";
6763
5941
  import { FabricImage as FabricImage3 } from "fabric";
6764
- var FILM_LAYER_ID = "overlay";
6765
5942
  var FILM_IMAGE_ID = "film-image";
6766
5943
  var DEFAULT_WIDTH2 = 800;
6767
5944
  var DEFAULT_HEIGHT2 = 600;
@@ -6776,8 +5953,10 @@ var FilmTool = class {
6776
5953
  this.specs = [];
6777
5954
  this.renderSeq = 0;
6778
5955
  this.renderImageUrl = "";
6779
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
6780
- this.pendingSizeBySrc = /* @__PURE__ */ new Map();
5956
+ this.sourceSizeCache = createSourceSizeCache(
5957
+ (src) => this.loadImageSize(src)
5958
+ );
5959
+ this.subscriptions = new SubscriptionBag();
6781
5960
  this.onCanvasResized = () => {
6782
5961
  this.updateFilm();
6783
5962
  };
@@ -6787,6 +5966,7 @@ var FilmTool = class {
6787
5966
  }
6788
5967
  activate(context) {
6789
5968
  var _a;
5969
+ this.subscriptions.disposeAll();
6790
5970
  this.canvasService = context.services.get("CanvasService");
6791
5971
  if (!this.canvasService) {
6792
5972
  console.warn("CanvasService not found for FilmTool");
@@ -6813,28 +5993,32 @@ var FilmTool = class {
6813
5993
  if (configService) {
6814
5994
  this.url = configService.get("film.url", this.url);
6815
5995
  this.opacity = configService.get("film.opacity", this.opacity);
6816
- configService.onAnyChange((e) => {
6817
- if (e.key.startsWith("film.")) {
6818
- const prop = e.key.split(".")[1];
6819
- console.log(
6820
- `[FilmTool] Config change detected: ${e.key} -> ${e.value}`
6821
- );
6822
- if (prop && prop in this) {
6823
- this[prop] = e.value;
6824
- 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
+ }
6825
6008
  }
6826
6009
  }
6827
- });
6010
+ );
6828
6011
  }
6829
- context.eventBus.on("canvas:resized", this.onCanvasResized);
6012
+ this.subscriptions.on(context.eventBus, "canvas:resized", this.onCanvasResized);
6830
6013
  this.updateFilm();
6831
6014
  }
6832
6015
  deactivate(context) {
6833
6016
  var _a;
6834
- context.eventBus.off("canvas:resized", this.onCanvasResized);
6017
+ this.subscriptions.disposeAll();
6835
6018
  this.renderSeq += 1;
6836
6019
  this.specs = [];
6837
6020
  this.renderImageUrl = "";
6021
+ this.sourceSizeCache.clear();
6838
6022
  (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6839
6023
  this.renderProducerDisposable = void 0;
6840
6024
  if (!this.canvasService) return;
@@ -6893,10 +6077,13 @@ var FilmTool = class {
6893
6077
  return [];
6894
6078
  }
6895
6079
  const { width, height } = this.getViewportSize();
6896
- const sourceSize = this.sourceSizeBySrc.get(imageUrl);
6080
+ const sourceSize = this.sourceSizeCache.getSourceSize(imageUrl);
6897
6081
  const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
6898
6082
  const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
6899
- const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
6083
+ const coverScale = getCoverScale(
6084
+ { width, height },
6085
+ { width: sourceWidth, height: sourceHeight }
6086
+ );
6900
6087
  return [
6901
6088
  {
6902
6089
  id: FILM_IMAGE_ID,
@@ -6923,24 +6110,6 @@ var FilmTool = class {
6923
6110
  }
6924
6111
  ];
6925
6112
  }
6926
- async ensureImageSize(src) {
6927
- if (!src) return null;
6928
- const cached = this.sourceSizeBySrc.get(src);
6929
- if (cached) return cached;
6930
- const pending = this.pendingSizeBySrc.get(src);
6931
- if (pending) {
6932
- return pending;
6933
- }
6934
- const task = this.loadImageSize(src);
6935
- this.pendingSizeBySrc.set(src, task);
6936
- try {
6937
- return await task;
6938
- } finally {
6939
- if (this.pendingSizeBySrc.get(src) === task) {
6940
- this.pendingSizeBySrc.delete(src);
6941
- }
6942
- }
6943
- }
6944
6113
  async loadImageSize(src) {
6945
6114
  try {
6946
6115
  const image = await FabricImage3.fromURL(src, {
@@ -6949,9 +6118,7 @@ var FilmTool = class {
6949
6118
  const width = Number((image == null ? void 0 : image.width) || 0);
6950
6119
  const height = Number((image == null ? void 0 : image.height) || 0);
6951
6120
  if (width > 0 && height > 0) {
6952
- const size = { width, height };
6953
- this.sourceSizeBySrc.set(src, size);
6954
- return size;
6121
+ return { width, height };
6955
6122
  }
6956
6123
  } catch (error) {
6957
6124
  console.error("[FilmTool] Failed to load film image", src, error);
@@ -6968,7 +6135,7 @@ var FilmTool = class {
6968
6135
  if (!nextUrl) {
6969
6136
  this.renderImageUrl = "";
6970
6137
  } else if (nextUrl !== this.renderImageUrl) {
6971
- const loaded = await this.ensureImageSize(nextUrl);
6138
+ const loaded = await this.sourceSizeCache.ensureImageSize(nextUrl);
6972
6139
  if (seq !== this.renderSeq) return;
6973
6140
  if (loaded) {
6974
6141
  this.renderImageUrl = nextUrl;
@@ -6981,7 +6148,7 @@ var FilmTool = class {
6981
6148
  }
6982
6149
  };
6983
6150
 
6984
- // src/extensions/mirror.ts
6151
+ // src/extensions/mirror/MirrorTool.ts
6985
6152
  import {
6986
6153
  ContributionPointIds as ContributionPointIds7
6987
6154
  } from "@pooder/core";
@@ -7073,11 +6240,10 @@ var MirrorTool = class {
7073
6240
  }
7074
6241
  };
7075
6242
 
7076
- // src/extensions/ruler.ts
6243
+ // src/extensions/ruler/RulerTool.ts
7077
6244
  import {
7078
6245
  ContributionPointIds as ContributionPointIds8
7079
6246
  } from "@pooder/core";
7080
- var RULER_LAYER_ID = "ruler-overlay";
7081
6247
  var EXTENSION_LINE_LENGTH = 5;
7082
6248
  var MIN_ARROW_SIZE = 4;
7083
6249
  var THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
@@ -7639,17 +6805,197 @@ var RulerTool = class {
7639
6805
  }
7640
6806
  };
7641
6807
 
7642
- // src/extensions/white-ink.ts
6808
+ // src/extensions/white-ink/WhiteInkTool.ts
7643
6809
  import {
7644
6810
  ContributionPointIds as ContributionPointIds9
7645
6811
  } from "@pooder/core";
7646
- var WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
7647
- var WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
7648
- var WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
7649
- var IMAGE_OBJECT_LAYER_ID3 = "image.user";
7650
- var WHITE_INK_DEBUG_KEY = "whiteInk.debug";
6812
+
6813
+ // src/extensions/white-ink/commands.ts
7651
6814
  var WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
7652
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;
7653
6999
  var WHITE_INK_AUTO_ITEM_ID = "white-ink-auto";
7654
7000
  var WHITE_INK_COVER_OPACITY_FACTOR = 0.45;
7655
7001
  var WHITE_INK_COVER_OPACITY_MIN = 0.15;
@@ -7665,7 +7011,9 @@ var WhiteInkTool = class {
7665
7011
  this.items = [];
7666
7012
  this.workingItems = [];
7667
7013
  this.hasWorkingChanges = false;
7668
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
7014
+ this.sourceSizeCache = createSourceSizeCache(
7015
+ (src) => this.loadImageSize(src)
7016
+ );
7669
7017
  this.previewMaskBySource = /* @__PURE__ */ new Map();
7670
7018
  this.pendingPreviewMaskBySource = /* @__PURE__ */ new Map();
7671
7019
  this.isUpdatingConfig = false;
@@ -7676,6 +7024,7 @@ var WhiteInkTool = class {
7676
7024
  this.whiteSpecs = [];
7677
7025
  this.coverSpecs = [];
7678
7026
  this.overlaySpecs = [];
7027
+ this.subscriptions = new SubscriptionBag();
7679
7028
  this.onToolActivated = (event) => {
7680
7029
  const before = this.isToolActive;
7681
7030
  this.syncToolActiveFromWorkbench(event.id);
@@ -7693,19 +7042,19 @@ var WhiteInkTool = class {
7693
7042
  this.onObjectAdded = (e) => {
7694
7043
  var _a, _b;
7695
7044
  const layerId = (_b = (_a = e == null ? void 0 : e.target) == null ? void 0 : _a.data) == null ? void 0 : _b.layerId;
7696
- if (layerId !== IMAGE_OBJECT_LAYER_ID3) return;
7045
+ if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
7697
7046
  this.updateWhiteInks();
7698
7047
  };
7699
7048
  this.onObjectModified = (e) => {
7700
7049
  var _a, _b;
7701
7050
  const layerId = (_b = (_a = e == null ? void 0 : e.target) == null ? void 0 : _a.data) == null ? void 0 : _b.layerId;
7702
- if (layerId !== IMAGE_OBJECT_LAYER_ID3) return;
7051
+ if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
7703
7052
  this.updateWhiteInks();
7704
7053
  };
7705
7054
  this.onObjectRemoved = (e) => {
7706
7055
  var _a, _b;
7707
7056
  const layerId = (_b = (_a = e == null ? void 0 : e.target) == null ? void 0 : _a.data) == null ? void 0 : _b.layerId;
7708
- if (layerId !== IMAGE_OBJECT_LAYER_ID3) return;
7057
+ if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
7709
7058
  this.updateWhiteInks();
7710
7059
  };
7711
7060
  this.onImageWorkingChanged = () => {
@@ -7714,6 +7063,7 @@ var WhiteInkTool = class {
7714
7063
  }
7715
7064
  activate(context) {
7716
7065
  var _a;
7066
+ this.subscriptions.disposeAll();
7717
7067
  this.context = context;
7718
7068
  this.canvasService = context.services.get("CanvasService");
7719
7069
  if (!this.canvasService) {
@@ -7747,62 +7097,73 @@ var WhiteInkTool = class {
7747
7097
  }),
7748
7098
  { priority: 260 }
7749
7099
  );
7750
- context.eventBus.on("tool:activated", this.onToolActivated);
7751
- context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
7752
- context.eventBus.on("object:added", this.onObjectAdded);
7753
- context.eventBus.on("object:modified", this.onObjectModified);
7754
- context.eventBus.on("object:removed", this.onObjectRemoved);
7755
- 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
+ );
7756
7122
  const configService = context.services.get(
7757
7123
  "ConfigurationService"
7758
7124
  );
7759
7125
  if (configService) {
7760
- this.items = this.normalizeItems(
7761
- configService.get("whiteInk.items", []) || []
7762
- );
7763
- this.workingItems = this.cloneItems(this.items);
7764
- this.hasWorkingChanges = false;
7126
+ this.applyCommittedItems(configService.get("whiteInk.items", []) || []);
7765
7127
  this.printWithWhiteInk = !!configService.get(
7766
7128
  "whiteInk.printWithWhiteInk",
7767
7129
  true
7768
7130
  );
7769
7131
  this.previewImageVisible = !!configService.get(
7770
- WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
7132
+ WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY2,
7771
7133
  true
7772
7134
  );
7773
7135
  this.migrateLegacyConfigIfNeeded(configService);
7774
- configService.onAnyChange((e) => {
7775
- if (this.isUpdatingConfig) return;
7776
- if (e.key === "whiteInk.items") {
7777
- this.items = this.normalizeItems(e.value || []);
7778
- if (!this.isToolActive || !this.hasWorkingChanges) {
7779
- this.workingItems = this.cloneItems(this.items);
7780
- 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();
7781
7164
  }
7782
- this.updateWhiteInks();
7783
- return;
7784
- }
7785
- if (e.key === "whiteInk.printWithWhiteInk") {
7786
- this.printWithWhiteInk = !!e.value;
7787
- this.updateWhiteInks();
7788
- return;
7789
- }
7790
- if (e.key === WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY) {
7791
- this.previewImageVisible = !!e.value;
7792
- this.updateWhiteInks();
7793
- return;
7794
- }
7795
- if (e.key === "image.items") {
7796
- this.updateWhiteInks();
7797
- return;
7798
- }
7799
- if (e.key === WHITE_INK_DEBUG_KEY) {
7800
- return;
7801
- }
7802
- if (e.key.startsWith("size.")) {
7803
- this.updateWhiteInks();
7804
7165
  }
7805
- });
7166
+ );
7806
7167
  }
7807
7168
  const toolSessionService = context.services.get("ToolSessionService");
7808
7169
  this.dirtyTrackerDisposable = toolSessionService == null ? void 0 : toolSessionService.registerDirtyTracker(
@@ -7813,14 +7174,10 @@ var WhiteInkTool = class {
7813
7174
  }
7814
7175
  deactivate(context) {
7815
7176
  var _a, _b;
7816
- context.eventBus.off("tool:activated", this.onToolActivated);
7817
- context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
7818
- context.eventBus.off("object:added", this.onObjectAdded);
7819
- context.eventBus.off("object:modified", this.onObjectModified);
7820
- context.eventBus.off("object:removed", this.onObjectRemoved);
7821
- context.eventBus.off("image:working:change", this.onImageWorkingChanged);
7177
+ this.subscriptions.disposeAll();
7822
7178
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
7823
7179
  this.dirtyTrackerDisposable = void 0;
7180
+ this.sourceSizeCache.clear();
7824
7181
  this.clearRenderedWhiteInks();
7825
7182
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7826
7183
  this.renderProducerDisposable = void 0;
@@ -7848,171 +7205,8 @@ var WhiteInkTool = class {
7848
7205
  }
7849
7206
  }
7850
7207
  ],
7851
- [ContributionPointIds9.CONFIGURATIONS]: [
7852
- {
7853
- id: "whiteInk.items",
7854
- type: "array",
7855
- label: "White Ink Images",
7856
- default: []
7857
- },
7858
- {
7859
- id: "whiteInk.printWithWhiteInk",
7860
- type: "boolean",
7861
- label: "Preview White Ink",
7862
- default: true
7863
- },
7864
- {
7865
- id: WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
7866
- type: "boolean",
7867
- label: "Show Cover During White Ink Preview",
7868
- default: true
7869
- },
7870
- {
7871
- id: WHITE_INK_DEBUG_KEY,
7872
- type: "boolean",
7873
- label: "White Ink Debug Log",
7874
- default: false
7875
- }
7876
- ],
7877
- [ContributionPointIds9.COMMANDS]: [
7878
- {
7879
- command: "addWhiteInk",
7880
- title: "Add White Ink",
7881
- handler: async (url, options) => {
7882
- return await this.addWhiteInkEntry(url, options);
7883
- }
7884
- },
7885
- {
7886
- command: "upsertWhiteInk",
7887
- title: "Upsert White Ink",
7888
- handler: async (url, options = {}) => {
7889
- return await this.upsertWhiteInkEntry(url, options);
7890
- }
7891
- },
7892
- {
7893
- command: "getWhiteInks",
7894
- title: "Get White Inks",
7895
- handler: () => this.cloneItems(this.items)
7896
- },
7897
- {
7898
- command: "getWhiteInkSettings",
7899
- title: "Get White Ink Settings",
7900
- handler: () => {
7901
- const first = this.getEffectiveWhiteInkItem(this.items);
7902
- const primarySource = this.getPrimaryImageSource();
7903
- const sourceUrl = this.resolveSourceUrl(first) || primarySource;
7904
- return {
7905
- id: (first == null ? void 0 : first.id) || null,
7906
- url: sourceUrl,
7907
- sourceUrl,
7908
- opacity: WHITE_INK_DEFAULT_OPACITY,
7909
- printWithWhiteInk: this.printWithWhiteInk,
7910
- previewImageVisible: this.previewImageVisible
7911
- };
7912
- }
7913
- },
7914
- {
7915
- command: "setWhiteInkPrintEnabled",
7916
- title: "Set White Ink Preview Enabled",
7917
- handler: (enabled) => {
7918
- var _a;
7919
- this.printWithWhiteInk = !!enabled;
7920
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
7921
- "ConfigurationService"
7922
- );
7923
- configService == null ? void 0 : configService.update(
7924
- "whiteInk.printWithWhiteInk",
7925
- this.printWithWhiteInk
7926
- );
7927
- this.updateWhiteInks();
7928
- return { ok: true };
7929
- }
7930
- },
7931
- {
7932
- command: "setWhiteInkPreviewImageVisible",
7933
- title: "Set White Ink Cover Visible",
7934
- handler: (visible) => {
7935
- var _a;
7936
- this.previewImageVisible = !!visible;
7937
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
7938
- "ConfigurationService"
7939
- );
7940
- configService == null ? void 0 : configService.update(
7941
- WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
7942
- this.previewImageVisible
7943
- );
7944
- this.updateWhiteInks();
7945
- return { ok: true };
7946
- }
7947
- },
7948
- {
7949
- command: "getWorkingWhiteInks",
7950
- title: "Get Working White Inks",
7951
- handler: () => this.cloneItems(this.workingItems)
7952
- },
7953
- {
7954
- command: "setWorkingWhiteInk",
7955
- title: "Set Working White Ink",
7956
- handler: (id, updates) => {
7957
- this.updateWhiteInkInWorking(id, updates);
7958
- }
7959
- },
7960
- {
7961
- command: "updateWhiteInk",
7962
- title: "Update White Ink",
7963
- handler: async (id, updates, options = {}) => {
7964
- await this.updateWhiteInkItem(id, updates, options);
7965
- }
7966
- },
7967
- {
7968
- command: "removeWhiteInk",
7969
- title: "Remove White Ink",
7970
- handler: (id) => {
7971
- this.removeWhiteInk(id);
7972
- }
7973
- },
7974
- {
7975
- command: "clearWhiteInks",
7976
- title: "Clear White Inks",
7977
- handler: () => {
7978
- this.clearWhiteInks();
7979
- }
7980
- },
7981
- {
7982
- command: "resetWorkingWhiteInks",
7983
- title: "Reset Working White Inks",
7984
- handler: () => {
7985
- this.workingItems = this.cloneItems(this.items);
7986
- this.hasWorkingChanges = false;
7987
- this.updateWhiteInks();
7988
- }
7989
- },
7990
- {
7991
- command: "completeWhiteInks",
7992
- title: "Complete White Inks",
7993
- handler: async () => {
7994
- return await this.completeWhiteInks();
7995
- }
7996
- },
7997
- {
7998
- command: "setWhiteInkImage",
7999
- title: "Set White Ink Image",
8000
- handler: async (url) => {
8001
- if (!url) {
8002
- this.clearWhiteInks();
8003
- return { ok: true };
8004
- }
8005
- const targetId = this.resolveReplaceTargetId(null);
8006
- const upsertResult = await this.upsertWhiteInkEntry(url, {
8007
- id: targetId || void 0,
8008
- mode: targetId ? "replace" : "add",
8009
- createIfMissing: true,
8010
- addOptions: {}
8011
- });
8012
- return { ok: true, id: upsertResult.id };
8013
- }
8014
- }
8015
- ]
7208
+ [ContributionPointIds9.CONFIGURATIONS]: createWhiteInkConfigurations(),
7209
+ [ContributionPointIds9.COMMANDS]: createWhiteInkCommands(this)
8016
7210
  };
8017
7211
  }
8018
7212
  migrateLegacyConfigIfNeeded(configService) {
@@ -8022,15 +7216,12 @@ var WhiteInkTool = class {
8022
7216
  const item = this.normalizeItem({
8023
7217
  id: this.generateId(),
8024
7218
  sourceUrl: legacyMask,
8025
- 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);
8026
7224
  });
8027
- this.items = [item];
8028
- this.workingItems = this.cloneItems(this.items);
8029
- this.isUpdatingConfig = true;
8030
- configService.update("whiteInk.items", this.items);
8031
- setTimeout(() => {
8032
- this.isUpdatingConfig = false;
8033
- }, 0);
8034
7225
  }
8035
7226
  syncToolActiveFromWorkbench(fallbackId) {
8036
7227
  var _a;
@@ -8072,7 +7263,7 @@ var WhiteInkTool = class {
8072
7263
  id: String(item.id || this.generateId()),
8073
7264
  sourceUrl,
8074
7265
  url: sourceUrl,
8075
- opacity: WHITE_INK_DEFAULT_OPACITY
7266
+ opacity: WHITE_INK_DEFAULT_OPACITY2
8076
7267
  };
8077
7268
  }
8078
7269
  normalizeItems(items) {
@@ -8091,7 +7282,7 @@ var WhiteInkTool = class {
8091
7282
  }
8092
7283
  return {
8093
7284
  id: WHITE_INK_AUTO_ITEM_ID,
8094
- opacity: WHITE_INK_DEFAULT_OPACITY
7285
+ opacity: WHITE_INK_DEFAULT_OPACITY2
8095
7286
  };
8096
7287
  }
8097
7288
  generateId() {
@@ -8114,31 +7305,45 @@ var WhiteInkTool = class {
8114
7305
  }
8115
7306
  return null;
8116
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
+ }
8117
7323
  updateConfig(newItems, skipCanvasUpdate = false) {
8118
7324
  if (!this.context) return;
8119
- this.isUpdatingConfig = true;
8120
- this.items = this.normalizeItems(newItems);
8121
- if (!this.isToolActive || !this.hasWorkingChanges) {
8122
- this.workingItems = this.cloneItems(this.items);
8123
- this.hasWorkingChanges = false;
8124
- }
8125
- const configService = this.context.services.get(
8126
- "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
8127
7339
  );
8128
- configService == null ? void 0 : configService.update("whiteInk.items", this.items);
8129
- if (!skipCanvasUpdate) {
8130
- this.updateWhiteInks();
8131
- }
8132
- setTimeout(() => {
8133
- this.isUpdatingConfig = false;
8134
- }, 50);
8135
7340
  }
8136
7341
  async addWhiteInkEntry(url, options) {
8137
7342
  const id = this.generateId();
8138
7343
  const item = this.normalizeItem({
8139
7344
  id,
8140
7345
  sourceUrl: url,
8141
- opacity: WHITE_INK_DEFAULT_OPACITY,
7346
+ opacity: WHITE_INK_DEFAULT_OPACITY2,
8142
7347
  ...options
8143
7348
  });
8144
7349
  const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
@@ -8223,7 +7428,7 @@ var WhiteInkTool = class {
8223
7428
  this.updateConfig(next);
8224
7429
  }
8225
7430
  clearWhiteInks() {
8226
- this.sourceSizeBySrc.clear();
7431
+ this.sourceSizeCache.clear();
8227
7432
  this.previewMaskBySource.clear();
8228
7433
  this.pendingPreviewMaskBySource.clear();
8229
7434
  this.updateConfig([]);
@@ -8235,41 +7440,19 @@ var WhiteInkTool = class {
8235
7440
  }
8236
7441
  getFrameRect() {
8237
7442
  var _a;
8238
- if (!this.canvasService) {
8239
- return { left: 0, top: 0, width: 0, height: 0 };
8240
- }
8241
7443
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
8242
7444
  "ConfigurationService"
8243
7445
  );
8244
- if (!configService) {
8245
- return { left: 0, top: 0, width: 0, height: 0 };
8246
- }
8247
- const sizeState = readSizeState(configService);
8248
- const layout = computeSceneLayout(this.canvasService, sizeState);
8249
- if (!layout) {
8250
- return { left: 0, top: 0, width: 0, height: 0 };
8251
- }
8252
- return this.canvasService.toSceneRect({
8253
- left: layout.cutRect.left,
8254
- top: layout.cutRect.top,
8255
- width: layout.cutRect.width,
8256
- height: layout.cutRect.height
8257
- });
7446
+ return resolveCutFrameRect(this.canvasService, configService);
8258
7447
  }
8259
7448
  toLayoutSceneRect(rect) {
8260
- return {
8261
- left: rect.left,
8262
- top: rect.top,
8263
- width: rect.width,
8264
- height: rect.height,
8265
- space: "scene"
8266
- };
7449
+ return toLayoutSceneRect(rect);
8267
7450
  }
8268
7451
  getImageObjects() {
8269
7452
  if (!this.canvasService) return [];
8270
7453
  return this.canvasService.canvas.getObjects().filter((obj) => {
8271
7454
  var _a;
8272
- 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;
8273
7456
  });
8274
7457
  }
8275
7458
  getPrimaryImageObject() {
@@ -8341,21 +7524,16 @@ var WhiteInkTool = class {
8341
7524
  return snapshot.src === placement.committedUrl;
8342
7525
  }
8343
7526
  getCoverScale(frame, source) {
8344
- const frameW = Math.max(1, frame.width);
8345
- const frameH = Math.max(1, frame.height);
8346
- const sourceW = Math.max(1, source.width);
8347
- const sourceH = Math.max(1, source.height);
8348
- return Math.max(frameW / sourceW, frameH / sourceH);
7527
+ return getCoverScale(frame, source);
8349
7528
  }
8350
7529
  async ensureSourceSize(sourceUrl) {
8351
- if (!sourceUrl) return null;
8352
- const cached = this.getSourceSize(sourceUrl);
8353
- if (cached) return cached;
7530
+ return this.sourceSizeCache.ensureImageSize(sourceUrl);
7531
+ }
7532
+ async loadImageSize(sourceUrl) {
8354
7533
  try {
8355
7534
  const image = await this.loadImageElement(sourceUrl);
8356
7535
  const size = this.getElementSize(image);
8357
7536
  if (!size) return null;
8358
- this.rememberSourceSize(sourceUrl, size);
8359
7537
  return {
8360
7538
  width: size.width,
8361
7539
  height: size.height
@@ -8400,22 +7578,10 @@ var WhiteInkTool = class {
8400
7578
  return (obj == null ? void 0 : obj._element) || (obj == null ? void 0 : obj._originalElement) || null;
8401
7579
  }
8402
7580
  rememberSourceSize(src, size) {
8403
- if (!src) return;
8404
- if (!Number.isFinite(size.width) || !Number.isFinite(size.height)) return;
8405
- if (size.width <= 0 || size.height <= 0) return;
8406
- this.sourceSizeBySrc.set(src, {
8407
- width: size.width,
8408
- height: size.height
8409
- });
7581
+ this.sourceSizeCache.rememberSourceSize(src, size);
8410
7582
  }
8411
7583
  getSourceSize(src) {
8412
- if (!src) return null;
8413
- const cached = this.sourceSizeBySrc.get(src);
8414
- if (!cached) return null;
8415
- return {
8416
- width: cached.width,
8417
- height: cached.height
8418
- };
7584
+ return this.sourceSizeCache.getSourceSize(src);
8419
7585
  }
8420
7586
  computeWhiteScaleAdjust(baseSource, whiteSource) {
8421
7587
  if (!baseSource || !whiteSource || baseSource === whiteSource) {
@@ -8435,7 +7601,7 @@ var WhiteInkTool = class {
8435
7601
  };
8436
7602
  }
8437
7603
  computeCoverOpacity() {
8438
- const raw = WHITE_INK_DEFAULT_OPACITY * WHITE_INK_COVER_OPACITY_FACTOR;
7604
+ const raw = WHITE_INK_DEFAULT_OPACITY2 * WHITE_INK_COVER_OPACITY_FACTOR;
8439
7605
  return Math.max(
8440
7606
  WHITE_INK_COVER_OPACITY_MIN,
8441
7607
  Math.min(WHITE_INK_COVER_OPACITY_MAX, raw)
@@ -8699,7 +7865,7 @@ var WhiteInkTool = class {
8699
7865
  purgeSourceCaches(item) {
8700
7866
  const sourceUrl = this.resolveSourceUrl(item);
8701
7867
  if (!sourceUrl) return;
8702
- this.sourceSizeBySrc.delete(sourceUrl);
7868
+ this.sourceSizeCache.deleteSourceSize(sourceUrl);
8703
7869
  const prefix = `${sourceUrl}::`;
8704
7870
  Array.from(this.previewMaskBySource.keys()).forEach((cacheKey) => {
8705
7871
  if (cacheKey.startsWith(prefix)) {
@@ -8738,7 +7904,7 @@ var WhiteInkTool = class {
8738
7904
  "white-ink.main",
8739
7905
  snapshot,
8740
7906
  sources.whiteSrc,
8741
- WHITE_INK_DEFAULT_OPACITY,
7907
+ WHITE_INK_DEFAULT_OPACITY2,
8742
7908
  WHITE_INK_OBJECT_LAYER_ID,
8743
7909
  "white-ink",
8744
7910
  sources.whiteScaleAdjustX,
@@ -8846,7 +8012,7 @@ var WhiteInkTool = class {
8846
8012
  }
8847
8013
  };
8848
8014
 
8849
- // src/extensions/sceneLayout.ts
8015
+ // src/services/SceneLayoutService.ts
8850
8016
  import {
8851
8017
  COMMAND_SERVICE,
8852
8018
  CONFIGURATION_SERVICE
@@ -8859,6 +8025,7 @@ var SceneLayoutService = class {
8859
8025
  constructor() {
8860
8026
  this.lastLayout = null;
8861
8027
  this.lastGeometry = null;
8028
+ this.subscriptions = new SubscriptionBag();
8862
8029
  this.commandDisposables = [];
8863
8030
  this.onCanvasResized = () => {
8864
8031
  this.refresh();
@@ -8894,16 +8061,13 @@ var SceneLayoutService = class {
8894
8061
  () => this.getGeometry()
8895
8062
  )
8896
8063
  );
8897
- this.onConfigChange = configService.onAnyChange(this.onConfigChanged);
8898
- 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);
8899
8067
  this.refresh();
8900
8068
  }
8901
8069
  dispose(context) {
8902
- var _a, _b;
8903
- const activeContext = (_a = this.context) != null ? _a : context;
8904
- activeContext.eventBus.off("canvas:resized", this.onCanvasResized);
8905
- (_b = this.onConfigChange) == null ? void 0 : _b.dispose();
8906
- this.onConfigChange = void 0;
8070
+ this.subscriptions.disposeAll();
8907
8071
  this.commandDisposables.forEach((item) => item.dispose());
8908
8072
  this.commandDisposables = [];
8909
8073
  this.context = void 0;
@@ -10084,5 +9248,13 @@ export {
10084
9248
  SizeTool,
10085
9249
  ViewportSystem,
10086
9250
  WhiteInkTool,
9251
+ getCoverScale as computeImageCoverScale,
9252
+ getCoverScale as computeWhiteInkCoverScale,
9253
+ createDielineCommands,
9254
+ createDielineConfigurations,
9255
+ createImageCommands,
9256
+ createImageConfigurations,
9257
+ createWhiteInkCommands,
9258
+ createWhiteInkConfigurations,
10087
9259
  evaluateVisibilityExpr
10088
9260
  };