forked from 1b-agent/opencadstudio-upstream
fix(units): carry rounded values
Round before splitting display units while preserving large lengths and high-precision cardinal bearings.
This commit is contained in:
commit
9d60c2a2ef
1 file changed
+159
-27
+159
-27
@@ -104,31 +104,31 @@ pub fn format_length(value: f64) -> String {
|
||||
match ctx.lunits {
|
||||
1 => format!("{:.*e}", prec, value),
|
||||
3 => {
|
||||
// Engineering: ft-inches, decimal inches.
|
||||
// Round before splitting so twelve inches carries into feet.
|
||||
let sign = if value < 0.0 { "-" } else { "" };
|
||||
let abs = value.abs();
|
||||
let feet = (abs / 12.0).trunc();
|
||||
let rem = abs - feet * 12.0;
|
||||
let scale = 10f64.powi(prec.min(15) as i32);
|
||||
let total = (abs * scale).round();
|
||||
let per_foot = 12.0 * scale;
|
||||
let feet = (total / per_foot).trunc();
|
||||
let rem = (total - feet * per_foot) / scale;
|
||||
format!("{}{:.0}'-{:.*}\"", sign, feet, prec, rem)
|
||||
}
|
||||
4 | 5 => {
|
||||
// Architectural / Fractional — n + fraction with 1/2^p denom (1
|
||||
// unit = 1 inch). Use 6 as a moderate denominator power so the
|
||||
// result reads like 1/64".
|
||||
// Architectural and fractional formats use 1/64-inch resolution.
|
||||
let sign = if value < 0.0 { "-" } else { "" };
|
||||
let abs = value.abs();
|
||||
let (feet, in_rem) = if ctx.lunits == 4 {
|
||||
let f = (abs / 12.0).trunc();
|
||||
(Some(f as i64), abs - f * 12.0)
|
||||
let denom = 64.0;
|
||||
let ticks = (abs * denom).round();
|
||||
let (feet, rest) = if ctx.lunits == 4 {
|
||||
let per_foot = 12.0 * denom;
|
||||
(Some((ticks / per_foot).floor()), ticks.rem_euclid(per_foot))
|
||||
} else {
|
||||
(None, abs)
|
||||
(None, ticks)
|
||||
};
|
||||
let whole = in_rem.trunc();
|
||||
let frac = in_rem - whole;
|
||||
let denom = 64u64;
|
||||
let numer = (frac * denom as f64).round() as i64;
|
||||
let mut n = numer as u64;
|
||||
let mut d = denom;
|
||||
let whole = (rest / denom).floor();
|
||||
let mut n = (rest - whole * denom).round() as u64;
|
||||
let mut d = denom as u64;
|
||||
while d > 1 && n % 2 == 0 && d % 2 == 0 {
|
||||
n /= 2;
|
||||
d /= 2;
|
||||
@@ -140,7 +140,7 @@ pub fn format_length(value: f64) -> String {
|
||||
};
|
||||
let unit_suffix = if ctx.lunits == 4 { "\"" } else { "" };
|
||||
match feet {
|
||||
Some(f) => format!("{}{}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix),
|
||||
Some(f) => format!("{}{:.0}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix),
|
||||
None => format!("{}{:.0}{}", sign, whole, frac_str),
|
||||
}
|
||||
}
|
||||
@@ -169,10 +169,15 @@ pub fn format_angle(value_rad: f64) -> String {
|
||||
fn dms(degrees: f64, prec: usize) -> String {
|
||||
let sign = if degrees < 0.0 { "-" } else { "" };
|
||||
let a = degrees.abs();
|
||||
let d = a.floor();
|
||||
let m_full = (a - d) * 60.0;
|
||||
let m = m_full.floor();
|
||||
let s = (m_full - m) * 60.0;
|
||||
// Round before splitting so seconds carry into minutes and degrees.
|
||||
let scale = 10f64.powi(prec.min(15) as i32);
|
||||
let per_minute = 60.0 * scale;
|
||||
let per_degree = 60.0 * per_minute;
|
||||
let ticks = (a * per_degree).round();
|
||||
let d = (ticks / per_degree).trunc();
|
||||
let rest = ticks - d * per_degree;
|
||||
let m = (rest / per_minute).trunc();
|
||||
let s = (rest - m * per_minute) / scale;
|
||||
format!("{}{:.0}d{:.0}'{:.*}\"", sign, d, m, prec, s)
|
||||
}
|
||||
|
||||
@@ -183,20 +188,26 @@ fn dms(degrees: f64, prec: usize) -> String {
|
||||
/// the single letter, which is also what keeps a 90° angle from reading as the
|
||||
/// contradictory `N 90d0'0" E`.
|
||||
fn surveyor(value_rad: f64, prec: usize) -> String {
|
||||
let deg = value_rad.to_degrees().rem_euclid(360.0);
|
||||
let near = |target: f64| (deg - target).abs() < 1e-9;
|
||||
if near(0.0) {
|
||||
// Quantize before choosing a cardinal or quadrant.
|
||||
let scale = 3600.0 * 10f64.powi(prec.min(15) as i32);
|
||||
let turn = 360.0 * scale;
|
||||
let ticks = (value_rad.to_degrees().rem_euclid(360.0) * scale)
|
||||
.round()
|
||||
.rem_euclid(turn);
|
||||
let at = |degrees: f64| (ticks - degrees * scale).abs() < 0.5;
|
||||
if at(0.0) {
|
||||
return "E".into();
|
||||
}
|
||||
if near(90.0) {
|
||||
if at(90.0) {
|
||||
return "N".into();
|
||||
}
|
||||
if near(180.0) {
|
||||
if at(180.0) {
|
||||
return "W".into();
|
||||
}
|
||||
if near(270.0) {
|
||||
if at(270.0) {
|
||||
return "S".into();
|
||||
}
|
||||
let deg = ticks / scale;
|
||||
// Measured from the nearer pole, toward the side the angle falls on.
|
||||
let (pole, bearing, side) = if deg < 90.0 {
|
||||
("N", 90.0 - deg, "E")
|
||||
@@ -842,3 +853,124 @@ pub(crate) fn polyline_segment_fill(
|
||||
Some(boundary)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod length_format_tests {
|
||||
use super::*;
|
||||
|
||||
fn with_units(lunits: i16, luprec: i16, value: f64) -> String {
|
||||
let mut ctx = unit_context();
|
||||
ctx.lunits = lunits;
|
||||
ctx.luprec = luprec;
|
||||
set_unit_context(ctx);
|
||||
format_length(value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fractional_carries_a_rounded_up_fraction() {
|
||||
assert_eq!(with_units(5, 4, 5.99), "5 63/64");
|
||||
assert_eq!(with_units(5, 4, 5.995), "6");
|
||||
assert_eq!(with_units(5, 4, 0.995), "1");
|
||||
assert_eq!(with_units(5, 4, 11.999), "12");
|
||||
assert_eq!(with_units(5, 4, -5.995), "-6");
|
||||
assert_eq!(with_units(5, 4, 0.5), "0 1/2");
|
||||
assert_eq!(with_units(5, 4, 9.25), "9 1/4");
|
||||
assert_eq!(with_units(5, 4, 3.0e17), "300000000000000000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architectural_carries_into_feet() {
|
||||
assert_eq!(with_units(4, 4, 11.99), "0'-11 63/64\"");
|
||||
assert_eq!(with_units(4, 4, 11.999), "1'-0\"");
|
||||
assert_eq!(with_units(4, 4, 23.999), "2'-0\"");
|
||||
assert_eq!(with_units(4, 4, 66.5), "5'-6 1/2\"");
|
||||
assert_eq!(with_units(4, 4, -11.999), "-1'-0\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engineering_carries_into_feet() {
|
||||
assert_eq!(with_units(3, 1, 11.94), "0'-11.9\"");
|
||||
assert_eq!(with_units(3, 1, 11.99), "1'-0.0\"");
|
||||
assert_eq!(with_units(3, 1, 23.99), "2'-0.0\"");
|
||||
assert_eq!(with_units(3, 2, 11.999), "1'-0.00\"");
|
||||
assert_eq!(with_units(3, 1, 66.5), "5'-6.5\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatted_lengths_read_back() {
|
||||
for lunits in [3, 4, 5] {
|
||||
for value in [0.995f64, 5.995, 11.999, 23.999, 66.5, 9.25] {
|
||||
let shown = with_units(lunits, if lunits == 3 { 4 } else { 4 }, value);
|
||||
let read = parse_length(&shown)
|
||||
.unwrap_or_else(|| panic!("lunits {lunits} wrote {shown:?}, which does not read back"));
|
||||
assert!(
|
||||
(read - value).abs() < 0.02,
|
||||
"lunits {lunits}: {value} wrote {shown:?}, read back as {read}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod angle_format_tests {
|
||||
use super::*;
|
||||
|
||||
fn shown(aunits: i16, auprec: i16, degrees: f64) -> String {
|
||||
let mut ctx = unit_context();
|
||||
ctx.aunits = aunits;
|
||||
ctx.auprec = auprec;
|
||||
set_unit_context(ctx);
|
||||
format_angle(degrees.to_radians())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degrees_minutes_seconds_carry() {
|
||||
assert_eq!(shown(1, 0, 45.9999), "46d0'0\"");
|
||||
assert_eq!(shown(1, 0, 89.99999), "90d0'0\"");
|
||||
assert_eq!(shown(1, 0, 0.99999), "1d0'0\"");
|
||||
assert_eq!(shown(1, 0, 45.5), "45d30'0\"");
|
||||
assert_eq!(shown(1, 2, 45.9999), "45d59'59.64\"");
|
||||
assert_eq!(shown(1, 2, 45.5), "45d30'0.00\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surveyor_bearings_stay_inside_a_quadrant() {
|
||||
assert_eq!(shown(4, 0, 359.99999), "E");
|
||||
assert_eq!(shown(4, 0, 89.99999), "N");
|
||||
assert_eq!(shown(4, 0, 45.0), "N 45d0'0\" E");
|
||||
assert_eq!(shown(4, 0, 180.0), "W");
|
||||
assert_ne!(shown(4, 8, 0.0000000005), "E");
|
||||
for prec in [0, 2] {
|
||||
for deg in [0.99999f64, 45.9999, 89.99999, 179.99999, 269.99999, 359.99999] {
|
||||
let text = shown(4, prec, deg);
|
||||
assert!(
|
||||
!text.contains("90d"),
|
||||
"prec {prec}: {deg} wrote {text:?}, a quarter turn inside a quadrant"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatted_angles_read_back() {
|
||||
for aunits in [0, 1, 4] {
|
||||
for prec in [0, 2] {
|
||||
for deg in [0.99999f64, 45.5, 45.9999, 89.99999, 200.25, 359.99999] {
|
||||
let text = shown(aunits, prec, deg);
|
||||
let read = parse_angle(&text).unwrap_or_else(|| {
|
||||
panic!("aunits {aunits} prec {prec} wrote {text:?}, which does not read back")
|
||||
});
|
||||
let delta = (read.to_degrees().rem_euclid(360.0) - deg.rem_euclid(360.0))
|
||||
.rem_euclid(360.0);
|
||||
let delta = delta.min(360.0 - delta);
|
||||
assert!(
|
||||
delta < 1.0,
|
||||
"aunits {aunits} prec {prec}: {deg} wrote {text:?}, read back as {}",
|
||||
read.to_degrees()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user