feat(units): read back every form the drawing writes
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::<f64>() — 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 <noreply@anthropic.com>
This commit is contained in:
parent
523b85f42b
commit
6cbec232bb
18 changed files with 298 additions and 58 deletions
|
|
@ -20,7 +20,13 @@ pub(crate) enum CoordKind {
|
||||||
/// `distance<angle<elevation`.
|
/// `distance<angle<elevation`.
|
||||||
/// A leading `@` marks the value relative to the last point; a leading
|
/// A leading `@` marks the value relative to the last point; a leading
|
||||||
/// `#` forces absolute. Separators: comma or semicolon.
|
/// `#` forces absolute. Separators: comma or semicolon.
|
||||||
|
///
|
||||||
|
/// Each number is read by the same pair that writes it, so anything the
|
||||||
|
/// drawing displays can be typed straight back: `5'-6 1/2"`, `9 1/2`,
|
||||||
|
/// `@72'8"<n45d20'6"e`. Angles after `<` are directions, and so are counted
|
||||||
|
/// from the drawing's zero and run the way it says.
|
||||||
pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
|
pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
|
||||||
|
use crate::entities::common::{parse_direction, parse_length};
|
||||||
let trimmed = text.trim();
|
let trimmed = text.trim();
|
||||||
let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') {
|
let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') {
|
||||||
(CoordKind::Relative, r)
|
(CoordKind::Relative, r)
|
||||||
|
|
@ -29,12 +35,13 @@ pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
|
||||||
} else {
|
} else {
|
||||||
(CoordKind::Default, trimmed)
|
(CoordKind::Default, trimmed)
|
||||||
};
|
};
|
||||||
let number = |value: &str| value.trim().parse::<f64>().ok();
|
|
||||||
if let Some((distance, angles)) = rest.split_once('<') {
|
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('<') {
|
if let Some((azimuth, elevation)) = angles.split_once('<') {
|
||||||
let azimuth = number(azimuth)?.to_radians();
|
let azimuth = parse_direction(azimuth)?;
|
||||||
let elevation = number(elevation)?.to_radians();
|
// 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();
|
let horizontal = distance * elevation.cos();
|
||||||
return Some((
|
return Some((
|
||||||
glam::DVec3::new(
|
glam::DVec3::new(
|
||||||
|
|
@ -45,13 +52,12 @@ pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
|
||||||
kind,
|
kind,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let angle_and_z: Vec<&str> = angles.split(|c| c == ',' || c == ';').collect();
|
// A cylindrical `distance<angle,z` splits on the comma — but a
|
||||||
let (angle, z) = match angle_and_z.as_slice() {
|
// surveyor's bearing has none, so nothing here can eat one.
|
||||||
[angle] => (number(angle)?, 0.0),
|
let (angle, z) = match angles.split_once(|c| c == ',' || c == ';') {
|
||||||
[angle, z] => (number(angle)?, number(z)?),
|
Some((angle, z)) => (parse_direction(angle)?, parse_length(z)?),
|
||||||
_ => return None,
|
None => (parse_direction(angles)?, 0.0),
|
||||||
};
|
};
|
||||||
let angle = angle.to_radians();
|
|
||||||
return Some((
|
return Some((
|
||||||
glam::DVec3::new(distance * angle.cos(), distance * angle.sin(), z),
|
glam::DVec3::new(distance * angle.cos(), distance * angle.sin(), z),
|
||||||
kind,
|
kind,
|
||||||
|
|
@ -59,9 +65,8 @@ pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
|
||||||
}
|
}
|
||||||
let parts: Vec<f64> = rest
|
let parts: Vec<f64> = rest
|
||||||
.split(|c| c == ',' || c == ';')
|
.split(|c| c == ',' || c == ';')
|
||||||
.map(|s| s.trim())
|
.map(parse_length)
|
||||||
.filter_map(|s| s.parse().ok())
|
.collect::<Option<Vec<f64>>>()?;
|
||||||
.collect();
|
|
||||||
match parts.as_slice() {
|
match parts.as_slice() {
|
||||||
[x, y] => Some((glam::DVec3::new(*x, *y, 0.0), kind)),
|
[x, y] => Some((glam::DVec3::new(*x, *y, 0.0), kind)),
|
||||||
[x, y, z] => Some((glam::DVec3::new(*x, *y, *z), kind)),
|
[x, y, z] => Some((glam::DVec3::new(*x, *y, *z), kind)),
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,9 @@ impl OpenCADStudio {
|
||||||
// lengths/angles per LUNITS / LUPREC / AUNITS / AUPREC.
|
// lengths/angles per LUNITS / LUPREC / AUNITS / AUPREC.
|
||||||
{
|
{
|
||||||
let h = &self.tabs[i].scene.document.header;
|
let h = &self.tabs[i].scene.document.header;
|
||||||
crate::entities::common::set_unit_context(crate::entities::common::UnitContext {
|
crate::entities::common::set_unit_context(
|
||||||
lunits: h.linear_unit_format,
|
crate::entities::common::UnitContext::from_header(h),
|
||||||
luprec: h.linear_unit_precision,
|
);
|
||||||
aunits: h.angular_unit_format,
|
|
||||||
auprec: h.angular_unit_precision,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let layer_names: Vec<String> = self.tabs[i]
|
let layer_names: Vec<String> = self.tabs[i]
|
||||||
|
|
|
||||||
|
|
@ -1664,12 +1664,9 @@ impl OpenCADStudio {
|
||||||
// every refresh; the status bar redraws on its own schedule
|
// every refresh; the status bar redraws on its own schedule
|
||||||
// and cannot rely on that having happened.
|
// and cannot rely on that having happened.
|
||||||
crate::entities::common::set_unit_context(
|
crate::entities::common::set_unit_context(
|
||||||
crate::entities::common::UnitContext {
|
crate::entities::common::UnitContext::from_header(
|
||||||
lunits: tab.scene.document.header.linear_unit_format,
|
&tab.scene.document.header,
|
||||||
luprec: tab.scene.document.header.linear_unit_precision,
|
),
|
||||||
aunits: tab.scene.document.header.angular_unit_format,
|
|
||||||
auprec: tab.scene.document.header.angular_unit_precision,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
let status_menu_data = crate::ui::statusbar::StatusMenuData {
|
let status_menu_data = crate::ui::statusbar::StatusMenuData {
|
||||||
layout_names: layout_names.clone(),
|
layout_names: layout_names.clone(),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,27 @@ pub struct UnitContext {
|
||||||
/// AUPREC — decimal places (angular)
|
/// AUPREC — decimal places (angular)
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub auprec: i16,
|
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! {
|
thread_local! {
|
||||||
|
|
@ -27,6 +48,8 @@ thread_local! {
|
||||||
luprec: 4,
|
luprec: 4,
|
||||||
aunits: 0,
|
aunits: 0,
|
||||||
auprec: 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))
|
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<f64> {
|
||||||
|
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<f64> {
|
||||||
|
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<f64> {
|
||||||
|
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::<f64>().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<f64> {
|
||||||
|
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::<f64>().ok()?,
|
||||||
|
rest[at + rest[at..].chars().next()?.len_utf8()..].trim(),
|
||||||
|
),
|
||||||
|
None => (rest.trim().parse::<f64>().ok()?, ""),
|
||||||
|
};
|
||||||
|
let (minutes, rest) = match rest.split_once('\'') {
|
||||||
|
Some((minutes, rest)) if !minutes.trim().is_empty() => {
|
||||||
|
(minutes.trim().parse::<f64>().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::<f64>().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<f64> {
|
||||||
|
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<f64> {
|
||||||
|
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<f64> {
|
||||||
|
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<f64> {
|
||||||
|
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
|
/// Two interior triangles covering a quad (flat list, 6 vertices) — the
|
||||||
/// click-anywhere pick surface for frame-like entities (image, OLE frame,
|
/// click-anywhere pick surface for frame-like entities (image, OLE frame,
|
||||||
/// underlay, wipeout). Corners in ring order.
|
/// underlay, wipeout). Corners in ring order.
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ impl CadCommand for AlignedDimensionCommand {
|
||||||
self.text_angle = if t.is_empty() {
|
self.text_angle = if t.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
t.parse::<f64>().ok().map(f64::to_radians)
|
crate::entities::common::parse_typed_angle(t)
|
||||||
};
|
};
|
||||||
self.awaiting_angle = false;
|
self.awaiting_angle = false;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ impl CadCommand for DiameterDimensionCommand {
|
||||||
self.text_angle = if t.is_empty() {
|
self.text_angle = if t.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
t.parse::<f64>().ok().map(f64::to_radians)
|
crate::entities::common::parse_typed_angle(t)
|
||||||
};
|
};
|
||||||
self.awaiting_angle = false;
|
self.awaiting_angle = false;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ impl CadCommand for LinearDimensionCommand {
|
||||||
self.text_angle = if t.is_empty() {
|
self.text_angle = if t.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
t.parse::<f64>().ok().map(f64::to_radians)
|
crate::entities::common::parse_typed_angle(t)
|
||||||
};
|
};
|
||||||
self.awaiting_angle = false;
|
self.awaiting_angle = false;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ impl CadCommand for RadiusDimensionCommand {
|
||||||
self.text_angle = if t.is_empty() {
|
self.text_angle = if t.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
t.parse::<f64>().ok().map(f64::to_radians)
|
crate::entities::common::parse_typed_angle(t)
|
||||||
};
|
};
|
||||||
self.awaiting_angle = false;
|
self.awaiting_angle = false;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ impl HelixCommand {
|
||||||
|
|
||||||
/// Parse a positive distance from typed text; returns `None` on failure.
|
/// Parse a positive distance from typed text; returns `None` on failure.
|
||||||
fn parse_value(text: &str) -> Option<f64> {
|
fn parse_value(text: &str) -> Option<f64> {
|
||||||
text.trim().parse::<f64>().ok()
|
crate::entities::common::parse_typed_length(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ impl CadCommand for TraceCommand {
|
||||||
self.width_set = true;
|
self.width_set = true;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
}
|
}
|
||||||
if let Ok(w) = t.parse::<f64>() {
|
if let Some(w) = crate::entities::common::parse_typed_length(t) {
|
||||||
if w > 0.0 {
|
if w > 0.0 {
|
||||||
self.width = w;
|
self.width = w;
|
||||||
self.width_set = true;
|
self.width_set = true;
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,8 @@ impl CadCommand for ArrayRectCommand {
|
||||||
let row_sp = if t.is_empty() {
|
let row_sp = if t.is_empty() {
|
||||||
self.default_row_sp
|
self.default_row_sp
|
||||||
} else {
|
} else {
|
||||||
let v = t.parse::<f64>().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);
|
defaults::set_array_row_sp(v);
|
||||||
self.default_row_sp = v;
|
self.default_row_sp = v;
|
||||||
v
|
v
|
||||||
|
|
@ -189,7 +190,8 @@ impl CadCommand for ArrayRectCommand {
|
||||||
let col_sp = if t.is_empty() {
|
let col_sp = if t.is_empty() {
|
||||||
self.default_col_sp
|
self.default_col_sp
|
||||||
} else {
|
} else {
|
||||||
let v = t.parse::<f64>().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);
|
defaults::set_array_col_sp(v);
|
||||||
v
|
v
|
||||||
};
|
};
|
||||||
|
|
@ -341,7 +343,10 @@ impl CadCommand for ArrayPolarCommand {
|
||||||
let total_deg = if t.is_empty() {
|
let total_deg = if t.is_empty() {
|
||||||
self.default_angle
|
self.default_angle
|
||||||
} else {
|
} else {
|
||||||
let v = t.parse::<f64>().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);
|
defaults::set_array_p_angle(v);
|
||||||
v
|
v
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1271,7 +1271,7 @@ impl CadCommand for FilletCommand {
|
||||||
self.resume_after_radius();
|
self.resume_after_radius();
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
}
|
}
|
||||||
if let Ok(v) = t.replace(',', ".").parse::<f64>() {
|
if let Some(v) = crate::entities::common::parse_typed_length(t) {
|
||||||
if v >= 0.0 {
|
if v >= 0.0 {
|
||||||
self.radius = v;
|
self.radius = v;
|
||||||
defaults::set_fillet_radius(v);
|
defaults::set_fillet_radius(v);
|
||||||
|
|
@ -1293,7 +1293,7 @@ impl CadCommand for FilletCommand {
|
||||||
// "R 5.0" inline shorthand
|
// "R 5.0" inline shorthand
|
||||||
if upper.starts_with('R') {
|
if upper.starts_with('R') {
|
||||||
let body = t[1..].trim();
|
let body = t[1..].trim();
|
||||||
if let Ok(v) = body.replace(',', ".").parse::<f64>() {
|
if let Some(v) = crate::entities::common::parse_typed_length(body) {
|
||||||
if v >= 0.0 {
|
if v >= 0.0 {
|
||||||
self.radius = v;
|
self.radius = v;
|
||||||
defaults::set_fillet_radius(v);
|
defaults::set_fillet_radius(v);
|
||||||
|
|
@ -1687,7 +1687,7 @@ impl CadCommand for ChamferCommand {
|
||||||
self.step = ChamferStep::WaitingForDist2;
|
self.step = ChamferStep::WaitingForDist2;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
}
|
}
|
||||||
if let Ok(v) = t.replace(',', ".").parse::<f64>() {
|
if let Some(v) = crate::entities::common::parse_typed_length(t) {
|
||||||
self.dist1 = v.max(0.0);
|
self.dist1 = v.max(0.0);
|
||||||
defaults::set_chamfer_dist1(self.dist1);
|
defaults::set_chamfer_dist1(self.dist1);
|
||||||
self.step = ChamferStep::WaitingForDist2;
|
self.step = ChamferStep::WaitingForDist2;
|
||||||
|
|
@ -1703,7 +1703,7 @@ impl CadCommand for ChamferCommand {
|
||||||
self.resume_after_dist();
|
self.resume_after_dist();
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
}
|
}
|
||||||
if let Ok(v) = t.replace(',', ".").parse::<f64>() {
|
if let Some(v) = crate::entities::common::parse_typed_length(t) {
|
||||||
self.dist2 = v.max(0.0);
|
self.dist2 = v.max(0.0);
|
||||||
defaults::set_chamfer_dist2(self.dist2);
|
defaults::set_chamfer_dist2(self.dist2);
|
||||||
self.resume_after_dist();
|
self.resume_after_dist();
|
||||||
|
|
@ -1727,7 +1727,7 @@ impl CadCommand for ChamferCommand {
|
||||||
let body = t[1..].trim();
|
let body = t[1..].trim();
|
||||||
let parts: Vec<f64> = body
|
let parts: Vec<f64> = body
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.filter_map(|s| s.replace(',', ".").parse::<f64>().ok())
|
.filter_map(crate::entities::common::parse_typed_length)
|
||||||
.collect();
|
.collect();
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
if let Some(&v) = parts.first() {
|
if let Some(&v) = parts.first() {
|
||||||
|
|
|
||||||
|
|
@ -1004,7 +1004,7 @@ impl CadCommand for OffsetCommand {
|
||||||
if t.eq_ignore_ascii_case("t") || t.eq_ignore_ascii_case("through") {
|
if t.eq_ignore_ascii_case("t") || t.eq_ignore_ascii_case("through") {
|
||||||
return Some(self.advance_from_distance(None));
|
return Some(self.advance_from_distance(None));
|
||||||
}
|
}
|
||||||
if let Ok(d) = t.parse::<f64>() {
|
if let Some(d) = crate::entities::common::parse_typed_length(&t) {
|
||||||
let d = d.abs().max(1e-9);
|
let d = d.abs().max(1e-9);
|
||||||
defaults::set_offset_dist(d);
|
defaults::set_offset_dist(d);
|
||||||
return Some(self.advance_from_distance(Some(d)));
|
return Some(self.advance_from_distance(Some(d)));
|
||||||
|
|
@ -1021,7 +1021,7 @@ impl CadCommand for OffsetCommand {
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
}
|
}
|
||||||
if !t.is_empty() {
|
if !t.is_empty() {
|
||||||
if let Ok(d) = t.parse::<f64>() {
|
if let Some(d) = crate::entities::common::parse_typed_length(&t) {
|
||||||
let d = d.abs().max(1e-9);
|
let d = d.abs().max(1e-9);
|
||||||
defaults::set_offset_dist(d);
|
defaults::set_offset_dist(d);
|
||||||
*locked = Some(d);
|
*locked = Some(d);
|
||||||
|
|
|
||||||
|
|
@ -220,16 +220,24 @@ impl CadCommand for InsertBlockCommand {
|
||||||
}
|
}
|
||||||
Step::FillAttr { .. } => Some(self.accept_attr_value(text)),
|
Step::FillAttr { .. } => Some(self.accept_attr_value(text)),
|
||||||
Step::Point { .. } => {
|
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 Some(kind) = self.awaiting {
|
||||||
if let Ok(v) = text.trim().parse::<f64>() {
|
match kind {
|
||||||
match kind {
|
AwaitKind::Scale => {
|
||||||
AwaitKind::Scale if v != 0.0 => {
|
if let Ok(v) = text.trim().parse::<f64>() {
|
||||||
self.x_scale = v;
|
if v != 0.0 {
|
||||||
self.y_scale = v;
|
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;
|
self.awaiting = None;
|
||||||
|
|
|
||||||
|
|
@ -228,12 +228,12 @@ impl CadCommand for MinsertCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamIdx::RowSpacing => {
|
ParamIdx::RowSpacing => {
|
||||||
if let Ok(v) = typed.parse::<f64>() {
|
if let Some(v) = crate::entities::common::parse_typed_length(typed) {
|
||||||
self.row_spacing = v;
|
self.row_spacing = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamIdx::ColumnSpacing => {
|
ParamIdx::ColumnSpacing => {
|
||||||
if let Ok(v) = typed.parse::<f64>() {
|
if let Some(v) = crate::entities::common::parse_typed_length(typed) {
|
||||||
self.column_spacing = v;
|
self.column_spacing = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,7 @@ impl CadCommand for ExtrudeCommand {
|
||||||
self.step == ExtrudeStep::Height
|
self.step == ExtrudeStep::Height
|
||||||
}
|
}
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
text.trim()
|
crate::entities::common::parse_typed_length(text)
|
||||||
.parse::<f64>()
|
|
||||||
.ok()
|
|
||||||
.filter(|&h| h.abs() > 1e-6)
|
.filter(|&h| h.abs() > 1e-6)
|
||||||
.map(|h| CmdResult::ExtrudeEntity {
|
.map(|h| CmdResult::ExtrudeEntity {
|
||||||
handle: self.target_handle,
|
handle: self.target_handle,
|
||||||
|
|
|
||||||
|
|
@ -574,13 +574,14 @@ fn format_coords(cursor: glam::DVec3, last: Option<glam::DVec3>, mode: i16, pick
|
||||||
(true, Some(l)) => {
|
(true, Some(l)) => {
|
||||||
let d = cursor - l;
|
let d = cursor - l;
|
||||||
let dist = (d.x * d.x + d.y * d.y).sqrt();
|
let dist = (d.x * d.x + d.y * d.y).sqrt();
|
||||||
// Kept in 0..2pi so the readout never shows a negative bearing,
|
// A bearing, so it is counted from the drawing's zero and runs
|
||||||
// then handed over in radians for AUNITS / AUPREC to format.
|
// the way the drawing says — and comes back out in the same
|
||||||
let ang = d.y.atan2(d.x).rem_euclid(std::f64::consts::TAU);
|
// form the polar input accepts.
|
||||||
|
let ang = d.y.atan2(d.x);
|
||||||
format!(
|
format!(
|
||||||
"{} < {}",
|
"{} < {}",
|
||||||
len(dist),
|
len(dist),
|
||||||
crate::entities::common::format_angle(ang)
|
crate::entities::common::format_direction(ang)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => abs(cursor),
|
_ => abs(cursor),
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,13 @@ fn with_context(state: &State, body: impl FnOnce() -> String) -> String {
|
||||||
luprec: state.linear_precision,
|
luprec: state.linear_precision,
|
||||||
aunits: state.angular_format,
|
aunits: state.angular_format,
|
||||||
auprec: state.angular_precision,
|
auprec: state.angular_precision,
|
||||||
|
angbase: state
|
||||||
|
.base_angle
|
||||||
|
.trim()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
.to_radians(),
|
||||||
|
angdir_cw: state.clockwise,
|
||||||
});
|
});
|
||||||
let out = body();
|
let out = body();
|
||||||
set_unit_context(previous);
|
set_unit_context(previous);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue