@pooder/kit 5.2.0 → 5.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/CanvasService.js +249 -249
- package/.test-dist/src/ViewportSystem.js +75 -75
- package/.test-dist/src/background.js +203 -203
- package/.test-dist/src/bridgeSelection.js +20 -20
- package/.test-dist/src/constraints.js +237 -237
- package/.test-dist/src/dieline.js +818 -818
- package/.test-dist/src/edgeScale.js +12 -12
- package/.test-dist/src/extensions/background.js +203 -0
- package/.test-dist/src/extensions/bridgeSelection.js +20 -0
- package/.test-dist/src/extensions/constraints.js +237 -0
- package/.test-dist/src/extensions/dieline.js +828 -0
- package/.test-dist/src/extensions/edgeScale.js +12 -0
- package/.test-dist/src/extensions/feature.js +825 -0
- package/.test-dist/src/extensions/featureComplete.js +32 -0
- package/.test-dist/src/extensions/film.js +167 -0
- package/.test-dist/src/extensions/geometry.js +545 -0
- package/.test-dist/src/extensions/image.js +1529 -0
- package/.test-dist/src/extensions/index.js +30 -0
- package/.test-dist/src/extensions/maskOps.js +279 -0
- package/.test-dist/src/extensions/mirror.js +104 -0
- package/.test-dist/src/extensions/ruler.js +345 -0
- package/.test-dist/src/extensions/sceneLayout.js +96 -0
- package/.test-dist/src/extensions/sceneLayoutModel.js +196 -0
- package/.test-dist/src/extensions/sceneVisibility.js +62 -0
- package/.test-dist/src/extensions/size.js +331 -0
- package/.test-dist/src/extensions/tracer.js +538 -0
- package/.test-dist/src/extensions/white-ink.js +1190 -0
- package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
- package/.test-dist/src/feature.js +826 -826
- package/.test-dist/src/featureComplete.js +32 -32
- package/.test-dist/src/film.js +167 -167
- package/.test-dist/src/geometry.js +506 -506
- package/.test-dist/src/image.js +1250 -1250
- package/.test-dist/src/index.js +2 -19
- package/.test-dist/src/maskOps.js +270 -270
- package/.test-dist/src/mirror.js +104 -104
- package/.test-dist/src/renderSpec.js +2 -2
- package/.test-dist/src/ruler.js +343 -343
- package/.test-dist/src/sceneLayout.js +99 -99
- package/.test-dist/src/sceneLayoutModel.js +196 -196
- package/.test-dist/src/sceneView.js +40 -40
- package/.test-dist/src/sceneVisibility.js +42 -42
- package/.test-dist/src/services/CanvasService.js +249 -0
- package/.test-dist/src/services/ViewportSystem.js +76 -0
- package/.test-dist/src/services/index.js +24 -0
- package/.test-dist/src/services/renderSpec.js +2 -0
- package/.test-dist/src/size.js +332 -332
- package/.test-dist/src/tracer.js +544 -544
- package/.test-dist/src/white-ink.js +829 -829
- package/.test-dist/src/wrappedOffsets.js +33 -33
- package/CHANGELOG.md +6 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +411 -375
- package/dist/index.mjs +411 -375
- package/package.json +1 -1
- package/src/coordinate.ts +106 -106
- package/src/extensions/background.ts +230 -230
- package/src/extensions/bridgeSelection.ts +17 -17
- package/src/extensions/constraints.ts +322 -322
- package/src/extensions/dieline.ts +20 -17
- package/src/extensions/edgeScale.ts +19 -19
- package/src/extensions/feature.ts +1021 -1021
- package/src/extensions/featureComplete.ts +46 -46
- package/src/extensions/film.ts +194 -194
- package/src/extensions/geometry.ts +719 -719
- package/src/extensions/image.ts +1924 -1924
- package/src/extensions/index.ts +11 -11
- package/src/extensions/maskOps.ts +365 -299
- package/src/extensions/mirror.ts +128 -128
- package/src/extensions/ruler.ts +451 -451
- package/src/extensions/sceneLayout.ts +140 -140
- package/src/extensions/sceneLayoutModel.ts +342 -342
- package/src/extensions/sceneVisibility.ts +71 -71
- package/src/extensions/size.ts +389 -389
- package/src/extensions/tracer.ts +302 -370
- package/src/extensions/white-ink.ts +1489 -1366
- package/src/extensions/wrappedOffsets.ts +33 -33
- package/src/index.ts +2 -2
- package/src/services/CanvasService.ts +300 -300
- package/src/services/ViewportSystem.ts +95 -95
- package/src/services/index.ts +3 -3
- package/src/services/renderSpec.ts +18 -18
- package/src/units.ts +27 -27
- package/tests/run.ts +118 -118
- package/tsconfig.test.json +15 -15
|
@@ -1,59 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Extension,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
ContributionPointIds,
|
|
5
|
-
CommandContribution,
|
|
6
|
-
ConfigurationContribution,
|
|
7
|
-
ConfigurationService,
|
|
8
|
-
ToolSessionService,
|
|
9
|
-
WorkbenchService,
|
|
10
|
-
} from "@pooder/core";
|
|
11
|
-
import { CanvasService, RenderObjectSpec } from "../services";
|
|
12
|
-
import { computeSceneLayout, readSizeState } from "./sceneLayoutModel";
|
|
13
|
-
|
|
14
|
-
export interface WhiteInkItem {
|
|
1
|
+
import {
|
|
2
|
+
Extension,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
5
|
+
CommandContribution,
|
|
6
|
+
ConfigurationContribution,
|
|
7
|
+
ConfigurationService,
|
|
8
|
+
ToolSessionService,
|
|
9
|
+
WorkbenchService,
|
|
10
|
+
} from "@pooder/core";
|
|
11
|
+
import { CanvasService, RenderObjectSpec } from "../services";
|
|
12
|
+
import { computeSceneLayout, readSizeState } from "./sceneLayoutModel";
|
|
13
|
+
|
|
14
|
+
export interface WhiteInkItem {
|
|
15
|
+
id: string;
|
|
16
|
+
sourceUrl?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
opacity: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SourceSize {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MaskTint {
|
|
27
|
+
r: number;
|
|
28
|
+
g: number;
|
|
29
|
+
b: number;
|
|
30
|
+
key: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FrameRect {
|
|
34
|
+
left: number;
|
|
35
|
+
top: number;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ImageSnapshot {
|
|
41
|
+
id: string;
|
|
42
|
+
src: string;
|
|
43
|
+
element: any;
|
|
44
|
+
left: number;
|
|
45
|
+
top: number;
|
|
46
|
+
scaleX: number;
|
|
47
|
+
scaleY: number;
|
|
48
|
+
angle: number;
|
|
49
|
+
originX: string;
|
|
50
|
+
originY: string;
|
|
51
|
+
flipX: boolean;
|
|
52
|
+
flipY: boolean;
|
|
53
|
+
skewX: number;
|
|
54
|
+
skewY: number;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ImagePlacementState {
|
|
15
60
|
id: string;
|
|
16
|
-
sourceUrl
|
|
17
|
-
|
|
18
|
-
opacity: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface SourceSize {
|
|
22
|
-
width: number;
|
|
23
|
-
height: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface MaskTint {
|
|
27
|
-
r: number;
|
|
28
|
-
g: number;
|
|
29
|
-
b: number;
|
|
30
|
-
key: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface FrameRect {
|
|
61
|
+
sourceUrl: string;
|
|
62
|
+
committedUrl: string;
|
|
34
63
|
left: number;
|
|
35
64
|
top: number;
|
|
36
|
-
|
|
37
|
-
height: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface ImageSnapshot {
|
|
41
|
-
id: string;
|
|
42
|
-
src: string;
|
|
43
|
-
element: any;
|
|
44
|
-
left: number;
|
|
45
|
-
top: number;
|
|
46
|
-
scaleX: number;
|
|
47
|
-
scaleY: number;
|
|
65
|
+
scale: number;
|
|
48
66
|
angle: number;
|
|
49
|
-
originX: string;
|
|
50
|
-
originY: string;
|
|
51
|
-
flipX: boolean;
|
|
52
|
-
flipY: boolean;
|
|
53
|
-
skewX: number;
|
|
54
|
-
skewY: number;
|
|
55
|
-
width: number;
|
|
56
|
-
height: number;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
interface RenderSources {
|
|
@@ -61,1342 +71,1455 @@ interface RenderSources {
|
|
|
61
71
|
coverSrc: string;
|
|
62
72
|
whiteScaleAdjustX: number;
|
|
63
73
|
whiteScaleAdjustY: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface UpsertWhiteInkOptions {
|
|
67
|
-
id?: string;
|
|
68
|
-
mode?: "auto" | "replace" | "add";
|
|
69
|
-
createIfMissing?: boolean;
|
|
70
|
-
addOptions?: Partial<WhiteInkItem>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface UpdateWhiteInkOptions {
|
|
74
|
-
target?: "auto" | "config" | "working";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
|
|
78
|
-
const WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
|
|
79
|
-
const WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
|
|
80
|
-
const IMAGE_OBJECT_LAYER_ID = "image.user";
|
|
81
|
-
const IMAGE_OVERLAY_LAYER_ID = "image-overlay";
|
|
82
|
-
|
|
83
|
-
const WHITE_INK_DEBUG_KEY = "whiteInk.debug";
|
|
84
|
-
const WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
|
|
85
|
-
const WHITE_INK_DEFAULT_OPACITY = 0.85;
|
|
86
|
-
const WHITE_INK_AUTO_ITEM_ID = "white-ink-auto";
|
|
87
|
-
|
|
88
|
-
const WHITE_INK_COVER_OPACITY_FACTOR = 0.45;
|
|
89
|
-
const WHITE_INK_COVER_OPACITY_MIN = 0.15;
|
|
90
|
-
const WHITE_INK_COVER_OPACITY_MAX = 0.65;
|
|
91
|
-
const WHITE_MASK_TINT: MaskTint = { r: 255, g: 255, b: 255, key: "white" };
|
|
92
|
-
const COVER_MASK_TINT: MaskTint = { r: 52, g: 136, b: 255, key: "blue" };
|
|
93
|
-
|
|
94
|
-
export class WhiteInkTool implements Extension {
|
|
95
|
-
id = "pooder.kit.white-ink";
|
|
96
|
-
|
|
97
|
-
metadata = {
|
|
98
|
-
name: "WhiteInkTool",
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
private items: WhiteInkItem[] = [];
|
|
102
|
-
private workingItems: WhiteInkItem[] = [];
|
|
103
|
-
private hasWorkingChanges = false;
|
|
104
|
-
|
|
105
|
-
private sourceSizeBySrc: Map<string, SourceSize> = new Map();
|
|
106
|
-
private previewMaskBySource: Map<string, string> = new Map();
|
|
107
|
-
private pendingPreviewMaskBySource: Map<string, Promise<string | null>> =
|
|
108
|
-
new Map();
|
|
109
|
-
|
|
110
|
-
private canvasService?: CanvasService;
|
|
111
|
-
private context?: ExtensionContext;
|
|
112
|
-
private isUpdatingConfig = false;
|
|
113
|
-
private isToolActive = false;
|
|
114
|
-
private printWithWhiteInk = true;
|
|
115
|
-
private previewImageVisible = true;
|
|
116
|
-
private renderSeq = 0;
|
|
117
|
-
private dirtyTrackerDisposable?: { dispose(): void };
|
|
118
|
-
|
|
119
|
-
activate(context: ExtensionContext) {
|
|
120
|
-
this.context = context;
|
|
121
|
-
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
122
|
-
if (!this.canvasService) {
|
|
123
|
-
console.warn("CanvasService not found for WhiteInkTool");
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
context.eventBus.on("tool:activated", this.onToolActivated);
|
|
128
|
-
context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
|
|
129
|
-
context.eventBus.on("object:added", this.onObjectAdded);
|
|
130
|
-
context.eventBus.on("object:modified", this.onObjectModified);
|
|
131
|
-
context.eventBus.on("object:removed", this.onObjectRemoved);
|
|
132
|
-
context.eventBus.on("image:working:change", this.onImageWorkingChanged);
|
|
133
|
-
|
|
134
|
-
const configService = context.services.get<ConfigurationService>(
|
|
135
|
-
"ConfigurationService",
|
|
136
|
-
);
|
|
137
|
-
if (configService) {
|
|
138
|
-
this.items = this.normalizeItems(
|
|
139
|
-
configService.get("whiteInk.items", []) || [],
|
|
140
|
-
);
|
|
141
|
-
this.workingItems = this.cloneItems(this.items);
|
|
142
|
-
this.hasWorkingChanges = false;
|
|
143
|
-
this.printWithWhiteInk = !!configService.get(
|
|
144
|
-
"whiteInk.printWithWhiteInk",
|
|
145
|
-
true,
|
|
146
|
-
);
|
|
147
|
-
this.previewImageVisible = !!configService.get(
|
|
148
|
-
WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
|
|
149
|
-
true,
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
this.migrateLegacyConfigIfNeeded(configService);
|
|
153
|
-
|
|
154
|
-
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
155
|
-
if (this.isUpdatingConfig) return;
|
|
156
|
-
|
|
157
|
-
if (e.key === "whiteInk.items") {
|
|
158
|
-
this.items = this.normalizeItems(e.value || []);
|
|
159
|
-
if (!this.isToolActive || !this.hasWorkingChanges) {
|
|
160
|
-
this.workingItems = this.cloneItems(this.items);
|
|
161
|
-
this.hasWorkingChanges = false;
|
|
162
|
-
}
|
|
163
|
-
this.updateWhiteInks();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (e.key === "whiteInk.printWithWhiteInk") {
|
|
168
|
-
this.printWithWhiteInk = !!e.value;
|
|
169
|
-
this.updateWhiteInks();
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (e.key === WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY) {
|
|
174
|
-
this.previewImageVisible = !!e.value;
|
|
175
|
-
this.updateWhiteInks();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (e.key === "image.items") {
|
|
180
|
-
this.updateWhiteInks();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (e.key === WHITE_INK_DEBUG_KEY) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (e.key.startsWith("size.")) {
|
|
189
|
-
this.updateWhiteInks();
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const toolSessionService =
|
|
195
|
-
context.services.get<ToolSessionService>("ToolSessionService");
|
|
196
|
-
this.dirtyTrackerDisposable = toolSessionService?.registerDirtyTracker(
|
|
197
|
-
this.id,
|
|
198
|
-
() => this.hasWorkingChanges,
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
this.updateWhiteInks();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
deactivate(context: ExtensionContext) {
|
|
205
|
-
context.eventBus.off("tool:activated", this.onToolActivated);
|
|
206
|
-
context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
|
|
207
|
-
context.eventBus.off("object:added", this.onObjectAdded);
|
|
208
|
-
context.eventBus.off("object:modified", this.onObjectModified);
|
|
209
|
-
context.eventBus.off("object:removed", this.onObjectRemoved);
|
|
210
|
-
context.eventBus.off("image:working:change", this.onImageWorkingChanged);
|
|
211
|
-
|
|
212
|
-
this.dirtyTrackerDisposable?.dispose();
|
|
213
|
-
this.dirtyTrackerDisposable = undefined;
|
|
214
|
-
this.clearRenderedWhiteInks();
|
|
215
|
-
this.applyImageVisibilityForWhiteInk(false);
|
|
216
|
-
|
|
217
|
-
this.canvasService = undefined;
|
|
218
|
-
this.context = undefined;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
contribute() {
|
|
222
|
-
return {
|
|
223
|
-
[ContributionPointIds.TOOLS]: [
|
|
224
|
-
{
|
|
225
|
-
id: this.id,
|
|
226
|
-
name: "White Ink",
|
|
227
|
-
interaction: "session",
|
|
228
|
-
commands: {
|
|
229
|
-
begin: "resetWorkingWhiteInks",
|
|
230
|
-
commit: "completeWhiteInks",
|
|
231
|
-
rollback: "resetWorkingWhiteInks",
|
|
232
|
-
},
|
|
233
|
-
session: {
|
|
234
|
-
autoBegin: true,
|
|
235
|
-
leavePolicy: "block",
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
],
|
|
239
|
-
[ContributionPointIds.CONFIGURATIONS]: [
|
|
240
|
-
{
|
|
241
|
-
id: "whiteInk.items",
|
|
242
|
-
type: "array",
|
|
243
|
-
label: "White Ink Images",
|
|
244
|
-
default: [],
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
id: "whiteInk.printWithWhiteInk",
|
|
248
|
-
type: "boolean",
|
|
249
|
-
label: "Preview White Ink",
|
|
250
|
-
default: true,
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
id: WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
|
|
254
|
-
type: "boolean",
|
|
255
|
-
label: "Show Cover During White Ink Preview",
|
|
256
|
-
default: true,
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
id: WHITE_INK_DEBUG_KEY,
|
|
260
|
-
type: "boolean",
|
|
261
|
-
label: "White Ink Debug Log",
|
|
262
|
-
default: false,
|
|
263
|
-
},
|
|
264
|
-
] as ConfigurationContribution[],
|
|
265
|
-
[ContributionPointIds.COMMANDS]: [
|
|
266
|
-
{
|
|
267
|
-
command: "addWhiteInk",
|
|
268
|
-
title: "Add White Ink",
|
|
269
|
-
handler: async (url: string, options?: Partial<WhiteInkItem>) => {
|
|
270
|
-
return await this.addWhiteInkEntry(url, options);
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
command: "upsertWhiteInk",
|
|
275
|
-
title: "Upsert White Ink",
|
|
276
|
-
handler: async (url: string, options: UpsertWhiteInkOptions = {}) => {
|
|
277
|
-
return await this.upsertWhiteInkEntry(url, options);
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
command: "getWhiteInks",
|
|
282
|
-
title: "Get White Inks",
|
|
283
|
-
handler: () => this.cloneItems(this.items),
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
command: "getWhiteInkSettings",
|
|
287
|
-
title: "Get White Ink Settings",
|
|
288
|
-
handler: () => {
|
|
289
|
-
const first = this.getEffectiveWhiteInkItem(this.items);
|
|
290
|
-
const primarySource = this.getPrimaryImageSource();
|
|
291
|
-
const sourceUrl = this.resolveSourceUrl(first) || primarySource;
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
id: first?.id || null,
|
|
295
|
-
url: sourceUrl,
|
|
296
|
-
sourceUrl,
|
|
297
|
-
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
298
|
-
printWithWhiteInk: this.printWithWhiteInk,
|
|
299
|
-
previewImageVisible: this.previewImageVisible,
|
|
300
|
-
};
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
command: "setWhiteInkPrintEnabled",
|
|
305
|
-
title: "Set White Ink Preview Enabled",
|
|
306
|
-
handler: (enabled: boolean) => {
|
|
307
|
-
this.printWithWhiteInk = !!enabled;
|
|
308
|
-
const configService =
|
|
309
|
-
this.context?.services.get<ConfigurationService>(
|
|
310
|
-
"ConfigurationService",
|
|
311
|
-
);
|
|
312
|
-
configService?.update(
|
|
313
|
-
"whiteInk.printWithWhiteInk",
|
|
314
|
-
this.printWithWhiteInk,
|
|
315
|
-
);
|
|
316
|
-
this.updateWhiteInks();
|
|
317
|
-
return { ok: true };
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
command: "setWhiteInkPreviewImageVisible",
|
|
322
|
-
title: "Set White Ink Cover Visible",
|
|
323
|
-
handler: (visible: boolean) => {
|
|
324
|
-
this.previewImageVisible = !!visible;
|
|
325
|
-
const configService =
|
|
326
|
-
this.context?.services.get<ConfigurationService>(
|
|
327
|
-
"ConfigurationService",
|
|
328
|
-
);
|
|
329
|
-
configService?.update(
|
|
330
|
-
WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
|
|
331
|
-
this.previewImageVisible,
|
|
332
|
-
);
|
|
333
|
-
this.updateWhiteInks();
|
|
334
|
-
return { ok: true };
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
command: "getWorkingWhiteInks",
|
|
339
|
-
title: "Get Working White Inks",
|
|
340
|
-
handler: () => this.cloneItems(this.workingItems),
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
command: "setWorkingWhiteInk",
|
|
344
|
-
title: "Set Working White Ink",
|
|
345
|
-
handler: (id: string, updates: Partial<WhiteInkItem>) => {
|
|
346
|
-
this.updateWhiteInkInWorking(id, updates);
|
|
347
|
-
},
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
command: "updateWhiteInk",
|
|
351
|
-
title: "Update White Ink",
|
|
352
|
-
handler: async (
|
|
353
|
-
id: string,
|
|
354
|
-
updates: Partial<WhiteInkItem>,
|
|
355
|
-
options: UpdateWhiteInkOptions = {},
|
|
356
|
-
) => {
|
|
357
|
-
await this.updateWhiteInkItem(id, updates, options);
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
command: "removeWhiteInk",
|
|
362
|
-
title: "Remove White Ink",
|
|
363
|
-
handler: (id: string) => {
|
|
364
|
-
this.removeWhiteInk(id);
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
command: "clearWhiteInks",
|
|
369
|
-
title: "Clear White Inks",
|
|
370
|
-
handler: () => {
|
|
371
|
-
this.clearWhiteInks();
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
command: "resetWorkingWhiteInks",
|
|
376
|
-
title: "Reset Working White Inks",
|
|
377
|
-
handler: () => {
|
|
378
|
-
this.workingItems = this.cloneItems(this.items);
|
|
379
|
-
this.hasWorkingChanges = false;
|
|
380
|
-
this.updateWhiteInks();
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
command: "completeWhiteInks",
|
|
385
|
-
title: "Complete White Inks",
|
|
386
|
-
handler: async () => {
|
|
387
|
-
return await this.completeWhiteInks();
|
|
388
|
-
},
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
command: "setWhiteInkImage",
|
|
392
|
-
title: "Set White Ink Image",
|
|
393
|
-
handler: async (url: string) => {
|
|
394
|
-
if (!url) {
|
|
395
|
-
this.clearWhiteInks();
|
|
396
|
-
return { ok: true };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const targetId = this.resolveReplaceTargetId(null);
|
|
400
|
-
const upsertResult = await this.upsertWhiteInkEntry(url, {
|
|
401
|
-
id: targetId || undefined,
|
|
402
|
-
mode: targetId ? "replace" : "add",
|
|
403
|
-
createIfMissing: true,
|
|
404
|
-
addOptions: {},
|
|
405
|
-
});
|
|
406
|
-
return { ok: true, id: upsertResult.id };
|
|
407
|
-
},
|
|
408
|
-
},
|
|
409
|
-
] as CommandContribution[],
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
private onToolActivated = (event: {
|
|
414
|
-
id: string | null;
|
|
415
|
-
previous?: string | null;
|
|
416
|
-
}) => {
|
|
417
|
-
const before = this.isToolActive;
|
|
418
|
-
this.syncToolActiveFromWorkbench(event.id);
|
|
419
|
-
this.debug("tool:activated", {
|
|
420
|
-
id: event.id,
|
|
421
|
-
previous: event.previous,
|
|
422
|
-
before,
|
|
423
|
-
isToolActive: this.isToolActive,
|
|
424
|
-
});
|
|
425
|
-
this.updateWhiteInks();
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
private onSceneLayoutChanged = () => {
|
|
429
|
-
this.updateWhiteInks();
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
private onObjectAdded = (e: any) => {
|
|
433
|
-
const layerId = e?.target?.data?.layerId;
|
|
434
|
-
if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
435
|
-
this.updateWhiteInks();
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
private onObjectModified = (e: any) => {
|
|
439
|
-
const layerId = e?.target?.data?.layerId;
|
|
440
|
-
if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
441
|
-
this.updateWhiteInks();
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
private onObjectRemoved = (e: any) => {
|
|
445
|
-
const layerId = e?.target?.data?.layerId;
|
|
446
|
-
if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
447
|
-
this.updateWhiteInks();
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
private onImageWorkingChanged = () => {
|
|
451
|
-
this.updateWhiteInks();
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
private migrateLegacyConfigIfNeeded(configService: ConfigurationService) {
|
|
455
|
-
if (this.items.length > 0) return;
|
|
456
|
-
const legacyMask = configService.get("whiteInk.customMask", "");
|
|
457
|
-
if (typeof legacyMask !== "string" || legacyMask.length === 0) return;
|
|
458
|
-
|
|
459
|
-
const item = this.normalizeItem({
|
|
460
|
-
id: this.generateId(),
|
|
461
|
-
sourceUrl: legacyMask,
|
|
462
|
-
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
this.items = [item];
|
|
466
|
-
this.workingItems = this.cloneItems(this.items);
|
|
467
|
-
this.isUpdatingConfig = true;
|
|
468
|
-
configService.update("whiteInk.items", this.items);
|
|
469
|
-
setTimeout(() => {
|
|
470
|
-
this.isUpdatingConfig = false;
|
|
471
|
-
}, 0);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private syncToolActiveFromWorkbench(fallbackId?: string | null) {
|
|
475
|
-
const wb = this.context?.services.get<WorkbenchService>("WorkbenchService");
|
|
476
|
-
const activeId = wb?.activeToolId;
|
|
477
|
-
if (typeof activeId === "string" || activeId === null) {
|
|
478
|
-
this.isToolActive = activeId === this.id;
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
this.isToolActive = fallbackId === this.id;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
private isPreviewActive(): boolean {
|
|
485
|
-
return this.isToolActive && this.printWithWhiteInk;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
private isDebugEnabled(): boolean {
|
|
489
|
-
return !!this.getConfig<boolean>(WHITE_INK_DEBUG_KEY, false);
|
|
490
|
-
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface UpsertWhiteInkOptions {
|
|
77
|
+
id?: string;
|
|
78
|
+
mode?: "auto" | "replace" | "add";
|
|
79
|
+
createIfMissing?: boolean;
|
|
80
|
+
addOptions?: Partial<WhiteInkItem>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface UpdateWhiteInkOptions {
|
|
84
|
+
target?: "auto" | "config" | "working";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
|
|
88
|
+
const WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
|
|
89
|
+
const WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
|
|
90
|
+
const IMAGE_OBJECT_LAYER_ID = "image.user";
|
|
91
|
+
const IMAGE_OVERLAY_LAYER_ID = "image-overlay";
|
|
92
|
+
|
|
93
|
+
const WHITE_INK_DEBUG_KEY = "whiteInk.debug";
|
|
94
|
+
const WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
|
|
95
|
+
const WHITE_INK_DEFAULT_OPACITY = 0.85;
|
|
96
|
+
const WHITE_INK_AUTO_ITEM_ID = "white-ink-auto";
|
|
97
|
+
|
|
98
|
+
const WHITE_INK_COVER_OPACITY_FACTOR = 0.45;
|
|
99
|
+
const WHITE_INK_COVER_OPACITY_MIN = 0.15;
|
|
100
|
+
const WHITE_INK_COVER_OPACITY_MAX = 0.65;
|
|
101
|
+
const WHITE_MASK_TINT: MaskTint = { r: 255, g: 255, b: 255, key: "white" };
|
|
102
|
+
const COVER_MASK_TINT: MaskTint = { r: 52, g: 136, b: 255, key: "blue" };
|
|
103
|
+
|
|
104
|
+
export class WhiteInkTool implements Extension {
|
|
105
|
+
id = "pooder.kit.white-ink";
|
|
106
|
+
|
|
107
|
+
metadata = {
|
|
108
|
+
name: "WhiteInkTool",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
private items: WhiteInkItem[] = [];
|
|
112
|
+
private workingItems: WhiteInkItem[] = [];
|
|
113
|
+
private hasWorkingChanges = false;
|
|
114
|
+
|
|
115
|
+
private sourceSizeBySrc: Map<string, SourceSize> = new Map();
|
|
116
|
+
private previewMaskBySource: Map<string, string> = new Map();
|
|
117
|
+
private pendingPreviewMaskBySource: Map<string, Promise<string | null>> =
|
|
118
|
+
new Map();
|
|
119
|
+
|
|
120
|
+
private canvasService?: CanvasService;
|
|
121
|
+
private context?: ExtensionContext;
|
|
122
|
+
private isUpdatingConfig = false;
|
|
123
|
+
private isToolActive = false;
|
|
124
|
+
private printWithWhiteInk = true;
|
|
125
|
+
private previewImageVisible = true;
|
|
126
|
+
private renderSeq = 0;
|
|
127
|
+
private dirtyTrackerDisposable?: { dispose(): void };
|
|
128
|
+
|
|
129
|
+
activate(context: ExtensionContext) {
|
|
130
|
+
this.context = context;
|
|
131
|
+
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
132
|
+
if (!this.canvasService) {
|
|
133
|
+
console.warn("CanvasService not found for WhiteInkTool");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
context.eventBus.on("tool:activated", this.onToolActivated);
|
|
138
|
+
context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
|
|
139
|
+
context.eventBus.on("object:added", this.onObjectAdded);
|
|
140
|
+
context.eventBus.on("object:modified", this.onObjectModified);
|
|
141
|
+
context.eventBus.on("object:removed", this.onObjectRemoved);
|
|
142
|
+
context.eventBus.on("image:working:change", this.onImageWorkingChanged);
|
|
143
|
+
|
|
144
|
+
const configService = context.services.get<ConfigurationService>(
|
|
145
|
+
"ConfigurationService",
|
|
146
|
+
);
|
|
147
|
+
if (configService) {
|
|
148
|
+
this.items = this.normalizeItems(
|
|
149
|
+
configService.get("whiteInk.items", []) || [],
|
|
150
|
+
);
|
|
151
|
+
this.workingItems = this.cloneItems(this.items);
|
|
152
|
+
this.hasWorkingChanges = false;
|
|
153
|
+
this.printWithWhiteInk = !!configService.get(
|
|
154
|
+
"whiteInk.printWithWhiteInk",
|
|
155
|
+
true,
|
|
156
|
+
);
|
|
157
|
+
this.previewImageVisible = !!configService.get(
|
|
158
|
+
WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
|
|
159
|
+
true,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
this.migrateLegacyConfigIfNeeded(configService);
|
|
163
|
+
|
|
164
|
+
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
165
|
+
if (this.isUpdatingConfig) return;
|
|
166
|
+
|
|
167
|
+
if (e.key === "whiteInk.items") {
|
|
168
|
+
this.items = this.normalizeItems(e.value || []);
|
|
169
|
+
if (!this.isToolActive || !this.hasWorkingChanges) {
|
|
170
|
+
this.workingItems = this.cloneItems(this.items);
|
|
171
|
+
this.hasWorkingChanges = false;
|
|
172
|
+
}
|
|
173
|
+
this.updateWhiteInks();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (e.key === "whiteInk.printWithWhiteInk") {
|
|
178
|
+
this.printWithWhiteInk = !!e.value;
|
|
179
|
+
this.updateWhiteInks();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (e.key === WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY) {
|
|
184
|
+
this.previewImageVisible = !!e.value;
|
|
185
|
+
this.updateWhiteInks();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (e.key === "image.items") {
|
|
190
|
+
this.updateWhiteInks();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (e.key === WHITE_INK_DEBUG_KEY) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (e.key.startsWith("size.")) {
|
|
199
|
+
this.updateWhiteInks();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const toolSessionService =
|
|
205
|
+
context.services.get<ToolSessionService>("ToolSessionService");
|
|
206
|
+
this.dirtyTrackerDisposable = toolSessionService?.registerDirtyTracker(
|
|
207
|
+
this.id,
|
|
208
|
+
() => this.hasWorkingChanges,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
this.updateWhiteInks();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
deactivate(context: ExtensionContext) {
|
|
215
|
+
context.eventBus.off("tool:activated", this.onToolActivated);
|
|
216
|
+
context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
|
|
217
|
+
context.eventBus.off("object:added", this.onObjectAdded);
|
|
218
|
+
context.eventBus.off("object:modified", this.onObjectModified);
|
|
219
|
+
context.eventBus.off("object:removed", this.onObjectRemoved);
|
|
220
|
+
context.eventBus.off("image:working:change", this.onImageWorkingChanged);
|
|
221
|
+
|
|
222
|
+
this.dirtyTrackerDisposable?.dispose();
|
|
223
|
+
this.dirtyTrackerDisposable = undefined;
|
|
224
|
+
this.clearRenderedWhiteInks();
|
|
225
|
+
this.applyImageVisibilityForWhiteInk(false);
|
|
226
|
+
|
|
227
|
+
this.canvasService = undefined;
|
|
228
|
+
this.context = undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
contribute() {
|
|
232
|
+
return {
|
|
233
|
+
[ContributionPointIds.TOOLS]: [
|
|
234
|
+
{
|
|
235
|
+
id: this.id,
|
|
236
|
+
name: "White Ink",
|
|
237
|
+
interaction: "session",
|
|
238
|
+
commands: {
|
|
239
|
+
begin: "resetWorkingWhiteInks",
|
|
240
|
+
commit: "completeWhiteInks",
|
|
241
|
+
rollback: "resetWorkingWhiteInks",
|
|
242
|
+
},
|
|
243
|
+
session: {
|
|
244
|
+
autoBegin: true,
|
|
245
|
+
leavePolicy: "block",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
[ContributionPointIds.CONFIGURATIONS]: [
|
|
250
|
+
{
|
|
251
|
+
id: "whiteInk.items",
|
|
252
|
+
type: "array",
|
|
253
|
+
label: "White Ink Images",
|
|
254
|
+
default: [],
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "whiteInk.printWithWhiteInk",
|
|
258
|
+
type: "boolean",
|
|
259
|
+
label: "Preview White Ink",
|
|
260
|
+
default: true,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
|
|
264
|
+
type: "boolean",
|
|
265
|
+
label: "Show Cover During White Ink Preview",
|
|
266
|
+
default: true,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: WHITE_INK_DEBUG_KEY,
|
|
270
|
+
type: "boolean",
|
|
271
|
+
label: "White Ink Debug Log",
|
|
272
|
+
default: false,
|
|
273
|
+
},
|
|
274
|
+
] as ConfigurationContribution[],
|
|
275
|
+
[ContributionPointIds.COMMANDS]: [
|
|
276
|
+
{
|
|
277
|
+
command: "addWhiteInk",
|
|
278
|
+
title: "Add White Ink",
|
|
279
|
+
handler: async (url: string, options?: Partial<WhiteInkItem>) => {
|
|
280
|
+
return await this.addWhiteInkEntry(url, options);
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
command: "upsertWhiteInk",
|
|
285
|
+
title: "Upsert White Ink",
|
|
286
|
+
handler: async (url: string, options: UpsertWhiteInkOptions = {}) => {
|
|
287
|
+
return await this.upsertWhiteInkEntry(url, options);
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
command: "getWhiteInks",
|
|
292
|
+
title: "Get White Inks",
|
|
293
|
+
handler: () => this.cloneItems(this.items),
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
command: "getWhiteInkSettings",
|
|
297
|
+
title: "Get White Ink Settings",
|
|
298
|
+
handler: () => {
|
|
299
|
+
const first = this.getEffectiveWhiteInkItem(this.items);
|
|
300
|
+
const primarySource = this.getPrimaryImageSource();
|
|
301
|
+
const sourceUrl = this.resolveSourceUrl(first) || primarySource;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
id: first?.id || null,
|
|
305
|
+
url: sourceUrl,
|
|
306
|
+
sourceUrl,
|
|
307
|
+
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
308
|
+
printWithWhiteInk: this.printWithWhiteInk,
|
|
309
|
+
previewImageVisible: this.previewImageVisible,
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
command: "setWhiteInkPrintEnabled",
|
|
315
|
+
title: "Set White Ink Preview Enabled",
|
|
316
|
+
handler: (enabled: boolean) => {
|
|
317
|
+
this.printWithWhiteInk = !!enabled;
|
|
318
|
+
const configService =
|
|
319
|
+
this.context?.services.get<ConfigurationService>(
|
|
320
|
+
"ConfigurationService",
|
|
321
|
+
);
|
|
322
|
+
configService?.update(
|
|
323
|
+
"whiteInk.printWithWhiteInk",
|
|
324
|
+
this.printWithWhiteInk,
|
|
325
|
+
);
|
|
326
|
+
this.updateWhiteInks();
|
|
327
|
+
return { ok: true };
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
command: "setWhiteInkPreviewImageVisible",
|
|
332
|
+
title: "Set White Ink Cover Visible",
|
|
333
|
+
handler: (visible: boolean) => {
|
|
334
|
+
this.previewImageVisible = !!visible;
|
|
335
|
+
const configService =
|
|
336
|
+
this.context?.services.get<ConfigurationService>(
|
|
337
|
+
"ConfigurationService",
|
|
338
|
+
);
|
|
339
|
+
configService?.update(
|
|
340
|
+
WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
|
|
341
|
+
this.previewImageVisible,
|
|
342
|
+
);
|
|
343
|
+
this.updateWhiteInks();
|
|
344
|
+
return { ok: true };
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
command: "getWorkingWhiteInks",
|
|
349
|
+
title: "Get Working White Inks",
|
|
350
|
+
handler: () => this.cloneItems(this.workingItems),
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
command: "setWorkingWhiteInk",
|
|
354
|
+
title: "Set Working White Ink",
|
|
355
|
+
handler: (id: string, updates: Partial<WhiteInkItem>) => {
|
|
356
|
+
this.updateWhiteInkInWorking(id, updates);
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
command: "updateWhiteInk",
|
|
361
|
+
title: "Update White Ink",
|
|
362
|
+
handler: async (
|
|
363
|
+
id: string,
|
|
364
|
+
updates: Partial<WhiteInkItem>,
|
|
365
|
+
options: UpdateWhiteInkOptions = {},
|
|
366
|
+
) => {
|
|
367
|
+
await this.updateWhiteInkItem(id, updates, options);
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
command: "removeWhiteInk",
|
|
372
|
+
title: "Remove White Ink",
|
|
373
|
+
handler: (id: string) => {
|
|
374
|
+
this.removeWhiteInk(id);
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
command: "clearWhiteInks",
|
|
379
|
+
title: "Clear White Inks",
|
|
380
|
+
handler: () => {
|
|
381
|
+
this.clearWhiteInks();
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
command: "resetWorkingWhiteInks",
|
|
386
|
+
title: "Reset Working White Inks",
|
|
387
|
+
handler: () => {
|
|
388
|
+
this.workingItems = this.cloneItems(this.items);
|
|
389
|
+
this.hasWorkingChanges = false;
|
|
390
|
+
this.updateWhiteInks();
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
command: "completeWhiteInks",
|
|
395
|
+
title: "Complete White Inks",
|
|
396
|
+
handler: async () => {
|
|
397
|
+
return await this.completeWhiteInks();
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
command: "setWhiteInkImage",
|
|
402
|
+
title: "Set White Ink Image",
|
|
403
|
+
handler: async (url: string) => {
|
|
404
|
+
if (!url) {
|
|
405
|
+
this.clearWhiteInks();
|
|
406
|
+
return { ok: true };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const targetId = this.resolveReplaceTargetId(null);
|
|
410
|
+
const upsertResult = await this.upsertWhiteInkEntry(url, {
|
|
411
|
+
id: targetId || undefined,
|
|
412
|
+
mode: targetId ? "replace" : "add",
|
|
413
|
+
createIfMissing: true,
|
|
414
|
+
addOptions: {},
|
|
415
|
+
});
|
|
416
|
+
return { ok: true, id: upsertResult.id };
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
] as CommandContribution[],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private onToolActivated = (event: {
|
|
424
|
+
id: string | null;
|
|
425
|
+
previous?: string | null;
|
|
426
|
+
}) => {
|
|
427
|
+
const before = this.isToolActive;
|
|
428
|
+
this.syncToolActiveFromWorkbench(event.id);
|
|
429
|
+
this.debug("tool:activated", {
|
|
430
|
+
id: event.id,
|
|
431
|
+
previous: event.previous,
|
|
432
|
+
before,
|
|
433
|
+
isToolActive: this.isToolActive,
|
|
434
|
+
});
|
|
435
|
+
this.updateWhiteInks();
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
private onSceneLayoutChanged = () => {
|
|
439
|
+
this.updateWhiteInks();
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
private onObjectAdded = (e: any) => {
|
|
443
|
+
const layerId = e?.target?.data?.layerId;
|
|
444
|
+
if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
445
|
+
this.updateWhiteInks();
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
private onObjectModified = (e: any) => {
|
|
449
|
+
const layerId = e?.target?.data?.layerId;
|
|
450
|
+
if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
451
|
+
this.updateWhiteInks();
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
private onObjectRemoved = (e: any) => {
|
|
455
|
+
const layerId = e?.target?.data?.layerId;
|
|
456
|
+
if (layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
457
|
+
this.updateWhiteInks();
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
private onImageWorkingChanged = () => {
|
|
461
|
+
this.updateWhiteInks();
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
private migrateLegacyConfigIfNeeded(configService: ConfigurationService) {
|
|
465
|
+
if (this.items.length > 0) return;
|
|
466
|
+
const legacyMask = configService.get("whiteInk.customMask", "");
|
|
467
|
+
if (typeof legacyMask !== "string" || legacyMask.length === 0) return;
|
|
468
|
+
|
|
469
|
+
const item = this.normalizeItem({
|
|
470
|
+
id: this.generateId(),
|
|
471
|
+
sourceUrl: legacyMask,
|
|
472
|
+
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
this.items = [item];
|
|
476
|
+
this.workingItems = this.cloneItems(this.items);
|
|
477
|
+
this.isUpdatingConfig = true;
|
|
478
|
+
configService.update("whiteInk.items", this.items);
|
|
479
|
+
setTimeout(() => {
|
|
480
|
+
this.isUpdatingConfig = false;
|
|
481
|
+
}, 0);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private syncToolActiveFromWorkbench(fallbackId?: string | null) {
|
|
485
|
+
const wb = this.context?.services.get<WorkbenchService>("WorkbenchService");
|
|
486
|
+
const activeId = wb?.activeToolId;
|
|
487
|
+
if (typeof activeId === "string" || activeId === null) {
|
|
488
|
+
this.isToolActive = activeId === this.id;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this.isToolActive = fallbackId === this.id;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private isPreviewActive(): boolean {
|
|
495
|
+
return this.isToolActive && this.printWithWhiteInk;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private isDebugEnabled(): boolean {
|
|
499
|
+
return !!this.getConfig<boolean>(WHITE_INK_DEBUG_KEY, false);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private debug(message: string, payload?: any) {
|
|
503
|
+
if (!this.isDebugEnabled()) return;
|
|
504
|
+
if (payload === undefined) {
|
|
505
|
+
console.log(`[WhiteInkTool] ${message}`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
console.log(`[WhiteInkTool] ${message}`, payload);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private resolveSourceUrl(item?: Partial<WhiteInkItem> | null): string {
|
|
512
|
+
if (!item) return "";
|
|
513
|
+
if (typeof item.sourceUrl === "string" && item.sourceUrl.length > 0) {
|
|
514
|
+
return item.sourceUrl;
|
|
515
|
+
}
|
|
516
|
+
if (typeof item.url === "string" && item.url.length > 0) {
|
|
517
|
+
return item.url;
|
|
518
|
+
}
|
|
519
|
+
return "";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private normalizeItem(item: Partial<WhiteInkItem>): WhiteInkItem {
|
|
523
|
+
const sourceUrl = this.resolveSourceUrl(item);
|
|
524
|
+
return {
|
|
525
|
+
id: String(item.id || this.generateId()),
|
|
526
|
+
sourceUrl,
|
|
527
|
+
url: sourceUrl,
|
|
528
|
+
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private normalizeItems(items: WhiteInkItem[]): WhiteInkItem[] {
|
|
533
|
+
return (items || [])
|
|
534
|
+
.map((item) => this.normalizeItem(item))
|
|
535
|
+
.filter((item) => !!item.id);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private cloneItems(items: WhiteInkItem[]): WhiteInkItem[] {
|
|
539
|
+
return this.normalizeItems((items || []).map((item) => ({ ...item })));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private getEffectiveWhiteInkItem(items: WhiteInkItem[]): WhiteInkItem | null {
|
|
543
|
+
const normalized = this.cloneItems(items || []);
|
|
544
|
+
if (normalized.length > 0) {
|
|
545
|
+
return normalized[0];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!this.getPrimaryImageSource()) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
id: WHITE_INK_AUTO_ITEM_ID,
|
|
554
|
+
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private generateId(): string {
|
|
559
|
+
return `white-ink-${Math.random().toString(36).slice(2, 9)}`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private getConfig<T>(key: string, fallback?: T): T | undefined {
|
|
563
|
+
if (!this.context) return fallback;
|
|
564
|
+
const configService = this.context.services.get<ConfigurationService>(
|
|
565
|
+
"ConfigurationService",
|
|
566
|
+
);
|
|
567
|
+
if (!configService) return fallback;
|
|
568
|
+
return (configService.get(key, fallback) as T) ?? fallback;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private resolveReplaceTargetId(explicitId?: string | null): string | null {
|
|
572
|
+
const has = (id: string | null | undefined) =>
|
|
573
|
+
!!id && this.items.some((item) => item.id === id);
|
|
574
|
+
if (has(explicitId)) return explicitId as string;
|
|
575
|
+
if (this.items.length >= 1) {
|
|
576
|
+
return this.items[0].id;
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private updateConfig(newItems: WhiteInkItem[], skipCanvasUpdate = false) {
|
|
582
|
+
if (!this.context) return;
|
|
583
|
+
|
|
584
|
+
this.isUpdatingConfig = true;
|
|
585
|
+
this.items = this.normalizeItems(newItems);
|
|
586
|
+
if (!this.isToolActive || !this.hasWorkingChanges) {
|
|
587
|
+
this.workingItems = this.cloneItems(this.items);
|
|
588
|
+
this.hasWorkingChanges = false;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const configService = this.context.services.get<ConfigurationService>(
|
|
592
|
+
"ConfigurationService",
|
|
593
|
+
);
|
|
594
|
+
configService?.update("whiteInk.items", this.items);
|
|
595
|
+
|
|
596
|
+
if (!skipCanvasUpdate) {
|
|
597
|
+
this.updateWhiteInks();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
setTimeout(() => {
|
|
601
|
+
this.isUpdatingConfig = false;
|
|
602
|
+
}, 50);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private async addWhiteInkEntry(
|
|
606
|
+
url: string,
|
|
607
|
+
options?: Partial<WhiteInkItem>,
|
|
608
|
+
): Promise<string> {
|
|
609
|
+
const id = this.generateId();
|
|
610
|
+
const item = this.normalizeItem({
|
|
611
|
+
id,
|
|
612
|
+
sourceUrl: url,
|
|
613
|
+
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
614
|
+
...options,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
|
|
618
|
+
this.updateConfig([...this.items, item]);
|
|
619
|
+
this.addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd);
|
|
620
|
+
return id;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private async upsertWhiteInkEntry(
|
|
624
|
+
url: string,
|
|
625
|
+
options: UpsertWhiteInkOptions = {},
|
|
626
|
+
): Promise<{ id: string; mode: "replace" | "add" }> {
|
|
627
|
+
const mode = options.mode || "auto";
|
|
628
|
+
if (mode === "add") {
|
|
629
|
+
const id = await this.addWhiteInkEntry(url, options.addOptions);
|
|
630
|
+
return { id, mode: "add" };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const targetId = this.resolveReplaceTargetId(options.id ?? null);
|
|
634
|
+
if (targetId) {
|
|
635
|
+
this.updateWhiteInkInConfig(targetId, {
|
|
636
|
+
...(options.addOptions || {}),
|
|
637
|
+
sourceUrl: url,
|
|
638
|
+
url,
|
|
639
|
+
});
|
|
640
|
+
return { id: targetId, mode: "replace" };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (mode === "replace" || options.createIfMissing === false) {
|
|
644
|
+
throw new Error("replace-target-not-found");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const id = await this.addWhiteInkEntry(url, options.addOptions);
|
|
648
|
+
return { id, mode: "add" };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private addItemToWorkingSessionIfNeeded(
|
|
652
|
+
item: WhiteInkItem,
|
|
653
|
+
sessionDirtyBeforeAdd: boolean,
|
|
654
|
+
) {
|
|
655
|
+
if (!sessionDirtyBeforeAdd || !this.isToolActive) return;
|
|
656
|
+
if (this.workingItems.some((existing) => existing.id === item.id)) return;
|
|
657
|
+
this.workingItems = this.cloneItems([...this.workingItems, item]);
|
|
658
|
+
this.updateWhiteInks();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private async updateWhiteInkItem(
|
|
662
|
+
id: string,
|
|
663
|
+
updates: Partial<WhiteInkItem>,
|
|
664
|
+
options: UpdateWhiteInkOptions = {},
|
|
665
|
+
) {
|
|
666
|
+
this.syncToolActiveFromWorkbench();
|
|
667
|
+
const target = options.target || "auto";
|
|
668
|
+
if (target === "working" || (target === "auto" && this.isToolActive)) {
|
|
669
|
+
this.updateWhiteInkInWorking(id, updates);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
this.updateWhiteInkInConfig(id, updates);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private updateWhiteInkInWorking(id: string, updates: Partial<WhiteInkItem>) {
|
|
677
|
+
let changed = false;
|
|
678
|
+
const next = this.workingItems.map((item) => {
|
|
679
|
+
if (item.id !== id) return item;
|
|
680
|
+
changed = true;
|
|
681
|
+
return this.normalizeItem({
|
|
682
|
+
...item,
|
|
683
|
+
...updates,
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
if (!changed) return;
|
|
687
|
+
|
|
688
|
+
this.workingItems = this.cloneItems(next);
|
|
689
|
+
this.hasWorkingChanges = true;
|
|
690
|
+
this.updateWhiteInks();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private updateWhiteInkInConfig(id: string, updates: Partial<WhiteInkItem>) {
|
|
694
|
+
let changed = false;
|
|
695
|
+
const next = this.items.map((item) => {
|
|
696
|
+
if (item.id !== id) return item;
|
|
697
|
+
changed = true;
|
|
698
|
+
const merged = this.normalizeItem({
|
|
699
|
+
...item,
|
|
700
|
+
...updates,
|
|
701
|
+
});
|
|
702
|
+
if (this.resolveSourceUrl(item) !== this.resolveSourceUrl(merged)) {
|
|
703
|
+
this.purgeSourceCaches(item);
|
|
704
|
+
}
|
|
705
|
+
return merged;
|
|
706
|
+
});
|
|
707
|
+
if (!changed) return;
|
|
708
|
+
|
|
709
|
+
this.updateConfig(next);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private removeWhiteInk(id: string) {
|
|
713
|
+
const removed = this.items.find((item) => item.id === id);
|
|
714
|
+
const next = this.items.filter((item) => item.id !== id);
|
|
715
|
+
if (next.length === this.items.length) return;
|
|
716
|
+
|
|
717
|
+
this.purgeSourceCaches(removed);
|
|
718
|
+
this.updateConfig(next);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private clearWhiteInks() {
|
|
722
|
+
this.sourceSizeBySrc.clear();
|
|
723
|
+
this.previewMaskBySource.clear();
|
|
724
|
+
this.pendingPreviewMaskBySource.clear();
|
|
725
|
+
this.updateConfig([]);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private async completeWhiteInks() {
|
|
729
|
+
this.updateConfig(this.cloneItems(this.workingItems));
|
|
730
|
+
this.hasWorkingChanges = false;
|
|
731
|
+
return { ok: true };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private getFrameRect(): FrameRect {
|
|
735
|
+
if (!this.canvasService) {
|
|
736
|
+
return { left: 0, top: 0, width: 0, height: 0 };
|
|
737
|
+
}
|
|
738
|
+
const configService = this.context?.services.get<ConfigurationService>(
|
|
739
|
+
"ConfigurationService",
|
|
740
|
+
);
|
|
741
|
+
if (!configService) {
|
|
742
|
+
return { left: 0, top: 0, width: 0, height: 0 };
|
|
743
|
+
}
|
|
744
|
+
const sizeState = readSizeState(configService);
|
|
745
|
+
const layout = computeSceneLayout(this.canvasService, sizeState);
|
|
746
|
+
if (!layout) {
|
|
747
|
+
return { left: 0, top: 0, width: 0, height: 0 };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
left: layout.cutRect.left,
|
|
752
|
+
top: layout.cutRect.top,
|
|
753
|
+
width: layout.cutRect.width,
|
|
754
|
+
height: layout.cutRect.height,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private getImageObjects(): any[] {
|
|
759
|
+
if (!this.canvasService) return [];
|
|
760
|
+
return this.canvasService.canvas.getObjects().filter((obj: any) => {
|
|
761
|
+
return obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID;
|
|
762
|
+
}) as any[];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private getPrimaryImageObject(): any | undefined {
|
|
766
|
+
return this.getImageObjects()[0];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private getPrimaryImageSource(): string {
|
|
770
|
+
return this.getCurrentSrc(this.getPrimaryImageObject()) || "";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private getCurrentSrc(obj: any): string | undefined {
|
|
774
|
+
if (!obj) return undefined;
|
|
775
|
+
if (typeof obj.getSrc === "function") return obj.getSrc();
|
|
776
|
+
return obj?._originalElement?.src;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private getImageSnapshot(obj: any): ImageSnapshot | null {
|
|
780
|
+
if (!obj) return null;
|
|
781
|
+
|
|
782
|
+
const src = this.getCurrentSrc(obj);
|
|
783
|
+
if (!src) return null;
|
|
784
|
+
|
|
785
|
+
const element = this.getImageElementFromObject(obj);
|
|
786
|
+
const width = Number(obj?.width || 0);
|
|
787
|
+
const height = Number(obj?.height || 0);
|
|
788
|
+
this.rememberSourceSize(src, { width, height });
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
id: String(obj?.data?.id || "image"),
|
|
792
|
+
src,
|
|
793
|
+
element,
|
|
794
|
+
left: Number.isFinite(obj?.left) ? Number(obj.left) : 0,
|
|
795
|
+
top: Number.isFinite(obj?.top) ? Number(obj.top) : 0,
|
|
796
|
+
scaleX: Number.isFinite(obj?.scaleX) ? Number(obj.scaleX) : 1,
|
|
797
|
+
scaleY: Number.isFinite(obj?.scaleY) ? Number(obj.scaleY) : 1,
|
|
798
|
+
angle: Number.isFinite(obj?.angle) ? Number(obj.angle) : 0,
|
|
799
|
+
originX: typeof obj?.originX === "string" ? obj.originX : "center",
|
|
800
|
+
originY: typeof obj?.originY === "string" ? obj.originY : "center",
|
|
801
|
+
flipX: !!obj?.flipX,
|
|
802
|
+
flipY: !!obj?.flipY,
|
|
803
|
+
skewX: Number.isFinite(obj?.skewX) ? Number(obj.skewX) : 0,
|
|
804
|
+
skewY: Number.isFinite(obj?.skewY) ? Number(obj.skewY) : 0,
|
|
805
|
+
width,
|
|
806
|
+
height,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private getImagePlacementState(id?: string): ImagePlacementState | null {
|
|
811
|
+
const rawItems = this.getConfig<any[]>("image.items", []);
|
|
812
|
+
if (!Array.isArray(rawItems) || rawItems.length === 0) return null;
|
|
813
|
+
|
|
814
|
+
const matched =
|
|
815
|
+
(id
|
|
816
|
+
? rawItems.find(
|
|
817
|
+
(item: any) =>
|
|
818
|
+
item &&
|
|
819
|
+
typeof item === "object" &&
|
|
820
|
+
typeof item.id === "string" &&
|
|
821
|
+
item.id === id,
|
|
822
|
+
)
|
|
823
|
+
: undefined) || rawItems[0];
|
|
824
|
+
|
|
825
|
+
if (!matched || typeof matched !== "object") return null;
|
|
826
|
+
|
|
827
|
+
const sourceUrl =
|
|
828
|
+
typeof matched.sourceUrl === "string" && matched.sourceUrl.length > 0
|
|
829
|
+
? matched.sourceUrl
|
|
830
|
+
: typeof matched.url === "string"
|
|
831
|
+
? matched.url
|
|
832
|
+
: "";
|
|
833
|
+
const committedUrl =
|
|
834
|
+
typeof matched.committedUrl === "string" ? matched.committedUrl : "";
|
|
491
835
|
|
|
492
|
-
private debug(message: string, payload?: any) {
|
|
493
|
-
if (!this.isDebugEnabled()) return;
|
|
494
|
-
if (payload === undefined) {
|
|
495
|
-
console.log(`[WhiteInkTool] ${message}`);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
console.log(`[WhiteInkTool] ${message}`, payload);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
private resolveSourceUrl(item?: Partial<WhiteInkItem> | null): string {
|
|
502
|
-
if (!item) return "";
|
|
503
|
-
if (typeof item.sourceUrl === "string" && item.sourceUrl.length > 0) {
|
|
504
|
-
return item.sourceUrl;
|
|
505
|
-
}
|
|
506
|
-
if (typeof item.url === "string" && item.url.length > 0) {
|
|
507
|
-
return item.url;
|
|
508
|
-
}
|
|
509
|
-
return "";
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
private normalizeItem(item: Partial<WhiteInkItem>): WhiteInkItem {
|
|
513
|
-
const sourceUrl = this.resolveSourceUrl(item);
|
|
514
836
|
return {
|
|
515
|
-
id:
|
|
837
|
+
id:
|
|
838
|
+
typeof matched.id === "string" && matched.id.length > 0
|
|
839
|
+
? matched.id
|
|
840
|
+
: id || "image",
|
|
516
841
|
sourceUrl,
|
|
517
|
-
|
|
518
|
-
|
|
842
|
+
committedUrl,
|
|
843
|
+
left: Number.isFinite(matched.left) ? Number(matched.left) : 0.5,
|
|
844
|
+
top: Number.isFinite(matched.top) ? Number(matched.top) : 0.5,
|
|
845
|
+
scale: Number.isFinite(matched.scale) ? Math.max(0.05, matched.scale) : 1,
|
|
846
|
+
angle: Number.isFinite(matched.angle) ? matched.angle : 0,
|
|
519
847
|
};
|
|
520
848
|
}
|
|
521
849
|
|
|
522
|
-
private
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
850
|
+
private shouldRestoreSnapshotToSource(
|
|
851
|
+
snapshot: ImageSnapshot,
|
|
852
|
+
placement: ImagePlacementState,
|
|
853
|
+
): boolean {
|
|
854
|
+
if (!placement.sourceUrl || !placement.committedUrl) return false;
|
|
855
|
+
if (placement.sourceUrl === placement.committedUrl) return false;
|
|
856
|
+
return snapshot.src === placement.committedUrl;
|
|
526
857
|
}
|
|
527
858
|
|
|
528
|
-
private
|
|
529
|
-
|
|
859
|
+
private getCoverScale(frame: FrameRect, source: SourceSize): number {
|
|
860
|
+
const frameW = Math.max(1, frame.width);
|
|
861
|
+
const frameH = Math.max(1, frame.height);
|
|
862
|
+
const sourceW = Math.max(1, source.width);
|
|
863
|
+
const sourceH = Math.max(1, source.height);
|
|
864
|
+
return Math.max(frameW / sourceW, frameH / sourceH);
|
|
530
865
|
}
|
|
531
866
|
|
|
532
|
-
private
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
867
|
+
private async ensureSourceSize(sourceUrl: string): Promise<SourceSize | null> {
|
|
868
|
+
if (!sourceUrl) return null;
|
|
869
|
+
const cached = this.getSourceSize(sourceUrl);
|
|
870
|
+
if (cached) return cached;
|
|
537
871
|
|
|
538
|
-
|
|
872
|
+
try {
|
|
873
|
+
const image = await this.loadImageElement(sourceUrl);
|
|
874
|
+
const size = this.getElementSize(image);
|
|
875
|
+
if (!size) return null;
|
|
876
|
+
this.rememberSourceSize(sourceUrl, size);
|
|
877
|
+
return {
|
|
878
|
+
width: size.width,
|
|
879
|
+
height: size.height,
|
|
880
|
+
};
|
|
881
|
+
} catch {
|
|
539
882
|
return null;
|
|
540
883
|
}
|
|
541
|
-
|
|
542
|
-
return {
|
|
543
|
-
id: WHITE_INK_AUTO_ITEM_ID,
|
|
544
|
-
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
private generateId(): string {
|
|
549
|
-
return `white-ink-${Math.random().toString(36).slice(2, 9)}`;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
private getConfig<T>(key: string, fallback?: T): T | undefined {
|
|
553
|
-
if (!this.context) return fallback;
|
|
554
|
-
const configService = this.context.services.get<ConfigurationService>(
|
|
555
|
-
"ConfigurationService",
|
|
556
|
-
);
|
|
557
|
-
if (!configService) return fallback;
|
|
558
|
-
return (configService.get(key, fallback) as T) ?? fallback;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
private resolveReplaceTargetId(explicitId?: string | null): string | null {
|
|
562
|
-
const has = (id: string | null | undefined) =>
|
|
563
|
-
!!id && this.items.some((item) => item.id === id);
|
|
564
|
-
if (has(explicitId)) return explicitId as string;
|
|
565
|
-
if (this.items.length >= 1) {
|
|
566
|
-
return this.items[0].id;
|
|
567
|
-
}
|
|
568
|
-
return null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
private updateConfig(newItems: WhiteInkItem[], skipCanvasUpdate = false) {
|
|
572
|
-
if (!this.context) return;
|
|
573
|
-
|
|
574
|
-
this.isUpdatingConfig = true;
|
|
575
|
-
this.items = this.normalizeItems(newItems);
|
|
576
|
-
if (!this.isToolActive || !this.hasWorkingChanges) {
|
|
577
|
-
this.workingItems = this.cloneItems(this.items);
|
|
578
|
-
this.hasWorkingChanges = false;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const configService = this.context.services.get<ConfigurationService>(
|
|
582
|
-
"ConfigurationService",
|
|
583
|
-
);
|
|
584
|
-
configService?.update("whiteInk.items", this.items);
|
|
585
|
-
|
|
586
|
-
if (!skipCanvasUpdate) {
|
|
587
|
-
this.updateWhiteInks();
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
setTimeout(() => {
|
|
591
|
-
this.isUpdatingConfig = false;
|
|
592
|
-
}, 50);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private async addWhiteInkEntry(
|
|
596
|
-
url: string,
|
|
597
|
-
options?: Partial<WhiteInkItem>,
|
|
598
|
-
): Promise<string> {
|
|
599
|
-
const id = this.generateId();
|
|
600
|
-
const item = this.normalizeItem({
|
|
601
|
-
id,
|
|
602
|
-
sourceUrl: url,
|
|
603
|
-
opacity: WHITE_INK_DEFAULT_OPACITY,
|
|
604
|
-
...options,
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
|
|
608
|
-
this.updateConfig([...this.items, item]);
|
|
609
|
-
this.addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd);
|
|
610
|
-
return id;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
private async upsertWhiteInkEntry(
|
|
614
|
-
url: string,
|
|
615
|
-
options: UpsertWhiteInkOptions = {},
|
|
616
|
-
): Promise<{ id: string; mode: "replace" | "add" }> {
|
|
617
|
-
const mode = options.mode || "auto";
|
|
618
|
-
if (mode === "add") {
|
|
619
|
-
const id = await this.addWhiteInkEntry(url, options.addOptions);
|
|
620
|
-
return { id, mode: "add" };
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const targetId = this.resolveReplaceTargetId(options.id ?? null);
|
|
624
|
-
if (targetId) {
|
|
625
|
-
this.updateWhiteInkInConfig(targetId, {
|
|
626
|
-
...(options.addOptions || {}),
|
|
627
|
-
sourceUrl: url,
|
|
628
|
-
url,
|
|
629
|
-
});
|
|
630
|
-
return { id: targetId, mode: "replace" };
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (mode === "replace" || options.createIfMissing === false) {
|
|
634
|
-
throw new Error("replace-target-not-found");
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const id = await this.addWhiteInkEntry(url, options.addOptions);
|
|
638
|
-
return { id, mode: "add" };
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
private addItemToWorkingSessionIfNeeded(
|
|
642
|
-
item: WhiteInkItem,
|
|
643
|
-
sessionDirtyBeforeAdd: boolean,
|
|
644
|
-
) {
|
|
645
|
-
if (!sessionDirtyBeforeAdd || !this.isToolActive) return;
|
|
646
|
-
if (this.workingItems.some((existing) => existing.id === item.id)) return;
|
|
647
|
-
this.workingItems = this.cloneItems([...this.workingItems, item]);
|
|
648
|
-
this.updateWhiteInks();
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
private async updateWhiteInkItem(
|
|
652
|
-
id: string,
|
|
653
|
-
updates: Partial<WhiteInkItem>,
|
|
654
|
-
options: UpdateWhiteInkOptions = {},
|
|
655
|
-
) {
|
|
656
|
-
this.syncToolActiveFromWorkbench();
|
|
657
|
-
const target = options.target || "auto";
|
|
658
|
-
if (target === "working" || (target === "auto" && this.isToolActive)) {
|
|
659
|
-
this.updateWhiteInkInWorking(id, updates);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
this.updateWhiteInkInConfig(id, updates);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
private updateWhiteInkInWorking(id: string, updates: Partial<WhiteInkItem>) {
|
|
667
|
-
let changed = false;
|
|
668
|
-
const next = this.workingItems.map((item) => {
|
|
669
|
-
if (item.id !== id) return item;
|
|
670
|
-
changed = true;
|
|
671
|
-
return this.normalizeItem({
|
|
672
|
-
...item,
|
|
673
|
-
...updates,
|
|
674
|
-
});
|
|
675
|
-
});
|
|
676
|
-
if (!changed) return;
|
|
677
|
-
|
|
678
|
-
this.workingItems = this.cloneItems(next);
|
|
679
|
-
this.hasWorkingChanges = true;
|
|
680
|
-
this.updateWhiteInks();
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
private updateWhiteInkInConfig(id: string, updates: Partial<WhiteInkItem>) {
|
|
684
|
-
let changed = false;
|
|
685
|
-
const next = this.items.map((item) => {
|
|
686
|
-
if (item.id !== id) return item;
|
|
687
|
-
changed = true;
|
|
688
|
-
const merged = this.normalizeItem({
|
|
689
|
-
...item,
|
|
690
|
-
...updates,
|
|
691
|
-
});
|
|
692
|
-
if (this.resolveSourceUrl(item) !== this.resolveSourceUrl(merged)) {
|
|
693
|
-
this.purgeSourceCaches(item);
|
|
694
|
-
}
|
|
695
|
-
return merged;
|
|
696
|
-
});
|
|
697
|
-
if (!changed) return;
|
|
698
|
-
|
|
699
|
-
this.updateConfig(next);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
private removeWhiteInk(id: string) {
|
|
703
|
-
const removed = this.items.find((item) => item.id === id);
|
|
704
|
-
const next = this.items.filter((item) => item.id !== id);
|
|
705
|
-
if (next.length === this.items.length) return;
|
|
706
|
-
|
|
707
|
-
this.purgeSourceCaches(removed);
|
|
708
|
-
this.updateConfig(next);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
private clearWhiteInks() {
|
|
712
|
-
this.sourceSizeBySrc.clear();
|
|
713
|
-
this.previewMaskBySource.clear();
|
|
714
|
-
this.pendingPreviewMaskBySource.clear();
|
|
715
|
-
this.updateConfig([]);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
private async completeWhiteInks() {
|
|
719
|
-
this.updateConfig(this.cloneItems(this.workingItems));
|
|
720
|
-
this.hasWorkingChanges = false;
|
|
721
|
-
return { ok: true };
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
private getFrameRect(): FrameRect {
|
|
725
|
-
if (!this.canvasService) {
|
|
726
|
-
return { left: 0, top: 0, width: 0, height: 0 };
|
|
727
|
-
}
|
|
728
|
-
const configService = this.context?.services.get<ConfigurationService>(
|
|
729
|
-
"ConfigurationService",
|
|
730
|
-
);
|
|
731
|
-
if (!configService) {
|
|
732
|
-
return { left: 0, top: 0, width: 0, height: 0 };
|
|
733
|
-
}
|
|
734
|
-
const sizeState = readSizeState(configService);
|
|
735
|
-
const layout = computeSceneLayout(this.canvasService, sizeState);
|
|
736
|
-
if (!layout) {
|
|
737
|
-
return { left: 0, top: 0, width: 0, height: 0 };
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
return {
|
|
741
|
-
left: layout.cutRect.left,
|
|
742
|
-
top: layout.cutRect.top,
|
|
743
|
-
width: layout.cutRect.width,
|
|
744
|
-
height: layout.cutRect.height,
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
private getImageObjects(): any[] {
|
|
749
|
-
if (!this.canvasService) return [];
|
|
750
|
-
return this.canvasService.canvas.getObjects().filter((obj: any) => {
|
|
751
|
-
return obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID;
|
|
752
|
-
}) as any[];
|
|
753
884
|
}
|
|
754
885
|
|
|
755
|
-
private
|
|
756
|
-
return this.getImageObjects()[0];
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
private getPrimaryImageSource(): string {
|
|
760
|
-
return this.getCurrentSrc(this.getPrimaryImageObject()) || "";
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
private getCurrentSrc(obj: any): string | undefined {
|
|
764
|
-
if (!obj) return undefined;
|
|
765
|
-
if (typeof obj.getSrc === "function") return obj.getSrc();
|
|
766
|
-
return obj?._originalElement?.src;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
private getImageSnapshot(obj: any): ImageSnapshot | null {
|
|
770
|
-
if (!obj) return null;
|
|
771
|
-
|
|
772
|
-
const src = this.getCurrentSrc(obj);
|
|
773
|
-
if (!src) return null;
|
|
774
|
-
|
|
775
|
-
const element = this.getImageElementFromObject(obj);
|
|
776
|
-
const width = Number(obj?.width || 0);
|
|
777
|
-
const height = Number(obj?.height || 0);
|
|
778
|
-
this.rememberSourceSize(src, { width, height });
|
|
779
|
-
|
|
780
|
-
return {
|
|
781
|
-
id: String(obj?.data?.id || "image"),
|
|
782
|
-
src,
|
|
783
|
-
element,
|
|
784
|
-
left: Number.isFinite(obj?.left) ? Number(obj.left) : 0,
|
|
785
|
-
top: Number.isFinite(obj?.top) ? Number(obj.top) : 0,
|
|
786
|
-
scaleX: Number.isFinite(obj?.scaleX) ? Number(obj.scaleX) : 1,
|
|
787
|
-
scaleY: Number.isFinite(obj?.scaleY) ? Number(obj.scaleY) : 1,
|
|
788
|
-
angle: Number.isFinite(obj?.angle) ? Number(obj.angle) : 0,
|
|
789
|
-
originX: typeof obj?.originX === "string" ? obj.originX : "center",
|
|
790
|
-
originY: typeof obj?.originY === "string" ? obj.originY : "center",
|
|
791
|
-
flipX: !!obj?.flipX,
|
|
792
|
-
flipY: !!obj?.flipY,
|
|
793
|
-
skewX: Number.isFinite(obj?.skewX) ? Number(obj.skewX) : 0,
|
|
794
|
-
skewY: Number.isFinite(obj?.skewY) ? Number(obj.skewY) : 0,
|
|
795
|
-
width,
|
|
796
|
-
height,
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
private getImageElementFromObject(obj: any): any {
|
|
801
|
-
if (!obj) return null;
|
|
802
|
-
if (typeof obj.getElement === "function") {
|
|
803
|
-
return obj.getElement();
|
|
804
|
-
}
|
|
805
|
-
return obj?._element || obj?._originalElement || null;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
private rememberSourceSize(src: string, size: SourceSize) {
|
|
809
|
-
if (!src) return;
|
|
810
|
-
if (!Number.isFinite(size.width) || !Number.isFinite(size.height)) return;
|
|
811
|
-
if (size.width <= 0 || size.height <= 0) return;
|
|
812
|
-
this.sourceSizeBySrc.set(src, {
|
|
813
|
-
width: size.width,
|
|
814
|
-
height: size.height,
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
private getSourceSize(src: string): SourceSize | null {
|
|
819
|
-
if (!src) return null;
|
|
820
|
-
const cached = this.sourceSizeBySrc.get(src);
|
|
821
|
-
if (!cached) return null;
|
|
822
|
-
return {
|
|
823
|
-
width: cached.width,
|
|
824
|
-
height: cached.height,
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
private computeWhiteScaleAdjust(
|
|
829
|
-
baseSource: string,
|
|
830
|
-
whiteSource: string,
|
|
831
|
-
): { x: number; y: number } {
|
|
832
|
-
if (!baseSource || !whiteSource || baseSource === whiteSource) {
|
|
833
|
-
return { x: 1, y: 1 };
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const baseSize = this.getSourceSize(baseSource);
|
|
837
|
-
const whiteSize = this.getSourceSize(whiteSource);
|
|
838
|
-
if (!baseSize || !whiteSize) {
|
|
839
|
-
return { x: 1, y: 1 };
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (whiteSize.width <= 0 || whiteSize.height <= 0) {
|
|
843
|
-
return { x: 1, y: 1 };
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
return {
|
|
847
|
-
x: baseSize.width / whiteSize.width,
|
|
848
|
-
y: baseSize.height / whiteSize.height,
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
private computeCoverOpacity(): number {
|
|
853
|
-
const raw = WHITE_INK_DEFAULT_OPACITY * WHITE_INK_COVER_OPACITY_FACTOR;
|
|
854
|
-
return Math.max(
|
|
855
|
-
WHITE_INK_COVER_OPACITY_MIN,
|
|
856
|
-
Math.min(WHITE_INK_COVER_OPACITY_MAX, raw),
|
|
857
|
-
);
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
private buildCloneImageSpec(
|
|
861
|
-
id: string,
|
|
886
|
+
private async resolveAlignedImageSnapshot(
|
|
862
887
|
snapshot: ImageSnapshot,
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
scaleAdjustY = 1,
|
|
869
|
-
): RenderObjectSpec {
|
|
870
|
-
return {
|
|
871
|
-
id,
|
|
872
|
-
type: "image",
|
|
873
|
-
src,
|
|
874
|
-
data: {
|
|
875
|
-
id,
|
|
876
|
-
layerId,
|
|
877
|
-
type,
|
|
878
|
-
imageId: snapshot.id,
|
|
879
|
-
},
|
|
880
|
-
props: {
|
|
881
|
-
left: snapshot.left,
|
|
882
|
-
top: snapshot.top,
|
|
883
|
-
originX: snapshot.originX,
|
|
884
|
-
originY: snapshot.originY,
|
|
885
|
-
angle: snapshot.angle,
|
|
886
|
-
scaleX: snapshot.scaleX * scaleAdjustX,
|
|
887
|
-
scaleY: snapshot.scaleY * scaleAdjustY,
|
|
888
|
-
flipX: snapshot.flipX,
|
|
889
|
-
flipY: snapshot.flipY,
|
|
890
|
-
skewX: snapshot.skewX,
|
|
891
|
-
skewY: snapshot.skewY,
|
|
892
|
-
selectable: false,
|
|
893
|
-
evented: false,
|
|
894
|
-
hasControls: false,
|
|
895
|
-
hasBorders: false,
|
|
896
|
-
uniformScaling: true,
|
|
897
|
-
lockScalingFlip: true,
|
|
898
|
-
opacity: Math.max(0, Math.min(1, Number(opacity))),
|
|
899
|
-
excludeFromExport: true,
|
|
900
|
-
},
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
private buildFrameSpecs(frame: FrameRect): RenderObjectSpec[] {
|
|
905
|
-
if (!this.isToolActive || !this.canvasService) return [];
|
|
906
|
-
if (frame.width <= 0 || frame.height <= 0) return [];
|
|
907
|
-
|
|
908
|
-
const canvasW = this.canvasService.canvas.width || 0;
|
|
909
|
-
const canvasH = this.canvasService.canvas.height || 0;
|
|
910
|
-
const strokeColor =
|
|
911
|
-
this.getConfig<string>("image.frame.strokeColor", "#808080") || "#808080";
|
|
912
|
-
const strokeWidthRaw = Number(
|
|
913
|
-
this.getConfig<number>("image.frame.strokeWidth", 2) ?? 2,
|
|
914
|
-
);
|
|
915
|
-
const dashLengthRaw = Number(
|
|
916
|
-
this.getConfig<number>("image.frame.dashLength", 8) ?? 8,
|
|
917
|
-
);
|
|
918
|
-
const outerBackground =
|
|
919
|
-
this.getConfig<string>("image.frame.outerBackground", "#f5f5f5") ||
|
|
920
|
-
"#f5f5f5";
|
|
921
|
-
const innerBackground =
|
|
922
|
-
this.getConfig<string>("image.frame.innerBackground", "rgba(0,0,0,0)") ||
|
|
923
|
-
"rgba(0,0,0,0)";
|
|
924
|
-
|
|
925
|
-
const strokeWidth = Number.isFinite(strokeWidthRaw)
|
|
926
|
-
? Math.max(0, strokeWidthRaw)
|
|
927
|
-
: 2;
|
|
928
|
-
const dashLength = Number.isFinite(dashLengthRaw)
|
|
929
|
-
? Math.max(1, dashLengthRaw)
|
|
930
|
-
: 8;
|
|
931
|
-
|
|
932
|
-
const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
|
|
933
|
-
const frameTop = Math.max(0, Math.min(canvasH, frame.top));
|
|
934
|
-
const frameRight = Math.max(
|
|
935
|
-
frameLeft,
|
|
936
|
-
Math.min(canvasW, frame.left + frame.width),
|
|
937
|
-
);
|
|
938
|
-
const frameBottom = Math.max(
|
|
939
|
-
frameTop,
|
|
940
|
-
Math.min(canvasH, frame.top + frame.height),
|
|
941
|
-
);
|
|
942
|
-
const visibleFrameH = Math.max(0, frameBottom - frameTop);
|
|
943
|
-
|
|
944
|
-
const topH = frameTop;
|
|
945
|
-
const bottomH = Math.max(0, canvasH - frameBottom);
|
|
946
|
-
const leftW = frameLeft;
|
|
947
|
-
const rightW = Math.max(0, canvasW - frameRight);
|
|
948
|
-
|
|
949
|
-
const maskSpecs: RenderObjectSpec[] = [
|
|
950
|
-
{
|
|
951
|
-
id: "white-ink.cropMask.top",
|
|
952
|
-
type: "rect",
|
|
953
|
-
data: {
|
|
954
|
-
id: "white-ink.cropMask.top",
|
|
955
|
-
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
956
|
-
type: "white-ink-mask",
|
|
957
|
-
},
|
|
958
|
-
props: {
|
|
959
|
-
left: canvasW / 2,
|
|
960
|
-
top: topH / 2,
|
|
961
|
-
width: canvasW,
|
|
962
|
-
height: topH,
|
|
963
|
-
originX: "center",
|
|
964
|
-
originY: "center",
|
|
965
|
-
fill: outerBackground,
|
|
966
|
-
selectable: false,
|
|
967
|
-
evented: false,
|
|
968
|
-
excludeFromExport: true,
|
|
969
|
-
},
|
|
970
|
-
},
|
|
971
|
-
{
|
|
972
|
-
id: "white-ink.cropMask.bottom",
|
|
973
|
-
type: "rect",
|
|
974
|
-
data: {
|
|
975
|
-
id: "white-ink.cropMask.bottom",
|
|
976
|
-
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
977
|
-
type: "white-ink-mask",
|
|
978
|
-
},
|
|
979
|
-
props: {
|
|
980
|
-
left: canvasW / 2,
|
|
981
|
-
top: frameBottom + bottomH / 2,
|
|
982
|
-
width: canvasW,
|
|
983
|
-
height: bottomH,
|
|
984
|
-
originX: "center",
|
|
985
|
-
originY: "center",
|
|
986
|
-
fill: outerBackground,
|
|
987
|
-
selectable: false,
|
|
988
|
-
evented: false,
|
|
989
|
-
excludeFromExport: true,
|
|
990
|
-
},
|
|
991
|
-
},
|
|
992
|
-
{
|
|
993
|
-
id: "white-ink.cropMask.left",
|
|
994
|
-
type: "rect",
|
|
995
|
-
data: {
|
|
996
|
-
id: "white-ink.cropMask.left",
|
|
997
|
-
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
998
|
-
type: "white-ink-mask",
|
|
999
|
-
},
|
|
1000
|
-
props: {
|
|
1001
|
-
left: leftW / 2,
|
|
1002
|
-
top: frameTop + visibleFrameH / 2,
|
|
1003
|
-
width: leftW,
|
|
1004
|
-
height: visibleFrameH,
|
|
1005
|
-
originX: "center",
|
|
1006
|
-
originY: "center",
|
|
1007
|
-
fill: outerBackground,
|
|
1008
|
-
selectable: false,
|
|
1009
|
-
evented: false,
|
|
1010
|
-
excludeFromExport: true,
|
|
1011
|
-
},
|
|
1012
|
-
},
|
|
1013
|
-
{
|
|
1014
|
-
id: "white-ink.cropMask.right",
|
|
1015
|
-
type: "rect",
|
|
1016
|
-
data: {
|
|
1017
|
-
id: "white-ink.cropMask.right",
|
|
1018
|
-
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
1019
|
-
type: "white-ink-mask",
|
|
1020
|
-
},
|
|
1021
|
-
props: {
|
|
1022
|
-
left: frameRight + rightW / 2,
|
|
1023
|
-
top: frameTop + visibleFrameH / 2,
|
|
1024
|
-
width: rightW,
|
|
1025
|
-
height: visibleFrameH,
|
|
1026
|
-
originX: "center",
|
|
1027
|
-
originY: "center",
|
|
1028
|
-
fill: outerBackground,
|
|
1029
|
-
selectable: false,
|
|
1030
|
-
evented: false,
|
|
1031
|
-
excludeFromExport: true,
|
|
1032
|
-
},
|
|
1033
|
-
},
|
|
1034
|
-
];
|
|
1035
|
-
|
|
1036
|
-
return [
|
|
1037
|
-
...maskSpecs,
|
|
1038
|
-
{
|
|
1039
|
-
id: "white-ink.cropFrame",
|
|
1040
|
-
type: "rect",
|
|
1041
|
-
data: {
|
|
1042
|
-
id: "white-ink.cropFrame",
|
|
1043
|
-
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
1044
|
-
type: "white-ink-frame",
|
|
1045
|
-
},
|
|
1046
|
-
props: {
|
|
1047
|
-
left: frame.left + frame.width / 2,
|
|
1048
|
-
top: frame.top + frame.height / 2,
|
|
1049
|
-
width: frame.width,
|
|
1050
|
-
height: frame.height,
|
|
1051
|
-
originX: "center",
|
|
1052
|
-
originY: "center",
|
|
1053
|
-
fill: innerBackground,
|
|
1054
|
-
stroke: strokeColor,
|
|
1055
|
-
strokeWidth,
|
|
1056
|
-
strokeDashArray: [dashLength, dashLength],
|
|
1057
|
-
selectable: false,
|
|
1058
|
-
evented: false,
|
|
1059
|
-
excludeFromExport: true,
|
|
1060
|
-
},
|
|
1061
|
-
},
|
|
1062
|
-
];
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
private applyImageVisibilityForWhiteInk(previewActive: boolean) {
|
|
1066
|
-
if (!this.canvasService) return;
|
|
1067
|
-
const visible = !previewActive;
|
|
1068
|
-
let changed = false;
|
|
1069
|
-
|
|
1070
|
-
this.canvasService.canvas.getObjects().forEach((obj: any) => {
|
|
1071
|
-
if (obj?.data?.layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
1072
|
-
if (obj.visible === visible) return;
|
|
1073
|
-
obj.set({ visible });
|
|
1074
|
-
obj.setCoords?.();
|
|
1075
|
-
changed = true;
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
if (changed) {
|
|
1079
|
-
this.canvasService.requestRenderAll();
|
|
888
|
+
): Promise<ImageSnapshot> {
|
|
889
|
+
const placement = this.getImagePlacementState(snapshot.id);
|
|
890
|
+
if (!placement) return snapshot;
|
|
891
|
+
if (!this.shouldRestoreSnapshotToSource(snapshot, placement)) {
|
|
892
|
+
return snapshot;
|
|
1080
893
|
}
|
|
1081
|
-
}
|
|
1082
894
|
|
|
1083
|
-
|
|
1084
|
-
if (
|
|
1085
|
-
return
|
|
895
|
+
const frame = this.getFrameRect();
|
|
896
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
897
|
+
return snapshot;
|
|
1086
898
|
}
|
|
1087
|
-
return this.cloneItems(this.items);
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
private async resolveRenderSources(
|
|
1091
|
-
snapshot: ImageSnapshot,
|
|
1092
|
-
item: WhiteInkItem,
|
|
1093
|
-
): Promise<RenderSources | null> {
|
|
1094
|
-
const imageSource = snapshot.src;
|
|
1095
|
-
if (!imageSource) return null;
|
|
1096
899
|
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
const whiteElement = whiteSource === imageSource ? imageElement : undefined;
|
|
1100
|
-
const [whiteMaskSrc, coverMaskSrc] = await Promise.all([
|
|
1101
|
-
this.getPreviewMaskSource(whiteSource, WHITE_MASK_TINT, whiteElement),
|
|
1102
|
-
this.getPreviewMaskSource(imageSource, COVER_MASK_TINT, imageElement),
|
|
1103
|
-
]);
|
|
900
|
+
const sourceSize = await this.ensureSourceSize(placement.sourceUrl);
|
|
901
|
+
if (!sourceSize) return snapshot;
|
|
1104
902
|
|
|
1105
|
-
const
|
|
903
|
+
const coverScale = this.getCoverScale(frame, sourceSize);
|
|
1106
904
|
|
|
1107
905
|
return {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
906
|
+
...snapshot,
|
|
907
|
+
src: placement.sourceUrl,
|
|
908
|
+
element: undefined,
|
|
909
|
+
left: frame.left + placement.left * frame.width,
|
|
910
|
+
top: frame.top + placement.top * frame.height,
|
|
911
|
+
scaleX: coverScale * placement.scale,
|
|
912
|
+
scaleY: coverScale * placement.scale,
|
|
913
|
+
angle: placement.angle,
|
|
914
|
+
originX: "center",
|
|
915
|
+
originY: "center",
|
|
916
|
+
width: sourceSize.width,
|
|
917
|
+
height: sourceSize.height,
|
|
1112
918
|
};
|
|
1113
919
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
)
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
:
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
}
|
|
920
|
+
private getImageElementFromObject(obj: any): any {
|
|
921
|
+
if (!obj) return null;
|
|
922
|
+
if (typeof obj.getElement === "function") {
|
|
923
|
+
return obj.getElement();
|
|
924
|
+
}
|
|
925
|
+
return obj?._element || obj?._originalElement || null;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private rememberSourceSize(src: string, size: SourceSize) {
|
|
929
|
+
if (!src) return;
|
|
930
|
+
if (!Number.isFinite(size.width) || !Number.isFinite(size.height)) return;
|
|
931
|
+
if (size.width <= 0 || size.height <= 0) return;
|
|
932
|
+
this.sourceSizeBySrc.set(src, {
|
|
933
|
+
width: size.width,
|
|
934
|
+
height: size.height,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private getSourceSize(src: string): SourceSize | null {
|
|
939
|
+
if (!src) return null;
|
|
940
|
+
const cached = this.sourceSizeBySrc.get(src);
|
|
941
|
+
if (!cached) return null;
|
|
942
|
+
return {
|
|
943
|
+
width: cached.width,
|
|
944
|
+
height: cached.height,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private computeWhiteScaleAdjust(
|
|
949
|
+
baseSource: string,
|
|
950
|
+
whiteSource: string,
|
|
951
|
+
): { x: number; y: number } {
|
|
952
|
+
if (!baseSource || !whiteSource || baseSource === whiteSource) {
|
|
953
|
+
return { x: 1, y: 1 };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const baseSize = this.getSourceSize(baseSource);
|
|
957
|
+
const whiteSize = this.getSourceSize(whiteSource);
|
|
958
|
+
if (!baseSize || !whiteSize) {
|
|
959
|
+
return { x: 1, y: 1 };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (whiteSize.width <= 0 || whiteSize.height <= 0) {
|
|
963
|
+
return { x: 1, y: 1 };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return {
|
|
967
|
+
x: baseSize.width / whiteSize.width,
|
|
968
|
+
y: baseSize.height / whiteSize.height,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private computeCoverOpacity(): number {
|
|
973
|
+
const raw = WHITE_INK_DEFAULT_OPACITY * WHITE_INK_COVER_OPACITY_FACTOR;
|
|
974
|
+
return Math.max(
|
|
975
|
+
WHITE_INK_COVER_OPACITY_MIN,
|
|
976
|
+
Math.min(WHITE_INK_COVER_OPACITY_MAX, raw),
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private buildCloneImageSpec(
|
|
981
|
+
id: string,
|
|
982
|
+
snapshot: ImageSnapshot,
|
|
983
|
+
src: string,
|
|
984
|
+
opacity: number,
|
|
985
|
+
layerId: string,
|
|
986
|
+
type: "white-ink" | "white-ink-cover",
|
|
987
|
+
scaleAdjustX = 1,
|
|
988
|
+
scaleAdjustY = 1,
|
|
989
|
+
): RenderObjectSpec {
|
|
990
|
+
return {
|
|
991
|
+
id,
|
|
992
|
+
type: "image",
|
|
993
|
+
src,
|
|
994
|
+
data: {
|
|
995
|
+
id,
|
|
996
|
+
layerId,
|
|
997
|
+
type,
|
|
998
|
+
imageId: snapshot.id,
|
|
999
|
+
},
|
|
1000
|
+
props: {
|
|
1001
|
+
left: snapshot.left,
|
|
1002
|
+
top: snapshot.top,
|
|
1003
|
+
originX: snapshot.originX,
|
|
1004
|
+
originY: snapshot.originY,
|
|
1005
|
+
angle: snapshot.angle,
|
|
1006
|
+
scaleX: snapshot.scaleX * scaleAdjustX,
|
|
1007
|
+
scaleY: snapshot.scaleY * scaleAdjustY,
|
|
1008
|
+
flipX: snapshot.flipX,
|
|
1009
|
+
flipY: snapshot.flipY,
|
|
1010
|
+
skewX: snapshot.skewX,
|
|
1011
|
+
skewY: snapshot.skewY,
|
|
1012
|
+
selectable: false,
|
|
1013
|
+
evented: false,
|
|
1014
|
+
hasControls: false,
|
|
1015
|
+
hasBorders: false,
|
|
1016
|
+
uniformScaling: true,
|
|
1017
|
+
lockScalingFlip: true,
|
|
1018
|
+
opacity: Math.max(0, Math.min(1, Number(opacity))),
|
|
1019
|
+
excludeFromExport: true,
|
|
1020
|
+
},
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private buildFrameSpecs(frame: FrameRect): RenderObjectSpec[] {
|
|
1025
|
+
if (!this.isToolActive || !this.canvasService) return [];
|
|
1026
|
+
if (frame.width <= 0 || frame.height <= 0) return [];
|
|
1027
|
+
|
|
1028
|
+
const canvasW = this.canvasService.canvas.width || 0;
|
|
1029
|
+
const canvasH = this.canvasService.canvas.height || 0;
|
|
1030
|
+
const strokeColor =
|
|
1031
|
+
this.getConfig<string>("image.frame.strokeColor", "#808080") || "#808080";
|
|
1032
|
+
const strokeWidthRaw = Number(
|
|
1033
|
+
this.getConfig<number>("image.frame.strokeWidth", 2) ?? 2,
|
|
1034
|
+
);
|
|
1035
|
+
const dashLengthRaw = Number(
|
|
1036
|
+
this.getConfig<number>("image.frame.dashLength", 8) ?? 8,
|
|
1037
|
+
);
|
|
1038
|
+
const outerBackground =
|
|
1039
|
+
this.getConfig<string>("image.frame.outerBackground", "#f5f5f5") ||
|
|
1040
|
+
"#f5f5f5";
|
|
1041
|
+
const innerBackground =
|
|
1042
|
+
this.getConfig<string>("image.frame.innerBackground", "rgba(0,0,0,0)") ||
|
|
1043
|
+
"rgba(0,0,0,0)";
|
|
1044
|
+
|
|
1045
|
+
const strokeWidth = Number.isFinite(strokeWidthRaw)
|
|
1046
|
+
? Math.max(0, strokeWidthRaw)
|
|
1047
|
+
: 2;
|
|
1048
|
+
const dashLength = Number.isFinite(dashLengthRaw)
|
|
1049
|
+
? Math.max(1, dashLengthRaw)
|
|
1050
|
+
: 8;
|
|
1051
|
+
|
|
1052
|
+
const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
|
|
1053
|
+
const frameTop = Math.max(0, Math.min(canvasH, frame.top));
|
|
1054
|
+
const frameRight = Math.max(
|
|
1055
|
+
frameLeft,
|
|
1056
|
+
Math.min(canvasW, frame.left + frame.width),
|
|
1057
|
+
);
|
|
1058
|
+
const frameBottom = Math.max(
|
|
1059
|
+
frameTop,
|
|
1060
|
+
Math.min(canvasH, frame.top + frame.height),
|
|
1061
|
+
);
|
|
1062
|
+
const visibleFrameH = Math.max(0, frameBottom - frameTop);
|
|
1063
|
+
|
|
1064
|
+
const topH = frameTop;
|
|
1065
|
+
const bottomH = Math.max(0, canvasH - frameBottom);
|
|
1066
|
+
const leftW = frameLeft;
|
|
1067
|
+
const rightW = Math.max(0, canvasW - frameRight);
|
|
1068
|
+
|
|
1069
|
+
const maskSpecs: RenderObjectSpec[] = [
|
|
1070
|
+
{
|
|
1071
|
+
id: "white-ink.cropMask.top",
|
|
1072
|
+
type: "rect",
|
|
1073
|
+
data: {
|
|
1074
|
+
id: "white-ink.cropMask.top",
|
|
1075
|
+
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
1076
|
+
type: "white-ink-mask",
|
|
1077
|
+
},
|
|
1078
|
+
props: {
|
|
1079
|
+
left: canvasW / 2,
|
|
1080
|
+
top: topH / 2,
|
|
1081
|
+
width: canvasW,
|
|
1082
|
+
height: topH,
|
|
1083
|
+
originX: "center",
|
|
1084
|
+
originY: "center",
|
|
1085
|
+
fill: outerBackground,
|
|
1086
|
+
selectable: false,
|
|
1087
|
+
evented: false,
|
|
1088
|
+
excludeFromExport: true,
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
id: "white-ink.cropMask.bottom",
|
|
1093
|
+
type: "rect",
|
|
1094
|
+
data: {
|
|
1095
|
+
id: "white-ink.cropMask.bottom",
|
|
1096
|
+
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
1097
|
+
type: "white-ink-mask",
|
|
1098
|
+
},
|
|
1099
|
+
props: {
|
|
1100
|
+
left: canvasW / 2,
|
|
1101
|
+
top: frameBottom + bottomH / 2,
|
|
1102
|
+
width: canvasW,
|
|
1103
|
+
height: bottomH,
|
|
1104
|
+
originX: "center",
|
|
1105
|
+
originY: "center",
|
|
1106
|
+
fill: outerBackground,
|
|
1107
|
+
selectable: false,
|
|
1108
|
+
evented: false,
|
|
1109
|
+
excludeFromExport: true,
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
id: "white-ink.cropMask.left",
|
|
1114
|
+
type: "rect",
|
|
1115
|
+
data: {
|
|
1116
|
+
id: "white-ink.cropMask.left",
|
|
1117
|
+
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
1118
|
+
type: "white-ink-mask",
|
|
1119
|
+
},
|
|
1120
|
+
props: {
|
|
1121
|
+
left: leftW / 2,
|
|
1122
|
+
top: frameTop + visibleFrameH / 2,
|
|
1123
|
+
width: leftW,
|
|
1124
|
+
height: visibleFrameH,
|
|
1125
|
+
originX: "center",
|
|
1126
|
+
originY: "center",
|
|
1127
|
+
fill: outerBackground,
|
|
1128
|
+
selectable: false,
|
|
1129
|
+
evented: false,
|
|
1130
|
+
excludeFromExport: true,
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
id: "white-ink.cropMask.right",
|
|
1135
|
+
type: "rect",
|
|
1136
|
+
data: {
|
|
1137
|
+
id: "white-ink.cropMask.right",
|
|
1138
|
+
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
1139
|
+
type: "white-ink-mask",
|
|
1140
|
+
},
|
|
1141
|
+
props: {
|
|
1142
|
+
left: frameRight + rightW / 2,
|
|
1143
|
+
top: frameTop + visibleFrameH / 2,
|
|
1144
|
+
width: rightW,
|
|
1145
|
+
height: visibleFrameH,
|
|
1146
|
+
originX: "center",
|
|
1147
|
+
originY: "center",
|
|
1148
|
+
fill: outerBackground,
|
|
1149
|
+
selectable: false,
|
|
1150
|
+
evented: false,
|
|
1151
|
+
excludeFromExport: true,
|
|
1152
|
+
},
|
|
1153
|
+
},
|
|
1154
|
+
];
|
|
1155
|
+
|
|
1156
|
+
return [
|
|
1157
|
+
...maskSpecs,
|
|
1158
|
+
{
|
|
1159
|
+
id: "white-ink.cropFrame",
|
|
1160
|
+
type: "rect",
|
|
1161
|
+
data: {
|
|
1162
|
+
id: "white-ink.cropFrame",
|
|
1163
|
+
layerId: WHITE_INK_OVERLAY_LAYER_ID,
|
|
1164
|
+
type: "white-ink-frame",
|
|
1165
|
+
},
|
|
1166
|
+
props: {
|
|
1167
|
+
left: frame.left + frame.width / 2,
|
|
1168
|
+
top: frame.top + frame.height / 2,
|
|
1169
|
+
width: frame.width,
|
|
1170
|
+
height: frame.height,
|
|
1171
|
+
originX: "center",
|
|
1172
|
+
originY: "center",
|
|
1173
|
+
fill: innerBackground,
|
|
1174
|
+
stroke: strokeColor,
|
|
1175
|
+
strokeWidth,
|
|
1176
|
+
strokeDashArray: [dashLength, dashLength],
|
|
1177
|
+
selectable: false,
|
|
1178
|
+
evented: false,
|
|
1179
|
+
excludeFromExport: true,
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
1182
|
+
];
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private applyImageVisibilityForWhiteInk(previewActive: boolean) {
|
|
1186
|
+
if (!this.canvasService) return;
|
|
1187
|
+
const visible = !previewActive;
|
|
1188
|
+
let changed = false;
|
|
1189
|
+
|
|
1190
|
+
this.canvasService.canvas.getObjects().forEach((obj: any) => {
|
|
1191
|
+
if (obj?.data?.layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
1192
|
+
if (obj.visible === visible) return;
|
|
1193
|
+
obj.set({ visible });
|
|
1194
|
+
obj.setCoords?.();
|
|
1195
|
+
changed = true;
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
if (changed) {
|
|
1199
|
+
this.canvasService.requestRenderAll();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
private resolveRenderItems(): WhiteInkItem[] {
|
|
1204
|
+
if (this.isToolActive) {
|
|
1205
|
+
return this.cloneItems(this.workingItems);
|
|
1206
|
+
}
|
|
1207
|
+
return this.cloneItems(this.items);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private async resolveRenderSources(
|
|
1211
|
+
snapshot: ImageSnapshot,
|
|
1212
|
+
item: WhiteInkItem,
|
|
1213
|
+
): Promise<RenderSources | null> {
|
|
1214
|
+
const imageSource = snapshot.src;
|
|
1215
|
+
if (!imageSource) return null;
|
|
1216
|
+
|
|
1217
|
+
const whiteSource = this.resolveSourceUrl(item) || imageSource;
|
|
1218
|
+
const imageElement = snapshot.element;
|
|
1219
|
+
const whiteElement = whiteSource === imageSource ? imageElement : undefined;
|
|
1220
|
+
const [whiteMaskSrc, coverMaskSrc] = await Promise.all([
|
|
1221
|
+
this.getPreviewMaskSource(whiteSource, WHITE_MASK_TINT, whiteElement),
|
|
1222
|
+
this.getPreviewMaskSource(imageSource, COVER_MASK_TINT, imageElement),
|
|
1223
|
+
]);
|
|
1224
|
+
|
|
1225
|
+
const scaleAdjust = this.computeWhiteScaleAdjust(imageSource, whiteSource);
|
|
1226
|
+
|
|
1227
|
+
return {
|
|
1228
|
+
whiteSrc: whiteMaskSrc || "",
|
|
1229
|
+
coverSrc: coverMaskSrc || "",
|
|
1230
|
+
whiteScaleAdjustX: scaleAdjust.x,
|
|
1231
|
+
whiteScaleAdjustY: scaleAdjust.y,
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
private resolveDefaultInsertIndex(objects: any[]): number {
|
|
1236
|
+
if (!this.canvasService) return 0;
|
|
1237
|
+
const backgroundLayer = this.canvasService.getLayer("background");
|
|
1238
|
+
if (!backgroundLayer) return 0;
|
|
1239
|
+
const bgIndex = objects.indexOf(backgroundLayer as any);
|
|
1240
|
+
if (bgIndex < 0) return 0;
|
|
1241
|
+
return bgIndex + 1;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private syncZOrder() {
|
|
1245
|
+
if (!this.canvasService) return;
|
|
1246
|
+
const canvas = this.canvasService.canvas;
|
|
1247
|
+
|
|
1248
|
+
const whiteObjects = this.canvasService.getRootLayerObjects(
|
|
1249
|
+
WHITE_INK_OBJECT_LAYER_ID,
|
|
1250
|
+
) as any[];
|
|
1251
|
+
const coverObjects = this.canvasService.getRootLayerObjects(
|
|
1252
|
+
WHITE_INK_COVER_LAYER_ID,
|
|
1253
|
+
) as any[];
|
|
1254
|
+
const frameObjects = this.canvasService.getRootLayerObjects(
|
|
1255
|
+
WHITE_INK_OVERLAY_LAYER_ID,
|
|
1256
|
+
) as any[];
|
|
1257
|
+
|
|
1258
|
+
const currentObjects = canvas.getObjects();
|
|
1259
|
+
const imageIndexes = currentObjects
|
|
1260
|
+
.map((obj: any, index: number) =>
|
|
1261
|
+
obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID ? index : -1,
|
|
1262
|
+
)
|
|
1263
|
+
.filter((index: number) => index >= 0);
|
|
1264
|
+
|
|
1265
|
+
let whiteInsertIndex = imageIndexes.length
|
|
1266
|
+
? Math.min(...imageIndexes)
|
|
1267
|
+
: this.resolveDefaultInsertIndex(currentObjects);
|
|
1268
|
+
|
|
1269
|
+
whiteObjects.forEach((obj) => {
|
|
1270
|
+
canvas.moveObjectTo(obj, whiteInsertIndex);
|
|
1271
|
+
whiteInsertIndex += 1;
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
const afterWhiteObjects = canvas.getObjects();
|
|
1275
|
+
const afterImageIndexes = afterWhiteObjects
|
|
1276
|
+
.map((obj: any, index: number) =>
|
|
1277
|
+
obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID ? index : -1,
|
|
1278
|
+
)
|
|
1279
|
+
.filter((index: number) => index >= 0);
|
|
1280
|
+
|
|
1281
|
+
let coverInsertIndex = afterImageIndexes.length
|
|
1282
|
+
? Math.max(...afterImageIndexes) + 1
|
|
1283
|
+
: whiteInsertIndex;
|
|
1284
|
+
|
|
1285
|
+
coverObjects.forEach((obj) => {
|
|
1286
|
+
canvas.moveObjectTo(obj, coverInsertIndex);
|
|
1287
|
+
coverInsertIndex += 1;
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
|
|
1291
|
+
|
|
1292
|
+
canvas
|
|
1293
|
+
.getObjects()
|
|
1294
|
+
.filter((obj: any) => obj?.data?.layerId === IMAGE_OVERLAY_LAYER_ID)
|
|
1295
|
+
.forEach((obj: any) => canvas.bringObjectToFront(obj));
|
|
1296
|
+
|
|
1297
|
+
const dielineOverlay = this.canvasService.getLayer("dieline-overlay");
|
|
1298
|
+
if (dielineOverlay) {
|
|
1299
|
+
canvas.bringObjectToFront(dielineOverlay as any);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const rulerOverlay = this.canvasService.getLayer("ruler-overlay");
|
|
1303
|
+
if (rulerOverlay) {
|
|
1304
|
+
canvas.bringObjectToFront(rulerOverlay as any);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
private clearRenderedWhiteInks() {
|
|
1309
|
+
if (!this.canvasService) return;
|
|
1310
|
+
void this.canvasService.applyObjectSpecsToRootLayer(
|
|
1311
|
+
WHITE_INK_OBJECT_LAYER_ID,
|
|
1312
|
+
[],
|
|
1313
|
+
);
|
|
1314
|
+
void this.canvasService.applyObjectSpecsToRootLayer(
|
|
1315
|
+
WHITE_INK_COVER_LAYER_ID,
|
|
1316
|
+
[],
|
|
1317
|
+
);
|
|
1318
|
+
void this.canvasService.applyObjectSpecsToRootLayer(
|
|
1319
|
+
WHITE_INK_OVERLAY_LAYER_ID,
|
|
1320
|
+
[],
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
private purgeSourceCaches(item?: WhiteInkItem) {
|
|
1325
|
+
const sourceUrl = this.resolveSourceUrl(item);
|
|
1326
|
+
if (!sourceUrl) return;
|
|
1327
|
+
this.sourceSizeBySrc.delete(sourceUrl);
|
|
1328
|
+
const prefix = `${sourceUrl}::`;
|
|
1329
|
+
Array.from(this.previewMaskBySource.keys()).forEach((cacheKey) => {
|
|
1330
|
+
if (cacheKey.startsWith(prefix)) {
|
|
1331
|
+
this.previewMaskBySource.delete(cacheKey);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
Array.from(this.pendingPreviewMaskBySource.keys()).forEach((cacheKey) => {
|
|
1335
|
+
if (cacheKey.startsWith(prefix)) {
|
|
1336
|
+
this.pendingPreviewMaskBySource.delete(cacheKey);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
private updateWhiteInks() {
|
|
1342
|
+
void this.updateWhiteInksAsync();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
private async updateWhiteInksAsync() {
|
|
1346
|
+
if (!this.canvasService) return;
|
|
1347
|
+
|
|
1348
|
+
this.syncToolActiveFromWorkbench();
|
|
1349
|
+
const seq = ++this.renderSeq;
|
|
1350
|
+
|
|
1351
|
+
const previewActive = this.isPreviewActive();
|
|
1352
|
+
this.applyImageVisibilityForWhiteInk(previewActive);
|
|
1353
|
+
|
|
1354
|
+
const frame = this.getFrameRect();
|
|
1355
|
+
const frameSpecs = this.buildFrameSpecs(frame);
|
|
1356
|
+
|
|
1357
|
+
let whiteSpecs: RenderObjectSpec[] = [];
|
|
1358
|
+
let coverSpecs: RenderObjectSpec[] = [];
|
|
1359
|
+
|
|
1360
|
+
if (previewActive) {
|
|
1361
|
+
const baseSnapshot = this.getImageSnapshot(this.getPrimaryImageObject());
|
|
1362
|
+
const item = this.getEffectiveWhiteInkItem(this.resolveRenderItems());
|
|
1363
|
+
|
|
1364
|
+
if (baseSnapshot && item) {
|
|
1365
|
+
const snapshot = await this.resolveAlignedImageSnapshot(baseSnapshot);
|
|
1366
|
+
if (seq !== this.renderSeq) return;
|
|
1367
|
+
const sources = await this.resolveRenderSources(snapshot, item);
|
|
1368
|
+
if (seq !== this.renderSeq) return;
|
|
1369
|
+
|
|
1370
|
+
if (sources?.whiteSrc) {
|
|
1371
|
+
whiteSpecs = [
|
|
1372
|
+
this.buildCloneImageSpec(
|
|
1373
|
+
"white-ink.main",
|
|
1374
|
+
snapshot,
|
|
1375
|
+
sources.whiteSrc,
|
|
1376
|
+
WHITE_INK_DEFAULT_OPACITY,
|
|
1377
|
+
WHITE_INK_OBJECT_LAYER_ID,
|
|
1378
|
+
"white-ink",
|
|
1379
|
+
sources.whiteScaleAdjustX,
|
|
1380
|
+
sources.whiteScaleAdjustY,
|
|
1381
|
+
),
|
|
1382
|
+
];
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (this.previewImageVisible && sources?.coverSrc) {
|
|
1386
|
+
coverSpecs = [
|
|
1387
|
+
this.buildCloneImageSpec(
|
|
1388
|
+
"white-ink.cover",
|
|
1389
|
+
snapshot,
|
|
1390
|
+
sources.coverSrc,
|
|
1391
|
+
this.computeCoverOpacity(),
|
|
1392
|
+
WHITE_INK_COVER_LAYER_ID,
|
|
1393
|
+
"white-ink-cover",
|
|
1394
|
+
),
|
|
1395
|
+
];
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
await this.canvasService.applyObjectSpecsToRootLayer(
|
|
1401
|
+
WHITE_INK_OBJECT_LAYER_ID,
|
|
1402
|
+
whiteSpecs,
|
|
1403
|
+
);
|
|
1404
|
+
if (seq !== this.renderSeq) return;
|
|
1405
|
+
|
|
1406
|
+
await this.canvasService.applyObjectSpecsToRootLayer(
|
|
1407
|
+
WHITE_INK_COVER_LAYER_ID,
|
|
1408
|
+
coverSpecs,
|
|
1409
|
+
);
|
|
1410
|
+
if (seq !== this.renderSeq) return;
|
|
1411
|
+
|
|
1412
|
+
await this.canvasService.applyObjectSpecsToRootLayer(
|
|
1413
|
+
WHITE_INK_OVERLAY_LAYER_ID,
|
|
1414
|
+
frameSpecs,
|
|
1415
|
+
);
|
|
1416
|
+
if (seq !== this.renderSeq) return;
|
|
1417
|
+
|
|
1418
|
+
this.syncZOrder();
|
|
1419
|
+
this.canvasService.requestRenderAll();
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
private getMaskCacheKey(sourceUrl: string, tint: MaskTint): string {
|
|
1423
|
+
return `${sourceUrl}::${tint.key}`;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
private async getPreviewMaskSource(
|
|
1427
|
+
sourceUrl: string,
|
|
1428
|
+
tint: MaskTint = WHITE_MASK_TINT,
|
|
1429
|
+
fallbackElement?: any,
|
|
1430
|
+
): Promise<string> {
|
|
1431
|
+
if (!sourceUrl) return "";
|
|
1432
|
+
if (typeof document === "undefined" || typeof Image === "undefined") {
|
|
1433
|
+
return "";
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const cacheKey = this.getMaskCacheKey(sourceUrl, tint);
|
|
1437
|
+
const cached = this.previewMaskBySource.get(cacheKey);
|
|
1438
|
+
if (cached) return cached;
|
|
1439
|
+
|
|
1440
|
+
const pending = this.pendingPreviewMaskBySource.get(cacheKey);
|
|
1441
|
+
if (pending) {
|
|
1442
|
+
const loaded = await pending;
|
|
1443
|
+
return loaded || "";
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const task = this.createOpaqueMaskSource(sourceUrl, tint, fallbackElement);
|
|
1447
|
+
this.pendingPreviewMaskBySource.set(cacheKey, task);
|
|
1448
|
+
const loaded = await task;
|
|
1449
|
+
this.pendingPreviewMaskBySource.delete(cacheKey);
|
|
1450
|
+
|
|
1451
|
+
if (!loaded) return "";
|
|
1452
|
+
this.previewMaskBySource.set(cacheKey, loaded);
|
|
1453
|
+
return loaded;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
private getElementSize(
|
|
1457
|
+
element: any,
|
|
1458
|
+
): { width: number; height: number } | null {
|
|
1459
|
+
if (!element) return null;
|
|
1460
|
+
|
|
1461
|
+
const width = Number(
|
|
1462
|
+
element?.naturalWidth || element?.videoWidth || element?.width || 0,
|
|
1463
|
+
);
|
|
1464
|
+
const height = Number(
|
|
1465
|
+
element?.naturalHeight || element?.videoHeight || element?.height || 0,
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
1469
|
+
if (width <= 0 || height <= 0) return null;
|
|
1470
|
+
|
|
1471
|
+
return { width, height };
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
private async createOpaqueMaskSource(
|
|
1475
|
+
sourceUrl: string,
|
|
1476
|
+
tint: MaskTint = WHITE_MASK_TINT,
|
|
1477
|
+
fallbackElement?: any,
|
|
1478
|
+
): Promise<string | null> {
|
|
1479
|
+
try {
|
|
1480
|
+
const element =
|
|
1481
|
+
fallbackElement || (await this.loadImageElement(sourceUrl));
|
|
1482
|
+
const size = this.getElementSize(element);
|
|
1483
|
+
if (!size) return null;
|
|
1484
|
+
const width = Math.max(1, size.width);
|
|
1485
|
+
const height = Math.max(1, size.height);
|
|
1486
|
+
|
|
1487
|
+
this.rememberSourceSize(sourceUrl, { width, height });
|
|
1488
|
+
|
|
1489
|
+
const canvas = document.createElement("canvas");
|
|
1490
|
+
canvas.width = width;
|
|
1491
|
+
canvas.height = height;
|
|
1492
|
+
const ctx = canvas.getContext("2d");
|
|
1493
|
+
if (!ctx) return null;
|
|
1494
|
+
|
|
1495
|
+
ctx.drawImage(element, 0, 0, width, height);
|
|
1496
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1497
|
+
const data = imageData.data;
|
|
1498
|
+
|
|
1499
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1500
|
+
const alpha = data[i + 3];
|
|
1501
|
+
data[i] = tint.r;
|
|
1502
|
+
data[i + 1] = tint.g;
|
|
1503
|
+
data[i + 2] = tint.b;
|
|
1504
|
+
data[i + 3] = alpha;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1508
|
+
return canvas.toDataURL("image/png");
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
this.debug("mask:extract:failed", { sourceUrl, tint: tint.key, error });
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
private loadImageElement(sourceUrl: string): Promise<HTMLImageElement> {
|
|
1516
|
+
return new Promise((resolve, reject) => {
|
|
1517
|
+
const image = new Image();
|
|
1518
|
+
image.crossOrigin = "anonymous";
|
|
1519
|
+
image.onload = () => resolve(image);
|
|
1520
|
+
image.onerror = () => reject(new Error("white-ink-image-load-failed"));
|
|
1521
|
+
image.src = sourceUrl;
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|