@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.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +248 -267
- package/dist/index.d.ts +248 -267
- package/dist/index.js +6729 -5797
- package/dist/index.mjs +6690 -5741
- package/package.json +2 -2
- package/src/{background.ts → extensions/background.ts} +1 -1
- package/src/{dieline.ts → extensions/dieline.ts} +39 -17
- package/src/{feature.ts → extensions/feature.ts} +80 -67
- package/src/{film.ts → extensions/film.ts} +1 -1
- package/src/{geometry.ts → extensions/geometry.ts} +151 -105
- package/src/{image.ts → extensions/image.ts} +436 -93
- package/src/extensions/index.ts +11 -0
- package/src/{maskOps.ts → extensions/maskOps.ts} +28 -10
- package/src/{mirror.ts → extensions/mirror.ts} +1 -1
- package/src/{ruler.ts → extensions/ruler.ts} +5 -3
- package/src/extensions/sceneLayout.ts +140 -0
- package/src/{sceneLayoutModel.ts → extensions/sceneLayoutModel.ts} +17 -10
- package/src/extensions/sceneVisibility.ts +71 -0
- package/src/{size.ts → extensions/size.ts} +23 -13
- package/src/{tracer.ts → extensions/tracer.ts} +374 -45
- package/src/{white-ink.ts → extensions/white-ink.ts} +620 -236
- package/src/index.ts +2 -14
- package/src/{ViewportSystem.ts → services/ViewportSystem.ts} +5 -2
- package/src/services/index.ts +3 -0
- package/src/sceneLayout.ts +0 -121
- package/src/sceneVisibility.ts +0 -49
- /package/src/{bridgeSelection.ts → extensions/bridgeSelection.ts} +0 -0
- /package/src/{constraints.ts → extensions/constraints.ts} +0 -0
- /package/src/{edgeScale.ts → extensions/edgeScale.ts} +0 -0
- /package/src/{featureComplete.ts → extensions/featureComplete.ts} +0 -0
- /package/src/{wrappedOffsets.ts → extensions/wrappedOffsets.ts} +0 -0
- /package/src/{CanvasService.ts → services/CanvasService.ts} +0 -0
- /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 {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
92
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
multiplier
|
|
105
|
-
|
|
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: "
|
|
435
|
-
title: "Export
|
|
436
|
-
handler: async (
|
|
437
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
1503
|
-
): Promise<
|
|
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
|
|
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
|
-
|
|
1525
|
-
|
|
1526
|
-
.
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1561
|
-
|
|
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
|
|
1565
|
-
options:
|
|
1566
|
-
): Promise<
|
|
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
|
-
|
|
1572
|
-
|
|
1573
|
-
.filter((id: any) => typeof id === "string");
|
|
1909
|
+
await this.updateImagesAsync();
|
|
1910
|
+
this.syncToolActiveFromWorkbench();
|
|
1574
1911
|
|
|
1575
|
-
const
|
|
1576
|
-
imageIds
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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";
|