@pooder/kit 3.4.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,471 +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
- 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
- }
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
+ }