Skip to content

feat(brep): conoid surfaces (radial rulings over a cylinder axis) - #43

Open
Basnederveen wants to merge 10 commits into
OpenGeometry-io:mainfrom
Bas-Automation-Services:pr/conoid
Open

Basnederveen wants to merge 10 commits into
OpenGeometry-io:mainfrom
Bas-Automation-Services:pr/conoid

Conversation

@Basnederveen

Copy link
Copy Markdown

Stacked on #36, #38 and #40. This branch merges all three: the conoid reuses #38's wrapped-surface plumbing and STEP B-spline fitting, its new pcurve helper lives in #36's public builder, and its test solid is built in #40's standard loop winding. Until they land, the diff includes them; the conoid itself is the last three commits:
feat(brep): conoid surfaces, test(support): read bicubic B-spline surfaces in the Part 21 checker, test: a cylindrical shell cut by a conoid.

What

A new surface kind: the right conoid over a cylinder axis.

SurfaceGeometry::Conoid { frame, c } with

S(u, v) = O + v (cos u x + sin u y) + (c0 + c1 cos u + c2 sin u) z

u is the angle around the axis frame.z, v the distance from it. Every ruling is a radial line perpendicular to the axis.

Why: a bend (a cylindrical shell) cut "square to the sheet" by a plane, as at a mitered corner, keeps whole radial lines through the thickness while one face's points lie on the plane. That cut face is exactly this conoid, and it has no analytic support today. Its edges at constant radius are planar sections of the cylinders, so they are exact ellipses.

  • conoid_ellipse(frame, c, radius) -> (CurveGeometry::Ellipse, phase): the curve at a radius as an exact ellipse, with ellipse parameter t = u - phase. On the conoid its pcurve is uv_line([phase, radius], [1, 0]); the rulings are uv_line([u, r0], [0, 1]).
  • projected_boundary(builder, surface, edge, from, to, sense, hint): a public helper that builds a ProjectedCurve pcurve for an edge on a face (here the ellipse on the bend's cylinder). A caller outside the crate could not build one before, since curve ids are internal. The builder now points a projected pcurve at the surface of the face it bounds.

Support across the kernel:

  • Geometry: point, first and second derivatives, normal S_u x S_v, UV and interval bounds. Projection starts from atan2 of the local (x, y) and the radial distance (exact for points on the surface), lifted by the hint like the cylinder, and refines off-surface points with Newton.
  • Tessellation: the grid is sized for the surface's twist. With the face's exact UV rectangle as its trim bounds, a quarter-turn cut meshes in about 4.7k triangles at deflection 1e-3.
  • Point classification and ray roots, placement (rigid moves, uniform scaling, mirroring).
  • STEP export: a bicubic B-spline fit over the faces' UV, as for wrapped surfaces, charged to the exchange budget. An exact rational form was not used: a rational quadratic arc is not parameterised by angle, so the conoid's Line2 pcurves would no longer hold, and the exporter has no rational B-spline writer. Ellipse edges with a projected pcurve are written as plain 3D ellipses.
  • Refused with UnsupportedGeometry: booleans, shells, SSI and IFC export, as for wrapped geometry.

Compatibility:

  • JSON gains a "kind": "Conoid" surface; existing bodies read unchanged.
  • In Rust, SurfaceGeometry gains a variant, so an exhaustive match downstream needs a new arm (two tests in this repo did).
  • Nothing changes on the TypeScript side.

Tests

  • Per-surface tests: the conoid joins the existing family list (derivatives, projection, normals, bounds). New unit tests check that rulings are straight and radial, that conoid_ellipse is planar and lies on the surface, off-surface projection, and period lifting.
  • tests/conoid_bend.rs (4 tests): a quarter-turn shell, radii 2 to 4, with one end cut by the conoid of the plane x - y = k through its inner face, built through the public builder with projected pcurves on the cylinders. It validates as a closed solid; its mesh volume matches the Simpson-integrated exact volume within 2e-4 at deflection 1e-3; points classify Inside and Outside; and its STEP export re-reads through the test-support Part 21 checker, which can now read B-spline surfaces and checks the pcurves on the fitted face.
  • cargo fmt --check, cargo clippy --all-targets and cargo test pass on Windows: 390 crate tests, conoid_bend 4, and every other target as on the stacked branches. The only failures are ones that predate this PR: 3 stored-fixture tests in cases and 1 in golden (they fail on main too), and the 2 IFC oracle tests in step_oracle (one fails on main, the other with fix(brep): standard loop winding, enforced by validate() #40's winding alone). npm run check:source, check:duplicates and check:cycles pass. Browser runs and the golden comparison were not run (golden files are recorded on macOS).

Files

  • New: src/brep/geometry/conoid.rs, tests/conoid_bend.rs.
  • Changed: the surface match sites (geometry, store, tessellation grid/trim/area bound, classification, ray roots, placement, STEP body, preflight, IFC, SSI, booleans), brep/builder/boundary.rs (projected_boundary), test-support Part 21 reader.

No behaviour changes for existing geometry: the new code paths are only reached by the new surface kind.

Basnederveen and others added 10 commits November 1, 2026 11:28
A crate that depends on OpenGeometry and needs a solid the primitives
don't cover can only build one by writing BRep JSON by hand. The builder
the kernel uses for every primitive and boolean result is crate-private.

- brep::{Builder, Use, boundary, plane_boundary, uv_line} are public:
  Builder::{new, vertex, edge, face, face_with_holes, finish_solid}
- face() and face_with_holes() return the new face id
- Builder::brep() gives read access while building
- Builder::set_face_sense() sets a face's sense by id
- the types those signatures need are public: brep::Orientation,
  brep::PcurveGeometry (and IntersectionSide), math::Interval::new
- the 14 internal tail calls of face() now end in `?; Ok(())`

tests/public_brep_builder.rs builds a box through the public API only:
valid, volume 24, inside/outside classification, STEP export; a face
sense set by id flips face_normal_at; an edge used by a third loop is
rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZvqx1RmXzPtbL9VXP4H7
…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
Face loops now follow the standard B-rep / STEP rule (ISO 10303-42):
seen against the face's material normal (surface normal times sense),
outer loops run counter-clockwise and holes clockwise, so the face lies
to the left of every loop. Most producers already did; the tube builders
(cylinder_with_circular_hole, annular_cylinder) wound the cap holes like
the outer loops and gave the inner wall the outer wall's loop before
setting its sense to Reverse, so their STEP export wrote the hole bounds
and the inner wall's bound running the wrong way. Boolean results that
keep those faces inherited them.

- validate() rejects a face whose loops run against its material normal,
  measured as signed area in UV (continuous across chart seams and inside
  an intersection pcurve; two loops bounding a seamless band are judged
  against each other). Straight pcurves on planes are measured from their
  ends.
- orient_loops() (crate-internal) reverses misoriented loops in place.
  Builder::finish_solid, the sweep builder and the boolean result sites
  call it before validate().
- from_json() turns the loops of bodies written with the old winding, so
  saved bodies keep loading.

The check refuses the cone-sphere intersection of
generic_closed_loop_handles_periodic_winding: on main it keeps the sphere
cap outside the cone (a point inside both operands classifies Outside),
and its cone patch's loop runs against the patch's normal. The test now
asserts the refusal.

Stored fixtures edited by hand because their bodies had the old winding:
only the half-edges' from/to/next/prev/sense and the vertices'
outgoing_halfedge change, every number is byte-identical (tube bodies,
validity builders, five boolean-matrix and six batch-matrix results, the
annular-first input). The tube STEP texts change in four ORIENTED_EDGE
senses each (the two hole bounds and the inner wall's circles). The tube
tessellations list the reversed loops' vertices the other way round;
eight positions differ by one ULP from sampling the circles in the other
direction, and were recorded on x86_64 Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZvqx1RmXzPtbL9VXP4H7
SurfaceGeometry::Conoid { frame, c } is the right conoid
S(u, v) = origin + v (cos u x + sin u y) + (c0 + c1 cos u + c2 sin u) z,
with UV = (angle around the axis, distance from it). Its rulings (u fixed)
are radial lines perpendicular to the axis; its curves v = r are the planar
ellipses where the cylinder of radius r meets z = c0 + (c1 x + c2 y) / r.
It describes a cut through a cylindrical shell that keeps every radial line
through the wall whole while one wall face is cut by a plane.

- geometry: exact point, first and second derivatives, normal (S_u x S_v),
  conservative bounds; projection starts from the exact angle and radius
  (lifted by the UV hint like the other revolved kinds) and refines by Newton
  for points off the surface.
- conoid_ellipse(frame, c, radius) returns a constant-radius curve as an exact
  CurveGeometry::Ellipse plus its phase (ellipse t = conoid u - phase), so the
  edge is a Line2 pcurve on the conoid.
- builder: projected_boundary() gives a boundary use a ProjectedCurve pcurve
  (curve id, range, UV hint and rate from sampled projections), so callers
  outside the crate can bound e.g. a cylinder face by an elliptic edge; the
  builder points projected pcurves at the surface of the face they bound.
- tessellation: grids sized from sampled second derivatives, with the radial
  count also following the twist |S_uv|; trimmed conoid faces refine edge by
  edge (an axis-aligned grid would leave T-junctions between ellipse edges
  sampled differently).
- query: exact ray roots from the quartic (z - c0)^2 (x^2 + y^2) =
  (c1 x + c2 y)^2, keeping roots on the surface itself.
- placement: rigid motion and uniform scaling (c scales), mirroring
  (c2 changes sign with the reflected chart).
- STEP: fitted as a bicubic B-spline over the faces' UV, like wrapped
  surfaces, so the faces' (u, v) pcurves stay valid; charged to the
  exchange budget.
- preflight numbers and validation (finite coefficients, orthonormal frame).
- booleans, shells, SSI and IFC refuse it with UnsupportedGeometry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZvqx1RmXzPtbL9VXP4H7
The checker rejected B_SPLINE_SURFACE_WITH_KNOTS, so fitted surfaces could
not be re-read. It now evaluates piecewise-Bezier bicubic surfaces (clamped
knots, interior multiplicity 3, as the exporter writes them), which lets the
pcurve check sample pcurves on fitted faces against their 3D edges.

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

A quarter turn of a shell with radii 2..4 whose one end is a right conoid
derived from the plane x - y = k cutting the inner face, the other end a
plane, with cylinder faces bounded by the conoid's ellipses (projected
pcurves) and two planar sides. It validates as a closed solid with either an
exact or a padded conoid trim, meshes to its numerically integrated volume,
classifies points on both sides of the conoid, exports to STEP that re-reads
with its pcurves checked, round-trips through JSON, moves, scales and mirrors
to the right volume, and is refused by booleans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFZvqx1RmXzPtbL9VXP4H7
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