Document Settings: design units (document.units: mm, cm, m, in, ft; values keep their meaning) from the browser's Units row, display precision row; new designs take the preferred units; the Properties window shows lengths, areas and volumes in the design's units with the chosen precision
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
c99e7e41ec
commit
13070abc55
6 files changed
+127
-17
No files matched your search
@@ -24,6 +24,9 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("browser.rename_group", "Rename Group", rename_group).params("group: id, name"),
|
||||
CommandSpec::new("browser.move", "Move Browser Items", move_items)
|
||||
.params("folder, component?, items: [keys]; group?: id (into it; omit to take them out of groups); before?: key (position in the group)"),
|
||||
CommandSpec::new("document.units", "Change Design Units", document_units)
|
||||
.icon("settings")
|
||||
.params("units: mm|cm|m|in|ft (display and unit-less parameters; values keep their meaning)"),
|
||||
CommandSpec::new("view.save", "New Named View", view_save)
|
||||
.icon("camera")
|
||||
.params("name?; camera: {target: [x,y,z], yaw, pitch, distance, fov?} (replaces a view of the same name)"),
|
||||
@@ -152,6 +155,18 @@ fn order(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({}))
|
||||
}
|
||||
|
||||
fn document_units(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "document.units";
|
||||
let u = str_(p, "units").ok_or_else(|| bad(cmd, "`units` is required"))?;
|
||||
if !["mm", "cm", "m", "in", "ft"].contains(&u) {
|
||||
return Err(bad(cmd, "`units` must be mm, cm, m, in or ft"));
|
||||
}
|
||||
if s.doc.units != u {
|
||||
s.doc_mut().units = u.to_string();
|
||||
}
|
||||
Ok(json!({ "units": u }))
|
||||
}
|
||||
|
||||
fn view_save(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "view.save";
|
||||
let c = p.get("camera").ok_or_else(|| bad(cmd, "`camera` is required"))?;
|
||||
@@ -365,6 +380,20 @@ mod tests {
|
||||
assert!(s.execute("sketch.redefine", &json!({"sketch": 999, "plane": "XY"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_units_change_without_moving_geometry() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "PrimitiveBox", json!({"length": 20, "width": 10, "height": 5}));
|
||||
let vol = |s: &mut Session| run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
|
||||
let v0 = vol(&mut s);
|
||||
run(&mut s, "document.units", json!({"units": "in"}));
|
||||
assert_eq!(s.doc.units, "in");
|
||||
assert!((vol(&mut s) - v0).abs() < 1e-6, "feature inputs keep their millimetres");
|
||||
assert!(s.execute("document.units", &json!({"units": "furlong"})).is_err());
|
||||
run(&mut s, "UndoCommand", json!({}));
|
||||
assert_eq!(s.doc.units, "mm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_views_are_saved_renamed_and_deleted() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -558,7 +558,19 @@ fn doc_settings(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
|
||||
return;
|
||||
}
|
||||
let units = format!("Units: {}", app.session.doc.units);
|
||||
draw_row(ui, ui.id().with("units"), &Row { depth: depth + 1, icon: "settings", label: &units, ..Default::default() });
|
||||
let r = draw_row(ui, ui.id().with("units"), &Row { depth: depth + 1, icon: "settings", label: &units, ..Default::default() });
|
||||
if let Some(p) = r.secondary.or(r.clicked.then(|| r.rect.map(|x| x.left_bottom())).flatten()) {
|
||||
crate::context_menu::open_for(app, p, Target::Units);
|
||||
}
|
||||
let prec = format!(
|
||||
"Precision: {} {}, {}°",
|
||||
format_args!("{:.*}", usize::from(app.preferences.length_decimals), 0.0),
|
||||
crate::prefs::unit_scale(&app.session.doc.units).1,
|
||||
format_args!("{:.*}", usize::from(app.preferences.angle_decimals), 0.0)
|
||||
);
|
||||
if draw_row(ui, ui.id().with("precision"), &Row { depth: depth + 1, icon: "settings", label: &prec, ..Default::default() }).clicked {
|
||||
app.prefs_window.open = true;
|
||||
}
|
||||
let params = format!("Parameters ({})", app.session.doc.params.len());
|
||||
if draw_row(ui, ui.id().with("params"), &Row { depth: depth + 1, icon: "params", label: ¶ms, ..Default::default() }).clicked {
|
||||
app.start("ChangeParameterCommand");
|
||||
|
||||
@@ -40,6 +40,8 @@ pub enum Target {
|
||||
},
|
||||
/// The Origin folder.
|
||||
Origin,
|
||||
/// Document Settings › Units.
|
||||
Units,
|
||||
/// The Named Views folder, and one view (saved or standard) in it.
|
||||
NamedViews,
|
||||
NamedView {
|
||||
@@ -213,9 +215,13 @@ pub fn target_bodies(app: &SolveApp, target: &Target) -> Vec<String> {
|
||||
Target::Body { name } => vec![name.clone()],
|
||||
Target::Component { id } => component_bodies(app, *id),
|
||||
Target::Group { id } => crate::browser::group_bodies(app, *id),
|
||||
Target::Sketch { .. } | Target::Folder { .. } | Target::Canvas { .. } | Target::Origin | Target::NamedViews | Target::NamedView { .. } => {
|
||||
Vec::new()
|
||||
}
|
||||
Target::Sketch { .. }
|
||||
| Target::Folder { .. }
|
||||
| Target::Canvas { .. }
|
||||
| Target::Origin
|
||||
| Target::Units
|
||||
| Target::NamedViews
|
||||
| Target::NamedView { .. } => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +281,13 @@ pub fn items(app: &SolveApp, target: &Target) -> Vec<Item> {
|
||||
Target::Canvas { id } => crate::browser::canvas_items(app, *id),
|
||||
Target::Origin => crate::browser::origin_items(app),
|
||||
Target::NamedViews => vec![act("ui.newView", "New Named View", "perspective")],
|
||||
Target::Units => crate::prefs::UNITS
|
||||
.iter()
|
||||
.map(|u| {
|
||||
let label = if app.session.doc.units == *u { format!("{u} ✓") } else { (*u).to_string() };
|
||||
cmd(app, "document.units", &label).with(json!({ "units": u }))
|
||||
})
|
||||
.collect(),
|
||||
Target::NamedView { name } => crate::browser::view_items(app, name),
|
||||
};
|
||||
// Several items of the folder selected: they can be grouped.
|
||||
@@ -1380,24 +1393,27 @@ fn show_props(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.label(format!("{:.3} g", f(total, "mass_g")));
|
||||
ui.end_row();
|
||||
ui.label("Volume");
|
||||
ui.label(format!("{:.3} mm³", f(total, "volume_mm3")));
|
||||
ui.label(crate::prefs::show_mm(app, f(total, "volume_mm3"), 3));
|
||||
ui.end_row();
|
||||
ui.label("Area");
|
||||
ui.label(format!("{:.3} mm²", f(total, "area_mm2")));
|
||||
ui.label(crate::prefs::show_mm(app, f(total, "area_mm2"), 2));
|
||||
ui.end_row();
|
||||
if let [b] = bodies.as_slice() {
|
||||
if let Some(c) = b["center_of_mass"].as_array() {
|
||||
let c: Vec<String> = c.iter().map(|x| format!("{:.3}", x.as_f64().unwrap_or(f64::NAN))).collect();
|
||||
let c: Vec<String> = c.iter().map(|x| crate::prefs::show_mm_bare(app, x.as_f64().unwrap_or(f64::NAN))).collect();
|
||||
ui.label("Center of mass");
|
||||
ui.label(format!("({}) mm", c.join(", ")));
|
||||
ui.label(format!("({}) {}", c.join(", "), crate::prefs::unit_scale(&app.session.doc.units).1));
|
||||
ui.end_row();
|
||||
}
|
||||
let (mn, mx) = (&b["bbox"]["min"], &b["bbox"]["max"]);
|
||||
if let (Some(a), Some(z)) = (mn.as_array(), mx.as_array()) {
|
||||
let d: Vec<String> =
|
||||
a.iter().zip(z).map(|(a, z)| format!("{:.3}", z.as_f64().unwrap_or(0.0) - a.as_f64().unwrap_or(0.0))).collect();
|
||||
let d: Vec<String> = a
|
||||
.iter()
|
||||
.zip(z)
|
||||
.map(|(a, z)| crate::prefs::show_mm_bare(app, z.as_f64().unwrap_or(0.0) - a.as_f64().unwrap_or(0.0)))
|
||||
.collect();
|
||||
ui.label("Bounding box");
|
||||
ui.label(format!("{} mm", d.join(" × ")));
|
||||
ui.label(format!("{} {}", d.join(" × "), crate::prefs::unit_scale(&app.session.doc.units).1));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ pub fn new_design(app: &mut SolveApp) {
|
||||
if let Err(e) = open_tab(app, Session::default()) {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
crate::prefs::apply_new_design(app);
|
||||
}
|
||||
|
||||
/// Before opening a file: a fresh tab unless the active design is blank.
|
||||
|
||||
@@ -402,3 +402,17 @@ fn named_views_save_restore_rename_and_delete() {
|
||||
context_menu::run_item(&mut app, &find(&top, "Restore"), pos2(0.0, 0.0));
|
||||
assert!(app.cam_anim.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn units_menu_changes_the_design_units_and_new_designs_take_the_preference() {
|
||||
let mut app = sample_app();
|
||||
let items = context_menu::items(&app, &Target::Units);
|
||||
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"));
|
||||
app.preferences.default_units = "cm".into();
|
||||
crate::documents::new_design(&mut app);
|
||||
assert_eq!(app.session.doc.units, "cm");
|
||||
assert!(!app.session.is_dirty(), "a fresh design stays unmodified");
|
||||
}
|
||||
@@ -56,6 +56,19 @@ pub struct PrefsWindow {
|
||||
section: usize,
|
||||
}
|
||||
|
||||
/// A new, empty design takes the preferred units (not an undo step, not a change to save).
|
||||
pub fn apply_new_design(app: &mut SolveApp) {
|
||||
if app.session.doc.features.is_empty() && app.session.doc.units != app.preferences.default_units {
|
||||
let saved = !app.session.is_dirty();
|
||||
if app.session.execute("document.units", &json!({ "units": app.preferences.default_units })).is_ok() {
|
||||
app.session.undo.pop();
|
||||
if saved {
|
||||
app.session.mark_saved();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The autosave interval follows `app.autosave_minutes` (0: off).
|
||||
pub fn set_autosave(app: &mut SolveApp) {
|
||||
let m = app.autosave_minutes;
|
||||
@@ -141,18 +154,41 @@ fn pivot_under(app: &SolveApp, proj: &crate::viewport::Proj, at: Pos2) -> Vec3 {
|
||||
if den.abs() > 1e-9 { o + d * ((app.cam.target - o).dot(n) / den) } else { app.cam.target }
|
||||
}
|
||||
|
||||
/// A length in millimetres in `units`, with `decimals` (for display).
|
||||
pub fn format_length(mm: f64, units: &str, decimals: u8) -> String {
|
||||
let (k, u) = match units {
|
||||
/// Units per millimetre and the unit's name.
|
||||
pub fn unit_scale(units: &str) -> (f64, &'static str) {
|
||||
match units {
|
||||
"cm" => (0.1, "cm"),
|
||||
"m" => (0.001, "m"),
|
||||
"in" => (1.0 / 25.4, "in"),
|
||||
"ft" => (1.0 / 304.8, "ft"),
|
||||
_ => (1.0, "mm"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// A length, area (`power` 2) or volume (3) given in millimetres, shown in the design's units
|
||||
/// with the length precision.
|
||||
pub fn show_mm(app: &SolveApp, v: f64, power: i32) -> String {
|
||||
let (k, u) = unit_scale(&app.session.doc.units);
|
||||
let sup = match power {
|
||||
2 => "²",
|
||||
3 => "³",
|
||||
_ => "",
|
||||
};
|
||||
format!("{:.*} {u}{sup}", usize::from(app.preferences.length_decimals.min(8)), v * k.powi(power))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Write the anti-aliasing setting where the host reads it at start (the window's samples can't
|
||||
/// change while it is open).
|
||||
fn save_graphics(app: &SolveApp) {
|
||||
@@ -201,8 +237,10 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Recovery copy every");
|
||||
ui.add(egui::DragValue::new(&mut app.preferences.autosave_minutes).range(0..=120).suffix(" min"));
|
||||
ui.label("Autosave every");
|
||||
if ui.add(egui::DragValue::new(&mut app.autosave_minutes).range(0.0..=120.0).speed(0.5).suffix(" min")).changed() {
|
||||
set_autosave(app);
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
ui.label(RichText::new("0 turns it off").size(11.0).color(t.text_dim));
|
||||
|
||||
Reference in new issue
Block a user