From 6cbec232bbcc3f8c12d5c93f420ba39f365519de Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 7 Aug 2026 23:46:49 +0300 Subject: [PATCH] feat(units): read back every form the drawing writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The readout learned to write lengths as 5'-6 1/2" and angles as N 45d0'0" E, but typed input was still a bare parse::() — a coordinate could be displayed and not entered. Give the formatters their inverses. parse_length takes feet and inches separated by a dash, a space, or nothing, with the closing quote optional, and whole-plus-fraction in any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading minus is a sign; the one inside 5'-9" is a separator, since after feet there is nothing left to subtract from. parse_angle reads the mark that says which convention an angle is in — g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass letters for a surveyor's bearing — and reads a bare number in whichever convention the drawing is set to. ANGBASE and ANGDIR now apply, but only where they mean something. Which way a thing points is measured from the drawing's zero and runs the way it says; how wide an arc opens is the same number whatever zero is counted from. format_direction and parse_direction are the pair for directions, and the polar readout and polar input both go through them, so the bearing shown is the bearing accepted. format_angle stays for sizes, where applying a base angle would corrupt the value. The command prompts follow the same rule, each classified rather than swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths; the four dimension text-rotation prompts, ARRAY's total angle and INSERT's rotation read angles. MLINE's scale and INSERT's scale factor are left alone — a multiplier has no unit to be written in. INSERT read both through one parse; they are now separate. The typed variants also take a decimal comma, which a single-value prompt has no other use for. parse_length itself does not: inside a coordinate the comma separates the axes. Co-Authored-By: Claude Opus 5 --- src/app/helpers.rs | 31 ++-- src/app/properties.rs | 9 +- src/app/view/mod.rs | 9 +- src/entities/common.rs | 222 +++++++++++++++++++++++++++ src/modules/annotate/aligned_dim.rs | 2 +- src/modules/annotate/diameter_dim.rs | 2 +- src/modules/annotate/linear_dim.rs | 2 +- src/modules/annotate/radius_dim.rs | 2 +- src/modules/draw/draw/helix.rs | 2 +- src/modules/draw/draw/trace.rs | 2 +- src/modules/draw/modify/array.rs | 11 +- src/modules/draw/modify/fillet.rs | 10 +- src/modules/draw/modify/offset.rs | 4 +- src/modules/insert/insert_block.rs | 24 ++- src/modules/insert/minsert.rs | 4 +- src/modules/insert/solid3d_cmds.rs | 4 +- src/ui/statusbar/mod.rs | 9 +- src/ui/window/drawing_units.rs | 7 + 18 files changed, 298 insertions(+), 58 deletions(-) diff --git a/src/app/helpers.rs b/src/app/helpers.rs index c273b5230..0c010b5f9 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -20,7 +20,13 @@ pub(crate) enum CoordKind { /// `distance Option<(glam::DVec3, CoordKind)> { + use crate::entities::common::{parse_direction, parse_length}; let trimmed = text.trim(); let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') { (CoordKind::Relative, r) @@ -29,12 +35,13 @@ pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> { } else { (CoordKind::Default, trimmed) }; - let number = |value: &str| value.trim().parse::().ok(); if let Some((distance, angles)) = rest.split_once('<') { - let distance = number(distance)?; + let distance = parse_length(distance)?; if let Some((azimuth, elevation)) = angles.split_once('<') { - let azimuth = number(azimuth)?.to_radians(); - let elevation = number(elevation)?.to_radians(); + let azimuth = parse_direction(azimuth)?; + // The rise out of the plane is a size, not a compass direction, so + // it is not counted from the drawing's zero. + let elevation = crate::entities::common::parse_angle(elevation)?; let horizontal = distance * elevation.cos(); return Some(( glam::DVec3::new( @@ -45,13 +52,12 @@ pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> { kind, )); } - let angle_and_z: Vec<&str> = angles.split(|c| c == ',' || c == ';').collect(); - let (angle, z) = match angle_and_z.as_slice() { - [angle] => (number(angle)?, 0.0), - [angle, z] => (number(angle)?, number(z)?), - _ => return None, + // A cylindrical `distance (parse_direction(angle)?, parse_length(z)?), + None => (parse_direction(angles)?, 0.0), }; - let angle = angle.to_radians(); return Some(( glam::DVec3::new(distance * angle.cos(), distance * angle.sin(), z), kind, @@ -59,9 +65,8 @@ pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> { } let parts: Vec = rest .split(|c| c == ',' || c == ';') - .map(|s| s.trim()) - .filter_map(|s| s.parse().ok()) - .collect(); + .map(parse_length) + .collect::>>()?; match parts.as_slice() { [x, y] => Some((glam::DVec3::new(*x, *y, 0.0), kind)), [x, y, z] => Some((glam::DVec3::new(*x, *y, *z), kind)), diff --git a/src/app/properties.rs b/src/app/properties.rs index 5b6405b8b..ebeda6228 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -89,12 +89,9 @@ impl OpenCADStudio { // lengths/angles per LUNITS / LUPREC / AUNITS / AUPREC. { let h = &self.tabs[i].scene.document.header; - crate::entities::common::set_unit_context(crate::entities::common::UnitContext { - lunits: h.linear_unit_format, - luprec: h.linear_unit_precision, - aunits: h.angular_unit_format, - auprec: h.angular_unit_precision, - }); + crate::entities::common::set_unit_context( + crate::entities::common::UnitContext::from_header(h), + ); } let layer_names: Vec = self.tabs[i] diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 97aab240d..20ee91047 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1664,12 +1664,9 @@ impl OpenCADStudio { // every refresh; the status bar redraws on its own schedule // and cannot rely on that having happened. crate::entities::common::set_unit_context( - crate::entities::common::UnitContext { - lunits: tab.scene.document.header.linear_unit_format, - luprec: tab.scene.document.header.linear_unit_precision, - aunits: tab.scene.document.header.angular_unit_format, - auprec: tab.scene.document.header.angular_unit_precision, - }, + crate::entities::common::UnitContext::from_header( + &tab.scene.document.header, + ), ); let status_menu_data = crate::ui::statusbar::StatusMenuData { layout_names: layout_names.clone(), diff --git a/src/entities/common.rs b/src/entities/common.rs index f0933e33c..6d1315ceb 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -19,6 +19,27 @@ pub struct UnitContext { /// AUPREC — decimal places (angular) #[allow(dead_code)] pub auprec: i16, + /// ANGBASE — the world direction that a written angle of zero points in, + /// in radians. Applies to directions, never to angular sizes. + pub angbase: f64, + /// ANGDIR — true when written angles grow clockwise. + pub angdir_cw: bool, +} + +impl UnitContext { + /// The settings as this drawing holds them. Every place that formats or + /// reads a number seeds the context from here, so none of them can be + /// working from a different idea of the drawing's conventions. + pub fn from_header(header: &acadrust::document::HeaderVariables) -> Self { + Self { + lunits: header.linear_unit_format, + luprec: header.linear_unit_precision, + aunits: header.angular_unit_format, + auprec: header.angular_unit_precision, + angbase: header.angle_base, + angdir_cw: header.angle_direction != 0, + } + } } thread_local! { @@ -27,6 +48,8 @@ thread_local! { luprec: 4, aunits: 0, auprec: 0, + angbase: 0.0, + angdir_cw: false, }) }; } @@ -155,6 +178,205 @@ fn surveyor(value_rad: f64, prec: usize) -> String { format!("{pole} {} {side}", dms(bearing, prec)) } +// ── Reading numbers back ─────────────────────────────────────────────────── +// +// The inverses of the formatters above, kept beside them so the pair cannot +// drift: whatever the drawing writes, it can be handed back. Reading is the +// looser of the two — it takes the shorthands people type as well as the full +// forms — but it never accepts a form the drawing could not have produced. + +/// Read a length. Accepts plain decimals, feet-and-inches, and fractions. +/// +/// Feet and inches separate with a dash, a space, or nothing, and the closing +/// `"` is optional: `5'-9 1/2"`, `5' 9-1/2`, `5'9`, `9 1/2`, `1/2`, `60"` and +/// `5'` all read. As with the architectural and engineering formats, the +/// notation itself means one unit is one inch. +/// +/// A leading `-` is a sign, but the `-` inside `5'-9"` is a separator — after +/// feet there is nothing left to subtract from. +pub fn parse_length(text: &str) -> Option { + let text = text.trim(); + let (sign, rest) = match text.strip_prefix('-') { + Some(rest) => (-1.0, rest.trim_start()), + None => (1.0, text.strip_prefix('+').unwrap_or(text).trim_start()), + }; + if rest.is_empty() { + return None; + } + let magnitude = match rest.split_once('\'') { + Some((feet, inches)) => { + let feet: f64 = feet.trim().parse().ok()?; + let inches = inches.trim().trim_end_matches('"').trim(); + feet * 12.0 + parse_inches(inches.trim_start_matches('-').trim())? + } + None => parse_inches(rest.trim_end_matches('"').trim())?, + }; + Some(sign * magnitude) +} + +/// A count of inches: `9`, `9.5`, `1/2`, `9 1/2`, `9-1/2`, or nothing at all. +fn parse_inches(text: &str) -> Option { + if text.is_empty() { + return Some(0.0); + } + let Some(slash) = text.find('/') else { + // No fraction, so any `-` left in here belongs to an exponent + // (`3.35E-01`) rather than to a separator. + return text.parse().ok(); + }; + let denominator: f64 = text[slash + 1..].trim().parse().ok()?; + if denominator == 0.0 { + return None; + } + // The numerator is the number nearest the slash; anything before the + // separator in front of it is a whole count of inches. + let head = text[..slash].trim(); + let (whole, numerator) = match head.rfind([' ', '-']) { + Some(cut) => (head[..cut].trim(), head[cut + 1..].trim()), + None => ("", head), + }; + let numerator: f64 = numerator.parse().ok()?; + let whole: f64 = if whole.is_empty() { + 0.0 + } else { + whole.parse().ok()? + }; + Some(whole + numerator / denominator) +} + +/// Read an angle, in radians. +/// +/// A mark decides the convention — `g` grads, `r` radians, `d`/`'`/`"` +/// degrees-minutes-seconds, compass letters a surveyor's bearing. A bare +/// number is read in whatever convention the drawing is set to, so what the +/// readout shows can be typed straight back. +pub fn parse_angle(text: &str) -> Option { + let text = text.trim(); + if text.is_empty() { + return None; + } + let upper = text.to_ascii_uppercase(); + if upper.starts_with('N') || upper.starts_with('S') { + return parse_surveyor(&upper); + } + if let Some(body) = upper.strip_suffix('G') { + return Some((body.trim().parse::().ok()? * 0.9).to_radians()); + } + if let Some(body) = upper.strip_suffix('R') { + return body.trim().parse().ok(); + } + // Bare east/west read as the directions they name. + if upper == "E" { + return Some(0.0); + } + if upper == "W" { + return Some(std::f64::consts::PI); + } + if upper.contains('D') || upper.contains('°') || upper.contains('\'') { + return Some(parse_dms(&upper)?.to_radians()); + } + let value: f64 = upper.parse().ok()?; + Some(match unit_context().aunits { + 2 => (value * 0.9).to_radians(), + 3 => value, + _ => value.to_radians(), + }) +} + +/// Degrees, minutes and seconds, each part optional after the first: +/// `45`, `45d`, `45d20'`, `45d20'6"`. +fn parse_dms(upper: &str) -> Option { + let (sign, rest) = match upper.strip_prefix('-') { + Some(rest) => (-1.0, rest.trim_start()), + None => (1.0, upper), + }; + let (degrees, rest) = match rest.find(['D', '°']) { + Some(at) => ( + rest[..at].trim().parse::().ok()?, + rest[at + rest[at..].chars().next()?.len_utf8()..].trim(), + ), + None => (rest.trim().parse::().ok()?, ""), + }; + let (minutes, rest) = match rest.split_once('\'') { + Some((minutes, rest)) if !minutes.trim().is_empty() => { + (minutes.trim().parse::().ok()?, rest.trim()) + } + Some((_, rest)) => (0.0, rest.trim()), + None => (0.0, rest), + }; + let seconds = match rest.trim_end_matches('"').trim() { + "" => 0.0, + seconds => seconds.parse::().ok()?, + }; + Some(sign * (degrees + minutes / 60.0 + seconds / 3600.0)) +} + +/// A surveyor's bearing — `N45D20'6"E`, or a lone `N` / `S` for due north and +/// south. The inverse of [`surveyor`]. +fn parse_surveyor(upper: &str) -> Option { + let pole = upper.chars().next()?; + let body = upper[1..].trim(); + if body.is_empty() { + return Some(if pole == 'N' { + std::f64::consts::FRAC_PI_2 + } else { + 3.0 * std::f64::consts::FRAC_PI_2 + }); + } + let side = body.chars().last()?; + if !matches!(side, 'E' | 'W') { + return None; + } + let bearing = parse_dms(body[..body.len() - 1].trim())?; + let degrees = match (pole, side) { + ('N', 'E') => 90.0 - bearing, + ('N', _) => 90.0 + bearing, + (_, 'W') => 270.0 - bearing, + _ => 270.0 + bearing, + }; + Some(degrees.to_radians()) +} + +/// Read a length typed at a command prompt. +/// +/// Same forms as [`parse_length`], plus the comma some keyboards put where a +/// decimal point belongs. A prompt asking for one value has no other use for a +/// comma, so taking it here is safe in a way it would not be inside a +/// coordinate, where the comma separates the axes. +pub fn parse_typed_length(text: &str) -> Option { + parse_length(&text.replace(',', ".")) +} + +/// Read an angle typed at a command prompt, in radians. Same forms as +/// [`parse_angle`], with the same tolerance for a decimal comma. +pub fn parse_typed_angle(text: &str) -> Option { + parse_angle(&text.replace(',', ".")) +} + +// ── Directions ───────────────────────────────────────────────────────────── +// +// A direction is not an angular size. Which way something points is measured +// from ANGBASE and runs the way ANGDIR says, while how wide an arc opens is +// the same number whatever zero the drawing counts from. Only directions go +// through the pair below; `format_angle` and `parse_angle` stay for sizes. + +/// Write a world direction the way this drawing counts directions. +pub fn format_direction(world_rad: f64) -> String { + let ctx = unit_context(); + let relative = world_rad - ctx.angbase; + let shown = if ctx.angdir_cw { -relative } else { relative }; + format_angle(shown.rem_euclid(std::f64::consts::TAU)) +} + +/// Read a direction written the way this drawing counts them, as a world +/// angle. +pub fn parse_direction(text: &str) -> Option { + let ctx = unit_context(); + let typed = parse_angle(text)?; + let relative = if ctx.angdir_cw { -typed } else { typed }; + Some(relative + ctx.angbase) +} + /// Two interior triangles covering a quad (flat list, 6 vertices) — the /// click-anywhere pick surface for frame-like entities (image, OLE frame, /// underlay, wipeout). Corners in ring order. diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index d32c9915e..7fba969e2 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -168,7 +168,7 @@ impl CadCommand for AlignedDimensionCommand { self.text_angle = if t.is_empty() { None } else { - t.parse::().ok().map(f64::to_radians) + crate::entities::common::parse_typed_angle(t) }; self.awaiting_angle = false; return Some(CmdResult::NeedPoint); diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index e1ac05d1e..43c29c20e 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -155,7 +155,7 @@ impl CadCommand for DiameterDimensionCommand { self.text_angle = if t.is_empty() { None } else { - t.parse::().ok().map(f64::to_radians) + crate::entities::common::parse_typed_angle(t) }; self.awaiting_angle = false; return Some(CmdResult::NeedPoint); diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 286f347c5..ffc929d1b 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -171,7 +171,7 @@ impl CadCommand for LinearDimensionCommand { self.text_angle = if t.is_empty() { None } else { - t.parse::().ok().map(f64::to_radians) + crate::entities::common::parse_typed_angle(t) }; self.awaiting_angle = false; return Some(CmdResult::NeedPoint); diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index b3fbb7af7..f890a6a3f 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -157,7 +157,7 @@ impl CadCommand for RadiusDimensionCommand { self.text_angle = if t.is_empty() { None } else { - t.parse::().ok().map(f64::to_radians) + crate::entities::common::parse_typed_angle(t) }; self.awaiting_angle = false; return Some(CmdResult::NeedPoint); diff --git a/src/modules/draw/draw/helix.rs b/src/modules/draw/draw/helix.rs index e35d33c62..486124d24 100644 --- a/src/modules/draw/draw/helix.rs +++ b/src/modules/draw/draw/helix.rs @@ -108,7 +108,7 @@ impl HelixCommand { /// Parse a positive distance from typed text; returns `None` on failure. fn parse_value(text: &str) -> Option { - text.trim().parse::().ok() + crate::entities::common::parse_typed_length(text) } } diff --git a/src/modules/draw/draw/trace.rs b/src/modules/draw/draw/trace.rs index 15b10f4da..16e3f9116 100644 --- a/src/modules/draw/draw/trace.rs +++ b/src/modules/draw/draw/trace.rs @@ -152,7 +152,7 @@ impl CadCommand for TraceCommand { self.width_set = true; return Some(CmdResult::NeedPoint); } - if let Ok(w) = t.parse::() { + if let Some(w) = crate::entities::common::parse_typed_length(t) { if w > 0.0 { self.width = w; self.width_set = true; diff --git a/src/modules/draw/modify/array.rs b/src/modules/draw/modify/array.rs index 707306105..4960a2d12 100644 --- a/src/modules/draw/modify/array.rs +++ b/src/modules/draw/modify/array.rs @@ -177,7 +177,8 @@ impl CadCommand for ArrayRectCommand { let row_sp = if t.is_empty() { self.default_row_sp } else { - let v = t.parse::().unwrap_or(self.default_row_sp); + let v = crate::entities::common::parse_typed_length(t) + .unwrap_or(self.default_row_sp); defaults::set_array_row_sp(v); self.default_row_sp = v; v @@ -189,7 +190,8 @@ impl CadCommand for ArrayRectCommand { let col_sp = if t.is_empty() { self.default_col_sp } else { - let v = t.parse::().unwrap_or(self.default_col_sp); + let v = crate::entities::common::parse_typed_length(t) + .unwrap_or(self.default_col_sp); defaults::set_array_col_sp(v); v }; @@ -341,7 +343,10 @@ impl CadCommand for ArrayPolarCommand { let total_deg = if t.is_empty() { self.default_angle } else { - let v = t.parse::().unwrap_or(self.default_angle); + // Held in degrees, which is what the stored default is. + let v = crate::entities::common::parse_typed_angle(t) + .map(f64::to_degrees) + .unwrap_or(self.default_angle); defaults::set_array_p_angle(v); v }; diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index 648a034ac..36b1e9ae6 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -1271,7 +1271,7 @@ impl CadCommand for FilletCommand { self.resume_after_radius(); return Some(CmdResult::NeedPoint); } - if let Ok(v) = t.replace(',', ".").parse::() { + if let Some(v) = crate::entities::common::parse_typed_length(t) { if v >= 0.0 { self.radius = v; defaults::set_fillet_radius(v); @@ -1293,7 +1293,7 @@ impl CadCommand for FilletCommand { // "R 5.0" inline shorthand if upper.starts_with('R') { let body = t[1..].trim(); - if let Ok(v) = body.replace(',', ".").parse::() { + if let Some(v) = crate::entities::common::parse_typed_length(body) { if v >= 0.0 { self.radius = v; defaults::set_fillet_radius(v); @@ -1687,7 +1687,7 @@ impl CadCommand for ChamferCommand { self.step = ChamferStep::WaitingForDist2; return Some(CmdResult::NeedPoint); } - if let Ok(v) = t.replace(',', ".").parse::() { + if let Some(v) = crate::entities::common::parse_typed_length(t) { self.dist1 = v.max(0.0); defaults::set_chamfer_dist1(self.dist1); self.step = ChamferStep::WaitingForDist2; @@ -1703,7 +1703,7 @@ impl CadCommand for ChamferCommand { self.resume_after_dist(); return Some(CmdResult::NeedPoint); } - if let Ok(v) = t.replace(',', ".").parse::() { + if let Some(v) = crate::entities::common::parse_typed_length(t) { self.dist2 = v.max(0.0); defaults::set_chamfer_dist2(self.dist2); self.resume_after_dist(); @@ -1727,7 +1727,7 @@ impl CadCommand for ChamferCommand { let body = t[1..].trim(); let parts: Vec = body .split_whitespace() - .filter_map(|s| s.replace(',', ".").parse::().ok()) + .filter_map(crate::entities::common::parse_typed_length) .collect(); if !parts.is_empty() { if let Some(&v) = parts.first() { diff --git a/src/modules/draw/modify/offset.rs b/src/modules/draw/modify/offset.rs index 519edcef4..f707344bb 100644 --- a/src/modules/draw/modify/offset.rs +++ b/src/modules/draw/modify/offset.rs @@ -1004,7 +1004,7 @@ impl CadCommand for OffsetCommand { if t.eq_ignore_ascii_case("t") || t.eq_ignore_ascii_case("through") { return Some(self.advance_from_distance(None)); } - if let Ok(d) = t.parse::() { + if let Some(d) = crate::entities::common::parse_typed_length(&t) { let d = d.abs().max(1e-9); defaults::set_offset_dist(d); return Some(self.advance_from_distance(Some(d))); @@ -1021,7 +1021,7 @@ impl CadCommand for OffsetCommand { return Some(CmdResult::NeedPoint); } if !t.is_empty() { - if let Ok(d) = t.parse::() { + if let Some(d) = crate::entities::common::parse_typed_length(&t) { let d = d.abs().max(1e-9); defaults::set_offset_dist(d); *locked = Some(d); diff --git a/src/modules/insert/insert_block.rs b/src/modules/insert/insert_block.rs index 9a1171096..89c3caf6d 100644 --- a/src/modules/insert/insert_block.rs +++ b/src/modules/insert/insert_block.rs @@ -220,16 +220,24 @@ impl CadCommand for InsertBlockCommand { } Step::FillAttr { .. } => Some(self.accept_attr_value(text)), Step::Point { .. } => { - // Typing the value awaited after a Scale / Rotate keyword. + // Typing the value awaited after a Scale / Rotate keyword. The + // two read differently: a scale is a plain multiplier with no + // unit to write it in, while a rotation is an angle and takes + // whichever convention the drawing is set to. if let Some(kind) = self.awaiting { - if let Ok(v) = text.trim().parse::() { - match kind { - AwaitKind::Scale if v != 0.0 => { - self.x_scale = v; - self.y_scale = v; + match kind { + AwaitKind::Scale => { + if let Ok(v) = text.trim().parse::() { + if v != 0.0 { + self.x_scale = v; + self.y_scale = v; + } + } + } + AwaitKind::Rotation => { + if let Some(v) = crate::entities::common::parse_typed_angle(text) { + self.rotation_rad = v; } - AwaitKind::Scale => {} - AwaitKind::Rotation => self.rotation_rad = v.to_radians(), } } self.awaiting = None; diff --git a/src/modules/insert/minsert.rs b/src/modules/insert/minsert.rs index d913018c1..b91de8d94 100644 --- a/src/modules/insert/minsert.rs +++ b/src/modules/insert/minsert.rs @@ -228,12 +228,12 @@ impl CadCommand for MinsertCommand { } } ParamIdx::RowSpacing => { - if let Ok(v) = typed.parse::() { + if let Some(v) = crate::entities::common::parse_typed_length(typed) { self.row_spacing = v; } } ParamIdx::ColumnSpacing => { - if let Ok(v) = typed.parse::() { + if let Some(v) = crate::entities::common::parse_typed_length(typed) { self.column_spacing = v; } } diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 4554c65ba..273fdbc59 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -77,9 +77,7 @@ impl CadCommand for ExtrudeCommand { self.step == ExtrudeStep::Height } fn on_text_input(&mut self, text: &str) -> Option { - text.trim() - .parse::() - .ok() + crate::entities::common::parse_typed_length(text) .filter(|&h| h.abs() > 1e-6) .map(|h| CmdResult::ExtrudeEntity { handle: self.target_handle, diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index be20635a4..a7de1029f 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -574,13 +574,14 @@ fn format_coords(cursor: glam::DVec3, last: Option, mode: i16, pick (true, Some(l)) => { let d = cursor - l; let dist = (d.x * d.x + d.y * d.y).sqrt(); - // Kept in 0..2pi so the readout never shows a negative bearing, - // then handed over in radians for AUNITS / AUPREC to format. - let ang = d.y.atan2(d.x).rem_euclid(std::f64::consts::TAU); + // A bearing, so it is counted from the drawing's zero and runs + // the way the drawing says — and comes back out in the same + // form the polar input accepts. + let ang = d.y.atan2(d.x); format!( "{} < {}", len(dist), - crate::entities::common::format_angle(ang) + crate::entities::common::format_direction(ang) ) } _ => abs(cursor), diff --git a/src/ui/window/drawing_units.rs b/src/ui/window/drawing_units.rs index 357a452a8..461901d2e 100644 --- a/src/ui/window/drawing_units.rs +++ b/src/ui/window/drawing_units.rs @@ -311,6 +311,13 @@ fn with_context(state: &State, body: impl FnOnce() -> String) -> String { luprec: state.linear_precision, aunits: state.angular_format, auprec: state.angular_precision, + angbase: state + .base_angle + .trim() + .parse::() + .unwrap_or(0.0) + .to_radians(), + angdir_cw: state.clockwise, }); let out = body(); set_unit_context(previous);