@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/src/geometry.ts CHANGED
@@ -14,13 +14,13 @@ export interface DielineFeature {
14
14
  height?: number;
15
15
  radius?: number;
16
16
  rotation?: number;
17
- placement?: "edge" | "internal";
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
- constraints?: {
22
- type: string;
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 or explicit 'edge')
164
+ // Filter for Edge Features (Default is Edge, unless explicit 'surface')
165
165
  const edgeFeatures = features.filter(
166
- (f) => !f.placement || f.placement === "edge",
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
- if (f.operation === "add") {
178
- adds.push(item);
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
- subtracts.push(item);
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 internalFeatures = features.filter((f) => f.placement === "internal");
227
-
228
- if (internalFeatures.length === 0) return shape;
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 internalFeatures) {
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
- const temp = result.unite(item);
243
- result.remove();
244
- item.remove();
245
- result = temp;
340
+ const temp = result.unite(item);
341
+ result.remove();
342
+ item.remove();
343
+ result = temp;
246
344
  } else {
247
- const temp = result.subtract(item);
248
- result.remove();
249
- item.remove();
250
- result = temp;
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(pOriginal, originalOptions.features, originalOptions);
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(pOffset, offsetOptions.features, offsetOptions);
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 nearest = shape.getNearestPoint(p);
491
+ const location = shape.getNearestLocation(p);
365
492
 
366
- const result = { x: nearest.x, y: nearest.y };
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;