feat(render): draw ACIS solids as B-rep feature edges, meshes as tri edges

Solid wireframes rendered every triangle edge of the tessellation, so a
curved solid read as a dense mesh instead of its real outline. Now each ACIS
solid's B-rep face-boundary edges are collected from the SAT `edge` records
(straight edges as segments, ellipse/circle edges sampled along their bounded
arc), body-transformed and split into the double-single pair, and stored on
the MeshLodSet. The block-instance and top-level offset paths transform these
edges alongside the mesh vertices.

The batch builder routes a solid with feature edges into a dedicated LineList
vertex buffer and skips its triangle wireframe; a mesh with no B-rep edges
keeps its triangulation edges. Edge colour follows the display mode: the flat
`fs_edge` shader draws the entity colour in the lines-only modes (wireframe,
hidden-line) and a `fs_edge_black` twin forces black for the edge overlay in
filled render modes.

Verified: the Clinic storefront arch bars now yield ~14-16 clean edge
segments (vs the full triangulation) bounded to the solid's true extent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-04 17:57:00 +03:00
1 parent a08cf477c1
commit 1065bc12b2
6 files changed
+285 -63

No files matched your search

+94 -12
View File
@@ -142,20 +142,101 @@ fn tessellate_acis(
color: [f32; 4],
facet_res: f64,
) -> Option<MeshLodSet> {
if let Some(set) = crate::scene::convert::acis_to_truck::tessellate_sat_truck(sat, name.clone(), color, facet_res) {
if std::env::var_os("OCS_TESS_DEBUG").is_some() {
let tris = set.lods.first().map(|m| m.indices.len() / 3).unwrap_or(0);
eprintln!("acis_tess[{name}]: truck ({tris} tris)");
let mut set =
if let Some(set) = crate::scene::convert::acis_to_truck::tessellate_sat_truck(sat, name.clone(), color, facet_res) {
if std::env::var_os("OCS_TESS_DEBUG").is_some() {
let tris = set.lods.first().map(|m| m.indices.len() / 3).unwrap_or(0);
eprintln!("acis_tess[{name}]: truck ({tris} tris)");
}
set
} else {
// truck couldn't rebuild the shell — fall back to the bespoke sampler.
let fallback = tessellate_sat_lods(sat, name.clone(), color, facet_res);
eprintln!(
"acis_tess[{name}]: manual fallback ({})",
if fallback.is_some() { "ok" } else { "empty" }
);
fallback?
};
// Attach the B-rep face-boundary edges (body-transformed, split into the
// double-single pair) so the solid's wireframe shows real edges.
attach_feature_edges(&mut set, sat);
Some(set)
}
/// Collect the ACIS `edge` records as world-space polyline segments (pairs of
/// endpoints), body-transform them, and store them on the set as the
/// double-single `edge_verts` / `edge_verts_low`.
fn attach_feature_edges(set: &mut MeshLodSet, sat: &SatDocument) {
let xform = body_transform(sat);
let seg_pts = collect_feature_edges(sat);
set.edge_verts.reserve(seg_pts.len());
set.edge_verts_low.reserve(seg_pts.len());
for p in seg_pts {
let (mut x, mut y, mut z) = (p[0], p[1], p[2]);
if let Some((m, tr, scale)) = xform {
let (lx, ly, lz) = (x, y, z);
x = scale * (lx * m[0] + ly * m[3] + lz * m[6]) + tr[0];
y = scale * (lx * m[1] + ly * m[4] + lz * m[7]) + tr[1];
z = scale * (lx * m[2] + ly * m[5] + lz * m[8]) + tr[2];
}
return Some(set);
let (hx, hy, hz) = (x as f32, y as f32, z as f32);
set.edge_verts.push([hx, hy, hz]);
set.edge_verts_low
.push([(x - hx as f64) as f32, (y - hy as f64) as f32, (z - hz as f64) as f32]);
}
// truck couldn't rebuild the shell — fall back to the bespoke sampler.
let fallback = tessellate_sat_lods(sat, name.clone(), color, facet_res);
eprintln!(
"acis_tess[{name}]: manual fallback ({})",
if fallback.is_some() { "ok" } else { "empty" }
);
fallback
}
/// Line-list endpoints (pairs) for every `edge` record: straight edges emit
/// their two vertex endpoints; ellipse/circle edges are sampled along their
/// bounded parametric arc. Points are in body-local space (pre-transform).
fn collect_feature_edges(sat: &SatDocument) -> Vec<[f64; 3]> {
const EDGE_SEGS: usize = 48;
let mut out: Vec<[f64; 3]> = Vec::new();
for er in sat.records_of_type("edge") {
let Some(edge) = SatEdge::from_record(er) else {
continue;
};
// Ordered points along the edge (≥2).
let mut pts: Vec<[f64; 3]> = Vec::new();
if let Some(cr) = sat.resolve(edge.curve()) {
if let Some(ellipse) = SatEllipseCurve::from_record(cr) {
pts = sample_ellipse_arc(&ellipse, edge.start_param(), edge.end_param(), EDGE_SEGS);
// sample_ellipse_arc drops the end param; append the true end so
// the polyline closes onto the shared vertex.
if let Some(p) = vertex_point(sat, edge.end_vertex()) {
pts.push(p);
}
}
}
if pts.len() < 2 {
// Straight edge (or unsampled curve): connect the two vertices.
pts.clear();
if let (Some(a), Some(b)) = (
vertex_point(sat, edge.start_vertex()),
vertex_point(sat, edge.end_vertex()),
) {
pts.push(a);
pts.push(b);
}
}
// Emit consecutive points as line-list segment pairs.
for w in pts.windows(2) {
out.push(w[0]);
out.push(w[1]);
}
}
out
}
/// Resolve a vertex pointer to its point coordinates.
fn vertex_point(sat: &SatDocument, vptr: SatPointer) -> Option<[f64; 3]> {
let vrec = sat.resolve(vptr)?;
let vertex = SatVertex::from_record(vrec)?;
let prec = sat.resolve(vertex.point())?;
let point = SatPoint::from_record(prec)?;
let (x, y, z) = point.position();
Some([x, y, z])
}
/// Tessellate a SAT document at all three LODs and bundle them into a
@@ -1016,3 +1097,4 @@ fn norm3(v: [f64; 3]) -> [f64; 3] {
}
}