Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
90 lines
3.8 KiB
Rust
90 lines
3.8 KiB
Rust
//! Components: the design as a tree. New sketches and features go into the active component;
|
|
//! bodies belong to their feature's component unless moved. (Groundwork for assemblies:
|
|
//! occurrences, joints and per-component physical properties build on this.)
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::CommandSpec;
|
|
use crate::params::{bad, str_, string_list};
|
|
use crate::{Result, Session};
|
|
|
|
pub static COMMANDS: &[CommandSpec] = &[
|
|
CommandSpec::new("FusionCreateNewComponentCommand", "New Component", new_component)
|
|
.at("SOLID", "ASSEMBLE")
|
|
.icon("component")
|
|
.params("name?, parent?: component id or name (default: the active one); activate?: bool (default true)"),
|
|
CommandSpec::new("FusionCreateComponentsFromBodiesCommand", "Create Components from Bodies", from_bodies)
|
|
.icon("component")
|
|
.params("bodies: [names] → one new component per body, named after it"),
|
|
CommandSpec::new("component.activate", "Activate Component", activate).noundo().params("component: id or name (0 or \"root\" for the design)"),
|
|
CommandSpec::new("component.list", "List Components", list).noundo(),
|
|
];
|
|
|
|
fn component_key(v: Option<&Value>) -> Option<String> {
|
|
match v {
|
|
Some(Value::Number(n)) => Some(n.to_string()),
|
|
Some(Value::String(s)) => Some(s.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn new_component(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "FusionCreateNewComponentCommand";
|
|
let parent = match component_key(p.get("parent")) {
|
|
Some(k) => s.doc.find_component(&k).ok_or_else(|| bad(cmd, format!("no component `{k}`")))?,
|
|
None => s.active_component,
|
|
};
|
|
let id = s.doc_mut().add_component(str_(p, "name"), parent)?;
|
|
if p.get("activate").and_then(Value::as_bool).unwrap_or(true) {
|
|
s.active_component = id;
|
|
}
|
|
let name = s.doc.components.iter().find(|c| c.id == id).map(|c| c.name.clone()).unwrap_or_default();
|
|
Ok(json!({"component": id, "name": name, "parent": parent}))
|
|
}
|
|
|
|
fn from_bodies(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "FusionCreateComponentsFromBodiesCommand";
|
|
let bodies = string_list(p, "bodies");
|
|
if bodies.is_empty() || bodies.len() > 1000 {
|
|
return Err(bad(cmd, "`bodies` must list 1…1000 body names"));
|
|
}
|
|
let st = s.model.state();
|
|
for b in &bodies {
|
|
if st.body(b).is_none() {
|
|
return Err(bad(cmd, format!("no body `{b}`")));
|
|
}
|
|
}
|
|
let parent = s.active_component;
|
|
let mut made = Vec::new();
|
|
for b in bodies {
|
|
let id = s.doc_mut().add_component(Some(&b), parent)?;
|
|
s.doc_mut().body_components.insert(b.clone(), id);
|
|
made.push(json!({"component": id, "body": b}));
|
|
}
|
|
Ok(json!({ "components": made }))
|
|
}
|
|
|
|
fn activate(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "component.activate";
|
|
let k = component_key(p.get("component")).ok_or_else(|| bad(cmd, "`component` must be an id or name"))?;
|
|
let id = s.doc.find_component(&k).ok_or_else(|| bad(cmd, format!("no component `{k}`")))?;
|
|
s.active_component = id;
|
|
s.revision += 1;
|
|
Ok(json!({ "active": id }))
|
|
}
|
|
|
|
fn list(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
let st = s.model.state();
|
|
let doc = &s.doc;
|
|
let node = |id: u64, name: &str, parent: Option<u64>| {
|
|
let bodies: Vec<&str> = st.bodies.iter().filter(|b| doc.body_component(&b.name, b.feature) == id).map(|b| b.name.as_str()).collect();
|
|
let features: Vec<&str> = doc.features.iter().filter(|f| f.component == id).map(|f| f.name.as_str()).collect();
|
|
json!({"id": id, "name": name, "parent": parent, "bodies": bodies, "features": features})
|
|
};
|
|
let mut out = vec![node(0, &doc.name, None)];
|
|
for c in &doc.components {
|
|
out.push(node(c.id, &c.name, Some(c.parent)));
|
|
}
|
|
Ok(json!({"active": s.active_component, "components": out}))
|
|
}
|