@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/film.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {Command, Editor, EditorState, Extension, Image, OptionSchema, PooderLayer} from '@pooder/core';
|
|
2
|
+
|
|
3
|
+
interface FilmToolOptions {
|
|
4
|
+
url: string;
|
|
5
|
+
opacity: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class FilmTool implements Extension<FilmToolOptions> {
|
|
9
|
+
public name = 'FilmTool';
|
|
10
|
+
public options: FilmToolOptions = {
|
|
11
|
+
url: '',
|
|
12
|
+
opacity: 0.5
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
public schema: Record<keyof FilmToolOptions, OptionSchema> = {
|
|
16
|
+
url: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
label: 'Film Image URL'
|
|
19
|
+
},
|
|
20
|
+
opacity: {
|
|
21
|
+
type: 'number',
|
|
22
|
+
min: 0,
|
|
23
|
+
max: 1,
|
|
24
|
+
step: 0.1,
|
|
25
|
+
label: 'Opacity'
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
onMount(editor: Editor) {
|
|
30
|
+
this.initLayer(editor);
|
|
31
|
+
this.updateFilm(editor, this.options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onUnmount(editor: Editor) {
|
|
35
|
+
const layer = editor.getLayer('overlay');
|
|
36
|
+
if (layer) {
|
|
37
|
+
const img = editor.getObject('film-image', 'overlay');
|
|
38
|
+
if (img) {
|
|
39
|
+
layer.remove(img);
|
|
40
|
+
editor.canvas.requestRenderAll();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onUpdate(editor: Editor, state: EditorState) {
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private initLayer(editor: Editor) {
|
|
49
|
+
let overlayLayer = editor.getLayer('overlay');
|
|
50
|
+
if (!overlayLayer) {
|
|
51
|
+
const width = editor.state.width;
|
|
52
|
+
const height = editor.state.height;
|
|
53
|
+
|
|
54
|
+
const layer = new PooderLayer([], {
|
|
55
|
+
width,
|
|
56
|
+
height,
|
|
57
|
+
left: 0,
|
|
58
|
+
top: 0,
|
|
59
|
+
originX: 'left',
|
|
60
|
+
originY: 'top',
|
|
61
|
+
selectable: false,
|
|
62
|
+
evented: false,
|
|
63
|
+
subTargetCheck: false,
|
|
64
|
+
interactive: false,
|
|
65
|
+
data: {
|
|
66
|
+
id: 'overlay'
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
editor.canvas.add(layer);
|
|
71
|
+
editor.canvas.bringObjectToFront(layer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
private async updateFilm(editor: Editor, options: FilmToolOptions) {
|
|
77
|
+
const layer = editor.getLayer('overlay');
|
|
78
|
+
if (!layer){
|
|
79
|
+
console.warn('[FilmTool] Overlay layer not found');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { url, opacity } = options;
|
|
84
|
+
|
|
85
|
+
if (!url) {
|
|
86
|
+
const img = editor.getObject('film-image', 'overlay');
|
|
87
|
+
if (img) {
|
|
88
|
+
layer.remove(img);
|
|
89
|
+
editor.canvas.requestRenderAll();
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const width = editor.state.width;
|
|
95
|
+
const height = editor.state.height;
|
|
96
|
+
|
|
97
|
+
let img = editor.getObject('film-image', 'overlay') as Image;
|
|
98
|
+
try {
|
|
99
|
+
if (img) {
|
|
100
|
+
if (img.getSrc() !== url) {
|
|
101
|
+
await img.setSrc(url);
|
|
102
|
+
}
|
|
103
|
+
img.set({ opacity });
|
|
104
|
+
} else {
|
|
105
|
+
img = await Image.fromURL(url, { crossOrigin: 'anonymous' });
|
|
106
|
+
img.scaleToWidth(width);
|
|
107
|
+
if (img.getScaledHeight() < height)
|
|
108
|
+
img.scaleToHeight(height);
|
|
109
|
+
img.set({
|
|
110
|
+
originX: 'left',
|
|
111
|
+
originY: 'top',
|
|
112
|
+
left: 0,
|
|
113
|
+
top: 0,
|
|
114
|
+
opacity,
|
|
115
|
+
selectable: false,
|
|
116
|
+
evented: false,
|
|
117
|
+
data: { id: 'film-image' }
|
|
118
|
+
});
|
|
119
|
+
layer.add(img);
|
|
120
|
+
}
|
|
121
|
+
editor.canvas.requestRenderAll();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('[FilmTool] Failed to load film image', url, error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
commands: Record<string, Command> = {
|
|
128
|
+
setFilmImage: {
|
|
129
|
+
execute: (editor: Editor, url: string, opacity: number) => {
|
|
130
|
+
if (this.options.url === url && this.options.opacity === opacity) return true;
|
|
131
|
+
|
|
132
|
+
this.options.url = url;
|
|
133
|
+
this.options.opacity = opacity;
|
|
134
|
+
|
|
135
|
+
this.updateFilm(editor, this.options);
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
},
|
|
139
|
+
schema: {
|
|
140
|
+
url: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
label: 'Image URL',
|
|
143
|
+
required: true
|
|
144
|
+
},
|
|
145
|
+
opacity: {
|
|
146
|
+
type: 'number',
|
|
147
|
+
label: 'Opacity',
|
|
148
|
+
min: 0,
|
|
149
|
+
max: 1,
|
|
150
|
+
required: true
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/geometry.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import paper from 'paper';
|
|
2
|
+
|
|
3
|
+
export interface HoleData {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
innerRadius: number;
|
|
7
|
+
outerRadius: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GeometryOptions {
|
|
11
|
+
shape: 'rect' | 'circle' | 'ellipse';
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
radius: number;
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
holes: Array<HoleData>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MaskGeometryOptions extends GeometryOptions {
|
|
21
|
+
canvasWidth: number;
|
|
22
|
+
canvasHeight: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initializes paper.js project if not already initialized.
|
|
27
|
+
*/
|
|
28
|
+
function ensurePaper(width: number, height: number) {
|
|
29
|
+
if (!paper.project) {
|
|
30
|
+
paper.setup(new paper.Size(width, height));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates the base dieline shape (Rect/Circle/Ellipse)
|
|
36
|
+
*/
|
|
37
|
+
function createBaseShape(options: GeometryOptions): paper.PathItem {
|
|
38
|
+
const { shape, width, height, radius, x, y } = options;
|
|
39
|
+
const center = new paper.Point(x, y);
|
|
40
|
+
|
|
41
|
+
if (shape === 'rect') {
|
|
42
|
+
return new paper.Path.Rectangle({
|
|
43
|
+
point: [x - width / 2, y - height / 2],
|
|
44
|
+
size: [Math.max(0, width), Math.max(0, height)],
|
|
45
|
+
radius: Math.max(0, radius)
|
|
46
|
+
});
|
|
47
|
+
} else if (shape === 'circle') {
|
|
48
|
+
const r = Math.min(width, height) / 2;
|
|
49
|
+
return new paper.Path.Circle({
|
|
50
|
+
center: center,
|
|
51
|
+
radius: Math.max(0, r)
|
|
52
|
+
});
|
|
53
|
+
} else { // ellipse
|
|
54
|
+
return new paper.Path.Ellipse({
|
|
55
|
+
center: center,
|
|
56
|
+
radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Internal helper to generate the Dieline Shape (Paper Item).
|
|
63
|
+
* Caller is responsible for cleanup.
|
|
64
|
+
*/
|
|
65
|
+
function getDielineShape(options: GeometryOptions): paper.PathItem {
|
|
66
|
+
// 1. Create Base Shape
|
|
67
|
+
let mainShape = createBaseShape(options);
|
|
68
|
+
|
|
69
|
+
const { holes } = options;
|
|
70
|
+
|
|
71
|
+
if (holes && holes.length > 0) {
|
|
72
|
+
let lugsPath: paper.PathItem | null = null;
|
|
73
|
+
let cutsPath: paper.PathItem | null = null;
|
|
74
|
+
|
|
75
|
+
holes.forEach(hole => {
|
|
76
|
+
// Create Lug (Outer Radius)
|
|
77
|
+
const lug = new paper.Path.Circle({
|
|
78
|
+
center: [hole.x, hole.y],
|
|
79
|
+
radius: hole.outerRadius
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Check intersection with main body
|
|
83
|
+
// Only add lug if it intersects (or is contained in) the main shape
|
|
84
|
+
// This prevents floating islands when bleed shrinks
|
|
85
|
+
if (!mainShape.intersects(lug) && !mainShape.contains(lug.position)) {
|
|
86
|
+
lug.remove();
|
|
87
|
+
return; // Skip this lug
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create Cut (Inner Radius)
|
|
91
|
+
const cut = new paper.Path.Circle({
|
|
92
|
+
center: [hole.x, hole.y],
|
|
93
|
+
radius: hole.innerRadius
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Union Lugs
|
|
97
|
+
if (!lugsPath) {
|
|
98
|
+
lugsPath = lug;
|
|
99
|
+
} else {
|
|
100
|
+
const temp = lugsPath.unite(lug);
|
|
101
|
+
lugsPath.remove();
|
|
102
|
+
lug.remove();
|
|
103
|
+
lugsPath = temp;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Union Cuts
|
|
107
|
+
if (!cutsPath) {
|
|
108
|
+
cutsPath = cut;
|
|
109
|
+
} else {
|
|
110
|
+
const temp = cutsPath.unite(cut);
|
|
111
|
+
cutsPath.remove();
|
|
112
|
+
cut.remove();
|
|
113
|
+
cutsPath = temp;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 2. Add Lugs to Main Shape (Union) - Additive Fusion
|
|
118
|
+
if (lugsPath) {
|
|
119
|
+
const temp = mainShape.unite(lugsPath);
|
|
120
|
+
mainShape.remove();
|
|
121
|
+
// @ts-ignore
|
|
122
|
+
lugsPath.remove();
|
|
123
|
+
mainShape = temp;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 3. Subtract Cuts from Main Shape (Difference)
|
|
127
|
+
if (cutsPath) {
|
|
128
|
+
const temp = mainShape.subtract(cutsPath);
|
|
129
|
+
mainShape.remove();
|
|
130
|
+
// @ts-ignore
|
|
131
|
+
cutsPath.remove();
|
|
132
|
+
mainShape = temp;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return mainShape;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generates the path data for the Dieline (Product Shape).
|
|
141
|
+
* Logic: (BaseShape UNION IntersectingLugs) SUBTRACT Cuts
|
|
142
|
+
*/
|
|
143
|
+
export function generateDielinePath(options: GeometryOptions): string {
|
|
144
|
+
ensurePaper(options.width * 2, options.height * 2);
|
|
145
|
+
paper.project.activeLayer.removeChildren();
|
|
146
|
+
|
|
147
|
+
const mainShape = getDielineShape(options);
|
|
148
|
+
|
|
149
|
+
const pathData = mainShape.pathData;
|
|
150
|
+
mainShape.remove();
|
|
151
|
+
|
|
152
|
+
return pathData;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generates the path data for the Mask (Background Overlay).
|
|
157
|
+
* Logic: Canvas SUBTRACT ProductShape
|
|
158
|
+
*/
|
|
159
|
+
export function generateMaskPath(options: MaskGeometryOptions): string {
|
|
160
|
+
ensurePaper(options.canvasWidth, options.canvasHeight);
|
|
161
|
+
paper.project.activeLayer.removeChildren();
|
|
162
|
+
|
|
163
|
+
const { canvasWidth, canvasHeight } = options;
|
|
164
|
+
|
|
165
|
+
// 1. Canvas Background
|
|
166
|
+
const maskRect = new paper.Path.Rectangle({
|
|
167
|
+
point: [0, 0],
|
|
168
|
+
size: [canvasWidth, canvasHeight]
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 2. Re-create Product Shape
|
|
172
|
+
const mainShape = getDielineShape(options);
|
|
173
|
+
|
|
174
|
+
// 3. Subtract Product from Mask
|
|
175
|
+
const finalMask = maskRect.subtract(mainShape);
|
|
176
|
+
|
|
177
|
+
maskRect.remove();
|
|
178
|
+
mainShape.remove();
|
|
179
|
+
|
|
180
|
+
const pathData = finalMask.pathData;
|
|
181
|
+
finalMask.remove();
|
|
182
|
+
|
|
183
|
+
return pathData;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Generates the path data for the Bleed Zone (Area between Original and Offset).
|
|
188
|
+
*/
|
|
189
|
+
export function generateBleedZonePath(options: GeometryOptions, offset: number): string {
|
|
190
|
+
// Ensure canvas is large enough
|
|
191
|
+
const maxDim = Math.max(options.width, options.height) + Math.abs(offset) * 4;
|
|
192
|
+
ensurePaper(maxDim, maxDim);
|
|
193
|
+
paper.project.activeLayer.removeChildren();
|
|
194
|
+
|
|
195
|
+
// 1. Original Shape
|
|
196
|
+
const shapeOriginal = getDielineShape(options);
|
|
197
|
+
|
|
198
|
+
// 2. Offset Shape
|
|
199
|
+
// Adjust dimensions for offset
|
|
200
|
+
const offsetOptions: GeometryOptions = {
|
|
201
|
+
...options,
|
|
202
|
+
width: Math.max(0, options.width + offset * 2),
|
|
203
|
+
height: Math.max(0, options.height + offset * 2),
|
|
204
|
+
radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset)
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const shapeOffset = getDielineShape(offsetOptions);
|
|
208
|
+
|
|
209
|
+
// 3. Calculate Difference
|
|
210
|
+
let bleedZone: paper.PathItem;
|
|
211
|
+
if (offset > 0) {
|
|
212
|
+
bleedZone = shapeOffset.subtract(shapeOriginal);
|
|
213
|
+
} else {
|
|
214
|
+
bleedZone = shapeOriginal.subtract(shapeOffset);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const pathData = bleedZone.pathData;
|
|
218
|
+
|
|
219
|
+
// Cleanup
|
|
220
|
+
shapeOriginal.remove();
|
|
221
|
+
shapeOffset.remove();
|
|
222
|
+
bleedZone.remove();
|
|
223
|
+
|
|
224
|
+
return pathData;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Finds the nearest point on the Dieline geometry for a given target point.
|
|
229
|
+
* Used for constraining hole movement.
|
|
230
|
+
*/
|
|
231
|
+
export function getNearestPointOnDieline(point: {x: number, y: number}, options: GeometryOptions): {x: number, y: number} {
|
|
232
|
+
ensurePaper(options.width * 2, options.height * 2);
|
|
233
|
+
paper.project.activeLayer.removeChildren();
|
|
234
|
+
|
|
235
|
+
const shape = createBaseShape(options);
|
|
236
|
+
|
|
237
|
+
const p = new paper.Point(point.x, point.y);
|
|
238
|
+
const nearest = shape.getNearestPoint(p);
|
|
239
|
+
|
|
240
|
+
const result = { x: nearest.x, y: nearest.y };
|
|
241
|
+
shape.remove();
|
|
242
|
+
|
|
243
|
+
return result;
|
|
244
|
+
}
|