Skip to content

feat: add Loft primitive that morphs between two 2D cross-sections - #58

Open
hbehrensj wants to merge 3 commits into
Formsmith746:mainfrom
hbehrensj:feature/loft-shape
Open

feat: add Loft primitive that morphs between two 2D cross-sections#58
hbehrensj wants to merge 3 commits into
Formsmith746:mainfrom
hbehrensj:feature/loft-shape

Conversation

@hbehrensj

@hbehrensj hbehrensj commented Aug 12, 2026

Copy link
Copy Markdown

Summary

I built loftmorph as a standalone tool for lofting a
solid between two 2D cross-sections. Found SketchForge and wanted that as a real, parametric
primitive in the editor rather than an import-a-static-STL workaround — so this ports the
algorithm in as a new "Loft" shape next to Box/Cylinder/Gear.

Oval/Rectangle/Triangle/Pentagon/Hexagon on each end, independent size + rotation (top mirrors
how cone's Top Radius works independently of its base). Wired into every place "gear" touches,
including the MCP bridge's sketchforge_create_shape tool (kind: "loft" with its own params).

Corner-twist fix: the initial port sampled both ends at the same shared lab-frame angle, so
two same-cornered ends rotated relative to each other (e.g. two rectangles 25° apart) got walls
that twisted/bulged instead of connecting corner to corner. Replaced with an explicit best-corner
matching (ported from a fix in loftmorph itself, d91b3df) —
DP over a circular subsequence alignment that minimizes total angular mismatch, so each ring point
carries independent bottom/top angles instead of one shared angle. Went a bit further than the
literal reference in two spots, both found by a combinatorial smoke test across shape pairs and
rotations: the unequal-corner-count fallback (e.g. hexagon → rectangle) now interpolates between
the nearest matched neighbors instead of copying the other end's raw angle (which could land
outside the matched range and self-intersect), and the segment-filling interpolation advances each
angle stream by its own forward wraparound instead of assuming both streams wrap at the same seam.

Affects: storage (new loft* fields in .skf), export (STL/OBJ/STEP via the existing
boolean/export pipeline), and undo/redo (new fields added to the shape-equality check that backs
history dedup). No changes to grouping/import mechanics themselves.

Testing

  • npm run typecheck + test suite (loftGeometry.test.ts: closed/manifold + correct-winding
    checks, a rotational-symmetry invariant that caught the original centering bug, a corner-
    matching symmetry check — two squares 90° apart must reduce to an untwisted mesh — and a
    combinatorial smoke test across all shape pairs and a spread of relative rotations that
    caught the two robustness gaps above)
  • Manually tested: place, edit every property live, resize via gizmo, duplicate, undo/redo,
    use as a boolean hole, export STL and re-import it, save a project and reload the page,
    create a loft via the MCP bridge end to end (sketchforge_create_shape with kind: "loft")
  • Visual: rectangle → rectangle at 25° now shows clean diagonal corner-to-corner edges instead
    of a bulged/twisted wall (before/after screenshots on request)

Notes

  • Known limitation: a strongly-rotated, non-round end whose top/bottom sizes differ can extend
    slightly past its nominal bounding box, so the resize gizmo won't always hug the mesh exactly
    in that combination. Cosmetic only — doesn't affect the generated geometry.
  • The toolbar icon is a placeholder.
  • I'm the author of both loftmorph and this port, so no licensing conflict contributing under AGPL.
  • Everything in my contribution was generated with Claude Code.

hbehrensj and others added 2 commits August 12, 2026 11:33
Ports the loft/morph algorithm from hbehrensj/loftmorph as a native
parametric shape, mirroring the existing "gear" shape's integration
pattern (toolbar entry, geometry builder, viewport render + boolean/
export dispatch, inspector panel, .skf format validation).

Bottom and top ends are each independently configurable: shape (Oval/
Rectangle/Triangle/Pentagon/Hexagon), size (bottom follows the shape's
own width/depth like every other primitive; top has its own absolute
width/depth, mirroring how cone's topRadius works independently of its
base), and rotation. Corner-anchored angle sampling keeps polygon
corners crisp regardless of segment count or which two shapes are
morphing between each other.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gearGeometry.ts (the direct template for this file) has zero comments
across 240 lines; keep only the ones that prevent a real regression
(axis-swap/winding, why polygon ends aren't bbox-centered, and the
buildAngles corner-anchoring summary) and tighten those to a line or
two, per CONTRIBUTING.md's "keep comments short and useful".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@hbehrensj hbehrensj changed the title feat: add Loft primitive that morphs between two 2D cross-sectionsFeature/loft shape feat: add Loft primitive that morphs between two 2D cross-sections Aug 12, 2026
Ported from hbehrensj/loftmorph (commit d91b3df). The bottom/top ring
correspondence used to sample both ends at the same absolute angle, so
two same-cornered ends rotated relative to each other (e.g. two
rectangles 25° apart) got walls that twisted/bulged instead of
connecting corner to corner -- a bottom corner would land wherever the
top happened to be smooth. Replaces it with an explicit best-matching
correspondence (DP over a circular subsequence alignment, minimizing
total angular mismatch), so each ring point carries independent
aBottom/aTop angles instead of one shared angle.

Two robustness fixes beyond the literal reference port, both found via
a combinatorial smoke test across shape pairs and rotations:
- Unequal corner counts (e.g. hexagon -> rectangle) leave some corners
  unmatched; falling back to the other end's raw angle (as in the
  reference) can land far outside the matched neighbors' range and
  break monotonicity. Interpolating between the nearest matched
  neighbors instead keeps it in range.
- The segment-filling interpolation assumed both angle streams only
  wrap around at the same point (last anchor -> first). They can wrap
  independently, so each stream now advances by its own forward
  angular delta per segment instead of a single shared wrap check.

Also wires "loft" into MCP: the sketchforge_create_shape tool schema
and the create_shape command handler both recognize kind: "loft" with
its own parameters now, matching every other native shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant