forked from 1b-agent/opencadstudio-upstream
fix: align dynamic angle input with drawing units
This commit is contained in:
1 parent
58405025ee
commit
65c0fe5494
10 files changed
+100
-35
No files matched your search
+1
-1
@@ -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
|
||||
|
||||
@@ -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<f64> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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(
|
||||
|
||||
@@ -677,11 +677,11 @@ impl CadCommand for ArcSCACommand {
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
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::<f64>().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::<f64>().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<CmdResult> {
|
||||
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 {
|
||||
|
||||
@@ -527,7 +527,15 @@ impl CadCommand for EllipseArcCommand {
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
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,
|
||||
))));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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::<f64>().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::<f64>().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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -151,8 +151,8 @@ impl CadCommand for TorientCommand {
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
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
|
||||
}
|
||||
|
||||
+2
-5
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user