@pooder/kit 3.3.0 → 3.5.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 +12 -0
- package/dist/index.d.mts +53 -57
- package/dist/index.d.ts +53 -57
- package/dist/index.js +1081 -930
- package/dist/index.mjs +1080 -929
- package/package.json +1 -1
- package/src/CanvasService.ts +65 -65
- package/src/background.ts +230 -230
- package/src/coordinate.ts +106 -106
- package/src/dieline.ts +282 -218
- package/src/feature.ts +724 -0
- package/src/film.ts +194 -194
- package/src/geometry.ts +118 -370
- package/src/image.ts +471 -496
- package/src/index.ts +1 -1
- package/src/mirror.ts +128 -128
- package/src/ruler.ts +500 -500
- package/src/tracer.ts +570 -372
- package/src/white-ink.ts +373 -373
- package/src/hole.ts +0 -786
package/src/hole.ts
DELETED
|
@@ -1,786 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Extension,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
ContributionPointIds,
|
|
5
|
-
CommandContribution,
|
|
6
|
-
ConfigurationContribution,
|
|
7
|
-
ConfigurationService,
|
|
8
|
-
} from "@pooder/core";
|
|
9
|
-
import { Circle, Group, Point, Rect } from "fabric";
|
|
10
|
-
import CanvasService from "./CanvasService";
|
|
11
|
-
import { DielineGeometry } from "./dieline";
|
|
12
|
-
import {
|
|
13
|
-
getNearestPointOnDieline,
|
|
14
|
-
HoleData,
|
|
15
|
-
resolveHolePosition,
|
|
16
|
-
} from "./geometry";
|
|
17
|
-
import { Coordinate } from "./coordinate";
|
|
18
|
-
|
|
19
|
-
export class HoleTool implements Extension {
|
|
20
|
-
id = "pooder.kit.hole";
|
|
21
|
-
|
|
22
|
-
public metadata = {
|
|
23
|
-
name: "HoleTool",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
private holes: HoleData[] = [];
|
|
27
|
-
private constraintTarget: "original" | "bleed" = "bleed";
|
|
28
|
-
|
|
29
|
-
private canvasService?: CanvasService;
|
|
30
|
-
private context?: ExtensionContext;
|
|
31
|
-
private isUpdatingConfig = false;
|
|
32
|
-
|
|
33
|
-
private handleMoving: ((e: any) => void) | null = null;
|
|
34
|
-
private handleModified: ((e: any) => void) | null = null;
|
|
35
|
-
private handleDielineChange: ((geometry: DielineGeometry) => void) | null =
|
|
36
|
-
null;
|
|
37
|
-
|
|
38
|
-
// Cache geometry to enforce constraints during drag
|
|
39
|
-
private currentGeometry: DielineGeometry | null = null;
|
|
40
|
-
|
|
41
|
-
constructor(
|
|
42
|
-
options?: Partial<{
|
|
43
|
-
holes: HoleData[];
|
|
44
|
-
constraintTarget: "original" | "bleed";
|
|
45
|
-
}>
|
|
46
|
-
) {
|
|
47
|
-
if (options) {
|
|
48
|
-
Object.assign(this, options);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
activate(context: ExtensionContext) {
|
|
53
|
-
this.context = context;
|
|
54
|
-
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
55
|
-
|
|
56
|
-
if (!this.canvasService) {
|
|
57
|
-
console.warn("CanvasService not found for HoleTool");
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const configService = context.services.get<ConfigurationService>(
|
|
62
|
-
"ConfigurationService"
|
|
63
|
-
);
|
|
64
|
-
if (configService) {
|
|
65
|
-
// Load initial config
|
|
66
|
-
this.constraintTarget = configService.get(
|
|
67
|
-
"hole.constraintTarget",
|
|
68
|
-
this.constraintTarget
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
// Load holes from dieline.holes (SSOT)
|
|
72
|
-
this.holes = configService.get("dieline.holes", []);
|
|
73
|
-
|
|
74
|
-
// Listen for changes
|
|
75
|
-
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
76
|
-
if (this.isUpdatingConfig) return;
|
|
77
|
-
|
|
78
|
-
if (e.key === "hole.constraintTarget") {
|
|
79
|
-
this.constraintTarget = e.value;
|
|
80
|
-
this.enforceConstraints();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Listen for dieline.holes changes (e.g. from undo/redo or other sources)
|
|
84
|
-
if (e.key === "dieline.holes") {
|
|
85
|
-
this.holes = e.value || [];
|
|
86
|
-
this.redraw();
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
this.setup();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
deactivate(context: ExtensionContext) {
|
|
95
|
-
this.teardown();
|
|
96
|
-
this.canvasService = undefined;
|
|
97
|
-
this.context = undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
contribute() {
|
|
101
|
-
return {
|
|
102
|
-
[ContributionPointIds.CONFIGURATIONS]: [
|
|
103
|
-
{
|
|
104
|
-
id: "hole.constraintTarget",
|
|
105
|
-
type: "select",
|
|
106
|
-
label: "Constraint Target",
|
|
107
|
-
options: ["original", "bleed"],
|
|
108
|
-
default: "bleed",
|
|
109
|
-
},
|
|
110
|
-
] as ConfigurationContribution[],
|
|
111
|
-
[ContributionPointIds.COMMANDS]: [
|
|
112
|
-
{
|
|
113
|
-
command: "resetHoles",
|
|
114
|
-
title: "Reset Holes",
|
|
115
|
-
handler: () => {
|
|
116
|
-
if (!this.canvasService) return false;
|
|
117
|
-
let defaultPos = { x: this.canvasService.canvas.width! / 2, y: 50 };
|
|
118
|
-
|
|
119
|
-
if (this.currentGeometry) {
|
|
120
|
-
const g = this.currentGeometry;
|
|
121
|
-
const topCenter = { x: g.x, y: g.y - g.height / 2 };
|
|
122
|
-
defaultPos = getNearestPointOnDieline(topCenter, {
|
|
123
|
-
...g,
|
|
124
|
-
holes: [],
|
|
125
|
-
} as any);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const { width, height } = this.canvasService.canvas;
|
|
129
|
-
const normalizedHole = Coordinate.normalizePoint(defaultPos, {
|
|
130
|
-
width: width || 800,
|
|
131
|
-
height: height || 600,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const configService = this.context?.services.get<ConfigurationService>(
|
|
135
|
-
"ConfigurationService"
|
|
136
|
-
);
|
|
137
|
-
if (configService) {
|
|
138
|
-
configService.update("dieline.holes", [
|
|
139
|
-
{
|
|
140
|
-
x: normalizedHole.x,
|
|
141
|
-
y: normalizedHole.y,
|
|
142
|
-
innerRadius: 15,
|
|
143
|
-
outerRadius: 25,
|
|
144
|
-
},
|
|
145
|
-
]);
|
|
146
|
-
}
|
|
147
|
-
return true;
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
command: "addHole",
|
|
152
|
-
title: "Add Hole",
|
|
153
|
-
handler: (x: number, y: number) => {
|
|
154
|
-
if (!this.canvasService) return false;
|
|
155
|
-
|
|
156
|
-
// Normalize relative to Dieline Geometry if available
|
|
157
|
-
let normalizedX = 0.5;
|
|
158
|
-
let normalizedY = 0.5;
|
|
159
|
-
|
|
160
|
-
if (this.currentGeometry) {
|
|
161
|
-
const { x: gx, y: gy, width: gw, height: gh } = this.currentGeometry;
|
|
162
|
-
const left = gx - gw / 2;
|
|
163
|
-
const top = gy - gh / 2;
|
|
164
|
-
normalizedX = gw > 0 ? (x - left) / gw : 0.5;
|
|
165
|
-
normalizedY = gh > 0 ? (y - top) / gh : 0.5;
|
|
166
|
-
} else {
|
|
167
|
-
const { width, height } = this.canvasService.canvas;
|
|
168
|
-
normalizedX = Coordinate.toNormalized(x, width || 800);
|
|
169
|
-
normalizedY = Coordinate.toNormalized(y, height || 600);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const configService = this.context?.services.get<ConfigurationService>(
|
|
173
|
-
"ConfigurationService"
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
if (configService) {
|
|
177
|
-
const currentHoles = configService.get("dieline.holes", []) as HoleData[];
|
|
178
|
-
// Use last hole's radii or default
|
|
179
|
-
const lastHole = currentHoles[currentHoles.length - 1];
|
|
180
|
-
const innerRadius = lastHole?.innerRadius ?? 15;
|
|
181
|
-
const outerRadius = lastHole?.outerRadius ?? 25;
|
|
182
|
-
const shape = lastHole?.shape ?? "circle";
|
|
183
|
-
|
|
184
|
-
const newHole = {
|
|
185
|
-
x: normalizedX,
|
|
186
|
-
y: normalizedY,
|
|
187
|
-
shape,
|
|
188
|
-
innerRadius,
|
|
189
|
-
outerRadius,
|
|
190
|
-
};
|
|
191
|
-
configService.update("dieline.holes", [...currentHoles, newHole]);
|
|
192
|
-
}
|
|
193
|
-
return true;
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
command: "clearHoles",
|
|
198
|
-
title: "Clear Holes",
|
|
199
|
-
handler: () => {
|
|
200
|
-
const configService = this.context?.services.get<ConfigurationService>(
|
|
201
|
-
"ConfigurationService"
|
|
202
|
-
);
|
|
203
|
-
if (configService) {
|
|
204
|
-
configService.update("dieline.holes", []);
|
|
205
|
-
}
|
|
206
|
-
return true;
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
] as CommandContribution[],
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private setup() {
|
|
214
|
-
if (!this.canvasService || !this.context) return;
|
|
215
|
-
const canvas = this.canvasService.canvas;
|
|
216
|
-
|
|
217
|
-
// 1. Listen for Dieline Geometry Changes
|
|
218
|
-
if (!this.handleDielineChange) {
|
|
219
|
-
this.handleDielineChange = (geometry: DielineGeometry) => {
|
|
220
|
-
this.currentGeometry = geometry;
|
|
221
|
-
this.redraw();
|
|
222
|
-
const changed = this.enforceConstraints();
|
|
223
|
-
// Only sync if constraints actually moved something
|
|
224
|
-
if (changed) {
|
|
225
|
-
this.syncHolesToDieline();
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
this.context.eventBus.on(
|
|
229
|
-
"dieline:geometry:change",
|
|
230
|
-
this.handleDielineChange,
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 2. Initial Fetch of Geometry
|
|
235
|
-
// Assuming DielineTool registered 'getGeometry' command which is now available via CommandService
|
|
236
|
-
// Since we don't have direct access to CommandService here (it was in activate),
|
|
237
|
-
// we can get it from context.services
|
|
238
|
-
const commandService = this.context.services.get<any>("CommandService");
|
|
239
|
-
if (commandService) {
|
|
240
|
-
try {
|
|
241
|
-
const geometry = commandService.executeCommand("getGeometry");
|
|
242
|
-
if (geometry) {
|
|
243
|
-
// If executeCommand returns a promise, await it?
|
|
244
|
-
// CommandService.executeCommand is async in previous definition.
|
|
245
|
-
// But here we are in sync setup.
|
|
246
|
-
// Let's assume we can handle the promise if needed, or if it returns value directly (if not async).
|
|
247
|
-
// Checking CommandService implementation: executeCommand IS async.
|
|
248
|
-
Promise.resolve(geometry).then((g) => {
|
|
249
|
-
if (g) {
|
|
250
|
-
this.currentGeometry = g as DielineGeometry;
|
|
251
|
-
// Re-run setup logic dependent on geometry
|
|
252
|
-
this.enforceConstraints();
|
|
253
|
-
this.initializeHoles();
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
} catch (e) {
|
|
258
|
-
// Command might not be ready
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// 3. Setup Canvas Interaction
|
|
263
|
-
if (!this.handleMoving) {
|
|
264
|
-
this.handleMoving = (e: any) => {
|
|
265
|
-
const target = e.target;
|
|
266
|
-
if (!target || target.data?.type !== "hole-marker") return;
|
|
267
|
-
|
|
268
|
-
if (!this.currentGeometry) return;
|
|
269
|
-
|
|
270
|
-
const index = target.data?.index ?? -1;
|
|
271
|
-
const holeData = this.holes[index];
|
|
272
|
-
|
|
273
|
-
// Calculate effective geometry based on constraint target
|
|
274
|
-
const effectiveOffset =
|
|
275
|
-
this.constraintTarget === "original"
|
|
276
|
-
? 0
|
|
277
|
-
: this.currentGeometry.offset;
|
|
278
|
-
const constraintGeometry = {
|
|
279
|
-
...this.currentGeometry,
|
|
280
|
-
width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
|
|
281
|
-
height: Math.max(
|
|
282
|
-
0,
|
|
283
|
-
this.currentGeometry.height + effectiveOffset * 2
|
|
284
|
-
),
|
|
285
|
-
radius: Math.max(0, this.currentGeometry.radius + effectiveOffset),
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const p = new Point(target.left, target.top);
|
|
289
|
-
const newPos = this.calculateConstrainedPosition(
|
|
290
|
-
p,
|
|
291
|
-
constraintGeometry,
|
|
292
|
-
holeData?.innerRadius ?? 15,
|
|
293
|
-
holeData?.outerRadius ?? 25
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
target.set({
|
|
297
|
-
left: newPos.x,
|
|
298
|
-
top: newPos.y,
|
|
299
|
-
});
|
|
300
|
-
};
|
|
301
|
-
canvas.on("object:moving", this.handleMoving);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (!this.handleModified) {
|
|
305
|
-
this.handleModified = (e: any) => {
|
|
306
|
-
const target = e.target;
|
|
307
|
-
if (!target || target.data?.type !== "hole-marker") return;
|
|
308
|
-
|
|
309
|
-
// Update state when hole is moved
|
|
310
|
-
// Ensure final position is constrained (handles case where 'modified' reports unconstrained coords)
|
|
311
|
-
const changed = this.enforceConstraints();
|
|
312
|
-
|
|
313
|
-
// If enforceConstraints changed something, it already synced.
|
|
314
|
-
// If not, we sync manually to save the move (which was valid).
|
|
315
|
-
if (!changed) {
|
|
316
|
-
this.syncHolesFromCanvas();
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
canvas.on("object:modified", this.handleModified);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
this.initializeHoles();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private initializeHoles() {
|
|
326
|
-
if (!this.canvasService) return;
|
|
327
|
-
this.redraw();
|
|
328
|
-
this.syncHolesToDieline();
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private teardown() {
|
|
332
|
-
if (!this.canvasService) return;
|
|
333
|
-
const canvas = this.canvasService.canvas;
|
|
334
|
-
|
|
335
|
-
if (this.handleMoving) {
|
|
336
|
-
canvas.off("object:moving", this.handleMoving);
|
|
337
|
-
this.handleMoving = null;
|
|
338
|
-
}
|
|
339
|
-
if (this.handleModified) {
|
|
340
|
-
canvas.off("object:modified", this.handleModified);
|
|
341
|
-
this.handleModified = null;
|
|
342
|
-
}
|
|
343
|
-
if (this.handleDielineChange && this.context) {
|
|
344
|
-
this.context.eventBus.off(
|
|
345
|
-
"dieline:geometry:change",
|
|
346
|
-
this.handleDielineChange,
|
|
347
|
-
);
|
|
348
|
-
this.handleDielineChange = null;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const objects = canvas
|
|
352
|
-
.getObjects()
|
|
353
|
-
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
354
|
-
objects.forEach((obj) => canvas.remove(obj));
|
|
355
|
-
|
|
356
|
-
// Clear holes from Dieline (visual only, state preserved in HoleTool options)
|
|
357
|
-
if (this.context) {
|
|
358
|
-
const commandService = this.context.services.get<any>("CommandService");
|
|
359
|
-
if (commandService) {
|
|
360
|
-
try {
|
|
361
|
-
commandService.executeCommand("setHoles", []);
|
|
362
|
-
} catch (e) {}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
this.canvasService.requestRenderAll();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private syncHolesFromCanvas() {
|
|
370
|
-
if (!this.canvasService) return;
|
|
371
|
-
const objects = this.canvasService.canvas
|
|
372
|
-
.getObjects()
|
|
373
|
-
.filter(
|
|
374
|
-
(obj: any) =>
|
|
375
|
-
obj.data?.type === "hole-marker" || obj.name === "hole-marker",
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
// If we have markers but no state, or mismatch, we should be careful.
|
|
379
|
-
// However, if we just dragged one, we expect them to match.
|
|
380
|
-
if (objects.length === 0 && this.holes.length > 0) {
|
|
381
|
-
console.warn("HoleTool: No markers found on canvas to sync from");
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Sort objects by index to match this.holes order
|
|
386
|
-
objects.sort(
|
|
387
|
-
(a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0),
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
// Update holes based on canvas positions
|
|
391
|
-
const newHoles = objects.map((obj, i) => {
|
|
392
|
-
const original = this.holes[i];
|
|
393
|
-
const newAbsX = obj.left!;
|
|
394
|
-
const newAbsY = obj.top!;
|
|
395
|
-
|
|
396
|
-
// Validate coordinates to prevent NaN issues
|
|
397
|
-
if (isNaN(newAbsX) || isNaN(newAbsY)) {
|
|
398
|
-
console.error("HoleTool: Invalid marker coordinates", {
|
|
399
|
-
newAbsX,
|
|
400
|
-
newAbsY,
|
|
401
|
-
});
|
|
402
|
-
return original;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Get current scale to denormalize offsets
|
|
406
|
-
const scale = this.currentGeometry?.scale || 1;
|
|
407
|
-
const unit = this.currentGeometry?.unit || "mm";
|
|
408
|
-
const unitScale = Coordinate.convertUnit(1, "mm", unit);
|
|
409
|
-
|
|
410
|
-
if (original && original.anchor && this.currentGeometry) {
|
|
411
|
-
// Reverse calculate offset from anchor
|
|
412
|
-
const { x, y, width, height } = this.currentGeometry;
|
|
413
|
-
let bx = x;
|
|
414
|
-
let by = y;
|
|
415
|
-
const left = x - width / 2;
|
|
416
|
-
const right = x + width / 2;
|
|
417
|
-
const top = y - height / 2;
|
|
418
|
-
const bottom = y + height / 2;
|
|
419
|
-
|
|
420
|
-
switch (original.anchor) {
|
|
421
|
-
case "top-left":
|
|
422
|
-
bx = left;
|
|
423
|
-
by = top;
|
|
424
|
-
break;
|
|
425
|
-
case "top-center":
|
|
426
|
-
bx = x;
|
|
427
|
-
by = top;
|
|
428
|
-
break;
|
|
429
|
-
case "top-right":
|
|
430
|
-
bx = right;
|
|
431
|
-
by = top;
|
|
432
|
-
break;
|
|
433
|
-
case "center-left":
|
|
434
|
-
bx = left;
|
|
435
|
-
by = y;
|
|
436
|
-
break;
|
|
437
|
-
case "center":
|
|
438
|
-
bx = x;
|
|
439
|
-
by = y;
|
|
440
|
-
break;
|
|
441
|
-
case "center-right":
|
|
442
|
-
bx = right;
|
|
443
|
-
by = y;
|
|
444
|
-
break;
|
|
445
|
-
case "bottom-left":
|
|
446
|
-
bx = left;
|
|
447
|
-
by = bottom;
|
|
448
|
-
break;
|
|
449
|
-
case "bottom-center":
|
|
450
|
-
bx = x;
|
|
451
|
-
by = bottom;
|
|
452
|
-
break;
|
|
453
|
-
case "bottom-right":
|
|
454
|
-
bx = right;
|
|
455
|
-
by = bottom;
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return {
|
|
460
|
-
...original,
|
|
461
|
-
// Denormalize offset back to physical units (mm)
|
|
462
|
-
offsetX: (newAbsX - bx) / scale / unitScale,
|
|
463
|
-
offsetY: (newAbsY - by) / scale / unitScale,
|
|
464
|
-
// Clear direct coordinates if we use anchor
|
|
465
|
-
x: undefined,
|
|
466
|
-
y: undefined,
|
|
467
|
-
// Ensure other properties are preserved
|
|
468
|
-
innerRadius: original.innerRadius,
|
|
469
|
-
outerRadius: original.outerRadius,
|
|
470
|
-
shape: original.shape || "circle",
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// If no anchor, use normalized coordinates relative to Dieline Geometry
|
|
475
|
-
let normalizedX = 0.5;
|
|
476
|
-
let normalizedY = 0.5;
|
|
477
|
-
|
|
478
|
-
if (this.currentGeometry) {
|
|
479
|
-
const { x, y, width, height } = this.currentGeometry;
|
|
480
|
-
const left = x - width / 2;
|
|
481
|
-
const top = y - height / 2;
|
|
482
|
-
normalizedX = width > 0 ? (newAbsX - left) / width : 0.5;
|
|
483
|
-
normalizedY = height > 0 ? (newAbsY - top) / height : 0.5;
|
|
484
|
-
} else {
|
|
485
|
-
// Fallback to Canvas normalization
|
|
486
|
-
const { width, height } = this.canvasService!.canvas;
|
|
487
|
-
normalizedX = Coordinate.toNormalized(newAbsX, width || 800);
|
|
488
|
-
normalizedY = Coordinate.toNormalized(newAbsY, height || 600);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return {
|
|
492
|
-
...original,
|
|
493
|
-
x: normalizedX,
|
|
494
|
-
y: normalizedY,
|
|
495
|
-
// Clear offsets if we are using direct normalized coordinates
|
|
496
|
-
offsetX: undefined,
|
|
497
|
-
offsetY: undefined,
|
|
498
|
-
// Ensure other properties are preserved
|
|
499
|
-
innerRadius: original?.innerRadius ?? 15,
|
|
500
|
-
outerRadius: original?.outerRadius ?? 25,
|
|
501
|
-
shape: original?.shape || "circle",
|
|
502
|
-
};
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
this.holes = newHoles;
|
|
506
|
-
this.syncHolesToDieline();
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
private syncHolesToDieline() {
|
|
510
|
-
if (!this.context || !this.canvasService) return;
|
|
511
|
-
|
|
512
|
-
const configService = this.context.services.get<ConfigurationService>(
|
|
513
|
-
"ConfigurationService"
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
if (configService) {
|
|
517
|
-
this.isUpdatingConfig = true;
|
|
518
|
-
try {
|
|
519
|
-
configService.update("dieline.holes", this.holes);
|
|
520
|
-
} finally {
|
|
521
|
-
this.isUpdatingConfig = false;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
private redraw() {
|
|
527
|
-
if (!this.canvasService) return;
|
|
528
|
-
const canvas = this.canvasService.canvas;
|
|
529
|
-
const { width, height } = canvas;
|
|
530
|
-
|
|
531
|
-
// Remove existing holes
|
|
532
|
-
const existing = canvas
|
|
533
|
-
.getObjects()
|
|
534
|
-
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
535
|
-
existing.forEach((obj) => canvas.remove(obj));
|
|
536
|
-
|
|
537
|
-
const holes = this.holes;
|
|
538
|
-
|
|
539
|
-
if (!holes || holes.length === 0) {
|
|
540
|
-
this.canvasService.requestRenderAll();
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Resolve geometry if needed for anchors
|
|
545
|
-
const geometry = this.currentGeometry || {
|
|
546
|
-
x: (width || 800) / 2,
|
|
547
|
-
y: (height || 600) / 2,
|
|
548
|
-
width: width || 800,
|
|
549
|
-
height: height || 600,
|
|
550
|
-
scale: 1, // Default scale if no geometry loaded
|
|
551
|
-
} as any;
|
|
552
|
-
|
|
553
|
-
holes.forEach((hole, index) => {
|
|
554
|
-
// Geometry scale is needed.
|
|
555
|
-
const scale = geometry.scale || 1;
|
|
556
|
-
const unit = geometry.unit || "mm";
|
|
557
|
-
const unitScale = Coordinate.convertUnit(1, 'mm', unit);
|
|
558
|
-
|
|
559
|
-
const visualInnerRadius = hole.innerRadius * unitScale * scale;
|
|
560
|
-
const visualOuterRadius = hole.outerRadius * unitScale * scale;
|
|
561
|
-
|
|
562
|
-
// Resolve position
|
|
563
|
-
// Apply unit conversion and scale to offsets before resolving (mm -> px)
|
|
564
|
-
const pos = resolveHolePosition(
|
|
565
|
-
{
|
|
566
|
-
...hole,
|
|
567
|
-
offsetX: (hole.offsetX || 0) * unitScale * scale,
|
|
568
|
-
offsetY: (hole.offsetY || 0) * unitScale * scale,
|
|
569
|
-
},
|
|
570
|
-
geometry,
|
|
571
|
-
{ width: geometry.width, height: geometry.height } // Use geometry dims instead of canvas
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
const isSquare = hole.shape === "square";
|
|
575
|
-
|
|
576
|
-
const innerMarker = isSquare
|
|
577
|
-
? new Rect({
|
|
578
|
-
width: visualInnerRadius * 2,
|
|
579
|
-
height: visualInnerRadius * 2,
|
|
580
|
-
fill: "transparent",
|
|
581
|
-
stroke: "red",
|
|
582
|
-
strokeWidth: 2,
|
|
583
|
-
originX: "center",
|
|
584
|
-
originY: "center",
|
|
585
|
-
})
|
|
586
|
-
: new Circle({
|
|
587
|
-
radius: visualInnerRadius,
|
|
588
|
-
fill: "transparent",
|
|
589
|
-
stroke: "red",
|
|
590
|
-
strokeWidth: 2,
|
|
591
|
-
originX: "center",
|
|
592
|
-
originY: "center",
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
const outerMarker = isSquare
|
|
596
|
-
? new Rect({
|
|
597
|
-
width: visualOuterRadius * 2,
|
|
598
|
-
height: visualOuterRadius * 2,
|
|
599
|
-
fill: "transparent",
|
|
600
|
-
stroke: "#666",
|
|
601
|
-
strokeWidth: 1,
|
|
602
|
-
strokeDashArray: [5, 5],
|
|
603
|
-
originX: "center",
|
|
604
|
-
originY: "center",
|
|
605
|
-
})
|
|
606
|
-
: new Circle({
|
|
607
|
-
radius: visualOuterRadius,
|
|
608
|
-
fill: "transparent",
|
|
609
|
-
stroke: "#666",
|
|
610
|
-
strokeWidth: 1,
|
|
611
|
-
strokeDashArray: [5, 5],
|
|
612
|
-
originX: "center",
|
|
613
|
-
originY: "center",
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
const holeGroup = new Group([outerMarker, innerMarker], {
|
|
617
|
-
left: pos.x,
|
|
618
|
-
top: pos.y,
|
|
619
|
-
originX: "center",
|
|
620
|
-
originY: "center",
|
|
621
|
-
selectable: true,
|
|
622
|
-
hasControls: false, // Don't allow resizing/rotating
|
|
623
|
-
hasBorders: false,
|
|
624
|
-
subTargetCheck: false,
|
|
625
|
-
opacity: 0, // Default hidden
|
|
626
|
-
hoverCursor: "move",
|
|
627
|
-
data: { type: "hole-marker", index },
|
|
628
|
-
} as any);
|
|
629
|
-
(holeGroup as any).name = "hole-marker";
|
|
630
|
-
|
|
631
|
-
// Auto-show/hide logic
|
|
632
|
-
holeGroup.on("mouseover", () => {
|
|
633
|
-
holeGroup.set("opacity", 1);
|
|
634
|
-
canvas.requestRenderAll();
|
|
635
|
-
});
|
|
636
|
-
holeGroup.on("mouseout", () => {
|
|
637
|
-
if (canvas.getActiveObject() !== holeGroup) {
|
|
638
|
-
holeGroup.set("opacity", 0);
|
|
639
|
-
canvas.requestRenderAll();
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
holeGroup.on("selected", () => {
|
|
643
|
-
holeGroup.set("opacity", 1);
|
|
644
|
-
canvas.requestRenderAll();
|
|
645
|
-
});
|
|
646
|
-
holeGroup.on("deselected", () => {
|
|
647
|
-
holeGroup.set("opacity", 0);
|
|
648
|
-
canvas.requestRenderAll();
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
canvas.add(holeGroup);
|
|
652
|
-
canvas.bringObjectToFront(holeGroup);
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
// Also bring all existing markers to front to be safe
|
|
656
|
-
const markers = canvas.getObjects().filter((o: any) => o.data?.type === "hole-marker");
|
|
657
|
-
markers.forEach(m => canvas.bringObjectToFront(m));
|
|
658
|
-
|
|
659
|
-
this.canvasService.requestRenderAll();
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
public enforceConstraints(): boolean {
|
|
663
|
-
const geometry = this.currentGeometry;
|
|
664
|
-
if (!geometry || !this.canvasService) {
|
|
665
|
-
return false;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const effectiveOffset =
|
|
669
|
-
this.constraintTarget === "original" ? 0 : geometry.offset;
|
|
670
|
-
const constraintGeometry = {
|
|
671
|
-
...geometry,
|
|
672
|
-
width: Math.max(0, geometry.width + effectiveOffset * 2),
|
|
673
|
-
height: Math.max(0, geometry.height + effectiveOffset * 2),
|
|
674
|
-
radius: Math.max(0, geometry.radius + effectiveOffset),
|
|
675
|
-
};
|
|
676
|
-
|
|
677
|
-
// Get all hole markers
|
|
678
|
-
const objects = this.canvasService.canvas
|
|
679
|
-
.getObjects()
|
|
680
|
-
.filter((obj: any) => obj.data?.type === "hole-marker");
|
|
681
|
-
|
|
682
|
-
let changed = false;
|
|
683
|
-
// Sort objects by index to maintain order in options.holes
|
|
684
|
-
objects.sort(
|
|
685
|
-
(a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0)
|
|
686
|
-
);
|
|
687
|
-
|
|
688
|
-
const newHoles: HoleData[] = [];
|
|
689
|
-
|
|
690
|
-
objects.forEach((obj: any, i: number) => {
|
|
691
|
-
const currentPos = new Point(obj.left, obj.top);
|
|
692
|
-
// We need to pass the hole's radii to calculateConstrainedPosition
|
|
693
|
-
const holeData = this.holes[i];
|
|
694
|
-
|
|
695
|
-
// Scale radii for constraint calculation (since geometry is in pixels)
|
|
696
|
-
// Geometry scale is needed.
|
|
697
|
-
const scale = geometry.scale || 1;
|
|
698
|
-
const unit = geometry.unit || "mm";
|
|
699
|
-
const unitScale = Coordinate.convertUnit(1, 'mm', unit);
|
|
700
|
-
|
|
701
|
-
const innerR = (holeData?.innerRadius ?? 15) * unitScale * scale;
|
|
702
|
-
const outerR = (holeData?.outerRadius ?? 25) * unitScale * scale;
|
|
703
|
-
|
|
704
|
-
const newPos = this.calculateConstrainedPosition(
|
|
705
|
-
currentPos,
|
|
706
|
-
constraintGeometry,
|
|
707
|
-
innerR,
|
|
708
|
-
outerR
|
|
709
|
-
);
|
|
710
|
-
|
|
711
|
-
if (currentPos.distanceFrom(newPos) > 0.1) {
|
|
712
|
-
obj.set({
|
|
713
|
-
left: newPos.x,
|
|
714
|
-
top: newPos.y,
|
|
715
|
-
});
|
|
716
|
-
obj.setCoords();
|
|
717
|
-
changed = true;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Update data logic is handled in syncHolesFromCanvas which is called on modified
|
|
721
|
-
// But here we are modifying programmatically.
|
|
722
|
-
// We should probably just let the visual update happen, and then sync?
|
|
723
|
-
// Or just push to newHoles list to verify change?
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
if (changed) {
|
|
727
|
-
// If we moved things programmatically, we should update the state
|
|
728
|
-
this.syncHolesFromCanvas();
|
|
729
|
-
return true;
|
|
730
|
-
}
|
|
731
|
-
return false;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
private calculateConstrainedPosition(
|
|
735
|
-
p: Point,
|
|
736
|
-
g: DielineGeometry,
|
|
737
|
-
innerRadius: number,
|
|
738
|
-
outerRadius: number
|
|
739
|
-
): Point {
|
|
740
|
-
// Use Paper.js to get accurate nearest point
|
|
741
|
-
const options = {
|
|
742
|
-
...g,
|
|
743
|
-
holes: [], // We don't need holes for boundary calculation
|
|
744
|
-
};
|
|
745
|
-
|
|
746
|
-
const nearest = getNearestPointOnDieline(
|
|
747
|
-
{ x: p.x, y: p.y },
|
|
748
|
-
options as any,
|
|
749
|
-
);
|
|
750
|
-
|
|
751
|
-
// Now constrain distance
|
|
752
|
-
const nearestP = new Point(nearest.x, nearest.y);
|
|
753
|
-
const dist = p.distanceFrom(nearestP);
|
|
754
|
-
|
|
755
|
-
// Vector from nearest to current point
|
|
756
|
-
const v = p.subtract(nearestP);
|
|
757
|
-
|
|
758
|
-
// Vector from center to nearest point (approximate normal for convex shapes)
|
|
759
|
-
const center = new Point(g.x, g.y);
|
|
760
|
-
|
|
761
|
-
const distToCenter = p.distanceFrom(center);
|
|
762
|
-
const nearestDistToCenter = nearestP.distanceFrom(center);
|
|
763
|
-
|
|
764
|
-
let signedDist = dist;
|
|
765
|
-
if (distToCenter < nearestDistToCenter) {
|
|
766
|
-
signedDist = -dist; // Inside
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// Clamp distance
|
|
770
|
-
let clampedDist = signedDist;
|
|
771
|
-
if (signedDist > 0) {
|
|
772
|
-
clampedDist = Math.min(signedDist, innerRadius);
|
|
773
|
-
} else {
|
|
774
|
-
clampedDist = Math.max(signedDist, -outerRadius);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Reconstruct point
|
|
778
|
-
if (dist < 0.001) return nearestP;
|
|
779
|
-
|
|
780
|
-
// We want the result to lie on the line connecting Nearest -> P
|
|
781
|
-
const scale = Math.abs(clampedDist) / (dist || 1);
|
|
782
|
-
const offset = v.scalarMultiply(scale);
|
|
783
|
-
|
|
784
|
-
return nearestP.add(offset);
|
|
785
|
-
}
|
|
786
|
-
}
|