@pooder/kit 3.3.0 → 3.5.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,496 +1,471 @@
1
- import {
2
- Extension,
3
- ExtensionContext,
4
- ContributionPointIds,
5
- CommandContribution,
6
- ConfigurationContribution,
7
- ConfigurationService,
8
- } from "@pooder/core";
9
- import { Image, Point, util, Object as FabricObject } from "fabric";
10
- import CanvasService from "./CanvasService";
11
- import { Coordinate } from "./coordinate";
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
-
24
- export class ImageTool implements Extension {
25
- id = "pooder.kit.image";
26
-
27
- metadata = {
28
- name: "ImageTool",
29
- };
30
-
31
- private items: ImageItem[] = [];
32
- private objectMap: Map<string, FabricObject> = new Map();
33
- private canvasService?: CanvasService;
34
- private context?: ExtensionContext;
35
- private isUpdatingConfig = false;
36
-
37
- activate(context: ExtensionContext) {
38
- this.context = context;
39
- this.canvasService = context.services.get<CanvasService>("CanvasService");
40
- if (!this.canvasService) {
41
- console.warn("CanvasService not found for ImageTool");
42
- return;
43
- }
44
-
45
- const configService = context.services.get<ConfigurationService>("ConfigurationService");
46
- if (configService) {
47
- // Load initial config
48
- this.items = configService.get("image.items", []) || [];
49
-
50
- // Listen for changes
51
- configService.onAnyChange((e: { key: string; value: any }) => {
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();
66
- }
67
- });
68
- }
69
-
70
- this.ensureLayer();
71
- this.updateImages();
72
- }
73
-
74
- deactivate(context: ExtensionContext) {
75
- if (this.canvasService) {
76
- const layer = this.canvasService.getLayer("user");
77
- if (layer) {
78
- this.objectMap.forEach((obj) => {
79
- layer.remove(obj);
80
- });
81
- this.objectMap.clear();
82
- this.canvasService.requestRenderAll();
83
- }
84
- this.canvasService = undefined;
85
- this.context = undefined;
86
- }
87
- }
88
-
89
- contribute() {
90
- return {
91
- [ContributionPointIds.CONFIGURATIONS]: [
92
- {
93
- id: "image.items",
94
- type: "array",
95
- label: "Images",
96
- default: [],
97
- },
98
- ] as ConfigurationContribution[],
99
- [ContributionPointIds.COMMANDS]: [
100
- {
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
- },
113
- },
114
- {
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
- },
123
- },
124
- {
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
- },
135
- },
136
- {
137
- command: "clearImages",
138
- title: "Clear Images",
139
- handler: () => {
140
- this.updateConfig([]);
141
- },
142
- },
143
- {
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
- },
155
- },
156
- {
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
- }
167
- },
168
- },
169
- ] as CommandContribution[],
170
- };
171
- }
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
-
197
- private ensureLayer() {
198
- if (!this.canvasService) return;
199
- let userLayer = this.canvasService.getLayer("user");
200
- if (!userLayer) {
201
- userLayer = this.canvasService.createLayer("user", {
202
- width: this.canvasService.canvas.width,
203
- height: this.canvasService.canvas.height,
204
- left: 0,
205
- top: 0,
206
- originX: "left",
207
- originY: "top",
208
- selectable: false,
209
- evented: true,
210
- subTargetCheck: true,
211
- interactive: true,
212
- });
213
-
214
- // Try to insert below dieline-overlay
215
- const dielineLayer = this.canvasService.getLayer("dieline-overlay");
216
- if (dielineLayer) {
217
- const index = this.canvasService.canvas.getObjects().indexOf(dielineLayer);
218
- // If dieline is at 0, move user to 0 (dieline shifts to 1)
219
- if (index >= 0) {
220
- this.canvasService.canvas.moveObjectTo(userLayer, index);
221
- }
222
- } else {
223
- // Ensure background is behind
224
- const bgLayer = this.canvasService.getLayer("background");
225
- if (bgLayer) {
226
- this.canvasService.canvas.sendObjectToBack(bgLayer);
227
- }
228
- }
229
- this.canvasService.requestRenderAll();
230
- }
231
- }
232
-
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
- }
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;
293
- const layer = this.canvasService.getLayer("user");
294
- if (!layer) {
295
- console.warn("[ImageTool] User layer not found");
296
- return;
297
- }
298
-
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
- }
307
-
308
- // 2. Add or Update objects
309
- const layout = this.getLayoutInfo();
310
-
311
- this.items.forEach((item, index) => {
312
- let obj = this.objectMap.get(item.id);
313
-
314
- if (!obj) {
315
- // New object, load it
316
- this.loadImage(item, layer, layout);
317
- } else {
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
- }
341
-
342
- private updateObjectProperties(obj: FabricObject, item: ImageItem, layout: any) {
343
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
344
- const updates: any = {};
345
-
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;
351
-
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
- }
361
-
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.
379
- }
380
-
381
- if (Object.keys(updates).length > 0) {
382
- obj.set(updates);
383
- }
384
- }
385
-
386
- private loadImage(item: ImageItem, layer: any, layout: any) {
387
- Image.fromURL(item.url, { crossOrigin: "anonymous" })
388
- .then((image) => {
389
- // Double check if item still exists
390
- if (!this.items.find(i => i.id === item.id)) return;
391
-
392
- image.set({
393
- originX: "center",
394
- originY: "center",
395
- data: { id: item.id },
396
- uniformScaling: true,
397
- lockScalingFlip: true,
398
- });
399
-
400
- image.setControlsVisibility({
401
- mt: false,
402
- mb: false,
403
- ml: false,
404
- mr: false,
405
- });
406
-
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
- }
429
-
430
- if (left === undefined && top === undefined) {
431
- left = 0.5;
432
- top = 0.5;
433
- item.left = left;
434
- item.top = top;
435
- }
436
-
437
- // Apply Props
438
- this.updateObjectProperties(image, item, layout);
439
-
440
- layer.add(image);
441
- this.objectMap.set(item.id, image);
442
-
443
- // Bind Events
444
- image.on("modified", (e: any) => {
445
- this.handleObjectModified(item.id, image);
446
- });
447
-
448
- layer.dirty = true;
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
- }
455
- })
456
- .catch((err) => {
457
- console.error("Failed to load image", item.url, err);
458
- });
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
- }
496
- }
1
+ import {
2
+ Extension,
3
+ ExtensionContext,
4
+ ContributionPointIds,
5
+ CommandContribution,
6
+ ConfigurationContribution,
7
+ ConfigurationService,
8
+ } from "@pooder/core";
9
+ import { Image, Point, util, Object as FabricObject } from "fabric";
10
+ import CanvasService from "./CanvasService";
11
+ import { Coordinate } from "./coordinate";
12
+
13
+ export interface ImageItem {
14
+ id: string;
15
+ url: string;
16
+ opacity: number;
17
+ scale?: number;
18
+ angle?: number;
19
+ left?: number;
20
+ top?: number;
21
+ }
22
+
23
+ export class ImageTool implements Extension {
24
+ id = "pooder.kit.image";
25
+
26
+ metadata = {
27
+ name: "ImageTool",
28
+ };
29
+
30
+ private items: ImageItem[] = [];
31
+ private objectMap: Map<string, FabricObject> = new Map();
32
+ private loadResolvers: Map<string, () => void> = new Map();
33
+ private canvasService?: CanvasService;
34
+ private context?: ExtensionContext;
35
+ private isUpdatingConfig = false;
36
+
37
+ activate(context: ExtensionContext) {
38
+ this.context = context;
39
+ this.canvasService = context.services.get<CanvasService>("CanvasService");
40
+ if (!this.canvasService) {
41
+ console.warn("CanvasService not found for ImageTool");
42
+ return;
43
+ }
44
+
45
+ const configService = context.services.get<ConfigurationService>(
46
+ "ConfigurationService",
47
+ );
48
+ if (configService) {
49
+ // Load initial config
50
+ this.items = configService.get("image.items", []) || [];
51
+
52
+ // Listen for changes
53
+ configService.onAnyChange((e: { key: string; value: any }) => {
54
+ if (this.isUpdatingConfig) return;
55
+
56
+ if (e.key === "image.items") {
57
+ this.items = e.value || [];
58
+ this.updateImages();
59
+ }
60
+ });
61
+ }
62
+
63
+ this.ensureLayer();
64
+ this.updateImages();
65
+ }
66
+
67
+ deactivate(context: ExtensionContext) {
68
+ if (this.canvasService) {
69
+ const layer = this.canvasService.getLayer("user");
70
+ if (layer) {
71
+ this.objectMap.forEach((obj) => {
72
+ layer.remove(obj);
73
+ });
74
+ this.objectMap.clear();
75
+ this.canvasService.requestRenderAll();
76
+ }
77
+ this.canvasService = undefined;
78
+ this.context = undefined;
79
+ }
80
+ }
81
+
82
+ contribute() {
83
+ return {
84
+ [ContributionPointIds.CONFIGURATIONS]: [
85
+ {
86
+ id: "image.items",
87
+ type: "array",
88
+ label: "Images",
89
+ default: [],
90
+ },
91
+ ] as ConfigurationContribution[],
92
+ [ContributionPointIds.COMMANDS]: [
93
+ {
94
+ command: "addImage",
95
+ title: "Add Image",
96
+ handler: async (url: string, options?: Partial<ImageItem>) => {
97
+ const id = this.generateId();
98
+ const newItem: ImageItem = {
99
+ id,
100
+ url,
101
+ opacity: 1,
102
+ ...options,
103
+ };
104
+
105
+ const promise = new Promise<string>((resolve) => {
106
+ this.loadResolvers.set(id, () => resolve(id));
107
+ });
108
+
109
+ this.updateConfig([...this.items, newItem]);
110
+ return promise;
111
+ },
112
+ },
113
+ {
114
+ command: "fitImageToArea",
115
+ title: "Fit Image to Area",
116
+ handler: (
117
+ id: string,
118
+ area: { width: number; height: number; left?: number; top?: number },
119
+ ) => {
120
+ const item = this.items.find((i) => i.id === id);
121
+ const obj = this.objectMap.get(id);
122
+ if (item && obj && obj.width && obj.height) {
123
+ const scale = Math.max(
124
+ area.width / obj.width,
125
+ area.height / obj.height,
126
+ );
127
+ this.updateImageInConfig(id, {
128
+ scale,
129
+ left: area.left ?? 0.5,
130
+ top: area.top ?? 0.5,
131
+ });
132
+ }
133
+ },
134
+ },
135
+ {
136
+ command: "removeImage",
137
+ title: "Remove Image",
138
+ handler: (id: string) => {
139
+ const newItems = this.items.filter((item) => item.id !== id);
140
+ if (newItems.length !== this.items.length) {
141
+ this.updateConfig(newItems);
142
+ }
143
+ },
144
+ },
145
+ {
146
+ command: "updateImage",
147
+ title: "Update Image",
148
+ handler: (id: string, updates: Partial<ImageItem>) => {
149
+ const index = this.items.findIndex((item) => item.id === id);
150
+ if (index !== -1) {
151
+ const newItems = [...this.items];
152
+ newItems[index] = { ...newItems[index], ...updates };
153
+ this.updateConfig(newItems);
154
+ }
155
+ },
156
+ },
157
+ {
158
+ command: "clearImages",
159
+ title: "Clear Images",
160
+ handler: () => {
161
+ this.updateConfig([]);
162
+ },
163
+ },
164
+ {
165
+ command: "bringToFront",
166
+ title: "Bring Image to Front",
167
+ handler: (id: string) => {
168
+ const index = this.items.findIndex((item) => item.id === id);
169
+ if (index !== -1 && index < this.items.length - 1) {
170
+ const newItems = [...this.items];
171
+ const [item] = newItems.splice(index, 1);
172
+ newItems.push(item);
173
+ this.updateConfig(newItems);
174
+ }
175
+ },
176
+ },
177
+ {
178
+ command: "sendToBack",
179
+ title: "Send Image to Back",
180
+ handler: (id: string) => {
181
+ const index = this.items.findIndex((item) => item.id === id);
182
+ if (index > 0) {
183
+ const newItems = [...this.items];
184
+ const [item] = newItems.splice(index, 1);
185
+ newItems.unshift(item);
186
+ this.updateConfig(newItems);
187
+ }
188
+ },
189
+ },
190
+ ] as CommandContribution[],
191
+ };
192
+ }
193
+
194
+ private generateId(): string {
195
+ return Math.random().toString(36).substring(2, 9);
196
+ }
197
+
198
+ private updateConfig(newItems: ImageItem[], skipCanvasUpdate = false) {
199
+ if (!this.context) return;
200
+ this.isUpdatingConfig = true;
201
+ this.items = newItems;
202
+ const configService = this.context.services.get<ConfigurationService>(
203
+ "ConfigurationService",
204
+ );
205
+ if (configService) {
206
+ configService.update("image.items", newItems);
207
+ }
208
+ // Update canvas immediately to reflect changes locally before config event comes back
209
+ // (Optional, but good for responsiveness)
210
+ if (!skipCanvasUpdate) {
211
+ this.updateImages();
212
+ }
213
+
214
+ // Reset flag after a short delay to allow config propagation
215
+ setTimeout(() => {
216
+ this.isUpdatingConfig = false;
217
+ }, 50);
218
+ }
219
+
220
+ private ensureLayer() {
221
+ if (!this.canvasService) return;
222
+ let userLayer = this.canvasService.getLayer("user");
223
+ if (!userLayer) {
224
+ userLayer = this.canvasService.createLayer("user", {
225
+ width: this.canvasService.canvas.width,
226
+ height: this.canvasService.canvas.height,
227
+ left: 0,
228
+ top: 0,
229
+ originX: "left",
230
+ originY: "top",
231
+ selectable: false,
232
+ evented: true,
233
+ subTargetCheck: true,
234
+ interactive: true,
235
+ });
236
+
237
+ // Try to insert below dieline-overlay
238
+ const dielineLayer = this.canvasService.getLayer("dieline-overlay");
239
+ if (dielineLayer) {
240
+ const index = this.canvasService.canvas
241
+ .getObjects()
242
+ .indexOf(dielineLayer);
243
+ // If dieline is at 0, move user to 0 (dieline shifts to 1)
244
+ if (index >= 0) {
245
+ this.canvasService.canvas.moveObjectTo(userLayer, index);
246
+ }
247
+ } else {
248
+ // Ensure background is behind
249
+ const bgLayer = this.canvasService.getLayer("background");
250
+ if (bgLayer) {
251
+ this.canvasService.canvas.sendObjectToBack(bgLayer);
252
+ }
253
+ }
254
+ this.canvasService.requestRenderAll();
255
+ }
256
+ }
257
+
258
+ private getLayoutInfo() {
259
+ const canvasW = this.canvasService?.canvas.width || 800;
260
+ const canvasH = this.canvasService?.canvas.height || 600;
261
+
262
+ return {
263
+ layoutScale: 1,
264
+ layoutOffsetX: 0,
265
+ layoutOffsetY: 0,
266
+ visualWidth: canvasW,
267
+ visualHeight: canvasH,
268
+ };
269
+ }
270
+
271
+ private updateImages() {
272
+ if (!this.canvasService) return;
273
+ const layer = this.canvasService.getLayer("user");
274
+ if (!layer) {
275
+ console.warn("[ImageTool] User layer not found");
276
+ return;
277
+ }
278
+
279
+ // 1. Remove objects that are no longer in items
280
+ const currentIds = new Set(this.items.map((i) => i.id));
281
+ for (const [id, obj] of this.objectMap) {
282
+ if (!currentIds.has(id)) {
283
+ layer.remove(obj);
284
+ this.objectMap.delete(id);
285
+ }
286
+ }
287
+
288
+ // 2. Add or Update objects
289
+ const layout = this.getLayoutInfo();
290
+
291
+ this.items.forEach((item, index) => {
292
+ let obj = this.objectMap.get(item.id);
293
+
294
+ if (!obj) {
295
+ // New object, load it
296
+ this.loadImage(item, layer, layout);
297
+ } else {
298
+ // Existing object, update properties
299
+ // We remove and re-add to ensure coordinates are correctly converted
300
+ // from absolute (updateObjectProperties) to relative (layer.add)
301
+ layer.remove(obj);
302
+ this.updateObjectProperties(obj, item, layout);
303
+ layer.add(obj);
304
+ }
305
+ });
306
+
307
+ layer.dirty = true;
308
+ this.canvasService.requestRenderAll();
309
+ }
310
+
311
+ private updateObjectProperties(
312
+ obj: FabricObject,
313
+ item: ImageItem,
314
+ layout: any,
315
+ ) {
316
+ const {
317
+ layoutScale,
318
+ layoutOffsetX,
319
+ layoutOffsetY,
320
+ visualWidth,
321
+ visualHeight,
322
+ } = layout;
323
+ const updates: any = {};
324
+
325
+ // Opacity
326
+ if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
327
+
328
+ // Angle
329
+ if (item.angle !== undefined && obj.angle !== item.angle)
330
+ updates.angle = item.angle;
331
+
332
+ // Position (Normalized -> Absolute)
333
+ if (item.left !== undefined) {
334
+ const globalLeft = layoutOffsetX + item.left * visualWidth;
335
+ if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
336
+ }
337
+ if (item.top !== undefined) {
338
+ const globalTop = layoutOffsetY + item.top * visualHeight;
339
+ if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
340
+ }
341
+
342
+ // Scale
343
+ if (item.scale !== undefined) {
344
+ const targetScale = item.scale * layoutScale;
345
+ if (Math.abs(obj.scaleX - targetScale) > 0.001) {
346
+ updates.scaleX = targetScale;
347
+ updates.scaleY = targetScale;
348
+ }
349
+ }
350
+
351
+ // Center origin if not set
352
+ if (obj.originX !== "center") {
353
+ updates.originX = "center";
354
+ updates.originY = "center";
355
+ // Adjust position because origin changed (Fabric logic)
356
+ // For simplicity, we just set it, next cycle will fix pos if needed,
357
+ // or we can calculate the shift. Ideally we set origin on creation.
358
+ }
359
+
360
+ if (Object.keys(updates).length > 0) {
361
+ obj.set(updates);
362
+ obj.setCoords();
363
+ }
364
+ }
365
+
366
+ private loadImage(item: ImageItem, layer: any, layout: any) {
367
+ Image.fromURL(item.url, { crossOrigin: "anonymous" })
368
+ .then((image) => {
369
+ // Double check if item still exists
370
+ if (!this.items.find((i) => i.id === item.id)) return;
371
+
372
+ image.set({
373
+ originX: "center",
374
+ originY: "center",
375
+ data: { id: item.id },
376
+ uniformScaling: true,
377
+ lockScalingFlip: true,
378
+ });
379
+
380
+ image.setControlsVisibility({
381
+ mt: false,
382
+ mb: false,
383
+ ml: false,
384
+ mr: false,
385
+ });
386
+
387
+ // Initial Layout
388
+ let { scale, left, top } = item;
389
+
390
+ if (scale === undefined) {
391
+ scale = 1; // Default scale if not provided and not fitted yet
392
+ item.scale = scale;
393
+ }
394
+
395
+ if (left === undefined && top === undefined) {
396
+ left = 0.5;
397
+ top = 0.5;
398
+ item.left = left;
399
+ item.top = top;
400
+ }
401
+
402
+ // Apply Props
403
+ this.updateObjectProperties(image, item, layout);
404
+
405
+ layer.add(image);
406
+ this.objectMap.set(item.id, image);
407
+
408
+ // Notify addImage that load is complete
409
+ const resolver = this.loadResolvers.get(item.id);
410
+ if (resolver) {
411
+ resolver();
412
+ this.loadResolvers.delete(item.id);
413
+ }
414
+
415
+ // Bind Events
416
+ image.on("modified", (e: any) => {
417
+ this.handleObjectModified(item.id, image);
418
+ });
419
+
420
+ layer.dirty = true;
421
+ this.canvasService?.requestRenderAll();
422
+
423
+ // Save defaults if we set them
424
+ if (item.scale !== scale || item.left !== left || item.top !== top) {
425
+ this.updateImageInConfig(item.id, { scale, left, top }, true);
426
+ }
427
+ })
428
+ .catch((err) => {
429
+ console.error("Failed to load image", item.url, err);
430
+ });
431
+ }
432
+
433
+ private handleObjectModified(id: string, image: FabricObject) {
434
+ const layout = this.getLayoutInfo();
435
+ const {
436
+ layoutScale,
437
+ layoutOffsetX,
438
+ layoutOffsetY,
439
+ visualWidth,
440
+ visualHeight,
441
+ } = layout;
442
+
443
+ const matrix = image.calcTransformMatrix();
444
+ const globalPoint = util.transformPoint(new Point(0, 0), matrix);
445
+
446
+ const updates: Partial<ImageItem> = {};
447
+
448
+ // Normalize Position
449
+ updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
450
+ updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
451
+ updates.angle = image.angle;
452
+
453
+ // Scale
454
+ updates.scale = image.scaleX / layoutScale;
455
+
456
+ this.updateImageInConfig(id, updates, true);
457
+ }
458
+
459
+ private updateImageInConfig(
460
+ id: string,
461
+ updates: Partial<ImageItem>,
462
+ skipCanvasUpdate = false,
463
+ ) {
464
+ const index = this.items.findIndex((i) => i.id === id);
465
+ if (index !== -1) {
466
+ const newItems = [...this.items];
467
+ newItems[index] = { ...newItems[index], ...updates };
468
+ this.updateConfig(newItems, skipCanvasUpdate);
469
+ }
470
+ }
471
+ }