forked from 1b-agent/opencadstudio-upstream
fix(solid3d): close torn cylinder walls from truck revolutions
A revolution comes back from rsweep as three 120-degree segment faces, but every hole was attached to the first of them. A window wider than a segment cannot sit on one face, so that face turned invalid and its whole third of the tube vanished -- the pump's cylinders lost 120 degrees of wall each and the solid read as open where they met the body. One of them shed a segment with no hole involved at all, so chasing the individual causes would not have covered it. Measure the result instead: after a cone face whose boundary closes the revolution is triangulated, check that the meshed vertices actually span it, and hand the face to the parametric sampler when they do not. That catches a tear whatever produced it, while genuinely partial cones and every non-cone surface pass straight through -- the tee's pierced run cylinder still meshes through the kernel. Also bring truck's chord tolerance in line with the detailed LOD. Truck emits a single mesh rather than an LOD ladder, so a tenth of the radius left a wide pipe as a hexagonal prism whose flats sank nine units inside the true surface, pulling the wall away from the caps and planar faces that meet it. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
2a1a8f4b94
commit
5c7aeca321
2 files changed
+81
-3
No files matched your search
@@ -150,7 +150,8 @@ pub fn tessellate_sat_truck(
|
||||
if !faces.is_empty() {
|
||||
let shell: Shell = faces.into();
|
||||
let poly = shell.triangulation(tol).to_polygon();
|
||||
if !poly.tri_faces().is_empty() {
|
||||
if !poly.tri_faces().is_empty() && cone_sweep_covered(sat, &face, surf_rec, &poly)
|
||||
{
|
||||
append_group(&mut verts, &mut normals, &mut indices, &poly, &outward);
|
||||
appended = true;
|
||||
}
|
||||
@@ -458,6 +459,79 @@ fn cone_faces(
|
||||
Some((faces, out))
|
||||
}
|
||||
|
||||
/// Whether a triangulated cone face actually spans the revolution its boundary
|
||||
/// claims.
|
||||
///
|
||||
/// `rsweep` returns a full turn as several segment faces (120° apiece), and a
|
||||
/// hole only ever lands on one of them: a window wider than a segment, or a
|
||||
/// segment truck degenerates on, silently costs that whole third of the tube.
|
||||
/// The face still reports triangles, so nothing downstream notices — the wall is
|
||||
/// simply missing there and the solid reads as open. Measuring the meshed
|
||||
/// coverage against the boundary catches every such loss, whatever caused it,
|
||||
/// and hands the face to the parametric sampler instead.
|
||||
///
|
||||
/// Non-cone faces and genuinely partial cones pass straight through.
|
||||
fn cone_sweep_covered(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
surf_rec: &acadrust::entities::acis::SatRecord,
|
||||
poly: &PolygonMesh,
|
||||
) -> bool {
|
||||
use std::f64::consts::TAU;
|
||||
/// A meshed full revolution may skip at most this much before we call it
|
||||
/// torn — comfortably wider than one chord step, far under a 120° segment.
|
||||
const MAX_GAP: f64 = std::f64::consts::FRAC_PI_4;
|
||||
|
||||
let Some(cone) = SatConeSurface::from_record(surf_rec) else {
|
||||
return true;
|
||||
};
|
||||
let (cx, cy, cz) = cone.center();
|
||||
let (ax, ay, az) = cone.axis();
|
||||
let (ux, uy, uz) = cone.major_axis();
|
||||
let axis = norm(Vector3::new(ax, ay, az));
|
||||
let udir = norm(Vector3::new(ux, uy, uz));
|
||||
let vdir = Vector3::new(
|
||||
axis.y * udir.z - axis.z * udir.y,
|
||||
axis.z * udir.x - axis.x * udir.z,
|
||||
axis.x * udir.y - axis.y * udir.x,
|
||||
);
|
||||
|
||||
// Only a face whose boundary closes the revolution owes us a full sweep.
|
||||
let loops = collect_face_loops(sat, face, BOUNDARY_CHORD_FRAC);
|
||||
let closed = loops
|
||||
.iter()
|
||||
.map(|lp| cone_boundary_arc(lp, [cx, cy, cz], axis, udir, vdir).1)
|
||||
.fold(0.0f64, f64::max)
|
||||
>= TAU;
|
||||
if !closed {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut angles: Vec<f64> = Vec::new();
|
||||
for p in poly.positions() {
|
||||
let d = vsub([p.x, p.y, p.z], [cx, cy, cz]);
|
||||
let ru = vdot(d, [udir.x, udir.y, udir.z]);
|
||||
let rv = vdot(d, [vdir.x, vdir.y, vdir.z]);
|
||||
if ru.hypot(rv) > 1e-9 {
|
||||
angles.push(rv.atan2(ru).rem_euclid(TAU));
|
||||
}
|
||||
}
|
||||
if angles.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
angles.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let mut widest = 0.0f64;
|
||||
for i in 0..angles.len() {
|
||||
let next = if i + 1 < angles.len() {
|
||||
angles[i + 1]
|
||||
} else {
|
||||
angles[0] + TAU
|
||||
};
|
||||
widest = widest.max(next - angles[i]);
|
||||
}
|
||||
widest <= MAX_GAP
|
||||
}
|
||||
|
||||
/// Smallest angular arc `(theta_start, sweep)` about the cone axis enclosing the
|
||||
/// face's boundary points, robust to the ±π seam. Returns `(0.0, FULL)` when the
|
||||
/// points wrap the whole revolution (a closed rim) or the boundary is unusable,
|
||||
|
||||
@@ -40,9 +40,13 @@ use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
|
||||
/// segments per full circle).
|
||||
pub(crate) const EDGE_CHORD_FRAC: f64 = 0.002;
|
||||
/// Truck's own triangulation chord tolerance for the cone faces still routed
|
||||
/// through its kernel, as a fraction of the surface radius.
|
||||
/// through its kernel, as a fraction of the surface radius. Matches
|
||||
/// [`LodConfig::HIGH`]: truck emits a single mesh rather than an LOD ladder, so
|
||||
/// it has to be the detailed one. A coarse fraction here turns a wide pipe into
|
||||
/// a hexagonal prism whose flats sink far inside the true radius, tearing the
|
||||
/// wall away from the planar faces and caps that meet it.
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub(crate) const TRUCK_CHORD_FRAC: f64 = 0.1;
|
||||
pub(crate) const TRUCK_CHORD_FRAC: f64 = 0.005;
|
||||
/// Boundary-loop sampling for parameter-range classification (which arc of a
|
||||
/// sphere/torus a face covers): a fine fraction so the classification is
|
||||
/// accurate; the points are not rendered.
|
||||
|
||||
Reference in new issue
Block a user