@pooder/kit 3.5.0 → 4.1.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.
@@ -0,0 +1,158 @@
1
+ import { DielineFeature } from "./geometry";
2
+
3
+ export interface ConstraintContext {
4
+ dielineWidth: number;
5
+ dielineHeight: number;
6
+ }
7
+
8
+ export type ConstraintHandler = (
9
+ x: number,
10
+ y: number,
11
+ feature: DielineFeature,
12
+ context: ConstraintContext
13
+ ) => { x: number; y: number };
14
+
15
+ export class ConstraintRegistry {
16
+ private static handlers = new Map<string, ConstraintHandler>();
17
+
18
+ static register(type: string, handler: ConstraintHandler) {
19
+ this.handlers.set(type, handler);
20
+ }
21
+
22
+ static apply(
23
+ x: number,
24
+ y: number,
25
+ feature: DielineFeature,
26
+ context: ConstraintContext
27
+ ): { x: number; y: number } {
28
+ if (!feature.constraints || !feature.constraints.type) {
29
+ return { x, y };
30
+ }
31
+
32
+ const handler = this.handlers.get(feature.constraints.type);
33
+ if (handler) {
34
+ return handler(x, y, feature, context);
35
+ }
36
+
37
+ return { x, y };
38
+ }
39
+ }
40
+
41
+ // --- Built-in Strategies ---
42
+
43
+ /**
44
+ * Edge Constraint Strategy
45
+ * Snaps the feature to the nearest allowed edge.
46
+ * Params:
47
+ * - allowedEdges: ('top' | 'bottom' | 'left' | 'right')[] (default: all)
48
+ * - confine: boolean (default: false) - if true, keeps feature within edge length
49
+ * - offset: number (default: 0) - physical offset from edge (positive = inwards usually, but here 0 is edge)
50
+ * For simplicity, let's say offset is additive to the edge position.
51
+ * Top: 0 + offset
52
+ * Bottom: 1 - offset
53
+ * Left: 0 + offset
54
+ * Right: 1 - offset
55
+ */
56
+ const edgeConstraint: ConstraintHandler = (x, y, feature, context) => {
57
+ const { dielineWidth, dielineHeight } = context;
58
+ const params = feature.constraints?.params || {};
59
+ const allowedEdges = params.allowedEdges || [
60
+ "top",
61
+ "bottom",
62
+ "left",
63
+ "right",
64
+ ];
65
+ const confine = params.confine || false;
66
+ const offset = params.offset || 0;
67
+
68
+ // Calculate physical distances to allowed edges
69
+ const distances: { edge: string; dist: number }[] = [];
70
+
71
+ if (allowedEdges.includes("top"))
72
+ distances.push({ edge: "top", dist: y * dielineHeight });
73
+ if (allowedEdges.includes("bottom"))
74
+ distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
75
+ if (allowedEdges.includes("left"))
76
+ distances.push({ edge: "left", dist: x * dielineWidth });
77
+ if (allowedEdges.includes("right"))
78
+ distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
79
+
80
+ if (distances.length === 0) return { x, y };
81
+
82
+ // Find nearest
83
+ distances.sort((a, b) => a.dist - b.dist);
84
+ const nearest = distances[0].edge;
85
+
86
+ let newX = x;
87
+ let newY = y;
88
+ const fw = feature.width || 0;
89
+ const fh = feature.height || 0;
90
+
91
+ // Snap to edge
92
+ switch (nearest) {
93
+ case "top":
94
+ newY = 0 + offset / dielineHeight;
95
+ if (confine) {
96
+ const minX = (fw / 2) / dielineWidth;
97
+ const maxX = 1 - minX;
98
+ newX = Math.max(minX, Math.min(newX, maxX));
99
+ }
100
+ break;
101
+ case "bottom":
102
+ newY = 1 - offset / dielineHeight;
103
+ if (confine) {
104
+ const minX = (fw / 2) / dielineWidth;
105
+ const maxX = 1 - minX;
106
+ newX = Math.max(minX, Math.min(newX, maxX));
107
+ }
108
+ break;
109
+ case "left":
110
+ newX = 0 + offset / dielineWidth;
111
+ if (confine) {
112
+ const minY = (fh / 2) / dielineHeight;
113
+ const maxY = 1 - minY;
114
+ newY = Math.max(minY, Math.min(newY, maxY));
115
+ }
116
+ break;
117
+ case "right":
118
+ newX = 1 - offset / dielineWidth;
119
+ if (confine) {
120
+ const minY = (fh / 2) / dielineHeight;
121
+ const maxY = 1 - minY;
122
+ newY = Math.max(minY, Math.min(newY, maxY));
123
+ }
124
+ break;
125
+ }
126
+
127
+ return { x: newX, y: newY };
128
+ };
129
+
130
+ /**
131
+ * Internal Constraint Strategy
132
+ * Keeps the feature strictly inside the dieline bounds with optional margin.
133
+ * Params:
134
+ * - margin: number (default: 0) - physical margin
135
+ */
136
+ const internalConstraint: ConstraintHandler = (x, y, feature, context) => {
137
+ const { dielineWidth, dielineHeight } = context;
138
+ const params = feature.constraints?.params || {};
139
+ const margin = params.margin || 0;
140
+ const fw = feature.width || 0;
141
+ const fh = feature.height || 0;
142
+
143
+ const minX = (margin + fw / 2) / dielineWidth;
144
+ const maxX = 1 - (margin + fw / 2) / dielineWidth;
145
+
146
+ const minY = (margin + fh / 2) / dielineHeight;
147
+ const maxY = 1 - (margin + fh / 2) / dielineHeight;
148
+
149
+ // Handle case where feature is larger than container
150
+ const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
151
+ const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
152
+
153
+ return { x: clampedX, y: clampedY };
154
+ };
155
+
156
+ // Register built-ins
157
+ ConstraintRegistry.register("edge", edgeConstraint);
158
+ ConstraintRegistry.register("internal", internalConstraint);
package/src/coordinate.ts CHANGED
@@ -1,106 +1,106 @@
1
- export interface Point {
2
- x: number;
3
- y: number;
4
- }
5
-
6
- export interface Size {
7
- width: number;
8
- height: number;
9
- }
10
-
11
- export type Unit = "px" | "mm" | "cm" | "in";
12
-
13
- export interface Layout {
14
- scale: number;
15
- offsetX: number;
16
- offsetY: number;
17
- width: number;
18
- height: number;
19
- }
20
-
21
- export class Coordinate {
22
- /**
23
- * Calculate layout to fit content within container while preserving aspect ratio.
24
- */
25
- static calculateLayout(
26
- container: Size,
27
- content: Size,
28
- padding: number = 0,
29
- ): Layout {
30
- const availableWidth = Math.max(0, container.width - padding * 2);
31
- const availableHeight = Math.max(0, container.height - padding * 2);
32
-
33
- if (content.width === 0 || content.height === 0) {
34
- return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
35
- }
36
-
37
- const scaleX = availableWidth / content.width;
38
- const scaleY = availableHeight / content.height;
39
- const scale = Math.min(scaleX, scaleY);
40
-
41
- const width = content.width * scale;
42
- const height = content.height * scale;
43
-
44
- const offsetX = (container.width - width) / 2;
45
- const offsetY = (container.height - height) / 2;
46
-
47
- return { scale, offsetX, offsetY, width, height };
48
- }
49
-
50
- /**
51
- * Convert an absolute value to a normalized value (0-1).
52
- * @param value Absolute value (e.g., pixels)
53
- * @param total Total dimension size (e.g., canvas width)
54
- */
55
- static toNormalized(value: number, total: number): number {
56
- return total === 0 ? 0 : value / total;
57
- }
58
-
59
- /**
60
- * Convert a normalized value (0-1) to an absolute value.
61
- * @param normalized Normalized value (0-1)
62
- * @param total Total dimension size (e.g., canvas width)
63
- */
64
- static toAbsolute(normalized: number, total: number): number {
65
- return normalized * total;
66
- }
67
-
68
- /**
69
- * Normalize a point's coordinates.
70
- */
71
- static normalizePoint(point: Point, size: Size): Point {
72
- return {
73
- x: this.toNormalized(point.x, size.width),
74
- y: this.toNormalized(point.y, size.height),
75
- };
76
- }
77
-
78
- /**
79
- * Denormalize a point's coordinates to absolute pixels.
80
- */
81
- static denormalizePoint(point: Point, size: Size): Point {
82
- return {
83
- x: this.toAbsolute(point.x, size.width),
84
- y: this.toAbsolute(point.y, size.height),
85
- };
86
- }
87
-
88
- static convertUnit(value: number, from: Unit, to: Unit): number {
89
- if (from === to) return value;
90
-
91
- // Base unit: mm
92
- const toMM: Record<Unit, number> = {
93
- px: 0.264583, // 1px = 0.264583mm (96 DPI)
94
- mm: 1,
95
- cm: 10,
96
- in: 25.4
97
- };
98
-
99
- const mmValue = value * (from === 'px' ? toMM.px : toMM[from] || 1);
100
-
101
- if (to === 'px') {
102
- return mmValue / toMM.px;
103
- }
104
- return mmValue / (toMM[to] || 1);
105
- }
106
- }
1
+ export interface Point {
2
+ x: number;
3
+ y: number;
4
+ }
5
+
6
+ export interface Size {
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ export type Unit = "px" | "mm" | "cm" | "in";
12
+
13
+ export interface Layout {
14
+ scale: number;
15
+ offsetX: number;
16
+ offsetY: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+
21
+ export class Coordinate {
22
+ /**
23
+ * Calculate layout to fit content within container while preserving aspect ratio.
24
+ */
25
+ static calculateLayout(
26
+ container: Size,
27
+ content: Size,
28
+ padding: number = 0,
29
+ ): Layout {
30
+ const availableWidth = Math.max(0, container.width - padding * 2);
31
+ const availableHeight = Math.max(0, container.height - padding * 2);
32
+
33
+ if (content.width === 0 || content.height === 0) {
34
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
35
+ }
36
+
37
+ const scaleX = availableWidth / content.width;
38
+ const scaleY = availableHeight / content.height;
39
+ const scale = Math.min(scaleX, scaleY);
40
+
41
+ const width = content.width * scale;
42
+ const height = content.height * scale;
43
+
44
+ const offsetX = (container.width - width) / 2;
45
+ const offsetY = (container.height - height) / 2;
46
+
47
+ return { scale, offsetX, offsetY, width, height };
48
+ }
49
+
50
+ /**
51
+ * Convert an absolute value to a normalized value (0-1).
52
+ * @param value Absolute value (e.g., pixels)
53
+ * @param total Total dimension size (e.g., canvas width)
54
+ */
55
+ static toNormalized(value: number, total: number): number {
56
+ return total === 0 ? 0 : value / total;
57
+ }
58
+
59
+ /**
60
+ * Convert a normalized value (0-1) to an absolute value.
61
+ * @param normalized Normalized value (0-1)
62
+ * @param total Total dimension size (e.g., canvas width)
63
+ */
64
+ static toAbsolute(normalized: number, total: number): number {
65
+ return normalized * total;
66
+ }
67
+
68
+ /**
69
+ * Normalize a point's coordinates.
70
+ */
71
+ static normalizePoint(point: Point, size: Size): Point {
72
+ return {
73
+ x: this.toNormalized(point.x, size.width),
74
+ y: this.toNormalized(point.y, size.height),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Denormalize a point's coordinates to absolute pixels.
80
+ */
81
+ static denormalizePoint(point: Point, size: Size): Point {
82
+ return {
83
+ x: this.toAbsolute(point.x, size.width),
84
+ y: this.toAbsolute(point.y, size.height),
85
+ };
86
+ }
87
+
88
+ static convertUnit(value: number, from: Unit, to: Unit): number {
89
+ if (from === to) return value;
90
+
91
+ // Base unit: mm
92
+ const toMM: Record<Unit, number> = {
93
+ px: 0.264583, // 1px = 0.264583mm (96 DPI)
94
+ mm: 1,
95
+ cm: 10,
96
+ in: 25.4
97
+ };
98
+
99
+ const mmValue = value * (from === 'px' ? toMM.px : toMM[from] || 1);
100
+
101
+ if (to === 'px') {
102
+ return mmValue / toMM.px;
103
+ }
104
+ return mmValue / (toMM[to] || 1);
105
+ }
106
+ }
package/src/dieline.ts CHANGED
@@ -14,8 +14,9 @@ import {
14
14
  generateMaskPath,
15
15
  generateBleedZonePath,
16
16
  getPathBounds,
17
- EdgeFeature,
17
+ DielineFeature,
18
18
  } from "./geometry";
19
+ import { ConstraintRegistry } from "./constraints";
19
20
 
20
21
  export interface DielineGeometry {
21
22
  shape: "rect" | "circle" | "ellipse" | "custom";
@@ -52,7 +53,7 @@ export interface DielineState {
52
53
  insideColor: string;
53
54
  outsideColor: string;
54
55
  showBleedLines: boolean;
55
- features: EdgeFeature[];
56
+ features: DielineFeature[];
56
57
  pathData?: string;
57
58
  }
58
59
 
@@ -332,6 +333,40 @@ export class DielineTool implements Extension {
332
333
  },
333
334
  ] as ConfigurationContribution[],
334
335
  [ContributionPointIds.COMMANDS]: [
336
+ {
337
+ command: "updateFeaturePosition",
338
+ title: "Update Feature Position",
339
+ handler: (groupId: string, x: number, y: number) => {
340
+ const configService = this.context?.services.get<any>(
341
+ "ConfigurationService",
342
+ );
343
+ if (!configService) return;
344
+
345
+ const features = configService.get("dieline.features") || [];
346
+ const dielineWidth = configService.get("dieline.width") || 500;
347
+ const dielineHeight = configService.get("dieline.height") || 500;
348
+
349
+ let changed = false;
350
+ const newFeatures = features.map((f: any) => {
351
+ if (f.groupId === groupId) {
352
+ const constrained = ConstraintRegistry.apply(x, y, f, {
353
+ dielineWidth,
354
+ dielineHeight,
355
+ });
356
+
357
+ if (f.x !== constrained.x || f.y !== constrained.y) {
358
+ changed = true;
359
+ return { ...f, x: constrained.x, y: constrained.y };
360
+ }
361
+ }
362
+ return f;
363
+ });
364
+
365
+ if (changed) {
366
+ configService.update("dieline.features", newFeatures);
367
+ }
368
+ },
369
+ },
335
370
  {
336
371
  command: "getGeometry",
337
372
  title: "Get Geometry",
@@ -512,12 +547,8 @@ export class DielineTool implements Extension {
512
547
  };
513
548
  });
514
549
 
515
- const originalFeatures = absoluteFeatures.filter(
516
- (f) => !f.target || f.target === "original" || f.target === "both",
517
- );
518
- const offsetFeatures = absoluteFeatures.filter(
519
- (f) => f.target === "offset" || f.target === "both",
520
- );
550
+ // Split features into Cut (Physical) and Visual (All)
551
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
521
552
 
522
553
  // 1. Draw Mask (Outside)
523
554
  const cutW = Math.max(0, visualWidth + visualOffset * 2);
@@ -525,10 +556,6 @@ export class DielineTool implements Extension {
525
556
  const cutR =
526
557
  visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
527
558
 
528
- // If no bleed offset, mask should match the original dieline (including its features)
529
- // If bleed offset exists (positive or negative), mask matches bleed line (which only includes offset features)
530
- const maskFeatures = visualOffset !== 0 ? offsetFeatures : originalFeatures;
531
-
532
559
  // Use Paper.js to generate the complex mask path
533
560
  const maskPathData = generateMaskPath({
534
561
  canvasWidth: canvasW,
@@ -539,7 +566,7 @@ export class DielineTool implements Extension {
539
566
  radius: cutR,
540
567
  x: cx,
541
568
  y: cy,
542
- features: maskFeatures,
569
+ features: cutFeatures,
543
570
  pathData: this.state.pathData,
544
571
  });
545
572
 
@@ -569,7 +596,7 @@ export class DielineTool implements Extension {
569
596
  radius: cutR,
570
597
  x: cx,
571
598
  y: cy,
572
- features: maskFeatures, // Use same features as mask for consistency
599
+ features: cutFeatures, // Use same features as mask for consistency
573
600
  pathData: this.state.pathData,
574
601
  canvasWidth: canvasW,
575
602
  canvasHeight: canvasH,
@@ -596,7 +623,7 @@ export class DielineTool implements Extension {
596
623
  radius: visualRadius,
597
624
  x: cx,
598
625
  y: cy,
599
- features: originalFeatures,
626
+ features: cutFeatures,
600
627
  pathData: this.state.pathData,
601
628
  canvasWidth: canvasW,
602
629
  canvasHeight: canvasH,
@@ -608,7 +635,7 @@ export class DielineTool implements Extension {
608
635
  radius: cutR,
609
636
  x: cx,
610
637
  y: cy,
611
- features: offsetFeatures,
638
+ features: cutFeatures,
612
639
  pathData: this.state.pathData,
613
640
  canvasWidth: canvasW,
614
641
  canvasHeight: canvasH,
@@ -641,7 +668,7 @@ export class DielineTool implements Extension {
641
668
  radius: cutR,
642
669
  x: cx,
643
670
  y: cy,
644
- features: offsetFeatures,
671
+ features: cutFeatures,
645
672
  pathData: this.state.pathData,
646
673
  canvasWidth: canvasW,
647
674
  canvasHeight: canvasH,
@@ -671,7 +698,7 @@ export class DielineTool implements Extension {
671
698
  radius: visualRadius,
672
699
  x: cx,
673
700
  y: cy,
674
- features: originalFeatures,
701
+ features: absoluteFeatures,
675
702
  pathData: this.state.pathData,
676
703
  canvasWidth: canvasW,
677
704
  canvasHeight: canvasH,
@@ -798,9 +825,7 @@ export class DielineTool implements Extension {
798
825
  };
799
826
  });
800
827
 
801
- const originalFeatures = absoluteFeatures.filter(
802
- (f) => !f.target || f.target === "original" || f.target === "both",
803
- );
828
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
804
829
 
805
830
  const generatedPathData = generateDielinePath({
806
831
  shape,
@@ -809,7 +834,7 @@ export class DielineTool implements Extension {
809
834
  radius: visualRadius,
810
835
  x: cx,
811
836
  y: cy,
812
- features: originalFeatures,
837
+ features: cutFeatures,
813
838
  pathData,
814
839
  canvasWidth: canvasW,
815
840
  canvasHeight: canvasH,