Reuse prepared analytic sources when certifying extrusions (#273)
This commit is contained in:
1 parent
e12758ff91
commit
b45cae1e48
3 files changed
+143
-30
No files matched your search
@@ -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 [
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user