Files
solidcraft-upstream/crates/engine/src/manufacturing/holes.rs
T

180 lines
8.6 KiB
Rust

//! Explicit hole picks and immutable resolution of their persistent identities.
use super::snapshot::{PlacedBody, Profile};
use crate::{
EngineError, Result, Session, cam,
geom::{Seg2, Vec3},
};
use serde::{Deserialize, Serialize};
use solidcraft_doc::apply_point;
fn bad(message: &str) -> EngineError {
EngineError::Other(message.into())
}
#[derive(Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub(crate) enum Pick {
Face {
body: String,
index: usize,
#[serde(default)]
occurrence: Option<Vec<u64>>,
},
Profile {
sketch: u64,
index: usize,
#[serde(default)]
occurrence: Vec<u64>,
},
}
#[derive(Serialize)]
pub(crate) struct Selected {
pub center: [f64; 2],
pub diameter: f64,
}
pub(super) enum Hole {
Face { body: Box<PlacedBody>, name: String },
Profile(Profile),
}
impl Hole {
pub(super) fn capture(s: &Session, setup: &cam::Setup, source: &cam::HoleSource, tolerance: f64) -> Result<Self> {
source.validate().map_err(super::error)?;
match source {
cam::HoleSource::Face { target, name } => {
if !setup.models.contains(target) {
return Err(bad("select a hole on one of this setup's model occurrences"));
}
Ok(Self::Face { body: Box::new(PlacedBody::capture(s, target)?), name: name.clone() })
}
cam::HoleSource::Profile { profile } => Ok(Self::Profile(Profile::capture(s, setup, profile, tolerance)?)),
}
}
pub(super) fn resolve(self, frame: cam::Frame, tolerance: f64) -> Result<Selected> {
let (origin, axis, diameter) = match self {
Self::Face { body, name } => {
let names = solidcraft_doc::naming::face_names(&body.body);
let mut indices = names.iter().enumerate().filter_map(|(i, n)| (*n == name).then_some(i));
let index = indices.next().ok_or_else(|| bad("CAM hole face missing; reselect it"))?;
if indices.next().is_some() {
return Err(bad("CAM hole face is ambiguous; reselect it"));
}
let (origin, axis, radius) = crate::kernel::hole_axis(&body.body.body, index, cam::profile_tolerance(tolerance))?;
let point = |p| frame.local(apply_point(&body.placement, p));
let center = point(origin);
let x = axis.any_perp();
let y = axis.cross(x);
let dx = point(origin + x) - center;
let dy = point(origin + y) - center;
if (dx.len() - dy.len()).abs() > 1e-9 || dx.dot(dy).abs() > 1e-9 {
return Err(bad("non-circular placed hole; select a rigid occurrence"));
}
(center, point(origin + axis) - center, 2. * radius * dx.len())
}
Self::Profile(profile) => {
if !profile.region.holes.is_empty() {
return Err(bad("select one circular profile per drill center"));
}
let Some(Seg2::Arc { center, radius, .. }) = profile.region.outer.segs.first() else {
return Err(bad("drilling profile must be circular"));
};
let (center, radius) = (*center, *radius);
let mut sweep = 0.;
let segments = &profile.region.outer.segs;
for (a, next) in segments.iter().zip(segments.iter().cycle().skip(1)).take(segments.len()) {
let Seg2::Arc { center: c, radius: r, sweep: angle, .. } = a else {
return Err(bad("drilling profile must be circular"));
};
if c.dist(center) > 1e-9 || (r - radius).abs() > 1e-9 || a.end().dist(next.start()) > 1e-8 {
return Err(bad("drilling profile must be a closed circle"));
}
sweep += angle;
}
if !sweep.is_finite() || (sweep.abs() - std::f64::consts::TAU).abs() > 1e-8 || radius <= 0. {
return Err(bad("drilling profile must be one complete circle"));
}
let (x, y) = (profile.plane.x, profile.plane.y);
if (x.len() - y.len()).abs() > 1e-9 || x.dot(y).abs() > 1e-9 {
return Err(bad("non-circular placed profile; select a rigid occurrence"));
}
(profile.plane.to_world(center), x.cross(y), 2. * radius * x.len())
}
};
let axis = axis.normalized().ok_or_else(|| bad("invalid hole axis"))?;
if !origin.is_finite() || !axis.is_finite() || !diameter.is_finite() || diameter <= 0. || axis.cross(Vec3::Z).len() > 1e-9 {
return Err(bad("drilling needs a finite circular hole parallel to setup Z"));
}
Ok(Selected { center: [origin.x, origin.y], diameter })
}
}
pub(crate) fn select(s: &Session, setup: &cam::Setup, picks: Vec<Pick>) -> Result<(Vec<cam::HoleSource>, Vec<Selected>)> {
if picks.is_empty() || picks.len() > cam::MAX_ITEMS {
return Err(bad("select between one and 1000 holes"));
}
let state = s.model.state();
let doc = s.placed_document();
let mut sources = Vec::new();
let mut seen = Vec::new();
let mut selected = Vec::new();
for pick in picks {
let source = match pick {
Pick::Face { body, index, occurrence } => {
let target = if let Some(occurrence) = occurrence {
cam::BodyRef { body, occurrence }
} else {
if setup.models.len().saturating_mul(doc.occurrences.len() + state.bodies.len() + 64) > 2_000_000 {
return Err(bad("hole selection work limit; provide a definition body and occurrence path"));
}
// Only these explicit model placements may be selected. Do not expand
// an entire shared assembly merely to interpret a viewport label.
let mut matching = setup.models.iter().filter(|target| {
state.body(&target.body).is_some_and(|model| {
doc.world_body_name(&state, &model.name, doc.model_body_component(model), &target.occurrence) == body
})
});
let target = matching.next().ok_or_else(|| bad("selected hole body is not in this setup; reselect its model occurrence"))?;
if matching.next().is_some() {
return Err(bad("selected hole body ambiguous; provide its occurrence path"));
}
target.clone()
};
let placed = PlacedBody::capture(s, &target)?;
let name = solidcraft_doc::naming::face_names(&placed.body).get(index).cloned().ok_or_else(|| bad("selected hole face missing"))?;
cam::HoleSource::Face { target, name }
}
Pick::Profile { sketch, index, occurrence } => {
let profile = state
.sketches
.iter()
.find(|s| s.feature == sketch)
.and_then(|s| s.profiles.get(index))
.ok_or_else(|| bad("selected circular profile missing"))?;
cam::HoleSource::Profile { profile: cam::ProfileRef { sketch, curves: profile.curve_ids(), occurrence } }
}
};
if seen.contains(&source) {
return Err(bad("duplicate hole selection"));
}
seen.push(source.clone());
let hole = Hole::capture(s, setup, &source, cam::TOLERANCE)?.resolve(setup.frame, cam::TOLERANCE)?;
if let Some((old, prior)) = sources
.iter()
.zip(&selected)
.find(|(_, p): &(_, &Selected)| (p.center[0] - hole.center[0]).hypot(p.center[1] - hole.center[1]) < cam::TOLERANCE)
{
// A viewport pick expands a cylinder across its topological seam. Its
// faces still describe one hole on one explicitly placed body.
if matches!((old, &source), (cam::HoleSource::Face { target: a, .. }, cam::HoleSource::Face { target: b, .. }) if a == b)
&& (prior.center[0] - hole.center[0]).hypot(prior.center[1] - hole.center[1]) < 1e-8
&& (prior.diameter - hole.diameter).abs() < 1e-8
{
continue;
}
return Err(bad("selected hole axes coincide; remove duplicate selections"));
}
sources.push(source);
selected.push(hole);
}
Ok((sources, selected))
}