@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
@@ -14,8 +14,10 @@ import {
14
14
  Pattern,
15
15
  Point,
16
16
  } from "fabric";
17
- import { CanvasService, RenderObjectSpec } from "../services";
18
- import { generateDielinePath } from "./geometry";
17
+ import { CanvasService, RenderLayoutRect, RenderObjectSpec } from "../services";
18
+ import { isDielineShape, normalizeShapeStyle } from "./dielineShape";
19
+ import type { DielineShape, DielineShapeStyle } from "./dielineShape";
20
+ import { generateDielinePath, getPathBounds } from "./geometry";
19
21
  import {
20
22
  buildSceneGeometry,
21
23
  computeSceneLayout,
@@ -64,11 +66,11 @@ interface FrameVisualConfig {
64
66
  outerBackground: string;
65
67
  }
66
68
 
67
- type DielineShape = "rect" | "circle" | "ellipse" | "custom";
68
69
  type ShapeOverlayShape = Exclude<DielineShape, "custom">;
69
70
 
70
71
  interface SceneGeometryLike {
71
72
  shape: DielineShape;
73
+ shapeStyle: DielineShapeStyle;
72
74
  radius: number;
73
75
  offset: number;
74
76
  }
@@ -134,6 +136,10 @@ export class ImageTool implements Extension {
134
136
  private dirtyTrackerDisposable?: { dispose(): void };
135
137
  private cropShapeHatchPattern?: Pattern;
136
138
  private cropShapeHatchPatternColor?: string;
139
+ private cropShapeHatchPatternKey?: string;
140
+ private imageSpecs: RenderObjectSpec[] = [];
141
+ private overlaySpecs: RenderObjectSpec[] = [];
142
+ private renderProducerDisposable?: { dispose: () => void };
137
143
 
138
144
  activate(context: ExtensionContext) {
139
145
  this.context = context;
@@ -142,6 +148,41 @@ export class ImageTool implements Extension {
142
148
  console.warn("CanvasService not found for ImageTool");
143
149
  return;
144
150
  }
151
+ this.renderProducerDisposable?.dispose();
152
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
153
+ this.id,
154
+ () => ({
155
+ passes: [
156
+ {
157
+ id: IMAGE_OBJECT_LAYER_ID,
158
+ stack: 500,
159
+ order: 0,
160
+ visibility: {
161
+ op: "not",
162
+ expr: {
163
+ op: "sessionActive",
164
+ toolId: "pooder.kit.white-ink",
165
+ },
166
+ },
167
+ objects: this.imageSpecs,
168
+ },
169
+ {
170
+ id: IMAGE_OVERLAY_LAYER_ID,
171
+ stack: 800,
172
+ order: 0,
173
+ visibility: {
174
+ op: "not",
175
+ expr: {
176
+ op: "sessionActive",
177
+ toolId: "pooder.kit.white-ink",
178
+ },
179
+ },
180
+ objects: this.overlaySpecs,
181
+ },
182
+ ],
183
+ }),
184
+ { priority: 300 },
185
+ );
145
186
 
146
187
  context.eventBus.on("tool:activated", this.onToolActivated);
147
188
  context.eventBus.on("object:modified", this.onObjectModified);
@@ -202,13 +243,15 @@ export class ImageTool implements Extension {
202
243
  this.dirtyTrackerDisposable = undefined;
203
244
  this.cropShapeHatchPattern = undefined;
204
245
  this.cropShapeHatchPatternColor = undefined;
246
+ this.cropShapeHatchPatternKey = undefined;
247
+ this.imageSpecs = [];
248
+ this.overlaySpecs = [];
205
249
 
206
250
  this.clearRenderedImages();
251
+ this.renderProducerDisposable?.dispose();
252
+ this.renderProducerDisposable = undefined;
207
253
  if (this.canvasService) {
208
- void this.canvasService.applyObjectSpecsToRootLayer(
209
- IMAGE_OVERLAY_LAYER_ID,
210
- [],
211
- );
254
+ void this.canvasService.flushRenderFromProducers();
212
255
  this.canvasService = undefined;
213
256
  }
214
257
  this.context = undefined;
@@ -766,47 +809,41 @@ export class ImageTool implements Extension {
766
809
  return { left: 0, top: 0, width: 0, height: 0 };
767
810
  }
768
811
 
769
- return {
812
+ return this.canvasService.toSceneRect({
770
813
  left: layout.cutRect.left,
771
814
  top: layout.cutRect.top,
772
815
  width: layout.cutRect.width,
773
816
  height: layout.cutRect.height,
774
- };
817
+ });
775
818
  }
776
819
 
777
- private async resolveDefaultFitArea(): Promise<DielineFitArea | null> {
778
- if (!this.context || !this.canvasService) return null;
779
- const commandService = this.context.services.get<any>("CommandService");
780
- if (!commandService) return null;
820
+ private getFrameRectScreen(frame?: FrameRect): FrameRect {
821
+ if (!this.canvasService) {
822
+ return { left: 0, top: 0, width: 0, height: 0 };
823
+ }
824
+ return this.canvasService.toScreenRect(frame || this.getFrameRect());
825
+ }
781
826
 
782
- try {
783
- const layout = await Promise.resolve(
784
- commandService.executeCommand("getSceneLayout"),
785
- );
786
- const cutRect = layout?.cutRect;
787
- const width = Number(cutRect?.width);
788
- const height = Number(cutRect?.height);
789
- const left = Number(cutRect?.left);
790
- const top = Number(cutRect?.top);
791
-
792
- if (
793
- !Number.isFinite(width) ||
794
- !Number.isFinite(height) ||
795
- !Number.isFinite(left) ||
796
- !Number.isFinite(top)
797
- ) {
798
- return null;
799
- }
827
+ private toLayoutSceneRect(rect: FrameRect): RenderLayoutRect {
828
+ return {
829
+ left: rect.left,
830
+ top: rect.top,
831
+ width: rect.width,
832
+ height: rect.height,
833
+ space: "scene",
834
+ };
835
+ }
800
836
 
801
- return {
802
- width: Math.max(1, width),
803
- height: Math.max(1, height),
804
- left: left + width / 2,
805
- top: top + height / 2,
806
- };
807
- } catch {
808
- return null;
809
- }
837
+ private async resolveDefaultFitArea(): Promise<DielineFitArea | null> {
838
+ if (!this.canvasService) return null;
839
+ const frame = this.getFrameRect();
840
+ if (frame.width <= 0 || frame.height <= 0) return null;
841
+ return {
842
+ width: Math.max(1, frame.width),
843
+ height: Math.max(1, frame.height),
844
+ left: frame.left + frame.width / 2,
845
+ top: frame.top + frame.height / 2,
846
+ };
810
847
  }
811
848
 
812
849
  private async fitImageToDefaultArea(id: string) {
@@ -818,13 +855,14 @@ export class ImageTool implements Extension {
818
855
  return;
819
856
  }
820
857
 
821
- const canvasW = Math.max(1, this.canvasService.canvas.width || 0);
822
- const canvasH = Math.max(1, this.canvasService.canvas.height || 0);
858
+ const viewport = this.canvasService.getSceneViewportRect();
859
+ const canvasW = Math.max(1, viewport.width || 0);
860
+ const canvasH = Math.max(1, viewport.height || 0);
823
861
  await this.fitImageToArea(id, {
824
862
  width: canvasW,
825
863
  height: canvasH,
826
- left: canvasW / 2,
827
- top: canvasH / 2,
864
+ left: viewport.left + canvasW / 2,
865
+ top: viewport.top + canvasH / 2,
828
866
  });
829
867
  }
830
868
 
@@ -837,9 +875,7 @@ export class ImageTool implements Extension {
837
875
 
838
876
  private getOverlayObjects(): any[] {
839
877
  if (!this.canvasService) return [];
840
- return this.canvasService.getRootLayerObjects(
841
- IMAGE_OVERLAY_LAYER_ID,
842
- ) as any[];
878
+ return this.canvasService.getPassObjects(IMAGE_OVERLAY_LAYER_ID) as any[];
843
879
  }
844
880
 
845
881
  private getImageObject(id: string): any | undefined {
@@ -848,9 +884,9 @@ export class ImageTool implements Extension {
848
884
 
849
885
  private clearRenderedImages() {
850
886
  if (!this.canvasService) return;
851
- const canvas = this.canvasService.canvas;
852
- this.getImageObjects().forEach((obj) => canvas.remove(obj));
853
- this.canvasService.requestRenderAll();
887
+ this.imageSpecs = [];
888
+ this.overlaySpecs = [];
889
+ this.canvasService.requestRenderFromProducers();
854
890
  }
855
891
 
856
892
  private purgeSourceSizeCacheForItem(item?: ImageItem) {
@@ -884,6 +920,32 @@ export class ImageTool implements Extension {
884
920
  return { width: 1, height: 1 };
885
921
  }
886
922
 
923
+ private async ensureSourceSize(src: string): Promise<SourceSize | null> {
924
+ if (!src) return null;
925
+ const cached = this.sourceSizeBySrc.get(src);
926
+ if (cached) return cached;
927
+
928
+ try {
929
+ const image = await FabricImage.fromURL(src, {
930
+ crossOrigin: "anonymous",
931
+ });
932
+ const width = Number(image?.width || 0);
933
+ const height = Number(image?.height || 0);
934
+ if (width > 0 && height > 0) {
935
+ const size = { width, height };
936
+ this.sourceSizeBySrc.set(src, size);
937
+ return size;
938
+ }
939
+ } catch (error) {
940
+ this.debug("image:size:load-failed", {
941
+ src,
942
+ error: error instanceof Error ? error.message : String(error),
943
+ });
944
+ }
945
+
946
+ return null;
947
+ }
948
+
887
949
  private getCoverScale(frame: FrameRect, size: SourceSize): number {
888
950
  const sw = Math.max(1, size.width);
889
951
  const sh = Math.max(1, size.height);
@@ -929,19 +991,24 @@ export class ImageTool implements Extension {
929
991
 
930
992
  private toSceneGeometryLike(raw: any): SceneGeometryLike | null {
931
993
  const shape = raw?.shape;
932
- if (
933
- shape !== "rect" &&
934
- shape !== "circle" &&
935
- shape !== "ellipse" &&
936
- shape !== "custom"
937
- ) {
994
+ if (!isDielineShape(shape)) {
938
995
  return null;
939
996
  }
940
997
 
941
- const radius = Number(raw?.radius);
942
- const offset = Number(raw?.offset);
998
+ const radiusRaw = Number(raw?.radius);
999
+ const offsetRaw = Number(raw?.offset);
1000
+ const unit = typeof raw?.unit === "string" ? raw.unit : "px";
1001
+ const radius =
1002
+ unit === "scene" || !this.canvasService
1003
+ ? radiusRaw
1004
+ : this.canvasService.toSceneLength(radiusRaw);
1005
+ const offset =
1006
+ unit === "scene" || !this.canvasService
1007
+ ? offsetRaw
1008
+ : this.canvasService.toSceneLength(offsetRaw);
943
1009
  return {
944
1010
  shape,
1011
+ shapeStyle: normalizeShapeStyle(raw?.shapeStyle),
945
1012
  radius: Number.isFinite(radius) ? radius : 0,
946
1013
  offset: Number.isFinite(offset) ? offset : 0,
947
1014
  };
@@ -1008,9 +1075,12 @@ export class ImageTool implements Extension {
1008
1075
  color = "rgba(255, 0, 0, 0.6)",
1009
1076
  ): Pattern | undefined {
1010
1077
  if (typeof document === "undefined") return undefined;
1078
+ const sceneScale = this.canvasService?.getSceneScale() || 1;
1079
+ const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
1011
1080
  if (
1012
1081
  this.cropShapeHatchPattern &&
1013
- this.cropShapeHatchPatternColor === color
1082
+ this.cropShapeHatchPatternColor === color &&
1083
+ this.cropShapeHatchPatternKey === cacheKey
1014
1084
  ) {
1015
1085
  return this.cropShapeHatchPattern;
1016
1086
  }
@@ -1043,8 +1113,18 @@ export class ImageTool implements Extension {
1043
1113
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1044
1114
  repetition: "repeat",
1045
1115
  });
1116
+ // Scene specs are scaled to screen by CanvasService; keep hatch density in screen pixels.
1117
+ (pattern as any).patternTransform = [
1118
+ 1 / sceneScale,
1119
+ 0,
1120
+ 0,
1121
+ 1 / sceneScale,
1122
+ 0,
1123
+ 0,
1124
+ ];
1046
1125
  this.cropShapeHatchPattern = pattern;
1047
1126
  this.cropShapeHatchPatternColor = color;
1127
+ this.cropShapeHatchPatternKey = cacheKey;
1048
1128
  return pattern;
1049
1129
  }
1050
1130
 
@@ -1062,6 +1142,7 @@ export class ImageTool implements Extension {
1062
1142
  }
1063
1143
 
1064
1144
  const shape = sceneGeometry.shape as ShapeOverlayShape;
1145
+ const shapeStyle = sceneGeometry.shapeStyle;
1065
1146
  const inset = 0;
1066
1147
  const shapeWidth = Math.max(1, frame.width);
1067
1148
  const shapeHeight = Math.max(1, frame.height);
@@ -1072,6 +1153,7 @@ export class ImageTool implements Extension {
1072
1153
  frameWidth: frame.width,
1073
1154
  frameHeight: frame.height,
1074
1155
  offset: sceneGeometry.offset,
1156
+ shapeStyle,
1075
1157
  inset,
1076
1158
  shapeWidth,
1077
1159
  shapeHeight,
@@ -1097,6 +1179,7 @@ export class ImageTool implements Extension {
1097
1179
  x: frame.width / 2,
1098
1180
  y: frame.height / 2,
1099
1181
  features: [],
1182
+ shapeStyle,
1100
1183
  canvasWidth: frame.width,
1101
1184
  canvasHeight: frame.height,
1102
1185
  };
@@ -1116,6 +1199,9 @@ export class ImageTool implements Extension {
1116
1199
 
1117
1200
  const patternFill = this.getCropShapeHatchPattern();
1118
1201
  const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
1202
+ const shapeBounds = getPathBounds(shapePathData);
1203
+ const hatchBounds = getPathBounds(hatchPathData);
1204
+ const frameRect = this.toLayoutSceneRect(frame);
1119
1205
  const hatchPathLength = hatchPathData.length;
1120
1206
  const shapePathLength = shapePathData.length;
1121
1207
  const specs: RenderObjectSpec[] = [
@@ -1123,10 +1209,16 @@ export class ImageTool implements Extension {
1123
1209
  id: "image.cropShapeHatch",
1124
1210
  type: "path",
1125
1211
  data: { id: "image.cropShapeHatch", zIndex: 5 },
1212
+ layout: {
1213
+ reference: "custom",
1214
+ referenceRect: frameRect,
1215
+ alignX: "start",
1216
+ alignY: "start",
1217
+ offsetX: hatchBounds.x,
1218
+ offsetY: hatchBounds.y,
1219
+ },
1126
1220
  props: {
1127
1221
  pathData: hatchPathData,
1128
- left: frame.left,
1129
- top: frame.top,
1130
1222
  originX: "left",
1131
1223
  originY: "top",
1132
1224
  fill: hatchFill,
@@ -1143,15 +1235,21 @@ export class ImageTool implements Extension {
1143
1235
  id: "image.cropShapePath",
1144
1236
  type: "path",
1145
1237
  data: { id: "image.cropShapePath", zIndex: 6 },
1238
+ layout: {
1239
+ reference: "custom",
1240
+ referenceRect: frameRect,
1241
+ alignX: "start",
1242
+ alignY: "start",
1243
+ offsetX: shapeBounds.x,
1244
+ offsetY: shapeBounds.y,
1245
+ },
1146
1246
  props: {
1147
1247
  pathData: shapePathData,
1148
- left: frame.left,
1149
- top: frame.top,
1150
1248
  originX: "left",
1151
1249
  originY: "top",
1152
1250
  fill: "rgba(0,0,0,0)",
1153
1251
  stroke: "rgba(255, 0, 0, 0.9)",
1154
- strokeWidth: 1,
1252
+ strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
1155
1253
  selectable: false,
1156
1254
  evented: false,
1157
1255
  excludeFromExport: true,
@@ -1168,6 +1266,8 @@ export class ImageTool implements Extension {
1168
1266
  fillRule: "evenodd",
1169
1267
  shapePathLength,
1170
1268
  hatchPathLength,
1269
+ shapeBounds,
1270
+ hatchBounds,
1171
1271
  hatchFillType:
1172
1272
  hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
1173
1273
  ids: specs.map((spec) => spec.id),
@@ -1241,126 +1341,45 @@ export class ImageTool implements Extension {
1241
1341
  };
1242
1342
  }
1243
1343
 
1344
+ private toSceneObjectScale(value: number): number {
1345
+ if (!this.canvasService) return value;
1346
+ return value / this.canvasService.getSceneScale();
1347
+ }
1348
+
1244
1349
  private getCurrentSrc(obj: any): string | undefined {
1245
1350
  if (!obj) return undefined;
1246
1351
  if (typeof obj.getSrc === "function") return obj.getSrc();
1247
1352
  return obj?._originalElement?.src;
1248
1353
  }
1249
1354
 
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
-
1265
- private async upsertImageObject(
1266
- item: ImageItem,
1355
+ private async buildImageSpecs(
1356
+ items: ImageItem[],
1267
1357
  frame: FrameRect,
1268
- seq: number,
1269
- ) {
1270
- if (!this.canvasService) return;
1271
- const canvas = this.canvasService.canvas;
1272
- const render = this.resolveRenderImageState(item);
1273
- if (!render.src) return;
1274
-
1275
- let obj = this.getImageObject(item.id);
1276
- const currentSrc = this.getCurrentSrc(obj);
1358
+ ): Promise<RenderObjectSpec[]> {
1359
+ const specs: RenderObjectSpec[] = [];
1277
1360
 
1278
- if (obj && currentSrc && currentSrc !== render.src) {
1279
- canvas.remove(obj);
1280
- obj = undefined;
1281
- }
1361
+ for (const item of items) {
1362
+ const render = this.resolveRenderImageState(item);
1363
+ if (!render.src) continue;
1282
1364
 
1283
- if (!obj) {
1284
- const created = await FabricImage.fromURL(render.src, {
1285
- crossOrigin: "anonymous",
1286
- });
1287
- if (seq !== this.renderSeq) return;
1365
+ const ensured = await this.ensureSourceSize(render.src);
1366
+ const sourceSize = ensured || this.getSourceSize(render.src);
1367
+ const props = this.computeCanvasProps(render, sourceSize, frame);
1288
1368
 
1289
- created.set({
1369
+ specs.push({
1370
+ id: item.id,
1371
+ type: "image",
1372
+ src: render.src,
1290
1373
  data: {
1291
1374
  id: item.id,
1292
1375
  layerId: IMAGE_OBJECT_LAYER_ID,
1293
1376
  type: "image-item",
1294
1377
  },
1295
- } as any);
1296
- canvas.add(created as any);
1297
- obj = created as any;
1298
- }
1299
-
1300
- this.rememberSourceSize(render.src, obj);
1301
- const sourceSize = this.getSourceSize(render.src, obj);
1302
- const props = this.computeCanvasProps(render, sourceSize, frame);
1303
-
1304
- obj.set({
1305
- ...props,
1306
- data: {
1307
- ...(obj.data || {}),
1308
- id: item.id,
1309
- layerId: IMAGE_OBJECT_LAYER_ID,
1310
- type: "image-item",
1311
- },
1312
- });
1313
- this.applyImageControlVisibility(obj);
1314
- obj.setCoords();
1315
-
1316
- const resolver = this.loadResolvers.get(item.id);
1317
- if (resolver) {
1318
- resolver();
1319
- this.loadResolvers.delete(item.id);
1320
- }
1321
- }
1322
-
1323
- private syncImageZOrder(items: ImageItem[]) {
1324
- if (!this.canvasService) return;
1325
- const canvas = this.canvasService.canvas;
1326
-
1327
- const objects = canvas.getObjects();
1328
- let insertIndex = 0;
1329
-
1330
- const backgroundLayer = this.canvasService.getLayer("background");
1331
- if (backgroundLayer) {
1332
- const bgIndex = objects.indexOf(backgroundLayer as any);
1333
- if (bgIndex >= 0) insertIndex = bgIndex + 1;
1378
+ props,
1379
+ });
1334
1380
  }
1335
1381
 
1336
- items.forEach((item) => {
1337
- const obj = this.getImageObject(item.id);
1338
- if (!obj) return;
1339
- canvas.moveObjectTo(obj, insertIndex);
1340
- insertIndex += 1;
1341
- });
1342
-
1343
- const overlayObjects = this.getOverlayObjects().sort((a: any, b: any) => {
1344
- const az = Number(a?.data?.zIndex ?? 0);
1345
- const bz = Number(b?.data?.zIndex ?? 0);
1346
- return az - bz;
1347
- });
1348
- overlayObjects.forEach((obj) => {
1349
- canvas.bringObjectToFront(obj);
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
- }
1382
+ return specs;
1364
1383
  }
1365
1384
 
1366
1385
  private buildOverlaySpecs(
@@ -1384,26 +1403,52 @@ export class ImageTool implements Extension {
1384
1403
  return [];
1385
1404
  }
1386
1405
 
1387
- const canvasW = this.canvasService.canvas.width || 0;
1388
- const canvasH = this.canvasService.canvas.height || 0;
1406
+ const viewport = this.canvasService.getSceneViewportRect();
1407
+ const canvasW = viewport.width || 0;
1408
+ const canvasH = viewport.height || 0;
1409
+ const canvasLeft = viewport.left || 0;
1410
+ const canvasTop = viewport.top || 0;
1389
1411
  const visual = this.getFrameVisualConfig();
1412
+ const strokeWidthScene = this.canvasService.toSceneLength(
1413
+ visual.strokeWidth,
1414
+ );
1415
+ const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
1390
1416
 
1391
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
1392
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
1417
+ const frameLeft = Math.max(
1418
+ canvasLeft,
1419
+ Math.min(canvasLeft + canvasW, frame.left),
1420
+ );
1421
+ const frameTop = Math.max(
1422
+ canvasTop,
1423
+ Math.min(canvasTop + canvasH, frame.top),
1424
+ );
1393
1425
  const frameRight = Math.max(
1394
1426
  frameLeft,
1395
- Math.min(canvasW, frame.left + frame.width),
1427
+ Math.min(canvasLeft + canvasW, frame.left + frame.width),
1396
1428
  );
1397
1429
  const frameBottom = Math.max(
1398
1430
  frameTop,
1399
- Math.min(canvasH, frame.top + frame.height),
1431
+ Math.min(canvasTop + canvasH, frame.top + frame.height),
1400
1432
  );
1401
1433
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
1402
1434
 
1403
- const topH = frameTop;
1404
- const bottomH = Math.max(0, canvasH - frameBottom);
1405
- const leftW = frameLeft;
1406
- const rightW = Math.max(0, canvasW - frameRight);
1435
+ const topH = Math.max(0, frameTop - canvasTop);
1436
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
1437
+ const leftW = Math.max(0, frameLeft - canvasLeft);
1438
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
1439
+ const viewportRect = this.toLayoutSceneRect({
1440
+ left: canvasLeft,
1441
+ top: canvasTop,
1442
+ width: canvasW,
1443
+ height: canvasH,
1444
+ });
1445
+ const visibleFrameBandRect = this.toLayoutSceneRect({
1446
+ left: canvasLeft,
1447
+ top: frameTop,
1448
+ width: canvasW,
1449
+ height: visibleFrameH,
1450
+ });
1451
+ const frameRect = this.toLayoutSceneRect(frame);
1407
1452
  const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
1408
1453
 
1409
1454
  const mask: RenderObjectSpec[] = [
@@ -1411,13 +1456,17 @@ export class ImageTool implements Extension {
1411
1456
  id: "image.cropMask.top",
1412
1457
  type: "rect",
1413
1458
  data: { id: "image.cropMask.top", zIndex: 1 },
1414
- props: {
1415
- left: canvasW / 2,
1416
- top: topH / 2,
1417
- width: canvasW,
1459
+ layout: {
1460
+ reference: "custom",
1461
+ referenceRect: viewportRect,
1462
+ alignX: "start",
1463
+ alignY: "start",
1464
+ width: "100%",
1418
1465
  height: topH,
1419
- originX: "center",
1420
- originY: "center",
1466
+ },
1467
+ props: {
1468
+ originX: "left",
1469
+ originY: "top",
1421
1470
  fill: visual.outerBackground,
1422
1471
  selectable: false,
1423
1472
  evented: false,
@@ -1427,13 +1476,17 @@ export class ImageTool implements Extension {
1427
1476
  id: "image.cropMask.bottom",
1428
1477
  type: "rect",
1429
1478
  data: { id: "image.cropMask.bottom", zIndex: 2 },
1430
- props: {
1431
- left: canvasW / 2,
1432
- top: frameBottom + bottomH / 2,
1433
- width: canvasW,
1479
+ layout: {
1480
+ reference: "custom",
1481
+ referenceRect: viewportRect,
1482
+ alignX: "start",
1483
+ alignY: "end",
1484
+ width: "100%",
1434
1485
  height: bottomH,
1435
- originX: "center",
1436
- originY: "center",
1486
+ },
1487
+ props: {
1488
+ originX: "left",
1489
+ originY: "top",
1437
1490
  fill: visual.outerBackground,
1438
1491
  selectable: false,
1439
1492
  evented: false,
@@ -1443,13 +1496,17 @@ export class ImageTool implements Extension {
1443
1496
  id: "image.cropMask.left",
1444
1497
  type: "rect",
1445
1498
  data: { id: "image.cropMask.left", zIndex: 3 },
1446
- props: {
1447
- left: leftW / 2,
1448
- top: frameTop + visibleFrameH / 2,
1499
+ layout: {
1500
+ reference: "custom",
1501
+ referenceRect: visibleFrameBandRect,
1502
+ alignX: "start",
1503
+ alignY: "start",
1449
1504
  width: leftW,
1450
- height: visibleFrameH,
1451
- originX: "center",
1452
- originY: "center",
1505
+ height: "100%",
1506
+ },
1507
+ props: {
1508
+ originX: "left",
1509
+ originY: "top",
1453
1510
  fill: visual.outerBackground,
1454
1511
  selectable: false,
1455
1512
  evented: false,
@@ -1459,13 +1516,17 @@ export class ImageTool implements Extension {
1459
1516
  id: "image.cropMask.right",
1460
1517
  type: "rect",
1461
1518
  data: { id: "image.cropMask.right", zIndex: 4 },
1462
- props: {
1463
- left: frameRight + rightW / 2,
1464
- top: frameTop + visibleFrameH / 2,
1519
+ layout: {
1520
+ reference: "custom",
1521
+ referenceRect: visibleFrameBandRect,
1522
+ alignX: "end",
1523
+ alignY: "start",
1465
1524
  width: rightW,
1466
- height: visibleFrameH,
1467
- originX: "center",
1468
- originY: "center",
1525
+ height: "100%",
1526
+ },
1527
+ props: {
1528
+ originX: "left",
1529
+ originY: "top",
1469
1530
  fill: visual.outerBackground,
1470
1531
  selectable: false,
1471
1532
  evented: false,
@@ -1477,29 +1538,36 @@ export class ImageTool implements Extension {
1477
1538
  id: "image.cropFrame",
1478
1539
  type: "rect",
1479
1540
  data: { id: "image.cropFrame", zIndex: 7 },
1541
+ layout: {
1542
+ reference: "custom",
1543
+ referenceRect: frameRect,
1544
+ alignX: "start",
1545
+ alignY: "start",
1546
+ width: "100%",
1547
+ height: "100%",
1548
+ },
1480
1549
  props: {
1481
- left: frame.left + frame.width / 2,
1482
- top: frame.top + frame.height / 2,
1483
- width: frame.width,
1484
- height: frame.height,
1485
- originX: "center",
1486
- originY: "center",
1550
+ originX: "left",
1551
+ originY: "top",
1487
1552
  fill: visual.innerBackground,
1488
1553
  stroke:
1489
1554
  visual.strokeStyle === "hidden"
1490
1555
  ? "rgba(0,0,0,0)"
1491
1556
  : visual.strokeColor,
1492
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
1557
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
1493
1558
  strokeDashArray:
1494
1559
  visual.strokeStyle === "dashed"
1495
- ? [visual.dashLength, visual.dashLength]
1560
+ ? [dashLengthScene, dashLengthScene]
1496
1561
  : undefined,
1497
1562
  selectable: false,
1498
1563
  evented: false,
1499
1564
  },
1500
1565
  };
1501
1566
 
1502
- const specs = [...mask, ...shapeOverlay, frameSpec];
1567
+ const specs =
1568
+ shapeOverlay.length > 0
1569
+ ? [...mask, ...shapeOverlay]
1570
+ : [...mask, ...shapeOverlay, frameSpec];
1503
1571
  this.debug("overlay:built", {
1504
1572
  frame,
1505
1573
  shape: sceneGeometry?.shape,
@@ -1530,35 +1598,38 @@ export class ImageTool implements Extension {
1530
1598
  });
1531
1599
  }
1532
1600
 
1533
- this.getImageObjects().forEach((obj: any) => {
1534
- const id = obj?.data?.id;
1535
- if (typeof id === "string" && !desiredIds.has(id)) {
1536
- this.canvasService?.canvas.remove(obj);
1537
- }
1538
- });
1539
-
1540
- for (const item of renderItems) {
1541
- if (seq !== this.renderSeq) return;
1542
- await this.upsertImageObject(item, frame, seq);
1543
- }
1601
+ const imageSpecs = await this.buildImageSpecs(renderItems, frame);
1544
1602
  if (seq !== this.renderSeq) return;
1545
1603
 
1546
- this.syncImageZOrder(renderItems);
1547
1604
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
1548
1605
  if (seq !== this.renderSeq) return;
1549
1606
 
1550
- const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1551
- await this.canvasService.applyObjectSpecsToRootLayer(
1552
- IMAGE_OVERLAY_LAYER_ID,
1553
- overlaySpecs,
1554
- );
1555
- this.syncImageZOrder(renderItems);
1607
+ this.imageSpecs = imageSpecs;
1608
+ this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1609
+ await this.canvasService.flushRenderFromProducers();
1610
+ if (seq !== this.renderSeq) return;
1611
+
1612
+ renderItems.forEach((item) => {
1613
+ if (!this.getImageObject(item.id)) return;
1614
+ const resolver = this.loadResolvers.get(item.id);
1615
+ if (!resolver) return;
1616
+ resolver();
1617
+ this.loadResolvers.delete(item.id);
1618
+ });
1619
+
1620
+ if (this.focusedImageId && this.isToolActive) {
1621
+ this.setImageFocus(this.focusedImageId, {
1622
+ syncCanvasSelection: true,
1623
+ skipRender: true,
1624
+ });
1625
+ }
1626
+
1556
1627
  const overlayCanvasCount = this.getOverlayObjects().length;
1557
1628
 
1558
1629
  this.debug("render:done", {
1559
1630
  seq,
1560
1631
  renderCount: renderItems.length,
1561
- overlayCount: overlaySpecs.length,
1632
+ overlayCount: this.overlaySpecs.length,
1562
1633
  overlayCanvasCount,
1563
1634
  isToolActive: this.isToolActive,
1564
1635
  isImageSelectionActive: this.isImageSelectionActive,
@@ -1584,8 +1655,12 @@ export class ImageTool implements Extension {
1584
1655
  const center = target.getCenterPoint
1585
1656
  ? target.getCenterPoint()
1586
1657
  : new Point(target.left ?? 0, target.top ?? 0);
1658
+ const centerScene = this.canvasService
1659
+ ? this.canvasService.toScenePoint({ x: center.x, y: center.y })
1660
+ : { x: center.x, y: center.y };
1587
1661
 
1588
1662
  const objectScale = Number.isFinite(target?.scaleX) ? target.scaleX : 1;
1663
+ const objectScaleScene = this.toSceneObjectScale(objectScale || 1);
1589
1664
 
1590
1665
  const workingItem = this.workingItems.find((item) => item.id === id);
1591
1666
  const sourceKey = workingItem?.sourceUrl || workingItem?.url || "";
@@ -1593,10 +1668,10 @@ export class ImageTool implements Extension {
1593
1668
  const coverScale = this.getCoverScale(frame, sourceSize);
1594
1669
 
1595
1670
  const updates: Partial<ImageItem> = {
1596
- left: this.clampNormalized((center.x - frame.left) / frame.width),
1597
- top: this.clampNormalized((center.y - frame.top) / frame.height),
1671
+ left: this.clampNormalized((centerScene.x - frame.left) / frame.width),
1672
+ top: this.clampNormalized((centerScene.y - frame.top) / frame.height),
1598
1673
  angle: Number.isFinite(target.angle) ? target.angle : 0,
1599
- scale: Math.max(0.05, (objectScale || 1) / coverScale),
1674
+ scale: Math.max(0.05, objectScaleScene / coverScale),
1600
1675
  };
1601
1676
 
1602
1677
  this.focusedImageId = id;
@@ -1691,7 +1766,7 @@ export class ImageTool implements Extension {
1691
1766
  const frame = this.getFrameRect();
1692
1767
  const coverScale = this.getCoverScale(frame, source);
1693
1768
 
1694
- const currentScale = obj.scaleX || 1;
1769
+ const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
1695
1770
  const zoom = Math.max(0.05, currentScale / coverScale);
1696
1771
 
1697
1772
  const updated: Partial<ImageItem> = {
@@ -1739,16 +1814,21 @@ export class ImageTool implements Extension {
1739
1814
  Math.max(1, area.height) / Math.max(1, source.height),
1740
1815
  );
1741
1816
 
1742
- const canvasW = this.canvasService.canvas.width || 1;
1743
- const canvasH = this.canvasService.canvas.height || 1;
1817
+ const viewport = this.canvasService.getSceneViewportRect();
1818
+ const canvasW = viewport.width || 1;
1819
+ const canvasH = viewport.height || 1;
1744
1820
 
1745
1821
  const areaLeftInput = area.left ?? 0.5;
1746
1822
  const areaTopInput = area.top ?? 0.5;
1747
1823
 
1748
1824
  const areaLeftPx =
1749
- areaLeftInput <= 1.5 ? areaLeftInput * canvasW : areaLeftInput;
1825
+ areaLeftInput <= 1.5
1826
+ ? viewport.left + areaLeftInput * canvasW
1827
+ : areaLeftInput;
1750
1828
  const areaTopPx =
1751
- areaTopInput <= 1.5 ? areaTopInput * canvasH : areaTopInput;
1829
+ areaTopInput <= 1.5
1830
+ ? viewport.top + areaTopInput * canvasH
1831
+ : areaTopInput;
1752
1832
 
1753
1833
  const updates: Partial<ImageItem> = {
1754
1834
  scale: Math.max(0.05, desiredScale / baseCover),
@@ -1827,7 +1907,8 @@ export class ImageTool implements Extension {
1827
1907
  throw new Error("image-ids-required");
1828
1908
  }
1829
1909
 
1830
- const frame = this.getFrameRect();
1910
+ const frameScene = this.getFrameRect();
1911
+ const frame = this.getFrameRectScreen(frameScene);
1831
1912
  const multiplier = Math.max(1, options.multiplier ?? 2);
1832
1913
  const format: "png" | "jpeg" = options.format === "jpeg" ? "jpeg" : "png";
1833
1914