@pooder/kit 2.0.0 → 3.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/CHANGELOG.md +11 -0
- package/dist/index.d.mts +246 -134
- package/dist/index.d.ts +246 -134
- package/dist/index.js +2051 -1045
- package/dist/index.mjs +2042 -1050
- package/package.json +3 -2
- package/src/CanvasService.ts +65 -0
- package/src/background.ts +156 -109
- package/src/coordinate.ts +49 -0
- package/src/dieline.ts +536 -336
- package/src/film.ts +120 -89
- package/src/geometry.ts +251 -38
- package/src/hole.ts +422 -286
- package/src/image.ts +374 -174
- package/src/index.ts +1 -0
- package/src/mirror.ts +86 -49
- package/src/ruler.ts +188 -118
- package/src/tracer.ts +372 -0
- package/src/white-ink.ts +186 -142
package/src/dieline.ts
CHANGED
|
@@ -1,133 +1,386 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Command,
|
|
3
|
-
Editor,
|
|
4
|
-
EditorState,
|
|
5
2
|
Extension,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Path,
|
|
11
|
-
PooderLayer,
|
|
12
|
-
Pattern,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
5
|
+
CommandContribution,
|
|
6
|
+
ConfigurationContribution,
|
|
13
7
|
} from "@pooder/core";
|
|
8
|
+
import { Path, Pattern } from "fabric";
|
|
9
|
+
import CanvasService from "./CanvasService";
|
|
10
|
+
import { ImageTracer } from "./tracer";
|
|
11
|
+
import { Coordinate } from "./coordinate";
|
|
14
12
|
import {
|
|
15
13
|
generateDielinePath,
|
|
16
14
|
generateMaskPath,
|
|
17
15
|
generateBleedZonePath,
|
|
16
|
+
getPathBounds,
|
|
18
17
|
HoleData,
|
|
19
18
|
} from "./geometry";
|
|
20
19
|
|
|
21
|
-
export interface DielineToolOptions {
|
|
22
|
-
shape: "rect" | "circle" | "ellipse";
|
|
23
|
-
width: number;
|
|
24
|
-
height: number;
|
|
25
|
-
radius: number; // corner radius for rect
|
|
26
|
-
position?: { x: number; y: number };
|
|
27
|
-
borderLength?: number;
|
|
28
|
-
offset: number;
|
|
29
|
-
style: "solid" | "dashed";
|
|
30
|
-
insideColor: string;
|
|
31
|
-
outsideColor: string;
|
|
32
|
-
showBleedLines?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Alias for compatibility if needed, or just use DielineToolOptions
|
|
36
|
-
export type DielineConfig = DielineToolOptions;
|
|
37
|
-
|
|
38
20
|
export interface DielineGeometry {
|
|
39
|
-
shape: "rect" | "circle" | "ellipse";
|
|
21
|
+
shape: "rect" | "circle" | "ellipse" | "custom";
|
|
40
22
|
x: number;
|
|
41
23
|
y: number;
|
|
42
24
|
width: number;
|
|
43
25
|
height: number;
|
|
44
26
|
radius: number;
|
|
27
|
+
offset: number;
|
|
28
|
+
borderLength?: number;
|
|
29
|
+
pathData?: string;
|
|
45
30
|
}
|
|
46
31
|
|
|
47
|
-
export class DielineTool implements Extension
|
|
48
|
-
|
|
49
|
-
public
|
|
50
|
-
|
|
51
|
-
width: 300,
|
|
52
|
-
height: 300,
|
|
53
|
-
radius: 0,
|
|
54
|
-
offset: 0,
|
|
55
|
-
style: "solid",
|
|
56
|
-
insideColor: "rgba(0,0,0,0)",
|
|
57
|
-
outsideColor: "#ffffff",
|
|
58
|
-
showBleedLines: true,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
public schema: Record<keyof DielineToolOptions, OptionSchema> = {
|
|
62
|
-
shape: {
|
|
63
|
-
type: "select",
|
|
64
|
-
options: ["rect", "circle", "ellipse"],
|
|
65
|
-
label: "Shape",
|
|
66
|
-
},
|
|
67
|
-
width: { type: "number", min: 10, max: 2000, label: "Width" },
|
|
68
|
-
height: { type: "number", min: 10, max: 2000, label: "Height" },
|
|
69
|
-
radius: { type: "number", min: 0, max: 500, label: "Corner Radius" },
|
|
70
|
-
position: { type: "string", label: "Position" }, // Complex object, simplified for now or need custom handler
|
|
71
|
-
borderLength: { type: "number", min: 0, max: 500, label: "Margin" },
|
|
72
|
-
offset: { type: "number", min: -100, max: 100, label: "Bleed Offset" },
|
|
73
|
-
showBleedLines: { type: "boolean", label: "Show Bleed Lines" },
|
|
74
|
-
style: {
|
|
75
|
-
type: "select",
|
|
76
|
-
options: ["solid", "dashed"],
|
|
77
|
-
label: "Line Style",
|
|
78
|
-
},
|
|
79
|
-
insideColor: { type: "color", label: "Inside Color" },
|
|
80
|
-
outsideColor: { type: "color", label: "Outside Color" },
|
|
32
|
+
export class DielineTool implements Extension {
|
|
33
|
+
id = "pooder.kit.dieline";
|
|
34
|
+
public metadata = {
|
|
35
|
+
name: "DielineTool",
|
|
81
36
|
};
|
|
82
37
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
38
|
+
private shape: "rect" | "circle" | "ellipse" | "custom" = "rect";
|
|
39
|
+
private width: number = 500;
|
|
40
|
+
private height: number = 500;
|
|
41
|
+
private radius: number = 0;
|
|
42
|
+
private offset: number = 0;
|
|
43
|
+
private style: "solid" | "dashed" = "solid";
|
|
44
|
+
private insideColor: string = "rgba(0,0,0,0)";
|
|
45
|
+
private outsideColor: string = "#ffffff";
|
|
46
|
+
private showBleedLines: boolean = true;
|
|
47
|
+
private holes: HoleData[] = [];
|
|
48
|
+
// Position is stored as normalized coordinates (0-1)
|
|
49
|
+
private position?: { x: number; y: number };
|
|
50
|
+
private borderLength?: number;
|
|
51
|
+
private pathData?: string;
|
|
52
|
+
|
|
53
|
+
private canvasService?: CanvasService;
|
|
54
|
+
private context?: ExtensionContext;
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
options?: Partial<{
|
|
58
|
+
shape: "rect" | "circle" | "ellipse" | "custom";
|
|
59
|
+
width: number;
|
|
60
|
+
height: number;
|
|
61
|
+
radius: number;
|
|
62
|
+
// Position is normalized (0-1)
|
|
63
|
+
position: { x: number; y: number };
|
|
64
|
+
borderLength: number;
|
|
65
|
+
offset: number;
|
|
66
|
+
style: "solid" | "dashed";
|
|
67
|
+
insideColor: string;
|
|
68
|
+
outsideColor: string;
|
|
69
|
+
showBleedLines: boolean;
|
|
70
|
+
holes: HoleData[];
|
|
71
|
+
pathData: string;
|
|
72
|
+
}>,
|
|
73
|
+
) {
|
|
74
|
+
if (options) {
|
|
75
|
+
Object.assign(this, options);
|
|
76
|
+
}
|
|
86
77
|
}
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
this.
|
|
79
|
+
activate(context: ExtensionContext) {
|
|
80
|
+
this.context = context;
|
|
81
|
+
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
82
|
+
if (!this.canvasService) {
|
|
83
|
+
console.warn("CanvasService not found for DielineTool");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const configService = context.services.get<any>("ConfigurationService");
|
|
88
|
+
if (configService) {
|
|
89
|
+
// Load initial config
|
|
90
|
+
this.shape = configService.get("dieline.shape", this.shape);
|
|
91
|
+
this.width = configService.get("dieline.width", this.width);
|
|
92
|
+
this.height = configService.get("dieline.height", this.height);
|
|
93
|
+
this.radius = configService.get("dieline.radius", this.radius);
|
|
94
|
+
this.borderLength = configService.get(
|
|
95
|
+
"dieline.borderLength",
|
|
96
|
+
this.borderLength,
|
|
97
|
+
);
|
|
98
|
+
this.offset = configService.get("dieline.offset", this.offset);
|
|
99
|
+
this.style = configService.get("dieline.style", this.style);
|
|
100
|
+
this.insideColor = configService.get(
|
|
101
|
+
"dieline.insideColor",
|
|
102
|
+
this.insideColor,
|
|
103
|
+
);
|
|
104
|
+
this.outsideColor = configService.get(
|
|
105
|
+
"dieline.outsideColor",
|
|
106
|
+
this.outsideColor,
|
|
107
|
+
);
|
|
108
|
+
this.showBleedLines = configService.get(
|
|
109
|
+
"dieline.showBleedLines",
|
|
110
|
+
this.showBleedLines,
|
|
111
|
+
);
|
|
112
|
+
this.holes = configService.get("dieline.holes", this.holes);
|
|
113
|
+
this.pathData = configService.get("dieline.pathData", this.pathData);
|
|
114
|
+
|
|
115
|
+
// Listen for changes
|
|
116
|
+
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
117
|
+
if (e.key.startsWith("dieline.")) {
|
|
118
|
+
const prop = e.key.split(".")[1];
|
|
119
|
+
console.log(
|
|
120
|
+
`[DielineTool] Config change detected: ${e.key} -> ${e.value}`,
|
|
121
|
+
);
|
|
122
|
+
if (prop && prop in this) {
|
|
123
|
+
(this as any)[prop] = e.value;
|
|
124
|
+
this.updateDieline();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.createLayer();
|
|
131
|
+
this.updateDieline();
|
|
90
132
|
}
|
|
91
133
|
|
|
92
|
-
|
|
93
|
-
this.
|
|
134
|
+
deactivate(context: ExtensionContext) {
|
|
135
|
+
this.destroyLayer();
|
|
136
|
+
this.canvasService = undefined;
|
|
137
|
+
this.context = undefined;
|
|
94
138
|
}
|
|
95
139
|
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
contribute() {
|
|
141
|
+
return {
|
|
142
|
+
[ContributionPointIds.CONFIGURATIONS]: [
|
|
143
|
+
{
|
|
144
|
+
id: "dieline.shape",
|
|
145
|
+
type: "select",
|
|
146
|
+
label: "Shape",
|
|
147
|
+
options: ["rect", "circle", "ellipse", "custom"],
|
|
148
|
+
default: this.shape,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "dieline.width",
|
|
152
|
+
type: "number",
|
|
153
|
+
label: "Width",
|
|
154
|
+
min: 10,
|
|
155
|
+
max: 2000,
|
|
156
|
+
default: this.width,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "dieline.height",
|
|
160
|
+
type: "number",
|
|
161
|
+
label: "Height",
|
|
162
|
+
min: 10,
|
|
163
|
+
max: 2000,
|
|
164
|
+
default: this.height,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "dieline.radius",
|
|
168
|
+
type: "number",
|
|
169
|
+
label: "Corner Radius",
|
|
170
|
+
min: 0,
|
|
171
|
+
max: 500,
|
|
172
|
+
default: this.radius,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "dieline.position",
|
|
176
|
+
type: "json",
|
|
177
|
+
label: "Position (Normalized)",
|
|
178
|
+
default: this.position,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "dieline.borderLength",
|
|
182
|
+
type: "number",
|
|
183
|
+
label: "Margin",
|
|
184
|
+
min: 0,
|
|
185
|
+
max: 500,
|
|
186
|
+
default: this.borderLength,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "dieline.offset",
|
|
190
|
+
type: "number",
|
|
191
|
+
label: "Bleed Offset",
|
|
192
|
+
min: -100,
|
|
193
|
+
max: 100,
|
|
194
|
+
default: this.offset,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "dieline.showBleedLines",
|
|
198
|
+
type: "boolean",
|
|
199
|
+
label: "Show Bleed Lines",
|
|
200
|
+
default: this.showBleedLines,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "dieline.style",
|
|
204
|
+
type: "select",
|
|
205
|
+
label: "Line Style",
|
|
206
|
+
options: ["solid", "dashed"],
|
|
207
|
+
default: this.style,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "dieline.insideColor",
|
|
211
|
+
type: "color",
|
|
212
|
+
label: "Inside Color",
|
|
213
|
+
default: this.insideColor,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "dieline.outsideColor",
|
|
217
|
+
type: "color",
|
|
218
|
+
label: "Outside Color",
|
|
219
|
+
default: this.outsideColor,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "dieline.holes",
|
|
223
|
+
type: "json",
|
|
224
|
+
label: "Holes",
|
|
225
|
+
default: this.holes,
|
|
226
|
+
},
|
|
227
|
+
] as ConfigurationContribution[],
|
|
228
|
+
[ContributionPointIds.COMMANDS]: [
|
|
229
|
+
{
|
|
230
|
+
command: "reset",
|
|
231
|
+
title: "Reset Dieline",
|
|
232
|
+
handler: () => {
|
|
233
|
+
this.shape = "rect";
|
|
234
|
+
this.width = 300;
|
|
235
|
+
this.height = 300;
|
|
236
|
+
this.radius = 0;
|
|
237
|
+
this.offset = 0;
|
|
238
|
+
this.style = "solid";
|
|
239
|
+
this.insideColor = "rgba(0,0,0,0)";
|
|
240
|
+
this.outsideColor = "#ffffff";
|
|
241
|
+
this.showBleedLines = true;
|
|
242
|
+
this.holes = [];
|
|
243
|
+
this.pathData = undefined;
|
|
244
|
+
this.updateDieline();
|
|
245
|
+
return true;
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
command: "setDimensions",
|
|
250
|
+
title: "Set Dimensions",
|
|
251
|
+
handler: (width: number, height: number) => {
|
|
252
|
+
if (this.width === width && this.height === height) return true;
|
|
253
|
+
this.width = width;
|
|
254
|
+
this.height = height;
|
|
255
|
+
this.updateDieline();
|
|
256
|
+
return true;
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
command: "setShape",
|
|
261
|
+
title: "Set Shape",
|
|
262
|
+
handler: (shape: "rect" | "circle" | "ellipse" | "custom") => {
|
|
263
|
+
if (this.shape === shape) return true;
|
|
264
|
+
this.shape = shape;
|
|
265
|
+
this.updateDieline();
|
|
266
|
+
return true;
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
command: "setBleed",
|
|
271
|
+
title: "Set Bleed",
|
|
272
|
+
handler: (bleed: number) => {
|
|
273
|
+
if (this.offset === bleed) return true;
|
|
274
|
+
this.offset = bleed;
|
|
275
|
+
this.updateDieline();
|
|
276
|
+
return true;
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
command: "setHoles",
|
|
281
|
+
title: "Set Holes",
|
|
282
|
+
handler: (holes: HoleData[]) => {
|
|
283
|
+
this.holes = holes;
|
|
284
|
+
this.updateDieline(false);
|
|
285
|
+
return true;
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
command: "getGeometry",
|
|
290
|
+
title: "Get Geometry",
|
|
291
|
+
handler: () => {
|
|
292
|
+
return this.getGeometry();
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
command: "exportCutImage",
|
|
297
|
+
title: "Export Cut Image",
|
|
298
|
+
handler: () => {
|
|
299
|
+
return this.exportCutImage();
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
command: "detectEdge",
|
|
304
|
+
title: "Detect Edge from Image",
|
|
305
|
+
handler: async (imageUrl: string, options?: any) => {
|
|
306
|
+
try {
|
|
307
|
+
// Pass current dimensions if we want to scale immediately?
|
|
308
|
+
// But wait, the user said "It should be scaled according to width and height".
|
|
309
|
+
// If the user already set width/height on the tool, we should respect it?
|
|
310
|
+
// Or should we set width/height based on the image aspect ratio?
|
|
311
|
+
// Usually for a new trace, we might want to respect the IMAGE aspect ratio but fit into current width/height?
|
|
312
|
+
// Or just replace width/height with image dimensions?
|
|
313
|
+
// Let's assume we want to keep the current "box" size but fit the shape inside?
|
|
314
|
+
// Or if options has width/height use that.
|
|
315
|
+
|
|
316
|
+
// Let's first trace to get the natural shape (and its aspect ratio)
|
|
317
|
+
// Then we can decide how to update this.width/this.height.
|
|
318
|
+
|
|
319
|
+
const pathData = await ImageTracer.trace(imageUrl, options);
|
|
320
|
+
|
|
321
|
+
// We need to set width/height from the path bounds to avoid distortion
|
|
322
|
+
const bounds = getPathBounds(pathData);
|
|
323
|
+
|
|
324
|
+
// If we want to scale the path to specific dimensions, we can do it via ImageTracer options.scaleToWidth/Height
|
|
325
|
+
// But here we got the raw path.
|
|
326
|
+
// Let's update the TOOL's dimensions to match the detected shape's aspect ratio,
|
|
327
|
+
// while keeping the size reasonable (e.g. max dimension 300 or current size).
|
|
328
|
+
|
|
329
|
+
// If current tool size is default 300x300, we might want to resize tool to match image ratio.
|
|
330
|
+
const currentMax = Math.max(this.width, this.height);
|
|
331
|
+
const scale = currentMax / Math.max(bounds.width, bounds.height);
|
|
332
|
+
|
|
333
|
+
this.width = bounds.width * scale;
|
|
334
|
+
this.height = bounds.height * scale;
|
|
335
|
+
|
|
336
|
+
this.shape = "custom";
|
|
337
|
+
this.pathData = pathData;
|
|
338
|
+
|
|
339
|
+
this.updateDieline();
|
|
340
|
+
return pathData;
|
|
341
|
+
} catch (e) {
|
|
342
|
+
console.error("Edge detection failed", e);
|
|
343
|
+
throw e;
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
] as CommandContribution[],
|
|
348
|
+
};
|
|
98
349
|
}
|
|
99
350
|
|
|
100
|
-
private getLayer(
|
|
101
|
-
return
|
|
102
|
-
.getObjects()
|
|
103
|
-
.find((obj: any) => obj.data?.id === id) as PooderLayer | undefined;
|
|
351
|
+
private getLayer() {
|
|
352
|
+
return this.canvasService?.getLayer("dieline-overlay");
|
|
104
353
|
}
|
|
105
354
|
|
|
106
|
-
private createLayer(
|
|
107
|
-
|
|
355
|
+
private createLayer() {
|
|
356
|
+
if (!this.canvasService) return;
|
|
357
|
+
const width = this.canvasService.canvas.width || 800;
|
|
358
|
+
const height = this.canvasService.canvas.height || 600;
|
|
108
359
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
360
|
+
const layer = this.canvasService.createLayer("dieline-overlay", {
|
|
361
|
+
width,
|
|
362
|
+
height,
|
|
363
|
+
selectable: false,
|
|
364
|
+
evented: false,
|
|
365
|
+
});
|
|
112
366
|
|
|
113
|
-
|
|
114
|
-
width,
|
|
115
|
-
height,
|
|
116
|
-
selectable: false,
|
|
117
|
-
evented: false,
|
|
118
|
-
data: { id: "dieline-overlay" },
|
|
119
|
-
} as any);
|
|
367
|
+
this.canvasService.canvas.bringObjectToFront(layer);
|
|
120
368
|
|
|
121
|
-
|
|
369
|
+
// Ensure above user layer
|
|
370
|
+
const userLayer = this.canvasService.getLayer("user");
|
|
371
|
+
if (userLayer) {
|
|
372
|
+
const userIndex = this.canvasService.canvas
|
|
373
|
+
.getObjects()
|
|
374
|
+
.indexOf(userLayer);
|
|
375
|
+
this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
|
|
122
376
|
}
|
|
123
|
-
|
|
124
|
-
editor.canvas.bringObjectToFront(layer);
|
|
125
377
|
}
|
|
126
378
|
|
|
127
|
-
private destroyLayer(
|
|
128
|
-
|
|
379
|
+
private destroyLayer() {
|
|
380
|
+
if (!this.canvasService) return;
|
|
381
|
+
const layer = this.getLayer();
|
|
129
382
|
if (layer) {
|
|
130
|
-
|
|
383
|
+
this.canvasService.canvas.remove(layer);
|
|
131
384
|
}
|
|
132
385
|
}
|
|
133
386
|
|
|
@@ -156,7 +409,11 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
156
409
|
return new Pattern({ source: canvas, repetition: "repeat" });
|
|
157
410
|
}
|
|
158
411
|
|
|
159
|
-
public updateDieline(
|
|
412
|
+
public updateDieline(emitEvent: boolean = true) {
|
|
413
|
+
if (!this.canvasService) return;
|
|
414
|
+
const layer = this.getLayer();
|
|
415
|
+
if (!layer) return;
|
|
416
|
+
|
|
160
417
|
const {
|
|
161
418
|
shape,
|
|
162
419
|
radius,
|
|
@@ -167,11 +424,12 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
167
424
|
position,
|
|
168
425
|
borderLength,
|
|
169
426
|
showBleedLines,
|
|
170
|
-
|
|
171
|
-
|
|
427
|
+
holes,
|
|
428
|
+
} = this;
|
|
429
|
+
let { width, height } = this;
|
|
172
430
|
|
|
173
|
-
const canvasW =
|
|
174
|
-
const canvasH =
|
|
431
|
+
const canvasW = this.canvasService.canvas.width || 800;
|
|
432
|
+
const canvasH = this.canvasService.canvas.height || 600;
|
|
175
433
|
|
|
176
434
|
// Handle borderLength (Margin)
|
|
177
435
|
if (borderLength && borderLength > 0) {
|
|
@@ -180,32 +438,26 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
180
438
|
}
|
|
181
439
|
|
|
182
440
|
// Handle Position
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
if (!layer) return;
|
|
441
|
+
// this.position is normalized (0-1). Default to center (0.5, 0.5).
|
|
442
|
+
const normalizedPos = position ?? { x: 0.5, y: 0.5 };
|
|
443
|
+
const cx = Coordinate.toAbsolute(normalizedPos.x, canvasW);
|
|
444
|
+
const cy = Coordinate.toAbsolute(normalizedPos.y, canvasH);
|
|
188
445
|
|
|
189
446
|
// Clear existing objects
|
|
190
447
|
layer.remove(...layer.getObjects());
|
|
191
448
|
|
|
192
|
-
//
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
x: h.x,
|
|
205
|
-
y: h.y,
|
|
206
|
-
innerRadius,
|
|
207
|
-
outerRadius,
|
|
208
|
-
}));
|
|
449
|
+
// Denormalize Holes for Geometry Generation
|
|
450
|
+
const absoluteHoles = (holes || []).map((h) => {
|
|
451
|
+
const p = Coordinate.denormalizePoint(
|
|
452
|
+
{ x: h.x, y: h.y },
|
|
453
|
+
{ width: canvasW, height: canvasH },
|
|
454
|
+
);
|
|
455
|
+
return {
|
|
456
|
+
...h,
|
|
457
|
+
x: p.x,
|
|
458
|
+
y: p.y,
|
|
459
|
+
};
|
|
460
|
+
});
|
|
209
461
|
|
|
210
462
|
// 1. Draw Mask (Outside)
|
|
211
463
|
const cutW = Math.max(0, width + offset * 2);
|
|
@@ -222,7 +474,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
222
474
|
radius: cutR,
|
|
223
475
|
x: cx,
|
|
224
476
|
y: cy,
|
|
225
|
-
holes:
|
|
477
|
+
holes: absoluteHoles,
|
|
478
|
+
pathData: this.pathData,
|
|
226
479
|
});
|
|
227
480
|
|
|
228
481
|
const mask = new Path(maskPathData, {
|
|
@@ -237,12 +490,7 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
237
490
|
});
|
|
238
491
|
layer.add(mask);
|
|
239
492
|
|
|
240
|
-
// 2. Draw Inside Fill (Dieline Shape itself, merged with holes if needed
|
|
241
|
-
// The user wants "fusion effect" so holes should be part of the dieline visually.
|
|
242
|
-
// If insideColor is transparent, it doesn't matter much.
|
|
243
|
-
// If insideColor is opaque, we need to punch holes in it too.
|
|
244
|
-
// Let's use Paper.js for this too if insideColor is not transparent.
|
|
245
|
-
|
|
493
|
+
// 2. Draw Inside Fill (Dieline Shape itself, merged with holes if needed)
|
|
246
494
|
if (
|
|
247
495
|
insideColor &&
|
|
248
496
|
insideColor !== "transparent" &&
|
|
@@ -256,7 +504,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
256
504
|
radius: cutR,
|
|
257
505
|
x: cx,
|
|
258
506
|
y: cy,
|
|
259
|
-
holes:
|
|
507
|
+
holes: absoluteHoles,
|
|
508
|
+
pathData: this.pathData,
|
|
260
509
|
});
|
|
261
510
|
|
|
262
511
|
const insideObj = new Path(productPathData, {
|
|
@@ -280,7 +529,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
280
529
|
radius,
|
|
281
530
|
x: cx,
|
|
282
531
|
y: cy,
|
|
283
|
-
holes:
|
|
532
|
+
holes: absoluteHoles,
|
|
533
|
+
pathData: this.pathData,
|
|
284
534
|
},
|
|
285
535
|
offset,
|
|
286
536
|
);
|
|
@@ -310,7 +560,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
310
560
|
radius: cutR,
|
|
311
561
|
x: cx,
|
|
312
562
|
y: cy,
|
|
313
|
-
holes:
|
|
563
|
+
holes: absoluteHoles,
|
|
564
|
+
pathData: this.pathData,
|
|
314
565
|
});
|
|
315
566
|
|
|
316
567
|
const offsetBorderObj = new Path(offsetPathData, {
|
|
@@ -328,8 +579,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
328
579
|
|
|
329
580
|
// 4. Draw Dieline (Visual Border)
|
|
330
581
|
// This should outline the product shape AND the holes.
|
|
331
|
-
//
|
|
332
|
-
|
|
582
|
+
// NOTE: We need to use absoluteHoles (denormalized) here, NOT holes (normalized 0-1)
|
|
583
|
+
// generateDielinePath expects holes to be in absolute coordinates (matching width/height scale)
|
|
333
584
|
const borderPathData = generateDielinePath({
|
|
334
585
|
shape,
|
|
335
586
|
width: width,
|
|
@@ -337,7 +588,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
337
588
|
radius: radius,
|
|
338
589
|
x: cx,
|
|
339
590
|
y: cy,
|
|
340
|
-
holes:
|
|
591
|
+
holes: absoluteHoles, // FIX: Use absoluteHoles instead of holes
|
|
592
|
+
pathData: this.pathData,
|
|
341
593
|
});
|
|
342
594
|
|
|
343
595
|
const borderObj = new Path(borderPathData, {
|
|
@@ -353,209 +605,46 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
353
605
|
|
|
354
606
|
layer.add(borderObj);
|
|
355
607
|
|
|
356
|
-
|
|
357
|
-
|
|
608
|
+
// Enforce z-index: Dieline > User
|
|
609
|
+
const userLayer = this.canvasService.getLayer("user");
|
|
610
|
+
if (layer && userLayer) {
|
|
611
|
+
const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
|
|
612
|
+
const userIndex = this.canvasService.canvas
|
|
613
|
+
.getObjects()
|
|
614
|
+
.indexOf(userLayer);
|
|
615
|
+
if (layerIndex < userIndex) {
|
|
616
|
+
this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
// If no user layer, just bring to front (safe default)
|
|
620
|
+
this.canvasService.canvas.bringObjectToFront(layer);
|
|
621
|
+
}
|
|
358
622
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
width: 300,
|
|
365
|
-
height: 300,
|
|
366
|
-
radius: 0,
|
|
367
|
-
offset: 0,
|
|
368
|
-
style: "solid",
|
|
369
|
-
insideColor: "rgba(0,0,0,0)",
|
|
370
|
-
outsideColor: "#ffffff",
|
|
371
|
-
showBleedLines: true,
|
|
372
|
-
};
|
|
373
|
-
this.updateDieline(editor);
|
|
374
|
-
return true;
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
destroy: {
|
|
378
|
-
execute: (editor: Editor) => {
|
|
379
|
-
this.destroyLayer(editor);
|
|
380
|
-
return true;
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
setDimensions: {
|
|
384
|
-
execute: (editor: Editor, width: number, height: number) => {
|
|
385
|
-
if (this.options.width === width && this.options.height === height)
|
|
386
|
-
return true;
|
|
387
|
-
this.options.width = width;
|
|
388
|
-
this.options.height = height;
|
|
389
|
-
this.updateDieline(editor);
|
|
390
|
-
return true;
|
|
391
|
-
},
|
|
392
|
-
schema: {
|
|
393
|
-
width: {
|
|
394
|
-
type: "number",
|
|
395
|
-
label: "Width",
|
|
396
|
-
min: 10,
|
|
397
|
-
max: 2000,
|
|
398
|
-
required: true,
|
|
399
|
-
},
|
|
400
|
-
height: {
|
|
401
|
-
type: "number",
|
|
402
|
-
label: "Height",
|
|
403
|
-
min: 10,
|
|
404
|
-
max: 2000,
|
|
405
|
-
required: true,
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
},
|
|
409
|
-
setShape: {
|
|
410
|
-
execute: (editor: Editor, shape: "rect" | "circle" | "ellipse") => {
|
|
411
|
-
if (this.options.shape === shape) return true;
|
|
412
|
-
this.options.shape = shape;
|
|
413
|
-
this.updateDieline(editor);
|
|
414
|
-
return true;
|
|
415
|
-
},
|
|
416
|
-
schema: {
|
|
417
|
-
shape: {
|
|
418
|
-
type: "string",
|
|
419
|
-
label: "Shape",
|
|
420
|
-
options: ["rect", "circle", "ellipse"],
|
|
421
|
-
required: true,
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
setBleed: {
|
|
426
|
-
execute: (editor: Editor, bleed: number) => {
|
|
427
|
-
if (this.options.offset === bleed) return true;
|
|
428
|
-
this.options.offset = bleed;
|
|
429
|
-
this.updateDieline(editor);
|
|
430
|
-
return true;
|
|
431
|
-
},
|
|
432
|
-
schema: {
|
|
433
|
-
bleed: {
|
|
434
|
-
type: "number",
|
|
435
|
-
label: "Bleed",
|
|
436
|
-
min: -100,
|
|
437
|
-
max: 100,
|
|
438
|
-
required: true,
|
|
439
|
-
},
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
exportCutImage: {
|
|
443
|
-
execute: (editor: Editor) => {
|
|
444
|
-
// 1. Generate Path Data
|
|
445
|
-
const { shape, width, height, radius, position } = this.options;
|
|
446
|
-
const canvasW = editor.canvas.width || 800;
|
|
447
|
-
const canvasH = editor.canvas.height || 600;
|
|
448
|
-
const cx = position?.x ?? canvasW / 2;
|
|
449
|
-
const cy = position?.y ?? canvasH / 2;
|
|
450
|
-
|
|
451
|
-
const holeTool = editor.getExtension("HoleTool") as any;
|
|
452
|
-
const holes = holeTool ? holeTool.options.holes || [] : [];
|
|
453
|
-
const innerRadius = holeTool ? holeTool.options.innerRadius || 15 : 15;
|
|
454
|
-
const outerRadius = holeTool ? holeTool.options.outerRadius || 25 : 25;
|
|
455
|
-
const holeData = holes.map((h: any) => ({
|
|
456
|
-
x: h.x,
|
|
457
|
-
y: h.y,
|
|
458
|
-
innerRadius,
|
|
459
|
-
outerRadius,
|
|
460
|
-
}));
|
|
461
|
-
|
|
462
|
-
const pathData = generateDielinePath({
|
|
463
|
-
shape,
|
|
464
|
-
width,
|
|
465
|
-
height,
|
|
466
|
-
radius,
|
|
467
|
-
x: cx,
|
|
468
|
-
y: cy,
|
|
469
|
-
holes: holeData,
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// 2. Create Clip Path
|
|
473
|
-
// @ts-ignore
|
|
474
|
-
const clipPath = new Path(pathData, {
|
|
475
|
-
left: 0,
|
|
476
|
-
top: 0,
|
|
477
|
-
originX: "left",
|
|
478
|
-
originY: "top",
|
|
479
|
-
absolutePositioned: true,
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// 3. Hide UI Layers
|
|
483
|
-
const layer = this.getLayer(editor, "dieline-overlay");
|
|
484
|
-
const wasVisible = layer?.visible ?? true;
|
|
485
|
-
if (layer) layer.visible = false;
|
|
486
|
-
|
|
487
|
-
// Hide hole markers
|
|
488
|
-
const holeMarkers = editor.canvas
|
|
489
|
-
.getObjects()
|
|
490
|
-
.filter((o: any) => o.data?.type === "hole-marker");
|
|
491
|
-
holeMarkers.forEach((o) => (o.visible = false));
|
|
492
|
-
|
|
493
|
-
// Hide Ruler Overlay
|
|
494
|
-
const rulerLayer = editor.canvas
|
|
495
|
-
.getObjects()
|
|
496
|
-
.find((obj: any) => obj.data?.id === "ruler-overlay");
|
|
497
|
-
const rulerWasVisible = rulerLayer?.visible ?? true;
|
|
498
|
-
if (rulerLayer) rulerLayer.visible = false;
|
|
499
|
-
|
|
500
|
-
// 4. Apply Clip & Export
|
|
501
|
-
const originalClip = editor.canvas.clipPath;
|
|
502
|
-
editor.canvas.clipPath = clipPath;
|
|
503
|
-
|
|
504
|
-
const bbox = clipPath.getBoundingRect();
|
|
505
|
-
// Adjust hole coordinates to be relative to the bounding box
|
|
506
|
-
const holeDataRelative = holes.map((h: any) => ({
|
|
507
|
-
x: h.x - bbox.left,
|
|
508
|
-
y: h.y - bbox.top,
|
|
509
|
-
innerRadius,
|
|
510
|
-
outerRadius,
|
|
511
|
-
}));
|
|
512
|
-
|
|
513
|
-
const clipPathCorrected = new Path(pathData, {
|
|
514
|
-
absolutePositioned: true,
|
|
515
|
-
left: 0,
|
|
516
|
-
top: 0,
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
const tempPath = new Path(pathData);
|
|
520
|
-
const tempBounds = tempPath.getBoundingRect();
|
|
521
|
-
|
|
522
|
-
clipPathCorrected.set({
|
|
523
|
-
left: tempBounds.left,
|
|
524
|
-
top: tempBounds.top,
|
|
525
|
-
originX: "left",
|
|
526
|
-
originY: "top",
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// 4. Apply Clip & Export
|
|
530
|
-
editor.canvas.clipPath = clipPathCorrected;
|
|
531
|
-
|
|
532
|
-
const exportBbox = clipPathCorrected.getBoundingRect();
|
|
533
|
-
const dataURL = editor.canvas.toDataURL({
|
|
534
|
-
format: "png",
|
|
535
|
-
multiplier: 2,
|
|
536
|
-
left: exportBbox.left,
|
|
537
|
-
top: exportBbox.top,
|
|
538
|
-
width: exportBbox.width,
|
|
539
|
-
height: exportBbox.height,
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
// 5. Restore
|
|
543
|
-
editor.canvas.clipPath = originalClip;
|
|
544
|
-
if (layer) layer.visible = wasVisible;
|
|
545
|
-
if (rulerLayer) rulerLayer.visible = rulerWasVisible;
|
|
546
|
-
holeMarkers.forEach((o) => (o.visible = true));
|
|
547
|
-
editor.canvas.requestRenderAll();
|
|
548
|
-
|
|
549
|
-
return dataURL;
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
};
|
|
623
|
+
// Ensure Ruler is above Dieline if it exists
|
|
624
|
+
const rulerLayer = this.canvasService.getLayer("ruler-overlay");
|
|
625
|
+
if (rulerLayer) {
|
|
626
|
+
this.canvasService.canvas.bringObjectToFront(rulerLayer);
|
|
627
|
+
}
|
|
553
628
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
629
|
+
layer.dirty = true;
|
|
630
|
+
this.canvasService.requestRenderAll();
|
|
631
|
+
|
|
632
|
+
// Emit change event so other tools (like HoleTool) can react
|
|
633
|
+
// Only emit if requested (to avoid loops when updating non-geometry props like holes)
|
|
634
|
+
if (emitEvent && this.context) {
|
|
635
|
+
const geometry = this.getGeometry();
|
|
636
|
+
if (geometry) {
|
|
637
|
+
this.context.eventBus.emit("dieline:geometry:change", geometry);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
public getGeometry(): DielineGeometry | null {
|
|
643
|
+
if (!this.canvasService) return null;
|
|
644
|
+
const { shape, width, height, radius, position, borderLength, offset } =
|
|
645
|
+
this;
|
|
646
|
+
const canvasW = this.canvasService.canvas.width || 800;
|
|
647
|
+
const canvasH = this.canvasService.canvas.height || 600;
|
|
559
648
|
|
|
560
649
|
let visualWidth = width;
|
|
561
650
|
let visualHeight = height;
|
|
@@ -565,8 +654,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
565
654
|
visualHeight = Math.max(0, canvasH - borderLength * 2);
|
|
566
655
|
}
|
|
567
656
|
|
|
568
|
-
const cx = position?.x ?? canvasW
|
|
569
|
-
const cy = position?.y ?? canvasH
|
|
657
|
+
const cx = Coordinate.toAbsolute(position?.x ?? 0.5, canvasW);
|
|
658
|
+
const cy = Coordinate.toAbsolute(position?.y ?? 0.5, canvasH);
|
|
570
659
|
|
|
571
660
|
return {
|
|
572
661
|
shape,
|
|
@@ -575,6 +664,117 @@ export class DielineTool implements Extension<DielineToolOptions> {
|
|
|
575
664
|
width: visualWidth,
|
|
576
665
|
height: visualHeight,
|
|
577
666
|
radius,
|
|
667
|
+
offset,
|
|
668
|
+
borderLength,
|
|
669
|
+
pathData: this.pathData,
|
|
578
670
|
};
|
|
579
671
|
}
|
|
672
|
+
|
|
673
|
+
public exportCutImage() {
|
|
674
|
+
if (!this.canvasService) return null;
|
|
675
|
+
const canvas = this.canvasService.canvas;
|
|
676
|
+
|
|
677
|
+
// 1. Generate Path Data
|
|
678
|
+
const { shape, width, height, radius, position, holes } = this;
|
|
679
|
+
const canvasW = canvas.width || 800;
|
|
680
|
+
const canvasH = canvas.height || 600;
|
|
681
|
+
const cx = Coordinate.toAbsolute(position?.x ?? 0.5, canvasW);
|
|
682
|
+
const cy = Coordinate.toAbsolute(position?.y ?? 0.5, canvasH);
|
|
683
|
+
|
|
684
|
+
// Denormalize Holes for Export
|
|
685
|
+
const absoluteHoles = (holes || []).map((h) => {
|
|
686
|
+
const p = Coordinate.denormalizePoint(
|
|
687
|
+
{ x: h.x, y: h.y },
|
|
688
|
+
{ width: canvasW, height: canvasH },
|
|
689
|
+
);
|
|
690
|
+
return {
|
|
691
|
+
...h,
|
|
692
|
+
x: p.x,
|
|
693
|
+
y: p.y,
|
|
694
|
+
};
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const pathData = generateDielinePath({
|
|
698
|
+
shape,
|
|
699
|
+
width,
|
|
700
|
+
height,
|
|
701
|
+
radius,
|
|
702
|
+
x: cx,
|
|
703
|
+
y: cy,
|
|
704
|
+
holes: absoluteHoles,
|
|
705
|
+
pathData: this.pathData,
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// 2. Create Clip Path
|
|
709
|
+
// @ts-ignore
|
|
710
|
+
const clipPath = new Path(pathData, {
|
|
711
|
+
left: 0,
|
|
712
|
+
top: 0,
|
|
713
|
+
originX: "left",
|
|
714
|
+
originY: "top",
|
|
715
|
+
absolutePositioned: true,
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// 3. Hide UI Layers
|
|
719
|
+
const layer = this.getLayer();
|
|
720
|
+
const wasVisible = layer?.visible ?? true;
|
|
721
|
+
if (layer) layer.visible = false;
|
|
722
|
+
|
|
723
|
+
// Hide hole markers
|
|
724
|
+
const holeMarkers = canvas
|
|
725
|
+
.getObjects()
|
|
726
|
+
.filter((o: any) => o.data?.type === "hole-marker");
|
|
727
|
+
holeMarkers.forEach((o) => (o.visible = false));
|
|
728
|
+
|
|
729
|
+
// Hide Ruler Overlay
|
|
730
|
+
const rulerLayer = canvas
|
|
731
|
+
.getObjects()
|
|
732
|
+
.find((obj: any) => obj.data?.id === "ruler-overlay");
|
|
733
|
+
const rulerWasVisible = rulerLayer?.visible ?? true;
|
|
734
|
+
if (rulerLayer) rulerLayer.visible = false;
|
|
735
|
+
|
|
736
|
+
// 4. Apply Clip & Export
|
|
737
|
+
const originalClip = canvas.clipPath;
|
|
738
|
+
canvas.clipPath = clipPath;
|
|
739
|
+
|
|
740
|
+
const bbox = clipPath.getBoundingRect();
|
|
741
|
+
|
|
742
|
+
const clipPathCorrected = new Path(pathData, {
|
|
743
|
+
absolutePositioned: true,
|
|
744
|
+
left: 0,
|
|
745
|
+
top: 0,
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const tempPath = new Path(pathData);
|
|
749
|
+
const tempBounds = tempPath.getBoundingRect();
|
|
750
|
+
|
|
751
|
+
clipPathCorrected.set({
|
|
752
|
+
left: tempBounds.left,
|
|
753
|
+
top: tempBounds.top,
|
|
754
|
+
originX: "left",
|
|
755
|
+
originY: "top",
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// 4. Apply Clip & Export
|
|
759
|
+
canvas.clipPath = clipPathCorrected;
|
|
760
|
+
|
|
761
|
+
const exportBbox = clipPathCorrected.getBoundingRect();
|
|
762
|
+
const dataURL = canvas.toDataURL({
|
|
763
|
+
format: "png",
|
|
764
|
+
multiplier: 2,
|
|
765
|
+
left: exportBbox.left,
|
|
766
|
+
top: exportBbox.top,
|
|
767
|
+
width: exportBbox.width,
|
|
768
|
+
height: exportBbox.height,
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// 5. Restore
|
|
772
|
+
canvas.clipPath = originalClip;
|
|
773
|
+
if (layer) layer.visible = wasVisible;
|
|
774
|
+
if (rulerLayer) rulerLayer.visible = rulerWasVisible;
|
|
775
|
+
holeMarkers.forEach((o) => (o.visible = true));
|
|
776
|
+
canvas.requestRenderAll();
|
|
777
|
+
|
|
778
|
+
return dataURL;
|
|
779
|
+
}
|
|
580
780
|
}
|