Skip to main content

klayout_geom/
edges.rs

1//! Edge collections — first-class edges as a complement to `Region`.
2//!
3//! KLayout treats edges as their own type with a rich operator set:
4//! `centers`, `extended`, `with_angle`, `interacting`, `length_at_least`,
5//! and so on. Many DRC primitives consume an `Edges` rather than a
6//! `Region` (e.g. measure spacing only on edges of a particular angle).
7//!
8//! Edges carry a direction (a → b). For polygon-derived edges that
9//! direction follows the hull's traversal order (CW for the outer
10//! boundary). Filter operators preserve the source direction.
11//!
12//! v1 covers axis-aligned and arbitrary-angle edges. Some operators
13//! (`outside_part`, `inside_part`) are implemented for axis-aligned
14//! input; non-axis-aligned cases fall back to bbox-based filtering.
15
16use crate::Region;
17use klayout_core::{Bbox, Point, Polygon};
18
19/// One directed edge from `a` to `b`. Edges with `a == b` are valid but
20/// don't contribute to most operators (they're zero-length).
21#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
22pub struct Edge {
23    pub a: Point,
24    pub b: Point,
25}
26
27impl Edge {
28    pub const fn new(a: Point, b: Point) -> Self {
29        Self { a, b }
30    }
31
32    pub fn dx(&self) -> i64 {
33        self.b.x - self.a.x
34    }
35
36    pub fn dy(&self) -> i64 {
37        self.b.y - self.a.y
38    }
39
40    pub fn length_squared(&self) -> i128 {
41        let dx = self.dx() as i128;
42        let dy = self.dy() as i128;
43        dx * dx + dy * dy
44    }
45
46    pub fn length(&self) -> f64 {
47        (self.length_squared() as f64).sqrt()
48    }
49
50    pub fn midpoint(&self) -> Point {
51        Point::new((self.a.x + self.b.x) / 2, (self.a.y + self.b.y) / 2)
52    }
53
54    pub fn bbox(&self) -> Bbox {
55        Bbox::new(
56            Point::new(self.a.x.min(self.b.x), self.a.y.min(self.b.y)),
57            Point::new(self.a.x.max(self.b.x), self.a.y.max(self.b.y)),
58        )
59    }
60
61    /// Angle in degrees from the +X axis, normalized to `[0, 180)`.
62    /// Reverse-direction edges have the same angle as their forward
63    /// counterpart — useful for "edges parallel to direction X".
64    pub fn angle_degrees(&self) -> f64 {
65        let mut a = (self.dy() as f64).atan2(self.dx() as f64).to_degrees();
66        if a < 0.0 {
67            a += 180.0;
68        }
69        if a >= 180.0 {
70            a -= 180.0;
71        }
72        a
73    }
74
75    pub fn is_horizontal(&self) -> bool {
76        self.dy() == 0 && self.dx() != 0
77    }
78
79    pub fn is_vertical(&self) -> bool {
80        self.dx() == 0 && self.dy() != 0
81    }
82
83    pub fn is_axis_aligned(&self) -> bool {
84        self.is_horizontal() || self.is_vertical()
85    }
86}
87
88#[derive(Default, Clone, Debug)]
89pub struct Edges {
90    edges: Vec<Edge>,
91}
92
93impl Edges {
94    pub fn empty() -> Self {
95        Self::default()
96    }
97
98    pub fn from_edges<I: IntoIterator<Item = Edge>>(it: I) -> Self {
99        Self {
100            edges: it.into_iter().collect(),
101        }
102    }
103
104    /// Edges of a single polygon — the hull plus every hole boundary,
105    /// in traversal order.
106    pub fn from_polygon(p: &Polygon) -> Self {
107        let mut edges = Vec::new();
108        push_loop(&mut edges, &p.hull);
109        for hole in &p.holes {
110            push_loop(&mut edges, hole);
111        }
112        Self { edges }
113    }
114
115    /// Edges of every polygon in a region.
116    pub fn from_region(r: &Region) -> Self {
117        let mut edges = Vec::new();
118        for p in r.polygons() {
119            push_loop(&mut edges, &p.hull);
120            for hole in &p.holes {
121                push_loop(&mut edges, hole);
122            }
123        }
124        Self { edges }
125    }
126
127    pub fn len(&self) -> usize {
128        self.edges.len()
129    }
130
131    pub fn is_empty(&self) -> bool {
132        self.edges.is_empty()
133    }
134
135    pub fn edges(&self) -> &[Edge] {
136        &self.edges
137    }
138
139    pub fn into_inner(self) -> Vec<Edge> {
140        self.edges
141    }
142
143    /// Replace each edge with a `length`-long segment centered at its
144    /// midpoint, oriented along the source edge. Edges shorter than
145    /// `length` are emitted unchanged.
146    pub fn centers(&self, length: i64) -> Edges {
147        if length <= 0 {
148            return Edges::empty();
149        }
150        let mut out = Vec::with_capacity(self.edges.len());
151        for e in &self.edges {
152            let len2 = e.length_squared();
153            if len2 == 0 {
154                continue;
155            }
156            let len = (len2 as f64).sqrt();
157            if (len as i64) <= length {
158                out.push(*e);
159                continue;
160            }
161            let t = length as f64 / (2.0 * len);
162            let mid = e.midpoint();
163            let dx = e.dx() as f64;
164            let dy = e.dy() as f64;
165            let ax = mid.x as f64 - dx * t;
166            let ay = mid.y as f64 - dy * t;
167            let bx = mid.x as f64 + dx * t;
168            let by = mid.y as f64 + dy * t;
169            out.push(Edge::new(
170                Point::new(ax.round() as i64, ay.round() as i64),
171                Point::new(bx.round() as i64, by.round() as i64),
172            ));
173        }
174        Edges::from_edges(out)
175    }
176
177    /// Extend each edge by `before` along the −direction at `a` and by
178    /// `after` along the +direction at `b`. Useful for converting an
179    /// edge to a "ray" with finite extent.
180    pub fn extended(&self, before: i64, after: i64) -> Edges {
181        let mut out = Vec::with_capacity(self.edges.len());
182        for e in &self.edges {
183            let len2 = e.length_squared();
184            if len2 == 0 {
185                out.push(*e);
186                continue;
187            }
188            let len = (len2 as f64).sqrt();
189            let dx = e.dx() as f64 / len;
190            let dy = e.dy() as f64 / len;
191            let ax = (e.a.x as f64) - dx * (before as f64);
192            let ay = (e.a.y as f64) - dy * (before as f64);
193            let bx = (e.b.x as f64) + dx * (after as f64);
194            let by = (e.b.y as f64) + dy * (after as f64);
195            out.push(Edge::new(
196                Point::new(ax.round() as i64, ay.round() as i64),
197                Point::new(bx.round() as i64, by.round() as i64),
198            ));
199        }
200        Edges::from_edges(out)
201    }
202
203    /// Filter edges whose angle (mod 180°) is within `tolerance_deg` of
204    /// `angle_deg`.
205    pub fn with_angle(&self, angle_deg: f64, tolerance_deg: f64) -> Edges {
206        Edges::from_edges(self.edges.iter().copied().filter(|e| {
207            if e.length_squared() == 0 {
208                return false;
209            }
210            angles_close(e.angle_degrees(), angle_deg, tolerance_deg)
211        }))
212    }
213
214    pub fn not_with_angle(&self, angle_deg: f64, tolerance_deg: f64) -> Edges {
215        Edges::from_edges(self.edges.iter().copied().filter(|e| {
216            if e.length_squared() == 0 {
217                return false;
218            }
219            !angles_close(e.angle_degrees(), angle_deg, tolerance_deg)
220        }))
221    }
222
223    pub fn length_at_least(&self, min: i64) -> Edges {
224        let m2 = (min as i128) * (min as i128);
225        Edges::from_edges(
226            self.edges
227                .iter()
228                .copied()
229                .filter(|e| e.length_squared() >= m2),
230        )
231    }
232
233    pub fn length_at_most(&self, max: i64) -> Edges {
234        let m2 = (max as i128) * (max as i128);
235        Edges::from_edges(
236            self.edges
237                .iter()
238                .copied()
239                .filter(|e| e.length_squared() <= m2),
240        )
241    }
242
243    /// Keep edges whose bbox overlaps any edge in `other`. v1 uses bbox
244    /// intersection — sufficient for axis-aligned edge interaction
245    /// (where touching edges share a bbox boundary point).
246    pub fn interacting(&self, other: &Edges) -> Edges {
247        if self.is_empty() || other.is_empty() {
248            return Edges::empty();
249        }
250        let other_bboxes: Vec<Bbox> = other.edges.iter().map(|e| e.bbox()).collect();
251        Edges::from_edges(self.edges.iter().copied().filter(|e| {
252            let eb = e.bbox();
253            other_bboxes.iter().any(|ob| eb.intersects(ob))
254        }))
255    }
256
257    /// Keep the parts of each edge that lie strictly outside `region`.
258    /// Implemented for axis-aligned edges over axis-aligned regions —
259    /// non-axis-aligned edges are returned unchanged.
260    pub fn outside_part(&self, region: &Region) -> Edges {
261        clip_edges(&self.edges, region, false)
262    }
263
264    /// Keep the parts of each edge that lie inside `region`.
265    pub fn inside_part(&self, region: &Region) -> Edges {
266        clip_edges(&self.edges, region, true)
267    }
268}
269
270fn push_loop(out: &mut Vec<Edge>, pts: &[Point]) {
271    let n = pts.len();
272    if n < 2 {
273        return;
274    }
275    for i in 0..n {
276        let a = pts[i];
277        let b = pts[(i + 1) % n];
278        if a != b {
279            out.push(Edge::new(a, b));
280        }
281    }
282}
283
284fn angles_close(a: f64, b: f64, tol: f64) -> bool {
285    let mut diff = (a - b).abs() % 180.0;
286    if diff > 90.0 {
287        diff = 180.0 - diff;
288    }
289    diff <= tol
290}
291
292/// Clip every edge against `region`. `inside == true` keeps the inside
293/// part; `inside == false` keeps the outside part.
294///
295/// For an axis-aligned edge (horizontal or vertical), we walk every
296/// region polygon-edge that is parallel to the axis-aligned edge's
297/// perpendicular and lies in its sweep range, computing crossings.
298/// Non-axis-aligned input edges are passed through unchanged — KLayout
299/// has a more elaborate algorithm that we leave for v2.
300fn clip_edges(edges: &[Edge], region: &Region, inside: bool) -> Edges {
301    if edges.is_empty() {
302        return Edges::empty();
303    }
304    if region.is_empty() {
305        return if inside {
306            Edges::empty()
307        } else {
308            Edges::from_edges(edges.iter().copied())
309        };
310    }
311    let mut out = Vec::with_capacity(edges.len());
312    for e in edges {
313        if !e.is_axis_aligned() {
314            out.push(*e);
315            continue;
316        }
317        let segments = clip_axis_aligned(e, region, inside);
318        out.extend(segments);
319    }
320    Edges::from_edges(out)
321}
322
323fn clip_axis_aligned(e: &Edge, region: &Region, inside: bool) -> Vec<Edge> {
324    let horizontal = e.is_horizontal();
325    let (axis_lo, axis_hi) = if horizontal {
326        let lo = e.a.x.min(e.b.x);
327        let hi = e.a.x.max(e.b.x);
328        (lo, hi)
329    } else {
330        let lo = e.a.y.min(e.b.y);
331        let hi = e.a.y.max(e.b.y);
332        (lo, hi)
333    };
334    let perp = if horizontal { e.a.y } else { e.a.x };
335    let bbox = e.bbox();
336
337    // Collect crossings: each region polygon edge that crosses our
338    // sweep at perp coordinate `perp` contributes a split point.
339    let mut splits: Vec<i64> = vec![axis_lo, axis_hi];
340    for poly in region.polygons() {
341        if !bbox.intersects(&poly.bbox()) {
342            continue;
343        }
344        for loop_pts in std::iter::once(poly.hull.as_slice()).chain(poly.holes.iter().map(|h| h.as_slice())) {
345            let n = loop_pts.len();
346            for i in 0..n {
347                let a = loop_pts[i];
348                let b = loop_pts[(i + 1) % n];
349                if let Some(x) = axis_aligned_crossing(a, b, perp, horizontal, axis_lo, axis_hi) {
350                    splits.push(x);
351                }
352            }
353        }
354    }
355    splits.sort();
356    splits.dedup();
357
358    let mut segments = Vec::new();
359    for w in splits.windows(2) {
360        let (s, t) = (w[0], w[1]);
361        if s == t {
362            continue;
363        }
364        let mid_axis = (s + t) / 2;
365        let mid_point = if horizontal {
366            Point::new(mid_axis, perp)
367        } else {
368            Point::new(perp, mid_axis)
369        };
370        let in_region = region_contains_point(region, mid_point);
371        if inside == in_region {
372            let p_a = if horizontal {
373                Point::new(s, perp)
374            } else {
375                Point::new(perp, s)
376            };
377            let p_b = if horizontal {
378                Point::new(t, perp)
379            } else {
380                Point::new(perp, t)
381            };
382            // Preserve original direction.
383            let forward_axis = if horizontal { e.b.x - e.a.x } else { e.b.y - e.a.y } >= 0;
384            if forward_axis {
385                segments.push(Edge::new(p_a, p_b));
386            } else {
387                segments.push(Edge::new(p_b, p_a));
388            }
389        }
390    }
391    segments
392}
393
394/// If the segment `a → b` crosses the perpendicular sweep at coordinate
395/// `perp` (y-coord if `horizontal`, else x-coord) and lands within the
396/// edge's axis range `[lo, hi]`, return the crossing axis coordinate.
397fn axis_aligned_crossing(
398    a: Point,
399    b: Point,
400    perp: i64,
401    horizontal: bool,
402    lo: i64,
403    hi: i64,
404) -> Option<i64> {
405    let (perp_a, perp_b, axis_a, axis_b) = if horizontal {
406        (a.y, b.y, a.x, b.x)
407    } else {
408        (a.x, b.x, a.y, b.y)
409    };
410    if perp_a == perp_b {
411        // Collinear — overlap range; return interior split points.
412        if perp_a == perp {
413            let lo_seg = axis_a.min(axis_b).max(lo);
414            let hi_seg = axis_a.max(axis_b).min(hi);
415            if lo_seg < hi_seg {
416                // Two split points — both endpoints. Collinearity
417                // causes both to be in `splits` already via lo/hi or
418                // adjacent edges.
419                return Some(lo_seg);
420            }
421        }
422        return None;
423    }
424    if (perp_a < perp && perp_b < perp) || (perp_a > perp && perp_b > perp) {
425        return None;
426    }
427    let t = (perp - perp_a) as f64 / (perp_b - perp_a) as f64;
428    let axis = (axis_a as f64 + t * (axis_b as f64 - axis_a as f64)).round() as i64;
429    if axis < lo || axis > hi {
430        return None;
431    }
432    Some(axis)
433}
434
435fn region_contains_point(r: &Region, p: Point) -> bool {
436    for poly in r.polygons() {
437        if !poly.bbox().contains(p) {
438            continue;
439        }
440        if point_in_polygon(p, &poly.hull) {
441            let in_hole = poly.holes.iter().any(|h| point_in_polygon(p, h));
442            if !in_hole {
443                return true;
444            }
445        }
446    }
447    false
448}
449
450fn point_in_polygon(p: Point, ring: &[Point]) -> bool {
451    // Even-odd ray-casting from `p` to `+x`. Robust enough for axis-
452    // aligned and rotated polygons over integer coords.
453    let mut inside = false;
454    let n = ring.len();
455    if n < 3 {
456        return false;
457    }
458    for i in 0..n {
459        let a = ring[i];
460        let b = ring[(i + 1) % n];
461        let crosses = (a.y > p.y) != (b.y > p.y);
462        if crosses {
463            let t = (p.y - a.y) as f64 / (b.y - a.y) as f64;
464            let x_at = a.x as f64 + t * (b.x - a.x) as f64;
465            if x_at > p.x as f64 {
466                inside = !inside;
467            }
468        }
469    }
470    inside
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use klayout_core::Polygon;
477
478    fn pt(x: i64, y: i64) -> Point {
479        Point::new(x, y)
480    }
481
482    #[test]
483    fn polygon_edges_are_directed() {
484        let p = Polygon::rect(Bbox::new(pt(0, 0), pt(10, 5)));
485        let e = Edges::from_polygon(&p);
486        assert_eq!(e.len(), 4);
487        // For a CCW rect (0,0) (10,0) (10,5) (0,5): edges are
488        // east, north, west, south.
489        let dirs: Vec<(i64, i64)> = e.edges().iter().map(|x| (x.dx(), x.dy())).collect();
490        assert!(dirs.contains(&(10, 0)));
491        assert!(dirs.contains(&(0, 5)));
492        assert!(dirs.contains(&(-10, 0)));
493        assert!(dirs.contains(&(0, -5)));
494    }
495
496    #[test]
497    fn with_angle_filters_horizontals() {
498        let p = Polygon::rect(Bbox::new(pt(0, 0), pt(10, 5)));
499        let e = Edges::from_polygon(&p);
500        let h = e.with_angle(0.0, 1.0);
501        assert_eq!(h.len(), 2); // east + west
502        let v = e.with_angle(90.0, 1.0);
503        assert_eq!(v.len(), 2); // north + south
504    }
505
506    #[test]
507    fn length_filter() {
508        let p = Polygon::rect(Bbox::new(pt(0, 0), pt(10, 5)));
509        let e = Edges::from_polygon(&p);
510        assert_eq!(e.length_at_least(8).len(), 2); // only the 10-long edges
511        assert_eq!(e.length_at_most(6).len(), 2); // only the 5-long edges
512    }
513
514    #[test]
515    fn centers_emits_centered_segment() {
516        let e = Edges::from_edges([Edge::new(pt(0, 0), pt(10, 0))]);
517        let c = e.centers(2);
518        assert_eq!(c.len(), 1);
519        let seg = c.edges()[0];
520        assert_eq!(seg.a, pt(4, 0));
521        assert_eq!(seg.b, pt(6, 0));
522    }
523
524    #[test]
525    fn extended_grows_endpoints() {
526        let e = Edges::from_edges([Edge::new(pt(0, 0), pt(10, 0))]);
527        let x = e.extended(2, 3);
528        assert_eq!(x.edges()[0].a, pt(-2, 0));
529        assert_eq!(x.edges()[0].b, pt(13, 0));
530    }
531
532    #[test]
533    fn interacting_finds_overlapping_bboxes() {
534        let a = Edges::from_edges([Edge::new(pt(0, 0), pt(10, 0))]);
535        let b = Edges::from_edges([
536            Edge::new(pt(5, 0), pt(5, 10)),
537            Edge::new(pt(100, 100), pt(110, 100)),
538        ]);
539        let i = a.interacting(&b);
540        assert_eq!(i.len(), 1);
541    }
542
543    #[test]
544    fn outside_part_clips_against_region() {
545        // Edge from (0,0) to (20,0); region covers x in [5,15].
546        let e = Edges::from_edges([Edge::new(pt(0, 0), pt(20, 0))]);
547        let r = Region::from_polygons([Polygon::rect(Bbox::new(pt(5, -5), pt(15, 5)))]);
548        let out = e.outside_part(&r);
549        // Two outside segments: [0,5] and [15,20].
550        assert_eq!(out.len(), 2);
551    }
552
553    #[test]
554    fn inside_part_clips_against_region() {
555        let e = Edges::from_edges([Edge::new(pt(0, 0), pt(20, 0))]);
556        let r = Region::from_polygons([Polygon::rect(Bbox::new(pt(5, -5), pt(15, 5)))]);
557        let inside = e.inside_part(&r);
558        assert_eq!(inside.len(), 1);
559        let seg = inside.edges()[0];
560        assert_eq!(seg.bbox().min.x, 5);
561        assert_eq!(seg.bbox().max.x, 15);
562    }
563}