@pooder/kit 5.0.4 → 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.
- package/CHANGELOG.md +11 -0
- package/dist/index.d.mts +239 -267
- package/dist/index.d.ts +239 -267
- package/dist/index.js +6603 -5936
- package/dist/index.mjs +6670 -5991
- package/package.json +2 -2
- package/src/{background.ts → extensions/background.ts} +1 -1
- package/src/{dieline.ts → extensions/dieline.ts} +39 -17
- package/src/{feature.ts → extensions/feature.ts} +80 -67
- package/src/{film.ts → extensions/film.ts} +1 -1
- package/src/{geometry.ts → extensions/geometry.ts} +151 -105
- package/src/{image.ts → extensions/image.ts} +97 -84
- package/src/extensions/index.ts +11 -0
- package/src/{maskOps.ts → extensions/maskOps.ts} +28 -10
- package/src/{mirror.ts → extensions/mirror.ts} +1 -1
- package/src/{ruler.ts → extensions/ruler.ts} +5 -3
- package/src/extensions/sceneLayout.ts +140 -0
- package/src/{sceneLayoutModel.ts → extensions/sceneLayoutModel.ts} +17 -10
- package/src/extensions/sceneVisibility.ts +71 -0
- package/src/{size.ts → extensions/size.ts} +23 -13
- package/src/{tracer.ts → extensions/tracer.ts} +374 -45
- package/src/{white-ink.ts → extensions/white-ink.ts} +620 -236
- package/src/index.ts +2 -14
- package/src/{ViewportSystem.ts → services/ViewportSystem.ts} +5 -2
- package/src/services/index.ts +3 -0
- package/src/sceneLayout.ts +0 -121
- package/src/sceneVisibility.ts +0 -49
- /package/src/{bridgeSelection.ts → extensions/bridgeSelection.ts} +0 -0
- /package/src/{constraints.ts → extensions/constraints.ts} +0 -0
- /package/src/{edgeScale.ts → extensions/edgeScale.ts} +0 -0
- /package/src/{featureComplete.ts → extensions/featureComplete.ts} +0 -0
- /package/src/{wrappedOffsets.ts → extensions/wrappedOffsets.ts} +0 -0
- /package/src/{CanvasService.ts → services/CanvasService.ts} +0 -0
- /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 {
|
|
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
|
|
40
|
+
interface ImageSnapshot {
|
|
41
|
+
id: string;
|
|
35
42
|
src: string;
|
|
36
|
-
|
|
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.
|
|
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
|
|
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
|
|
238
|
-
const
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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) => !!
|
|
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
|
|
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 ===
|
|
751
|
+
return obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID;
|
|
704
752
|
}) as any[];
|
|
705
753
|
}
|
|
706
754
|
|
|
707
|
-
private
|
|
708
|
-
return this.
|
|
755
|
+
private getPrimaryImageObject(): any | undefined {
|
|
756
|
+
return this.getImageObjects()[0];
|
|
709
757
|
}
|
|
710
758
|
|
|
711
|
-
private
|
|
712
|
-
|
|
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
|
|
719
|
-
|
|
720
|
-
if (
|
|
721
|
-
|
|
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
|
|
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
|
-
|
|
730
|
-
|
|
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
|
|
735
|
-
|
|
736
|
-
if (
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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 {
|
|
846
|
+
return {
|
|
847
|
+
x: baseSize.width / whiteSize.width,
|
|
848
|
+
y: baseSize.height / whiteSize.height,
|
|
849
|
+
};
|
|
747
850
|
}
|
|
748
851
|
|
|
749
|
-
private
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
|
758
|
-
|
|
860
|
+
private buildCloneImageSpec(
|
|
861
|
+
id: string,
|
|
862
|
+
snapshot: ImageSnapshot,
|
|
759
863
|
src: string,
|
|
760
|
-
|
|
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
|
-
|
|
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
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
802
|
-
item: WhiteInkItem,
|
|
803
|
-
frame: FrameRect,
|
|
804
|
-
seq: number,
|
|
805
|
-
) {
|
|
1065
|
+
private applyImageVisibilityForWhiteInk(previewActive: boolean) {
|
|
806
1066
|
if (!this.canvasService) return;
|
|
807
|
-
const
|
|
808
|
-
|
|
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
|
-
|
|
815
|
-
|
|
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
|
-
|
|
818
|
-
|
|
1078
|
+
if (changed) {
|
|
1079
|
+
this.canvasService.requestRenderAll();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
819
1082
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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.
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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 ===
|
|
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
|
|
1188
|
+
private clearRenderedWhiteInks() {
|
|
900
1189
|
if (!this.canvasService) return;
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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.
|
|
928
|
-
|
|
1232
|
+
this.applyImageVisibilityForWhiteInk(previewActive);
|
|
1233
|
+
|
|
929
1234
|
const frame = this.getFrameRect();
|
|
930
|
-
const
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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(
|
|
1296
|
+
this.syncZOrder();
|
|
946
1297
|
this.canvasService.requestRenderAll();
|
|
947
1298
|
}
|
|
948
1299
|
|
|
949
|
-
private
|
|
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
|
|
1311
|
+
return "";
|
|
953
1312
|
}
|
|
954
1313
|
|
|
955
|
-
const
|
|
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(
|
|
1318
|
+
const pending = this.pendingPreviewMaskBySource.get(cacheKey);
|
|
959
1319
|
if (pending) {
|
|
960
1320
|
const loaded = await pending;
|
|
961
|
-
return loaded ||
|
|
1321
|
+
return loaded || "";
|
|
962
1322
|
}
|
|
963
1323
|
|
|
964
|
-
const task = this.createOpaqueMaskSource(sourceUrl);
|
|
965
|
-
this.pendingPreviewMaskBySource.set(
|
|
1324
|
+
const task = this.createOpaqueMaskSource(sourceUrl, tint, fallbackElement);
|
|
1325
|
+
this.pendingPreviewMaskBySource.set(cacheKey, task);
|
|
966
1326
|
const loaded = await task;
|
|
967
|
-
this.pendingPreviewMaskBySource.delete(
|
|
1327
|
+
this.pendingPreviewMaskBySource.delete(cacheKey);
|
|
968
1328
|
|
|
969
|
-
if (!loaded) return
|
|
970
|
-
this.previewMaskBySource.set(
|
|
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
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
if (
|
|
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(
|
|
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] =
|
|
996
|
-
data[i + 1] =
|
|
997
|
-
data[i + 2] =
|
|
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
|
}
|