@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/CHANGELOG.md +9 -0
- package/dist/index.d.mts +167 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.js +1693 -0
- package/dist/index.mjs +1654 -0
- package/package.json +27 -0
- package/src/background.ts +173 -0
- package/src/dieline.ts +424 -0
- package/src/film.ts +155 -0
- package/src/geometry.ts +244 -0
- package/src/hole.ts +413 -0
- package/src/image.ts +146 -0
- package/src/index.ts +7 -0
- package/src/ruler.ts +238 -0
- package/src/white-ink.ts +302 -0
- package/tsconfig.json +13 -0
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
|
+
}
|