fix(props): angle rows format via AUNITS, not LUNITS (#297)

Every angle row went through the linear formatter, so switching LUNITS
to Architectural showed a block's Rotation as feet-and-inches — and the
string wouldn't parse back, wedging the field until it was cleared and
retyped. Only the decimal/fractional LUNITS survived by accident.

New edit_angle row helper formats angles with format_angle
(AUNITS/AUPREC: decimal degrees, DMS, grads or radians), applied to all
17 angle rows (rotations, obliquing, arc start/end, hatch pattern
angle, multileader text rotation). parse_angle_deg converts every
AUNITS display form back to degrees ("30°"/"30d", DMS 30°15'20",
33.33g, 0.52r) and parse_f64 falls back to it, so the displayed value
round-trips through an edit.

Closes #297

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-22 16:42:14 +03:00
1 parent 51f07f1208
commit b998082c82
11 files changed
+87 -29

No files matched your search

+60 -2
View File
@@ -94,7 +94,6 @@ pub fn format_length(value: f64) -> String {
}
/// Format an angle (input in radians) using AUNITS / AUPREC.
#[allow(dead_code)]
pub fn format_angle(value_rad: f64) -> String {
let ctx = unit_context();
let prec = ctx.auprec.max(0) as usize;
@@ -184,6 +183,18 @@ pub fn triangle_grip(id: usize, world: glam::DVec3) -> GripDef {
}
}
/// Editable ANGLE row: displays via AUNITS/AUPREC. Angle rows used the
/// LINEAR formatter, so LUNITS=Architectural showed a block rotation as
/// feet-and-inches and the string wouldn't parse back (#297). Value in
/// DEGREES, matching every angle call site.
pub fn edit_angle_prop(label: &'static str, field: &'static str, value_deg: f64) -> Property {
Property {
label: label.into(),
field,
value: PropValue::EditText(format_angle(value_deg.to_radians())),
}
}
pub fn edit_prop(label: &'static str, field: &'static str, value: f64) -> Property {
Property {
label: label.into(),
@@ -230,7 +241,54 @@ pub fn stepper_prop(
}
pub fn parse_f64(value: &str) -> Option<f64> {
value.trim().parse::<f64>().ok()
let t = value.trim();
// Angle rows display via AUNITS (#297) — accept those formats back.
t.parse::<f64>().ok().or_else(|| parse_angle_deg(t))
}
/// Parse an angle string the panel displayed via AUNITS back to DEGREES:
/// "30", "30°"/"30d", DMS "30°15'20.5\"", grads "33.33g", radians "0.52r".
pub fn parse_angle_deg(value: &str) -> Option<f64> {
let s = value.trim();
if s.is_empty() {
return None;
}
if let Ok(v) = s.parse::<f64>() {
return Some(v);
}
let lower = s.to_ascii_lowercase();
if let Some(num) = lower.strip_suffix('g') {
return num.trim().parse::<f64>().ok().map(|g| g * 0.9);
}
if let Some(num) = lower.strip_suffix('r') {
return num.trim().parse::<f64>().ok().map(f64::to_degrees);
}
// Degrees with optional DMS parts: [-]D(°|d)[M'[S"]]
let mut rest = s;
let neg = rest.starts_with('-');
if neg {
rest = &rest[1..];
}
let i = rest.find(['°', 'd', 'D'])?;
let d: f64 = rest[..i].trim().parse().ok()?;
let mut total = d;
let tail = rest[i..]
.trim_start_matches(['°', 'd', 'D'])
.trim();
if !tail.is_empty() {
let (mpart, spart) = match tail.find('\'') {
Some(j) => (&tail[..j], &tail[j + 1..]),
None => (tail, ""),
};
if !mpart.trim().is_empty() {
total += mpart.trim().parse::<f64>().ok()? / 60.0;
}
let sp = spart.trim().trim_end_matches('"').trim();
if !sp.is_empty() {
total += sp.parse::<f64>().ok()? / 3600.0;
}
}
Some(if neg { -total } else { total })
}
/// Bulge → arc geometry for a polyline segment.