Skip to content

feat(loft): preserve curved corners for two-profile lofts#797

Merged
andymai merged 2 commits into
mainfrom
feat/loft-preserve-curves
Jun 14, 2026
Merged

feat(loft): preserve curved corners for two-profile lofts#797
andymai merged 2 commits into
mainfrom
feat/loft-preserve-curves

Conversation

@andymai

@andymai andymai commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

loft sampled every profile to a polygon and built straight Line ring edges with planar side faces, so a rounded-rect loft became an octagonal frustum. A coincident-cap fuse of such a faceted frustum against an arc-cornered solid could then never annihilate the cap cleanly — the long-open fuse_coincident_rrect_cap_with_frustum (#790-residual) case.

This adds a curve-preserving path for two-profile lofts. When both profiles share the same boundary edge structure (same edge count, matching per-edge curve type) and wind CCW, the loft preserves each edge's curve: ring edges keep their EdgeCurve, arc corners become ruled side faces, and the caps are the real arc-edged profile wires. Mirrors the reference BRepFill_Generator:

  • Coaxial corner arcs (centers differ only along the axis) → exact Cylinder (equal radii) / Cone (unequal radii) — analytic surfaces the boolean handles robustly.
  • Genuinely non-coaxial corner arcs → a degree-(1,2) ruled NURBS.
  • Line edges keep planar quad side faces. Orientation is set per face against the outward direction.

Scope (deliberately conservative)

Gated to two profiles (a single ruled band). Multi-section curve lofts (e.g. a 5-section gridfinity lip) chain curved bands whose downstream cut/fuse/shell sequences hit near-degenerate corner-junction robustness gaps — those fall back to the existing polygon loft, which already handles them. So gridfinity and every other multi-profile loft are unchanged.

The deeper multi-section curved-corner boolean robustness (cut → fixed via cone/cylinder recognition on a WIP branch; fuse → isolated to near-degenerate tiny-corner junctions) is tracked for a future effort.

Testing

  • ✅ Closes fuse_coincident_rrect_cap_with_frustum (now passing, was #[ignore]d).
  • ✅ Adds loft_rounded_rect_preserves_arc_corners (10 faces, 4 NURBS arc corners, watertight, euler=2).
  • Zero regressions: operations 664, algo 113, io 156, wasm 211 (gridfinity 24). Clippy + layer boundaries clean.

Review note

Contained to the loft (operations L3), gated to the two-profile single-band case. It does feed curved-corner solids into the boolean, but only the well-conditioned single-band geometry the FF-trimming + edge-merge from #795 already handle. Not set to auto-merge — flagging for review given the history.

Loft previously sampled every profile to a polygon and built straight Line
ring edges with planar side faces, so a rounded-rect loft became an octagonal
frustum. A coincident-cap fuse of such a faceted frustum against an
arc-cornered solid could then never annihilate the cap cleanly (the long-open
fuse_coincident_rrect_cap_with_frustum / #790-residual case).

This adds a curve-preserving path for two-profile lofts: when both profiles
share the same boundary edge structure (same edge count, matching per-edge
curve type) and wind CCW, build the loft preserving each edge's curve —
ring edges keep their EdgeCurve, arc corners become ruled side faces, and the
caps are the real arc-edged profile wires. Mirrors the reference
BRepFill_Generator:
- Coaxial corner arcs (centers differ only along the axis) become an exact
  Cylinder (equal radii) or Cone (unequal radii) — analytic surfaces the
  boolean handles robustly.
- Genuinely non-coaxial corner arcs become a degree-(1,2) ruled NURBS.
Line edges keep planar quad side faces. Orientation is set per face against
the outward direction.

Scope: gated to two profiles (a single ruled band). Multi-section curve
lofts (e.g. a 5-section gridfinity lip) chain curved bands whose downstream
cut/fuse/shell sequences hit near-degenerate corner-junction robustness gaps;
they fall back to the existing polygon loft, which already handles them. So
gridfinity and every other multi-profile loft are unchanged.

Closes fuse_coincident_rrect_cap_with_frustum (now a passing test, was
#[ignore]d). Adds loft_rounded_rect_preserves_arc_corners (10 faces, 4 NURBS
arc corners, watertight). Zero regressions: operations 664, algo 113, io 156,
wasm 211 (gridfinity 24).
Copilot AI review requested due to automatic review settings June 14, 2026 01:38
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

WASM Binary Size

File main PR Delta
brepkit_wasm.wasm 4.94 MB 4.96 MB +19.4 KB (+.3%)

⚠️ Size increased slightly.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a curve-preserving loft path for the two-profile case so profiles with circular-arc corners (e.g., rounded rectangles) loft into true curved frustums instead of being faceted into polygonal frustums, improving downstream boolean robustness for coincident cap fuses.

Changes:

  • Introduces a gated two-profile “matching boundary structure” loft path that reuses per-edge EdgeCurve (Line/Circle) and builds ruled side faces (analytic Cylinder/Cone when coaxial, otherwise ruled NURBS).
  • Builds caps directly from the curve-preserving ring edges (so arc-cornered profile wires remain arc-cornered).
  • Un-ignores and updates the coincident rounded-rect cap fuse boolean test now that the loft preserves arc corners.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
crates/operations/src/loft.rs Adds curve-preserving two-profile loft implementation, including ruled arc side faces and a new rounded-rect loft test.
crates/operations/src/boolean/tests.rs Re-enables and updates documentation for fuse_coincident_rrect_cap_with_frustum now that loft no longer facets rounded-rect corners.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/operations/src/loft.rs Outdated
Comment on lines +662 to +666
let Some(surf) =
ruled_arc_surface(c0, p0s, p0e, c1, p1s, profs[s + 1][i].end)
else {
return Ok(None);
};
@andymai

andymai commented Jun 14, 2026

Copy link
Copy Markdown
Owner Author

Addressed the Copilot review in 16031c5: try_loft_matching_curved_profiles now pre-computes every side-face surface (step 3b) before creating any vertices/edges/faces, so the rare ruled_arc_surface fallback returns Ok(None) without leaving orphaned topology behind. Build + clippy clean, reproducer + loft tests still pass.

@andymai andymai merged commit 29ea1b3 into main Jun 14, 2026
18 checks passed
@andymai andymai deleted the feat/loft-preserve-curves branch June 14, 2026 02:04
@brepkit brepkit Bot mentioned this pull request Jun 14, 2026
@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a curve-preserving fast path (try_loft_matching_curved_profiles) for two-profile lofts whose profiles share the same edge structure. Instead of tessellating arc corners into polygon chords, it builds ruled side faces that keep arcs as arcs — coaxial corner pairs become analytic Cylinder/Cone patches and non-coaxial pairs become degree-(1,2) ruled NURBS, closing the long-open fuse_coincident_rrect_cap_with_frustum test.

  • Gated strictly to the two-profile case; multi-section lofts fall through to the existing polygon path unchanged.
  • Pre-computes all side surfaces before touching the topology arena — any NURBS incompatibility causes a clean fallback with no orphaned entities.
  • New test verifies 10 faces, 4 NURBS corners, closed manifold (Euler = 2), and volume within 2 % of the prismatoid estimate.

Confidence Score: 4/5

Safe to merge for the targeted two-profile rounded-rect case; the multi-profile polygon path is completely untouched.

The wire topology, cone/cylinder formula, and pre-mutation surface computation are all correct. Two small findings — a duplicated centroid helper and a coaxial axis check that admits anti-parallel normals — are unlikely to affect any real geometry but worth tidying.

crates/operations/src/loft.rs — specifically the coaxial_corner_surface axis-direction check and the centroid closure.

Important Files Changed

Filename Overview
crates/operations/src/loft.rs Adds try_loft_matching_curved_profiles — a curve-preserving two-profile loft path that keeps arc corners as Cylinder/Cone/NURBS side faces instead of faceting them. Wire traversal and vertex connectivity are correct; cone formula mirrors build_coaxial_band_stack; pre-computing surfaces before mutating topo is a good defensive pattern. Two minor issues: the local centroid closure duplicates winding::polygon_centroid, and the coaxial axis check admits anti-parallel arc normals via .abs().
crates/operations/src/boolean/tests.rs Removes the #[ignore] from fuse_coincident_rrect_cap_with_frustum (now unblocked) and updates its doc comment. Adds loft_rounded_rect_preserves_arc_corners with manifold, Euler, NURBS-count, and volume assertions.

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
crates/operations/src/loft.rs:543-553
**Duplicated centroid helper**

The `centroid` closure (with its `#[allow(clippy::cast_precision_loss)]`) replicates `crate::winding::polygon_centroid` which already takes `&[Point3]` and returns `Point3`. Using the existing utility would remove the suppression annotation and the duplication.

### Issue 2 of 2
crates/operations/src/loft.rs:420-423
**Coaxial check accepts anti-parallel arc normals**

`.abs()` on the dot product lets arcs whose normals are anti-parallel (one `+Z`, one `-Z`) pass the coaxial gate and fall into the Cylinder/Cone branch. The resulting surface would have its axis direction flipped relative to the wire winding, potentially producing an inside-out face. In practice, a CCW rounded-rect profile will always have all corner-arc normals pointing the same direction, so this is unlikely to fire — but an explicit same-sign check (`c1.normal().normalize().ok()?.dot(n) > 1.0 - tol.angular`) would prevent any possibility of a mis-oriented analytic patch.

Reviews (1): Last reviewed commit: "refactor(loft): pre-compute side surface..." | Re-trigger Greptile

Comment on lines +543 to +553
#[allow(clippy::cast_precision_loss)]
let centroid = |pts: &[Point3]| -> Point3 {
let (mut x, mut y, mut z) = (0.0, 0.0, 0.0);
for p in pts {
x += p.x();
y += p.y();
z += p.z();
}
let inv = 1.0 / pts.len() as f64;
Point3::new(x * inv, y * inv, z * inv)
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated centroid helper

The centroid closure (with its #[allow(clippy::cast_precision_loss)]) replicates crate::winding::polygon_centroid which already takes &[Point3] and returns Point3. Using the existing utility would remove the suppression annotation and the duplication.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/operations/src/loft.rs
Line: 543-553

Comment:
**Duplicated centroid helper**

The `centroid` closure (with its `#[allow(clippy::cast_precision_loss)]`) replicates `crate::winding::polygon_centroid` which already takes `&[Point3]` and returns `Point3`. Using the existing utility would remove the suppression annotation and the duplication.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment on lines +420 to +423
let n = c0.normal().normalize().ok()?;
// Both arcs must share the same axis direction.
if (c1.normal().normalize().ok()?.dot(n)).abs() < 1.0 - tol.angular {
return None;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Coaxial check accepts anti-parallel arc normals

.abs() on the dot product lets arcs whose normals are anti-parallel (one +Z, one -Z) pass the coaxial gate and fall into the Cylinder/Cone branch. The resulting surface would have its axis direction flipped relative to the wire winding, potentially producing an inside-out face. In practice, a CCW rounded-rect profile will always have all corner-arc normals pointing the same direction, so this is unlikely to fire — but an explicit same-sign check (c1.normal().normalize().ok()?.dot(n) > 1.0 - tol.angular) would prevent any possibility of a mis-oriented analytic patch.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/operations/src/loft.rs
Line: 420-423

Comment:
**Coaxial check accepts anti-parallel arc normals**

`.abs()` on the dot product lets arcs whose normals are anti-parallel (one `+Z`, one `-Z`) pass the coaxial gate and fall into the Cylinder/Cone branch. The resulting surface would have its axis direction flipped relative to the wire winding, potentially producing an inside-out face. In practice, a CCW rounded-rect profile will always have all corner-arc normals pointing the same direction, so this is unlikely to fire — but an explicit same-sign check (`c1.normal().normalize().ok()?.dot(n) > 1.0 - tol.angular`) would prevent any possibility of a mis-oriented analytic patch.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

brepkit Bot added a commit that referenced this pull request Jun 14, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.103.0](v2.102.13...v2.103.0)
(2026-06-14)


### Features

* **loft:** preserve curved corners for two-profile lofts
([#797](#797))
([29ea1b3](29ea1b3))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: brepkit[bot] <265643962+brepkit[bot]@users.noreply.github.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.

2 participants