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 <[email protected]>
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

+3 -3
View File
@@ -3,7 +3,7 @@ use truck_modeling::{builder, Point3};
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
};
use crate::entities::traits::TruckConvertible;
use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject};
@@ -164,8 +164,8 @@ fn properties(arc: &Arc) -> Vec<PropSection> {
ro("End Y", "end_y", format!("{ey:.4}")),
ro("End Z", "end_z", format!("{ez:.4}")),
edit("Radius", "radius", arc.radius),
edit("Start angle", "start_angle", sa.to_degrees()),
edit("End angle", "end_angle", ea.to_degrees()),
edit_angle("Start angle", "start_angle", sa.to_degrees()),
edit_angle("End angle", "end_angle", ea.to_degrees()),
ro("Total angle", "total_angle", format!("{total_angle:.2}")),
ro("Arc length", "arc_length", format!("{arc_length:.4}")),
ro("Area", "area", format!("{area:.4}")),
+5 -5
View File
@@ -5,7 +5,7 @@ use acadrust::entities::{AttributeDefinition, AttributeEntity};
use acadrust::types::Vector3;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
use crate::entities::common::{edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
use crate::entities::text_support::{
layout_mtext, resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
@@ -468,9 +468,9 @@ impl PropertyEditable for AttributeDefinition {
},
},
edit("Height", "att_h", self.height),
edit("Rotation", "att_rot", self.rotation.to_degrees()),
edit_angle("Rotation", "att_rot", self.rotation.to_degrees()),
edit("Width factor", "att_wf", self.width_factor),
edit("Obliquing", "att_ob", self.oblique_angle.to_degrees()),
edit_angle("Obliquing", "att_ob", self.oblique_angle.to_degrees()),
edit("Text alignment X", "att_ax", self.alignment_point.x),
edit("Text alignment Y", "att_ay", self.alignment_point.y),
edit("Text alignment Z", "att_az", self.alignment_point.z),
@@ -739,9 +739,9 @@ impl PropertyEditable for AttributeEntity {
bool_yn(self.flags.annotative),
),
edit("Height", "atte_h", self.height),
edit("Rotation", "atte_rot", self.rotation.to_degrees()),
edit_angle("Rotation", "atte_rot", self.rotation.to_degrees()),
edit("Width factor", "atte_wf", self.width_factor),
edit("Obliquing", "atte_ob", self.oblique_angle.to_degrees()),
edit_angle("Obliquing", "atte_ob", self.oblique_angle.to_degrees()),
edit("Text alignment X", "atte_ax", self.alignment_point.x),
edit("Text alignment Y", "atte_ay", self.alignment_point.y),
edit("Text alignment Z", "atte_az", self.alignment_point.z),
+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.
+2 -2
View File
@@ -7,7 +7,7 @@ use glam::{DVec3, Vec3};
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
@@ -78,7 +78,7 @@ fn properties(dim: &Dimension) -> Vec<PropSection> {
d.second_point,
d.definition_point,
));
props.push(edit("Rotation (deg)", "rotation", d.rotation.to_degrees()));
props.push(edit_angle("Rotation", "rotation", d.rotation.to_degrees()));
props.push(edit(
"Ext Rotation (deg)",
"ext_line_rotation",
+3 -3
View File
@@ -2,7 +2,7 @@ use acadrust::entities::{BoundaryEdge, Hatch};
use glam::Vec3;
use crate::command::EntityTransform;
use crate::entities::common::{center_grip, circle_grip, edit_prop as edit, parse_f64, ro_prop as ro};
use crate::entities::common::{center_grip, circle_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro};
use crate::entities::traits::{FallbackTess, Grippable, PropertyEditable, Transformable};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::convert::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, FallbackGeometry};
@@ -250,7 +250,7 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
field: "gradient_color_2",
value: PropValue::ColorChoice(grad_c2),
},
edit("Angle", "pattern_angle", g.angle.to_degrees()),
edit_angle("Angle", "pattern_angle", g.angle.to_degrees()),
ro("Centered", "gradient_centered", centered),
],
},
@@ -359,7 +359,7 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
type_row,
pattern_name_row,
ro("Annotative", "annotative", String::new()),
edit("Angle", "pattern_angle", h.pattern_angle.to_degrees()),
edit_angle("Angle", "pattern_angle", h.pattern_angle.to_degrees()),
edit("Scale", "pattern_scale", h.pattern_scale),
edit("Origin X", "origin_x", origin_x),
edit("Origin Y", "origin_y", origin_y),
+2 -2
View File
@@ -4,7 +4,7 @@ use acadrust::{EntityType, Handle};
use glam::Vec3;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
use crate::entities::common::{edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::WireModel;
@@ -53,7 +53,7 @@ fn properties(ins: &Insert) -> Vec<PropSection> {
title: "Misc".into(),
props: vec![
ro("Name", "block", ins.block_name.clone()),
edit("Rotation", "rotation", ins.rotation.to_degrees()),
edit_angle("Rotation", "rotation", ins.rotation.to_degrees()),
ro(
"Annotative",
"annotative",
+2 -2
View File
@@ -2,7 +2,7 @@ use acadrust::entities::{AttachmentPoint, DrawingDirection, MText};
use crate::command::EntityTransform;
use crate::entities::common::{
edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip,
edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip,
};
use crate::entities::text_support::{
layout_mtext, resolve_text_style, GlyphBox, MTextColumns, MTextRenderOpts, MTextVAnchor,
@@ -275,7 +275,7 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
},
},
edit("Text height", "height", t.height),
edit("Rotation", "rotation", t.rotation.to_degrees()),
edit_angle("Rotation", "rotation", t.rotation.to_degrees()),
edit("Line space factor", "line_spacing", t.line_spacing_factor),
edit("Line space distance", "line_space_distance", line_space_distance),
Property {
+2 -2
View File
@@ -31,7 +31,7 @@ use glam::DVec3;
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip,
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip,
};
use crate::entities::traits::TruckConvertible;
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
@@ -663,7 +663,7 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
),
edit("Width", "text_width", ctx.text_width),
edit("Height", "text_height", ml.text_height),
edit("Rotation", "text_rotation", ctx.text_rotation.to_degrees()),
edit_angle("Rotation", "text_rotation", ctx.text_rotation.to_degrees()),
edit("Line space factor", "line_spacing", ctx.line_spacing_factor),
edit(
"Line space distance",
+3 -3
View File
@@ -8,7 +8,7 @@
use acadrust::entities::Shape;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::common::{edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, Property, PropValue};
@@ -149,9 +149,9 @@ impl PropertyEditable for Shape {
edit("Insert Y", "shp_iy", self.insertion_point.y),
edit("Insert Z", "shp_iz", self.insertion_point.z),
edit("Size", "shp_sz", self.size),
edit("Rotation", "shp_rot", self.rotation.to_degrees()),
edit_angle("Rotation", "shp_rot", self.rotation.to_degrees()),
edit("Width Factor", "shp_xs", self.relative_x_scale),
edit("Oblique Angle", "shp_ob", self.oblique_angle.to_degrees()),
edit_angle("Oblique Angle", "shp_ob", self.oblique_angle.to_degrees()),
edit("Normal X", "shp_nx", self.normal.x),
edit("Normal Y", "shp_ny", self.normal.y),
edit("Normal Z", "shp_nz", self.normal.z),
+3 -3
View File
@@ -2,7 +2,7 @@ use acadrust::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignm
use crate::command::EntityTransform;
use crate::entities::common::{
edit_prop as edit, num_prop as num_row, parse_f64, ro_prop as ro, square_grip,
edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, parse_f64, ro_prop as ro, square_grip,
};
use crate::entities::text_support::{
resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
@@ -325,9 +325,9 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
},
},
edit("Height", "height", t.height),
edit("Rotation", "rotation", t.rotation.to_degrees()),
edit_angle("Rotation", "rotation", t.rotation.to_degrees()),
edit("Width factor", "width_factor", t.width_factor),
edit("Obliquing", "oblique_angle", t.oblique_angle.to_degrees()),
edit_angle("Obliquing", "oblique_angle", t.oblique_angle.to_degrees()),
num_row("Text alignment X", "align_x", ax, align_editable),
num_row("Text alignment Y", "align_y", ay, align_editable),
num_row("Text alignment Z", "align_z", az, align_editable),
+2 -2
View File
@@ -7,7 +7,7 @@
use acadrust::entities::{Underlay, UnderlayDisplayFlags};
use crate::command::EntityTransform;
use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::common::{center_grip, edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
@@ -189,7 +189,7 @@ impl PropertyEditable for Underlay {
edit("Scale Z", "ul_sz", self.z_scale),
ro("Width", "ul_width", format!("{:.4}", width)),
ro("Height", "ul_height", format!("{:.4}", height)),
edit("Rotation", "ul_rot", self.rotation.to_degrees()),
edit_angle("Rotation", "ul_rot", self.rotation.to_degrees()),
],
},
PropSection {