refactor(tess): move solid sampling to kernel

Remove local face, edge, spline, sweep, loft, and silhouette
samplers. Meshes and overlays share the kernel tolerance and edge
schedule.
This commit is contained in:
Hakan Seven committed 2026-08-10 10:18:53 +03:00
1 parent 60f5bca0d4
commit dc998fe46b
15 files changed
+410 -3938

No files matched your search

+120 -152
View File
@@ -6,112 +6,143 @@
//! the kernel for triangles, rather than to re-derive each surface's extent by
//! sampling it.
//!
//! # Why it can still fall short
//!
//! A face on a surface the kernel does not model, a curve it has no form for,
//! a pointer graph that does not hold together: [`lift`] reports each as a
//! [`Loss`] rather than quietly dropping it. What comes back then is a body
//! with faces missing, and the mesh it makes has holes — which is why the
//! result is marked incomplete and the caller keeps its own sampler for those.
//!
//! Saying so is the point. A partial mesh that claimed to be whole would show
//! a solid with a wall missing and nothing to suggest anything was wrong.
//! Lift and tessellation failures are reported as an incomplete result.
use acadrust::acis::lift;
use acadrust::entities::acis::SatDocument;
use acadrust::kernel::brep;
use crate::scene::convert::solid3d_tess::{body_transform, finalize_mesh};
use crate::scene::model::mesh_model::MeshLodSet;
use crate::scene::model::mesh_model::{CurvedGen, MeshLodSet};
/// How far a triangle may sit from the surface it lies on, as a fraction of
/// that surface's own radius.
///
/// A fraction rather than a length, because a length carries an assumption
/// about the drawing's units: a centimetre of sag is nothing on a pipeline
/// and is the whole of a bolt.
///
/// The *same* fraction the feature edges use, and deliberately so. Those edges
/// are drawn over these faces, so sampling the two differently leaves the wire
/// cutting across a facet instead of running along its corners — the rim of a
/// cylinder standing proud of the wall it bounds. Sharing the constant is what
/// keeps them from drifting apart when one is tuned.
use crate::scene::convert::solid3d_tess::EDGE_CHORD_FRAC as CHORD_FRAC;
/// Relative chord tolerance, resolved once per body.
const CHORD_FRAC: f64 = 0.002;
/// What counts as the same point when the kernel reads a body over.
///
/// A micrometre, in a drawing measured in metres. Not slackness: an edge is
/// shared by two faces, and in a real file it cannot sit exactly on both,
/// because the two surfaces were fitted separately and written to finite
/// precision. Asked for exactness the kernel decides the edge is not on its
/// own plane, declines to project it, and the face is dropped — twenty-six
/// walls of one building went missing at a nanometre that no drawing means.
///
/// Loosening further buys almost nothing: a hundredth of this recovers one
/// more face in sixty thousand, and past that the tolerance would start
/// accepting geometry that really is wrong.
/// ACIS topology fit tolerance.
const TOL: f64 = 1e-6;
/// Tessellate an ACIS document by lifting it into the kernel.
///
/// `None` when nothing in the document lifts at all. The result's `complete`
/// flag says whether every face made it; a caller with a fallback sampler
/// uses it to decide whether to run one.
/// Tessellate an ACIS document through the kernel.
pub fn tessellate_sat(
document: &SatDocument,
name: String,
color: [f32; 4],
facet_res: f64,
isolines: usize,
) -> Option<MeshLodSet> {
let (bodies, loss) = lift(document);
if bodies.is_empty() {
return None;
}
// `facet_res` is a resolution multiplier, not a length — the same one
// `scale_lod` divides the fallback sampler's chord fraction by. Using it
// as a sag made every solid as coarse as its own boundary: at the default
// it asked for a whole world unit of departure, which on anything smaller
// than that means no subdivision at all, and a pipe came out with as many
// sides as its rim had points.
//
// It is not applied here at all. The feature edges these faces are drawn
// under are built once at highest detail and never scaled, so scaling the
// faces would pull the two apart again at any setting but one.
let _ = facet_res;
let frac = CHORD_FRAC;
let mut placed_bodies = Vec::with_capacity(bodies.len());
for body in bodies {
let source = body.provenance.source()?;
let transform = body_transform(document, source.index() as usize).ok()?;
let placed = if let Some((matrix, translation, scale)) = transform {
let placement = brep::Placement {
x_axis: [scale * matrix[0], scale * matrix[1], scale * matrix[2]],
y_axis: [scale * matrix[3], scale * matrix[4], scale * matrix[5]],
z_axis: [scale * matrix[6], scale * matrix[7], scale * matrix[8]],
origin: translation,
};
brep::transform(&body, &placement)?
} else {
body
};
placed_bodies.push(placed);
}
let bodies = placed_bodies;
let resolution = if facet_res.is_finite() && facet_res > 0.0 {
facet_res.clamp(0.01, 10.0)
} else {
1.0
};
let frac = CHORD_FRAC / resolution;
// Positions stay f64 until `finalize_mesh` splits them into the coarse
// and fine pair, so a solid at survey coordinates keeps its millimetres.
let mut positions: Vec<[f64; 3]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let mut edges: Vec<[f64; 3]> = Vec::new();
let mut triangle_materials = Vec::new();
let mut triangle_colors = Vec::new();
let mut curved_gens = Vec::new();
let face_materials: std::collections::HashMap<i32, acadrust::Handle> = document
.records
.iter()
.filter(|record| record.entity_type == "material-adesk-attrib")
.filter_map(|record| {
let owner = record.token_pointer(2)?.0;
let handle = record.token(3)?.as_integer()?;
(owner >= 0 && handle > 0)
.then(|| (owner, acadrust::Handle::new(handle as u64)))
})
.collect();
let face_colors: std::collections::HashMap<i32, [f32; 4]> = document
.records
.iter()
.filter(|record| record.entity_type == "color-adesk-attrib")
.filter_map(|record| {
let owner = record.token_pointer(2)?.0;
let value = record.token(3)?.as_integer()?;
let source = if (1..=255).contains(&value) {
acadrust::Color::from_index(value as i16)
} else if value > 257 {
acadrust::Color::from_true_color_value(value as i32)
} else {
return None;
};
let mut rgba = crate::scene::convert::tess_util::aci_to_rgba(&source);
rgba[3] = color[3];
Some((owner, rgba))
})
.collect();
// A face the kernel holds but cannot express in its surface's own
// parameters leaves a hole, the same as one that never lifted — so both
// are counted before calling the mesh whole.
let mut undrawn = 0usize;
let tolerance = brep::mesh::TessellationTolerance::relative(frac, TOL)
.with_isolines(isolines);
for body in &bodies {
// What a flat face is sampled against: it never departs from its own
// plane, so only its boundary arcs care, and the body's own size is
// the nearest thing to a radius they have.
let span = body_span(body);
for face in body.face_keys() {
let sag = frac * face_radius(body, face).unwrap_or(span);
let Some(mesh) = brep::mesh::face(body, face, sag, TOL) else {
undrawn += 1;
continue;
};
let base = positions.len() as u32;
positions.extend_from_slice(&mesh.positions);
normals.extend(
mesh.normals
.iter()
.map(|n| [n[0] as f32, n[1] as f32, n[2] as f32]),
);
indices.extend(
mesh.triangles
.iter()
.flat_map(|t| [base + t[0] as u32, base + t[1] as u32, base + t[2] as u32]),
);
let tessellation = brep::mesh::tessellate(body, tolerance);
undrawn += tessellation.missing_faces.len();
for face in &tessellation.triangle_faces {
let record = body
.faces
.get(*face)
.and_then(|face| face.provenance.source())
.map(|source| source.index() as i32);
triangle_materials.push(record.and_then(|record| face_materials.get(&record).copied()));
triangle_colors.push(record.and_then(|record| face_colors.get(&record).copied()));
}
curved_gens.push(CurvedGen {
source: tessellation.silhouette_source(),
});
let base = positions.len() as u32;
positions.extend_from_slice(&tessellation.mesh.positions);
normals.extend(
tessellation
.mesh
.normals
.iter()
.map(|n| [n[0] as f32, n[1] as f32, n[2] as f32]),
);
indices.extend(tessellation.mesh.triangles.iter().flat_map(|triangle| {
[
base + triangle[0] as u32,
base + triangle[1] as u32,
base + triangle[2] as u32,
]
}));
for edge in tessellation.edges {
for segment in edge.positions.windows(2) {
edges.extend_from_slice(segment);
}
}
for isoline in tessellation.isolines {
for segment in isoline.positions.windows(2) {
edges.extend_from_slice(segment);
}
}
}
if indices.is_empty() {
@@ -127,81 +158,31 @@ pub fn tessellate_sat(
positions,
normals,
indices,
Vec::new(),
Vec::new(),
triangle_materials,
triangle_colors,
color,
body_transform(document),
None,
));
set.curved_gens = curved_gens;
for point in edges {
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
set.edge_verts.push(high);
set.edge_verts_low.push([
(point[0] - high[0] as f64) as f32,
(point[1] - high[1] as f64) as f32,
(point[2] - high[2] as f64) as f32,
]);
}
set.complete = loss.is_empty() && undrawn == 0;
Some(set)
}
/// The radius of the surface a face lies on, where it has one.
///
/// A torus is measured by its tube rather than its ring: the tube is the
/// tighter bend, and sampling to the ring would leave the section a hexagon.
fn face_radius(body: &brep::Body, face: brep::FaceKey) -> Option<f64> {
let surface = body.surfaces.get(body.faces.get(face)?.surface)?;
match surface {
brep::Surface::Plane(_) => None,
brep::Surface::Cylinder(cylinder) => Some(cylinder.radius),
brep::Surface::Cone(cone) => Some(cone.radius),
brep::Surface::Sphere(sphere) => Some(sphere.radius),
brep::Surface::Torus(torus) => Some(torus.minor_radius),
}
}
/// How big a body is, from the corners it is built on.
fn body_span(body: &brep::Body) -> f64 {
let mut low = [f64::INFINITY; 3];
let mut high = [f64::NEG_INFINITY; 3];
for (_, vertex) in body.vertices.iter() {
for axis in 0..3 {
low[axis] = low[axis].min(vertex.point[axis]);
high[axis] = high[axis].max(vertex.point[axis]);
}
}
if low[0] > high[0] {
return 1.0;
}
(0..3)
.map(|axis| high[axis] - low[axis])
.fold(0.0_f64, f64::max)
.max(1e-9)
}
/// The edges of every body in an ACIS document, as polylines.
///
/// What draws a solid's wireframe and what a click hit-tests against. Taken
/// from the kernel's own curves rather than from the mesh, so a rim is a
/// circle sampled to tolerance instead of whatever the triangulation left
/// along it.
///
/// Not called yet: the solid tessellator keeps its own feature-edge pass,
/// which also carries isolines. Here because it is the kernel's answer to the
/// same question, and the two should converge on it.
#[allow(dead_code)]
pub fn edge_polylines(document: &SatDocument, sag: f64) -> Vec<Vec<[f64; 3]>> {
let (bodies, _) = lift(document);
let sag = if sag > 0.0 { sag } else { CHORD_FRAC };
let placement = body_transform(document);
bodies
.iter()
.flat_map(|body| brep::edge_polylines(body, sag))
.map(|polyline| {
polyline
.into_iter()
.map(|point| placed(point, placement))
.collect()
})
.collect()
}
/// A body-local point moved to where the body sits.
///
/// ACIS treats points as row vectors — `p' = scale·(p·M) + T` — so the stored
/// 3×3 is indexed transposed from a column-vector multiply. Getting that the
/// wrong way round mirrors a placed solid rather than moving it.
#[cfg(test)]
fn placed(point: [f64; 3], xform: Option<([f64; 9], [f64; 3], f64)>) -> [f64; 3] {
let Some((m, translation, scale)) = xform else {
return point;
@@ -275,17 +256,4 @@ mod tests {
assert!(sides(CHORD_FRAC) < 96.0, "{}", sides(CHORD_FRAC));
}
/// And it is the edges' own density, so the wire drawn over a face lands
/// on the facet corners rather than cutting across them.
#[test]
fn a_face_is_sampled_as_finely_as_the_edges_over_it() {
let rim = crate::scene::convert::solid3d_tess::edge_arc_segs(
5.0,
std::f64::consts::TAU,
) as f64;
let wall = sides(CHORD_FRAC);
assert!((rim - wall).abs() <= 1.0, "rim {rim} vs wall {wall}");
}
}
-1
View File
@@ -8,4 +8,3 @@ pub(crate) mod tess;
pub mod proxy_graphics;
pub mod tess_util;
pub mod solid3d_tess;
pub mod spline_tess;
File diff suppressed because it is too large. Load diff
-454
View File
@@ -1,454 +0,0 @@
// B-spline (NURBS) surface tessellation for ACIS `spline-surface` faces.
//
// Lofted / swept / revolved surfaces store their geometry as an ACIS
// `nubs` (non-uniform B-spline) block inside the `spline-surface` record.
// Rather than evaluate the basis functions by hand, we parse the control net
// and knot vectors out of the SAT tokens, hand them to the kernel's
// `BSplineSurface` (the same NURBS kernel the Model tab already builds on),
// and sample its parametric grid into triangles.
use acadrust::entities::acis::types::Sense;
use acadrust::entities::acis::{
SatCoedge, SatDocument, SatFace, SatLoop, SatPCurve, SatRecord, SatSplineSurface, SatToken,
};
use rustc_hash::FxHashSet;
use acadrust::kernel::space::NurbsSurface3;
use crate::scene::convert::solid3d_tess::LodConfig;
// A `spline-surface` block is either a non-rational `nubs`, whose control
// points are plain xyz, or a rational `nurbs`, whose carry a weight. The
// kernel's surface holds both — weights absent means polynomial — so there is
// nothing here to tell apart.
/// Tessellate one `spline-surface` face by sampling its B-spline surface.
/// Appends triangles to the shared mesh buffers; a no-op when the surface
/// record can't be parsed into a B-spline.
pub fn tess_spline_face(
sat: &SatDocument,
face: &SatFace,
lod: LodConfig,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
) -> bool {
let Some(surf_rec) = sat.resolve(face.surface()) else {
return false;
};
let Some(surface) = build_spline_surface(sat, surf_rec) else {
return false;
};
// Sample over the knot domain and clip cells against ACIS pcurves when the
// face carries a parametric trim. This preserves holes and non-rectangular
// spline faces instead of always filling the complete UV rectangle.
let ((u0, u1), (v0, v1)) = surface.domain();
if !(u1 > u0) || !(v1 > v0) {
return false;
}
// A B-spline patch has no single analytic radius to drive a chord-tolerance
// count, so sample at the LOD's nominal density (its unit-circle segment
// count). Floor 8 so a curved patch stays smooth.
let n = crate::scene::convert::solid3d_tess::nominal_segs(lod.chord_frac).max(8);
let (su, sv) = (n, n);
let trim_loops = collect_trim_loops(sat, face, n);
let reversed = matches!(face.sense(), Sense::Reversed);
let index_start = indices.len();
let base = verts.len() as u32;
for j in 0..=sv {
let v = v0 + (v1 - v0) * (j as f64 / sv as f64);
for i in 0..=su {
let u = u0 + (u1 - u0) * (i as f64 / su as f64);
let p = surface.point_at_knot(u, v);
// A pole, or a row of coincident control points, has no plane to
// be perpendicular to. Up is as good an answer as any there and
// better than an invented one, since the patch has no area at
// that point to shade.
let mut n = surface.normal_at_knot(u, v).unwrap_or([0.0, 0.0, 1.0]);
if reversed {
n = [-n[0], -n[1], -n[2]];
}
verts.push(p);
normals.push([n[0] as f32, n[1] as f32, n[2] as f32]);
}
}
let row = (su + 1) as u32;
for j in 0..sv as u32 {
for i in 0..su as u32 {
if let Some(loops) = trim_loops.as_ref() {
let u = u0 + (u1 - u0) * ((i as f64 + 0.5) / su as f64);
let v = v0 + (v1 - v0) * ((j as f64 + 0.5) / sv as f64);
if !inside_trim((u, v), loops, (u0, u1, v0, v1)) {
continue;
}
}
let a = base + j * row + i;
let b = a + 1;
let c = a + row;
let d = c + 1;
if reversed {
indices.extend_from_slice(&[a, d, b, a, c, d]);
} else {
indices.extend_from_slice(&[a, b, d, a, d, c]);
}
}
}
indices.len() > index_start
}
/// Collect complete face-loop pcurves in UV space. Missing pcurves disable
/// clipping for that face; a partial trim would be worse than the old full
/// patch fallback.
fn collect_trim_loops(
sat: &SatDocument,
face: &SatFace,
segments: usize,
) -> Option<Vec<Vec<(f64, f64)>>> {
let mut result = Vec::new();
let mut loop_ptr = face.first_loop();
let mut seen_loops = FxHashSet::default();
while !loop_ptr.is_null() && seen_loops.insert(loop_ptr.0) {
let sat_loop = SatLoop::from_record(sat.resolve(loop_ptr)?)?;
let first = sat_loop.first_coedge();
let mut coedge_ptr = first;
let mut seen_coedges = FxHashSet::default();
let mut polygon: Vec<(f64, f64)> = Vec::new();
while !coedge_ptr.is_null() && seen_coedges.insert(coedge_ptr.0) {
let coedge = SatCoedge::from_record(sat.resolve(coedge_ptr)?)?;
let pcurve = SatPCurve::from_record(sat.resolve(coedge.pcurve())?)?;
let mut points = pcurve.sample_in(sat, segments);
if points.len() < 2 {
return None;
}
if matches!(coedge.sense(), Sense::Reversed) {
points.reverse();
}
if let Some(&last) = polygon.last() {
let first_gap =
(last.0 - points[0].0).powi(2) + (last.1 - points[0].1).powi(2);
let end = points[points.len() - 1];
let last_gap = (last.0 - end.0).powi(2) + (last.1 - end.1).powi(2);
if last_gap < first_gap {
points.reverse();
}
}
points.pop();
polygon.extend(points);
coedge_ptr = coedge.next();
if coedge_ptr == first {
break;
}
}
if polygon.len() < 3 {
return None;
}
result.push(polygon);
loop_ptr = sat_loop.next_loop();
}
if result.is_empty() {
None
} else {
Some(result)
}
}
fn inside_trim(
point: (f64, f64),
loops: &[Vec<(f64, f64)>],
domain: (f64, f64, f64, f64),
) -> bool {
let domain_area = ((domain.1 - domain.0) * (domain.3 - domain.2)).abs();
let area_epsilon = domain_area.max(1.0) * 1e-10;
let periodic_boundary = loops.iter().any(|polygon| {
if polygon_area(polygon).abs() > area_epsilon {
return false;
}
let bounds = polygon_bounds(polygon);
let u_span = (bounds[2] - bounds[0]).abs();
let v_span = (bounds[3] - bounds[1]).abs();
u_span >= (domain.1 - domain.0).abs() * 0.9
|| v_span >= (domain.3 - domain.2).abs() * 0.9
});
if periodic_boundary {
return loops.iter().all(|polygon| {
polygon_area(polygon).abs() <= area_epsilon
|| !point_in_polygon(point, polygon)
});
}
let Some((outer_index, _)) = loops
.iter()
.enumerate()
.map(|(index, polygon)| (index, polygon_area(polygon).abs()))
.max_by(|a, b| a.1.total_cmp(&b.1))
else {
return true;
};
point_in_polygon(point, &loops[outer_index])
&& loops
.iter()
.enumerate()
.all(|(index, polygon)| index == outer_index || !point_in_polygon(point, polygon))
}
fn polygon_bounds(polygon: &[(f64, f64)]) -> [f64; 4] {
polygon.iter().fold(
[
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
],
|mut bounds, &(u, v)| {
bounds[0] = bounds[0].min(u);
bounds[1] = bounds[1].min(v);
bounds[2] = bounds[2].max(u);
bounds[3] = bounds[3].max(v);
bounds
},
)
}
fn polygon_area(polygon: &[(f64, f64)]) -> f64 {
polygon
.iter()
.zip(polygon.iter().cycle().skip(1))
.take(polygon.len())
.map(|(&(ax, ay), &(bx, by))| ax * by - bx * ay)
.sum::<f64>()
* 0.5
}
fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
let (x, y) = point;
let mut inside = false;
let mut previous = polygon[polygon.len() - 1];
for &current in polygon {
let crosses = (current.1 > y) != (previous.1 > y)
&& x
< (previous.0 - current.0) * (y - current.1)
/ (previous.1 - current.1)
+ current.0;
if crosses {
inside = !inside;
}
previous = current;
}
inside
}
/// Parse the `nubs` control net + knot vectors out of a `spline-surface`
/// record's token stream into a kernel surface.
fn build_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option<NurbsSurface3> {
if let Some(surface) = build_decoded_spline_surface(sat, rec) {
return Some(surface);
}
let mut toks = rec.tokens.as_slice();
if let Some(reference) = primary_subtype_reference(toks) {
toks = sat.subtype_tokens(reference)?;
}
// Locate the real B-spline block. `nullbs` placeholders (for absent
// rail/path surfaces) precede it; the actual surface is `nubs` (plain xyz
// control points) or `nurbs` (rational — each control point carries a
// weight, so it is stored as xyzw).
let start = toks
.iter()
.rposition(|t| matches!(t, SatToken::Ident(s) if s == "nubs" || s == "nurbs"))?;
let rational = matches!(&toks[start], SatToken::Ident(s) if s == "nurbs");
let mut p = start + 1;
let deg_u = read_int(toks, &mut p)? as usize;
let deg_v = read_int(toks, &mut p)? as usize;
// Four form flags (closure / singularity in u and v) — skip.
for _ in 0..4 {
read_int(toks, &mut p)?;
}
let n_uknot = read_int(toks, &mut p)? as usize;
let n_vknot = read_int(toks, &mut p)? as usize;
let raw_u_knots = read_knot_vec(toks, &mut p, n_uknot)?;
let raw_v_knots = read_knot_vec(toks, &mut p, n_vknot)?;
let stride = if rational { 4 } else { 3 };
let available = toks[p..]
.iter()
.take_while(|token| token.as_float().is_some())
.count();
let base_u = raw_u_knots.len().checked_sub(deg_u + 1)?;
let base_v = raw_v_knots.len().checked_sub(deg_v + 1)?;
let mut best: Option<(usize, usize, bool, bool, usize)> = None;
for clamp_u in [false, true] {
for clamp_v in [false, true] {
let n_ctrl_u = base_u + usize::from(clamp_u) * 2;
let n_ctrl_v = base_v + usize::from(clamp_v) * 2;
if n_ctrl_u <= deg_u || n_ctrl_v <= deg_v {
continue;
}
let needed = n_ctrl_u.checked_mul(n_ctrl_v)?.checked_mul(stride)?;
if needed > available {
continue;
}
let remaining = available - needed;
if best.as_ref().is_none_or(|candidate| remaining < candidate.4) {
best = Some((n_ctrl_u, n_ctrl_v, clamp_u, clamp_v, remaining));
}
}
}
let Some((n_ctrl_u, n_ctrl_v, clamp_u, clamp_v, _)) = best else {
if std::env::var_os("OCS_TESS_DEBUG").is_some() {
eprintln!(
"acis_spline_parse[{}]: no control-net match degree={deg_u}x{deg_v} raw_knots={}x{} available={available}",
rec.index,
raw_u_knots.len(),
raw_v_knots.len()
);
}
return None;
};
let u_knots = with_clamped_ends(raw_u_knots, clamp_u)?;
let v_knots = with_clamped_ends(raw_v_knots, clamp_v)?;
// Control points are stored row-major with u varying fastest (a full row
// of u control points per v step). the kernel wants `ctrl[i_u][j_v]`.
let total = n_ctrl_u * n_ctrl_v;
let mut flat: Vec<[f64; 3]> = Vec::with_capacity(total);
let mut flat_weights: Vec<f64> = Vec::with_capacity(total);
for _ in 0..total {
let x = read_float(toks, &mut p)?;
let y = read_float(toks, &mut p)?;
let z = read_float(toks, &mut p)?;
// A rational net stores the weight alongside each point, and the
// point itself unweighted — the kernel carries the two separately and
// does the homogeneous multiply where it belongs.
flat_weights.push(if rational { read_float(toks, &mut p)? } else { 1.0 });
flat.push([x, y, z]);
}
let mut net = vec![Vec::with_capacity(n_ctrl_v); n_ctrl_u];
let mut weights = vec![Vec::with_capacity(n_ctrl_v); n_ctrl_u];
for v in 0..n_ctrl_v {
for u in 0..n_ctrl_u {
net[u].push(flat[v * n_ctrl_u + u]);
weights[u].push(flat_weights[v * n_ctrl_u + u]);
}
}
let surface = NurbsSurface3::new(
deg_u,
deg_v,
net,
u_knots,
v_knots,
rational.then_some(weights),
);
if surface.is_none() && std::env::var_os("OCS_TESS_DEBUG").is_some() {
eprintln!(
"acis_spline_parse[{}]: surface rejected degree={deg_u}x{deg_v} control={n_ctrl_u}x{n_ctrl_v}",
rec.index
);
}
surface
}
fn build_decoded_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option<NurbsSurface3> {
let spline = SatSplineSurface::from_record(rec)?;
let decoded = spline.bspline(sat)?;
// Row-major with u varying fastest, which is how ACIS writes it and the
// other way round from the net the kernel reads.
let mut net = vec![Vec::with_capacity(decoded.control_count_v); decoded.control_count_u];
let mut weights = vec![Vec::with_capacity(decoded.control_count_v); decoded.control_count_u];
for v in 0..decoded.control_count_v {
for u in 0..decoded.control_count_u {
let point = decoded.control_points[v * decoded.control_count_u + u];
net[u].push([point[0], point[1], point[2]]);
weights[u].push(point[3]);
}
}
NurbsSurface3::new(
decoded.degree_u,
decoded.degree_v,
net,
decoded.u_knots,
decoded.v_knots,
decoded.rational.then_some(weights),
)
}
/// Read `count` `(knot value, multiplicity)` pairs into an expanded raw knot
/// vector. Some ACIS families store degree-sized ends and others already carry
/// the complete knot vector; the control-net size decides that after both axes
/// have been read.
fn read_knot_vec(
toks: &[SatToken],
p: &mut usize,
count: usize,
) -> Option<Vec<f64>> {
let mut knots: Vec<f64> = Vec::new();
for _ in 0..count {
let value = read_float(toks, p)?;
let mult = read_int(toks, p)? as usize;
for _ in 0..mult {
knots.push(value);
}
}
if knots.len() < 2 {
return None;
}
Some(knots)
}
fn with_clamped_ends(mut knots: Vec<f64>, clamp: bool) -> Option<Vec<f64>> {
if clamp {
let first = *knots.first()?;
let last = *knots.last()?;
knots.insert(0, first);
knots.push(last);
}
Some(knots)
}
fn primary_subtype_reference(tokens: &[SatToken]) -> Option<usize> {
let start = tokens
.iter()
.position(|token| token.as_ident() == Some("{"))?;
if tokens.get(start + 1).and_then(SatToken::as_ident) != Some("ref")
|| tokens.get(start + 3).and_then(SatToken::as_ident) != Some("}")
{
return None;
}
tokens
.get(start + 2)?
.as_integer()
.and_then(|index| usize::try_from(index).ok())
}
fn read_int(toks: &[SatToken], p: &mut usize) -> Option<i64> {
while *p < toks.len() {
let t = &toks[*p];
*p += 1;
match t {
SatToken::Integer(v) => return Some(*v),
SatToken::Float(v) => return Some(*v as i64),
// Skip block delimiters / idents that may appear inline.
SatToken::Ident(_) | SatToken::Enum(_) => continue,
_ => return None,
}
}
None
}
fn read_float(toks: &[SatToken], p: &mut usize) -> Option<f64> {
while *p < toks.len() {
let t = &toks[*p];
*p += 1;
match t {
SatToken::Float(v) => return Some(*v),
SatToken::Integer(v) => return Some(*v as f64),
SatToken::Ident(_) | SatToken::Enum(_) => continue,
_ => return None,
}
}
None
}
+1 -35
View File
@@ -1922,7 +1922,6 @@ fn fallback_geometry(entity: &EntityType) -> Geometry {
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_) => {
let pts = solid_wire_fallback(entity);
let mut snap = vec![];
if let Some(p) = crate::entities::solid3d::point_of_reference(entity) {
snap.push((
@@ -1930,7 +1929,7 @@ fn fallback_geometry(entity: &EntityType) -> Geometry {
SnapHint::Insertion,
));
}
(pts, snap, vec![], vec![])
(vec![], snap, vec![], vec![])
}
_ => {
let s = 0.5_f64;
@@ -1939,39 +1938,6 @@ fn fallback_geometry(entity: &EntityType) -> Geometry {
}
}
/// Extract pre-computed edge-wire points from Solid3D / Region / Body entities.
///
/// Some drawings store explicit wire geometry alongside the
/// ACIS data. We use this as a visible fallback when the SAT tessellator
/// produces no mesh (e.g. binary SAB data or unsupported geometry).
fn solid_wire_fallback(entity: &EntityType) -> Vec<[f64; 3]> {
let Some(wires) = crate::entities::solid3d::fallback_wires(entity) else {
return vec![];
};
if wires.is_empty() {
return vec![];
}
// Fully supported ACIS → mesh pipeline draws body. Parseable-but-partial
// ACIS keeps source display wires so unsupported faces never disappear.
if crate::entities::solid3d::acis_has_complete_surface_support(entity) {
return vec![];
}
let mut pts: Vec<[f64; 3]> = Vec::new();
for wire in wires {
if wire.points.len() < 2 {
continue;
}
for v in &wire.points {
let transformed = crate::entities::solid3d::wire_point(wire, v);
pts.push([transformed.x, transformed.y, transformed.z]);
}
// NaN sentinel separates distinct wire segments.
pts.push([f64::NAN, f64::NAN, f64::NAN]);
}
pts
}
pub(crate) fn push_tri(out: &mut Vec<[f32; 3]>, a: Vec3, b: Vec3, c: Vec3) {
out.push([a.x, a.y, a.z]);
out.push([b.x, b.y, b.z]);