use klayout_core::{Path, PathCap, Point, Polygon};
pub fn path_to_polygon(p: &Path) -> Option<Polygon> {
if p.points.len() < 2 || p.width <= 0 {
return None;
}
let half = p.width / 2;
let (begin_ext, end_ext) = match p.cap {
PathCap::Flat => (0, 0),
PathCap::Round => (half, half),
PathCap::Extended => {
if p.begin_ext != 0 || p.end_ext != 0 {
(p.begin_ext, p.end_ext)
} else {
(half, half)
}
}
};
let mut center: Vec<Point> = p.points.to_vec();
if begin_ext != 0 {
center[0] = extend(center[1], center[0], begin_ext);
}
if end_ext != 0 {
let n = center.len();
center[n - 1] = extend(center[n - 2], center[n - 1], end_ext);
}
let left = offset_polyline(¢er, half, OffsetSide::Left);
let right = offset_polyline(¢er, half, OffsetSide::Right);
let mut hull = Vec::with_capacity(left.len() + right.len());
hull.extend(left);
for p in right.iter().rev() {
hull.push(*p);
}
Some(Polygon::from_hull(hull))
}
#[derive(Copy, Clone)]
enum OffsetSide {
Left,
Right,
}
fn offset_polyline(pts: &[Point], dist: i64, side: OffsetSide) -> Vec<Point> {
let mut out = Vec::with_capacity(pts.len());
let n = pts.len();
for i in 0..n {
let dir = if i == 0 {
edge_dir(pts[0], pts[1])
} else if i == n - 1 {
edge_dir(pts[n - 2], pts[n - 1])
} else {
let d1 = edge_dir(pts[i - 1], pts[i]);
let d2 = edge_dir(pts[i], pts[i + 1]);
avg_dir(d1, d2)
};
let perp = match side {
OffsetSide::Left => (-dir.1, dir.0),
OffsetSide::Right => (dir.1, -dir.0),
};
let off_x = perp.0 * dist as f64;
let off_y = perp.1 * dist as f64;
out.push(Point::new(
pts[i].x + off_x.round() as i64,
pts[i].y + off_y.round() as i64,
));
}
out
}
fn edge_dir(a: Point, b: Point) -> (f64, f64) {
let dx = (b.x - a.x) as f64;
let dy = (b.y - a.y) as f64;
let len = (dx * dx + dy * dy).sqrt();
if len == 0.0 {
(0.0, 0.0)
} else {
(dx / len, dy / len)
}
}
fn avg_dir(d1: (f64, f64), d2: (f64, f64)) -> (f64, f64) {
let x = d1.0 + d2.0;
let y = d1.1 + d2.1;
let len = (x * x + y * y).sqrt();
if len == 0.0 {
(1.0, 0.0)
} else {
(x / len, y / len)
}
}
fn extend(prev: Point, end: Point, by: i64) -> Point {
let (dx, dy) = edge_dir(prev, end);
Point::new(
end.x + (dx * by as f64).round() as i64,
end.y + (dy * by as f64).round() as i64,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn horizontal_path_flat_cap() {
let p = Path {
points: smallvec::smallvec![Point::new(0, 0), Point::new(100, 0)],
width: 20,
begin_ext: 0,
end_ext: 0,
cap: PathCap::Flat,
};
let poly = path_to_polygon(&p).unwrap();
assert_eq!(poly.hull.len(), 4);
let bbox = poly.bbox();
assert_eq!(
bbox,
klayout_core::Bbox::new(Point::new(0, -10), Point::new(100, 10))
);
}
#[test]
fn horizontal_path_round_cap_extends() {
let p = Path {
points: smallvec::smallvec![Point::new(0, 0), Point::new(100, 0)],
width: 20,
begin_ext: 0,
end_ext: 0,
cap: PathCap::Round,
};
let poly = path_to_polygon(&p).unwrap();
let bbox = poly.bbox();
assert_eq!(
bbox,
klayout_core::Bbox::new(Point::new(-10, -10), Point::new(110, 10))
);
}
#[test]
fn l_path_axis_aligned() {
let p = Path {
points: smallvec::smallvec![
Point::new(0, 0),
Point::new(100, 0),
Point::new(100, 100),
],
width: 20,
begin_ext: 0,
end_ext: 0,
cap: PathCap::Flat,
};
let poly = path_to_polygon(&p).unwrap();
let bbox = poly.bbox();
assert!(bbox.min.x <= 0);
assert!(bbox.min.y <= -10);
assert!(bbox.max.x >= 110);
assert!(bbox.max.y >= 100);
}
#[test]
fn zero_width_returns_none() {
let p = Path {
points: smallvec::smallvec![Point::new(0, 0), Point::new(100, 0)],
width: 0,
begin_ext: 0,
end_ext: 0,
cap: PathCap::Flat,
};
assert!(path_to_polygon(&p).is_none());
}
}