Properties: a rotation shows and is typed as the drawing measures directions (#1275)

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 <noreply@anthropic.com>
This commit is contained in:
Coast
2026-10-19 02:31:29 -04:00
committed by GitHub
co-authored by Cody Vogt Claude Opus 5.5
parent e635248b1e
commit c400d6d48c
3 changed files with 53 additions and 6 deletions
+30
View File
@@ -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");
+12 -3
View File
@@ -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<Value>, bad: &mut Option<String>, ids: &Vec<String>| {
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);
+11 -3
View File
@@ -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<String> = 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 {