Give solid.move the copy half of its Move/Copy label (#498)
solid.move takes copy: true to keep the source bodies and transform a new copy of each, with a Create Copy box in the Move dialog. Designs without it save and load exactly as before.
This commit is contained in:
1 parent
69e54f85d2
commit
7d00df0a3f
15 files changed
+395
-11
No files matched your search
@@ -107,7 +107,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("presspull")
|
||||
.key("Q")
|
||||
.params("face: [x,y,z] and distance: expr (positive adds material, negative cuts), or edges: [[x,y,z]…] and distance (a fillet)"),
|
||||
CommandSpec::new("solid.move", "Move/Copy", move_bodies).at("SOLID", "MODIFY").icon("move").key("M").params("bodies: [names]; translate?: [x,y,z] (exprs or numbers); axis?: [x,y,z]; angle?: expr"),
|
||||
CommandSpec::new("solid.move", "Move/Copy", move_bodies).at("SOLID", "MODIFY").icon("move").key("M").params("bodies: [names]; translate?: [x,y,z] (exprs or numbers); axis?: [x,y,z]; angle?: expr; copy?: bool (default false: keeps the original bodies and transforms a new copy instead of moving them in place)"),
|
||||
];
|
||||
|
||||
/// Where an extrude starts and which way is positive: a point inside the (first) profile or
|
||||
@@ -875,7 +875,11 @@ fn move_bodies(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(a) = &angle {
|
||||
check_expr(s, a, Kind::Angle, cmd, "angle")?;
|
||||
}
|
||||
add_feature(s, p, FeatureKind::Move { bodies, translate: t, rotate_axis: p.get("axis").and_then(vec3), angle })
|
||||
add_feature(
|
||||
s,
|
||||
p,
|
||||
FeatureKind::Move { bodies, translate: t, rotate_axis: p.get("axis").and_then(vec3), angle, copy: bool_(p, "copy").unwrap_or(false) },
|
||||
)
|
||||
}
|
||||
|
||||
fn source_features(s: &Session, p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||
@@ -1536,3 +1540,7 @@ mod offset_plane_face_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "stacked_face_tests.rs"]
|
||||
mod stacked_face_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "solid_move_copy_tests.rs"]
|
||||
mod solid_move_copy_tests;
|
||||
@@ -0,0 +1,86 @@
|
||||
//! `solid.move` with `copy: true`: the original body stays where it was, a new body is
|
||||
//! pushed at the transform, and an older design file (no `copy` key at all) still loads.
|
||||
use super::*;
|
||||
|
||||
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
|
||||
s.execute(cmd, &p).unwrap()
|
||||
}
|
||||
|
||||
fn cube20(s: &mut Session, name: &str) {
|
||||
run(s, "solid.box", json!({"length": 20, "width": 20, "height": 20, "body_name": name}));
|
||||
}
|
||||
|
||||
fn volume(body: &solidcraft_doc::ModelBody) -> f64 {
|
||||
solidcraft_kernel::measure(&body.body).unwrap().volume
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_keeps_the_original_and_adds_a_transformed_body() {
|
||||
let mut s = Session::default();
|
||||
cube20(&mut s, "Cube");
|
||||
run(&mut s, "solid.move", json!({"bodies": ["Cube"], "translate": [30, 0, 0], "copy": true}));
|
||||
let state = s.model.state();
|
||||
assert_eq!(state.bodies.len(), 2, "a copy move must add one body, not replace the original");
|
||||
|
||||
let original = state.body("Cube").unwrap();
|
||||
let original_bounds = original.mesh().bounds();
|
||||
assert!(original_bounds.min.dist(Vec3::ZERO) < 1e-6, "the original body must stay at its own position: {original_bounds:?}");
|
||||
assert!(original_bounds.max.dist(Vec3::new(20.0, 20.0, 20.0)) < 1e-6, "{original_bounds:?}");
|
||||
|
||||
let copy = state.bodies.iter().find(|b| b.name != "Cube").unwrap();
|
||||
let copy_bounds = copy.mesh().bounds();
|
||||
assert!(copy_bounds.min.dist(Vec3::new(30.0, 0.0, 0.0)) < 1e-6, "the copy must land at the translated position: {copy_bounds:?}");
|
||||
assert!(copy_bounds.max.dist(Vec3::new(50.0, 20.0, 20.0)) < 1e-6, "{copy_bounds:?}");
|
||||
|
||||
let original_volume = volume(original);
|
||||
let copy_volume = volume(copy);
|
||||
assert!((original_volume - 8000.0).abs() < 1e-3, "original volume: {original_volume}");
|
||||
assert!((copy_volume - 8000.0).abs() < 1e-3, "copy volume: {copy_volume}");
|
||||
assert!((original_volume - copy_volume).abs() < 1e-6, "the copy must have the same mass properties as the source");
|
||||
|
||||
let total: f64 = state.bodies.iter().map(volume).sum();
|
||||
assert!((total - 16000.0).abs() < 1e-3, "total volume across both bodies: {total}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_copy_the_move_still_replaces_the_body_in_place() {
|
||||
let mut s = Session::default();
|
||||
cube20(&mut s, "Cube");
|
||||
run(&mut s, "solid.move", json!({"bodies": ["Cube"], "translate": [30, 0, 0]}));
|
||||
let state = s.model.state();
|
||||
assert_eq!(state.bodies.len(), 1, "a plain move must not create a second body");
|
||||
let moved = state.body("Cube").unwrap();
|
||||
let bounds = moved.mesh().bounds();
|
||||
assert!(bounds.min.dist(Vec3::new(30.0, 0.0, 0.0)) < 1e-6, "{bounds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_older_design_with_no_copy_key_at_all_still_loads_and_moves_in_place() {
|
||||
let mut s = Session::default();
|
||||
cube20(&mut s, "Cube");
|
||||
run(&mut s, "solid.move", json!({"bodies": ["Cube"], "translate": [5, 0, 0]}));
|
||||
let saved = s.doc.to_json();
|
||||
assert!(!saved.contains("\"copy\""), "a move without the flag must serialize with no `copy` key, as an older file would read: {saved}");
|
||||
|
||||
// Simulate a design saved before this feature existed: strip the key even if present, then
|
||||
// reopen through the same loader a real design file goes through.
|
||||
let mut old: Value = serde_json::from_str(&saved).unwrap();
|
||||
let features = old.get_mut("features").and_then(Value::as_array_mut).expect("features array");
|
||||
let mut touched = false;
|
||||
for f in features.iter_mut() {
|
||||
if f.get("type").and_then(Value::as_str) == Some("move")
|
||||
&& let Some(obj) = f.as_object_mut()
|
||||
{
|
||||
obj.remove("copy");
|
||||
touched = true;
|
||||
}
|
||||
}
|
||||
assert!(touched, "the move feature must be present in the saved design");
|
||||
|
||||
let migrated = solidcraft_doc::Document::from_json(&old.to_string()).unwrap();
|
||||
let reopened = Session::new(migrated);
|
||||
let state = reopened.model.state();
|
||||
assert_eq!(state.bodies.len(), 1, "a pre-copy design must still move the one body in place, never duplicate it");
|
||||
let moved = state.body("Cube").unwrap();
|
||||
assert!(moved.mesh().bounds().min.dist(Vec3::new(5.0, 0.0, 0.0)) < 1e-6);
|
||||
}
|
||||
Reference in new issue
Block a user