Honor design units in sketch dimensions and measurements (#241)
* Honor design units in sketch dimensions and measurements
* fix: apply document units to specialized length controls
* Accept explicit units in length fields; document design-unit dimension values
Length fields in a design-unit control now evaluate what is typed as a
length expression: a bare number stays in the design's units, while an
explicit unit ("10 mm", "0.5 in", "1 in + 5 mm") is converted.
Document that a bare numeric sketch.dimension value from scripts, the
control channel or MCP is read in the design's units.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
---------
Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
23 files changed
+285
-71
No files matched your search
@@ -73,6 +73,11 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
||||
Type a value and press Enter to commit one undo step; rejected values remain editable and
|
||||
cancellation leaves the sketch unchanged. Fit keeps geometry clear of the Sketch Palette
|
||||
and navigation controls, including enlarged displays and nested pending placements.
|
||||
- Sketch annotations, live drawing lengths and Measure use document units. Inches show
|
||||
three decimals and retain a meaningful fourth digit. Bare length dimension entry captures
|
||||
the current design unit; changing units leaves existing geometry and expressions intact.
|
||||
Component/canvas distance controls, sketch cursor coordinates, flat-pattern dimensions
|
||||
and pocket depths, interference volumes and parts-library lengths also use design units.
|
||||
- Curvature and Polygon constraints accept complete preselected operands. Fix/Unfix
|
||||
consumes complete point/curve selections consistently; mixed or ambiguous selections
|
||||
retain the picking workflow. Rejected constraints preserve selection and undo history.
|
||||
|
||||
@@ -103,7 +103,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.params(concat!("p0, p1: overall ends, width", connect_doc!(), created_doc!())),
|
||||
CommandSpec::new("sketch.point", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params(concat!("point: [x,y], id?", connect_doc!(), created_doc!())),
|
||||
CommandSpec::new("sketch.dimension", "Sketch Dimension", dimension).at("SKETCH", "CREATE").icon("dimension").key("D").enabled(in_sketch).params(
|
||||
"entities: [refs], type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current); text_at?: [x, y]. Two lines: the angle between them (acute, or 180° − it when the value is nearer that) unless text_at picks a sector. Returns measured in unit (mm or deg)",
|
||||
"entities: [refs], type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current; a bare number is in the design's units, e.g. 2 means 2 in in an inch design; give a unit such as \"50 mm\" to override); text_at?: [x, y]. Two lines: the angle between them (acute, or 180° − it when the value is nearer that) unless text_at picks a sector. Returns measured in unit (mm or deg)",
|
||||
),
|
||||
CommandSpec::new("sketch.constraint.horizontal_vertical", "Horizontal/Vertical", c_horizontal_vertical)
|
||||
.at("SKETCH", "CONSTRAINTS")
|
||||
@@ -1601,7 +1601,7 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let (unit, default_expr) =
|
||||
if is_angle { ("deg", format!("{} deg", round6(cur.to_degrees()))) } else { ("mm", format!("{} mm", round6(cur))) };
|
||||
let e = value.clone().unwrap_or(default_expr);
|
||||
let e = value.as_ref().map(|e| doc.with_design_unit(e, if is_angle { Kind::Angle } else { Kind::Length })).unwrap_or(default_expr);
|
||||
let pname = doc.new_model_param(&e, unit);
|
||||
// The value must evaluate.
|
||||
let (vals, errs) = doc.param_values();
|
||||
|
||||
@@ -747,7 +747,7 @@ fn doc_settings(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
|
||||
}
|
||||
let prec = format!(
|
||||
"Precision: {} {}, {}°",
|
||||
format_args!("{:.*}", usize::from(app.preferences.length_decimals), 0.0),
|
||||
crate::prefs::show_mm_bare(app, 0.0),
|
||||
crate::prefs::unit_scale(&app.session.doc.units).1,
|
||||
format_args!("{:.*}", usize::from(app.preferences.angle_decimals), 0.0)
|
||||
);
|
||||
@@ -1011,24 +1011,24 @@ fn canvas_panel(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
for (label, p) in [("Point A", &mut m.a), ("Point B", &mut m.b)] {
|
||||
ui.label(label);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut p[0]).speed(0.5).suffix(" mm"));
|
||||
ui.add(egui::DragValue::new(&mut p[1]).speed(0.5).suffix(" mm"));
|
||||
ui.add(crate::prefs::length_drag(&mut p[0], &app.session.doc.units, app.preferences.length_decimals));
|
||||
ui.add(crate::prefs::length_drag(&mut p[1], &app.session.doc.units, app.preferences.length_decimals));
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label("Distance");
|
||||
ui.add(egui::DragValue::new(&mut m.distance).speed(0.5).range(0.001..=1e6).suffix(" mm"));
|
||||
ui.add(crate::prefs::length_drag(&mut m.distance, &app.session.doc.units, app.preferences.length_decimals).range(0.001..=1e6));
|
||||
ui.end_row();
|
||||
} else {
|
||||
let (mut x, mut y, mut w, mut ang, mut op, mut flip) = (c.center.x, c.center.y, c.width, c.angle.to_degrees(), c.opacity, c.flip);
|
||||
ui.label("Center X");
|
||||
let r1 = ui.add(egui::DragValue::new(&mut x).speed(0.5).suffix(" mm"));
|
||||
let r1 = ui.add(crate::prefs::length_drag(&mut x, &app.session.doc.units, app.preferences.length_decimals));
|
||||
ui.end_row();
|
||||
ui.label("Center Y");
|
||||
let r2 = ui.add(egui::DragValue::new(&mut y).speed(0.5).suffix(" mm"));
|
||||
let r2 = ui.add(crate::prefs::length_drag(&mut y, &app.session.doc.units, app.preferences.length_decimals));
|
||||
ui.end_row();
|
||||
ui.label("Width");
|
||||
let r3 = ui.add(egui::DragValue::new(&mut w).speed(0.5).range(0.01..=1e6).suffix(" mm"));
|
||||
let r3 = ui.add(crate::prefs::length_drag(&mut w, &app.session.doc.units, app.preferences.length_decimals).range(0.01..=1e6));
|
||||
ui.end_row();
|
||||
ui.label("Angle");
|
||||
let r4 = ui.add(egui::DragValue::new(&mut ang).speed(1.0).suffix(" deg"));
|
||||
@@ -1573,7 +1573,7 @@ fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
for (k, axis) in ["X distance", "Y distance", "Z distance"].iter().enumerate() {
|
||||
ui.label(*axis);
|
||||
if let Some(v) = m.translate.get_mut(k) {
|
||||
let response = ui.add(egui::DragValue::new(v).speed(0.5).suffix(" mm"));
|
||||
let response = ui.add(crate::prefs::length_drag(v, &app.session.doc.units, app.preferences.length_decimals));
|
||||
crate::scenario::publish_handle(&format!("occurrence-move:distance:{k}"), response.rect.center());
|
||||
}
|
||||
ui.end_row();
|
||||
@@ -1709,7 +1709,11 @@ pub fn dims_overlay(app: &SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
let expr = c.param.as_ref().and_then(|p| app.session.doc.param(p)).map(|p| p.expr.as_str());
|
||||
let col = if c.driven { tk.dimension_driven } else { tk.dimension };
|
||||
let galley = painter.layout_no_wrap(crate::dim_view::label(&c.kind, expr, c.driven), FontId::proportional(11.5), col);
|
||||
let galley = painter.layout_no_wrap(
|
||||
crate::dim_view::label(&c.kind, expr, c.driven, &app.session.doc.units, app.preferences.length_decimals),
|
||||
FontId::proportional(11.5),
|
||||
col,
|
||||
);
|
||||
let size = galley.size() + vec2(6.0, 2.0);
|
||||
let half = Vec2::new(f64::from(size.x) * 0.5 * px, f64::from(size.y) * 0.5 * px);
|
||||
let lay = dim_layout(&frame, c.text.or_else(|| default_text(&frame, middle, px)), px, half);
|
||||
|
||||
@@ -125,12 +125,12 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj
|
||||
app.dialog = Some(d);
|
||||
return;
|
||||
}
|
||||
if crate::dialogs_sheet::overlay(painter, proj.rect, &d) {
|
||||
if crate::dialogs_sheet::overlay(app, painter, proj.rect, &d) {
|
||||
app.dialog = Some(d);
|
||||
return;
|
||||
}
|
||||
if let Kind::Measure { result: Some(r), .. } = &d.kind {
|
||||
measure_line(painter, proj, r);
|
||||
measure_line(app, painter, proj, r);
|
||||
app.dialog = Some(d);
|
||||
return;
|
||||
}
|
||||
@@ -380,7 +380,7 @@ fn drag_arrow(
|
||||
}
|
||||
|
||||
/// The measured distance: a line between the closest points with its length.
|
||||
fn measure_line(painter: &egui::Painter, proj: &Proj, r: &serde_json::Value) {
|
||||
fn measure_line(app: &SolveApp, painter: &egui::Painter, proj: &Proj, r: &serde_json::Value) {
|
||||
let t = Tokens::get();
|
||||
let p = |k: &str| -> Option<Vec3> {
|
||||
let a = r.get(k)?.as_array()?;
|
||||
@@ -393,7 +393,7 @@ fn measure_line(painter: &egui::Painter, proj: &Proj, r: &serde_json::Value) {
|
||||
painter.circle(s, 3.5, t.manipulator, Stroke::new(1.0, Color32::WHITE));
|
||||
}
|
||||
let mid = sa + (sb - sa) * 0.5 + vec2(10.0, -14.0);
|
||||
let galley = painter.layout_no_wrap(format!("{d:.3} mm"), egui::FontId::proportional(12.5), t.text);
|
||||
let galley = painter.layout_no_wrap(crate::prefs::show_mm(app, d, 1), egui::FontId::proportional(12.5), t.text);
|
||||
let rect = egui::Rect::from_min_size(mid, galley.size() + vec2(10.0, 6.0));
|
||||
painter.rect(rect, 3.0, t.overlay, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
painter.galley(rect.min + vec2(5.0, 3.0), galley, t.text);
|
||||
|
||||
@@ -1200,12 +1200,12 @@ fn update_measure(app: &SolveApp, d: &mut Dialog) {
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_mm(v: &Value) -> String {
|
||||
v.as_f64().map(|x| format!("{x:.3} mm")).unwrap_or_default()
|
||||
fn fmt_mm(app: &SolveApp, v: &Value) -> String {
|
||||
v.as_f64().map(|x| crate::prefs::show_mm(app, x, 1)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The measurement in the dialog.
|
||||
fn measure_rows(ui: &mut egui::Ui, r: Option<&Value>) {
|
||||
fn measure_rows(app: &SolveApp, ui: &mut egui::Ui, r: Option<&Value>) {
|
||||
let t = Tokens::get();
|
||||
let Some(r) = r else {
|
||||
ui.label("");
|
||||
@@ -1221,11 +1221,11 @@ fn measure_rows(ui: &mut egui::Ui, r: Option<&Value>) {
|
||||
}
|
||||
for (i, it) in r["items"].as_array().into_iter().flatten().enumerate() {
|
||||
let what = match it["type"].as_str() {
|
||||
Some("edge") => format!("Length {}", fmt_mm(&it["length_mm"])),
|
||||
Some("face") => format!("Area {:.3} mm²", it["area_mm2"].as_f64().unwrap_or(0.0)),
|
||||
Some("edge") => format!("Length {}", fmt_mm(app, &it["length_mm"])),
|
||||
Some("face") => format!("Area {}", crate::prefs::show_mm(app, it["area_mm2"].as_f64().unwrap_or(0.0), 2)),
|
||||
Some("vertex") => {
|
||||
let p = &it["position"];
|
||||
format!("At {:.3}, {:.3}, {:.3}", p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0), p[2].as_f64().unwrap_or(0.0))
|
||||
format!("At {}, {}, {}", fmt_mm(app, &p[0]), fmt_mm(app, &p[1]), fmt_mm(app, &p[2]))
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
@@ -1235,12 +1235,12 @@ fn measure_rows(ui: &mut egui::Ui, r: Option<&Value>) {
|
||||
}
|
||||
if r.get("distance_mm").is_some() {
|
||||
ui.label(RichText::new("Distance").strong());
|
||||
ui.label(RichText::new(fmt_mm(&r["distance_mm"])).strong());
|
||||
ui.label(RichText::new(fmt_mm(app, &r["distance_mm"])).strong());
|
||||
ui.end_row();
|
||||
let d = &r["delta"];
|
||||
for (k, l) in ["ΔX", "ΔY", "ΔZ"].iter().enumerate() {
|
||||
ui.label(*l);
|
||||
ui.label(format!("{:.3} mm", d[k].as_f64().unwrap_or(0.0).abs()));
|
||||
ui.label(crate::prefs::show_mm(app, d[k].as_f64().unwrap_or(0.0).abs(), 1));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
@@ -1638,7 +1638,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
d.extra.insert("combine".into(), json!(op == 1));
|
||||
ui.end_row();
|
||||
}
|
||||
Kind::Measure { result, .. } => measure_rows(ui, result.as_ref()),
|
||||
Kind::Measure { result, .. } => measure_rows(app, ui, result.as_ref()),
|
||||
Kind::OffsetPlane { offset } => {
|
||||
row_label(ui, "Distance");
|
||||
enter |= field(ui, offset);
|
||||
|
||||
@@ -591,7 +591,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
|
||||
}
|
||||
ui.end_row();
|
||||
if let Some(r) = result.as_ref() {
|
||||
interference_rows(ui, r);
|
||||
interference_rows(app, ui, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,7 +623,7 @@ fn interference(app: &mut SolveApp, picked: &[Sel]) -> (Value, Vec<Mesh>) {
|
||||
(r, meshes)
|
||||
}
|
||||
|
||||
fn interference_rows(ui: &mut egui::Ui, r: &Value) {
|
||||
fn interference_rows(app: &SolveApp, ui: &mut egui::Ui, r: &Value) {
|
||||
let t = Tokens::get();
|
||||
if let Some(e) = r.get("error").and_then(Value::as_str) {
|
||||
row_label(ui, "");
|
||||
@@ -646,7 +646,7 @@ fn interference_rows(ui: &mut egui::Ui, r: &Value) {
|
||||
ui.end_row();
|
||||
for h in hits {
|
||||
row_label(ui, &format!("{} · {}", h["a"].as_str().unwrap_or(""), h["b"].as_str().unwrap_or("")));
|
||||
ui.label(RichText::new(format!("{:.3} mm³", h["volume_mm3"].as_f64().unwrap_or(0.0))).color(t.error));
|
||||
ui.label(RichText::new(crate::prefs::show_mm(app, h["volume_mm3"].as_f64().unwrap_or(0.0), 3)).color(t.error));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pt, inputs: &[SelInpu
|
||||
ui.end_row();
|
||||
if let Some(ls) = &f.lengths {
|
||||
let mut labels = vec!["Default".to_string()];
|
||||
labels.extend(ls.iter().map(|l| format!("{l} mm")));
|
||||
labels.extend(ls.iter().map(|l| crate::prefs::show_mm(app, *l, 1)));
|
||||
let refs: Vec<&str> = labels.iter().map(String::as_str).collect();
|
||||
let mut i = k.length.map_or(0, |x| x + 1);
|
||||
row_label(ui, "Length");
|
||||
@@ -187,7 +187,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pt, inputs: &[SelInpu
|
||||
{
|
||||
for (key, v) in o.iter().filter(|(key, _)| key.as_str() != "size") {
|
||||
row_label(ui, &key.replace('_', " "));
|
||||
ui.label(RichText::new(v.as_f64().map(|x| format!("{x} mm")).unwrap_or_else(|| v.to_string())).color(t.text_dim));
|
||||
ui.label(RichText::new(v.as_f64().map(|x| crate::prefs::show_mm(app, x, 1)).unwrap_or_else(|| v.to_string())).color(t.text_dim));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
|
||||
ui.end_row();
|
||||
row_label(ui, "Flat size");
|
||||
ui.label(format!(
|
||||
"{:.2} × {:.2} × {:.2} mm",
|
||||
size[0].as_f64().unwrap_or(0.0),
|
||||
size[1].as_f64().unwrap_or(0.0),
|
||||
size[2].as_f64().unwrap_or(0.0)
|
||||
"{} × {} × {}",
|
||||
crate::prefs::show_mm(app, size[0].as_f64().unwrap_or(0.0), 1),
|
||||
crate::prefs::show_mm(app, size[1].as_f64().unwrap_or(0.0), 1),
|
||||
crate::prefs::show_mm(app, size[2].as_f64().unwrap_or(0.0), 1)
|
||||
));
|
||||
ui.end_row();
|
||||
let bends = v["bends"].as_array().cloned().unwrap_or_default();
|
||||
@@ -349,10 +349,10 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
|
||||
row_label(ui, &format!(" Bend {}", b["bend"].as_u64().unwrap_or(0)));
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"{} {:.1}° R{:.2}",
|
||||
"{} {:.1}° R{}",
|
||||
if up { "Up" } else { "Down" },
|
||||
b["angle_deg"].as_f64().unwrap_or(0.0),
|
||||
b["radius"].as_f64().unwrap_or(0.0)
|
||||
crate::prefs::show_mm(app, b["radius"].as_f64().unwrap_or(0.0), 1)
|
||||
))
|
||||
.color(if up { up_col } else { down_col }),
|
||||
);
|
||||
@@ -691,7 +691,7 @@ fn bend_colors() -> (Color32, Color32) {
|
||||
|
||||
/// The flat pattern view: the outline, cut-outs and bend lines (up and down in their colours)
|
||||
/// drawn flat over the viewport, fitted to it. True when the dialog is a flat pattern.
|
||||
pub fn overlay(painter: &egui::Painter, rect: Rect, d: &Dialog) -> bool {
|
||||
pub fn overlay(app: &SolveApp, painter: &egui::Painter, rect: Rect, d: &Dialog) -> bool {
|
||||
let Kind::Sheet(Sm::FlatPattern { data, .. }) = &d.kind else { return false };
|
||||
let t = Tokens::get();
|
||||
let (up_col, down_col) = bend_colors();
|
||||
@@ -757,7 +757,7 @@ pub fn overlay(painter: &egui::Painter, rect: Rect, d: &Dialog) -> bool {
|
||||
if !outer.is_empty() {
|
||||
let bounds = outer.iter().map(|p| to(*p)).fold(Rect::NOTHING, |r, p| r.union(Rect::from_min_max(p, p)));
|
||||
let anchor = bounds.center_top() - vec2(0.0, 6.0);
|
||||
if let Some(label) = pocket_label(pocket) {
|
||||
if let Some(label) = pocket_label(app, pocket) {
|
||||
if let Some((_, labels)) = pocket_annotations.iter_mut().find(|(at, _)| at.distance(anchor) < 1.0) {
|
||||
if !labels.contains(&label) {
|
||||
labels.push(label);
|
||||
@@ -797,7 +797,7 @@ pub fn overlay(painter: &egui::Painter, rect: Rect, d: &Dialog) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn pocket_label(pocket: &Value) -> Option<String> {
|
||||
fn pocket_label(app: &SolveApp, pocket: &Value) -> Option<String> {
|
||||
let depth = pocket["depth_mm"].as_f64()?;
|
||||
if !depth.is_finite() || depth <= 0.0 {
|
||||
return None;
|
||||
@@ -808,7 +808,7 @@ fn pocket_label(pocket: &Value) -> Option<String> {
|
||||
"internal" => "Internal",
|
||||
_ => return None,
|
||||
};
|
||||
Some(format!("{side} pocket: {depth} mm deep"))
|
||||
Some(format!("{side} pocket: {} deep", crate::prefs::show_mm(app, depth, 1)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -145,7 +145,7 @@ fn shallow_pocket_overlay_labels_depth_without_drawing_a_through_void() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("flat-pocket-test")));
|
||||
assert!(overlay(&painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
|
||||
assert!(overlay(&app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
let texts: Vec<&str> = output
|
||||
@@ -156,7 +156,7 @@ fn shallow_pocket_overlay_labels_depth_without_drawing_a_through_void() {
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(texts.contains(&"Bottom pocket: 1 mm deep"));
|
||||
assert!(texts.contains(&"Bottom pocket: 1.00 mm deep"));
|
||||
assert!(texts.contains(&"Pocket boundary (material remains)"));
|
||||
let paths: Vec<_> = output
|
||||
.shapes
|
||||
@@ -198,7 +198,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
|
||||
ui.label("Flat size: 50 × 10 × 2.5 mm");
|
||||
});
|
||||
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Background, egui::Id::new("island-fit")));
|
||||
assert!(overlay(&painter, Rect::from_min_max(pos2(250.0, 126.0), pos2(1400.0, 832.0)), &dialog));
|
||||
assert!(overlay(&app, &painter, Rect::from_min_max(pos2(250.0, 126.0), pos2(1400.0, 832.0)), &dialog));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
}
|
||||
@@ -237,7 +237,7 @@ fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("coincident-pocket-test")));
|
||||
assert!(overlay(&painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
|
||||
assert!(overlay(&app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
let labels: Vec<_> = output
|
||||
@@ -249,8 +249,8 @@ fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() {
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(labels.len(), 1, "coincident depths must not overwrite one another");
|
||||
assert!(labels[0].contains("Top pocket: 1 mm deep"));
|
||||
assert!(labels[0].contains("Bottom pocket: 0.5 mm deep"));
|
||||
assert!(labels[0].contains("Top pocket: 1.00 mm deep"));
|
||||
assert!(labels[0].contains("Bottom pocket: 0.50 mm deep"));
|
||||
assert!(labels[0].contains('\n'));
|
||||
assert_eq!(app.session.doc.to_json(), doc);
|
||||
}
|
||||
@@ -310,3 +310,11 @@ fn bend_table_picker_writes_the_current_body() {
|
||||
assert!(contents.contains(",90.000000000,up,2.500000000,"), "{contents}");
|
||||
assert_eq!(app.session.undo.len(), undo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
|
||||
let mut app = plate();
|
||||
app.run("document.units", json!({"units":"in"})).unwrap();
|
||||
assert_eq!(super::pocket_label(&app, &json!({"side":"top","depth_mm":25.4*0.1234})).as_deref(), Some("Top pocket: 0.1234 in deep"));
|
||||
assert!(super::pocket_label(&app, &json!({"side":"top","depth_mm":-1})).is_none());
|
||||
}
|
||||
@@ -290,9 +290,18 @@ pub fn drag_end() {
|
||||
|
||||
/// The text shown for a dimension: the value (R / Ø for radius and diameter, degrees for
|
||||
/// angles), "fx: value" when an expression drives it, in parentheses when driven.
|
||||
pub fn label(k: &ConstraintKind, expr: Option<&str>, driven: bool) -> String {
|
||||
pub fn label(k: &ConstraintKind, expr: Option<&str>, driven: bool, units: &str, decimals: u8) -> String {
|
||||
let v = k.value().unwrap_or(0.0);
|
||||
let num = if k.is_angle() { format!("{:.1}°", v.to_degrees()) } else { format!("{v:.2}") };
|
||||
// Retain deliberately authored trailing zeros in a four-place inch literal too.
|
||||
let authored_four = units == "in"
|
||||
&& expr.is_some_and(|e| {
|
||||
e.trim().strip_suffix("in").map(str::trim).is_some_and(|number| {
|
||||
number.parse::<f64>().is_ok()
|
||||
&& number.split_once('.').is_some_and(|(_, fraction)| fraction.len() >= 4 && fraction.bytes().all(|b| b.is_ascii_digit()))
|
||||
})
|
||||
});
|
||||
let decimals = if authored_four { decimals.max(4) } else { decimals };
|
||||
let num = if k.is_angle() { format!("{:.1}°", v.to_degrees()) } else { crate::prefs::length_number(v, units, decimals) };
|
||||
let num = match k {
|
||||
ConstraintKind::Radius { .. } => format!("R{num}"),
|
||||
ConstraintKind::Diameter { .. } | ConstraintKind::LinearDiameter { .. } => format!("Ø{num}"),
|
||||
@@ -300,7 +309,7 @@ pub fn label(k: &ConstraintKind, expr: Option<&str>, driven: bool) -> String {
|
||||
};
|
||||
let plain = expr.is_none_or(|e| {
|
||||
let e = e.trim();
|
||||
let e = e.strip_suffix("mm").or_else(|| e.strip_suffix("deg")).or_else(|| e.strip_suffix('°')).unwrap_or(e);
|
||||
let e = ["mm", "cm", "in", "ft", "m", "deg", "rad", "°"].iter().find_map(|unit| e.strip_suffix(unit)).unwrap_or(e);
|
||||
e.trim().parse::<f64>().is_ok()
|
||||
});
|
||||
let text = if plain { num } else { format!("fx: {num}") };
|
||||
@@ -392,7 +401,8 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P
|
||||
} else {
|
||||
tk.dimension
|
||||
};
|
||||
let galley = painter.layout_no_wrap(label(&c.kind, expr, c.driven), font.clone(), col);
|
||||
let galley =
|
||||
painter.layout_no_wrap(label(&c.kind, expr, c.driven, &app.session.doc.units, app.preferences.length_decimals), font.clone(), col);
|
||||
let size = galley.size() + vec2(6.0, 2.0);
|
||||
let half = Vec2::new(f64::from(size.x) * 0.5 * px, f64::from(size.y) * 0.5 * px);
|
||||
// Unplaced linear dimensions sit outside the shape they measure.
|
||||
@@ -528,7 +538,15 @@ fn edit_box(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
Err("The sketch changed; place the dimension again".into())
|
||||
}
|
||||
} else {
|
||||
app.run("parameters.change", json!({"name": e.param, "expression": e.expr}))
|
||||
let kind = app
|
||||
.session
|
||||
.doc
|
||||
.param(&e.param)
|
||||
.and_then(|p| solvecraft_engine::doc::expr::unit_info(&p.unit))
|
||||
.map(|(k, _)| k)
|
||||
.unwrap_or(solvecraft_engine::doc::expr::Kind::Unitless);
|
||||
let expression = app.session.doc.with_design_unit(&e.expr, kind);
|
||||
app.run("parameters.change", json!({"name": e.param, "expression": expression}))
|
||||
};
|
||||
if let Err(err) = result {
|
||||
app.set_status(err, true);
|
||||
@@ -621,15 +639,33 @@ mod tests {
|
||||
cancel();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inch_labels_convert_all_lengths_preserve_four_digits_and_leave_angles_alone() {
|
||||
let length = ConstraintKind::Length { l: 0, value: 25.4 };
|
||||
assert_eq!(label(&length, Some("25.4 mm"), false, "in", 2), "1.000");
|
||||
assert_eq!(label(&length, Some("1 in"), false, "in", 2), "1.000");
|
||||
assert_eq!(label(&length, Some("1.0000 in"), false, "in", 2), "1.0000");
|
||||
let radius = ConstraintKind::Radius { c: 0, value: 1.2345 * 25.4 };
|
||||
assert_eq!(label(&radius, Some("1.2345 in"), false, "in", 2), "R1.2345");
|
||||
assert_eq!(label(&radius, Some("width / 2"), true, "in", 2), "(fx: R1.2345)");
|
||||
let diameter = ConstraintKind::Diameter { c: 0, value: 0.001 * 25.4 };
|
||||
assert_eq!(label(&diameter, None, false, "in", 2), "Ø0.001");
|
||||
assert_eq!(crate::prefs::length_number(25.4 * (1.0 + 1e-12), "in", 2), "1.000");
|
||||
assert_eq!(crate::prefs::length_number(25.4, "in", 5), "1.00000");
|
||||
assert_eq!(crate::prefs::length_number(25.4, "cm", 2), "2.54");
|
||||
let angle = ConstraintKind::Angle { a: 0, b: 1, value: std::f64::consts::FRAC_PI_4, flip: false };
|
||||
assert_eq!(label(&angle, Some("45 deg"), false, "in", 2), "45.0°");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_show_the_value_or_fx() {
|
||||
let k = ConstraintKind::Length { l: 0, value: 25.0 };
|
||||
assert_eq!(label(&k, Some("25 mm"), false), "25.00");
|
||||
assert_eq!(label(&k, Some("width * 2"), false), "fx: 25.00");
|
||||
assert_eq!(label(&k, None, true), "(25.00)");
|
||||
assert_eq!(label(&ConstraintKind::Radius { c: 0, value: 5.0 }, Some("5 mm"), false), "R5.00");
|
||||
assert_eq!(label(&ConstraintKind::Diameter { c: 0, value: 10.0 }, Some("10"), false), "Ø10.00");
|
||||
assert_eq!(label(&k, Some("25 mm"), false, "mm", 2), "25.00");
|
||||
assert_eq!(label(&k, Some("width * 2"), false, "mm", 2), "fx: 25.00");
|
||||
assert_eq!(label(&k, None, true, "mm", 2), "(25.00)");
|
||||
assert_eq!(label(&ConstraintKind::Radius { c: 0, value: 5.0 }, Some("5 mm"), false, "mm", 2), "R5.00");
|
||||
assert_eq!(label(&ConstraintKind::Diameter { c: 0, value: 10.0 }, Some("10"), false, "mm", 2), "Ø10.00");
|
||||
let a = ConstraintKind::Angle { a: 0, b: 1, value: std::f64::consts::FRAC_PI_4, flip: false };
|
||||
assert_eq!(label(&a, Some("45 deg"), false), "45.0°");
|
||||
assert_eq!(label(&a, Some("45 deg"), false, "mm", 2), "45.0°");
|
||||
}
|
||||
}
|
||||
@@ -290,3 +290,36 @@ fn draft_annotation_does_not_move_committed_dimension_hitboxes() {
|
||||
h.step(&json!({"key":"Escape"})).unwrap();
|
||||
assert_eq!(crate::dim_view::drawn(), drawn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inch_dimension_entry_survives_unit_changes_undo_and_reopen() {
|
||||
let mut h = line([25.4, 0.]);
|
||||
h.app.run("document.units", json!({"units":"in"})).unwrap();
|
||||
place(&mut h, &[[12.7, 0.]], [12.7, 8.]);
|
||||
type_value(&mut h, "1.2345");
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
let sketch = h.app.session.active_sketch.unwrap();
|
||||
h.settle();
|
||||
crate::dim_view::edit_param(&h.app, "d1");
|
||||
h.settle();
|
||||
type_value(&mut h, "1.3456");
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
let edited = h.app.session.world_state();
|
||||
let edited = &edited.sketch(sketch).unwrap().sketch;
|
||||
assert!(edited.constraints.iter().any(|c| c.kind.value().is_some_and(|v| (v - 1.3456 * 25.4).abs() < 1e-6)));
|
||||
h.app.run("edit.undo", json!({})).unwrap();
|
||||
let before = h.app.session.world_state().sketch(sketch).unwrap().sketch.clone();
|
||||
let c = before.constraints.iter().find(|c| c.param.is_some()).unwrap();
|
||||
let p = h.app.session.doc.param(c.param.as_deref().unwrap()).unwrap();
|
||||
assert_eq!(crate::dim_view::label(&c.kind, Some(&p.expr), false, "in", 2), "1.2345");
|
||||
assert!((c.kind.value().unwrap() - 31.3563).abs() < 1e-6);
|
||||
h.app.run("document.units", json!({"units":"mm"})).unwrap();
|
||||
assert_eq!(h.app.session.world_state().sketch(sketch).unwrap().sketch, before);
|
||||
h.app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(h.app.session.doc.units, "in");
|
||||
let doc = solvecraft_engine::doc::Document::from_json(&h.app.session.doc.to_json()).unwrap();
|
||||
let reopened = solvecraft_engine::Session::new(doc);
|
||||
assert_eq!(reopened.doc.units, "in");
|
||||
assert_eq!(reopened.world_state().sketch(sketch).unwrap().sketch, before);
|
||||
crate::dim_view::cancel();
|
||||
}
|
||||
@@ -499,7 +499,7 @@ fn units_menu_changes_the_design_units_and_new_designs_take_the_preference() {
|
||||
assert_eq!(items.len(), 5);
|
||||
act(&mut app, &Target::Units, "in");
|
||||
assert_eq!(app.session.doc.units, "in");
|
||||
assert!(crate::prefs::show_mm(&app, 25.4, 1).starts_with("1.00 in"));
|
||||
assert!(crate::prefs::show_mm(&app, 25.4, 1).starts_with("1.000 in"));
|
||||
app.preferences.default_units = "cm".into();
|
||||
crate::documents::new_design(&mut app);
|
||||
assert_eq!(app.session.doc.units, "cm");
|
||||
|
||||
@@ -202,3 +202,31 @@ fn occurrence_nested_shared_child_has_distinct_rows_and_scope() {
|
||||
context_menu::run_item(&mut h.app, delete, egui::Pos2::ZERO);
|
||||
assert!(h.app.menu.confirm.is_some(), "shared child deletion must communicate scope before removing both renderings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occurrence_distance_editor_reads_inches_and_keeps_four_digits_in_model_space() {
|
||||
let (mut h, _, first, _) = fixture();
|
||||
h.app.run("document.units", json!({"units":"in"})).unwrap();
|
||||
let target = Target::Occurrence { path: vec![first] };
|
||||
let items = context_menu::items(&h.app, &target);
|
||||
let movement = items.iter().find(|i| i.id == "ui.moveOccurrence").unwrap();
|
||||
context_menu::run_item(&mut h.app, movement, egui::Pos2::ZERO);
|
||||
h.settle();
|
||||
let original = h.app.session.doc.to_json();
|
||||
let point = h.call("ui.at", json!({"handle":"occurrence-move:distance:0"}));
|
||||
let at = &point["result"];
|
||||
h.call("ui.click", json!({"x":at[0],"y":at[1],"double":true}));
|
||||
h.settle();
|
||||
h.step(&json!({"key":"A","cmd":true})).unwrap();
|
||||
h.step(&json!({"text":"1.2345"})).unwrap();
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
let m = h.app.tree.occurrence_move.as_ref().unwrap();
|
||||
assert!((m.translate[0] - 31.3563).abs() < 1e-8, "{:?}", m.translate);
|
||||
assert_eq!(h.app.session.doc.to_json(), original, "pending movement does not alter the stored design");
|
||||
h.app.session.selection = vec![Sel::Occurrence { path: vec![first] }];
|
||||
let bounds = crate::viewport::selection_bounds(&h.app).unwrap();
|
||||
assert!((bounds.min.x - 31.3563).abs() < 1e-6);
|
||||
let movement = h.app.tree.occurrence_move.clone().unwrap();
|
||||
crate::browser::revert_occurrence_dialog(&mut h.app, &movement);
|
||||
assert!(h.app.session.pending_moves.is_empty());
|
||||
}
|
||||
@@ -347,10 +347,49 @@ pub fn unit_scale(units: &str) -> (f64, &'static str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inch labels retain a meaningful fourth digit without promoting floating-point noise.
|
||||
pub fn length_precision(value: f64, units: &str, decimals: u8) -> usize {
|
||||
let base = usize::from(decimals.min(8));
|
||||
if units != "in" {
|
||||
return base;
|
||||
}
|
||||
let needs_four = ((value * 10_000.0).round() - (value * 1000.0).round() * 10.0).abs() >= 0.5;
|
||||
base.max(if needs_four { 4 } else { 3 })
|
||||
}
|
||||
|
||||
pub fn length_number(mm: f64, units: &str, decimals: u8) -> String {
|
||||
let value = mm * unit_scale(units).0;
|
||||
format!("{:.*}", length_precision(value, units, decimals), value)
|
||||
}
|
||||
|
||||
/// Edit a millimetre value through a control expressed in the current design units.
|
||||
/// Keep the stored value untouched unless the user actually changes the field.
|
||||
pub fn length_drag<'a>(value: &'a mut f64, units: &str, decimals: u8) -> egui::DragValue<'a> {
|
||||
let (scale, unit) = unit_scale(units);
|
||||
egui::DragValue::new(value)
|
||||
.speed(0.5)
|
||||
.suffix(format!(" {unit}"))
|
||||
.custom_formatter(move |mm, _| length_number(mm, unit, decimals))
|
||||
.custom_parser(move |text| parse_length_mm(text, scale))
|
||||
}
|
||||
|
||||
/// A length typed into a field, in millimetres: a bare number is in the design's units (`scale`
|
||||
/// units per millimetre); an explicit unit ("10 mm", "0.5 in", "2 cm + 3 mm") is honoured.
|
||||
pub fn parse_length_mm(text: &str, scale: f64) -> Option<f64> {
|
||||
use solvecraft_engine::doc::expr;
|
||||
if !(scale.is_finite() && scale > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let defaults = expr::Defaults { len: 1.0 / scale, ..expr::Defaults::default() };
|
||||
let no_params = |n: &str| Err(solvecraft_engine::doc::DocError::Expr(format!("unknown parameter `{n}`")));
|
||||
let value = expr::eval_with_defaults(text.trim(), &no_params, defaults).ok()?.to_kind_in(expr::Kind::Length, defaults).ok()?;
|
||||
value.is_finite().then_some(value)
|
||||
}
|
||||
|
||||
/// A length in millimetres in `units`, with `decimals` (for display).
|
||||
pub fn format_length(mm: f64, units: &str, decimals: u8) -> String {
|
||||
let (k, u) = unit_scale(units);
|
||||
format!("{:.*} {u}", usize::from(decimals.min(8)), mm * k)
|
||||
let (_, u) = unit_scale(units);
|
||||
format!("{} {u}", length_number(mm, units, decimals))
|
||||
}
|
||||
|
||||
/// A length, area (`power` 2) or volume (3) given in millimetres, shown in the design's units
|
||||
@@ -362,13 +401,13 @@ pub fn show_mm(app: &SolveApp, v: f64, power: i32) -> String {
|
||||
3 => "³",
|
||||
_ => "",
|
||||
};
|
||||
format!("{:.*} {u}{sup}", usize::from(app.preferences.length_decimals.min(8)), v * k.powi(power))
|
||||
let value = v * k.powi(power);
|
||||
format!("{:.*} {u}{sup}", length_precision(value, &app.session.doc.units, app.preferences.length_decimals), value)
|
||||
}
|
||||
|
||||
/// Numbers in millimetres, in the design's units without the unit name.
|
||||
pub fn show_mm_bare(app: &SolveApp, v: f64) -> String {
|
||||
let (k, _) = unit_scale(&app.session.doc.units);
|
||||
format!("{:.*}", usize::from(app.preferences.length_decimals.min(8)), v * k)
|
||||
length_number(v, &app.session.doc.units, app.preferences.length_decimals)
|
||||
}
|
||||
|
||||
/// Write the anti-aliasing setting where the host reads it at start (the window's samples can't
|
||||
@@ -787,6 +826,22 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn length_fields_read_bare_numbers_in_design_units_and_honour_explicit_units() {
|
||||
let inch = unit_scale("in").0;
|
||||
let close = |a: Option<f64>, b: f64| a.is_some_and(|a| (a - b).abs() < 1e-9);
|
||||
assert!(close(parse_length_mm("2", inch), 50.8));
|
||||
assert!(close(parse_length_mm(" 10 mm ", inch), 10.0));
|
||||
assert!(close(parse_length_mm("0.5 in", 1.0), 12.7));
|
||||
assert!(close(parse_length_mm("1 in + 5 mm", unit_scale("cm").0), 30.4));
|
||||
assert!(close(parse_length_mm("3", unit_scale("cm").0), 30.0));
|
||||
assert!(close(parse_length_mm("3", 1.0), 3.0));
|
||||
assert_eq!(parse_length_mm("10 deg", inch), None);
|
||||
assert_eq!(parse_length_mm("abc", inch), None);
|
||||
assert_eq!(parse_length_mm("", inch), None);
|
||||
assert_eq!(parse_length_mm("1e400", 1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radial_context_menu_defaults_off_and_follows_preferences_transactions() {
|
||||
assert!(!Prefs::default().radial_context_menu);
|
||||
|
||||
@@ -205,7 +205,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) -
|
||||
painter.text(
|
||||
s + vec2(10.0, 10.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("{:.2}, {:.2}, {:.2}", h.x, h.y, h.z),
|
||||
format!("{}, {}, {}", crate::prefs::show_mm(app, h.x, 1), crate::prefs::show_mm(app, h.y, 1), crate::prefs::show_mm(app, h.z, 1)),
|
||||
egui::FontId::proportional(11.0),
|
||||
tk.text_dim,
|
||||
);
|
||||
@@ -265,7 +265,12 @@ pub fn click(app: &mut SolveApp, proj: &Proj, pos: Pos2) -> bool {
|
||||
TRIAD.with(|t| *t.borrow_mut() = hit.as_ref().map(|(w, i, _)| (w.clone(), *i)));
|
||||
if let Some((w, _, p)) = hit {
|
||||
app.set_status(
|
||||
format!("{w} at {:.2}, {:.2}, {:.2}: drag an arrow (X, Y, Z) or the square (in the sketch plane); Delete removes it", p.x, p.y, p.z),
|
||||
format!(
|
||||
"{w} at {}, {}, {}: drag an arrow (X, Y, Z) or the square (in the sketch plane); Delete removes it",
|
||||
crate::prefs::show_mm(app, p.x, 1),
|
||||
crate::prefs::show_mm(app, p.y, 1),
|
||||
crate::prefs::show_mm(app, p.z, 1)
|
||||
),
|
||||
false,
|
||||
);
|
||||
return true;
|
||||
|
||||
@@ -170,11 +170,11 @@ fn live(t: &Tool, e: Vec2, px: f64) -> Vec<(f64, Vec2)> {
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt(kind: ValueKind, v: f64) -> String {
|
||||
fn fmt(app: &SolveApp, kind: ValueKind, v: f64) -> String {
|
||||
match kind {
|
||||
ValueKind::Angle => format!("{:.1} deg", v.to_degrees()),
|
||||
ValueKind::Unitless => format!("{}", v.round()),
|
||||
ValueKind::Length => format!("{v:.2}"),
|
||||
ValueKind::Length => crate::prefs::show_mm_bare(app, v),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ fn boxes(app: &SolveApp, ui: &mut egui::Ui, proj: &Proj, t: &mut Tool) -> Option
|
||||
for (i, b) in t.dims.iter_mut().enumerate() {
|
||||
let Some((v, at)) = lv.get(i).copied() else { continue };
|
||||
if !b.locked {
|
||||
b.value = fmt(b.kind, v);
|
||||
b.value = fmt(app, b.kind, v);
|
||||
}
|
||||
let Some(sp) = proj.to_screen(plane.to_world(at)) else { continue };
|
||||
let id = egui::Id::new(("sc_dim", i));
|
||||
|
||||
@@ -126,3 +126,24 @@ fn forward_tab_completes_expression_then_cycles_and_shift_tab_moves_back() {
|
||||
assert_eq!(sketch.profiles.len(), 1);
|
||||
assert!((sketch.profiles[0].area - 120.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inch_live_lengths_use_design_units_and_typed_four_digits_commit_in_inches() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("document.units", json!({"units":"in"})).unwrap();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
h.step(&json!({"start":"sketch.rectangle.two_point"})).unwrap();
|
||||
h.step(&json!({"click":{"world":[0,0,0]}})).unwrap();
|
||||
h.step(&json!({"move":{"world":[25.4,12.7,0]}})).unwrap();
|
||||
let dims = &h.app.tool.as_ref().unwrap().dims;
|
||||
// Pointer snapping rounds in model space; displayed values must still be inches.
|
||||
assert!((dims[0].value.parse::<f64>().unwrap() - 1.0).abs() < 0.01);
|
||||
assert!((dims[1].value.parse::<f64>().unwrap() - 0.5).abs() < 0.01);
|
||||
type_value(&mut h, "1.2345");
|
||||
tab(&mut h, false);
|
||||
type_value(&mut h, "0.500");
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
let st = h.app.session.world_state();
|
||||
let sk = &st.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
|
||||
assert!(sk.constraints.iter().any(|c| c.kind.value().is_some_and(|v| (v - 31.3563).abs() < 1e-6)));
|
||||
}
|
||||
@@ -75,7 +75,7 @@ fn analysis(app: &mut SolveApp, cmd: &str, p: Value) {
|
||||
let Ok(v) = app.run(cmd, p) else { return };
|
||||
let mark = match cmd {
|
||||
"inspect.center_of_mass" => v3(&v["center"]).map(|c| (c, "Center of mass".to_string())),
|
||||
_ => v3(&v["at"]).zip(v["min_radius"].as_f64()).map(|(c, r)| (c, format!("R min {r:.3} mm"))),
|
||||
_ => v3(&v["at"]).zip(v["min_radius"].as_f64()).map(|(c, r)| (c, format!("R min {}", crate::prefs::show_mm(app, r, 1)))),
|
||||
};
|
||||
if let Some(m) = mark {
|
||||
MARKS.with(|k| k.borrow_mut().push(m));
|
||||
|
||||
@@ -579,7 +579,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
painter.text(
|
||||
sp + egui::vec2(10.0, 10.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("{:.2}, {:.2}", hp.x, hp.y),
|
||||
format!("{}, {}", crate::prefs::show_mm(app, hp.x, 1), crate::prefs::show_mm(app, hp.y, 1)),
|
||||
egui::FontId::proportional(11.0),
|
||||
tk.text_dim,
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget": "100.0 mm",
|
||||
"widget": "100.00 mm",
|
||||
"near": "Distance"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget": "0.0 mm",
|
||||
"widget": "0.00 mm",
|
||||
"near": "X distance"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -132,3 +132,16 @@ when a parameter moves the geometry. Islands inside the region are its holes. Wh
|
||||
more than one (both halves of a circle cut by a line), is bounded by exactly those curves, the
|
||||
feature reports an error rather than taking another region. Profiles picked by index or by point
|
||||
(`[{"point": [x, y]}]`) are stored the same way when the feature is made.
|
||||
|
||||
## Measurement units
|
||||
|
||||
Change the current design through **Document Settings > Units** in the browser.
|
||||
The units preference applies to new designs. Existing sketch annotations, live drawing
|
||||
lengths and Measure follow the current design units without resizing geometry.
|
||||
Inches display at least three decimal places and retain a meaningful fourth digit;
|
||||
a higher display precision can be selected in Preferences. Bare length values typed
|
||||
in the dimension tool use the current design units. Explicit units such as `25.4 mm`
|
||||
remain valid in an inch design. Angular dimensions keep their angular units. Deliberately typed four-place inch literals
|
||||
retain trailing zeros. Component/canvas distance controls and inspection, parts-library
|
||||
and flat-pattern measurements follow design units too; parameter expressions and metric
|
||||
standard names retain their explicit authored units.
|
||||
@@ -37,6 +37,12 @@ rolls the numbering back with the design), so they can go straight into later co
|
||||
{"id":8,"method":"engine.execute","params":{"command":"sketch.dimension","params":{"entities":["l1"],"value":50}}}
|
||||
```
|
||||
|
||||
A bare numeric `value` given to `sketch.dimension` (from the control channel, scripts or MCP) is
|
||||
read in the design's units, exactly as if it were typed into the dimension box: in an inch design
|
||||
`"value": 2` makes the line 2 in (50.8 mm) long. Point coordinates stay in millimetres. To pin a
|
||||
unit regardless of the design, pass an expression with one: `"value": "50 mm"` or
|
||||
`"value": "0.5 in"`. Angles are degrees unless the expression says otherwise.
|
||||
|
||||
## UI
|
||||
|
||||
| Method | Params | Notes |
|
||||
|
||||
Reference in new issue
Block a user