diff --git a/src/app/document.rs b/src/app/document.rs index 6f42c0660..24e43f196 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -33,7 +33,7 @@ pub(super) enum DynComponent { Z, /// Linear distance from the last point. Distance, - /// Angle from the last point, in degrees. + /// Angle from the last point, displayed and entered in the drawing's units. Angle, /// A scalar the command reads from the command line (a count, a radius, /// a delta). Typed-only — it has no geometric live value derived from diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index 2e9161ac0..d03d1c2d3 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -20,6 +20,10 @@ use acadrust::{EntityType as AcadEntityType, Handle}; use iced::time::Instant; use iced::{mouse, Point, Task}; +fn eval_dynamic_angle(text: &str) -> Option { + let value = crate::app::expr_eval::eval_number(&text.trim().replace(',', "."))?; + crate::entities::common::parse_angle(&value.to_string()) +} impl OpenCADStudio { pub(in crate::app) fn active_distance_ray( @@ -402,9 +406,9 @@ impl OpenCADStudio { let explicit_sign = raw .as_deref() .is_some_and(|s| s.starts_with('-') || s.starts_with('+')); - match raw.and_then(|s| crate::app::expr_eval::eval_number(&s)) { - Some(v) if explicit_sign => v.to_radians(), - Some(mag) => mag.abs().to_radians().copysign(dy), + match raw.as_deref().and_then(eval_dynamic_angle) { + Some(v) if explicit_sign => v, + Some(mag) => mag.abs().copysign(dy), None => live_a, } }; @@ -474,7 +478,11 @@ impl OpenCADStudio { // absolute CCW angle in the UCS plane, not a cursor-signed // magnitude — keep it literal. Only the polar Distance+Angle // pair uses the cursor-signed `angle_rad`. - let a = val(0, live_a.to_degrees()).to_radians(); + let a = fields[0] + .buffer + .as_deref() + .and_then(eval_dynamic_angle) + .unwrap_or(live_a); Some(rel(glam::DVec3::new(live_d * a.cos(), live_d * a.sin(), 0.0))) } _ => None, @@ -794,3 +802,33 @@ impl OpenCADStudio { self.tabs[i].scene.set_preview_wires(previews); } } + +#[cfg(test)] +mod tests { + use super::eval_dynamic_angle; + use crate::entities::common::{set_unit_context, unit_context}; + + #[test] + fn dynamic_angle_expressions_follow_drawing_units() { + let previous = unit_context(); + let mut context = previous; + + context.aunits = 2; + set_unit_context(context); + assert!( + (eval_dynamic_angle("2 * 50").unwrap() - std::f64::consts::FRAC_PI_2).abs() + < 1e-12 + ); + + context.aunits = 3; + set_unit_context(context); + assert!( + (eval_dynamic_angle("1.5707963267948966").unwrap() + - std::f64::consts::FRAC_PI_2) + .abs() + < 1e-12 + ); + + set_unit_context(previous); + } +} diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 86d4f5bbf..06cbe15d2 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1093,7 +1093,7 @@ bg={bg_ms:.1}ms n={view_count}" if f.component == DynComponent::Distance { crate::entities::common::format_length(lv) } else { - lv.to_string() + format!("{lv:.4}") } } _ => dyn_component_value( diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index c1851af62..ecf09bdba 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -677,11 +677,11 @@ impl CadCommand for ArcSCACommand { } fn on_text_input(&mut self, text: &str) -> Option { if self.step == 2 { - let mut span: f64 = text.trim().replace(',', ".").parse().ok()?; + let mut span = crate::entities::common::parse_typed_angle(text)?; if self.cw { span = -span; } - let ea = self.sa + span.to_radians(); + let ea = self.sa + span; return Some(if span < 0.0 { arc_result(self.c, self.r, ea, self.sa, self.plane) } else { @@ -939,7 +939,7 @@ impl CadCommand for ArcSEACommand { if self.step != 2 { return None; } - let mut included = text.trim().replace(',', ".").parse::().ok()?.to_radians(); + let mut included = crate::entities::common::parse_typed_angle(text)?; if self.ctrl { included = -included; } @@ -1166,7 +1166,7 @@ impl CadCommand for ArcSEDCommand { if self.step != 2 { return None; } - let angle = text.trim().replace(',', ".").parse::().ok()?.to_radians(); + let angle = crate::entities::common::parse_typed_angle(text)?; let tangent = self.plane.x * angle.cos() + self.plane.y * angle.sin(); let (center, radius, sa, ea) = arc_continue(self.s, tangent, self.e, self.ctrl, self.plane)?; @@ -1285,11 +1285,11 @@ impl CadCommand for ArcCSACommand { } fn on_text_input(&mut self, text: &str) -> Option { if self.step == 2 { - let mut span: f64 = text.trim().replace(',', ".").parse().ok()?; + let mut span = crate::entities::common::parse_typed_angle(text)?; if self.cw { span = -span; } - let ea = self.sa + span.to_radians(); + let ea = self.sa + span; return Some(if span < 0.0 { arc_result(self.c, self.r, ea, self.sa, self.plane) } else { diff --git a/src/modules/draw/draw/ellipse.rs b/src/modules/draw/draw/ellipse.rs index db8af61c4..dbd2ebeba 100644 --- a/src/modules/draw/draw/ellipse.rs +++ b/src/modules/draw/draw/ellipse.rs @@ -527,7 +527,15 @@ impl CadCommand for EllipseArcCommand { } fn on_text_input(&mut self, text: &str) -> Option { - let val = parse_num(text)?; + let angle_step = matches!( + &self.step, + ArcStep::StartAngle { .. } | ArcStep::EndAngle { .. } + ); + let val = if angle_step { + crate::entities::common::parse_typed_angle(text)? + } else { + parse_num(text)? + }; match &self.step { ArcStep::MinorRatio { center, major } => { if val > 0.0 { @@ -546,7 +554,7 @@ impl CadCommand for EllipseArcCommand { major, ratio, } => { - let t_start = val.to_radians(); + let t_start = val; let (c, m, r) = (*center, *major, *ratio); self.prev_pt = None; self.step = ArcStep::EndAngle { @@ -563,8 +571,8 @@ impl CadCommand for EllipseArcCommand { ratio, t_start, } => { - // Typed degrees: positive = CCW, negative = CW. - let t_end = val.to_radians(); + // Positive is CCW; negative is CW. + let t_end = val; return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( *center, *major, *ratio, *t_start, t_end, self.plane, )))); diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index e045cfa80..6a931b707 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -338,8 +338,9 @@ impl CadCommand for XLineCommand { if self.value_origin.is_some() { return None; } self.mode = XLineMode::AngleReference; } else { - let angle: f64 = key.parse().ok().filter(|v: &f64| v.is_finite())?; - let (sin, cos) = angle.to_radians().sin_cos(); + let angle = crate::entities::common::parse_typed_angle(&key) + .filter(|angle| angle.is_finite())?; + let (sin, cos) = angle.sin_cos(); let axis = self.reference.map_or(self.plane.x, |(_, dir)| dir); self.mode = XLineMode::Direction(axis * cos + self.plane.z.cross(axis) * sin); } diff --git a/src/modules/draw/modify/lengthen.rs b/src/modules/draw/modify/lengthen.rs index 96948cf1c..9f54ecc5e 100644 --- a/src/modules/draw/modify/lengthen.rs +++ b/src/modules/draw/modify/lengthen.rs @@ -61,8 +61,18 @@ impl CadCommand for LengthenCommand { LenState::Value(ValueMode::Delta) => format!("LENGTHEN Enter delta length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().delta), LenState::Value(ValueMode::Total) => format!("LENGTHEN Enter total length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().total), LenState::Value(ValueMode::Percent) => format!("LENGTHEN Enter percentage length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().percent), - LenState::Value(ValueMode::DeltaAngle) => format!("LENGTHEN Enter delta angle <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().delta_angle), - LenState::Value(ValueMode::TotalAngle) => format!("LENGTHEN Enter total angle <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().total_angle), + LenState::Value(ValueMode::DeltaAngle) => format!( + "LENGTHEN Enter delta angle <{}>:", + crate::entities::common::format_angle( + LENGTHEN_DEFAULTS.lock().unwrap().delta_angle.to_radians(), + ) + ), + LenState::Value(ValueMode::TotalAngle) => format!( + "LENGTHEN Enter total angle <{}>:", + crate::entities::common::format_angle( + LENGTHEN_DEFAULTS.lock().unwrap().total_angle.to_radians(), + ) + ), LenState::DynamicPoint { .. } => "LENGTHEN Specify new end point:".into(), LenState::Apply(_) | LenState::DynamicPick | LenState::Value(ValueMode::Dynamic) => t!("LENGTHEN Select an object to change or [Undo]:").into_owned(), } @@ -146,7 +156,13 @@ impl CadCommand for LengthenCommand { }; return Some(CmdResult::NeedPoint); } - let Some(value) = upper.replace(',', ".").parse::().ok().filter(|v| v.is_finite()) else { + let angle_mode = matches!(mode, ValueMode::DeltaAngle | ValueMode::TotalAngle); + let parsed = if angle_mode { + crate::entities::common::parse_typed_angle(&upper).map(f64::to_degrees) + } else { + upper.replace(',', ".").parse::().ok() + }; + let Some(value) = parsed.filter(|v| v.is_finite()) else { return Some(CmdResult::NeedPoint); }; if !matches!(mode, ValueMode::Delta | ValueMode::DeltaAngle) && value <= 0.0 { return Some(CmdResult::NeedPoint); } @@ -201,7 +217,12 @@ impl CadCommand for LengthenCommand { match mode { ValueMode::Delta => defaults.delta, ValueMode::Total => defaults.total, ValueMode::Percent => defaults.percent, ValueMode::DeltaAngle => defaults.delta_angle, ValueMode::TotalAngle => defaults.total_angle, ValueMode::Dynamic => 0.0 } }; - self.on_text_input(&value.to_string()).unwrap_or(CmdResult::NeedPoint) + let text = if matches!(mode, ValueMode::DeltaAngle | ValueMode::TotalAngle) { + format!("{value}d") + } else { + value.to_string() + }; + self.on_text_input(&text).unwrap_or(CmdResult::NeedPoint) } _ => CmdResult::Cancel, } diff --git a/src/modules/draw/modify/rotate.rs b/src/modules/draw/modify/rotate.rs index 333326e80..ec2ae3b30 100644 --- a/src/modules/draw/modify/rotate.rs +++ b/src/modules/draw/modify/rotate.rs @@ -237,22 +237,22 @@ impl CadCommand for RotateCommand { } // The value already carries the correct sign when it comes // from dynamic input. - let deg: f64 = t.replace(',', ".").parse().ok()?; - Some(self.commit(center, deg.to_radians())) + let angle = crate::entities::common::parse_typed_angle(t)?; + Some(self.commit(center, angle)) } Step::RefFirst { center } => { let center = *center; - let ref_deg: f64 = t.replace(',', ".").parse().ok()?; + let ref_angle = crate::entities::common::parse_typed_angle(t)?; self.step = Step::RefNew { center, - ref_angle: ref_deg.to_radians(), + ref_angle, }; Some(CmdResult::NeedPoint) } Step::RefNew { center, ref_angle } => { let (center, ref_angle) = (*center, *ref_angle); - let new_deg: f64 = t.replace(',', ".").parse().ok()?; - Some(self.commit(center, new_deg.to_radians() - ref_angle)) + let new_angle = crate::entities::common::parse_typed_angle(t)?; + Some(self.commit(center, new_angle - ref_angle)) } Step::Center | Step::RefSecond { .. } => None, } diff --git a/src/modules/draw/modify/torient.rs b/src/modules/draw/modify/torient.rs index 34808b043..593abaaa5 100644 --- a/src/modules/draw/modify/torient.rs +++ b/src/modules/draw/modify/torient.rs @@ -151,8 +151,8 @@ impl CadCommand for TorientCommand { fn on_text_input(&mut self, text: &str) -> Option { if let Step::AngleOrFirstPoint = self.step { - let deg: f64 = text.trim().replace(',', ".").parse().ok()?; - Some(self.commit_angle(Some(deg.to_radians()))) + let angle = crate::entities::common::parse_typed_angle(text)?; + Some(self.commit_angle(Some(angle))) } else { None } diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index e9da97a99..00c642206 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -3069,11 +3069,8 @@ impl DynInputCanvas { fn box_content(b: &DynBox) -> String { match b.role { - // Live angle values already come from `format_angle`, which adds - // the drawing's appropriate marker (°, g, r, DMS, or bearing). - // A locked value is the user's raw numeric buffer and retains the - // legacy degree marker while it is being edited. - DynRole::Angle if b.locked => format!("{}\u{00B0}", b.value), + // Formatted live values already carry their unit marker; typed + // buffers stay unadorned while they are being edited. DynRole::Angle => b.value.clone(), _ if b.label.is_empty() => b.value.clone(), _ => format!("{}{}", b.label, b.value),