* WIP deep rename (not for push) * WIP2 * WIP3 * WIP4 wxs fix * rustfmt * Rename the remaining identifiers in merged crash-report code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
6.4 KiB
Rust
130 lines
6.4 KiB
Rust
use crate::Session;
|
|
use serde_json::{Value, json};
|
|
|
|
fn run(s: &mut Session, cmd: &str, params: Value) -> Value {
|
|
s.execute(cmd, ¶ms).unwrap()
|
|
}
|
|
fn close(actual: &Value, expected: f64) {
|
|
assert!((actual.as_f64().unwrap() - expected).abs() < 1e-6, "{actual} != {expected}");
|
|
}
|
|
fn box_in(s: &mut Session, name: &str) -> (u64, u64, String) {
|
|
let c = run(s, "component.create", json!({"name": name}));
|
|
run(s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
|
|
let body = s.model.state().bodies.last().unwrap().name.clone();
|
|
(c["component"].as_u64().unwrap(), c["occurrence"].as_u64().unwrap(), body)
|
|
}
|
|
|
|
#[test]
|
|
fn assembly_counts_instances_and_resolves_original_materials() {
|
|
let mut s = Session::default();
|
|
let (component, occurrence, body) = box_in(&mut s, "Block");
|
|
run(&mut s, "material.assign", json!({"bodies": [body], "material": "Steel"}));
|
|
let copy = run(&mut s, "occurrence.copy", json!({"component": component, "translate": [20, 0, 0]}));
|
|
let rho = s.doc.density(&body);
|
|
let root = run(&mut s, "component.properties", json!({"component": "root"}));
|
|
assert_eq!(root["body_count"], 2);
|
|
close(&root["total"]["mass_g"], 2.0 * rho);
|
|
close(&root["total"]["volume_mm3"], 2000.0);
|
|
close(&root["total"]["center_of_mass"][0], 15.0);
|
|
let first = run(&mut s, "component.properties", json!({"component": component}));
|
|
assert_eq!(first["body_count"], 1);
|
|
close(&first["total"]["center_of_mass"][0], 5.0);
|
|
let second = run(&mut s, "occurrence.properties", json!({"occurrence": copy["occurrence"]}));
|
|
close(&second["total"]["center_of_mass"][0], 25.0);
|
|
assert_eq!(second["bodies"][0]["material"], "Steel");
|
|
assert_eq!(first["path"], json!([occurrence]));
|
|
}
|
|
|
|
#[test]
|
|
fn mass_centroid_is_density_weighted_and_pending_moves_are_read_only() {
|
|
let mut s = Session::default();
|
|
let (a, oa, steel) = box_in(&mut s, "Steel block");
|
|
run(&mut s, "material.assign", json!({"bodies": [steel], "material": "Steel"}));
|
|
run(&mut s, "component.activate", json!({"component": "root"}));
|
|
let (_, ob, plastic) = box_in(&mut s, "Plastic block");
|
|
run(&mut s, "material.assign", json!({"bodies": [plastic], "material": "ABS Plastic"}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence": ob, "translate": [30, 0, 0], "capture": false}));
|
|
let doc = s.doc.clone();
|
|
let moves = s.pending_moves.clone();
|
|
let r = run(&mut s, "component.properties", json!({"component": 0}));
|
|
let ra = s.doc.density(&steel);
|
|
let rb = s.doc.density(&plastic);
|
|
close(&r["total"]["center_of_mass"][0], (5.0 * ra + 35.0 * rb) / (ra + rb));
|
|
close(&r["total"]["bbox"]["max"][0], 40.0);
|
|
assert!(std::sync::Arc::ptr_eq(&s.doc, &doc));
|
|
assert_eq!(s.pending_moves, moves);
|
|
run(&mut s, "occurrence.move", json!({"occurrence": oa, "translate": [10, 0, 0], "axis": [0, 0, 1], "angle": "90 deg"}));
|
|
let r = run(&mut s, "component.properties", json!({"component": a}));
|
|
close(&r["total"]["center_of_mass"][0], 5.0);
|
|
close(&r["total"]["center_of_mass"][1], 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_instances_require_a_path_and_can_exclude_descendants() {
|
|
let mut s = Session::default();
|
|
let parent = run(&mut s, "component.create", json!({"name": "Assembly"}));
|
|
let (_, child_occurrence, _) = box_in(&mut s, "Child");
|
|
let copy = run(&mut s, "occurrence.copy", json!({"component": parent["component"], "translate": [100, 0, 0]}));
|
|
assert!(s.execute("occurrence.properties", &json!({"occurrence": child_occurrence})).is_err());
|
|
let r = run(&mut s, "occurrence.properties", json!({"occurrence": child_occurrence, "path": [copy["occurrence"], child_occurrence]}));
|
|
close(&r["total"]["center_of_mass"][0], 105.0);
|
|
let r = run(&mut s, "component.properties", json!({"component": parent["component"]}));
|
|
assert_eq!(r["body_count"], 1);
|
|
let r = run(&mut s, "component.properties", json!({"component": parent["component"], "descendants": false}));
|
|
assert_eq!(r["body_count"], 0);
|
|
assert!(r["total"]["center_of_mass"].is_null());
|
|
assert!(r["total"]["bbox"].is_null());
|
|
}
|
|
|
|
#[test]
|
|
fn body_reassignment_offsets_preserve_world_properties() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
|
|
let body = s.model.state().bodies.first().unwrap().name.clone();
|
|
let c = run(&mut s, "component.create", json!({"name": "Moved"}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence": c["occurrence"], "translate": [100, 0, 0]}));
|
|
run(&mut s, "component.move_bodies", json!({"bodies": [body], "component": c["component"]}));
|
|
let r = run(&mut s, "component.properties", json!({"component": c["component"]}));
|
|
close(&r["total"]["center_of_mass"][0], 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn properties_reject_hostile_identifiers_and_options() {
|
|
let mut s = Session::default();
|
|
let (_, occurrence, _) = box_in(&mut s, "Block");
|
|
for p in [json!({"component": []}), json!({"component": "missing"}), json!({"descendants": 1})] {
|
|
assert!(s.execute("component.properties", &p).is_err());
|
|
}
|
|
for path in [json!([]), json!(["bad"]), json!([0]), json!(vec![occurrence; 65])] {
|
|
assert!(s.execute("occurrence.properties", &json!({"occurrence": occurrence, "path": path})).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn compact_exponentially_instanced_assembly_refuses_partial_totals() {
|
|
let mut s = Session::default();
|
|
let mut last_occurrence = 0;
|
|
// Only 17 component definitions and 34 occurrences, but over 100000 placements.
|
|
for _ in 0..17 {
|
|
let parent = s.active_component;
|
|
let component = s.doc_mut().add_component(None, parent).unwrap();
|
|
last_occurrence = s.doc_mut().add_occurrence(component, parent, solidcraft_doc::IDENTITY).unwrap();
|
|
s.active_component = component;
|
|
}
|
|
for command in ["component.properties", "occurrence.properties"] {
|
|
let error = s.execute(command, &json!({"component": 0, "occurrence": last_occurrence})).unwrap_err();
|
|
assert!(error.to_string().contains("traversal limits"), "{error}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn excessively_nested_assembly_refuses_partial_totals() {
|
|
let mut s = Session::default();
|
|
for _ in 0..65 {
|
|
let parent = s.active_component;
|
|
s.active_component = s.doc_mut().add_component(None, parent).unwrap();
|
|
}
|
|
let error = s.execute("component.properties", &json!({"component": 0})).unwrap_err();
|
|
assert!(error.to_string().contains("traversal limits"), "{error}");
|
|
}
|