@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/hole.ts
CHANGED
|
@@ -1,413 +1,629 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
if (
|
|
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
|
-
const
|
|
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
|
-
const
|
|
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
|
-
|
|
1
|
+
import {
|
|
2
|
+
Extension,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
5
|
+
CommandContribution,
|
|
6
|
+
ConfigurationContribution,
|
|
7
|
+
ConfigurationService,
|
|
8
|
+
} from "@pooder/core";
|
|
9
|
+
import { Circle, Group, Point } from "fabric";
|
|
10
|
+
import CanvasService from "./CanvasService";
|
|
11
|
+
import { DielineGeometry } from "./dieline";
|
|
12
|
+
import { getNearestPointOnDieline, HoleData } from "./geometry";
|
|
13
|
+
import { Coordinate } from "./coordinate";
|
|
14
|
+
|
|
15
|
+
export class HoleTool implements Extension {
|
|
16
|
+
id = "pooder.kit.hole";
|
|
17
|
+
|
|
18
|
+
public metadata = {
|
|
19
|
+
name: "HoleTool",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
private innerRadius: number = 15;
|
|
23
|
+
private outerRadius: number = 25;
|
|
24
|
+
private style: "solid" | "dashed" = "solid";
|
|
25
|
+
private holes: Array<{ x: number; y: number }> = [];
|
|
26
|
+
private constraintTarget: "original" | "bleed" = "bleed";
|
|
27
|
+
|
|
28
|
+
private canvasService?: CanvasService;
|
|
29
|
+
private context?: ExtensionContext;
|
|
30
|
+
private isUpdatingConfig = false;
|
|
31
|
+
|
|
32
|
+
private handleMoving: ((e: any) => void) | null = null;
|
|
33
|
+
private handleModified: ((e: any) => void) | null = null;
|
|
34
|
+
private handleDielineChange: ((geometry: DielineGeometry) => void) | null =
|
|
35
|
+
null;
|
|
36
|
+
|
|
37
|
+
// Cache geometry to enforce constraints during drag
|
|
38
|
+
private currentGeometry: DielineGeometry | null = null;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
options?: Partial<{
|
|
42
|
+
innerRadius: number;
|
|
43
|
+
outerRadius: number;
|
|
44
|
+
style: "solid" | "dashed";
|
|
45
|
+
holes: Array<{ x: number; y: number }>;
|
|
46
|
+
constraintTarget: "original" | "bleed";
|
|
47
|
+
}>,
|
|
48
|
+
) {
|
|
49
|
+
if (options) {
|
|
50
|
+
Object.assign(this, options);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
activate(context: ExtensionContext) {
|
|
55
|
+
this.context = context;
|
|
56
|
+
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
57
|
+
|
|
58
|
+
if (!this.canvasService) {
|
|
59
|
+
console.warn("CanvasService not found for HoleTool");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const configService = context.services.get<ConfigurationService>(
|
|
64
|
+
"ConfigurationService",
|
|
65
|
+
);
|
|
66
|
+
if (configService) {
|
|
67
|
+
// Load initial config
|
|
68
|
+
this.innerRadius = configService.get(
|
|
69
|
+
"hole.innerRadius",
|
|
70
|
+
this.innerRadius,
|
|
71
|
+
);
|
|
72
|
+
this.outerRadius = configService.get(
|
|
73
|
+
"hole.outerRadius",
|
|
74
|
+
this.outerRadius,
|
|
75
|
+
);
|
|
76
|
+
this.style = configService.get("hole.style", this.style);
|
|
77
|
+
this.constraintTarget = configService.get(
|
|
78
|
+
"hole.constraintTarget",
|
|
79
|
+
this.constraintTarget,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Load holes from dieline.holes (SSOT)
|
|
83
|
+
const dielineHoles = configService.get("dieline.holes", []);
|
|
84
|
+
if (this.canvasService) {
|
|
85
|
+
const { width, height } = this.canvasService.canvas;
|
|
86
|
+
this.holes = dielineHoles.map((h: any) => {
|
|
87
|
+
const p = Coordinate.denormalizePoint(h, {
|
|
88
|
+
width: width || 800,
|
|
89
|
+
height: height || 600,
|
|
90
|
+
});
|
|
91
|
+
return { x: p.x, y: p.y };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Listen for changes
|
|
96
|
+
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
97
|
+
if (this.isUpdatingConfig) return;
|
|
98
|
+
|
|
99
|
+
if (e.key.startsWith("hole.")) {
|
|
100
|
+
const prop = e.key.split(".")[1];
|
|
101
|
+
if (prop && prop in this) {
|
|
102
|
+
(this as any)[prop] = e.value;
|
|
103
|
+
this.redraw();
|
|
104
|
+
// Allow syncHolesToDieline to run to update dieline.holes
|
|
105
|
+
this.syncHolesToDieline();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Listen for dieline.holes changes (e.g. from undo/redo or other sources)
|
|
109
|
+
if (e.key === "dieline.holes") {
|
|
110
|
+
const holes = e.value || [];
|
|
111
|
+
if (this.canvasService) {
|
|
112
|
+
const { width, height } = this.canvasService.canvas;
|
|
113
|
+
this.holes = holes.map((h: any) => {
|
|
114
|
+
const p = Coordinate.denormalizePoint(h, {
|
|
115
|
+
width: width || 800,
|
|
116
|
+
height: height || 600,
|
|
117
|
+
});
|
|
118
|
+
return { x: p.x, y: p.y };
|
|
119
|
+
});
|
|
120
|
+
this.redraw();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.setup();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
deactivate(context: ExtensionContext) {
|
|
130
|
+
this.teardown();
|
|
131
|
+
this.canvasService = undefined;
|
|
132
|
+
this.context = undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
contribute() {
|
|
136
|
+
return {
|
|
137
|
+
[ContributionPointIds.CONFIGURATIONS]: [
|
|
138
|
+
{
|
|
139
|
+
id: "hole.innerRadius",
|
|
140
|
+
type: "number",
|
|
141
|
+
label: "Inner Radius",
|
|
142
|
+
min: 1,
|
|
143
|
+
max: 100,
|
|
144
|
+
default: 15,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "hole.outerRadius",
|
|
148
|
+
type: "number",
|
|
149
|
+
label: "Outer Radius",
|
|
150
|
+
min: 1,
|
|
151
|
+
max: 100,
|
|
152
|
+
default: 25,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "hole.style",
|
|
156
|
+
type: "select",
|
|
157
|
+
label: "Line Style",
|
|
158
|
+
options: ["solid", "dashed"],
|
|
159
|
+
default: "solid",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "hole.constraintTarget",
|
|
163
|
+
type: "select",
|
|
164
|
+
label: "Constraint Target",
|
|
165
|
+
options: ["original", "bleed"],
|
|
166
|
+
default: "bleed",
|
|
167
|
+
},
|
|
168
|
+
] as ConfigurationContribution[],
|
|
169
|
+
[ContributionPointIds.COMMANDS]: [
|
|
170
|
+
{
|
|
171
|
+
command: "resetHoles",
|
|
172
|
+
title: "Reset Holes",
|
|
173
|
+
handler: () => {
|
|
174
|
+
if (!this.canvasService) return false;
|
|
175
|
+
let defaultPos = { x: this.canvasService.canvas.width! / 2, y: 50 };
|
|
176
|
+
|
|
177
|
+
if (this.currentGeometry) {
|
|
178
|
+
const g = this.currentGeometry;
|
|
179
|
+
const topCenter = { x: g.x, y: g.y - g.height / 2 };
|
|
180
|
+
defaultPos = getNearestPointOnDieline(topCenter, {
|
|
181
|
+
...g,
|
|
182
|
+
holes: [],
|
|
183
|
+
} as any);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.innerRadius = 15;
|
|
187
|
+
this.outerRadius = 25;
|
|
188
|
+
this.style = "solid";
|
|
189
|
+
this.holes = [defaultPos];
|
|
190
|
+
|
|
191
|
+
this.redraw();
|
|
192
|
+
this.syncHolesToDieline();
|
|
193
|
+
return true;
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
command: "addHole",
|
|
198
|
+
title: "Add Hole",
|
|
199
|
+
handler: (x: number, y: number) => {
|
|
200
|
+
if (!this.holes) this.holes = [];
|
|
201
|
+
this.holes.push({ x, y });
|
|
202
|
+
this.redraw();
|
|
203
|
+
this.syncHolesToDieline();
|
|
204
|
+
return true;
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
command: "clearHoles",
|
|
209
|
+
title: "Clear Holes",
|
|
210
|
+
handler: () => {
|
|
211
|
+
this.holes = [];
|
|
212
|
+
this.redraw();
|
|
213
|
+
this.syncHolesToDieline();
|
|
214
|
+
return true;
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
] as CommandContribution[],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private setup() {
|
|
222
|
+
if (!this.canvasService || !this.context) return;
|
|
223
|
+
const canvas = this.canvasService.canvas;
|
|
224
|
+
|
|
225
|
+
// 1. Listen for Dieline Geometry Changes
|
|
226
|
+
if (!this.handleDielineChange) {
|
|
227
|
+
this.handleDielineChange = (geometry: DielineGeometry) => {
|
|
228
|
+
this.currentGeometry = geometry;
|
|
229
|
+
const changed = this.enforceConstraints();
|
|
230
|
+
// Only sync if constraints actually moved something
|
|
231
|
+
if (changed) {
|
|
232
|
+
this.syncHolesToDieline();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
this.context.eventBus.on(
|
|
236
|
+
"dieline:geometry:change",
|
|
237
|
+
this.handleDielineChange,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2. Initial Fetch of Geometry
|
|
242
|
+
// Assuming DielineTool registered 'getGeometry' command which is now available via CommandService
|
|
243
|
+
// Since we don't have direct access to CommandService here (it was in activate),
|
|
244
|
+
// we can get it from context.services
|
|
245
|
+
const commandService = this.context.services.get<any>("CommandService");
|
|
246
|
+
if (commandService) {
|
|
247
|
+
try {
|
|
248
|
+
const geometry = commandService.executeCommand("getGeometry");
|
|
249
|
+
if (geometry) {
|
|
250
|
+
// If executeCommand returns a promise, await it?
|
|
251
|
+
// CommandService.executeCommand is async in previous definition.
|
|
252
|
+
// But here we are in sync setup.
|
|
253
|
+
// Let's assume we can handle the promise if needed, or if it returns value directly (if not async).
|
|
254
|
+
// Checking CommandService implementation: executeCommand IS async.
|
|
255
|
+
Promise.resolve(geometry).then((g) => {
|
|
256
|
+
if (g) {
|
|
257
|
+
this.currentGeometry = g as DielineGeometry;
|
|
258
|
+
// Re-run setup logic dependent on geometry
|
|
259
|
+
this.enforceConstraints();
|
|
260
|
+
this.initializeHoles();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {
|
|
265
|
+
// Command might not be ready
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 3. Setup Canvas Interaction
|
|
270
|
+
if (!this.handleMoving) {
|
|
271
|
+
this.handleMoving = (e: any) => {
|
|
272
|
+
const target = e.target;
|
|
273
|
+
if (!target || target.data?.type !== "hole-marker") return;
|
|
274
|
+
|
|
275
|
+
if (!this.currentGeometry) return;
|
|
276
|
+
|
|
277
|
+
// Calculate effective geometry based on constraint target
|
|
278
|
+
const effectiveOffset =
|
|
279
|
+
this.constraintTarget === "original"
|
|
280
|
+
? 0
|
|
281
|
+
: this.currentGeometry.offset;
|
|
282
|
+
const constraintGeometry = {
|
|
283
|
+
...this.currentGeometry,
|
|
284
|
+
width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
|
|
285
|
+
height: Math.max(
|
|
286
|
+
0,
|
|
287
|
+
this.currentGeometry.height + effectiveOffset * 2,
|
|
288
|
+
),
|
|
289
|
+
radius: Math.max(0, this.currentGeometry.radius + effectiveOffset),
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const p = new Point(target.left, target.top);
|
|
293
|
+
const newPos = this.calculateConstrainedPosition(p, constraintGeometry);
|
|
294
|
+
|
|
295
|
+
target.set({
|
|
296
|
+
left: newPos.x,
|
|
297
|
+
top: newPos.y,
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
canvas.on("object:moving", this.handleMoving);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!this.handleModified) {
|
|
304
|
+
this.handleModified = (e: any) => {
|
|
305
|
+
const target = e.target;
|
|
306
|
+
if (!target || target.data?.type !== "hole-marker") return;
|
|
307
|
+
|
|
308
|
+
// Update state when hole is moved
|
|
309
|
+
this.syncHolesFromCanvas();
|
|
310
|
+
};
|
|
311
|
+
canvas.on("object:modified", this.handleModified);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.initializeHoles();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private initializeHoles() {
|
|
318
|
+
if (!this.canvasService) return;
|
|
319
|
+
// Default hole if none exist
|
|
320
|
+
if (!this.holes || this.holes.length === 0) {
|
|
321
|
+
let defaultPos = { x: this.canvasService.canvas.width! / 2, y: 50 };
|
|
322
|
+
|
|
323
|
+
if (this.currentGeometry) {
|
|
324
|
+
const g = this.currentGeometry;
|
|
325
|
+
// Default to Top-Center of Dieline shape
|
|
326
|
+
const topCenter = { x: g.x, y: g.y - g.height / 2 };
|
|
327
|
+
// Snap to exact shape edge
|
|
328
|
+
const snapped = getNearestPointOnDieline(topCenter, {
|
|
329
|
+
...g,
|
|
330
|
+
holes: [],
|
|
331
|
+
} as any);
|
|
332
|
+
defaultPos = snapped;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.holes = [defaultPos];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.redraw();
|
|
339
|
+
this.syncHolesToDieline();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private teardown() {
|
|
343
|
+
if (!this.canvasService) return;
|
|
344
|
+
const canvas = this.canvasService.canvas;
|
|
345
|
+
|
|
346
|
+
if (this.handleMoving) {
|
|
347
|
+
canvas.off("object:moving", this.handleMoving);
|
|
348
|
+
this.handleMoving = null;
|
|
349
|
+
}
|
|
350
|
+
if (this.handleModified) {
|
|
351
|
+
canvas.off("object:modified", this.handleModified);
|
|
352
|
+
this.handleModified = null;
|
|
353
|
+
}
|
|
354
|
+
if (this.handleDielineChange && this.context) {
|
|
355
|
+
this.context.eventBus.off(
|
|
356
|
+
"dieline:geometry:change",
|
|
357
|
+
this.handleDielineChange,
|
|
358
|
+
);
|
|
359
|
+
this.handleDielineChange = null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const objects = canvas
|
|
363
|
+
.getObjects()
|
|
364
|
+
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
365
|
+
objects.forEach((obj) => canvas.remove(obj));
|
|
366
|
+
|
|
367
|
+
// Clear holes from Dieline (visual only, state preserved in HoleTool options)
|
|
368
|
+
if (this.context) {
|
|
369
|
+
const commandService = this.context.services.get<any>("CommandService");
|
|
370
|
+
if (commandService) {
|
|
371
|
+
try {
|
|
372
|
+
commandService.executeCommand("setHoles", []);
|
|
373
|
+
} catch (e) {}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.canvasService.requestRenderAll();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private syncHolesFromCanvas() {
|
|
381
|
+
if (!this.canvasService) return;
|
|
382
|
+
const objects = this.canvasService.canvas
|
|
383
|
+
.getObjects()
|
|
384
|
+
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
385
|
+
|
|
386
|
+
const holes = objects.map((obj) => ({ x: obj.left!, y: obj.top! }));
|
|
387
|
+
this.holes = holes;
|
|
388
|
+
|
|
389
|
+
this.syncHolesToDieline();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private syncHolesToDieline() {
|
|
393
|
+
if (!this.context || !this.canvasService) return;
|
|
394
|
+
|
|
395
|
+
const { holes, innerRadius, outerRadius } = this;
|
|
396
|
+
const currentHoles = holes || [];
|
|
397
|
+
const width = this.canvasService.canvas.width || 800;
|
|
398
|
+
const height = this.canvasService.canvas.height || 600;
|
|
399
|
+
|
|
400
|
+
const configService = this.context.services.get<ConfigurationService>(
|
|
401
|
+
"ConfigurationService",
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
if (configService) {
|
|
405
|
+
this.isUpdatingConfig = true;
|
|
406
|
+
try {
|
|
407
|
+
// Update dieline.holes (Normalized coordinates)
|
|
408
|
+
const normalizedHoles = currentHoles.map((h) => {
|
|
409
|
+
const p = Coordinate.normalizePoint(h, { width, height });
|
|
410
|
+
return {
|
|
411
|
+
x: p.x,
|
|
412
|
+
y: p.y,
|
|
413
|
+
innerRadius,
|
|
414
|
+
outerRadius,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
configService.update("dieline.holes", normalizedHoles);
|
|
418
|
+
} finally {
|
|
419
|
+
this.isUpdatingConfig = false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private redraw() {
|
|
425
|
+
if (!this.canvasService) return;
|
|
426
|
+
const canvas = this.canvasService.canvas;
|
|
427
|
+
|
|
428
|
+
// Remove existing holes
|
|
429
|
+
const existing = canvas
|
|
430
|
+
.getObjects()
|
|
431
|
+
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
432
|
+
existing.forEach((obj) => canvas.remove(obj));
|
|
433
|
+
|
|
434
|
+
const { innerRadius, outerRadius, style, holes } = this;
|
|
435
|
+
|
|
436
|
+
if (!holes || holes.length === 0) {
|
|
437
|
+
this.canvasService.requestRenderAll();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
holes.forEach((hole, index) => {
|
|
442
|
+
const innerCircle = new Circle({
|
|
443
|
+
radius: innerRadius,
|
|
444
|
+
fill: "transparent",
|
|
445
|
+
stroke: "red",
|
|
446
|
+
strokeWidth: 2,
|
|
447
|
+
originX: "center",
|
|
448
|
+
originY: "center",
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const outerCircle = new Circle({
|
|
452
|
+
radius: outerRadius,
|
|
453
|
+
fill: "transparent",
|
|
454
|
+
stroke: "#666",
|
|
455
|
+
strokeWidth: 1,
|
|
456
|
+
strokeDashArray: style === "dashed" ? [5, 5] : undefined,
|
|
457
|
+
originX: "center",
|
|
458
|
+
originY: "center",
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const holeGroup = new Group([outerCircle, innerCircle], {
|
|
462
|
+
left: hole.x,
|
|
463
|
+
top: hole.y,
|
|
464
|
+
originX: "center",
|
|
465
|
+
originY: "center",
|
|
466
|
+
selectable: true,
|
|
467
|
+
hasControls: false, // Don't allow resizing/rotating
|
|
468
|
+
hasBorders: false,
|
|
469
|
+
subTargetCheck: false,
|
|
470
|
+
opacity: 0, // Default hidden
|
|
471
|
+
hoverCursor: "move",
|
|
472
|
+
data: { type: "hole-marker", index },
|
|
473
|
+
} as any);
|
|
474
|
+
(holeGroup as any).name = "hole-marker";
|
|
475
|
+
|
|
476
|
+
// Auto-show/hide logic
|
|
477
|
+
holeGroup.on("mouseover", () => {
|
|
478
|
+
holeGroup.set("opacity", 1);
|
|
479
|
+
canvas.requestRenderAll();
|
|
480
|
+
});
|
|
481
|
+
holeGroup.on("mouseout", () => {
|
|
482
|
+
if (canvas.getActiveObject() !== holeGroup) {
|
|
483
|
+
holeGroup.set("opacity", 0);
|
|
484
|
+
canvas.requestRenderAll();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
holeGroup.on("selected", () => {
|
|
488
|
+
holeGroup.set("opacity", 1);
|
|
489
|
+
canvas.requestRenderAll();
|
|
490
|
+
});
|
|
491
|
+
holeGroup.on("deselected", () => {
|
|
492
|
+
holeGroup.set("opacity", 0);
|
|
493
|
+
canvas.requestRenderAll();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
canvas.add(holeGroup);
|
|
497
|
+
|
|
498
|
+
// Ensure hole markers are always on top of Dieline layer
|
|
499
|
+
// Dieline layer uses bringObjectToFront, so we must be aggressive
|
|
500
|
+
// But we can't control when Dieline updates.
|
|
501
|
+
// Ideally, HoleTool should use a dedicated overlay layer above Dieline.
|
|
502
|
+
// For now, let's just bring to front.
|
|
503
|
+
canvas.bringObjectToFront(holeGroup);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Also bring all existing markers to front to be safe
|
|
507
|
+
const markers = canvas.getObjects().filter((o: any) => o.data?.type === "hole-marker");
|
|
508
|
+
markers.forEach(m => canvas.bringObjectToFront(m));
|
|
509
|
+
|
|
510
|
+
this.canvasService.requestRenderAll();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
public enforceConstraints(): boolean {
|
|
514
|
+
const geometry = this.currentGeometry;
|
|
515
|
+
if (!geometry || !this.canvasService) {
|
|
516
|
+
console.log(
|
|
517
|
+
"[HoleTool] Skipping enforceConstraints: No geometry or canvas service",
|
|
518
|
+
);
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const effectiveOffset =
|
|
523
|
+
this.constraintTarget === "original" ? 0 : geometry.offset;
|
|
524
|
+
const constraintGeometry = {
|
|
525
|
+
...geometry,
|
|
526
|
+
width: Math.max(0, geometry.width + effectiveOffset * 2),
|
|
527
|
+
height: Math.max(0, geometry.height + effectiveOffset * 2),
|
|
528
|
+
radius: Math.max(0, geometry.radius + effectiveOffset),
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// Get all hole markers
|
|
532
|
+
const objects = this.canvasService.canvas
|
|
533
|
+
.getObjects()
|
|
534
|
+
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
535
|
+
|
|
536
|
+
console.log(
|
|
537
|
+
`[HoleTool] Enforcing constraints on ${objects.length} markers`,
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
let changed = false;
|
|
541
|
+
// Sort objects by index to maintain order in options.holes
|
|
542
|
+
objects.sort(
|
|
543
|
+
(a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const newHoles: { x: number; y: number }[] = [];
|
|
547
|
+
|
|
548
|
+
objects.forEach((obj: any) => {
|
|
549
|
+
const currentPos = new Point(obj.left, obj.top);
|
|
550
|
+
const newPos = this.calculateConstrainedPosition(
|
|
551
|
+
currentPos,
|
|
552
|
+
constraintGeometry,
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
if (currentPos.distanceFrom(newPos) > 0.1) {
|
|
556
|
+
console.log(
|
|
557
|
+
`[HoleTool] Moving hole from (${currentPos.x}, ${currentPos.y}) to (${newPos.x}, ${newPos.y})`,
|
|
558
|
+
);
|
|
559
|
+
obj.set({
|
|
560
|
+
left: newPos.x,
|
|
561
|
+
top: newPos.y,
|
|
562
|
+
});
|
|
563
|
+
obj.setCoords();
|
|
564
|
+
changed = true;
|
|
565
|
+
}
|
|
566
|
+
newHoles.push({ x: obj.left, y: obj.top });
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
if (changed) {
|
|
570
|
+
this.holes = newHoles;
|
|
571
|
+
this.canvasService.requestRenderAll();
|
|
572
|
+
// We return true instead of syncing directly to avoid recursion
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private calculateConstrainedPosition(p: Point, g: DielineGeometry): Point {
|
|
579
|
+
// Use Paper.js to get accurate nearest point
|
|
580
|
+
// This handles ellipses, rects, and rounded rects correctly
|
|
581
|
+
|
|
582
|
+
// Convert to holes format for geometry options
|
|
583
|
+
const options = {
|
|
584
|
+
...g,
|
|
585
|
+
holes: [], // We don't need holes for boundary calculation
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const nearest = getNearestPointOnDieline(
|
|
589
|
+
{ x: p.x, y: p.y },
|
|
590
|
+
options as any,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
// Now constrain distance
|
|
594
|
+
const nearestP = new Point(nearest.x, nearest.y);
|
|
595
|
+
const dist = p.distanceFrom(nearestP);
|
|
596
|
+
|
|
597
|
+
// Vector from nearest to current point
|
|
598
|
+
const v = p.subtract(nearestP);
|
|
599
|
+
|
|
600
|
+
// Vector from center to nearest point (approximate normal for convex shapes)
|
|
601
|
+
const center = new Point(g.x, g.y);
|
|
602
|
+
const centerToNearest = nearestP.subtract(center);
|
|
603
|
+
|
|
604
|
+
const distToCenter = p.distanceFrom(center);
|
|
605
|
+
const nearestDistToCenter = nearestP.distanceFrom(center);
|
|
606
|
+
|
|
607
|
+
let signedDist = dist;
|
|
608
|
+
if (distToCenter < nearestDistToCenter) {
|
|
609
|
+
signedDist = -dist; // Inside
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Clamp distance
|
|
613
|
+
let clampedDist = signedDist;
|
|
614
|
+
if (signedDist > 0) {
|
|
615
|
+
clampedDist = Math.min(signedDist, this.innerRadius);
|
|
616
|
+
} else {
|
|
617
|
+
clampedDist = Math.max(signedDist, -this.outerRadius);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Reconstruct point
|
|
621
|
+
if (dist < 0.001) return nearestP;
|
|
622
|
+
|
|
623
|
+
// We want the result to lie on the line connecting Nearest -> P
|
|
624
|
+
const scale = Math.abs(clampedDist) / (dist || 1);
|
|
625
|
+
const offset = v.scalarMultiply(scale);
|
|
626
|
+
|
|
627
|
+
return nearestP.add(offset);
|
|
628
|
+
}
|
|
629
|
+
}
|