@pooder/kit 5.0.3 → 5.1.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 (34) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.d.mts +239 -269
  3. package/dist/index.d.ts +239 -269
  4. package/dist/index.js +6485 -5833
  5. package/dist/index.mjs +6587 -5923
  6. package/package.json +2 -2
  7. package/src/{background.ts → extensions/background.ts} +1 -1
  8. package/src/{dieline.ts → extensions/dieline.ts} +39 -17
  9. package/src/{feature.ts → extensions/feature.ts} +80 -67
  10. package/src/{film.ts → extensions/film.ts} +1 -1
  11. package/src/{geometry.ts → extensions/geometry.ts} +151 -105
  12. package/src/{image.ts → extensions/image.ts} +190 -192
  13. package/src/extensions/index.ts +11 -0
  14. package/src/{maskOps.ts → extensions/maskOps.ts} +28 -10
  15. package/src/{mirror.ts → extensions/mirror.ts} +1 -1
  16. package/src/{ruler.ts → extensions/ruler.ts} +5 -3
  17. package/src/extensions/sceneLayout.ts +140 -0
  18. package/src/{sceneLayoutModel.ts → extensions/sceneLayoutModel.ts} +17 -10
  19. package/src/extensions/sceneVisibility.ts +71 -0
  20. package/src/{size.ts → extensions/size.ts} +23 -13
  21. package/src/{tracer.ts → extensions/tracer.ts} +374 -45
  22. package/src/{white-ink.ts → extensions/white-ink.ts} +620 -236
  23. package/src/index.ts +2 -14
  24. package/src/{ViewportSystem.ts → services/ViewportSystem.ts} +5 -2
  25. package/src/services/index.ts +3 -0
  26. package/src/sceneLayout.ts +0 -121
  27. package/src/sceneVisibility.ts +0 -49
  28. /package/src/{bridgeSelection.ts → extensions/bridgeSelection.ts} +0 -0
  29. /package/src/{constraints.ts → extensions/constraints.ts} +0 -0
  30. /package/src/{edgeScale.ts → extensions/edgeScale.ts} +0 -0
  31. /package/src/{featureComplete.ts → extensions/featureComplete.ts} +0 -0
  32. /package/src/{wrappedOffsets.ts → extensions/wrappedOffsets.ts} +0 -0
  33. /package/src/{CanvasService.ts → services/CanvasService.ts} +0 -0
  34. /package/src/{renderSpec.ts → services/renderSpec.ts} +0 -0
@@ -8,8 +8,7 @@ import {
8
8
  ToolSessionService,
9
9
  WorkbenchService,
10
10
  } from "@pooder/core";
11
- import { Image as FabricImage } from "fabric";
12
- import CanvasService from "./CanvasService";
11
+ import { CanvasService, RenderObjectSpec } from "../services";
13
12
  import { computeSceneLayout, readSizeState } from "./sceneLayoutModel";
14
13
 
15
14
  export interface WhiteInkItem {
@@ -24,6 +23,13 @@ interface SourceSize {
24
23
  height: number;
25
24
  }
26
25
 
26
+ interface MaskTint {
27
+ r: number;
28
+ g: number;
29
+ b: number;
30
+ key: string;
31
+ }
32
+
27
33
  interface FrameRect {
28
34
  left: number;
29
35
  top: number;
@@ -31,9 +37,30 @@ interface FrameRect {
31
37
  height: number;
32
38
  }
33
39
 
34
- interface RenderWhiteInkState {
40
+ interface ImageSnapshot {
41
+ id: string;
35
42
  src: string;
36
- opacity: number;
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 RenderSources {
60
+ whiteSrc: string;
61
+ coverSrc: string;
62
+ whiteScaleAdjustX: number;
63
+ whiteScaleAdjustY: number;
37
64
  }
38
65
 
39
66
  interface UpsertWhiteInkOptions {
@@ -48,10 +75,21 @@ interface UpdateWhiteInkOptions {
48
75
  }
49
76
 
50
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";
51
80
  const IMAGE_OBJECT_LAYER_ID = "image.user";
81
+ const IMAGE_OVERLAY_LAYER_ID = "image-overlay";
82
+
52
83
  const WHITE_INK_DEBUG_KEY = "whiteInk.debug";
53
84
  const WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
54
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" };
55
93
 
56
94
  export class WhiteInkTool implements Extension {
57
95
  id = "pooder.kit.white-ink";
@@ -63,10 +101,12 @@ export class WhiteInkTool implements Extension {
63
101
  private items: WhiteInkItem[] = [];
64
102
  private workingItems: WhiteInkItem[] = [];
65
103
  private hasWorkingChanges = false;
104
+
66
105
  private sourceSizeBySrc: Map<string, SourceSize> = new Map();
67
106
  private previewMaskBySource: Map<string, string> = new Map();
68
107
  private pendingPreviewMaskBySource: Map<string, Promise<string | null>> =
69
108
  new Map();
109
+
70
110
  private canvasService?: CanvasService;
71
111
  private context?: ExtensionContext;
72
112
  private isUpdatingConfig = false;
@@ -87,6 +127,9 @@ export class WhiteInkTool implements Extension {
87
127
  context.eventBus.on("tool:activated", this.onToolActivated);
88
128
  context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
89
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);
90
133
 
91
134
  const configService = context.services.get<ConfigurationService>(
92
135
  "ConfigurationService",
@@ -133,6 +176,11 @@ export class WhiteInkTool implements Extension {
133
176
  return;
134
177
  }
135
178
 
179
+ if (e.key === "image.items") {
180
+ this.updateWhiteInks();
181
+ return;
182
+ }
183
+
136
184
  if (e.key === WHITE_INK_DEBUG_KEY) {
137
185
  return;
138
186
  }
@@ -157,11 +205,15 @@ export class WhiteInkTool implements Extension {
157
205
  context.eventBus.off("tool:activated", this.onToolActivated);
158
206
  context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
159
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);
160
211
 
161
212
  this.dirtyTrackerDisposable?.dispose();
162
213
  this.dirtyTrackerDisposable = undefined;
163
214
  this.clearRenderedWhiteInks();
164
- this.applyImagePreviewVisibility(false);
215
+ this.applyImageVisibilityForWhiteInk(false);
216
+
165
217
  this.canvasService = undefined;
166
218
  this.context = undefined;
167
219
  }
@@ -200,7 +252,7 @@ export class WhiteInkTool implements Extension {
200
252
  {
201
253
  id: WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
202
254
  type: "boolean",
203
- label: "Show Image During White Ink Preview",
255
+ label: "Show Cover During White Ink Preview",
204
256
  default: true,
205
257
  },
206
258
  {
@@ -234,13 +286,15 @@ export class WhiteInkTool implements Extension {
234
286
  command: "getWhiteInkSettings",
235
287
  title: "Get White Ink Settings",
236
288
  handler: () => {
237
- const first = this.items[0] || null;
238
- const sourceUrl = this.resolveSourceUrl(first);
289
+ const first = this.getEffectiveWhiteInkItem(this.items);
290
+ const primarySource = this.getPrimaryImageSource();
291
+ const sourceUrl = this.resolveSourceUrl(first) || primarySource;
292
+
239
293
  return {
240
294
  id: first?.id || null,
241
295
  url: sourceUrl,
242
296
  sourceUrl,
243
- opacity: first?.opacity ?? WHITE_INK_DEFAULT_OPACITY,
297
+ opacity: WHITE_INK_DEFAULT_OPACITY,
244
298
  printWithWhiteInk: this.printWithWhiteInk,
245
299
  previewImageVisible: this.previewImageVisible,
246
300
  };
@@ -265,7 +319,7 @@ export class WhiteInkTool implements Extension {
265
319
  },
266
320
  {
267
321
  command: "setWhiteInkPreviewImageVisible",
268
- title: "Set White Ink Preview Image Visible",
322
+ title: "Set White Ink Cover Visible",
269
323
  handler: (visible: boolean) => {
270
324
  this.previewImageVisible = !!visible;
271
325
  const configService =
@@ -280,23 +334,6 @@ export class WhiteInkTool implements Extension {
280
334
  return { ok: true };
281
335
  },
282
336
  },
283
- {
284
- command: "setWhiteInkOpacity",
285
- title: "Set White Ink Opacity",
286
- handler: async (opacity: number) => {
287
- const targetId = this.resolveReplaceTargetId(null);
288
- if (!targetId) {
289
- return { ok: false, reason: "no-white-ink-item" };
290
- }
291
- const nextOpacity = this.clampOpacity(opacity);
292
- await this.updateWhiteInkItem(
293
- targetId,
294
- { opacity: nextOpacity },
295
- { target: "config" },
296
- );
297
- return { ok: true, id: targetId, opacity: nextOpacity };
298
- },
299
- },
300
337
  {
301
338
  command: "getWorkingWhiteInks",
302
339
  title: "Get Working White Inks",
@@ -353,30 +390,19 @@ export class WhiteInkTool implements Extension {
353
390
  {
354
391
  command: "setWhiteInkImage",
355
392
  title: "Set White Ink Image",
356
- handler: async (url: string, opacity?: number) => {
393
+ handler: async (url: string) => {
357
394
  if (!url) {
358
395
  this.clearWhiteInks();
359
396
  return { ok: true };
360
397
  }
361
398
 
362
- const resolvedOpacity = Number.isFinite(opacity as any)
363
- ? this.clampOpacity(Number(opacity))
364
- : WHITE_INK_DEFAULT_OPACITY;
365
-
366
399
  const targetId = this.resolveReplaceTargetId(null);
367
400
  const upsertResult = await this.upsertWhiteInkEntry(url, {
368
401
  id: targetId || undefined,
369
402
  mode: targetId ? "replace" : "add",
370
403
  createIfMissing: true,
371
- addOptions: {
372
- opacity: resolvedOpacity,
373
- },
404
+ addOptions: {},
374
405
  });
375
- await this.updateWhiteInkItem(
376
- upsertResult.id,
377
- { opacity: resolvedOpacity },
378
- { target: "config" },
379
- );
380
406
  return { ok: true, id: upsertResult.id };
381
407
  },
382
408
  },
@@ -403,8 +429,26 @@ export class WhiteInkTool implements Extension {
403
429
  this.updateWhiteInks();
404
430
  };
405
431
 
406
- private onObjectAdded = () => {
407
- this.applyImagePreviewVisibility(this.isPreviewActive());
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();
408
452
  };
409
453
 
410
454
  private migrateLegacyConfigIfNeeded(configService: ConfigurationService) {
@@ -412,17 +456,10 @@ export class WhiteInkTool implements Extension {
412
456
  const legacyMask = configService.get("whiteInk.customMask", "");
413
457
  if (typeof legacyMask !== "string" || legacyMask.length === 0) return;
414
458
 
415
- const legacyOpacityRaw = configService.get(
416
- "whiteInk.opacity",
417
- WHITE_INK_DEFAULT_OPACITY,
418
- );
419
- const legacyOpacity = Number(legacyOpacityRaw);
420
459
  const item = this.normalizeItem({
421
460
  id: this.generateId(),
422
461
  sourceUrl: legacyMask,
423
- opacity: Number.isFinite(legacyOpacity)
424
- ? legacyOpacity
425
- : WHITE_INK_DEFAULT_OPACITY,
462
+ opacity: WHITE_INK_DEFAULT_OPACITY,
426
463
  });
427
464
 
428
465
  this.items = [item];
@@ -472,31 +509,42 @@ export class WhiteInkTool implements Extension {
472
509
  return "";
473
510
  }
474
511
 
475
- private clampOpacity(value: number): number {
476
- if (!Number.isFinite(value as any)) return WHITE_INK_DEFAULT_OPACITY;
477
- return Math.max(0, Math.min(1, Number(value)));
478
- }
479
-
480
512
  private normalizeItem(item: Partial<WhiteInkItem>): WhiteInkItem {
481
513
  const sourceUrl = this.resolveSourceUrl(item);
482
514
  return {
483
515
  id: String(item.id || this.generateId()),
484
516
  sourceUrl,
485
517
  url: sourceUrl,
486
- opacity: this.clampOpacity(item.opacity as number),
518
+ opacity: WHITE_INK_DEFAULT_OPACITY,
487
519
  };
488
520
  }
489
521
 
490
522
  private normalizeItems(items: WhiteInkItem[]): WhiteInkItem[] {
491
523
  return (items || [])
492
524
  .map((item) => this.normalizeItem(item))
493
- .filter((item) => !!this.resolveSourceUrl(item));
525
+ .filter((item) => !!item.id);
494
526
  }
495
527
 
496
528
  private cloneItems(items: WhiteInkItem[]): WhiteInkItem[] {
497
529
  return this.normalizeItems((items || []).map((item) => ({ ...item })));
498
530
  }
499
531
 
532
+ private getEffectiveWhiteInkItem(items: WhiteInkItem[]): WhiteInkItem | null {
533
+ const normalized = this.cloneItems(items || []);
534
+ if (normalized.length > 0) {
535
+ return normalized[0];
536
+ }
537
+
538
+ if (!this.getPrimaryImageSource()) {
539
+ return null;
540
+ }
541
+
542
+ return {
543
+ id: WHITE_INK_AUTO_ITEM_ID,
544
+ opacity: WHITE_INK_DEFAULT_OPACITY,
545
+ };
546
+ }
547
+
500
548
  private generateId(): string {
501
549
  return `white-ink-${Math.random().toString(36).slice(2, 9)}`;
502
550
  }
@@ -697,221 +745,477 @@ export class WhiteInkTool implements Extension {
697
745
  };
698
746
  }
699
747
 
700
- private getWhiteInkObjects(): any[] {
748
+ private getImageObjects(): any[] {
701
749
  if (!this.canvasService) return [];
702
750
  return this.canvasService.canvas.getObjects().filter((obj: any) => {
703
- return obj?.data?.layerId === WHITE_INK_OBJECT_LAYER_ID;
751
+ return obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID;
704
752
  }) as any[];
705
753
  }
706
754
 
707
- private getWhiteInkObject(id: string): any | undefined {
708
- return this.getWhiteInkObjects().find((obj: any) => obj?.data?.id === id);
755
+ private getPrimaryImageObject(): any | undefined {
756
+ return this.getImageObjects()[0];
709
757
  }
710
758
 
711
- private clearRenderedWhiteInks() {
712
- if (!this.canvasService) return;
713
- const canvas = this.canvasService.canvas;
714
- this.getWhiteInkObjects().forEach((obj) => canvas.remove(obj));
715
- this.canvasService.requestRenderAll();
759
+ private getPrimaryImageSource(): string {
760
+ return this.getCurrentSrc(this.getPrimaryImageObject()) || "";
716
761
  }
717
762
 
718
- private purgeSourceCaches(item?: WhiteInkItem) {
719
- const sourceUrl = this.resolveSourceUrl(item);
720
- if (!sourceUrl) return;
721
- this.sourceSizeBySrc.delete(sourceUrl);
722
- this.previewMaskBySource.delete(sourceUrl);
723
- this.pendingPreviewMaskBySource.delete(sourceUrl);
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;
724
767
  }
725
768
 
726
- private rememberSourceSize(src: string, obj: any) {
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);
727
776
  const width = Number(obj?.width || 0);
728
777
  const height = Number(obj?.height || 0);
729
- if (src && width > 0 && height > 0) {
730
- this.sourceSizeBySrc.set(src, { width, height });
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();
731
804
  }
805
+ return obj?._element || obj?._originalElement || null;
732
806
  }
733
807
 
734
- private getSourceSize(src: string, obj?: any): SourceSize {
735
- const cached = src ? this.sourceSizeBySrc.get(src) : undefined;
736
- if (cached) return cached;
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
+ }
737
817
 
738
- const width = Number(obj?.width || 0);
739
- const height = Number(obj?.height || 0);
740
- if (src && width > 0 && height > 0) {
741
- const size = { width, height };
742
- this.sourceSizeBySrc.set(src, size);
743
- return size;
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 };
744
844
  }
745
845
 
746
- return { width: 1, height: 1 };
846
+ return {
847
+ x: baseSize.width / whiteSize.width,
848
+ y: baseSize.height / whiteSize.height,
849
+ };
747
850
  }
748
851
 
749
- private getCoverScale(frame: FrameRect, size: SourceSize): number {
750
- const sw = Math.max(1, size.width);
751
- const sh = Math.max(1, size.height);
752
- const fw = Math.max(1, frame.width);
753
- const fh = Math.max(1, frame.height);
754
- return Math.max(fw / sw, fh / sh);
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
+ );
755
858
  }
756
859
 
757
- private resolveRenderState(
758
- item: WhiteInkItem,
860
+ private buildCloneImageSpec(
861
+ id: string,
862
+ snapshot: ImageSnapshot,
759
863
  src: string,
760
- ): RenderWhiteInkState {
864
+ opacity: number,
865
+ layerId: string,
866
+ type: "white-ink" | "white-ink-cover",
867
+ scaleAdjustX = 1,
868
+ scaleAdjustY = 1,
869
+ ): RenderObjectSpec {
761
870
  return {
871
+ id,
872
+ type: "image",
762
873
  src,
763
- opacity: this.clampOpacity(item.opacity),
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
+ },
764
901
  };
765
902
  }
766
903
 
767
- private computeCanvasProps(
768
- render: RenderWhiteInkState,
769
- size: SourceSize,
770
- frame: FrameRect,
771
- ) {
772
- const centerX = frame.left + frame.width / 2;
773
- const centerY = frame.top + frame.height / 2;
774
- const scale = this.getCoverScale(frame, size);
904
+ private buildFrameSpecs(frame: FrameRect): RenderObjectSpec[] {
905
+ if (!this.isToolActive || !this.canvasService) return [];
906
+ if (frame.width <= 0 || frame.height <= 0) return [];
775
907
 
776
- return {
777
- left: centerX,
778
- top: centerY,
779
- scaleX: scale,
780
- scaleY: scale,
781
- angle: 0,
782
- originX: "center" as const,
783
- originY: "center" as const,
784
- uniformScaling: true,
785
- lockScalingFlip: true,
786
- selectable: false,
787
- evented: false,
788
- hasControls: false,
789
- hasBorders: false,
790
- opacity: render.opacity,
791
- excludeFromExport: true,
792
- };
793
- }
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);
794
943
 
795
- private getCurrentSrc(obj: any): string | undefined {
796
- if (!obj) return undefined;
797
- if (typeof obj.getSrc === "function") return obj.getSrc();
798
- return obj?._originalElement?.src;
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
+ ];
799
1063
  }
800
1064
 
801
- private async upsertWhiteInkObject(
802
- item: WhiteInkItem,
803
- frame: FrameRect,
804
- seq: number,
805
- ) {
1065
+ private applyImageVisibilityForWhiteInk(previewActive: boolean) {
806
1066
  if (!this.canvasService) return;
807
- const canvas = this.canvasService.canvas;
808
- const sourceUrl = this.resolveSourceUrl(item);
809
- if (!sourceUrl) return;
810
-
811
- const previewSrc = await this.getPreviewMaskSource(sourceUrl);
812
- if (seq !== this.renderSeq) return;
1067
+ const visible = !previewActive;
1068
+ let changed = false;
813
1069
 
814
- const render = this.resolveRenderState(item, previewSrc);
815
- if (!render.src) return;
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
+ });
816
1077
 
817
- let obj = this.getWhiteInkObject(item.id);
818
- const currentSrc = this.getCurrentSrc(obj);
1078
+ if (changed) {
1079
+ this.canvasService.requestRenderAll();
1080
+ }
1081
+ }
819
1082
 
820
- if (obj && currentSrc && currentSrc !== render.src) {
821
- canvas.remove(obj);
822
- obj = undefined;
1083
+ private resolveRenderItems(): WhiteInkItem[] {
1084
+ if (this.isToolActive) {
1085
+ return this.cloneItems(this.workingItems);
823
1086
  }
1087
+ return this.cloneItems(this.items);
1088
+ }
824
1089
 
825
- if (!obj) {
826
- const created = await FabricImage.fromURL(render.src, {
827
- crossOrigin: "anonymous",
828
- });
829
- if (seq !== this.renderSeq) return;
1090
+ private async resolveRenderSources(
1091
+ snapshot: ImageSnapshot,
1092
+ item: WhiteInkItem,
1093
+ ): Promise<RenderSources | null> {
1094
+ const imageSource = snapshot.src;
1095
+ if (!imageSource) return null;
830
1096
 
831
- created.set({
832
- excludeFromExport: true,
833
- data: {
834
- id: item.id,
835
- layerId: WHITE_INK_OBJECT_LAYER_ID,
836
- type: "white-ink-item",
837
- },
838
- } as any);
839
- canvas.add(created as any);
840
- obj = created as any;
841
- }
1097
+ const whiteSource = this.resolveSourceUrl(item) || imageSource;
1098
+ const imageElement = snapshot.element;
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
+ ]);
842
1104
 
843
- this.rememberSourceSize(render.src, obj);
844
- const sourceSize = this.getSourceSize(render.src, obj);
845
- const props = this.computeCanvasProps(render, sourceSize, frame);
1105
+ const scaleAdjust = this.computeWhiteScaleAdjust(imageSource, whiteSource);
846
1106
 
847
- obj.set({
848
- ...props,
849
- data: {
850
- ...(obj.data || {}),
851
- id: item.id,
852
- layerId: WHITE_INK_OBJECT_LAYER_ID,
853
- type: "white-ink-item",
854
- },
855
- });
856
- obj.setCoords();
1107
+ return {
1108
+ whiteSrc: whiteMaskSrc || "",
1109
+ coverSrc: coverMaskSrc || "",
1110
+ whiteScaleAdjustX: scaleAdjust.x,
1111
+ whiteScaleAdjustY: scaleAdjust.y,
1112
+ };
1113
+ }
1114
+
1115
+ private resolveDefaultInsertIndex(objects: any[]): number {
1116
+ if (!this.canvasService) return 0;
1117
+ const backgroundLayer = this.canvasService.getLayer("background");
1118
+ if (!backgroundLayer) return 0;
1119
+ const bgIndex = objects.indexOf(backgroundLayer as any);
1120
+ if (bgIndex < 0) return 0;
1121
+ return bgIndex + 1;
857
1122
  }
858
1123
 
859
- private syncZOrder(items: WhiteInkItem[]) {
1124
+ private syncZOrder() {
860
1125
  if (!this.canvasService) return;
861
1126
  const canvas = this.canvasService.canvas;
862
- const objects = canvas.getObjects();
863
- let insertIndex = 0;
864
1127
 
865
- const imageIndexes = objects
1128
+ const whiteObjects = this.canvasService.getRootLayerObjects(
1129
+ WHITE_INK_OBJECT_LAYER_ID,
1130
+ ) as any[];
1131
+ const coverObjects = this.canvasService.getRootLayerObjects(
1132
+ WHITE_INK_COVER_LAYER_ID,
1133
+ ) as any[];
1134
+ const frameObjects = this.canvasService.getRootLayerObjects(
1135
+ WHITE_INK_OVERLAY_LAYER_ID,
1136
+ ) as any[];
1137
+
1138
+ const currentObjects = canvas.getObjects();
1139
+ const imageIndexes = currentObjects
866
1140
  .map((obj: any, index: number) =>
867
1141
  obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID ? index : -1,
868
1142
  )
869
1143
  .filter((index: number) => index >= 0);
870
1144
 
871
- if (imageIndexes.length > 0) {
872
- insertIndex = Math.min(...imageIndexes);
873
- } else {
874
- const backgroundLayer = this.canvasService.getLayer("background");
875
- if (backgroundLayer) {
876
- const bgIndex = objects.indexOf(backgroundLayer as any);
877
- if (bgIndex >= 0) insertIndex = bgIndex + 1;
878
- }
879
- }
1145
+ let whiteInsertIndex = imageIndexes.length
1146
+ ? Math.min(...imageIndexes)
1147
+ : this.resolveDefaultInsertIndex(currentObjects);
1148
+
1149
+ whiteObjects.forEach((obj) => {
1150
+ canvas.moveObjectTo(obj, whiteInsertIndex);
1151
+ whiteInsertIndex += 1;
1152
+ });
1153
+
1154
+ const afterWhiteObjects = canvas.getObjects();
1155
+ const afterImageIndexes = afterWhiteObjects
1156
+ .map((obj: any, index: number) =>
1157
+ obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID ? index : -1,
1158
+ )
1159
+ .filter((index: number) => index >= 0);
1160
+
1161
+ let coverInsertIndex = afterImageIndexes.length
1162
+ ? Math.max(...afterImageIndexes) + 1
1163
+ : whiteInsertIndex;
880
1164
 
881
- items.forEach((item) => {
882
- const obj = this.getWhiteInkObject(item.id);
883
- if (!obj) return;
884
- canvas.moveObjectTo(obj, insertIndex);
885
- insertIndex += 1;
1165
+ coverObjects.forEach((obj) => {
1166
+ canvas.moveObjectTo(obj, coverInsertIndex);
1167
+ coverInsertIndex += 1;
886
1168
  });
887
1169
 
1170
+ frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
1171
+
888
1172
  canvas
889
1173
  .getObjects()
890
- .filter((obj: any) => obj?.data?.layerId === "image-overlay")
1174
+ .filter((obj: any) => obj?.data?.layerId === IMAGE_OVERLAY_LAYER_ID)
891
1175
  .forEach((obj: any) => canvas.bringObjectToFront(obj));
892
1176
 
893
1177
  const dielineOverlay = this.canvasService.getLayer("dieline-overlay");
894
1178
  if (dielineOverlay) {
895
1179
  canvas.bringObjectToFront(dielineOverlay as any);
896
1180
  }
1181
+
1182
+ const rulerOverlay = this.canvasService.getLayer("ruler-overlay");
1183
+ if (rulerOverlay) {
1184
+ canvas.bringObjectToFront(rulerOverlay as any);
1185
+ }
897
1186
  }
898
1187
 
899
- private applyImagePreviewVisibility(previewActive: boolean) {
1188
+ private clearRenderedWhiteInks() {
900
1189
  if (!this.canvasService) return;
901
- const visible = previewActive ? this.previewImageVisible : true;
902
- let changed = false;
1190
+ void this.canvasService.applyObjectSpecsToRootLayer(
1191
+ WHITE_INK_OBJECT_LAYER_ID,
1192
+ [],
1193
+ );
1194
+ void this.canvasService.applyObjectSpecsToRootLayer(
1195
+ WHITE_INK_COVER_LAYER_ID,
1196
+ [],
1197
+ );
1198
+ void this.canvasService.applyObjectSpecsToRootLayer(
1199
+ WHITE_INK_OVERLAY_LAYER_ID,
1200
+ [],
1201
+ );
1202
+ }
903
1203
 
904
- this.canvasService.canvas.getObjects().forEach((obj: any) => {
905
- if (obj?.data?.layerId !== IMAGE_OBJECT_LAYER_ID) return;
906
- if (obj.visible === visible) return;
907
- obj.set({ visible });
908
- obj.setCoords?.();
909
- changed = true;
1204
+ private purgeSourceCaches(item?: WhiteInkItem) {
1205
+ const sourceUrl = this.resolveSourceUrl(item);
1206
+ if (!sourceUrl) return;
1207
+ this.sourceSizeBySrc.delete(sourceUrl);
1208
+ const prefix = `${sourceUrl}::`;
1209
+ Array.from(this.previewMaskBySource.keys()).forEach((cacheKey) => {
1210
+ if (cacheKey.startsWith(prefix)) {
1211
+ this.previewMaskBySource.delete(cacheKey);
1212
+ }
1213
+ });
1214
+ Array.from(this.pendingPreviewMaskBySource.keys()).forEach((cacheKey) => {
1215
+ if (cacheKey.startsWith(prefix)) {
1216
+ this.pendingPreviewMaskBySource.delete(cacheKey);
1217
+ }
910
1218
  });
911
-
912
- if (changed) {
913
- this.canvasService.requestRenderAll();
914
- }
915
1219
  }
916
1220
 
917
1221
  private updateWhiteInks() {
@@ -920,65 +1224,145 @@ export class WhiteInkTool implements Extension {
920
1224
 
921
1225
  private async updateWhiteInksAsync() {
922
1226
  if (!this.canvasService) return;
1227
+
923
1228
  this.syncToolActiveFromWorkbench();
924
1229
  const seq = ++this.renderSeq;
925
1230
 
926
1231
  const previewActive = this.isPreviewActive();
927
- this.applyImagePreviewVisibility(previewActive);
928
- const renderItems = previewActive ? this.workingItems : [];
1232
+ this.applyImageVisibilityForWhiteInk(previewActive);
1233
+
929
1234
  const frame = this.getFrameRect();
930
- const desiredIds = new Set(renderItems.map((item) => item.id));
1235
+ const frameSpecs = this.buildFrameSpecs(frame);
1236
+
1237
+ let whiteSpecs: RenderObjectSpec[] = [];
1238
+ let coverSpecs: RenderObjectSpec[] = [];
1239
+
1240
+ if (previewActive) {
1241
+ const snapshot = this.getImageSnapshot(this.getPrimaryImageObject());
1242
+ const item = this.getEffectiveWhiteInkItem(this.resolveRenderItems());
1243
+
1244
+ if (snapshot && item) {
1245
+ const sources = await this.resolveRenderSources(snapshot, item);
1246
+ if (seq !== this.renderSeq) return;
1247
+
1248
+ if (sources?.whiteSrc) {
1249
+ whiteSpecs = [
1250
+ this.buildCloneImageSpec(
1251
+ "white-ink.main",
1252
+ snapshot,
1253
+ sources.whiteSrc,
1254
+ WHITE_INK_DEFAULT_OPACITY,
1255
+ WHITE_INK_OBJECT_LAYER_ID,
1256
+ "white-ink",
1257
+ sources.whiteScaleAdjustX,
1258
+ sources.whiteScaleAdjustY,
1259
+ ),
1260
+ ];
1261
+ }
931
1262
 
932
- this.getWhiteInkObjects().forEach((obj: any) => {
933
- const id = obj?.data?.id;
934
- if (typeof id === "string" && !desiredIds.has(id)) {
935
- this.canvasService?.canvas.remove(obj);
1263
+ if (this.previewImageVisible && sources?.coverSrc) {
1264
+ coverSpecs = [
1265
+ this.buildCloneImageSpec(
1266
+ "white-ink.cover",
1267
+ snapshot,
1268
+ sources.coverSrc,
1269
+ this.computeCoverOpacity(),
1270
+ WHITE_INK_COVER_LAYER_ID,
1271
+ "white-ink-cover",
1272
+ ),
1273
+ ];
1274
+ }
936
1275
  }
937
- });
938
-
939
- for (const item of renderItems) {
940
- if (seq !== this.renderSeq) return;
941
- await this.upsertWhiteInkObject(item, frame, seq);
942
1276
  }
1277
+
1278
+ await this.canvasService.applyObjectSpecsToRootLayer(
1279
+ WHITE_INK_OBJECT_LAYER_ID,
1280
+ whiteSpecs,
1281
+ );
1282
+ if (seq !== this.renderSeq) return;
1283
+
1284
+ await this.canvasService.applyObjectSpecsToRootLayer(
1285
+ WHITE_INK_COVER_LAYER_ID,
1286
+ coverSpecs,
1287
+ );
1288
+ if (seq !== this.renderSeq) return;
1289
+
1290
+ await this.canvasService.applyObjectSpecsToRootLayer(
1291
+ WHITE_INK_OVERLAY_LAYER_ID,
1292
+ frameSpecs,
1293
+ );
943
1294
  if (seq !== this.renderSeq) return;
944
1295
 
945
- this.syncZOrder(renderItems);
1296
+ this.syncZOrder();
946
1297
  this.canvasService.requestRenderAll();
947
1298
  }
948
1299
 
949
- private async getPreviewMaskSource(sourceUrl: string): Promise<string> {
1300
+ private getMaskCacheKey(sourceUrl: string, tint: MaskTint): string {
1301
+ return `${sourceUrl}::${tint.key}`;
1302
+ }
1303
+
1304
+ private async getPreviewMaskSource(
1305
+ sourceUrl: string,
1306
+ tint: MaskTint = WHITE_MASK_TINT,
1307
+ fallbackElement?: any,
1308
+ ): Promise<string> {
950
1309
  if (!sourceUrl) return "";
951
1310
  if (typeof document === "undefined" || typeof Image === "undefined") {
952
- return sourceUrl;
1311
+ return "";
953
1312
  }
954
1313
 
955
- const cached = this.previewMaskBySource.get(sourceUrl);
1314
+ const cacheKey = this.getMaskCacheKey(sourceUrl, tint);
1315
+ const cached = this.previewMaskBySource.get(cacheKey);
956
1316
  if (cached) return cached;
957
1317
 
958
- const pending = this.pendingPreviewMaskBySource.get(sourceUrl);
1318
+ const pending = this.pendingPreviewMaskBySource.get(cacheKey);
959
1319
  if (pending) {
960
1320
  const loaded = await pending;
961
- return loaded || sourceUrl;
1321
+ return loaded || "";
962
1322
  }
963
1323
 
964
- const task = this.createOpaqueMaskSource(sourceUrl);
965
- this.pendingPreviewMaskBySource.set(sourceUrl, task);
1324
+ const task = this.createOpaqueMaskSource(sourceUrl, tint, fallbackElement);
1325
+ this.pendingPreviewMaskBySource.set(cacheKey, task);
966
1326
  const loaded = await task;
967
- this.pendingPreviewMaskBySource.delete(sourceUrl);
1327
+ this.pendingPreviewMaskBySource.delete(cacheKey);
968
1328
 
969
- if (!loaded) return sourceUrl;
970
- this.previewMaskBySource.set(sourceUrl, loaded);
1329
+ if (!loaded) return "";
1330
+ this.previewMaskBySource.set(cacheKey, loaded);
971
1331
  return loaded;
972
1332
  }
973
1333
 
1334
+ private getElementSize(
1335
+ element: any,
1336
+ ): { width: number; height: number } | null {
1337
+ if (!element) return null;
1338
+
1339
+ const width = Number(
1340
+ element?.naturalWidth || element?.videoWidth || element?.width || 0,
1341
+ );
1342
+ const height = Number(
1343
+ element?.naturalHeight || element?.videoHeight || element?.height || 0,
1344
+ );
1345
+
1346
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
1347
+ if (width <= 0 || height <= 0) return null;
1348
+
1349
+ return { width, height };
1350
+ }
1351
+
974
1352
  private async createOpaqueMaskSource(
975
1353
  sourceUrl: string,
1354
+ tint: MaskTint = WHITE_MASK_TINT,
1355
+ fallbackElement?: any,
976
1356
  ): Promise<string | null> {
977
1357
  try {
978
- const img = await this.loadImageElement(sourceUrl);
979
- const width = Math.max(1, Number(img.naturalWidth || img.width || 0));
980
- const height = Math.max(1, Number(img.naturalHeight || img.height || 0));
981
- if (width <= 0 || height <= 0) return null;
1358
+ const element =
1359
+ fallbackElement || (await this.loadImageElement(sourceUrl));
1360
+ const size = this.getElementSize(element);
1361
+ if (!size) return null;
1362
+ const width = Math.max(1, size.width);
1363
+ const height = Math.max(1, size.height);
1364
+
1365
+ this.rememberSourceSize(sourceUrl, { width, height });
982
1366
 
983
1367
  const canvas = document.createElement("canvas");
984
1368
  canvas.width = width;
@@ -986,22 +1370,22 @@ export class WhiteInkTool implements Extension {
986
1370
  const ctx = canvas.getContext("2d");
987
1371
  if (!ctx) return null;
988
1372
 
989
- ctx.drawImage(img, 0, 0, width, height);
1373
+ ctx.drawImage(element, 0, 0, width, height);
990
1374
  const imageData = ctx.getImageData(0, 0, width, height);
991
1375
  const data = imageData.data;
992
1376
 
993
1377
  for (let i = 0; i < data.length; i += 4) {
994
1378
  const alpha = data[i + 3];
995
- data[i] = 255;
996
- data[i + 1] = 255;
997
- data[i + 2] = 255;
1379
+ data[i] = tint.r;
1380
+ data[i + 1] = tint.g;
1381
+ data[i + 2] = tint.b;
998
1382
  data[i + 3] = alpha;
999
1383
  }
1000
1384
 
1001
1385
  ctx.putImageData(imageData, 0, 0);
1002
1386
  return canvas.toDataURL("image/png");
1003
1387
  } catch (error) {
1004
- this.debug("mask:extract:failed", { sourceUrl, error });
1388
+ this.debug("mask:extract:failed", { sourceUrl, tint: tint.key, error });
1005
1389
  return null;
1006
1390
  }
1007
1391
  }