@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
@@ -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 "./CanvasService";
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?: "auto" | "replace" | "add";
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 DetectBounds {
78
- x: number;
79
- y: number;
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 ImageRenderSnapshot {
93
- id: string;
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 DetectFromFrameOptions {
102
- expand?: number;
103
- smoothing?: boolean;
104
- simplifyTolerance?: number;
105
- multiplier?: number;
106
- debug?: boolean;
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
- const now = Date.now();
222
- const inGuardWindow =
223
- now <= this.suppressSelectionClearUntil && !!this.focusedImageId;
224
- if (!inGuardWindow) {
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
- const now = Date.now();
275
- if (now <= this.suppressSelectionClearUntil && this.focusedImageId) {
276
- this.debug("selection:cleared ignored", {
277
- suppressUntil: this.suppressSelectionClearUntil,
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: "#FF0000",
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: "solid",
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: "exportImageFrameUrl",
448
- title: "Export Image Frame Url",
416
+ command: "exportUserCroppedImage",
417
+ title: "Export User Cropped Image",
449
418
  handler: async (
450
- options: { multiplier?: number; format?: "png" | "jpeg" } = {},
419
+ options: ExportUserCroppedImageOptions = {},
451
420
  ) => {
452
- return await this.exportImageFrameUrl(options);
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.focusedImageId = null;
487
- this.isImageSelectionActive = false;
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.focusedImageId = null;
510
- this.isImageSelectionActive = false;
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 getImageIdFromActiveObject(): string | null {
588
- const active = this.canvasService?.canvas.getActiveObject() as any;
589
- if (
590
- active?.data?.layerId === IMAGE_OBJECT_LAYER_ID &&
591
- typeof active?.data?.id === "string"
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 resolveReplaceTargetId(explicitId?: string | null): string | null {
599
- const has = (id: string | null | undefined) =>
600
- !!id && this.items.some((item) => item.id === id);
577
+ private setImageFocus(
578
+ id: string | null,
579
+ options: { syncCanvasSelection?: boolean; skipRender?: boolean } = {},
580
+ ) {
581
+ const syncCanvasSelection = options.syncCanvasSelection !== false;
601
582
 
602
- if (has(explicitId)) return explicitId as string;
603
- if (has(this.focusedImageId)) return this.focusedImageId as string;
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
- const activeId = this.getImageIdFromActiveObject();
606
- if (has(activeId)) return activeId;
609
+ if (!options.skipRender) {
610
+ this.updateImages();
611
+ }
607
612
 
608
- if (this.items.length === 1) return this.items[0].id;
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.focusImageSelection(id);
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 || "auto";
647
+ const mode = options.mode || (options.id ? "replace" : "add");
647
648
  const fitOnAdd = options.fitOnAdd !== false;
648
- if (mode === "add") {
649
- const id = await this.addImageEntry(url, options.addOptions, fitOnAdd);
650
- return { id, mode: "add" };
651
- }
652
-
653
- const targetId = this.resolveReplaceTargetId(options.id ?? null);
654
- if (targetId) {
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
- "solid",
874
- ) || "solid") as string;
871
+ "dashed",
872
+ ) || "dashed") as string;
875
873
  const strokeStyle: "solid" | "dashed" | "hidden" =
876
874
  strokeStyleRaw === "dashed" || strokeStyleRaw === "hidden"
877
875
  ? strokeStyleRaw
878
- : "solid";
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", "#FF0000") ||
890
- "#FF0000",
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.focusedImageId = null;
1203
- this.isImageSelectionActive = false;
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.isImageSelectionActive = true;
1285
- this.focusedImageId = id;
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.focusImageSelection(id);
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 url = await this.exportCroppedImageByIds([item.id], {
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(focusId);
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: { multiplier?: number; format?: "png" | "jpeg" },
1518
- ): Promise<string> {
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 ?? "png";
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
- const idSet = new Set(imageIds);
1540
- const sourceObjects = this.canvasService.canvas
1541
- .getObjects()
1542
- .filter((obj: any) => {
1543
- return (
1544
- obj?.data?.layerId === IMAGE_OBJECT_LAYER_ID &&
1545
- typeof obj?.data?.id === "string" &&
1546
- idSet.has(obj.data.id)
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
- for (const source of sourceObjects as any[]) {
1551
- const clone = await source.clone();
1552
- const center = source.getCenterPoint
1553
- ? source.getCenterPoint()
1554
- : new Point(source.left ?? 0, source.top ?? 0);
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
- clone.set({
1557
- originX: "center",
1558
- originY: "center",
1559
- left: (center.x - frame.left) * multiplier,
1560
- top: (center.y - frame.top) * multiplier,
1561
- scaleX: (source.scaleX || 1) * multiplier,
1562
- scaleY: (source.scaleY || 1) * multiplier,
1563
- angle: source.angle || 0,
1564
- selectable: false,
1565
- evented: false,
1566
- });
1567
- clone.setCoords();
1568
- tempCanvas.add(clone);
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
- tempCanvas.renderAll();
1572
- const dataUrl = tempCanvas.toDataURL({ format, multiplier: 1 });
1573
- tempCanvas.dispose();
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
- const blob = await (await fetch(dataUrl)).blob();
1576
- return URL.createObjectURL(blob);
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 exportImageFrameUrl(
1580
- options: { multiplier?: number; format?: "png" | "jpeg" } = {},
1581
- ): Promise<{ url: string }> {
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
- const imageIds = this.getImageObjects()
1587
- .map((obj: any) => obj?.data?.id)
1588
- .filter((id: any) => typeof id === "string");
1579
+ await this.updateImagesAsync();
1580
+ this.syncToolActiveFromWorkbench();
1589
1581
 
1590
- const url = await this.exportCroppedImageByIds(
1591
- imageIds as string[],
1592
- options,
1593
- );
1594
- return { url };
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(imageData: ImageData, alphaOpaqueCutoff: number): MaskMode {
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
- if (minAlpha === 255) return "whitebg";
74
-
75
- const belowOpaqueRatio = belowOpaque / total;
76
- const veryTransparentRatio = veryTransparent / total;
77
-
78
- if (veryTransparentRatio >= 0.0005) return "alpha";
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(