@pooder/kit 5.3.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.test-dist/src/extensions/background.js +475 -131
  2. package/.test-dist/src/extensions/dieline.js +283 -180
  3. package/.test-dist/src/extensions/dielineShape.js +66 -0
  4. package/.test-dist/src/extensions/feature.js +388 -303
  5. package/.test-dist/src/extensions/film.js +133 -74
  6. package/.test-dist/src/extensions/geometry.js +120 -56
  7. package/.test-dist/src/extensions/image.js +296 -212
  8. package/.test-dist/src/extensions/index.js +1 -3
  9. package/.test-dist/src/extensions/maskOps.js +75 -20
  10. package/.test-dist/src/extensions/ruler.js +312 -215
  11. package/.test-dist/src/extensions/sceneLayoutModel.js +9 -3
  12. package/.test-dist/src/extensions/sceneVisibility.js +3 -10
  13. package/.test-dist/src/extensions/tracer.js +229 -58
  14. package/.test-dist/src/extensions/white-ink.js +139 -129
  15. package/.test-dist/src/services/CanvasService.js +888 -126
  16. package/.test-dist/src/services/index.js +1 -0
  17. package/.test-dist/src/services/visibility.js +54 -0
  18. package/.test-dist/tests/run.js +58 -4
  19. package/CHANGELOG.md +12 -0
  20. package/dist/index.d.mts +377 -82
  21. package/dist/index.d.ts +377 -82
  22. package/dist/index.js +3920 -2178
  23. package/dist/index.mjs +3992 -2247
  24. package/package.json +1 -1
  25. package/src/extensions/background.ts +631 -145
  26. package/src/extensions/dieline.ts +280 -187
  27. package/src/extensions/dielineShape.ts +109 -0
  28. package/src/extensions/feature.ts +485 -366
  29. package/src/extensions/film.ts +152 -76
  30. package/src/extensions/geometry.ts +203 -104
  31. package/src/extensions/image.ts +319 -238
  32. package/src/extensions/index.ts +0 -1
  33. package/src/extensions/ruler.ts +481 -268
  34. package/src/extensions/sceneLayoutModel.ts +18 -6
  35. package/src/extensions/white-ink.ts +157 -171
  36. package/src/services/CanvasService.ts +1126 -140
  37. package/src/services/index.ts +1 -0
  38. package/src/services/renderSpec.ts +69 -4
  39. package/src/services/visibility.ts +78 -0
  40. package/tests/run.ts +139 -4
  41. package/.test-dist/src/CanvasService.js +0 -249
  42. package/.test-dist/src/ViewportSystem.js +0 -75
  43. package/.test-dist/src/background.js +0 -203
  44. package/.test-dist/src/bridgeSelection.js +0 -20
  45. package/.test-dist/src/constraints.js +0 -237
  46. package/.test-dist/src/dieline.js +0 -818
  47. package/.test-dist/src/edgeScale.js +0 -12
  48. package/.test-dist/src/feature.js +0 -826
  49. package/.test-dist/src/featureComplete.js +0 -32
  50. package/.test-dist/src/film.js +0 -167
  51. package/.test-dist/src/geometry.js +0 -506
  52. package/.test-dist/src/image.js +0 -1250
  53. package/.test-dist/src/maskOps.js +0 -270
  54. package/.test-dist/src/mirror.js +0 -104
  55. package/.test-dist/src/renderSpec.js +0 -2
  56. package/.test-dist/src/ruler.js +0 -343
  57. package/.test-dist/src/sceneLayout.js +0 -99
  58. package/.test-dist/src/sceneLayoutModel.js +0 -196
  59. package/.test-dist/src/sceneView.js +0 -40
  60. package/.test-dist/src/sceneVisibility.js +0 -42
  61. package/.test-dist/src/size.js +0 -332
  62. package/.test-dist/src/tracer.js +0 -544
  63. package/.test-dist/src/white-ink.js +0 -829
  64. package/.test-dist/src/wrappedOffsets.js +0 -33
  65. package/src/extensions/sceneVisibility.ts +0 -71
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ImageTool = void 0;
4
4
  const core_1 = require("@pooder/core");
5
5
  const fabric_1 = require("fabric");
6
+ const dielineShape_1 = require("./dielineShape");
6
7
  const geometry_1 = require("./geometry");
7
8
  const sceneLayoutModel_1 = require("./sceneLayoutModel");
8
9
  const IMAGE_OBJECT_LAYER_ID = "image.user";
@@ -23,6 +24,8 @@ class ImageTool {
23
24
  this.isImageSelectionActive = false;
24
25
  this.focusedImageId = null;
25
26
  this.renderSeq = 0;
27
+ this.imageSpecs = [];
28
+ this.overlaySpecs = [];
26
29
  this.onToolActivated = (event) => {
27
30
  const before = this.isToolActive;
28
31
  this.syncToolActiveFromWorkbench(event.id);
@@ -99,16 +102,20 @@ class ImageTool {
99
102
  const center = target.getCenterPoint
100
103
  ? target.getCenterPoint()
101
104
  : new fabric_1.Point(target.left ?? 0, target.top ?? 0);
105
+ const centerScene = this.canvasService
106
+ ? this.canvasService.toScenePoint({ x: center.x, y: center.y })
107
+ : { x: center.x, y: center.y };
102
108
  const objectScale = Number.isFinite(target?.scaleX) ? target.scaleX : 1;
109
+ const objectScaleScene = this.toSceneObjectScale(objectScale || 1);
103
110
  const workingItem = this.workingItems.find((item) => item.id === id);
104
111
  const sourceKey = workingItem?.sourceUrl || workingItem?.url || "";
105
112
  const sourceSize = this.getSourceSize(sourceKey, target);
106
113
  const coverScale = this.getCoverScale(frame, sourceSize);
107
114
  const updates = {
108
- left: this.clampNormalized((center.x - frame.left) / frame.width),
109
- top: this.clampNormalized((center.y - frame.top) / frame.height),
115
+ left: this.clampNormalized((centerScene.x - frame.left) / frame.width),
116
+ top: this.clampNormalized((centerScene.y - frame.top) / frame.height),
110
117
  angle: Number.isFinite(target.angle) ? target.angle : 0,
111
- scale: Math.max(0.05, (objectScale || 1) / coverScale),
118
+ scale: Math.max(0.05, objectScaleScene / coverScale),
112
119
  };
113
120
  this.focusedImageId = id;
114
121
  this.updateImageInWorking(id, updates);
@@ -121,6 +128,37 @@ class ImageTool {
121
128
  console.warn("CanvasService not found for ImageTool");
122
129
  return;
123
130
  }
131
+ this.renderProducerDisposable?.dispose();
132
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(this.id, () => ({
133
+ passes: [
134
+ {
135
+ id: IMAGE_OBJECT_LAYER_ID,
136
+ stack: 500,
137
+ order: 0,
138
+ visibility: {
139
+ op: "not",
140
+ expr: {
141
+ op: "sessionActive",
142
+ toolId: "pooder.kit.white-ink",
143
+ },
144
+ },
145
+ objects: this.imageSpecs,
146
+ },
147
+ {
148
+ id: IMAGE_OVERLAY_LAYER_ID,
149
+ stack: 800,
150
+ order: 0,
151
+ visibility: {
152
+ op: "not",
153
+ expr: {
154
+ op: "sessionActive",
155
+ toolId: "pooder.kit.white-ink",
156
+ },
157
+ },
158
+ objects: this.overlaySpecs,
159
+ },
160
+ ],
161
+ }), { priority: 300 });
124
162
  context.eventBus.on("tool:activated", this.onToolActivated);
125
163
  context.eventBus.on("object:modified", this.onObjectModified);
126
164
  context.eventBus.on("selection:created", this.onSelectionChanged);
@@ -166,9 +204,14 @@ class ImageTool {
166
204
  this.dirtyTrackerDisposable = undefined;
167
205
  this.cropShapeHatchPattern = undefined;
168
206
  this.cropShapeHatchPatternColor = undefined;
207
+ this.cropShapeHatchPatternKey = undefined;
208
+ this.imageSpecs = [];
209
+ this.overlaySpecs = [];
169
210
  this.clearRenderedImages();
211
+ this.renderProducerDisposable?.dispose();
212
+ this.renderProducerDisposable = undefined;
170
213
  if (this.canvasService) {
171
- void this.canvasService.applyObjectSpecsToRootLayer(IMAGE_OVERLAY_LAYER_ID, []);
214
+ void this.canvasService.flushRenderFromProducers();
172
215
  this.canvasService = undefined;
173
216
  }
174
217
  this.context = undefined;
@@ -581,42 +624,40 @@ class ImageTool {
581
624
  if (!layout) {
582
625
  return { left: 0, top: 0, width: 0, height: 0 };
583
626
  }
584
- return {
627
+ return this.canvasService.toSceneRect({
585
628
  left: layout.cutRect.left,
586
629
  top: layout.cutRect.top,
587
630
  width: layout.cutRect.width,
588
631
  height: layout.cutRect.height,
632
+ });
633
+ }
634
+ getFrameRectScreen(frame) {
635
+ if (!this.canvasService) {
636
+ return { left: 0, top: 0, width: 0, height: 0 };
637
+ }
638
+ return this.canvasService.toScreenRect(frame || this.getFrameRect());
639
+ }
640
+ toLayoutSceneRect(rect) {
641
+ return {
642
+ left: rect.left,
643
+ top: rect.top,
644
+ width: rect.width,
645
+ height: rect.height,
646
+ space: "scene",
589
647
  };
590
648
  }
591
649
  async resolveDefaultFitArea() {
592
- if (!this.context || !this.canvasService)
593
- return null;
594
- const commandService = this.context.services.get("CommandService");
595
- if (!commandService)
650
+ if (!this.canvasService)
596
651
  return null;
597
- try {
598
- const layout = await Promise.resolve(commandService.executeCommand("getSceneLayout"));
599
- const cutRect = layout?.cutRect;
600
- const width = Number(cutRect?.width);
601
- const height = Number(cutRect?.height);
602
- const left = Number(cutRect?.left);
603
- const top = Number(cutRect?.top);
604
- if (!Number.isFinite(width) ||
605
- !Number.isFinite(height) ||
606
- !Number.isFinite(left) ||
607
- !Number.isFinite(top)) {
608
- return null;
609
- }
610
- return {
611
- width: Math.max(1, width),
612
- height: Math.max(1, height),
613
- left: left + width / 2,
614
- top: top + height / 2,
615
- };
616
- }
617
- catch {
652
+ const frame = this.getFrameRect();
653
+ if (frame.width <= 0 || frame.height <= 0)
618
654
  return null;
619
- }
655
+ return {
656
+ width: Math.max(1, frame.width),
657
+ height: Math.max(1, frame.height),
658
+ left: frame.left + frame.width / 2,
659
+ top: frame.top + frame.height / 2,
660
+ };
620
661
  }
621
662
  async fitImageToDefaultArea(id) {
622
663
  if (!this.canvasService)
@@ -626,13 +667,14 @@ class ImageTool {
626
667
  await this.fitImageToArea(id, area);
627
668
  return;
628
669
  }
629
- const canvasW = Math.max(1, this.canvasService.canvas.width || 0);
630
- const canvasH = Math.max(1, this.canvasService.canvas.height || 0);
670
+ const viewport = this.canvasService.getSceneViewportRect();
671
+ const canvasW = Math.max(1, viewport.width || 0);
672
+ const canvasH = Math.max(1, viewport.height || 0);
631
673
  await this.fitImageToArea(id, {
632
674
  width: canvasW,
633
675
  height: canvasH,
634
- left: canvasW / 2,
635
- top: canvasH / 2,
676
+ left: viewport.left + canvasW / 2,
677
+ top: viewport.top + canvasH / 2,
636
678
  });
637
679
  }
638
680
  getImageObjects() {
@@ -645,7 +687,7 @@ class ImageTool {
645
687
  getOverlayObjects() {
646
688
  if (!this.canvasService)
647
689
  return [];
648
- return this.canvasService.getRootLayerObjects(IMAGE_OVERLAY_LAYER_ID);
690
+ return this.canvasService.getPassObjects(IMAGE_OVERLAY_LAYER_ID);
649
691
  }
650
692
  getImageObject(id) {
651
693
  return this.getImageObjects().find((obj) => obj?.data?.id === id);
@@ -653,9 +695,9 @@ class ImageTool {
653
695
  clearRenderedImages() {
654
696
  if (!this.canvasService)
655
697
  return;
656
- const canvas = this.canvasService.canvas;
657
- this.getImageObjects().forEach((obj) => canvas.remove(obj));
658
- this.canvasService.requestRenderAll();
698
+ this.imageSpecs = [];
699
+ this.overlaySpecs = [];
700
+ this.canvasService.requestRenderFromProducers();
659
701
  }
660
702
  purgeSourceSizeCacheForItem(item) {
661
703
  if (!item)
@@ -683,6 +725,32 @@ class ImageTool {
683
725
  }
684
726
  return { width: 1, height: 1 };
685
727
  }
728
+ async ensureSourceSize(src) {
729
+ if (!src)
730
+ return null;
731
+ const cached = this.sourceSizeBySrc.get(src);
732
+ if (cached)
733
+ return cached;
734
+ try {
735
+ const image = await fabric_1.Image.fromURL(src, {
736
+ crossOrigin: "anonymous",
737
+ });
738
+ const width = Number(image?.width || 0);
739
+ const height = Number(image?.height || 0);
740
+ if (width > 0 && height > 0) {
741
+ const size = { width, height };
742
+ this.sourceSizeBySrc.set(src, size);
743
+ return size;
744
+ }
745
+ }
746
+ catch (error) {
747
+ this.debug("image:size:load-failed", {
748
+ src,
749
+ error: error instanceof Error ? error.message : String(error),
750
+ });
751
+ }
752
+ return null;
753
+ }
686
754
  getCoverScale(frame, size) {
687
755
  const sw = Math.max(1, size.width);
688
756
  const sh = Math.max(1, size.height);
@@ -710,16 +778,21 @@ class ImageTool {
710
778
  }
711
779
  toSceneGeometryLike(raw) {
712
780
  const shape = raw?.shape;
713
- if (shape !== "rect" &&
714
- shape !== "circle" &&
715
- shape !== "ellipse" &&
716
- shape !== "custom") {
781
+ if (!(0, dielineShape_1.isDielineShape)(shape)) {
717
782
  return null;
718
783
  }
719
- const radius = Number(raw?.radius);
720
- const offset = Number(raw?.offset);
784
+ const radiusRaw = Number(raw?.radius);
785
+ const offsetRaw = Number(raw?.offset);
786
+ const unit = typeof raw?.unit === "string" ? raw.unit : "px";
787
+ const radius = unit === "scene" || !this.canvasService
788
+ ? radiusRaw
789
+ : this.canvasService.toSceneLength(radiusRaw);
790
+ const offset = unit === "scene" || !this.canvasService
791
+ ? offsetRaw
792
+ : this.canvasService.toSceneLength(offsetRaw);
721
793
  return {
722
794
  shape,
795
+ shapeStyle: (0, dielineShape_1.normalizeShapeStyle)(raw?.shapeStyle),
723
796
  radius: Number.isFinite(radius) ? radius : 0,
724
797
  offset: Number.isFinite(offset) ? offset : 0,
725
798
  };
@@ -773,8 +846,11 @@ class ImageTool {
773
846
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
774
847
  if (typeof document === "undefined")
775
848
  return undefined;
849
+ const sceneScale = this.canvasService?.getSceneScale() || 1;
850
+ const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
776
851
  if (this.cropShapeHatchPattern &&
777
- this.cropShapeHatchPatternColor === color) {
852
+ this.cropShapeHatchPatternColor === color &&
853
+ this.cropShapeHatchPatternKey === cacheKey) {
778
854
  return this.cropShapeHatchPattern;
779
855
  }
780
856
  const size = 16;
@@ -804,8 +880,18 @@ class ImageTool {
804
880
  // @ts-ignore: Fabric Pattern accepts canvas source here.
805
881
  repetition: "repeat",
806
882
  });
883
+ // Scene specs are scaled to screen by CanvasService; keep hatch density in screen pixels.
884
+ pattern.patternTransform = [
885
+ 1 / sceneScale,
886
+ 0,
887
+ 0,
888
+ 1 / sceneScale,
889
+ 0,
890
+ 0,
891
+ ];
807
892
  this.cropShapeHatchPattern = pattern;
808
893
  this.cropShapeHatchPatternColor = color;
894
+ this.cropShapeHatchPatternKey = cacheKey;
809
895
  return pattern;
810
896
  }
811
897
  buildCropShapeOverlaySpecs(frame, sceneGeometry) {
@@ -818,6 +904,7 @@ class ImageTool {
818
904
  return [];
819
905
  }
820
906
  const shape = sceneGeometry.shape;
907
+ const shapeStyle = sceneGeometry.shapeStyle;
821
908
  const inset = 0;
822
909
  const shapeWidth = Math.max(1, frame.width);
823
910
  const shapeHeight = Math.max(1, frame.height);
@@ -827,6 +914,7 @@ class ImageTool {
827
914
  frameWidth: frame.width,
828
915
  frameHeight: frame.height,
829
916
  offset: sceneGeometry.offset,
917
+ shapeStyle,
830
918
  inset,
831
919
  shapeWidth,
832
920
  shapeHeight,
@@ -849,6 +937,7 @@ class ImageTool {
849
937
  x: frame.width / 2,
850
938
  y: frame.height / 2,
851
939
  features: [],
940
+ shapeStyle,
852
941
  canvasWidth: frame.width,
853
942
  canvasHeight: frame.height,
854
943
  };
@@ -866,6 +955,9 @@ class ImageTool {
866
955
  }
867
956
  const patternFill = this.getCropShapeHatchPattern();
868
957
  const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
958
+ const shapeBounds = (0, geometry_1.getPathBounds)(shapePathData);
959
+ const hatchBounds = (0, geometry_1.getPathBounds)(hatchPathData);
960
+ const frameRect = this.toLayoutSceneRect(frame);
869
961
  const hatchPathLength = hatchPathData.length;
870
962
  const shapePathLength = shapePathData.length;
871
963
  const specs = [
@@ -873,10 +965,16 @@ class ImageTool {
873
965
  id: "image.cropShapeHatch",
874
966
  type: "path",
875
967
  data: { id: "image.cropShapeHatch", zIndex: 5 },
968
+ layout: {
969
+ reference: "custom",
970
+ referenceRect: frameRect,
971
+ alignX: "start",
972
+ alignY: "start",
973
+ offsetX: hatchBounds.x,
974
+ offsetY: hatchBounds.y,
975
+ },
876
976
  props: {
877
977
  pathData: hatchPathData,
878
- left: frame.left,
879
- top: frame.top,
880
978
  originX: "left",
881
979
  originY: "top",
882
980
  fill: hatchFill,
@@ -893,15 +991,21 @@ class ImageTool {
893
991
  id: "image.cropShapePath",
894
992
  type: "path",
895
993
  data: { id: "image.cropShapePath", zIndex: 6 },
994
+ layout: {
995
+ reference: "custom",
996
+ referenceRect: frameRect,
997
+ alignX: "start",
998
+ alignY: "start",
999
+ offsetX: shapeBounds.x,
1000
+ offsetY: shapeBounds.y,
1001
+ },
896
1002
  props: {
897
1003
  pathData: shapePathData,
898
- left: frame.left,
899
- top: frame.top,
900
1004
  originX: "left",
901
1005
  originY: "top",
902
1006
  fill: "rgba(0,0,0,0)",
903
1007
  stroke: "rgba(255, 0, 0, 0.9)",
904
- strokeWidth: 1,
1008
+ strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
905
1009
  selectable: false,
906
1010
  evented: false,
907
1011
  excludeFromExport: true,
@@ -918,6 +1022,8 @@ class ImageTool {
918
1022
  fillRule: "evenodd",
919
1023
  shapePathLength,
920
1024
  hatchPathLength,
1025
+ shapeBounds,
1026
+ hatchBounds,
921
1027
  hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
922
1028
  ids: specs.map((spec) => spec.id),
923
1029
  });
@@ -980,6 +1086,11 @@ class ImageTool {
980
1086
  opacity: render.opacity,
981
1087
  };
982
1088
  }
1089
+ toSceneObjectScale(value) {
1090
+ if (!this.canvasService)
1091
+ return value;
1092
+ return value / this.canvasService.getSceneScale();
1093
+ }
983
1094
  getCurrentSrc(obj) {
984
1095
  if (!obj)
985
1096
  return undefined;
@@ -987,109 +1098,28 @@ class ImageTool {
987
1098
  return obj.getSrc();
988
1099
  return obj?._originalElement?.src;
989
1100
  }
990
- applyImageControlVisibility(obj) {
991
- if (typeof obj?.setControlsVisibility !== "function")
992
- return;
993
- obj.setControlsVisibility({
994
- mt: false,
995
- mb: false,
996
- ml: false,
997
- mr: false,
998
- tl: true,
999
- tr: true,
1000
- bl: true,
1001
- br: true,
1002
- mtr: true,
1003
- });
1004
- }
1005
- async upsertImageObject(item, frame, seq) {
1006
- if (!this.canvasService)
1007
- return;
1008
- const canvas = this.canvasService.canvas;
1009
- const render = this.resolveRenderImageState(item);
1010
- if (!render.src)
1011
- return;
1012
- let obj = this.getImageObject(item.id);
1013
- const currentSrc = this.getCurrentSrc(obj);
1014
- if (obj && currentSrc && currentSrc !== render.src) {
1015
- canvas.remove(obj);
1016
- obj = undefined;
1017
- }
1018
- if (!obj) {
1019
- const created = await fabric_1.Image.fromURL(render.src, {
1020
- crossOrigin: "anonymous",
1021
- });
1022
- if (seq !== this.renderSeq)
1023
- return;
1024
- created.set({
1101
+ async buildImageSpecs(items, frame) {
1102
+ const specs = [];
1103
+ for (const item of items) {
1104
+ const render = this.resolveRenderImageState(item);
1105
+ if (!render.src)
1106
+ continue;
1107
+ const ensured = await this.ensureSourceSize(render.src);
1108
+ const sourceSize = ensured || this.getSourceSize(render.src);
1109
+ const props = this.computeCanvasProps(render, sourceSize, frame);
1110
+ specs.push({
1111
+ id: item.id,
1112
+ type: "image",
1113
+ src: render.src,
1025
1114
  data: {
1026
1115
  id: item.id,
1027
1116
  layerId: IMAGE_OBJECT_LAYER_ID,
1028
1117
  type: "image-item",
1029
1118
  },
1119
+ props,
1030
1120
  });
1031
- canvas.add(created);
1032
- obj = created;
1033
- }
1034
- this.rememberSourceSize(render.src, obj);
1035
- const sourceSize = this.getSourceSize(render.src, obj);
1036
- const props = this.computeCanvasProps(render, sourceSize, frame);
1037
- obj.set({
1038
- ...props,
1039
- data: {
1040
- ...(obj.data || {}),
1041
- id: item.id,
1042
- layerId: IMAGE_OBJECT_LAYER_ID,
1043
- type: "image-item",
1044
- },
1045
- });
1046
- this.applyImageControlVisibility(obj);
1047
- obj.setCoords();
1048
- const resolver = this.loadResolvers.get(item.id);
1049
- if (resolver) {
1050
- resolver();
1051
- this.loadResolvers.delete(item.id);
1052
- }
1053
- }
1054
- syncImageZOrder(items) {
1055
- if (!this.canvasService)
1056
- return;
1057
- const canvas = this.canvasService.canvas;
1058
- const objects = canvas.getObjects();
1059
- let insertIndex = 0;
1060
- const backgroundLayer = this.canvasService.getLayer("background");
1061
- if (backgroundLayer) {
1062
- const bgIndex = objects.indexOf(backgroundLayer);
1063
- if (bgIndex >= 0)
1064
- insertIndex = bgIndex + 1;
1065
- }
1066
- items.forEach((item) => {
1067
- const obj = this.getImageObject(item.id);
1068
- if (!obj)
1069
- return;
1070
- canvas.moveObjectTo(obj, insertIndex);
1071
- insertIndex += 1;
1072
- });
1073
- const overlayObjects = this.getOverlayObjects().sort((a, b) => {
1074
- const az = Number(a?.data?.zIndex ?? 0);
1075
- const bz = Number(b?.data?.zIndex ?? 0);
1076
- return az - bz;
1077
- });
1078
- overlayObjects.forEach((obj) => {
1079
- canvas.bringObjectToFront(obj);
1080
- });
1081
- if (this.isDebugEnabled()) {
1082
- const stack = canvas
1083
- .getObjects()
1084
- .map((obj, index) => ({
1085
- index,
1086
- id: obj?.data?.id,
1087
- layerId: obj?.data?.layerId,
1088
- zIndex: obj?.data?.zIndex,
1089
- }))
1090
- .filter((item) => item.layerId === IMAGE_OVERLAY_LAYER_ID);
1091
- this.debug("overlay:stack", stack);
1092
1121
  }
1122
+ return specs;
1093
1123
  }
1094
1124
  buildOverlaySpecs(frame, sceneGeometry) {
1095
1125
  const visible = this.isImageEditingVisible();
@@ -1106,31 +1136,53 @@ class ImageTool {
1106
1136
  });
1107
1137
  return [];
1108
1138
  }
1109
- const canvasW = this.canvasService.canvas.width || 0;
1110
- const canvasH = this.canvasService.canvas.height || 0;
1139
+ const viewport = this.canvasService.getSceneViewportRect();
1140
+ const canvasW = viewport.width || 0;
1141
+ const canvasH = viewport.height || 0;
1142
+ const canvasLeft = viewport.left || 0;
1143
+ const canvasTop = viewport.top || 0;
1111
1144
  const visual = this.getFrameVisualConfig();
1112
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
1113
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
1114
- const frameRight = Math.max(frameLeft, Math.min(canvasW, frame.left + frame.width));
1115
- const frameBottom = Math.max(frameTop, Math.min(canvasH, frame.top + frame.height));
1145
+ const strokeWidthScene = this.canvasService.toSceneLength(visual.strokeWidth);
1146
+ const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
1147
+ const frameLeft = Math.max(canvasLeft, Math.min(canvasLeft + canvasW, frame.left));
1148
+ const frameTop = Math.max(canvasTop, Math.min(canvasTop + canvasH, frame.top));
1149
+ const frameRight = Math.max(frameLeft, Math.min(canvasLeft + canvasW, frame.left + frame.width));
1150
+ const frameBottom = Math.max(frameTop, Math.min(canvasTop + canvasH, frame.top + frame.height));
1116
1151
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
1117
- const topH = frameTop;
1118
- const bottomH = Math.max(0, canvasH - frameBottom);
1119
- const leftW = frameLeft;
1120
- const rightW = Math.max(0, canvasW - frameRight);
1152
+ const topH = Math.max(0, frameTop - canvasTop);
1153
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
1154
+ const leftW = Math.max(0, frameLeft - canvasLeft);
1155
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
1156
+ const viewportRect = this.toLayoutSceneRect({
1157
+ left: canvasLeft,
1158
+ top: canvasTop,
1159
+ width: canvasW,
1160
+ height: canvasH,
1161
+ });
1162
+ const visibleFrameBandRect = this.toLayoutSceneRect({
1163
+ left: canvasLeft,
1164
+ top: frameTop,
1165
+ width: canvasW,
1166
+ height: visibleFrameH,
1167
+ });
1168
+ const frameRect = this.toLayoutSceneRect(frame);
1121
1169
  const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
1122
1170
  const mask = [
1123
1171
  {
1124
1172
  id: "image.cropMask.top",
1125
1173
  type: "rect",
1126
1174
  data: { id: "image.cropMask.top", zIndex: 1 },
1127
- props: {
1128
- left: canvasW / 2,
1129
- top: topH / 2,
1130
- width: canvasW,
1175
+ layout: {
1176
+ reference: "custom",
1177
+ referenceRect: viewportRect,
1178
+ alignX: "start",
1179
+ alignY: "start",
1180
+ width: "100%",
1131
1181
  height: topH,
1132
- originX: "center",
1133
- originY: "center",
1182
+ },
1183
+ props: {
1184
+ originX: "left",
1185
+ originY: "top",
1134
1186
  fill: visual.outerBackground,
1135
1187
  selectable: false,
1136
1188
  evented: false,
@@ -1140,13 +1192,17 @@ class ImageTool {
1140
1192
  id: "image.cropMask.bottom",
1141
1193
  type: "rect",
1142
1194
  data: { id: "image.cropMask.bottom", zIndex: 2 },
1143
- props: {
1144
- left: canvasW / 2,
1145
- top: frameBottom + bottomH / 2,
1146
- width: canvasW,
1195
+ layout: {
1196
+ reference: "custom",
1197
+ referenceRect: viewportRect,
1198
+ alignX: "start",
1199
+ alignY: "end",
1200
+ width: "100%",
1147
1201
  height: bottomH,
1148
- originX: "center",
1149
- originY: "center",
1202
+ },
1203
+ props: {
1204
+ originX: "left",
1205
+ originY: "top",
1150
1206
  fill: visual.outerBackground,
1151
1207
  selectable: false,
1152
1208
  evented: false,
@@ -1156,13 +1212,17 @@ class ImageTool {
1156
1212
  id: "image.cropMask.left",
1157
1213
  type: "rect",
1158
1214
  data: { id: "image.cropMask.left", zIndex: 3 },
1159
- props: {
1160
- left: leftW / 2,
1161
- top: frameTop + visibleFrameH / 2,
1215
+ layout: {
1216
+ reference: "custom",
1217
+ referenceRect: visibleFrameBandRect,
1218
+ alignX: "start",
1219
+ alignY: "start",
1162
1220
  width: leftW,
1163
- height: visibleFrameH,
1164
- originX: "center",
1165
- originY: "center",
1221
+ height: "100%",
1222
+ },
1223
+ props: {
1224
+ originX: "left",
1225
+ originY: "top",
1166
1226
  fill: visual.outerBackground,
1167
1227
  selectable: false,
1168
1228
  evented: false,
@@ -1172,13 +1232,17 @@ class ImageTool {
1172
1232
  id: "image.cropMask.right",
1173
1233
  type: "rect",
1174
1234
  data: { id: "image.cropMask.right", zIndex: 4 },
1175
- props: {
1176
- left: frameRight + rightW / 2,
1177
- top: frameTop + visibleFrameH / 2,
1235
+ layout: {
1236
+ reference: "custom",
1237
+ referenceRect: visibleFrameBandRect,
1238
+ alignX: "end",
1239
+ alignY: "start",
1178
1240
  width: rightW,
1179
- height: visibleFrameH,
1180
- originX: "center",
1181
- originY: "center",
1241
+ height: "100%",
1242
+ },
1243
+ props: {
1244
+ originX: "left",
1245
+ originY: "top",
1182
1246
  fill: visual.outerBackground,
1183
1247
  selectable: false,
1184
1248
  evented: false,
@@ -1189,26 +1253,32 @@ class ImageTool {
1189
1253
  id: "image.cropFrame",
1190
1254
  type: "rect",
1191
1255
  data: { id: "image.cropFrame", zIndex: 7 },
1256
+ layout: {
1257
+ reference: "custom",
1258
+ referenceRect: frameRect,
1259
+ alignX: "start",
1260
+ alignY: "start",
1261
+ width: "100%",
1262
+ height: "100%",
1263
+ },
1192
1264
  props: {
1193
- left: frame.left + frame.width / 2,
1194
- top: frame.top + frame.height / 2,
1195
- width: frame.width,
1196
- height: frame.height,
1197
- originX: "center",
1198
- originY: "center",
1265
+ originX: "left",
1266
+ originY: "top",
1199
1267
  fill: visual.innerBackground,
1200
1268
  stroke: visual.strokeStyle === "hidden"
1201
1269
  ? "rgba(0,0,0,0)"
1202
1270
  : visual.strokeColor,
1203
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
1271
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
1204
1272
  strokeDashArray: visual.strokeStyle === "dashed"
1205
- ? [visual.dashLength, visual.dashLength]
1273
+ ? [dashLengthScene, dashLengthScene]
1206
1274
  : undefined,
1207
1275
  selectable: false,
1208
1276
  evented: false,
1209
1277
  },
1210
1278
  };
1211
- const specs = [...mask, ...shapeOverlay, frameSpec];
1279
+ const specs = shapeOverlay.length > 0
1280
+ ? [...mask, ...shapeOverlay]
1281
+ : [...mask, ...shapeOverlay, frameSpec];
1212
1282
  this.debug("overlay:built", {
1213
1283
  frame,
1214
1284
  shape: sceneGeometry?.shape,
@@ -1236,31 +1306,37 @@ class ImageTool {
1236
1306
  skipRender: true,
1237
1307
  });
1238
1308
  }
1239
- this.getImageObjects().forEach((obj) => {
1240
- const id = obj?.data?.id;
1241
- if (typeof id === "string" && !desiredIds.has(id)) {
1242
- this.canvasService?.canvas.remove(obj);
1243
- }
1244
- });
1245
- for (const item of renderItems) {
1246
- if (seq !== this.renderSeq)
1247
- return;
1248
- await this.upsertImageObject(item, frame, seq);
1249
- }
1309
+ const imageSpecs = await this.buildImageSpecs(renderItems, frame);
1250
1310
  if (seq !== this.renderSeq)
1251
1311
  return;
1252
- this.syncImageZOrder(renderItems);
1253
1312
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
1254
1313
  if (seq !== this.renderSeq)
1255
1314
  return;
1256
- const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1257
- await this.canvasService.applyObjectSpecsToRootLayer(IMAGE_OVERLAY_LAYER_ID, overlaySpecs);
1258
- this.syncImageZOrder(renderItems);
1315
+ this.imageSpecs = imageSpecs;
1316
+ this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1317
+ await this.canvasService.flushRenderFromProducers();
1318
+ if (seq !== this.renderSeq)
1319
+ return;
1320
+ renderItems.forEach((item) => {
1321
+ if (!this.getImageObject(item.id))
1322
+ return;
1323
+ const resolver = this.loadResolvers.get(item.id);
1324
+ if (!resolver)
1325
+ return;
1326
+ resolver();
1327
+ this.loadResolvers.delete(item.id);
1328
+ });
1329
+ if (this.focusedImageId && this.isToolActive) {
1330
+ this.setImageFocus(this.focusedImageId, {
1331
+ syncCanvasSelection: true,
1332
+ skipRender: true,
1333
+ });
1334
+ }
1259
1335
  const overlayCanvasCount = this.getOverlayObjects().length;
1260
1336
  this.debug("render:done", {
1261
1337
  seq,
1262
1338
  renderCount: renderItems.length,
1263
- overlayCount: overlaySpecs.length,
1339
+ overlayCount: this.overlaySpecs.length,
1264
1340
  overlayCanvasCount,
1265
1341
  isToolActive: this.isToolActive,
1266
1342
  isImageSelectionActive: this.isImageSelectionActive,
@@ -1350,7 +1426,7 @@ class ImageTool {
1350
1426
  const source = this.getSourceSize(render.src, obj);
1351
1427
  const frame = this.getFrameRect();
1352
1428
  const coverScale = this.getCoverScale(frame, source);
1353
- const currentScale = obj.scaleX || 1;
1429
+ const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
1354
1430
  const zoom = Math.max(0.05, currentScale / coverScale);
1355
1431
  const updated = {
1356
1432
  scale: Number.isFinite(zoom) ? zoom : 1,
@@ -1388,12 +1464,17 @@ class ImageTool {
1388
1464
  const frame = this.getFrameRect();
1389
1465
  const baseCover = this.getCoverScale(frame, source);
1390
1466
  const desiredScale = Math.max(Math.max(1, area.width) / Math.max(1, source.width), Math.max(1, area.height) / Math.max(1, source.height));
1391
- const canvasW = this.canvasService.canvas.width || 1;
1392
- const canvasH = this.canvasService.canvas.height || 1;
1467
+ const viewport = this.canvasService.getSceneViewportRect();
1468
+ const canvasW = viewport.width || 1;
1469
+ const canvasH = viewport.height || 1;
1393
1470
  const areaLeftInput = area.left ?? 0.5;
1394
1471
  const areaTopInput = area.top ?? 0.5;
1395
- const areaLeftPx = areaLeftInput <= 1.5 ? areaLeftInput * canvasW : areaLeftInput;
1396
- const areaTopPx = areaTopInput <= 1.5 ? areaTopInput * canvasH : areaTopInput;
1472
+ const areaLeftPx = areaLeftInput <= 1.5
1473
+ ? viewport.left + areaLeftInput * canvasW
1474
+ : areaLeftInput;
1475
+ const areaTopPx = areaTopInput <= 1.5
1476
+ ? viewport.top + areaTopInput * canvasH
1477
+ : areaTopInput;
1397
1478
  const updates = {
1398
1479
  scale: Math.max(0.05, desiredScale / baseCover),
1399
1480
  left: this.clampNormalized((areaLeftPx - frame.left) / Math.max(1, frame.width)),
@@ -1426,6 +1507,8 @@ class ImageTool {
1426
1507
  next.push(this.normalizeItem({
1427
1508
  ...item,
1428
1509
  url,
1510
+ // Keep original source for next image-tool session editing,
1511
+ // and use committedUrl as non-image-tools render source.
1429
1512
  sourceUrl,
1430
1513
  committedUrl: url,
1431
1514
  }));
@@ -1447,7 +1530,8 @@ class ImageTool {
1447
1530
  if (!normalizedIds.length) {
1448
1531
  throw new Error("image-ids-required");
1449
1532
  }
1450
- const frame = this.getFrameRect();
1533
+ const frameScene = this.getFrameRect();
1534
+ const frame = this.getFrameRectScreen(frameScene);
1451
1535
  const multiplier = Math.max(1, options.multiplier ?? 2);
1452
1536
  const format = options.format === "jpeg" ? "jpeg" : "png";
1453
1537
  const width = Math.max(1, Math.round(frame.width * multiplier));