@pooder/kit 3.2.0 → 3.4.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
@@ -14,8 +14,7 @@ export interface ImageItem {
14
14
  id: string;
15
15
  url: string;
16
16
  opacity: number;
17
- width?: number;
18
- height?: number;
17
+ scale?: number;
19
18
  angle?: number;
20
19
  left?: number;
21
20
  top?: number;
@@ -30,6 +29,7 @@ export class ImageTool implements Extension {
30
29
 
31
30
  private items: ImageItem[] = [];
32
31
  private objectMap: Map<string, FabricObject> = new Map();
32
+ private loadResolvers: Map<string, () => void> = new Map();
33
33
  private canvasService?: CanvasService;
34
34
  private context?: ExtensionContext;
35
35
  private isUpdatingConfig = false;
@@ -42,7 +42,9 @@ export class ImageTool implements Extension {
42
42
  return;
43
43
  }
44
44
 
45
- const configService = context.services.get<ConfigurationService>("ConfigurationService");
45
+ const configService = context.services.get<ConfigurationService>(
46
+ "ConfigurationService",
47
+ );
46
48
  if (configService) {
47
49
  // Load initial config
48
50
  this.items = configService.get("image.items", []) || [];
@@ -51,17 +53,8 @@ export class ImageTool implements Extension {
51
53
  configService.onAnyChange((e: { key: string; value: any }) => {
52
54
  if (this.isUpdatingConfig) return;
53
55
 
54
- let shouldUpdate = false;
55
56
  if (e.key === "image.items") {
56
57
  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
58
  this.updateImages();
66
59
  }
67
60
  });
@@ -100,15 +93,43 @@ export class ImageTool implements Extension {
100
93
  {
101
94
  command: "addImage",
102
95
  title: "Add Image",
103
- handler: (url: string, options?: Partial<ImageItem>) => {
96
+ handler: async (url: string, options?: Partial<ImageItem>) => {
97
+ const id = this.generateId();
104
98
  const newItem: ImageItem = {
105
- id: this.generateId(),
99
+ id,
106
100
  url,
107
101
  opacity: 1,
108
102
  ...options,
109
103
  };
104
+
105
+ const promise = new Promise<string>((resolve) => {
106
+ this.loadResolvers.set(id, () => resolve(id));
107
+ });
108
+
110
109
  this.updateConfig([...this.items, newItem]);
111
- return newItem.id;
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
+ }
112
133
  },
113
134
  },
114
135
  {
@@ -178,7 +199,9 @@ export class ImageTool implements Extension {
178
199
  if (!this.context) return;
179
200
  this.isUpdatingConfig = true;
180
201
  this.items = newItems;
181
- const configService = this.context.services.get<ConfigurationService>("ConfigurationService");
202
+ const configService = this.context.services.get<ConfigurationService>(
203
+ "ConfigurationService",
204
+ );
182
205
  if (configService) {
183
206
  configService.update("image.items", newItems);
184
207
  }
@@ -187,7 +210,7 @@ export class ImageTool implements Extension {
187
210
  if (!skipCanvasUpdate) {
188
211
  this.updateImages();
189
212
  }
190
-
213
+
191
214
  // Reset flag after a short delay to allow config propagation
192
215
  setTimeout(() => {
193
216
  this.isUpdatingConfig = false;
@@ -214,7 +237,9 @@ export class ImageTool implements Extension {
214
237
  // Try to insert below dieline-overlay
215
238
  const dielineLayer = this.canvasService.getLayer("dieline-overlay");
216
239
  if (dielineLayer) {
217
- const index = this.canvasService.canvas.getObjects().indexOf(dielineLayer);
240
+ const index = this.canvasService.canvas
241
+ .getObjects()
242
+ .indexOf(dielineLayer);
218
243
  // If dieline is at 0, move user to 0 (dieline shifts to 1)
219
244
  if (index >= 0) {
220
245
  this.canvasService.canvas.moveObjectTo(userLayer, index);
@@ -233,43 +258,13 @@ export class ImageTool implements Extension {
233
258
  private getLayoutInfo() {
234
259
  const canvasW = this.canvasService?.canvas.width || 800;
235
260
  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
261
 
265
262
  return {
266
- layoutScale,
267
- layoutOffsetX,
268
- layoutOffsetY,
269
- visualWidth,
270
- visualHeight,
271
- dielinePhysicalWidth,
272
- dielinePhysicalHeight
263
+ layoutScale: 1,
264
+ layoutOffsetX: 0,
265
+ layoutOffsetY: 0,
266
+ visualWidth: canvasW,
267
+ visualHeight: canvasH,
273
268
  };
274
269
  }
275
270
 
@@ -282,7 +277,7 @@ export class ImageTool implements Extension {
282
277
  }
283
278
 
284
279
  // 1. Remove objects that are no longer in items
285
- const currentIds = new Set(this.items.map(i => i.id));
280
+ const currentIds = new Set(this.items.map((i) => i.id));
286
281
  for (const [id, obj] of this.objectMap) {
287
282
  if (!currentIds.has(id)) {
288
283
  layer.remove(obj);
@@ -301,38 +296,38 @@ export class ImageTool implements Extension {
301
296
  this.loadImage(item, layer, layout);
302
297
  } else {
303
298
  // 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`
299
+ // We remove and re-add to ensure coordinates are correctly converted
300
+ // from absolute (updateObjectProperties) to relative (layer.add)
318
301
  layer.remove(obj);
319
- layer.add(obj); // Move to top of layer stack, effectively reordering if we iterate in order
302
+ this.updateObjectProperties(obj, item, layout);
303
+ layer.add(obj);
320
304
  }
321
305
  });
322
-
306
+
323
307
  layer.dirty = true;
324
308
  this.canvasService.requestRenderAll();
325
309
  }
326
310
 
327
- private updateObjectProperties(obj: FabricObject, item: ImageItem, layout: any) {
328
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
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;
329
323
  const updates: any = {};
330
324
 
331
325
  // Opacity
332
326
  if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
333
-
327
+
334
328
  // Angle
335
- if (item.angle !== undefined && obj.angle !== item.angle) updates.angle = item.angle;
329
+ if (item.angle !== undefined && obj.angle !== item.angle)
330
+ updates.angle = item.angle;
336
331
 
337
332
  // Position (Normalized -> Absolute)
338
333
  if (item.left !== undefined) {
@@ -344,27 +339,27 @@ export class ImageTool implements Extension {
344
339
  if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
345
340
  }
346
341
 
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;
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
+ }
355
349
  }
356
-
350
+
357
351
  // Center origin if not set
358
352
  if (obj.originX !== "center") {
359
353
  updates.originX = "center";
360
354
  updates.originY = "center";
361
355
  // Adjust position because origin changed (Fabric logic)
362
- // For simplicity, we just set it, next cycle will fix pos if needed,
356
+ // For simplicity, we just set it, next cycle will fix pos if needed,
363
357
  // or we can calculate the shift. Ideally we set origin on creation.
364
358
  }
365
359
 
366
360
  if (Object.keys(updates).length > 0) {
367
361
  obj.set(updates);
362
+ obj.setCoords();
368
363
  }
369
364
  }
370
365
 
@@ -372,36 +367,29 @@ export class ImageTool implements Extension {
372
367
  Image.fromURL(item.url, { crossOrigin: "anonymous" })
373
368
  .then((image) => {
374
369
  // Double check if item still exists
375
- if (!this.items.find(i => i.id === item.id)) return;
370
+ if (!this.items.find((i) => i.id === item.id)) return;
376
371
 
377
372
  image.set({
378
373
  originX: "center",
379
374
  originY: "center",
380
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,
381
385
  });
382
386
 
383
387
  // 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;
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;
405
393
  }
406
394
 
407
395
  if (left === undefined && top === undefined) {
@@ -417,6 +405,13 @@ export class ImageTool implements Extension {
417
405
  layer.add(image);
418
406
  this.objectMap.set(item.id, image);
419
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
+
420
415
  // Bind Events
421
416
  image.on("modified", (e: any) => {
422
417
  this.handleObjectModified(item.id, image);
@@ -424,10 +419,10 @@ export class ImageTool implements Extension {
424
419
 
425
420
  layer.dirty = true;
426
421
  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 });
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);
431
426
  }
432
427
  })
433
428
  .catch((err) => {
@@ -437,7 +432,13 @@ export class ImageTool implements Extension {
437
432
 
438
433
  private handleObjectModified(id: string, image: FabricObject) {
439
434
  const layout = this.getLayoutInfo();
440
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
435
+ const {
436
+ layoutScale,
437
+ layoutOffsetX,
438
+ layoutOffsetY,
439
+ visualWidth,
440
+ visualHeight,
441
+ } = layout;
441
442
 
442
443
  const matrix = image.calcTransformMatrix();
443
444
  const globalPoint = util.transformPoint(new Point(0, 0), matrix);
@@ -449,25 +450,22 @@ export class ImageTool implements Extension {
449
450
  updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
450
451
  updates.angle = image.angle;
451
452
 
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
- }
453
+ // Scale
454
+ updates.scale = image.scaleX / layoutScale;
461
455
 
462
- this.updateImageInConfig(id, updates);
456
+ this.updateImageInConfig(id, updates, true);
463
457
  }
464
458
 
465
- private updateImageInConfig(id: string, updates: Partial<ImageItem>) {
466
- const index = this.items.findIndex(i => i.id === id);
459
+ private updateImageInConfig(
460
+ id: string,
461
+ updates: Partial<ImageItem>,
462
+ skipCanvasUpdate = false,
463
+ ) {
464
+ const index = this.items.findIndex((i) => i.id === id);
467
465
  if (index !== -1) {
468
466
  const newItems = [...this.items];
469
467
  newItems[index] = { ...newItems[index], ...updates };
470
- this.updateConfig(newItems, true);
468
+ this.updateConfig(newItems, skipCanvasUpdate);
471
469
  }
472
470
  }
473
471
  }