@pooder/kit 3.1.0 → 3.3.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,267 @@ 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
+ let bleedOffset = 0;
245
+
246
+ if (this.context) {
247
+ const configService = this.context.services.get<ConfigurationService>("ConfigurationService");
248
+ if (configService) {
249
+ dielinePhysicalWidth = configService.get("dieline.width") || 500;
250
+ dielinePhysicalHeight = configService.get("dieline.height") || 500;
251
+ bleedOffset = configService.get("dieline.offset") || 0;
252
+
253
+ const paddingValue = configService.get("dieline.padding") || 40;
254
+ let padding = 0;
255
+ if (typeof paddingValue === "number") {
256
+ padding = paddingValue;
257
+ } else if (typeof paddingValue === "string") {
258
+ if (paddingValue.endsWith("%")) {
259
+ const percent = parseFloat(paddingValue) / 100;
260
+ padding = Math.min(canvasW, canvasH) * percent;
261
+ } else {
262
+ padding = parseFloat(paddingValue) || 0;
263
+ }
264
+ }
265
+
266
+ const layout = Coordinate.calculateLayout(
267
+ { width: canvasW, height: canvasH },
268
+ { width: dielinePhysicalWidth, height: dielinePhysicalHeight },
269
+ padding
270
+ );
271
+ layoutScale = layout.scale;
272
+ layoutOffsetX = layout.offsetX;
273
+ layoutOffsetY = layout.offsetY;
274
+ visualWidth = layout.width;
275
+ visualHeight = layout.height;
276
+ }
277
+ }
243
278
 
279
+ return {
280
+ layoutScale,
281
+ layoutOffsetX,
282
+ layoutOffsetY,
283
+ visualWidth,
284
+ visualHeight,
285
+ dielinePhysicalWidth,
286
+ dielinePhysicalHeight,
287
+ bleedOffset
288
+ };
289
+ }
290
+
291
+ private updateImages() {
292
+ if (!this.canvasService) return;
244
293
  const layer = this.canvasService.getLayer("user");
245
294
  if (!layer) {
246
295
  console.warn("[ImageTool] User layer not found");
247
296
  return;
248
297
  }
249
298
 
250
- const userImage = this.canvasService.getObject("user-image", "user") as any;
299
+ // 1. Remove objects that are no longer in items
300
+ const currentIds = new Set(this.items.map(i => i.id));
301
+ for (const [id, obj] of this.objectMap) {
302
+ if (!currentIds.has(id)) {
303
+ layer.remove(obj);
304
+ this.objectMap.delete(id);
305
+ }
306
+ }
251
307
 
252
- if (this._loadingUrl === url) return;
308
+ // 2. Add or Update objects
309
+ const layout = this.getLayoutInfo();
253
310
 
254
- if (userImage) {
255
- const currentSrc = userImage.getSrc?.() || userImage._element?.src;
311
+ this.items.forEach((item, index) => {
312
+ let obj = this.objectMap.get(item.id);
256
313
 
257
- if (currentSrc !== url) {
258
- this.loadImage(layer);
314
+ if (!obj) {
315
+ // New object, load it
316
+ this.loadImage(item, layer, layout);
259
317
  } 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
- }
318
+ // Existing object, update properties
319
+ this.updateObjectProperties(obj, item, layout);
320
+
321
+ // Ensure Z-Index order
322
+ // Note: layer.add() appends to end, so if we process in order, they should be roughly correct.
323
+ // However, if we need strict ordering, we might need to verify index.
324
+ // For simplicity, we rely on the fact that if it exists, it's already on canvas.
325
+ // To enforce strict Z-order matching array order:
326
+ // We can check if the object at layer._objects[index] is this object.
327
+ // But Fabric's Group/Layer handling might be complex.
328
+ // A simple way is: remove and re-add if order is wrong, or use moveObjectTo.
329
+
330
+ // Since we are iterating items in order, we can check if the object is at the expected visual index relative to other user images.
331
+ // But for now, let's assume update logic is sufficient.
332
+ // If we want to support reordering, we should probably just `moveTo`
333
+ layer.remove(obj);
334
+ layer.add(obj); // Move to top of layer stack, effectively reordering if we iterate in order
335
+ }
336
+ });
337
+
338
+ layer.dirty = true;
339
+ this.canvasService.requestRenderAll();
340
+ }
278
341
 
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
- }
342
+ private updateObjectProperties(obj: FabricObject, item: ImageItem, layout: any) {
343
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
344
+ const updates: any = {};
285
345
 
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
- }
346
+ // Opacity
347
+ if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
348
+
349
+ // Angle
350
+ if (item.angle !== undefined && obj.angle !== item.angle) updates.angle = item.angle;
291
351
 
292
- if (width !== undefined && userImage.width)
293
- updates.scaleX = width / userImage.width;
294
- if (height !== undefined && userImage.height)
295
- updates.scaleY = height / userImage.height;
352
+ // Position (Normalized -> Absolute)
353
+ if (item.left !== undefined) {
354
+ const globalLeft = layoutOffsetX + item.left * visualWidth;
355
+ if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
356
+ }
357
+ if (item.top !== undefined) {
358
+ const globalTop = layoutOffsetY + item.top * visualHeight;
359
+ if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
360
+ }
296
361
 
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);
362
+ // Scale (Physical Dimensions -> Scale Factor)
363
+ if (item.width !== undefined && obj.width) {
364
+ const targetScaleX = (item.width * layoutScale) / obj.width;
365
+ if (Math.abs(obj.scaleX - targetScaleX) > 0.001) updates.scaleX = targetScaleX;
366
+ }
367
+ if (item.height !== undefined && obj.height) {
368
+ const targetScaleY = (item.height * layoutScale) / obj.height;
369
+ if (Math.abs(obj.scaleY - targetScaleY) > 0.001) updates.scaleY = targetScaleY;
370
+ }
371
+
372
+ // Center origin if not set
373
+ if (obj.originX !== "center") {
374
+ updates.originX = "center";
375
+ updates.originY = "center";
376
+ // Adjust position because origin changed (Fabric logic)
377
+ // For simplicity, we just set it, next cycle will fix pos if needed,
378
+ // or we can calculate the shift. Ideally we set origin on creation.
305
379
  }
306
- }
307
380
 
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;
381
+ if (Object.keys(updates).length > 0) {
382
+ obj.set(updates);
383
+ }
384
+ }
313
385
 
314
- Image.fromURL(url, { crossOrigin: "anonymous" })
386
+ private loadImage(item: ImageItem, layer: any, layout: any) {
387
+ Image.fromURL(item.url, { crossOrigin: "anonymous" })
315
388
  .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
- }
389
+ // Double check if item still exists
390
+ if (!this.items.find(i => i.id === item.id)) return;
353
391
 
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
- }
392
+ image.set({
393
+ originX: "center",
394
+ originY: "center",
395
+ data: { id: item.id },
396
+ uniformScaling: true,
397
+ lockScalingFlip: true,
398
+ });
368
399
 
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
- }
400
+ image.setControlsVisibility({
401
+ mt: false,
402
+ mb: false,
403
+ ml: false,
404
+ mr: false,
405
+ });
420
406
 
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
- }
407
+ // Initial Layout
408
+ let { width, height, left, top } = item;
409
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight, dielinePhysicalWidth, dielinePhysicalHeight, bleedOffset } = layout;
410
+
411
+ // Auto-scale if needed
412
+ if (width === undefined && height === undefined) {
413
+ // Calculate target dimensions including bleed
414
+ const targetWidth = dielinePhysicalWidth + 2 * bleedOffset;
415
+ const targetHeight = dielinePhysicalHeight + 2 * bleedOffset;
416
+
417
+ // "适应最长边" (Fit to longest side) logic
418
+ const targetMax = Math.max(targetWidth, targetHeight);
419
+ const imageMax = Math.max(image.width || 1, image.height || 1);
420
+ const scale = targetMax / imageMax;
421
+
422
+ width = (image.width || 1) * scale;
423
+ height = (image.height || 1) * scale;
424
+
425
+ // Update item with defaults
426
+ item.width = width;
427
+ item.height = height;
428
+ }
461
429
 
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
- }
430
+ if (left === undefined && top === undefined) {
431
+ left = 0.5;
432
+ top = 0.5;
433
+ item.left = left;
434
+ item.top = top;
467
435
  }
468
436
 
469
- image.set({
470
- opacity: opacity !== undefined ? opacity : 1,
471
- data: {
472
- id: "user-image",
473
- },
474
- });
437
+ // Apply Props
438
+ this.updateObjectProperties(image, item, layout);
439
+
475
440
  layer.add(image);
441
+ this.objectMap.set(item.id, image);
476
442
 
477
- // Bind events to keep options in sync
443
+ // Bind Events
478
444
  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;
483
-
484
- this.left = Coordinate.toNormalized(globalPoint.x, canvasW);
485
- this.top = Coordinate.toNormalized(globalPoint.y, canvasH);
486
- this.angle = e.target.angle;
487
-
488
- if (image.width) this.width = e.target.width * e.target.scaleX;
489
- if (image.height) this.height = e.target.height * e.target.scaleY;
490
-
491
- if (this.context) {
492
- this.context.eventBus.emit("update");
493
- }
445
+ this.handleObjectModified(item.id, image);
494
446
  });
495
447
 
496
448
  layer.dirty = true;
497
- this.canvasService!.requestRenderAll();
449
+ this.canvasService?.requestRenderAll();
450
+
451
+ // Save defaults if we calculated them
452
+ if (item.width !== width || item.height !== height || item.left !== left || item.top !== top) {
453
+ this.updateImageInConfig(item.id, { width, height, left, top });
454
+ }
498
455
  })
499
456
  .catch((err) => {
500
- if (this._loadingUrl === url) this._loadingUrl = null;
501
- console.error("Failed to load image", url, err);
457
+ console.error("Failed to load image", item.url, err);
502
458
  });
503
459
  }
460
+
461
+ private handleObjectModified(id: string, image: FabricObject) {
462
+ const layout = this.getLayoutInfo();
463
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
464
+
465
+ const matrix = image.calcTransformMatrix();
466
+ const globalPoint = util.transformPoint(new Point(0, 0), matrix);
467
+
468
+ const updates: Partial<ImageItem> = {};
469
+
470
+ // Normalize Position
471
+ updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
472
+ updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
473
+ updates.angle = image.angle;
474
+
475
+ // Physical Dimensions
476
+ if (image.width) {
477
+ const pixelWidth = image.width * image.scaleX;
478
+ updates.width = pixelWidth / layoutScale;
479
+ }
480
+ if (image.height) {
481
+ const pixelHeight = image.height * image.scaleY;
482
+ updates.height = pixelHeight / layoutScale;
483
+ }
484
+
485
+ this.updateImageInConfig(id, updates);
486
+ }
487
+
488
+ private updateImageInConfig(id: string, updates: Partial<ImageItem>) {
489
+ const index = this.items.findIndex(i => i.id === id);
490
+ if (index !== -1) {
491
+ const newItems = [...this.items];
492
+ newItems[index] = { ...newItems[index], ...updates };
493
+ this.updateConfig(newItems, true);
494
+ }
495
+ }
504
496
  }