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 <[email protected]>
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)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
//! 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()
|
||||
}
|
||||
@@ -1,679 +0,0 @@
|
||||
// ACIS B-rep → truck B-rep → mesh.
|
||||
//
|
||||
// Rebuilds every ACIS face as a truck topological `Face` and lets truck's
|
||||
// meshing kernel triangulate it, routing loaded 3DSOLID / BODY / REGION /
|
||||
// SURFACE geometry through the same NURBS/B-rep kernel the Model tab builds on
|
||||
// instead of the bespoke per-surface sampler in `solid3d_tess`.
|
||||
//
|
||||
// plane-surface → planar face from the sampled boundary loop
|
||||
// cone-surface → partial revolutions via rsweep; closed revolutions use
|
||||
// the parametric sampler
|
||||
// sphere-surface → bespoke sampler, clipped to the boundary window (truck
|
||||
// builds a full ball and cannot trim it to a partial face)
|
||||
// torus-surface → bespoke sampler, clipped to the revolution arc between
|
||||
// the tube's end caps (truck builds a full closed ring)
|
||||
// spline-surface → truck BSplineSurface, grid-sampled (see spline_tess)
|
||||
//
|
||||
// Each face is meshed independently and its triangles are oriented outward
|
||||
// using an analytic per-surface normal — truck's own face orientation is not
|
||||
// consistent across independently built faces, so normals/winding are derived
|
||||
// from geometry instead. Face coverage is recorded on `MeshLodSet`; partial
|
||||
// shells remain displayable but cannot masquerade as complete solid topology.
|
||||
|
||||
use truck_meshalgo::tessellation::{MeshableShape, MeshedShape};
|
||||
use truck_modeling::{builder, Face, InnerSpace, Point3, Rad, Shell, Vector3, Wire};
|
||||
use truck_polymesh::PolygonMesh;
|
||||
|
||||
use acadrust::entities::acis::types::Sense;
|
||||
use acadrust::entities::acis::{
|
||||
SatConeSurface, SatDocument, SatFace, SatPlaneSurface, SatSphereSurface, SatTorusSurface,
|
||||
};
|
||||
|
||||
use crate::scene::convert::solid3d_tess::{
|
||||
body_transform, collect_face_loops, cone_axis_span, cone_radius, finalize_mesh,
|
||||
tess_cone_face, tess_plane_face, tess_sphere_face, tess_torus_face, LodConfig,
|
||||
BOUNDARY_CHORD_FRAC, TRUCK_CHORD_FRAC,
|
||||
};
|
||||
use crate::scene::model::mesh_model::MeshLodSet;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
/// Slightly over 2π so revolution builders close the loop.
|
||||
const FULL: f64 = std::f64::consts::TAU + 0.2;
|
||||
/// Triangle mesh chord tolerance (world units) for planar faces, where the
|
||||
/// surface itself adds no curvature.
|
||||
const MESH_TOL: f64 = 0.01;
|
||||
|
||||
/// Analytic outward-normal rule for a face, used to orient triangles and
|
||||
/// supply smooth per-vertex normals (truck's face orientation is unreliable
|
||||
/// for independently built faces).
|
||||
enum Outward {
|
||||
/// Constant normal (planar face).
|
||||
Const([f64; 3]),
|
||||
/// Cone/cylinder: radial away from the axis, tilted by the half-angle.
|
||||
Cone {
|
||||
center: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
sin: f64,
|
||||
cos: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl Outward {
|
||||
fn at(&self, p: [f64; 3]) -> [f64; 3] {
|
||||
match self {
|
||||
Outward::Const(n) => *n,
|
||||
Outward::Cone {
|
||||
center,
|
||||
axis,
|
||||
sin,
|
||||
cos,
|
||||
} => {
|
||||
let d = vsub(p, *center);
|
||||
let h = vdot(d, *axis);
|
||||
let radial = vnorm(vsub(d, vscale(*axis, h)));
|
||||
vnorm(vsub(vscale(radial, *cos), vscale(*axis, *sin)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tessellate an ACIS SAT document by rebuilding it as truck faces.
|
||||
///
|
||||
/// Returns `None` when no face could be rebuilt (caller should fall back to
|
||||
/// the bespoke sampler).
|
||||
pub fn tessellate_sat_truck(
|
||||
sat: &SatDocument,
|
||||
name: String,
|
||||
color: [f32; 4],
|
||||
_facet_res: f64,
|
||||
) -> Option<MeshLodSet> {
|
||||
// Vertices accumulate in f64 world/local space; `finalize_mesh` splits them
|
||||
// into the double-single (high, low) pair once, so a solid at UTM scale
|
||||
// keeps full precision instead of quantizing to the f32 grid.
|
||||
let mut verts: Vec<[f64; 3]> = Vec::new();
|
||||
let mut normals: Vec<[f32; 3]> = Vec::new();
|
||||
let mut indices: Vec<u32> = Vec::new();
|
||||
let face_materials: FxHashMap<i32, acadrust::Handle> = sat
|
||||
.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: FxHashMap<i32, [f32; 4]> = sat
|
||||
.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 face_color = 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;
|
||||
};
|
||||
Some((
|
||||
owner,
|
||||
{
|
||||
let mut rgba =
|
||||
crate::scene::convert::tess_util::aci_to_rgba(&face_color);
|
||||
rgba[3] = color[3];
|
||||
rgba
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
let mut triangle_material_handles: Vec<Option<acadrust::Handle>> = Vec::new();
|
||||
let mut triangle_colors: Vec<Option<[f32; 4]>> = Vec::new();
|
||||
let face_record_indices: Vec<i32> = sat
|
||||
.records
|
||||
.iter()
|
||||
.filter(|record| record.is_a("face"))
|
||||
.map(|record| record.index)
|
||||
.collect();
|
||||
let mut complete = true;
|
||||
|
||||
for (face, face_record_index) in sat.faces().into_iter().zip(face_record_indices) {
|
||||
let Some(surf_rec) = sat.resolve(face.surface()) else {
|
||||
complete = false;
|
||||
continue;
|
||||
};
|
||||
let before = indices.len();
|
||||
let mut appended = false;
|
||||
if let Some((faces, outward, tol)) = build_face_group(sat, &face, surf_rec) {
|
||||
if !faces.is_empty() {
|
||||
let shell: Shell = faces.into();
|
||||
let poly = shell.triangulation(tol).to_polygon();
|
||||
if !poly.tri_faces().is_empty() && cone_sweep_covered(sat, &face, surf_rec, &poly)
|
||||
{
|
||||
append_group(&mut verts, &mut normals, &mut indices, &poly, &outward);
|
||||
appended = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Truck's rsweep/triangulation degenerates to zero triangles on some
|
||||
// short-arc cones (a curved wall face whose profile passes through the
|
||||
// local origin), leaving a see-through gap. Fall back to the bespoke
|
||||
// parametric sampler for just that face — it writes into the same
|
||||
// buffers, so the shared finalize below still applies uniformly.
|
||||
if !appended {
|
||||
bespoke_face(sat, &face, surf_rec, &mut verts, &mut normals, &mut indices);
|
||||
appended = indices.len() > before;
|
||||
}
|
||||
if !appended {
|
||||
complete = false;
|
||||
} else {
|
||||
let material = face_materials.get(&face_record_index).copied();
|
||||
triangle_material_handles.extend(
|
||||
std::iter::repeat(material).take((indices.len() - before) / 3),
|
||||
);
|
||||
let face_color = face_colors.get(&face_record_index).copied();
|
||||
triangle_colors.extend(
|
||||
std::iter::repeat(face_color).take((indices.len() - before) / 3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mesh = finalize_mesh(
|
||||
name,
|
||||
verts,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles,
|
||||
triangle_colors,
|
||||
color,
|
||||
body_transform(sat),
|
||||
);
|
||||
let mut set = MeshLodSet::from_lods(vec![mesh]);
|
||||
set.complete = complete;
|
||||
Some(set)
|
||||
}
|
||||
|
||||
/// Fill one face with the bespoke parametric sampler (body-local verts into the
|
||||
/// shared mesh buffers) when truck produced no triangles for it. Mirrors the
|
||||
/// surface dispatch of the standalone sampler.
|
||||
fn bespoke_face(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
surf_rec: &acadrust::entities::acis::SatRecord,
|
||||
v: &mut Vec<[f64; 3]>,
|
||||
n: &mut Vec<[f32; 3]>,
|
||||
i: &mut Vec<u32>,
|
||||
) {
|
||||
match surf_rec.entity_type.as_str() {
|
||||
"plane-surface" => {
|
||||
if let Some(p) = SatPlaneSurface::from_record(surf_rec) {
|
||||
tess_plane_face(sat, face, &p, LodConfig::HIGH.chord_frac, v, n, i);
|
||||
}
|
||||
}
|
||||
"cone-surface" => {
|
||||
if let Some(c) = SatConeSurface::from_record(surf_rec) {
|
||||
tess_cone_face(sat, face, &c, LodConfig::HIGH, v, n, i);
|
||||
}
|
||||
}
|
||||
"sphere-surface" => {
|
||||
if let Some(s) = SatSphereSurface::from_record(surf_rec) {
|
||||
tess_sphere_face(sat, face, &s, LodConfig::HIGH, v, n, i);
|
||||
}
|
||||
}
|
||||
"torus-surface" => {
|
||||
if let Some(t) = SatTorusSurface::from_record(surf_rec) {
|
||||
tess_torus_face(sat, face, &t, LodConfig::HIGH, v, n, i);
|
||||
}
|
||||
}
|
||||
"spline-surface" | "meshsurf-surface" | "bs3-surface" => {
|
||||
crate::scene::convert::spline_tess::tess_spline_face(
|
||||
sat,
|
||||
face,
|
||||
LodConfig::HIGH,
|
||||
v,
|
||||
n,
|
||||
i,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the truck face(s) + outward rule for one analytic ACIS face.
|
||||
fn build_face_group(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
surf_rec: &acadrust::entities::acis::SatRecord,
|
||||
) -> Option<(Vec<Face>, Outward, f64)> {
|
||||
// Chord tolerance scaled to a curved surface's radius, so the facet count
|
||||
// is radius-independent (matching the circle/arc wire tessellation) rather
|
||||
// than exploding on large radii.
|
||||
let curve_tol = |radius: f64| (radius.abs() * TRUCK_CHORD_FRAC).max(1e-6);
|
||||
match surf_rec.entity_type.as_str() {
|
||||
"plane-surface" => {
|
||||
let plane = SatPlaneSurface::from_record(surf_rec)?;
|
||||
let f = plane_face(sat, face)?;
|
||||
let (nx, ny, nz) = plane.normal();
|
||||
let n = if matches!(face.sense(), Sense::Reversed) {
|
||||
[-nx, -ny, -nz]
|
||||
} else {
|
||||
[nx, ny, nz]
|
||||
};
|
||||
Some((vec![f], Outward::Const(vnorm(n)), MESH_TOL))
|
||||
}
|
||||
"cone-surface" => {
|
||||
let cone = SatConeSurface::from_record(surf_rec)?;
|
||||
let tol = curve_tol(cone_radius(&cone));
|
||||
let (faces, out) = cone_faces(sat, face, &cone)?;
|
||||
Some((faces, out, tol))
|
||||
}
|
||||
// Truck builds a sphere as a full revolution and cannot trim it to the
|
||||
// face's boundary, so a partial sphere renders as a whole ball. Route
|
||||
// sphere faces to the bespoke sampler instead — it meshes only the
|
||||
// boundary's parametric window (see `tess_sphere_face`).
|
||||
"sphere-surface" => None,
|
||||
// Like a sphere, truck builds a torus as a full revolution and cannot
|
||||
// trim it to the face's boundary, so an open "C" tube renders as a whole
|
||||
// closed ring. Route torus faces to the bespoke sampler, which meshes
|
||||
// only the revolution arc between the tube's end caps (see
|
||||
// `tess_torus_face` / `torus_phi_range`).
|
||||
"torus-surface" => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Planar face ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build a planar truck face from a face's sampled boundary loop. Curved
|
||||
/// boundary edges (circles) are sampled into line segments, which keeps the
|
||||
/// wire planar so `try_attach_plane` can fit the plane.
|
||||
fn plane_face(sat: &SatDocument, face: &SatFace) -> Option<Face> {
|
||||
// A pierced face (e.g. a wall with a window opening) has one outer boundary
|
||||
// loop plus an inner loop per hole. `try_attach_plane` fits the plane from
|
||||
// the first wire and cuts the rest as holes, letting truck's mesher trim
|
||||
// them. ACIS records no outer-vs-hole flag — the kernel classifies loops at
|
||||
// runtime from geometry — so we derive the outer boundary here. (#123)
|
||||
let loops = collect_face_loops(sat, face, BOUNDARY_CHORD_FRAC);
|
||||
if loops.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// The outer boundary is the loop that encloses the rest — i.e. the one with
|
||||
// the largest area. ACIS does not guarantee it comes first in the face's
|
||||
// loop list (a pierced wall lists its window holes before the wall edge),
|
||||
// so pick it by area rather than trusting index 0. (#123)
|
||||
let outer_idx = (0..loops.len())
|
||||
.max_by(|&a, &b| {
|
||||
vlen(loop_normal(&loops[a]))
|
||||
.partial_cmp(&vlen(loop_normal(&loops[b])))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.unwrap();
|
||||
if loops[outer_idx].len() < 3 {
|
||||
return None;
|
||||
}
|
||||
// `try_attach_plane` fits its plane normal from the first (outer) wire, so
|
||||
// feeding the outer in its sampled order makes truck read it as CCW. truck
|
||||
// then cuts an inner wire as a hole only when it winds the *opposite* way,
|
||||
// so reverse any hole loop whose area normal agrees with the outer's. The
|
||||
// outer normal has the largest magnitude (largest area), keeping the sign
|
||||
// test robust against sampling noise on small holes. (#123)
|
||||
let outer_n = loop_normal(&loops[outer_idx]);
|
||||
let mut wires: Vec<Wire> = Vec::new();
|
||||
wires.push(polygon_wire(&loops[outer_idx], false)?);
|
||||
for (i, lp) in loops.iter().enumerate() {
|
||||
if i == outer_idx {
|
||||
continue;
|
||||
}
|
||||
let same = vdot(loop_normal(lp), outer_n) > 0.0;
|
||||
if let Some(w) = polygon_wire(lp, same) {
|
||||
wires.push(w);
|
||||
}
|
||||
}
|
||||
builder::try_attach_plane(&wires).ok()
|
||||
}
|
||||
|
||||
fn polygon_wire(pts: &[[f64; 3]], reverse: bool) -> Option<Wire> {
|
||||
if pts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let verts: Vec<_> = if reverse {
|
||||
pts.iter()
|
||||
.rev()
|
||||
.map(|p| builder::vertex(Point3::new(p[0], p[1], p[2])))
|
||||
.collect()
|
||||
} else {
|
||||
pts.iter()
|
||||
.map(|p| builder::vertex(Point3::new(p[0], p[1], p[2])))
|
||||
.collect()
|
||||
};
|
||||
let n = verts.len();
|
||||
let edges: Vec<_> = (0..n)
|
||||
.map(|i| builder::line(&verts[i], &verts[(i + 1) % n]))
|
||||
.collect();
|
||||
Some(edges.into())
|
||||
}
|
||||
|
||||
/// Newell area-weighted normal of a closed 3-D polygon (orientation only).
|
||||
fn loop_normal(pts: &[[f64; 3]]) -> [f64; 3] {
|
||||
let mut n = [0.0f64; 3];
|
||||
let m = pts.len();
|
||||
for i in 0..m {
|
||||
let a = pts[i];
|
||||
let b = pts[(i + 1) % m];
|
||||
n[0] += (a[1] - b[1]) * (a[2] + b[2]);
|
||||
n[1] += (a[2] - b[2]) * (a[0] + b[0]);
|
||||
n[2] += (a[0] - b[0]) * (a[1] + b[1]);
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
// ── Cone / cylinder face ─────────────────────────────────────────────────────
|
||||
|
||||
/// Build a pierced cone/cylinder lateral surface as revolution faces. A
|
||||
/// single-loop face uses the parametric sampler, avoiding partial revolution
|
||||
/// segments that can disappear when the profile crosses a coordinate plane.
|
||||
fn cone_faces(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
cone: &SatConeSurface,
|
||||
) -> Option<(Vec<Face>, Outward)> {
|
||||
let (cx, cy, cz) = cone.center();
|
||||
let (ax, ay, az) = cone.axis();
|
||||
let (ux, uy, uz) = cone.major_axis();
|
||||
let radius = cone_radius(cone);
|
||||
let sin = cone.sin_half_angle();
|
||||
let cos = cone.cos_half_angle();
|
||||
|
||||
let axis = norm(Vector3::new(ax, ay, az));
|
||||
let udir = norm(Vector3::new(ux, uy, uz));
|
||||
// v = axis × u completes the right-handed frame; angles increase from u
|
||||
// toward v (CCW about the axis), matching `builder::rsweep`.
|
||||
let vdir = Vector3::new(
|
||||
axis.y * udir.z - axis.z * udir.y,
|
||||
axis.z * udir.x - axis.x * udir.z,
|
||||
axis.x * udir.y - axis.y * udir.x,
|
||||
);
|
||||
let center = Point3::new(cx, cy, cz);
|
||||
|
||||
let (hmin, hmax) =
|
||||
cone_axis_span(sat, face, cone, [axis.x, axis.y, axis.z], [cx, cy, cz])?;
|
||||
let r_at = |h: f64| {
|
||||
if cos.abs() > 1e-9 {
|
||||
radius + h * sin / cos
|
||||
} else {
|
||||
radius
|
||||
}
|
||||
};
|
||||
|
||||
let loops = collect_face_loops(sat, face, BOUNDARY_CHORD_FRAC);
|
||||
if loops.len() == 1 {
|
||||
return None;
|
||||
}
|
||||
let arcs: Vec<(f64, f64)> = loops
|
||||
.iter()
|
||||
.map(|lp| cone_boundary_arc(lp, [cx, cy, cz], axis, udir, vdir))
|
||||
.collect();
|
||||
let outer_idx = arcs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|(_, a), (_, b)| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
|
||||
.map(|(i, _)| i)?;
|
||||
let (theta0, sweep) = arcs[outer_idx];
|
||||
let closed = sweep >= std::f64::consts::TAU;
|
||||
let hole_indices: Vec<usize> = arcs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, arc)| (i != outer_idx && arc.1 < std::f64::consts::TAU).then_some(i))
|
||||
.collect();
|
||||
if closed && hole_indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Radial direction at the arc's start angle (u rotated by theta0 about axis).
|
||||
let rad0 = udir * theta0.cos() + vdir * theta0.sin();
|
||||
|
||||
let p0 = center + rad0 * r_at(hmin) + axis * hmin;
|
||||
let p1 = center + rad0 * r_at(hmax) + axis * hmax;
|
||||
let profile = builder::line(&builder::vertex(p0), &builder::vertex(p1));
|
||||
let shell: Shell = builder::rsweep(&profile, center, axis, Rad(sweep));
|
||||
let mut faces: Vec<Face> = shell.face_iter().cloned().collect();
|
||||
if closed {
|
||||
let lateral = faces.first_mut()?;
|
||||
for index in hole_indices {
|
||||
let wire = polygon_wire(&loops[index], false)?;
|
||||
lateral.try_add_boundary(wire).ok()?;
|
||||
}
|
||||
}
|
||||
|
||||
let out = Outward::Cone {
|
||||
center: [cx, cy, cz],
|
||||
axis: [axis.x, axis.y, axis.z],
|
||||
sin,
|
||||
cos,
|
||||
};
|
||||
Some((faces, out))
|
||||
}
|
||||
|
||||
/// Whether a triangulated cone face actually spans the revolution its boundary
|
||||
/// claims.
|
||||
///
|
||||
/// `rsweep` returns a full turn as several segment faces (120° apiece), and a
|
||||
/// hole only ever lands on one of them: a window wider than a segment, or a
|
||||
/// segment truck degenerates on, silently costs that whole third of the tube.
|
||||
/// The face still reports triangles, so nothing downstream notices — the wall is
|
||||
/// simply missing there and the solid reads as open. Measuring the meshed
|
||||
/// coverage against the boundary catches every such loss, whatever caused it,
|
||||
/// and hands the face to the parametric sampler instead.
|
||||
///
|
||||
/// Non-cone faces and genuinely partial cones pass straight through.
|
||||
fn cone_sweep_covered(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
surf_rec: &acadrust::entities::acis::SatRecord,
|
||||
poly: &PolygonMesh,
|
||||
) -> bool {
|
||||
use std::f64::consts::TAU;
|
||||
/// A meshed full revolution may skip at most this much before we call it
|
||||
/// torn — comfortably wider than one chord step, far under a 120° segment.
|
||||
const MAX_GAP: f64 = std::f64::consts::FRAC_PI_4;
|
||||
|
||||
let Some(cone) = SatConeSurface::from_record(surf_rec) else {
|
||||
return true;
|
||||
};
|
||||
let (cx, cy, cz) = cone.center();
|
||||
let (ax, ay, az) = cone.axis();
|
||||
let (ux, uy, uz) = cone.major_axis();
|
||||
let axis = norm(Vector3::new(ax, ay, az));
|
||||
let udir = norm(Vector3::new(ux, uy, uz));
|
||||
let vdir = Vector3::new(
|
||||
axis.y * udir.z - axis.z * udir.y,
|
||||
axis.z * udir.x - axis.x * udir.z,
|
||||
axis.x * udir.y - axis.y * udir.x,
|
||||
);
|
||||
|
||||
// Only a face whose boundary closes the revolution owes us a full sweep.
|
||||
let loops = collect_face_loops(sat, face, BOUNDARY_CHORD_FRAC);
|
||||
let closed = loops
|
||||
.iter()
|
||||
.map(|lp| cone_boundary_arc(lp, [cx, cy, cz], axis, udir, vdir).1)
|
||||
.fold(0.0f64, f64::max)
|
||||
>= TAU;
|
||||
if !closed {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut angles: Vec<f64> = Vec::new();
|
||||
for p in poly.positions() {
|
||||
let d = vsub([p.x, p.y, p.z], [cx, cy, cz]);
|
||||
let ru = vdot(d, [udir.x, udir.y, udir.z]);
|
||||
let rv = vdot(d, [vdir.x, vdir.y, vdir.z]);
|
||||
if ru.hypot(rv) > 1e-9 {
|
||||
angles.push(rv.atan2(ru).rem_euclid(TAU));
|
||||
}
|
||||
}
|
||||
if angles.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
angles.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let mut widest = 0.0f64;
|
||||
for i in 0..angles.len() {
|
||||
let next = if i + 1 < angles.len() {
|
||||
angles[i + 1]
|
||||
} else {
|
||||
angles[0] + TAU
|
||||
};
|
||||
widest = widest.max(next - angles[i]);
|
||||
}
|
||||
widest <= MAX_GAP
|
||||
}
|
||||
|
||||
/// Smallest angular arc `(theta_start, sweep)` about the cone axis enclosing the
|
||||
/// face's boundary points, robust to the ±π seam. Returns `(0.0, FULL)` when the
|
||||
/// points wrap the whole revolution (a closed rim) or the boundary is unusable,
|
||||
/// so a real cylinder/cone still sweeps a complete (overlapped) circle.
|
||||
fn cone_boundary_arc(
|
||||
poly: &[[f64; 3]],
|
||||
center: [f64; 3],
|
||||
_axis: Vector3,
|
||||
udir: Vector3,
|
||||
vdir: Vector3,
|
||||
) -> (f64, f64) {
|
||||
use std::f64::consts::TAU;
|
||||
let mut angles: Vec<f64> = Vec::new();
|
||||
for p in poly {
|
||||
let d = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
|
||||
let ru = d[0] * udir.x + d[1] * udir.y + d[2] * udir.z;
|
||||
let rv = d[0] * vdir.x + d[1] * vdir.y + d[2] * vdir.z;
|
||||
if ru.hypot(rv) > 1e-9 {
|
||||
angles.push(rv.atan2(ru));
|
||||
}
|
||||
}
|
||||
if angles.len() < 2 {
|
||||
return (0.0, FULL);
|
||||
}
|
||||
angles.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
// Largest angular gap between consecutive samples (wrapping past the seam);
|
||||
// the enclosing arc is its complement.
|
||||
let mut max_gap = 0.0;
|
||||
let mut gap_at = angles.len() - 1;
|
||||
for i in 0..angles.len() {
|
||||
let a = angles[i];
|
||||
let b = if i + 1 < angles.len() {
|
||||
angles[i + 1]
|
||||
} else {
|
||||
angles[0] + TAU
|
||||
};
|
||||
let g = b - a;
|
||||
if g > max_gap {
|
||||
max_gap = g;
|
||||
gap_at = i;
|
||||
}
|
||||
}
|
||||
let span = TAU - max_gap;
|
||||
// Nearly a full revolution → treat as a closed rim.
|
||||
if span >= TAU * 0.98 - 1e-9 || span < 1e-6 {
|
||||
return (0.0, FULL);
|
||||
}
|
||||
// Start at the sample just after the largest gap, sweep CCW by `span`.
|
||||
let start = angles[(gap_at + 1) % angles.len()];
|
||||
(start, span)
|
||||
}
|
||||
|
||||
// ── Mesh append with analytic outward normals ────────────────────────────────
|
||||
|
||||
/// Append one face's triangulation to `mesh`, computing smooth per-vertex
|
||||
/// normals from the outward rule and flipping any triangle whose winding
|
||||
/// disagrees with that outward direction.
|
||||
fn append_group(
|
||||
verts: &mut Vec<[f64; 3]>,
|
||||
normals: &mut Vec<[f32; 3]>,
|
||||
out_indices: &mut Vec<u32>,
|
||||
poly: &PolygonMesh,
|
||||
outward: &Outward,
|
||||
) {
|
||||
let positions = poly.positions();
|
||||
let base = verts.len() as u32;
|
||||
for p in positions {
|
||||
let pos = [p.x, p.y, p.z];
|
||||
verts.push(pos);
|
||||
let n = outward.at(pos);
|
||||
normals.push([n[0] as f32, n[1] as f32, n[2] as f32]);
|
||||
}
|
||||
for tri in poly.tri_faces() {
|
||||
let (i0, i1, i2) = (tri[0].pos, tri[1].pos, tri[2].pos);
|
||||
let a = pt(positions[i0]);
|
||||
let b = pt(positions[i1]);
|
||||
let c = pt(positions[i2]);
|
||||
let gn = vcross(vsub(b, a), vsub(c, a));
|
||||
let cen = [
|
||||
(a[0] + b[0] + c[0]) / 3.0,
|
||||
(a[1] + b[1] + c[1]) / 3.0,
|
||||
(a[2] + b[2] + c[2]) / 3.0,
|
||||
];
|
||||
let out = outward.at(cen);
|
||||
let (j0, j1, j2) = (base + i0 as u32, base + i1 as u32, base + i2 as u32);
|
||||
if vdot(gn, out) < 0.0 {
|
||||
out_indices.extend_from_slice(&[j0, j2, j1]);
|
||||
} else {
|
||||
out_indices.extend_from_slice(&[j0, j1, j2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn pt(p: Point3) -> [f64; 3] {
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
// ── Vector helpers (cgmath Vector3) ──────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
fn norm(v: Vector3) -> Vector3 {
|
||||
let len = v.magnitude();
|
||||
if len < 1e-12 {
|
||||
Vector3::unit_z()
|
||||
} else {
|
||||
v / len
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vector helpers ([f64; 3]) ────────────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
fn vsub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
#[inline]
|
||||
fn vscale(a: [f64; 3], s: f64) -> [f64; 3] {
|
||||
[a[0] * s, a[1] * s, a[2] * s]
|
||||
}
|
||||
#[inline]
|
||||
fn vdot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
#[inline]
|
||||
fn vcross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[
|
||||
a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0],
|
||||
]
|
||||
}
|
||||
#[inline]
|
||||
fn vlen(a: [f64; 3]) -> f64 {
|
||||
vdot(a, a).sqrt()
|
||||
}
|
||||
#[inline]
|
||||
fn vnorm(a: [f64; 3]) -> [f64; 3] {
|
||||
let l = vlen(a);
|
||||
if l < 1e-12 {
|
||||
[0.0, 0.0, 1.0]
|
||||
} else {
|
||||
[a[0] / l, a[1] / l, a[2] / l]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,7 @@
|
||||
pub mod acad_to_truck;
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub mod acis_export;
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub mod acis_to_truck;
|
||||
/// Without `solid3d` (e.g. wasm) ACIS/SAT meshing is unavailable; the entry
|
||||
/// point stays so callers compile, returning no mesh.
|
||||
#[cfg(not(feature = "solid3d"))]
|
||||
pub mod acis_to_truck {
|
||||
use crate::scene::model::mesh_model::MeshLodSet;
|
||||
use acadrust::entities::acis::SatDocument;
|
||||
pub fn tessellate_sat_truck(
|
||||
_sat: &SatDocument,
|
||||
_name: String,
|
||||
_color: [f32; 4],
|
||||
_facet_res: f64,
|
||||
) -> Option<MeshLodSet> {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub mod acis_kernel;
|
||||
pub mod dgn_linestyle;
|
||||
pub mod truck_tess;
|
||||
pub mod tessellate;
|
||||
|
||||
@@ -46,7 +46,6 @@ pub(crate) const EDGE_CHORD_FRAC: f64 = 0.002;
|
||||
/// a hexagonal prism whose flats sink far inside the true radius, tearing the
|
||||
/// wall away from the planar faces and caps that meet it.
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub(crate) const TRUCK_CHORD_FRAC: f64 = 0.005;
|
||||
/// Boundary-loop sampling for parameter-range classification (which arc of a
|
||||
/// sphere/torus a face covers): a fine fraction so the classification is
|
||||
/// accurate; the points are not rendered.
|
||||
@@ -276,9 +275,10 @@ fn tessellate_sat(
|
||||
))
|
||||
}
|
||||
|
||||
/// Tessellate an ACIS document, preferring the truck B-rep kernel and falling
|
||||
/// back to the bespoke per-surface sampler when truck can't rebuild the shell
|
||||
/// (e.g. an unhandled surface type).
|
||||
/// Tessellate an ACIS document, preferring the geometry kernel and falling
|
||||
/// back to the bespoke per-surface sampler when the kernel cannot rebuild the
|
||||
/// whole shell (a surface kind it does not model, a face whose boundary it
|
||||
/// cannot express in that surface's own parameters).
|
||||
fn tessellate_acis(
|
||||
sat: &SatDocument,
|
||||
name: String,
|
||||
@@ -286,7 +286,7 @@ fn tessellate_acis(
|
||||
facet_res: f64,
|
||||
isolines: usize,
|
||||
) -> Option<MeshLodSet> {
|
||||
let truck = crate::scene::convert::acis_to_truck::tessellate_sat_truck(
|
||||
let truck = crate::scene::convert::acis_kernel::tessellate_sat(
|
||||
sat,
|
||||
name.clone(),
|
||||
color,
|
||||
@@ -2049,7 +2049,7 @@ pub(crate) fn tess_plane_face(
|
||||
}
|
||||
|
||||
// Fan triangulation from vertex 0 (outer loop only; holes are handled by
|
||||
// the truck B-rep path in `acis_to_truck`).
|
||||
// the kernel B-rep path in `acis_kernel`).
|
||||
let n = poly.len() as u32;
|
||||
for i in 1..(n - 1) {
|
||||
indices.extend_from_slice(&[base, base + i, base + i + 1]);
|
||||
|
||||
Reference in new issue
Block a user