@pooder/kit 6.2.0 → 6.2.2

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,7 @@ 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 sessionOverlay_1 = require("./sessionOverlay");
16
15
  const IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
17
16
  "rotate",
18
17
  "scale",
@@ -427,10 +426,20 @@ class ImageTool {
427
426
  this.canvasService?.requestRenderAll();
428
427
  }
429
428
  }
429
+ clearSnapGuideContext() {
430
+ const topContext = this.canvasService?.canvas.contextTop;
431
+ if (!this.canvasService || !topContext)
432
+ return;
433
+ this.canvasService.canvas.clearContext(topContext);
434
+ }
430
435
  clearSnapPreview() {
436
+ const shouldClearCanvas = this.hasRenderedSnapGuides || !!this.activeSnapX || !!this.activeSnapY;
431
437
  this.activeSnapX = null;
432
438
  this.activeSnapY = null;
433
439
  this.hasRenderedSnapGuides = false;
440
+ if (shouldClearCanvas) {
441
+ this.clearSnapGuideContext();
442
+ }
434
443
  this.canvasService?.requestRenderAll();
435
444
  }
436
445
  endMoveSnapInteraction() {
@@ -816,9 +825,6 @@ class ImageTool {
816
825
  }
817
826
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
818
827
  }
819
- toLayoutSceneRect(rect) {
820
- return (0, frame_1.toLayoutSceneRect)(rect);
821
- }
822
828
  async resolveDefaultFitArea() {
823
829
  if (!this.canvasService)
824
830
  return null;
@@ -941,78 +947,33 @@ class ImageTool {
941
947
  "#f5f5f5",
942
948
  };
943
949
  }
944
- toSceneGeometryLike(raw) {
945
- const shape = raw?.shape;
946
- if (!(0, dielineShape_1.isDielineShape)(shape)) {
950
+ resolveSessionOverlayState() {
951
+ if (!this.canvasService || !this.context) {
947
952
  return null;
948
953
  }
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
954
  const configService = this.context.services.get("ConfigurationService");
988
- if (!configService)
955
+ if (!configService) {
989
956
  return null;
990
- const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
991
- const layout = (0, sceneLayoutModel_1.computeSceneLayout)(this.canvasService, sizeState);
957
+ }
958
+ const layout = (0, sceneLayoutModel_1.computeSceneLayout)(this.canvasService, (0, sceneLayoutModel_1.readSizeState)(configService));
992
959
  if (!layout) {
993
- this.debug("overlay:sceneGeometry:fallback:missing-layout");
960
+ this.debug("overlay:layout:missing");
994
961
  return null;
995
962
  }
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));
963
+ const geometry = (0, sceneLayoutModel_1.buildSceneGeometry)(configService, layout);
964
+ this.debug("overlay:state:resolved", {
965
+ cutRect: layout.cutRect,
966
+ shape: geometry.shape,
967
+ shapeStyle: geometry.shapeStyle,
968
+ radius: geometry.radius,
969
+ offset: geometry.offset,
970
+ });
971
+ return { layout, geometry };
1010
972
  }
1011
973
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
1012
974
  if (typeof document === "undefined")
1013
975
  return undefined;
1014
- const sceneScale = this.canvasService?.getSceneScale() || 1;
1015
- const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
976
+ const cacheKey = color;
1016
977
  if (this.cropShapeHatchPattern &&
1017
978
  this.cropShapeHatchPatternColor === color &&
1018
979
  this.cropShapeHatchPatternKey === cacheKey) {
@@ -1045,140 +1006,11 @@ class ImageTool {
1045
1006
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1046
1007
  repetition: "repeat",
1047
1008
  });
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
1009
  this.cropShapeHatchPattern = pattern;
1058
1010
  this.cropShapeHatchPatternColor = color;
1059
1011
  this.cropShapeHatchPatternKey = cacheKey;
1060
1012
  return pattern;
1061
1013
  }
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
1014
  resolveRenderImageState(item) {
1183
1015
  const active = this.isToolActive;
1184
1016
  const sourceUrl = item.sourceUrl || item.url;
@@ -1262,167 +1094,35 @@ class ImageTool {
1262
1094
  }
1263
1095
  return specs;
1264
1096
  }
1265
- buildOverlaySpecs(frame, sceneGeometry) {
1097
+ buildOverlaySpecs(overlayState) {
1266
1098
  const visible = this.isImageEditingVisible();
1267
- if (!visible ||
1268
- frame.width <= 0 ||
1269
- frame.height <= 0 ||
1270
- !this.canvasService) {
1099
+ if (!visible || !overlayState || !this.canvasService) {
1271
1100
  this.debug("overlay:hidden", {
1272
1101
  visible,
1273
- frame,
1102
+ cutRect: overlayState?.layout.cutRect,
1274
1103
  isToolActive: this.isToolActive,
1275
1104
  isImageSelectionActive: this.isImageSelectionActive,
1276
1105
  focusedImageId: this.focusedImageId,
1277
1106
  });
1278
1107
  return [];
1279
1108
  }
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;
1109
+ const viewport = this.canvasService.getScreenViewportRect();
1285
1110
  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
- },
1111
+ const specs = (0, sessionOverlay_1.buildImageSessionOverlaySpecs)({
1112
+ viewport: {
1113
+ left: viewport.left,
1114
+ top: viewport.top,
1115
+ width: viewport.width,
1116
+ height: viewport.height,
1331
1117
  },
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];
1118
+ layout: overlayState.layout,
1119
+ geometry: overlayState.geometry,
1120
+ visual,
1121
+ hatchPattern: this.getCropShapeHatchPattern(),
1122
+ });
1423
1123
  this.debug("overlay:built", {
1424
- frame,
1425
- shape: sceneGeometry?.shape,
1124
+ cutRect: overlayState.layout.cutRect,
1125
+ shape: overlayState.geometry.shape,
1426
1126
  overlayIds: specs.map((spec) => ({
1427
1127
  id: spec.id,
1428
1128
  zIndex: spec.data?.zIndex,
@@ -1450,11 +1150,9 @@ class ImageTool {
1450
1150
  const imageSpecs = await this.buildImageSpecs(renderItems, frame);
1451
1151
  if (seq !== this.renderSeq)
1452
1152
  return;
1453
- const sceneGeometry = await this.resolveSceneGeometryForOverlay();
1454
- if (seq !== this.renderSeq)
1455
- return;
1153
+ const overlayState = this.resolveSessionOverlayState();
1456
1154
  this.imageSpecs = imageSpecs;
1457
- this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1155
+ this.overlaySpecs = this.buildOverlaySpecs(overlayState);
1458
1156
  await this.canvasService.flushRenderFromProducers();
1459
1157
  if (seq !== this.renderSeq)
1460
1158
  return;
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildImageSessionOverlaySpecs = buildImageSessionOverlaySpecs;
4
+ const geometry_1 = require("../geometry");
5
+ const EPSILON = 0.0001;
6
+ const SHAPE_OUTLINE_COLOR = "rgba(255, 0, 0, 0.9)";
7
+ const DEFAULT_HATCH_FILL = "rgba(255, 0, 0, 0.22)";
8
+ function buildRectPath(width, height) {
9
+ return `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`;
10
+ }
11
+ function buildViewportMaskPath(viewport, cutRect) {
12
+ const cutLeft = cutRect.left - viewport.left;
13
+ const cutTop = cutRect.top - viewport.top;
14
+ return [
15
+ buildRectPath(viewport.width, viewport.height),
16
+ `M ${cutLeft} ${cutTop} L ${cutLeft + cutRect.width} ${cutTop} L ${cutLeft + cutRect.width} ${cutTop + cutRect.height} L ${cutLeft} ${cutTop + cutRect.height} Z`,
17
+ ].join(" ");
18
+ }
19
+ function resolveCutShapeRadiusPx(geometry, cutRect) {
20
+ const visualRadius = Number.isFinite(geometry.radius)
21
+ ? Math.max(0, geometry.radius)
22
+ : 0;
23
+ const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
24
+ const rawCutRadius = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
25
+ const maxRadius = Math.max(0, Math.min(cutRect.width, cutRect.height) / 2);
26
+ return Math.max(0, Math.min(maxRadius, rawCutRadius));
27
+ }
28
+ function buildBuiltinShapeOverlayPaths(cutRect, geometry) {
29
+ if (!geometry || geometry.shape === "custom") {
30
+ return null;
31
+ }
32
+ const radius = resolveCutShapeRadiusPx(geometry, cutRect);
33
+ if (geometry.shape === "rect" && radius <= EPSILON) {
34
+ return null;
35
+ }
36
+ const shapePathData = (0, geometry_1.generateDielinePath)({
37
+ shape: geometry.shape,
38
+ shapeStyle: geometry.shapeStyle,
39
+ width: Math.max(1, cutRect.width),
40
+ height: Math.max(1, cutRect.height),
41
+ radius,
42
+ x: cutRect.width / 2,
43
+ y: cutRect.height / 2,
44
+ features: [],
45
+ canvasWidth: Math.max(1, cutRect.width),
46
+ canvasHeight: Math.max(1, cutRect.height),
47
+ });
48
+ if (!shapePathData) {
49
+ return null;
50
+ }
51
+ return {
52
+ shapePathData,
53
+ hatchPathData: `${buildRectPath(cutRect.width, cutRect.height)} ${shapePathData}`,
54
+ };
55
+ }
56
+ function buildImageSessionOverlaySpecs(args) {
57
+ const { viewport, layout, geometry, visual, hatchPattern } = args;
58
+ const cutRect = layout.cutRect;
59
+ const specs = [];
60
+ specs.push({
61
+ id: "image.cropMask.rect",
62
+ type: "path",
63
+ space: "screen",
64
+ data: { id: "image.cropMask.rect", zIndex: 1 },
65
+ props: {
66
+ pathData: buildViewportMaskPath(viewport, cutRect),
67
+ left: viewport.left,
68
+ top: viewport.top,
69
+ originX: "left",
70
+ originY: "top",
71
+ fill: visual.outerBackground,
72
+ stroke: null,
73
+ fillRule: "evenodd",
74
+ selectable: false,
75
+ evented: false,
76
+ excludeFromExport: true,
77
+ objectCaching: false,
78
+ },
79
+ });
80
+ const shapeOverlay = buildBuiltinShapeOverlayPaths(cutRect, geometry);
81
+ if (shapeOverlay) {
82
+ specs.push({
83
+ id: "image.cropShapeHatch",
84
+ type: "path",
85
+ space: "screen",
86
+ data: { id: "image.cropShapeHatch", zIndex: 5 },
87
+ props: {
88
+ pathData: shapeOverlay.hatchPathData,
89
+ left: cutRect.left,
90
+ top: cutRect.top,
91
+ originX: "left",
92
+ originY: "top",
93
+ fill: hatchPattern || DEFAULT_HATCH_FILL,
94
+ opacity: hatchPattern ? 1 : 0.8,
95
+ stroke: null,
96
+ fillRule: "evenodd",
97
+ selectable: false,
98
+ evented: false,
99
+ excludeFromExport: true,
100
+ objectCaching: false,
101
+ },
102
+ });
103
+ specs.push({
104
+ id: "image.cropShapeOutline",
105
+ type: "path",
106
+ space: "screen",
107
+ data: { id: "image.cropShapeOutline", zIndex: 6 },
108
+ props: {
109
+ pathData: shapeOverlay.shapePathData,
110
+ left: cutRect.left,
111
+ top: cutRect.top,
112
+ originX: "left",
113
+ originY: "top",
114
+ fill: "transparent",
115
+ stroke: SHAPE_OUTLINE_COLOR,
116
+ strokeWidth: 1,
117
+ selectable: false,
118
+ evented: false,
119
+ excludeFromExport: true,
120
+ objectCaching: false,
121
+ },
122
+ });
123
+ }
124
+ specs.push({
125
+ id: "image.cropFrame",
126
+ type: "rect",
127
+ space: "screen",
128
+ data: { id: "image.cropFrame", zIndex: 7 },
129
+ props: {
130
+ left: cutRect.left,
131
+ top: cutRect.top,
132
+ width: cutRect.width,
133
+ height: cutRect.height,
134
+ originX: "left",
135
+ originY: "top",
136
+ fill: visual.innerBackground,
137
+ stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
138
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
139
+ strokeDashArray: visual.strokeStyle === "dashed"
140
+ ? [visual.dashLength, visual.dashLength]
141
+ : undefined,
142
+ selectable: false,
143
+ evented: false,
144
+ excludeFromExport: true,
145
+ },
146
+ });
147
+ return specs;
148
+ }