forked from 1b-agent/opencadstudio-upstream
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
8016b79ddf
commit
1f642bb40a
15 files changed
+905
-1627
No files matched your search
@@ -1,131 +1,32 @@
|
||||
//! Export a truck B-rep `Solid` to an exact ACIS `SatDocument`.
|
||||
//! Export a B-rep [`Body`] to an exact ACIS `SatDocument`.
|
||||
//!
|
||||
//! OCS models 3-D solids with truck (`scene.solid_models`), but EXTRUDE /
|
||||
//! REVOLVE / SWEEP / LOFT / boolean results carry no ACIS geometry, so other
|
||||
//! CAD applications drop them on open. Converting the truck solid to ACIS lets
|
||||
//! the DWG/DXF writer store real modeler geometry.
|
||||
//! A solid built by the Model tab, by EXTRUDE / REVOLVE, or by a boolean
|
||||
//! carries no ACIS geometry of its own, so other CAD applications drop it on
|
||||
//! open. Writing the body out as real modeller geometry is what stops that.
|
||||
//!
|
||||
//! This module handles the **planar** case: a solid whose faces are all planes
|
||||
//! bounded by straight edges (boxes, extruded polygons, planar boolean results).
|
||||
//! It walks the topology into a vertex/face-ring mesh and hands it to acadrust's
|
||||
//! [`build_planar_body`], which assembles the exact B-rep. Any curved face or
|
||||
//! edge makes it return `None` — those solids are left for the NURBS path.
|
||||
//! # Exact, not facetted
|
||||
//!
|
||||
//! The kernel keeps analytic surfaces — a cylinder is a `cylinder` record and
|
||||
//! a sphere a `sphere` — so what lands in the document is the shape itself
|
||||
//! rather than a triangulation of it. That is the whole reason the Model tab
|
||||
//! builds bodies at all: a facetted export is a one-way door, and the next
|
||||
//! application to open the file would have no way back to the circle.
|
||||
//!
|
||||
//! The writing is [`acis::append`], which lives with the codec because it has
|
||||
//! to know both sides. What is left here is the small matter of putting one
|
||||
//! body into a fresh document.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use acadrust::acis::append;
|
||||
use acadrust::entities::acis::SatDocument;
|
||||
use acadrust::kernel::brep::Body;
|
||||
|
||||
use acadrust::entities::acis::{primitives::build_planar_body, SatDocument};
|
||||
use truck_modeling::{Curve, Solid, Surface};
|
||||
|
||||
/// Signed volume × 6 of the closed mesh (positive when every face ring is wound
|
||||
/// counter-clockwise as seen from outside). Used to make the export independent
|
||||
/// of truck's face-orientation convention.
|
||||
fn signed_volume6(vertices: &[[f64; 3]], faces: &[Vec<usize>]) -> f64 {
|
||||
let mut acc = 0.0;
|
||||
for f in faces {
|
||||
let a = vertices[f[0]];
|
||||
// Fan-triangulate the (planar, convex-or-not) ring from its first vertex.
|
||||
for i in 1..f.len() - 1 {
|
||||
let b = vertices[f[i]];
|
||||
let c = vertices[f[i + 1]];
|
||||
acc += a[0] * (b[1] * c[2] - b[2] * c[1])
|
||||
- a[1] * (b[0] * c[2] - b[2] * c[0])
|
||||
+ a[2] * (b[0] * c[1] - b[1] * c[0]);
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
/// Build an exact ACIS `SatDocument` from a truck solid whose faces are all
|
||||
/// planar and edges all straight. Returns `None` for any curved face/edge (left
|
||||
/// for the NURBS export), for faces with holes, or for a degenerate body.
|
||||
pub fn planar_solid_to_sat(solid: &Solid) -> Option<SatDocument> {
|
||||
let mut positions: Vec<[f64; 3]> = Vec::new();
|
||||
let mut vert_index: HashMap<_, usize> = HashMap::new();
|
||||
let mut faces: Vec<Vec<usize>> = Vec::new();
|
||||
|
||||
for shell in solid.boundaries() {
|
||||
for face in shell.face_iter() {
|
||||
// Planar faces only — a curved surface needs the NURBS path.
|
||||
if !matches!(face.surface(), Surface::Plane(_)) {
|
||||
return None;
|
||||
}
|
||||
// Single outer loop only (no holes) in the planar path.
|
||||
let boundaries = face.boundaries();
|
||||
if boundaries.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let wire = &boundaries[0];
|
||||
|
||||
// Every bounding edge must be a straight line.
|
||||
for edge in wire.edge_iter() {
|
||||
if !matches!(edge.curve(), Curve::Line(_)) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Ordered vertex ring of the loop.
|
||||
let mut ring: Vec<usize> = Vec::new();
|
||||
for v in wire.vertex_iter() {
|
||||
let key = v.id();
|
||||
let p = v.point();
|
||||
let idx = *vert_index.entry(key).or_insert_with(|| {
|
||||
positions.push([p.x, p.y, p.z]);
|
||||
positions.len() - 1
|
||||
});
|
||||
// Drop a closing vertex that repeats the ring's start.
|
||||
if ring.last() != Some(&idx) {
|
||||
ring.push(idx);
|
||||
}
|
||||
}
|
||||
if ring.len() >= 2 && ring.first() == ring.last() {
|
||||
ring.pop();
|
||||
}
|
||||
if ring.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
faces.push(ring);
|
||||
}
|
||||
}
|
||||
|
||||
if faces.len() < 4 || positions.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Make the winding outward-CCW regardless of truck's convention: a
|
||||
// correctly-oriented closed shell has positive signed volume; if it came out
|
||||
// negative every ring is inside-out, so reverse them all.
|
||||
if signed_volume6(&positions, &faces) < 0.0 {
|
||||
for f in faces.iter_mut() {
|
||||
f.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
build_planar_body(&positions, &faces)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use truck_modeling::builder;
|
||||
use truck_modeling::{Point3, Vector3};
|
||||
|
||||
/// A truck box (dimension-raising sweep vertex→edge→face→solid) must export
|
||||
/// to a valid 6-face / 12-edge / 8-vertex ACIS body.
|
||||
#[test]
|
||||
fn box_exports_to_valid_planar_sat() {
|
||||
let p = builder::vertex(Point3::new(0.0, 0.0, 0.0));
|
||||
let e = builder::tsweep(&p, Vector3::new(2.0, 0.0, 0.0));
|
||||
let f = builder::tsweep(&e, Vector3::new(0.0, 2.0, 0.0));
|
||||
let solid = builder::tsweep(&f, Vector3::new(0.0, 0.0, 2.0));
|
||||
|
||||
let sat = planar_solid_to_sat(&solid).expect("box should export to SAT");
|
||||
assert_eq!(sat.faces().len(), 6, "box has 6 planar faces");
|
||||
assert_eq!(sat.edges().len(), 12, "box has 12 straight edges");
|
||||
assert_eq!(sat.vertices().len(), 8, "box has 8 vertices");
|
||||
assert!(
|
||||
sat.validate().is_empty(),
|
||||
"exported box failed ACIS validation: {:?}",
|
||||
sat.validate()
|
||||
);
|
||||
}
|
||||
/// An exact ACIS document holding `body`, or `None` when the kernel has no
|
||||
/// record form for something in it — a spline curve, at present.
|
||||
///
|
||||
/// Refusing is the point: a document missing a face is one that parses into a
|
||||
/// different solid, and nothing downstream could tell.
|
||||
pub fn planar_solid_to_sat(body: &Body) -> Option<SatDocument> {
|
||||
let mut document = SatDocument::new();
|
||||
append(body, &mut document).ok()?;
|
||||
Some(document)
|
||||
}
|
||||
Reference in new issue
Block a user