@pooder/kit 6.0.0 → 6.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 (99) hide show
  1. package/.test-dist/src/extensions/background/BackgroundTool.js +524 -0
  2. package/.test-dist/src/extensions/background/index.js +17 -0
  3. package/.test-dist/src/extensions/background.js +1 -1
  4. package/.test-dist/src/extensions/dieline/DielineTool.js +748 -0
  5. package/.test-dist/src/extensions/dieline/commands.js +127 -0
  6. package/.test-dist/src/extensions/dieline/config.js +107 -0
  7. package/.test-dist/src/extensions/dieline/index.js +21 -0
  8. package/.test-dist/src/extensions/dieline/model.js +2 -0
  9. package/.test-dist/src/extensions/dieline/renderer.js +2 -0
  10. package/.test-dist/src/extensions/dieline.js +4 -0
  11. package/.test-dist/src/extensions/feature/FeatureTool.js +914 -0
  12. package/.test-dist/src/extensions/feature/index.js +17 -0
  13. package/.test-dist/src/extensions/film/FilmTool.js +207 -0
  14. package/.test-dist/src/extensions/film/index.js +17 -0
  15. package/.test-dist/src/extensions/image/ImageTool.js +1499 -0
  16. package/.test-dist/src/extensions/image/commands.js +162 -0
  17. package/.test-dist/src/extensions/image/config.js +129 -0
  18. package/.test-dist/src/extensions/image/index.js +21 -0
  19. package/.test-dist/src/extensions/image/model.js +2 -0
  20. package/.test-dist/src/extensions/image/renderer.js +5 -0
  21. package/.test-dist/src/extensions/image.js +182 -7
  22. package/.test-dist/src/extensions/mirror/MirrorTool.js +104 -0
  23. package/.test-dist/src/extensions/mirror/index.js +17 -0
  24. package/.test-dist/src/extensions/ruler/RulerTool.js +442 -0
  25. package/.test-dist/src/extensions/ruler/index.js +17 -0
  26. package/.test-dist/src/extensions/sceneLayout.js +2 -93
  27. package/.test-dist/src/extensions/sceneLayoutModel.js +15 -200
  28. package/.test-dist/src/extensions/size/SizeTool.js +332 -0
  29. package/.test-dist/src/extensions/size/index.js +17 -0
  30. package/.test-dist/src/extensions/white-ink/WhiteInkTool.js +1003 -0
  31. package/.test-dist/src/extensions/white-ink/commands.js +148 -0
  32. package/.test-dist/src/extensions/white-ink/config.js +31 -0
  33. package/.test-dist/src/extensions/white-ink/index.js +21 -0
  34. package/.test-dist/src/extensions/white-ink/model.js +2 -0
  35. package/.test-dist/src/extensions/white-ink/renderer.js +5 -0
  36. package/.test-dist/src/services/CanvasService.js +34 -13
  37. package/.test-dist/src/services/SceneLayoutService.js +96 -0
  38. package/.test-dist/src/services/index.js +1 -0
  39. package/.test-dist/src/services/visibility.js +3 -0
  40. package/.test-dist/src/shared/constants/layers.js +25 -0
  41. package/.test-dist/src/shared/imaging/sourceSizeCache.js +82 -0
  42. package/.test-dist/src/shared/index.js +22 -0
  43. package/.test-dist/src/shared/runtime/sessionState.js +74 -0
  44. package/.test-dist/src/shared/runtime/subscriptions.js +30 -0
  45. package/.test-dist/src/shared/scene/frame.js +34 -0
  46. package/.test-dist/src/shared/scene/sceneLayoutModel.js +202 -0
  47. package/.test-dist/tests/run.js +118 -0
  48. package/CHANGELOG.md +12 -0
  49. package/dist/index.d.mts +403 -366
  50. package/dist/index.d.ts +403 -366
  51. package/dist/index.js +5172 -4752
  52. package/dist/index.mjs +1410 -2027
  53. package/dist/tracer-PO7CRBYY.mjs +1016 -0
  54. package/package.json +1 -1
  55. package/src/extensions/{background.ts → background/BackgroundTool.ts} +33 -50
  56. package/src/extensions/background/index.ts +1 -0
  57. package/src/extensions/{dieline.ts → dieline/DielineTool.ts} +18 -218
  58. package/src/extensions/dieline/commands.ts +109 -0
  59. package/src/extensions/dieline/config.ts +106 -0
  60. package/src/extensions/dieline/index.ts +5 -0
  61. package/src/extensions/dieline/model.ts +1 -0
  62. package/src/extensions/dieline/renderer.ts +1 -0
  63. package/src/extensions/{feature.ts → feature/FeatureTool.ts} +27 -21
  64. package/src/extensions/feature/index.ts +1 -0
  65. package/src/extensions/{film.ts → film/FilmTool.ts} +36 -48
  66. package/src/extensions/film/index.ts +1 -0
  67. package/src/extensions/{image.ts → image/ImageTool.ts} +289 -335
  68. package/src/extensions/image/commands.ts +176 -0
  69. package/src/extensions/image/config.ts +128 -0
  70. package/src/extensions/image/index.ts +5 -0
  71. package/src/extensions/image/model.ts +1 -0
  72. package/src/extensions/image/renderer.ts +1 -0
  73. package/src/extensions/{mirror.ts → mirror/MirrorTool.ts} +1 -1
  74. package/src/extensions/mirror/index.ts +1 -0
  75. package/src/extensions/{ruler.ts → ruler/RulerTool.ts} +4 -5
  76. package/src/extensions/ruler/index.ts +1 -0
  77. package/src/extensions/sceneLayout.ts +1 -140
  78. package/src/extensions/sceneLayoutModel.ts +1 -364
  79. package/src/extensions/{size.ts → size/SizeTool.ts} +7 -6
  80. package/src/extensions/size/index.ts +1 -0
  81. package/src/extensions/{white-ink.ts → white-ink/WhiteInkTool.ts} +130 -317
  82. package/src/extensions/white-ink/commands.ts +157 -0
  83. package/src/extensions/white-ink/config.ts +30 -0
  84. package/src/extensions/white-ink/index.ts +5 -0
  85. package/src/extensions/white-ink/model.ts +1 -0
  86. package/src/extensions/white-ink/renderer.ts +1 -0
  87. package/src/services/CanvasService.ts +43 -12
  88. package/src/services/SceneLayoutService.ts +139 -0
  89. package/src/services/index.ts +1 -0
  90. package/src/services/renderSpec.ts +2 -0
  91. package/src/services/visibility.ts +5 -0
  92. package/src/shared/constants/layers.ts +23 -0
  93. package/src/shared/imaging/sourceSizeCache.ts +103 -0
  94. package/src/shared/index.ts +6 -0
  95. package/src/shared/runtime/sessionState.ts +105 -0
  96. package/src/shared/runtime/subscriptions.ts +45 -0
  97. package/src/shared/scene/frame.ts +46 -0
  98. package/src/shared/scene/sceneLayoutModel.ts +367 -0
  99. package/tests/run.ts +151 -0
@@ -2,27 +2,48 @@ import {
2
2
  Extension,
3
3
  ExtensionContext,
4
4
  ContributionPointIds,
5
- CommandContribution,
6
- ConfigurationContribution,
7
5
  ConfigurationService,
8
6
  ToolSessionService,
9
7
  WorkbenchService,
10
8
  } from "@pooder/core";
11
9
  import {
12
10
  Canvas as FabricCanvas,
11
+ Control,
13
12
  Image as FabricImage,
14
13
  Pattern,
15
14
  Point,
15
+ controlsUtils,
16
16
  } from "fabric";
17
- import { CanvasService, RenderLayoutRect, RenderObjectSpec } from "../services";
18
- import { isDielineShape, normalizeShapeStyle } from "./dielineShape";
19
- import type { DielineShape, DielineShapeStyle } from "./dielineShape";
20
- import { generateDielinePath, getPathBounds } from "./geometry";
17
+ import { CanvasService, RenderLayoutRect, RenderObjectSpec } from "../../services";
18
+ import { isDielineShape, normalizeShapeStyle } from "../dielineShape";
19
+ import type { DielineShape, DielineShapeStyle } from "../dielineShape";
20
+ import { generateDielinePath, getPathBounds } from "../geometry";
21
21
  import {
22
22
  buildSceneGeometry,
23
23
  computeSceneLayout,
24
24
  readSizeState,
25
- } from "./sceneLayoutModel";
25
+ } from "../../shared/scene/sceneLayoutModel";
26
+ import {
27
+ type FrameRect,
28
+ resolveCutFrameRect,
29
+ toLayoutSceneRect as toSceneLayoutRect,
30
+ } from "../../shared/scene/frame";
31
+ import {
32
+ createSourceSizeCache,
33
+ getCoverScale as getCoverScaleFromRect,
34
+ type SourceSize,
35
+ } from "../../shared/imaging/sourceSizeCache";
36
+ import { SubscriptionBag } from "../../shared/runtime/subscriptions";
37
+ import {
38
+ applyCommittedSnapshot,
39
+ runDeferredConfigUpdate,
40
+ } from "../../shared/runtime/sessionState";
41
+ import {
42
+ IMAGE_OBJECT_LAYER_ID,
43
+ IMAGE_OVERLAY_LAYER_ID,
44
+ } from "../../shared/constants/layers";
45
+ import { createImageCommands } from "./commands";
46
+ import { createImageConfigurations } from "./config";
26
47
 
27
48
  export interface ImageItem {
28
49
  id: string;
@@ -36,18 +57,6 @@ export interface ImageItem {
36
57
  committedUrl?: string;
37
58
  }
38
59
 
39
- interface FrameRect {
40
- left: number;
41
- top: number;
42
- width: number;
43
- height: number;
44
- }
45
-
46
- interface SourceSize {
47
- width: number;
48
- height: number;
49
- }
50
-
51
60
  interface RenderImageState {
52
61
  src: string;
53
62
  left: number;
@@ -66,6 +75,18 @@ interface FrameVisualConfig {
66
75
  outerBackground: string;
67
76
  }
68
77
 
78
+ interface ImageControlVisualConfig {
79
+ cornerSize: number;
80
+ touchCornerSize: number;
81
+ cornerStyle: "rect" | "circle";
82
+ cornerColor: string;
83
+ cornerStrokeColor: string;
84
+ transparentCorners: boolean;
85
+ borderColor: string;
86
+ borderScaleFactor: number;
87
+ padding: number;
88
+ }
89
+
69
90
  type ShapeOverlayShape = Exclude<DielineShape, "custom">;
70
91
 
71
92
  interface SceneGeometryLike {
@@ -111,8 +132,45 @@ interface ExportUserCroppedImageResult {
111
132
  imageIds: string[];
112
133
  }
113
134
 
114
- const IMAGE_OBJECT_LAYER_ID = "image.user";
115
- const IMAGE_OVERLAY_LAYER_ID = "image-overlay";
135
+ type ImageControlCapability = "rotate" | "scale" | "flipX" | "flipY";
136
+
137
+ interface ImageControlDescriptor {
138
+ key: string;
139
+ capability: ImageControlCapability;
140
+ create: () => Control;
141
+ }
142
+
143
+ const IMAGE_DEFAULT_CONTROL_CAPABILITIES: ImageControlCapability[] = [
144
+ "rotate",
145
+ "scale",
146
+ ];
147
+
148
+ const IMAGE_CONTROL_DESCRIPTORS: ImageControlDescriptor[] = [
149
+ {
150
+ key: "tl",
151
+ capability: "rotate",
152
+ create: () =>
153
+ new Control({
154
+ x: -0.5,
155
+ y: -0.5,
156
+ actionName: "rotate",
157
+ actionHandler: controlsUtils.rotationWithSnapping,
158
+ cursorStyleHandler: controlsUtils.rotationStyleHandler,
159
+ }),
160
+ },
161
+ {
162
+ key: "br",
163
+ capability: "scale",
164
+ create: () =>
165
+ new Control({
166
+ x: 0.5,
167
+ y: 0.5,
168
+ actionName: "scale",
169
+ actionHandler: controlsUtils.scalingEqually,
170
+ cursorStyleHandler: controlsUtils.scaleCursorStyleHandler,
171
+ }),
172
+ },
173
+ ];
116
174
 
117
175
  export class ImageTool implements Extension {
118
176
  id = "pooder.kit.image";
@@ -125,7 +183,9 @@ export class ImageTool implements Extension {
125
183
  private workingItems: ImageItem[] = [];
126
184
  private hasWorkingChanges = false;
127
185
  private loadResolvers: Map<string, () => void> = new Map();
128
- private sourceSizeBySrc: Map<string, SourceSize> = new Map();
186
+ private sourceSizeCache = createSourceSizeCache((src) =>
187
+ this.loadImageSize(src),
188
+ );
129
189
  private canvasService?: CanvasService;
130
190
  private context?: ExtensionContext;
131
191
  private isUpdatingConfig = false;
@@ -140,8 +200,12 @@ export class ImageTool implements Extension {
140
200
  private imageSpecs: RenderObjectSpec[] = [];
141
201
  private overlaySpecs: RenderObjectSpec[] = [];
142
202
  private renderProducerDisposable?: { dispose: () => void };
203
+ private readonly subscriptions = new SubscriptionBag();
204
+ private imageControlsByCapabilityKey: Map<string, Record<string, Control>> =
205
+ new Map();
143
206
 
144
207
  activate(context: ExtensionContext) {
208
+ this.subscriptions.disposeAll();
145
209
  this.context = context;
146
210
  this.canvasService = context.services.get<CanvasService>("CanvasService");
147
211
  if (!this.canvasService) {
@@ -184,41 +248,63 @@ export class ImageTool implements Extension {
184
248
  { priority: 300 },
185
249
  );
186
250
 
187
- context.eventBus.on("tool:activated", this.onToolActivated);
188
- context.eventBus.on("object:modified", this.onObjectModified);
189
- context.eventBus.on("selection:created", this.onSelectionChanged);
190
- context.eventBus.on("selection:updated", this.onSelectionChanged);
191
- context.eventBus.on("selection:cleared", this.onSelectionCleared);
192
- context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
193
- context.eventBus.on("scene:geometry:change", this.onSceneGeometryChanged);
251
+ this.subscriptions.on(context.eventBus, "tool:activated", this.onToolActivated);
252
+ this.subscriptions.on(context.eventBus, "object:modified", this.onObjectModified);
253
+ this.subscriptions.on(
254
+ context.eventBus,
255
+ "selection:created",
256
+ this.onSelectionChanged,
257
+ );
258
+ this.subscriptions.on(
259
+ context.eventBus,
260
+ "selection:updated",
261
+ this.onSelectionChanged,
262
+ );
263
+ this.subscriptions.on(
264
+ context.eventBus,
265
+ "selection:cleared",
266
+ this.onSelectionCleared,
267
+ );
268
+ this.subscriptions.on(
269
+ context.eventBus,
270
+ "scene:layout:change",
271
+ this.onSceneLayoutChanged,
272
+ );
273
+ this.subscriptions.on(
274
+ context.eventBus,
275
+ "scene:geometry:change",
276
+ this.onSceneGeometryChanged,
277
+ );
194
278
 
195
279
  const configService = context.services.get<ConfigurationService>(
196
280
  "ConfigurationService",
197
281
  );
198
282
  if (configService) {
199
- this.items = this.normalizeItems(
200
- configService.get("image.items", []) || [],
201
- );
202
- this.workingItems = this.cloneItems(this.items);
203
- this.hasWorkingChanges = false;
283
+ this.applyCommittedItems(configService.get("image.items", []) || []);
204
284
 
205
- configService.onAnyChange((e: { key: string; value: any }) => {
206
- if (this.isUpdatingConfig) return;
285
+ this.subscriptions.onConfigChange(
286
+ configService,
287
+ (e: { key: string; value: any }) => {
288
+ if (this.isUpdatingConfig) return;
207
289
 
208
- if (e.key === "image.items") {
209
- this.items = this.normalizeItems(e.value || []);
210
- if (!this.isToolActive || !this.hasWorkingChanges) {
211
- this.workingItems = this.cloneItems(this.items);
212
- this.hasWorkingChanges = false;
290
+ if (e.key === "image.items") {
291
+ this.applyCommittedItems(e.value || []);
292
+ this.updateImages();
293
+ return;
213
294
  }
214
- this.updateImages();
215
- return;
216
- }
217
295
 
218
- if (e.key.startsWith("size.") || e.key.startsWith("image.frame.")) {
219
- this.updateImages();
220
- }
221
- });
296
+ if (
297
+ e.key.startsWith("size.") ||
298
+ e.key.startsWith("image.frame.") ||
299
+ e.key.startsWith("image.control.")
300
+ ) {
301
+ if (e.key.startsWith("image.control.")) {
302
+ this.imageControlsByCapabilityKey.clear();
303
+ }
304
+ this.updateImages();
305
+ }
306
+ },
307
+ );
222
308
  }
223
309
 
224
310
  const toolSessionService =
@@ -232,20 +318,16 @@ export class ImageTool implements Extension {
232
318
  }
233
319
 
234
320
  deactivate(context: ExtensionContext) {
235
- context.eventBus.off("tool:activated", this.onToolActivated);
236
- context.eventBus.off("object:modified", this.onObjectModified);
237
- context.eventBus.off("selection:created", this.onSelectionChanged);
238
- context.eventBus.off("selection:updated", this.onSelectionChanged);
239
- context.eventBus.off("selection:cleared", this.onSelectionCleared);
240
- context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
241
- context.eventBus.off("scene:geometry:change", this.onSceneGeometryChanged);
321
+ this.subscriptions.disposeAll();
242
322
  this.dirtyTrackerDisposable?.dispose();
243
323
  this.dirtyTrackerDisposable = undefined;
244
324
  this.cropShapeHatchPattern = undefined;
245
325
  this.cropShapeHatchPatternColor = undefined;
246
326
  this.cropShapeHatchPatternKey = undefined;
327
+ this.sourceSizeCache.clear();
247
328
  this.imageSpecs = [];
248
329
  this.overlaySpecs = [];
330
+ this.imageControlsByCapabilityKey.clear();
249
331
 
250
332
  this.clearRenderedImages();
251
333
  this.renderProducerDisposable?.dispose();
@@ -346,6 +428,113 @@ export class ImageTool implements Extension {
346
428
  );
347
429
  }
348
430
 
431
+ private getEnabledImageControlCapabilities(): ImageControlCapability[] {
432
+ return IMAGE_DEFAULT_CONTROL_CAPABILITIES;
433
+ }
434
+
435
+ private getImageControls(
436
+ capabilities: ImageControlCapability[],
437
+ ): Record<string, Control> {
438
+ const normalized = [...new Set(capabilities)].sort();
439
+ const cacheKey = normalized.join("|");
440
+ const cached = this.imageControlsByCapabilityKey.get(cacheKey);
441
+ if (cached) {
442
+ return cached;
443
+ }
444
+
445
+ const enabled = new Set(normalized);
446
+ const controls: Record<string, Control> = {};
447
+ IMAGE_CONTROL_DESCRIPTORS.forEach((descriptor) => {
448
+ if (!enabled.has(descriptor.capability)) return;
449
+ controls[descriptor.key] = descriptor.create();
450
+ });
451
+
452
+ this.imageControlsByCapabilityKey.set(cacheKey, controls);
453
+ return controls;
454
+ }
455
+
456
+ private getImageControlVisualConfig(): ImageControlVisualConfig {
457
+ const cornerSizeRaw = Number(
458
+ this.getConfig<number>("image.control.cornerSize", 14) ?? 14,
459
+ );
460
+ const touchCornerSizeRaw = Number(
461
+ this.getConfig<number>("image.control.touchCornerSize", 24) ?? 24,
462
+ );
463
+ const borderScaleFactorRaw = Number(
464
+ this.getConfig<number>("image.control.borderScaleFactor", 1.5) ?? 1.5,
465
+ );
466
+ const paddingRaw = Number(
467
+ this.getConfig<number>("image.control.padding", 0) ?? 0,
468
+ );
469
+ const cornerStyleRaw = (this.getConfig<string>(
470
+ "image.control.cornerStyle",
471
+ "circle",
472
+ ) || "circle") as string;
473
+ const cornerStyle: "rect" | "circle" =
474
+ cornerStyleRaw === "rect" ? "rect" : "circle";
475
+
476
+ return {
477
+ cornerSize: Number.isFinite(cornerSizeRaw)
478
+ ? Math.max(4, Math.min(64, cornerSizeRaw))
479
+ : 14,
480
+ touchCornerSize: Number.isFinite(touchCornerSizeRaw)
481
+ ? Math.max(8, Math.min(96, touchCornerSizeRaw))
482
+ : 24,
483
+ cornerStyle,
484
+ cornerColor:
485
+ this.getConfig<string>("image.control.cornerColor", "#ffffff") ||
486
+ "#ffffff",
487
+ cornerStrokeColor:
488
+ this.getConfig<string>("image.control.cornerStrokeColor", "#1677ff") ||
489
+ "#1677ff",
490
+ transparentCorners: !!this.getConfig<boolean>(
491
+ "image.control.transparentCorners",
492
+ false,
493
+ ),
494
+ borderColor:
495
+ this.getConfig<string>("image.control.borderColor", "#1677ff") ||
496
+ "#1677ff",
497
+ borderScaleFactor: Number.isFinite(borderScaleFactorRaw)
498
+ ? Math.max(0.5, Math.min(8, borderScaleFactorRaw))
499
+ : 1.5,
500
+ padding: Number.isFinite(paddingRaw)
501
+ ? Math.max(0, Math.min(64, paddingRaw))
502
+ : 0,
503
+ };
504
+ }
505
+
506
+ private applyImageObjectInteractionState(obj: any) {
507
+ if (!obj) return;
508
+ const visible = this.isImageEditingVisible();
509
+ const visual = this.getImageControlVisualConfig();
510
+ obj.set({
511
+ selectable: visible,
512
+ evented: visible,
513
+ hasControls: visible,
514
+ hasBorders: visible,
515
+ lockScalingFlip: true,
516
+ cornerSize: visual.cornerSize,
517
+ touchCornerSize: visual.touchCornerSize,
518
+ cornerStyle: visual.cornerStyle,
519
+ cornerColor: visual.cornerColor,
520
+ cornerStrokeColor: visual.cornerStrokeColor,
521
+ transparentCorners: visual.transparentCorners,
522
+ borderColor: visual.borderColor,
523
+ borderScaleFactor: visual.borderScaleFactor,
524
+ padding: visual.padding,
525
+ });
526
+ obj.controls = this.getImageControls(
527
+ this.getEnabledImageControlCapabilities(),
528
+ );
529
+ obj.setCoords?.();
530
+ }
531
+
532
+ private refreshImageObjectInteractionState() {
533
+ this.getImageObjects().forEach((obj) =>
534
+ this.applyImageObjectInteractionState(obj),
535
+ );
536
+ }
537
+
349
538
  private isDebugEnabled(): boolean {
350
539
  return !!this.getConfig<boolean>("image.debug", false);
351
540
  }
@@ -377,220 +566,8 @@ export class ImageTool implements Extension {
377
566
  },
378
567
  },
379
568
  ],
380
- [ContributionPointIds.CONFIGURATIONS]: [
381
- {
382
- id: "image.items",
383
- type: "array",
384
- label: "Images",
385
- default: [],
386
- },
387
- {
388
- id: "image.debug",
389
- type: "boolean",
390
- label: "Image Debug Log",
391
- default: false,
392
- },
393
- {
394
- id: "image.frame.strokeColor",
395
- type: "color",
396
- label: "Image Frame Stroke Color",
397
- default: "#808080",
398
- },
399
- {
400
- id: "image.frame.strokeWidth",
401
- type: "number",
402
- label: "Image Frame Stroke Width",
403
- min: 0,
404
- max: 20,
405
- step: 0.5,
406
- default: 2,
407
- },
408
- {
409
- id: "image.frame.strokeStyle",
410
- type: "select",
411
- label: "Image Frame Stroke Style",
412
- options: ["solid", "dashed", "hidden"],
413
- default: "dashed",
414
- },
415
- {
416
- id: "image.frame.dashLength",
417
- type: "number",
418
- label: "Image Frame Dash Length",
419
- min: 1,
420
- max: 40,
421
- step: 1,
422
- default: 8,
423
- },
424
- {
425
- id: "image.frame.innerBackground",
426
- type: "color",
427
- label: "Image Frame Inner Background",
428
- default: "rgba(0,0,0,0)",
429
- },
430
- {
431
- id: "image.frame.outerBackground",
432
- type: "color",
433
- label: "Image Frame Outer Background",
434
- default: "#f5f5f5",
435
- },
436
- ] as ConfigurationContribution[],
437
- [ContributionPointIds.COMMANDS]: [
438
- {
439
- command: "addImage",
440
- title: "Add Image",
441
- handler: async (url: string, options?: Partial<ImageItem>) => {
442
- const result = await this.upsertImageEntry(url, {
443
- mode: "add",
444
- addOptions: options,
445
- });
446
- return result.id;
447
- },
448
- },
449
- {
450
- command: "upsertImage",
451
- title: "Upsert Image",
452
- handler: async (url: string, options: UpsertImageOptions = {}) => {
453
- return await this.upsertImageEntry(url, options);
454
- },
455
- },
456
- {
457
- command: "getWorkingImages",
458
- title: "Get Working Images",
459
- handler: () => {
460
- return this.cloneItems(this.workingItems);
461
- },
462
- },
463
- {
464
- command: "setWorkingImage",
465
- title: "Set Working Image",
466
- handler: (id: string, updates: Partial<ImageItem>) => {
467
- this.updateImageInWorking(id, updates);
468
- },
469
- },
470
- {
471
- command: "resetWorkingImages",
472
- title: "Reset Working Images",
473
- handler: () => {
474
- this.workingItems = this.cloneItems(this.items);
475
- this.hasWorkingChanges = false;
476
- this.updateImages();
477
- this.emitWorkingChange();
478
- },
479
- },
480
- {
481
- command: "completeImages",
482
- title: "Complete Images",
483
- handler: async () => {
484
- return await this.commitWorkingImagesAsCropped();
485
- },
486
- },
487
- {
488
- command: "exportUserCroppedImage",
489
- title: "Export User Cropped Image",
490
- handler: async (options: ExportUserCroppedImageOptions = {}) => {
491
- return await this.exportUserCroppedImage(options);
492
- },
493
- },
494
- {
495
- command: "fitImageToArea",
496
- title: "Fit Image to Area",
497
- handler: async (
498
- id: string,
499
- area: {
500
- width: number;
501
- height: number;
502
- left?: number;
503
- top?: number;
504
- },
505
- ) => {
506
- await this.fitImageToArea(id, area);
507
- },
508
- },
509
- {
510
- command: "fitImageToDefaultArea",
511
- title: "Fit Image to Default Area",
512
- handler: async (id: string) => {
513
- await this.fitImageToDefaultArea(id);
514
- },
515
- },
516
- {
517
- command: "focusImage",
518
- title: "Focus Image",
519
- handler: (
520
- id: string | null,
521
- options: { syncCanvasSelection?: boolean } = {},
522
- ) => {
523
- return this.setImageFocus(id, options);
524
- },
525
- },
526
- {
527
- command: "removeImage",
528
- title: "Remove Image",
529
- handler: (id: string) => {
530
- const removed = this.items.find((item) => item.id === id);
531
- const next = this.items.filter((item) => item.id !== id);
532
- if (next.length !== this.items.length) {
533
- this.purgeSourceSizeCacheForItem(removed);
534
- if (this.focusedImageId === id) {
535
- this.setImageFocus(null, {
536
- syncCanvasSelection: true,
537
- skipRender: true,
538
- });
539
- }
540
- this.updateConfig(next);
541
- }
542
- },
543
- },
544
- {
545
- command: "updateImage",
546
- title: "Update Image",
547
- handler: async (
548
- id: string,
549
- updates: Partial<ImageItem>,
550
- options: UpdateImageOptions = {},
551
- ) => {
552
- await this.updateImage(id, updates, options);
553
- },
554
- },
555
- {
556
- command: "clearImages",
557
- title: "Clear Images",
558
- handler: () => {
559
- this.sourceSizeBySrc.clear();
560
- this.setImageFocus(null, {
561
- syncCanvasSelection: true,
562
- skipRender: true,
563
- });
564
- this.updateConfig([]);
565
- },
566
- },
567
- {
568
- command: "bringToFront",
569
- title: "Bring Image to Front",
570
- handler: (id: string) => {
571
- const index = this.items.findIndex((item) => item.id === id);
572
- if (index !== -1 && index < this.items.length - 1) {
573
- const next = [...this.items];
574
- const [item] = next.splice(index, 1);
575
- next.push(item);
576
- this.updateConfig(next);
577
- }
578
- },
579
- },
580
- {
581
- command: "sendToBack",
582
- title: "Send Image to Back",
583
- handler: (id: string) => {
584
- const index = this.items.findIndex((item) => item.id === id);
585
- if (index > 0) {
586
- const next = [...this.items];
587
- const [item] = next.splice(index, 1);
588
- next.unshift(item);
589
- this.updateConfig(next);
590
- }
591
- },
592
- },
593
- ] as CommandContribution[],
569
+ [ContributionPointIds.CONFIGURATIONS]: createImageConfigurations(),
570
+ [ContributionPointIds.COMMANDS]: createImageCommands(this),
594
571
  };
595
572
  }
596
573
 
@@ -664,12 +641,7 @@ export class ImageTool implements Extension {
664
641
  } else {
665
642
  const obj = this.getImageObject(id);
666
643
  if (obj) {
667
- obj.set({
668
- selectable: true,
669
- evented: true,
670
- hasControls: true,
671
- hasBorders: true,
672
- });
644
+ this.applyImageObjectInteractionState(obj);
673
645
  canvas.setActiveObject(obj);
674
646
  }
675
647
  }
@@ -768,53 +740,46 @@ export class ImageTool implements Extension {
768
740
  return (configService.get(key, fallback) as T) ?? fallback;
769
741
  }
770
742
 
743
+ private applyCommittedItems(nextItems: ImageItem[]) {
744
+ const session = {
745
+ committed: this.items,
746
+ working: this.workingItems,
747
+ hasWorkingChanges: this.hasWorkingChanges,
748
+ };
749
+ applyCommittedSnapshot(session, this.normalizeItems(nextItems), {
750
+ clone: (items) => this.cloneItems(items),
751
+ toolActive: this.isToolActive,
752
+ preserveDirtyWorking: true,
753
+ });
754
+ this.items = session.committed;
755
+ this.workingItems = session.working;
756
+ this.hasWorkingChanges = session.hasWorkingChanges;
757
+ }
758
+
771
759
  private updateConfig(newItems: ImageItem[], skipCanvasUpdate = false) {
772
760
  if (!this.context) return;
761
+ this.applyCommittedItems(newItems);
762
+ runDeferredConfigUpdate(
763
+ this,
764
+ () => {
765
+ const configService = this.context?.services.get<ConfigurationService>(
766
+ "ConfigurationService",
767
+ );
768
+ configService?.update("image.items", this.items);
773
769
 
774
- this.isUpdatingConfig = true;
775
- this.items = this.normalizeItems(newItems);
776
- if (!this.isToolActive || !this.hasWorkingChanges) {
777
- this.workingItems = this.cloneItems(this.items);
778
- this.hasWorkingChanges = false;
779
- }
780
-
781
- const configService = this.context.services.get<ConfigurationService>(
782
- "ConfigurationService",
770
+ if (!skipCanvasUpdate) {
771
+ this.updateImages();
772
+ }
773
+ },
774
+ 50,
783
775
  );
784
- configService?.update("image.items", this.items);
785
-
786
- if (!skipCanvasUpdate) {
787
- this.updateImages();
788
- }
789
-
790
- setTimeout(() => {
791
- this.isUpdatingConfig = false;
792
- }, 50);
793
776
  }
794
777
 
795
778
  private getFrameRect(): FrameRect {
796
- if (!this.canvasService) {
797
- return { left: 0, top: 0, width: 0, height: 0 };
798
- }
799
779
  const configService = this.context?.services.get<ConfigurationService>(
800
780
  "ConfigurationService",
801
781
  );
802
- if (!configService) {
803
- return { left: 0, top: 0, width: 0, height: 0 };
804
- }
805
-
806
- const sizeState = readSizeState(configService);
807
- const layout = computeSceneLayout(this.canvasService, sizeState);
808
- if (!layout) {
809
- return { left: 0, top: 0, width: 0, height: 0 };
810
- }
811
-
812
- return this.canvasService.toSceneRect({
813
- left: layout.cutRect.left,
814
- top: layout.cutRect.top,
815
- width: layout.cutRect.width,
816
- height: layout.cutRect.height,
817
- });
782
+ return resolveCutFrameRect(this.canvasService, configService);
818
783
  }
819
784
 
820
785
  private getFrameRectScreen(frame?: FrameRect): FrameRect {
@@ -825,13 +790,7 @@ export class ImageTool implements Extension {
825
790
  }
826
791
 
827
792
  private toLayoutSceneRect(rect: FrameRect): RenderLayoutRect {
828
- return {
829
- left: rect.left,
830
- top: rect.top,
831
- width: rect.width,
832
- height: rect.height,
833
- space: "scene",
834
- };
793
+ return toSceneLayoutRect(rect);
835
794
  }
836
795
 
837
796
  private async resolveDefaultFitArea(): Promise<DielineFitArea | null> {
@@ -894,26 +853,26 @@ export class ImageTool implements Extension {
894
853
  const sources = [item.url, item.sourceUrl, item.committedUrl].filter(
895
854
  (value): value is string => typeof value === "string" && value.length > 0,
896
855
  );
897
- sources.forEach((src) => this.sourceSizeBySrc.delete(src));
856
+ sources.forEach((src) => this.sourceSizeCache.deleteSourceSize(src));
898
857
  }
899
858
 
900
859
  private rememberSourceSize(src: string, obj: any) {
901
860
  const width = Number(obj?.width || 0);
902
861
  const height = Number(obj?.height || 0);
903
862
  if (src && width > 0 && height > 0) {
904
- this.sourceSizeBySrc.set(src, { width, height });
863
+ this.sourceSizeCache.rememberSourceSize(src, { width, height });
905
864
  }
906
865
  }
907
866
 
908
867
  private getSourceSize(src: string, obj?: any): SourceSize {
909
- const cached = src ? this.sourceSizeBySrc.get(src) : undefined;
868
+ const cached = src ? this.sourceSizeCache.getSourceSize(src) : undefined;
910
869
  if (cached) return cached;
911
870
 
912
871
  const width = Number(obj?.width || 0);
913
872
  const height = Number(obj?.height || 0);
914
873
  if (src && width > 0 && height > 0) {
915
874
  const size = { width, height };
916
- this.sourceSizeBySrc.set(src, size);
875
+ this.sourceSizeCache.rememberSourceSize(src, size);
917
876
  return size;
918
877
  }
919
878
 
@@ -921,10 +880,10 @@ export class ImageTool implements Extension {
921
880
  }
922
881
 
923
882
  private async ensureSourceSize(src: string): Promise<SourceSize | null> {
924
- if (!src) return null;
925
- const cached = this.sourceSizeBySrc.get(src);
926
- if (cached) return cached;
883
+ return this.sourceSizeCache.ensureImageSize(src);
884
+ }
927
885
 
886
+ private async loadImageSize(src: string): Promise<SourceSize | null> {
928
887
  try {
929
888
  const image = await FabricImage.fromURL(src, {
930
889
  crossOrigin: "anonymous",
@@ -932,9 +891,7 @@ export class ImageTool implements Extension {
932
891
  const width = Number(image?.width || 0);
933
892
  const height = Number(image?.height || 0);
934
893
  if (width > 0 && height > 0) {
935
- const size = { width, height };
936
- this.sourceSizeBySrc.set(src, size);
937
- return size;
894
+ return { width, height };
938
895
  }
939
896
  } catch (error) {
940
897
  this.debug("image:size:load-failed", {
@@ -947,11 +904,7 @@ export class ImageTool implements Extension {
947
904
  }
948
905
 
949
906
  private getCoverScale(frame: FrameRect, size: SourceSize): number {
950
- const sw = Math.max(1, size.width);
951
- const sh = Math.max(1, size.height);
952
- const fw = Math.max(1, frame.width);
953
- const fh = Math.max(1, frame.height);
954
- return Math.max(fw / sw, fh / sh);
907
+ return getCoverScaleFromRect(frame, size);
955
908
  }
956
909
 
957
910
  private getFrameVisualConfig(): FrameVisualConfig {
@@ -1608,6 +1561,7 @@ export class ImageTool implements Extension {
1608
1561
  this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1609
1562
  await this.canvasService.flushRenderFromProducers();
1610
1563
  if (seq !== this.renderSeq) return;
1564
+ this.refreshImageObjectInteractionState();
1611
1565
 
1612
1566
  renderItems.forEach((item) => {
1613
1567
  if (!this.getImageObject(item.id)) return;
@@ -1881,7 +1835,7 @@ export class ImageTool implements Extension {
1881
1835
  }),
1882
1836
  );
1883
1837
  if (previousCommitted && previousCommitted !== url) {
1884
- this.sourceSizeBySrc.delete(previousCommitted);
1838
+ this.sourceSizeCache.deleteSourceSize(previousCommitted);
1885
1839
  }
1886
1840
  }
1887
1841