partforge 0.85.2 → 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.
@@ -529,9 +529,9 @@ Variant literal for a curve-adjacent corner: `filletProfile: corner <i> at (<x>,
529
529
 
530
530
  ## loftsmooth-samples-out-of-range
531
531
 
532
- - **Symptom:** `loftSmooth: samples must be 8…2048` — possibly without ever passing `samples`.
533
- - **Cause:** `samples` is clamped to 8…2048. The default is `max(64, largest section)`, so a control section with more than 2048 points pushes the DEFAULT out of rangethe throw then names an option the caller never passed.
534
- - **Fix:** pass a `samples` value in 8…2048, and thin any control section above ~2048 points `loftSmooth` interpolates a smooth outline through the 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).
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 thiseven 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
535
 
536
536
  ## duplicate-preset-name-throws
537
537
 
@@ -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.2",
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",
@@ -309,7 +309,8 @@ function reconcile(resolved, V) {
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,10 +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
- // The default caps at the clamp ceiling: at 129+ sections the raw formula
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)
324
326
  // would trip the range check on an option the caller never passed.
325
327
  const S = stations ?? Math.min(closed ? n * 8 : (n - 1) * 8 + 1, 1024);
326
- const V = samples ?? Math.max(64, ...resolved.map((r) => r.pts2d.length));
328
+ const V = samples ?? Math.min(Math.max(64, ...resolved.map((r) => r.pts2d.length)), 2048);
327
329
  if (stations !== "controls" && !(Number.isFinite(S) && S >= 2 && S <= 1024))
328
330
  throw new Error('loftSmooth: stations must be 2…1024 (or "controls")');
329
331
  if (!(Number.isFinite(V) && V >= 8 && V <= 2048)) throw new Error("loftSmooth: samples must be 8…2048");
package/types/kernel.d.ts CHANGED
@@ -401,7 +401,7 @@ export interface LoftSmoothOptions {
401
401
  sections: LoftSmoothSection[];
402
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. */