Skip to content

feat(brep): wrapped curves and surfaces (exact geometry bent around an axis) - #38

Open
Basnederveen wants to merge 2 commits into
OpenGeometry-io:mainfrom
Bas-Automation-Services:feat/wrapped-geometry
Open

Basnederveen wants to merge 2 commits into
OpenGeometry-io:mainfrom
Bas-Automation-Services:feat/wrapped-geometry

Conversation

@Basnederveen

Copy link
Copy Markdown

What

New curve and surface family: geometry given in a flat space and bent around a cylinder axis, the way a sheet metal bend wraps a flat sheet.

Wrap { frame, radius } maps a flat point (x, y, z) to frame.point([z cos(x/r), z sin(x/r), y]). x is arc length on radius r, y runs along the axis, z is the distance from the axis, so lines along flat z become radial lines. The map is exact and preserves orientation (Jacobian determinant z / r).

  • CurveGeometry::Wrapped { wrap, curve } wraps a line, circle or ellipse.
  • SurfaceGeometry::Wrapped { wrap, surface } wraps any elementary surface.

Both keep the flat geometry's parameters and charts, so pcurves are written in the flat surface's UV.

Why: a cut through a bend has no analytic support. A round hole through a bend has a wrapped cylinder as its wall and wrapped circles as its edges. A slanted cut is a wrapped plane bounded by helices (wrapped lines). With this family such parts are exact B-reps rather than approximations.

Support across the kernel:

  • Geometry: point, first and second derivatives (chain rule through the wrap), normal (cofactor of the Jacobian), projection (exact on the surface; the angle is taken in (-pi, pi] around frame.x unless a hint picks the turn), and conservative interval bounds for curves and surfaces.
  • Tessellation:
    • Grid sizes come from the exact second derivatives, sampled on a 9 x 9 grid of the patch with a 1.5x margin. A strict closed-form bound is far too loose along the rulings.
    • Periodic parameters keep the existing turn floor.
    • Wrapped circles and ellipses get the same per-turn floor as circles.
    • The area error bound reports infinity for wrapped faces (no closed-form bound yet).
  • Point classification: ray roots on a wrapped face come from UV subdivision while the face's conservative box meets the ray, then Newton from each small box. There is no implicit form.
  • STEP export:
    • Wrapped surfaces become bicubic B_SPLINE_SURFACE_WITH_KNOTS over the UV of all faces on that support. The knots are the surface's own parameters, so pcurves stay valid.
    • Wrapped edges become cubic B_SPLINE_CURVE_WITH_KNOTS over every range an edge uses.
    • Both are Hermite fits, checked against the exact geometry at 16 samples per span or patch side, and their error is charged to the exchange budget.
  • Placement: rigid moves only. Scaling wrapped geometry is refused.
  • Refused with UnsupportedGeometry: booleans, shells, SSI, mirroring and IFC export of wrapped geometry.

Compatibility:

  • JSON gains a "kind": "Wrapped" for curves and surfaces, and existing bodies read unchanged.
  • In Rust, CurveGeometry and SurfaceGeometry gain a variant, so an exhaustive match over them in downstream code needs a new arm. Two integration tests in this repo did (tests/primitives.rs, tests/creating/sweep_bends.rs).
  • Nothing changes on the TypeScript side.

Robustness

  • A fitted surface reaches 10% past its faces in each direction, but never around a periodic direction onto itself (a hole wall spans a full turn). Some CAD readers rebuild a face's trim by intersecting its neighbouring surfaces and ignore the pcurves. A B-spline that ends exactly on the face's edges makes such a reader trim short, by up to the fit error.
  • A wrapped surface cannot wrap another wrapped surface. A wrapped curve must wrap a line, circle or ellipse.
  • The normal is refused (SingularParameterization) on the axis, where z <= 0.
  • Ray classification gives up with Unknown after 50,000 boxes rather than guessing.
  • The fitters stop at a span or patch budget with LimitExceeded, and STEP refuses when the exchange budget leaves no room for fitting.

Tests

  • Per-family geometry tests: two wrapped surfaces join the existing family list (a hole wall through a bend, a slanted cut), covering derivatives, normals, projection and conservative boxes. A new test checks that the wrap keeps arc length on its radius, round-trips through flatten, and that a wrapped line is a constant-speed helix with a correct tangent and box. Another checks that wrapped circle samples meet their chord error.
  • Fitting: a wrapped plane and a wrapped circle fit within tolerance between their samples.
  • A bend sector with a slanted wrapped side (a builder fixture, 6 faces):
    • its mesh volume matches the exact volume (flat area times the radial integral of z / r) to 1e-4;
    • points on either side of the wrapped wall classify Inside and Outside;
    • it exports to STEP with one B-spline surface, two B-spline curves and a pcurve per edge use, within the exchange budget;
    • it moves rigidly, and refuses scaling, mirroring, booleans and shelling.
  • On Windows with CI=true: cargo fmt --check, cargo clippy --all-targets -D warnings (kernel and test-support) and cargo test --all-targets pass, including the source_rules binary. That's 382 crate tests here, against 373 on main. npm run check:source, check:duplicates and check:cycles pass. Browser runs and the golden comparison were not run, because golden files are only recorded on macOS.

Files

  • New:
    • src/brep/geometry/wrap.rs
    • src/query/wrapped_roots.rs
    • src/tessellation/wrapped.rs
    • src/exchange/fit_bezier/
    • src/exchange/step/wrapped.rs
    • src/brep/builder/fixtures.rs (test only)
  • Variant arms: brep/geometry/{curve,surface,store,turned}.rs, brep/placement/, query/{classify,ray_roots}.rs, tessellation/{area_bound,deviation,grid,trim}.rs, intersection/ssi/, intersection/universal_ssi/implicit_bounds.rs, operations/modifying/boolean/{dispatch,batch,shell,handlers/sphere_sphere}, exchange/{preflight,step,ifc_body}.
  • lift moved from surface.rs to turned.rs, so wrap.rs can share it without a sibling import cycle.
  • tests/primitives.rs, tests/creating/sweep_bends.rs: a "wrapped" arm in their surface-kind helpers.

🤖 Generated with Claude Code

Basnederveen and others added 2 commits November 1, 2026 12:15
…n axis)

A Wrap maps flat space onto a cylinder the way a sheet metal bend wraps a
flat sheet: (x, y, z) -> frame.point([z cos(x/r), z sin(x/r), y]). x is arc
length on radius r, y runs along the axis, z is the distance from it.
CurveGeometry::Wrapped and SurfaceGeometry::Wrapped wrap a line, circle,
ellipse or any elementary surface exactly and keep its parameters and charts.
A hole through a bend is a wrapped cylinder bounded by wrapped circles; a
slanted cut is a wrapped plane bounded by helices.

- geometry: point, derivatives (chain rule), normal (cofactor), projection
  (exact on the surface), conservative bounds for curves and surfaces.
- tessellation: grid sizes and chord deviation from the exact second
  derivatives, sampled with a 1.5x margin; arcs keep the per-turn floor.
- query: ray roots on wrapped faces by UV subdivision plus Newton.
- STEP: wrapped surfaces as bicubic B-splines over the support's face UV,
  reaching 10% past the faces (never around a period onto itself) so readers
  that rebuild trims by intersecting neighbouring surfaces don't trim short;
  wrapped edges as cubic B-splines; both Hermite fits checked against the
  exact geometry and charged to the exchange budget.
- placement moves wrapped geometry rigidly; scaling, mirroring, booleans,
  shells, SSI and IFC refuse it with UnsupportedGeometry.

Tests: wrapped families in the per-family geometry tests, fitting tests, and
a bend sector with a slanted wrapped side that meshes to its exact volume,
classifies points on both sides of the wall, exports to STEP within budget,
and refuses scaling, mirroring, booleans and shelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZvqx1RmXzPtbL9VXP4H7
@Basnederveen

Copy link
Copy Markdown
Author

Heads-up for merging this together with #37: there's no textual conflict, but the build fails once both are in. #37 adds raisable() in tessellation/loop_minimum.rs, which matches on CurveGeometry without the Wrapped variant from this PR:

error[E0004]: non-exhaustive patterns: `CurveGeometry::Wrapped { .. }` not covered
  --> src\tessellation\loop_minimum.rs:54:11

A wrapped curve is curved in 3D, so it should be raised in a non-planar loop just like an intersection curve:

CurveGeometry::Intersection { .. } | CurveGeometry::Wrapped { .. } => !planar,

With that one line, all seven open PRs (#35–#41) merged together pass fmt, clippy, the full test suite, check:source, check:duplicates and check:cycles.

I also pushed 8a22f57 to this PR. It puts the unsupported_wrapped import in boolean/shell.rs on its own line, so the file stays at 600 lines when #40's one-line addition lands next to it.

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