@pooder/kit 4.3.1 → 5.0.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 (60) hide show
  1. package/.test-dist/src/CanvasService.js +249 -0
  2. package/.test-dist/src/ViewportSystem.js +75 -0
  3. package/.test-dist/src/background.js +203 -0
  4. package/.test-dist/src/bridgeSelection.js +20 -0
  5. package/.test-dist/src/constraints.js +237 -0
  6. package/.test-dist/src/coordinate.js +74 -0
  7. package/.test-dist/src/dieline.js +723 -0
  8. package/.test-dist/src/edgeScale.js +12 -0
  9. package/.test-dist/src/feature.js +752 -0
  10. package/.test-dist/src/featureComplete.js +32 -0
  11. package/.test-dist/src/film.js +167 -0
  12. package/.test-dist/src/geometry.js +506 -0
  13. package/.test-dist/src/image.js +1234 -0
  14. package/.test-dist/src/index.js +35 -0
  15. package/.test-dist/src/maskOps.js +270 -0
  16. package/.test-dist/src/mirror.js +104 -0
  17. package/.test-dist/src/renderSpec.js +2 -0
  18. package/.test-dist/src/ruler.js +343 -0
  19. package/.test-dist/src/sceneLayout.js +99 -0
  20. package/.test-dist/src/sceneLayoutModel.js +196 -0
  21. package/.test-dist/src/sceneView.js +40 -0
  22. package/.test-dist/src/sceneVisibility.js +42 -0
  23. package/.test-dist/src/size.js +332 -0
  24. package/.test-dist/src/tracer.js +544 -0
  25. package/.test-dist/src/units.js +30 -0
  26. package/.test-dist/src/white-ink.js +829 -0
  27. package/.test-dist/src/wrappedOffsets.js +33 -0
  28. package/.test-dist/tests/run.js +94 -0
  29. package/CHANGELOG.md +11 -0
  30. package/dist/index.d.mts +339 -36
  31. package/dist/index.d.ts +339 -36
  32. package/dist/index.js +3572 -850
  33. package/dist/index.mjs +3565 -852
  34. package/package.json +2 -2
  35. package/src/CanvasService.ts +300 -96
  36. package/src/ViewportSystem.ts +92 -92
  37. package/src/background.ts +230 -230
  38. package/src/bridgeSelection.ts +17 -0
  39. package/src/coordinate.ts +106 -106
  40. package/src/dieline.ts +897 -973
  41. package/src/edgeScale.ts +19 -0
  42. package/src/feature.ts +83 -30
  43. package/src/film.ts +194 -194
  44. package/src/geometry.ts +242 -84
  45. package/src/image.ts +1582 -512
  46. package/src/index.ts +14 -10
  47. package/src/maskOps.ts +326 -0
  48. package/src/mirror.ts +128 -128
  49. package/src/renderSpec.ts +18 -0
  50. package/src/ruler.ts +449 -508
  51. package/src/sceneLayout.ts +121 -0
  52. package/src/sceneLayoutModel.ts +335 -0
  53. package/src/sceneVisibility.ts +49 -0
  54. package/src/size.ts +379 -0
  55. package/src/tracer.ts +719 -570
  56. package/src/units.ts +27 -27
  57. package/src/white-ink.ts +1018 -373
  58. package/src/wrappedOffsets.ts +33 -0
  59. package/tests/run.ts +118 -0
  60. package/tsconfig.test.json +15 -15
@@ -0,0 +1,829 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WhiteInkTool = void 0;
4
+ const core_1 = require("@pooder/core");
5
+ const fabric_1 = require("fabric");
6
+ const sceneLayoutModel_1 = require("./sceneLayoutModel");
7
+ const WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
8
+ const IMAGE_OBJECT_LAYER_ID = "image.user";
9
+ const WHITE_INK_DEBUG_KEY = "whiteInk.debug";
10
+ const WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
11
+ const WHITE_INK_DEFAULT_OPACITY = 0.85;
12
+ class WhiteInkTool {
13
+ constructor() {
14
+ this.id = "pooder.kit.white-ink";
15
+ this.metadata = {
16
+ name: "WhiteInkTool",
17
+ };
18
+ this.items = [];
19
+ this.workingItems = [];
20
+ this.hasWorkingChanges = false;
21
+ this.sourceSizeBySrc = new Map();
22
+ this.previewMaskBySource = new Map();
23
+ this.pendingPreviewMaskBySource = new Map();
24
+ this.isUpdatingConfig = false;
25
+ this.isToolActive = false;
26
+ this.printWithWhiteInk = true;
27
+ this.previewImageVisible = true;
28
+ this.renderSeq = 0;
29
+ this.onToolActivated = (event) => {
30
+ const before = this.isToolActive;
31
+ this.syncToolActiveFromWorkbench(event.id);
32
+ this.debug("tool:activated", {
33
+ id: event.id,
34
+ previous: event.previous,
35
+ before,
36
+ isToolActive: this.isToolActive,
37
+ });
38
+ this.updateWhiteInks();
39
+ };
40
+ this.onSceneLayoutChanged = () => {
41
+ this.updateWhiteInks();
42
+ };
43
+ this.onObjectAdded = () => {
44
+ this.applyImagePreviewVisibility(this.isPreviewActive());
45
+ };
46
+ }
47
+ activate(context) {
48
+ this.context = context;
49
+ this.canvasService = context.services.get("CanvasService");
50
+ if (!this.canvasService) {
51
+ console.warn("CanvasService not found for WhiteInkTool");
52
+ return;
53
+ }
54
+ context.eventBus.on("tool:activated", this.onToolActivated);
55
+ context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
56
+ context.eventBus.on("object:added", this.onObjectAdded);
57
+ const configService = context.services.get("ConfigurationService");
58
+ if (configService) {
59
+ this.items = this.normalizeItems(configService.get("whiteInk.items", []) || []);
60
+ this.workingItems = this.cloneItems(this.items);
61
+ this.hasWorkingChanges = false;
62
+ this.printWithWhiteInk = !!configService.get("whiteInk.printWithWhiteInk", true);
63
+ this.previewImageVisible = !!configService.get(WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY, true);
64
+ this.migrateLegacyConfigIfNeeded(configService);
65
+ configService.onAnyChange((e) => {
66
+ if (this.isUpdatingConfig)
67
+ return;
68
+ if (e.key === "whiteInk.items") {
69
+ this.items = this.normalizeItems(e.value || []);
70
+ if (!this.isToolActive || !this.hasWorkingChanges) {
71
+ this.workingItems = this.cloneItems(this.items);
72
+ this.hasWorkingChanges = false;
73
+ }
74
+ this.updateWhiteInks();
75
+ return;
76
+ }
77
+ if (e.key === "whiteInk.printWithWhiteInk") {
78
+ this.printWithWhiteInk = !!e.value;
79
+ this.updateWhiteInks();
80
+ return;
81
+ }
82
+ if (e.key === WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY) {
83
+ this.previewImageVisible = !!e.value;
84
+ this.updateWhiteInks();
85
+ return;
86
+ }
87
+ if (e.key === WHITE_INK_DEBUG_KEY) {
88
+ return;
89
+ }
90
+ if (e.key.startsWith("size.")) {
91
+ this.updateWhiteInks();
92
+ }
93
+ });
94
+ }
95
+ const toolSessionService = context.services.get("ToolSessionService");
96
+ this.dirtyTrackerDisposable = toolSessionService?.registerDirtyTracker(this.id, () => this.hasWorkingChanges);
97
+ this.updateWhiteInks();
98
+ }
99
+ deactivate(context) {
100
+ context.eventBus.off("tool:activated", this.onToolActivated);
101
+ context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
102
+ context.eventBus.off("object:added", this.onObjectAdded);
103
+ this.dirtyTrackerDisposable?.dispose();
104
+ this.dirtyTrackerDisposable = undefined;
105
+ this.clearRenderedWhiteInks();
106
+ this.applyImagePreviewVisibility(false);
107
+ this.canvasService = undefined;
108
+ this.context = undefined;
109
+ }
110
+ contribute() {
111
+ return {
112
+ [core_1.ContributionPointIds.TOOLS]: [
113
+ {
114
+ id: this.id,
115
+ name: "White Ink",
116
+ interaction: "session",
117
+ commands: {
118
+ begin: "resetWorkingWhiteInks",
119
+ commit: "completeWhiteInks",
120
+ rollback: "resetWorkingWhiteInks",
121
+ },
122
+ session: {
123
+ autoBegin: true,
124
+ leavePolicy: "block",
125
+ },
126
+ },
127
+ ],
128
+ [core_1.ContributionPointIds.CONFIGURATIONS]: [
129
+ {
130
+ id: "whiteInk.items",
131
+ type: "array",
132
+ label: "White Ink Images",
133
+ default: [],
134
+ },
135
+ {
136
+ id: "whiteInk.printWithWhiteInk",
137
+ type: "boolean",
138
+ label: "Preview White Ink",
139
+ default: true,
140
+ },
141
+ {
142
+ id: WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
143
+ type: "boolean",
144
+ label: "Show Image During White Ink Preview",
145
+ default: true,
146
+ },
147
+ {
148
+ id: WHITE_INK_DEBUG_KEY,
149
+ type: "boolean",
150
+ label: "White Ink Debug Log",
151
+ default: false,
152
+ },
153
+ ],
154
+ [core_1.ContributionPointIds.COMMANDS]: [
155
+ {
156
+ command: "addWhiteInk",
157
+ title: "Add White Ink",
158
+ handler: async (url, options) => {
159
+ return await this.addWhiteInkEntry(url, options);
160
+ },
161
+ },
162
+ {
163
+ command: "upsertWhiteInk",
164
+ title: "Upsert White Ink",
165
+ handler: async (url, options = {}) => {
166
+ return await this.upsertWhiteInkEntry(url, options);
167
+ },
168
+ },
169
+ {
170
+ command: "getWhiteInks",
171
+ title: "Get White Inks",
172
+ handler: () => this.cloneItems(this.items),
173
+ },
174
+ {
175
+ command: "getWhiteInkSettings",
176
+ title: "Get White Ink Settings",
177
+ handler: () => {
178
+ const first = this.items[0] || null;
179
+ const sourceUrl = this.resolveSourceUrl(first);
180
+ return {
181
+ id: first?.id || null,
182
+ url: sourceUrl,
183
+ sourceUrl,
184
+ opacity: first?.opacity ?? WHITE_INK_DEFAULT_OPACITY,
185
+ printWithWhiteInk: this.printWithWhiteInk,
186
+ previewImageVisible: this.previewImageVisible,
187
+ };
188
+ },
189
+ },
190
+ {
191
+ command: "setWhiteInkPrintEnabled",
192
+ title: "Set White Ink Preview Enabled",
193
+ handler: (enabled) => {
194
+ this.printWithWhiteInk = !!enabled;
195
+ const configService = this.context?.services.get("ConfigurationService");
196
+ configService?.update("whiteInk.printWithWhiteInk", this.printWithWhiteInk);
197
+ this.updateWhiteInks();
198
+ return { ok: true };
199
+ },
200
+ },
201
+ {
202
+ command: "setWhiteInkPreviewImageVisible",
203
+ title: "Set White Ink Preview Image Visible",
204
+ handler: (visible) => {
205
+ this.previewImageVisible = !!visible;
206
+ const configService = this.context?.services.get("ConfigurationService");
207
+ configService?.update(WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY, this.previewImageVisible);
208
+ this.updateWhiteInks();
209
+ return { ok: true };
210
+ },
211
+ },
212
+ {
213
+ command: "setWhiteInkOpacity",
214
+ title: "Set White Ink Opacity",
215
+ handler: async (opacity) => {
216
+ const targetId = this.resolveReplaceTargetId(null);
217
+ if (!targetId) {
218
+ return { ok: false, reason: "no-white-ink-item" };
219
+ }
220
+ const nextOpacity = this.clampOpacity(opacity);
221
+ await this.updateWhiteInkItem(targetId, { opacity: nextOpacity }, { target: "config" });
222
+ return { ok: true, id: targetId, opacity: nextOpacity };
223
+ },
224
+ },
225
+ {
226
+ command: "getWorkingWhiteInks",
227
+ title: "Get Working White Inks",
228
+ handler: () => this.cloneItems(this.workingItems),
229
+ },
230
+ {
231
+ command: "setWorkingWhiteInk",
232
+ title: "Set Working White Ink",
233
+ handler: (id, updates) => {
234
+ this.updateWhiteInkInWorking(id, updates);
235
+ },
236
+ },
237
+ {
238
+ command: "updateWhiteInk",
239
+ title: "Update White Ink",
240
+ handler: async (id, updates, options = {}) => {
241
+ await this.updateWhiteInkItem(id, updates, options);
242
+ },
243
+ },
244
+ {
245
+ command: "removeWhiteInk",
246
+ title: "Remove White Ink",
247
+ handler: (id) => {
248
+ this.removeWhiteInk(id);
249
+ },
250
+ },
251
+ {
252
+ command: "clearWhiteInks",
253
+ title: "Clear White Inks",
254
+ handler: () => {
255
+ this.clearWhiteInks();
256
+ },
257
+ },
258
+ {
259
+ command: "resetWorkingWhiteInks",
260
+ title: "Reset Working White Inks",
261
+ handler: () => {
262
+ this.workingItems = this.cloneItems(this.items);
263
+ this.hasWorkingChanges = false;
264
+ this.updateWhiteInks();
265
+ },
266
+ },
267
+ {
268
+ command: "completeWhiteInks",
269
+ title: "Complete White Inks",
270
+ handler: async () => {
271
+ return await this.completeWhiteInks();
272
+ },
273
+ },
274
+ {
275
+ command: "setWhiteInkImage",
276
+ title: "Set White Ink Image",
277
+ handler: async (url, opacity) => {
278
+ if (!url) {
279
+ this.clearWhiteInks();
280
+ return { ok: true };
281
+ }
282
+ const resolvedOpacity = Number.isFinite(opacity)
283
+ ? this.clampOpacity(Number(opacity))
284
+ : WHITE_INK_DEFAULT_OPACITY;
285
+ const targetId = this.resolveReplaceTargetId(null);
286
+ const upsertResult = await this.upsertWhiteInkEntry(url, {
287
+ id: targetId || undefined,
288
+ mode: targetId ? "replace" : "add",
289
+ createIfMissing: true,
290
+ addOptions: {
291
+ opacity: resolvedOpacity,
292
+ },
293
+ });
294
+ await this.updateWhiteInkItem(upsertResult.id, { opacity: resolvedOpacity }, { target: "config" });
295
+ return { ok: true, id: upsertResult.id };
296
+ },
297
+ },
298
+ ],
299
+ };
300
+ }
301
+ migrateLegacyConfigIfNeeded(configService) {
302
+ if (this.items.length > 0)
303
+ return;
304
+ const legacyMask = configService.get("whiteInk.customMask", "");
305
+ if (typeof legacyMask !== "string" || legacyMask.length === 0)
306
+ return;
307
+ const legacyOpacityRaw = configService.get("whiteInk.opacity", WHITE_INK_DEFAULT_OPACITY);
308
+ const legacyOpacity = Number(legacyOpacityRaw);
309
+ const item = this.normalizeItem({
310
+ id: this.generateId(),
311
+ sourceUrl: legacyMask,
312
+ opacity: Number.isFinite(legacyOpacity)
313
+ ? legacyOpacity
314
+ : WHITE_INK_DEFAULT_OPACITY,
315
+ });
316
+ this.items = [item];
317
+ this.workingItems = this.cloneItems(this.items);
318
+ this.isUpdatingConfig = true;
319
+ configService.update("whiteInk.items", this.items);
320
+ setTimeout(() => {
321
+ this.isUpdatingConfig = false;
322
+ }, 0);
323
+ }
324
+ syncToolActiveFromWorkbench(fallbackId) {
325
+ const wb = this.context?.services.get("WorkbenchService");
326
+ const activeId = wb?.activeToolId;
327
+ if (typeof activeId === "string" || activeId === null) {
328
+ this.isToolActive = activeId === this.id;
329
+ return;
330
+ }
331
+ this.isToolActive = fallbackId === this.id;
332
+ }
333
+ isPreviewActive() {
334
+ return this.isToolActive && this.printWithWhiteInk;
335
+ }
336
+ isDebugEnabled() {
337
+ return !!this.getConfig(WHITE_INK_DEBUG_KEY, false);
338
+ }
339
+ debug(message, payload) {
340
+ if (!this.isDebugEnabled())
341
+ return;
342
+ if (payload === undefined) {
343
+ console.log(`[WhiteInkTool] ${message}`);
344
+ return;
345
+ }
346
+ console.log(`[WhiteInkTool] ${message}`, payload);
347
+ }
348
+ resolveSourceUrl(item) {
349
+ if (!item)
350
+ return "";
351
+ if (typeof item.sourceUrl === "string" && item.sourceUrl.length > 0) {
352
+ return item.sourceUrl;
353
+ }
354
+ if (typeof item.url === "string" && item.url.length > 0) {
355
+ return item.url;
356
+ }
357
+ return "";
358
+ }
359
+ clampOpacity(value) {
360
+ if (!Number.isFinite(value))
361
+ return WHITE_INK_DEFAULT_OPACITY;
362
+ return Math.max(0, Math.min(1, Number(value)));
363
+ }
364
+ normalizeItem(item) {
365
+ const sourceUrl = this.resolveSourceUrl(item);
366
+ return {
367
+ id: String(item.id || this.generateId()),
368
+ sourceUrl,
369
+ url: sourceUrl,
370
+ opacity: this.clampOpacity(item.opacity),
371
+ };
372
+ }
373
+ normalizeItems(items) {
374
+ return (items || [])
375
+ .map((item) => this.normalizeItem(item))
376
+ .filter((item) => !!this.resolveSourceUrl(item));
377
+ }
378
+ cloneItems(items) {
379
+ return this.normalizeItems((items || []).map((item) => ({ ...item })));
380
+ }
381
+ generateId() {
382
+ return `white-ink-${Math.random().toString(36).slice(2, 9)}`;
383
+ }
384
+ getConfig(key, fallback) {
385
+ if (!this.context)
386
+ return fallback;
387
+ const configService = this.context.services.get("ConfigurationService");
388
+ if (!configService)
389
+ return fallback;
390
+ return configService.get(key, fallback) ?? fallback;
391
+ }
392
+ resolveReplaceTargetId(explicitId) {
393
+ const has = (id) => !!id && this.items.some((item) => item.id === id);
394
+ if (has(explicitId))
395
+ return explicitId;
396
+ if (this.items.length >= 1) {
397
+ return this.items[0].id;
398
+ }
399
+ return null;
400
+ }
401
+ updateConfig(newItems, skipCanvasUpdate = false) {
402
+ if (!this.context)
403
+ return;
404
+ this.isUpdatingConfig = true;
405
+ this.items = this.normalizeItems(newItems);
406
+ if (!this.isToolActive || !this.hasWorkingChanges) {
407
+ this.workingItems = this.cloneItems(this.items);
408
+ this.hasWorkingChanges = false;
409
+ }
410
+ const configService = this.context.services.get("ConfigurationService");
411
+ configService?.update("whiteInk.items", this.items);
412
+ if (!skipCanvasUpdate) {
413
+ this.updateWhiteInks();
414
+ }
415
+ setTimeout(() => {
416
+ this.isUpdatingConfig = false;
417
+ }, 50);
418
+ }
419
+ async addWhiteInkEntry(url, options) {
420
+ const id = this.generateId();
421
+ const item = this.normalizeItem({
422
+ id,
423
+ sourceUrl: url,
424
+ opacity: WHITE_INK_DEFAULT_OPACITY,
425
+ ...options,
426
+ });
427
+ const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
428
+ this.updateConfig([...this.items, item]);
429
+ this.addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd);
430
+ return id;
431
+ }
432
+ async upsertWhiteInkEntry(url, options = {}) {
433
+ const mode = options.mode || "auto";
434
+ if (mode === "add") {
435
+ const id = await this.addWhiteInkEntry(url, options.addOptions);
436
+ return { id, mode: "add" };
437
+ }
438
+ const targetId = this.resolveReplaceTargetId(options.id ?? null);
439
+ if (targetId) {
440
+ this.updateWhiteInkInConfig(targetId, {
441
+ ...(options.addOptions || {}),
442
+ sourceUrl: url,
443
+ url,
444
+ });
445
+ return { id: targetId, mode: "replace" };
446
+ }
447
+ if (mode === "replace" || options.createIfMissing === false) {
448
+ throw new Error("replace-target-not-found");
449
+ }
450
+ const id = await this.addWhiteInkEntry(url, options.addOptions);
451
+ return { id, mode: "add" };
452
+ }
453
+ addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd) {
454
+ if (!sessionDirtyBeforeAdd || !this.isToolActive)
455
+ return;
456
+ if (this.workingItems.some((existing) => existing.id === item.id))
457
+ return;
458
+ this.workingItems = this.cloneItems([...this.workingItems, item]);
459
+ this.updateWhiteInks();
460
+ }
461
+ async updateWhiteInkItem(id, updates, options = {}) {
462
+ this.syncToolActiveFromWorkbench();
463
+ const target = options.target || "auto";
464
+ if (target === "working" || (target === "auto" && this.isToolActive)) {
465
+ this.updateWhiteInkInWorking(id, updates);
466
+ return;
467
+ }
468
+ this.updateWhiteInkInConfig(id, updates);
469
+ }
470
+ updateWhiteInkInWorking(id, updates) {
471
+ let changed = false;
472
+ const next = this.workingItems.map((item) => {
473
+ if (item.id !== id)
474
+ return item;
475
+ changed = true;
476
+ return this.normalizeItem({
477
+ ...item,
478
+ ...updates,
479
+ });
480
+ });
481
+ if (!changed)
482
+ return;
483
+ this.workingItems = this.cloneItems(next);
484
+ this.hasWorkingChanges = true;
485
+ this.updateWhiteInks();
486
+ }
487
+ updateWhiteInkInConfig(id, updates) {
488
+ let changed = false;
489
+ const next = this.items.map((item) => {
490
+ if (item.id !== id)
491
+ return item;
492
+ changed = true;
493
+ const merged = this.normalizeItem({
494
+ ...item,
495
+ ...updates,
496
+ });
497
+ if (this.resolveSourceUrl(item) !== this.resolveSourceUrl(merged)) {
498
+ this.purgeSourceCaches(item);
499
+ }
500
+ return merged;
501
+ });
502
+ if (!changed)
503
+ return;
504
+ this.updateConfig(next);
505
+ }
506
+ removeWhiteInk(id) {
507
+ const removed = this.items.find((item) => item.id === id);
508
+ const next = this.items.filter((item) => item.id !== id);
509
+ if (next.length === this.items.length)
510
+ return;
511
+ this.purgeSourceCaches(removed);
512
+ this.updateConfig(next);
513
+ }
514
+ clearWhiteInks() {
515
+ this.sourceSizeBySrc.clear();
516
+ this.previewMaskBySource.clear();
517
+ this.pendingPreviewMaskBySource.clear();
518
+ this.updateConfig([]);
519
+ }
520
+ async completeWhiteInks() {
521
+ this.updateConfig(this.cloneItems(this.workingItems));
522
+ this.hasWorkingChanges = false;
523
+ return { ok: true };
524
+ }
525
+ getFrameRect() {
526
+ if (!this.canvasService) {
527
+ return { left: 0, top: 0, width: 0, height: 0 };
528
+ }
529
+ const configService = this.context?.services.get("ConfigurationService");
530
+ if (!configService) {
531
+ return { left: 0, top: 0, width: 0, height: 0 };
532
+ }
533
+ const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
534
+ const layout = (0, sceneLayoutModel_1.computeSceneLayout)(this.canvasService, sizeState);
535
+ if (!layout) {
536
+ return { left: 0, top: 0, width: 0, height: 0 };
537
+ }
538
+ return {
539
+ left: layout.cutRect.left,
540
+ top: layout.cutRect.top,
541
+ width: layout.cutRect.width,
542
+ height: layout.cutRect.height,
543
+ };
544
+ }
545
+ getWhiteInkObjects() {
546
+ if (!this.canvasService)
547
+ return [];
548
+ return this.canvasService.canvas.getObjects().filter((obj) => {
549
+ return obj?.data?.layerId === WHITE_INK_OBJECT_LAYER_ID;
550
+ });
551
+ }
552
+ getWhiteInkObject(id) {
553
+ return this.getWhiteInkObjects().find((obj) => obj?.data?.id === id);
554
+ }
555
+ clearRenderedWhiteInks() {
556
+ if (!this.canvasService)
557
+ return;
558
+ const canvas = this.canvasService.canvas;
559
+ this.getWhiteInkObjects().forEach((obj) => canvas.remove(obj));
560
+ this.canvasService.requestRenderAll();
561
+ }
562
+ purgeSourceCaches(item) {
563
+ const sourceUrl = this.resolveSourceUrl(item);
564
+ if (!sourceUrl)
565
+ return;
566
+ this.sourceSizeBySrc.delete(sourceUrl);
567
+ this.previewMaskBySource.delete(sourceUrl);
568
+ this.pendingPreviewMaskBySource.delete(sourceUrl);
569
+ }
570
+ rememberSourceSize(src, obj) {
571
+ const width = Number(obj?.width || 0);
572
+ const height = Number(obj?.height || 0);
573
+ if (src && width > 0 && height > 0) {
574
+ this.sourceSizeBySrc.set(src, { width, height });
575
+ }
576
+ }
577
+ getSourceSize(src, obj) {
578
+ const cached = src ? this.sourceSizeBySrc.get(src) : undefined;
579
+ if (cached)
580
+ return cached;
581
+ const width = Number(obj?.width || 0);
582
+ const height = Number(obj?.height || 0);
583
+ if (src && width > 0 && height > 0) {
584
+ const size = { width, height };
585
+ this.sourceSizeBySrc.set(src, size);
586
+ return size;
587
+ }
588
+ return { width: 1, height: 1 };
589
+ }
590
+ getCoverScale(frame, size) {
591
+ const sw = Math.max(1, size.width);
592
+ const sh = Math.max(1, size.height);
593
+ const fw = Math.max(1, frame.width);
594
+ const fh = Math.max(1, frame.height);
595
+ return Math.max(fw / sw, fh / sh);
596
+ }
597
+ resolveRenderState(item, src) {
598
+ return {
599
+ src,
600
+ opacity: this.clampOpacity(item.opacity),
601
+ };
602
+ }
603
+ computeCanvasProps(render, size, frame) {
604
+ const centerX = frame.left + frame.width / 2;
605
+ const centerY = frame.top + frame.height / 2;
606
+ const scale = this.getCoverScale(frame, size);
607
+ return {
608
+ left: centerX,
609
+ top: centerY,
610
+ scaleX: scale,
611
+ scaleY: scale,
612
+ angle: 0,
613
+ originX: "center",
614
+ originY: "center",
615
+ uniformScaling: true,
616
+ lockScalingFlip: true,
617
+ selectable: false,
618
+ evented: false,
619
+ hasControls: false,
620
+ hasBorders: false,
621
+ opacity: render.opacity,
622
+ excludeFromExport: true,
623
+ };
624
+ }
625
+ getCurrentSrc(obj) {
626
+ if (!obj)
627
+ return undefined;
628
+ if (typeof obj.getSrc === "function")
629
+ return obj.getSrc();
630
+ return obj?._originalElement?.src;
631
+ }
632
+ async upsertWhiteInkObject(item, frame, seq) {
633
+ if (!this.canvasService)
634
+ return;
635
+ const canvas = this.canvasService.canvas;
636
+ const sourceUrl = this.resolveSourceUrl(item);
637
+ if (!sourceUrl)
638
+ return;
639
+ const previewSrc = await this.getPreviewMaskSource(sourceUrl);
640
+ if (seq !== this.renderSeq)
641
+ return;
642
+ const render = this.resolveRenderState(item, previewSrc);
643
+ if (!render.src)
644
+ return;
645
+ let obj = this.getWhiteInkObject(item.id);
646
+ const currentSrc = this.getCurrentSrc(obj);
647
+ if (obj && currentSrc && currentSrc !== render.src) {
648
+ canvas.remove(obj);
649
+ obj = undefined;
650
+ }
651
+ if (!obj) {
652
+ const created = await fabric_1.Image.fromURL(render.src, {
653
+ crossOrigin: "anonymous",
654
+ });
655
+ if (seq !== this.renderSeq)
656
+ return;
657
+ created.set({
658
+ excludeFromExport: true,
659
+ data: {
660
+ id: item.id,
661
+ layerId: WHITE_INK_OBJECT_LAYER_ID,
662
+ type: "white-ink-item",
663
+ },
664
+ });
665
+ canvas.add(created);
666
+ obj = created;
667
+ }
668
+ this.rememberSourceSize(render.src, obj);
669
+ const sourceSize = this.getSourceSize(render.src, obj);
670
+ const props = this.computeCanvasProps(render, sourceSize, frame);
671
+ obj.set({
672
+ ...props,
673
+ data: {
674
+ ...(obj.data || {}),
675
+ id: item.id,
676
+ layerId: WHITE_INK_OBJECT_LAYER_ID,
677
+ type: "white-ink-item",
678
+ },
679
+ });
680
+ obj.setCoords();
681
+ }
682
+ syncZOrder(items) {
683
+ if (!this.canvasService)
684
+ return;
685
+ const canvas = this.canvasService.canvas;
686
+ const objects = canvas.getObjects();
687
+ let insertIndex = 0;
688
+ const imageIndexes = objects
689
+ .map((obj, index) => obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID ? index : -1)
690
+ .filter((index) => index >= 0);
691
+ if (imageIndexes.length > 0) {
692
+ insertIndex = Math.min(...imageIndexes);
693
+ }
694
+ else {
695
+ const backgroundLayer = this.canvasService.getLayer("background");
696
+ if (backgroundLayer) {
697
+ const bgIndex = objects.indexOf(backgroundLayer);
698
+ if (bgIndex >= 0)
699
+ insertIndex = bgIndex + 1;
700
+ }
701
+ }
702
+ items.forEach((item) => {
703
+ const obj = this.getWhiteInkObject(item.id);
704
+ if (!obj)
705
+ return;
706
+ canvas.moveObjectTo(obj, insertIndex);
707
+ insertIndex += 1;
708
+ });
709
+ canvas
710
+ .getObjects()
711
+ .filter((obj) => obj?.data?.layerId === "image-overlay")
712
+ .forEach((obj) => canvas.bringObjectToFront(obj));
713
+ const dielineOverlay = this.canvasService.getLayer("dieline-overlay");
714
+ if (dielineOverlay) {
715
+ canvas.bringObjectToFront(dielineOverlay);
716
+ }
717
+ }
718
+ applyImagePreviewVisibility(previewActive) {
719
+ if (!this.canvasService)
720
+ return;
721
+ const visible = previewActive ? this.previewImageVisible : true;
722
+ let changed = false;
723
+ this.canvasService.canvas.getObjects().forEach((obj) => {
724
+ if (obj?.data?.layerId !== IMAGE_OBJECT_LAYER_ID)
725
+ return;
726
+ if (obj.visible === visible)
727
+ return;
728
+ obj.set({ visible });
729
+ obj.setCoords?.();
730
+ changed = true;
731
+ });
732
+ if (changed) {
733
+ this.canvasService.requestRenderAll();
734
+ }
735
+ }
736
+ updateWhiteInks() {
737
+ void this.updateWhiteInksAsync();
738
+ }
739
+ async updateWhiteInksAsync() {
740
+ if (!this.canvasService)
741
+ return;
742
+ this.syncToolActiveFromWorkbench();
743
+ const seq = ++this.renderSeq;
744
+ const previewActive = this.isPreviewActive();
745
+ this.applyImagePreviewVisibility(previewActive);
746
+ const renderItems = previewActive ? this.workingItems : [];
747
+ const frame = this.getFrameRect();
748
+ const desiredIds = new Set(renderItems.map((item) => item.id));
749
+ this.getWhiteInkObjects().forEach((obj) => {
750
+ const id = obj?.data?.id;
751
+ if (typeof id === "string" && !desiredIds.has(id)) {
752
+ this.canvasService?.canvas.remove(obj);
753
+ }
754
+ });
755
+ for (const item of renderItems) {
756
+ if (seq !== this.renderSeq)
757
+ return;
758
+ await this.upsertWhiteInkObject(item, frame, seq);
759
+ }
760
+ if (seq !== this.renderSeq)
761
+ return;
762
+ this.syncZOrder(renderItems);
763
+ this.canvasService.requestRenderAll();
764
+ }
765
+ async getPreviewMaskSource(sourceUrl) {
766
+ if (!sourceUrl)
767
+ return "";
768
+ if (typeof document === "undefined" || typeof Image === "undefined") {
769
+ return sourceUrl;
770
+ }
771
+ const cached = this.previewMaskBySource.get(sourceUrl);
772
+ if (cached)
773
+ return cached;
774
+ const pending = this.pendingPreviewMaskBySource.get(sourceUrl);
775
+ if (pending) {
776
+ const loaded = await pending;
777
+ return loaded || sourceUrl;
778
+ }
779
+ const task = this.createOpaqueMaskSource(sourceUrl);
780
+ this.pendingPreviewMaskBySource.set(sourceUrl, task);
781
+ const loaded = await task;
782
+ this.pendingPreviewMaskBySource.delete(sourceUrl);
783
+ if (!loaded)
784
+ return sourceUrl;
785
+ this.previewMaskBySource.set(sourceUrl, loaded);
786
+ return loaded;
787
+ }
788
+ async createOpaqueMaskSource(sourceUrl) {
789
+ try {
790
+ const img = await this.loadImageElement(sourceUrl);
791
+ const width = Math.max(1, Number(img.naturalWidth || img.width || 0));
792
+ const height = Math.max(1, Number(img.naturalHeight || img.height || 0));
793
+ if (width <= 0 || height <= 0)
794
+ return null;
795
+ const canvas = document.createElement("canvas");
796
+ canvas.width = width;
797
+ canvas.height = height;
798
+ const ctx = canvas.getContext("2d");
799
+ if (!ctx)
800
+ return null;
801
+ ctx.drawImage(img, 0, 0, width, height);
802
+ const imageData = ctx.getImageData(0, 0, width, height);
803
+ const data = imageData.data;
804
+ for (let i = 0; i < data.length; i += 4) {
805
+ const alpha = data[i + 3];
806
+ data[i] = 255;
807
+ data[i + 1] = 255;
808
+ data[i + 2] = 255;
809
+ data[i + 3] = alpha;
810
+ }
811
+ ctx.putImageData(imageData, 0, 0);
812
+ return canvas.toDataURL("image/png");
813
+ }
814
+ catch (error) {
815
+ this.debug("mask:extract:failed", { sourceUrl, error });
816
+ return null;
817
+ }
818
+ }
819
+ loadImageElement(sourceUrl) {
820
+ return new Promise((resolve, reject) => {
821
+ const image = new Image();
822
+ image.crossOrigin = "anonymous";
823
+ image.onload = () => resolve(image);
824
+ image.onerror = () => reject(new Error("white-ink-image-load-failed"));
825
+ image.src = sourceUrl;
826
+ });
827
+ }
828
+ }
829
+ exports.WhiteInkTool = WhiteInkTool;