@pooder/kit 4.3.0 → 5.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/.test-dist/src/CanvasService.js +249 -0
- package/.test-dist/src/ViewportSystem.js +75 -0
- package/.test-dist/src/background.js +203 -0
- package/.test-dist/src/bridgeSelection.js +20 -0
- package/.test-dist/src/constraints.js +237 -0
- package/.test-dist/src/coordinate.js +74 -0
- package/.test-dist/src/dieline.js +723 -0
- package/.test-dist/src/edgeScale.js +12 -0
- package/.test-dist/src/feature.js +752 -0
- package/.test-dist/src/featureComplete.js +32 -0
- package/.test-dist/src/film.js +167 -0
- package/.test-dist/src/geometry.js +506 -0
- package/.test-dist/src/image.js +1234 -0
- package/.test-dist/src/index.js +35 -0
- package/.test-dist/src/maskOps.js +270 -0
- package/.test-dist/src/mirror.js +104 -0
- package/.test-dist/src/renderSpec.js +2 -0
- package/.test-dist/src/ruler.js +343 -0
- package/.test-dist/src/sceneLayout.js +99 -0
- package/.test-dist/src/sceneLayoutModel.js +196 -0
- package/.test-dist/src/sceneView.js +40 -0
- package/.test-dist/src/sceneVisibility.js +42 -0
- package/.test-dist/src/size.js +332 -0
- package/.test-dist/src/tracer.js +544 -0
- package/.test-dist/src/units.js +30 -0
- package/.test-dist/src/white-ink.js +829 -0
- package/.test-dist/src/wrappedOffsets.js +33 -0
- package/.test-dist/tests/run.js +94 -0
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +339 -36
- package/dist/index.d.ts +339 -36
- package/dist/index.js +3587 -854
- package/dist/index.mjs +3580 -856
- package/package.json +2 -2
- package/src/CanvasService.ts +300 -96
- package/src/ViewportSystem.ts +92 -92
- package/src/background.ts +230 -230
- package/src/bridgeSelection.ts +17 -0
- package/src/coordinate.ts +106 -106
- package/src/dieline.ts +897 -955
- package/src/edgeScale.ts +19 -0
- package/src/feature.ts +83 -30
- package/src/film.ts +194 -194
- package/src/geometry.ts +234 -80
- package/src/image.ts +1582 -512
- package/src/index.ts +14 -10
- package/src/maskOps.ts +326 -0
- package/src/mirror.ts +128 -128
- package/src/renderSpec.ts +18 -0
- package/src/ruler.ts +449 -508
- package/src/sceneLayout.ts +121 -0
- package/src/sceneLayoutModel.ts +335 -0
- package/src/sceneVisibility.ts +49 -0
- package/src/size.ts +379 -0
- package/src/tracer.ts +719 -570
- package/src/units.ts +27 -27
- package/src/white-ink.ts +1018 -373
- package/src/wrappedOffsets.ts +33 -0
- package/tests/run.ts +118 -0
- package/tsconfig.test.json +15 -15
package/src/geometry.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import paper from "paper";
|
|
2
|
+
import { pickExitIndex, scoreOutsideAbove } from "./bridgeSelection";
|
|
3
|
+
import { sampleWrappedOffsets, wrappedDistance } from "./wrappedOffsets";
|
|
2
4
|
|
|
3
5
|
export type FeatureOperation = "add" | "subtract";
|
|
4
6
|
export type FeatureShape = "rect" | "circle";
|
|
@@ -71,6 +73,120 @@ function ensurePaper(width: number, height: number) {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
const isBridgeDebugEnabled = () =>
|
|
77
|
+
Boolean((globalThis as any).__POODER_BRIDGE_DEBUG__);
|
|
78
|
+
|
|
79
|
+
function normalizePathItem(shape: paper.PathItem): paper.PathItem {
|
|
80
|
+
let result: any = shape;
|
|
81
|
+
if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
|
|
82
|
+
if (typeof result.reduce === "function") result = result.reduce({});
|
|
83
|
+
if (typeof result.reorient === "function") result = result.reorient(true, true);
|
|
84
|
+
if (typeof result.reduce === "function") result = result.reduce({});
|
|
85
|
+
return result as paper.PathItem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getBridgeDelta(itemBounds: paper.Rectangle, overlap: number) {
|
|
89
|
+
return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getExitHit(args: {
|
|
93
|
+
mainShape: paper.Path;
|
|
94
|
+
x: number;
|
|
95
|
+
bridgeBottom: number;
|
|
96
|
+
toY: number;
|
|
97
|
+
eps: number;
|
|
98
|
+
delta: number;
|
|
99
|
+
overlap: number;
|
|
100
|
+
op: FeatureOperation;
|
|
101
|
+
}) {
|
|
102
|
+
const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
|
|
103
|
+
|
|
104
|
+
const ray = new paper.Path.Line({
|
|
105
|
+
from: [x, bridgeBottom],
|
|
106
|
+
to: [x, toY],
|
|
107
|
+
insert: false,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const intersections = mainShape.getIntersections(ray) || [];
|
|
111
|
+
ray.remove();
|
|
112
|
+
|
|
113
|
+
const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
|
|
114
|
+
if (validHits.length === 0) return null;
|
|
115
|
+
|
|
116
|
+
validHits.sort((a, b) => b.point.y - a.point.y);
|
|
117
|
+
const flags = validHits.map((h) => {
|
|
118
|
+
const above = h.point.add(new paper.Point(0, -delta));
|
|
119
|
+
const below = h.point.add(new paper.Point(0, delta));
|
|
120
|
+
return {
|
|
121
|
+
insideAbove: mainShape.contains(above),
|
|
122
|
+
insideBelow: mainShape.contains(below),
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const idx = pickExitIndex(flags);
|
|
127
|
+
if (idx < 0) return null;
|
|
128
|
+
|
|
129
|
+
if (isBridgeDebugEnabled()) {
|
|
130
|
+
console.debug("Geometry: Bridge ray", {
|
|
131
|
+
x,
|
|
132
|
+
validHits: validHits.length,
|
|
133
|
+
idx,
|
|
134
|
+
delta,
|
|
135
|
+
overlap,
|
|
136
|
+
op,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hit = validHits[idx];
|
|
141
|
+
return { point: hit.point, location: hit };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function selectOuterChain(args: {
|
|
145
|
+
mainShape: paper.Path;
|
|
146
|
+
pointsA: paper.Point[];
|
|
147
|
+
pointsB: paper.Point[];
|
|
148
|
+
delta: number;
|
|
149
|
+
overlap: number;
|
|
150
|
+
op: FeatureOperation;
|
|
151
|
+
}) {
|
|
152
|
+
const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
|
|
153
|
+
|
|
154
|
+
const scoreA = scoreOutsideAbove(
|
|
155
|
+
pointsA.map((p) => ({
|
|
156
|
+
outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta))),
|
|
157
|
+
})),
|
|
158
|
+
);
|
|
159
|
+
const scoreB = scoreOutsideAbove(
|
|
160
|
+
pointsB.map((p) => ({
|
|
161
|
+
outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta))),
|
|
162
|
+
})),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const ratioA = scoreA / pointsA.length;
|
|
166
|
+
const ratioB = scoreB / pointsB.length;
|
|
167
|
+
|
|
168
|
+
if (isBridgeDebugEnabled()) {
|
|
169
|
+
console.debug("Geometry: Bridge chain", {
|
|
170
|
+
scoreA,
|
|
171
|
+
scoreB,
|
|
172
|
+
lenA: pointsA.length,
|
|
173
|
+
lenB: pointsB.length,
|
|
174
|
+
ratioA,
|
|
175
|
+
ratioB,
|
|
176
|
+
delta,
|
|
177
|
+
overlap,
|
|
178
|
+
op,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ratioEps = 1e-6;
|
|
183
|
+
if (Math.abs(ratioA - ratioB) > ratioEps) {
|
|
184
|
+
return ratioA > ratioB ? pointsA : pointsB;
|
|
185
|
+
}
|
|
186
|
+
if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
|
|
187
|
+
return pointsA.length <= pointsB.length ? pointsA : pointsB;
|
|
188
|
+
}
|
|
189
|
+
|
|
74
190
|
/**
|
|
75
191
|
* Creates the base dieline shape (Rect/Circle/Ellipse/Custom)
|
|
76
192
|
*/
|
|
@@ -182,91 +298,129 @@ function getPerimeterShape(options: GeometryOptions): paper.PathItem {
|
|
|
182
298
|
const bridgeBottom = itemBounds.top;
|
|
183
299
|
|
|
184
300
|
if (bridgeBottom > bridgeTop) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
301
|
+
const overlap = 2;
|
|
302
|
+
const rayPadding = 10;
|
|
303
|
+
const eps = 0.1;
|
|
304
|
+
const delta = getBridgeDelta(itemBounds, overlap);
|
|
305
|
+
|
|
306
|
+
const toY = bridgeTop - rayPadding;
|
|
307
|
+
const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
|
|
308
|
+
const xLeft = itemBounds.left + inset;
|
|
309
|
+
const xRight = itemBounds.right - inset;
|
|
310
|
+
|
|
311
|
+
if (!(mainShape instanceof paper.Path)) {
|
|
312
|
+
throw new Error("Geometry: Bridge requires base shape to be a Path");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const leftHit = getExitHit({
|
|
316
|
+
mainShape,
|
|
317
|
+
x: xLeft,
|
|
318
|
+
bridgeBottom,
|
|
319
|
+
toY,
|
|
320
|
+
eps,
|
|
321
|
+
delta,
|
|
322
|
+
overlap,
|
|
323
|
+
op: f.operation,
|
|
192
324
|
});
|
|
325
|
+
const rightHit = getExitHit({
|
|
326
|
+
mainShape,
|
|
327
|
+
x: xRight,
|
|
328
|
+
bridgeBottom,
|
|
329
|
+
toY,
|
|
330
|
+
eps,
|
|
331
|
+
delta,
|
|
332
|
+
overlap,
|
|
333
|
+
op: f.operation,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (!leftHit || !rightHit || xRight - xLeft <= eps) {
|
|
337
|
+
throw new Error("Geometry: Bridge ray intersection not found");
|
|
338
|
+
}
|
|
193
339
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
340
|
+
const path = mainShape as paper.Path;
|
|
341
|
+
const pathLength = path.length;
|
|
342
|
+
const leftOffset = leftHit.location.offset;
|
|
343
|
+
const rightOffset = rightHit.location.offset;
|
|
344
|
+
|
|
345
|
+
const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
|
|
346
|
+
const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
|
|
347
|
+
const countFor = (d: number) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
|
|
348
|
+
|
|
349
|
+
const offsetsA = sampleWrappedOffsets(
|
|
350
|
+
pathLength,
|
|
351
|
+
leftOffset,
|
|
352
|
+
rightOffset,
|
|
353
|
+
countFor(distanceA),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const offsetsB = sampleWrappedOffsets(
|
|
357
|
+
pathLength,
|
|
358
|
+
rightOffset,
|
|
359
|
+
leftOffset,
|
|
360
|
+
countFor(distanceB),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const pointsA = offsetsA
|
|
364
|
+
.map((o) => path.getPointAt(o))
|
|
365
|
+
.filter((p): p is paper.Point => Boolean(p));
|
|
366
|
+
const pointsB = offsetsB
|
|
367
|
+
.map((o) => path.getPointAt(o))
|
|
368
|
+
.filter((p): p is paper.Point => Boolean(p));
|
|
369
|
+
|
|
370
|
+
if (pointsA.length < 2 || pointsB.length < 2) {
|
|
371
|
+
throw new Error("Geometry: Bridge contour sampling failed");
|
|
372
|
+
}
|
|
198
373
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
374
|
+
let topBase = selectOuterChain({
|
|
375
|
+
mainShape,
|
|
376
|
+
pointsA,
|
|
377
|
+
pointsB,
|
|
378
|
+
delta,
|
|
379
|
+
overlap,
|
|
380
|
+
op: f.operation,
|
|
381
|
+
});
|
|
202
382
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
383
|
+
const dist2 = (a: paper.Point, b: paper.Point) => {
|
|
384
|
+
const dx = a.x - b.x;
|
|
385
|
+
const dy = a.y - b.y;
|
|
386
|
+
return dx * dx + dy * dy;
|
|
207
387
|
};
|
|
208
388
|
|
|
209
|
-
if (
|
|
210
|
-
|
|
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
|
-
}
|
|
389
|
+
if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
|
|
390
|
+
topBase = topBase.slice().reverse();
|
|
229
391
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
}
|
|
392
|
+
|
|
393
|
+
topBase = topBase.slice();
|
|
394
|
+
topBase[0] = leftHit.point;
|
|
395
|
+
topBase[topBase.length - 1] = rightHit.point;
|
|
396
|
+
|
|
397
|
+
const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
|
|
398
|
+
const topPoints = topBase.map(
|
|
399
|
+
(p) => p.add(new paper.Point(0, capShiftY)),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const bridgeBottomY = bridgeBottom + overlap * 2;
|
|
403
|
+
const bridgePoly = new paper.Path({ insert: false });
|
|
404
|
+
for (const p of topPoints) bridgePoly.add(p);
|
|
405
|
+
bridgePoly.add(new paper.Point(xRight, bridgeBottomY));
|
|
406
|
+
bridgePoly.add(new paper.Point(xLeft, bridgeBottomY));
|
|
407
|
+
bridgePoly.closed = true;
|
|
408
|
+
|
|
409
|
+
const unitedItem = item.unite(bridgePoly);
|
|
410
|
+
item.remove();
|
|
411
|
+
bridgePoly.remove();
|
|
412
|
+
|
|
413
|
+
if (f.operation === "add") {
|
|
414
|
+
adds.push(unitedItem);
|
|
415
|
+
} else {
|
|
416
|
+
subtracts.push(unitedItem);
|
|
263
417
|
}
|
|
264
418
|
} else {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
419
|
+
if (f.operation === "add") {
|
|
420
|
+
adds.push(item);
|
|
421
|
+
} else {
|
|
422
|
+
subtracts.push(item);
|
|
423
|
+
}
|
|
270
424
|
}
|
|
271
425
|
} else {
|
|
272
426
|
if (f.operation === "add") {
|
|
@@ -284,7 +438,7 @@ function getPerimeterShape(options: GeometryOptions): paper.PathItem {
|
|
|
284
438
|
const temp = mainShape.unite(item);
|
|
285
439
|
mainShape.remove();
|
|
286
440
|
item.remove();
|
|
287
|
-
mainShape = temp;
|
|
441
|
+
mainShape = normalizePathItem(temp);
|
|
288
442
|
} catch (e) {
|
|
289
443
|
console.error("Geometry: Failed to unite feature", e);
|
|
290
444
|
item.remove();
|
|
@@ -299,7 +453,7 @@ function getPerimeterShape(options: GeometryOptions): paper.PathItem {
|
|
|
299
453
|
const temp = mainShape.subtract(item);
|
|
300
454
|
mainShape.remove();
|
|
301
455
|
item.remove();
|
|
302
|
-
mainShape = temp;
|
|
456
|
+
mainShape = normalizePathItem(temp);
|
|
303
457
|
} catch (e) {
|
|
304
458
|
console.error("Geometry: Failed to subtract feature", e);
|
|
305
459
|
item.remove();
|
|
@@ -340,12 +494,12 @@ function applySurfaceFeatures(
|
|
|
340
494
|
const temp = result.unite(item);
|
|
341
495
|
result.remove();
|
|
342
496
|
item.remove();
|
|
343
|
-
result = temp;
|
|
497
|
+
result = normalizePathItem(temp);
|
|
344
498
|
} else {
|
|
345
499
|
const temp = result.subtract(item);
|
|
346
500
|
result.remove();
|
|
347
501
|
item.remove();
|
|
348
|
-
result = temp;
|
|
502
|
+
result = normalizePathItem(temp);
|
|
349
503
|
}
|
|
350
504
|
} catch (e) {
|
|
351
505
|
console.error("Geometry: Failed to apply surface feature", e);
|