Reuse prepared analytic sources when certifying extrusions (#273)

This commit is contained in:
selune authored and GitHub committed 2026-10-12 10:45:26 -04:00
1 parent e12758ff91
commit b45cae1e48
3 files changed
+143 -30

No files matched your search

+116 -29
View File
@@ -68,12 +68,12 @@ pub(crate) fn cap_key(outer: &[String], holes: &[Vec<String>]) -> String {
}
format!("{:x}", hash.finalize())
}
pub(crate) fn exact_region_signature(sketch: &SolvedSketch, region: &solvecraft_geom::Region2) -> Option<solvecraft_kernel::CapSignature> {
fn exact_region_signature(curves: &ExactSketchCurves, region: &solvecraft_geom::Region2) -> Option<solvecraft_kernel::CapSignature> {
if region.outer.segs.len() + region.holes.iter().map(|hole| hole.segs.len()).sum::<usize>() > 4096 || region.holes.len() > 128 {
return None;
}
let signature = |outline: &solvecraft_geom::Loop2| {
let mut ids = outline.segs.iter().map(|segment| exact_sketch_curve(sketch, segment)).collect::<Option<Vec<_>>>()?;
let mut ids = outline.segs.iter().map(|segment| curves.find(segment)).collect::<Option<Vec<_>>>()?;
ids.sort();
ids.dedup();
Some(ids)
@@ -112,6 +112,7 @@ pub(crate) fn extrude_cap_bindings(sketch: &SolvedSketch, bodies: &[solvecraft_k
}
}
}
let curves = ExactSketchCurves::new(sketch);
let mut counts = std::collections::BTreeMap::new();
let mut bindings = Vec::with_capacity(bodies.len());
for body in bodies {
@@ -121,7 +122,7 @@ pub(crate) fn extrude_cap_bindings(sketch: &SolvedSketch, bodies: &[solvecraft_k
let signature = if let Some(FaceSource::ExtrudeCap { region, high, .. }) = source {
let signature = match &cached {
Some((previous, signature)) if *previous == region => signature.clone(),
_ => exact_region_signature(sketch, region),
_ => exact_region_signature(&curves, region),
};
cached = Some((region, signature.clone()));
if let Some((outer, holes)) = &signature {
@@ -307,36 +308,55 @@ pub(crate) fn flat_source_matches(certificate: &FaceCertificate, source: &solvec
/// Match a generated segment to one exact analytic sketch curve. Coincident
/// candidates are ambiguous and never resolved by distance or candidate order.
pub(crate) fn exact_sketch_curve(sketch: &SolvedSketch, generated: &Seg2) -> Option<String> {
if sketch.sketch.curves.len() > 2000 {
return None;
}
let mut found = None;
for (index, curve) in sketch.sketch.curves.iter().enumerate() {
if curve.construction
|| !matches!(
curve.kind,
solvecraft_sketch::CurveKind::Line { .. } | solvecraft_sketch::CurveKind::Arc { .. } | solvecraft_sketch::CurveKind::Circle { .. }
)
{
continue;
pub(crate) struct ExactSketchCurves<'a> {
segments: Vec<(&'a str, Vec<Seg2>)>,
}
impl<'a> ExactSketchCurves<'a> {
/// Prepare only bounded analytic curves, once per certification pass. Point lookup
/// and segment allocation must not be repeated for every generated face.
pub(crate) fn new(sketch: &'a SolvedSketch) -> Self {
let mut segments = Vec::new();
if sketch.sketch.curves.len() > 2000 {
return Self { segments };
}
let matches = if let solvecraft_sketch::CurveKind::Circle { c, r } = curve.kind {
sketch
.sketch
.point(c)
.is_some_and(|center| exact_segment_contains(&Seg2::Arc { center, radius: r, start: 0., sweep: std::f64::consts::TAU }, generated))
} else {
sketch.sketch.segs(index).iter().any(|original| exact_segment_contains(original, generated))
};
if matches {
if found.is_some() {
return None;
for (index, curve) in sketch.sketch.curves.iter().enumerate() {
if curve.construction
|| !matches!(
curve.kind,
solvecraft_sketch::CurveKind::Line { .. }
| solvecraft_sketch::CurveKind::Arc { .. }
| solvecraft_sketch::CurveKind::Circle { .. }
)
{
continue;
}
found = Some(curve.id.clone());
let originals = if let solvecraft_sketch::CurveKind::Circle { c, r } = curve.kind {
sketch
.sketch
.point(c)
.map(|center| vec![Seg2::Arc { center, radius: r, start: 0., sweep: std::f64::consts::TAU }])
.unwrap_or_default()
} else {
sketch.sketch.segs(index)
};
segments.push((curve.id.as_str(), originals));
}
Self { segments }
}
pub(crate) fn find(&self, generated: &Seg2) -> Option<String> {
let mut found = None;
for (id, originals) in &self.segments {
if originals.iter().any(|original| exact_segment_contains(original, generated)) {
if found.is_some() {
return None;
}
found = Some(*id);
}
}
found.map(str::to_owned)
}
found
}
fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool {
@@ -486,6 +506,73 @@ mod shell_tests;
mod tests {
use super::*;
use serde_json::json;
fn solved(mut sketch: solvecraft_sketch::Sketch) -> SolvedSketch {
let report = solvecraft_sketch::solve(&mut sketch);
SolvedSketch { feature: 1, name: "sources".into(), plane: solvecraft_geom::Plane::XY, sketch, report, profiles: Vec::new() }
}
#[test]
fn prepared_sources_keep_exact_identity_and_reject_ambiguity() {
use solvecraft_geom::Vec2;
let mut sketch = solvecraft_sketch::Sketch::new();
sketch.add_line(Vec2::ZERO, Vec2::new(10., 0.), None, None, Some("line")).unwrap();
sketch.add_circle(Vec2::new(20., 0.), 2., None, Some("circle")).unwrap();
let construction = sketch.add_line(Vec2::ZERO, Vec2::new(10., 0.), None, None, Some("construction")).unwrap();
sketch.curves[construction].construction = true;
let sketch = solved(sketch);
let curves = ExactSketchCurves::new(&sketch);
let partial = Seg2::Line { a: Vec2::new(8., 0.), b: Vec2::new(2., 0.) };
assert_eq!(curves.find(&partial).as_deref(), Some("line"));
assert_eq!(curves.find(&Seg2::Arc { center: Vec2::new(20., 0.), radius: 2., start: 1., sweep: -2. }).as_deref(), Some("circle"));
assert_eq!(curves.find(&Seg2::Line { a: Vec2::ZERO, b: Vec2::new(11., 0.) }), None);
let mut ambiguous = sketch.clone();
ambiguous.sketch.add_line(Vec2::new(1., 0.), Vec2::new(9., 0.), None, None, Some("overlap")).unwrap();
assert_eq!(ExactSketchCurves::new(&ambiguous).find(&partial), None);
// A prepared pass borrows one immutable evaluated sketch, so an upstream edit
// necessarily prepares fresh sources rather than retaining stale coordinates.
let mut moved = sketch.clone();
moved.sketch = solvecraft_sketch::Sketch::new();
moved.sketch.add_line(Vec2::new(0., 1.), Vec2::new(10., 1.), None, None, Some("line")).unwrap();
assert_eq!(ExactSketchCurves::new(&moved).find(&partial), None);
}
#[test]
#[ignore = "manual optimized certification microbenchmark; no timing assertion"]
fn benchmark_prepared_extrude_sources() {
use solvecraft_geom::Vec2;
let mut sketch = solvecraft_sketch::Sketch::new();
for i in 0..1000 {
let y = i as f64;
sketch.add_line(Vec2::new(0., y), Vec2::new(10., y), None, None, None).unwrap();
}
let sketch = solved(sketch);
let queries: Vec<_> = (0..1000).map(|i| Seg2::Line { a: Vec2::new(2., i as f64), b: Vec2::new(8., i as f64) }).collect();
let start = std::time::Instant::now();
let old: Vec<_> = queries
.iter()
.map(|generated| {
let mut found = None;
for (index, curve) in sketch.sketch.curves.iter().enumerate() {
if sketch.sketch.segs(index).iter().any(|original| exact_segment_contains(original, generated)) {
if found.is_some() {
return None;
}
found = Some(curve.id.clone());
}
}
found
})
.collect();
let old_ms = start.elapsed().as_secs_f64() * 1000.;
let start = std::time::Instant::now();
let curves = ExactSketchCurves::new(&sketch);
let new: Vec<_> = queries.iter().map(|query| curves.find(query)).collect();
let new_ms = start.elapsed().as_secs_f64() * 1000.;
assert_eq!(old, new);
assert!(new.iter().all(Option::is_some));
eprintln!("1000 analytic sources / 1000 face segments: repeated={old_ms:.3}ms prepared={new_ms:.3}ms (includes preparation)");
}
#[test]
fn certificate_budgets_apply_before_saved_target_and_paint_decode() {
for certificate in [
+2 -1
View File
@@ -1800,6 +1800,7 @@ fn feature_tools_with_constructor(
}
if *operation == Operation::NewBody && extent.taper.is_none() {
let cap_bindings = crate::config::extrude_cap_bindings(&ss, &tools, 10_000_000);
let source_curves = cap_bindings.as_ref().map(|_| crate::config::ExactSketchCurves::new(&ss));
tools = tools
.into_iter()
.enumerate()
@@ -1810,7 +1811,7 @@ fn feature_tools_with_constructor(
.flatten()
.map(|source| match source {
Some(kernel::FaceSource::ExtrudeSide { segment, .. }) if cap_bindings.is_some() => {
crate::config::exact_sketch_curve(&ss, segment)
source_curves.as_ref().and_then(|curves| curves.find(segment))
}
_ => None,
})
+25
View File
@@ -1,5 +1,30 @@
# Design loading
## Analytic extrusion source preparation
New-body extrusion certificates now prepare the sketch's bounded analytic segments once
per certification pass instead of allocating segments and resolving their points for each
generated cap boundary and side face. Saved designs run this same code during cold rebuilds.
Exact containment tolerances, ambiguous-source rejection and the certification work budget
are unchanged. Prepared segments borrow an immutable evaluated sketch and are not retained
across edits.
The reproducible microbenchmark is:
```sh
cargo test -p solvecraft-doc --profile ci benchmark_prepared_extrude_sources -- --ignored --nocapture
```
Three Windows x64 optimized `ci` runs, during other compilation, compared the previous
segment reconstruction loop and the prepared matcher in the same process. For 1,000
analytic lines and 1,000 generated face segments, median time fell from **45.90 ms to
12.30 ms** (73% less, about 3.7x faster). Preparation is included and every returned source
identity is compared. This measures certification only, not complete extrusion, file-open
latency or Boolean operations; it does not establish an end-to-end loading speedup.
Remaining cold geometry rebuild and import costs are tracked in
[#255](https://github.com/bherbruck/solidcraft/issues/255); certification work is
[#254](https://github.com/bherbruck/solidcraft/issues/254).
## Cold geometry rebuilds after 0.3.1
Three alternating cold-process comparisons on the same Windows machine and optimized