@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/white-ink.ts CHANGED
@@ -1,56 +1,146 @@
1
1
  import {
2
- Command,
3
- Editor,
4
- EditorState,
5
- EventHandler,
6
2
  Extension,
7
- OptionSchema,
8
- Image,
9
- filters,
10
- PooderObject,
11
- PooderLayer,
3
+ ExtensionContext,
4
+ ContributionPointIds,
5
+ CommandContribution,
6
+ ConfigurationContribution,
12
7
  } from "@pooder/core";
8
+ import { FabricImage as Image, filters } from "fabric";
9
+ import CanvasService from "./CanvasService";
13
10
 
14
- interface WhiteInkToolOptions {
15
- customMask: string;
16
- opacity: number;
17
- enableClip: boolean;
18
- }
19
- export class WhiteInkTool implements Extension<WhiteInkToolOptions> {
20
- public name = "WhiteInkTool";
21
- public options: WhiteInkToolOptions = {
22
- customMask: "",
23
- opacity: 1,
24
- enableClip: false,
25
- };
11
+ export class WhiteInkTool implements Extension {
12
+ id = "pooder.kit.white-ink";
26
13
 
27
- public schema: Record<keyof WhiteInkToolOptions, OptionSchema> = {
28
- customMask: { type: "string", label: "Custom Mask URL" },
29
- opacity: { type: "number", min: 0, max: 1, step: 0.01, label: "Opacity" },
30
- enableClip: { type: "boolean", label: "Enable Clip" },
14
+ public metadata = {
15
+ name: "WhiteInkTool",
31
16
  };
32
17
 
33
- private syncHandler: EventHandler | undefined;
18
+ private customMask: string = "";
19
+ private opacity: number = 1;
20
+ private enableClip: boolean = false;
34
21
 
35
- onMount(editor: Editor) {
36
- this.setup(editor);
37
- this.updateWhiteInk(editor, this.options);
22
+ private canvasService?: CanvasService;
23
+ private syncHandler: ((e: any) => void) | undefined;
24
+ private _loadingUrl: string | null = null;
25
+
26
+ constructor(
27
+ options?: Partial<{
28
+ customMask: string;
29
+ opacity: number;
30
+ enableClip: boolean;
31
+ }>,
32
+ ) {
33
+ if (options) {
34
+ Object.assign(this, options);
35
+ }
38
36
  }
39
37
 
40
- onUnmount(editor: Editor) {
41
- this.teardown(editor);
38
+ activate(context: ExtensionContext) {
39
+ this.canvasService = context.services.get<CanvasService>("CanvasService");
40
+ if (!this.canvasService) {
41
+ console.warn("CanvasService not found for WhiteInkTool");
42
+ return;
43
+ }
44
+
45
+ const configService = context.services.get<any>("ConfigurationService");
46
+ if (configService) {
47
+ // Load initial config
48
+ this.customMask = configService.get(
49
+ "whiteInk.customMask",
50
+ this.customMask,
51
+ );
52
+ this.opacity = configService.get("whiteInk.opacity", this.opacity);
53
+ this.enableClip = configService.get(
54
+ "whiteInk.enableClip",
55
+ this.enableClip,
56
+ );
57
+
58
+ // Listen for changes
59
+ configService.onAnyChange((e: { key: string; value: any }) => {
60
+ if (e.key.startsWith("whiteInk.")) {
61
+ const prop = e.key.split(".")[1];
62
+ console.log(
63
+ `[WhiteInkTool] Config change detected: ${e.key} -> ${e.value}`,
64
+ );
65
+ if (prop && prop in this) {
66
+ (this as any)[prop] = e.value;
67
+ this.updateWhiteInk();
68
+ }
69
+ }
70
+ });
71
+ }
72
+
73
+ this.setup();
74
+ this.updateWhiteInk();
42
75
  }
43
76
 
44
- onDestroy(editor: Editor) {
45
- this.teardown(editor);
77
+ deactivate(context: ExtensionContext) {
78
+ this.teardown();
79
+ this.canvasService = undefined;
80
+ }
81
+
82
+ contribute() {
83
+ return {
84
+ [ContributionPointIds.CONFIGURATIONS]: [
85
+ {
86
+ id: "whiteInk.customMask",
87
+ type: "string",
88
+ label: "Custom Mask URL",
89
+ default: "",
90
+ },
91
+ {
92
+ id: "whiteInk.opacity",
93
+ type: "number",
94
+ label: "Opacity",
95
+ min: 0,
96
+ max: 1,
97
+ step: 0.01,
98
+ default: 1,
99
+ },
100
+ {
101
+ id: "whiteInk.enableClip",
102
+ type: "boolean",
103
+ label: "Enable Clip",
104
+ default: false,
105
+ },
106
+ ] as ConfigurationContribution[],
107
+ [ContributionPointIds.COMMANDS]: [
108
+ {
109
+ command: "setWhiteInkImage",
110
+ title: "Set White Ink Image",
111
+ handler: (
112
+ customMask: string,
113
+ opacity: number,
114
+ enableClip: boolean = true,
115
+ ) => {
116
+ if (
117
+ this.customMask === customMask &&
118
+ this.opacity === opacity &&
119
+ this.enableClip === enableClip
120
+ )
121
+ return true;
122
+
123
+ this.customMask = customMask;
124
+ this.opacity = opacity;
125
+ this.enableClip = enableClip;
126
+
127
+ this.updateWhiteInk();
128
+ return true;
129
+ },
130
+ },
131
+ ] as CommandContribution[],
132
+ };
46
133
  }
47
134
 
48
- private setup(editor: Editor) {
49
- let userLayer = editor.getLayer("user");
135
+ private setup() {
136
+ if (!this.canvasService) return;
137
+ const canvas = this.canvasService.canvas;
138
+
139
+ let userLayer = this.canvasService.getLayer("user");
50
140
  if (!userLayer) {
51
- userLayer = new PooderLayer([], {
52
- width: editor.state.width,
53
- height: editor.state.height,
141
+ userLayer = this.canvasService.createLayer("user", {
142
+ width: canvas.width,
143
+ height: canvas.height,
54
144
  left: 0,
55
145
  top: 0,
56
146
  originX: "left",
@@ -59,114 +149,65 @@ export class WhiteInkTool implements Extension<WhiteInkToolOptions> {
59
149
  evented: true,
60
150
  subTargetCheck: true,
61
151
  interactive: true,
62
- data: {
63
- id: "user",
64
- },
65
152
  });
66
- editor.canvas.add(userLayer);
153
+ canvas.add(userLayer);
67
154
  }
68
155
 
69
156
  if (!this.syncHandler) {
70
157
  this.syncHandler = (e: any) => {
71
158
  const target = e.target;
72
159
  if (target && target.data?.id === "user-image") {
73
- this.syncWithUserImage(editor);
160
+ this.syncWithUserImage();
74
161
  }
75
162
  };
76
163
 
77
- editor.canvas.on("object:moving", this.syncHandler);
78
- editor.canvas.on("object:scaling", this.syncHandler);
79
- editor.canvas.on("object:rotating", this.syncHandler);
80
- editor.canvas.on("object:modified", this.syncHandler);
164
+ canvas.on("object:moving", this.syncHandler);
165
+ canvas.on("object:scaling", this.syncHandler);
166
+ canvas.on("object:rotating", this.syncHandler);
167
+ canvas.on("object:modified", this.syncHandler);
81
168
  }
82
169
  }
83
170
 
84
- private teardown(editor: Editor) {
171
+ private teardown() {
172
+ if (!this.canvasService) return;
173
+ const canvas = this.canvasService.canvas;
174
+
85
175
  if (this.syncHandler) {
86
- editor.canvas.off("object:moving", this.syncHandler);
87
- editor.canvas.off("object:scaling", this.syncHandler);
88
- editor.canvas.off("object:rotating", this.syncHandler);
89
- editor.canvas.off("object:modified", this.syncHandler);
176
+ canvas.off("object:moving", this.syncHandler);
177
+ canvas.off("object:scaling", this.syncHandler);
178
+ canvas.off("object:rotating", this.syncHandler);
179
+ canvas.off("object:modified", this.syncHandler);
90
180
  this.syncHandler = undefined;
91
181
  }
92
182
 
93
- const layer = editor.getLayer("user");
183
+ const layer = this.canvasService.getLayer("user");
94
184
  if (layer) {
95
- const whiteInk = editor.getObject("white-ink", "user");
185
+ const whiteInk = this.canvasService.getObject("white-ink", "user");
96
186
  if (whiteInk) {
97
187
  layer.remove(whiteInk);
98
188
  }
99
189
  }
100
190
 
101
- const userImage = editor.getObject("user-image", "user") as any;
191
+ const userImage = this.canvasService.getObject("user-image", "user") as any;
102
192
  if (userImage && userImage.clipPath) {
103
193
  userImage.set({ clipPath: undefined });
104
194
  }
105
195
 
106
- editor.canvas.requestRenderAll();
196
+ this.canvasService.requestRenderAll();
107
197
  }
108
198
 
109
- onUpdate(editor: Editor, state: EditorState) {
110
- this.updateWhiteInk(editor, this.options);
111
- }
199
+ private updateWhiteInk() {
200
+ if (!this.canvasService) return;
201
+ const { customMask, opacity, enableClip } = this;
112
202
 
113
- commands: Record<string, Command> = {
114
- setWhiteInkImage: {
115
- execute: (
116
- editor: Editor,
117
- customMask: string,
118
- opacity: number,
119
- enableClip: boolean = true,
120
- ) => {
121
- if (
122
- this.options.customMask === customMask &&
123
- this.options.opacity === opacity &&
124
- this.options.enableClip === enableClip
125
- )
126
- return true;
127
-
128
- this.options.customMask = customMask;
129
- this.options.opacity = opacity;
130
- this.options.enableClip = enableClip;
131
-
132
- this.updateWhiteInk(editor, this.options);
133
-
134
- return true;
135
- },
136
- schema: {
137
- customMask: {
138
- type: "string",
139
- label: "Custom Mask URL",
140
- required: true,
141
- },
142
- opacity: {
143
- type: "number",
144
- label: "Opacity",
145
- min: 0,
146
- max: 1,
147
- required: true,
148
- },
149
- enableClip: {
150
- type: "boolean",
151
- label: "Enable Clip",
152
- default: true,
153
- required: false,
154
- },
155
- },
156
- },
157
- };
158
-
159
- private updateWhiteInk(editor: Editor, opts: WhiteInkToolOptions) {
160
- const { customMask, opacity, enableClip } = opts;
161
-
162
- const layer = editor.getLayer("user");
203
+ const layer = this.canvasService.getLayer("user");
163
204
  if (!layer) {
164
205
  console.warn("[WhiteInkTool] User layer not found");
165
206
  return;
166
207
  }
167
208
 
168
- const whiteInk = editor.getObject("white-ink", "user") as any;
169
- const userImage = editor.getObject("user-image", "user") as any;
209
+ const whiteInk = this.canvasService.getObject("white-ink", "user") as any;
210
+ const userImage = this.canvasService.getObject("user-image", "user") as any;
170
211
 
171
212
  if (!customMask) {
172
213
  if (whiteInk) {
@@ -175,7 +216,8 @@ export class WhiteInkTool implements Extension<WhiteInkToolOptions> {
175
216
  if (userImage && userImage.clipPath) {
176
217
  userImage.set({ clipPath: undefined });
177
218
  }
178
- editor.canvas.requestRenderAll();
219
+ layer.dirty = true;
220
+ this.canvasService.requestRenderAll();
179
221
  return;
180
222
  }
181
223
 
@@ -183,55 +225,52 @@ export class WhiteInkTool implements Extension<WhiteInkToolOptions> {
183
225
  if (whiteInk) {
184
226
  const currentSrc = whiteInk.getSrc?.() || whiteInk._element?.src;
185
227
  if (currentSrc !== customMask) {
186
- this.loadWhiteInk(
187
- editor,
188
- layer,
189
- customMask,
190
- opacity,
191
- enableClip,
192
- whiteInk,
193
- );
228
+ this.loadWhiteInk(layer, customMask, opacity, enableClip, whiteInk);
194
229
  } else {
195
230
  if (whiteInk.opacity !== opacity) {
196
231
  whiteInk.set({ opacity });
197
- editor.canvas.requestRenderAll();
232
+ layer.dirty = true;
233
+ this.canvasService.requestRenderAll();
198
234
  }
199
235
  }
200
236
  } else {
201
- this.loadWhiteInk(editor, layer, customMask, opacity, enableClip);
237
+ this.loadWhiteInk(layer, customMask, opacity, enableClip);
202
238
  }
203
239
 
204
240
  // Handle Clip Path Toggle
205
241
  if (userImage) {
206
242
  if (enableClip) {
207
- // If enabled but missing, or mask changed (handled by re-load above, but good to ensure), apply it
208
- // We check if clipPath is present. Ideally we should check if it matches current mask,
209
- // but re-applying is safe.
210
243
  if (!userImage.clipPath) {
211
- this.applyClipPath(editor, customMask);
244
+ this.applyClipPath(customMask);
212
245
  }
213
246
  } else {
214
- // If disabled but present, remove it
215
247
  if (userImage.clipPath) {
216
248
  userImage.set({ clipPath: undefined });
217
- editor.canvas.requestRenderAll();
249
+ layer.dirty = true;
250
+ this.canvasService.requestRenderAll();
218
251
  }
219
252
  }
220
253
  }
221
254
  }
222
255
 
223
256
  private loadWhiteInk(
224
- editor: Editor,
225
- layer: PooderLayer,
257
+ layer: any,
226
258
  url: string,
227
259
  opacity: number,
228
260
  enableClip: boolean,
229
261
  oldImage?: any,
230
262
  ) {
263
+ if (!this.canvasService) return;
264
+
265
+ if (this._loadingUrl === url) return;
266
+ this._loadingUrl = url;
267
+
231
268
  Image.fromURL(url, { crossOrigin: "anonymous" })
232
269
  .then((image) => {
270
+ if (this._loadingUrl !== url) return;
271
+ this._loadingUrl = null;
272
+
233
273
  if (oldImage) {
234
- // Remove old image but don't copy properties yet, we'll sync with user-image
235
274
  layer.remove(oldImage);
236
275
  }
237
276
 
@@ -256,7 +295,7 @@ export class WhiteInkTool implements Extension<WhiteInkToolOptions> {
256
295
  layer.add(image);
257
296
 
258
297
  // Ensure white-ink is behind user-image
259
- const userImage = editor.getObject("user-image", "user");
298
+ const userImage = this.canvasService!.getObject("user-image", "user");
260
299
  if (userImage) {
261
300
  // Re-adding moves it to the top of the stack
262
301
  layer.remove(userImage);
@@ -265,29 +304,31 @@ export class WhiteInkTool implements Extension<WhiteInkToolOptions> {
265
304
 
266
305
  // Apply clip path to user-image if enabled
267
306
  if (enableClip) {
268
- this.applyClipPath(editor, url);
307
+ this.applyClipPath(url);
269
308
  } else if (userImage) {
270
309
  userImage.set({ clipPath: undefined });
271
310
  }
272
311
 
273
312
  // Sync position immediately
274
- this.syncWithUserImage(editor);
313
+ this.syncWithUserImage();
275
314
 
276
- editor.canvas.requestRenderAll();
315
+ layer.dirty = true;
316
+ this.canvasService!.requestRenderAll();
277
317
  })
278
318
  .catch((err) => {
279
319
  console.error("Failed to load white ink mask", url, err);
320
+ this._loadingUrl = null;
280
321
  });
281
322
  }
282
323
 
283
- private applyClipPath(editor: Editor, url: string) {
284
- const userImage = editor.getObject("user-image", "user") as any;
324
+ private applyClipPath(url: string) {
325
+ if (!this.canvasService) return;
326
+ const userImage = this.canvasService.getObject("user-image", "user") as any;
285
327
  if (!userImage) return;
286
328
 
287
329
  Image.fromURL(url, { crossOrigin: "anonymous" })
288
330
  .then((maskImage) => {
289
331
  // Configure clipPath
290
- // It needs to be relative to the object center
291
332
  maskImage.set({
292
333
  originX: "center",
293
334
  originY: "center",
@@ -299,16 +340,19 @@ export class WhiteInkTool implements Extension<WhiteInkToolOptions> {
299
340
  });
300
341
 
301
342
  userImage.set({ clipPath: maskImage });
302
- editor.canvas.requestRenderAll();
343
+ const layer = this.canvasService!.getLayer("user");
344
+ if (layer) layer.dirty = true;
345
+ this.canvasService!.requestRenderAll();
303
346
  })
304
347
  .catch((err) => {
305
348
  console.error("Failed to load clip path", url, err);
306
349
  });
307
350
  }
308
351
 
309
- private syncWithUserImage(editor: Editor) {
310
- const userImage = editor.getObject("user-image", "user");
311
- const whiteInk = editor.getObject("white-ink", "user");
352
+ private syncWithUserImage() {
353
+ if (!this.canvasService) return;
354
+ const userImage = this.canvasService.getObject("user-image", "user");
355
+ const whiteInk = this.canvasService.getObject("white-ink", "user");
312
356
 
313
357
  if (userImage && whiteInk) {
314
358
  whiteInk.set({