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