partforge 0.85.1 → 0.85.3

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.
@@ -521,6 +521,18 @@ Variant literal for a curve-adjacent corner: `filletProfile: corner <i> at (<x>,
521
521
  - **Cause:** two sharp-tagged vertices (or two contour corners) in the same section are numerically coincident, so the arc between them has zero length — e.g. tagging *both* ends of a closed NACA trailing edge, whose closure coefficient already brings them to the same point (or the same point after a per-section `rotate`). This also fires for the v1 meaning of the message: a genuinely degenerate section.
522
522
  - **Fix:** tag only one of the coincident vertices as the corner (see `src/parts/propeller.js`'s `sharpTE` comment for why a second tag there would create exactly this zero-length arc). See [AUTHORING-PARTS.md](AUTHORING-PARTS.md)'s `k.loftSmooth` row.
523
523
 
524
+ ## loftsmooth-stations-out-of-range
525
+
526
+ - **Symptom:** `loftSmooth: stations must be 2…1024 (or "controls")` — an explicit `stations` was rejected.
527
+ - **Cause:** `stations` is clamped to 2…1024 (a preview/export density guard); a non-integer, non-finite, or out-of-range value throws. The default (8 per span + 1 open, 8 per section closed) caps itself at 1024, so omitting the option never trips this.
528
+ - **Fix:** pass an integer in 2…1024, or omit `stations` for the capped default. More rings than 1024 along the spine is a density smell — the surface is already spline-smooth between stations; raise `samples` instead if the banding is around the ring. See the `loftSmooth` row in [KERNEL-CONTRACT.md](KERNEL-CONTRACT.md).
529
+
530
+ ## loftsmooth-samples-out-of-range
531
+
532
+ - **Symptom:** `loftSmooth: samples must be 8…2048` — an explicit `samples` was rejected.
533
+ - **Cause:** `samples` is clamped to 8…2048. The default `max(64, largest section)` caps itself at 2048, so omitting the option never trips this — even when a control section has more than 2048 points.
534
+ - **Fix:** pass a `samples` value in 8…2048, or omit it for the capped default — and thin any control section above ~2048 points: `loftSmooth` interpolates a smooth outline through sparse control points, so sections that dense defeat the sparse-sections design (pass the dense rings straight to `k.loft` instead). See the `loftSmooth` row in [KERNEL-CONTRACT.md](KERNEL-CONTRACT.md).
535
+
524
536
  ## duplicate-preset-name-throws
525
537
 
526
538
  - **Symptom:** `duplicate preset name across sections:` thrown from verify/measure, naming the repeated preset (e.g. `duplicate preset name across sections: "Compact"`).
@@ -288,7 +288,7 @@ above. All ops return a `Solid`.
288
288
  | `sweep({profile, path, closed?, cornerRadius?, ruled?, smooth?})` | Sweep a fixed CCW profile along a polyline with a rotation-minimizing frame; sharp mitered corners, or `cornerRadius` fillets; capped ends. |
289
289
  | `helixSweptTube({pathR, profileR, pitch, turns, z0, lefthand})` | Circle of radius `profileR` swept along a helix (e.g. a rope groove). Circular profile on a frenet frame that rolls with the helix — **not for threads**; use `screwSweep`. |
290
290
  | `screwSweep({profile, pitch, turns, lefthand})` | Screw-motion sweep of an axial lathe profile `[[r, z], …]` (r ≥ 0) — threads. The profile travels to `(r·cosθ, r·sinθ, z + pitch·θ/2π)`; `h = pitch · turns`. Axial extent must not exceed `pitch` or consecutive turns interpenetrate (throws). A profile spanning exactly `pitch` is **periodic**: first and last radius must agree, and it yields a complete threaded body needing no boolean. Compound: the polar-remapped, densified section extruded with `twist = 360 · turns`, exactly as composed in `kernel-front.js`; a backend may override only for caching, never for different geometry. Options-only. Parity: **within tolerance, not by construction** — both backends receive the identical densified polygon, but the mesh backend facets the twist at its own resolution while the B-rep backend builds an exact spline (`hull`'s parity class). |
291
- | `loftSmooth({sections, stations?, samples?, shading?, closed?})` | Spline-interpolated loft of ≥2 sparse control sections — loft-style ring specs `{polygon\|sides+radius\|curve contour\|Shape2D, z, rotate?, scale?, sharp?}`; vertex counts **may differ**. A point section may tag `sharp: [indices]` as true corners (integers in `0…points.length-1`, sorted/deduped silently); a curve/`Shape2D` section takes corners implicitly from its non-smooth joints (single-region, hole-free, `loftSmooth:`-prefixed `k.loft` validation) and rejects an explicit `sharp`. Every section must resolve to the **same corner count `m`** (frozen error otherwise); with `m ≥ 1` corner 0 anchors the seam (replacing vertex 0), with `m = 0` v1's vertex-0 anchor holds verbatim. Compound (`kernel-front.js` + `loft-smooth.js`): each section's outline is a closed centripetal Catmull-Rom split into `m` clamped open arcs at its corners (or one closed periodic CR when `m = 0`); the `samples` budget is apportioned across arcs by mean arc-length fraction (largest-remainder, min 1 span/arc) and each arc resampled by arc length — total ring vertex count is `samples`, identical across sections, exactly v1's invariant now corner-anchored. The cross-station direction is v1 verbatim (shared centroid-spine knots, per-vertex CR, reflection phantoms at the ends, or periodic knots when `closed: true`). What's new is emission: every station — the dense list and the sparse `stations:"controls"` list alike — is fitted back to an **all-cubic Bézier contour**, arc-by-arc, via exact 4-point CR→Bézier inversion, so **both backends receive identical curve rings**. A B-rep kernel lofts the sparse control wires with its native smooth skin (`ruled: false`) — curve-exact around each ring in STEP (the densified-*point*-wire alternative measured 23 s / WASM-abort territory, which curve wires don't hit). A mesh kernel densifies `stations` rings and lofts them through `k.loft`'s curve-mode per-segment sampling, creasing sharp/corner columns via loft's geometric corner policy. `closed: true` (default false; needs ≥3 control sections, frozen error otherwise) makes the cross-station CR periodic (no reflection phantoms, ring 0 not repeated) and is **Manifold-only**, same restriction as `loft` `closed: true`: a B-rep kernel throws `loftSmooth: closed:true loops are only supported on the Manifold backend` in the composition, before building any rings; combining `closed: true` with `stations:"controls"` is rejected as a defensive invariant (reachable only by explicitly passing the internal `stations:"controls"` value; the composition never produces the combination itself). Options-only. Defaults `stations = (n−1)·8+1` open / `n·8` closed (raised to the section count `n` when lower), `samples = max(64, largest section)` (raised to the corner count `m` when lower); clamps 2…1024 / 8…2048. The surface interpolates every control section exactly. Parity: **within tolerance** (`screwSweep`'s class, unchanged from v1 — ~0.4% measured on the propeller reference part, test-gated at 2%). STEP is now curve-exact around each ring (previously faceted at the `samples` LOD); the cross-station skin remains ThruSections' native fit, not the shared CR — exact cross-station B-splines are a v3 candidate. Additive: `sharp`, curve/`Shape2D` sections, and `closed` are new options on top of v1's `{sections, stations?, samples?, shading?}`; `CONTRACT_VERSION` stays 4 — the same non-bump precedent as `import` above, a refinement inside the op's already-stated tolerance class rather than a new one. |
291
+ | `loftSmooth({sections, stations?, samples?, shading?, closed?})` | Spline-interpolated loft of ≥2 sparse control sections — loft-style ring specs `{polygon\|sides+radius\|curve contour\|Shape2D, z, rotate?, scale?, sharp?}`; vertex counts **may differ**. A point section may tag `sharp: [indices]` as true corners (integers in `0…points.length-1`, sorted/deduped silently); a curve/`Shape2D` section takes corners implicitly from its non-smooth joints (single-region, hole-free, `loftSmooth:`-prefixed `k.loft` validation) and rejects an explicit `sharp`. Every section must resolve to the **same corner count `m`** (frozen error otherwise); with `m ≥ 1` corner 0 anchors the seam (replacing vertex 0), with `m = 0` v1's vertex-0 anchor holds verbatim. Compound (`kernel-front.js` + `loft-smooth.js`): each section's outline is a closed centripetal Catmull-Rom split into `m` clamped open arcs at its corners (or one closed periodic CR when `m = 0`); the `samples` budget is apportioned across arcs by mean arc-length fraction (largest-remainder, min 1 span/arc) and each arc resampled by arc length — total ring vertex count is `samples`, identical across sections, exactly v1's invariant now corner-anchored. The cross-station direction is v1 verbatim (shared centroid-spine knots, per-vertex CR, reflection phantoms at the ends, or periodic knots when `closed: true`). What's new is emission: every station — the dense list and the sparse `stations:"controls"` list alike — is fitted back to an **all-cubic Bézier contour**, arc-by-arc, via exact 4-point CR→Bézier inversion, so **both backends receive identical curve rings**. A B-rep kernel lofts the sparse control wires with its native smooth skin (`ruled: false`) — curve-exact around each ring in STEP (the densified-*point*-wire alternative measured 23 s / WASM-abort territory, which curve wires don't hit). A mesh kernel densifies `stations` rings and lofts them through `k.loft`'s curve-mode per-segment sampling, creasing sharp/corner columns via loft's geometric corner policy. `closed: true` (default false; needs ≥3 control sections, frozen error otherwise) makes the cross-station CR periodic (no reflection phantoms, ring 0 not repeated) and is **Manifold-only**, same restriction as `loft` `closed: true`: a B-rep kernel throws `loftSmooth: closed:true loops are only supported on the Manifold backend` in the composition, before building any rings; combining `closed: true` with `stations:"controls"` is rejected as a defensive invariant (reachable only by explicitly passing the internal `stations:"controls"` value; the composition never produces the combination itself). Options-only. Defaults `stations = (n−1)·8+1` open / `n·8` closed (raised to the section count `n` when lower), `samples = max(64, largest section)` (raised to the corner count `m` when lower); clamps 2…1024 / 8…2048 — the defaults cap themselves at the ceilings, only explicit out-of-range values throw. The surface interpolates every control section exactly. Parity: **within tolerance** (`screwSweep`'s class, unchanged from v1 — ~0.4% measured on the propeller reference part, test-gated at 2%). STEP is now curve-exact around each ring (previously faceted at the `samples` LOD); the cross-station skin remains ThruSections' native fit, not the shared CR — exact cross-station B-splines are a v3 candidate. Additive: `sharp`, curve/`Shape2D` sections, and `closed` are new options on top of v1's `{sections, stations?, samples?, shading?}`; `CONTRACT_VERSION` stays 4 — the same non-bump precedent as `import` above, a refinement inside the op's already-stated tolerance class rather than a new one. |
292
292
  | `union(solids[])` | Boolean union of one or more solids. |
293
293
  | `text2d(string, {size, font?, align?, valign?, lineHeight?, tracking?, kerning?})` | Outline-font text → `Shape2D`. `size` = cap height (mm). `font` = declared name / inline bytes / default. Build-time; curve-exact on OCCT, faceted on Manifold. |
294
294
  | `hull(inputs[])` | Convex hull of all inputs (each a `Shape2D`, a curve contour, or an `[[x,y],…]` point list) → a convex `Shape2D`. Backend-agnostic: a pure-JS monotone-chain hull over the inputs' sampled points (curved inputs tessellated at a fixed LOD), lifted via `shape2d` (see the parity note below). Throws on an empty input array or a degenerate (collinear/point-count < 3) hull. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.85.1",
3
+ "version": "0.85.3",
4
4
  "description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -303,13 +303,14 @@ function reconcile(resolved, V) {
303
303
  * corners implicitly.
304
304
  * @param {{stations?: number|"controls", samples?: number, closed?: boolean}} opts
305
305
  * stations — output ring count along the spine (default 8 per span + 1 open,
306
- * 8 per section closed; ≥ 2; raised to the section count when lower; every
307
- * control knot is always emitted). The string "controls" skips cross-station
306
+ * 8 per section closed, capped at 1024; ≥ 2; raised to the section count when
307
+ * lower; every control knot is always emitted). The string "controls" skips cross-station
308
308
  * interpolation entirely and emits one ring per control section at its own z
309
309
  * — the B-rep path, where the backend's native smooth loft (`ruled: false`)
310
310
  * does the skinning through exact wires and only the around-ring
311
311
  * reconciliation is needed; incompatible with closed:true;
312
- * samples — output vertex count around each ring (default max(64, largest section));
312
+ * samples — output vertex count around each ring (default max(64, largest
313
+ * section), capped at 2048);
313
314
  * closed — periodic spine: the spline wraps from the last control section back
314
315
  * to the first (no duplicate ring at the wrap point), needs ≥ 3 sections.
315
316
  * @returns {Array<{polygon: {start, segments}, z: number}>}
@@ -320,8 +321,11 @@ export function smoothLoftRings(sections, { stations, samples, closed = false }
320
321
  if (closed && stations === "controls")
321
322
  throw new Error('loftSmooth: closed:true cannot combine with stations:"controls"');
322
323
  if (closed && n < 3) throw new Error("loftSmooth: closed:true needs at least 3 control sections");
323
- const S = stations ?? (closed ? n * 8 : (n - 1) * 8 + 1);
324
- const V = samples ?? Math.max(64, ...resolved.map((r) => r.pts2d.length));
324
+ // The defaults cap at the clamp ceilings: at 129+ sections the raw 8-per-span
325
+ // stations (and at a 2049+-point section the raw largest-section samples)
326
+ // would trip the range check on an option the caller never passed.
327
+ const S = stations ?? Math.min(closed ? n * 8 : (n - 1) * 8 + 1, 1024);
328
+ const V = samples ?? Math.min(Math.max(64, ...resolved.map((r) => r.pts2d.length)), 2048);
325
329
  if (stations !== "controls" && !(Number.isFinite(S) && S >= 2 && S <= 1024))
326
330
  throw new Error('loftSmooth: stations must be 2…1024 (or "controls")');
327
331
  if (!(Number.isFinite(V) && V >= 8 && V <= 2048)) throw new Error("loftSmooth: samples must be 8…2048");
package/types/kernel.d.ts CHANGED
@@ -399,9 +399,9 @@ export interface LoftSmoothSection {
399
399
  export interface LoftSmoothOptions {
400
400
  /** Sparse control sections; vertex counts may differ between sections. */
401
401
  sections: LoftSmoothSection[];
402
- /** Output ring count along the spine (default 8 per span + 1; closed: 8 per section). */
402
+ /** Output ring count along the spine (default 8 per span + 1; closed: 8 per section; capped at 1024). */
403
403
  stations?: number;
404
- /** Output vertex count around each ring (default max(64, largest section)). */
404
+ /** Output vertex count around each ring (default max(64, largest section), capped at 2048). */
405
405
  samples?: number;
406
406
  shading?: "smooth" | "faceted";
407
407
  /** Capless loop — Manifold only, ≥3 sections. */