241 lines
14 KiB
Rust
241 lines
14 KiB
Rust
use crate::scenario::Harness;
|
|
use serde_json::{Value, json};
|
|
use solvecraft_engine::doc::{AxisRef, FeatureKind};
|
|
|
|
fn make(h: Harness, axis: Value) -> Harness {
|
|
make_params(h, axis, json!({}))
|
|
}
|
|
fn make_params(mut h: Harness, axis: Value, extra: Value) -> Harness {
|
|
h.app.run("sketch.create", json!({"plane":"XY","name":"Profile"})).unwrap();
|
|
h.app.run("sketch.rectangle.two_point", json!({"p0":[10,0],"p1":[15,5]})).unwrap();
|
|
h.app.run("sketch.finish", json!({})).unwrap();
|
|
let mut params = json!({"sketch":"Profile","axis":axis,"angle":"90 deg","operation":"new","name":"Turn","body_name":"Main"});
|
|
params.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
|
|
h.app.run("solid.revolve", params).unwrap();
|
|
h.frames(4);
|
|
h
|
|
}
|
|
|
|
#[test]
|
|
fn unchanged_all_profiles_retains_future_geometry_but_reselection_is_explicit() {
|
|
for reselect in [false, true] {
|
|
let mut h = make(Harness::new(), json!("Y"));
|
|
open(&mut h);
|
|
if reselect {
|
|
let selection = h.app.dialog.as_ref().unwrap().inputs[0].items[0].clone();
|
|
let mut dialog = h.app.dialog.take().unwrap();
|
|
dialog.active = 0;
|
|
dialog.pick(&h.app.session, selection.clone());
|
|
dialog.pick(&h.app.session, selection);
|
|
h.app.dialog = Some(dialog);
|
|
h.frames(4);
|
|
}
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
let FeatureKind::Revolve { profiles, .. } = &h.app.session.doc.find_feature("Turn").unwrap().kind else { panic!("revolve") };
|
|
assert_eq!(matches!(profiles, solvecraft_engine::doc::ProfileSel::All), !reselect);
|
|
h.app.run("sketch.edit", json!({"sketch":"Profile"})).unwrap();
|
|
h.app.run("sketch.rectangle.two_point", json!({"p0":[20,0],"p1":[25,5]})).unwrap();
|
|
h.app.run("sketch.finish", json!({})).unwrap();
|
|
let state = h.app.session.world_state();
|
|
let volume: f64 = state.bodies.iter().map(|b| solvecraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum();
|
|
let expected = if reselect { 490.873852 } else { 1374.446786 };
|
|
assert!((volume - expected).abs() < 0.3, "{volume} != {expected}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn to_object_noop_preserves_cut_targets_and_untargeted_body() {
|
|
let mut h = Harness::new();
|
|
for name in ["A", "B"] {
|
|
h.app.run("solid.box", json!({"length":20,"width":5,"height":20,"corner":[0,0,0],"body_name":name})).unwrap();
|
|
}
|
|
let mut h = make_params(h, json!("Y"), json!({"to":[0,2,12],"angle":"45 deg","operation":"cut","targets":["A"]}));
|
|
let state = h.app.session.world_state();
|
|
// A quarter annulus separates the stock into inner and outer solids. Both are
|
|
// results of cutting A; checking only the first named solid loses material.
|
|
let cut_volume = |state: &solvecraft_engine::doc::ModelState| -> f64 {
|
|
state.bodies.iter().filter(|b| b.name != "B").map(|b| solvecraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum()
|
|
};
|
|
let before_a = cut_volume(&state);
|
|
let removed = 0.5 * (15.0f64.powi(2) - 10.0f64.powi(2)) * 5.0 * std::f64::consts::FRAC_PI_2;
|
|
assert!((before_a - (2000.0 - removed)).abs() < 0.2, "cut results {:?}: {before_a}", state.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
|
|
let in_cut_stock = |p| state.bodies.iter().filter(|b| b.name != "B").any(|b| b.body.contains(p));
|
|
assert!(in_cut_stock(solvecraft_engine::geom::Vec3::new(2.0, 2.0, 2.0)));
|
|
assert!(in_cut_stock(solvecraft_engine::geom::Vec3::new(18.0, 2.0, 18.0)));
|
|
assert!(!in_cut_stock(solvecraft_engine::geom::Vec3::new(9.0, 2.0, 9.0)));
|
|
assert!(state.body("B").unwrap().body.contains(solvecraft_engine::geom::Vec3::new(9.0, 2.0, 9.0)));
|
|
let before_bounds: Vec<_> = state.bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect();
|
|
let undo = h.app.session.undo.len();
|
|
open(&mut h);
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
assert!(h.app.dialog.is_none());
|
|
let state = h.app.session.world_state();
|
|
assert!((solvecraft_engine::doc::kernel::measure(&state.body("B").unwrap().body).unwrap().volume - 2000.0).abs() < 1e-7);
|
|
assert!((cut_volume(&state) - before_a).abs() < 1e-7);
|
|
assert_eq!(state.bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect::<Vec<_>>(), before_bounds);
|
|
assert_eq!(h.app.session.undo.len(), undo + 1);
|
|
let FeatureKind::Revolve { targets, .. } = &h.app.session.doc.find_feature("Turn").unwrap().kind else { panic!("revolve") };
|
|
assert_eq!(targets, &["A"]);
|
|
}
|
|
|
|
#[test]
|
|
fn to_object_noop_retains_dormant_secondary_angle_and_downstream_dimension() {
|
|
let mut h = make_params(Harness::new(), json!("Y"), json!({"to":[0,2,12],"angle":"45 deg","angle2":"30 deg","direction":"symmetric"}));
|
|
h.app.run("solid.box", json!({"length":2,"width":2,"height":"(d2 / (1 rad)) * 1 mm","corner":[30,0,0],"body_name":"Dependent"})).unwrap();
|
|
let original = serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap();
|
|
let names = h.app.session.doc.find_feature("Turn").unwrap().param_names.clone();
|
|
open(&mut h);
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
assert_eq!(serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap(), original);
|
|
assert_eq!(h.app.session.doc.find_feature("Turn").unwrap().param_names, names);
|
|
assert!((h.app.session.world_state().body("Dependent").unwrap().mesh().bounds().max.z - std::f64::consts::PI / 6.0).abs() < 1e-8);
|
|
}
|
|
|
|
#[test]
|
|
fn rotated_owner_to_point_remains_exact_on_noop_edit() {
|
|
let mut h = Harness::new();
|
|
let owner = h.app.run("component.create", json!({"name":"Owner"})).unwrap();
|
|
let mut h = make_params(h, json!({"origin":[0,0,0],"dir":[0,2,0]}), json!({"to":[0,2,12],"angle":"45 deg"}));
|
|
h.app.run("occurrence.move", json!({"occurrence":owner["occurrence"],"translate":[100,0,20],"axis":[0,0,1],"angle":"17 deg"})).unwrap();
|
|
h.app.run("component.create", json!({"name":"Other","parent":0})).unwrap();
|
|
let original = serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap();
|
|
let bounds = h.app.session.world_state().body("Main").unwrap().mesh().bounds();
|
|
open(&mut h);
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
assert_eq!(serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap(), original);
|
|
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds(), bounds);
|
|
}
|
|
|
|
#[test]
|
|
fn second_active_occurrence_noop_preserves_definition_axis_target_and_instances() {
|
|
let mut h = Harness::new();
|
|
let owner = h.app.run("component.create", json!({"name":"Owner"})).unwrap();
|
|
let mut h = make_params(h, json!({"origin":[0,0,0],"dir":[0,2,0]}), json!({"to":[0,2,12],"angle":"45 deg"}));
|
|
let second = h.app.run("occurrence.copy", json!({"component":owner["component"],"translate":[100,0,20]})).unwrap()["occurrence"].clone();
|
|
h.app.run("occurrence.move", json!({"occurrence":second,"axis":[0,0,1],"angle":"17 deg"})).unwrap();
|
|
h.app.run("component.activate", json!({"component":owner["component"],"occurrence":second,"path":[second]})).unwrap();
|
|
let original = serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap();
|
|
let frame = h.app.session.active_component_frame();
|
|
let expected_origin = solvecraft_engine::doc::apply_point(&frame, solvecraft_engine::geom::Vec3::ZERO);
|
|
let before: Vec<_> = h.app.session.world_state().bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect();
|
|
open(&mut h);
|
|
let (origin, _) = crate::canvas::revolve_axis(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
|
|
assert!((origin - expected_origin).len() < 1e-8);
|
|
assert!(origin.len() > 50.0, "fixture must distinguish first and second placements");
|
|
let commands = crate::dialogs::apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
|
|
let preview = h.app.session.preview_world(&commands).unwrap();
|
|
let placed = preview.after.bodies.iter().find(|body| body.mesh().bounds().center().x > 50.0).unwrap();
|
|
assert!(placed.mesh().triangles.len() > 10);
|
|
assert_eq!(placed.mesh().bounds(), before.iter().find(|(_, bounds)| bounds.center().x > 50.0).unwrap().1);
|
|
assert!(h.widget("↖ Custom Axis").is_some());
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
assert_eq!(serde_json::to_value(&h.app.session.doc.find_feature("Turn").unwrap().kind).unwrap(), original);
|
|
let after: Vec<_> = h.app.session.world_state().bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect();
|
|
assert_eq!(after, before);
|
|
assert_eq!(h.app.session.active_occurrence_path.as_ref(), Some(&vec![second.as_u64().unwrap()]));
|
|
}
|
|
fn axis(h: &Harness) -> Value {
|
|
let FeatureKind::Revolve { axis, .. } = &h.app.session.doc.find_feature("Turn").unwrap().kind else { panic!("revolve") };
|
|
serde_json::to_value(axis).unwrap()
|
|
}
|
|
fn open(h: &mut Harness) {
|
|
h.step(&json!({"call":"ui.editFeature","params":{"feature":"Turn"}})).unwrap();
|
|
h.frames(8);
|
|
}
|
|
|
|
#[test]
|
|
fn actual_noop_editor_retains_all_revolve_axis_kinds_and_analytic_volume() {
|
|
for axis_value in [json!("Y"), json!("y"), json!("l4"), json!({"origin":[0,0,0],"dir":[0,2,0]})] {
|
|
let mut h = make(Harness::new(), axis_value.clone());
|
|
let original = axis(&h);
|
|
let names = h.app.session.doc.find_feature("Turn").unwrap().param_names.clone();
|
|
let state = h.app.session.world_state();
|
|
let body = state.body("Main").unwrap();
|
|
let bounds = body.mesh().bounds();
|
|
let radii = if axis_value == json!("l4") { 25.0 } else { 225.0 - 100.0 };
|
|
let expected = 0.5 * radii * 5.0 * std::f64::consts::FRAC_PI_2;
|
|
let volume = solvecraft_engine::doc::kernel::measure(&body.body).unwrap().volume;
|
|
assert!((volume - expected).abs() < 0.03, "{axis_value}: {volume}");
|
|
open(&mut h);
|
|
let commands = crate::dialogs::apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
|
|
let preview = h.app.session.preview(&commands).unwrap();
|
|
assert_eq!(preview.after.body("Main").unwrap().mesh().bounds(), bounds);
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
assert!(h.app.dialog.is_none(), "{axis_value}: {:?}", h.app.session.log);
|
|
assert_eq!(axis(&h), original);
|
|
assert_eq!(h.app.session.doc.find_feature("Turn").unwrap().param_names, names);
|
|
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds(), bounds);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn revolve_angle_edit_preserves_axis_downstream_binding_and_one_undo() {
|
|
let mut h = make(Harness::new(), json!({"origin":[0,0,0],"dir":[0,2,0]}));
|
|
h.app.run("solid.box", json!({"length":2,"width":2,"height":"(d1 / (1 rad)) * 1 mm","corner":[30,0,0],"body_name":"Dependent"})).unwrap();
|
|
let original = axis(&h);
|
|
let undo = h.app.session.undo.len();
|
|
open(&mut h);
|
|
h.step(&json!({"key":"A","cmd":true})).unwrap();
|
|
h.step(&json!({"text":"180 deg"})).unwrap();
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
assert_eq!(axis(&h), original);
|
|
assert_eq!(h.app.session.undo.len(), undo + 1);
|
|
assert!((h.app.session.world_state().body("Dependent").unwrap().mesh().bounds().max.z - std::f64::consts::PI).abs() < 1e-8);
|
|
h.app.run("edit.undo", json!({})).unwrap();
|
|
assert_eq!(axis(&h), original);
|
|
assert!((h.app.session.world_state().body("Dependent").unwrap().mesh().bounds().max.z - std::f64::consts::FRAC_PI_2).abs() < 1e-8);
|
|
}
|
|
|
|
#[test]
|
|
fn rotated_owner_explicit_axis_is_lossless_with_another_component_active() {
|
|
let mut h = Harness::new();
|
|
let owner = h.app.run("component.create", json!({"name":"Owner"})).unwrap();
|
|
let mut h = make(h, json!({"origin":[0,0,0],"dir":[0,2,0]}));
|
|
h.app.run("occurrence.move", json!({"occurrence":owner["occurrence"],"translate":[100,0,20],"axis":[0,0,1],"angle":"17 deg"})).unwrap();
|
|
h.app.run("component.create", json!({"name":"Other","parent":0})).unwrap();
|
|
let active = h.app.session.active_component;
|
|
let original = axis(&h);
|
|
let bounds = h.app.session.world_state().body("Main").unwrap().mesh().bounds();
|
|
open(&mut h);
|
|
let (origin, direction) = crate::canvas::revolve_axis(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
|
|
assert!((origin.x - 100.0).abs() < 1e-8 && (origin.z - 20.0).abs() < 1e-8);
|
|
let angle = 17.0f64.to_radians();
|
|
assert!((direction.x + angle.sin()).abs() < 1e-8 && (direction.y - angle.cos()).abs() < 1e-8);
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
assert_eq!(axis(&h), original);
|
|
assert_eq!(h.app.session.active_component, active);
|
|
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds(), bounds);
|
|
}
|
|
|
|
#[test]
|
|
fn world_and_sketch_axis_handles_follow_authoring_occurrence() {
|
|
for selected in [json!("Y"), json!("y")] {
|
|
let mut h = Harness::new();
|
|
let owner = h.app.run("component.create", json!({"name":"Owner"})).unwrap();
|
|
let mut h = make(h, selected);
|
|
h.app.run("occurrence.move", json!({"occurrence":owner["occurrence"],"translate":[100,0,20],"axis":[0,0,1],"angle":"17 deg"})).unwrap();
|
|
let original = axis(&h);
|
|
open(&mut h);
|
|
let (origin, direction) = crate::canvas::revolve_axis(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
|
|
let angle = 17.0f64.to_radians();
|
|
assert!((origin.x - 100.0).abs() < 1e-8 && (origin.z - 20.0).abs() < 1e-8);
|
|
assert!((direction.x + angle.sin()).abs() < 1e-8 && (direction.y - angle.cos()).abs() < 1e-8);
|
|
h.step(&json!({"widget":"OK"})).unwrap();
|
|
assert_eq!(axis(&h), original);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sketch_axis_cancel_restores_original_document_and_history() {
|
|
let mut h = make(Harness::new(), json!("y"));
|
|
let original = serde_json::to_value(&*h.app.session.doc).unwrap();
|
|
let undo = h.app.session.undo.len();
|
|
open(&mut h);
|
|
h.step(&json!({"key":"A","cmd":true})).unwrap();
|
|
h.step(&json!({"text":"180 deg"})).unwrap();
|
|
h.step(&json!({"widget":"Cancel"})).unwrap();
|
|
assert_eq!(serde_json::to_value(&*h.app.session.doc).unwrap(), original);
|
|
assert_eq!(h.app.session.undo.len(), undo);
|
|
assert!(matches!(&h.app.session.doc.find_feature("Turn").unwrap().kind,FeatureKind::Revolve{axis:AxisRef::SketchAxis{axis},..} if axis=="y"));
|
|
}
|