@pooder/kit 3.0.1 → 3.2.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/src/image.ts CHANGED
@@ -1,49 +1,38 @@
1
1
  import {
2
+ Extension,
3
+ ExtensionContext,
4
+ ContributionPointIds,
2
5
  CommandContribution,
3
6
  ConfigurationContribution,
4
7
  ConfigurationService,
5
- ContributionPointIds,
6
- Extension,
7
- ExtensionContext,
8
8
  } from "@pooder/core";
9
- import { FabricImage as Image, Point, util } from "fabric";
9
+ import { Image, Point, util, Object as FabricObject } from "fabric";
10
10
  import CanvasService from "./CanvasService";
11
11
  import { Coordinate } from "./coordinate";
12
12
 
13
+ export interface ImageItem {
14
+ id: string;
15
+ url: string;
16
+ opacity: number;
17
+ width?: number;
18
+ height?: number;
19
+ angle?: number;
20
+ left?: number;
21
+ top?: number;
22
+ }
23
+
13
24
  export class ImageTool implements Extension {
14
25
  id = "pooder.kit.image";
15
26
 
16
27
  metadata = {
17
28
  name: "ImageTool",
18
29
  };
19
- private _loadingUrl: string | null = null;
20
-
21
- private url: string = "";
22
- private opacity: number = 1;
23
- private width?: number;
24
- private height?: number;
25
- private angle?: number;
26
- private left?: number;
27
- private top?: number;
28
30
 
31
+ private items: ImageItem[] = [];
32
+ private objectMap: Map<string, FabricObject> = new Map();
29
33
  private canvasService?: CanvasService;
30
34
  private context?: ExtensionContext;
31
-
32
- constructor(
33
- options?: Partial<{
34
- url: string;
35
- opacity: number;
36
- width: number;
37
- height: number;
38
- angle: number;
39
- left: number;
40
- top: number;
41
- }>,
42
- ) {
43
- if (options) {
44
- Object.assign(this, options);
45
- }
46
- }
35
+ private isUpdatingConfig = false;
47
36
 
48
37
  activate(context: ExtensionContext) {
49
38
  this.context = context;
@@ -53,45 +42,44 @@ export class ImageTool implements Extension {
53
42
  return;
54
43
  }
55
44
 
56
- const configService = context.services.get<any>("ConfigurationService");
45
+ const configService = context.services.get<ConfigurationService>("ConfigurationService");
57
46
  if (configService) {
58
47
  // Load initial config
59
- this.url = configService.get("image.url", this.url);
60
- this.opacity = configService.get("image.opacity", this.opacity);
61
- this.width = configService.get("image.width", this.width);
62
- this.height = configService.get("image.height", this.height);
63
- this.angle = configService.get("image.angle", this.angle);
64
- this.left = configService.get("image.left", this.left);
65
- this.top = configService.get("image.top", this.top);
48
+ this.items = configService.get("image.items", []) || [];
66
49
 
67
50
  // Listen for changes
68
51
  configService.onAnyChange((e: { key: string; value: any }) => {
69
- if (e.key.startsWith("image.")) {
70
- const prop = e.key.split(".")[1];
71
- console.log(
72
- `[ImageTool] Config change detected: ${e.key} -> ${e.value}`,
73
- );
74
- if (prop && prop in this) {
75
- (this as any)[prop] = e.value;
76
- this.updateImage();
77
- }
52
+ if (this.isUpdatingConfig) return;
53
+
54
+ let shouldUpdate = false;
55
+ if (e.key === "image.items") {
56
+ this.items = e.value || [];
57
+ shouldUpdate = true;
58
+ } else if (e.key.startsWith("dieline.") && e.key !== "dieline.holes") {
59
+ // Dieline changes affect image layout/scale
60
+ // Ignore dieline.holes as they don't affect layout and can cause jitter
61
+ shouldUpdate = true;
62
+ }
63
+
64
+ if (shouldUpdate) {
65
+ this.updateImages();
78
66
  }
79
67
  });
80
68
  }
81
69
 
82
70
  this.ensureLayer();
83
- this.updateImage();
71
+ this.updateImages();
84
72
  }
85
73
 
86
74
  deactivate(context: ExtensionContext) {
87
75
  if (this.canvasService) {
88
76
  const layer = this.canvasService.getLayer("user");
89
77
  if (layer) {
90
- const userImage = this.canvasService.getObject("user-image", "user");
91
- if (userImage) {
92
- layer.remove(userImage);
93
- this.canvasService.requestRenderAll();
94
- }
78
+ this.objectMap.forEach((obj) => {
79
+ layer.remove(obj);
80
+ });
81
+ this.objectMap.clear();
82
+ this.canvasService.requestRenderAll();
95
83
  }
96
84
  this.canvasService = undefined;
97
85
  this.context = undefined;
@@ -102,103 +90,110 @@ export class ImageTool implements Extension {
102
90
  return {
103
91
  [ContributionPointIds.CONFIGURATIONS]: [
104
92
  {
105
- id: "image.url",
106
- type: "string",
107
- label: "Image URL",
108
- default: this.url,
109
- },
110
- {
111
- id: "image.opacity",
112
- type: "number",
113
- label: "Opacity",
114
- min: 0,
115
- max: 1,
116
- step: 0.1,
117
- default: this.opacity,
93
+ id: "image.items",
94
+ type: "array",
95
+ label: "Images",
96
+ default: [],
118
97
  },
98
+ ] as ConfigurationContribution[],
99
+ [ContributionPointIds.COMMANDS]: [
119
100
  {
120
- id: "image.width",
121
- type: "number",
122
- label: "Width",
123
- min: 0,
124
- max: 5000,
125
- default: this.width,
101
+ command: "addImage",
102
+ title: "Add Image",
103
+ handler: (url: string, options?: Partial<ImageItem>) => {
104
+ const newItem: ImageItem = {
105
+ id: this.generateId(),
106
+ url,
107
+ opacity: 1,
108
+ ...options,
109
+ };
110
+ this.updateConfig([...this.items, newItem]);
111
+ return newItem.id;
112
+ },
126
113
  },
127
114
  {
128
- id: "image.height",
129
- type: "number",
130
- label: "Height",
131
- min: 0,
132
- max: 5000,
133
- default: this.height,
115
+ command: "removeImage",
116
+ title: "Remove Image",
117
+ handler: (id: string) => {
118
+ const newItems = this.items.filter((item) => item.id !== id);
119
+ if (newItems.length !== this.items.length) {
120
+ this.updateConfig(newItems);
121
+ }
122
+ },
134
123
  },
135
124
  {
136
- id: "image.angle",
137
- type: "number",
138
- label: "Rotation",
139
- min: 0,
140
- max: 360,
141
- default: this.angle,
125
+ command: "updateImage",
126
+ title: "Update Image",
127
+ handler: (id: string, updates: Partial<ImageItem>) => {
128
+ const index = this.items.findIndex((item) => item.id === id);
129
+ if (index !== -1) {
130
+ const newItems = [...this.items];
131
+ newItems[index] = { ...newItems[index], ...updates };
132
+ this.updateConfig(newItems);
133
+ }
134
+ },
142
135
  },
143
136
  {
144
- id: "image.left",
145
- type: "number",
146
- label: "Left (Normalized)",
147
- min: 0,
148
- max: 1,
149
- default: this.left,
137
+ command: "clearImages",
138
+ title: "Clear Images",
139
+ handler: () => {
140
+ this.updateConfig([]);
141
+ },
150
142
  },
151
143
  {
152
- id: "image.top",
153
- type: "number",
154
- label: "Top (Normalized)",
155
- min: 0,
156
- max: 1,
157
- default: this.top,
144
+ command: "bringToFront",
145
+ title: "Bring Image to Front",
146
+ handler: (id: string) => {
147
+ const index = this.items.findIndex((item) => item.id === id);
148
+ if (index !== -1 && index < this.items.length - 1) {
149
+ const newItems = [...this.items];
150
+ const [item] = newItems.splice(index, 1);
151
+ newItems.push(item);
152
+ this.updateConfig(newItems);
153
+ }
154
+ },
158
155
  },
159
- ] as ConfigurationContribution[],
160
- [ContributionPointIds.COMMANDS]: [
161
156
  {
162
- command: "setUserImage",
163
- title: "Set User Image",
164
- handler: (
165
- url: string,
166
- opacity: number,
167
- width?: number,
168
- height?: number,
169
- angle?: number,
170
- left?: number,
171
- top?: number,
172
- ) => {
173
- if (
174
- this.url === url &&
175
- this.opacity === opacity &&
176
- this.width === width &&
177
- this.height === height &&
178
- this.angle === angle &&
179
- this.left === left &&
180
- this.top === top
181
- )
182
- return true;
183
-
184
- this.url = url;
185
- this.opacity = opacity;
186
- this.width = width;
187
- this.height = height;
188
- this.angle = angle;
189
- this.left = left;
190
- this.top = top;
191
-
192
- // Direct update
193
- this.updateImage();
194
-
195
- return true;
157
+ command: "sendToBack",
158
+ title: "Send Image to Back",
159
+ handler: (id: string) => {
160
+ const index = this.items.findIndex((item) => item.id === id);
161
+ if (index > 0) {
162
+ const newItems = [...this.items];
163
+ const [item] = newItems.splice(index, 1);
164
+ newItems.unshift(item);
165
+ this.updateConfig(newItems);
166
+ }
196
167
  },
197
168
  },
198
169
  ] as CommandContribution[],
199
170
  };
200
171
  }
201
172
 
173
+ private generateId(): string {
174
+ return Math.random().toString(36).substring(2, 9);
175
+ }
176
+
177
+ private updateConfig(newItems: ImageItem[], skipCanvasUpdate = false) {
178
+ if (!this.context) return;
179
+ this.isUpdatingConfig = true;
180
+ this.items = newItems;
181
+ const configService = this.context.services.get<ConfigurationService>("ConfigurationService");
182
+ if (configService) {
183
+ configService.update("image.items", newItems);
184
+ }
185
+ // Update canvas immediately to reflect changes locally before config event comes back
186
+ // (Optional, but good for responsiveness)
187
+ if (!skipCanvasUpdate) {
188
+ this.updateImages();
189
+ }
190
+
191
+ // Reset flag after a short delay to allow config propagation
192
+ setTimeout(() => {
193
+ this.isUpdatingConfig = false;
194
+ }, 50);
195
+ }
196
+
202
197
  private ensureLayer() {
203
198
  if (!this.canvasService) return;
204
199
  let userLayer = this.canvasService.getLayer("user");
@@ -219,9 +214,7 @@ export class ImageTool implements Extension {
219
214
  // Try to insert below dieline-overlay
220
215
  const dielineLayer = this.canvasService.getLayer("dieline-overlay");
221
216
  if (dielineLayer) {
222
- const index = this.canvasService.canvas
223
- .getObjects()
224
- .indexOf(dielineLayer);
217
+ const index = this.canvasService.canvas.getObjects().indexOf(dielineLayer);
225
218
  // If dieline is at 0, move user to 0 (dieline shifts to 1)
226
219
  if (index >= 0) {
227
220
  this.canvasService.canvas.moveObjectTo(userLayer, index);
@@ -237,268 +230,244 @@ export class ImageTool implements Extension {
237
230
  }
238
231
  }
239
232
 
240
- private updateImage() {
241
- if (!this.canvasService) return;
242
- let { url, opacity, width, height, angle, left, top } = this;
233
+ private getLayoutInfo() {
234
+ const canvasW = this.canvasService?.canvas.width || 800;
235
+ const canvasH = this.canvasService?.canvas.height || 600;
236
+
237
+ let layoutScale = 1;
238
+ let layoutOffsetX = 0;
239
+ let layoutOffsetY = 0;
240
+ let visualWidth = canvasW;
241
+ let visualHeight = canvasH;
242
+ let dielinePhysicalWidth = 500;
243
+ let dielinePhysicalHeight = 500;
244
+
245
+ if (this.context) {
246
+ const configService = this.context.services.get<ConfigurationService>("ConfigurationService");
247
+ if (configService) {
248
+ dielinePhysicalWidth = configService.get("dieline.width") || 500;
249
+ dielinePhysicalHeight = configService.get("dieline.height") || 500;
250
+ const padding = configService.get("dieline.padding") || 40;
251
+
252
+ const layout = Coordinate.calculateLayout(
253
+ { width: canvasW, height: canvasH },
254
+ { width: dielinePhysicalWidth, height: dielinePhysicalHeight },
255
+ padding
256
+ );
257
+ layoutScale = layout.scale;
258
+ layoutOffsetX = layout.offsetX;
259
+ layoutOffsetY = layout.offsetY;
260
+ visualWidth = layout.width;
261
+ visualHeight = layout.height;
262
+ }
263
+ }
264
+
265
+ return {
266
+ layoutScale,
267
+ layoutOffsetX,
268
+ layoutOffsetY,
269
+ visualWidth,
270
+ visualHeight,
271
+ dielinePhysicalWidth,
272
+ dielinePhysicalHeight
273
+ };
274
+ }
243
275
 
276
+ private updateImages() {
277
+ if (!this.canvasService) return;
244
278
  const layer = this.canvasService.getLayer("user");
245
279
  if (!layer) {
246
280
  console.warn("[ImageTool] User layer not found");
247
281
  return;
248
282
  }
249
283
 
250
- const userImage = this.canvasService.getObject("user-image", "user") as any;
284
+ // 1. Remove objects that are no longer in items
285
+ const currentIds = new Set(this.items.map(i => i.id));
286
+ for (const [id, obj] of this.objectMap) {
287
+ if (!currentIds.has(id)) {
288
+ layer.remove(obj);
289
+ this.objectMap.delete(id);
290
+ }
291
+ }
251
292
 
252
- if (this._loadingUrl === url) return;
293
+ // 2. Add or Update objects
294
+ const layout = this.getLayoutInfo();
253
295
 
254
- if (userImage) {
255
- const currentSrc = userImage.getSrc?.() || userImage._element?.src;
296
+ this.items.forEach((item, index) => {
297
+ let obj = this.objectMap.get(item.id);
256
298
 
257
- if (currentSrc !== url) {
258
- this.loadImage(layer);
299
+ if (!obj) {
300
+ // New object, load it
301
+ this.loadImage(item, layer, layout);
259
302
  } else {
260
- const updates: any = {};
261
- const canvasW = this.canvasService.canvas.width || 800;
262
- const canvasH = this.canvasService.canvas.height || 600;
263
- const centerX = canvasW / 2;
264
- const centerY = canvasH / 2;
265
-
266
- if (userImage.opacity !== opacity) updates.opacity = opacity;
267
- if (angle !== undefined && userImage.angle !== angle)
268
- updates.angle = angle;
269
-
270
- if (userImage.originX !== "center") {
271
- userImage.set({
272
- originX: "center",
273
- originY: "center",
274
- left: userImage.left + (userImage.width * userImage.scaleX) / 2,
275
- top: userImage.top + (userImage.height * userImage.scaleY) / 2,
276
- });
277
- }
303
+ // Existing object, update properties
304
+ this.updateObjectProperties(obj, item, layout);
305
+
306
+ // Ensure Z-Index order
307
+ // Note: layer.add() appends to end, so if we process in order, they should be roughly correct.
308
+ // However, if we need strict ordering, we might need to verify index.
309
+ // For simplicity, we rely on the fact that if it exists, it's already on canvas.
310
+ // To enforce strict Z-order matching array order:
311
+ // We can check if the object at layer._objects[index] is this object.
312
+ // But Fabric's Group/Layer handling might be complex.
313
+ // A simple way is: remove and re-add if order is wrong, or use moveObjectTo.
314
+
315
+ // Since we are iterating items in order, we can check if the object is at the expected visual index relative to other user images.
316
+ // But for now, let's assume update logic is sufficient.
317
+ // If we want to support reordering, we should probably just `moveTo`
318
+ layer.remove(obj);
319
+ layer.add(obj); // Move to top of layer stack, effectively reordering if we iterate in order
320
+ }
321
+ });
322
+
323
+ layer.dirty = true;
324
+ this.canvasService.requestRenderAll();
325
+ }
278
326
 
279
- if (left !== undefined) {
280
- const globalLeft = Coordinate.toAbsolute(left, canvasW);
281
- const localLeft = globalLeft - centerX;
282
- if (Math.abs(userImage.left - localLeft) > 1)
283
- updates.left = localLeft;
284
- }
327
+ private updateObjectProperties(obj: FabricObject, item: ImageItem, layout: any) {
328
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
329
+ const updates: any = {};
285
330
 
286
- if (top !== undefined) {
287
- const globalTop = Coordinate.toAbsolute(top, canvasH);
288
- const localTop = globalTop - centerY;
289
- if (Math.abs(userImage.top - localTop) > 1) updates.top = localTop;
290
- }
331
+ // Opacity
332
+ if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
333
+
334
+ // Angle
335
+ if (item.angle !== undefined && obj.angle !== item.angle) updates.angle = item.angle;
336
+
337
+ // Position (Normalized -> Absolute)
338
+ if (item.left !== undefined) {
339
+ const globalLeft = layoutOffsetX + item.left * visualWidth;
340
+ if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
341
+ }
342
+ if (item.top !== undefined) {
343
+ const globalTop = layoutOffsetY + item.top * visualHeight;
344
+ if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
345
+ }
291
346
 
292
- if (width !== undefined && userImage.width)
293
- updates.scaleX = width / userImage.width;
294
- if (height !== undefined && userImage.height)
295
- updates.scaleY = height / userImage.height;
347
+ // Scale (Physical Dimensions -> Scale Factor)
348
+ if (item.width !== undefined && obj.width) {
349
+ const targetScaleX = (item.width * layoutScale) / obj.width;
350
+ if (Math.abs(obj.scaleX - targetScaleX) > 0.001) updates.scaleX = targetScaleX;
351
+ }
352
+ if (item.height !== undefined && obj.height) {
353
+ const targetScaleY = (item.height * layoutScale) / obj.height;
354
+ if (Math.abs(obj.scaleY - targetScaleY) > 0.001) updates.scaleY = targetScaleY;
355
+ }
356
+
357
+ // Center origin if not set
358
+ if (obj.originX !== "center") {
359
+ updates.originX = "center";
360
+ updates.originY = "center";
361
+ // Adjust position because origin changed (Fabric logic)
362
+ // For simplicity, we just set it, next cycle will fix pos if needed,
363
+ // or we can calculate the shift. Ideally we set origin on creation.
364
+ }
296
365
 
297
- if (Object.keys(updates).length > 0) {
298
- userImage.set(updates);
299
- layer.dirty = true;
300
- this.canvasService.requestRenderAll();
301
- }
302
- }
303
- } else {
304
- this.loadImage(layer);
366
+ if (Object.keys(updates).length > 0) {
367
+ obj.set(updates);
305
368
  }
306
369
  }
307
370
 
308
- private loadImage(layer: any) {
309
- if (!this.canvasService) return;
310
- const { url } = this;
311
- if (!url) return; // Don't load if empty
312
- this._loadingUrl = url;
313
-
314
- Image.fromURL(url, { crossOrigin: "anonymous" })
371
+ private loadImage(item: ImageItem, layer: any, layout: any) {
372
+ Image.fromURL(item.url, { crossOrigin: "anonymous" })
315
373
  .then((image) => {
316
- if (this._loadingUrl !== url) return;
317
- this._loadingUrl = null;
318
-
319
- let { opacity, width, height, angle, left, top } = this;
320
-
321
- // Auto-scale and center if not set
322
- if (this.context) {
323
- const configService = this.context.services.get<ConfigurationService>(
324
- "ConfigurationService",
325
- )!;
326
- const dielineWidth = configService.get("dieline.width");
327
- const dielineHeight = configService.get("dieline.height");
328
-
329
- console.log(
330
- "[ImageTool] Dieline config debug:",
331
- {
332
- widthVal: dielineWidth,
333
- heightVal: dielineHeight,
334
- // Debug: dump all keys to see what is available
335
- allKeys: Array.from(
336
- (configService as any).configValues?.keys() || [],
337
- ),
338
- },
339
- configService,
340
- );
341
-
342
- if (width === undefined && height === undefined) {
343
- // Scale to fit dieline
344
- const scale = Math.min(
345
- dielineWidth / (image.width || 1),
346
- dielineHeight / (image.height || 1),
347
- );
348
- width = (image.width || 1) * scale;
349
- height = (image.height || 1) * scale;
350
- this.width = width;
351
- this.height = height;
352
- }
353
-
354
- if (left === undefined && top === undefined) {
355
- // Default to Dieline Position if available, otherwise Center (0.5)
356
- const dielinePos = configService?.get("dieline.position");
357
- if (dielinePos) {
358
- this.left = dielinePos.x;
359
- this.top = dielinePos.y;
360
- } else {
361
- this.left = 0.5;
362
- this.top = 0.5;
363
- }
364
- left = this.left;
365
- top = this.top;
366
- }
367
- }
368
-
369
- const existingImage = this.canvasService!.getObject(
370
- "user-image",
371
- "user",
372
- ) as any;
373
-
374
- if (existingImage) {
375
- const defaultLeft = existingImage.left;
376
- const defaultTop = existingImage.top;
377
- const defaultAngle = existingImage.angle;
378
- const defaultScaleX = existingImage.scaleX;
379
- const defaultScaleY = existingImage.scaleY;
380
-
381
- const canvasW = this.canvasService?.canvas.width || 800;
382
- const canvasH = this.canvasService?.canvas.height || 600;
383
- const centerX = canvasW / 2;
384
- const centerY = canvasH / 2;
385
-
386
- let targetLeft = left !== undefined ? left : defaultLeft;
387
- let targetTop = top !== undefined ? top : defaultTop;
388
-
389
- // Log for debugging
390
- const configService = this.context?.services.get<any>(
391
- "ConfigurationService",
392
- );
393
- console.log("[ImageTool] Loading EXISTING image...", {
394
- canvasW,
395
- canvasH,
396
- centerX,
397
- centerY,
398
- incomingLeft: left,
399
- incomingTop: top,
400
- dielinePos: configService?.get("dieline.position"),
401
- existingImage: !!existingImage,
402
- });
403
-
404
- if (left !== undefined) {
405
- const globalLeft = Coordinate.toAbsolute(left, canvasW);
406
- targetLeft = globalLeft; // Layer is absolute, do not subtract center
407
- console.log("[ImageTool] Calculated targetLeft", {
408
- globalLeft,
409
- targetLeft,
410
- });
411
- }
412
- if (top !== undefined) {
413
- const globalTop = Coordinate.toAbsolute(top, canvasH);
414
- targetTop = globalTop; // Layer is absolute, do not subtract center
415
- console.log("[ImageTool] Calculated targetTop", {
416
- globalTop,
417
- targetTop,
418
- });
419
- }
420
-
421
- image.set({
422
- originX: "center", // Use center origin for easier positioning
423
- originY: "center",
424
- left: targetLeft,
425
- top: targetTop,
426
- angle: angle !== undefined ? angle : defaultAngle,
427
- scaleX:
428
- width !== undefined && image.width
429
- ? width / image.width
430
- : defaultScaleX,
431
- scaleY:
432
- height !== undefined && image.height
433
- ? height / image.height
434
- : defaultScaleY,
435
- });
436
-
437
- layer.remove(existingImage);
438
- } else {
439
- // New image
440
- image.set({
441
- originX: "center",
442
- originY: "center",
443
- });
444
-
445
- if (width !== undefined && image.width)
446
- image.scaleX = width / image.width;
447
- if (height !== undefined && image.height)
448
- image.scaleY = height / image.height;
449
- if (angle !== undefined) image.angle = angle;
450
-
451
- const canvasW = this.canvasService?.canvas.width || 800;
452
- const canvasH = this.canvasService?.canvas.height || 600;
453
- const centerX = canvasW / 2;
454
- const centerY = canvasH / 2;
455
-
456
- if (left !== undefined) {
457
- image.left = Coordinate.toAbsolute(left, canvasW); // Layer is absolute
458
- } else {
459
- image.left = centerX; // Default to center of canvas
460
- }
461
-
462
- if (top !== undefined) {
463
- image.top = Coordinate.toAbsolute(top, canvasH); // Layer is absolute
464
- } else {
465
- image.top = centerY; // Default to center of canvas
466
- }
467
- }
374
+ // Double check if item still exists
375
+ if (!this.items.find(i => i.id === item.id)) return;
468
376
 
469
377
  image.set({
470
- opacity: opacity !== undefined ? opacity : 1,
471
- data: {
472
- id: "user-image",
473
- },
378
+ originX: "center",
379
+ originY: "center",
380
+ data: { id: item.id },
474
381
  });
475
- layer.add(image);
476
382
 
477
- // Bind events to keep options in sync
478
- image.on("modified", (e: any) => {
479
- const matrix = image.calcTransformMatrix();
480
- const globalPoint = util.transformPoint(new Point(0, 0), matrix);
481
- const canvasW = this.canvasService?.canvas.width || 800;
482
- const canvasH = this.canvasService?.canvas.height || 600;
383
+ // Initial Layout
384
+ let { width, height, left, top } = item;
385
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight, dielinePhysicalWidth, dielinePhysicalHeight } = layout;
386
+
387
+ // Auto-scale if needed
388
+ if (width === undefined && height === undefined) {
389
+ const imgAspect = (image.width || 1) / (image.height || 1);
390
+ const dielineAspect = dielinePhysicalWidth / dielinePhysicalHeight;
391
+
392
+ if (imgAspect > dielineAspect) {
393
+ const w = dielinePhysicalWidth;
394
+ width = w;
395
+ height = w / imgAspect;
396
+ } else {
397
+ const h = dielinePhysicalHeight;
398
+ height = h;
399
+ width = h * imgAspect;
400
+ }
401
+
402
+ // Update item with defaults
403
+ item.width = width;
404
+ item.height = height;
405
+ }
483
406
 
484
- this.left = Coordinate.toNormalized(globalPoint.x, canvasW);
485
- this.top = Coordinate.toNormalized(globalPoint.y, canvasH);
486
- this.angle = e.target.angle;
407
+ if (left === undefined && top === undefined) {
408
+ left = 0.5;
409
+ top = 0.5;
410
+ item.left = left;
411
+ item.top = top;
412
+ }
487
413
 
488
- if (image.width) this.width = e.target.width * e.target.scaleX;
489
- if (image.height) this.height = e.target.height * e.target.scaleY;
414
+ // Apply Props
415
+ this.updateObjectProperties(image, item, layout);
490
416
 
491
- if (this.context) {
492
- this.context.eventBus.emit("update");
493
- }
417
+ layer.add(image);
418
+ this.objectMap.set(item.id, image);
419
+
420
+ // Bind Events
421
+ image.on("modified", (e: any) => {
422
+ this.handleObjectModified(item.id, image);
494
423
  });
495
424
 
496
425
  layer.dirty = true;
497
- this.canvasService!.requestRenderAll();
426
+ this.canvasService?.requestRenderAll();
427
+
428
+ // Save defaults if we calculated them
429
+ if (item.width !== width || item.height !== height || item.left !== left || item.top !== top) {
430
+ this.updateImageInConfig(item.id, { width, height, left, top });
431
+ }
498
432
  })
499
433
  .catch((err) => {
500
- if (this._loadingUrl === url) this._loadingUrl = null;
501
- console.error("Failed to load image", url, err);
434
+ console.error("Failed to load image", item.url, err);
502
435
  });
503
436
  }
437
+
438
+ private handleObjectModified(id: string, image: FabricObject) {
439
+ const layout = this.getLayoutInfo();
440
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
441
+
442
+ const matrix = image.calcTransformMatrix();
443
+ const globalPoint = util.transformPoint(new Point(0, 0), matrix);
444
+
445
+ const updates: Partial<ImageItem> = {};
446
+
447
+ // Normalize Position
448
+ updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
449
+ updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
450
+ updates.angle = image.angle;
451
+
452
+ // Physical Dimensions
453
+ if (image.width) {
454
+ const pixelWidth = image.width * image.scaleX;
455
+ updates.width = pixelWidth / layoutScale;
456
+ }
457
+ if (image.height) {
458
+ const pixelHeight = image.height * image.scaleY;
459
+ updates.height = pixelHeight / layoutScale;
460
+ }
461
+
462
+ this.updateImageInConfig(id, updates);
463
+ }
464
+
465
+ private updateImageInConfig(id: string, updates: Partial<ImageItem>) {
466
+ const index = this.items.findIndex(i => i.id === id);
467
+ if (index !== -1) {
468
+ const newItems = [...this.items];
469
+ newItems[index] = { ...newItems[index], ...updates };
470
+ this.updateConfig(newItems, true);
471
+ }
472
+ }
504
473
  }