Persist typed sketch dimensions and bounded ordered configuration rules in format 8. Use exact occurrence paths and source identity for editing, movement, appearance, and mass. Preserve sheet material regions through cutting, folding, and exact DXF export; retain nested cavities and material islands. Validation: cargo xtask ci passed, including 368 engine tests, 178 UI tests, 68 UI scenarios, 136 kernel tests, and the 10-crate wasm check. Actual preview workflows and legacy format-7 origin-corner reopening were inspected.
422 lines
23 KiB
Rust
422 lines
23 KiB
Rust
// Exact occurrence workflows through the same commands used by the Browser.
|
|
use crate::Session;
|
|
use serde_json::{Value, json};
|
|
|
|
fn run(s: &mut Session, command: &str, p: Value) -> Value {
|
|
s.execute(command, &p).unwrap()
|
|
}
|
|
fn created(s: &mut Session, name: &str) -> (u64, u64) {
|
|
let c = run(s, "component.create", json!({"name": name}));
|
|
(c["component"].as_u64().unwrap(), c["occurrence"].as_u64().unwrap())
|
|
}
|
|
fn near(value: &Value, expected: f64) {
|
|
assert!((value.as_f64().unwrap() - expected).abs() < 1e-6, "{value} != {expected}");
|
|
}
|
|
|
|
#[test]
|
|
fn instance_copy_uses_selected_source_transform() {
|
|
let mut s = Session::default();
|
|
let (component, first) = created(&mut s, "Block");
|
|
let second = run(&mut s, "occurrence.copy", json!({"component": component, "translate": [20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
run(&mut s, "occurrence.move", json!({"occurrence": second, "translate": [30,0,0]}));
|
|
let third = run(&mut s, "occurrence.copy", json!({"occurrence": second, "translate": [5,0,0]}))["occurrence"].as_u64().unwrap();
|
|
let placement = s.doc.occurrences.iter().find(|o| o.id == third).unwrap();
|
|
assert!((placement.transform[3][0] - 55.0).abs() < 1e-6);
|
|
assert_eq!(s.doc.occurrences.iter().find(|o| o.id == first).unwrap().transform[3][0], 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn instance_world_move_under_rotated_parent_uses_selected_path() {
|
|
let mut s = Session::default();
|
|
let (_, parent) = created(&mut s, "Parent");
|
|
run(&mut s, "occurrence.move", json!({"occurrence": parent, "axis": [0,0,1], "angle": "90 deg"}));
|
|
let (_, child) = created(&mut s, "Child");
|
|
run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence": child, "path": [parent, child], "space": "world", "translate": [10,0,0]}));
|
|
let result = run(&mut s, "occurrence.properties", json!({"occurrence": child, "path": [parent,child]}));
|
|
near(&result["total"]["center_of_mass"][0], 9.5);
|
|
near(&result["total"]["center_of_mass"][1], 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn instance_authoring_at_second_placement_keeps_definition_shared() {
|
|
let mut s = Session::default();
|
|
let (component, first) = created(&mut s, "Block");
|
|
let second = run(&mut s, "occurrence.copy", json!({"component": component, "translate": [20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
run(&mut s, "component.activate", json!({"component": component, "occurrence": second, "path": [second]}));
|
|
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2, "corner": [20,0,0], "operation": "new"}));
|
|
for (occurrence, x) in [(first, 1.0), (second, 21.0)] {
|
|
let result = run(&mut s, "occurrence.properties", json!({"occurrence": occurrence, "path": [occurrence]}));
|
|
near(&result["total"]["center_of_mass"][0], x);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn instance_selection_survives_rename_move_capture_and_serde() {
|
|
let mut s = Session::default();
|
|
let (component, _) = created(&mut s, "Block");
|
|
let second = run(&mut s, "occurrence.copy", json!({"component": component, "translate": [20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
let selected = json!({"type": "occurrence", "path": [second]});
|
|
run(&mut s, "select.set", json!({"items": [selected]}));
|
|
run(&mut s, "component.rename", json!({"component": component, "name": "Renamed"}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence": second, "translate": [5,0,0], "capture": false}));
|
|
run(&mut s, "component.capture_position", json!({}));
|
|
assert_eq!(serde_json::to_value(&s.selection).unwrap(), json!([selected]));
|
|
let roundtrip: Vec<crate::Sel> = serde_json::from_value(json!([selected])).unwrap();
|
|
assert_eq!(roundtrip, s.selection);
|
|
}
|
|
|
|
#[test]
|
|
fn instance_selection_delete_removes_only_selected_instance() {
|
|
let mut s = Session::default();
|
|
let (component, first) = created(&mut s, "Block");
|
|
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2}));
|
|
let second = run(&mut s, "occurrence.copy", json!({"component": component, "translate": [20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
run(&mut s, "select.set", json!({"items": [{"type": "occurrence", "path": [second]}]}));
|
|
let items = s.selection.clone();
|
|
run(&mut s, "selection.delete", json!({"items": items}));
|
|
assert!(s.doc.occurrences.iter().any(|o| o.id == first));
|
|
assert!(!s.doc.occurrences.iter().any(|o| o.id == second));
|
|
assert_eq!(s.world_state().bodies.len(), 1);
|
|
assert!(s.doc.components.iter().any(|c| c.id == component));
|
|
}
|
|
|
|
#[test]
|
|
fn world_move_includes_pending_parent_and_leaf_and_shared_scope() {
|
|
let mut s = Session::default();
|
|
let (parent_component, parent) = created(&mut s, "Parent");
|
|
let (_, child) = created(&mut s, "Child");
|
|
run(&mut s, "occurrence.move", json!({"occurrence":parent,"axis":[0,0,1],"angle":"90 deg","translate":[3,0,0],"capture":false}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence":child,"translate":[2,0,0],"capture":false}));
|
|
let before = s.occurrence_placement(&[parent, child]).unwrap().world;
|
|
run(&mut s, "occurrence.move", json!({"occurrence":child,"path":[parent,child],"space":"world","translate":[10,0,0],"capture":false}));
|
|
let after = s.occurrence_placement(&[parent, child]).unwrap().world;
|
|
assert!((after[3][0] - before[3][0] - 10.0).abs() < 1e-8);
|
|
assert!((after[3][1] - before[3][1]).abs() < 1e-8);
|
|
run(
|
|
&mut s,
|
|
"occurrence.move",
|
|
json!({"occurrence":child,"path":[parent,child],"space":"world","axis":[0,0,1],"angle":"90 deg","origin":[0,0,0],"capture":false}),
|
|
);
|
|
let rotated = s.occurrence_placement(&[parent, child]).unwrap().world;
|
|
assert!((rotated[3][0] + after[3][1]).abs() < 1e-8);
|
|
assert!((rotated[3][1] - after[3][0]).abs() < 1e-8);
|
|
run(&mut s, "component.activate", json!({"component":parent_component}));
|
|
let second_parent = run(&mut s, "occurrence.copy", json!({"occurrence":parent,"translate":[30,0,0]}))["occurrence"].as_u64().unwrap();
|
|
assert_eq!(s.occurrence_scope_count(&[second_parent, child]).unwrap(), 2);
|
|
assert!(s.execute("occurrence.move", &json!({"occurrence":child,"space":"world","translate":[1,0,0]})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_paths_are_atomic_for_session_state() {
|
|
let mut s = Session::default();
|
|
let (component, first) = created(&mut s, "Part");
|
|
let second = run(&mut s, "occurrence.copy", json!({"occurrence":first}))["occurrence"].as_u64().unwrap();
|
|
run(&mut s, "component.activate", json!({"component":component,"occurrence":second,"path":[second]}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[5,0,0],"capture":false}));
|
|
run(&mut s, "browser.visibility", json!({"visible":false,"paths":[[second]]}));
|
|
let pending = s.pending_moves.clone();
|
|
let active = s.active_occurrence_path.clone();
|
|
let vis = s.visibility.clone();
|
|
let doc = s.doc.clone();
|
|
for (cmd, p) in [
|
|
("occurrence.move", json!({"occurrence":second,"path":[first],"space":"world","translate":[10,0,0]})),
|
|
("occurrence.move", json!({"occurrence":second,"space":17,"translate":[10,0,0]})),
|
|
("component.activate", json!({"component":component,"occurrence":second,"path":[first,second]})),
|
|
("browser.visibility", json!({"visible":true,"paths":[[second],[9999]]})),
|
|
("select.set", json!({"items":[{"type":"occurrence","path":[first,second]}]})),
|
|
("occurrence.copy", json!({"occurrence":second,"path":[first]})),
|
|
] {
|
|
assert!(s.execute(cmd, &p).is_err(), "{cmd}");
|
|
assert_eq!(s.pending_moves, pending);
|
|
assert_eq!(s.active_occurrence_path, active);
|
|
assert_eq!(s.visibility, vis);
|
|
assert_eq!(s.doc, doc);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_visibility_isolate_rename_and_async_accept() {
|
|
let mut s = Session::default();
|
|
let (component, first) = created(&mut s, "Part");
|
|
run(&mut s, "solid.box", json!({"length":2,"width":2,"height":2}));
|
|
let second = run(&mut s, "occurrence.copy", json!({"occurrence":first,"translate":[20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
let first_body = s.occurrence_bodies(&[first], true).unwrap()[0].clone();
|
|
let second_body = s.occurrence_bodies(&[second], true).unwrap()[0].clone();
|
|
run(&mut s, "browser.visibility", json!({"visible":false,"paths":[[second]]}));
|
|
assert!(s.body_visible(&first_body));
|
|
assert!(!s.body_visible(&second_body));
|
|
let mut scratch = s.scratch();
|
|
assert_eq!(scratch.visibility, s.visibility);
|
|
run(&mut scratch, "component.rename", json!({"component":component,"name":"Changed"}));
|
|
s.accept_computed(scratch);
|
|
assert!(!s.occurrence_visible(&[second]));
|
|
run(&mut s, "browser.visibility", json!({"visible":true,"paths":[[second]],"isolate":true}));
|
|
assert!(!s.body_visible(&first_body));
|
|
assert!(s.body_visible(&second_body));
|
|
run(&mut s, "browser.visibility", json!({"visible":true,"folder":"bodies"}));
|
|
assert!(s.body_visible(&first_body));
|
|
assert!(s.body_visible(&second_body));
|
|
}
|
|
|
|
#[test]
|
|
fn synthesized_name_collision_keeps_source_and_instance_separate() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "solid.box", json!({"length":1,"width":1,"height":1}));
|
|
let root = s.model.state().bodies[0].name.clone();
|
|
let (_, first) = created(&mut s, "Part");
|
|
run(&mut s, "solid.box", json!({"length":2,"width":2,"height":2}));
|
|
let second = run(&mut s, "occurrence.copy", json!({"occurrence":first,"translate":[20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
let candidate = s.occurrence_bodies(&[second], false).unwrap()[0].clone();
|
|
run(&mut s, "body.rename", json!({"body":root,"name":candidate}));
|
|
let instance = s.occurrence_bodies(&[second], false).unwrap()[0].clone();
|
|
assert_ne!(instance, candidate);
|
|
let frames = s.doc.body_frames(&s.model.state());
|
|
let world = s.world_state();
|
|
assert!(frames.iter().all(|(name, _, _, _)| world.body(name).is_some()));
|
|
assert_eq!(world.bodies.len(), world.bodies.iter().map(|b| &b.name).collect::<std::collections::BTreeSet<_>>().len());
|
|
run(&mut s, "browser.visibility", json!({"visible":false,"paths":[[second]]}));
|
|
assert!(s.body_visible(&candidate));
|
|
assert!(!s.body_visible(&instance));
|
|
let source = s.occurrence_placement(&[second]).unwrap().bodies[0].source.clone();
|
|
run(&mut s, "material.assign", json!({"bodies":[instance],"material":"Brass"}));
|
|
assert_eq!(s.doc.materials.get(&source).map(String::as_str), Some("Brass"));
|
|
assert!(!s.doc.materials.contains_key(&candidate));
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_nested_labels_use_full_path_and_reopen_undo_preserve_placements() {
|
|
let mut s = Session::default();
|
|
let (parent_component, parent) = created(&mut s, "Parent");
|
|
let (_, child) = created(&mut s, "Child");
|
|
run(&mut s, "solid.box", json!({"length":1,"width":1,"height":1}));
|
|
let parent2 = s.doc_mut().add_occurrence(parent_component, 0, solvecraft_doc::IDENTITY).unwrap();
|
|
let parent3 = s.doc_mut().add_occurrence(parent_component, 0, solvecraft_doc::IDENTITY).unwrap();
|
|
for o in s.doc_mut().occurrences.iter_mut().filter(|o| o.id == parent2 || o.id == parent3) {
|
|
o.name = "Same".into();
|
|
}
|
|
s.refresh();
|
|
let a = s.occurrence_bodies(&[parent2, child], false).unwrap()[0].clone();
|
|
let b = s.occurrence_bodies(&[parent3, child], false).unwrap()[0].clone();
|
|
assert_ne!(a, b);
|
|
let mut reopened = Session::new(solvecraft_doc::format::read(&s.doc.to_json()).unwrap());
|
|
assert_eq!(reopened.occurrence_bodies(&[parent2, child], false).unwrap()[0], a);
|
|
assert_eq!(reopened.occurrence_bodies(&[parent3, child], false).unwrap()[0], b);
|
|
run(&mut reopened, "selection.delete", json!({"occurrences":[parent3]}));
|
|
assert!(reopened.occurrence_placement(&[parent3, child]).is_err());
|
|
run(&mut reopened, "edit.undo", json!({}));
|
|
assert!(reopened.occurrence_placement(&[parent3, child]).is_ok());
|
|
assert!(reopened.occurrence_placement(&[parent, child]).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn compact_duplicated_assembly_exact_resolution_and_expansion_limit() {
|
|
let mut s = Session::default();
|
|
let mut parent = 0;
|
|
let mut path = Vec::new();
|
|
for i in 0..18 {
|
|
let component = s.doc_mut().add_component(Some(&format!("Level{i}")), parent).unwrap();
|
|
let occurrence = s.doc.occurrence_of(component).unwrap().id;
|
|
path.push(occurrence);
|
|
s.doc_mut().add_occurrence(component, parent, solvecraft_doc::IDENTITY).unwrap();
|
|
parent = component;
|
|
}
|
|
assert!(s.occurrence_placement(&path).is_ok());
|
|
let top = [path[0]];
|
|
assert!(s.occurrence_bodies(&top, true).is_err());
|
|
assert!(s.execute("select.set", &json!({"items":[{"type":"occurrence","path":top}]})).is_err());
|
|
assert!(s.selection.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn identity_instances_map_world_keys_to_shared_sources() {
|
|
let mut s = Session::default();
|
|
let (_, first) = created(&mut s, "Part");
|
|
run(&mut s, "solid.box", json!({"length":2,"width":2,"height":2}));
|
|
let second = run(&mut s, "occurrence.copy", json!({"occurrence":first}))["occurrence"].as_u64().unwrap();
|
|
let placement = s.occurrence_placement(&[second]).unwrap();
|
|
let body = &placement.bodies[0];
|
|
assert_ne!(body.source, body.world);
|
|
run(&mut s, "material.assign", json!({"bodies":[body.world],"material":"Brass"}));
|
|
assert_eq!(s.doc.materials.get(&body.source).map(String::as_str), Some("Brass"));
|
|
}
|
|
|
|
#[test]
|
|
fn dialog_baseline_move_preserves_other_pending_positions_without_drift() {
|
|
let mut s = Session::default();
|
|
let (_, first) = created(&mut s, "Part");
|
|
let second = run(&mut s, "occurrence.copy", json!({"occurrence":first,"translate":[20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
run(&mut s, "occurrence.move", json!({"occurrence":first,"translate":[5,0,0],"capture":false}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[3,0,0],"capture":false}));
|
|
let baseline = s.occurrence_placement(&[second]).unwrap().local;
|
|
for offset in [10.0, 15.0, 0.0] {
|
|
run(
|
|
&mut s,
|
|
"occurrence.move",
|
|
json!({"occurrence":second,"path":[second],"space":"world","base_transform":baseline,"translate":[offset,0,0],"capture":false}),
|
|
);
|
|
assert_eq!(s.pending_moves.get(&first).unwrap()[3][0], 5.0);
|
|
assert_eq!(s.pending_moves.get(&second).unwrap()[3][0], 23.0 + offset);
|
|
}
|
|
let pending = s.pending_moves.clone();
|
|
assert!(s.execute("occurrence.move", &json!({"occurrence":second,"base_transform":[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,1]]})).is_err());
|
|
assert_eq!(s.pending_moves, pending);
|
|
for base in [
|
|
[[2.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]],
|
|
[[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]],
|
|
] {
|
|
assert!(s.execute("occurrence.move", &json!({"occurrence":second,"base_transform":base})).is_err());
|
|
assert_eq!(s.pending_moves, pending);
|
|
}
|
|
assert!(s.execute("component.revert_position", &json!({"occurrences":[second,9999]})).is_err());
|
|
assert_eq!(s.pending_moves, pending);
|
|
run(&mut s, "component.revert_position", json!({"occurrences":[second]}));
|
|
assert!(!s.pending_moves.contains_key(&second));
|
|
assert_eq!(s.pending_moves.get(&first).unwrap()[3][0], 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_moves_copies_and_reused_ids_do_not_leak_state() {
|
|
let mut s = Session::default();
|
|
let (_, first) = created(&mut s, "Part");
|
|
run(&mut s, "occurrence.move", json!({"occurrence":first,"translate":[10,0,0],"capture":false}));
|
|
let pending = s.pending_moves.clone();
|
|
let doc = s.doc.clone();
|
|
for (cmd, p) in [
|
|
("occurrence.move", json!({"occurrence":first,"origin":[1e308,0,0],"angle":"180 deg"})),
|
|
("occurrence.move", json!({"occurrence":first,"translate":[1e9,0,0],"capture":false})),
|
|
("occurrence.move", json!({"occurrence":first,"translate":[1,2]})),
|
|
("occurrence.move", json!({"occurrence":first,"angle":false})),
|
|
("occurrence.move", json!({"occurrence":first,"axis":[1e308,0,0],"angle":"90 deg"})),
|
|
("occurrence.move", json!({"occurrence":first,"axis":[0,0,0],"angle":"90 deg"})),
|
|
("occurrence.copy", json!({"occurrence":first,"translate":[1e9,0,0]})),
|
|
("occurrence.copy", json!({"occurrence":first,"translate":"oops"})),
|
|
] {
|
|
assert!(s.execute(cmd, &p).is_err(), "{cmd}");
|
|
assert_eq!(s.pending_moves, pending);
|
|
assert_eq!(s.doc, doc);
|
|
}
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(s.pending_moves.is_empty());
|
|
let (_, other) = created(&mut s, "Other");
|
|
assert_eq!(first, other);
|
|
assert_eq!(s.occurrence_placement(&[other]).unwrap().world[3][0], 0.0);
|
|
run(&mut s, "occurrence.move", json!({"occurrence":other,"translate":[20,0,0],"capture":false}));
|
|
run(&mut s, "component.capture_position", json!({}));
|
|
assert!(s.pending_moves.is_empty());
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(s.occurrence_placement(&[other]).unwrap().world[3][0], 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn isolate_nested_child_hides_ancestor_material_and_sibling() {
|
|
let mut s = Session::default();
|
|
let (_, parent) = created(&mut s, "Parent");
|
|
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10}));
|
|
let (child_component, child) = created(&mut s, "Child");
|
|
run(&mut s, "solid.box", json!({"length":1,"width":1,"height":1}));
|
|
let parent_component = s.doc.occurrences.iter().find(|o| o.id == child).unwrap().parent;
|
|
let sibling = s
|
|
.doc_mut()
|
|
.add_occurrence(
|
|
child_component,
|
|
parent_component,
|
|
solvecraft_doc::rigid(crate::geom::Vec3::new(20.0, 0.0, 0.0), crate::geom::Vec3::ZERO, crate::geom::Vec3::Z, 0.0),
|
|
)
|
|
.unwrap();
|
|
s.refresh();
|
|
run(&mut s, "browser.visibility", json!({"visible":true,"paths":[[parent,child]],"isolate":true}));
|
|
for (path, expected) in [(vec![parent], false), (vec![parent, child], true), (vec![parent, sibling], false)] {
|
|
let body = s.occurrence_bodies(&path, false).unwrap()[0].clone();
|
|
assert_eq!(s.body_visible(&body), expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn context_new_child_uses_inactive_parent_placement() {
|
|
let mut s = Session::default();
|
|
let (parent_component, parent) = created(&mut s, "Parent");
|
|
let second = run(&mut s, "occurrence.copy", json!({"occurrence":parent,"translate":[20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
run(&mut s, "component.activate", json!({"component":0}));
|
|
let child =
|
|
run(&mut s, "component.create", json!({"parent":parent_component,"parent_path":[second],"name":"Child"}))["occurrence"].as_u64().unwrap();
|
|
assert_eq!(s.active_occurrence_path, Some(vec![second, child]));
|
|
assert_eq!(s.active_component_frame()[3][0], 20.0);
|
|
let doc = s.doc.clone();
|
|
let active = s.active_occurrence_path.clone();
|
|
assert!(s.execute("component.create", &json!({"parent":parent_component,"parent_path":[999],"name":"Invalid"})).is_err());
|
|
assert_eq!(s.doc, doc);
|
|
assert_eq!(s.active_occurrence_path, active);
|
|
}
|
|
|
|
#[test]
|
|
fn paste_new_uses_selected_pending_placement() {
|
|
let mut s = Session::default();
|
|
let (component, first) = created(&mut s, "Part");
|
|
run(&mut s, "solid.box", json!({"length":1,"width":1,"height":1}));
|
|
let second = run(&mut s, "occurrence.copy", json!({"occurrence":first,"translate":[20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[30,0,0],"capture":false}));
|
|
let copied =
|
|
run(&mut s, "component.paste_new", json!({"component":component,"occurrence":second,"path":[second],"translate":[5,0,0]}))["component"]
|
|
.as_u64()
|
|
.unwrap();
|
|
assert_eq!(s.doc.occurrence_of(copied).unwrap().transform[3][0], 55.0);
|
|
let result = run(&mut s, "component.properties", json!({"component":copied}));
|
|
near(&result["total"]["center_of_mass"][0], 55.5);
|
|
let result = run(&mut s, "occurrence.properties", json!({"occurrence":first,"path":[first]}));
|
|
near(&result["total"]["center_of_mass"][0], 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn instance_styles_follow_source_identity_after_collision_rename_and_pending_move() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "solid.box", json!({"length":1,"width":1,"height":1}));
|
|
let root = s.model.state().bodies[0].name.clone();
|
|
let (component, first) = created(&mut s, "Part");
|
|
run(&mut s, "solid.box", json!({"length":2,"width":2,"height":2}));
|
|
let second = run(&mut s, "occurrence.copy", json!({"occurrence":first,"translate":[20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
let candidate = s.occurrence_bodies(&[second], false).unwrap()[0].clone();
|
|
run(&mut s, "body.rename", json!({"body":root,"name":candidate}));
|
|
let instance = s.occurrence_bodies(&[second], false).unwrap()[0].clone();
|
|
let source = s.occurrence_placement(&[second]).unwrap().bodies[0].source.clone();
|
|
run(&mut s, "material.assign", json!({"bodies":[instance],"material":"Brass"}));
|
|
let world = s.world_state();
|
|
assert_eq!(s.doc.body_color(world.body(&instance).unwrap()), s.doc.body_color(world.body(&source).unwrap()));
|
|
run(&mut s, "appearance.assign", json!({"components":[component.to_string()],"color":"#0000ff","opacity":0.7}));
|
|
assert_eq!(s.doc.body_color(s.world_state().body(&instance).unwrap()), Some([0.0, 0.0, 1.0]));
|
|
run(&mut s, "appearance.assign", json!({"bodies":[instance],"color":"#ff0000","opacity":0.5}));
|
|
run(&mut s, "appearance.assign", json!({"faces":[{"body":instance,"point":[21,1,2]}],"color":"#00ff00","opacity":0.4}));
|
|
// Exercise the point-based face fallback as well as persistent face-name matching.
|
|
for face in &mut s.doc_mut().appearances.faces {
|
|
face.name = None;
|
|
}
|
|
run(&mut s, "body.rename", json!({"body":source,"name":"Shared Renamed"}));
|
|
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[5,0,0],"capture":false}));
|
|
let world = s.world_state();
|
|
for id in [first, second] {
|
|
let bodyname = s.occurrence_bodies(&[id], false).unwrap()[0].clone();
|
|
let body = world.body(&bodyname).unwrap();
|
|
assert_eq!(body.definition_name(), "Shared Renamed");
|
|
assert_eq!(body.component, component);
|
|
let look = s.doc.model_body_look(body).unwrap();
|
|
assert_eq!(look.color, [255, 0, 0]);
|
|
assert_eq!(look.opacity, 0.5);
|
|
assert!(s.doc.face_colors(body).iter().any(|(_, l)| l.color == [0, 255, 0] && l.opacity == 0.4));
|
|
let measured = run(&mut s, "inspect.measure", json!({"bodies":[bodyname]}));
|
|
assert_eq!(measured["bodies"][0]["material"], "Brass");
|
|
assert!((measured["bodies"][0]["mass_g"].as_f64().unwrap() - 0.068).abs() < 1e-8);
|
|
}
|
|
let definition = s.model.state();
|
|
assert_eq!(solvecraft_kernel::measure(&definition.body("Shared Renamed").unwrap().body).unwrap().bbox.min, crate::geom::Vec3::ZERO);
|
|
let painted = s.doc.painted(&world);
|
|
let secondname = s.occurrence_bodies(&[second], false).unwrap()[0].clone();
|
|
let body = painted.body(&secondname).unwrap();
|
|
assert_eq!(body.body.color(), Some([1.0, 0.0, 0.0]));
|
|
assert!(body.body.paint().unwrap().faces.iter().any(|f| f.color == [0.0, 1.0, 0.0]));
|
|
let bytes = solvecraft_io::export(&painted, &[secondname], solvecraft_io::Format::Step, "InstanceStyles").unwrap();
|
|
let imported = solvecraft_kernel::step_import(&String::from_utf8(bytes).unwrap()).unwrap();
|
|
assert_eq!(imported.bodies.len(), 1);
|
|
assert_eq!(imported.bodies[0].body.color(), Some([1.0, 0.0, 0.0]));
|
|
assert!(imported.bodies[0].body.paint().unwrap().faces.iter().any(|f| f.color == [0.0, 1.0, 0.0]));
|
|
}
|