@pooder/kit 1.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,147 +1,504 @@
1
- import {Command, Editor, EditorState, Extension, Image, OptionSchema, PooderLayer} from '@pooder/core';
2
-
3
- interface ImageToolOptions {
4
- url: string;
5
- opacity: number;
6
- }
7
- export class ImageTool implements Extension<ImageToolOptions> {
8
- name = 'ImageTool';
9
- options: ImageToolOptions = {
10
- url: '',
11
- opacity: 1
12
- };
13
-
14
- public schema: Record<keyof ImageToolOptions, OptionSchema> = {
15
- url: {
16
- type: 'string',
17
- label: 'Image URL'
18
- },
19
- opacity: {
20
- type: 'number',
21
- min: 0,
22
- max: 1,
23
- step: 0.1,
24
- label: 'Opacity'
25
- }
26
- };
27
-
28
- onMount(editor: Editor) {
29
- this.ensureLayer(editor);
30
- this.updateImage(editor, this.options);
31
- }
32
-
33
- onUnmount(editor: Editor) {
34
- const layer = editor.getLayer("user");
35
- if (layer) {
36
- const userImage = editor.getObject("user-image", "user");
37
- if (userImage) {
38
- layer.remove(userImage);
39
- editor.canvas.requestRenderAll();
40
- }
41
- }
42
- }
43
-
44
- onUpdate(editor: Editor, state: EditorState) {
45
- this.updateImage(editor, this.options);
46
- }
47
-
48
- private ensureLayer(editor: Editor) {
49
- let userLayer = editor.getLayer("user")
50
- if (!userLayer) {
51
- userLayer = new PooderLayer([], {
52
- width: editor.state.width,
53
- height: editor.state.height,
54
- left: 0,
55
- top: 0,
56
- originX: 'left',
57
- originY: 'top',
58
- selectable: false,
59
- evented: true,
60
- subTargetCheck: true,
61
- interactive: true,
62
- data: {
63
- id: 'user'
64
- }
65
- });
66
- editor.canvas.add(userLayer)
67
- }
68
- }
69
-
70
- private updateImage(editor: Editor, opts: ImageToolOptions) {
71
- let { url, opacity } = opts;
72
-
73
- const layer = editor.getLayer("user");
74
- if (!layer) {
75
- console.warn('[ImageTool] User layer not found');
76
- return;
77
- }
78
-
79
- const userImage = editor.getObject("user-image","user") as any;
80
-
81
- if (userImage) {
82
- const currentSrc = userImage.getSrc?.() || userImage._element?.src;
83
-
84
- if (currentSrc !== url) {
85
- this.loadImage(editor, layer, url, opacity, userImage);
86
- } else {
87
- if (userImage.opacity !== opacity) {
88
- userImage.set({ opacity });
89
- editor.canvas.requestRenderAll();
90
- }
91
- }
92
- } else {
93
- this.loadImage(editor, layer, url, opacity);
94
- }
95
- }
96
-
97
- private loadImage(editor: Editor, layer: PooderLayer, url: string, opacity: number, oldImage?: any) {
98
- Image.fromURL(url).then(image => {
99
- if (oldImage) {
100
- const { left, top, scaleX, scaleY, angle } = oldImage;
101
- image.set({ left, top, scaleX, scaleY, angle });
102
- layer.remove(oldImage);
103
- }
104
-
105
- image.set({
106
- opacity,
107
- data: {
108
- id: 'user-image'
109
- }
110
- });
111
- layer.add(image);
112
- editor.canvas.requestRenderAll();
113
- }).catch(err => {
114
- console.error("Failed to load image", url, err);
115
- });
116
- }
117
-
118
- commands:Record<string, Command>={
119
- setUserImage:{
120
- execute:(editor: Editor, url: string, opacity: number)=>{
121
- if (this.options.url === url && this.options.opacity === opacity) return true;
122
-
123
- this.options.url = url;
124
- this.options.opacity = opacity;
125
-
126
- // Direct update
127
- this.updateImage(editor, this.options);
128
-
129
- return true
130
- },
131
- schema: {
132
- url: {
133
- type: 'string',
134
- label: 'Image URL',
135
- required: true
136
- },
137
- opacity: {
138
- type: 'number',
139
- label: 'Opacity',
140
- min: 0,
141
- max: 1,
142
- required: true
143
- }
144
- }
145
- }
146
- }
147
- }
1
+ import {
2
+ CommandContribution,
3
+ ConfigurationContribution,
4
+ ConfigurationService,
5
+ ContributionPointIds,
6
+ Extension,
7
+ ExtensionContext,
8
+ } from "@pooder/core";
9
+ import { FabricImage as Image, Point, util } from "fabric";
10
+ import CanvasService from "./CanvasService";
11
+ import { Coordinate } from "./coordinate";
12
+
13
+ export class ImageTool implements Extension {
14
+ id = "pooder.kit.image";
15
+
16
+ metadata = {
17
+ name: "ImageTool",
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
+ }
55
+
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();
84
+ }
85
+
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
+ }
95
+ }
96
+ this.canvasService = undefined;
97
+ this.context = undefined;
98
+ }
99
+ }
100
+
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
+ };
200
+ }
201
+
202
+ private ensureLayer() {
203
+ if (!this.canvasService) return;
204
+ let userLayer = this.canvasService.getLayer("user");
205
+ if (!userLayer) {
206
+ userLayer = this.canvasService.createLayer("user", {
207
+ width: this.canvasService.canvas.width,
208
+ height: this.canvasService.canvas.height,
209
+ left: 0,
210
+ top: 0,
211
+ originX: "left",
212
+ originY: "top",
213
+ selectable: false,
214
+ evented: true,
215
+ subTargetCheck: true,
216
+ interactive: true,
217
+ });
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();
237
+ }
238
+ }
239
+
240
+ private updateImage() {
241
+ if (!this.canvasService) return;
242
+ let { url, opacity, width, height, angle, left, top } = this;
243
+
244
+ const layer = this.canvasService.getLayer("user");
245
+ if (!layer) {
246
+ console.warn("[ImageTool] User layer not found");
247
+ return;
248
+ }
249
+
250
+ const userImage = this.canvasService.getObject("user-image", "user") as any;
251
+
252
+ if (this._loadingUrl === url) return;
253
+
254
+ if (userImage) {
255
+ const currentSrc = userImage.getSrc?.() || userImage._element?.src;
256
+
257
+ if (currentSrc !== url) {
258
+ this.loadImage(layer);
259
+ } else {
260
+ const updates: any = {};
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;
265
+
266
+ if (userImage.opacity !== opacity) updates.opacity = opacity;
267
+ if (angle !== undefined && userImage.angle !== angle)
268
+ updates.angle = angle;
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
+
279
+ if (left !== undefined) {
280
+ const globalLeft = Coordinate.toAbsolute(left, canvasW);
281
+ const localLeft = globalLeft - centerX;
282
+ if (Math.abs(userImage.left - localLeft) > 1)
283
+ updates.left = localLeft;
284
+ }
285
+
286
+ if (top !== undefined) {
287
+ const globalTop = Coordinate.toAbsolute(top, canvasH);
288
+ const localTop = globalTop - centerY;
289
+ if (Math.abs(userImage.top - localTop) > 1) updates.top = localTop;
290
+ }
291
+
292
+ if (width !== undefined && userImage.width)
293
+ updates.scaleX = width / userImage.width;
294
+ if (height !== undefined && userImage.height)
295
+ updates.scaleY = height / userImage.height;
296
+
297
+ if (Object.keys(updates).length > 0) {
298
+ userImage.set(updates);
299
+ layer.dirty = true;
300
+ this.canvasService.requestRenderAll();
301
+ }
302
+ }
303
+ } else {
304
+ this.loadImage(layer);
305
+ }
306
+ }
307
+
308
+ private loadImage(layer: any) {
309
+ if (!this.canvasService) return;
310
+ const { url } = this;
311
+ if (!url) return; // Don't load if empty
312
+ this._loadingUrl = url;
313
+
314
+ Image.fromURL(url, { crossOrigin: "anonymous" })
315
+ .then((image) => {
316
+ if (this._loadingUrl !== url) return;
317
+ this._loadingUrl = null;
318
+
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
+ }
368
+
369
+ const existingImage = this.canvasService!.getObject(
370
+ "user-image",
371
+ "user",
372
+ ) as any;
373
+
374
+ if (existingImage) {
375
+ const defaultLeft = existingImage.left;
376
+ const defaultTop = existingImage.top;
377
+ const defaultAngle = existingImage.angle;
378
+ const defaultScaleX = existingImage.scaleX;
379
+ const defaultScaleY = existingImage.scaleY;
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
+
421
+ image.set({
422
+ originX: "center", // Use center origin for easier positioning
423
+ originY: "center",
424
+ left: targetLeft,
425
+ top: targetTop,
426
+ angle: angle !== undefined ? angle : defaultAngle,
427
+ scaleX:
428
+ width !== undefined && image.width
429
+ ? width / image.width
430
+ : defaultScaleX,
431
+ scaleY:
432
+ height !== undefined && image.height
433
+ ? height / image.height
434
+ : defaultScaleY,
435
+ });
436
+
437
+ layer.remove(existingImage);
438
+ } else {
439
+ // New image
440
+ image.set({
441
+ originX: "center",
442
+ originY: "center",
443
+ });
444
+
445
+ if (width !== undefined && image.width)
446
+ image.scaleX = width / image.width;
447
+ if (height !== undefined && image.height)
448
+ image.scaleY = height / image.height;
449
+ if (angle !== undefined) image.angle = angle;
450
+
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
+ }
467
+ }
468
+
469
+ image.set({
470
+ opacity: opacity !== undefined ? opacity : 1,
471
+ data: {
472
+ id: "user-image",
473
+ },
474
+ });
475
+ layer.add(image);
476
+
477
+ // Bind events to keep options in sync
478
+ image.on("modified", (e: any) => {
479
+ const matrix = image.calcTransformMatrix();
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;
483
+
484
+ this.left = Coordinate.toNormalized(globalPoint.x, canvasW);
485
+ this.top = Coordinate.toNormalized(globalPoint.y, canvasH);
486
+ this.angle = e.target.angle;
487
+
488
+ if (image.width) this.width = e.target.width * e.target.scaleX;
489
+ if (image.height) this.height = e.target.height * e.target.scaleY;
490
+
491
+ if (this.context) {
492
+ this.context.eventBus.emit("update");
493
+ }
494
+ });
495
+
496
+ layer.dirty = true;
497
+ this.canvasService!.requestRenderAll();
498
+ })
499
+ .catch((err) => {
500
+ if (this._loadingUrl === url) this._loadingUrl = null;
501
+ console.error("Failed to load image", url, err);
502
+ });
503
+ }
504
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
- export * from './background';
2
- export * from './dieline';
3
- export * from './film';
4
- export * from './hole';
5
- export * from './image';
6
- export * from './white-ink';
7
- export * from './ruler';
1
+ export * from "./background";
2
+ export * from "./dieline";
3
+ export * from "./film";
4
+ export * from "./hole";
5
+ export * from "./image";
6
+ export * from "./white-ink";
7
+ export * from "./ruler";
8
+ export * from "./mirror";
9
+ export { default as CanvasService } from "./CanvasService";