Files
OneCAD-Draft/src/scene/convert/acis_kernel.rs
T
Hakan SevenandClaude Opus 5 1f642bb40a feat(model): build and read solids with the geometry kernel
Every 3-D solid OCS makes or opens now goes through cadkernel, which holds
analytic surfaces — a cylinder is a cylinder record, not a spline that looks
like one. That is what lets a solid built here be written back out as exact
ACIS instead of a triangulation, and a facetted export is a one-way door: the
next application to open the file would have no way back to the circle.

The Model tab's seven primitives, the Design-group booleans, 3DROTATE,
3DMIRROR, 3DALIGN, SLICE, SECTION, POLYSOLID and PYRAMID all come from the
kernel now, and `scene.solid_models` holds a `Body`. A mirrored solid needed
its own care before; the kernel reverses the loops on the way through, so it
comes back the right way out rather than lighting black.

EXTRUDE and REVOLVE sweep the profile `entities::curve` already defines, so a
circle, an arc-bulged polyline and a closed spline arrive as the same thing.
A circle extrudes into four cylinder patches rather than a run of chords, and
a profile turned about an axis becomes a cone, a sphere or a torus. A spline
profile has no analytic side wall and is refused rather than approximated.

ACIS goes both ways through the codec's own bridge: `lift` to read and
`append` to write. That drops the 679-line truck conversion and the planar-
only exporter, and the tessellator keeps its bespoke sampler as a fallback for
whatever the kernel cannot yet express — reported rather than passed off as
whole, since a mesh missing a wall looks finished.

truck-shapeops is no longer used and is gone from the manifest. truck-modeling
stays for now: fourteen files use it as a 3-D NURBS evaluator, which the
kernel has no answer for yet, and SWEEP and LOFT have no kernel operation
behind them either.

The two `automation` test failures are unchanged from before this and are not
caused by it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-10 00:32:34 +03:00

129 lines
4.7 KiB
Rust

//! Drawing an ACIS solid by lifting it into the geometry kernel.
//!
//! A DWG or DXF carries its solids as ACIS records — analytic surfaces and the
//! topology joining them — and the kernel holds exactly that. So the shortest
//! path from a file to a picture is to lift the document into a `Body` and ask
//! 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.
use acadrust::acis::lift;
use acadrust::entities::acis::SatDocument;
use acadrust::kernel::brep;
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
/// How far a triangle may sit from the surface it lies on.
const SAG: f64 = 0.05;
/// What counts as the same point when the kernel reads a body over.
const TOL: f64 = 1e-9;
/// 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.
pub fn tessellate_sat(
document: &SatDocument,
name: String,
color: [f32; 4],
sag: f64,
) -> Option<MeshLodSet> {
let (bodies, loss) = lift(document);
if bodies.is_empty() {
return None;
}
let sag = if sag > 0.0 { sag } else { SAG };
let mut positions: Vec<[f64; 3]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
// 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;
for body in &bodies {
for face in body.face_keys() {
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]),
);
}
}
if indices.is_empty() {
return None;
}
// The renderer holds each position as a coarse float plus a fine
// correction, so a solid at survey coordinates keeps its last millimetres
// instead of losing them to f32.
let mut verts = Vec::with_capacity(positions.len());
let mut verts_low = Vec::with_capacity(positions.len());
for point in &positions {
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
verts.push(high);
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,
]);
}
let mut set = MeshLodSet::from_single(MeshModel {
name,
verts,
verts_low,
normals,
indices,
triangle_material_handles: Vec::new(),
triangle_colors: Vec::new(),
color,
selected: false,
});
set.complete = loss.is_empty() && undrawn == 0;
Some(set)
}
/// 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 { SAG };
bodies
.iter()
.flat_map(|body| brep::edge_polylines(body, sag))
.collect()
}