@pooder/kit 5.0.4 → 5.2.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 (34) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.d.mts +248 -267
  3. package/dist/index.d.ts +248 -267
  4. package/dist/index.js +6729 -5797
  5. package/dist/index.mjs +6690 -5741
  6. package/package.json +2 -2
  7. package/src/{background.ts → extensions/background.ts} +1 -1
  8. package/src/{dieline.ts → extensions/dieline.ts} +39 -17
  9. package/src/{feature.ts → extensions/feature.ts} +80 -67
  10. package/src/{film.ts → extensions/film.ts} +1 -1
  11. package/src/{geometry.ts → extensions/geometry.ts} +151 -105
  12. package/src/{image.ts → extensions/image.ts} +436 -93
  13. package/src/extensions/index.ts +11 -0
  14. package/src/{maskOps.ts → extensions/maskOps.ts} +28 -10
  15. package/src/{mirror.ts → extensions/mirror.ts} +1 -1
  16. package/src/{ruler.ts → extensions/ruler.ts} +5 -3
  17. package/src/extensions/sceneLayout.ts +140 -0
  18. package/src/{sceneLayoutModel.ts → extensions/sceneLayoutModel.ts} +17 -10
  19. package/src/extensions/sceneVisibility.ts +71 -0
  20. package/src/{size.ts → extensions/size.ts} +23 -13
  21. package/src/{tracer.ts → extensions/tracer.ts} +374 -45
  22. package/src/{white-ink.ts → extensions/white-ink.ts} +620 -236
  23. package/src/index.ts +2 -14
  24. package/src/{ViewportSystem.ts → services/ViewportSystem.ts} +5 -2
  25. package/src/services/index.ts +3 -0
  26. package/src/sceneLayout.ts +0 -121
  27. package/src/sceneVisibility.ts +0 -49
  28. /package/src/{bridgeSelection.ts → extensions/bridgeSelection.ts} +0 -0
  29. /package/src/{constraints.ts → extensions/constraints.ts} +0 -0
  30. /package/src/{edgeScale.ts → extensions/edgeScale.ts} +0 -0
  31. /package/src/{featureComplete.ts → extensions/featureComplete.ts} +0 -0
  32. /package/src/{wrappedOffsets.ts → extensions/wrappedOffsets.ts} +0 -0
  33. /package/src/{CanvasService.ts → services/CanvasService.ts} +0 -0
  34. /package/src/{renderSpec.ts → services/renderSpec.ts} +0 -0
@@ -8,10 +8,19 @@ import {
8
8
  ToolSessionService,
9
9
  WorkbenchService,
10
10
  } from "@pooder/core";
11
- import { Canvas as FabricCanvas, Image as FabricImage, Point } from "fabric";
12
- import CanvasService from "./CanvasService";
13
- import type { RenderObjectSpec } from "./renderSpec";
14
- import { computeSceneLayout, readSizeState } from "./sceneLayoutModel";
11
+ import {
12
+ Canvas as FabricCanvas,
13
+ Image as FabricImage,
14
+ Pattern,
15
+ Point,
16
+ } from "fabric";
17
+ import { CanvasService, RenderObjectSpec } from "../services";
18
+ import { generateDielinePath } from "./geometry";
19
+ import {
20
+ buildSceneGeometry,
21
+ computeSceneLayout,
22
+ readSizeState,
23
+ } from "./sceneLayoutModel";
15
24
 
16
25
  export interface ImageItem {
17
26
  id: string;
@@ -55,6 +64,15 @@ interface FrameVisualConfig {
55
64
  outerBackground: string;
56
65
  }
57
66
 
67
+ type DielineShape = "rect" | "circle" | "ellipse" | "custom";
68
+ type ShapeOverlayShape = Exclude<DielineShape, "custom">;
69
+
70
+ interface SceneGeometryLike {
71
+ shape: DielineShape;
72
+ radius: number;
73
+ offset: number;
74
+ }
75
+
58
76
  interface UpsertImageOptions {
59
77
  id?: string;
60
78
  mode?: "replace" | "add";
@@ -73,43 +91,26 @@ interface UpdateImageOptions {
73
91
  target?: "auto" | "config" | "working";
74
92
  }
75
93
 
76
- interface DetectBounds {
77
- x: number;
78
- y: number;
79
- width: number;
80
- height: number;
81
- }
82
-
83
- interface DetectEdgeResult {
84
- pathData: string;
85
- rawBounds?: DetectBounds;
86
- baseBounds?: DetectBounds;
87
- imageWidth?: number;
88
- imageHeight?: number;
94
+ interface ExportCroppedImageOptions {
95
+ multiplier?: number;
96
+ format?: "png" | "jpeg";
89
97
  }
90
98
 
91
- interface ImageRenderSnapshot {
92
- id: string;
93
- centerX: number;
94
- centerY: number;
95
- objectScale: number;
96
- sourceWidth: number;
97
- sourceHeight: number;
99
+ interface ExportUserCroppedImageOptions extends ExportCroppedImageOptions {
100
+ imageIds?: string[];
98
101
  }
99
102
 
100
- interface DetectFromFrameOptions {
101
- expand?: number;
102
- smoothing?: boolean;
103
- simplifyTolerance?: number;
104
- multiplier?: number;
105
- debug?: boolean;
103
+ interface ExportUserCroppedImageResult {
104
+ url: string;
105
+ width: number;
106
+ height: number;
107
+ multiplier: number;
108
+ format: "png" | "jpeg";
109
+ imageIds: string[];
106
110
  }
107
111
 
108
112
  const IMAGE_OBJECT_LAYER_ID = "image.user";
109
113
  const IMAGE_OVERLAY_LAYER_ID = "image-overlay";
110
- const IMAGE_DETECT_EXPAND_DEFAULT = 30;
111
- const IMAGE_DETECT_SIMPLIFY_TOLERANCE_DEFAULT = 2;
112
- const IMAGE_DETECT_MULTIPLIER_DEFAULT = 2;
113
114
 
114
115
  export class ImageTool implements Extension {
115
116
  id = "pooder.kit.image";
@@ -131,6 +132,8 @@ export class ImageTool implements Extension {
131
132
  private focusedImageId: string | null = null;
132
133
  private renderSeq = 0;
133
134
  private dirtyTrackerDisposable?: { dispose(): void };
135
+ private cropShapeHatchPattern?: Pattern;
136
+ private cropShapeHatchPatternColor?: string;
134
137
 
135
138
  activate(context: ExtensionContext) {
136
139
  this.context = context;
@@ -146,6 +149,7 @@ export class ImageTool implements Extension {
146
149
  context.eventBus.on("selection:updated", this.onSelectionChanged);
147
150
  context.eventBus.on("selection:cleared", this.onSelectionCleared);
148
151
  context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
152
+ context.eventBus.on("scene:geometry:change", this.onSceneGeometryChanged);
149
153
 
150
154
  const configService = context.services.get<ConfigurationService>(
151
155
  "ConfigurationService",
@@ -193,8 +197,11 @@ export class ImageTool implements Extension {
193
197
  context.eventBus.off("selection:updated", this.onSelectionChanged);
194
198
  context.eventBus.off("selection:cleared", this.onSelectionCleared);
195
199
  context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
200
+ context.eventBus.off("scene:geometry:change", this.onSceneGeometryChanged);
196
201
  this.dirtyTrackerDisposable?.dispose();
197
202
  this.dirtyTrackerDisposable = undefined;
203
+ this.cropShapeHatchPattern = undefined;
204
+ this.cropShapeHatchPatternColor = undefined;
198
205
 
199
206
  this.clearRenderedImages();
200
207
  if (this.canvasService) {
@@ -276,6 +283,10 @@ export class ImageTool implements Extension {
276
283
  this.updateImages();
277
284
  };
278
285
 
286
+ private onSceneGeometryChanged = () => {
287
+ this.updateImages();
288
+ };
289
+
279
290
  private syncToolActiveFromWorkbench(fallbackId?: string | null) {
280
291
  const wb = this.context?.services.get<WorkbenchService>("WorkbenchService");
281
292
  const activeId = wb?.activeToolId;
@@ -431,12 +442,10 @@ export class ImageTool implements Extension {
431
442
  },
432
443
  },
433
444
  {
434
- command: "exportImageFrameUrl",
435
- title: "Export Image Frame Url",
436
- handler: async (
437
- options: { multiplier?: number; format?: "png" | "jpeg" } = {},
438
- ) => {
439
- return await this.exportImageFrameUrl(options);
445
+ command: "exportUserCroppedImage",
446
+ title: "Export User Cropped Image",
447
+ handler: async (options: ExportUserCroppedImageOptions = {}) => {
448
+ return await this.exportUserCroppedImage(options);
440
449
  },
441
450
  },
442
451
  {
@@ -912,10 +921,268 @@ export class ImageTool implements Extension {
912
921
  "image.frame.innerBackground",
913
922
  "rgba(0,0,0,0)",
914
923
  ) || "rgba(0,0,0,0)",
915
- outerBackground: "#f5f5f5",
924
+ outerBackground:
925
+ this.getConfig<string>("image.frame.outerBackground", "#f5f5f5") ||
926
+ "#f5f5f5",
927
+ };
928
+ }
929
+
930
+ private toSceneGeometryLike(raw: any): SceneGeometryLike | null {
931
+ const shape = raw?.shape;
932
+ if (
933
+ shape !== "rect" &&
934
+ shape !== "circle" &&
935
+ shape !== "ellipse" &&
936
+ shape !== "custom"
937
+ ) {
938
+ return null;
939
+ }
940
+
941
+ const radius = Number(raw?.radius);
942
+ const offset = Number(raw?.offset);
943
+ return {
944
+ shape,
945
+ radius: Number.isFinite(radius) ? radius : 0,
946
+ offset: Number.isFinite(offset) ? offset : 0,
916
947
  };
917
948
  }
918
949
 
950
+ private async resolveSceneGeometryForOverlay(): Promise<SceneGeometryLike | null> {
951
+ if (!this.context) return null;
952
+ const commandService = this.context.services.get<any>("CommandService");
953
+ if (commandService) {
954
+ try {
955
+ const raw = await Promise.resolve(
956
+ commandService.executeCommand("getSceneGeometry"),
957
+ );
958
+ const geometry = this.toSceneGeometryLike(raw);
959
+ if (geometry) {
960
+ this.debug("overlay:sceneGeometry:command", geometry);
961
+ return geometry;
962
+ }
963
+ this.debug("overlay:sceneGeometry:command:invalid", { raw });
964
+ } catch (error) {
965
+ this.debug("overlay:sceneGeometry:command:error", {
966
+ error: error instanceof Error ? error.message : String(error),
967
+ });
968
+ }
969
+ }
970
+
971
+ if (!this.canvasService) return null;
972
+ const configService = this.context.services.get<ConfigurationService>(
973
+ "ConfigurationService",
974
+ );
975
+ if (!configService) return null;
976
+
977
+ const sizeState = readSizeState(configService);
978
+ const layout = computeSceneLayout(this.canvasService, sizeState);
979
+ if (!layout) {
980
+ this.debug("overlay:sceneGeometry:fallback:missing-layout");
981
+ return null;
982
+ }
983
+
984
+ const geometry = this.toSceneGeometryLike(
985
+ buildSceneGeometry(configService, layout),
986
+ );
987
+ if (geometry) {
988
+ this.debug("overlay:sceneGeometry:fallback", geometry);
989
+ }
990
+ return geometry;
991
+ }
992
+
993
+ private resolveCutShapeRadius(
994
+ geometry: SceneGeometryLike,
995
+ frame: FrameRect,
996
+ ): number {
997
+ const visualRadius = Number.isFinite(geometry.radius)
998
+ ? Math.max(0, geometry.radius)
999
+ : 0;
1000
+ const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
1001
+ const rawCutRadius =
1002
+ visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
1003
+ const maxRadius = Math.max(0, Math.min(frame.width, frame.height) / 2);
1004
+ return Math.max(0, Math.min(maxRadius, rawCutRadius));
1005
+ }
1006
+
1007
+ private getCropShapeHatchPattern(
1008
+ color = "rgba(255, 0, 0, 0.6)",
1009
+ ): Pattern | undefined {
1010
+ if (typeof document === "undefined") return undefined;
1011
+ if (
1012
+ this.cropShapeHatchPattern &&
1013
+ this.cropShapeHatchPatternColor === color
1014
+ ) {
1015
+ return this.cropShapeHatchPattern;
1016
+ }
1017
+
1018
+ const size = 16;
1019
+ const patternCanvas = document.createElement("canvas");
1020
+ patternCanvas.width = size;
1021
+ patternCanvas.height = size;
1022
+ const ctx = patternCanvas.getContext("2d");
1023
+ if (!ctx) return undefined;
1024
+
1025
+ ctx.clearRect(0, 0, size, size);
1026
+ ctx.fillStyle = "rgba(255, 0, 0, 0.08)";
1027
+ ctx.fillRect(0, 0, size, size);
1028
+ ctx.strokeStyle = color;
1029
+ ctx.lineWidth = 1.5;
1030
+ ctx.beginPath();
1031
+ ctx.moveTo(-size, size);
1032
+ ctx.lineTo(size, -size);
1033
+ ctx.moveTo(-size / 2, size + size / 2);
1034
+ ctx.lineTo(size + size / 2, -size / 2);
1035
+ ctx.moveTo(0, size);
1036
+ ctx.lineTo(size, 0);
1037
+ ctx.moveTo(size / 2, size + size / 2);
1038
+ ctx.lineTo(size + size + size / 2, -size / 2);
1039
+ ctx.stroke();
1040
+
1041
+ const pattern = new Pattern({
1042
+ source: patternCanvas,
1043
+ // @ts-ignore: Fabric Pattern accepts canvas source here.
1044
+ repetition: "repeat",
1045
+ });
1046
+ this.cropShapeHatchPattern = pattern;
1047
+ this.cropShapeHatchPatternColor = color;
1048
+ return pattern;
1049
+ }
1050
+
1051
+ private buildCropShapeOverlaySpecs(
1052
+ frame: FrameRect,
1053
+ sceneGeometry: SceneGeometryLike | null,
1054
+ ): RenderObjectSpec[] {
1055
+ if (!sceneGeometry) {
1056
+ this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
1057
+ return [];
1058
+ }
1059
+ if (sceneGeometry.shape === "custom") {
1060
+ this.debug("overlay:shape:skip", { reason: "shape-custom" });
1061
+ return [];
1062
+ }
1063
+
1064
+ const shape = sceneGeometry.shape as ShapeOverlayShape;
1065
+ const inset = 0;
1066
+ const shapeWidth = Math.max(1, frame.width);
1067
+ const shapeHeight = Math.max(1, frame.height);
1068
+ const radius = this.resolveCutShapeRadius(sceneGeometry, frame);
1069
+
1070
+ this.debug("overlay:shape:geometry", {
1071
+ shape,
1072
+ frameWidth: frame.width,
1073
+ frameHeight: frame.height,
1074
+ offset: sceneGeometry.offset,
1075
+ inset,
1076
+ shapeWidth,
1077
+ shapeHeight,
1078
+ baseRadius: sceneGeometry.radius,
1079
+ radius,
1080
+ });
1081
+
1082
+ const isSameAsFrame =
1083
+ Math.abs(shapeWidth - frame.width) <= 0.0001 &&
1084
+ Math.abs(shapeHeight - frame.height) <= 0.0001;
1085
+ if (shape === "rect" && radius <= 0.0001 && isSameAsFrame) {
1086
+ this.debug("overlay:shape:skip", {
1087
+ reason: "shape-rect-no-radius",
1088
+ });
1089
+ return [];
1090
+ }
1091
+
1092
+ const baseOptions = {
1093
+ shape,
1094
+ width: shapeWidth,
1095
+ height: shapeHeight,
1096
+ radius,
1097
+ x: frame.width / 2,
1098
+ y: frame.height / 2,
1099
+ features: [],
1100
+ canvasWidth: frame.width,
1101
+ canvasHeight: frame.height,
1102
+ };
1103
+
1104
+ try {
1105
+ const shapePathData = generateDielinePath(baseOptions);
1106
+ const outerRectPathData = `M 0 0 L ${frame.width} 0 L ${frame.width} ${frame.height} L 0 ${frame.height} Z`;
1107
+ const hatchPathData = `${outerRectPathData} ${shapePathData}`;
1108
+ if (!shapePathData || !hatchPathData) {
1109
+ this.debug("overlay:shape:skip", {
1110
+ reason: "path-generation-empty",
1111
+ shape,
1112
+ radius,
1113
+ });
1114
+ return [];
1115
+ }
1116
+
1117
+ const patternFill = this.getCropShapeHatchPattern();
1118
+ const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
1119
+ const hatchPathLength = hatchPathData.length;
1120
+ const shapePathLength = shapePathData.length;
1121
+ const specs: RenderObjectSpec[] = [
1122
+ {
1123
+ id: "image.cropShapeHatch",
1124
+ type: "path",
1125
+ data: { id: "image.cropShapeHatch", zIndex: 5 },
1126
+ props: {
1127
+ pathData: hatchPathData,
1128
+ left: frame.left,
1129
+ top: frame.top,
1130
+ originX: "left",
1131
+ originY: "top",
1132
+ fill: hatchFill,
1133
+ opacity: patternFill ? 1 : 0.8,
1134
+ stroke: null,
1135
+ fillRule: "evenodd",
1136
+ selectable: false,
1137
+ evented: false,
1138
+ excludeFromExport: true,
1139
+ objectCaching: false,
1140
+ },
1141
+ },
1142
+ {
1143
+ id: "image.cropShapePath",
1144
+ type: "path",
1145
+ data: { id: "image.cropShapePath", zIndex: 6 },
1146
+ props: {
1147
+ pathData: shapePathData,
1148
+ left: frame.left,
1149
+ top: frame.top,
1150
+ originX: "left",
1151
+ originY: "top",
1152
+ fill: "rgba(0,0,0,0)",
1153
+ stroke: "rgba(255, 0, 0, 0.9)",
1154
+ strokeWidth: 1,
1155
+ selectable: false,
1156
+ evented: false,
1157
+ excludeFromExport: true,
1158
+ objectCaching: false,
1159
+ },
1160
+ },
1161
+ ];
1162
+ this.debug("overlay:shape:built", {
1163
+ shape,
1164
+ radius,
1165
+ inset,
1166
+ shapeWidth,
1167
+ shapeHeight,
1168
+ fillRule: "evenodd",
1169
+ shapePathLength,
1170
+ hatchPathLength,
1171
+ hatchFillType:
1172
+ hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
1173
+ ids: specs.map((spec) => spec.id),
1174
+ });
1175
+ return specs;
1176
+ } catch (error) {
1177
+ this.debug("overlay:shape:error", {
1178
+ shape,
1179
+ radius,
1180
+ error: error instanceof Error ? error.message : String(error),
1181
+ });
1182
+ return [];
1183
+ }
1184
+ }
1185
+
919
1186
  private resolveRenderImageState(item: ImageItem): RenderImageState {
920
1187
  const active = this.isToolActive;
921
1188
  const sourceUrl = item.sourceUrl || item.url;
@@ -980,6 +1247,21 @@ export class ImageTool implements Extension {
980
1247
  return obj?._originalElement?.src;
981
1248
  }
982
1249
 
1250
+ private applyImageControlVisibility(obj: any) {
1251
+ if (typeof obj?.setControlsVisibility !== "function") return;
1252
+ obj.setControlsVisibility({
1253
+ mt: false,
1254
+ mb: false,
1255
+ ml: false,
1256
+ mr: false,
1257
+ tl: true,
1258
+ tr: true,
1259
+ bl: true,
1260
+ br: true,
1261
+ mtr: true,
1262
+ });
1263
+ }
1264
+
983
1265
  private async upsertImageObject(
984
1266
  item: ImageItem,
985
1267
  frame: FrameRect,
@@ -1028,6 +1310,7 @@ export class ImageTool implements Extension {
1028
1310
  type: "image-item",
1029
1311
  },
1030
1312
  });
1313
+ this.applyImageControlVisibility(obj);
1031
1314
  obj.setCoords();
1032
1315
 
1033
1316
  const resolver = this.loadResolvers.get(item.id);
@@ -1065,9 +1348,25 @@ export class ImageTool implements Extension {
1065
1348
  overlayObjects.forEach((obj) => {
1066
1349
  canvas.bringObjectToFront(obj);
1067
1350
  });
1351
+
1352
+ if (this.isDebugEnabled()) {
1353
+ const stack = canvas
1354
+ .getObjects()
1355
+ .map((obj: any, index: number) => ({
1356
+ index,
1357
+ id: obj?.data?.id,
1358
+ layerId: obj?.data?.layerId,
1359
+ zIndex: obj?.data?.zIndex,
1360
+ }))
1361
+ .filter((item) => item.layerId === IMAGE_OVERLAY_LAYER_ID);
1362
+ this.debug("overlay:stack", stack);
1363
+ }
1068
1364
  }
1069
1365
 
1070
- private buildOverlaySpecs(frame: FrameRect): RenderObjectSpec[] {
1366
+ private buildOverlaySpecs(
1367
+ frame: FrameRect,
1368
+ sceneGeometry: SceneGeometryLike | null,
1369
+ ): RenderObjectSpec[] {
1071
1370
  const visible = this.isImageEditingVisible();
1072
1371
  if (
1073
1372
  !visible ||
@@ -1105,6 +1404,7 @@ export class ImageTool implements Extension {
1105
1404
  const bottomH = Math.max(0, canvasH - frameBottom);
1106
1405
  const leftW = frameLeft;
1107
1406
  const rightW = Math.max(0, canvasW - frameRight);
1407
+ const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
1108
1408
 
1109
1409
  const mask: RenderObjectSpec[] = [
1110
1410
  {
@@ -1176,7 +1476,7 @@ export class ImageTool implements Extension {
1176
1476
  const frameSpec: RenderObjectSpec = {
1177
1477
  id: "image.cropFrame",
1178
1478
  type: "rect",
1179
- data: { id: "image.cropFrame", zIndex: 5 },
1479
+ data: { id: "image.cropFrame", zIndex: 7 },
1180
1480
  props: {
1181
1481
  left: frame.left + frame.width / 2,
1182
1482
  top: frame.top + frame.height / 2,
@@ -1199,7 +1499,16 @@ export class ImageTool implements Extension {
1199
1499
  },
1200
1500
  };
1201
1501
 
1202
- return [...mask, frameSpec];
1502
+ const specs = [...mask, ...shapeOverlay, frameSpec];
1503
+ this.debug("overlay:built", {
1504
+ frame,
1505
+ shape: sceneGeometry?.shape,
1506
+ overlayIds: specs.map((spec) => ({
1507
+ id: spec.id,
1508
+ zIndex: spec.data?.zIndex,
1509
+ })),
1510
+ });
1511
+ return specs;
1203
1512
  }
1204
1513
 
1205
1514
  private updateImages() {
@@ -1235,7 +1544,10 @@ export class ImageTool implements Extension {
1235
1544
  if (seq !== this.renderSeq) return;
1236
1545
 
1237
1546
  this.syncImageZOrder(renderItems);
1238
- const overlaySpecs = this.buildOverlaySpecs(frame);
1547
+ const sceneGeometry = await this.resolveSceneGeometryForOverlay();
1548
+ if (seq !== this.renderSeq) return;
1549
+
1550
+ const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1239
1551
  await this.canvasService.applyObjectSpecsToRootLayer(
1240
1552
  IMAGE_OVERLAY_LAYER_ID,
1241
1553
  overlaySpecs,
@@ -1470,10 +1782,11 @@ export class ImageTool implements Extension {
1470
1782
 
1471
1783
  const next: ImageItem[] = [];
1472
1784
  for (const item of this.workingItems) {
1473
- const url = await this.exportCroppedImageByIds([item.id], {
1785
+ const exported = await this.exportCroppedImageByIds([item.id], {
1474
1786
  multiplier: 2,
1475
1787
  format: "png",
1476
1788
  });
1789
+ const url = exported.url;
1477
1790
 
1478
1791
  const sourceUrl = item.sourceUrl || item.url;
1479
1792
  const previousCommitted = item.committedUrl;
@@ -1499,15 +1812,22 @@ export class ImageTool implements Extension {
1499
1812
 
1500
1813
  private async exportCroppedImageByIds(
1501
1814
  imageIds: string[],
1502
- options: { multiplier?: number; format?: "png" | "jpeg" },
1503
- ): Promise<string> {
1815
+ options: ExportCroppedImageOptions,
1816
+ ): Promise<ExportUserCroppedImageResult> {
1504
1817
  if (!this.canvasService) {
1505
1818
  throw new Error("CanvasService not initialized");
1506
1819
  }
1507
1820
 
1821
+ const normalizedIds = [...new Set(imageIds)].filter(
1822
+ (id): id is string => typeof id === "string" && id.length > 0,
1823
+ );
1824
+ if (!normalizedIds.length) {
1825
+ throw new Error("image-ids-required");
1826
+ }
1827
+
1508
1828
  const frame = this.getFrameRect();
1509
1829
  const multiplier = Math.max(1, options.multiplier ?? 2);
1510
- const format = options.format ?? "png";
1830
+ const format: "png" | "jpeg" = options.format === "jpeg" ? "jpeg" : "png";
1511
1831
 
1512
1832
  const width = Math.max(1, Math.round(frame.width * multiplier));
1513
1833
  const height = Math.max(1, Math.round(frame.height * multiplier));
@@ -1521,61 +1841,84 @@ export class ImageTool implements Extension {
1521
1841
  } as any);
1522
1842
  tempCanvas.setDimensions({ width, height });
1523
1843
 
1524
- const idSet = new Set(imageIds);
1525
- const sourceObjects = this.canvasService.canvas
1526
- .getObjects()
1527
- .filter((obj: any) => {
1528
- return (
1529
- obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID &&
1530
- typeof obj?.data?.id === "string" &&
1531
- idSet.has(obj.data.id)
1532
- );
1533
- });
1844
+ try {
1845
+ const idSet = new Set(normalizedIds);
1846
+ const sourceObjects = this.canvasService.canvas
1847
+ .getObjects()
1848
+ .filter((obj: any) => {
1849
+ return (
1850
+ obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID &&
1851
+ typeof obj?.data?.id === "string" &&
1852
+ idSet.has(obj.data.id)
1853
+ );
1854
+ });
1855
+
1856
+ if (!sourceObjects.length) {
1857
+ throw new Error("image-objects-not-found");
1858
+ }
1534
1859
 
1535
- for (const source of sourceObjects as any[]) {
1536
- const clone = await source.clone();
1537
- const center = source.getCenterPoint
1538
- ? source.getCenterPoint()
1539
- : new Point(source.left ?? 0, source.top ?? 0);
1860
+ for (const source of sourceObjects as any[]) {
1861
+ const clone = await source.clone();
1862
+ const center = source.getCenterPoint
1863
+ ? source.getCenterPoint()
1864
+ : new Point(source.left ?? 0, source.top ?? 0);
1540
1865
 
1541
- clone.set({
1542
- originX: "center",
1543
- originY: "center",
1544
- left: (center.x - frame.left) * multiplier,
1545
- top: (center.y - frame.top) * multiplier,
1546
- scaleX: (source.scaleX || 1) * multiplier,
1547
- scaleY: (source.scaleY || 1) * multiplier,
1548
- angle: source.angle || 0,
1549
- selectable: false,
1550
- evented: false,
1551
- });
1552
- clone.setCoords();
1553
- tempCanvas.add(clone);
1554
- }
1866
+ clone.set({
1867
+ originX: "center",
1868
+ originY: "center",
1869
+ left: (center.x - frame.left) * multiplier,
1870
+ top: (center.y - frame.top) * multiplier,
1871
+ scaleX: (source.scaleX || 1) * multiplier,
1872
+ scaleY: (source.scaleY || 1) * multiplier,
1873
+ angle: source.angle || 0,
1874
+ selectable: false,
1875
+ evented: false,
1876
+ });
1877
+ clone.setCoords();
1878
+ tempCanvas.add(clone);
1879
+ }
1555
1880
 
1556
- tempCanvas.renderAll();
1557
- const dataUrl = tempCanvas.toDataURL({ format, multiplier: 1 });
1558
- tempCanvas.dispose();
1881
+ tempCanvas.renderAll();
1882
+ const blob = await tempCanvas.toBlob({ format, multiplier: 1 });
1883
+ if (!blob) {
1884
+ throw new Error("image-export-failed");
1885
+ }
1559
1886
 
1560
- const blob = await (await fetch(dataUrl)).blob();
1561
- return URL.createObjectURL(blob);
1887
+ return {
1888
+ url: URL.createObjectURL(blob),
1889
+ width,
1890
+ height,
1891
+ multiplier,
1892
+ format,
1893
+ imageIds: (sourceObjects as any[])
1894
+ .map((obj: any) => obj?.data?.id)
1895
+ .filter((id: any): id is string => typeof id === "string"),
1896
+ };
1897
+ } finally {
1898
+ tempCanvas.dispose();
1899
+ }
1562
1900
  }
1563
1901
 
1564
- private async exportImageFrameUrl(
1565
- options: { multiplier?: number; format?: "png" | "jpeg" } = {},
1566
- ): Promise<{ url: string }> {
1902
+ private async exportUserCroppedImage(
1903
+ options: ExportUserCroppedImageOptions = {},
1904
+ ): Promise<ExportUserCroppedImageResult> {
1567
1905
  if (!this.canvasService) {
1568
1906
  throw new Error("CanvasService not initialized");
1569
1907
  }
1570
1908
 
1571
- const imageIds = this.getImageObjects()
1572
- .map((obj: any) => obj?.data?.id)
1573
- .filter((id: any) => typeof id === "string");
1909
+ await this.updateImagesAsync();
1910
+ this.syncToolActiveFromWorkbench();
1574
1911
 
1575
- const url = await this.exportCroppedImageByIds(
1576
- imageIds as string[],
1577
- options,
1578
- );
1579
- return { url };
1912
+ const imageIds =
1913
+ options.imageIds && options.imageIds.length > 0
1914
+ ? options.imageIds
1915
+ : (this.isToolActive ? this.workingItems : this.items).map(
1916
+ (item) => item.id,
1917
+ );
1918
+ if (!imageIds.length) {
1919
+ throw new Error("no-images-to-export");
1920
+ }
1921
+
1922
+ return await this.exportCroppedImageByIds(imageIds, options);
1580
1923
  }
1581
1924
  }
@@ -0,0 +1,11 @@
1
+ export * from "./background";
2
+ export * from "./image";
3
+ export * from "./size";
4
+ export * from "./dieline";
5
+ export * from "./feature";
6
+ export * from "./film";
7
+ export * from "./mirror";
8
+ export * from "./ruler";
9
+ export * from "./white-ink";
10
+ export { SceneLayoutService } from "./sceneLayout";
11
+ export { SceneVisibilityService } from "./sceneVisibility";