@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/src/hole.ts ADDED
@@ -0,0 +1,413 @@
1
+ import { Command, Editor, EditorState, Extension, OptionSchema, Circle, Group, Point } from '@pooder/core';
2
+ import { DielineTool, DielineGeometry } from './dieline';
3
+ import { getNearestPointOnDieline } from './geometry';
4
+ import paper from 'paper';
5
+
6
+ export interface HoleToolOptions {
7
+ innerRadius: number;
8
+ outerRadius: number;
9
+ style: 'solid' | 'dashed';
10
+ holes?: Array<{ x: number, y: number }>;
11
+ }
12
+
13
+ export class HoleTool implements Extension<HoleToolOptions> {
14
+ public name = 'HoleTool';
15
+ public options: HoleToolOptions = {
16
+ innerRadius: 15,
17
+ outerRadius: 25,
18
+ style: 'solid',
19
+ holes: []
20
+ };
21
+
22
+ public schema: Record<keyof HoleToolOptions, OptionSchema> = {
23
+ innerRadius: {
24
+ type: 'number',
25
+ min: 1,
26
+ max: 100,
27
+ label: 'Inner Radius'
28
+ },
29
+ outerRadius: {
30
+ type: 'number',
31
+ min: 1,
32
+ max: 100,
33
+ label: 'Outer Radius'
34
+ },
35
+ style: {
36
+ type: 'select',
37
+ options: ['solid', 'dashed'],
38
+ label: 'Line Style'
39
+ },
40
+ holes: {
41
+ type: 'json',
42
+ label: 'Holes'
43
+ } as any
44
+ };
45
+
46
+ private handleMoving: ((e: any) => void) | null = null;
47
+ private handleModified: ((e: any) => void) | null = null;
48
+
49
+ onMount(editor: Editor) {
50
+ this.setup(editor);
51
+ }
52
+
53
+ onUnmount(editor: Editor) {
54
+ this.teardown(editor);
55
+ }
56
+
57
+ onDestroy(editor: Editor) {
58
+ this.teardown(editor);
59
+ }
60
+
61
+ private getDielineGeometry(editor: Editor): DielineGeometry | null {
62
+ const dielineTool = editor.getExtension('DielineTool') as DielineTool;
63
+ if (!dielineTool) return null;
64
+
65
+ const geometry = dielineTool.getGeometry(editor);
66
+ if (!geometry) return null;
67
+
68
+ const offset = dielineTool.options.offset || 0;
69
+
70
+ // Apply offset to geometry
71
+ return {
72
+ ...geometry,
73
+ width: Math.max(0, geometry.width + offset * 2),
74
+ height: Math.max(0, geometry.height + offset * 2),
75
+ radius: Math.max(0, geometry.radius + offset)
76
+ };
77
+ }
78
+
79
+ private setup(editor: Editor) {
80
+ if (!this.handleMoving) {
81
+ this.handleMoving = (e: any) => {
82
+ const target = e.target;
83
+ if (!target || target.data?.type !== 'hole-marker') return;
84
+
85
+ const geometry = this.getDielineGeometry(editor);
86
+ if (!geometry) return;
87
+
88
+ const p = new Point(target.left, target.top);
89
+ const newPos = this.calculateConstrainedPosition(p, geometry);
90
+
91
+ target.set({
92
+ left: newPos.x,
93
+ top: newPos.y
94
+ });
95
+ };
96
+ editor.canvas.on('object:moving', this.handleMoving);
97
+ }
98
+
99
+ if (!this.handleModified) {
100
+ this.handleModified = (e: any) => {
101
+ const target = e.target;
102
+ if (!target || target.data?.type !== 'hole-marker') return;
103
+
104
+ // Update state when hole is moved
105
+ this.syncHolesFromCanvas(editor);
106
+ };
107
+ editor.canvas.on('object:modified', this.handleModified);
108
+ }
109
+
110
+ const opts = this.options;
111
+ // Default hole if none exist
112
+ if (!opts.holes || opts.holes.length === 0) {
113
+ let defaultPos = { x: editor.canvas.width! / 2, y: 50 };
114
+
115
+ const g = this.getDielineGeometry(editor);
116
+ if (g) {
117
+ // Default to Top-Center of Dieline shape
118
+ const topCenter = { x: g.x, y: g.y - g.height / 2 };
119
+ // Snap to exact shape edge
120
+ const snapped = getNearestPointOnDieline(topCenter, { ...g, holes: [] } as any);
121
+ defaultPos = snapped;
122
+ }
123
+
124
+ opts.holes = [defaultPos];
125
+ }
126
+
127
+ this.options = { ...opts };
128
+ this.redraw(editor);
129
+
130
+ // Ensure Dieline updates to reflect current holes (fusion effect)
131
+ const dielineTool = editor.getExtension('DielineTool') as DielineTool;
132
+ if (dielineTool && dielineTool.updateDieline) {
133
+ dielineTool.updateDieline(editor);
134
+ }
135
+ }
136
+
137
+ private teardown(editor: Editor) {
138
+ if (this.handleMoving) {
139
+ editor.canvas.off('object:moving', this.handleMoving);
140
+ this.handleMoving = null;
141
+ }
142
+ if (this.handleModified) {
143
+ editor.canvas.off('object:modified', this.handleModified);
144
+ this.handleModified = null;
145
+ }
146
+
147
+ const objects = editor.canvas.getObjects().filter((obj: any) => obj.data?.type === 'hole-marker');
148
+ objects.forEach(obj => editor.canvas.remove(obj));
149
+
150
+ editor.canvas.requestRenderAll();
151
+ }
152
+
153
+ onUpdate(editor: Editor, state: EditorState) {
154
+ }
155
+
156
+ commands: Record<string, Command> = {
157
+ reset: {
158
+ execute: (editor: Editor) => {
159
+ let defaultPos = { x: editor.canvas.width! / 2, y: 50 };
160
+
161
+ const g = this.getDielineGeometry(editor);
162
+ if (g) {
163
+ const topCenter = { x: g.x, y: g.y - g.height / 2 };
164
+ defaultPos = getNearestPointOnDieline(topCenter, { ...g, holes: [] } as any);
165
+ }
166
+
167
+ this.options = {
168
+ innerRadius: 15,
169
+ outerRadius: 25,
170
+ style: 'solid',
171
+ holes: [defaultPos]
172
+ };
173
+ this.redraw(editor);
174
+
175
+ // Trigger Dieline update
176
+ const dielineTool = editor.getExtension('DielineTool') as DielineTool;
177
+ if (dielineTool && dielineTool.updateDieline) {
178
+ dielineTool.updateDieline(editor);
179
+ }
180
+
181
+ return true;
182
+ }
183
+ },
184
+ addHole: {
185
+ execute: (editor: Editor, x: number, y: number) => {
186
+ if (!this.options.holes) this.options.holes = [];
187
+ this.options.holes.push({ x, y });
188
+ this.redraw(editor);
189
+
190
+ // Trigger Dieline update
191
+ const dielineTool = editor.getExtension('DielineTool') as any;
192
+ if (dielineTool && dielineTool.updateDieline) {
193
+ dielineTool.updateDieline(editor);
194
+ }
195
+
196
+ return true;
197
+ },
198
+ schema: {
199
+ x: {
200
+ type: 'number',
201
+ label: 'X Position',
202
+ required: true
203
+ },
204
+ y: {
205
+ type: 'number',
206
+ label: 'Y Position',
207
+ required: true
208
+ }
209
+ }
210
+ },
211
+ clearHoles: {
212
+ execute: (editor: Editor) => {
213
+ this.options.holes = [];
214
+ this.redraw(editor);
215
+
216
+ // Trigger Dieline update
217
+ const dielineTool = editor.getExtension('DielineTool') as any;
218
+ if (dielineTool && dielineTool.updateDieline) {
219
+ dielineTool.updateDieline(editor);
220
+ }
221
+
222
+ return true;
223
+ }
224
+ }
225
+ };
226
+
227
+ private syncHolesFromCanvas(editor: Editor) {
228
+ const objects = editor.canvas.getObjects().filter((obj: any) => obj.data?.type === 'hole-marker');
229
+ const holes = objects.map(obj => ({ x: obj.left!, y: obj.top! }));
230
+ this.options.holes = holes;
231
+
232
+ // Trigger Dieline update for real-time fusion effect
233
+ const dielineTool = editor.getExtension('DielineTool') as any;
234
+ if (dielineTool && dielineTool.updateDieline) {
235
+ dielineTool.updateDieline(editor);
236
+ }
237
+ }
238
+
239
+ private redraw(editor: Editor) {
240
+ const canvas = editor.canvas;
241
+
242
+ // Remove existing holes
243
+ const existing = canvas.getObjects().filter((obj: any) => obj.data?.type === 'hole-marker');
244
+ existing.forEach(obj => canvas.remove(obj));
245
+
246
+ const { innerRadius, outerRadius, style, holes } = this.options;
247
+
248
+ if (!holes || holes.length === 0) {
249
+ canvas.requestRenderAll();
250
+ return;
251
+ }
252
+
253
+ holes.forEach((hole, index) => {
254
+ const innerCircle = new Circle({
255
+ radius: innerRadius,
256
+ fill: 'transparent',
257
+ stroke: 'red',
258
+ strokeWidth: 2,
259
+ originX: 'center',
260
+ originY: 'center'
261
+ });
262
+
263
+ const outerCircle = new Circle({
264
+ radius: outerRadius,
265
+ fill: 'transparent',
266
+ stroke: '#666',
267
+ strokeWidth: 1,
268
+ strokeDashArray: style === 'dashed' ? [5, 5] : undefined,
269
+ originX: 'center',
270
+ originY: 'center'
271
+ });
272
+
273
+ const holeGroup = new Group([outerCircle, innerCircle], {
274
+ left: hole.x,
275
+ top: hole.y,
276
+ originX: 'center',
277
+ originY: 'center',
278
+ selectable: true,
279
+ hasControls: false, // Don't allow resizing/rotating
280
+ hasBorders: false,
281
+ subTargetCheck: false,
282
+ opacity: 0, // Default hidden
283
+ hoverCursor: 'move',
284
+ data: { type: 'hole-marker', index }
285
+ }as any);
286
+ (holeGroup as any).name = 'hole-marker';
287
+
288
+ // Auto-show/hide logic
289
+ holeGroup.on('mouseover', () => {
290
+ holeGroup.set('opacity', 1);
291
+ canvas.requestRenderAll();
292
+ });
293
+ holeGroup.on('mouseout', () => {
294
+ if (canvas.getActiveObject() !== holeGroup) {
295
+ holeGroup.set('opacity', 0);
296
+ canvas.requestRenderAll();
297
+ }
298
+ });
299
+ holeGroup.on('selected', () => {
300
+ holeGroup.set('opacity', 1);
301
+ canvas.requestRenderAll();
302
+ });
303
+ holeGroup.on('deselected', () => {
304
+ holeGroup.set('opacity', 0);
305
+ canvas.requestRenderAll();
306
+ });
307
+
308
+ canvas.add(holeGroup);
309
+ canvas.bringObjectToFront(holeGroup);
310
+ });
311
+
312
+ canvas.requestRenderAll();
313
+ }
314
+
315
+ private calculateConstrainedPosition(p: Point, g: DielineGeometry): Point {
316
+ // Use Paper.js to get accurate nearest point
317
+ // This handles ellipses, rects, and rounded rects correctly
318
+
319
+ // Convert to holes format for geometry options
320
+ const options = {
321
+ ...g,
322
+ holes: [] // We don't need holes for boundary calculation
323
+ };
324
+
325
+ const nearest = getNearestPointOnDieline({x: p.x, y: p.y}, options as any);
326
+
327
+ // Now constrain distance
328
+ const nearestP = new Point(nearest.x, nearest.y);
329
+ const dist = p.distanceFrom(nearestP);
330
+
331
+ // Determine if point is inside or outside
332
+ // Simple heuristic: distance from center
333
+ // Or using paper.js contains() if we had the full path object
334
+ // For convex shapes, center distance works mostly, but let's use the vector direction
335
+
336
+ // Vector from nearest to current point
337
+ const v = p.subtract(nearestP);
338
+
339
+ // Vector from center to nearest point (approximate normal for convex shapes)
340
+ const center = new Point(g.x, g.y);
341
+ const centerToNearest = nearestP.subtract(center);
342
+
343
+ // Dot product to see if they align (outside) or oppose (inside)
344
+ // If point is exactly on line, dist is 0.
345
+
346
+ // However, we want to constrain the point to be within [innerRadius, -outerRadius] distance from the edge.
347
+ // Actually, usually users want to snap to the edge or stay within a reasonable margin.
348
+ // The previous logic clamped the distance.
349
+
350
+ // Let's implement a simple snap-to-edge if close, otherwise allow free movement but clamp max distance?
351
+ // Or reproduce the previous "slide along edge" behavior.
352
+ // Previous behavior: "clampedDist = Math.min(dist, innerRadius); ... Math.max(dist, -outerRadius)"
353
+ // This implies the hole center can be slightly inside or outside the main shape edge.
354
+
355
+ // Let's determine sign of distance
356
+ // We can use paper.js Shape.contains(point) to check if inside.
357
+ // But getNearestPointOnDieline returns just coordinates.
358
+
359
+ // Optimization: Let's assume for Dieline shapes (convex-ish),
360
+ // if distance from center > distance of nearest from center, it's outside.
361
+ const distToCenter = p.distanceFrom(center);
362
+ const nearestDistToCenter = nearestP.distanceFrom(center);
363
+
364
+ let signedDist = dist;
365
+ if (distToCenter < nearestDistToCenter) {
366
+ signedDist = -dist; // Inside
367
+ }
368
+
369
+ // Clamp distance
370
+ let clampedDist = signedDist;
371
+ if (signedDist > 0) {
372
+ clampedDist = Math.min(signedDist, this.options.innerRadius);
373
+ } else {
374
+ clampedDist = Math.max(signedDist, -this.options.outerRadius);
375
+ }
376
+
377
+ // Reconstruct point
378
+ // If dist is very small, just use nearestP
379
+ if (dist < 0.001) return nearestP;
380
+
381
+ // Direction vector normalized
382
+ const dir = v.scalarDivide(dist);
383
+
384
+ // New point = nearest + dir * clampedDist
385
+ // Note: if inside (signedDist < 0), v points towards center (roughly), dist is positive magnitude.
386
+ // Wait, v = p - nearest.
387
+ // If p is inside, p is closer to center. v points Inwards.
388
+ // If we want clampedDist to be negative, we should probably stick to normal vectors.
389
+
390
+ // Let's simplify:
391
+ // Just place it at nearest point + offset vector.
392
+ // Offset vector is 'v' scaled to clampedDist.
393
+
394
+ // If p is inside, v points in. length is 'dist'.
395
+ // We want length to be 'clampedDist' (magnitude).
396
+ // Since clampedDist is negative for inside, we need to be careful with signs.
397
+
398
+ // Actually simpler:
399
+ // We want the result to lie on the line connecting Center -> P -> Nearest? No.
400
+ // We want it on the line Nearest -> P.
401
+
402
+ // Current distance is 'dist'.
403
+ // Desired distance is abs(clampedDist).
404
+ // If clampedDist sign matches signedDist sign, we just scale v.
405
+
406
+ const scale = Math.abs(clampedDist) / (dist || 1);
407
+
408
+ // If we are clamping, we just scale the vector from nearest.
409
+ const offset = v.scalarMultiply(scale);
410
+
411
+ return nearestP.add(offset);
412
+ }
413
+ }
package/src/image.ts ADDED
@@ -0,0 +1,146 @@
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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
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';