mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* Hole: reference Distance shows the document precision (#10693) A measured reference distance showed the raw float (10.000000868569572 mm). The field now reads at the document's length precision while Refs keeps the measured value for the model, so OK still sends what was measured. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Hole: restored literals use the document precision (#10693) Round every literal reference distance on Edit Feature, including comma decimals and exact whole values; expressions stay as written. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6048,7 +6048,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
let exprs: Vec<&str> = 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<usize>) -> Option<Dia
|
||||
if let Some(inp) = d.inputs.first_mut() {
|
||||
inp.items = vec![face];
|
||||
}
|
||||
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);
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
|
||||
placement_row(d, ui, app);
|
||||
if crate::hole_refs::takes(d) {
|
||||
// Each reference edge with its distance (#37, #6380).
|
||||
enter |= crate::hole_refs::input_rows(d, ui, &app.session, app.preferences.origin_plane_names);
|
||||
enter |= crate::hole_refs::input_rows(d, ui, &app.session, app.preferences.origin_plane_names, app.preferences.length_decimals);
|
||||
} else {
|
||||
input_rows(d, ui, &app.session, app.preferences.origin_plane_names);
|
||||
}
|
||||
|
||||
@@ -34,11 +34,25 @@ pub(crate) const DISTANCES: [&str; 2] = ["Distance 1", "Distance 2"];
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Refs {
|
||||
pub distance: [String; 2],
|
||||
/// The model-space text each distance was measured from, while its field still shows the
|
||||
/// rounded form below.
|
||||
exact: [Option<String>; 2],
|
||||
/// The rounded text written into `distance` for that model value.
|
||||
shown: [Option<String>; 2],
|
||||
seen: Option<Seen>,
|
||||
frame: Option<Framed>,
|
||||
focus: Option<usize>,
|
||||
}
|
||||
|
||||
/// 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<usize>)> {
|
||||
/// 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<Vec<f64>> =
|
||||
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<Value> {
|
||||
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<Vec<f64>> = refs.distance.iter().take(f.lines.len()).map(|e| s.doc.eval(e, ValueKind::Length).ok()).collect();
|
||||
let values: Option<Vec<f64>> = (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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user