From c400d6d48c57635a125de9c9e8dc719d59cd5f52 Mon Sep 17 00:00:00 2001 From: Coast Date: Mon, 19 Oct 2026 00:31:29 -0600 Subject: [PATCH] Properties: a rotation shows and is typed as the drawing measures directions (#1275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AutoCAD's Properties shows a block reference's or text's rotation as it shows any direction: from ANGBASE, clockwise when ANGDIR is 1 (a block turned 2.23° shows as 87.77 in a drawing measured clockwise from north). CADCraft showed the raw angle from +X. Properties and Quick Properties now show it that way and read a typed rotation back the same way (`AngleSettings::shown`, `unshown`); properties.set still takes the angle from +X counterclockwise. Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- crates/engine/src/units.rs | 30 ++++++++++++++++++++++++++++++ crates/ui-egui/src/palettes.rs | 15 ++++++++++++--- crates/ui-egui/src/quick.rs | 14 +++++++++++--- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/units.rs b/crates/engine/src/units.rs index b1b6a152a..073a6948c 100644 --- a/crates/engine/src/units.rs +++ b/crates/engine/src/units.rs @@ -200,6 +200,20 @@ impl AngleSettings { let v = self.angbase + if self.clockwise { -a } else { a }; v.is_finite().then_some(v) } + + /// Direction `dir` (radians counterclockwise from +X) as the drawing shows directions, in + /// radians: from ANGBASE in the ANGDIR sense, 0 up to a full turn. (A block turned 2.23° + /// shows as 87.77 in a drawing measured clockwise from north, as in AutoCAD's Properties.) + pub fn shown(&self, dir: f64) -> f64 { + let a = if self.clockwise { self.angbase - dir } else { dir - self.angbase }; + a.rem_euclid(std::f64::consts::TAU) + } + + /// The direction (radians counterclockwise from +X) the drawing shows as `v` (radians): the + /// inverse of [`AngleSettings::shown`]. + pub fn unshown(&self, v: f64) -> f64 { + (self.angbase + if self.clockwise { -v } else { v }).rem_euclid(std::f64::consts::TAU) + } } /// Parse a direction typed with the default angle settings (degrees, 0 = +X, counterclockwise): @@ -289,6 +303,22 @@ pub const DEG: f64 = PI / 180.0; mod tests { use super::*; + #[test] + fn directions_show_from_angbase_in_the_angdir_sense() { + let north_clockwise = AngleSettings { aunits: 0, angbase: 90f64.to_radians(), clockwise: true }; + // AutoCAD shows a block turned 2.22729505553431° as 87.77 in such a drawing. + let shown = north_clockwise.shown(2.22729505553431f64.to_radians()).to_degrees(); + assert!((shown - 87.77270494446569).abs() < 1e-9, "{shown}"); + let back = north_clockwise.unshown(87.77270494446569f64.to_radians()).to_degrees(); + assert!((back - 2.22729505553431).abs() < 1e-9, "{back}"); + // The defaults show directions as they are. + let plain = AngleSettings::default(); + assert!((plain.shown(0.5) - 0.5).abs() < 1e-12 && (plain.unshown(0.5) - 0.5).abs() < 1e-12); + // Counterclockwise from a base of 30°: 45° shows as 15. + let based = AngleSettings { aunits: 0, angbase: 30f64.to_radians(), clockwise: false }; + assert!((based.shown(45f64.to_radians()).to_degrees() - 15.0).abs() < 1e-9); + } + #[test] fn decimal_and_arch() { assert_eq!(format_distance(28.98524, 2, 4), "28.9852"); diff --git a/crates/ui-egui/src/palettes.rs b/crates/ui-egui/src/palettes.rs index ca119e592..ef34b9fd6 100644 --- a/crates/ui-egui/src/palettes.rs +++ b/crates/ui-egui/src/palettes.rs @@ -968,6 +968,8 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { if sel.len() == 1 { group(ui, "Geometry"); let real = |v: f64| format!("{v:.4}"); + // Rotations show as the drawing measures directions (ANGBASE, ANGDIR). + let angles = app.session.angle_settings(); let num = |ui: &mut egui::Ui, label: &str, key: &str, shown: String, set: &mut Option, bad: &mut Option, ids: &Vec| { prop_row(ui, label, |ui| { if let Some(f) = number(edit_field(ui, ui.id().with((key, ids)), shown), bad) { @@ -1034,7 +1036,7 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { } }); num(ui, "Height", "height", len(tx.height), &mut set, &mut bad, &ids); - num(ui, "Rotation", "rotation", real(tx.rotation.to_degrees()), &mut set, &mut bad, &ids); + num(ui, "Rotation", "rotation", real(angles.shown(tx.rotation).to_degrees()), &mut set, &mut bad, &ids); num(ui, "Width factor", "widthFactor", real(tx.width_factor), &mut set, &mut bad, &ids); } EntityKind::MText(tx) => { @@ -1094,7 +1096,7 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { prop_row(ui, "Position X", |ui| read_only(ui, &len(i.insert.x))); prop_row(ui, "Position Y", |ui| read_only(ui, &len(i.insert.y))); prop_row(ui, "Position Z", |ui| read_only(ui, &len(i.insert.z))); - num(ui, "Rotation", "rotation", real(i.rotation.to_degrees()), &mut set, &mut bad, &ids); + num(ui, "Rotation", "rotation", real(angles.shown(i.rotation).to_degrees()), &mut set, &mut bad, &ids); num(ui, "Scale", "scale", real(i.scale.x), &mut set, &mut bad, &ids); if let Some((_, props)) = custom.filter(|(_, p)| !p.is_empty()) { group(ui, "Custom"); @@ -1118,7 +1120,14 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { _ => {} } } - if let Some(p) = set { + if let Some(mut p) = set { + // A rotation is typed as shown (from ANGBASE, in the ANGDIR sense); properties.set takes it + // from +X counterclockwise. + if let Some(r) = p.get_mut("rotation") + && let Some(v) = r.as_f64() + { + *r = json!(app.session.angle_settings().unshown(v.to_radians()).to_degrees()); + } let _ = app.run("properties.set", p); } report_bad(app, bad); diff --git a/crates/ui-egui/src/quick.rs b/crates/ui-egui/src/quick.rs index afad70734..5effd59cf 100644 --- a/crates/ui-egui/src/quick.rs +++ b/crates/ui-egui/src/quick.rs @@ -295,6 +295,8 @@ pub fn quick_properties(app: &mut CadApp, ctx: &egui::Context) { let kind = if ents.len() == 1 { Some(first.kind.clone()) } else { None }; let ids: Vec = sel.iter().map(|h| h.hex()).collect(); let mut set: Option<(String, Value)> = None; + // Rotations show as the drawing measures directions (ANGBASE, ANGDIR). + let angles = app.session.angle_settings(); let mut close = false; egui::Window::new(title) .id(egui::Id::new(("quick_properties", &st.sel))) @@ -373,7 +375,7 @@ pub fn quick_properties(app: &mut CadApp, ctx: &egui::Context) { } ui.end_row(); num_row(ui, "Height", id("h"), tx.height, &mut set, "height"); - num_row(ui, "Rotation", id("rot"), tx.rotation.to_degrees(), &mut set, "rotation"); + num_row(ui, "Rotation", id("rot"), angles.shown(tx.rotation).to_degrees(), &mut set, "rotation"); } Some(EntityKind::MText(tx)) => { ui.label(RichText::new(crate::tl!("Contents")).color(Tokens::get().text_dim)); @@ -385,7 +387,7 @@ pub fn quick_properties(app: &mut CadApp, ctx: &egui::Context) { } Some(EntityKind::Insert(i)) => { ro_row(ui, "Name", i.block.clone()); - num_row(ui, "Rotation", id("rot"), i.rotation.to_degrees(), &mut set, "rotation"); + num_row(ui, "Rotation", id("rot"), angles.shown(i.rotation).to_degrees(), &mut set, "rotation"); num_row(ui, "Scale", id("sc"), i.scale.x, &mut set, "scale"); } Some(EntityKind::Dimension(dm)) => { @@ -407,7 +409,13 @@ pub fn quick_properties(app: &mut CadApp, ctx: &egui::Context) { }); }); ctx.data_mut(|dd| dd.insert_temp(sid, st)); - if let Some((k, v)) = set { + if let Some((k, mut v)) = set { + // A rotation is typed as shown; properties.set takes it from +X counterclockwise. + if k == "rotation" + && let Some(f) = v.as_f64() + { + v = json!(angles.unshown(f.to_radians()).to_degrees()); + } let _ = app.run("properties.set", json!({ "handles": ids, k: v })); } if close {