@pooder/kit 0.0.2 → 2.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,146 +1,304 @@
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
- }
31
-
32
- onUnmount(editor: Editor) {
33
- const layer = editor.getLayer("user");
34
- if (layer) {
35
- const userImage = editor.getObject("user-image", "user");
36
- if (userImage) {
37
- layer.remove(userImage);
38
- editor.canvas.requestRenderAll();
39
- }
40
- }
41
- }
42
-
43
- onUpdate(editor: Editor, state: EditorState) {
44
- this.updateImage(editor, this.options);
45
- }
46
-
47
- private ensureLayer(editor: Editor) {
48
- let userLayer = editor.getLayer("user")
49
- if (!userLayer) {
50
- userLayer = new PooderLayer([], {
51
- width: editor.state.width,
52
- height: editor.state.height,
53
- left: 0,
54
- top: 0,
55
- originX: 'left',
56
- originY: 'top',
57
- selectable: false,
58
- evented: true,
59
- subTargetCheck: true,
60
- interactive: true,
61
- data: {
62
- id: 'user'
63
- }
64
- });
65
- editor.canvas.add(userLayer)
66
- }
67
- }
68
-
69
- private updateImage(editor: Editor, opts: ImageToolOptions) {
70
- let { url, opacity } = opts;
71
-
72
- const layer = editor.getLayer("user");
73
- if (!layer) {
74
- console.warn('[ImageTool] User layer not found');
75
- return;
76
- }
77
-
78
- const userImage = editor.getObject("user-image","user") as any;
79
-
80
- if (userImage) {
81
- const currentSrc = userImage.getSrc?.() || userImage._element?.src;
82
-
83
- if (currentSrc !== url) {
84
- this.loadImage(editor, layer, url, opacity, userImage);
85
- } else {
86
- if (userImage.opacity !== opacity) {
87
- userImage.set({ opacity });
88
- editor.canvas.requestRenderAll();
89
- }
90
- }
91
- } else {
92
- this.loadImage(editor, layer, url, opacity);
93
- }
94
- }
95
-
96
- private loadImage(editor: Editor, layer: PooderLayer, url: string, opacity: number, oldImage?: any) {
97
- Image.fromURL(url).then(image => {
98
- if (oldImage) {
99
- const { left, top, scaleX, scaleY, angle } = oldImage;
100
- image.set({ left, top, scaleX, scaleY, angle });
101
- layer.remove(oldImage);
102
- }
103
-
104
- image.set({
105
- opacity,
106
- data: {
107
- id: 'user-image'
108
- }
109
- });
110
- layer.add(image);
111
- editor.canvas.requestRenderAll();
112
- }).catch(err => {
113
- console.error("Failed to load image", url, err);
114
- });
115
- }
116
-
117
- commands:Record<string, Command>={
118
- setUserImage:{
119
- execute:(editor: Editor, url: string, opacity: number)=>{
120
- if (this.options.url === url && this.options.opacity === opacity) return true;
121
-
122
- this.options.url = url;
123
- this.options.opacity = opacity;
124
-
125
- // Direct update
126
- this.updateImage(editor, this.options);
127
-
128
- return true
129
- },
130
- schema: {
131
- url: {
132
- type: 'string',
133
- label: 'Image URL',
134
- required: true
135
- },
136
- opacity: {
137
- type: 'number',
138
- label: 'Opacity',
139
- min: 0,
140
- max: 1,
141
- required: true
142
- }
143
- }
144
- }
145
- }
146
- }
1
+ import {
2
+ Command,
3
+ Editor,
4
+ EditorState,
5
+ Extension,
6
+ Image,
7
+ OptionSchema,
8
+ PooderLayer,
9
+ util,
10
+ Point,
11
+ } from "@pooder/core";
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
+ };
29
+
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
+ },
72
+ };
73
+
74
+ onMount(editor: Editor) {
75
+ this.ensureLayer(editor);
76
+ this.updateImage(editor, this.options);
77
+ }
78
+
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
+ }
87
+ }
88
+ }
89
+
90
+ onUpdate(editor: Editor, state: EditorState) {
91
+ this.updateImage(editor, this.options);
92
+ }
93
+
94
+ private ensureLayer(editor: Editor) {
95
+ let userLayer = editor.getLayer("user");
96
+ if (!userLayer) {
97
+ userLayer = new PooderLayer([], {
98
+ width: editor.state.width,
99
+ height: editor.state.height,
100
+ left: 0,
101
+ top: 0,
102
+ originX: "left",
103
+ originY: "top",
104
+ selectable: false,
105
+ evented: true,
106
+ subTargetCheck: true,
107
+ interactive: true,
108
+ data: {
109
+ id: "user",
110
+ },
111
+ });
112
+ editor.canvas.add(userLayer);
113
+ }
114
+ }
115
+
116
+ private updateImage(editor: Editor, opts: ImageToolOptions) {
117
+ let { url, opacity, width, height, angle, left, top } = opts;
118
+
119
+ const layer = editor.getLayer("user");
120
+ if (!layer) {
121
+ console.warn("[ImageTool] User layer not found");
122
+ return;
123
+ }
124
+
125
+ const userImage = editor.getObject("user-image", "user") as any;
126
+
127
+ if (this._loadingUrl === url) return;
128
+
129
+ if (userImage) {
130
+ const currentSrc = userImage.getSrc?.() || userImage._element?.src;
131
+
132
+ if (currentSrc !== url) {
133
+ this.loadImage(editor, layer, opts);
134
+ } else {
135
+ const updates: any = {};
136
+ const centerX = editor.state.width / 2;
137
+ const centerY = editor.state.height / 2;
138
+
139
+ if (userImage.opacity !== opacity) updates.opacity = opacity;
140
+ if (angle !== undefined && userImage.angle !== angle)
141
+ updates.angle = angle;
142
+
143
+ if (left !== undefined) {
144
+ const localLeft = left - centerX;
145
+ if (Math.abs(userImage.left - localLeft) > 1)
146
+ updates.left = localLeft;
147
+ }
148
+
149
+ if (top !== undefined) {
150
+ const localTop = top - centerY;
151
+ if (Math.abs(userImage.top - localTop) > 1) updates.top = localTop;
152
+ }
153
+
154
+ if (width !== undefined && userImage.width)
155
+ updates.scaleX = width / userImage.width;
156
+ if (height !== undefined && userImage.height)
157
+ updates.scaleY = height / userImage.height;
158
+
159
+ if (Object.keys(updates).length > 0) {
160
+ userImage.set(updates);
161
+ editor.canvas.requestRenderAll();
162
+ }
163
+ }
164
+ } else {
165
+ this.loadImage(editor, layer, opts);
166
+ }
167
+ }
168
+
169
+ private loadImage(
170
+ editor: Editor,
171
+ layer: PooderLayer,
172
+ opts: ImageToolOptions,
173
+ ) {
174
+ const { url } = opts;
175
+ this._loadingUrl = url;
176
+
177
+ Image.fromURL(url)
178
+ .then((image) => {
179
+ if (this._loadingUrl !== url) return;
180
+ this._loadingUrl = null;
181
+
182
+ const currentOpts = this.options;
183
+ const { opacity, width, height, angle, left, top } = currentOpts;
184
+
185
+ const existingImage = editor.getObject("user-image", "user") as any;
186
+
187
+ if (existingImage) {
188
+ const defaultLeft = existingImage.left;
189
+ const defaultTop = existingImage.top;
190
+ const defaultAngle = existingImage.angle;
191
+ const defaultScaleX = existingImage.scaleX;
192
+ const defaultScaleY = existingImage.scaleY;
193
+
194
+ image.set({
195
+ left: left !== undefined ? left : defaultLeft,
196
+ top: top !== undefined ? top : defaultTop,
197
+ angle: angle !== undefined ? angle : defaultAngle,
198
+ scaleX:
199
+ width !== undefined && image.width
200
+ ? width / image.width
201
+ : defaultScaleX,
202
+ scaleY:
203
+ height !== undefined && image.height
204
+ ? height / image.height
205
+ : defaultScaleY,
206
+ });
207
+
208
+ layer.remove(existingImage);
209
+ } else {
210
+ if (width !== undefined && image.width)
211
+ image.scaleX = width / image.width;
212
+ if (height !== undefined && image.height)
213
+ image.scaleY = height / image.height;
214
+ if (angle !== undefined) image.angle = angle;
215
+
216
+ if (left !== undefined) image.left = left;
217
+ if (top !== undefined) image.top = top;
218
+ }
219
+
220
+ image.set({
221
+ opacity: opacity !== undefined ? opacity : 1,
222
+ data: {
223
+ id: "user-image",
224
+ },
225
+ });
226
+ layer.add(image);
227
+
228
+ // Bind events to keep options in sync
229
+ image.on("modified", (e) => {
230
+ const matrix = image.calcTransformMatrix();
231
+ const globalPoint = util.transformPoint(new Point(0, 0), matrix);
232
+
233
+ this.options.left = globalPoint.x;
234
+ this.options.top = globalPoint.y;
235
+ this.options.angle = e.target.angle;
236
+
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;
241
+
242
+ editor.emit("update");
243
+ });
244
+
245
+ editor.canvas.requestRenderAll();
246
+ })
247
+ .catch((err) => {
248
+ if (this._loadingUrl === url) this._loadingUrl = null;
249
+ console.error("Failed to load image", url, err);
250
+ });
251
+ }
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
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
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";
package/src/mirror.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { Command, Editor, Extension, OptionSchema } from "@pooder/core";
2
+
3
+ export interface MirrorToolOptions {
4
+ enabled: boolean;
5
+ }
6
+
7
+ export class MirrorTool implements Extension<MirrorToolOptions> {
8
+ public name = "MirrorTool";
9
+ public options: MirrorToolOptions = {
10
+ enabled: false,
11
+ };
12
+
13
+ public schema: Record<keyof MirrorToolOptions, OptionSchema> = {
14
+ enabled: {
15
+ type: "boolean",
16
+ label: "Mirror View",
17
+ },
18
+ };
19
+
20
+ onMount(editor: Editor) {
21
+ if (this.options.enabled) {
22
+ this.applyMirror(editor, true);
23
+ }
24
+ }
25
+
26
+ onUpdate(editor: Editor) {
27
+ this.applyMirror(editor, this.options.enabled);
28
+ }
29
+
30
+ onUnmount(editor: Editor) {
31
+ // Force disable on unmount to restore view
32
+ this.applyMirror(editor, false);
33
+ }
34
+
35
+ private applyMirror(editor: Editor, enabled: boolean) {
36
+ const canvas = editor.canvas;
37
+ if (!canvas) return;
38
+
39
+ const width = canvas.width || 800;
40
+
41
+ // Fabric.js v6+ uses viewportTransform property
42
+ let vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
43
+ // Create a copy to avoid mutating the reference directly before setting
44
+ vpt = [...vpt];
45
+
46
+ // If we are enabling and currently not flipped (scaleX > 0)
47
+ // Or disabling and currently flipped (scaleX < 0)
48
+ const isFlipped = vpt[0] < 0;
49
+
50
+ if (enabled && !isFlipped) {
51
+ // Flip scale X
52
+ vpt[0] = -vpt[0]; // Flip scale
53
+ vpt[4] = width - vpt[4]; // Adjust pan X
54
+
55
+ canvas.setViewportTransform(vpt as any);
56
+ canvas.requestRenderAll();
57
+ } else if (!enabled && isFlipped) {
58
+ // Restore
59
+ vpt[0] = -vpt[0]; // Unflip scale
60
+ vpt[4] = width - vpt[4]; // Restore pan X
61
+
62
+ canvas.setViewportTransform(vpt as any);
63
+ canvas.requestRenderAll();
64
+ }
65
+ }
66
+
67
+ commands: Record<string, Command> = {
68
+ toggleMirror: {
69
+ execute: (editor: Editor) => {
70
+ this.options.enabled = !this.options.enabled;
71
+ this.applyMirror(editor, this.options.enabled);
72
+ return true;
73
+ },
74
+ },
75
+ setMirror: {
76
+ execute: (editor: Editor, enabled: boolean) => {
77
+ if (this.options.enabled === enabled) return true;
78
+ this.options.enabled = enabled;
79
+ this.applyMirror(editor, enabled);
80
+ return true;
81
+ },
82
+ schema: {
83
+ enabled: {
84
+ type: "boolean",
85
+ label: "Enabled",
86
+ required: true,
87
+ },
88
+ },
89
+ },
90
+ };
91
+ }