@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/hole.ts
CHANGED
|
@@ -1,156 +1,303 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Command,
|
|
3
|
-
Editor,
|
|
4
|
-
EditorState,
|
|
5
2
|
Extension,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
5
|
+
CommandContribution,
|
|
6
|
+
ConfigurationContribution,
|
|
7
|
+
ConfigurationService,
|
|
10
8
|
} from "@pooder/core";
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
innerRadius: number;
|
|
17
|
-
outerRadius: number;
|
|
18
|
-
style: "solid" | "dashed";
|
|
19
|
-
holes?: Array<{ x: number; y: number }>;
|
|
20
|
-
constraintTarget?: "original" | "bleed";
|
|
21
|
-
}
|
|
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";
|
|
22
14
|
|
|
23
|
-
export class HoleTool implements Extension
|
|
24
|
-
|
|
25
|
-
public options: HoleToolOptions = {
|
|
26
|
-
innerRadius: 15,
|
|
27
|
-
outerRadius: 25,
|
|
28
|
-
style: "solid",
|
|
29
|
-
holes: [],
|
|
30
|
-
constraintTarget: "bleed",
|
|
31
|
-
};
|
|
15
|
+
export class HoleTool implements Extension {
|
|
16
|
+
id = "pooder.kit.hole";
|
|
32
17
|
|
|
33
|
-
public
|
|
34
|
-
|
|
35
|
-
type: "number",
|
|
36
|
-
min: 1,
|
|
37
|
-
max: 100,
|
|
38
|
-
label: "Inner Radius",
|
|
39
|
-
},
|
|
40
|
-
outerRadius: {
|
|
41
|
-
type: "number",
|
|
42
|
-
min: 1,
|
|
43
|
-
max: 100,
|
|
44
|
-
label: "Outer Radius",
|
|
45
|
-
},
|
|
46
|
-
style: {
|
|
47
|
-
type: "select",
|
|
48
|
-
options: ["solid", "dashed"],
|
|
49
|
-
label: "Line Style",
|
|
50
|
-
},
|
|
51
|
-
constraintTarget: {
|
|
52
|
-
type: "select",
|
|
53
|
-
options: ["original", "bleed"],
|
|
54
|
-
label: "Constraint Target",
|
|
55
|
-
},
|
|
56
|
-
holes: {
|
|
57
|
-
type: "json",
|
|
58
|
-
label: "Holes",
|
|
59
|
-
} as any,
|
|
18
|
+
public metadata = {
|
|
19
|
+
name: "HoleTool",
|
|
60
20
|
};
|
|
61
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
|
+
|
|
62
32
|
private handleMoving: ((e: any) => void) | null = null;
|
|
63
33
|
private handleModified: ((e: any) => void) | null = null;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
67
52
|
}
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
54
|
+
activate(context: ExtensionContext) {
|
|
55
|
+
this.context = context;
|
|
56
|
+
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
72
57
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
if (!this.canvasService) {
|
|
59
|
+
console.warn("CanvasService not found for HoleTool");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
76
62
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}
|
|
83
125
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
? 0
|
|
87
|
-
: dielineTool.options.offset || 0;
|
|
126
|
+
this.setup();
|
|
127
|
+
}
|
|
88
128
|
|
|
89
|
-
|
|
129
|
+
deactivate(context: ExtensionContext) {
|
|
130
|
+
this.teardown();
|
|
131
|
+
this.canvasService = undefined;
|
|
132
|
+
this.context = undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
contribute() {
|
|
90
136
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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[],
|
|
95
218
|
};
|
|
96
219
|
}
|
|
97
220
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const newPos = this.calculateConstrainedPosition(currentPos, geometry);
|
|
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
|
+
}
|
|
118
240
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
126
266
|
}
|
|
127
|
-
newHoles.push({ x: obj.left, y: obj.top });
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (changed) {
|
|
131
|
-
this.options.holes = newHoles;
|
|
132
|
-
editor.canvas.requestRenderAll();
|
|
133
267
|
}
|
|
134
|
-
}
|
|
135
268
|
|
|
136
|
-
|
|
269
|
+
// 3. Setup Canvas Interaction
|
|
137
270
|
if (!this.handleMoving) {
|
|
138
271
|
this.handleMoving = (e: any) => {
|
|
139
272
|
const target = e.target;
|
|
140
273
|
if (!target || target.data?.type !== "hole-marker") return;
|
|
141
274
|
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
};
|
|
144
291
|
|
|
145
292
|
const p = new Point(target.left, target.top);
|
|
146
|
-
const newPos = this.calculateConstrainedPosition(p,
|
|
293
|
+
const newPos = this.calculateConstrainedPosition(p, constraintGeometry);
|
|
147
294
|
|
|
148
295
|
target.set({
|
|
149
296
|
left: newPos.x,
|
|
150
297
|
top: newPos.y,
|
|
151
298
|
});
|
|
152
299
|
};
|
|
153
|
-
|
|
300
|
+
canvas.on("object:moving", this.handleMoving);
|
|
154
301
|
}
|
|
155
302
|
|
|
156
303
|
if (!this.handleModified) {
|
|
@@ -159,18 +306,22 @@ export class HoleTool implements Extension<HoleToolOptions> {
|
|
|
159
306
|
if (!target || target.data?.type !== "hole-marker") return;
|
|
160
307
|
|
|
161
308
|
// Update state when hole is moved
|
|
162
|
-
this.syncHolesFromCanvas(
|
|
309
|
+
this.syncHolesFromCanvas();
|
|
163
310
|
};
|
|
164
|
-
|
|
311
|
+
canvas.on("object:modified", this.handleModified);
|
|
165
312
|
}
|
|
166
313
|
|
|
167
|
-
|
|
314
|
+
this.initializeHoles();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private initializeHoles() {
|
|
318
|
+
if (!this.canvasService) return;
|
|
168
319
|
// Default hole if none exist
|
|
169
|
-
if (!
|
|
170
|
-
let defaultPos = { x:
|
|
320
|
+
if (!this.holes || this.holes.length === 0) {
|
|
321
|
+
let defaultPos = { x: this.canvasService.canvas.width! / 2, y: 50 };
|
|
171
322
|
|
|
172
|
-
|
|
173
|
-
|
|
323
|
+
if (this.currentGeometry) {
|
|
324
|
+
const g = this.currentGeometry;
|
|
174
325
|
// Default to Top-Center of Dieline shape
|
|
175
326
|
const topCenter = { x: g.x, y: g.y - g.height / 2 };
|
|
176
327
|
// Snap to exact shape edge
|
|
@@ -181,138 +332,98 @@ export class HoleTool implements Extension<HoleToolOptions> {
|
|
|
181
332
|
defaultPos = snapped;
|
|
182
333
|
}
|
|
183
334
|
|
|
184
|
-
|
|
335
|
+
this.holes = [defaultPos];
|
|
185
336
|
}
|
|
186
337
|
|
|
187
|
-
this.
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
// Ensure Dieline updates to reflect current holes (fusion effect)
|
|
191
|
-
const dielineTool = editor.getExtension("DielineTool") as DielineTool;
|
|
192
|
-
if (dielineTool && dielineTool.updateDieline) {
|
|
193
|
-
dielineTool.updateDieline(editor);
|
|
194
|
-
}
|
|
338
|
+
this.redraw();
|
|
339
|
+
this.syncHolesToDieline();
|
|
195
340
|
}
|
|
196
341
|
|
|
197
|
-
private teardown(
|
|
342
|
+
private teardown() {
|
|
343
|
+
if (!this.canvasService) return;
|
|
344
|
+
const canvas = this.canvasService.canvas;
|
|
345
|
+
|
|
198
346
|
if (this.handleMoving) {
|
|
199
|
-
|
|
347
|
+
canvas.off("object:moving", this.handleMoving);
|
|
200
348
|
this.handleMoving = null;
|
|
201
349
|
}
|
|
202
350
|
if (this.handleModified) {
|
|
203
|
-
|
|
351
|
+
canvas.off("object:modified", this.handleModified);
|
|
204
352
|
this.handleModified = null;
|
|
205
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
|
+
}
|
|
206
361
|
|
|
207
|
-
const objects =
|
|
362
|
+
const objects = canvas
|
|
208
363
|
.getObjects()
|
|
209
364
|
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
210
|
-
objects.forEach((obj) =>
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const dielineTool = editor.getExtension("DielineTool") as any;
|
|
221
|
-
if (dielineTool && dielineTool.updateDieline) {
|
|
222
|
-
dielineTool.updateDieline(editor);
|
|
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
|
+
}
|
|
223
375
|
}
|
|
224
|
-
}
|
|
225
376
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
execute: (editor: Editor) => {
|
|
229
|
-
let defaultPos = { x: editor.canvas.width! / 2, y: 50 };
|
|
230
|
-
|
|
231
|
-
const g = this.getDielineGeometry(editor);
|
|
232
|
-
if (g) {
|
|
233
|
-
const topCenter = { x: g.x, y: g.y - g.height / 2 };
|
|
234
|
-
defaultPos = getNearestPointOnDieline(topCenter, {
|
|
235
|
-
...g,
|
|
236
|
-
holes: [],
|
|
237
|
-
} as any);
|
|
238
|
-
}
|
|
377
|
+
this.canvasService.requestRenderAll();
|
|
378
|
+
}
|
|
239
379
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
};
|
|
246
|
-
this.redraw(editor);
|
|
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");
|
|
247
385
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (dielineTool && dielineTool.updateDieline) {
|
|
251
|
-
dielineTool.updateDieline(editor);
|
|
252
|
-
}
|
|
386
|
+
const holes = objects.map((obj) => ({ x: obj.left!, y: obj.top! }));
|
|
387
|
+
this.holes = holes;
|
|
253
388
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
},
|
|
257
|
-
addHole: {
|
|
258
|
-
execute: (editor: Editor, x: number, y: number) => {
|
|
259
|
-
if (!this.options.holes) this.options.holes = [];
|
|
260
|
-
this.options.holes.push({ x, y });
|
|
261
|
-
this.redraw(editor);
|
|
262
|
-
|
|
263
|
-
// Trigger Dieline update
|
|
264
|
-
const dielineTool = editor.getExtension("DielineTool") as any;
|
|
265
|
-
if (dielineTool && dielineTool.updateDieline) {
|
|
266
|
-
dielineTool.updateDieline(editor);
|
|
267
|
-
}
|
|
389
|
+
this.syncHolesToDieline();
|
|
390
|
+
}
|
|
268
391
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
schema: {
|
|
272
|
-
x: {
|
|
273
|
-
type: "number",
|
|
274
|
-
label: "X Position",
|
|
275
|
-
required: true,
|
|
276
|
-
},
|
|
277
|
-
y: {
|
|
278
|
-
type: "number",
|
|
279
|
-
label: "Y Position",
|
|
280
|
-
required: true,
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
clearHoles: {
|
|
285
|
-
execute: (editor: Editor) => {
|
|
286
|
-
this.options.holes = [];
|
|
287
|
-
this.redraw(editor);
|
|
288
|
-
|
|
289
|
-
// Trigger Dieline update
|
|
290
|
-
const dielineTool = editor.getExtension("DielineTool") as any;
|
|
291
|
-
if (dielineTool && dielineTool.updateDieline) {
|
|
292
|
-
dielineTool.updateDieline(editor);
|
|
293
|
-
}
|
|
392
|
+
private syncHolesToDieline() {
|
|
393
|
+
if (!this.context || !this.canvasService) return;
|
|
294
394
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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;
|
|
299
399
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
304
|
-
const holes = objects.map((obj) => ({ x: obj.left!, y: obj.top! }));
|
|
305
|
-
this.options.holes = holes;
|
|
400
|
+
const configService = this.context.services.get<ConfigurationService>(
|
|
401
|
+
"ConfigurationService",
|
|
402
|
+
);
|
|
306
403
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
}
|
|
311
421
|
}
|
|
312
422
|
}
|
|
313
423
|
|
|
314
|
-
private redraw(
|
|
315
|
-
|
|
424
|
+
private redraw() {
|
|
425
|
+
if (!this.canvasService) return;
|
|
426
|
+
const canvas = this.canvasService.canvas;
|
|
316
427
|
|
|
317
428
|
// Remove existing holes
|
|
318
429
|
const existing = canvas
|
|
@@ -320,10 +431,10 @@ export class HoleTool implements Extension<HoleToolOptions> {
|
|
|
320
431
|
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
321
432
|
existing.forEach((obj) => canvas.remove(obj));
|
|
322
433
|
|
|
323
|
-
const { innerRadius, outerRadius, style, holes } = this
|
|
434
|
+
const { innerRadius, outerRadius, style, holes } = this;
|
|
324
435
|
|
|
325
436
|
if (!holes || holes.length === 0) {
|
|
326
|
-
|
|
437
|
+
this.canvasService.requestRenderAll();
|
|
327
438
|
return;
|
|
328
439
|
}
|
|
329
440
|
|
|
@@ -383,10 +494,85 @@ export class HoleTool implements Extension<HoleToolOptions> {
|
|
|
383
494
|
});
|
|
384
495
|
|
|
385
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.
|
|
386
503
|
canvas.bringObjectToFront(holeGroup);
|
|
387
504
|
});
|
|
388
505
|
|
|
389
|
-
|
|
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;
|
|
390
576
|
}
|
|
391
577
|
|
|
392
578
|
private calculateConstrainedPosition(p: Point, g: DielineGeometry): Point {
|
|
@@ -408,11 +594,6 @@ export class HoleTool implements Extension<HoleToolOptions> {
|
|
|
408
594
|
const nearestP = new Point(nearest.x, nearest.y);
|
|
409
595
|
const dist = p.distanceFrom(nearestP);
|
|
410
596
|
|
|
411
|
-
// Determine if point is inside or outside
|
|
412
|
-
// Simple heuristic: distance from center
|
|
413
|
-
// Or using paper.js contains() if we had the full path object
|
|
414
|
-
// For convex shapes, center distance works mostly, but let's use the vector direction
|
|
415
|
-
|
|
416
597
|
// Vector from nearest to current point
|
|
417
598
|
const v = p.subtract(nearestP);
|
|
418
599
|
|
|
@@ -420,24 +601,6 @@ export class HoleTool implements Extension<HoleToolOptions> {
|
|
|
420
601
|
const center = new Point(g.x, g.y);
|
|
421
602
|
const centerToNearest = nearestP.subtract(center);
|
|
422
603
|
|
|
423
|
-
// Dot product to see if they align (outside) or oppose (inside)
|
|
424
|
-
// If point is exactly on line, dist is 0.
|
|
425
|
-
|
|
426
|
-
// However, we want to constrain the point to be within [innerRadius, -outerRadius] distance from the edge.
|
|
427
|
-
// Actually, usually users want to snap to the edge or stay within a reasonable margin.
|
|
428
|
-
// The previous logic clamped the distance.
|
|
429
|
-
|
|
430
|
-
// Let's implement a simple snap-to-edge if close, otherwise allow free movement but clamp max distance?
|
|
431
|
-
// Or reproduce the previous "slide along edge" behavior.
|
|
432
|
-
// Previous behavior: "clampedDist = Math.min(dist, innerRadius); ... Math.max(dist, -outerRadius)"
|
|
433
|
-
// This implies the hole center can be slightly inside or outside the main shape edge.
|
|
434
|
-
|
|
435
|
-
// Let's determine sign of distance
|
|
436
|
-
// We can use paper.js Shape.contains(point) to check if inside.
|
|
437
|
-
// But getNearestPointOnDieline returns just coordinates.
|
|
438
|
-
|
|
439
|
-
// Optimization: Let's assume for Dieline shapes (convex-ish),
|
|
440
|
-
// if distance from center > distance of nearest from center, it's outside.
|
|
441
604
|
const distToCenter = p.distanceFrom(center);
|
|
442
605
|
const nearestDistToCenter = nearestP.distanceFrom(center);
|
|
443
606
|
|
|
@@ -449,43 +612,16 @@ export class HoleTool implements Extension<HoleToolOptions> {
|
|
|
449
612
|
// Clamp distance
|
|
450
613
|
let clampedDist = signedDist;
|
|
451
614
|
if (signedDist > 0) {
|
|
452
|
-
clampedDist = Math.min(signedDist, this.
|
|
615
|
+
clampedDist = Math.min(signedDist, this.innerRadius);
|
|
453
616
|
} else {
|
|
454
|
-
clampedDist = Math.max(signedDist, -this.
|
|
617
|
+
clampedDist = Math.max(signedDist, -this.outerRadius);
|
|
455
618
|
}
|
|
456
619
|
|
|
457
620
|
// Reconstruct point
|
|
458
|
-
// If dist is very small, just use nearestP
|
|
459
621
|
if (dist < 0.001) return nearestP;
|
|
460
622
|
|
|
461
|
-
//
|
|
462
|
-
const dir = v.scalarDivide(dist);
|
|
463
|
-
|
|
464
|
-
// New point = nearest + dir * clampedDist
|
|
465
|
-
// Note: if inside (signedDist < 0), v points towards center (roughly), dist is positive magnitude.
|
|
466
|
-
// Wait, v = p - nearest.
|
|
467
|
-
// If p is inside, p is closer to center. v points Inwards.
|
|
468
|
-
// If we want clampedDist to be negative, we should probably stick to normal vectors.
|
|
469
|
-
|
|
470
|
-
// Let's simplify:
|
|
471
|
-
// Just place it at nearest point + offset vector.
|
|
472
|
-
// Offset vector is 'v' scaled to clampedDist.
|
|
473
|
-
|
|
474
|
-
// If p is inside, v points in. length is 'dist'.
|
|
475
|
-
// We want length to be 'clampedDist' (magnitude).
|
|
476
|
-
// Since clampedDist is negative for inside, we need to be careful with signs.
|
|
477
|
-
|
|
478
|
-
// Actually simpler:
|
|
479
|
-
// We want the result to lie on the line connecting Center -> P -> Nearest? No.
|
|
480
|
-
// We want it on the line Nearest -> P.
|
|
481
|
-
|
|
482
|
-
// Current distance is 'dist'.
|
|
483
|
-
// Desired distance is abs(clampedDist).
|
|
484
|
-
// If clampedDist sign matches signedDist sign, we just scale v.
|
|
485
|
-
|
|
623
|
+
// We want the result to lie on the line connecting Nearest -> P
|
|
486
624
|
const scale = Math.abs(clampedDist) / (dist || 1);
|
|
487
|
-
|
|
488
|
-
// If we are clamping, we just scale the vector from nearest.
|
|
489
625
|
const offset = v.scalarMultiply(scale);
|
|
490
626
|
|
|
491
627
|
return nearestP.add(offset);
|