@pooder/kit 2.0.0 → 3.0.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,102 +1,211 @@
1
1
  import {
2
- Command,
3
- Editor,
4
- EditorState,
2
+ CommandContribution,
3
+ ConfigurationContribution,
4
+ ConfigurationService,
5
+ ContributionPointIds,
5
6
  Extension,
6
- Image,
7
- OptionSchema,
8
- PooderLayer,
9
- util,
10
- Point,
7
+ ExtensionContext,
11
8
  } from "@pooder/core";
9
+ import { FabricImage as Image, Point, util } from "fabric";
10
+ import CanvasService from "./CanvasService";
11
+ import { Coordinate } from "./coordinate";
12
12
 
13
- interface ImageToolOptions {
14
- url: string;
15
- opacity: number;
16
- width?: number;
17
- height?: number;
18
- angle?: number;
19
- left?: number;
20
- top?: number;
21
- }
22
- export class ImageTool implements Extension<ImageToolOptions> {
23
- name = "ImageTool";
24
- private _loadingUrl: string | null = null;
25
- options: ImageToolOptions = {
26
- url: "",
27
- opacity: 1,
28
- };
13
+ export class ImageTool implements Extension {
14
+ id = "pooder.kit.image";
29
15
 
30
- public schema: Record<keyof ImageToolOptions, OptionSchema> = {
31
- url: {
32
- type: "string",
33
- label: "Image URL",
34
- },
35
- opacity: {
36
- type: "number",
37
- min: 0,
38
- max: 1,
39
- step: 0.1,
40
- label: "Opacity",
41
- },
42
- width: {
43
- type: "number",
44
- label: "Width",
45
- min: 0,
46
- max: 5000,
47
- },
48
- height: {
49
- type: "number",
50
- label: "Height",
51
- min: 0,
52
- max: 5000,
53
- },
54
- angle: {
55
- type: "number",
56
- label: "Rotation",
57
- min: 0,
58
- max: 360,
59
- },
60
- left: {
61
- type: "number",
62
- label: "Left",
63
- min: 0,
64
- max: 1000,
65
- },
66
- top: {
67
- type: "number",
68
- label: "Top",
69
- min: 0,
70
- max: 1000,
71
- },
16
+ metadata = {
17
+ name: "ImageTool",
72
18
  };
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
+
29
+ private canvasService?: CanvasService;
30
+ 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
+ }
47
+
48
+ activate(context: ExtensionContext) {
49
+ this.context = context;
50
+ this.canvasService = context.services.get<CanvasService>("CanvasService");
51
+ if (!this.canvasService) {
52
+ console.warn("CanvasService not found for ImageTool");
53
+ return;
54
+ }
73
55
 
74
- onMount(editor: Editor) {
75
- this.ensureLayer(editor);
76
- this.updateImage(editor, this.options);
56
+ const configService = context.services.get<any>("ConfigurationService");
57
+ if (configService) {
58
+ // 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);
66
+
67
+ // Listen for changes
68
+ 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
+ }
78
+ }
79
+ });
80
+ }
81
+
82
+ this.ensureLayer();
83
+ this.updateImage();
77
84
  }
78
85
 
79
- onUnmount(editor: Editor) {
80
- const layer = editor.getLayer("user");
81
- if (layer) {
82
- const userImage = editor.getObject("user-image", "user");
83
- if (userImage) {
84
- layer.remove(userImage);
85
- editor.canvas.requestRenderAll();
86
+ deactivate(context: ExtensionContext) {
87
+ if (this.canvasService) {
88
+ const layer = this.canvasService.getLayer("user");
89
+ if (layer) {
90
+ const userImage = this.canvasService.getObject("user-image", "user");
91
+ if (userImage) {
92
+ layer.remove(userImage);
93
+ this.canvasService.requestRenderAll();
94
+ }
86
95
  }
96
+ this.canvasService = undefined;
97
+ this.context = undefined;
87
98
  }
88
99
  }
89
100
 
90
- onUpdate(editor: Editor, state: EditorState) {
91
- this.updateImage(editor, this.options);
101
+ contribute() {
102
+ return {
103
+ [ContributionPointIds.CONFIGURATIONS]: [
104
+ {
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,
118
+ },
119
+ {
120
+ id: "image.width",
121
+ type: "number",
122
+ label: "Width",
123
+ min: 0,
124
+ max: 5000,
125
+ default: this.width,
126
+ },
127
+ {
128
+ id: "image.height",
129
+ type: "number",
130
+ label: "Height",
131
+ min: 0,
132
+ max: 5000,
133
+ default: this.height,
134
+ },
135
+ {
136
+ id: "image.angle",
137
+ type: "number",
138
+ label: "Rotation",
139
+ min: 0,
140
+ max: 360,
141
+ default: this.angle,
142
+ },
143
+ {
144
+ id: "image.left",
145
+ type: "number",
146
+ label: "Left (Normalized)",
147
+ min: 0,
148
+ max: 1,
149
+ default: this.left,
150
+ },
151
+ {
152
+ id: "image.top",
153
+ type: "number",
154
+ label: "Top (Normalized)",
155
+ min: 0,
156
+ max: 1,
157
+ default: this.top,
158
+ },
159
+ ] as ConfigurationContribution[],
160
+ [ContributionPointIds.COMMANDS]: [
161
+ {
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;
196
+ },
197
+ },
198
+ ] as CommandContribution[],
199
+ };
92
200
  }
93
201
 
94
- private ensureLayer(editor: Editor) {
95
- let userLayer = editor.getLayer("user");
202
+ private ensureLayer() {
203
+ if (!this.canvasService) return;
204
+ let userLayer = this.canvasService.getLayer("user");
96
205
  if (!userLayer) {
97
- userLayer = new PooderLayer([], {
98
- width: editor.state.width,
99
- height: editor.state.height,
206
+ userLayer = this.canvasService.createLayer("user", {
207
+ width: this.canvasService.canvas.width,
208
+ height: this.canvasService.canvas.height,
100
209
  left: 0,
101
210
  top: 0,
102
211
  originX: "left",
@@ -105,24 +214,40 @@ export class ImageTool implements Extension<ImageToolOptions> {
105
214
  evented: true,
106
215
  subTargetCheck: true,
107
216
  interactive: true,
108
- data: {
109
- id: "user",
110
- },
111
217
  });
112
- editor.canvas.add(userLayer);
218
+
219
+ // Try to insert below dieline-overlay
220
+ const dielineLayer = this.canvasService.getLayer("dieline-overlay");
221
+ if (dielineLayer) {
222
+ const index = this.canvasService.canvas
223
+ .getObjects()
224
+ .indexOf(dielineLayer);
225
+ // If dieline is at 0, move user to 0 (dieline shifts to 1)
226
+ if (index >= 0) {
227
+ this.canvasService.canvas.moveObjectTo(userLayer, index);
228
+ }
229
+ } else {
230
+ // Ensure background is behind
231
+ const bgLayer = this.canvasService.getLayer("background");
232
+ if (bgLayer) {
233
+ this.canvasService.canvas.sendObjectToBack(bgLayer);
234
+ }
235
+ }
236
+ this.canvasService.requestRenderAll();
113
237
  }
114
238
  }
115
239
 
116
- private updateImage(editor: Editor, opts: ImageToolOptions) {
117
- let { url, opacity, width, height, angle, left, top } = opts;
240
+ private updateImage() {
241
+ if (!this.canvasService) return;
242
+ let { url, opacity, width, height, angle, left, top } = this;
118
243
 
119
- const layer = editor.getLayer("user");
244
+ const layer = this.canvasService.getLayer("user");
120
245
  if (!layer) {
121
246
  console.warn("[ImageTool] User layer not found");
122
247
  return;
123
248
  }
124
249
 
125
- const userImage = editor.getObject("user-image", "user") as any;
250
+ const userImage = this.canvasService.getObject("user-image", "user") as any;
126
251
 
127
252
  if (this._loadingUrl === url) return;
128
253
 
@@ -130,24 +255,37 @@ export class ImageTool implements Extension<ImageToolOptions> {
130
255
  const currentSrc = userImage.getSrc?.() || userImage._element?.src;
131
256
 
132
257
  if (currentSrc !== url) {
133
- this.loadImage(editor, layer, opts);
258
+ this.loadImage(layer);
134
259
  } else {
135
260
  const updates: any = {};
136
- const centerX = editor.state.width / 2;
137
- const centerY = editor.state.height / 2;
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;
138
265
 
139
266
  if (userImage.opacity !== opacity) updates.opacity = opacity;
140
267
  if (angle !== undefined && userImage.angle !== angle)
141
268
  updates.angle = angle;
142
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
+ }
278
+
143
279
  if (left !== undefined) {
144
- const localLeft = left - centerX;
280
+ const globalLeft = Coordinate.toAbsolute(left, canvasW);
281
+ const localLeft = globalLeft - centerX;
145
282
  if (Math.abs(userImage.left - localLeft) > 1)
146
283
  updates.left = localLeft;
147
284
  }
148
285
 
149
286
  if (top !== undefined) {
150
- const localTop = top - centerY;
287
+ const globalTop = Coordinate.toAbsolute(top, canvasH);
288
+ const localTop = globalTop - centerY;
151
289
  if (Math.abs(userImage.top - localTop) > 1) updates.top = localTop;
152
290
  }
153
291
 
@@ -158,31 +296,80 @@ export class ImageTool implements Extension<ImageToolOptions> {
158
296
 
159
297
  if (Object.keys(updates).length > 0) {
160
298
  userImage.set(updates);
161
- editor.canvas.requestRenderAll();
299
+ layer.dirty = true;
300
+ this.canvasService.requestRenderAll();
162
301
  }
163
302
  }
164
303
  } else {
165
- this.loadImage(editor, layer, opts);
304
+ this.loadImage(layer);
166
305
  }
167
306
  }
168
307
 
169
- private loadImage(
170
- editor: Editor,
171
- layer: PooderLayer,
172
- opts: ImageToolOptions,
173
- ) {
174
- const { url } = opts;
308
+ private loadImage(layer: any) {
309
+ if (!this.canvasService) return;
310
+ const { url } = this;
311
+ if (!url) return; // Don't load if empty
175
312
  this._loadingUrl = url;
176
313
 
177
- Image.fromURL(url)
314
+ Image.fromURL(url, { crossOrigin: "anonymous" })
178
315
  .then((image) => {
179
316
  if (this._loadingUrl !== url) return;
180
317
  this._loadingUrl = null;
181
318
 
182
- const currentOpts = this.options;
183
- const { opacity, width, height, angle, left, top } = currentOpts;
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
+ }
353
+
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
+ }
184
368
 
185
- const existingImage = editor.getObject("user-image", "user") as any;
369
+ const existingImage = this.canvasService!.getObject(
370
+ "user-image",
371
+ "user",
372
+ ) as any;
186
373
 
187
374
  if (existingImage) {
188
375
  const defaultLeft = existingImage.left;
@@ -191,9 +378,51 @@ export class ImageTool implements Extension<ImageToolOptions> {
191
378
  const defaultScaleX = existingImage.scaleX;
192
379
  const defaultScaleY = existingImage.scaleY;
193
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
+ }
420
+
194
421
  image.set({
195
- left: left !== undefined ? left : defaultLeft,
196
- top: top !== undefined ? top : defaultTop,
422
+ originX: "center", // Use center origin for easier positioning
423
+ originY: "center",
424
+ left: targetLeft,
425
+ top: targetTop,
197
426
  angle: angle !== undefined ? angle : defaultAngle,
198
427
  scaleX:
199
428
  width !== undefined && image.width
@@ -207,14 +436,34 @@ export class ImageTool implements Extension<ImageToolOptions> {
207
436
 
208
437
  layer.remove(existingImage);
209
438
  } else {
439
+ // New image
440
+ image.set({
441
+ originX: "center",
442
+ originY: "center",
443
+ });
444
+
210
445
  if (width !== undefined && image.width)
211
446
  image.scaleX = width / image.width;
212
447
  if (height !== undefined && image.height)
213
448
  image.scaleY = height / image.height;
214
449
  if (angle !== undefined) image.angle = angle;
215
450
 
216
- if (left !== undefined) image.left = left;
217
- if (top !== undefined) image.top = top;
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
+ }
461
+
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
+ }
218
467
  }
219
468
 
220
469
  image.set({
@@ -226,79 +475,30 @@ export class ImageTool implements Extension<ImageToolOptions> {
226
475
  layer.add(image);
227
476
 
228
477
  // Bind events to keep options in sync
229
- image.on("modified", (e) => {
478
+ image.on("modified", (e: any) => {
230
479
  const matrix = image.calcTransformMatrix();
231
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;
232
483
 
233
- this.options.left = globalPoint.x;
234
- this.options.top = globalPoint.y;
235
- this.options.angle = e.target.angle;
484
+ this.left = Coordinate.toNormalized(globalPoint.x, canvasW);
485
+ this.top = Coordinate.toNormalized(globalPoint.y, canvasH);
486
+ this.angle = e.target.angle;
236
487
 
237
- if (image.width)
238
- this.options.width = e.target.width * e.target.scaleX;
239
- if (image.height)
240
- this.options.height = e.target.height * e.target.scaleY;
488
+ if (image.width) this.width = e.target.width * e.target.scaleX;
489
+ if (image.height) this.height = e.target.height * e.target.scaleY;
241
490
 
242
- editor.emit("update");
491
+ if (this.context) {
492
+ this.context.eventBus.emit("update");
493
+ }
243
494
  });
244
495
 
245
- editor.canvas.requestRenderAll();
496
+ layer.dirty = true;
497
+ this.canvasService!.requestRenderAll();
246
498
  })
247
499
  .catch((err) => {
248
500
  if (this._loadingUrl === url) this._loadingUrl = null;
249
501
  console.error("Failed to load image", url, err);
250
502
  });
251
503
  }
252
-
253
- commands: Record<string, Command> = {
254
- setUserImage: {
255
- execute: (
256
- editor: Editor,
257
- url: string,
258
- opacity: number,
259
- width?: number,
260
- height?: number,
261
- angle?: number,
262
- left?: number,
263
- top?: number,
264
- ) => {
265
- if (
266
- this.options.url === url &&
267
- this.options.opacity === opacity &&
268
- this.options.width === width &&
269
- this.options.height === height &&
270
- this.options.angle === angle &&
271
- this.options.left === left &&
272
- this.options.top === top
273
- )
274
- return true;
275
-
276
- this.options = { url, opacity, width, height, angle, left, top };
277
-
278
- // Direct update
279
- this.updateImage(editor, this.options);
280
-
281
- return true;
282
- },
283
- schema: {
284
- url: {
285
- type: "string",
286
- label: "Image URL",
287
- required: true,
288
- },
289
- opacity: {
290
- type: "number",
291
- label: "Opacity",
292
- min: 0,
293
- max: 1,
294
- required: true,
295
- },
296
- width: { type: "number", label: "Width" },
297
- height: { type: "number", label: "Height" },
298
- angle: { type: "number", label: "Angle" },
299
- left: { type: "number", label: "Left" },
300
- top: { type: "number", label: "Top" },
301
- },
302
- },
303
- };
304
504
  }
package/src/index.ts CHANGED
@@ -6,3 +6,4 @@ export * from "./image";
6
6
  export * from "./white-ink";
7
7
  export * from "./ruler";
8
8
  export * from "./mirror";
9
+ export { default as CanvasService } from "./CanvasService";