@pooder/kit 5.3.0 → 5.4.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 (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +249 -36
  3. package/dist/index.d.ts +249 -36
  4. package/dist/index.js +2374 -1049
  5. package/dist/index.mjs +2375 -1050
  6. package/package.json +1 -1
  7. package/src/extensions/background.ts +178 -85
  8. package/src/extensions/dieline.ts +1149 -1030
  9. package/src/extensions/dielineShape.ts +109 -0
  10. package/src/extensions/feature.ts +482 -366
  11. package/src/extensions/film.ts +148 -76
  12. package/src/extensions/geometry.ts +210 -44
  13. package/src/extensions/image.ts +244 -114
  14. package/src/extensions/ruler.ts +471 -268
  15. package/src/extensions/sceneLayoutModel.ts +28 -6
  16. package/src/extensions/sceneVisibility.ts +3 -10
  17. package/src/extensions/tracer.ts +1019 -980
  18. package/src/extensions/white-ink.ts +284 -231
  19. package/src/services/CanvasService.ts +543 -11
  20. package/src/services/renderSpec.ts +37 -2
  21. package/.test-dist/src/CanvasService.js +0 -249
  22. package/.test-dist/src/ViewportSystem.js +0 -75
  23. package/.test-dist/src/background.js +0 -203
  24. package/.test-dist/src/bridgeSelection.js +0 -20
  25. package/.test-dist/src/constraints.js +0 -237
  26. package/.test-dist/src/coordinate.js +0 -74
  27. package/.test-dist/src/dieline.js +0 -818
  28. package/.test-dist/src/edgeScale.js +0 -12
  29. package/.test-dist/src/extensions/background.js +0 -203
  30. package/.test-dist/src/extensions/bridgeSelection.js +0 -20
  31. package/.test-dist/src/extensions/constraints.js +0 -237
  32. package/.test-dist/src/extensions/dieline.js +0 -828
  33. package/.test-dist/src/extensions/edgeScale.js +0 -12
  34. package/.test-dist/src/extensions/feature.js +0 -825
  35. package/.test-dist/src/extensions/featureComplete.js +0 -32
  36. package/.test-dist/src/extensions/film.js +0 -167
  37. package/.test-dist/src/extensions/geometry.js +0 -545
  38. package/.test-dist/src/extensions/image.js +0 -1529
  39. package/.test-dist/src/extensions/index.js +0 -30
  40. package/.test-dist/src/extensions/maskOps.js +0 -279
  41. package/.test-dist/src/extensions/mirror.js +0 -104
  42. package/.test-dist/src/extensions/ruler.js +0 -345
  43. package/.test-dist/src/extensions/sceneLayout.js +0 -96
  44. package/.test-dist/src/extensions/sceneLayoutModel.js +0 -196
  45. package/.test-dist/src/extensions/sceneVisibility.js +0 -62
  46. package/.test-dist/src/extensions/size.js +0 -331
  47. package/.test-dist/src/extensions/tracer.js +0 -538
  48. package/.test-dist/src/extensions/white-ink.js +0 -1190
  49. package/.test-dist/src/extensions/wrappedOffsets.js +0 -33
  50. package/.test-dist/src/feature.js +0 -826
  51. package/.test-dist/src/featureComplete.js +0 -32
  52. package/.test-dist/src/film.js +0 -167
  53. package/.test-dist/src/geometry.js +0 -506
  54. package/.test-dist/src/image.js +0 -1250
  55. package/.test-dist/src/index.js +0 -18
  56. package/.test-dist/src/maskOps.js +0 -270
  57. package/.test-dist/src/mirror.js +0 -104
  58. package/.test-dist/src/renderSpec.js +0 -2
  59. package/.test-dist/src/ruler.js +0 -343
  60. package/.test-dist/src/sceneLayout.js +0 -99
  61. package/.test-dist/src/sceneLayoutModel.js +0 -196
  62. package/.test-dist/src/sceneView.js +0 -40
  63. package/.test-dist/src/sceneVisibility.js +0 -42
  64. package/.test-dist/src/services/CanvasService.js +0 -249
  65. package/.test-dist/src/services/ViewportSystem.js +0 -76
  66. package/.test-dist/src/services/index.js +0 -24
  67. package/.test-dist/src/services/renderSpec.js +0 -2
  68. package/.test-dist/src/size.js +0 -332
  69. package/.test-dist/src/tracer.js +0 -544
  70. package/.test-dist/src/units.js +0 -30
  71. package/.test-dist/src/white-ink.js +0 -829
  72. package/.test-dist/src/wrappedOffsets.js +0 -33
  73. package/.test-dist/tests/run.js +0 -94
@@ -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,9 @@ 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 overlaySpecs: RenderObjectSpec[] = [];
141
+ private renderProducerDisposable?: { dispose: () => void };
137
142
 
138
143
  activate(context: ExtensionContext) {
139
144
  this.context = context;
@@ -142,6 +147,16 @@ export class ImageTool implements Extension {
142
147
  console.warn("CanvasService not found for ImageTool");
143
148
  return;
144
149
  }
150
+ this.renderProducerDisposable?.dispose();
151
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
152
+ this.id,
153
+ () => ({
154
+ rootLayerSpecs: {
155
+ [IMAGE_OVERLAY_LAYER_ID]: this.overlaySpecs,
156
+ },
157
+ }),
158
+ { priority: 300 },
159
+ );
145
160
 
146
161
  context.eventBus.on("tool:activated", this.onToolActivated);
147
162
  context.eventBus.on("object:modified", this.onObjectModified);
@@ -202,13 +217,14 @@ export class ImageTool implements Extension {
202
217
  this.dirtyTrackerDisposable = undefined;
203
218
  this.cropShapeHatchPattern = undefined;
204
219
  this.cropShapeHatchPatternColor = undefined;
220
+ this.cropShapeHatchPatternKey = undefined;
221
+ this.overlaySpecs = [];
205
222
 
206
223
  this.clearRenderedImages();
224
+ this.renderProducerDisposable?.dispose();
225
+ this.renderProducerDisposable = undefined;
207
226
  if (this.canvasService) {
208
- void this.canvasService.applyObjectSpecsToRootLayer(
209
- IMAGE_OVERLAY_LAYER_ID,
210
- [],
211
- );
227
+ void this.canvasService.flushRenderFromProducers();
212
228
  this.canvasService = undefined;
213
229
  }
214
230
  this.context = undefined;
@@ -766,47 +782,41 @@ export class ImageTool implements Extension {
766
782
  return { left: 0, top: 0, width: 0, height: 0 };
767
783
  }
768
784
 
769
- return {
785
+ return this.canvasService.toSceneRect({
770
786
  left: layout.cutRect.left,
771
787
  top: layout.cutRect.top,
772
788
  width: layout.cutRect.width,
773
789
  height: layout.cutRect.height,
774
- };
790
+ });
775
791
  }
776
792
 
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;
793
+ private getFrameRectScreen(frame?: FrameRect): FrameRect {
794
+ if (!this.canvasService) {
795
+ return { left: 0, top: 0, width: 0, height: 0 };
796
+ }
797
+ return this.canvasService.toScreenRect(frame || this.getFrameRect());
798
+ }
781
799
 
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
- }
800
+ private toLayoutSceneRect(rect: FrameRect): RenderLayoutRect {
801
+ return {
802
+ left: rect.left,
803
+ top: rect.top,
804
+ width: rect.width,
805
+ height: rect.height,
806
+ space: "scene",
807
+ };
808
+ }
800
809
 
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
- }
810
+ private async resolveDefaultFitArea(): Promise<DielineFitArea | null> {
811
+ if (!this.canvasService) return null;
812
+ const frame = this.getFrameRect();
813
+ if (frame.width <= 0 || frame.height <= 0) return null;
814
+ return {
815
+ width: Math.max(1, frame.width),
816
+ height: Math.max(1, frame.height),
817
+ left: frame.left + frame.width / 2,
818
+ top: frame.top + frame.height / 2,
819
+ };
810
820
  }
811
821
 
812
822
  private async fitImageToDefaultArea(id: string) {
@@ -818,13 +828,14 @@ export class ImageTool implements Extension {
818
828
  return;
819
829
  }
820
830
 
821
- const canvasW = Math.max(1, this.canvasService.canvas.width || 0);
822
- const canvasH = Math.max(1, this.canvasService.canvas.height || 0);
831
+ const viewport = this.canvasService.getSceneViewportRect();
832
+ const canvasW = Math.max(1, viewport.width || 0);
833
+ const canvasH = Math.max(1, viewport.height || 0);
823
834
  await this.fitImageToArea(id, {
824
835
  width: canvasW,
825
836
  height: canvasH,
826
- left: canvasW / 2,
827
- top: canvasH / 2,
837
+ left: viewport.left + canvasW / 2,
838
+ top: viewport.top + canvasH / 2,
828
839
  });
829
840
  }
830
841
 
@@ -929,19 +940,24 @@ export class ImageTool implements Extension {
929
940
 
930
941
  private toSceneGeometryLike(raw: any): SceneGeometryLike | null {
931
942
  const shape = raw?.shape;
932
- if (
933
- shape !== "rect" &&
934
- shape !== "circle" &&
935
- shape !== "ellipse" &&
936
- shape !== "custom"
937
- ) {
943
+ if (!isDielineShape(shape)) {
938
944
  return null;
939
945
  }
940
946
 
941
- const radius = Number(raw?.radius);
942
- const offset = Number(raw?.offset);
947
+ const radiusRaw = Number(raw?.radius);
948
+ const offsetRaw = Number(raw?.offset);
949
+ const unit = typeof raw?.unit === "string" ? raw.unit : "px";
950
+ const radius =
951
+ unit === "scene" || !this.canvasService
952
+ ? radiusRaw
953
+ : this.canvasService.toSceneLength(radiusRaw);
954
+ const offset =
955
+ unit === "scene" || !this.canvasService
956
+ ? offsetRaw
957
+ : this.canvasService.toSceneLength(offsetRaw);
943
958
  return {
944
959
  shape,
960
+ shapeStyle: normalizeShapeStyle(raw?.shapeStyle),
945
961
  radius: Number.isFinite(radius) ? radius : 0,
946
962
  offset: Number.isFinite(offset) ? offset : 0,
947
963
  };
@@ -1008,9 +1024,12 @@ export class ImageTool implements Extension {
1008
1024
  color = "rgba(255, 0, 0, 0.6)",
1009
1025
  ): Pattern | undefined {
1010
1026
  if (typeof document === "undefined") return undefined;
1027
+ const sceneScale = this.canvasService?.getSceneScale() || 1;
1028
+ const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
1011
1029
  if (
1012
1030
  this.cropShapeHatchPattern &&
1013
- this.cropShapeHatchPatternColor === color
1031
+ this.cropShapeHatchPatternColor === color &&
1032
+ this.cropShapeHatchPatternKey === cacheKey
1014
1033
  ) {
1015
1034
  return this.cropShapeHatchPattern;
1016
1035
  }
@@ -1043,8 +1062,18 @@ export class ImageTool implements Extension {
1043
1062
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1044
1063
  repetition: "repeat",
1045
1064
  });
1065
+ // Scene specs are scaled to screen by CanvasService; keep hatch density in screen pixels.
1066
+ (pattern as any).patternTransform = [
1067
+ 1 / sceneScale,
1068
+ 0,
1069
+ 0,
1070
+ 1 / sceneScale,
1071
+ 0,
1072
+ 0,
1073
+ ];
1046
1074
  this.cropShapeHatchPattern = pattern;
1047
1075
  this.cropShapeHatchPatternColor = color;
1076
+ this.cropShapeHatchPatternKey = cacheKey;
1048
1077
  return pattern;
1049
1078
  }
1050
1079
 
@@ -1062,6 +1091,7 @@ export class ImageTool implements Extension {
1062
1091
  }
1063
1092
 
1064
1093
  const shape = sceneGeometry.shape as ShapeOverlayShape;
1094
+ const shapeStyle = sceneGeometry.shapeStyle;
1065
1095
  const inset = 0;
1066
1096
  const shapeWidth = Math.max(1, frame.width);
1067
1097
  const shapeHeight = Math.max(1, frame.height);
@@ -1072,6 +1102,7 @@ export class ImageTool implements Extension {
1072
1102
  frameWidth: frame.width,
1073
1103
  frameHeight: frame.height,
1074
1104
  offset: sceneGeometry.offset,
1105
+ shapeStyle,
1075
1106
  inset,
1076
1107
  shapeWidth,
1077
1108
  shapeHeight,
@@ -1097,6 +1128,7 @@ export class ImageTool implements Extension {
1097
1128
  x: frame.width / 2,
1098
1129
  y: frame.height / 2,
1099
1130
  features: [],
1131
+ shapeStyle,
1100
1132
  canvasWidth: frame.width,
1101
1133
  canvasHeight: frame.height,
1102
1134
  };
@@ -1116,6 +1148,9 @@ export class ImageTool implements Extension {
1116
1148
 
1117
1149
  const patternFill = this.getCropShapeHatchPattern();
1118
1150
  const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
1151
+ const shapeBounds = getPathBounds(shapePathData);
1152
+ const hatchBounds = getPathBounds(hatchPathData);
1153
+ const frameRect = this.toLayoutSceneRect(frame);
1119
1154
  const hatchPathLength = hatchPathData.length;
1120
1155
  const shapePathLength = shapePathData.length;
1121
1156
  const specs: RenderObjectSpec[] = [
@@ -1123,10 +1158,16 @@ export class ImageTool implements Extension {
1123
1158
  id: "image.cropShapeHatch",
1124
1159
  type: "path",
1125
1160
  data: { id: "image.cropShapeHatch", zIndex: 5 },
1161
+ layout: {
1162
+ reference: "custom",
1163
+ referenceRect: frameRect,
1164
+ alignX: "start",
1165
+ alignY: "start",
1166
+ offsetX: hatchBounds.x,
1167
+ offsetY: hatchBounds.y,
1168
+ },
1126
1169
  props: {
1127
1170
  pathData: hatchPathData,
1128
- left: frame.left,
1129
- top: frame.top,
1130
1171
  originX: "left",
1131
1172
  originY: "top",
1132
1173
  fill: hatchFill,
@@ -1143,15 +1184,21 @@ export class ImageTool implements Extension {
1143
1184
  id: "image.cropShapePath",
1144
1185
  type: "path",
1145
1186
  data: { id: "image.cropShapePath", zIndex: 6 },
1187
+ layout: {
1188
+ reference: "custom",
1189
+ referenceRect: frameRect,
1190
+ alignX: "start",
1191
+ alignY: "start",
1192
+ offsetX: shapeBounds.x,
1193
+ offsetY: shapeBounds.y,
1194
+ },
1146
1195
  props: {
1147
1196
  pathData: shapePathData,
1148
- left: frame.left,
1149
- top: frame.top,
1150
1197
  originX: "left",
1151
1198
  originY: "top",
1152
1199
  fill: "rgba(0,0,0,0)",
1153
1200
  stroke: "rgba(255, 0, 0, 0.9)",
1154
- strokeWidth: 1,
1201
+ strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
1155
1202
  selectable: false,
1156
1203
  evented: false,
1157
1204
  excludeFromExport: true,
@@ -1168,6 +1215,8 @@ export class ImageTool implements Extension {
1168
1215
  fillRule: "evenodd",
1169
1216
  shapePathLength,
1170
1217
  hatchPathLength,
1218
+ shapeBounds,
1219
+ hatchBounds,
1171
1220
  hatchFillType:
1172
1221
  hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
1173
1222
  ids: specs.map((spec) => spec.id),
@@ -1241,6 +1290,30 @@ export class ImageTool implements Extension {
1241
1290
  };
1242
1291
  }
1243
1292
 
1293
+ private toScreenObjectProps(props: Record<string, any>): Record<string, any> {
1294
+ if (!this.canvasService) return props;
1295
+ const next = { ...props };
1296
+ if (Number.isFinite(next.left) || Number.isFinite(next.top)) {
1297
+ const mapped = this.canvasService.toScreenPoint({
1298
+ x: Number.isFinite(next.left) ? Number(next.left) : 0,
1299
+ y: Number.isFinite(next.top) ? Number(next.top) : 0,
1300
+ });
1301
+ if (Number.isFinite(next.left)) next.left = mapped.x;
1302
+ if (Number.isFinite(next.top)) next.top = mapped.y;
1303
+ }
1304
+ const sceneScale = this.canvasService.getSceneScale();
1305
+ const sx = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
1306
+ const sy = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
1307
+ next.scaleX = sx * sceneScale;
1308
+ next.scaleY = sy * sceneScale;
1309
+ return next;
1310
+ }
1311
+
1312
+ private toSceneObjectScale(value: number): number {
1313
+ if (!this.canvasService) return value;
1314
+ return value / this.canvasService.getSceneScale();
1315
+ }
1316
+
1244
1317
  private getCurrentSrc(obj: any): string | undefined {
1245
1318
  if (!obj) return undefined;
1246
1319
  if (typeof obj.getSrc === "function") return obj.getSrc();
@@ -1300,9 +1373,10 @@ export class ImageTool implements Extension {
1300
1373
  this.rememberSourceSize(render.src, obj);
1301
1374
  const sourceSize = this.getSourceSize(render.src, obj);
1302
1375
  const props = this.computeCanvasProps(render, sourceSize, frame);
1376
+ const screenProps = this.toScreenObjectProps(props);
1303
1377
 
1304
1378
  obj.set({
1305
- ...props,
1379
+ ...screenProps,
1306
1380
  data: {
1307
1381
  ...(obj.data || {}),
1308
1382
  id: item.id,
@@ -1384,26 +1458,52 @@ export class ImageTool implements Extension {
1384
1458
  return [];
1385
1459
  }
1386
1460
 
1387
- const canvasW = this.canvasService.canvas.width || 0;
1388
- const canvasH = this.canvasService.canvas.height || 0;
1461
+ const viewport = this.canvasService.getSceneViewportRect();
1462
+ const canvasW = viewport.width || 0;
1463
+ const canvasH = viewport.height || 0;
1464
+ const canvasLeft = viewport.left || 0;
1465
+ const canvasTop = viewport.top || 0;
1389
1466
  const visual = this.getFrameVisualConfig();
1467
+ const strokeWidthScene = this.canvasService.toSceneLength(
1468
+ visual.strokeWidth,
1469
+ );
1470
+ const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
1390
1471
 
1391
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
1392
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
1472
+ const frameLeft = Math.max(
1473
+ canvasLeft,
1474
+ Math.min(canvasLeft + canvasW, frame.left),
1475
+ );
1476
+ const frameTop = Math.max(
1477
+ canvasTop,
1478
+ Math.min(canvasTop + canvasH, frame.top),
1479
+ );
1393
1480
  const frameRight = Math.max(
1394
1481
  frameLeft,
1395
- Math.min(canvasW, frame.left + frame.width),
1482
+ Math.min(canvasLeft + canvasW, frame.left + frame.width),
1396
1483
  );
1397
1484
  const frameBottom = Math.max(
1398
1485
  frameTop,
1399
- Math.min(canvasH, frame.top + frame.height),
1486
+ Math.min(canvasTop + canvasH, frame.top + frame.height),
1400
1487
  );
1401
1488
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
1402
1489
 
1403
- const topH = frameTop;
1404
- const bottomH = Math.max(0, canvasH - frameBottom);
1405
- const leftW = frameLeft;
1406
- const rightW = Math.max(0, canvasW - frameRight);
1490
+ const topH = Math.max(0, frameTop - canvasTop);
1491
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
1492
+ const leftW = Math.max(0, frameLeft - canvasLeft);
1493
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
1494
+ const viewportRect = this.toLayoutSceneRect({
1495
+ left: canvasLeft,
1496
+ top: canvasTop,
1497
+ width: canvasW,
1498
+ height: canvasH,
1499
+ });
1500
+ const visibleFrameBandRect = this.toLayoutSceneRect({
1501
+ left: canvasLeft,
1502
+ top: frameTop,
1503
+ width: canvasW,
1504
+ height: visibleFrameH,
1505
+ });
1506
+ const frameRect = this.toLayoutSceneRect(frame);
1407
1507
  const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
1408
1508
 
1409
1509
  const mask: RenderObjectSpec[] = [
@@ -1411,13 +1511,17 @@ export class ImageTool implements Extension {
1411
1511
  id: "image.cropMask.top",
1412
1512
  type: "rect",
1413
1513
  data: { id: "image.cropMask.top", zIndex: 1 },
1414
- props: {
1415
- left: canvasW / 2,
1416
- top: topH / 2,
1417
- width: canvasW,
1514
+ layout: {
1515
+ reference: "custom",
1516
+ referenceRect: viewportRect,
1517
+ alignX: "start",
1518
+ alignY: "start",
1519
+ width: "100%",
1418
1520
  height: topH,
1419
- originX: "center",
1420
- originY: "center",
1521
+ },
1522
+ props: {
1523
+ originX: "left",
1524
+ originY: "top",
1421
1525
  fill: visual.outerBackground,
1422
1526
  selectable: false,
1423
1527
  evented: false,
@@ -1427,13 +1531,17 @@ export class ImageTool implements Extension {
1427
1531
  id: "image.cropMask.bottom",
1428
1532
  type: "rect",
1429
1533
  data: { id: "image.cropMask.bottom", zIndex: 2 },
1430
- props: {
1431
- left: canvasW / 2,
1432
- top: frameBottom + bottomH / 2,
1433
- width: canvasW,
1534
+ layout: {
1535
+ reference: "custom",
1536
+ referenceRect: viewportRect,
1537
+ alignX: "start",
1538
+ alignY: "end",
1539
+ width: "100%",
1434
1540
  height: bottomH,
1435
- originX: "center",
1436
- originY: "center",
1541
+ },
1542
+ props: {
1543
+ originX: "left",
1544
+ originY: "top",
1437
1545
  fill: visual.outerBackground,
1438
1546
  selectable: false,
1439
1547
  evented: false,
@@ -1443,13 +1551,17 @@ export class ImageTool implements Extension {
1443
1551
  id: "image.cropMask.left",
1444
1552
  type: "rect",
1445
1553
  data: { id: "image.cropMask.left", zIndex: 3 },
1446
- props: {
1447
- left: leftW / 2,
1448
- top: frameTop + visibleFrameH / 2,
1554
+ layout: {
1555
+ reference: "custom",
1556
+ referenceRect: visibleFrameBandRect,
1557
+ alignX: "start",
1558
+ alignY: "start",
1449
1559
  width: leftW,
1450
- height: visibleFrameH,
1451
- originX: "center",
1452
- originY: "center",
1560
+ height: "100%",
1561
+ },
1562
+ props: {
1563
+ originX: "left",
1564
+ originY: "top",
1453
1565
  fill: visual.outerBackground,
1454
1566
  selectable: false,
1455
1567
  evented: false,
@@ -1459,13 +1571,17 @@ export class ImageTool implements Extension {
1459
1571
  id: "image.cropMask.right",
1460
1572
  type: "rect",
1461
1573
  data: { id: "image.cropMask.right", zIndex: 4 },
1462
- props: {
1463
- left: frameRight + rightW / 2,
1464
- top: frameTop + visibleFrameH / 2,
1574
+ layout: {
1575
+ reference: "custom",
1576
+ referenceRect: visibleFrameBandRect,
1577
+ alignX: "end",
1578
+ alignY: "start",
1465
1579
  width: rightW,
1466
- height: visibleFrameH,
1467
- originX: "center",
1468
- originY: "center",
1580
+ height: "100%",
1581
+ },
1582
+ props: {
1583
+ originX: "left",
1584
+ originY: "top",
1469
1585
  fill: visual.outerBackground,
1470
1586
  selectable: false,
1471
1587
  evented: false,
@@ -1477,22 +1593,26 @@ export class ImageTool implements Extension {
1477
1593
  id: "image.cropFrame",
1478
1594
  type: "rect",
1479
1595
  data: { id: "image.cropFrame", zIndex: 7 },
1596
+ layout: {
1597
+ reference: "custom",
1598
+ referenceRect: frameRect,
1599
+ alignX: "start",
1600
+ alignY: "start",
1601
+ width: "100%",
1602
+ height: "100%",
1603
+ },
1480
1604
  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",
1605
+ originX: "left",
1606
+ originY: "top",
1487
1607
  fill: visual.innerBackground,
1488
1608
  stroke:
1489
1609
  visual.strokeStyle === "hidden"
1490
1610
  ? "rgba(0,0,0,0)"
1491
1611
  : visual.strokeColor,
1492
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
1612
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
1493
1613
  strokeDashArray:
1494
1614
  visual.strokeStyle === "dashed"
1495
- ? [visual.dashLength, visual.dashLength]
1615
+ ? [dashLengthScene, dashLengthScene]
1496
1616
  : undefined,
1497
1617
  selectable: false,
1498
1618
  evented: false,
@@ -1548,10 +1668,8 @@ export class ImageTool implements Extension {
1548
1668
  if (seq !== this.renderSeq) return;
1549
1669
 
1550
1670
  const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1551
- await this.canvasService.applyObjectSpecsToRootLayer(
1552
- IMAGE_OVERLAY_LAYER_ID,
1553
- overlaySpecs,
1554
- );
1671
+ this.overlaySpecs = overlaySpecs;
1672
+ await this.canvasService.flushRenderFromProducers();
1555
1673
  this.syncImageZOrder(renderItems);
1556
1674
  const overlayCanvasCount = this.getOverlayObjects().length;
1557
1675
 
@@ -1584,8 +1702,12 @@ export class ImageTool implements Extension {
1584
1702
  const center = target.getCenterPoint
1585
1703
  ? target.getCenterPoint()
1586
1704
  : new Point(target.left ?? 0, target.top ?? 0);
1705
+ const centerScene = this.canvasService
1706
+ ? this.canvasService.toScenePoint({ x: center.x, y: center.y })
1707
+ : { x: center.x, y: center.y };
1587
1708
 
1588
1709
  const objectScale = Number.isFinite(target?.scaleX) ? target.scaleX : 1;
1710
+ const objectScaleScene = this.toSceneObjectScale(objectScale || 1);
1589
1711
 
1590
1712
  const workingItem = this.workingItems.find((item) => item.id === id);
1591
1713
  const sourceKey = workingItem?.sourceUrl || workingItem?.url || "";
@@ -1593,10 +1715,10 @@ export class ImageTool implements Extension {
1593
1715
  const coverScale = this.getCoverScale(frame, sourceSize);
1594
1716
 
1595
1717
  const updates: Partial<ImageItem> = {
1596
- left: this.clampNormalized((center.x - frame.left) / frame.width),
1597
- top: this.clampNormalized((center.y - frame.top) / frame.height),
1718
+ left: this.clampNormalized((centerScene.x - frame.left) / frame.width),
1719
+ top: this.clampNormalized((centerScene.y - frame.top) / frame.height),
1598
1720
  angle: Number.isFinite(target.angle) ? target.angle : 0,
1599
- scale: Math.max(0.05, (objectScale || 1) / coverScale),
1721
+ scale: Math.max(0.05, objectScaleScene / coverScale),
1600
1722
  };
1601
1723
 
1602
1724
  this.focusedImageId = id;
@@ -1691,7 +1813,7 @@ export class ImageTool implements Extension {
1691
1813
  const frame = this.getFrameRect();
1692
1814
  const coverScale = this.getCoverScale(frame, source);
1693
1815
 
1694
- const currentScale = obj.scaleX || 1;
1816
+ const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
1695
1817
  const zoom = Math.max(0.05, currentScale / coverScale);
1696
1818
 
1697
1819
  const updated: Partial<ImageItem> = {
@@ -1739,16 +1861,21 @@ export class ImageTool implements Extension {
1739
1861
  Math.max(1, area.height) / Math.max(1, source.height),
1740
1862
  );
1741
1863
 
1742
- const canvasW = this.canvasService.canvas.width || 1;
1743
- const canvasH = this.canvasService.canvas.height || 1;
1864
+ const viewport = this.canvasService.getSceneViewportRect();
1865
+ const canvasW = viewport.width || 1;
1866
+ const canvasH = viewport.height || 1;
1744
1867
 
1745
1868
  const areaLeftInput = area.left ?? 0.5;
1746
1869
  const areaTopInput = area.top ?? 0.5;
1747
1870
 
1748
1871
  const areaLeftPx =
1749
- areaLeftInput <= 1.5 ? areaLeftInput * canvasW : areaLeftInput;
1872
+ areaLeftInput <= 1.5
1873
+ ? viewport.left + areaLeftInput * canvasW
1874
+ : areaLeftInput;
1750
1875
  const areaTopPx =
1751
- areaTopInput <= 1.5 ? areaTopInput * canvasH : areaTopInput;
1876
+ areaTopInput <= 1.5
1877
+ ? viewport.top + areaTopInput * canvasH
1878
+ : areaTopInput;
1752
1879
 
1753
1880
  const updates: Partial<ImageItem> = {
1754
1881
  scale: Math.max(0.05, desiredScale / baseCover),
@@ -1794,6 +1921,8 @@ export class ImageTool implements Extension {
1794
1921
  this.normalizeItem({
1795
1922
  ...item,
1796
1923
  url,
1924
+ // Keep original source for next image-tool session editing,
1925
+ // and use committedUrl as non-image-tools render source.
1797
1926
  sourceUrl,
1798
1927
  committedUrl: url,
1799
1928
  }),
@@ -1825,7 +1954,8 @@ export class ImageTool implements Extension {
1825
1954
  throw new Error("image-ids-required");
1826
1955
  }
1827
1956
 
1828
- const frame = this.getFrameRect();
1957
+ const frameScene = this.getFrameRect();
1958
+ const frame = this.getFrameRectScreen(frameScene);
1829
1959
  const multiplier = Math.max(1, options.multiplier ?? 2);
1830
1960
  const format: "png" | "jpeg" = options.format === "jpeg" ? "jpeg" : "png";
1831
1961