forked from 1b-agent/opencadstudio-upstream
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]>
129 lines
4.7 KiB
Rust
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()
|
|
}
|