@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/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
- // We assume x/y are normalized to canvas size if no anchor is present
87
- // Or should we support absolute?
88
- // Current system uses normalized.
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 * canvasSize.width,
92
- y: hole.y * canvasSize.height,
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 = new paper.Path.Circle({
293
- center: [hole.x, hole.y],
294
- radius: hole.outerRadius,
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 = new paper.Path.Circle({
303
- center: [hole.x, hole.y],
304
- radius: hole.innerRadius,
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("Geometry: Failed to subtract cutsPath from mainShape", e);
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
- ensurePaper(options.width * 2, options.height * 2);
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 maxDim = Math.max(options.width, options.height) + Math.abs(offset) * 4;
425
- ensurePaper(maxDim, maxDim);
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 { width: bounds.width, height: bounds.height };
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
- const normalizedHole = Coordinate.normalizePoint(
158
- { x, y },
159
- { width: width || 800, height: height || 600 }
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: normalizedHole.x,
175
- y: normalizedHole.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
- this.syncHolesFromCanvas();
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((obj: any) => obj.data?.type === "hole-marker");
373
+ .filter(
374
+ (obj: any) =>
375
+ obj.data?.type === "hole-marker" || obj.name === "hole-marker",
376
+ );
354
377
 
355
- // Sort objects by index
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
- offsetX: newAbsX - bx,
420
- offsetY: newAbsY - by,
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
- const { width, height } = this.canvasService!.canvas;
429
- const p = Coordinate.normalizePoint(
430
- { x: newAbsX, y: newAbsY },
431
- { width: width || 800, height: height || 600 }
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: p.x,
437
- y: p.y,
438
- // Ensure radii are preserved
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
- hole,
565
+ {
566
+ ...hole,
567
+ offsetX: (hole.offsetX || 0) * unitScale * scale,
568
+ offsetY: (hole.offsetY || 0) * unitScale * scale,
569
+ },
495
570
  geometry,
496
- { width: width || 800, height: height || 600 }
571
+ { width: geometry.width, height: geometry.height } // Use geometry dims instead of canvas
497
572
  );
498
573
 
499
- const innerCircle = new Circle({
500
- radius: hole.innerRadius,
501
- fill: "transparent",
502
- stroke: "red",
503
- strokeWidth: 2,
504
- originX: "center",
505
- originY: "center",
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 outerCircle = new Circle({
509
- radius: hole.outerRadius,
510
- fill: "transparent",
511
- stroke: "#666",
512
- strokeWidth: 1,
513
- strokeDashArray: [5, 5],
514
- originX: "center",
515
- originY: "center",
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([outerCircle, innerCircle], {
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
- const holeData = this.holes[i];
596
-
597
- const newPos = this.calculateConstrainedPosition(
598
- currentPos,
599
- constraintGeometry,
600
- holeData?.innerRadius ?? 15,
601
- holeData?.outerRadius ?? 25
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({