@pooder/kit 3.1.0 → 3.3.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 +17 -0
- package/dist/index.d.mts +40 -24
- package/dist/index.d.ts +40 -24
- package/dist/index.js +849 -557
- package/dist/index.mjs +854 -562
- package/package.json +2 -2
- package/src/coordinate.ts +57 -0
- package/src/dieline.ts +196 -129
- package/src/geometry.ts +56 -21
- package/src/hole.ts +163 -56
- package/src/image.ts +355 -363
- package/src/ruler.ts +295 -120
package/src/geometry.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type PositionAnchor =
|
|
|
14
14
|
export interface HoleData {
|
|
15
15
|
x?: number;
|
|
16
16
|
y?: number;
|
|
17
|
+
shape?: "circle" | "square";
|
|
17
18
|
anchor?: PositionAnchor;
|
|
18
19
|
offsetX?: number;
|
|
19
20
|
offsetY?: number;
|
|
@@ -24,7 +25,7 @@ export interface HoleData {
|
|
|
24
25
|
export function resolveHolePosition(
|
|
25
26
|
hole: HoleData,
|
|
26
27
|
geometry: { x: number; y: number; width: number; height: number },
|
|
27
|
-
canvasSize: { width: number; height: number }
|
|
28
|
+
canvasSize: { width: number; height: number },
|
|
28
29
|
): { x: number; y: number } {
|
|
29
30
|
if (hole.anchor) {
|
|
30
31
|
const { x, y, width, height } = geometry;
|
|
@@ -82,14 +83,13 @@ export function resolveHolePosition(
|
|
|
82
83
|
y: by + (hole.offsetY || 0),
|
|
83
84
|
};
|
|
84
85
|
} else if (hole.x !== undefined && hole.y !== undefined) {
|
|
85
|
-
// Legacy / Direct coordinates (Normalized)
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
// Coordinate.denormalizePoint logic:
|
|
86
|
+
// Legacy / Direct coordinates (Normalized relative to Dieline Geometry)
|
|
87
|
+
// Formula: absolute = normalized * width + (center - width/2)
|
|
88
|
+
// This handles padding correctly.
|
|
89
|
+
const { x, width, y, height } = geometry;
|
|
90
90
|
return {
|
|
91
|
-
x: hole.x *
|
|
92
|
-
y: hole.y *
|
|
91
|
+
x: hole.x * width + (x - width / 2) + (hole.offsetX || 0),
|
|
92
|
+
y: hole.y * height + (y - height / 2) + (hole.offsetY || 0),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
return { x: 0, y: 0 };
|
|
@@ -104,6 +104,8 @@ export interface GeometryOptions {
|
|
|
104
104
|
y: number;
|
|
105
105
|
holes: Array<HoleData>;
|
|
106
106
|
pathData?: string;
|
|
107
|
+
canvasWidth?: number;
|
|
108
|
+
canvasHeight?: number;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
export interface MaskGeometryOptions extends GeometryOptions {
|
|
@@ -288,21 +290,41 @@ function getDielineShape(options: GeometryOptions): paper.PathItem {
|
|
|
288
290
|
let cutsPath: paper.PathItem | null = null;
|
|
289
291
|
|
|
290
292
|
holes.forEach((hole) => {
|
|
293
|
+
const center = new paper.Point(hole.x!, hole.y!);
|
|
294
|
+
|
|
291
295
|
// Create Lug (Outer Radius)
|
|
292
|
-
const lug =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
+
const lug =
|
|
297
|
+
hole.shape === "square"
|
|
298
|
+
? new paper.Path.Rectangle({
|
|
299
|
+
point: [
|
|
300
|
+
center.x - hole.outerRadius,
|
|
301
|
+
center.y - hole.outerRadius,
|
|
302
|
+
],
|
|
303
|
+
size: [hole.outerRadius * 2, hole.outerRadius * 2],
|
|
304
|
+
})
|
|
305
|
+
: new paper.Path.Circle({
|
|
306
|
+
center: center,
|
|
307
|
+
radius: hole.outerRadius,
|
|
308
|
+
});
|
|
296
309
|
|
|
297
310
|
// REMOVED: Intersects check. We want to process all holes defined in config.
|
|
298
311
|
// If a hole is completely outside, it might form an island, but that's better than missing it.
|
|
299
312
|
// Users can remove the hole if they don't want it.
|
|
300
313
|
|
|
301
314
|
// Create Cut (Inner Radius)
|
|
302
|
-
const cut =
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
315
|
+
const cut =
|
|
316
|
+
hole.shape === "square"
|
|
317
|
+
? new paper.Path.Rectangle({
|
|
318
|
+
point: [
|
|
319
|
+
center.x - hole.innerRadius,
|
|
320
|
+
center.y - hole.innerRadius,
|
|
321
|
+
],
|
|
322
|
+
size: [hole.innerRadius * 2, hole.innerRadius * 2],
|
|
323
|
+
})
|
|
324
|
+
: new paper.Path.Circle({
|
|
325
|
+
center: center,
|
|
326
|
+
radius: hole.innerRadius,
|
|
327
|
+
});
|
|
306
328
|
|
|
307
329
|
// Union Lugs
|
|
308
330
|
if (!lugsPath) {
|
|
@@ -358,7 +380,10 @@ function getDielineShape(options: GeometryOptions): paper.PathItem {
|
|
|
358
380
|
cutsPath.remove();
|
|
359
381
|
mainShape = temp;
|
|
360
382
|
} catch (e) {
|
|
361
|
-
console.error(
|
|
383
|
+
console.error(
|
|
384
|
+
"Geometry: Failed to subtract cutsPath from mainShape",
|
|
385
|
+
e,
|
|
386
|
+
);
|
|
362
387
|
}
|
|
363
388
|
}
|
|
364
389
|
}
|
|
@@ -371,7 +396,9 @@ function getDielineShape(options: GeometryOptions): paper.PathItem {
|
|
|
371
396
|
* Logic: (BaseShape UNION IntersectingLugs) SUBTRACT Cuts
|
|
372
397
|
*/
|
|
373
398
|
export function generateDielinePath(options: GeometryOptions): string {
|
|
374
|
-
|
|
399
|
+
const paperWidth = options.canvasWidth || options.width * 2 || 2000;
|
|
400
|
+
const paperHeight = options.canvasHeight || options.height * 2 || 2000;
|
|
401
|
+
ensurePaper(paperWidth, paperHeight);
|
|
375
402
|
paper.project.activeLayer.removeChildren();
|
|
376
403
|
|
|
377
404
|
const mainShape = getDielineShape(options);
|
|
@@ -421,8 +448,9 @@ export function generateBleedZonePath(
|
|
|
421
448
|
offset: number,
|
|
422
449
|
): string {
|
|
423
450
|
// Ensure canvas is large enough
|
|
424
|
-
const
|
|
425
|
-
|
|
451
|
+
const paperWidth = options.canvasWidth || options.width * 2 || 2000;
|
|
452
|
+
const paperHeight = options.canvasHeight || options.height * 2 || 2000;
|
|
453
|
+
ensurePaper(paperWidth, paperHeight);
|
|
426
454
|
paper.project.activeLayer.removeChildren();
|
|
427
455
|
|
|
428
456
|
// 1. Original Shape
|
|
@@ -541,6 +569,8 @@ export function getNearestPointOnDieline(
|
|
|
541
569
|
}
|
|
542
570
|
|
|
543
571
|
export function getPathBounds(pathData: string): {
|
|
572
|
+
x: number;
|
|
573
|
+
y: number;
|
|
544
574
|
width: number;
|
|
545
575
|
height: number;
|
|
546
576
|
} {
|
|
@@ -548,5 +578,10 @@ export function getPathBounds(pathData: string): {
|
|
|
548
578
|
path.pathData = pathData;
|
|
549
579
|
const bounds = path.bounds;
|
|
550
580
|
path.remove();
|
|
551
|
-
return {
|
|
581
|
+
return {
|
|
582
|
+
x: bounds.x,
|
|
583
|
+
y: bounds.y,
|
|
584
|
+
width: bounds.width,
|
|
585
|
+
height: bounds.height,
|
|
586
|
+
};
|
|
552
587
|
}
|
package/src/hole.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
ConfigurationContribution,
|
|
7
7
|
ConfigurationService,
|
|
8
8
|
} from "@pooder/core";
|
|
9
|
-
import { Circle, Group, Point } from "fabric";
|
|
9
|
+
import { Circle, Group, Point, Rect } from "fabric";
|
|
10
10
|
import CanvasService from "./CanvasService";
|
|
11
11
|
import { DielineGeometry } from "./dieline";
|
|
12
12
|
import {
|
|
@@ -152,12 +152,22 @@ export class HoleTool implements Extension {
|
|
|
152
152
|
title: "Add Hole",
|
|
153
153
|
handler: (x: number, y: number) => {
|
|
154
154
|
if (!this.canvasService) return false;
|
|
155
|
-
const { width, height } = this.canvasService.canvas;
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|
|
161
171
|
|
|
162
172
|
const configService = this.context?.services.get<ConfigurationService>(
|
|
163
173
|
"ConfigurationService"
|
|
@@ -169,10 +179,12 @@ export class HoleTool implements Extension {
|
|
|
169
179
|
const lastHole = currentHoles[currentHoles.length - 1];
|
|
170
180
|
const innerRadius = lastHole?.innerRadius ?? 15;
|
|
171
181
|
const outerRadius = lastHole?.outerRadius ?? 25;
|
|
182
|
+
const shape = lastHole?.shape ?? "circle";
|
|
172
183
|
|
|
173
184
|
const newHole = {
|
|
174
|
-
x:
|
|
175
|
-
y:
|
|
185
|
+
x: normalizedX,
|
|
186
|
+
y: normalizedY,
|
|
187
|
+
shape,
|
|
176
188
|
innerRadius,
|
|
177
189
|
outerRadius,
|
|
178
190
|
};
|
|
@@ -206,6 +218,7 @@ export class HoleTool implements Extension {
|
|
|
206
218
|
if (!this.handleDielineChange) {
|
|
207
219
|
this.handleDielineChange = (geometry: DielineGeometry) => {
|
|
208
220
|
this.currentGeometry = geometry;
|
|
221
|
+
this.redraw();
|
|
209
222
|
const changed = this.enforceConstraints();
|
|
210
223
|
// Only sync if constraints actually moved something
|
|
211
224
|
if (changed) {
|
|
@@ -294,7 +307,14 @@ export class HoleTool implements Extension {
|
|
|
294
307
|
if (!target || target.data?.type !== "hole-marker") return;
|
|
295
308
|
|
|
296
309
|
// Update state when hole is moved
|
|
297
|
-
|
|
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
|
+
}
|
|
298
318
|
};
|
|
299
319
|
canvas.on("object:modified", this.handleModified);
|
|
300
320
|
}
|
|
@@ -350,21 +370,43 @@ export class HoleTool implements Extension {
|
|
|
350
370
|
if (!this.canvasService) return;
|
|
351
371
|
const objects = this.canvasService.canvas
|
|
352
372
|
.getObjects()
|
|
353
|
-
.filter(
|
|
373
|
+
.filter(
|
|
374
|
+
(obj: any) =>
|
|
375
|
+
obj.data?.type === "hole-marker" || obj.name === "hole-marker",
|
|
376
|
+
);
|
|
354
377
|
|
|
355
|
-
//
|
|
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
|
|
356
386
|
objects.sort(
|
|
357
|
-
(a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0)
|
|
387
|
+
(a: any, b: any) => (a.data?.index ?? 0) - (b.data?.index ?? 0),
|
|
358
388
|
);
|
|
359
389
|
|
|
360
390
|
// Update holes based on canvas positions
|
|
361
|
-
// We need to preserve original hole properties (radii, anchor)
|
|
362
|
-
// If a hole has an anchor, we update offsetX/Y instead of x/y
|
|
363
391
|
const newHoles = objects.map((obj, i) => {
|
|
364
392
|
const original = this.holes[i];
|
|
365
393
|
const newAbsX = obj.left!;
|
|
366
394
|
const newAbsY = obj.top!;
|
|
367
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
|
+
|
|
368
410
|
if (original && original.anchor && this.currentGeometry) {
|
|
369
411
|
// Reverse calculate offset from anchor
|
|
370
412
|
const { x, y, width, height } = this.currentGeometry;
|
|
@@ -413,31 +455,50 @@ export class HoleTool implements Extension {
|
|
|
413
455
|
by = bottom;
|
|
414
456
|
break;
|
|
415
457
|
}
|
|
416
|
-
|
|
458
|
+
|
|
417
459
|
return {
|
|
418
460
|
...original,
|
|
419
|
-
|
|
420
|
-
|
|
461
|
+
// Denormalize offset back to physical units (mm)
|
|
462
|
+
offsetX: (newAbsX - bx) / scale / unitScale,
|
|
463
|
+
offsetY: (newAbsY - by) / scale / unitScale,
|
|
421
464
|
// Clear direct coordinates if we use anchor
|
|
422
465
|
x: undefined,
|
|
423
466
|
y: undefined,
|
|
467
|
+
// Ensure other properties are preserved
|
|
468
|
+
innerRadius: original.innerRadius,
|
|
469
|
+
outerRadius: original.outerRadius,
|
|
470
|
+
shape: original.shape || "circle",
|
|
424
471
|
};
|
|
425
472
|
}
|
|
426
473
|
|
|
427
|
-
// If no anchor, use normalized coordinates
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
|
|
434
491
|
return {
|
|
435
492
|
...original,
|
|
436
|
-
x:
|
|
437
|
-
y:
|
|
438
|
-
//
|
|
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
|
|
439
499
|
innerRadius: original?.innerRadius ?? 15,
|
|
440
500
|
outerRadius: original?.outerRadius ?? 25,
|
|
501
|
+
shape: original?.shape || "circle",
|
|
441
502
|
};
|
|
442
503
|
});
|
|
443
504
|
|
|
@@ -486,36 +547,73 @@ export class HoleTool implements Extension {
|
|
|
486
547
|
y: (height || 600) / 2,
|
|
487
548
|
width: width || 800,
|
|
488
549
|
height: height || 600,
|
|
489
|
-
|
|
550
|
+
scale: 1, // Default scale if no geometry loaded
|
|
551
|
+
} as any;
|
|
490
552
|
|
|
491
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
|
+
|
|
492
562
|
// Resolve position
|
|
563
|
+
// Apply unit conversion and scale to offsets before resolving (mm -> px)
|
|
493
564
|
const pos = resolveHolePosition(
|
|
494
|
-
|
|
565
|
+
{
|
|
566
|
+
...hole,
|
|
567
|
+
offsetX: (hole.offsetX || 0) * unitScale * scale,
|
|
568
|
+
offsetY: (hole.offsetY || 0) * unitScale * scale,
|
|
569
|
+
},
|
|
495
570
|
geometry,
|
|
496
|
-
{ width: width
|
|
571
|
+
{ width: geometry.width, height: geometry.height } // Use geometry dims instead of canvas
|
|
497
572
|
);
|
|
498
573
|
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
+
});
|
|
507
594
|
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
});
|
|
517
615
|
|
|
518
|
-
const holeGroup = new Group([
|
|
616
|
+
const holeGroup = new Group([outerMarker, innerMarker], {
|
|
519
617
|
left: pos.x,
|
|
520
618
|
top: pos.y,
|
|
521
619
|
originX: "center",
|
|
@@ -592,14 +690,23 @@ export class HoleTool implements Extension {
|
|
|
592
690
|
objects.forEach((obj: any, i: number) => {
|
|
593
691
|
const currentPos = new Point(obj.left, obj.top);
|
|
594
692
|
// We need to pass the hole's radii to calculateConstrainedPosition
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
);
|
|
603
710
|
|
|
604
711
|
if (currentPos.distanceFrom(newPos) > 0.1) {
|
|
605
712
|
obj.set({
|