Files
solidcraft-upstream/crates/engine/src/cmd/sketch_face.rs
T

216 lines
9.8 KiB
Rust

//! Exact planar face capture for sketch attachment. World labels are lookup keys only.
use crate::params::{bad, vec3};
use crate::{Result, Session};
use serde_json::Value;
use solvecraft_doc::{EmbossFace, PlaneRef, apply_point, apply_vector, mat_inverse, mat_mul};
use solvecraft_geom::Plane;
pub(in crate::cmd) fn is_face(p: &Value) -> bool {
p.get("plane").and_then(|plane| plane.get("face")).is_some()
}
pub(in crate::cmd) fn capture(s: &Session, value: &Value, cmd: &str) -> Result<PlaneRef> {
capture_impl(s, value, cmd, false)
}
/// Internal face-profile compatibility only; never exposed as a command parameter.
pub(in crate::cmd) fn capture_internal(s: &Session, value: &Value, cmd: &str) -> Result<PlaneRef> {
capture_impl(s, value, cmd, true)
}
fn capture_impl(s: &Session, value: &Value, cmd: &str, internal: bool) -> Result<PlaneRef> {
let point = value
.get("face")
.and_then(vec3)
.filter(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite())
.ok_or_else(|| bad(cmd, "face point must be three finite world coordinates"))?;
let fixed = match value.get("fixed") {
None => false,
Some(Value::Bool(fixed)) => *fixed,
_ => return Err(bad(cmd, "fixed must be a boolean")),
};
let state = s.model.state();
let doc = s.placed_document();
let author_path = s.active_occurrence_path.clone().unwrap_or_else(|| doc.first_path(s.active_component));
let author_frame = EmbossFace::component_frame(&doc, s.active_component, &author_path)?;
let author_inverse = mat_inverse(&author_frame).ok_or_else(|| bad(cmd, "invalid sketch author frame"))?;
let requested_body = match value.get("body") {
None => None,
Some(Value::String(body)) if !body.is_empty() => Some(body.as_str()),
_ => return Err(bad(cmd, "body must be the picked world body name")),
};
let requested_index = match value.get("index") {
None => None,
Some(value) => {
Some(value.as_u64().and_then(|index| usize::try_from(index).ok()).ok_or_else(|| bad(cmd, "index must be a nonnegative face index"))?)
}
};
if requested_body.is_some() != requested_index.is_some() {
return Err(bad(cmd, "an exact face pick requires both body and index"));
}
let requested_path = match value.get("path") {
None => None,
Some(value) => Some(
value
.as_array()
.filter(|path| path.len() <= 64)
.ok_or_else(|| bad(cmd, "path must contain at most64 occurrence ids"))?
.iter()
.map(|id| id.as_u64().filter(|id| *id != 0).ok_or_else(|| bad(cmd, "path ids must be nonzero integers")))
.collect::<Result<Vec<_>>>()?,
),
};
let selected_path = requested_path
.clone()
.or_else(|| requested_body.and_then(|name| state.body(name)).map(|body| doc.first_path(doc.model_body_component(body))));
let placements = if let Some(path) = selected_path {
let component = path
.last()
.and_then(|id| doc.occurrences.iter().find(|occurrence| occurrence.id == *id))
.map(|occurrence| occurrence.component)
.unwrap_or(0);
let frame = EmbossFace::component_frame(&doc, component, &path)?;
vec![(component, path, frame)]
} else {
crate::cmd::component::properties::placements(&doc, cmd)?
};
let mut by_component: std::collections::BTreeMap<u64, Vec<_>> = std::collections::BTreeMap::new();
for body in &state.bodies {
by_component.entry(doc.model_body_component(body)).or_default().push(body);
}
let mut chosen = None;
let mut work = state.bodies.len().saturating_add(doc.occurrences.len());
for (component, path, placement) in placements {
let Some(bodies) = by_component.get(&component) else {
continue;
};
for body in bodies {
if let Some(source) = requested_body.and_then(|name| state.body(name))
&& source.name != body.name
{
continue;
}
work = work.saturating_add(1);
if let Some(requested) = requested_body {
work = work.saturating_add(doc.occurrences.len()).saturating_add(state.bodies.len()).saturating_add(path.len());
if work > 2_000_000 {
return Err(bad(cmd, "face capture work limit exceeded; supply exact path"));
}
if requested != doc.world_body_name(&state, &body.name, component, &path) {
continue;
}
}
let frame = doc.model_body_offset(body).map(|offset| mat_mul(&placement, offset)).unwrap_or(placement);
let inverse = mat_inverse(&frame).ok_or_else(|| bad(cmd, "invalid selected face frame"))?;
let local_point = apply_point(&inverse, point);
let tolerance = (body.body.size() * 1e-4).max(1e-5);
let faces = body.body.faces(tolerance)?;
let mesh = body.body.tessellate(tolerance)?;
work = work.saturating_add(mesh.triangles.len().saturating_mul(if requested_index.is_some() { 1 } else { faces.len() }));
if work > 2_000_000 {
return Err(bad(cmd, "face capture work limit exceeded; use an exact body and face index"));
}
for face in faces {
if requested_index.is_some_and(|index| index != face.index) {
continue;
}
let Some(exact_plane) = (if fixed {
body.body.planar_face(face.index).or_else(|| body.body.face_plane(face.index))
} else {
body.body.face_plane(face.index).or_else(|| body.body.planar_face(face.index))
}) else {
continue;
};
let normal = exact_plane.normal();
if (local_point - face.centroid).dot(normal).abs() > tolerance * 10.0
|| !super::face_contains(&mesh, face.index, local_point, tolerance * 10.0)
{
continue;
}
if chosen.is_some() {
return Err(bad(cmd, "face point is ambiguous; pick an exact body and face index"));
}
let relative = mat_mul(&author_inverse, &frame);
let normal = apply_vector(&relative, normal).normalized().ok_or_else(|| bad(cmd, "invalid planar face normal"))?;
let centroid = apply_point(&relative, face.centroid);
let picked = Plane::from_normal(normal * centroid.dot(normal), normal).ok_or_else(|| bad(cmd, "invalid selected planar face"))?;
chosen = Some((body, face.index, path.clone(), picked));
}
}
}
let (body, index, path, plane) =
chosen.ok_or_else(|| bad(cmd, "the selected planar face no longer exists; choose another face or an explicit fixed plane"))?;
if fixed {
return Ok(PlaneRef::Custom { plane });
}
let at = apply_point(&author_inverse, point);
let name = solvecraft_doc::naming::face_names(body).get(index).cloned();
let anchor = match doc.capture_face_anchor(body, index, &plane, &path, s.active_component, &author_path) {
Ok(anchor) => Some(Box::new(anchor)),
Err(solvecraft_doc::DocError::Invalid(message))
if internal
&& matches!(
message.as_str(),
"this face has no exact planar source; choose a fixed plane instead"
| "this face has no supported construction certificate; choose a fixed plane instead"
) =>
{
if path.iter().any(|id| s.pending_moves.contains_key(id) && !author_path.contains(id)) {
return Err(bad(cmd, "capture the source occurrence position before generating a legacy face profile across components"));
}
None
}
Err(error) => return Err(bad(cmd, format!("{error}; choose a supported exact face or explicitly use fixed:true"))),
};
Ok(PlaneRef::Face { plane, at, name, anchor })
}
/// The sole durable face attachment beneath an offset/angle wrapper.
pub(in crate::cmd) fn anchor(mut plane: &PlaneRef) -> Option<&solvecraft_doc::FaceAnchor> {
loop {
match plane {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => plane = base,
PlaneRef::Face { anchor, .. } => return anchor.as_deref(),
_ => return None,
}
}
}
pub(in crate::cmd) fn anchor_mut(mut plane: &mut PlaneRef) -> Option<&mut solvecraft_doc::FaceAnchor> {
loop {
match plane {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => plane = base,
PlaneRef::Face { anchor, .. } => return anchor.as_deref_mut(),
_ => return None,
}
}
}
pub(in crate::cmd) fn capture_positions(s: &mut Session, plane: &PlaneRef) -> Result<()> {
let mut involved = std::collections::BTreeSet::new();
if let Some(anchor) = anchor(plane) {
involved.extend(anchor.path.iter().copied());
involved.extend(anchor.author_path.iter().copied());
} else {
involved.extend(s.active_occurrence_path.clone().unwrap_or_else(|| s.doc.first_path(s.active_component)));
}
let changes: Vec<_> = involved.into_iter().filter_map(|id| s.pending_moves.get(&id).copied().map(|matrix| (id, matrix))).collect();
for (id, matrix) in &changes {
let occurrence = s
.doc_mut()
.occurrences
.iter_mut()
.find(|occurrence| occurrence.id == *id)
.ok_or_else(|| bad("sketch attachment", "pending author/source path disappeared"))?;
occurrence.transform = *matrix;
}
for (id, _) in changes {
s.pending_moves.remove(&id);
}
Ok(())
}
#[cfg(test)]
#[path = "sketch_face_tests.rs"]
mod tests;