diff --git a/crates/engine/src/cmd/component.rs b/crates/engine/src/cmd/component.rs index 4f086549..a4869631 100644 --- a/crates/engine/src/cmd/component.rs +++ b/crates/engine/src/cmd/component.rs @@ -30,7 +30,10 @@ pub static COMMANDS: &[CommandSpec] = &[ .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); occurrence?, path?: exact placement used to author shared geometry"), - CommandSpec::new("component.list", "List Components", list).noundo(), + CommandSpec::new("component.list", "List Components", list).noundo().params( + "compact?: bool (counts of bodies, features, sketches and construction planes instead of their names; occurrences leave out \ + identity transforms; children as occurrence ids); bodies?: bool (with compact: body names, not their count)", + ), CommandSpec::new("component.properties", "Component Physical Properties", properties::component) .icon("measure").noundo() .params("component?: id or name (default active); descendants?: bool (default true). Measures the first placement, or all placements for root, including pending moves; material-aware mass and center of mass in world coordinates"), @@ -156,9 +159,23 @@ fn activate(s: &mut Session, p: &Value) -> Result { Ok(json!({ "active": id })) } -fn list(s: &mut Session, _p: &Value) -> Result { +fn list(s: &mut Session, p: &Value) -> Result { let st = s.model.state(); let doc = &s.doc; + // Compact: what a large assembly needs to be found around in, without every name and matrix. + let compact = bool_(p, "compact").unwrap_or(false); + let names = |v: Vec<&str>| if compact { json!(v.len()) } else { json!(v) }; + let body_names = !compact || bool_(p, "bodies").unwrap_or(false); + let placement = |o: &Occurrence| { + if !compact { + return json!(o); + } + let mut v = json!({"id": o.id, "name": o.name, "parent": o.parent, "grounded": o.grounded}); + if o.transform.iter().flatten().zip(solidcraft_doc::IDENTITY.iter().flatten()).any(|(a, b)| (a - b).abs() > 1e-12) { + v["transform"] = json!(o.transform); + } + v + }; let node = |id: u64, name: &str, parent: Option| { let bodies: Vec<&str> = st.bodies.iter().filter(|b| doc.model_body_component(b) == id).map(|b| b.name.as_str()).collect(); let features: Vec<&str> = doc @@ -175,10 +192,11 @@ fn list(s: &mut Session, _p: &Value) -> Result { .filter(|f| f.component == id && matches!(f.kind, FeatureKind::ConstructionPlane { .. })) .map(|f| f.name.as_str()) .collect(); - let occurrences: Vec<&Occurrence> = doc.occurrences.iter().filter(|o| o.component == id).collect(); - let children: Vec<&Occurrence> = doc.occurrences.iter().filter(|o| o.parent == id).collect(); - json!({"id": id, "name": name, "parent": parent, "bodies": bodies, "features": features, "sketches": sketches, - "construction": construction, "occurrences": occurrences, "children": children}) + let occurrences: Vec = doc.occurrences.iter().filter(|o| o.component == id).map(placement).collect(); + let children: Vec = doc.occurrences.iter().filter(|o| o.parent == id).map(|o| if compact { json!(o.id) } else { json!(o) }).collect(); + let bodies = if body_names { json!(bodies) } else { json!(bodies.len()) }; + json!({"id": id, "name": name, "parent": parent, "bodies": bodies, "features": names(features), "sketches": names(sketches), + "construction": names(construction), "occurrences": occurrences, "children": children}) }; let mut out = vec![node(0, &doc.name, None)]; for c in &doc.components { diff --git a/crates/engine/src/cmd/occurrence_tests.rs b/crates/engine/src/cmd/occurrence_tests.rs index b83be778..31a1bed8 100644 --- a/crates/engine/src/cmd/occurrence_tests.rs +++ b/crates/engine/src/cmd/occurrence_tests.rs @@ -539,3 +539,28 @@ fn a_linked_design_brings_its_bodies_in_and_follows_edits_to_the_part_file() { near(&measured["total"]["center_of_mass"][0], 55.0); let _ = std::fs::remove_dir_all(&dir); } + +/// The compact list counts what a component holds and leaves out identity transforms; the full +/// list stays as it was. +#[test] +fn compact_component_list_counts_and_skips_identity_transforms() { + let mut s = Session::default(); + let (component, first) = created(&mut s, "Block"); + run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1, "body_name": "Cube"})); + run(&mut s, "component.activate", json!({"component": "root"})); + let second = run(&mut s, "occurrence.copy", json!({"component": component, "translate": [20, 0, 0]}))["occurrence"].as_u64().unwrap(); + let full = run(&mut s, "component.list", json!({})); + let block = |l: &Value| l["components"].as_array().unwrap().iter().find(|c| c["id"] == component).unwrap().clone(); + assert_eq!(block(&full)["bodies"], json!(["Cube"])); + assert!(block(&full)["occurrences"].as_array().unwrap().iter().all(|o| o["transform"].is_array())); + let compact = run(&mut s, "component.list", json!({"compact": true})); + let b = block(&compact); + assert_eq!((&b["bodies"], &b["features"], &b["sketches"]), (&json!(1), &json!(1), &json!(0)), "{b}"); + let placed = |id: u64| b["occurrences"].as_array().unwrap().iter().find(|o| o["id"] == id).unwrap().clone(); + assert!(placed(first).get("transform").is_none(), "{b}"); + assert_eq!(placed(second)["transform"][3][0], 20.0, "{b}"); + let root = compact["components"].as_array().unwrap().iter().find(|c| c["id"] == 0).unwrap().clone(); + assert_eq!(root["children"], json!([first, second]), "{root}"); + // Body names on request. + assert_eq!(block(&run(&mut s, "component.list", json!({"compact": true, "bodies": true})))["bodies"], json!(["Cube"])); +}