M0: truck-based kernel with local fillets and chamfers, parametric document timeline
The kernel moved from monstertruck to truck: truck's booleans handle blind holes, pockets and bosses that monstertruck's did not (see the ADR). Fillets and chamfers on straight convex edges between planar faces are our own exact local B-rep operation. The document holds parameters with unit-aware expressions and a timeline that re-evaluates incrementally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dafce1f4e2
commit
a97af44979
18 files changed
+2740
-625
No files matched your search
@@ -0,0 +1,545 @@
|
||||
//! Timeline evaluation.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use serde::Serialize;
|
||||
use solvecraft_geom::{Mesh, Plane, Region2, Vec2, Vec3};
|
||||
use solvecraft_kernel::{self as kernel, Body, BoolOp};
|
||||
use solvecraft_sketch::{CurveKind, Profile, SolveReport, find_profiles, solve};
|
||||
|
||||
use crate::expr::{self, Kind, Value};
|
||||
use crate::{AxisRef, DocError, Document, Feature, FeatureKind, Operation, ProfileSel, Result};
|
||||
|
||||
/// A body in the evaluated model.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ModelBody {
|
||||
pub name: String,
|
||||
pub body: Body,
|
||||
/// Feature that created it.
|
||||
pub feature: u64,
|
||||
mesh: OnceLock<Arc<Mesh>>,
|
||||
}
|
||||
|
||||
impl ModelBody {
|
||||
pub fn new(name: String, body: Body, feature: u64) -> Self {
|
||||
ModelBody { name, body, feature, mesh: OnceLock::new() }
|
||||
}
|
||||
/// Display mesh (chord tolerance 1/1000 of the body size), computed once.
|
||||
pub fn mesh(&self) -> Arc<Mesh> {
|
||||
self.mesh
|
||||
.get_or_init(|| {
|
||||
let tol = (self.body.size() * 1e-3).max(1e-3);
|
||||
Arc::new(self.body.tessellate(tol).unwrap_or_default())
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A solved sketch in the evaluated model.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SolvedSketch {
|
||||
pub feature: u64,
|
||||
pub name: String,
|
||||
pub plane: Plane,
|
||||
pub sketch: solvecraft_sketch::Sketch,
|
||||
pub report: SolveReport,
|
||||
pub profiles: Vec<Profile>,
|
||||
}
|
||||
|
||||
/// Model after some prefix of the timeline.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ModelState {
|
||||
pub bodies: Vec<ModelBody>,
|
||||
pub sketches: Vec<SolvedSketch>,
|
||||
pub body_counter: usize,
|
||||
}
|
||||
|
||||
impl ModelState {
|
||||
pub fn body(&self, name: &str) -> Option<&ModelBody> {
|
||||
self.bodies.iter().find(|b| b.name == name)
|
||||
}
|
||||
pub fn sketch(&self, feature: u64) -> Option<&SolvedSketch> {
|
||||
self.sketches.iter().find(|s| s.feature == feature)
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluation result of one feature.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct FeatureResult {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub error: Option<String>,
|
||||
pub warning: Option<String>,
|
||||
pub ms: f64,
|
||||
pub skipped: bool,
|
||||
#[serde(skip)]
|
||||
fingerprint: u64,
|
||||
#[serde(skip)]
|
||||
pub state: Arc<ModelState>,
|
||||
}
|
||||
|
||||
/// Incremental evaluator: holds the state after every feature.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Model {
|
||||
pub results: Vec<FeatureResult>,
|
||||
pub param_errors: BTreeMap<String, String>,
|
||||
empty: Arc<ModelState>,
|
||||
/// Features recomputed by the last `evaluate`.
|
||||
pub last_recomputed: usize,
|
||||
}
|
||||
|
||||
fn fingerprint(prev: u64, f: &Feature, vals: &BTreeMap<String, Value>, rolled_back: bool) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
prev.hash(&mut h);
|
||||
rolled_back.hash(&mut h);
|
||||
serde_json::to_string(f).unwrap_or_default().hash(&mut h);
|
||||
let mut names: Vec<String> = f.kind.expressions().iter().flat_map(|e| expr::references(e)).collect();
|
||||
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
|
||||
names.extend(sketch.constraints.iter().filter_map(|c| c.param.clone()));
|
||||
}
|
||||
names.sort();
|
||||
names.dedup();
|
||||
for n in names {
|
||||
n.hash(&mut h);
|
||||
match vals.get(&n) {
|
||||
Some(v) => {
|
||||
v.v.to_bits().hash(&mut h);
|
||||
v.len.hash(&mut h);
|
||||
v.ang.hash(&mut h);
|
||||
}
|
||||
None => 0u8.hash(&mut h),
|
||||
}
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn now() -> std::time::Instant {
|
||||
std::time::Instant::now()
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn new() -> Self {
|
||||
Model::default()
|
||||
}
|
||||
|
||||
/// The final model state.
|
||||
pub fn state(&self) -> Arc<ModelState> {
|
||||
self.results.iter().rev().find(|r| !r.skipped).map(|r| r.state.clone()).unwrap_or_else(|| self.empty.clone())
|
||||
}
|
||||
|
||||
/// State just before the feature with this id (for editing a sketch in context).
|
||||
pub fn state_before(&self, id: u64) -> Arc<ModelState> {
|
||||
let Some(i) = self.results.iter().position(|r| r.id == id) else { return self.state() };
|
||||
self.results.get(..i).and_then(|s| s.iter().rev().find(|r| !r.skipped)).map(|r| r.state.clone()).unwrap_or_else(|| self.empty.clone())
|
||||
}
|
||||
|
||||
pub fn result(&self, id: u64) -> Option<&FeatureResult> {
|
||||
self.results.iter().find(|r| r.id == id)
|
||||
}
|
||||
|
||||
/// Re-evaluate the document, reusing results up to the first changed feature.
|
||||
pub fn evaluate(&mut self, doc: &Document) {
|
||||
let (vals, perr) = doc.param_values();
|
||||
self.param_errors = perr;
|
||||
let mut prev_fp = 0u64;
|
||||
let mut state = self.empty.clone();
|
||||
let mut out: Vec<FeatureResult> = Vec::with_capacity(doc.features.len());
|
||||
let mut reuse = true;
|
||||
let mut recomputed = 0;
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
for (i, f) in doc.features.iter().enumerate() {
|
||||
let rolled_back = i >= marker;
|
||||
let fp = fingerprint(prev_fp, f, &vals, rolled_back);
|
||||
prev_fp = fp;
|
||||
if reuse
|
||||
&& let Some(old) = self.results.get(i)
|
||||
&& old.id == f.id
|
||||
&& old.fingerprint == fp
|
||||
{
|
||||
if !old.skipped {
|
||||
state = old.state.clone();
|
||||
}
|
||||
out.push(old.clone());
|
||||
continue;
|
||||
}
|
||||
reuse = false;
|
||||
if f.suppressed || rolled_back {
|
||||
out.push(FeatureResult {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
error: None,
|
||||
warning: None,
|
||||
ms: 0.0,
|
||||
skipped: true,
|
||||
fingerprint: fp,
|
||||
state: state.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
recomputed += 1;
|
||||
let t0 = now();
|
||||
let mut next = (*state).clone();
|
||||
let mut warning = None;
|
||||
let r = kernel::guard(&f.name, || {
|
||||
eval_feature(doc, &vals, f, &mut next, &mut warning).map_err(|e| kernel::KernelError::Failed(e.to_string()))
|
||||
});
|
||||
let ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
let error = match r {
|
||||
Ok(()) => {
|
||||
state = Arc::new(next);
|
||||
None
|
||||
}
|
||||
Err(e) => Some(e.to_string().trim_start_matches("the operation failed: ").to_string()),
|
||||
};
|
||||
out.push(FeatureResult { id: f.id, name: f.name.clone(), error, warning, ms, skipped: false, fingerprint: fp, state: state.clone() });
|
||||
}
|
||||
self.results = out;
|
||||
self.last_recomputed = recomputed;
|
||||
}
|
||||
}
|
||||
|
||||
fn val(vals: &BTreeMap<String, Value>, e: &str, k: Kind) -> Result<f64> {
|
||||
Document::eval_in(vals, e, k)
|
||||
}
|
||||
|
||||
fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region2>> {
|
||||
let ps = &ss.profiles;
|
||||
if ps.is_empty() {
|
||||
return Err(DocError::Invalid(format!("{} has no closed profiles", ss.name)));
|
||||
}
|
||||
let out: Vec<Region2> = match sel {
|
||||
ProfileSel::All => ps.iter().map(|p| p.region.clone()).collect(),
|
||||
ProfileSel::Indices { indices } => {
|
||||
let mut v = Vec::new();
|
||||
for i in indices {
|
||||
v.push(ps.get(*i).ok_or_else(|| DocError::Unknown(format!("profile {i} of {}", ss.name)))?.region.clone());
|
||||
}
|
||||
v
|
||||
}
|
||||
ProfileSel::Curves { loops } => {
|
||||
let mut v = Vec::new();
|
||||
for l in loops {
|
||||
let mut want: Vec<&String> = l.iter().collect();
|
||||
want.sort();
|
||||
let found = ps.iter().find(|p| {
|
||||
let mut have: Vec<&String> = p.outer_curves.iter().collect();
|
||||
have.sort();
|
||||
have == want
|
||||
});
|
||||
v.push(found.ok_or_else(|| DocError::Unknown(format!("profile bounded by {l:?} in {}", ss.name)))?.region.clone());
|
||||
}
|
||||
v
|
||||
}
|
||||
ProfileSel::Points { points } => {
|
||||
let mut v = Vec::new();
|
||||
for p in points {
|
||||
// Innermost profile containing the point.
|
||||
let found = ps.iter().filter(|q| q.region.contains(*p)).min_by(|a, b| a.area.total_cmp(&b.area));
|
||||
v.push(found.ok_or_else(|| DocError::Unknown(format!("profile at {:?} in {}", [p.x, p.y], ss.name)))?.region.clone());
|
||||
}
|
||||
v
|
||||
}
|
||||
};
|
||||
if out.is_empty() {
|
||||
return Err(DocError::Invalid("no profile selected".into()));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn new_name(state: &mut ModelState, f: &Feature, k: usize) -> String {
|
||||
if let Some(n) = f.body_names.get(k)
|
||||
&& !n.is_empty()
|
||||
&& state.body(n).is_none()
|
||||
{
|
||||
return n.clone();
|
||||
}
|
||||
loop {
|
||||
state.body_counter += 1;
|
||||
let n = format!("Body{}", state.body_counter);
|
||||
if state.body(&n).is_none() {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add tool bodies to the model according to the operation.
|
||||
fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation, targets: &[String]) -> Result<()> {
|
||||
let target_idx: Vec<usize> = if targets.is_empty() {
|
||||
(0..state.bodies.len()).collect()
|
||||
} else {
|
||||
let mut v = Vec::new();
|
||||
for t in targets {
|
||||
v.push(state.bodies.iter().position(|b| &b.name == t).ok_or_else(|| DocError::Unknown(format!("body `{t}`")))?);
|
||||
}
|
||||
v
|
||||
};
|
||||
if op == Operation::NewBody || target_idx.is_empty() {
|
||||
if op == Operation::Cut || op == Operation::Intersect {
|
||||
return Err(DocError::Invalid("there is no body to cut or intersect".into()));
|
||||
}
|
||||
for (k, b) in tools.into_iter().enumerate() {
|
||||
let name = new_name(state, f, k);
|
||||
state.bodies.push(ModelBody::new(name, b, f.id));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
match op {
|
||||
Operation::Join => {
|
||||
let Some(&first) = target_idx.first() else { return Ok(()) };
|
||||
let Some(mut acc) = state.bodies.get(first).map(|b| b.body.clone()) else { return Ok(()) };
|
||||
for t in &tools {
|
||||
acc = kernel::boolean(&acc, t, BoolOp::Union)?.ok_or_else(|| DocError::Invalid("join produced nothing".into()))?;
|
||||
}
|
||||
if let Some(mb) = state.bodies.get_mut(first) {
|
||||
*mb = ModelBody::new(mb.name.clone(), acc, mb.feature);
|
||||
}
|
||||
}
|
||||
Operation::Cut | Operation::Intersect => {
|
||||
let bop = if op == Operation::Cut { BoolOp::Cut } else { BoolOp::Intersect };
|
||||
let mut remove = Vec::new();
|
||||
for &ti in &target_idx {
|
||||
let Some(mut acc) = state.bodies.get(ti).map(|b| Some(b.body.clone())) else { continue };
|
||||
for t in &tools {
|
||||
acc = match acc {
|
||||
Some(a) => kernel::boolean(&a, t, bop)?,
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
match acc {
|
||||
Some(b) => {
|
||||
if let Some(mb) = state.bodies.get_mut(ti) {
|
||||
*mb = ModelBody::new(mb.name.clone(), b, mb.feature);
|
||||
}
|
||||
}
|
||||
None => remove.push(ti),
|
||||
}
|
||||
}
|
||||
let mut i = 0;
|
||||
state.bodies.retain(|_| {
|
||||
let keep = !remove.contains(&i);
|
||||
i += 1;
|
||||
keep
|
||||
});
|
||||
}
|
||||
Operation::NewBody => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sample points inside a region (sketch coordinates).
|
||||
fn region_samples(r: &Region2) -> Vec<Vec2> {
|
||||
let mut out = vec![r.interior_point()];
|
||||
let pts = r.outer.polyline(1e-2);
|
||||
let (mut lo, mut hi) = (Vec2::new(f64::INFINITY, f64::INFINITY), Vec2::new(f64::NEG_INFINITY, f64::NEG_INFINITY));
|
||||
for p in &pts {
|
||||
lo = Vec2::new(lo.x.min(p.x), lo.y.min(p.y));
|
||||
hi = Vec2::new(hi.x.max(p.x), hi.y.max(p.y));
|
||||
}
|
||||
for i in 0..5 {
|
||||
for j in 0..5 {
|
||||
let p = Vec2::new(lo.x + (hi.x - lo.x) * (0.1 + 0.2 * i as f64), lo.y + (hi.y - lo.y) * (0.1 + 0.2 * j as f64));
|
||||
if r.contains(p) {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Booleans struggle with coincident faces (a cut starting on the face it cuts, a boss
|
||||
/// starting on the face it joins). Extending the tool past such an end changes nothing when the
|
||||
/// space beyond is empty (cut) or already material (join), so do exactly that.
|
||||
fn extend_for_coplanar(st: &ModelState, plane: &Plane, r: &Region2, lo: f64, hi: f64, op: Operation, targets: &[String]) -> (f64, f64) {
|
||||
if !matches!(op, Operation::Cut | Operation::Join) || st.bodies.is_empty() {
|
||||
return (lo, hi);
|
||||
}
|
||||
let bodies: Vec<&ModelBody> = st.bodies.iter().filter(|b| targets.is_empty() || targets.contains(&b.name)).collect();
|
||||
if bodies.is_empty() {
|
||||
return (lo, hi);
|
||||
}
|
||||
let size = bodies.iter().map(|b| b.body.size()).fold(0.0, f64::max);
|
||||
let delta = (size * 0.01).max(1e-3).min((hi - lo).abs().max(1e-3));
|
||||
let meshes: Vec<Arc<Mesh>> = bodies.iter().map(|b| b.mesh()).collect();
|
||||
let samples = region_samples(r);
|
||||
let n = plane.normal();
|
||||
let inside_any = |p: Vec3| meshes.iter().any(|m| m.contains(p));
|
||||
let ok_beyond = |h: f64, dir: f64| {
|
||||
samples.iter().all(|s| {
|
||||
let base = plane.to_world(*s);
|
||||
[0.5, 1.0].iter().all(|k| {
|
||||
let p = base + n * (h + dir * delta * k);
|
||||
let inside = inside_any(p);
|
||||
if op == Operation::Cut { !inside } else { inside }
|
||||
})
|
||||
})
|
||||
};
|
||||
let lo2 = if ok_beyond(lo, -1.0) { lo - delta } else { lo };
|
||||
let hi2 = if ok_beyond(hi, 1.0) { hi + delta } else { hi };
|
||||
(lo2, hi2)
|
||||
}
|
||||
|
||||
/// The body a fillet/chamfer applies to: by name, or the one closest to the first edge point.
|
||||
fn blend_target(state: &ModelState, body: &Option<String>, edges: &[Vec3]) -> Result<usize> {
|
||||
if let Some(n) = body {
|
||||
return state.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")));
|
||||
}
|
||||
let p = edges.first().ok_or_else(|| DocError::Invalid("no edges selected".into()))?;
|
||||
let mut best: Option<(usize, f64)> = None;
|
||||
for (i, b) in state.bodies.iter().enumerate() {
|
||||
let tol = (b.body.size() * 2e-3).max(1e-3);
|
||||
if let Ok(Some((_, d))) = b.body.nearest_edge(*p, tol)
|
||||
&& best.is_none_or(|(_, bd)| d < bd)
|
||||
{
|
||||
best = Some((i, d));
|
||||
}
|
||||
}
|
||||
best.map(|(i, _)| i).ok_or_else(|| DocError::Invalid("there is no body to modify".into()))
|
||||
}
|
||||
|
||||
fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, Vec2)> {
|
||||
match axis {
|
||||
AxisRef::SketchLine { curve } => {
|
||||
let ci = ss.sketch.curve_index(curve).ok_or_else(|| DocError::Unknown(format!("sketch line `{curve}`")))?;
|
||||
match ss.sketch.curves.get(ci).map(|c| &c.kind) {
|
||||
Some(CurveKind::Line { a, b }) => {
|
||||
let (pa, pb) = (ss.sketch.point(*a).unwrap_or_default(), ss.sketch.point(*b).unwrap_or_default());
|
||||
Ok((pa, pb - pa))
|
||||
}
|
||||
_ => Err(DocError::Invalid(format!("`{curve}` is not a line"))),
|
||||
}
|
||||
}
|
||||
AxisRef::SketchAxis { axis } => match axis.to_ascii_lowercase().as_str() {
|
||||
"x" => Ok((Vec2::ZERO, Vec2::X)),
|
||||
"y" => Ok((Vec2::ZERO, Vec2::Y)),
|
||||
other => Err(DocError::Unknown(format!("sketch axis `{other}`"))),
|
||||
},
|
||||
AxisRef::World { axis } => {
|
||||
let d = match axis.to_ascii_uppercase().as_str() {
|
||||
"X" => Vec3::X,
|
||||
"Y" => Vec3::Y,
|
||||
"Z" => Vec3::Z,
|
||||
other => return Err(DocError::Unknown(format!("axis `{other}`"))),
|
||||
};
|
||||
let pl = &ss.plane;
|
||||
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(Vec3::ZERO).abs() > 1e-9 {
|
||||
return Err(DocError::Invalid(format!("the {axis} axis does not lie in the sketch plane")));
|
||||
}
|
||||
Ok((pl.to_local(Vec3::ZERO), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
|
||||
match &f.kind {
|
||||
FeatureKind::Sketch { plane, sketch } => {
|
||||
let plane = doc.resolve_plane(vals, plane, 0)?;
|
||||
let mut sk = sketch.clone();
|
||||
doc.apply_dimension_values(vals, &mut sk)?;
|
||||
let report = solve(&mut sk);
|
||||
if !report.ok() {
|
||||
*warning = Some(format!("the sketch constraints conflict ({})", report.failing.join(", ")));
|
||||
}
|
||||
let profiles = find_profiles(&sk);
|
||||
st.sketches.retain(|s| s.feature != f.id);
|
||||
st.sketches.push(SolvedSketch { feature: f.id, name: f.name.clone(), plane, sketch: sk, report, profiles });
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Extrude { sketch, profiles, extent, operation, targets } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?.clone();
|
||||
let regions = select_profiles(&ss, profiles)?;
|
||||
let d = val(vals, &extent.distance, Kind::Length)?;
|
||||
let off = match &extent.start_offset {
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => 0.0,
|
||||
};
|
||||
let (a, b) = match (&extent.distance2, extent.direction) {
|
||||
(Some(d2), _) => (-val(vals, d2, Kind::Length)?, d),
|
||||
(None, crate::Direction::Positive) => (0.0, d),
|
||||
(None, crate::Direction::Negative) => (-d, 0.0),
|
||||
(None, crate::Direction::Symmetric) => (-d, d),
|
||||
};
|
||||
// A negative distance flips the side.
|
||||
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
|
||||
let mut tools = Vec::new();
|
||||
for r in ®ions {
|
||||
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo + off, hi + off, *operation, targets);
|
||||
tools.extend(kernel::extrude(&ss.plane, std::slice::from_ref(r), l2, h2)?);
|
||||
}
|
||||
apply_op(st, f, tools, *operation, targets)
|
||||
}
|
||||
FeatureKind::Revolve { sketch, profiles, axis, angle, operation, targets } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?.clone();
|
||||
let regions = select_profiles(&ss, profiles)?;
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
let ang = val(vals, angle, Kind::Angle)?;
|
||||
let tools = kernel::revolve(&ss.plane, ®ions, o, d, ang)?;
|
||||
apply_op(st, f, tools, *operation, targets)
|
||||
}
|
||||
FeatureKind::Fillet { edges, radius, body } | FeatureKind::Chamfer { edges, distance: radius, body } => {
|
||||
let r = val(vals, radius, Kind::Length)?;
|
||||
let ti = blend_target(st, body, edges)?;
|
||||
let Some(mb) = st.bodies.get(ti) else { return Err(DocError::Invalid("body".into())) };
|
||||
let nb =
|
||||
if matches!(f.kind, FeatureKind::Fillet { .. }) { kernel::fillet(&mb.body, edges, r)? } else { kernel::chamfer(&mb.body, edges, r)? };
|
||||
let (name, feat) = (mb.name.clone(), mb.feature);
|
||||
if let Some(slot) = st.bodies.get_mut(ti) {
|
||||
*slot = ModelBody::new(name, nb, feat);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Box { corner, length, width, height, operation } => {
|
||||
let s = Vec3::new(val(vals, length, Kind::Length)?, val(vals, width, Kind::Length)?, val(vals, height, Kind::Length)?);
|
||||
let b = kernel::box_solid(*corner, *corner + s)?;
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
|
||||
let b = kernel::cylinder(*base, *axis, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?)?;
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Sphere { center, radius, operation } => {
|
||||
let b = kernel::sphere(*center, val(vals, radius, Kind::Length)?)?;
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Torus { center, major, minor, operation } => {
|
||||
let b = kernel::torus(*center, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?;
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Combine { target, tools, operation, keep_tools } => {
|
||||
if tools.iter().any(|t| t == target) {
|
||||
return Err(DocError::Invalid("a body cannot be combined with itself".into()));
|
||||
}
|
||||
let mut tb = Vec::new();
|
||||
for t in tools {
|
||||
tb.push(st.body(t).ok_or_else(|| DocError::Unknown(format!("body `{t}`")))?.body.clone());
|
||||
}
|
||||
if st.body(target).is_none() {
|
||||
return Err(DocError::Unknown(format!("body `{target}`")));
|
||||
}
|
||||
let op = if *operation == Operation::NewBody { Operation::Join } else { *operation };
|
||||
apply_op(st, f, tb, op, std::slice::from_ref(target))?;
|
||||
if !keep_tools {
|
||||
st.bodies.retain(|b| !tools.contains(&b.name));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Move { bodies, translate, rotate_axis, angle } => {
|
||||
let t =
|
||||
Vec3::new(val(vals, &translate[0], Kind::Length)?, val(vals, &translate[1], Kind::Length)?, val(vals, &translate[2], Kind::Length)?);
|
||||
let ang = match angle {
|
||||
Some(a) => val(vals, a, Kind::Angle)?,
|
||||
None => 0.0,
|
||||
};
|
||||
let axis = rotate_axis.unwrap_or(Vec3::Z);
|
||||
for n in bodies {
|
||||
let i = st.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
if let Some(mb) = st.bodies.get_mut(i) {
|
||||
let nb = kernel::transform(&mb.body, t, Vec3::ZERO, axis, ang)?;
|
||||
*mb = ModelBody::new(mb.name.clone(), nb, mb.feature);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user