diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 838c26b050..d8a8678c84 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -6048,7 +6048,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option = at.iter().map(|e| e.as_str()).collect(); d.extra.insert("position_exprs".into(), json!({"at": exprs, "point": pt3(position.0)})); } - crate::hole_refs::restore(s, &mut d, references, &f.edge_names); + crate::hole_refs::restore(s, &mut d, references, &f.edge_names, app.preferences.length_decimals); } } if to.is_some() { @@ -6101,7 +6101,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option; 2], + /// The rounded text written into `distance` for that model value. + shown: [Option; 2], seen: Option, frame: Option, focus: Option, } +/// The text that drives the model for reference `k`: the exact measured value while the field +/// still shows its rounded form, else what the user typed. +fn model_text(refs: &Refs, k: usize) -> Option<&str> { + match (refs.exact.get(k).and_then(Option::as_deref), refs.shown.get(k).and_then(Option::as_deref)) { + (Some(exact), Some(shown)) if refs.distance.get(k).is_some_and(|d| d == shown) => Some(exact), + _ => refs.distance.get(k).map(String::as_str), + } +} + /// The place, edges and distances the dialog last had in step. #[derive(Clone, Debug, PartialEq)] struct Seen { @@ -186,7 +200,7 @@ fn frame(s: &Session, d: &mut Dialog) -> Option<(ReferenceFrame, Vec)> { /// Keep the distances and the hole's place in step: a new place or a new edge is measured, a /// typed distance moves the place there (when it stays on the face). Run before the dialog's /// commands are made. -pub(crate) fn sync(s: &Session, d: &mut Dialog) { +pub(crate) fn sync(s: &Session, d: &mut Dialog, decimals: u8) { if !takes(d) { return; } @@ -207,7 +221,7 @@ pub(crate) fn sync(s: &Session, d: &mut Dialog) { // Typed: the hole goes to the distances. Some(sn) if sn.distance != refs.distance => { let values: Option> = - picked.iter().map(|k| refs.distance.get(*k).and_then(|e| s.doc.eval(e, ValueKind::Length).ok())).collect(); + picked.iter().map(|k| model_text(refs, *k).and_then(|e| s.doc.eval(e, ValueKind::Length).ok())).collect(); if let Some(c) = values.and_then(|v| f.place(at, &v).ok()).filter(|c| on_reference_face(&s.world_state(), f, *c)) { to = c; } @@ -221,7 +235,13 @@ pub(crate) fn sync(s: &Session, d: &mut Dialog) { if let Some(slot) = refs.distance.get_mut(*k) && (moved || new_edge || slot.trim().is_empty()) { - *slot = crate::canvas::format_len(&s.doc, v.max(0.0)); + // The field shows the document's length precision; the model keeps the + // measured value until the user edits that field. + let measured = v.max(0.0); + refs.exact[*k] = Some(crate::length_input::model(&s.doc, measured)); + let shown = crate::prefs::format_length(measured, &s.doc.units, decimals); + refs.shown[*k] = Some(shown.clone()); + *slot = shown; } if new_edge && edges.get(*k).is_some_and(Option::is_some) { refs.focus = Some(*k); @@ -238,7 +258,7 @@ pub(crate) fn sync(s: &Session, d: &mut Dialog) { /// The Single hole's (or the Cylinder's) selection rows, each reference edge with its distance on /// the row below it once it is picked. Whether Enter was pressed in a distance. -pub(crate) fn input_rows(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, names: crate::prefs::OriginPlaneNames) -> bool { +pub(crate) fn input_rows(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, names: crate::prefs::OriginPlaneNames, decimals: u8) -> bool { let mut enter = false; for i in 0..d.inputs.len() { crate::dialogs::input_rows_range(d, ui, s, names, i..i + 1); @@ -250,6 +270,11 @@ pub(crate) fn input_rows(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, names: && let (Some(label), Some(value)) = (DISTANCES.get(k), refs.distance.get_mut(k)) { enter |= crate::ui_kit::expr_row(ui, label, value); + // The field no longer shows the measured value: the user now owns what drives it. + if refs.shown[k].as_deref().is_some_and(|shown| shown != value.as_str()) { + refs.exact[k] = None; + refs.shown[k] = None; + } // Without value boxes in the view (#9479), a newly picked edge's distance takes the // keyboard here, as its box would. if refs.focus == Some(k) && !crate::canvas::boxes_shown(ui.ctx()) { @@ -261,19 +286,26 @@ pub(crate) fn input_rows(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, names: d.give_keyboard(id); } } - sync(s, d); + sync(s, d, decimals); enter } /// The `references` OK sends: each picked edge (a point on it) with its distance. pub(crate) fn params(d: &Dialog) -> Vec { let Some(refs) = refs(d).filter(|_| takes(d)) else { return Vec::new() }; - edges(d).iter().zip(&refs.distance).filter_map(|(e, distance)| e.map(|p| json!({"edge": [p.x, p.y, p.z], "distance": distance.trim()}))).collect() + edges(d) + .iter() + .enumerate() + .filter_map(|(k, e)| { + let distance = model_text(refs, k)?; + e.map(|p| json!({"edge": [p.x, p.y, p.z], "distance": distance.trim()})) + }) + .collect() } /// Editing a hole (or a cylinder) with reference edges: the edges picked again where they are /// now, their distances, and the place where those distances put it now. -pub(crate) fn restore(s: &Session, d: &mut Dialog, references: &[HoleReference], names: &[String]) { +pub(crate) fn restore(s: &Session, d: &mut Dialog, references: &[HoleReference], names: &[String], decimals: u8) { if references.is_empty() { return; } @@ -298,7 +330,23 @@ pub(crate) fn restore(s: &Session, d: &mut Dialog, references: &[HoleReference], if let Some(refs) = refs_mut(d) && let Some(slot) = refs.distance.get_mut(k) { - *slot = r.distance.to_string(); + // A literal distance shows at the document's precision; its exact value stays for the + // model. An expression stays as written, since rounding it would break its parameters. + let exact = r.distance.to_string(); + let literal = if solidcraft_engine::doc::expr::is_literal(&exact) { s.doc.eval(&exact, ValueKind::Length).ok() } else { None }; + match literal { + Some(value) => { + let shown = crate::prefs::format_length(value, &s.doc.units, decimals); + refs.exact[k] = Some(exact); + refs.shown[k] = Some(shown.clone()); + *slot = shown; + } + None => { + *slot = exact; + refs.exact[k] = None; + refs.shown[k] = None; + } + } } } // The hole where its distances put it now (the part may have changed since it was placed). @@ -306,7 +354,7 @@ pub(crate) fn restore(s: &Session, d: &mut Dialog, references: &[HoleReference], if let Some(f) = &found && let Some(refs) = refs(d) { - let values: Option> = refs.distance.iter().take(f.lines.len()).map(|e| s.doc.eval(e, ValueKind::Length).ok()).collect(); + let values: Option> = (0..f.lines.len()).map(|k| model_text(refs, k).and_then(|e| s.doc.eval(e, ValueKind::Length).ok())).collect(); if let Some(c) = values.and_then(|v| f.place(at, &v).ok()).filter(|c| on_reference_face(&st, f, *c)) { to = c; } diff --git a/crates/ui-egui/src/hole_refs_ui_tests.rs b/crates/ui-egui/src/hole_refs_ui_tests.rs index 12d3d64e42..45c73043e5 100644 --- a/crates/ui-egui/src/hole_refs_ui_tests.rs +++ b/crates/ui-egui/src/hole_refs_ui_tests.rs @@ -110,19 +110,70 @@ fn a_hole_is_placed_by_its_distances_from_two_edges() { h.frames(4); assert!(picked_edge(&h, "Reference 1").is_some_and(|p| p.x.abs() < 1e-6), "{:?}", picked_edge(&h, "Reference 1")); assert!(picked_edge(&h, "Reference 2").is_some_and(|p| p.y.abs() < 1e-6), "{:?}", picked_edge(&h, "Reference 2")); - assert_eq!(distances(&h), ["10 mm", "12 mm"]); + assert_eq!(distances(&h), ["10.00 mm", "12.00 mm"]); assert!(place(&h).dist(Vec3::new(10.0, 12.0, 10.0)) < 1e-6, "{:?}", place(&h)); // The hole moved on its face (a new pick, or its handle dragged): the distances follow. if let Some(Sel::Face { point, .. }) = h.app.dialog.as_mut().and_then(|d| d.inputs.first_mut()).and_then(|i| i.items.first_mut()) { *point = Vec3::new(20.0, 30.0, 10.0); } h.frames(2); - assert_eq!(distances(&h), ["20 mm", "30 mm"]); + assert_eq!(distances(&h), ["20.00 mm", "30.00 mm"]); h.step(&json!({"widget": "OK"})).unwrap(); assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog.as_ref().map(|d| &d.error)); assert!(!solid_at(&h, Vec3::new(20.0, 30.0, 8.0)) && solid_at(&h, Vec3::new(10.0, 12.0, 8.0)), "the edit moved the hole to (20, 30)"); } +/// #10693: a measured distance reads at the document's precision, while OK still sends the +/// measured value, not the rounded one. +#[test] +fn a_measured_distance_shows_the_document_precision_but_keeps_its_model_value() { + let mut h = plate(); + h.step(&json!({"start": "solid.hole"})).unwrap(); + let at = screen(&mut h, Vec3::new(30.0, 25.0, 10.0)); + h.step(&json!({"click": at})).unwrap(); + h.step(&json!({"click": {"handle": "dialog:Reference 1"}})).unwrap(); + let left = screen(&mut h, Vec3::new(0.0, 20.0, 10.0)); + h.step(&json!({"click": left})).unwrap(); + h.frames(2); + // A face pick lands a hair off the whole millimetre, as a real pick does. + let exact = 20.000000868569572; + if let Some(Sel::Face { point, .. }) = h.app.dialog.as_mut().and_then(|d| d.inputs.first_mut()).and_then(|i| i.items.first_mut()) { + *point = Vec3::new(exact, 30.0, 10.0); + } + h.frames(2); + assert_eq!(distances(&h)[0], "20.00 mm"); + h.step(&json!({"widget": "OK"})).unwrap(); + let f = h.app.session.doc.features.last().unwrap().clone(); + let FeatureKind::Hole { references, .. } = &f.kind else { panic!("the last feature is a hole") }; + let text = references[0].distance.to_string(); + let value = h.app.session.doc.eval(&text, solidcraft_engine::doc::expr::Kind::Length).unwrap(); + assert!((value - exact).abs() < 1e-9, "OK keeps the measured distance, not the rounded text: {text}"); +} + +/// #10693: a comma decimal literal stored on a reference reads at the document's precision. +#[test] +fn a_comma_decimal_reference_distance_shows_the_document_precision() { + let mut h = plate(); + h.app + .run( + "solid.hole", + json!({ + "position": [30.0, 25.0, 10.0], + "diameter": 5, + "depth": 6, + "references": [ + {"edge": [0.0, 20.0, 10.0], "distance": "29,99999999999999 mm"}, + {"edge": [25.0, 0.0, 10.0], "distance": "25,000001373022986 mm"}, + ], + }), + ) + .unwrap(); + let id = h.app.session.doc.features.last().unwrap().id; + h.step(&json!({"call": "ui.editFeature", "params": {"feature": id}})).unwrap(); + h.frames(4); + assert_eq!(distances(&h), ["30.00 mm", "25.00 mm"]); +} + /// Reference edges place one hole: with places on two faces, OK says to pick one. #[test] fn reference_edges_place_one_hole() {