perf: prism pairs before the mesh, bodies measured once, and a STEP import that copies less (#714)
A cut or an intersection between prisms (a plate and the tools through it) is now decided in 2D before either operand is meshed. A body's size and volume are measured once, a placement reads its orientation from its matrix, the evaluation cache is bounded to 64 MiB, and STEP import copies fewer shells. Hole plates evaluate 20-30% faster with less memory, and models are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a1cb8d4d36
commit
2bb24d1d2a
13 files changed
+630
-71
No files matched your search
+96
-12
@@ -20,6 +20,13 @@ pub(crate) type Solid = mt::Solid;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Body {
|
||||
pub(crate) solid: std::sync::Arc<Solid>,
|
||||
/// Bounding-box diagonal, measured by [`Body::new`] while it read the vertices anyway; 0
|
||||
/// means "not measured, walk the geometry". Every kernel tolerance is a fraction of it.
|
||||
pub(crate) size: f64,
|
||||
/// The last volume measured for this solid and the tolerance it was measured at, shared by
|
||||
/// clones because they share the solid. A boolean measures its operands, and a later boolean
|
||||
/// takes the result as an operand again, so the same body is measured twice otherwise.
|
||||
pub(crate) volume: Option<std::sync::Arc<std::sync::Mutex<Option<(u64, f64)>>>>,
|
||||
/// Missing-face check from the orientation mesh, reusable at exactly its tolerance.
|
||||
pub(crate) coarse_missing: Option<(f64, usize)>,
|
||||
/// Set for mesh bodies (`solid` is then empty).
|
||||
@@ -441,6 +448,25 @@ fn polyline_mid(pts: &[Vec3]) -> (Vec3, f64) {
|
||||
(pts.first().copied().unwrap_or_default(), len)
|
||||
}
|
||||
|
||||
/// Deflection the orientation mesh uses: coarse enough for a large solid, fine enough that a
|
||||
/// small one still tessellates. `unmeshed_faces` counts at the same value, so the count the
|
||||
/// orientation mesh produced is reused instead of meshing the body again.
|
||||
fn orientation_tolerance(span: f64) -> f64 {
|
||||
(span * 0.01).clamp(1e-3, 1.0)
|
||||
}
|
||||
|
||||
/// Bounding-box diagonal of a solid's vertices (1 when it has no finite extent).
|
||||
fn span_of(solid: &Solid) -> f64 {
|
||||
let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
|
||||
let mut hi = -lo;
|
||||
for v in solid.vertex_iter() {
|
||||
let p = from_p3(v.point());
|
||||
lo = Vec3::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
|
||||
hi = Vec3::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
|
||||
}
|
||||
if lo.x.is_finite() { (hi - lo).len() } else { 1.0 }
|
||||
}
|
||||
|
||||
impl Body {
|
||||
/// The very same body (shared data), not just an equal-looking one. Its history (how an
|
||||
/// operation made it) is not part of it: an operation that leaves a body as it is (a cut by
|
||||
@@ -466,17 +492,8 @@ impl Body {
|
||||
let mut solid = rephase_revolved(solid);
|
||||
// Orient outward: a closed solid must have positive volume (meshed finely enough that
|
||||
// every face comes out: a face missing from a coarse mesh can flip the sign).
|
||||
let span = {
|
||||
let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
|
||||
let mut hi = -lo;
|
||||
for v in solid.vertex_iter() {
|
||||
let p = from_p3(v.point());
|
||||
lo = Vec3::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
|
||||
hi = Vec3::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
|
||||
}
|
||||
if lo.x.is_finite() { (hi - lo).len() } else { 1.0 }
|
||||
};
|
||||
let tol = (span * 0.01).clamp(1e-3, 1.0);
|
||||
let span = span_of(&solid);
|
||||
let tol = orientation_tolerance(span);
|
||||
let orient = |tol: f64| {
|
||||
guard("orient", || {
|
||||
let shells = mesh_shells(&solid, tol);
|
||||
@@ -502,6 +519,8 @@ impl Body {
|
||||
}
|
||||
Ok(Body {
|
||||
solid: std::sync::Arc::new(solid),
|
||||
size: if span.is_finite() && span > 0.0 { span } else { 0.0 },
|
||||
volume: Some(std::sync::Arc::new(std::sync::Mutex::new(None))),
|
||||
coarse_missing: (v >= 0.0).then_some((tol, missing)),
|
||||
mesh: None,
|
||||
color: None,
|
||||
@@ -513,6 +532,35 @@ impl Body {
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`Body::new`], for a solid made by placing an already-oriented body (see
|
||||
/// [`crate::ops::transform_matrix`]): a placement keeps the faces pointing the way they did
|
||||
/// unless it mirrors them, so its orientation is read from the matrix rather than meshed
|
||||
/// again. Only for closed solids; `coarse` is the input's cached missing-face count, which a
|
||||
/// rigid placement preserves exactly.
|
||||
pub(crate) fn new_placed(solid: Solid, flip: bool, coarse: Option<(f64, usize)>) -> Result<Body> {
|
||||
if solid.boundaries().is_empty() {
|
||||
return Err(KernelError::Failed("empty result".into()));
|
||||
}
|
||||
let mut solid = rephase_revolved(solid);
|
||||
let span = span_of(&solid);
|
||||
if flip {
|
||||
solid.not();
|
||||
}
|
||||
Ok(Body {
|
||||
solid: std::sync::Arc::new(solid),
|
||||
size: if span.is_finite() && span > 0.0 { span } else { 0.0 },
|
||||
volume: Some(std::sync::Arc::new(std::sync::Mutex::new(None))),
|
||||
coarse_missing: coarse,
|
||||
mesh: None,
|
||||
color: None,
|
||||
paint: None,
|
||||
splits: None,
|
||||
face_sources: None,
|
||||
regularization_protected: false,
|
||||
history: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn inherit_regularization_protection(mut self, input: &Body) -> Self {
|
||||
self.regularization_protected |= input.regularization_protected;
|
||||
self
|
||||
@@ -843,8 +891,13 @@ impl Body {
|
||||
self
|
||||
}
|
||||
|
||||
/// Characteristic size (bounding box diagonal) for tolerances.
|
||||
/// Characteristic size (bounding box diagonal) for tolerances. [`Body::new`] measured it
|
||||
/// while it read the vertices anyway, so the answer is cached; a body built by hand carries
|
||||
/// 0 and is walked here once, and mesh bodies always measure their triangles.
|
||||
pub fn size(&self) -> f64 {
|
||||
if self.mesh.is_none() && self.size > 0.0 {
|
||||
return self.size;
|
||||
}
|
||||
let mut b = solidcraft_geom::Aabb3::EMPTY;
|
||||
if let Some(m) = &self.mesh {
|
||||
for p in &m.positions {
|
||||
@@ -858,6 +911,37 @@ impl Body {
|
||||
if d.is_finite() && d > 0.0 { d } else { 1.0 }
|
||||
}
|
||||
|
||||
/// The body's volume from a mesh at `tol`, measured once per (solid, tolerance): clones of
|
||||
/// a body share the measurement, so an operand a later boolean takes again is not meshed
|
||||
/// twice for the same number. `None` when the body cannot be meshed.
|
||||
pub fn volume_at(&self, tol: f64) -> Option<f64> {
|
||||
let key = if tol.is_finite() && tol > 0.0 { tol } else { 0.05 };
|
||||
let key = key.to_bits();
|
||||
if let Some(cell) = &self.volume
|
||||
&& let Ok(cached) = cell.lock()
|
||||
&& let Some((cached_tol, volume)) = *cached
|
||||
&& cached_tol == key
|
||||
{
|
||||
return Some(volume);
|
||||
}
|
||||
let volume = self.tessellate(f64::from_bits(key)).ok()?.measure().volume;
|
||||
if let Some(cell) = &self.volume
|
||||
&& let Ok(mut slot) = cell.lock()
|
||||
{
|
||||
*slot = Some((key, volume));
|
||||
}
|
||||
Some(volume)
|
||||
}
|
||||
|
||||
/// Rough heap footprint, for the evaluation cache's byte budget: the B-rep topology is what
|
||||
/// makes a kept timeline snapshot expensive, so it dominates the estimate.
|
||||
pub fn retained_bytes(&self) -> usize {
|
||||
if let Some(m) = &self.mesh {
|
||||
return m.positions.len() * 24 + m.triangles.len() * 12;
|
||||
}
|
||||
self.solid.face_iter().count() * 2048 + self.solid.edge_iter().count() * 512 + self.solid.vertex_iter().count() * 96
|
||||
}
|
||||
|
||||
/// Distinct edges in a stable order (the order of `edges()` and `tessellate().edges`).
|
||||
pub(crate) fn unique_edges(solid: &Solid) -> Vec<mt::Edge> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
|
||||
Reference in new issue
Block a user