@pooder/kit 4.1.0 → 4.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 +12 -0
- package/dist/index.d.mts +62 -10
- package/dist/index.d.ts +62 -10
- package/dist/index.js +756 -348
- package/dist/index.mjs +753 -347
- package/package.json +3 -2
- package/src/CanvasService.ts +96 -89
- package/src/ViewportSystem.ts +92 -0
- package/src/background.ts +230 -230
- package/src/constraints.ts +191 -27
- package/src/coordinate.ts +106 -106
- package/src/dieline.ts +955 -871
- package/src/feature.ts +282 -195
- package/src/featureComplete.ts +46 -0
- package/src/film.ts +194 -194
- package/src/geometry.ts +161 -30
- package/src/image.ts +512 -512
- package/src/index.ts +10 -9
- package/src/mirror.ts +128 -128
- package/src/ruler.ts +508 -500
- package/src/tracer.ts +570 -570
- package/src/units.ts +27 -0
- package/src/white-ink.ts +373 -373
- package/tsconfig.test.json +15 -0
package/src/geometry.ts
CHANGED
|
@@ -14,13 +14,13 @@ export interface DielineFeature {
|
|
|
14
14
|
height?: number;
|
|
15
15
|
radius?: number;
|
|
16
16
|
rotation?: number;
|
|
17
|
-
|
|
17
|
+
// Rendering behavior: 'edge' (modifies perimeter) or 'surface' (hole/island)
|
|
18
|
+
renderBehavior?: "edge" | "surface";
|
|
18
19
|
color?: string;
|
|
19
20
|
strokeDash?: number[];
|
|
20
21
|
skipCut?: boolean;
|
|
21
|
-
|
|
22
|
-
type:
|
|
23
|
-
params?: any;
|
|
22
|
+
bridge?: {
|
|
23
|
+
type: "vertical";
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -161,9 +161,9 @@ function getPerimeterShape(options: GeometryOptions): paper.PathItem {
|
|
|
161
161
|
const { features } = options;
|
|
162
162
|
|
|
163
163
|
if (features && features.length > 0) {
|
|
164
|
-
// Filter for Edge Features (Default
|
|
164
|
+
// Filter for Edge Features (Default is Edge, unless explicit 'surface')
|
|
165
165
|
const edgeFeatures = features.filter(
|
|
166
|
-
(f) => !f.
|
|
166
|
+
(f) => !f.renderBehavior || f.renderBehavior === "edge",
|
|
167
167
|
);
|
|
168
168
|
|
|
169
169
|
const adds: paper.PathItem[] = [];
|
|
@@ -173,11 +173,107 @@ function getPerimeterShape(options: GeometryOptions): paper.PathItem {
|
|
|
173
173
|
const pos = resolveFeaturePosition(f, options);
|
|
174
174
|
const center = new paper.Point(pos.x, pos.y);
|
|
175
175
|
const item = createFeatureItem(f, center);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
|
|
177
|
+
// Handle Bridge logic: Create a connection shape to the main body
|
|
178
|
+
if (f.bridge && f.bridge.type === "vertical") {
|
|
179
|
+
const itemBounds = item.bounds;
|
|
180
|
+
const mainBounds = mainShape.bounds;
|
|
181
|
+
const bridgeTop = mainBounds.top;
|
|
182
|
+
const bridgeBottom = itemBounds.top;
|
|
183
|
+
|
|
184
|
+
if (bridgeBottom > bridgeTop) {
|
|
185
|
+
// 1. Create a full column up to the top of the main shape
|
|
186
|
+
// Start slightly inside the feature to ensure overlap at the bottom
|
|
187
|
+
const startY = bridgeBottom + 1;
|
|
188
|
+
const bridgeRect = new paper.Path.Rectangle({
|
|
189
|
+
from: [itemBounds.left, bridgeTop],
|
|
190
|
+
to: [itemBounds.right, startY],
|
|
191
|
+
insert: false,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 2. Subtract the main shape from this column
|
|
195
|
+
// This leaves us with the parts of the column that are NOT inside the main shape (gaps)
|
|
196
|
+
const gaps = bridgeRect.subtract(mainShape);
|
|
197
|
+
bridgeRect.remove();
|
|
198
|
+
|
|
199
|
+
// 3. Find the gap piece that connects to our feature
|
|
200
|
+
// It should be the piece with the lowest bottom (highest Y) matching our feature top
|
|
201
|
+
let bridgePart: paper.PathItem | null = null;
|
|
202
|
+
|
|
203
|
+
// Helper to check if a part is the bottom one
|
|
204
|
+
const isBottomPart = (part: paper.PathItem) => {
|
|
205
|
+
// Check if bottom aligns with feature top (allow small tolerance)
|
|
206
|
+
return Math.abs(part.bounds.bottom - startY) < 2;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (gaps instanceof paper.CompoundPath) {
|
|
210
|
+
// Find the child that is at the bottom
|
|
211
|
+
const children = gaps.children;
|
|
212
|
+
let maxBottom = -Infinity;
|
|
213
|
+
let bestChild = null;
|
|
214
|
+
|
|
215
|
+
for (const child of children) {
|
|
216
|
+
if (child.bounds.bottom > maxBottom) {
|
|
217
|
+
maxBottom = child.bounds.bottom;
|
|
218
|
+
bestChild = child;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (bestChild && isBottomPart(bestChild as paper.PathItem)) {
|
|
223
|
+
bridgePart = (bestChild as paper.PathItem).clone();
|
|
224
|
+
}
|
|
225
|
+
} else if (gaps instanceof paper.Path) {
|
|
226
|
+
if (isBottomPart(gaps)) {
|
|
227
|
+
bridgePart = gaps.clone();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
gaps.remove();
|
|
232
|
+
|
|
233
|
+
if (bridgePart) {
|
|
234
|
+
// Overlap fix:
|
|
235
|
+
// Scale the bridge up slightly from the bottom to ensure it overlaps with the main shape at the top.
|
|
236
|
+
// This prevents hairline gaps due to perfect alignment from subtract().
|
|
237
|
+
const bounds = bridgePart.bounds;
|
|
238
|
+
if (bounds.height > 0) {
|
|
239
|
+
const overlap = 1;
|
|
240
|
+
const scaleY = (bounds.height + overlap) / bounds.height;
|
|
241
|
+
// Scale around the bottom-center to keep the connection to the feature intact
|
|
242
|
+
bridgePart.scale(1, scaleY, new paper.Point(bounds.center.x, bounds.bottom));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Unite the bridge with the feature
|
|
246
|
+
const unitedItem = item.unite(bridgePart);
|
|
247
|
+
item.remove();
|
|
248
|
+
bridgePart.remove();
|
|
249
|
+
|
|
250
|
+
if (f.operation === "add") {
|
|
251
|
+
adds.push(unitedItem);
|
|
252
|
+
} else {
|
|
253
|
+
subtracts.push(unitedItem);
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
// No bridge needed (feature touches or intersects main shape directly)
|
|
257
|
+
// or calculation failed. Fallback to original item.
|
|
258
|
+
if (f.operation === "add") {
|
|
259
|
+
adds.push(item);
|
|
260
|
+
} else {
|
|
261
|
+
subtracts.push(item);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
if (f.operation === "add") {
|
|
266
|
+
adds.push(item);
|
|
267
|
+
} else {
|
|
268
|
+
subtracts.push(item);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
179
271
|
} else {
|
|
180
|
-
|
|
272
|
+
if (f.operation === "add") {
|
|
273
|
+
adds.push(item);
|
|
274
|
+
} else {
|
|
275
|
+
subtracts.push(item);
|
|
276
|
+
}
|
|
181
277
|
}
|
|
182
278
|
});
|
|
183
279
|
|
|
@@ -223,38 +319,40 @@ function applySurfaceFeatures(
|
|
|
223
319
|
features: DielineFeature[],
|
|
224
320
|
options: GeometryOptions,
|
|
225
321
|
): paper.PathItem {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
322
|
+
const surfaceFeatures = features.filter(
|
|
323
|
+
(f) => f.renderBehavior === "surface",
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (surfaceFeatures.length === 0) return shape;
|
|
229
327
|
|
|
230
328
|
let result = shape;
|
|
231
|
-
|
|
329
|
+
|
|
232
330
|
// Internal features are usually subtractive (holes)
|
|
233
331
|
// But we support 'add' too (islands? maybe just unite)
|
|
234
|
-
|
|
235
|
-
for (const f of
|
|
332
|
+
|
|
333
|
+
for (const f of surfaceFeatures) {
|
|
236
334
|
const pos = resolveFeaturePosition(f, options);
|
|
237
335
|
const center = new paper.Point(pos.x, pos.y);
|
|
238
336
|
const item = createFeatureItem(f, center);
|
|
239
337
|
|
|
240
338
|
try {
|
|
241
339
|
if (f.operation === "add") {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
340
|
+
const temp = result.unite(item);
|
|
341
|
+
result.remove();
|
|
342
|
+
item.remove();
|
|
343
|
+
result = temp;
|
|
246
344
|
} else {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
345
|
+
const temp = result.subtract(item);
|
|
346
|
+
result.remove();
|
|
347
|
+
item.remove();
|
|
348
|
+
result = temp;
|
|
251
349
|
}
|
|
252
350
|
} catch (e) {
|
|
253
351
|
console.error("Geometry: Failed to apply surface feature", e);
|
|
254
352
|
item.remove();
|
|
255
353
|
}
|
|
256
354
|
}
|
|
257
|
-
|
|
355
|
+
|
|
258
356
|
return result;
|
|
259
357
|
}
|
|
260
358
|
|
|
@@ -322,11 +420,19 @@ export function generateBleedZonePath(
|
|
|
322
420
|
|
|
323
421
|
// 1. Generate Original Shape
|
|
324
422
|
const pOriginal = getPerimeterShape(originalOptions);
|
|
325
|
-
const shapeOriginal = applySurfaceFeatures(
|
|
423
|
+
const shapeOriginal = applySurfaceFeatures(
|
|
424
|
+
pOriginal,
|
|
425
|
+
originalOptions.features,
|
|
426
|
+
originalOptions,
|
|
427
|
+
);
|
|
326
428
|
|
|
327
429
|
// 2. Generate Offset Shape
|
|
328
430
|
const pOffset = getPerimeterShape(offsetOptions);
|
|
329
|
-
const shapeOffset = applySurfaceFeatures(
|
|
431
|
+
const shapeOffset = applySurfaceFeatures(
|
|
432
|
+
pOffset,
|
|
433
|
+
offsetOptions.features,
|
|
434
|
+
offsetOptions,
|
|
435
|
+
);
|
|
330
436
|
|
|
331
437
|
// 3. Calculate Difference
|
|
332
438
|
let bleedZone: paper.PathItem;
|
|
@@ -345,6 +451,27 @@ export function generateBleedZonePath(
|
|
|
345
451
|
return pathData;
|
|
346
452
|
}
|
|
347
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Finds the lowest point (Max Y) on the Dieline geometry (Base Shape ONLY).
|
|
456
|
+
*/
|
|
457
|
+
export function getLowestPointOnDieline(
|
|
458
|
+
options: GeometryOptions,
|
|
459
|
+
): { x: number; y: number } {
|
|
460
|
+
ensurePaper(options.width * 2, options.height * 2);
|
|
461
|
+
paper.project.activeLayer.removeChildren();
|
|
462
|
+
|
|
463
|
+
const shape = createBaseShape(options);
|
|
464
|
+
const bounds = shape.bounds;
|
|
465
|
+
|
|
466
|
+
const result = {
|
|
467
|
+
x: bounds.center.x,
|
|
468
|
+
y: bounds.bottom,
|
|
469
|
+
};
|
|
470
|
+
shape.remove();
|
|
471
|
+
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
348
475
|
/**
|
|
349
476
|
* Finds the nearest point on the Dieline geometry (Base Shape ONLY) for a given target point.
|
|
350
477
|
* Used for constraining feature movement.
|
|
@@ -352,7 +479,7 @@ export function generateBleedZonePath(
|
|
|
352
479
|
export function getNearestPointOnDieline(
|
|
353
480
|
point: { x: number; y: number },
|
|
354
481
|
options: GeometryOptions,
|
|
355
|
-
): { x: number; y: number } {
|
|
482
|
+
): { x: number; y: number; normal?: { x: number; y: number } } {
|
|
356
483
|
ensurePaper(options.width * 2, options.height * 2);
|
|
357
484
|
paper.project.activeLayer.removeChildren();
|
|
358
485
|
|
|
@@ -361,9 +488,13 @@ export function getNearestPointOnDieline(
|
|
|
361
488
|
const shape = createBaseShape(options);
|
|
362
489
|
|
|
363
490
|
const p = new paper.Point(point.x, point.y);
|
|
364
|
-
const
|
|
491
|
+
const location = shape.getNearestLocation(p);
|
|
365
492
|
|
|
366
|
-
const result = {
|
|
493
|
+
const result = {
|
|
494
|
+
x: location.point.x,
|
|
495
|
+
y: location.point.y,
|
|
496
|
+
normal: location.normal ? { x: location.normal.x, y: location.normal.y } : undefined
|
|
497
|
+
};
|
|
367
498
|
shape.remove();
|
|
368
499
|
|
|
369
500
|
return result;
|