-
Notifications
You must be signed in to change notification settings - Fork 1
fix(fillet): variable fillet removes material instead of inflating volume #739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -500,8 +500,74 @@ pub fn fillet_variable( | |||||||||||||||||||||||||||||||||||||||||||||||||
| .map(|(eid, law)| (eid.index(), law)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use the constant-radius trimming from the basic fillet for the planar faces. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // The NURBS canal surface replaces the fillet face. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Shared contact map: the SAME inward contact point used both to trim the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // adjacent faces and to anchor the blend boundary, keyed by | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // (vertex_index, edge_index, face_index). Computing it once guarantees the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // trimmed face boundary and the blend boundary coincide (watertight shell). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Per-end radius: the edge's start vertex uses R(0), the end uses R(1). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let fillet_contact_map: HashMap<(usize, usize, usize), Point3> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut map = HashMap::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (edge_id, law) in edge_laws { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let edge = topo.edge(*edge_id)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let p_start = topo.vertex(edge.start())?.point(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let p_end = topo.vertex(edge.end())?.point(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let Some(face_list) = edge_to_faces.get(&edge_id.index()) else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if face_list.len() < 2 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let f1 = face_list[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let f2 = face_list[1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let (Some(surf1), Some(surf2)) = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| face_surfaces.get(&f1.index()), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| face_surfaces.get(&f2.index()), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let edge_curve = edge.curve().clone(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if geometry::sample_edge_tangent(&edge_curve, p_start, p_end, 0.0).length() < tol.linear | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for &(t, vid) in &[(0.0_f64, edge.start()), (1.0_f64, edge.end())] { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let r = law.evaluate(t); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let p = geometry::sample_edge_point(&edge_curve, p_start, p_end, t); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let tan = geometry::sample_edge_tangent(&edge_curve, p_start, p_end, t); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let Ok(local_dir) = tan.normalize() else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let (Some(n1), Some(n2)) = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| face_surface_normal_at(surf1, p), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| face_surface_normal_at(surf2, p), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let cs = geometry::cross_section_dirs(local_dir, n1, n2, local_dir, local_dir); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| map.insert((vid.index(), edge_id.index(), f1.index()), p + cs.ld1 * r); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| map.insert((vid.index(), edge_id.index(), f2.index()), p + cs.ld2 * r); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| map | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Vertices at endpoints of filleted edges. A side face (one that shares | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // such a vertex but whose own edges are not filleted) must split that | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // corner into the two blend contact points, or the blend boundary is left | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // unmatched and the shell becomes non-manifold. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut vertex_fillet_endpoints: HashSet<usize> = HashSet::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (edge_id, _) in edge_laws { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let edge = topo.edge(*edge_id)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| vertex_fillet_endpoints.insert(edge.start().index()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| vertex_fillet_endpoints.insert(edge.end().index()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Trim planar faces by replacing each filleted-edge boundary vertex with | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // the shared contact point. The NURBS canal surface replaces the fillet face. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut all_specs: Vec<FaceSpec> = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for &face_id in &shell_face_ids { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -531,6 +597,7 @@ pub fn fillet_variable( | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut new_verts: Vec<Point3> = Vec::with_capacity(n + target_set.len()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let fi = face_id.index(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in 0..n { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let prev_i = if i == 0 { n - 1 } else { i - 1 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -540,32 +607,76 @@ pub fn fillet_variable( | |||||||||||||||||||||||||||||||||||||||||||||||||
| let pos = poly.positions[i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let prev_pos = poly.positions[prev_i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let next_pos = poly.positions[next_i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let vi = poly.vertex_ids[i].index(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let at_fillet_endpoint = vertex_fillet_endpoints.contains(&vi); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Look up per-edge radius at this vertex: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // - "before" edge (prev_i): vertex i is at its end → evaluate(1.0) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // - "after" edge (i): vertex i is at its start → evaluate(0.0) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let radius_before = edge_law_map | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .get(&poly.wire_edge_ids[prev_i].index()) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .map_or(0.0, |law| law.evaluate(1.0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let radius_after = edge_law_map | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .get(&poly.wire_edge_ids[i].index()) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .map_or(0.0, |law| law.evaluate(0.0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| match (before_filleted, after_filleted) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (false, false) => new_verts.push(pos), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (true, false) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let dir = (next_pos - pos).normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos + dir * radius_before); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| match (before_filleted, after_filleted, at_fillet_endpoint) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (false, false, false) => new_verts.push(pos), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Side face: vertex sits at a filleted-edge endpoint but neither | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // of this face's edges is filleted. Split the corner into the two | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // blend contacts at this vertex (one per filleted-adjacent face), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // ordered toward prev/next to keep the wire convex. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (false, false, true) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut unique_contacts: Vec<Point3> = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (&(vi_k, _, _), &pt) in &fillet_contact_map { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if vi_k == vi | ||||||||||||||||||||||||||||||||||||||||||||||||||
| && !unique_contacts | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .any(|uc| (*uc - pt).length() < tol.linear) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| unique_contacts.push(pt); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+620
to
+627
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if unique_contacts.len() >= 2 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let approx_prev = (prev_pos - pos) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .normalize() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .map_or(pos, |d| pos + d * tol.linear); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let d0 = (unique_contacts[0] - approx_prev).length(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let d1 = (unique_contacts[1] - approx_prev).length(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if d0 <= d1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(unique_contacts[0]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(unique_contacts[1]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(unique_contacts[1]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(unique_contacts[0]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+619
to
+645
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/operations/src/fillet/mod.rs
Line: 619-645
Comment:
**Contact collection ignores adjacency — wrong corners for multi-edge fillets**
The `(false, false, true)` arm collects every contact where `vi_k == vi`, across all edges and faces in `fillet_contact_map`. For a single filleted edge (the current test case) this yields exactly the 2 correct contacts. But if two filleted edges share this vertex — which `vertex_fillet_endpoints` would mark as `at_fillet_endpoint = true` — the map contains 4 contacts at `vi` (2 per edge × 2 faces). The code then takes `unique_contacts[0]` and `unique_contacts[1]` based on non-deterministic `HashMap` iteration order, and the proximity sort only decides which of those two comes first. The two selected contacts may belong to different edges, producing a side face that doesn't connect correctly to either blend boundary and leaving the shell non-manifold with no error raised.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (false, true) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let dir = (prev_pos - pos).normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos + dir * radius_after); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (true, false, _) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ei = poly.wire_edge_ids[prev_i].index(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(&pt) = fillet_contact_map.get(&(vi, ei, fi)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pt); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let dir = (next_pos - pos).normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos + dir * edge_law_map[&ei].evaluate(1.0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (true, true) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let dir_prev = (prev_pos - pos).normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos + dir_prev * radius_before); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let dir_next = (next_pos - pos).normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos + dir_next * radius_after); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (false, true, _) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ei = poly.wire_edge_ids[i].index(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(&pt) = fillet_contact_map.get(&(vi, ei, fi)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pt); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let dir = (prev_pos - pos).normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos + dir * edge_law_map[&ei].evaluate(0.0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (true, true, _) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ei_after = poly.wire_edge_ids[i].index(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(&pt) = fillet_contact_map.get(&(vi, ei_after, fi)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pt); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let dir_prev = (prev_pos - pos).normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos + dir_prev * edge_law_map[&ei_after].evaluate(0.0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ei_before = poly.wire_edge_ids[prev_i].index(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(&pt) = fillet_contact_map.get(&(vi, ei_before, fi)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pt); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let dir_next = (next_pos - pos).normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new_verts.push(pos + dir_next * edge_law_map[&ei_before].evaluate(1.0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+665
to
+679
Comment on lines
+669
to
+679
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Rule Used: New code must not introduce unwrap(), expect(), or... (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/operations/src/fillet/mod.rs
Line: 669-679
Comment:
Direct HashMap indexing `edge_law_map[&ei]` panics if the key is absent. The previous code used `map_or(0.0, |law| ...)` as a safe fallback. While the invariant (`after_filleted == true` implies `ei ∈ edge_law_map`) currently holds, replacing `.map_or` with direct indexing removes the defensive pattern the codebase was using everywhere else. The same pattern appears at the fallback for all three filleted-arm branches.
```suggestion
} else {
let dir_prev = (prev_pos - pos).normalize()?;
let r = edge_law_map.get(&ei_after).map_or(0.0, |l| l.evaluate(0.0));
new_verts.push(pos + dir_prev * r);
}
let ei_before = poly.wire_edge_ids[prev_i].index();
if let Some(&pt) = fillet_contact_map.get(&(vi, ei_before, fi)) {
new_verts.push(pt);
} else {
let dir_next = (next_pos - pos).normalize()?;
let r = edge_law_map.get(&ei_before).map_or(0.0, |l| l.evaluate(1.0));
new_verts.push(pos + dir_next * r);
}
```
**Rule Used:** New code must not introduce unwrap(), expect(), or... ([source](https://app.greptile.com/review/custom-context?memory=no-new-unwrap))
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -581,6 +692,7 @@ pub fn fillet_variable( | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Build variable-radius NURBS canal surfaces for each edge. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let n_samples = 5; // Number of cross-sections along each edge | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut fillet_face_indices: Vec<usize> = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for (edge_id, law) in edge_laws { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let edge = topo.edge(*edge_id)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -621,24 +733,11 @@ pub fn fillet_variable( | |||||||||||||||||||||||||||||||||||||||||||||||||
| let edge_dir = edge_tan.normalize()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Reference cross-section at t=0 for fallback directions. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let cross1_ref = edge_dir.cross(n1_start); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let cross2_ref = edge_dir.cross(n2_start); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let d1_ref = if cross1_ref.dot(n2_start) > 0.0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| cross1_ref | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -cross1_ref | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let d2_ref = if cross2_ref.dot(n1_start) > 0.0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| cross2_ref | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -cross2_ref | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let d1_ref = d1_ref.normalize().unwrap_or(d1_ref); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let d2_ref = d2_ref.normalize().unwrap_or(d2_ref); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let cos_half_ref = d1_ref.dot(d2_ref).clamp(-1.0, 1.0); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let half_angle = cos_half_ref.acos() / 2.0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let cs_ref = geometry::cross_section_dirs(edge_dir, n1_start, n2_start, edge_dir, edge_dir); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let d1_ref = cs_ref.ld1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let d2_ref = cs_ref.ld2; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if half_angle.abs() < tol.angular { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if cs_ref.half_angle.abs() < tol.angular { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -667,32 +766,44 @@ pub fn fillet_variable( | |||||||||||||||||||||||||||||||||||||||||||||||||
| let ln1 = face_surface_normal_at(surf1, p).unwrap_or(n1_start); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ln2 = face_surface_normal_at(surf2, p).unwrap_or(n2_start); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Vertex blend direction: uses original convention (toward other face's | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // outward normal). The vertex blend fills the gap between fillet strips | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // at a vertex, and its geometry depends on the edge fillet positions. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO(#260): vertex blend direction may need adjustment after edge | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // fillet direction fix — currently causes ~30% volume inflation on | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // all-edges fillet. Investigate vertex blend contact point computation. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let c1 = local_dir.cross(ln1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let c2 = local_dir.cross(ln2); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ld1 = if c1.dot(ln2) > 0.0 { c1 } else { -c1 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ld2 = if c2.dot(ln1) > 0.0 { c2 } else { -c2 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ld1 = ld1.normalize().unwrap_or(d1_ref); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let ld2 = ld2.normalize().unwrap_or(d2_ref); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let local_cos = ld1.dot(ld2).clamp(-1.0, 1.0); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let local_half = local_cos.acos() / 2.0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let bisector = (ld1 + ld2).normalize().unwrap_or(d1_ref); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let contact1 = p + ld1 * r; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let contact2 = p + ld2 * r; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mid_dist = r / local_half.cos().max(0.01); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mid_cp = p + bisector * mid_dist; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| sample_weights.push(local_half.cos().max(0.01)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let cs = geometry::cross_section_dirs(local_dir, ln1, ln2, d1_ref, d2_ref); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // cos(φ/2) is the rational-quadratic arc weight; clamp to a positive | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // floor so nearly-coplanar faces (φ/2 → π/2) don't yield a zero | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // weight (degenerate control point). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let w = cs.half_angle.cos().max(0.01); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let contact1 = p + cs.ld1 * r; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let contact2 = p + cs.ld2 * r; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // The middle control point is the apex of the tangent cone — the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // intersection of the two contact tangents. For a rolling ball on | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // surfaces meeting at the edge this is the edge point itself, so the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // weighted arc bulges concavely toward the solid interior (cutting | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // material). Placing it on the bisector ray past the ball center | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // would bulge the blend outward and add volume. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mid_cp = p; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| sample_weights.push(w); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| grid.push(vec![contact1, mid_cp, contact2]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Anchor the blend boundary contacts to the shared contact map so the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // interpolated NURBS boundary coincides exactly with the trimmed-face | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // vertices (bitwise-identical, no duplicate vertices in assembly). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let v_start = edge.start().index(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let v_end = edge.end().index(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(&pt) = fillet_contact_map.get(&(v_start, edge_id.index(), f1.index())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| grid[0][0] = pt; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(&pt) = fillet_contact_map.get(&(v_start, edge_id.index(), f2.index())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| grid[0][2] = pt; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(&pt) = fillet_contact_map.get(&(v_end, edge_id.index(), f1.index())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| grid[n_v - 1][0] = pt; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(&pt) = fillet_contact_map.get(&(v_end, edge_id.index(), f2.index())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| grid[n_v - 1][2] = pt; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Build a rational NURBS surface with exact circular arc cross-sections. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // u-direction: degree 2, 3 CPs with weights [1, cos(α/2), 1] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // v-direction: interpolated through sampled stations along the edge | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -758,7 +869,33 @@ pub fn fillet_variable( | |||||||||||||||||||||||||||||||||||||||||||||||||
| reversed: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inner_wires: vec![], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mark for reversal if the surface mid-normal points into the dihedral | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // (toward the solid) rather than outward. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let srf_mid_normal = match &all_specs[all_specs.len() - 1] { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| FaceSpec::Surface { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| surface: FaceSurface::Nurbs(srf), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } => srf.normal(0.5, 0.5).unwrap_or(cs_ref.bisector), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| _ => cs_ref.bisector, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if srf_mid_normal.dot(cs_ref.bisector) > 0.0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fillet_face_indices.push(all_specs.len() - 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let solid_id = crate::boolean::assemble_solid_mixed(topo, &all_specs, tol)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if !fillet_face_indices.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let solid_data = topo.solid(solid_id)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let shell = topo.shell(solid_data.outer_shell())?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+887
to
+891
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let face_ids: Vec<_> = shell.faces().to_vec(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for &fi in &fillet_face_indices { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if fi < face_ids.len() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| topo.face_mut(face_ids[fi])?.set_reversed(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+887
to
898
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/operations/src/fillet/mod.rs
Line: 887-898
Comment:
**Face reversal depends on undocumented index correspondence between `all_specs` and assembled `shell.faces()`**
`fillet_face_indices` stores raw indices into `all_specs` (e.g., the value of `all_specs.len() - 1` at the time each fillet face is pushed). After `assemble_solid_mixed` those indices are then used as indices into `shell.faces().to_vec()`. This works today because: (a) no `CylindricalFace` specs are present (so the cylindrical-first reordering in the assembler does not apply), and (b) no face spec has fewer than 3 vertices (which would cause the assembler's `continue` to skip a face, shifting all subsequent indices). Neither constraint is documented, checked at the call-site, or enforced. A future caller that adds even one `CylindricalFace` spec or receives a degenerate planar face would silently reverse the wrong face without a compile-time or runtime warning.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| crate::boolean::assemble_solid_mixed(topo, &all_specs, tol) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(solid_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cos_halfis named as if it holdscos(half_angle), but it actually holdscos(full_angle)—ld1.dot(ld2)is the cosine of the full arc sweep. The half-angle is then computed asacos(cos_full) / 2. This inverted naming could cause confusion when reading the formula, e.g. in the weight expressioncs.half_angle.cos()downstream.Prompt To Fix With AI
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!