@pooder/kit 0.0.2

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/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@pooder/kit",
3
+ "version": "0.0.2",
4
+ "description": "Standard plugins for Pooder editor",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "keywords": [],
9
+ "author": "",
10
+ "license": "ISC",
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "devDependencies": {
15
+ "@types/paper": "^0.12.3",
16
+ "tsup": "^8.0.0",
17
+ "typescript": "^5.0.0"
18
+ },
19
+ "dependencies": {
20
+ "paper": "^0.12.18",
21
+ "@pooder/core": "0.0.2"
22
+ },
23
+ "scripts": {
24
+ "build": "tsup src/index.ts --format cjs,esm --dts",
25
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
26
+ }
27
+ }
@@ -0,0 +1,173 @@
1
+ import {Command, Editor, EditorState, Extension, Image, OptionSchema, PooderLayer, Rect} from '@pooder/core';
2
+
3
+ interface BackgroundToolOptions {
4
+ color: string;
5
+ url: string;
6
+ }
7
+ export class BackgroundTool implements Extension<BackgroundToolOptions> {
8
+ public name = 'BackgroundTool';
9
+ public options: BackgroundToolOptions = {
10
+ color: '',
11
+ url: ''
12
+ };
13
+ public schema: Record<keyof BackgroundToolOptions, OptionSchema> = {
14
+ color: {
15
+ type: 'color',
16
+ label: 'Background Color'
17
+ },
18
+ url: {
19
+ type: 'string',
20
+ label: 'Image URL'
21
+ }
22
+ };
23
+
24
+ private initLayer(editor: Editor) {
25
+ let backgroundLayer=editor.getLayer('background')
26
+ if(!backgroundLayer){
27
+ backgroundLayer=new PooderLayer([], {
28
+ width: editor.canvas.width,
29
+ height: editor.canvas.height,
30
+ selectable: false,
31
+ evented: false,
32
+ data: {
33
+ id: 'background'
34
+ },
35
+ })
36
+
37
+ editor.canvas.add(backgroundLayer)
38
+ editor.canvas.sendObjectToBack(backgroundLayer)
39
+ }
40
+
41
+ this.updateBackground(editor, this.options);
42
+ }
43
+ onMount(editor: Editor) {
44
+ this.initLayer(editor);
45
+ }
46
+
47
+ onUnmount(editor: Editor) {
48
+ const layer = editor.getLayer('background');
49
+ if (layer) {
50
+ editor.canvas.remove(layer);
51
+ }
52
+ }
53
+
54
+ onUpdate(editor: Editor, state: EditorState) {
55
+ this.updateBackground(editor, this.options);
56
+ }
57
+
58
+ private async updateBackground(editor: Editor,options: BackgroundToolOptions) {
59
+ const layer = editor.getLayer('background');
60
+ if (!layer) {
61
+ console.warn('[BackgroundTool] Background layer not found');
62
+ return;
63
+ }
64
+
65
+ const { color, url } = options;
66
+ const width = editor.state.width;
67
+ const height = editor.state.height;
68
+
69
+ let rect=editor.getObject('background-color-rect',"background")
70
+ if (rect) {
71
+ rect.set({
72
+ fill: color
73
+ })
74
+ } else {
75
+ rect = new Rect({
76
+ width,
77
+ height,
78
+ fill: color,
79
+ selectable: false,
80
+ evented: false,
81
+ data: {
82
+ id: 'background-color-rect'
83
+ }
84
+ });
85
+ layer.add(rect);
86
+ layer.sendObjectToBack(rect);
87
+ }
88
+
89
+ let img=editor.getObject('background-image',"background") as Image;
90
+ try {
91
+ if(img){
92
+ if(img.getSrc() !== url){
93
+ if(url){
94
+ await img.setSrc(url);
95
+ }else {
96
+ layer.remove(img)
97
+ }
98
+ }
99
+ }else {
100
+ if (url) {
101
+ img = await Image.fromURL(url, { crossOrigin: 'anonymous' });
102
+ img.set({
103
+ originX: 'left',
104
+ originY: 'top',
105
+ left: 0,
106
+ top: 0,
107
+ selectable: false,
108
+ evented: false,
109
+ data:{
110
+ id: 'background-image'
111
+ }
112
+ })
113
+ img.scaleToWidth(width)
114
+ if (img.getScaledHeight() < height)
115
+ img.scaleToHeight(height);
116
+ layer.add(img);
117
+ }
118
+ }
119
+ editor.canvas.requestRenderAll();
120
+ } catch (e) {
121
+ console.error('[BackgroundTool] Failed to load image', e);
122
+ }
123
+ }
124
+
125
+ commands: Record<string, Command> = {
126
+ reset: {
127
+ execute: (editor: Editor) => {
128
+ this.updateBackground(editor, this.options);
129
+ return true;
130
+ }
131
+ },
132
+ clear: {
133
+ execute: (editor: Editor) => {
134
+ this.options = {
135
+ color: 'transparent',
136
+ url: ''
137
+ };
138
+ this.updateBackground(editor, this.options);
139
+ return true;
140
+ }
141
+ },
142
+ setBackgroundColor: {
143
+ execute: (editor: Editor, color: string) => {
144
+ if (this.options.color === color) return true;
145
+ this.options.color = color;
146
+ this.updateBackground(editor, this.options);
147
+ return true;
148
+ },
149
+ schema: {
150
+ color: {
151
+ type: 'string', // Should be 'color' if supported by CommandArgSchema, but using 'string' for now as per previous plan
152
+ label: 'Background Color',
153
+ required: true
154
+ }
155
+ }
156
+ },
157
+ setBackgroundImage: {
158
+ execute: (editor: Editor, url: string) => {
159
+ if (this.options.url === url) return true;
160
+ this.options.url = url;
161
+ this.updateBackground(editor, this.options);
162
+ return true;
163
+ },
164
+ schema: {
165
+ url: {
166
+ type: 'string',
167
+ label: 'Image URL',
168
+ required: true
169
+ }
170
+ }
171
+ }
172
+ };
173
+ }
package/src/dieline.ts ADDED
@@ -0,0 +1,424 @@
1
+ import { Command, Editor, EditorState, Extension, OptionSchema, Rect, Circle, Ellipse, Path, PooderLayer, Pattern } from '@pooder/core';
2
+ import { generateDielinePath, generateMaskPath, generateBleedZonePath, HoleData } from './geometry';
3
+
4
+ export interface DielineToolOptions {
5
+ shape: 'rect' | 'circle' | 'ellipse';
6
+ width: number;
7
+ height: number;
8
+ radius: number; // corner radius for rect
9
+ position?: { x: number, y: number };
10
+ borderLength?: number;
11
+ offset: number;
12
+ style: 'solid' | 'dashed';
13
+ insideColor: string;
14
+ outsideColor: string;
15
+ }
16
+
17
+ // Alias for compatibility if needed, or just use DielineToolOptions
18
+ export type DielineConfig = DielineToolOptions;
19
+
20
+ export interface DielineGeometry {
21
+ shape: 'rect' | 'circle' | 'ellipse';
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ radius: number;
27
+ }
28
+
29
+ export class DielineTool implements Extension<DielineToolOptions> {
30
+ public name = 'DielineTool';
31
+ public options: DielineToolOptions = {
32
+ shape: 'rect',
33
+ width: 300,
34
+ height: 300,
35
+ radius: 0,
36
+ offset: 0,
37
+ style: 'solid',
38
+ insideColor: 'rgba(0,0,0,0)',
39
+ outsideColor: '#ffffff'
40
+ };
41
+
42
+ public schema: Record<keyof DielineToolOptions, OptionSchema> = {
43
+ shape: {
44
+ type: 'select',
45
+ options: ['rect', 'circle', 'ellipse'],
46
+ label: 'Shape'
47
+ },
48
+ width: { type: 'number', min: 10, max: 2000, label: 'Width' },
49
+ height: { type: 'number', min: 10, max: 2000, label: 'Height' },
50
+ radius: { type: 'number', min: 0, max: 500, label: 'Corner Radius' },
51
+ position: { type: 'string', label: 'Position' }, // Complex object, simplified for now or need custom handler
52
+ borderLength: { type: 'number', min: 0, max: 500, label: 'Margin' },
53
+ offset: { type: 'number', min: -100, max: 100, label: 'Bleed Offset' },
54
+ style: {
55
+ type: 'select',
56
+ options: ['solid', 'dashed'],
57
+ label: 'Line Style'
58
+ },
59
+ insideColor: { type: 'color', label: 'Inside Color' },
60
+ outsideColor: { type: 'color', label: 'Outside Color' }
61
+ };
62
+
63
+ onMount(editor: Editor) {
64
+ this.createLayer(editor);
65
+ }
66
+
67
+ onUnmount(editor: Editor) {
68
+ this.destroyLayer(editor);
69
+ }
70
+
71
+ onUpdate(editor: Editor, state: EditorState) {
72
+ this.updateDieline(editor);
73
+ }
74
+
75
+ onDestroy(editor: Editor) {
76
+ this.destroyLayer(editor);
77
+ }
78
+
79
+ private getLayer(editor: Editor, id: string) {
80
+ return editor.canvas.getObjects().find((obj: any) => obj.data?.id === id) as PooderLayer | undefined;
81
+ }
82
+
83
+ private createLayer(editor: Editor) {
84
+ let layer = this.getLayer(editor, 'dieline-overlay');
85
+
86
+ if (!layer) {
87
+ const width = editor.canvas.width || 800;
88
+ const height = editor.canvas.height || 600;
89
+
90
+ layer = new PooderLayer([], {
91
+ width,
92
+ height,
93
+ selectable: false,
94
+ evented: false,
95
+ data: { id: 'dieline-overlay' }
96
+ }as any);
97
+
98
+ editor.canvas.add(layer);
99
+ }
100
+
101
+ // Ensure it's on top
102
+ editor.canvas.bringObjectToFront(layer);
103
+
104
+ // Initial draw
105
+ this.updateDieline(editor);
106
+ }
107
+
108
+ private destroyLayer(editor: Editor) {
109
+ const layer = this.getLayer(editor, 'dieline-overlay');
110
+ if (layer) {
111
+ editor.canvas.remove(layer);
112
+ }
113
+ }
114
+
115
+ private createHatchPattern(color: string = 'rgba(0, 0, 0, 0.3)') {
116
+ if (typeof document === 'undefined') {
117
+ return undefined;
118
+ }
119
+ const size = 20;
120
+ const canvas = document.createElement('canvas');
121
+ canvas.width = size;
122
+ canvas.height = size;
123
+ const ctx = canvas.getContext('2d');
124
+ if (ctx) {
125
+ // Transparent background
126
+ ctx.clearRect(0, 0, size, size);
127
+
128
+ // Draw diagonal /
129
+ ctx.strokeStyle = color;
130
+ ctx.lineWidth = 1;
131
+ ctx.beginPath();
132
+ ctx.moveTo(0, size);
133
+ ctx.lineTo(size, 0);
134
+ ctx.stroke();
135
+ }
136
+ // @ts-ignore
137
+ return new Pattern({ source: canvas, repetition: 'repeat' });
138
+ }
139
+
140
+ public updateDieline(editor: Editor) {
141
+ const { shape, radius, offset, style, insideColor, outsideColor, position, borderLength } = this.options;
142
+ let { width, height } = this.options;
143
+
144
+ const canvasW = editor.canvas.width || 800;
145
+ const canvasH = editor.canvas.height || 600;
146
+
147
+ // Handle borderLength (Margin)
148
+ if (borderLength && borderLength > 0) {
149
+ width = Math.max(0, canvasW - borderLength * 2);
150
+ height = Math.max(0, canvasH - borderLength * 2);
151
+ }
152
+
153
+ // Handle Position
154
+ const cx = position?.x ?? canvasW / 2;
155
+ const cy = position?.y ?? canvasH / 2;
156
+
157
+ const layer = this.getLayer(editor, 'dieline-overlay');
158
+ if (!layer) return;
159
+
160
+ // Clear existing objects
161
+ layer.remove(...layer.getObjects());
162
+
163
+ // Get Hole Data
164
+ const holeTool = editor.getExtension('HoleTool') as any;
165
+ const holes = holeTool ? (holeTool.options.holes || []) : [];
166
+ const innerRadius = holeTool ? (holeTool.options.innerRadius || 15) : 15;
167
+ const outerRadius = holeTool ? (holeTool.options.outerRadius || 25) : 25;
168
+
169
+ const holeData: HoleData[] = holes.map((h: any) => ({
170
+ x: h.x,
171
+ y: h.y,
172
+ innerRadius,
173
+ outerRadius
174
+ }));
175
+
176
+ // 1. Draw Mask (Outside)
177
+ const cutW = Math.max(0, width + offset * 2);
178
+ const cutH = Math.max(0, height + offset * 2);
179
+ const cutR = radius === 0 ? 0 : Math.max(0, radius + offset);
180
+
181
+ // Use Paper.js to generate the complex mask path
182
+ const maskPathData = generateMaskPath({
183
+ canvasWidth: canvasW,
184
+ canvasHeight: canvasH,
185
+ shape,
186
+ width: cutW,
187
+ height: cutH,
188
+ radius: cutR,
189
+ x: cx,
190
+ y: cy,
191
+ holes: holeData
192
+ });
193
+
194
+ const mask = new Path(maskPathData, {
195
+ fill: outsideColor,
196
+ stroke: null,
197
+ selectable: false,
198
+ evented: false,
199
+ originX: 'left' as const,
200
+ originY: 'top' as const,
201
+ left: 0,
202
+ top: 0
203
+ });
204
+ layer.add(mask);
205
+
206
+ // 2. Draw Inside Fill (Dieline Shape itself, merged with holes if needed, or just the shape?)
207
+ // The user wants "fusion effect" so holes should be part of the dieline visually.
208
+ // If insideColor is transparent, it doesn't matter much.
209
+ // If insideColor is opaque, we need to punch holes in it too.
210
+ // Let's use Paper.js for this too if insideColor is not transparent.
211
+
212
+ if (insideColor && insideColor !== 'transparent' && insideColor !== 'rgba(0,0,0,0)') {
213
+ // Generate path for the product shape (Paper) = Dieline - Holes
214
+ const productPathData = generateDielinePath({
215
+ shape,
216
+ width: cutW,
217
+ height: cutH,
218
+ radius: cutR,
219
+ x: cx,
220
+ y: cy,
221
+ holes: holeData
222
+ });
223
+
224
+ const insideObj = new Path(productPathData, {
225
+ fill: insideColor,
226
+ stroke: null,
227
+ selectable: false,
228
+ evented: false,
229
+ originX: 'left', // paper.js paths are absolute
230
+ originY: 'top'
231
+ });
232
+ layer.add(insideObj);
233
+ }
234
+
235
+ // 3. Draw Bleed Zone (Hatch Fill) and Offset Border
236
+ if (offset !== 0) {
237
+ const bleedPathData = generateBleedZonePath({
238
+ shape,
239
+ width,
240
+ height,
241
+ radius,
242
+ x: cx,
243
+ y: cy,
244
+ holes: holeData
245
+ }, offset);
246
+
247
+ // Use solid red for hatch lines to match dieline, background is transparent
248
+ const pattern = this.createHatchPattern('red');
249
+ if (pattern) {
250
+ const bleedObj = new Path(bleedPathData, {
251
+ fill: pattern,
252
+ stroke: null,
253
+ selectable: false,
254
+ evented: false,
255
+ objectCaching: false,
256
+ originX: 'left',
257
+ originY: 'top'
258
+ });
259
+ layer.add(bleedObj);
260
+ }
261
+
262
+ // Offset Dieline Border
263
+ const offsetPathData = generateDielinePath({
264
+ shape,
265
+ width: cutW,
266
+ height: cutH,
267
+ radius: cutR,
268
+ x: cx,
269
+ y: cy,
270
+ holes: holeData
271
+ });
272
+
273
+ const offsetBorderObj = new Path(offsetPathData, {
274
+ fill: null,
275
+ stroke: '#666', // Grey
276
+ strokeWidth: 1,
277
+ strokeDashArray: [4, 4], // Dashed
278
+ selectable: false,
279
+ evented: false,
280
+ originX: 'left',
281
+ originY: 'top'
282
+ });
283
+ layer.add(offsetBorderObj);
284
+ }
285
+
286
+ // 4. Draw Dieline (Visual Border)
287
+ // This should outline the product shape AND the holes.
288
+ // Paper.js `generateDielinePath` returns exactly this (Dieline - Holes).
289
+
290
+ const borderPathData = generateDielinePath({
291
+ shape,
292
+ width: width,
293
+ height: height,
294
+ radius: radius,
295
+ x: cx,
296
+ y: cy,
297
+ holes: holeData
298
+ });
299
+
300
+ const borderObj = new Path(borderPathData, {
301
+ fill: 'transparent',
302
+ stroke: 'red',
303
+ strokeWidth: 1,
304
+ strokeDashArray: style === 'dashed' ? [5, 5] : undefined,
305
+ selectable: false,
306
+ evented: false,
307
+ originX: 'left',
308
+ originY: 'top'
309
+ });
310
+
311
+ layer.add(borderObj);
312
+
313
+ editor.canvas.requestRenderAll();
314
+ }
315
+
316
+ commands: Record<string, Command> = {
317
+ reset: {
318
+ execute: (editor: Editor) => {
319
+ this.options = {
320
+ shape: 'rect',
321
+ width: 300,
322
+ height: 300,
323
+ radius: 0,
324
+ offset: 0,
325
+ style: 'solid',
326
+ insideColor: 'rgba(0,0,0,0)',
327
+ outsideColor: '#ffffff'
328
+ };
329
+ this.updateDieline(editor);
330
+ return true;
331
+ }
332
+ },
333
+ destroy: {
334
+ execute: (editor: Editor) => {
335
+ this.destroyLayer(editor);
336
+ return true;
337
+ }
338
+ },
339
+ setDimensions: {
340
+ execute: (editor: Editor, width: number, height: number) => {
341
+ if (this.options.width === width && this.options.height === height) return true;
342
+ this.options.width = width;
343
+ this.options.height = height;
344
+ this.updateDieline(editor);
345
+ return true;
346
+ },
347
+ schema: {
348
+ width: {
349
+ type: 'number',
350
+ label: 'Width',
351
+ min: 10,
352
+ max: 2000,
353
+ required: true
354
+ },
355
+ height: {
356
+ type: 'number',
357
+ label: 'Height',
358
+ min: 10,
359
+ max: 2000,
360
+ required: true
361
+ }
362
+ }
363
+ },
364
+ setShape: {
365
+ execute: (editor: Editor, shape: 'rect' | 'circle' | 'ellipse') => {
366
+ if (this.options.shape === shape) return true;
367
+ this.options.shape = shape;
368
+ this.updateDieline(editor);
369
+ return true;
370
+ },
371
+ schema: {
372
+ shape: {
373
+ type: 'string',
374
+ label: 'Shape',
375
+ options: ['rect', 'circle', 'ellipse'],
376
+ required: true
377
+ }
378
+ }
379
+ },
380
+ setBleed: {
381
+ execute: (editor: Editor, bleed: number) => {
382
+ if (this.options.offset === bleed) return true;
383
+ this.options.offset = bleed;
384
+ this.updateDieline(editor);
385
+ return true;
386
+ },
387
+ schema: {
388
+ bleed: {
389
+ type: 'number',
390
+ label: 'Bleed',
391
+ min: -100,
392
+ max: 100,
393
+ required: true
394
+ }
395
+ }
396
+ }
397
+ };
398
+
399
+ public getGeometry(editor: Editor): DielineGeometry | null {
400
+ const { shape, width, height, radius, position, borderLength } = this.options;
401
+ const canvasW = editor.canvas.width || 800;
402
+ const canvasH = editor.canvas.height || 600;
403
+
404
+ let visualWidth = width;
405
+ let visualHeight = height;
406
+
407
+ if (borderLength && borderLength > 0) {
408
+ visualWidth = Math.max(0, canvasW - borderLength * 2);
409
+ visualHeight = Math.max(0, canvasH - borderLength * 2);
410
+ }
411
+
412
+ const cx = position?.x ?? canvasW / 2;
413
+ const cy = position?.y ?? canvasH / 2;
414
+
415
+ return {
416
+ shape,
417
+ x: cx,
418
+ y: cy,
419
+ width: visualWidth,
420
+ height: visualHeight,
421
+ radius
422
+ };
423
+ }
424
+ }