Files
OneCAD/crates/engine/src/cmd/component_properties.rs
T
bherbruck d71bdb78ec feat: strengthen modeling, configuration, and assembly workflows
Preserve linked sketch identities through deterministic face projection and resizing. Fix origin-corner picking, persistent inline navigation, appearance editing, configuration styles, component properties, contact sweeps, sheet exports, analytic STEP offsets, and bounded rolling-ball fit accuracy.

Validated with full cargo xtask ci, native workflow screenshots, repeated enclosure replays, and analytic geometry regressions.
2026-10-11 00:37:26 -04:00

143 lines
6.8 KiB
Rust

//! Physical properties of an assembly placement, with original body material assignments.
use serde_json::{Value, json};
use solvecraft_doc::{Document, Mat};
use solvecraft_geom::Vec3;
use crate::params::bad;
use crate::{Result, Session};
fn descendants(p: &Value, cmd: &str) -> Result<bool> {
match p.get("descendants") {
None => Ok(true),
Some(Value::Bool(v)) => Ok(*v),
Some(_) => Err(bad(cmd, "`descendants` must be a boolean")),
}
}
fn placed_doc(s: &Session) -> Document {
let mut doc = (*s.doc).clone();
for occurrence in &mut doc.occurrences {
if let Some(transform) = s.pending_moves.get(&occurrence.id) {
occurrence.transform = *transform;
}
}
doc
}
/// A property total must cover the entire assembly. The display traversal intentionally
/// truncates hostile designs, so use an explicit error instead of accepting its prefix.
fn placements(doc: &Document, cmd: &str) -> Result<Vec<(u64, Vec<u64>, Mat)>> {
let mut out = vec![(0, Vec::new(), solvecraft_doc::IDENTITY)];
let mut stack = vec![(0, Vec::<u64>::new(), solvecraft_doc::IDENTITY)];
while let Some((component, path, transform)) = stack.pop() {
for occurrence in doc.occurrences.iter().filter(|o| o.parent == component) {
if path.len() >= 64 || out.len() >= 100_000 {
return Err(bad(cmd, "assembly exceeds physical-properties traversal limits (64 levels or 100000 placements)"));
}
let mut child_path = path.clone();
child_path.push(occurrence.id);
let world = solvecraft_doc::mat_mul(&transform, &occurrence.transform);
out.push((occurrence.component, child_path.clone(), world));
stack.push((occurrence.component, child_path, world));
}
}
Ok(out)
}
pub(super) fn component(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "component.properties";
let id = if p.get("component").is_some() { super::component(s, p, "component", cmd)? } else { s.active_component };
let doc = placed_doc(s);
let path = doc.first_path(id);
let name = if id == 0 {
doc.name.clone()
} else {
doc.components.iter().find(|c| c.id == id).map(|c| c.name.clone()).ok_or_else(|| bad(cmd, "no active component"))?
};
let mut out = properties(s, &doc, &path, descendants(p, cmd)?, cmd)?;
out["component"] = json!(id);
out["name"] = json!(name);
Ok(out)
}
pub(super) fn occurrence(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "occurrence.properties";
let id = super::occurrence(s, p, cmd)?;
let doc = placed_doc(s);
let paths: Vec<Vec<u64>> = placements(&doc, cmd)?.into_iter().filter(|(_, path, _)| path.last() == Some(&id)).map(|(_, path, _)| path).collect();
let path = if let Some(value) = p.get("path") {
let path = value
.as_array()
.filter(|a| !a.is_empty() && a.len() <= 64)
.and_then(|a| a.iter().map(Value::as_u64).collect::<Option<Vec<_>>>())
.ok_or_else(|| bad(cmd, "`path` must list 1…64 occurrence ids"))?;
if !paths.contains(&path) {
return Err(bad(cmd, "`path` does not identify a placement of this occurrence"));
}
path
} else {
match paths.as_slice() {
[path] => path.clone(),
[] => return Err(bad(cmd, "occurrence has no placement")),
_ => return Err(bad(cmd, "occurrence has multiple placements; supply its `path`")),
}
};
let mut out = properties(s, &doc, &path, descendants(p, cmd)?, cmd)?;
out["occurrence"] = json!(id);
out["name"] = json!(doc.occurrences.iter().find(|o| o.id == id).map(|o| o.name.as_str()));
Ok(out)
}
fn properties(s: &Session, doc: &Document, selected: &[u64], descendants: bool, cmd: &str) -> Result<Value> {
let state = s.model.state();
let mut bodies = Vec::new();
let (mut volume, mut area, mut mass) = (0.0, 0.0, 0.0);
let mut moment = Vec3::ZERO;
let (mut min, mut max) =
(Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY), Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY));
let (mut faces, mut edges, mut vertices) = (0, 0, 0);
let mut placement_count = 0;
for (component, path, transform) in
placements(doc, cmd)?.into_iter().filter(|(_, path, _)| if descendants { path.starts_with(selected) } else { path == selected })
{
placement_count += 1;
for body in state.bodies.iter().filter(|b| doc.body_component(&b.name, b.feature) == component) {
let transform: Mat = doc.body_offsets.get(&body.name).map(|offset| solvecraft_doc::mat_mul(&transform, offset)).unwrap_or(transform);
let placed = solvecraft_kernel::transform_matrix(&body.body, transform)?;
let measured = solvecraft_kernel::measure(&placed)?;
let density = doc.density(&body.name);
let body_mass = measured.volume * density / 1000.0;
volume += measured.volume;
area += measured.area;
mass += body_mass;
moment += measured.centroid * body_mass;
min = Vec3::new(min.x.min(measured.bbox.min.x), min.y.min(measured.bbox.min.y), min.z.min(measured.bbox.min.z));
max = Vec3::new(max.x.max(measured.bbox.max.x), max.y.max(measured.bbox.max.y), max.z.max(measured.bbox.max.z));
faces += measured.merged.faces;
edges += measured.merged.edges;
vertices += measured.merged.vertices;
bodies.push(json!({"name": body.name, "component": component, "path": path,
"material": doc.materials.get(&body.name).map(String::as_str).unwrap_or("Default"),
"density_g_cm3": density, "mass_g": body_mass, "volume_mm3": measured.volume,
"area_mm2": measured.area, "center_of_mass": measured.centroid,
"bbox": {"min": measured.bbox.min, "max": measured.bbox.max}}));
}
}
if !(volume.is_finite() && area.is_finite() && mass.is_finite() && moment.is_finite()) {
return Err(bad(cmd, "physical properties exceed supported numeric range"));
}
let centroid = if mass > 0.0 { Some(moment / mass) } else { None };
if centroid.is_some_and(|point| !point.is_finite()) {
return Err(bad(cmd, "center of mass exceeds supported numeric range"));
}
let bbox = if bodies.is_empty() { Value::Null } else { json!({"min": min, "max": max}) };
Ok(json!({"path": selected, "coordinate_frame": "world", "descendants": descendants,
"placement_count": placement_count, "body_count": bodies.len(), "bodies": bodies,
"total": {"volume_mm3": volume, "area_mm2": area, "mass_g": mass,
"center_of_mass": centroid, "bbox": bbox, "faces": faces, "edges": edges, "vertices": vertices}}))
}
#[cfg(test)]
#[path = "component_properties_tests.rs"]
mod tests;