use std::f64::consts::PI;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BBox {
pub min_x: f64,
pub min_y: f64,
pub max_x: f64,
pub max_y: f64,
}
impl Default for BBox {
fn default() -> Self {
Self {
min_x: f64::INFINITY,
min_y: f64::INFINITY,
max_x: f64::NEG_INFINITY,
max_y: f64::NEG_INFINITY,
}
}
}
impl BBox {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, x: f64, y: f64) {
if x.is_finite() {
if x < self.min_x {
self.min_x = x;
}
if x > self.max_x {
self.max_x = x;
}
}
if y.is_finite() {
if y < self.min_y {
self.min_y = y;
}
if y > self.max_y {
self.max_y = y;
}
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.min_x.is_finite()
&& self.min_y.is_finite()
&& self.max_x.is_finite()
&& self.max_y.is_finite()
&& self.min_x <= self.max_x
&& self.min_y <= self.max_y
}
#[must_use]
pub fn width(&self) -> f64 {
if self.is_valid() {
(self.max_x - self.min_x).max(0.0)
} else {
0.0
}
}
#[must_use]
pub fn height(&self) -> f64 {
if self.is_valid() {
(self.max_y - self.min_y).max(0.0)
} else {
0.0
}
}
}
#[must_use]
pub fn fmt_coord(v: f64) -> String {
if !v.is_finite() {
return "0".into();
}
let rounded = (v * 100_000.0).round() / 100_000.0;
if rounded == 0.0 {
"0".into()
} else {
let mut s = format!("{rounded:.5}");
while s.ends_with('0') {
s.pop();
}
if s.ends_with('.') {
s.pop();
}
s
}
}
#[must_use]
pub fn parse_cad_float(s: &str) -> Option<f64> {
let trimmed = s.trim();
if trimmed.is_empty() {
return None;
}
if let Ok(v) = trimmed.parse::<f64>() {
return Some(v);
}
if trimmed.starts_with('.') {
format!("0{trimmed}").parse().ok()
} else if let Some(rest) = trimmed.strip_prefix("-.") {
format!("-0.{rest}").parse().ok()
} else if let Some(rest) = trimmed.strip_prefix("+.") {
format!("+0.{rest}").parse().ok()
} else {
None
}
}
#[must_use]
pub fn deg_to_rad(deg: f64) -> f64 {
deg * PI / 180.0
}
#[must_use]
pub fn circle_path(cx: f64, cy: f64, r: f64) -> String {
if !r.is_finite() || r <= 0.0 {
return String::new();
}
let x1 = cx - r;
let x2 = cx + r;
format!(
"M {} {} A {} {} 0 1 0 {} {} A {} {} 0 1 0 {} {} Z",
fmt_coord(x1),
fmt_coord(cy),
fmt_coord(r),
fmt_coord(r),
fmt_coord(x2),
fmt_coord(cy),
fmt_coord(r),
fmt_coord(r),
fmt_coord(x1),
fmt_coord(cy)
)
}
#[must_use]
pub fn bulge_to_svg_arc(
x1: f64,
y1: f64,
x2: f64,
y2: f64,
bulge: f64,
y_is_flipped: bool,
) -> String {
if !bulge.is_finite() || bulge.abs() < 1e-9 {
return format!("L {} {}", fmt_coord(x2), fmt_coord(y2));
}
let dx = x2 - x1;
let dy = y2 - y1;
let chord_len = (dx * dx + dy * dy).sqrt();
if chord_len < 1e-9 {
return String::new();
}
let r = (chord_len * (1.0 + bulge * bulge)) / (4.0 * bulge.abs());
let large_arc = if bulge.abs() > 1.0 { 1 } else { 0 };
let sweep = if y_is_flipped {
if bulge > 0.0 { 1 } else { 0 }
} else if bulge > 0.0 {
0
} else {
1
};
format!(
"A {} {} 0 {} {} {} {}",
fmt_coord(r),
fmt_coord(r),
large_arc,
sweep,
fmt_coord(x2),
fmt_coord(y2)
)
}
#[must_use]
pub fn arc_to_svg_path(
cx: f64,
cy: f64,
r: f64,
start_deg: f64,
end_deg: f64,
y_is_flipped: bool,
) -> String {
if r <= 0.0 {
return String::new();
}
let mut sweep_angle = end_deg - start_deg;
while sweep_angle <= 0.0 {
sweep_angle += 360.0;
}
while sweep_angle > 360.0 {
sweep_angle -= 360.0;
}
let start_rad = deg_to_rad(start_deg);
let end_rad = deg_to_rad(end_deg);
let (start_x, start_y, end_x, end_y, sweep_flag) = if y_is_flipped {
let sx = cx + r * start_rad.cos();
let sy = cy - r * start_rad.sin();
let ex = cx + r * end_rad.cos();
let ey = cy - r * end_rad.sin();
(sx, sy, ex, ey, 0)
} else {
let sx = cx + r * start_rad.cos();
let sy = cy + r * start_rad.sin();
let ex = cx + r * end_rad.cos();
let ey = cy + r * end_rad.sin();
(sx, sy, ex, ey, 1)
};
let large_arc = if sweep_angle > 180.0 { 1 } else { 0 };
format!(
"M {} {} A {} {} 0 {} {} {} {}",
fmt_coord(start_x),
fmt_coord(start_y),
fmt_coord(r),
fmt_coord(r),
large_arc,
sweep_flag,
fmt_coord(end_x),
fmt_coord(end_y)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bbox_calculation() {
let mut bbox = BBox::new();
assert!(!bbox.is_valid());
bbox.update(10.0, 20.0);
bbox.update(50.0, -10.0);
assert!(bbox.is_valid());
assert_eq!(bbox.min_x, 10.0);
assert_eq!(bbox.max_x, 50.0);
assert_eq!(bbox.min_y, -10.0);
assert_eq!(bbox.max_y, 20.0);
assert_eq!(bbox.width(), 40.0);
assert_eq!(bbox.height(), 30.0);
}
#[test]
fn test_circle_path() {
let path = circle_path(100.0, 100.0, 50.0);
assert!(path.starts_with("M 50 100 A 50 50 0 1 0 150 100"));
assert!(path.ends_with("Z"));
}
#[test]
fn test_bulge_to_arc() {
let arc_cmd = bulge_to_svg_arc(0.0, 0.0, 100.0, 0.0, 1.0, true);
assert!(arc_cmd.starts_with("A 50 50 0 0 1 100 0"));
let line_cmd = bulge_to_svg_arc(0.0, 0.0, 100.0, 0.0, 0.0, true);
assert_eq!(line_cmd, "L 100 0");
}
}