@pooder/kit 0.0.2 → 2.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/src/geometry.ts CHANGED
@@ -1,244 +1,251 @@
1
- import paper from 'paper';
2
-
3
- export interface HoleData {
4
- x: number;
5
- y: number;
6
- innerRadius: number;
7
- outerRadius: number;
8
- }
9
-
10
- export interface GeometryOptions {
11
- shape: 'rect' | 'circle' | 'ellipse';
12
- width: number;
13
- height: number;
14
- radius: number;
15
- x: number;
16
- y: number;
17
- holes: Array<HoleData>;
18
- }
19
-
20
- export interface MaskGeometryOptions extends GeometryOptions {
21
- canvasWidth: number;
22
- canvasHeight: number;
23
- }
24
-
25
- /**
26
- * Initializes paper.js project if not already initialized.
27
- */
28
- function ensurePaper(width: number, height: number) {
29
- if (!paper.project) {
30
- paper.setup(new paper.Size(width, height));
31
- }
32
- }
33
-
34
- /**
35
- * Creates the base dieline shape (Rect/Circle/Ellipse)
36
- */
37
- function createBaseShape(options: GeometryOptions): paper.PathItem {
38
- const { shape, width, height, radius, x, y } = options;
39
- const center = new paper.Point(x, y);
40
-
41
- if (shape === 'rect') {
42
- return new paper.Path.Rectangle({
43
- point: [x - width / 2, y - height / 2],
44
- size: [Math.max(0, width), Math.max(0, height)],
45
- radius: Math.max(0, radius)
46
- });
47
- } else if (shape === 'circle') {
48
- const r = Math.min(width, height) / 2;
49
- return new paper.Path.Circle({
50
- center: center,
51
- radius: Math.max(0, r)
52
- });
53
- } else { // ellipse
54
- return new paper.Path.Ellipse({
55
- center: center,
56
- radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
57
- });
58
- }
59
- }
60
-
61
- /**
62
- * Internal helper to generate the Dieline Shape (Paper Item).
63
- * Caller is responsible for cleanup.
64
- */
65
- function getDielineShape(options: GeometryOptions): paper.PathItem {
66
- // 1. Create Base Shape
67
- let mainShape = createBaseShape(options);
68
-
69
- const { holes } = options;
70
-
71
- if (holes && holes.length > 0) {
72
- let lugsPath: paper.PathItem | null = null;
73
- let cutsPath: paper.PathItem | null = null;
74
-
75
- holes.forEach(hole => {
76
- // Create Lug (Outer Radius)
77
- const lug = new paper.Path.Circle({
78
- center: [hole.x, hole.y],
79
- radius: hole.outerRadius
80
- });
81
-
82
- // Check intersection with main body
83
- // Only add lug if it intersects (or is contained in) the main shape
84
- // This prevents floating islands when bleed shrinks
85
- if (!mainShape.intersects(lug) && !mainShape.contains(lug.position)) {
86
- lug.remove();
87
- return; // Skip this lug
88
- }
89
-
90
- // Create Cut (Inner Radius)
91
- const cut = new paper.Path.Circle({
92
- center: [hole.x, hole.y],
93
- radius: hole.innerRadius
94
- });
95
-
96
- // Union Lugs
97
- if (!lugsPath) {
98
- lugsPath = lug;
99
- } else {
100
- const temp = lugsPath.unite(lug);
101
- lugsPath.remove();
102
- lug.remove();
103
- lugsPath = temp;
104
- }
105
-
106
- // Union Cuts
107
- if (!cutsPath) {
108
- cutsPath = cut;
109
- } else {
110
- const temp = cutsPath.unite(cut);
111
- cutsPath.remove();
112
- cut.remove();
113
- cutsPath = temp;
114
- }
115
- });
116
-
117
- // 2. Add Lugs to Main Shape (Union) - Additive Fusion
118
- if (lugsPath) {
119
- const temp = mainShape.unite(lugsPath);
120
- mainShape.remove();
121
- // @ts-ignore
122
- lugsPath.remove();
123
- mainShape = temp;
124
- }
125
-
126
- // 3. Subtract Cuts from Main Shape (Difference)
127
- if (cutsPath) {
128
- const temp = mainShape.subtract(cutsPath);
129
- mainShape.remove();
130
- // @ts-ignore
131
- cutsPath.remove();
132
- mainShape = temp;
133
- }
134
- }
135
-
136
- return mainShape;
137
- }
138
-
139
- /**
140
- * Generates the path data for the Dieline (Product Shape).
141
- * Logic: (BaseShape UNION IntersectingLugs) SUBTRACT Cuts
142
- */
143
- export function generateDielinePath(options: GeometryOptions): string {
144
- ensurePaper(options.width * 2, options.height * 2);
145
- paper.project.activeLayer.removeChildren();
146
-
147
- const mainShape = getDielineShape(options);
148
-
149
- const pathData = mainShape.pathData;
150
- mainShape.remove();
151
-
152
- return pathData;
153
- }
154
-
155
- /**
156
- * Generates the path data for the Mask (Background Overlay).
157
- * Logic: Canvas SUBTRACT ProductShape
158
- */
159
- export function generateMaskPath(options: MaskGeometryOptions): string {
160
- ensurePaper(options.canvasWidth, options.canvasHeight);
161
- paper.project.activeLayer.removeChildren();
162
-
163
- const { canvasWidth, canvasHeight } = options;
164
-
165
- // 1. Canvas Background
166
- const maskRect = new paper.Path.Rectangle({
167
- point: [0, 0],
168
- size: [canvasWidth, canvasHeight]
169
- });
170
-
171
- // 2. Re-create Product Shape
172
- const mainShape = getDielineShape(options);
173
-
174
- // 3. Subtract Product from Mask
175
- const finalMask = maskRect.subtract(mainShape);
176
-
177
- maskRect.remove();
178
- mainShape.remove();
179
-
180
- const pathData = finalMask.pathData;
181
- finalMask.remove();
182
-
183
- return pathData;
184
- }
185
-
186
- /**
187
- * Generates the path data for the Bleed Zone (Area between Original and Offset).
188
- */
189
- export function generateBleedZonePath(options: GeometryOptions, offset: number): string {
190
- // Ensure canvas is large enough
191
- const maxDim = Math.max(options.width, options.height) + Math.abs(offset) * 4;
192
- ensurePaper(maxDim, maxDim);
193
- paper.project.activeLayer.removeChildren();
194
-
195
- // 1. Original Shape
196
- const shapeOriginal = getDielineShape(options);
197
-
198
- // 2. Offset Shape
199
- // Adjust dimensions for offset
200
- const offsetOptions: GeometryOptions = {
201
- ...options,
202
- width: Math.max(0, options.width + offset * 2),
203
- height: Math.max(0, options.height + offset * 2),
204
- radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset)
205
- };
206
-
207
- const shapeOffset = getDielineShape(offsetOptions);
208
-
209
- // 3. Calculate Difference
210
- let bleedZone: paper.PathItem;
211
- if (offset > 0) {
212
- bleedZone = shapeOffset.subtract(shapeOriginal);
213
- } else {
214
- bleedZone = shapeOriginal.subtract(shapeOffset);
215
- }
216
-
217
- const pathData = bleedZone.pathData;
218
-
219
- // Cleanup
220
- shapeOriginal.remove();
221
- shapeOffset.remove();
222
- bleedZone.remove();
223
-
224
- return pathData;
225
- }
226
-
227
- /**
228
- * Finds the nearest point on the Dieline geometry for a given target point.
229
- * Used for constraining hole movement.
230
- */
231
- export function getNearestPointOnDieline(point: {x: number, y: number}, options: GeometryOptions): {x: number, y: number} {
232
- ensurePaper(options.width * 2, options.height * 2);
233
- paper.project.activeLayer.removeChildren();
234
-
235
- const shape = createBaseShape(options);
236
-
237
- const p = new paper.Point(point.x, point.y);
238
- const nearest = shape.getNearestPoint(p);
239
-
240
- const result = { x: nearest.x, y: nearest.y };
241
- shape.remove();
242
-
243
- return result;
244
- }
1
+ import paper from "paper";
2
+
3
+ export interface HoleData {
4
+ x: number;
5
+ y: number;
6
+ innerRadius: number;
7
+ outerRadius: number;
8
+ }
9
+
10
+ export interface GeometryOptions {
11
+ shape: "rect" | "circle" | "ellipse";
12
+ width: number;
13
+ height: number;
14
+ radius: number;
15
+ x: number;
16
+ y: number;
17
+ holes: Array<HoleData>;
18
+ }
19
+
20
+ export interface MaskGeometryOptions extends GeometryOptions {
21
+ canvasWidth: number;
22
+ canvasHeight: number;
23
+ }
24
+
25
+ /**
26
+ * Initializes paper.js project if not already initialized.
27
+ */
28
+ function ensurePaper(width: number, height: number) {
29
+ if (!paper.project) {
30
+ paper.setup(new paper.Size(width, height));
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Creates the base dieline shape (Rect/Circle/Ellipse)
36
+ */
37
+ function createBaseShape(options: GeometryOptions): paper.PathItem {
38
+ const { shape, width, height, radius, x, y } = options;
39
+ const center = new paper.Point(x, y);
40
+
41
+ if (shape === "rect") {
42
+ return new paper.Path.Rectangle({
43
+ point: [x - width / 2, y - height / 2],
44
+ size: [Math.max(0, width), Math.max(0, height)],
45
+ radius: Math.max(0, radius),
46
+ });
47
+ } else if (shape === "circle") {
48
+ const r = Math.min(width, height) / 2;
49
+ return new paper.Path.Circle({
50
+ center: center,
51
+ radius: Math.max(0, r),
52
+ });
53
+ } else {
54
+ // ellipse
55
+ return new paper.Path.Ellipse({
56
+ center: center,
57
+ radius: [Math.max(0, width / 2), Math.max(0, height / 2)],
58
+ });
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Internal helper to generate the Dieline Shape (Paper Item).
64
+ * Caller is responsible for cleanup.
65
+ */
66
+ function getDielineShape(options: GeometryOptions): paper.PathItem {
67
+ // 1. Create Base Shape
68
+ let mainShape = createBaseShape(options);
69
+
70
+ const { holes } = options;
71
+
72
+ if (holes && holes.length > 0) {
73
+ let lugsPath: paper.PathItem | null = null;
74
+ let cutsPath: paper.PathItem | null = null;
75
+
76
+ holes.forEach((hole) => {
77
+ // Create Lug (Outer Radius)
78
+ const lug = new paper.Path.Circle({
79
+ center: [hole.x, hole.y],
80
+ radius: hole.outerRadius,
81
+ });
82
+
83
+ // Check intersection with main body
84
+ // Only add lug if it intersects (or is contained in) the main shape
85
+ // This prevents floating islands when bleed shrinks
86
+ if (!mainShape.intersects(lug) && !mainShape.contains(lug.position)) {
87
+ lug.remove();
88
+ return; // Skip this lug
89
+ }
90
+
91
+ // Create Cut (Inner Radius)
92
+ const cut = new paper.Path.Circle({
93
+ center: [hole.x, hole.y],
94
+ radius: hole.innerRadius,
95
+ });
96
+
97
+ // Union Lugs
98
+ if (!lugsPath) {
99
+ lugsPath = lug;
100
+ } else {
101
+ const temp = lugsPath.unite(lug);
102
+ lugsPath.remove();
103
+ lug.remove();
104
+ lugsPath = temp;
105
+ }
106
+
107
+ // Union Cuts
108
+ if (!cutsPath) {
109
+ cutsPath = cut;
110
+ } else {
111
+ const temp = cutsPath.unite(cut);
112
+ cutsPath.remove();
113
+ cut.remove();
114
+ cutsPath = temp;
115
+ }
116
+ });
117
+
118
+ // 2. Add Lugs to Main Shape (Union) - Additive Fusion
119
+ if (lugsPath) {
120
+ const temp = mainShape.unite(lugsPath);
121
+ mainShape.remove();
122
+ // @ts-ignore
123
+ lugsPath.remove();
124
+ mainShape = temp;
125
+ }
126
+
127
+ // 3. Subtract Cuts from Main Shape (Difference)
128
+ if (cutsPath) {
129
+ const temp = mainShape.subtract(cutsPath);
130
+ mainShape.remove();
131
+ // @ts-ignore
132
+ cutsPath.remove();
133
+ mainShape = temp;
134
+ }
135
+ }
136
+
137
+ return mainShape;
138
+ }
139
+
140
+ /**
141
+ * Generates the path data for the Dieline (Product Shape).
142
+ * Logic: (BaseShape UNION IntersectingLugs) SUBTRACT Cuts
143
+ */
144
+ export function generateDielinePath(options: GeometryOptions): string {
145
+ ensurePaper(options.width * 2, options.height * 2);
146
+ paper.project.activeLayer.removeChildren();
147
+
148
+ const mainShape = getDielineShape(options);
149
+
150
+ const pathData = mainShape.pathData;
151
+ mainShape.remove();
152
+
153
+ return pathData;
154
+ }
155
+
156
+ /**
157
+ * Generates the path data for the Mask (Background Overlay).
158
+ * Logic: Canvas SUBTRACT ProductShape
159
+ */
160
+ export function generateMaskPath(options: MaskGeometryOptions): string {
161
+ ensurePaper(options.canvasWidth, options.canvasHeight);
162
+ paper.project.activeLayer.removeChildren();
163
+
164
+ const { canvasWidth, canvasHeight } = options;
165
+
166
+ // 1. Canvas Background
167
+ const maskRect = new paper.Path.Rectangle({
168
+ point: [0, 0],
169
+ size: [canvasWidth, canvasHeight],
170
+ });
171
+
172
+ // 2. Re-create Product Shape
173
+ const mainShape = getDielineShape(options);
174
+
175
+ // 3. Subtract Product from Mask
176
+ const finalMask = maskRect.subtract(mainShape);
177
+
178
+ maskRect.remove();
179
+ mainShape.remove();
180
+
181
+ const pathData = finalMask.pathData;
182
+ finalMask.remove();
183
+
184
+ return pathData;
185
+ }
186
+
187
+ /**
188
+ * Generates the path data for the Bleed Zone (Area between Original and Offset).
189
+ */
190
+ export function generateBleedZonePath(
191
+ options: GeometryOptions,
192
+ offset: number,
193
+ ): string {
194
+ // Ensure canvas is large enough
195
+ const maxDim = Math.max(options.width, options.height) + Math.abs(offset) * 4;
196
+ ensurePaper(maxDim, maxDim);
197
+ paper.project.activeLayer.removeChildren();
198
+
199
+ // 1. Original Shape
200
+ const shapeOriginal = getDielineShape(options);
201
+
202
+ // 2. Offset Shape
203
+ // Adjust dimensions for offset
204
+ const offsetOptions: GeometryOptions = {
205
+ ...options,
206
+ width: Math.max(0, options.width + offset * 2),
207
+ height: Math.max(0, options.height + offset * 2),
208
+ radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset),
209
+ };
210
+
211
+ const shapeOffset = getDielineShape(offsetOptions);
212
+
213
+ // 3. Calculate Difference
214
+ let bleedZone: paper.PathItem;
215
+ if (offset > 0) {
216
+ bleedZone = shapeOffset.subtract(shapeOriginal);
217
+ } else {
218
+ bleedZone = shapeOriginal.subtract(shapeOffset);
219
+ }
220
+
221
+ const pathData = bleedZone.pathData;
222
+
223
+ // Cleanup
224
+ shapeOriginal.remove();
225
+ shapeOffset.remove();
226
+ bleedZone.remove();
227
+
228
+ return pathData;
229
+ }
230
+
231
+ /**
232
+ * Finds the nearest point on the Dieline geometry for a given target point.
233
+ * Used for constraining hole movement.
234
+ */
235
+ export function getNearestPointOnDieline(
236
+ point: { x: number; y: number },
237
+ options: GeometryOptions,
238
+ ): { x: number; y: number } {
239
+ ensurePaper(options.width * 2, options.height * 2);
240
+ paper.project.activeLayer.removeChildren();
241
+
242
+ const shape = createBaseShape(options);
243
+
244
+ const p = new paper.Point(point.x, point.y);
245
+ const nearest = shape.getNearestPoint(p);
246
+
247
+ const result = { x: nearest.x, y: nearest.y };
248
+ shape.remove();
249
+
250
+ return result;
251
+ }