@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.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +239 -269
- package/dist/index.d.ts +239 -269
- package/dist/index.js +6485 -5833
- package/dist/index.mjs +6587 -5923
- 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} +190 -192
- 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
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
WorkbenchService,
|
|
10
10
|
} from "@pooder/core";
|
|
11
11
|
import { Canvas as FabricCanvas, Image as FabricImage, Point } from "fabric";
|
|
12
|
-
import CanvasService from "
|
|
13
|
-
import type { RenderObjectSpec } from "./renderSpec";
|
|
12
|
+
import { CanvasService, RenderObjectSpec } from "../services";
|
|
14
13
|
import { computeSceneLayout, readSizeState } from "./sceneLayoutModel";
|
|
15
14
|
|
|
16
15
|
export interface ImageItem {
|
|
@@ -57,8 +56,7 @@ interface FrameVisualConfig {
|
|
|
57
56
|
|
|
58
57
|
interface UpsertImageOptions {
|
|
59
58
|
id?: string;
|
|
60
|
-
mode?: "
|
|
61
|
-
createIfMissing?: boolean;
|
|
59
|
+
mode?: "replace" | "add";
|
|
62
60
|
addOptions?: Partial<ImageItem>;
|
|
63
61
|
fitOnAdd?: boolean;
|
|
64
62
|
}
|
|
@@ -74,44 +72,26 @@ interface UpdateImageOptions {
|
|
|
74
72
|
target?: "auto" | "config" | "working";
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
interface
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
width: number;
|
|
81
|
-
height: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
interface DetectEdgeResult {
|
|
85
|
-
pathData: string;
|
|
86
|
-
rawBounds?: DetectBounds;
|
|
87
|
-
baseBounds?: DetectBounds;
|
|
88
|
-
imageWidth?: number;
|
|
89
|
-
imageHeight?: number;
|
|
75
|
+
interface ExportCroppedImageOptions {
|
|
76
|
+
multiplier?: number;
|
|
77
|
+
format?: "png" | "jpeg";
|
|
90
78
|
}
|
|
91
79
|
|
|
92
|
-
interface
|
|
93
|
-
|
|
94
|
-
centerX: number;
|
|
95
|
-
centerY: number;
|
|
96
|
-
objectScale: number;
|
|
97
|
-
sourceWidth: number;
|
|
98
|
-
sourceHeight: number;
|
|
80
|
+
interface ExportUserCroppedImageOptions extends ExportCroppedImageOptions {
|
|
81
|
+
imageIds?: string[];
|
|
99
82
|
}
|
|
100
83
|
|
|
101
|
-
interface
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
multiplier
|
|
106
|
-
|
|
84
|
+
interface ExportUserCroppedImageResult {
|
|
85
|
+
url: string;
|
|
86
|
+
width: number;
|
|
87
|
+
height: number;
|
|
88
|
+
multiplier: number;
|
|
89
|
+
format: "png" | "jpeg";
|
|
90
|
+
imageIds: string[];
|
|
107
91
|
}
|
|
108
92
|
|
|
109
93
|
const IMAGE_OBJECT_LAYER_ID = "image.user";
|
|
110
94
|
const IMAGE_OVERLAY_LAYER_ID = "image-overlay";
|
|
111
|
-
const IMAGE_REPLACE_GUARD_MS = 2500;
|
|
112
|
-
const IMAGE_DETECT_EXPAND_DEFAULT = 30;
|
|
113
|
-
const IMAGE_DETECT_SIMPLIFY_TOLERANCE_DEFAULT = 2;
|
|
114
|
-
const IMAGE_DETECT_MULTIPLIER_DEFAULT = 2;
|
|
115
95
|
|
|
116
96
|
export class ImageTool implements Extension {
|
|
117
97
|
id = "pooder.kit.image";
|
|
@@ -131,7 +111,6 @@ export class ImageTool implements Extension {
|
|
|
131
111
|
private isToolActive = false;
|
|
132
112
|
private isImageSelectionActive = false;
|
|
133
113
|
private focusedImageId: string | null = null;
|
|
134
|
-
private suppressSelectionClearUntil = 0;
|
|
135
114
|
private renderSeq = 0;
|
|
136
115
|
private dirtyTrackerDisposable?: { dispose(): void };
|
|
137
116
|
|
|
@@ -218,13 +197,10 @@ export class ImageTool implements Extension {
|
|
|
218
197
|
const before = this.isToolActive;
|
|
219
198
|
this.syncToolActiveFromWorkbench(event.id);
|
|
220
199
|
if (!this.isToolActive) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.isImageSelectionActive = false;
|
|
226
|
-
this.focusedImageId = null;
|
|
227
|
-
}
|
|
200
|
+
this.setImageFocus(null, {
|
|
201
|
+
syncCanvasSelection: true,
|
|
202
|
+
skipRender: true,
|
|
203
|
+
});
|
|
228
204
|
}
|
|
229
205
|
this.debug("tool:activated", {
|
|
230
206
|
id: event.id,
|
|
@@ -233,7 +209,6 @@ export class ImageTool implements Extension {
|
|
|
233
209
|
before,
|
|
234
210
|
isToolActive: this.isToolActive,
|
|
235
211
|
focusedImageId: this.focusedImageId,
|
|
236
|
-
suppressSelectionClearUntil: this.suppressSelectionClearUntil,
|
|
237
212
|
});
|
|
238
213
|
if (!this.isToolActive && this.isDebugEnabled()) {
|
|
239
214
|
console.trace("[ImageTool] tool deactivated trace");
|
|
@@ -271,16 +246,10 @@ export class ImageTool implements Extension {
|
|
|
271
246
|
};
|
|
272
247
|
|
|
273
248
|
private onSelectionCleared = () => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
focusedImageId: this.focusedImageId,
|
|
279
|
-
});
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
this.isImageSelectionActive = false;
|
|
283
|
-
this.focusedImageId = null;
|
|
249
|
+
this.setImageFocus(null, {
|
|
250
|
+
syncCanvasSelection: false,
|
|
251
|
+
skipRender: true,
|
|
252
|
+
});
|
|
284
253
|
this.debug("selection:cleared applied");
|
|
285
254
|
this.updateImages();
|
|
286
255
|
};
|
|
@@ -353,7 +322,7 @@ export class ImageTool implements Extension {
|
|
|
353
322
|
id: "image.frame.strokeColor",
|
|
354
323
|
type: "color",
|
|
355
324
|
label: "Image Frame Stroke Color",
|
|
356
|
-
default: "#
|
|
325
|
+
default: "#808080",
|
|
357
326
|
},
|
|
358
327
|
{
|
|
359
328
|
id: "image.frame.strokeWidth",
|
|
@@ -369,7 +338,7 @@ export class ImageTool implements Extension {
|
|
|
369
338
|
type: "select",
|
|
370
339
|
label: "Image Frame Stroke Style",
|
|
371
340
|
options: ["solid", "dashed", "hidden"],
|
|
372
|
-
default: "
|
|
341
|
+
default: "dashed",
|
|
373
342
|
},
|
|
374
343
|
{
|
|
375
344
|
id: "image.frame.dashLength",
|
|
@@ -444,12 +413,12 @@ export class ImageTool implements Extension {
|
|
|
444
413
|
},
|
|
445
414
|
},
|
|
446
415
|
{
|
|
447
|
-
command: "
|
|
448
|
-
title: "Export
|
|
416
|
+
command: "exportUserCroppedImage",
|
|
417
|
+
title: "Export User Cropped Image",
|
|
449
418
|
handler: async (
|
|
450
|
-
options:
|
|
419
|
+
options: ExportUserCroppedImageOptions = {},
|
|
451
420
|
) => {
|
|
452
|
-
return await this.
|
|
421
|
+
return await this.exportUserCroppedImage(options);
|
|
453
422
|
},
|
|
454
423
|
},
|
|
455
424
|
{
|
|
@@ -474,6 +443,16 @@ export class ImageTool implements Extension {
|
|
|
474
443
|
await this.fitImageToDefaultArea(id);
|
|
475
444
|
},
|
|
476
445
|
},
|
|
446
|
+
{
|
|
447
|
+
command: "focusImage",
|
|
448
|
+
title: "Focus Image",
|
|
449
|
+
handler: (
|
|
450
|
+
id: string | null,
|
|
451
|
+
options: { syncCanvasSelection?: boolean } = {},
|
|
452
|
+
) => {
|
|
453
|
+
return this.setImageFocus(id, options);
|
|
454
|
+
},
|
|
455
|
+
},
|
|
477
456
|
{
|
|
478
457
|
command: "removeImage",
|
|
479
458
|
title: "Remove Image",
|
|
@@ -483,8 +462,10 @@ export class ImageTool implements Extension {
|
|
|
483
462
|
if (next.length !== this.items.length) {
|
|
484
463
|
this.purgeSourceSizeCacheForItem(removed);
|
|
485
464
|
if (this.focusedImageId === id) {
|
|
486
|
-
this.
|
|
487
|
-
|
|
465
|
+
this.setImageFocus(null, {
|
|
466
|
+
syncCanvasSelection: true,
|
|
467
|
+
skipRender: true,
|
|
468
|
+
});
|
|
488
469
|
}
|
|
489
470
|
this.updateConfig(next);
|
|
490
471
|
}
|
|
@@ -506,8 +487,10 @@ export class ImageTool implements Extension {
|
|
|
506
487
|
title: "Clear Images",
|
|
507
488
|
handler: () => {
|
|
508
489
|
this.sourceSizeBySrc.clear();
|
|
509
|
-
this.
|
|
510
|
-
|
|
490
|
+
this.setImageFocus(null, {
|
|
491
|
+
syncCanvasSelection: true,
|
|
492
|
+
skipRender: true,
|
|
493
|
+
});
|
|
511
494
|
this.updateConfig([]);
|
|
512
495
|
},
|
|
513
496
|
},
|
|
@@ -584,29 +567,50 @@ export class ImageTool implements Extension {
|
|
|
584
567
|
return Math.random().toString(36).substring(2, 9);
|
|
585
568
|
}
|
|
586
569
|
|
|
587
|
-
private
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
) {
|
|
593
|
-
return active.data.id;
|
|
594
|
-
}
|
|
595
|
-
return null;
|
|
570
|
+
private hasImageItem(id: string): boolean {
|
|
571
|
+
return (
|
|
572
|
+
this.items.some((item) => item.id === id) ||
|
|
573
|
+
this.workingItems.some((item) => item.id === id)
|
|
574
|
+
);
|
|
596
575
|
}
|
|
597
576
|
|
|
598
|
-
private
|
|
599
|
-
|
|
600
|
-
|
|
577
|
+
private setImageFocus(
|
|
578
|
+
id: string | null,
|
|
579
|
+
options: { syncCanvasSelection?: boolean; skipRender?: boolean } = {},
|
|
580
|
+
) {
|
|
581
|
+
const syncCanvasSelection = options.syncCanvasSelection !== false;
|
|
601
582
|
|
|
602
|
-
if (
|
|
603
|
-
|
|
583
|
+
if (id && !this.hasImageItem(id)) {
|
|
584
|
+
return { ok: false, reason: "image-not-found" as const };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
this.focusedImageId = id;
|
|
588
|
+
this.isImageSelectionActive = !!id;
|
|
589
|
+
|
|
590
|
+
if (syncCanvasSelection && this.canvasService) {
|
|
591
|
+
const canvas = this.canvasService.canvas;
|
|
592
|
+
if (!id) {
|
|
593
|
+
canvas.discardActiveObject();
|
|
594
|
+
} else {
|
|
595
|
+
const obj = this.getImageObject(id);
|
|
596
|
+
if (obj) {
|
|
597
|
+
obj.set({
|
|
598
|
+
selectable: true,
|
|
599
|
+
evented: true,
|
|
600
|
+
hasControls: true,
|
|
601
|
+
hasBorders: true,
|
|
602
|
+
});
|
|
603
|
+
canvas.setActiveObject(obj);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
this.canvasService.requestRenderAll();
|
|
607
|
+
}
|
|
604
608
|
|
|
605
|
-
|
|
606
|
-
|
|
609
|
+
if (!options.skipRender) {
|
|
610
|
+
this.updateImages();
|
|
611
|
+
}
|
|
607
612
|
|
|
608
|
-
|
|
609
|
-
return null;
|
|
613
|
+
return { ok: true, id };
|
|
610
614
|
}
|
|
611
615
|
|
|
612
616
|
private async addImageEntry(
|
|
@@ -622,9 +626,6 @@ export class ImageTool implements Extension {
|
|
|
622
626
|
...options,
|
|
623
627
|
} as ImageItem);
|
|
624
628
|
|
|
625
|
-
this.focusedImageId = id;
|
|
626
|
-
this.isImageSelectionActive = true;
|
|
627
|
-
this.suppressSelectionClearUntil = Date.now() + IMAGE_REPLACE_GUARD_MS;
|
|
628
629
|
const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
|
|
629
630
|
const waitLoaded = this.waitImageLoaded(id, true);
|
|
630
631
|
this.updateConfig([...this.items, newItem]);
|
|
@@ -634,7 +635,7 @@ export class ImageTool implements Extension {
|
|
|
634
635
|
await this.fitImageToDefaultArea(id);
|
|
635
636
|
}
|
|
636
637
|
if (loaded) {
|
|
637
|
-
this.
|
|
638
|
+
this.setImageFocus(id);
|
|
638
639
|
}
|
|
639
640
|
return id;
|
|
640
641
|
}
|
|
@@ -643,23 +644,20 @@ export class ImageTool implements Extension {
|
|
|
643
644
|
url: string,
|
|
644
645
|
options: UpsertImageOptions = {},
|
|
645
646
|
): Promise<{ id: string; mode: "replace" | "add" }> {
|
|
646
|
-
const mode = options.mode || "
|
|
647
|
+
const mode = options.mode || (options.id ? "replace" : "add");
|
|
647
648
|
const fitOnAdd = options.fitOnAdd !== false;
|
|
648
|
-
if (mode === "
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
649
|
+
if (mode === "replace") {
|
|
650
|
+
if (!options.id) {
|
|
651
|
+
throw new Error("replace-target-id-required");
|
|
652
|
+
}
|
|
653
|
+
const targetId = options.id;
|
|
654
|
+
if (!this.hasImageItem(targetId)) {
|
|
655
|
+
throw new Error("replace-target-not-found");
|
|
656
|
+
}
|
|
655
657
|
await this.updateImageInConfig(targetId, { url });
|
|
656
658
|
return { id: targetId, mode: "replace" };
|
|
657
659
|
}
|
|
658
660
|
|
|
659
|
-
if (mode === "replace" || options.createIfMissing === false) {
|
|
660
|
-
throw new Error("replace-target-not-found");
|
|
661
|
-
}
|
|
662
|
-
|
|
663
661
|
const id = await this.addImageEntry(url, options.addOptions, fitOnAdd);
|
|
664
662
|
return { id, mode: "add" };
|
|
665
663
|
}
|
|
@@ -870,12 +868,12 @@ export class ImageTool implements Extension {
|
|
|
870
868
|
private getFrameVisualConfig(): FrameVisualConfig {
|
|
871
869
|
const strokeStyleRaw = (this.getConfig<string>(
|
|
872
870
|
"image.frame.strokeStyle",
|
|
873
|
-
"
|
|
874
|
-
) || "
|
|
871
|
+
"dashed",
|
|
872
|
+
) || "dashed") as string;
|
|
875
873
|
const strokeStyle: "solid" | "dashed" | "hidden" =
|
|
876
874
|
strokeStyleRaw === "dashed" || strokeStyleRaw === "hidden"
|
|
877
875
|
? strokeStyleRaw
|
|
878
|
-
: "
|
|
876
|
+
: "dashed";
|
|
879
877
|
|
|
880
878
|
const strokeWidth = Number(
|
|
881
879
|
this.getConfig<number>("image.frame.strokeWidth", 2) ?? 2,
|
|
@@ -886,8 +884,8 @@ export class ImageTool implements Extension {
|
|
|
886
884
|
|
|
887
885
|
return {
|
|
888
886
|
strokeColor:
|
|
889
|
-
this.getConfig<string>("image.frame.strokeColor", "#
|
|
890
|
-
"#
|
|
887
|
+
this.getConfig<string>("image.frame.strokeColor", "#808080") ||
|
|
888
|
+
"#808080",
|
|
891
889
|
strokeWidth: Number.isFinite(strokeWidth) ? Math.max(0, strokeWidth) : 2,
|
|
892
890
|
strokeStyle,
|
|
893
891
|
dashLength: Number.isFinite(dashLength) ? Math.max(1, dashLength) : 8,
|
|
@@ -1199,8 +1197,10 @@ export class ImageTool implements Extension {
|
|
|
1199
1197
|
const frame = this.getFrameRect();
|
|
1200
1198
|
const desiredIds = new Set(renderItems.map((item) => item.id));
|
|
1201
1199
|
if (this.focusedImageId && !desiredIds.has(this.focusedImageId)) {
|
|
1202
|
-
this.
|
|
1203
|
-
|
|
1200
|
+
this.setImageFocus(null, {
|
|
1201
|
+
syncCanvasSelection: false,
|
|
1202
|
+
skipRender: true,
|
|
1203
|
+
});
|
|
1204
1204
|
}
|
|
1205
1205
|
|
|
1206
1206
|
this.getImageObjects().forEach((obj: any) => {
|
|
@@ -1281,8 +1281,10 @@ export class ImageTool implements Extension {
|
|
|
1281
1281
|
next[index] = this.normalizeItem({ ...next[index], ...updates });
|
|
1282
1282
|
this.workingItems = next;
|
|
1283
1283
|
this.hasWorkingChanges = true;
|
|
1284
|
-
this.
|
|
1285
|
-
|
|
1284
|
+
this.setImageFocus(id, {
|
|
1285
|
+
syncCanvasSelection: false,
|
|
1286
|
+
skipRender: true,
|
|
1287
|
+
});
|
|
1286
1288
|
if (this.isToolActive) {
|
|
1287
1289
|
this.updateImages();
|
|
1288
1290
|
}
|
|
@@ -1318,16 +1320,13 @@ export class ImageTool implements Extension {
|
|
|
1318
1320
|
this.updateConfig(next);
|
|
1319
1321
|
|
|
1320
1322
|
if (replacingSource) {
|
|
1321
|
-
this.focusedImageId = id;
|
|
1322
|
-
this.isImageSelectionActive = true;
|
|
1323
|
-
this.suppressSelectionClearUntil = Date.now() + IMAGE_REPLACE_GUARD_MS;
|
|
1324
1323
|
this.debug("replace:image:begin", { id, replacingUrl });
|
|
1325
1324
|
this.purgeSourceSizeCacheForItem(base);
|
|
1326
1325
|
const loaded = await this.waitImageLoaded(id, true);
|
|
1327
1326
|
this.debug("replace:image:loaded", { id, loaded });
|
|
1328
1327
|
if (loaded) {
|
|
1329
1328
|
await this.refitImageToFrame(id);
|
|
1330
|
-
this.
|
|
1329
|
+
this.setImageFocus(id);
|
|
1331
1330
|
}
|
|
1332
1331
|
}
|
|
1333
1332
|
}
|
|
@@ -1380,32 +1379,10 @@ export class ImageTool implements Extension {
|
|
|
1380
1379
|
this.updateConfig(next);
|
|
1381
1380
|
this.workingItems = this.cloneItems(next);
|
|
1382
1381
|
this.hasWorkingChanges = false;
|
|
1383
|
-
this.isImageSelectionActive = true;
|
|
1384
|
-
this.focusedImageId = id;
|
|
1385
1382
|
this.updateImages();
|
|
1386
1383
|
this.emitWorkingChange(id);
|
|
1387
1384
|
}
|
|
1388
1385
|
|
|
1389
|
-
private focusImageSelection(id: string) {
|
|
1390
|
-
if (!this.canvasService) return;
|
|
1391
|
-
const obj = this.getImageObject(id);
|
|
1392
|
-
if (!obj) return;
|
|
1393
|
-
|
|
1394
|
-
this.isImageSelectionActive = true;
|
|
1395
|
-
this.focusedImageId = id;
|
|
1396
|
-
this.suppressSelectionClearUntil = Date.now() + 700;
|
|
1397
|
-
obj.set({
|
|
1398
|
-
selectable: true,
|
|
1399
|
-
evented: true,
|
|
1400
|
-
hasControls: true,
|
|
1401
|
-
hasBorders: true,
|
|
1402
|
-
});
|
|
1403
|
-
this.canvasService.canvas.setActiveObject(obj);
|
|
1404
|
-
this.debug("focus:image", { id });
|
|
1405
|
-
this.canvasService.requestRenderAll();
|
|
1406
|
-
this.updateImages();
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
1386
|
private async fitImageToArea(
|
|
1410
1387
|
id: string,
|
|
1411
1388
|
area: { width: number; height: number; left?: number; top?: number },
|
|
@@ -1473,16 +1450,13 @@ export class ImageTool implements Extension {
|
|
|
1473
1450
|
return { ok: false, reason: "frame-not-ready" };
|
|
1474
1451
|
}
|
|
1475
1452
|
|
|
1476
|
-
const focusId =
|
|
1477
|
-
this.resolveReplaceTargetId(this.focusedImageId) ||
|
|
1478
|
-
(this.workingItems.length === 1 ? this.workingItems[0].id : null);
|
|
1479
|
-
|
|
1480
1453
|
const next: ImageItem[] = [];
|
|
1481
1454
|
for (const item of this.workingItems) {
|
|
1482
|
-
const
|
|
1455
|
+
const exported = await this.exportCroppedImageByIds([item.id], {
|
|
1483
1456
|
multiplier: 2,
|
|
1484
1457
|
format: "png",
|
|
1485
1458
|
});
|
|
1459
|
+
const url = exported.url;
|
|
1486
1460
|
|
|
1487
1461
|
const sourceUrl = item.sourceUrl || item.url;
|
|
1488
1462
|
const previousCommitted = item.committedUrl;
|
|
@@ -1502,27 +1476,28 @@ export class ImageTool implements Extension {
|
|
|
1502
1476
|
this.hasWorkingChanges = false;
|
|
1503
1477
|
this.workingItems = this.cloneItems(next);
|
|
1504
1478
|
this.updateConfig(next);
|
|
1505
|
-
this.emitWorkingChange(
|
|
1506
|
-
if (focusId) {
|
|
1507
|
-
this.focusedImageId = focusId;
|
|
1508
|
-
this.isImageSelectionActive = true;
|
|
1509
|
-
this.suppressSelectionClearUntil = Date.now() + IMAGE_REPLACE_GUARD_MS;
|
|
1510
|
-
this.focusImageSelection(focusId);
|
|
1511
|
-
}
|
|
1479
|
+
this.emitWorkingChange(this.focusedImageId);
|
|
1512
1480
|
return { ok: true };
|
|
1513
1481
|
}
|
|
1514
1482
|
|
|
1515
1483
|
private async exportCroppedImageByIds(
|
|
1516
1484
|
imageIds: string[],
|
|
1517
|
-
options:
|
|
1518
|
-
): Promise<
|
|
1485
|
+
options: ExportCroppedImageOptions,
|
|
1486
|
+
): Promise<ExportUserCroppedImageResult> {
|
|
1519
1487
|
if (!this.canvasService) {
|
|
1520
1488
|
throw new Error("CanvasService not initialized");
|
|
1521
1489
|
}
|
|
1522
1490
|
|
|
1491
|
+
const normalizedIds = [...new Set(imageIds)].filter(
|
|
1492
|
+
(id): id is string => typeof id === "string" && id.length > 0,
|
|
1493
|
+
);
|
|
1494
|
+
if (!normalizedIds.length) {
|
|
1495
|
+
throw new Error("image-ids-required");
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1523
1498
|
const frame = this.getFrameRect();
|
|
1524
1499
|
const multiplier = Math.max(1, options.multiplier ?? 2);
|
|
1525
|
-
const format = options.format
|
|
1500
|
+
const format: "png" | "jpeg" = options.format === "jpeg" ? "jpeg" : "png";
|
|
1526
1501
|
|
|
1527
1502
|
const width = Math.max(1, Math.round(frame.width * multiplier));
|
|
1528
1503
|
const height = Math.max(1, Math.round(frame.height * multiplier));
|
|
@@ -1536,61 +1511,84 @@ export class ImageTool implements Extension {
|
|
|
1536
1511
|
} as any);
|
|
1537
1512
|
tempCanvas.setDimensions({ width, height });
|
|
1538
1513
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
.
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1514
|
+
try {
|
|
1515
|
+
const idSet = new Set(normalizedIds);
|
|
1516
|
+
const sourceObjects = this.canvasService.canvas
|
|
1517
|
+
.getObjects()
|
|
1518
|
+
.filter((obj: any) => {
|
|
1519
|
+
return (
|
|
1520
|
+
obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID &&
|
|
1521
|
+
typeof obj?.data?.id === "string" &&
|
|
1522
|
+
idSet.has(obj.data.id)
|
|
1523
|
+
);
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
if (!sourceObjects.length) {
|
|
1527
|
+
throw new Error("image-objects-not-found");
|
|
1528
|
+
}
|
|
1549
1529
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1530
|
+
for (const source of sourceObjects as any[]) {
|
|
1531
|
+
const clone = await source.clone();
|
|
1532
|
+
const center = source.getCenterPoint
|
|
1533
|
+
? source.getCenterPoint()
|
|
1534
|
+
: new Point(source.left ?? 0, source.top ?? 0);
|
|
1555
1535
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1536
|
+
clone.set({
|
|
1537
|
+
originX: "center",
|
|
1538
|
+
originY: "center",
|
|
1539
|
+
left: (center.x - frame.left) * multiplier,
|
|
1540
|
+
top: (center.y - frame.top) * multiplier,
|
|
1541
|
+
scaleX: (source.scaleX || 1) * multiplier,
|
|
1542
|
+
scaleY: (source.scaleY || 1) * multiplier,
|
|
1543
|
+
angle: source.angle || 0,
|
|
1544
|
+
selectable: false,
|
|
1545
|
+
evented: false,
|
|
1546
|
+
});
|
|
1547
|
+
clone.setCoords();
|
|
1548
|
+
tempCanvas.add(clone);
|
|
1549
|
+
}
|
|
1570
1550
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1551
|
+
tempCanvas.renderAll();
|
|
1552
|
+
const blob = await tempCanvas.toBlob({ format, multiplier: 1 });
|
|
1553
|
+
if (!blob) {
|
|
1554
|
+
throw new Error("image-export-failed");
|
|
1555
|
+
}
|
|
1574
1556
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1557
|
+
return {
|
|
1558
|
+
url: URL.createObjectURL(blob),
|
|
1559
|
+
width,
|
|
1560
|
+
height,
|
|
1561
|
+
multiplier,
|
|
1562
|
+
format,
|
|
1563
|
+
imageIds: (sourceObjects as any[])
|
|
1564
|
+
.map((obj: any) => obj?.data?.id)
|
|
1565
|
+
.filter((id: any): id is string => typeof id === "string"),
|
|
1566
|
+
};
|
|
1567
|
+
} finally {
|
|
1568
|
+
tempCanvas.dispose();
|
|
1569
|
+
}
|
|
1577
1570
|
}
|
|
1578
1571
|
|
|
1579
|
-
private async
|
|
1580
|
-
options:
|
|
1581
|
-
): Promise<
|
|
1572
|
+
private async exportUserCroppedImage(
|
|
1573
|
+
options: ExportUserCroppedImageOptions = {},
|
|
1574
|
+
): Promise<ExportUserCroppedImageResult> {
|
|
1582
1575
|
if (!this.canvasService) {
|
|
1583
1576
|
throw new Error("CanvasService not initialized");
|
|
1584
1577
|
}
|
|
1585
1578
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
.filter((id: any) => typeof id === "string");
|
|
1579
|
+
await this.updateImagesAsync();
|
|
1580
|
+
this.syncToolActiveFromWorkbench();
|
|
1589
1581
|
|
|
1590
|
-
const
|
|
1591
|
-
imageIds
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1582
|
+
const imageIds =
|
|
1583
|
+
options.imageIds && options.imageIds.length > 0
|
|
1584
|
+
? options.imageIds
|
|
1585
|
+
: (this.isToolActive ? this.workingItems : this.items).map(
|
|
1586
|
+
(item) => item.id,
|
|
1587
|
+
);
|
|
1588
|
+
if (!imageIds.length) {
|
|
1589
|
+
throw new Error("no-images-to-export");
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
return await this.exportCroppedImageByIds(imageIds, options);
|
|
1595
1593
|
}
|
|
1596
1594
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./background";
|
|
2
|
+
export * from "./image";
|
|
3
|
+
export * from "./size";
|
|
4
|
+
export * from "./dieline";
|
|
5
|
+
export * from "./feature";
|
|
6
|
+
export * from "./film";
|
|
7
|
+
export * from "./mirror";
|
|
8
|
+
export * from "./ruler";
|
|
9
|
+
export * from "./white-ink";
|
|
10
|
+
export { SceneLayoutService } from "./sceneLayout";
|
|
11
|
+
export { SceneVisibilityService } from "./sceneVisibility";
|
|
@@ -10,6 +10,13 @@ export interface CreateMaskOptions {
|
|
|
10
10
|
alphaOpaqueCutoff?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface AlphaAnalysis {
|
|
14
|
+
total: number;
|
|
15
|
+
minAlpha: number;
|
|
16
|
+
belowOpaqueRatio: number;
|
|
17
|
+
veryTransparentRatio: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export function createMask(
|
|
14
21
|
imageData: ImageData,
|
|
15
22
|
options: CreateMaskOptions,
|
|
@@ -55,7 +62,21 @@ export function createMask(
|
|
|
55
62
|
return mask;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
function inferMaskMode(
|
|
65
|
+
export function inferMaskMode(
|
|
66
|
+
imageData: ImageData,
|
|
67
|
+
alphaOpaqueCutoff: number,
|
|
68
|
+
): MaskMode {
|
|
69
|
+
const analysis = analyzeAlpha(imageData, alphaOpaqueCutoff);
|
|
70
|
+
if (analysis.minAlpha === 255) return "whitebg";
|
|
71
|
+
if (analysis.veryTransparentRatio >= 0.0005) return "alpha";
|
|
72
|
+
if (analysis.belowOpaqueRatio >= 0.01) return "alpha";
|
|
73
|
+
return "whitebg";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function analyzeAlpha(
|
|
77
|
+
imageData: ImageData,
|
|
78
|
+
alphaOpaqueCutoff: number,
|
|
79
|
+
): AlphaAnalysis {
|
|
59
80
|
const { data } = imageData;
|
|
60
81
|
const total = data.length / 4;
|
|
61
82
|
|
|
@@ -70,15 +91,12 @@ function inferMaskMode(imageData: ImageData, alphaOpaqueCutoff: number): MaskMod
|
|
|
70
91
|
if (a < 32) veryTransparent++;
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (belowOpaqueRatio >= 0.01) return "alpha";
|
|
80
|
-
|
|
81
|
-
return "whitebg";
|
|
94
|
+
return {
|
|
95
|
+
total,
|
|
96
|
+
minAlpha,
|
|
97
|
+
belowOpaqueRatio: belowOpaque / total,
|
|
98
|
+
veryTransparentRatio: veryTransparent / total,
|
|
99
|
+
};
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
export function circularMorphology(
|