@pooder/kit 1.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 +22 -0
- package/dist/index.d.mts +250 -115
- package/dist/index.d.ts +250 -115
- package/dist/index.js +2177 -831
- package/dist/index.mjs +2182 -826
- package/package.json +3 -2
- package/src/CanvasService.ts +65 -0
- package/src/background.ts +230 -172
- package/src/coordinate.ts +49 -0
- package/src/dieline.ts +780 -421
- package/src/film.ts +194 -156
- package/src/geometry.ts +464 -244
- package/src/hole.ts +629 -413
- package/src/image.ts +504 -147
- package/src/index.ts +9 -7
- package/src/mirror.ts +128 -0
- package/src/ruler.ts +325 -239
- package/src/tracer.ts +372 -0
- package/src/white-ink.ts +373 -301
- package/tsconfig.json +13 -13
package/src/dieline.ts
CHANGED
|
@@ -1,421 +1,780 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export interface DielineGeometry {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
1
|
+
import {
|
|
2
|
+
Extension,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
5
|
+
CommandContribution,
|
|
6
|
+
ConfigurationContribution,
|
|
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";
|
|
12
|
+
import {
|
|
13
|
+
generateDielinePath,
|
|
14
|
+
generateMaskPath,
|
|
15
|
+
generateBleedZonePath,
|
|
16
|
+
getPathBounds,
|
|
17
|
+
HoleData,
|
|
18
|
+
} from "./geometry";
|
|
19
|
+
|
|
20
|
+
export interface DielineGeometry {
|
|
21
|
+
shape: "rect" | "circle" | "ellipse" | "custom";
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
radius: number;
|
|
27
|
+
offset: number;
|
|
28
|
+
borderLength?: number;
|
|
29
|
+
pathData?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class DielineTool implements Extension {
|
|
33
|
+
id = "pooder.kit.dieline";
|
|
34
|
+
public metadata = {
|
|
35
|
+
name: "DielineTool",
|
|
36
|
+
};
|
|
37
|
+
|
|
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
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
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();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
deactivate(context: ExtensionContext) {
|
|
135
|
+
this.destroyLayer();
|
|
136
|
+
this.canvasService = undefined;
|
|
137
|
+
this.context = undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
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
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private getLayer() {
|
|
352
|
+
return this.canvasService?.getLayer("dieline-overlay");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private createLayer() {
|
|
356
|
+
if (!this.canvasService) return;
|
|
357
|
+
const width = this.canvasService.canvas.width || 800;
|
|
358
|
+
const height = this.canvasService.canvas.height || 600;
|
|
359
|
+
|
|
360
|
+
const layer = this.canvasService.createLayer("dieline-overlay", {
|
|
361
|
+
width,
|
|
362
|
+
height,
|
|
363
|
+
selectable: false,
|
|
364
|
+
evented: false,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
this.canvasService.canvas.bringObjectToFront(layer);
|
|
368
|
+
|
|
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);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private destroyLayer() {
|
|
380
|
+
if (!this.canvasService) return;
|
|
381
|
+
const layer = this.getLayer();
|
|
382
|
+
if (layer) {
|
|
383
|
+
this.canvasService.canvas.remove(layer);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private createHatchPattern(color: string = "rgba(0, 0, 0, 0.3)") {
|
|
388
|
+
if (typeof document === "undefined") {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
const size = 20;
|
|
392
|
+
const canvas = document.createElement("canvas");
|
|
393
|
+
canvas.width = size;
|
|
394
|
+
canvas.height = size;
|
|
395
|
+
const ctx = canvas.getContext("2d");
|
|
396
|
+
if (ctx) {
|
|
397
|
+
// Transparent background
|
|
398
|
+
ctx.clearRect(0, 0, size, size);
|
|
399
|
+
|
|
400
|
+
// Draw diagonal /
|
|
401
|
+
ctx.strokeStyle = color;
|
|
402
|
+
ctx.lineWidth = 1;
|
|
403
|
+
ctx.beginPath();
|
|
404
|
+
ctx.moveTo(0, size);
|
|
405
|
+
ctx.lineTo(size, 0);
|
|
406
|
+
ctx.stroke();
|
|
407
|
+
}
|
|
408
|
+
// @ts-ignore
|
|
409
|
+
return new Pattern({ source: canvas, repetition: "repeat" });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public updateDieline(emitEvent: boolean = true) {
|
|
413
|
+
if (!this.canvasService) return;
|
|
414
|
+
const layer = this.getLayer();
|
|
415
|
+
if (!layer) return;
|
|
416
|
+
|
|
417
|
+
const {
|
|
418
|
+
shape,
|
|
419
|
+
radius,
|
|
420
|
+
offset,
|
|
421
|
+
style,
|
|
422
|
+
insideColor,
|
|
423
|
+
outsideColor,
|
|
424
|
+
position,
|
|
425
|
+
borderLength,
|
|
426
|
+
showBleedLines,
|
|
427
|
+
holes,
|
|
428
|
+
} = this;
|
|
429
|
+
let { width, height } = this;
|
|
430
|
+
|
|
431
|
+
const canvasW = this.canvasService.canvas.width || 800;
|
|
432
|
+
const canvasH = this.canvasService.canvas.height || 600;
|
|
433
|
+
|
|
434
|
+
// Handle borderLength (Margin)
|
|
435
|
+
if (borderLength && borderLength > 0) {
|
|
436
|
+
width = Math.max(0, canvasW - borderLength * 2);
|
|
437
|
+
height = Math.max(0, canvasH - borderLength * 2);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Handle Position
|
|
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);
|
|
445
|
+
|
|
446
|
+
// Clear existing objects
|
|
447
|
+
layer.remove(...layer.getObjects());
|
|
448
|
+
|
|
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
|
+
});
|
|
461
|
+
|
|
462
|
+
// 1. Draw Mask (Outside)
|
|
463
|
+
const cutW = Math.max(0, width + offset * 2);
|
|
464
|
+
const cutH = Math.max(0, height + offset * 2);
|
|
465
|
+
const cutR = radius === 0 ? 0 : Math.max(0, radius + offset);
|
|
466
|
+
|
|
467
|
+
// Use Paper.js to generate the complex mask path
|
|
468
|
+
const maskPathData = generateMaskPath({
|
|
469
|
+
canvasWidth: canvasW,
|
|
470
|
+
canvasHeight: canvasH,
|
|
471
|
+
shape,
|
|
472
|
+
width: cutW,
|
|
473
|
+
height: cutH,
|
|
474
|
+
radius: cutR,
|
|
475
|
+
x: cx,
|
|
476
|
+
y: cy,
|
|
477
|
+
holes: absoluteHoles,
|
|
478
|
+
pathData: this.pathData,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const mask = new Path(maskPathData, {
|
|
482
|
+
fill: outsideColor,
|
|
483
|
+
stroke: null,
|
|
484
|
+
selectable: false,
|
|
485
|
+
evented: false,
|
|
486
|
+
originX: "left" as const,
|
|
487
|
+
originY: "top" as const,
|
|
488
|
+
left: 0,
|
|
489
|
+
top: 0,
|
|
490
|
+
});
|
|
491
|
+
layer.add(mask);
|
|
492
|
+
|
|
493
|
+
// 2. Draw Inside Fill (Dieline Shape itself, merged with holes if needed)
|
|
494
|
+
if (
|
|
495
|
+
insideColor &&
|
|
496
|
+
insideColor !== "transparent" &&
|
|
497
|
+
insideColor !== "rgba(0,0,0,0)"
|
|
498
|
+
) {
|
|
499
|
+
// Generate path for the product shape (Paper) = Dieline - Holes
|
|
500
|
+
const productPathData = generateDielinePath({
|
|
501
|
+
shape,
|
|
502
|
+
width: cutW,
|
|
503
|
+
height: cutH,
|
|
504
|
+
radius: cutR,
|
|
505
|
+
x: cx,
|
|
506
|
+
y: cy,
|
|
507
|
+
holes: absoluteHoles,
|
|
508
|
+
pathData: this.pathData,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const insideObj = new Path(productPathData, {
|
|
512
|
+
fill: insideColor,
|
|
513
|
+
stroke: null,
|
|
514
|
+
selectable: false,
|
|
515
|
+
evented: false,
|
|
516
|
+
originX: "left", // paper.js paths are absolute
|
|
517
|
+
originY: "top",
|
|
518
|
+
});
|
|
519
|
+
layer.add(insideObj);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 3. Draw Bleed Zone (Hatch Fill) and Offset Border
|
|
523
|
+
if (offset !== 0) {
|
|
524
|
+
const bleedPathData = generateBleedZonePath(
|
|
525
|
+
{
|
|
526
|
+
shape,
|
|
527
|
+
width,
|
|
528
|
+
height,
|
|
529
|
+
radius,
|
|
530
|
+
x: cx,
|
|
531
|
+
y: cy,
|
|
532
|
+
holes: absoluteHoles,
|
|
533
|
+
pathData: this.pathData,
|
|
534
|
+
},
|
|
535
|
+
offset,
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
// Use solid red for hatch lines to match dieline, background is transparent
|
|
539
|
+
if (showBleedLines !== false) {
|
|
540
|
+
const pattern = this.createHatchPattern("red");
|
|
541
|
+
if (pattern) {
|
|
542
|
+
const bleedObj = new Path(bleedPathData, {
|
|
543
|
+
fill: pattern,
|
|
544
|
+
stroke: null,
|
|
545
|
+
selectable: false,
|
|
546
|
+
evented: false,
|
|
547
|
+
objectCaching: false,
|
|
548
|
+
originX: "left",
|
|
549
|
+
originY: "top",
|
|
550
|
+
});
|
|
551
|
+
layer.add(bleedObj);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Offset Dieline Border
|
|
556
|
+
const offsetPathData = generateDielinePath({
|
|
557
|
+
shape,
|
|
558
|
+
width: cutW,
|
|
559
|
+
height: cutH,
|
|
560
|
+
radius: cutR,
|
|
561
|
+
x: cx,
|
|
562
|
+
y: cy,
|
|
563
|
+
holes: absoluteHoles,
|
|
564
|
+
pathData: this.pathData,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const offsetBorderObj = new Path(offsetPathData, {
|
|
568
|
+
fill: null,
|
|
569
|
+
stroke: "#666", // Grey
|
|
570
|
+
strokeWidth: 1,
|
|
571
|
+
strokeDashArray: [4, 4], // Dashed
|
|
572
|
+
selectable: false,
|
|
573
|
+
evented: false,
|
|
574
|
+
originX: "left",
|
|
575
|
+
originY: "top",
|
|
576
|
+
});
|
|
577
|
+
layer.add(offsetBorderObj);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 4. Draw Dieline (Visual Border)
|
|
581
|
+
// This should outline the product shape AND the holes.
|
|
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)
|
|
584
|
+
const borderPathData = generateDielinePath({
|
|
585
|
+
shape,
|
|
586
|
+
width: width,
|
|
587
|
+
height: height,
|
|
588
|
+
radius: radius,
|
|
589
|
+
x: cx,
|
|
590
|
+
y: cy,
|
|
591
|
+
holes: absoluteHoles, // FIX: Use absoluteHoles instead of holes
|
|
592
|
+
pathData: this.pathData,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const borderObj = new Path(borderPathData, {
|
|
596
|
+
fill: "transparent",
|
|
597
|
+
stroke: "red",
|
|
598
|
+
strokeWidth: 1,
|
|
599
|
+
strokeDashArray: style === "dashed" ? [5, 5] : undefined,
|
|
600
|
+
selectable: false,
|
|
601
|
+
evented: false,
|
|
602
|
+
originX: "left",
|
|
603
|
+
originY: "top",
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
layer.add(borderObj);
|
|
607
|
+
|
|
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
|
+
}
|
|
622
|
+
|
|
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
|
+
}
|
|
628
|
+
|
|
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;
|
|
648
|
+
|
|
649
|
+
let visualWidth = width;
|
|
650
|
+
let visualHeight = height;
|
|
651
|
+
|
|
652
|
+
if (borderLength && borderLength > 0) {
|
|
653
|
+
visualWidth = Math.max(0, canvasW - borderLength * 2);
|
|
654
|
+
visualHeight = Math.max(0, canvasH - borderLength * 2);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const cx = Coordinate.toAbsolute(position?.x ?? 0.5, canvasW);
|
|
658
|
+
const cy = Coordinate.toAbsolute(position?.y ?? 0.5, canvasH);
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
shape,
|
|
662
|
+
x: cx,
|
|
663
|
+
y: cy,
|
|
664
|
+
width: visualWidth,
|
|
665
|
+
height: visualHeight,
|
|
666
|
+
radius,
|
|
667
|
+
offset,
|
|
668
|
+
borderLength,
|
|
669
|
+
pathData: this.pathData,
|
|
670
|
+
};
|
|
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
|
+
}
|
|
780
|
+
}
|