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 <[email protected]>
This commit is contained in:
SahenandClaude Opus 5.5 authored and GitHub committed 2026-10-13 10:55:56 -04:00
1 parent a1cb8d4d36
commit 2bb24d1d2a
13 files changed
+630 -71

No files matched your search

+42 -5
View File
@@ -459,6 +459,8 @@ impl ModelState {
pub struct EvalCache {
map: std::collections::HashMap<u64, CachedEval>,
order: std::collections::VecDeque<u64>,
/// What the stored results cost together.
bytes: usize,
pub hits: usize,
}
@@ -469,11 +471,20 @@ struct CachedEval {
boolean_diagnostics: kernel::BooleanDiagnostics,
ms: f64,
state: Arc<ModelState>,
/// What the state costs, worked out once when it was stored (see
/// [`ModelState::retained_bytes`]).
bytes: usize,
}
/// Results kept (oldest dropped first).
const CACHE_ENTRIES: usize = 4096;
/// Bytes of results kept (oldest dropped first). A cached result is as big as the model it
/// holds, so bounding the cache by entry count alone let a long editing session keep every
/// superseded state: the entries nothing else refers to any more are exactly the ones a byte
/// budget drops.
const CACHE_BYTES: usize = 64 << 20;
impl EvalCache {
fn get(&mut self, key: u64) -> Option<CachedEval> {
let r = self.map.get(&key).cloned();
@@ -483,18 +494,43 @@ impl EvalCache {
r
}
fn put(&mut self, key: u64, e: CachedEval) {
if self.map.insert(key, e).is_none() {
self.order.push_back(key);
let bytes = e.bytes;
match self.map.insert(key, e) {
Some(old) => self.bytes = self.bytes.saturating_sub(old.bytes),
None => self.order.push_back(key),
}
while self.order.len() > CACHE_ENTRIES {
if let Some(k) = self.order.pop_front() {
self.map.remove(&k);
self.bytes = self.bytes.saturating_add(bytes);
// One result is always kept, so a single state larger than the budget cannot empty the
// cache and force a full recompute on every undo.
while self.order.len() > CACHE_ENTRIES || (self.bytes > CACHE_BYTES && self.order.len() > 1) {
if let Some(k) = self.order.pop_front()
&& let Some(old) = self.map.remove(&k)
{
self.bytes = self.bytes.saturating_sub(old.bytes);
}
}
}
}
impl SolvedSketch {
/// Rough heap footprint (see [`ModelState::retained_bytes`]).
pub fn retained_bytes(&self) -> usize {
let entities = self.sketch.points.len() + self.sketch.curves.len() + self.sketch.constraints.len() + self.sketch.links.len();
let loops: usize = self.profiles.iter().map(|p| p.region.outer.segs.len() + p.region.holes.iter().map(|h| h.segs.len()).sum::<usize>()).sum();
entities * 192 + loops * 64 + 512
}
}
impl ModelState {
/// Rough heap footprint, for the evaluation cache's byte budget. The bodies' B-rep
/// topology is what makes a kept timeline snapshot expensive, so it dominates; sketches
/// and the rest are counted from their entity counts.
pub fn retained_bytes(&self) -> usize {
let bodies: usize = self.bodies.iter().map(|b| b.body.retained_bytes()).sum();
let sketches: usize = self.sketches.iter().map(|s| s.retained_bytes()).sum();
bodies + sketches + self.sheets.len() * 4096 + self.threads.len() * 192 + self.construct.len() * 192
}
pub fn body(&self, name: &str) -> Option<&ModelBody> {
self.bodies.iter().find(|b| b.name == name)
}
@@ -1048,6 +1084,7 @@ impl Model {
warning: warning.clone(),
boolean_diagnostics: boolean_diagnostics.clone(),
ms,
bytes: state.retained_bytes(),
state: state.clone(),
},
);