@pooder/kit 6.2.1 → 6.3.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/.test-dist/src/extensions/dieline/renderBuilder.js +19 -2
- package/.test-dist/src/extensions/image/ImageTool.js +180 -467
- package/.test-dist/src/extensions/image/commands.js +60 -40
- package/.test-dist/src/extensions/image/imageOperations.js +75 -0
- package/.test-dist/src/extensions/image/index.js +1 -0
- package/.test-dist/src/extensions/image/model.js +4 -0
- package/.test-dist/src/extensions/image/sessionOverlay.js +148 -0
- package/.test-dist/src/extensions/ruler/RulerTool.js +1 -1
- package/.test-dist/tests/run.js +39 -5
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +252 -168
- package/dist/index.d.ts +252 -168
- package/dist/index.js +806 -846
- package/dist/index.mjs +802 -845
- package/package.json +1 -1
- package/src/extensions/dieline/renderBuilder.ts +26 -4
- package/src/extensions/image/ImageTool.ts +229 -557
- package/src/extensions/image/commands.ts +69 -48
- package/src/extensions/image/imageOperations.ts +135 -0
- package/src/extensions/image/index.ts +1 -0
- package/src/extensions/image/model.ts +13 -1
- package/src/extensions/image/sessionOverlay.ts +206 -0
- package/tests/run.ts +49 -8
|
@@ -14,24 +14,15 @@ import {
|
|
|
14
14
|
Point,
|
|
15
15
|
controlsUtils,
|
|
16
16
|
} from "fabric";
|
|
17
|
-
import {
|
|
18
|
-
CanvasService,
|
|
19
|
-
RenderLayoutRect,
|
|
20
|
-
RenderObjectSpec,
|
|
21
|
-
} from "../../services";
|
|
22
|
-
import { isDielineShape, normalizeShapeStyle } from "../dielineShape";
|
|
23
|
-
import type { DielineShape, DielineShapeStyle } from "../dielineShape";
|
|
24
|
-
import { generateDielinePath, getPathBounds } from "../geometry";
|
|
17
|
+
import { CanvasService, RenderObjectSpec } from "../../services";
|
|
25
18
|
import {
|
|
26
19
|
buildSceneGeometry,
|
|
27
20
|
computeSceneLayout,
|
|
28
21
|
readSizeState,
|
|
22
|
+
type SceneGeometrySnapshot,
|
|
23
|
+
type SceneLayoutSnapshot,
|
|
29
24
|
} from "../../shared/scene/sceneLayoutModel";
|
|
30
|
-
import {
|
|
31
|
-
type FrameRect,
|
|
32
|
-
resolveCutFrameRect,
|
|
33
|
-
toLayoutSceneRect as toSceneLayoutRect,
|
|
34
|
-
} from "../../shared/scene/frame";
|
|
25
|
+
import { type FrameRect, resolveCutFrameRect } from "../../shared/scene/frame";
|
|
35
26
|
import {
|
|
36
27
|
createSourceSizeCache,
|
|
37
28
|
getCoverScale as getCoverScaleFromRect,
|
|
@@ -48,6 +39,12 @@ import {
|
|
|
48
39
|
} from "../../shared/constants/layers";
|
|
49
40
|
import { createImageCommands } from "./commands";
|
|
50
41
|
import { createImageConfigurations } from "./config";
|
|
42
|
+
import {
|
|
43
|
+
computeImageOperationUpdates,
|
|
44
|
+
resolveImageOperationArea,
|
|
45
|
+
type ImageOperation,
|
|
46
|
+
} from "./imageOperations";
|
|
47
|
+
import { buildImageSessionOverlaySpecs } from "./sessionOverlay";
|
|
51
48
|
|
|
52
49
|
export interface ImageItem {
|
|
53
50
|
id: string;
|
|
@@ -61,6 +58,25 @@ export interface ImageItem {
|
|
|
61
58
|
committedUrl?: string;
|
|
62
59
|
}
|
|
63
60
|
|
|
61
|
+
export interface ImageTransformUpdates {
|
|
62
|
+
scale?: number;
|
|
63
|
+
angle?: number;
|
|
64
|
+
left?: number;
|
|
65
|
+
top?: number;
|
|
66
|
+
opacity?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ImageViewState {
|
|
70
|
+
items: ImageItem[];
|
|
71
|
+
hasAnyImage: boolean;
|
|
72
|
+
focusedId: string | null;
|
|
73
|
+
focusedItem: ImageItem | null;
|
|
74
|
+
isToolActive: boolean;
|
|
75
|
+
isImageSelectionActive: boolean;
|
|
76
|
+
hasWorkingChanges: boolean;
|
|
77
|
+
source: "working" | "committed";
|
|
78
|
+
}
|
|
79
|
+
|
|
64
80
|
interface RenderImageState {
|
|
65
81
|
src: string;
|
|
66
82
|
left: number;
|
|
@@ -91,27 +107,16 @@ interface ImageControlVisualConfig {
|
|
|
91
107
|
padding: number;
|
|
92
108
|
}
|
|
93
109
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
shape: DielineShape;
|
|
98
|
-
shapeStyle: DielineShapeStyle;
|
|
99
|
-
radius: number;
|
|
100
|
-
offset: number;
|
|
110
|
+
interface ImageSessionOverlayState {
|
|
111
|
+
layout: SceneLayoutSnapshot;
|
|
112
|
+
geometry: SceneGeometrySnapshot;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
interface UpsertImageOptions {
|
|
104
116
|
id?: string;
|
|
105
117
|
mode?: "replace" | "add";
|
|
106
118
|
addOptions?: Partial<ImageItem>;
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
interface DielineFitArea {
|
|
111
|
-
width: number;
|
|
112
|
-
height: number;
|
|
113
|
-
left: number;
|
|
114
|
-
top: number;
|
|
119
|
+
operation?: ImageOperation;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
interface UpdateImageOptions {
|
|
@@ -382,6 +387,7 @@ export class ImageTool implements Extension {
|
|
|
382
387
|
this.clearRenderedImages();
|
|
383
388
|
this.renderProducerDisposable?.dispose();
|
|
384
389
|
this.renderProducerDisposable = undefined;
|
|
390
|
+
this.emitImageStateChange();
|
|
385
391
|
if (this.canvasService) {
|
|
386
392
|
void this.canvasService.flushRenderFromProducers();
|
|
387
393
|
this.canvasService = undefined;
|
|
@@ -663,10 +669,21 @@ export class ImageTool implements Extension {
|
|
|
663
669
|
}
|
|
664
670
|
}
|
|
665
671
|
|
|
672
|
+
private clearSnapGuideContext() {
|
|
673
|
+
const topContext = this.canvasService?.canvas.contextTop;
|
|
674
|
+
if (!this.canvasService || !topContext) return;
|
|
675
|
+
this.canvasService.canvas.clearContext(topContext);
|
|
676
|
+
}
|
|
677
|
+
|
|
666
678
|
private clearSnapPreview() {
|
|
679
|
+
const shouldClearCanvas =
|
|
680
|
+
this.hasRenderedSnapGuides || !!this.activeSnapX || !!this.activeSnapY;
|
|
667
681
|
this.activeSnapX = null;
|
|
668
682
|
this.activeSnapY = null;
|
|
669
683
|
this.hasRenderedSnapGuides = false;
|
|
684
|
+
if (shouldClearCanvas) {
|
|
685
|
+
this.clearSnapGuideContext();
|
|
686
|
+
}
|
|
670
687
|
this.canvasService?.requestRenderAll();
|
|
671
688
|
}
|
|
672
689
|
|
|
@@ -933,9 +950,9 @@ export class ImageTool implements Extension {
|
|
|
933
950
|
name: "Image",
|
|
934
951
|
interaction: "session",
|
|
935
952
|
commands: {
|
|
936
|
-
begin: "
|
|
953
|
+
begin: "imageSessionReset",
|
|
937
954
|
commit: "completeImages",
|
|
938
|
-
rollback: "
|
|
955
|
+
rollback: "imageSessionReset",
|
|
939
956
|
},
|
|
940
957
|
session: {
|
|
941
958
|
autoBegin: true,
|
|
@@ -980,6 +997,34 @@ export class ImageTool implements Extension {
|
|
|
980
997
|
return this.normalizeItems((items || []).map((i) => ({ ...i })));
|
|
981
998
|
}
|
|
982
999
|
|
|
1000
|
+
private getViewItems(): ImageItem[] {
|
|
1001
|
+
return this.isToolActive ? this.workingItems : this.items;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private getImageViewState(): ImageViewState {
|
|
1005
|
+
this.syncToolActiveFromWorkbench();
|
|
1006
|
+
const items = this.cloneItems(this.getViewItems());
|
|
1007
|
+
const focusedItem =
|
|
1008
|
+
this.focusedImageId == null
|
|
1009
|
+
? null
|
|
1010
|
+
: items.find((item) => item.id === this.focusedImageId) || null;
|
|
1011
|
+
|
|
1012
|
+
return {
|
|
1013
|
+
items,
|
|
1014
|
+
hasAnyImage: items.length > 0,
|
|
1015
|
+
focusedId: this.focusedImageId,
|
|
1016
|
+
focusedItem,
|
|
1017
|
+
isToolActive: this.isToolActive,
|
|
1018
|
+
isImageSelectionActive: this.isImageSelectionActive,
|
|
1019
|
+
hasWorkingChanges: this.hasWorkingChanges,
|
|
1020
|
+
source: this.isToolActive ? "working" : "committed",
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private emitImageStateChange() {
|
|
1025
|
+
this.context?.eventBus.emit("image:state:change", this.getImageViewState());
|
|
1026
|
+
}
|
|
1027
|
+
|
|
983
1028
|
private emitWorkingChange(changedId: string | null = null) {
|
|
984
1029
|
this.context?.eventBus.emit("image:working:change", {
|
|
985
1030
|
changedId,
|
|
@@ -1027,6 +1072,8 @@ export class ImageTool implements Extension {
|
|
|
1027
1072
|
|
|
1028
1073
|
if (!options.skipRender) {
|
|
1029
1074
|
this.updateImages();
|
|
1075
|
+
} else {
|
|
1076
|
+
this.emitImageStateChange();
|
|
1030
1077
|
}
|
|
1031
1078
|
|
|
1032
1079
|
return { ok: true, id };
|
|
@@ -1035,8 +1082,9 @@ export class ImageTool implements Extension {
|
|
|
1035
1082
|
private async addImageEntry(
|
|
1036
1083
|
url: string,
|
|
1037
1084
|
options?: Partial<ImageItem>,
|
|
1038
|
-
|
|
1085
|
+
operation?: ImageOperation,
|
|
1039
1086
|
): Promise<string> {
|
|
1087
|
+
this.syncToolActiveFromWorkbench();
|
|
1040
1088
|
const id = this.generateId();
|
|
1041
1089
|
const newItem = this.normalizeItem({
|
|
1042
1090
|
id,
|
|
@@ -1045,13 +1093,20 @@ export class ImageTool implements Extension {
|
|
|
1045
1093
|
...options,
|
|
1046
1094
|
} as ImageItem);
|
|
1047
1095
|
|
|
1048
|
-
const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
|
|
1049
1096
|
const waitLoaded = this.waitImageLoaded(id, true);
|
|
1050
|
-
|
|
1051
|
-
|
|
1097
|
+
if (this.isToolActive) {
|
|
1098
|
+
this.workingItems = this.cloneItems([...this.workingItems, newItem]);
|
|
1099
|
+
this.hasWorkingChanges = true;
|
|
1100
|
+
this.updateImages();
|
|
1101
|
+
this.emitWorkingChange(id);
|
|
1102
|
+
} else {
|
|
1103
|
+
this.updateConfig([...this.items, newItem]);
|
|
1104
|
+
}
|
|
1052
1105
|
const loaded = await waitLoaded;
|
|
1053
|
-
if (loaded &&
|
|
1054
|
-
await this.
|
|
1106
|
+
if (loaded && operation) {
|
|
1107
|
+
await this.applyImageOperation(id, operation, {
|
|
1108
|
+
target: this.isToolActive ? "working" : "config",
|
|
1109
|
+
});
|
|
1055
1110
|
}
|
|
1056
1111
|
if (loaded) {
|
|
1057
1112
|
this.setImageFocus(id);
|
|
@@ -1063,8 +1118,8 @@ export class ImageTool implements Extension {
|
|
|
1063
1118
|
url: string,
|
|
1064
1119
|
options: UpsertImageOptions = {},
|
|
1065
1120
|
): Promise<{ id: string; mode: "replace" | "add" }> {
|
|
1121
|
+
this.syncToolActiveFromWorkbench();
|
|
1066
1122
|
const mode = options.mode || (options.id ? "replace" : "add");
|
|
1067
|
-
const fitOnAdd = options.fitOnAdd !== false;
|
|
1068
1123
|
if (mode === "replace") {
|
|
1069
1124
|
if (!options.id) {
|
|
1070
1125
|
throw new Error("replace-target-id-required");
|
|
@@ -1073,25 +1128,35 @@ export class ImageTool implements Extension {
|
|
|
1073
1128
|
if (!this.hasImageItem(targetId)) {
|
|
1074
1129
|
throw new Error("replace-target-not-found");
|
|
1075
1130
|
}
|
|
1076
|
-
|
|
1131
|
+
if (this.isToolActive) {
|
|
1132
|
+
const current =
|
|
1133
|
+
this.workingItems.find((item) => item.id === targetId) ||
|
|
1134
|
+
this.items.find((item) => item.id === targetId);
|
|
1135
|
+
this.purgeSourceSizeCacheForItem(current);
|
|
1136
|
+
this.updateImageInWorking(targetId, {
|
|
1137
|
+
url,
|
|
1138
|
+
sourceUrl: url,
|
|
1139
|
+
committedUrl: undefined,
|
|
1140
|
+
});
|
|
1141
|
+
} else {
|
|
1142
|
+
await this.updateImageInConfig(targetId, { url });
|
|
1143
|
+
}
|
|
1144
|
+
const loaded = await this.waitImageLoaded(targetId, true);
|
|
1145
|
+
if (loaded && options.operation) {
|
|
1146
|
+
await this.applyImageOperation(targetId, options.operation, {
|
|
1147
|
+
target: this.isToolActive ? "working" : "config",
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
if (loaded) {
|
|
1151
|
+
this.setImageFocus(targetId);
|
|
1152
|
+
}
|
|
1077
1153
|
return { id: targetId, mode: "replace" };
|
|
1078
1154
|
}
|
|
1079
1155
|
|
|
1080
|
-
const id = await this.addImageEntry(url, options.addOptions,
|
|
1156
|
+
const id = await this.addImageEntry(url, options.addOptions, options.operation);
|
|
1081
1157
|
return { id, mode: "add" };
|
|
1082
1158
|
}
|
|
1083
1159
|
|
|
1084
|
-
private addItemToWorkingSessionIfNeeded(
|
|
1085
|
-
item: ImageItem,
|
|
1086
|
-
sessionDirtyBeforeAdd: boolean,
|
|
1087
|
-
) {
|
|
1088
|
-
if (!sessionDirtyBeforeAdd || !this.isToolActive) return;
|
|
1089
|
-
if (this.workingItems.some((existing) => existing.id === item.id)) return;
|
|
1090
|
-
this.workingItems = this.cloneItems([...this.workingItems, item]);
|
|
1091
|
-
this.updateImages();
|
|
1092
|
-
this.emitWorkingChange(item.id);
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
1160
|
private async updateImage(
|
|
1096
1161
|
id: string,
|
|
1097
1162
|
updates: Partial<ImageItem>,
|
|
@@ -1166,42 +1231,6 @@ export class ImageTool implements Extension {
|
|
|
1166
1231
|
return this.canvasService.toScreenRect(frame || this.getFrameRect());
|
|
1167
1232
|
}
|
|
1168
1233
|
|
|
1169
|
-
private toLayoutSceneRect(rect: FrameRect): RenderLayoutRect {
|
|
1170
|
-
return toSceneLayoutRect(rect);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
private async resolveDefaultFitArea(): Promise<DielineFitArea | null> {
|
|
1174
|
-
if (!this.canvasService) return null;
|
|
1175
|
-
const frame = this.getFrameRect();
|
|
1176
|
-
if (frame.width <= 0 || frame.height <= 0) return null;
|
|
1177
|
-
return {
|
|
1178
|
-
width: Math.max(1, frame.width),
|
|
1179
|
-
height: Math.max(1, frame.height),
|
|
1180
|
-
left: frame.left + frame.width / 2,
|
|
1181
|
-
top: frame.top + frame.height / 2,
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
private async fitImageToDefaultArea(id: string) {
|
|
1186
|
-
if (!this.canvasService) return;
|
|
1187
|
-
const area = await this.resolveDefaultFitArea();
|
|
1188
|
-
|
|
1189
|
-
if (area) {
|
|
1190
|
-
await this.fitImageToArea(id, area);
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
const viewport = this.canvasService.getSceneViewportRect();
|
|
1195
|
-
const canvasW = Math.max(1, viewport.width || 0);
|
|
1196
|
-
const canvasH = Math.max(1, viewport.height || 0);
|
|
1197
|
-
await this.fitImageToArea(id, {
|
|
1198
|
-
width: canvasW,
|
|
1199
|
-
height: canvasH,
|
|
1200
|
-
left: viewport.left + canvasW / 2,
|
|
1201
|
-
top: viewport.top + canvasH / 2,
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
1234
|
private getImageObjects(): any[] {
|
|
1206
1235
|
if (!this.canvasService) return [];
|
|
1207
1236
|
return this.canvasService.canvas.getObjects().filter((obj: any) => {
|
|
@@ -1319,94 +1348,42 @@ export class ImageTool implements Extension {
|
|
|
1319
1348
|
};
|
|
1320
1349
|
}
|
|
1321
1350
|
|
|
1322
|
-
private
|
|
1323
|
-
|
|
1324
|
-
if (!isDielineShape(shape)) {
|
|
1351
|
+
private resolveSessionOverlayState(): ImageSessionOverlayState | null {
|
|
1352
|
+
if (!this.canvasService || !this.context) {
|
|
1325
1353
|
return null;
|
|
1326
1354
|
}
|
|
1327
|
-
|
|
1328
|
-
const radiusRaw = Number(raw?.radius);
|
|
1329
|
-
const offsetRaw = Number(raw?.offset);
|
|
1330
|
-
const unit = typeof raw?.unit === "string" ? raw.unit : "px";
|
|
1331
|
-
const radius =
|
|
1332
|
-
unit === "scene" || !this.canvasService
|
|
1333
|
-
? radiusRaw
|
|
1334
|
-
: this.canvasService.toSceneLength(radiusRaw);
|
|
1335
|
-
const offset =
|
|
1336
|
-
unit === "scene" || !this.canvasService
|
|
1337
|
-
? offsetRaw
|
|
1338
|
-
: this.canvasService.toSceneLength(offsetRaw);
|
|
1339
|
-
return {
|
|
1340
|
-
shape,
|
|
1341
|
-
shapeStyle: normalizeShapeStyle(raw?.shapeStyle),
|
|
1342
|
-
radius: Number.isFinite(radius) ? radius : 0,
|
|
1343
|
-
offset: Number.isFinite(offset) ? offset : 0,
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
private async resolveSceneGeometryForOverlay(): Promise<SceneGeometryLike | null> {
|
|
1348
|
-
if (!this.context) return null;
|
|
1349
|
-
const commandService = this.context.services.get<any>("CommandService");
|
|
1350
|
-
if (commandService) {
|
|
1351
|
-
try {
|
|
1352
|
-
const raw = await Promise.resolve(
|
|
1353
|
-
commandService.executeCommand("getSceneGeometry"),
|
|
1354
|
-
);
|
|
1355
|
-
const geometry = this.toSceneGeometryLike(raw);
|
|
1356
|
-
if (geometry) {
|
|
1357
|
-
this.debug("overlay:sceneGeometry:command", geometry);
|
|
1358
|
-
return geometry;
|
|
1359
|
-
}
|
|
1360
|
-
this.debug("overlay:sceneGeometry:command:invalid", { raw });
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
this.debug("overlay:sceneGeometry:command:error", {
|
|
1363
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
if (!this.canvasService) return null;
|
|
1369
1355
|
const configService = this.context.services.get<ConfigurationService>(
|
|
1370
1356
|
"ConfigurationService",
|
|
1371
1357
|
);
|
|
1372
|
-
if (!configService)
|
|
1373
|
-
|
|
1374
|
-
const sizeState = readSizeState(configService);
|
|
1375
|
-
const layout = computeSceneLayout(this.canvasService, sizeState);
|
|
1376
|
-
if (!layout) {
|
|
1377
|
-
this.debug("overlay:sceneGeometry:fallback:missing-layout");
|
|
1358
|
+
if (!configService) {
|
|
1378
1359
|
return null;
|
|
1379
1360
|
}
|
|
1380
1361
|
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1362
|
+
const layout = computeSceneLayout(
|
|
1363
|
+
this.canvasService,
|
|
1364
|
+
readSizeState(configService),
|
|
1383
1365
|
);
|
|
1384
|
-
if (
|
|
1385
|
-
this.debug("overlay:
|
|
1366
|
+
if (!layout) {
|
|
1367
|
+
this.debug("overlay:layout:missing");
|
|
1368
|
+
return null;
|
|
1386
1369
|
}
|
|
1387
|
-
return geometry;
|
|
1388
|
-
}
|
|
1389
1370
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
:
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
|
|
1400
|
-
const maxRadius = Math.max(0, Math.min(frame.width, frame.height) / 2);
|
|
1401
|
-
return Math.max(0, Math.min(maxRadius, rawCutRadius));
|
|
1371
|
+
const geometry = buildSceneGeometry(configService, layout);
|
|
1372
|
+
this.debug("overlay:state:resolved", {
|
|
1373
|
+
cutRect: layout.cutRect,
|
|
1374
|
+
shape: geometry.shape,
|
|
1375
|
+
shapeStyle: geometry.shapeStyle,
|
|
1376
|
+
radius: geometry.radius,
|
|
1377
|
+
offset: geometry.offset,
|
|
1378
|
+
});
|
|
1379
|
+
return { layout, geometry };
|
|
1402
1380
|
}
|
|
1403
1381
|
|
|
1404
1382
|
private getCropShapeHatchPattern(
|
|
1405
1383
|
color = "rgba(255, 0, 0, 0.6)",
|
|
1406
1384
|
): Pattern | undefined {
|
|
1407
1385
|
if (typeof document === "undefined") return undefined;
|
|
1408
|
-
const
|
|
1409
|
-
const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
|
|
1386
|
+
const cacheKey = color;
|
|
1410
1387
|
if (
|
|
1411
1388
|
this.cropShapeHatchPattern &&
|
|
1412
1389
|
this.cropShapeHatchPatternColor === color &&
|
|
@@ -1443,152 +1420,12 @@ export class ImageTool implements Extension {
|
|
|
1443
1420
|
// @ts-ignore: Fabric Pattern accepts canvas source here.
|
|
1444
1421
|
repetition: "repeat",
|
|
1445
1422
|
});
|
|
1446
|
-
// Scene specs are scaled to screen by CanvasService; keep hatch density in screen pixels.
|
|
1447
|
-
(pattern as any).patternTransform = [
|
|
1448
|
-
1 / sceneScale,
|
|
1449
|
-
0,
|
|
1450
|
-
0,
|
|
1451
|
-
1 / sceneScale,
|
|
1452
|
-
0,
|
|
1453
|
-
0,
|
|
1454
|
-
];
|
|
1455
1423
|
this.cropShapeHatchPattern = pattern;
|
|
1456
1424
|
this.cropShapeHatchPatternColor = color;
|
|
1457
1425
|
this.cropShapeHatchPatternKey = cacheKey;
|
|
1458
1426
|
return pattern;
|
|
1459
1427
|
}
|
|
1460
1428
|
|
|
1461
|
-
private buildCropShapeOverlaySpecs(
|
|
1462
|
-
frame: FrameRect,
|
|
1463
|
-
sceneGeometry: SceneGeometryLike | null,
|
|
1464
|
-
): RenderObjectSpec[] {
|
|
1465
|
-
if (!sceneGeometry) {
|
|
1466
|
-
this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
|
|
1467
|
-
return [];
|
|
1468
|
-
}
|
|
1469
|
-
if (sceneGeometry.shape === "custom") {
|
|
1470
|
-
this.debug("overlay:shape:skip", { reason: "shape-custom" });
|
|
1471
|
-
return [];
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
const shape = sceneGeometry.shape as ShapeOverlayShape;
|
|
1475
|
-
const shapeStyle = sceneGeometry.shapeStyle;
|
|
1476
|
-
const inset = 0;
|
|
1477
|
-
const shapeWidth = Math.max(1, frame.width);
|
|
1478
|
-
const shapeHeight = Math.max(1, frame.height);
|
|
1479
|
-
const radius = this.resolveCutShapeRadius(sceneGeometry, frame);
|
|
1480
|
-
|
|
1481
|
-
this.debug("overlay:shape:geometry", {
|
|
1482
|
-
shape,
|
|
1483
|
-
frameWidth: frame.width,
|
|
1484
|
-
frameHeight: frame.height,
|
|
1485
|
-
offset: sceneGeometry.offset,
|
|
1486
|
-
shapeStyle,
|
|
1487
|
-
inset,
|
|
1488
|
-
shapeWidth,
|
|
1489
|
-
shapeHeight,
|
|
1490
|
-
baseRadius: sceneGeometry.radius,
|
|
1491
|
-
radius,
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
const isSameAsFrame =
|
|
1495
|
-
Math.abs(shapeWidth - frame.width) <= 0.0001 &&
|
|
1496
|
-
Math.abs(shapeHeight - frame.height) <= 0.0001;
|
|
1497
|
-
if (shape === "rect" && radius <= 0.0001 && isSameAsFrame) {
|
|
1498
|
-
this.debug("overlay:shape:skip", {
|
|
1499
|
-
reason: "shape-rect-no-radius",
|
|
1500
|
-
});
|
|
1501
|
-
return [];
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
const baseOptions = {
|
|
1505
|
-
shape,
|
|
1506
|
-
width: shapeWidth,
|
|
1507
|
-
height: shapeHeight,
|
|
1508
|
-
radius,
|
|
1509
|
-
x: frame.width / 2,
|
|
1510
|
-
y: frame.height / 2,
|
|
1511
|
-
features: [],
|
|
1512
|
-
shapeStyle,
|
|
1513
|
-
canvasWidth: frame.width,
|
|
1514
|
-
canvasHeight: frame.height,
|
|
1515
|
-
};
|
|
1516
|
-
|
|
1517
|
-
try {
|
|
1518
|
-
const shapePathData = generateDielinePath(baseOptions);
|
|
1519
|
-
const outerRectPathData = `M 0 0 L ${frame.width} 0 L ${frame.width} ${frame.height} L 0 ${frame.height} Z`;
|
|
1520
|
-
const hatchPathData = `${outerRectPathData} ${shapePathData}`;
|
|
1521
|
-
if (!shapePathData || !hatchPathData) {
|
|
1522
|
-
this.debug("overlay:shape:skip", {
|
|
1523
|
-
reason: "path-generation-empty",
|
|
1524
|
-
shape,
|
|
1525
|
-
radius,
|
|
1526
|
-
});
|
|
1527
|
-
return [];
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
const patternFill = this.getCropShapeHatchPattern();
|
|
1531
|
-
const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
|
|
1532
|
-
const shapeBounds = getPathBounds(shapePathData);
|
|
1533
|
-
const hatchBounds = getPathBounds(hatchPathData);
|
|
1534
|
-
const frameRect = this.toLayoutSceneRect(frame);
|
|
1535
|
-
const hatchPathLength = hatchPathData.length;
|
|
1536
|
-
const shapePathLength = shapePathData.length;
|
|
1537
|
-
const specs: RenderObjectSpec[] = [
|
|
1538
|
-
{
|
|
1539
|
-
id: "image.cropShapeHatch",
|
|
1540
|
-
type: "path",
|
|
1541
|
-
data: { id: "image.cropShapeHatch", zIndex: 5 },
|
|
1542
|
-
layout: {
|
|
1543
|
-
reference: "custom",
|
|
1544
|
-
referenceRect: frameRect,
|
|
1545
|
-
alignX: "start",
|
|
1546
|
-
alignY: "start",
|
|
1547
|
-
offsetX: hatchBounds.x,
|
|
1548
|
-
offsetY: hatchBounds.y,
|
|
1549
|
-
},
|
|
1550
|
-
props: {
|
|
1551
|
-
pathData: hatchPathData,
|
|
1552
|
-
originX: "left",
|
|
1553
|
-
originY: "top",
|
|
1554
|
-
fill: hatchFill,
|
|
1555
|
-
opacity: patternFill ? 1 : 0.8,
|
|
1556
|
-
stroke: "rgba(255, 0, 0, 0.9)",
|
|
1557
|
-
strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
|
|
1558
|
-
fillRule: "evenodd",
|
|
1559
|
-
selectable: false,
|
|
1560
|
-
evented: false,
|
|
1561
|
-
excludeFromExport: true,
|
|
1562
|
-
objectCaching: false,
|
|
1563
|
-
},
|
|
1564
|
-
},
|
|
1565
|
-
];
|
|
1566
|
-
this.debug("overlay:shape:built", {
|
|
1567
|
-
shape,
|
|
1568
|
-
radius,
|
|
1569
|
-
inset,
|
|
1570
|
-
shapeWidth,
|
|
1571
|
-
shapeHeight,
|
|
1572
|
-
fillRule: "evenodd",
|
|
1573
|
-
shapePathLength,
|
|
1574
|
-
hatchPathLength,
|
|
1575
|
-
shapeBounds,
|
|
1576
|
-
hatchBounds,
|
|
1577
|
-
hatchFillType:
|
|
1578
|
-
hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
|
|
1579
|
-
ids: specs.map((spec) => spec.id),
|
|
1580
|
-
});
|
|
1581
|
-
return specs;
|
|
1582
|
-
} catch (error) {
|
|
1583
|
-
this.debug("overlay:shape:error", {
|
|
1584
|
-
shape,
|
|
1585
|
-
radius,
|
|
1586
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1587
|
-
});
|
|
1588
|
-
return [];
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
1429
|
private resolveRenderImageState(item: ImageItem): RenderImageState {
|
|
1593
1430
|
const active = this.isToolActive;
|
|
1594
1431
|
const sourceUrl = item.sourceUrl || item.url;
|
|
@@ -1689,19 +1526,13 @@ export class ImageTool implements Extension {
|
|
|
1689
1526
|
}
|
|
1690
1527
|
|
|
1691
1528
|
private buildOverlaySpecs(
|
|
1692
|
-
|
|
1693
|
-
sceneGeometry: SceneGeometryLike | null,
|
|
1529
|
+
overlayState: ImageSessionOverlayState | null,
|
|
1694
1530
|
): RenderObjectSpec[] {
|
|
1695
1531
|
const visible = this.isImageEditingVisible();
|
|
1696
|
-
if (
|
|
1697
|
-
!visible ||
|
|
1698
|
-
frame.width <= 0 ||
|
|
1699
|
-
frame.height <= 0 ||
|
|
1700
|
-
!this.canvasService
|
|
1701
|
-
) {
|
|
1532
|
+
if (!visible || !overlayState || !this.canvasService) {
|
|
1702
1533
|
this.debug("overlay:hidden", {
|
|
1703
1534
|
visible,
|
|
1704
|
-
|
|
1535
|
+
cutRect: overlayState?.layout.cutRect,
|
|
1705
1536
|
isToolActive: this.isToolActive,
|
|
1706
1537
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
1707
1538
|
focusedImageId: this.focusedImageId,
|
|
@@ -1709,174 +1540,23 @@ export class ImageTool implements Extension {
|
|
|
1709
1540
|
return [];
|
|
1710
1541
|
}
|
|
1711
1542
|
|
|
1712
|
-
const viewport = this.canvasService.
|
|
1713
|
-
const canvasW = viewport.width || 0;
|
|
1714
|
-
const canvasH = viewport.height || 0;
|
|
1715
|
-
const canvasLeft = viewport.left || 0;
|
|
1716
|
-
const canvasTop = viewport.top || 0;
|
|
1543
|
+
const viewport = this.canvasService.getScreenViewportRect();
|
|
1717
1544
|
const visual = this.getFrameVisualConfig();
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
canvasLeft,
|
|
1725
|
-
Math.min(canvasLeft + canvasW, frame.left),
|
|
1726
|
-
);
|
|
1727
|
-
const frameTop = Math.max(
|
|
1728
|
-
canvasTop,
|
|
1729
|
-
Math.min(canvasTop + canvasH, frame.top),
|
|
1730
|
-
);
|
|
1731
|
-
const frameRight = Math.max(
|
|
1732
|
-
frameLeft,
|
|
1733
|
-
Math.min(canvasLeft + canvasW, frame.left + frame.width),
|
|
1734
|
-
);
|
|
1735
|
-
const frameBottom = Math.max(
|
|
1736
|
-
frameTop,
|
|
1737
|
-
Math.min(canvasTop + canvasH, frame.top + frame.height),
|
|
1738
|
-
);
|
|
1739
|
-
const visibleFrameH = Math.max(0, frameBottom - frameTop);
|
|
1740
|
-
|
|
1741
|
-
const topH = Math.max(0, frameTop - canvasTop);
|
|
1742
|
-
const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
|
|
1743
|
-
const leftW = Math.max(0, frameLeft - canvasLeft);
|
|
1744
|
-
const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
|
|
1745
|
-
const viewportRect = this.toLayoutSceneRect({
|
|
1746
|
-
left: canvasLeft,
|
|
1747
|
-
top: canvasTop,
|
|
1748
|
-
width: canvasW,
|
|
1749
|
-
height: canvasH,
|
|
1750
|
-
});
|
|
1751
|
-
const visibleFrameBandRect = this.toLayoutSceneRect({
|
|
1752
|
-
left: canvasLeft,
|
|
1753
|
-
top: frameTop,
|
|
1754
|
-
width: canvasW,
|
|
1755
|
-
height: visibleFrameH,
|
|
1756
|
-
});
|
|
1757
|
-
const frameRect = this.toLayoutSceneRect(frame);
|
|
1758
|
-
const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
|
|
1759
|
-
|
|
1760
|
-
const mask: RenderObjectSpec[] = [
|
|
1761
|
-
{
|
|
1762
|
-
id: "image.cropMask.top",
|
|
1763
|
-
type: "rect",
|
|
1764
|
-
data: { id: "image.cropMask.top", zIndex: 1 },
|
|
1765
|
-
layout: {
|
|
1766
|
-
reference: "custom",
|
|
1767
|
-
referenceRect: viewportRect,
|
|
1768
|
-
alignX: "start",
|
|
1769
|
-
alignY: "start",
|
|
1770
|
-
width: "100%",
|
|
1771
|
-
height: topH,
|
|
1772
|
-
},
|
|
1773
|
-
props: {
|
|
1774
|
-
originX: "left",
|
|
1775
|
-
originY: "top",
|
|
1776
|
-
fill: visual.outerBackground,
|
|
1777
|
-
selectable: false,
|
|
1778
|
-
evented: false,
|
|
1779
|
-
},
|
|
1780
|
-
},
|
|
1781
|
-
{
|
|
1782
|
-
id: "image.cropMask.bottom",
|
|
1783
|
-
type: "rect",
|
|
1784
|
-
data: { id: "image.cropMask.bottom", zIndex: 2 },
|
|
1785
|
-
layout: {
|
|
1786
|
-
reference: "custom",
|
|
1787
|
-
referenceRect: viewportRect,
|
|
1788
|
-
alignX: "start",
|
|
1789
|
-
alignY: "end",
|
|
1790
|
-
width: "100%",
|
|
1791
|
-
height: bottomH,
|
|
1792
|
-
},
|
|
1793
|
-
props: {
|
|
1794
|
-
originX: "left",
|
|
1795
|
-
originY: "top",
|
|
1796
|
-
fill: visual.outerBackground,
|
|
1797
|
-
selectable: false,
|
|
1798
|
-
evented: false,
|
|
1799
|
-
},
|
|
1800
|
-
},
|
|
1801
|
-
{
|
|
1802
|
-
id: "image.cropMask.left",
|
|
1803
|
-
type: "rect",
|
|
1804
|
-
data: { id: "image.cropMask.left", zIndex: 3 },
|
|
1805
|
-
layout: {
|
|
1806
|
-
reference: "custom",
|
|
1807
|
-
referenceRect: visibleFrameBandRect,
|
|
1808
|
-
alignX: "start",
|
|
1809
|
-
alignY: "start",
|
|
1810
|
-
width: leftW,
|
|
1811
|
-
height: "100%",
|
|
1812
|
-
},
|
|
1813
|
-
props: {
|
|
1814
|
-
originX: "left",
|
|
1815
|
-
originY: "top",
|
|
1816
|
-
fill: visual.outerBackground,
|
|
1817
|
-
selectable: false,
|
|
1818
|
-
evented: false,
|
|
1819
|
-
},
|
|
1820
|
-
},
|
|
1821
|
-
{
|
|
1822
|
-
id: "image.cropMask.right",
|
|
1823
|
-
type: "rect",
|
|
1824
|
-
data: { id: "image.cropMask.right", zIndex: 4 },
|
|
1825
|
-
layout: {
|
|
1826
|
-
reference: "custom",
|
|
1827
|
-
referenceRect: visibleFrameBandRect,
|
|
1828
|
-
alignX: "end",
|
|
1829
|
-
alignY: "start",
|
|
1830
|
-
width: rightW,
|
|
1831
|
-
height: "100%",
|
|
1832
|
-
},
|
|
1833
|
-
props: {
|
|
1834
|
-
originX: "left",
|
|
1835
|
-
originY: "top",
|
|
1836
|
-
fill: visual.outerBackground,
|
|
1837
|
-
selectable: false,
|
|
1838
|
-
evented: false,
|
|
1839
|
-
},
|
|
1545
|
+
const specs = buildImageSessionOverlaySpecs({
|
|
1546
|
+
viewport: {
|
|
1547
|
+
left: viewport.left,
|
|
1548
|
+
top: viewport.top,
|
|
1549
|
+
width: viewport.width,
|
|
1550
|
+
height: viewport.height,
|
|
1840
1551
|
},
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
data: { id: "image.cropFrame", zIndex: 7 },
|
|
1847
|
-
layout: {
|
|
1848
|
-
reference: "custom",
|
|
1849
|
-
referenceRect: frameRect,
|
|
1850
|
-
alignX: "start",
|
|
1851
|
-
alignY: "start",
|
|
1852
|
-
width: "100%",
|
|
1853
|
-
height: "100%",
|
|
1854
|
-
},
|
|
1855
|
-
props: {
|
|
1856
|
-
originX: "left",
|
|
1857
|
-
originY: "top",
|
|
1858
|
-
fill: visual.innerBackground,
|
|
1859
|
-
stroke:
|
|
1860
|
-
visual.strokeStyle === "hidden"
|
|
1861
|
-
? "rgba(0,0,0,0)"
|
|
1862
|
-
: visual.strokeColor,
|
|
1863
|
-
strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
|
|
1864
|
-
strokeDashArray:
|
|
1865
|
-
visual.strokeStyle === "dashed"
|
|
1866
|
-
? [dashLengthScene, dashLengthScene]
|
|
1867
|
-
: undefined,
|
|
1868
|
-
selectable: false,
|
|
1869
|
-
evented: false,
|
|
1870
|
-
},
|
|
1871
|
-
};
|
|
1872
|
-
|
|
1873
|
-
const specs =
|
|
1874
|
-
shapeOverlay.length > 0
|
|
1875
|
-
? [...mask, ...shapeOverlay]
|
|
1876
|
-
: [...mask, ...shapeOverlay, frameSpec];
|
|
1552
|
+
layout: overlayState.layout,
|
|
1553
|
+
geometry: overlayState.geometry,
|
|
1554
|
+
visual,
|
|
1555
|
+
hatchPattern: this.getCropShapeHatchPattern(),
|
|
1556
|
+
});
|
|
1877
1557
|
this.debug("overlay:built", {
|
|
1878
|
-
|
|
1879
|
-
shape:
|
|
1558
|
+
cutRect: overlayState.layout.cutRect,
|
|
1559
|
+
shape: overlayState.geometry.shape,
|
|
1880
1560
|
overlayIds: specs.map((spec) => ({
|
|
1881
1561
|
id: spec.id,
|
|
1882
1562
|
zIndex: spec.data?.zIndex,
|
|
@@ -1907,11 +1587,10 @@ export class ImageTool implements Extension {
|
|
|
1907
1587
|
const imageSpecs = await this.buildImageSpecs(renderItems, frame);
|
|
1908
1588
|
if (seq !== this.renderSeq) return;
|
|
1909
1589
|
|
|
1910
|
-
const
|
|
1911
|
-
if (seq !== this.renderSeq) return;
|
|
1590
|
+
const overlayState = this.resolveSessionOverlayState();
|
|
1912
1591
|
|
|
1913
1592
|
this.imageSpecs = imageSpecs;
|
|
1914
|
-
this.overlaySpecs = this.buildOverlaySpecs(
|
|
1593
|
+
this.overlaySpecs = this.buildOverlaySpecs(overlayState);
|
|
1915
1594
|
await this.canvasService.flushRenderFromProducers();
|
|
1916
1595
|
if (seq !== this.renderSeq) return;
|
|
1917
1596
|
this.refreshImageObjectInteractionState();
|
|
@@ -1942,6 +1621,7 @@ export class ImageTool implements Extension {
|
|
|
1942
1621
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
1943
1622
|
focusedImageId: this.focusedImageId,
|
|
1944
1623
|
});
|
|
1624
|
+
this.emitImageStateChange();
|
|
1945
1625
|
this.canvasService.requestRenderAll();
|
|
1946
1626
|
}
|
|
1947
1627
|
|
|
@@ -1949,6 +1629,40 @@ export class ImageTool implements Extension {
|
|
|
1949
1629
|
return Math.max(-1, Math.min(2, value));
|
|
1950
1630
|
}
|
|
1951
1631
|
|
|
1632
|
+
private async setImageTransform(
|
|
1633
|
+
id: string,
|
|
1634
|
+
updates: ImageTransformUpdates,
|
|
1635
|
+
options: UpdateImageOptions = {},
|
|
1636
|
+
) {
|
|
1637
|
+
const next: Partial<ImageItem> = {};
|
|
1638
|
+
|
|
1639
|
+
if (Number.isFinite(updates.scale as number)) {
|
|
1640
|
+
next.scale = Math.max(0.05, Number(updates.scale));
|
|
1641
|
+
}
|
|
1642
|
+
if (Number.isFinite(updates.angle as number)) {
|
|
1643
|
+
next.angle = Number(updates.angle);
|
|
1644
|
+
}
|
|
1645
|
+
if (Number.isFinite(updates.left as number)) {
|
|
1646
|
+
next.left = this.clampNormalized(Number(updates.left));
|
|
1647
|
+
}
|
|
1648
|
+
if (Number.isFinite(updates.top as number)) {
|
|
1649
|
+
next.top = this.clampNormalized(Number(updates.top));
|
|
1650
|
+
}
|
|
1651
|
+
if (Number.isFinite(updates.opacity as number)) {
|
|
1652
|
+
next.opacity = Math.max(0, Math.min(1, Number(updates.opacity)));
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (!Object.keys(next).length) return;
|
|
1656
|
+
await this.updateImage(id, next, options);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
private resetImageSession() {
|
|
1660
|
+
this.workingItems = this.cloneItems(this.items);
|
|
1661
|
+
this.hasWorkingChanges = false;
|
|
1662
|
+
this.updateImages();
|
|
1663
|
+
this.emitWorkingChange();
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1952
1666
|
private onObjectModified = (e: any) => {
|
|
1953
1667
|
if (!this.isToolActive) return;
|
|
1954
1668
|
const target = e?.target;
|
|
@@ -2024,10 +1738,6 @@ export class ImageTool implements Extension {
|
|
|
2024
1738
|
url: replacingUrl,
|
|
2025
1739
|
sourceUrl: replacingUrl,
|
|
2026
1740
|
committedUrl: undefined,
|
|
2027
|
-
scale: updates.scale ?? 1,
|
|
2028
|
-
angle: updates.angle ?? 0,
|
|
2029
|
-
left: updates.left ?? 0.5,
|
|
2030
|
-
top: updates.top ?? 0.5,
|
|
2031
1741
|
}
|
|
2032
1742
|
: {}),
|
|
2033
1743
|
});
|
|
@@ -2035,14 +1745,7 @@ export class ImageTool implements Extension {
|
|
|
2035
1745
|
this.updateConfig(next);
|
|
2036
1746
|
|
|
2037
1747
|
if (replacingSource) {
|
|
2038
|
-
this.debug("replace:image:begin", { id, replacingUrl });
|
|
2039
1748
|
this.purgeSourceSizeCacheForItem(base);
|
|
2040
|
-
const loaded = await this.waitImageLoaded(id, true);
|
|
2041
|
-
this.debug("replace:image:loaded", { id, loaded });
|
|
2042
|
-
if (loaded) {
|
|
2043
|
-
await this.refitImageToFrame(id);
|
|
2044
|
-
this.setImageFocus(id);
|
|
2045
|
-
}
|
|
2046
1749
|
}
|
|
2047
1750
|
}
|
|
2048
1751
|
|
|
@@ -2064,93 +1767,62 @@ export class ImageTool implements Extension {
|
|
|
2064
1767
|
});
|
|
2065
1768
|
}
|
|
2066
1769
|
|
|
2067
|
-
private async
|
|
1770
|
+
private async resolveImageSourceSize(
|
|
1771
|
+
id: string,
|
|
1772
|
+
src: string,
|
|
1773
|
+
): Promise<SourceSize | null> {
|
|
2068
1774
|
const obj = this.getImageObject(id);
|
|
2069
|
-
if (
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
const
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
const source = this.getSourceSize(render.src, obj);
|
|
2076
|
-
const frame = this.getFrameRect();
|
|
2077
|
-
const coverScale = this.getCoverScale(frame, source);
|
|
2078
|
-
|
|
2079
|
-
const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
|
|
2080
|
-
const zoom = Math.max(0.05, currentScale / coverScale);
|
|
2081
|
-
|
|
2082
|
-
const updated: Partial<ImageItem> = {
|
|
2083
|
-
scale: Number.isFinite(zoom) ? zoom : 1,
|
|
2084
|
-
angle: 0,
|
|
2085
|
-
left: 0.5,
|
|
2086
|
-
top: 0.5,
|
|
2087
|
-
};
|
|
2088
|
-
|
|
2089
|
-
const index = this.items.findIndex((item) => item.id === id);
|
|
2090
|
-
if (index < 0) return;
|
|
1775
|
+
if (obj) {
|
|
1776
|
+
this.rememberSourceSize(src, obj);
|
|
1777
|
+
}
|
|
1778
|
+
const ensured = await this.ensureSourceSize(src);
|
|
1779
|
+
if (ensured) return ensured;
|
|
1780
|
+
if (!obj) return null;
|
|
2091
1781
|
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
this.hasWorkingChanges = false;
|
|
2097
|
-
this.updateImages();
|
|
2098
|
-
this.emitWorkingChange(id);
|
|
1782
|
+
const width = Number(obj?.width || 0);
|
|
1783
|
+
const height = Number(obj?.height || 0);
|
|
1784
|
+
if (width <= 0 || height <= 0) return null;
|
|
1785
|
+
return { width, height };
|
|
2099
1786
|
}
|
|
2100
1787
|
|
|
2101
|
-
private async
|
|
1788
|
+
private async applyImageOperation(
|
|
2102
1789
|
id: string,
|
|
2103
|
-
|
|
1790
|
+
operation: ImageOperation,
|
|
1791
|
+
options: UpdateImageOptions = {},
|
|
2104
1792
|
) {
|
|
2105
1793
|
if (!this.canvasService) return;
|
|
2106
1794
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
1795
|
+
this.syncToolActiveFromWorkbench();
|
|
1796
|
+
const target = options.target || "auto";
|
|
1797
|
+
const renderItems =
|
|
1798
|
+
target === "working" || (target === "auto" && this.isToolActive)
|
|
1799
|
+
? this.workingItems
|
|
1800
|
+
: this.items;
|
|
2113
1801
|
const current = renderItems.find((item) => item.id === id);
|
|
2114
1802
|
if (!current) return;
|
|
1803
|
+
|
|
2115
1804
|
const render = this.resolveRenderImageState(current);
|
|
1805
|
+
const source = await this.resolveImageSourceSize(id, render.src);
|
|
1806
|
+
if (!source) return;
|
|
2116
1807
|
|
|
2117
|
-
this.rememberSourceSize(render.src, obj);
|
|
2118
|
-
const source = this.getSourceSize(render.src, obj);
|
|
2119
1808
|
const frame = this.getFrameRect();
|
|
2120
|
-
const baseCover = this.getCoverScale(frame, source);
|
|
2121
|
-
|
|
2122
|
-
const desiredScale = Math.max(
|
|
2123
|
-
Math.max(1, area.width) / Math.max(1, source.width),
|
|
2124
|
-
Math.max(1, area.height) / Math.max(1, source.height),
|
|
2125
|
-
);
|
|
2126
|
-
|
|
2127
1809
|
const viewport = this.canvasService.getSceneViewportRect();
|
|
2128
|
-
const
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
const updates: Partial<ImageItem> = {
|
|
2144
|
-
scale: Math.max(0.05, desiredScale / baseCover),
|
|
2145
|
-
left: this.clampNormalized(
|
|
2146
|
-
(areaLeftPx - frame.left) / Math.max(1, frame.width),
|
|
2147
|
-
),
|
|
2148
|
-
top: this.clampNormalized(
|
|
2149
|
-
(areaTopPx - frame.top) / Math.max(1, frame.height),
|
|
2150
|
-
),
|
|
2151
|
-
};
|
|
1810
|
+
const area =
|
|
1811
|
+
operation.type === "resetTransform"
|
|
1812
|
+
? resolveImageOperationArea({ frame, viewport })
|
|
1813
|
+
: resolveImageOperationArea({
|
|
1814
|
+
frame,
|
|
1815
|
+
viewport,
|
|
1816
|
+
area: operation.area,
|
|
1817
|
+
});
|
|
1818
|
+
const updates = computeImageOperationUpdates({
|
|
1819
|
+
frame,
|
|
1820
|
+
source,
|
|
1821
|
+
operation,
|
|
1822
|
+
area,
|
|
1823
|
+
});
|
|
2152
1824
|
|
|
2153
|
-
if (this.isToolActive) {
|
|
1825
|
+
if (target === "working" || (target === "auto" && this.isToolActive)) {
|
|
2154
1826
|
this.updateImageInWorking(id, updates);
|
|
2155
1827
|
return;
|
|
2156
1828
|
}
|