@pooder/kit 3.3.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,58 +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
- 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
261
 
279
262
  return {
280
- layoutScale,
281
- layoutOffsetX,
282
- layoutOffsetY,
283
- visualWidth,
284
- visualHeight,
285
- dielinePhysicalWidth,
286
- dielinePhysicalHeight,
287
- bleedOffset
263
+ layoutScale: 1,
264
+ layoutOffsetX: 0,
265
+ layoutOffsetY: 0,
266
+ visualWidth: canvasW,
267
+ visualHeight: canvasH,
288
268
  };
289
269
  }
290
270
 
@@ -297,7 +277,7 @@ export class ImageTool implements Extension {
297
277
  }
298
278
 
299
279
  // 1. Remove objects that are no longer in items
300
- const currentIds = new Set(this.items.map(i => i.id));
280
+ const currentIds = new Set(this.items.map((i) => i.id));
301
281
  for (const [id, obj] of this.objectMap) {
302
282
  if (!currentIds.has(id)) {
303
283
  layer.remove(obj);
@@ -316,38 +296,38 @@ export class ImageTool implements Extension {
316
296
  this.loadImage(item, layer, layout);
317
297
  } else {
318
298
  // 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`
299
+ // We remove and re-add to ensure coordinates are correctly converted
300
+ // from absolute (updateObjectProperties) to relative (layer.add)
333
301
  layer.remove(obj);
334
- 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);
335
304
  }
336
305
  });
337
-
306
+
338
307
  layer.dirty = true;
339
308
  this.canvasService.requestRenderAll();
340
309
  }
341
310
 
342
- private updateObjectProperties(obj: FabricObject, item: ImageItem, layout: any) {
343
- 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;
344
323
  const updates: any = {};
345
324
 
346
325
  // Opacity
347
326
  if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
348
-
327
+
349
328
  // Angle
350
- 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;
351
331
 
352
332
  // Position (Normalized -> Absolute)
353
333
  if (item.left !== undefined) {
@@ -359,27 +339,27 @@ export class ImageTool implements Extension {
359
339
  if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
360
340
  }
361
341
 
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;
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
+ }
370
349
  }
371
-
350
+
372
351
  // Center origin if not set
373
352
  if (obj.originX !== "center") {
374
353
  updates.originX = "center";
375
354
  updates.originY = "center";
376
355
  // Adjust position because origin changed (Fabric logic)
377
- // 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,
378
357
  // or we can calculate the shift. Ideally we set origin on creation.
379
358
  }
380
359
 
381
360
  if (Object.keys(updates).length > 0) {
382
361
  obj.set(updates);
362
+ obj.setCoords();
383
363
  }
384
364
  }
385
365
 
@@ -387,7 +367,7 @@ export class ImageTool implements Extension {
387
367
  Image.fromURL(item.url, { crossOrigin: "anonymous" })
388
368
  .then((image) => {
389
369
  // Double check if item still exists
390
- if (!this.items.find(i => i.id === item.id)) return;
370
+ if (!this.items.find((i) => i.id === item.id)) return;
391
371
 
392
372
  image.set({
393
373
  originX: "center",
@@ -405,26 +385,11 @@ export class ImageTool implements Extension {
405
385
  });
406
386
 
407
387
  // 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;
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;
428
393
  }
429
394
 
430
395
  if (left === undefined && top === undefined) {
@@ -440,6 +405,13 @@ export class ImageTool implements Extension {
440
405
  layer.add(image);
441
406
  this.objectMap.set(item.id, image);
442
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
+
443
415
  // Bind Events
444
416
  image.on("modified", (e: any) => {
445
417
  this.handleObjectModified(item.id, image);
@@ -447,10 +419,10 @@ export class ImageTool implements Extension {
447
419
 
448
420
  layer.dirty = true;
449
421
  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 });
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);
454
426
  }
455
427
  })
456
428
  .catch((err) => {
@@ -460,7 +432,13 @@ export class ImageTool implements Extension {
460
432
 
461
433
  private handleObjectModified(id: string, image: FabricObject) {
462
434
  const layout = this.getLayoutInfo();
463
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
435
+ const {
436
+ layoutScale,
437
+ layoutOffsetX,
438
+ layoutOffsetY,
439
+ visualWidth,
440
+ visualHeight,
441
+ } = layout;
464
442
 
465
443
  const matrix = image.calcTransformMatrix();
466
444
  const globalPoint = util.transformPoint(new Point(0, 0), matrix);
@@ -472,25 +450,22 @@ export class ImageTool implements Extension {
472
450
  updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
473
451
  updates.angle = image.angle;
474
452
 
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
- }
453
+ // Scale
454
+ updates.scale = image.scaleX / layoutScale;
484
455
 
485
- this.updateImageInConfig(id, updates);
456
+ this.updateImageInConfig(id, updates, true);
486
457
  }
487
458
 
488
- private updateImageInConfig(id: string, updates: Partial<ImageItem>) {
489
- 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);
490
465
  if (index !== -1) {
491
466
  const newItems = [...this.items];
492
467
  newItems[index] = { ...newItems[index], ...updates };
493
- this.updateConfig(newItems, true);
468
+ this.updateConfig(newItems, skipCanvasUpdate);
494
469
  }
495
470
  }
496
471
  }