@pooder/kit 6.2.1 → 6.3.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.
@@ -3,8 +3,6 @@ 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");
7
- const geometry_1 = require("../geometry");
8
6
  const sceneLayoutModel_1 = require("../../shared/scene/sceneLayoutModel");
9
7
  const frame_1 = require("../../shared/scene/frame");
10
8
  const sourceSizeCache_1 = require("../../shared/imaging/sourceSizeCache");
@@ -13,6 +11,8 @@ const sessionState_1 = require("../../shared/runtime/sessionState");
13
11
  const layers_1 = require("../../shared/constants/layers");
14
12
  const commands_1 = require("./commands");
15
13
  const config_1 = require("./config");
14
+ const imageOperations_1 = require("./imageOperations");
15
+ const sessionOverlay_1 = require("./sessionOverlay");
16
16
  const IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
17
17
  "rotate",
18
18
  "scale",
@@ -256,6 +256,7 @@ class ImageTool {
256
256
  this.clearRenderedImages();
257
257
  this.renderProducerDisposable?.dispose();
258
258
  this.renderProducerDisposable = undefined;
259
+ this.emitImageStateChange();
259
260
  if (this.canvasService) {
260
261
  void this.canvasService.flushRenderFromProducers();
261
262
  this.canvasService = undefined;
@@ -427,10 +428,20 @@ class ImageTool {
427
428
  this.canvasService?.requestRenderAll();
428
429
  }
429
430
  }
431
+ clearSnapGuideContext() {
432
+ const topContext = this.canvasService?.canvas.contextTop;
433
+ if (!this.canvasService || !topContext)
434
+ return;
435
+ this.canvasService.canvas.clearContext(topContext);
436
+ }
430
437
  clearSnapPreview() {
438
+ const shouldClearCanvas = this.hasRenderedSnapGuides || !!this.activeSnapX || !!this.activeSnapY;
431
439
  this.activeSnapX = null;
432
440
  this.activeSnapY = null;
433
441
  this.hasRenderedSnapGuides = false;
442
+ if (shouldClearCanvas) {
443
+ this.clearSnapGuideContext();
444
+ }
434
445
  this.canvasService?.requestRenderAll();
435
446
  }
436
447
  endMoveSnapInteraction() {
@@ -636,9 +647,9 @@ class ImageTool {
636
647
  name: "Image",
637
648
  interaction: "session",
638
649
  commands: {
639
- begin: "resetWorkingImages",
650
+ begin: "imageSessionReset",
640
651
  commit: "completeImages",
641
- rollback: "resetWorkingImages",
652
+ rollback: "imageSessionReset",
642
653
  },
643
654
  session: {
644
655
  autoBegin: true,
@@ -676,6 +687,29 @@ class ImageTool {
676
687
  cloneItems(items) {
677
688
  return this.normalizeItems((items || []).map((i) => ({ ...i })));
678
689
  }
690
+ getViewItems() {
691
+ return this.isToolActive ? this.workingItems : this.items;
692
+ }
693
+ getImageViewState() {
694
+ this.syncToolActiveFromWorkbench();
695
+ const items = this.cloneItems(this.getViewItems());
696
+ const focusedItem = this.focusedImageId == null
697
+ ? null
698
+ : items.find((item) => item.id === this.focusedImageId) || null;
699
+ return {
700
+ items,
701
+ hasAnyImage: items.length > 0,
702
+ focusedId: this.focusedImageId,
703
+ focusedItem,
704
+ isToolActive: this.isToolActive,
705
+ isImageSelectionActive: this.isImageSelectionActive,
706
+ hasWorkingChanges: this.hasWorkingChanges,
707
+ source: this.isToolActive ? "working" : "committed",
708
+ };
709
+ }
710
+ emitImageStateChange() {
711
+ this.context?.eventBus.emit("image:state:change", this.getImageViewState());
712
+ }
679
713
  emitWorkingChange(changedId = null) {
680
714
  this.context?.eventBus.emit("image:working:change", {
681
715
  changedId,
@@ -713,9 +747,13 @@ class ImageTool {
713
747
  if (!options.skipRender) {
714
748
  this.updateImages();
715
749
  }
750
+ else {
751
+ this.emitImageStateChange();
752
+ }
716
753
  return { ok: true, id };
717
754
  }
718
- async addImageEntry(url, options, fitOnAdd = true) {
755
+ async addImageEntry(url, options, operation) {
756
+ this.syncToolActiveFromWorkbench();
719
757
  const id = this.generateId();
720
758
  const newItem = this.normalizeItem({
721
759
  id,
@@ -723,13 +761,21 @@ class ImageTool {
723
761
  opacity: 1,
724
762
  ...options,
725
763
  });
726
- const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
727
764
  const waitLoaded = this.waitImageLoaded(id, true);
728
- this.updateConfig([...this.items, newItem]);
729
- this.addItemToWorkingSessionIfNeeded(newItem, sessionDirtyBeforeAdd);
765
+ if (this.isToolActive) {
766
+ this.workingItems = this.cloneItems([...this.workingItems, newItem]);
767
+ this.hasWorkingChanges = true;
768
+ this.updateImages();
769
+ this.emitWorkingChange(id);
770
+ }
771
+ else {
772
+ this.updateConfig([...this.items, newItem]);
773
+ }
730
774
  const loaded = await waitLoaded;
731
- if (loaded && fitOnAdd) {
732
- await this.fitImageToDefaultArea(id);
775
+ if (loaded && operation) {
776
+ await this.applyImageOperation(id, operation, {
777
+ target: this.isToolActive ? "working" : "config",
778
+ });
733
779
  }
734
780
  if (loaded) {
735
781
  this.setImageFocus(id);
@@ -737,8 +783,8 @@ class ImageTool {
737
783
  return id;
738
784
  }
739
785
  async upsertImageEntry(url, options = {}) {
786
+ this.syncToolActiveFromWorkbench();
740
787
  const mode = options.mode || (options.id ? "replace" : "add");
741
- const fitOnAdd = options.fitOnAdd !== false;
742
788
  if (mode === "replace") {
743
789
  if (!options.id) {
744
790
  throw new Error("replace-target-id-required");
@@ -747,21 +793,33 @@ class ImageTool {
747
793
  if (!this.hasImageItem(targetId)) {
748
794
  throw new Error("replace-target-not-found");
749
795
  }
750
- await this.updateImageInConfig(targetId, { url });
796
+ if (this.isToolActive) {
797
+ const current = this.workingItems.find((item) => item.id === targetId) ||
798
+ this.items.find((item) => item.id === targetId);
799
+ this.purgeSourceSizeCacheForItem(current);
800
+ this.updateImageInWorking(targetId, {
801
+ url,
802
+ sourceUrl: url,
803
+ committedUrl: undefined,
804
+ });
805
+ }
806
+ else {
807
+ await this.updateImageInConfig(targetId, { url });
808
+ }
809
+ const loaded = await this.waitImageLoaded(targetId, true);
810
+ if (loaded && options.operation) {
811
+ await this.applyImageOperation(targetId, options.operation, {
812
+ target: this.isToolActive ? "working" : "config",
813
+ });
814
+ }
815
+ if (loaded) {
816
+ this.setImageFocus(targetId);
817
+ }
751
818
  return { id: targetId, mode: "replace" };
752
819
  }
753
- const id = await this.addImageEntry(url, options.addOptions, fitOnAdd);
820
+ const id = await this.addImageEntry(url, options.addOptions, options.operation);
754
821
  return { id, mode: "add" };
755
822
  }
756
- addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd) {
757
- if (!sessionDirtyBeforeAdd || !this.isToolActive)
758
- return;
759
- if (this.workingItems.some((existing) => existing.id === item.id))
760
- return;
761
- this.workingItems = this.cloneItems([...this.workingItems, item]);
762
- this.updateImages();
763
- this.emitWorkingChange(item.id);
764
- }
765
823
  async updateImage(id, updates, options = {}) {
766
824
  this.syncToolActiveFromWorkbench();
767
825
  const target = options.target || "auto";
@@ -816,40 +874,6 @@ class ImageTool {
816
874
  }
817
875
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
818
876
  }
819
- toLayoutSceneRect(rect) {
820
- return (0, frame_1.toLayoutSceneRect)(rect);
821
- }
822
- async resolveDefaultFitArea() {
823
- if (!this.canvasService)
824
- return null;
825
- const frame = this.getFrameRect();
826
- if (frame.width <= 0 || frame.height <= 0)
827
- return null;
828
- return {
829
- width: Math.max(1, frame.width),
830
- height: Math.max(1, frame.height),
831
- left: frame.left + frame.width / 2,
832
- top: frame.top + frame.height / 2,
833
- };
834
- }
835
- async fitImageToDefaultArea(id) {
836
- if (!this.canvasService)
837
- return;
838
- const area = await this.resolveDefaultFitArea();
839
- if (area) {
840
- await this.fitImageToArea(id, area);
841
- return;
842
- }
843
- const viewport = this.canvasService.getSceneViewportRect();
844
- const canvasW = Math.max(1, viewport.width || 0);
845
- const canvasH = Math.max(1, viewport.height || 0);
846
- await this.fitImageToArea(id, {
847
- width: canvasW,
848
- height: canvasH,
849
- left: viewport.left + canvasW / 2,
850
- top: viewport.top + canvasH / 2,
851
- });
852
- }
853
877
  getImageObjects() {
854
878
  if (!this.canvasService)
855
879
  return [];
@@ -941,78 +965,33 @@ class ImageTool {
941
965
  "#f5f5f5",
942
966
  };
943
967
  }
944
- toSceneGeometryLike(raw) {
945
- const shape = raw?.shape;
946
- if (!(0, dielineShape_1.isDielineShape)(shape)) {
968
+ resolveSessionOverlayState() {
969
+ if (!this.canvasService || !this.context) {
947
970
  return null;
948
971
  }
949
- const radiusRaw = Number(raw?.radius);
950
- const offsetRaw = Number(raw?.offset);
951
- const unit = typeof raw?.unit === "string" ? raw.unit : "px";
952
- const radius = unit === "scene" || !this.canvasService
953
- ? radiusRaw
954
- : this.canvasService.toSceneLength(radiusRaw);
955
- const offset = unit === "scene" || !this.canvasService
956
- ? offsetRaw
957
- : this.canvasService.toSceneLength(offsetRaw);
958
- return {
959
- shape,
960
- shapeStyle: (0, dielineShape_1.normalizeShapeStyle)(raw?.shapeStyle),
961
- radius: Number.isFinite(radius) ? radius : 0,
962
- offset: Number.isFinite(offset) ? offset : 0,
963
- };
964
- }
965
- async resolveSceneGeometryForOverlay() {
966
- if (!this.context)
967
- return null;
968
- const commandService = this.context.services.get("CommandService");
969
- if (commandService) {
970
- try {
971
- const raw = await Promise.resolve(commandService.executeCommand("getSceneGeometry"));
972
- const geometry = this.toSceneGeometryLike(raw);
973
- if (geometry) {
974
- this.debug("overlay:sceneGeometry:command", geometry);
975
- return geometry;
976
- }
977
- this.debug("overlay:sceneGeometry:command:invalid", { raw });
978
- }
979
- catch (error) {
980
- this.debug("overlay:sceneGeometry:command:error", {
981
- error: error instanceof Error ? error.message : String(error),
982
- });
983
- }
984
- }
985
- if (!this.canvasService)
986
- return null;
987
972
  const configService = this.context.services.get("ConfigurationService");
988
- if (!configService)
973
+ if (!configService) {
989
974
  return null;
990
- const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
991
- const layout = (0, sceneLayoutModel_1.computeSceneLayout)(this.canvasService, sizeState);
975
+ }
976
+ const layout = (0, sceneLayoutModel_1.computeSceneLayout)(this.canvasService, (0, sceneLayoutModel_1.readSizeState)(configService));
992
977
  if (!layout) {
993
- this.debug("overlay:sceneGeometry:fallback:missing-layout");
978
+ this.debug("overlay:layout:missing");
994
979
  return null;
995
980
  }
996
- const geometry = this.toSceneGeometryLike((0, sceneLayoutModel_1.buildSceneGeometry)(configService, layout));
997
- if (geometry) {
998
- this.debug("overlay:sceneGeometry:fallback", geometry);
999
- }
1000
- return geometry;
1001
- }
1002
- resolveCutShapeRadius(geometry, frame) {
1003
- const visualRadius = Number.isFinite(geometry.radius)
1004
- ? Math.max(0, geometry.radius)
1005
- : 0;
1006
- const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
1007
- const rawCutRadius = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
1008
- const maxRadius = Math.max(0, Math.min(frame.width, frame.height) / 2);
1009
- return Math.max(0, Math.min(maxRadius, rawCutRadius));
981
+ const geometry = (0, sceneLayoutModel_1.buildSceneGeometry)(configService, layout);
982
+ this.debug("overlay:state:resolved", {
983
+ cutRect: layout.cutRect,
984
+ shape: geometry.shape,
985
+ shapeStyle: geometry.shapeStyle,
986
+ radius: geometry.radius,
987
+ offset: geometry.offset,
988
+ });
989
+ return { layout, geometry };
1010
990
  }
1011
991
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
1012
992
  if (typeof document === "undefined")
1013
993
  return undefined;
1014
- const sceneScale = this.canvasService?.getSceneScale() || 1;
1015
- const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
994
+ const cacheKey = color;
1016
995
  if (this.cropShapeHatchPattern &&
1017
996
  this.cropShapeHatchPatternColor === color &&
1018
997
  this.cropShapeHatchPatternKey === cacheKey) {
@@ -1045,140 +1024,11 @@ class ImageTool {
1045
1024
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1046
1025
  repetition: "repeat",
1047
1026
  });
1048
- // Scene specs are scaled to screen by CanvasService; keep hatch density in screen pixels.
1049
- pattern.patternTransform = [
1050
- 1 / sceneScale,
1051
- 0,
1052
- 0,
1053
- 1 / sceneScale,
1054
- 0,
1055
- 0,
1056
- ];
1057
1027
  this.cropShapeHatchPattern = pattern;
1058
1028
  this.cropShapeHatchPatternColor = color;
1059
1029
  this.cropShapeHatchPatternKey = cacheKey;
1060
1030
  return pattern;
1061
1031
  }
1062
- buildCropShapeOverlaySpecs(frame, sceneGeometry) {
1063
- if (!sceneGeometry) {
1064
- this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
1065
- return [];
1066
- }
1067
- if (sceneGeometry.shape === "custom") {
1068
- this.debug("overlay:shape:skip", { reason: "shape-custom" });
1069
- return [];
1070
- }
1071
- const shape = sceneGeometry.shape;
1072
- const shapeStyle = sceneGeometry.shapeStyle;
1073
- const inset = 0;
1074
- const shapeWidth = Math.max(1, frame.width);
1075
- const shapeHeight = Math.max(1, frame.height);
1076
- const radius = this.resolveCutShapeRadius(sceneGeometry, frame);
1077
- this.debug("overlay:shape:geometry", {
1078
- shape,
1079
- frameWidth: frame.width,
1080
- frameHeight: frame.height,
1081
- offset: sceneGeometry.offset,
1082
- shapeStyle,
1083
- inset,
1084
- shapeWidth,
1085
- shapeHeight,
1086
- baseRadius: sceneGeometry.radius,
1087
- radius,
1088
- });
1089
- const isSameAsFrame = Math.abs(shapeWidth - frame.width) <= 0.0001 &&
1090
- Math.abs(shapeHeight - frame.height) <= 0.0001;
1091
- if (shape === "rect" && radius <= 0.0001 && isSameAsFrame) {
1092
- this.debug("overlay:shape:skip", {
1093
- reason: "shape-rect-no-radius",
1094
- });
1095
- return [];
1096
- }
1097
- const baseOptions = {
1098
- shape,
1099
- width: shapeWidth,
1100
- height: shapeHeight,
1101
- radius,
1102
- x: frame.width / 2,
1103
- y: frame.height / 2,
1104
- features: [],
1105
- shapeStyle,
1106
- canvasWidth: frame.width,
1107
- canvasHeight: frame.height,
1108
- };
1109
- try {
1110
- const shapePathData = (0, geometry_1.generateDielinePath)(baseOptions);
1111
- const outerRectPathData = `M 0 0 L ${frame.width} 0 L ${frame.width} ${frame.height} L 0 ${frame.height} Z`;
1112
- const hatchPathData = `${outerRectPathData} ${shapePathData}`;
1113
- if (!shapePathData || !hatchPathData) {
1114
- this.debug("overlay:shape:skip", {
1115
- reason: "path-generation-empty",
1116
- shape,
1117
- radius,
1118
- });
1119
- return [];
1120
- }
1121
- const patternFill = this.getCropShapeHatchPattern();
1122
- const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
1123
- const shapeBounds = (0, geometry_1.getPathBounds)(shapePathData);
1124
- const hatchBounds = (0, geometry_1.getPathBounds)(hatchPathData);
1125
- const frameRect = this.toLayoutSceneRect(frame);
1126
- const hatchPathLength = hatchPathData.length;
1127
- const shapePathLength = shapePathData.length;
1128
- const specs = [
1129
- {
1130
- id: "image.cropShapeHatch",
1131
- type: "path",
1132
- data: { id: "image.cropShapeHatch", zIndex: 5 },
1133
- layout: {
1134
- reference: "custom",
1135
- referenceRect: frameRect,
1136
- alignX: "start",
1137
- alignY: "start",
1138
- offsetX: hatchBounds.x,
1139
- offsetY: hatchBounds.y,
1140
- },
1141
- props: {
1142
- pathData: hatchPathData,
1143
- originX: "left",
1144
- originY: "top",
1145
- fill: hatchFill,
1146
- opacity: patternFill ? 1 : 0.8,
1147
- stroke: "rgba(255, 0, 0, 0.9)",
1148
- strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
1149
- fillRule: "evenodd",
1150
- selectable: false,
1151
- evented: false,
1152
- excludeFromExport: true,
1153
- objectCaching: false,
1154
- },
1155
- },
1156
- ];
1157
- this.debug("overlay:shape:built", {
1158
- shape,
1159
- radius,
1160
- inset,
1161
- shapeWidth,
1162
- shapeHeight,
1163
- fillRule: "evenodd",
1164
- shapePathLength,
1165
- hatchPathLength,
1166
- shapeBounds,
1167
- hatchBounds,
1168
- hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
1169
- ids: specs.map((spec) => spec.id),
1170
- });
1171
- return specs;
1172
- }
1173
- catch (error) {
1174
- this.debug("overlay:shape:error", {
1175
- shape,
1176
- radius,
1177
- error: error instanceof Error ? error.message : String(error),
1178
- });
1179
- return [];
1180
- }
1181
- }
1182
1032
  resolveRenderImageState(item) {
1183
1033
  const active = this.isToolActive;
1184
1034
  const sourceUrl = item.sourceUrl || item.url;
@@ -1262,167 +1112,35 @@ class ImageTool {
1262
1112
  }
1263
1113
  return specs;
1264
1114
  }
1265
- buildOverlaySpecs(frame, sceneGeometry) {
1115
+ buildOverlaySpecs(overlayState) {
1266
1116
  const visible = this.isImageEditingVisible();
1267
- if (!visible ||
1268
- frame.width <= 0 ||
1269
- frame.height <= 0 ||
1270
- !this.canvasService) {
1117
+ if (!visible || !overlayState || !this.canvasService) {
1271
1118
  this.debug("overlay:hidden", {
1272
1119
  visible,
1273
- frame,
1120
+ cutRect: overlayState?.layout.cutRect,
1274
1121
  isToolActive: this.isToolActive,
1275
1122
  isImageSelectionActive: this.isImageSelectionActive,
1276
1123
  focusedImageId: this.focusedImageId,
1277
1124
  });
1278
1125
  return [];
1279
1126
  }
1280
- const viewport = this.canvasService.getSceneViewportRect();
1281
- const canvasW = viewport.width || 0;
1282
- const canvasH = viewport.height || 0;
1283
- const canvasLeft = viewport.left || 0;
1284
- const canvasTop = viewport.top || 0;
1127
+ const viewport = this.canvasService.getScreenViewportRect();
1285
1128
  const visual = this.getFrameVisualConfig();
1286
- const strokeWidthScene = this.canvasService.toSceneLength(visual.strokeWidth);
1287
- const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
1288
- const frameLeft = Math.max(canvasLeft, Math.min(canvasLeft + canvasW, frame.left));
1289
- const frameTop = Math.max(canvasTop, Math.min(canvasTop + canvasH, frame.top));
1290
- const frameRight = Math.max(frameLeft, Math.min(canvasLeft + canvasW, frame.left + frame.width));
1291
- const frameBottom = Math.max(frameTop, Math.min(canvasTop + canvasH, frame.top + frame.height));
1292
- const visibleFrameH = Math.max(0, frameBottom - frameTop);
1293
- const topH = Math.max(0, frameTop - canvasTop);
1294
- const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
1295
- const leftW = Math.max(0, frameLeft - canvasLeft);
1296
- const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
1297
- const viewportRect = this.toLayoutSceneRect({
1298
- left: canvasLeft,
1299
- top: canvasTop,
1300
- width: canvasW,
1301
- height: canvasH,
1302
- });
1303
- const visibleFrameBandRect = this.toLayoutSceneRect({
1304
- left: canvasLeft,
1305
- top: frameTop,
1306
- width: canvasW,
1307
- height: visibleFrameH,
1308
- });
1309
- const frameRect = this.toLayoutSceneRect(frame);
1310
- const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
1311
- const mask = [
1312
- {
1313
- id: "image.cropMask.top",
1314
- type: "rect",
1315
- data: { id: "image.cropMask.top", zIndex: 1 },
1316
- layout: {
1317
- reference: "custom",
1318
- referenceRect: viewportRect,
1319
- alignX: "start",
1320
- alignY: "start",
1321
- width: "100%",
1322
- height: topH,
1323
- },
1324
- props: {
1325
- originX: "left",
1326
- originY: "top",
1327
- fill: visual.outerBackground,
1328
- selectable: false,
1329
- evented: false,
1330
- },
1129
+ const specs = (0, sessionOverlay_1.buildImageSessionOverlaySpecs)({
1130
+ viewport: {
1131
+ left: viewport.left,
1132
+ top: viewport.top,
1133
+ width: viewport.width,
1134
+ height: viewport.height,
1331
1135
  },
1332
- {
1333
- id: "image.cropMask.bottom",
1334
- type: "rect",
1335
- data: { id: "image.cropMask.bottom", zIndex: 2 },
1336
- layout: {
1337
- reference: "custom",
1338
- referenceRect: viewportRect,
1339
- alignX: "start",
1340
- alignY: "end",
1341
- width: "100%",
1342
- height: bottomH,
1343
- },
1344
- props: {
1345
- originX: "left",
1346
- originY: "top",
1347
- fill: visual.outerBackground,
1348
- selectable: false,
1349
- evented: false,
1350
- },
1351
- },
1352
- {
1353
- id: "image.cropMask.left",
1354
- type: "rect",
1355
- data: { id: "image.cropMask.left", zIndex: 3 },
1356
- layout: {
1357
- reference: "custom",
1358
- referenceRect: visibleFrameBandRect,
1359
- alignX: "start",
1360
- alignY: "start",
1361
- width: leftW,
1362
- height: "100%",
1363
- },
1364
- props: {
1365
- originX: "left",
1366
- originY: "top",
1367
- fill: visual.outerBackground,
1368
- selectable: false,
1369
- evented: false,
1370
- },
1371
- },
1372
- {
1373
- id: "image.cropMask.right",
1374
- type: "rect",
1375
- data: { id: "image.cropMask.right", zIndex: 4 },
1376
- layout: {
1377
- reference: "custom",
1378
- referenceRect: visibleFrameBandRect,
1379
- alignX: "end",
1380
- alignY: "start",
1381
- width: rightW,
1382
- height: "100%",
1383
- },
1384
- props: {
1385
- originX: "left",
1386
- originY: "top",
1387
- fill: visual.outerBackground,
1388
- selectable: false,
1389
- evented: false,
1390
- },
1391
- },
1392
- ];
1393
- const frameSpec = {
1394
- id: "image.cropFrame",
1395
- type: "rect",
1396
- data: { id: "image.cropFrame", zIndex: 7 },
1397
- layout: {
1398
- reference: "custom",
1399
- referenceRect: frameRect,
1400
- alignX: "start",
1401
- alignY: "start",
1402
- width: "100%",
1403
- height: "100%",
1404
- },
1405
- props: {
1406
- originX: "left",
1407
- originY: "top",
1408
- fill: visual.innerBackground,
1409
- stroke: visual.strokeStyle === "hidden"
1410
- ? "rgba(0,0,0,0)"
1411
- : visual.strokeColor,
1412
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
1413
- strokeDashArray: visual.strokeStyle === "dashed"
1414
- ? [dashLengthScene, dashLengthScene]
1415
- : undefined,
1416
- selectable: false,
1417
- evented: false,
1418
- },
1419
- };
1420
- const specs = shapeOverlay.length > 0
1421
- ? [...mask, ...shapeOverlay]
1422
- : [...mask, ...shapeOverlay, frameSpec];
1136
+ layout: overlayState.layout,
1137
+ geometry: overlayState.geometry,
1138
+ visual,
1139
+ hatchPattern: this.getCropShapeHatchPattern(),
1140
+ });
1423
1141
  this.debug("overlay:built", {
1424
- frame,
1425
- shape: sceneGeometry?.shape,
1142
+ cutRect: overlayState.layout.cutRect,
1143
+ shape: overlayState.geometry.shape,
1426
1144
  overlayIds: specs.map((spec) => ({
1427
1145
  id: spec.id,
1428
1146
  zIndex: spec.data?.zIndex,
@@ -1450,11 +1168,9 @@ class ImageTool {
1450
1168
  const imageSpecs = await this.buildImageSpecs(renderItems, frame);
1451
1169
  if (seq !== this.renderSeq)
1452
1170
  return;
1453
- const sceneGeometry = await this.resolveSceneGeometryForOverlay();
1454
- if (seq !== this.renderSeq)
1455
- return;
1171
+ const overlayState = this.resolveSessionOverlayState();
1456
1172
  this.imageSpecs = imageSpecs;
1457
- this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1173
+ this.overlaySpecs = this.buildOverlaySpecs(overlayState);
1458
1174
  await this.canvasService.flushRenderFromProducers();
1459
1175
  if (seq !== this.renderSeq)
1460
1176
  return;
@@ -1484,11 +1200,39 @@ class ImageTool {
1484
1200
  isImageSelectionActive: this.isImageSelectionActive,
1485
1201
  focusedImageId: this.focusedImageId,
1486
1202
  });
1203
+ this.emitImageStateChange();
1487
1204
  this.canvasService.requestRenderAll();
1488
1205
  }
1489
1206
  clampNormalized(value) {
1490
1207
  return Math.max(-1, Math.min(2, value));
1491
1208
  }
1209
+ async setImageTransform(id, updates, options = {}) {
1210
+ const next = {};
1211
+ if (Number.isFinite(updates.scale)) {
1212
+ next.scale = Math.max(0.05, Number(updates.scale));
1213
+ }
1214
+ if (Number.isFinite(updates.angle)) {
1215
+ next.angle = Number(updates.angle);
1216
+ }
1217
+ if (Number.isFinite(updates.left)) {
1218
+ next.left = this.clampNormalized(Number(updates.left));
1219
+ }
1220
+ if (Number.isFinite(updates.top)) {
1221
+ next.top = this.clampNormalized(Number(updates.top));
1222
+ }
1223
+ if (Number.isFinite(updates.opacity)) {
1224
+ next.opacity = Math.max(0, Math.min(1, Number(updates.opacity)));
1225
+ }
1226
+ if (!Object.keys(next).length)
1227
+ return;
1228
+ await this.updateImage(id, next, options);
1229
+ }
1230
+ resetImageSession() {
1231
+ this.workingItems = this.cloneItems(this.items);
1232
+ this.hasWorkingChanges = false;
1233
+ this.updateImages();
1234
+ this.emitWorkingChange();
1235
+ }
1492
1236
  updateImageInWorking(id, updates) {
1493
1237
  const index = this.workingItems.findIndex((item) => item.id === id);
1494
1238
  if (index < 0)
@@ -1522,23 +1266,12 @@ class ImageTool {
1522
1266
  url: replacingUrl,
1523
1267
  sourceUrl: replacingUrl,
1524
1268
  committedUrl: undefined,
1525
- scale: updates.scale ?? 1,
1526
- angle: updates.angle ?? 0,
1527
- left: updates.left ?? 0.5,
1528
- top: updates.top ?? 0.5,
1529
1269
  }
1530
1270
  : {}),
1531
1271
  });
1532
1272
  this.updateConfig(next);
1533
1273
  if (replacingSource) {
1534
- this.debug("replace:image:begin", { id, replacingUrl });
1535
1274
  this.purgeSourceSizeCacheForItem(base);
1536
- const loaded = await this.waitImageLoaded(id, true);
1537
- this.debug("replace:image:loaded", { id, loaded });
1538
- if (loaded) {
1539
- await this.refitImageToFrame(id);
1540
- this.setImageFocus(id);
1541
- }
1542
1275
  }
1543
1276
  }
1544
1277
  waitImageLoaded(id, forceWait = false) {
@@ -1556,73 +1289,53 @@ class ImageTool {
1556
1289
  });
1557
1290
  });
1558
1291
  }
1559
- async refitImageToFrame(id) {
1292
+ async resolveImageSourceSize(id, src) {
1560
1293
  const obj = this.getImageObject(id);
1561
- if (!obj || !this.canvasService)
1562
- return;
1563
- const current = this.items.find((item) => item.id === id);
1564
- if (!current)
1565
- return;
1566
- const render = this.resolveRenderImageState(current);
1567
- this.rememberSourceSize(render.src, obj);
1568
- const source = this.getSourceSize(render.src, obj);
1569
- const frame = this.getFrameRect();
1570
- const coverScale = this.getCoverScale(frame, source);
1571
- const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
1572
- const zoom = Math.max(0.05, currentScale / coverScale);
1573
- const updated = {
1574
- scale: Number.isFinite(zoom) ? zoom : 1,
1575
- angle: 0,
1576
- left: 0.5,
1577
- top: 0.5,
1578
- };
1579
- const index = this.items.findIndex((item) => item.id === id);
1580
- if (index < 0)
1581
- return;
1582
- const next = [...this.items];
1583
- next[index] = this.normalizeItem({ ...next[index], ...updated });
1584
- this.updateConfig(next);
1585
- this.workingItems = this.cloneItems(next);
1586
- this.hasWorkingChanges = false;
1587
- this.updateImages();
1588
- this.emitWorkingChange(id);
1294
+ if (obj) {
1295
+ this.rememberSourceSize(src, obj);
1296
+ }
1297
+ const ensured = await this.ensureSourceSize(src);
1298
+ if (ensured)
1299
+ return ensured;
1300
+ if (!obj)
1301
+ return null;
1302
+ const width = Number(obj?.width || 0);
1303
+ const height = Number(obj?.height || 0);
1304
+ if (width <= 0 || height <= 0)
1305
+ return null;
1306
+ return { width, height };
1589
1307
  }
1590
- async fitImageToArea(id, area) {
1308
+ async applyImageOperation(id, operation, options = {}) {
1591
1309
  if (!this.canvasService)
1592
1310
  return;
1593
- const loaded = await this.waitImageLoaded(id, false);
1594
- if (!loaded)
1595
- return;
1596
- const obj = this.getImageObject(id);
1597
- if (!obj)
1598
- return;
1599
- const renderItems = this.isToolActive ? this.workingItems : this.items;
1311
+ this.syncToolActiveFromWorkbench();
1312
+ const target = options.target || "auto";
1313
+ const renderItems = target === "working" || (target === "auto" && this.isToolActive)
1314
+ ? this.workingItems
1315
+ : this.items;
1600
1316
  const current = renderItems.find((item) => item.id === id);
1601
1317
  if (!current)
1602
1318
  return;
1603
1319
  const render = this.resolveRenderImageState(current);
1604
- this.rememberSourceSize(render.src, obj);
1605
- const source = this.getSourceSize(render.src, obj);
1320
+ const source = await this.resolveImageSourceSize(id, render.src);
1321
+ if (!source)
1322
+ return;
1606
1323
  const frame = this.getFrameRect();
1607
- const baseCover = this.getCoverScale(frame, source);
1608
- const desiredScale = Math.max(Math.max(1, area.width) / Math.max(1, source.width), Math.max(1, area.height) / Math.max(1, source.height));
1609
1324
  const viewport = this.canvasService.getSceneViewportRect();
1610
- const canvasW = viewport.width || 1;
1611
- const canvasH = viewport.height || 1;
1612
- const areaLeftInput = area.left ?? 0.5;
1613
- const areaTopInput = area.top ?? 0.5;
1614
- const areaLeftPx = areaLeftInput <= 1.5
1615
- ? viewport.left + areaLeftInput * canvasW
1616
- : areaLeftInput;
1617
- const areaTopPx = areaTopInput <= 1.5
1618
- ? viewport.top + areaTopInput * canvasH
1619
- : areaTopInput;
1620
- const updates = {
1621
- scale: Math.max(0.05, desiredScale / baseCover),
1622
- left: this.clampNormalized((areaLeftPx - frame.left) / Math.max(1, frame.width)),
1623
- top: this.clampNormalized((areaTopPx - frame.top) / Math.max(1, frame.height)),
1624
- };
1625
- if (this.isToolActive) {
1325
+ const area = operation.type === "resetTransform"
1326
+ ? (0, imageOperations_1.resolveImageOperationArea)({ frame, viewport })
1327
+ : (0, imageOperations_1.resolveImageOperationArea)({
1328
+ frame,
1329
+ viewport,
1330
+ area: operation.area,
1331
+ });
1332
+ const updates = (0, imageOperations_1.computeImageOperationUpdates)({
1333
+ frame,
1334
+ source,
1335
+ operation,
1336
+ area,
1337
+ });
1338
+ if (target === "working" || (target === "auto" && this.isToolActive)) {
1626
1339
  this.updateImageInWorking(id, updates);
1627
1340
  return;
1628
1341
  }