@pooder/kit 6.2.2 → 6.3.1
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/.test-dist/src/extensions/image/ImageTool.js +250 -119
- package/.test-dist/src/extensions/image/commands.js +69 -41
- package/.test-dist/src/extensions/image/config.js +7 -0
- package/.test-dist/src/extensions/image/imageOperations.js +75 -0
- package/.test-dist/src/extensions/image/imagePlacement.js +44 -0
- package/.test-dist/src/extensions/image/index.js +1 -0
- package/.test-dist/src/extensions/image/model.js +4 -0
- package/.test-dist/tests/run.js +51 -9
- package/CHANGELOG.md +14 -0
- package/dist/index.d.mts +270 -163
- package/dist/index.d.ts +270 -163
- package/dist/index.js +458 -159
- package/dist/index.mjs +454 -158
- package/package.json +2 -2
- package/src/extensions/image/ImageTool.ts +351 -145
- package/src/extensions/image/commands.ts +78 -49
- package/src/extensions/image/config.ts +7 -0
- package/src/extensions/image/imageOperations.ts +135 -0
- package/src/extensions/image/imagePlacement.ts +78 -0
- package/src/extensions/image/index.ts +1 -0
- package/src/extensions/image/model.ts +13 -1
- package/tests/run.ts +89 -15
|
@@ -39,6 +39,12 @@ import {
|
|
|
39
39
|
} from "../../shared/constants/layers";
|
|
40
40
|
import { createImageCommands } from "./commands";
|
|
41
41
|
import { createImageConfigurations } from "./config";
|
|
42
|
+
import {
|
|
43
|
+
computeImageOperationUpdates,
|
|
44
|
+
resolveImageOperationArea,
|
|
45
|
+
type ImageOperation,
|
|
46
|
+
} from "./imageOperations";
|
|
47
|
+
import { validateImagePlacement } from "./imagePlacement";
|
|
42
48
|
import { buildImageSessionOverlaySpecs } from "./sessionOverlay";
|
|
43
49
|
|
|
44
50
|
export interface ImageItem {
|
|
@@ -53,6 +59,37 @@ export interface ImageItem {
|
|
|
53
59
|
committedUrl?: string;
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
export interface ImageTransformUpdates {
|
|
63
|
+
scale?: number;
|
|
64
|
+
angle?: number;
|
|
65
|
+
left?: number;
|
|
66
|
+
top?: number;
|
|
67
|
+
opacity?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ImageViewState {
|
|
71
|
+
items: ImageItem[];
|
|
72
|
+
hasAnyImage: boolean;
|
|
73
|
+
focusedId: string | null;
|
|
74
|
+
focusedItem: ImageItem | null;
|
|
75
|
+
isToolActive: boolean;
|
|
76
|
+
isImageSelectionActive: boolean;
|
|
77
|
+
hasWorkingChanges: boolean;
|
|
78
|
+
source: "working" | "committed";
|
|
79
|
+
placementPolicy: ImageSessionPlacementPolicy;
|
|
80
|
+
sessionNotice: ImageSessionNotice | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ImageSessionPlacementPolicy = "free" | "warn" | "strict";
|
|
84
|
+
|
|
85
|
+
export interface ImageSessionNotice {
|
|
86
|
+
code: "image-outside-frame";
|
|
87
|
+
level: "warning" | "error";
|
|
88
|
+
message: string;
|
|
89
|
+
imageIds: string[];
|
|
90
|
+
policy: ImageSessionPlacementPolicy;
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
interface RenderImageState {
|
|
57
94
|
src: string;
|
|
58
95
|
left: number;
|
|
@@ -92,14 +129,7 @@ interface UpsertImageOptions {
|
|
|
92
129
|
id?: string;
|
|
93
130
|
mode?: "replace" | "add";
|
|
94
131
|
addOptions?: Partial<ImageItem>;
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface DielineFitArea {
|
|
99
|
-
width: number;
|
|
100
|
-
height: number;
|
|
101
|
-
left: number;
|
|
102
|
-
top: number;
|
|
132
|
+
operation?: ImageOperation;
|
|
103
133
|
}
|
|
104
134
|
|
|
105
135
|
interface UpdateImageOptions {
|
|
@@ -221,6 +251,7 @@ export class ImageTool implements Extension {
|
|
|
221
251
|
private activeSnapX: SnapMatch | null = null;
|
|
222
252
|
private activeSnapY: SnapMatch | null = null;
|
|
223
253
|
private movingImageId: string | null = null;
|
|
254
|
+
private sessionNotice: ImageSessionNotice | null = null;
|
|
224
255
|
private hasRenderedSnapGuides = false;
|
|
225
256
|
private canvasObjectMovingHandler?: (e: any) => void;
|
|
226
257
|
private canvasMouseUpHandler?: (e: any) => void;
|
|
@@ -332,8 +363,12 @@ export class ImageTool implements Extension {
|
|
|
332
363
|
if (
|
|
333
364
|
e.key.startsWith("size.") ||
|
|
334
365
|
e.key.startsWith("image.frame.") ||
|
|
366
|
+
e.key.startsWith("image.session.") ||
|
|
335
367
|
e.key.startsWith("image.control.")
|
|
336
368
|
) {
|
|
369
|
+
if (e.key === "image.session.placementPolicy") {
|
|
370
|
+
this.clearSessionNotice();
|
|
371
|
+
}
|
|
337
372
|
if (e.key.startsWith("image.control.")) {
|
|
338
373
|
this.imageControlsByCapabilityKey.clear();
|
|
339
374
|
}
|
|
@@ -370,6 +405,7 @@ export class ImageTool implements Extension {
|
|
|
370
405
|
this.clearRenderedImages();
|
|
371
406
|
this.renderProducerDisposable?.dispose();
|
|
372
407
|
this.renderProducerDisposable = undefined;
|
|
408
|
+
this.emitImageStateChange();
|
|
373
409
|
if (this.canvasService) {
|
|
374
410
|
void this.canvasService.flushRenderFromProducers();
|
|
375
411
|
this.canvasService = undefined;
|
|
@@ -932,9 +968,10 @@ export class ImageTool implements Extension {
|
|
|
932
968
|
name: "Image",
|
|
933
969
|
interaction: "session",
|
|
934
970
|
commands: {
|
|
935
|
-
begin: "
|
|
971
|
+
begin: "imageSessionReset",
|
|
972
|
+
validate: "validateImageSession",
|
|
936
973
|
commit: "completeImages",
|
|
937
|
-
rollback: "
|
|
974
|
+
rollback: "imageSessionReset",
|
|
938
975
|
},
|
|
939
976
|
session: {
|
|
940
977
|
autoBegin: true,
|
|
@@ -979,6 +1016,77 @@ export class ImageTool implements Extension {
|
|
|
979
1016
|
return this.normalizeItems((items || []).map((i) => ({ ...i })));
|
|
980
1017
|
}
|
|
981
1018
|
|
|
1019
|
+
private getViewItems(): ImageItem[] {
|
|
1020
|
+
return this.isToolActive ? this.workingItems : this.items;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
private getPlacementPolicy(): ImageSessionPlacementPolicy {
|
|
1024
|
+
const policy = this.getConfig<ImageSessionPlacementPolicy>(
|
|
1025
|
+
"image.session.placementPolicy",
|
|
1026
|
+
"free",
|
|
1027
|
+
);
|
|
1028
|
+
return policy === "warn" || policy === "strict" ? policy : "free";
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private areSessionNoticesEqual(
|
|
1032
|
+
a: ImageSessionNotice | null,
|
|
1033
|
+
b: ImageSessionNotice | null,
|
|
1034
|
+
): boolean {
|
|
1035
|
+
if (!a && !b) return true;
|
|
1036
|
+
if (!a || !b) return false;
|
|
1037
|
+
return (
|
|
1038
|
+
a.code === b.code &&
|
|
1039
|
+
a.level === b.level &&
|
|
1040
|
+
a.message === b.message &&
|
|
1041
|
+
a.policy === b.policy &&
|
|
1042
|
+
JSON.stringify(a.imageIds) === JSON.stringify(b.imageIds)
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private setSessionNotice(
|
|
1047
|
+
notice: ImageSessionNotice | null,
|
|
1048
|
+
options: { emit?: boolean } = {},
|
|
1049
|
+
) {
|
|
1050
|
+
if (this.areSessionNoticesEqual(this.sessionNotice, notice)) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
this.sessionNotice = notice;
|
|
1054
|
+
if (options.emit !== false) {
|
|
1055
|
+
this.context?.eventBus.emit("image:session:notice", this.sessionNotice);
|
|
1056
|
+
this.emitImageStateChange();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private clearSessionNotice(options: { emit?: boolean } = {}) {
|
|
1061
|
+
this.setSessionNotice(null, options);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private getImageViewState(): ImageViewState {
|
|
1065
|
+
this.syncToolActiveFromWorkbench();
|
|
1066
|
+
const items = this.cloneItems(this.getViewItems());
|
|
1067
|
+
const focusedItem =
|
|
1068
|
+
this.focusedImageId == null
|
|
1069
|
+
? null
|
|
1070
|
+
: items.find((item) => item.id === this.focusedImageId) || null;
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
items,
|
|
1074
|
+
hasAnyImage: items.length > 0,
|
|
1075
|
+
focusedId: this.focusedImageId,
|
|
1076
|
+
focusedItem,
|
|
1077
|
+
isToolActive: this.isToolActive,
|
|
1078
|
+
isImageSelectionActive: this.isImageSelectionActive,
|
|
1079
|
+
hasWorkingChanges: this.hasWorkingChanges,
|
|
1080
|
+
source: this.isToolActive ? "working" : "committed",
|
|
1081
|
+
placementPolicy: this.getPlacementPolicy(),
|
|
1082
|
+
sessionNotice: this.sessionNotice,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private emitImageStateChange() {
|
|
1087
|
+
this.context?.eventBus.emit("image:state:change", this.getImageViewState());
|
|
1088
|
+
}
|
|
1089
|
+
|
|
982
1090
|
private emitWorkingChange(changedId: string | null = null) {
|
|
983
1091
|
this.context?.eventBus.emit("image:working:change", {
|
|
984
1092
|
changedId,
|
|
@@ -1026,6 +1134,8 @@ export class ImageTool implements Extension {
|
|
|
1026
1134
|
|
|
1027
1135
|
if (!options.skipRender) {
|
|
1028
1136
|
this.updateImages();
|
|
1137
|
+
} else {
|
|
1138
|
+
this.emitImageStateChange();
|
|
1029
1139
|
}
|
|
1030
1140
|
|
|
1031
1141
|
return { ok: true, id };
|
|
@@ -1034,8 +1144,10 @@ export class ImageTool implements Extension {
|
|
|
1034
1144
|
private async addImageEntry(
|
|
1035
1145
|
url: string,
|
|
1036
1146
|
options?: Partial<ImageItem>,
|
|
1037
|
-
|
|
1147
|
+
operation?: ImageOperation,
|
|
1038
1148
|
): Promise<string> {
|
|
1149
|
+
this.syncToolActiveFromWorkbench();
|
|
1150
|
+
this.clearSessionNotice({ emit: false });
|
|
1039
1151
|
const id = this.generateId();
|
|
1040
1152
|
const newItem = this.normalizeItem({
|
|
1041
1153
|
id,
|
|
@@ -1044,13 +1156,20 @@ export class ImageTool implements Extension {
|
|
|
1044
1156
|
...options,
|
|
1045
1157
|
} as ImageItem);
|
|
1046
1158
|
|
|
1047
|
-
const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
|
|
1048
1159
|
const waitLoaded = this.waitImageLoaded(id, true);
|
|
1049
|
-
|
|
1050
|
-
|
|
1160
|
+
if (this.isToolActive) {
|
|
1161
|
+
this.workingItems = this.cloneItems([...this.workingItems, newItem]);
|
|
1162
|
+
this.hasWorkingChanges = true;
|
|
1163
|
+
this.updateImages();
|
|
1164
|
+
this.emitWorkingChange(id);
|
|
1165
|
+
} else {
|
|
1166
|
+
this.updateConfig([...this.items, newItem]);
|
|
1167
|
+
}
|
|
1051
1168
|
const loaded = await waitLoaded;
|
|
1052
|
-
if (loaded &&
|
|
1053
|
-
await this.
|
|
1169
|
+
if (loaded && operation) {
|
|
1170
|
+
await this.applyImageOperation(id, operation, {
|
|
1171
|
+
target: this.isToolActive ? "working" : "config",
|
|
1172
|
+
});
|
|
1054
1173
|
}
|
|
1055
1174
|
if (loaded) {
|
|
1056
1175
|
this.setImageFocus(id);
|
|
@@ -1062,8 +1181,8 @@ export class ImageTool implements Extension {
|
|
|
1062
1181
|
url: string,
|
|
1063
1182
|
options: UpsertImageOptions = {},
|
|
1064
1183
|
): Promise<{ id: string; mode: "replace" | "add" }> {
|
|
1184
|
+
this.syncToolActiveFromWorkbench();
|
|
1065
1185
|
const mode = options.mode || (options.id ? "replace" : "add");
|
|
1066
|
-
const fitOnAdd = options.fitOnAdd !== false;
|
|
1067
1186
|
if (mode === "replace") {
|
|
1068
1187
|
if (!options.id) {
|
|
1069
1188
|
throw new Error("replace-target-id-required");
|
|
@@ -1072,25 +1191,39 @@ export class ImageTool implements Extension {
|
|
|
1072
1191
|
if (!this.hasImageItem(targetId)) {
|
|
1073
1192
|
throw new Error("replace-target-not-found");
|
|
1074
1193
|
}
|
|
1075
|
-
|
|
1194
|
+
if (this.isToolActive) {
|
|
1195
|
+
const current =
|
|
1196
|
+
this.workingItems.find((item) => item.id === targetId) ||
|
|
1197
|
+
this.items.find((item) => item.id === targetId);
|
|
1198
|
+
this.purgeSourceSizeCacheForItem(current);
|
|
1199
|
+
this.updateImageInWorking(targetId, {
|
|
1200
|
+
url,
|
|
1201
|
+
sourceUrl: url,
|
|
1202
|
+
committedUrl: undefined,
|
|
1203
|
+
});
|
|
1204
|
+
} else {
|
|
1205
|
+
await this.updateImageInConfig(targetId, { url });
|
|
1206
|
+
}
|
|
1207
|
+
const loaded = await this.waitImageLoaded(targetId, true);
|
|
1208
|
+
if (loaded && options.operation) {
|
|
1209
|
+
await this.applyImageOperation(targetId, options.operation, {
|
|
1210
|
+
target: this.isToolActive ? "working" : "config",
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
if (loaded) {
|
|
1214
|
+
this.setImageFocus(targetId);
|
|
1215
|
+
}
|
|
1076
1216
|
return { id: targetId, mode: "replace" };
|
|
1077
1217
|
}
|
|
1078
1218
|
|
|
1079
|
-
const id = await this.addImageEntry(
|
|
1219
|
+
const id = await this.addImageEntry(
|
|
1220
|
+
url,
|
|
1221
|
+
options.addOptions,
|
|
1222
|
+
options.operation,
|
|
1223
|
+
);
|
|
1080
1224
|
return { id, mode: "add" };
|
|
1081
1225
|
}
|
|
1082
1226
|
|
|
1083
|
-
private addItemToWorkingSessionIfNeeded(
|
|
1084
|
-
item: ImageItem,
|
|
1085
|
-
sessionDirtyBeforeAdd: boolean,
|
|
1086
|
-
) {
|
|
1087
|
-
if (!sessionDirtyBeforeAdd || !this.isToolActive) return;
|
|
1088
|
-
if (this.workingItems.some((existing) => existing.id === item.id)) return;
|
|
1089
|
-
this.workingItems = this.cloneItems([...this.workingItems, item]);
|
|
1090
|
-
this.updateImages();
|
|
1091
|
-
this.emitWorkingChange(item.id);
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
1227
|
private async updateImage(
|
|
1095
1228
|
id: string,
|
|
1096
1229
|
updates: Partial<ImageItem>,
|
|
@@ -1134,6 +1267,7 @@ export class ImageTool implements Extension {
|
|
|
1134
1267
|
|
|
1135
1268
|
private updateConfig(newItems: ImageItem[], skipCanvasUpdate = false) {
|
|
1136
1269
|
if (!this.context) return;
|
|
1270
|
+
this.clearSessionNotice({ emit: false });
|
|
1137
1271
|
this.applyCommittedItems(newItems);
|
|
1138
1272
|
runDeferredConfigUpdate(
|
|
1139
1273
|
this,
|
|
@@ -1165,38 +1299,6 @@ export class ImageTool implements Extension {
|
|
|
1165
1299
|
return this.canvasService.toScreenRect(frame || this.getFrameRect());
|
|
1166
1300
|
}
|
|
1167
1301
|
|
|
1168
|
-
private async resolveDefaultFitArea(): Promise<DielineFitArea | null> {
|
|
1169
|
-
if (!this.canvasService) return null;
|
|
1170
|
-
const frame = this.getFrameRect();
|
|
1171
|
-
if (frame.width <= 0 || frame.height <= 0) return null;
|
|
1172
|
-
return {
|
|
1173
|
-
width: Math.max(1, frame.width),
|
|
1174
|
-
height: Math.max(1, frame.height),
|
|
1175
|
-
left: frame.left + frame.width / 2,
|
|
1176
|
-
top: frame.top + frame.height / 2,
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
private async fitImageToDefaultArea(id: string) {
|
|
1181
|
-
if (!this.canvasService) return;
|
|
1182
|
-
const area = await this.resolveDefaultFitArea();
|
|
1183
|
-
|
|
1184
|
-
if (area) {
|
|
1185
|
-
await this.fitImageToArea(id, area);
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
const viewport = this.canvasService.getSceneViewportRect();
|
|
1190
|
-
const canvasW = Math.max(1, viewport.width || 0);
|
|
1191
|
-
const canvasH = Math.max(1, viewport.height || 0);
|
|
1192
|
-
await this.fitImageToArea(id, {
|
|
1193
|
-
width: canvasW,
|
|
1194
|
-
height: canvasH,
|
|
1195
|
-
left: viewport.left + canvasW / 2,
|
|
1196
|
-
top: viewport.top + canvasH / 2,
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
1302
|
private getImageObjects(): any[] {
|
|
1201
1303
|
if (!this.canvasService) return [];
|
|
1202
1304
|
return this.canvasService.canvas.getObjects().filter((obj: any) => {
|
|
@@ -1279,6 +1381,82 @@ export class ImageTool implements Extension {
|
|
|
1279
1381
|
return getCoverScaleFromRect(frame, size);
|
|
1280
1382
|
}
|
|
1281
1383
|
|
|
1384
|
+
private resolvePlacementState(item: ImageItem) {
|
|
1385
|
+
return {
|
|
1386
|
+
left: Number.isFinite(item.left as any) ? (item.left as number) : 0.5,
|
|
1387
|
+
top: Number.isFinite(item.top as any) ? (item.top as number) : 0.5,
|
|
1388
|
+
scale: Math.max(0.05, item.scale ?? 1),
|
|
1389
|
+
angle: Number.isFinite(item.angle as any) ? (item.angle as number) : 0,
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
private async validatePlacementForItem(item: ImageItem): Promise<boolean> {
|
|
1394
|
+
const frame = this.getFrameRect();
|
|
1395
|
+
if (!frame.width || !frame.height) {
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const src = item.sourceUrl || item.url;
|
|
1400
|
+
if (!src) {
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const source = await this.resolveImageSourceSize(item.id, src);
|
|
1405
|
+
if (!source) {
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
return validateImagePlacement({
|
|
1410
|
+
frame,
|
|
1411
|
+
source,
|
|
1412
|
+
placement: this.resolvePlacementState(item),
|
|
1413
|
+
}).ok;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
private async validateImageSession() {
|
|
1417
|
+
const policy = this.getPlacementPolicy();
|
|
1418
|
+
if (policy === "free") {
|
|
1419
|
+
this.clearSessionNotice();
|
|
1420
|
+
return { ok: true, policy };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const invalidImageIds: string[] = [];
|
|
1424
|
+
for (const item of this.workingItems) {
|
|
1425
|
+
const valid = await this.validatePlacementForItem(item);
|
|
1426
|
+
if (!valid) {
|
|
1427
|
+
invalidImageIds.push(item.id);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (!invalidImageIds.length) {
|
|
1432
|
+
this.clearSessionNotice();
|
|
1433
|
+
return { ok: true, policy };
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const notice: ImageSessionNotice = {
|
|
1437
|
+
code: "image-outside-frame",
|
|
1438
|
+
level: policy === "strict" ? "error" : "warning",
|
|
1439
|
+
message:
|
|
1440
|
+
policy === "strict"
|
|
1441
|
+
? "图片位置不能超出 frame,请调整后再提交。"
|
|
1442
|
+
: "图片位置已超出 frame,建议调整后再提交。",
|
|
1443
|
+
imageIds: invalidImageIds,
|
|
1444
|
+
policy,
|
|
1445
|
+
};
|
|
1446
|
+
this.setSessionNotice(notice);
|
|
1447
|
+
this.setImageFocus(invalidImageIds[0], {
|
|
1448
|
+
syncCanvasSelection: true,
|
|
1449
|
+
skipRender: true,
|
|
1450
|
+
});
|
|
1451
|
+
return {
|
|
1452
|
+
ok: policy !== "strict",
|
|
1453
|
+
reason: notice.code,
|
|
1454
|
+
message: notice.message,
|
|
1455
|
+
imageIds: notice.imageIds,
|
|
1456
|
+
policy: notice.policy,
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1282
1460
|
private getFrameVisualConfig(): FrameVisualConfig {
|
|
1283
1461
|
const strokeStyleRaw = (this.getConfig<string>(
|
|
1284
1462
|
"image.frame.strokeStyle",
|
|
@@ -1587,6 +1765,7 @@ export class ImageTool implements Extension {
|
|
|
1587
1765
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
1588
1766
|
focusedImageId: this.focusedImageId,
|
|
1589
1767
|
});
|
|
1768
|
+
this.emitImageStateChange();
|
|
1590
1769
|
this.canvasService.requestRenderAll();
|
|
1591
1770
|
}
|
|
1592
1771
|
|
|
@@ -1594,6 +1773,41 @@ export class ImageTool implements Extension {
|
|
|
1594
1773
|
return Math.max(-1, Math.min(2, value));
|
|
1595
1774
|
}
|
|
1596
1775
|
|
|
1776
|
+
private async setImageTransform(
|
|
1777
|
+
id: string,
|
|
1778
|
+
updates: ImageTransformUpdates,
|
|
1779
|
+
options: UpdateImageOptions = {},
|
|
1780
|
+
) {
|
|
1781
|
+
const next: Partial<ImageItem> = {};
|
|
1782
|
+
|
|
1783
|
+
if (Number.isFinite(updates.scale as number)) {
|
|
1784
|
+
next.scale = Math.max(0.05, Number(updates.scale));
|
|
1785
|
+
}
|
|
1786
|
+
if (Number.isFinite(updates.angle as number)) {
|
|
1787
|
+
next.angle = Number(updates.angle);
|
|
1788
|
+
}
|
|
1789
|
+
if (Number.isFinite(updates.left as number)) {
|
|
1790
|
+
next.left = this.clampNormalized(Number(updates.left));
|
|
1791
|
+
}
|
|
1792
|
+
if (Number.isFinite(updates.top as number)) {
|
|
1793
|
+
next.top = this.clampNormalized(Number(updates.top));
|
|
1794
|
+
}
|
|
1795
|
+
if (Number.isFinite(updates.opacity as number)) {
|
|
1796
|
+
next.opacity = Math.max(0, Math.min(1, Number(updates.opacity)));
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (!Object.keys(next).length) return;
|
|
1800
|
+
await this.updateImage(id, next, options);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
private resetImageSession() {
|
|
1804
|
+
this.clearSessionNotice({ emit: false });
|
|
1805
|
+
this.workingItems = this.cloneItems(this.items);
|
|
1806
|
+
this.hasWorkingChanges = false;
|
|
1807
|
+
this.updateImages();
|
|
1808
|
+
this.emitWorkingChange();
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1597
1811
|
private onObjectModified = (e: any) => {
|
|
1598
1812
|
if (!this.isToolActive) return;
|
|
1599
1813
|
const target = e?.target;
|
|
@@ -1637,6 +1851,7 @@ export class ImageTool implements Extension {
|
|
|
1637
1851
|
const index = this.workingItems.findIndex((item) => item.id === id);
|
|
1638
1852
|
if (index < 0) return;
|
|
1639
1853
|
|
|
1854
|
+
this.clearSessionNotice({ emit: false });
|
|
1640
1855
|
const next = [...this.workingItems];
|
|
1641
1856
|
next[index] = this.normalizeItem({ ...next[index], ...updates });
|
|
1642
1857
|
this.workingItems = next;
|
|
@@ -1655,6 +1870,7 @@ export class ImageTool implements Extension {
|
|
|
1655
1870
|
const index = this.items.findIndex((item) => item.id === id);
|
|
1656
1871
|
if (index < 0) return;
|
|
1657
1872
|
|
|
1873
|
+
this.clearSessionNotice({ emit: false });
|
|
1658
1874
|
const replacingSource =
|
|
1659
1875
|
typeof updates.url === "string" && updates.url.length > 0;
|
|
1660
1876
|
const next = [...this.items];
|
|
@@ -1669,10 +1885,6 @@ export class ImageTool implements Extension {
|
|
|
1669
1885
|
url: replacingUrl,
|
|
1670
1886
|
sourceUrl: replacingUrl,
|
|
1671
1887
|
committedUrl: undefined,
|
|
1672
|
-
scale: updates.scale ?? 1,
|
|
1673
|
-
angle: updates.angle ?? 0,
|
|
1674
|
-
left: updates.left ?? 0.5,
|
|
1675
|
-
top: updates.top ?? 0.5,
|
|
1676
1888
|
}
|
|
1677
1889
|
: {}),
|
|
1678
1890
|
});
|
|
@@ -1680,14 +1892,7 @@ export class ImageTool implements Extension {
|
|
|
1680
1892
|
this.updateConfig(next);
|
|
1681
1893
|
|
|
1682
1894
|
if (replacingSource) {
|
|
1683
|
-
this.debug("replace:image:begin", { id, replacingUrl });
|
|
1684
1895
|
this.purgeSourceSizeCacheForItem(base);
|
|
1685
|
-
const loaded = await this.waitImageLoaded(id, true);
|
|
1686
|
-
this.debug("replace:image:loaded", { id, loaded });
|
|
1687
|
-
if (loaded) {
|
|
1688
|
-
await this.refitImageToFrame(id);
|
|
1689
|
-
this.setImageFocus(id);
|
|
1690
|
-
}
|
|
1691
1896
|
}
|
|
1692
1897
|
}
|
|
1693
1898
|
|
|
@@ -1709,93 +1914,62 @@ export class ImageTool implements Extension {
|
|
|
1709
1914
|
});
|
|
1710
1915
|
}
|
|
1711
1916
|
|
|
1712
|
-
private async
|
|
1917
|
+
private async resolveImageSourceSize(
|
|
1918
|
+
id: string,
|
|
1919
|
+
src: string,
|
|
1920
|
+
): Promise<SourceSize | null> {
|
|
1713
1921
|
const obj = this.getImageObject(id);
|
|
1714
|
-
if (
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
const source = this.getSourceSize(render.src, obj);
|
|
1721
|
-
const frame = this.getFrameRect();
|
|
1722
|
-
const coverScale = this.getCoverScale(frame, source);
|
|
1723
|
-
|
|
1724
|
-
const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
|
|
1725
|
-
const zoom = Math.max(0.05, currentScale / coverScale);
|
|
1726
|
-
|
|
1727
|
-
const updated: Partial<ImageItem> = {
|
|
1728
|
-
scale: Number.isFinite(zoom) ? zoom : 1,
|
|
1729
|
-
angle: 0,
|
|
1730
|
-
left: 0.5,
|
|
1731
|
-
top: 0.5,
|
|
1732
|
-
};
|
|
1733
|
-
|
|
1734
|
-
const index = this.items.findIndex((item) => item.id === id);
|
|
1735
|
-
if (index < 0) return;
|
|
1922
|
+
if (obj) {
|
|
1923
|
+
this.rememberSourceSize(src, obj);
|
|
1924
|
+
}
|
|
1925
|
+
const ensured = await this.ensureSourceSize(src);
|
|
1926
|
+
if (ensured) return ensured;
|
|
1927
|
+
if (!obj) return null;
|
|
1736
1928
|
|
|
1737
|
-
const
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
this.hasWorkingChanges = false;
|
|
1742
|
-
this.updateImages();
|
|
1743
|
-
this.emitWorkingChange(id);
|
|
1929
|
+
const width = Number(obj?.width || 0);
|
|
1930
|
+
const height = Number(obj?.height || 0);
|
|
1931
|
+
if (width <= 0 || height <= 0) return null;
|
|
1932
|
+
return { width, height };
|
|
1744
1933
|
}
|
|
1745
1934
|
|
|
1746
|
-
private async
|
|
1935
|
+
private async applyImageOperation(
|
|
1747
1936
|
id: string,
|
|
1748
|
-
|
|
1937
|
+
operation: ImageOperation,
|
|
1938
|
+
options: UpdateImageOptions = {},
|
|
1749
1939
|
) {
|
|
1750
1940
|
if (!this.canvasService) return;
|
|
1751
1941
|
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1942
|
+
this.syncToolActiveFromWorkbench();
|
|
1943
|
+
const target = options.target || "auto";
|
|
1944
|
+
const renderItems =
|
|
1945
|
+
target === "working" || (target === "auto" && this.isToolActive)
|
|
1946
|
+
? this.workingItems
|
|
1947
|
+
: this.items;
|
|
1758
1948
|
const current = renderItems.find((item) => item.id === id);
|
|
1759
1949
|
if (!current) return;
|
|
1950
|
+
|
|
1760
1951
|
const render = this.resolveRenderImageState(current);
|
|
1952
|
+
const source = await this.resolveImageSourceSize(id, render.src);
|
|
1953
|
+
if (!source) return;
|
|
1761
1954
|
|
|
1762
|
-
this.rememberSourceSize(render.src, obj);
|
|
1763
|
-
const source = this.getSourceSize(render.src, obj);
|
|
1764
1955
|
const frame = this.getFrameRect();
|
|
1765
|
-
const baseCover = this.getCoverScale(frame, source);
|
|
1766
|
-
|
|
1767
|
-
const desiredScale = Math.max(
|
|
1768
|
-
Math.max(1, area.width) / Math.max(1, source.width),
|
|
1769
|
-
Math.max(1, area.height) / Math.max(1, source.height),
|
|
1770
|
-
);
|
|
1771
|
-
|
|
1772
1956
|
const viewport = this.canvasService.getSceneViewportRect();
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
const updates: Partial<ImageItem> = {
|
|
1789
|
-
scale: Math.max(0.05, desiredScale / baseCover),
|
|
1790
|
-
left: this.clampNormalized(
|
|
1791
|
-
(areaLeftPx - frame.left) / Math.max(1, frame.width),
|
|
1792
|
-
),
|
|
1793
|
-
top: this.clampNormalized(
|
|
1794
|
-
(areaTopPx - frame.top) / Math.max(1, frame.height),
|
|
1795
|
-
),
|
|
1796
|
-
};
|
|
1957
|
+
const area =
|
|
1958
|
+
operation.type === "resetTransform"
|
|
1959
|
+
? resolveImageOperationArea({ frame, viewport })
|
|
1960
|
+
: resolveImageOperationArea({
|
|
1961
|
+
frame,
|
|
1962
|
+
viewport,
|
|
1963
|
+
area: operation.area,
|
|
1964
|
+
});
|
|
1965
|
+
const updates = computeImageOperationUpdates({
|
|
1966
|
+
frame,
|
|
1967
|
+
source,
|
|
1968
|
+
operation,
|
|
1969
|
+
area,
|
|
1970
|
+
});
|
|
1797
1971
|
|
|
1798
|
-
if (this.isToolActive) {
|
|
1972
|
+
if (target === "working" || (target === "auto" && this.isToolActive)) {
|
|
1799
1973
|
this.updateImageInWorking(id, updates);
|
|
1800
1974
|
return;
|
|
1801
1975
|
}
|
|
@@ -1841,12 +2015,44 @@ export class ImageTool implements Extension {
|
|
|
1841
2015
|
}
|
|
1842
2016
|
|
|
1843
2017
|
this.hasWorkingChanges = false;
|
|
2018
|
+
this.clearSessionNotice({ emit: false });
|
|
1844
2019
|
this.workingItems = this.cloneItems(next);
|
|
1845
2020
|
this.updateConfig(next);
|
|
1846
2021
|
this.emitWorkingChange(this.focusedImageId);
|
|
1847
2022
|
return { ok: true };
|
|
1848
2023
|
}
|
|
1849
2024
|
|
|
2025
|
+
private async completeImageSession() {
|
|
2026
|
+
const sessionState =
|
|
2027
|
+
this.context?.services.get<ToolSessionService>("ToolSessionService");
|
|
2028
|
+
const workbench = this.context?.services.get<any>("WorkbenchService");
|
|
2029
|
+
console.info("[ImageTool] completeImageSession:start", {
|
|
2030
|
+
activeToolId: workbench?.activeToolId ?? null,
|
|
2031
|
+
isToolActive: this.isToolActive,
|
|
2032
|
+
dirtyBeforeComplete: this.hasWorkingChanges,
|
|
2033
|
+
workingCount: this.workingItems.length,
|
|
2034
|
+
committedCount: this.items.length,
|
|
2035
|
+
sessionDirty: sessionState?.isDirty(this.id),
|
|
2036
|
+
});
|
|
2037
|
+
const validation = await this.validateImageSession();
|
|
2038
|
+
if (!validation.ok) {
|
|
2039
|
+
console.warn("[ImageTool] completeImageSession:validation-failed", {
|
|
2040
|
+
validation,
|
|
2041
|
+
dirtyAfterValidation: this.hasWorkingChanges,
|
|
2042
|
+
});
|
|
2043
|
+
return validation;
|
|
2044
|
+
}
|
|
2045
|
+
const result = await this.commitWorkingImagesAsCropped();
|
|
2046
|
+
console.info("[ImageTool] completeImageSession:done", {
|
|
2047
|
+
result,
|
|
2048
|
+
dirtyAfterComplete: this.hasWorkingChanges,
|
|
2049
|
+
workingCount: this.workingItems.length,
|
|
2050
|
+
committedCount: this.items.length,
|
|
2051
|
+
sessionDirty: sessionState?.isDirty(this.id),
|
|
2052
|
+
});
|
|
2053
|
+
return result;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1850
2056
|
private async exportCroppedImageByIds(
|
|
1851
2057
|
imageIds: string[],
|
|
1852
2058
|
options: ExportCroppedImageOptions,
|