//! #630: the Box, Cylinder, Sphere, Torus and Coil dialogs show the unit of their sizes, as the //! other dialogs do ("10 mm"), in the design's units. OK makes what the bare numbers made before, //! and a bare number typed into a field still means the design's units. use serde_json::{Value, json}; use solidcraft_engine::Session; use crate::dialogs::{Kind, apply_commands}; use crate::scenario::Harness; /// The primitives with their sizes as a bare number reads them: the command and fields. const PRIMITIVES: [(&str, &[(&str, &str)]); 5] = [ ("solid.box", &[("length", "20"), ("width", "20"), ("height", "20")]), ("solid.cylinder", &[("diameter", "20"), ("height", "20")]), ("solid.sphere", &[("diameter", "20")]), ("solid.torus", &[("major", "20"), ("minor", "5")]), ("solid.coil", &[("diameter", "40"), ("revolutions", "4"), ("pitch", "8"), ("section_size", "3")]), ]; /// A design in `units`, with nothing in it. fn design(units: &str) -> Harness { let mut h = Harness::new(); h.app.home.open = false; if units != "mm" { h.app.run("document.units", json!({ "units": units })).unwrap(); } h.frames(2); h } /// The open dialog's fields. fn fields(h: &Harness) -> Vec<(&'static str, String)> { match h.app.dialog.as_ref().map(|d| &d.kind) { Some(Kind::Primitive { fields, .. }) => fields.clone(), other => panic!("no primitive dialog: {other:?}"), } } /// Every field shows its text in its row, as a person sees it. fn assert_shown(h: &Harness, fields: &[(&str, String)]) { for (label, text) in fields { assert!(h.widget_near(text, label).is_some(), "the {label} row shows {text:?}"); } } /// Replace a field's text by typing, as a person does. fn type_into(h: &mut Harness, label: &str, text: &str) { h.step(&json!({"click": {"handle": format!("dialog:{label}")}})).unwrap(); h.step(&json!({"key": "A", "cmd": true})).unwrap(); h.step(&json!({"text": text})).unwrap(); } fn ok(h: &mut Harness) { h.step(&json!({"click": {"handle": "dialog:OK"}})).unwrap(); h.frames(2); assert!(h.app.dialog.is_none(), "OK closes the dialog: {:?}", h.app.dialog.as_ref().and_then(|d| d.error.clone())); } /// The volume of everything in the design (mm³). fn volume(s: &mut Session) -> f64 { s.execute("inspect.measure", &json!({})).unwrap()["total"]["volume_mm3"].as_f64().unwrap() } /// The size of the design's only body (mm). fn size(s: &mut Session) -> [f64; 3] { let r = s.execute("inspect.measure", &json!({})).unwrap(); let b = &r["bodies"][0]["bbox"]; std::array::from_fn(|i| b["max"][i].as_f64().unwrap() - b["min"][i].as_f64().unwrap()) } fn assert_size(s: &mut Session, want: [f64; 3]) { let got = size(s); assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-6), "{got:?}, want {want:?}"); } /// The volume the command makes from the bare numbers in a design in `units` (what the dialog /// made before its fields showed units). fn bare_volume(units: &str, cmd: &str, sizes: &[(&str, &str)]) -> f64 { let mut s = Session::default(); s.execute("document.units", &json!({ "units": units })).unwrap(); let params: serde_json::Map = sizes.iter().map(|(k, v)| ((*k).to_string(), json!(v))).collect(); s.execute(cmd, &Value::Object(params)).unwrap(); volume(&mut s) } #[test] fn primitive_dialogs_show_millimetres_and_make_the_same_sizes() { for (cmd, sizes) in PRIMITIVES { let mut h = design("mm"); h.app.start(cmd); h.frames(3); let shown = fields(&h); let want: Vec<(&str, String)> = sizes.iter().map(|(k, v)| (*k, if *k == "revolutions" { (*v).to_string() } else { format!("{v} mm") })).collect(); assert_eq!(shown, want, "{cmd}"); assert_shown(&h, &shown); // OK sends the fields' texts, units and all. let (id, params) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0); assert_eq!(id, cmd); for (k, v) in &want { assert_eq!(params[*k], json!(v), "{cmd} {k}"); } ok(&mut h); let made = volume(&mut h.app.session); let before = bare_volume("mm", cmd, sizes); assert!(made > 0.0 && (made - before).abs() < 1e-6 * before, "{cmd}: {made} mm³, the bare numbers made {before} mm³"); } let mut h = design("mm"); h.app.start("solid.box"); h.frames(3); ok(&mut h); assert_size(&mut h.app.session, [20.0, 20.0, 20.0]); } #[test] fn in_an_inch_design_primitive_dialogs_show_inches_and_read_bare_numbers_as_inches() { for (cmd, sizes) in PRIMITIVES { let mut h = design("in"); h.app.start(cmd); h.frames(3); let shown = fields(&h); let want: Vec<(&str, String)> = sizes.iter().map(|(k, v)| (*k, if *k == "revolutions" { (*v).to_string() } else { format!("{v} in") })).collect(); assert_eq!(shown, want, "{cmd}"); assert_shown(&h, &shown); ok(&mut h); // The same size the bare numbers made in an inch design. let made = volume(&mut h.app.session); let before = bare_volume("in", cmd, sizes); assert!(made > 0.0 && (made - before).abs() < 1e-6 * before, "{cmd}: {made} mm³, the bare numbers made {before} mm³"); } // A bare number typed into a field is in the design's units; a typed unit is honoured. let mut h = design("in"); h.app.start("solid.box"); h.frames(3); type_into(&mut h, "width", "2"); type_into(&mut h, "height", "10 mm"); h.frames(2); let (_, params) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0); assert_eq!((¶ms["length"], ¶ms["width"], ¶ms["height"]), (&json!("20 in"), &json!("2"), &json!("10 mm"))); ok(&mut h); assert_size(&mut h.app.session, [508.0, 50.8, 10.0]); } #[test] fn a_centimetre_design_shows_centimetres() { let mut h = design("cm"); h.app.start("solid.sphere"); h.frames(3); assert_eq!(fields(&h), vec![("diameter", "20 cm".to_string())]); ok(&mut h); assert_size(&mut h.app.session, [200.0, 200.0, 200.0]); } /// The sizes a primitive feature keeps, as stored. fn stored(h: &Harness, id: u64) -> Vec { use solidcraft_engine::doc::FeatureKind; match h.app.session.doc.feature(id).map(|f| &f.kind) { Some(FeatureKind::Box { length, width, height, .. }) => vec![length.clone(), width.clone(), height.clone()], Some(FeatureKind::Cylinder { radius, height, .. }) => vec![radius.clone(), height.clone()], Some(FeatureKind::Sphere { radius, .. }) => vec![radius.clone()], Some(FeatureKind::Torus { major, minor, .. }) => vec![major.clone(), minor.clone()], other => panic!("not a primitive: {other:?}"), } } #[test] fn editing_a_primitive_after_the_design_goes_to_inches_keeps_its_millimetres() { // Made by a script in a millimetre design, then the design goes to inches: bare sizes are // still millimetres, and Edit shows them so; OK without a change keeps the part. Sizes with // a unit, expressions and parameters show as they are kept. let mut h = design("mm"); h.app.run("parameters.add", json!({"name": "w", "expression": "6 mm"})).unwrap(); h.app.run("solid.box", json!({"length": 40, "width": "w", "height": 20})).unwrap(); h.app.run("solid.cylinder", json!({"diameter": "10 mm", "height": 12, "base": [60, 0, 0], "operation": "new"})).unwrap(); h.app.run("solid.sphere", json!({"radius": 4, "center": [0, 60, 0], "operation": "new"})).unwrap(); h.app.run("solid.torus", json!({"major": 10, "minor": "2 mm", "center": [60, 60, 0], "operation": "new"})).unwrap(); h.app.run("document.units", json!({"units": "in"})).unwrap(); h.frames(2); let before = volume(&mut h.app.session); let ids: Vec = h.app.session.doc.features.iter().map(|f| f.id).collect(); let want: [&[(&str, &str)]; 4] = [ &[("length", "40 mm"), ("width", "w"), ("height", "20 mm")], &[("radius", "(10 mm) / 2"), ("height", "12 mm")], &[("radius", "4 mm")], &[("major", "10 mm"), ("minor", "2 mm")], ]; assert_eq!(ids.len(), want.len()); for (id, want) in ids.into_iter().zip(want) { h.app.edit_feature(id); h.frames(3); let shown = fields(&h); assert_eq!(shown, want.iter().map(|(k, v)| (*k, (*v).to_string())).collect::>()); assert_shown(&h, &shown); ok(&mut h); assert_eq!(stored(&h, id), want.iter().map(|(_, v)| (*v).to_string()).collect::>()); let after = volume(&mut h.app.session); assert!((after - before).abs() < 1e-6 * before, "the part is unchanged: {after} mm³, was {before} mm³"); } // A size typed bare in the edit is in the design's units, as anywhere else. let first = h.app.session.doc.features.first().map(|f| f.id).unwrap(); h.app.edit_feature(first); h.frames(3); type_into(&mut h, "height", "1"); h.frames(2); ok(&mut h); let r = h.app.session.execute("inspect.measure", &json!({})).unwrap(); let height = r["bodies"].as_array().unwrap().iter().map(|b| b["bbox"]["max"][2].as_f64().unwrap()).fold(f64::MIN, f64::max); assert!((height - 25.4).abs() < 1e-6, "1 in tall: {height}"); } #[test] fn editing_in_a_millimetre_design_keeps_the_sizes_as_they_are_kept() { // A bare number already means millimetres here: Edit shows and sends it as kept, so OK // without a change leaves the feature (and the active configuration's match) alone. let mut h = design("mm"); h.app.run("solid.box", json!({"length": 40, "width": 30, "height": 20})).unwrap(); let (id, length) = h.app.session.doc.features.first().map(|f| (f.id, f.param_names[0].clone())).unwrap(); h.app.run("config.configure", json!({ "params": [length] })).unwrap(); h.app.run("config.activate", json!({"row": "Default"})).unwrap(); assert!(h.app.session.doc.configuration_matches()); h.app.edit_feature(id); h.frames(3); let shown = fields(&h); assert_eq!(shown, vec![("length", "40".to_string()), ("width", "30".to_string()), ("height", "20".to_string())]); ok(&mut h); assert_eq!(stored(&h, id), ["40", "30", "20"]); assert!(h.app.session.doc.configuration_matches(), "an edit without a change keeps the configuration matched"); assert_size(&mut h.app.session, [40.0, 30.0, 20.0]); }