Skip to main content

clipper2_rust/
rectclip.rs

1//! FAST rectangular clipping implementation
2//!
3//! Direct port from clipper.rectclip.h and clipper.rectclip.cpp
4//! Copyright (c) Angus Johnson 2010-2025
5//! Provides high-performance rectangle clipping functionality
6
7use crate::core::*;
8
9// ============================================================================
10// OutPt2 - Arena-allocated doubly-linked circular list node
11// Direct port from clipper.rectclip.h line 25
12// ============================================================================
13
14/// Output point for rectangle clipping, using arena indices instead of raw pointers
15/// Direct port from clipper.rectclip.h line 25
16struct OutPt2 {
17    pt: Point64,
18    owner_idx: usize,
19    /// Which edge array (0-7) this point belongs to, or None
20    edge_idx: Option<usize>,
21    /// Index of next OutPt2 in the arena
22    next: usize,
23    /// Index of previous OutPt2 in the arena
24    prev: usize,
25}
26
27impl OutPt2 {
28    fn new(pt: Point64) -> Self {
29        Self {
30            pt,
31            owner_idx: 0,
32            edge_idx: None,
33            next: 0,
34            prev: 0,
35        }
36    }
37}
38
39// ============================================================================
40// Free helper functions
41// Direct port from clipper.rectclip.cpp lines 19-311
42// ============================================================================
43
44/// Check if path1 contains path2
45/// Direct port from clipper.rectclip.cpp line 19
46fn path1_contains_path2(path1: &Path64, path2: &Path64) -> bool {
47    let mut io_count = 0i32;
48    for pt in path2 {
49        let pip = point_in_polygon(*pt, path1);
50        match pip {
51            PointInPolygonResult::IsOutside => io_count += 1,
52            PointInPolygonResult::IsInside => io_count -= 1,
53            _ => continue,
54        }
55        if io_count.abs() > 1 {
56            break;
57        }
58    }
59    io_count <= 0
60}
61
62/// Get segment intersection with improved edge-case handling for rect clipping
63/// Direct port from clipper.rectclip.cpp line 73
64fn get_segment_intersection(
65    p1: Point64,
66    p2: Point64,
67    p3: Point64,
68    p4: Point64,
69    ip: &mut Point64,
70) -> bool {
71    let res1 = cross_product_three_points(p1, p3, p4);
72    let res2 = cross_product_three_points(p2, p3, p4);
73    if res1 == 0.0 {
74        *ip = p1;
75        if res2 == 0.0 {
76            return false; // segments are collinear
77        } else if p1 == p3 || p1 == p4 {
78            return true;
79        } else if is_horizontal(&p3, &p4) {
80            return (p1.x > p3.x) == (p1.x < p4.x);
81        } else {
82            return (p1.y > p3.y) == (p1.y < p4.y);
83        }
84    } else if res2 == 0.0 {
85        *ip = p2;
86        if p2 == p3 || p2 == p4 {
87            return true;
88        } else if is_horizontal(&p3, &p4) {
89            return (p2.x > p3.x) == (p2.x < p4.x);
90        } else {
91            return (p2.y > p3.y) == (p2.y < p4.y);
92        }
93    }
94    if (res1 > 0.0) == (res2 > 0.0) {
95        return false;
96    }
97
98    let res3 = cross_product_three_points(p3, p1, p2);
99    let res4 = cross_product_three_points(p4, p1, p2);
100    if res3 == 0.0 {
101        *ip = p3;
102        if p3 == p1 || p3 == p2 {
103            return true;
104        } else if is_horizontal(&p1, &p2) {
105            return (p3.x > p1.x) == (p3.x < p2.x);
106        } else {
107            return (p3.y > p1.y) == (p3.y < p2.y);
108        }
109    } else if res4 == 0.0 {
110        *ip = p4;
111        if p4 == p1 || p4 == p2 {
112            return true;
113        } else if is_horizontal(&p1, &p2) {
114            return (p4.x > p1.x) == (p4.x < p2.x);
115        } else {
116            return (p4.y > p1.y) == (p4.y < p2.y);
117        }
118    }
119    if (res3 > 0.0) == (res4 > 0.0) {
120        return false;
121    }
122
123    // segments must intersect to get here
124    get_segment_intersect_pt(p1, p2, p3, p4, ip)
125}
126
127/// Get intersection of a point-pair with the rect boundary closest to 'p'
128/// Direct port from clipper.rectclip.cpp line 118
129fn get_intersection(
130    rect_path: &Path64,
131    p: Point64,
132    p2: Point64,
133    loc: &mut Location,
134    ip: &mut Point64,
135) -> bool {
136    match *loc {
137        Location::Left => {
138            if get_segment_intersection(p, p2, rect_path[0], rect_path[3], ip) {
139                return true;
140            } else if p.y < rect_path[0].y
141                && get_segment_intersection(p, p2, rect_path[0], rect_path[1], ip)
142            {
143                *loc = Location::Top;
144                return true;
145            } else if get_segment_intersection(p, p2, rect_path[2], rect_path[3], ip) {
146                *loc = Location::Bottom;
147                return true;
148            }
149            false
150        }
151        Location::Top => {
152            if get_segment_intersection(p, p2, rect_path[0], rect_path[1], ip) {
153                return true;
154            } else if p.x < rect_path[0].x
155                && get_segment_intersection(p, p2, rect_path[0], rect_path[3], ip)
156            {
157                *loc = Location::Left;
158                return true;
159            } else if get_segment_intersection(p, p2, rect_path[1], rect_path[2], ip) {
160                *loc = Location::Right;
161                return true;
162            }
163            false
164        }
165        Location::Right => {
166            if get_segment_intersection(p, p2, rect_path[1], rect_path[2], ip) {
167                return true;
168            } else if p.y < rect_path[1].y
169                && get_segment_intersection(p, p2, rect_path[0], rect_path[1], ip)
170            {
171                *loc = Location::Top;
172                return true;
173            } else if get_segment_intersection(p, p2, rect_path[2], rect_path[3], ip) {
174                *loc = Location::Bottom;
175                return true;
176            }
177            false
178        }
179        Location::Bottom => {
180            if get_segment_intersection(p, p2, rect_path[2], rect_path[3], ip) {
181                return true;
182            } else if p.x < rect_path[3].x
183                && get_segment_intersection(p, p2, rect_path[0], rect_path[3], ip)
184            {
185                *loc = Location::Left;
186                return true;
187            } else if get_segment_intersection(p, p2, rect_path[1], rect_path[2], ip) {
188                *loc = Location::Right;
189                return true;
190            }
191            false
192        }
193        Location::Inside => {
194            if get_segment_intersection(p, p2, rect_path[0], rect_path[3], ip) {
195                *loc = Location::Left;
196                return true;
197            } else if get_segment_intersection(p, p2, rect_path[0], rect_path[1], ip) {
198                *loc = Location::Top;
199                return true;
200            } else if get_segment_intersection(p, p2, rect_path[1], rect_path[2], ip) {
201                *loc = Location::Right;
202                return true;
203            } else if get_segment_intersection(p, p2, rect_path[2], rect_path[3], ip) {
204                *loc = Location::Bottom;
205                return true;
206            }
207            false
208        }
209    }
210}
211
212/// Get adjacent location (clockwise or counter-clockwise)
213/// Direct port from clipper.rectclip.cpp line 206
214#[inline]
215fn get_adjacent_location(loc: Location, is_clockwise: bool) -> Location {
216    let delta = if is_clockwise { 1 } else { 3 };
217    let idx = (loc as i32 + delta) % 4;
218    match idx {
219        0 => Location::Left,
220        1 => Location::Top,
221        2 => Location::Right,
222        3 => Location::Bottom,
223        _ => unreachable!(),
224    }
225}
226
227/// Check if heading is clockwise between two locations
228/// Direct port from clipper.rectclip.cpp line 212
229#[inline]
230fn heading_clockwise(prev: Location, curr: Location) -> bool {
231    (prev as i32 + 1) % 4 == curr as i32
232}
233
234/// Check if two locations are opposite
235/// Direct port from clipper.rectclip.cpp line 217
236#[inline]
237fn are_opposites(prev: Location, curr: Location) -> bool {
238    (prev as i32 - curr as i32).abs() == 2
239}
240
241/// Determine if the path from prev to curr is clockwise
242/// Direct port from clipper.rectclip.cpp line 222
243#[inline]
244fn is_clockwise_dir(
245    prev: Location,
246    curr: Location,
247    prev_pt: &Point64,
248    curr_pt: &Point64,
249    rect_mp: &Point64,
250) -> bool {
251    if are_opposites(prev, curr) {
252        cross_product_three_points(*prev_pt, *rect_mp, *curr_pt) < 0.0
253    } else {
254        heading_clockwise(prev, curr)
255    }
256}
257
258/// Get which edges of the rectangle the point is on (bitmask)
259/// Direct port from clipper.rectclip.cpp line 247
260#[inline]
261fn get_edges_for_pt(pt: &Point64, rec: &Rect64) -> u32 {
262    let mut result = 0u32;
263    if pt.x == rec.left {
264        result = 1;
265    } else if pt.x == rec.right {
266        result = 4;
267    }
268    if pt.y == rec.top {
269        result += 2;
270    } else if pt.y == rec.bottom {
271        result += 8;
272    }
273    result
274}
275
276/// Check if heading is clockwise along a specific edge
277/// Direct port from clipper.rectclip.cpp line 257
278#[inline]
279fn is_heading_clockwise(pt1: &Point64, pt2: &Point64, edge_idx: i32) -> bool {
280    match edge_idx {
281        0 => pt2.y < pt1.y,
282        1 => pt2.x > pt1.x,
283        2 => pt2.y > pt1.y,
284        _ => pt2.x < pt1.x,
285    }
286}
287
288/// Check for horizontal overlap between two segments
289/// Direct port from clipper.rectclip.cpp line 268
290#[inline]
291fn has_horz_overlap(left1: &Point64, right1: &Point64, left2: &Point64, right2: &Point64) -> bool {
292    (left1.x < right2.x) && (right1.x > left2.x)
293}
294
295/// Check for vertical overlap between two segments
296/// Direct port from clipper.rectclip.cpp line 274
297#[inline]
298fn has_vert_overlap(top1: &Point64, bottom1: &Point64, top2: &Point64, bottom2: &Point64) -> bool {
299    (top1.y < bottom2.y) && (bottom1.y > top2.y)
300}
301
302/// Check if start locations indicate a clockwise direction
303/// Direct port from clipper.rectclip.cpp line 426
304fn start_locs_are_clockwise(start_locs: &[Location]) -> bool {
305    let mut result = 0i32;
306    for i in 1..start_locs.len() {
307        let d = start_locs[i] as i32 - start_locs[i - 1] as i32;
308        match d {
309            -1 => result -= 1,
310            1 => result += 1,
311            -3 => result += 1,
312            3 => result -= 1,
313            _ => {}
314        }
315    }
316    result > 0
317}
318
319// ============================================================================
320// RectClip64 - Main rectangular clipper for polygon clipping
321// Direct port from clipper.rectclip.h line 38
322// ============================================================================
323
324/// Main rectangular clipper class for polygon clipping
325/// Direct port from clipper.rectclip.h line 38
326pub struct RectClip64 {
327    rect: Rect64,
328    rect_as_path: Path64,
329    rect_mp: Point64,
330    path_bounds: Rect64,
331    arena: Vec<OutPt2>,
332    results: Vec<Option<usize>>,
333    edges: [Vec<Option<usize>>; 8],
334    start_locs: Vec<Location>,
335}
336
337impl RectClip64 {
338    /// Create new rectangular clipper
339    /// Direct port from clipper.rectclip.h line 59
340    pub fn new(rect: Rect64) -> Self {
341        let rect_as_path = rect.as_path();
342        let rect_mp = rect.mid_point();
343        Self {
344            rect,
345            rect_as_path,
346            rect_mp,
347            path_bounds: Rect64::new(0, 0, 0, 0),
348            arena: Vec::new(),
349            results: Vec::new(),
350            edges: Default::default(),
351            start_locs: Vec::new(),
352        }
353    }
354
355    /// Clear all internal state for reuse
356    fn clear(&mut self) {
357        self.arena.clear();
358        self.results.clear();
359        for edge in &mut self.edges {
360            edge.clear();
361        }
362        self.start_locs.clear();
363    }
364
365    /// Add a point to the current result path
366    /// Direct port from clipper.rectclip.cpp line 317
367    fn add(&mut self, pt: Point64, start_new: bool) -> usize {
368        let curr_idx = self.results.len();
369        if curr_idx == 0 || start_new {
370            let new_idx = self.arena.len();
371            let mut op = OutPt2::new(pt);
372            op.next = new_idx;
373            op.prev = new_idx;
374            self.arena.push(op);
375            self.results.push(Some(new_idx));
376            new_idx
377        } else {
378            let result_idx = curr_idx - 1;
379            let prev_op_idx = self.results[result_idx].unwrap();
380            if self.arena[prev_op_idx].pt == pt {
381                return prev_op_idx;
382            }
383            let new_idx = self.arena.len();
384            let mut op = OutPt2::new(pt);
385            op.owner_idx = result_idx;
386
387            // Insert after prev_op in the circular list
388            let prev_next = self.arena[prev_op_idx].next;
389            op.next = prev_next;
390            op.prev = prev_op_idx;
391            self.arena.push(op);
392
393            self.arena[prev_next].prev = new_idx;
394            self.arena[prev_op_idx].next = new_idx;
395
396            self.results[result_idx] = Some(new_idx);
397            new_idx
398        }
399    }
400
401    /// Add a corner of the rectangle
402    /// Direct port from clipper.rectclip.cpp line 349
403    fn add_corner_prev_curr(&mut self, prev: Location, curr: Location) {
404        if heading_clockwise(prev, curr) {
405            self.add(self.rect_as_path[prev as usize], false);
406        } else {
407            self.add(self.rect_as_path[curr as usize], false);
408        }
409    }
410
411    /// Add a corner and advance location
412    /// Direct port from clipper.rectclip.cpp line 357
413    fn add_corner_loc(&mut self, loc: &mut Location, is_clockwise: bool) {
414        if is_clockwise {
415            let pt = self.rect_as_path[*loc as usize];
416            self.add(pt, false);
417            *loc = get_adjacent_location(*loc, true);
418        } else {
419            *loc = get_adjacent_location(*loc, false);
420            let pt = self.rect_as_path[*loc as usize];
421            self.add(pt, false);
422        }
423    }
424
425    /// Get next location along the path
426    /// Direct port from clipper.rectclip.cpp line 371
427    fn get_next_location(
428        &mut self,
429        path: &Path64,
430        loc: &mut Location,
431        i: &mut usize,
432        high_i: usize,
433    ) {
434        match *loc {
435            Location::Left => {
436                while *i <= high_i && path[*i].x <= self.rect.left {
437                    *i += 1;
438                }
439                if *i > high_i {
440                    return;
441                }
442                if path[*i].x >= self.rect.right {
443                    *loc = Location::Right;
444                } else if path[*i].y <= self.rect.top {
445                    *loc = Location::Top;
446                } else if path[*i].y >= self.rect.bottom {
447                    *loc = Location::Bottom;
448                } else {
449                    *loc = Location::Inside;
450                }
451            }
452            Location::Top => {
453                while *i <= high_i && path[*i].y <= self.rect.top {
454                    *i += 1;
455                }
456                if *i > high_i {
457                    return;
458                }
459                if path[*i].y >= self.rect.bottom {
460                    *loc = Location::Bottom;
461                } else if path[*i].x <= self.rect.left {
462                    *loc = Location::Left;
463                } else if path[*i].x >= self.rect.right {
464                    *loc = Location::Right;
465                } else {
466                    *loc = Location::Inside;
467                }
468            }
469            Location::Right => {
470                while *i <= high_i && path[*i].x >= self.rect.right {
471                    *i += 1;
472                }
473                if *i > high_i {
474                    return;
475                }
476                if path[*i].x <= self.rect.left {
477                    *loc = Location::Left;
478                } else if path[*i].y <= self.rect.top {
479                    *loc = Location::Top;
480                } else if path[*i].y >= self.rect.bottom {
481                    *loc = Location::Bottom;
482                } else {
483                    *loc = Location::Inside;
484                }
485            }
486            Location::Bottom => {
487                while *i <= high_i && path[*i].y >= self.rect.bottom {
488                    *i += 1;
489                }
490                if *i > high_i {
491                    return;
492                }
493                if path[*i].y <= self.rect.top {
494                    *loc = Location::Top;
495                } else if path[*i].x <= self.rect.left {
496                    *loc = Location::Left;
497                } else if path[*i].x >= self.rect.right {
498                    *loc = Location::Right;
499                } else {
500                    *loc = Location::Inside;
501                }
502            }
503            Location::Inside => {
504                while *i <= high_i {
505                    if path[*i].x < self.rect.left {
506                        *loc = Location::Left;
507                    } else if path[*i].x > self.rect.right {
508                        *loc = Location::Right;
509                    } else if path[*i].y > self.rect.bottom {
510                        *loc = Location::Bottom;
511                    } else if path[*i].y < self.rect.top {
512                        *loc = Location::Top;
513                    } else {
514                        let pt = path[*i];
515                        self.add(pt, false);
516                        *i += 1;
517                        continue;
518                    }
519                    break;
520                }
521            }
522        }
523    }
524
525    /// Unlink a node from the circular list, return the next node (or None if list becomes empty)
526    /// Direct port from clipper.rectclip.cpp line 231
527    fn unlink_op(&mut self, op_idx: usize) -> Option<usize> {
528        let next = self.arena[op_idx].next;
529        if next == op_idx {
530            return None;
531        }
532        let prev = self.arena[op_idx].prev;
533        self.arena[prev].next = next;
534        self.arena[next].prev = prev;
535        Some(next)
536    }
537
538    /// Unlink a node from the circular list, return the prev node (or None if list becomes empty)
539    /// Direct port from clipper.rectclip.cpp line 239
540    fn unlink_op_back(&mut self, op_idx: usize) -> Option<usize> {
541        let next = self.arena[op_idx].next;
542        if next == op_idx {
543            return None;
544        }
545        let prev = self.arena[op_idx].prev;
546        self.arena[prev].next = next;
547        self.arena[next].prev = prev;
548        Some(prev)
549    }
550
551    /// Add a point to an edge list
552    /// Direct port from clipper.rectclip.cpp line 280
553    fn add_to_edge(&mut self, edge_idx: usize, op_idx: usize) {
554        if self.arena[op_idx].edge_idx.is_some() {
555            return;
556        }
557        self.arena[op_idx].edge_idx = Some(edge_idx);
558        self.edges[edge_idx].push(Some(op_idx));
559    }
560
561    /// Remove a point from its edge list
562    /// Direct port from clipper.rectclip.cpp line 287
563    fn uncouple_edge(&mut self, op_idx: usize) {
564        let edge_idx = match self.arena[op_idx].edge_idx {
565            Some(idx) => idx,
566            None => return,
567        };
568        for i in 0..self.edges[edge_idx].len() {
569            if self.edges[edge_idx][i] == Some(op_idx) {
570                self.edges[edge_idx][i] = None;
571                break;
572            }
573        }
574        self.arena[op_idx].edge_idx = None;
575    }
576
577    /// Set new owner for all nodes in a circular list
578    /// Direct port from clipper.rectclip.cpp line 302
579    fn set_new_owner(&mut self, op_idx: usize, new_idx: usize) {
580        self.arena[op_idx].owner_idx = new_idx;
581        let mut op2 = self.arena[op_idx].next;
582        while op2 != op_idx {
583            self.arena[op2].owner_idx = new_idx;
584            op2 = self.arena[op2].next;
585        }
586    }
587
588    /// Internal execution for polygon clipping against a single path
589    /// Direct port from clipper.rectclip.cpp line 443
590    fn execute_internal(&mut self, path: &Path64) {
591        if path.is_empty() {
592            return;
593        }
594
595        let high_i = path.len() - 1;
596        let mut prev = Location::Inside;
597        let mut loc = Location::Inside;
598        let mut crossing_loc = Location::Inside;
599        let mut first_cross = Location::Inside;
600
601        if !get_location(&self.rect, &path[high_i], &mut loc) {
602            let mut i = high_i;
603            while i > 0 && !get_location(&self.rect, &path[i - 1], &mut prev) {
604                i -= 1;
605            }
606            if i == 0 {
607                // all of path must be inside rect
608                for pt in path {
609                    self.add(*pt, false);
610                }
611                return;
612            }
613            if prev == Location::Inside {
614                loc = Location::Inside;
615            }
616        }
617        let starting_loc = loc;
618
619        let mut i = 0usize;
620        while i <= high_i {
621            prev = loc;
622            let crossing_prev = crossing_loc;
623
624            self.get_next_location(path, &mut loc, &mut i, high_i);
625
626            if i > high_i {
627                break;
628            }
629            let mut ip = Point64::new(0, 0);
630            let mut ip2 = Point64::new(0, 0);
631            let prev_pt = if i > 0 { path[i - 1] } else { path[high_i] };
632
633            crossing_loc = loc;
634            if !get_intersection(
635                &self.rect_as_path.clone(),
636                path[i],
637                prev_pt,
638                &mut crossing_loc,
639                &mut ip,
640            ) {
641                // ie remaining outside
642                if crossing_prev == Location::Inside {
643                    let is_clockw = is_clockwise_dir(prev, loc, &prev_pt, &path[i], &self.rect_mp);
644                    let mut p = prev;
645                    loop {
646                        self.start_locs.push(p);
647                        p = get_adjacent_location(p, is_clockw);
648                        if p == loc {
649                            break;
650                        }
651                    }
652                    crossing_loc = crossing_prev; // still not crossed
653                } else if prev != Location::Inside && prev != loc {
654                    let is_clockw = is_clockwise_dir(prev, loc, &prev_pt, &path[i], &self.rect_mp);
655                    let mut p = prev;
656                    loop {
657                        self.add_corner_loc(&mut p, is_clockw);
658                        if p == loc {
659                            break;
660                        }
661                    }
662                }
663                i += 1;
664                continue;
665            }
666
667            // we must be crossing the rect boundary to get here
668
669            if loc == Location::Inside {
670                // path must be entering rect
671                if first_cross == Location::Inside {
672                    first_cross = crossing_loc;
673                    self.start_locs.push(prev);
674                } else if prev != crossing_loc {
675                    let is_clockw =
676                        is_clockwise_dir(prev, crossing_loc, &prev_pt, &path[i], &self.rect_mp);
677                    let mut p = prev;
678                    loop {
679                        self.add_corner_loc(&mut p, is_clockw);
680                        if p == crossing_loc {
681                            break;
682                        }
683                    }
684                }
685            } else if prev != Location::Inside {
686                // passing right through rect
687                loc = prev;
688                let rect_as_path = self.rect_as_path.clone();
689                get_intersection(&rect_as_path, prev_pt, path[i], &mut loc, &mut ip2);
690
691                if crossing_prev != Location::Inside && crossing_prev != loc {
692                    self.add_corner_prev_curr(crossing_prev, loc);
693                }
694
695                if first_cross == Location::Inside {
696                    first_cross = loc;
697                    self.start_locs.push(prev);
698                }
699
700                loc = crossing_loc;
701                self.add(ip2, false);
702                if ip == ip2 {
703                    // it's very likely that path[i] is on rect
704                    get_location(&self.rect, &path[i], &mut loc);
705                    self.add_corner_prev_curr(crossing_loc, loc);
706                    crossing_loc = loc;
707                    continue;
708                }
709            } else {
710                // path must be exiting rect
711                loc = crossing_loc;
712                if first_cross == Location::Inside {
713                    first_cross = crossing_loc;
714                }
715            }
716
717            self.add(ip, false);
718        } // while i <= high_i
719
720        if first_cross == Location::Inside {
721            // path never intersects
722            if starting_loc != Location::Inside {
723                // path is outside rect but may contain it
724                if self.path_bounds.contains_point(&self.rect_mp)
725                    && path1_contains_path2(path, &self.rect_as_path)
726                {
727                    let is_clockwise_path = start_locs_are_clockwise(&self.start_locs);
728                    for j in 0..4usize {
729                        let k = if is_clockwise_path { j } else { 3 - j };
730                        let pt = self.rect_as_path[k];
731                        self.add(pt, false);
732                        let results_0 = self.results[0].unwrap();
733                        self.add_to_edge(k * 2, results_0);
734                    }
735                }
736            }
737        } else if loc != Location::Inside && (loc != first_cross || self.start_locs.len() > 2) {
738            if !self.start_locs.is_empty() {
739                let mut p = loc;
740                let start_locs_clone = self.start_locs.clone();
741                for &loc2 in &start_locs_clone {
742                    if p == loc2 {
743                        continue;
744                    }
745                    // C++ calls AddCorner(prev, HeadingClockwise(prev, loc2))
746                    // which is the (Location&, bool) overload, not (Location, Location)
747                    let hcw = heading_clockwise(p, loc2);
748                    self.add_corner_loc(&mut p, hcw);
749                    p = loc2;
750                }
751                loc = p;
752            }
753            if loc != first_cross {
754                // C++ calls AddCorner(loc, HeadingClockwise(loc, first_cross_))
755                // which is the (Location&, bool) overload
756                let hcw = heading_clockwise(loc, first_cross);
757                self.add_corner_loc(&mut loc, hcw);
758            }
759        }
760    }
761
762    /// Check edges after internal execution
763    /// Direct port from clipper.rectclip.cpp line 606
764    fn check_edges(&mut self) {
765        for i in 0..self.results.len() {
766            let mut op_idx = match self.results[i] {
767                Some(idx) => idx,
768                None => continue,
769            };
770
771            // Remove collinear points
772            let mut op2_idx = op_idx;
773            loop {
774                let prev_idx = self.arena[op2_idx].prev;
775                let next_idx = self.arena[op2_idx].next;
776                let prev_pt = self.arena[prev_idx].pt;
777                let op2_pt = self.arena[op2_idx].pt;
778                let next_pt = self.arena[next_idx].pt;
779
780                if is_collinear(prev_pt, op2_pt, next_pt) {
781                    if op2_idx == op_idx {
782                        match self.unlink_op_back(op2_idx) {
783                            Some(new_idx) => {
784                                op2_idx = new_idx;
785                                op_idx = self.arena[op2_idx].prev;
786                            }
787                            None => {
788                                op2_idx = usize::MAX; // signal break
789                                break;
790                            }
791                        }
792                    } else {
793                        match self.unlink_op_back(op2_idx) {
794                            Some(new_idx) => op2_idx = new_idx,
795                            None => {
796                                op2_idx = usize::MAX;
797                                break;
798                            }
799                        }
800                    }
801                } else {
802                    op2_idx = self.arena[op2_idx].next;
803                }
804
805                if op2_idx == op_idx {
806                    break;
807                }
808            }
809
810            if op2_idx == usize::MAX {
811                self.results[i] = None;
812                continue;
813            }
814            self.results[i] = Some(op_idx);
815
816            // Assign edges
817            let prev_idx = self.arena[op_idx].prev;
818            let mut edge_set1 = get_edges_for_pt(&self.arena[prev_idx].pt, &self.rect);
819            let mut op2_idx = op_idx;
820            loop {
821                let edge_set2 = get_edges_for_pt(&self.arena[op2_idx].pt, &self.rect);
822                if edge_set2 != 0 && self.arena[op2_idx].edge_idx.is_none() {
823                    let combined_set = edge_set1 & edge_set2;
824                    for j in 0..4i32 {
825                        if combined_set & (1 << j) != 0 {
826                            let prev_idx = self.arena[op2_idx].prev;
827                            let prev_pt = self.arena[prev_idx].pt;
828                            let op2_pt = self.arena[op2_idx].pt;
829                            if is_heading_clockwise(&prev_pt, &op2_pt, j) {
830                                self.add_to_edge(j as usize * 2, op2_idx);
831                            } else {
832                                self.add_to_edge(j as usize * 2 + 1, op2_idx);
833                            }
834                        }
835                    }
836                }
837                edge_set1 = edge_set2;
838                op2_idx = self.arena[op2_idx].next;
839
840                if op2_idx == op_idx {
841                    break;
842                }
843            }
844        }
845    }
846
847    /// Tidy edges by merging/splitting where cw and ccw edges overlap
848    /// Direct port from clipper.rectclip.cpp line 665
849    fn tidy_edges(&mut self, idx: usize, cw_idx: usize, ccw_idx: usize) {
850        if self.edges[ccw_idx].is_empty() {
851            return;
852        }
853
854        let is_horz = idx == 1 || idx == 3;
855        let cw_is_toward_larger = idx == 1 || idx == 2;
856        let mut i = 0usize;
857        let mut j = 0usize;
858
859        while i < self.edges[cw_idx].len() {
860            let p1_root = match self.edges[cw_idx][i] {
861                Some(idx) => idx,
862                None => {
863                    i += 1;
864                    j = 0;
865                    continue;
866                }
867            };
868
869            // Check if degenerate (next == prev)
870            if self.arena[p1_root].next == self.arena[p1_root].prev {
871                self.edges[cw_idx][i] = None;
872                i += 1;
873                j = 0;
874                continue;
875            }
876
877            let j_lim = self.edges[ccw_idx].len();
878            while j < j_lim {
879                match self.edges[ccw_idx][j] {
880                    Some(idx) if self.arena[idx].next != self.arena[idx].prev => break,
881                    _ => j += 1,
882                }
883            }
884
885            if j == j_lim {
886                i += 1;
887                j = 0;
888                continue;
889            }
890
891            let p2_root = self.edges[ccw_idx][j].unwrap();
892
893            let (p1, p1a, p2, p2a);
894            if cw_is_toward_larger {
895                p1 = self.arena[p1_root].prev;
896                p1a = p1_root;
897                p2 = p2_root;
898                p2a = self.arena[p2_root].prev;
899            } else {
900                p1 = p1_root;
901                p1a = self.arena[p1_root].prev;
902                p2 = self.arena[p2_root].prev;
903                p2a = p2_root;
904            }
905
906            let p1_pt = self.arena[p1].pt;
907            let p1a_pt = self.arena[p1a].pt;
908            let p2_pt = self.arena[p2].pt;
909            let p2a_pt = self.arena[p2a].pt;
910
911            if (is_horz && !has_horz_overlap(&p1_pt, &p1a_pt, &p2_pt, &p2a_pt))
912                || (!is_horz && !has_vert_overlap(&p1_pt, &p1a_pt, &p2_pt, &p2a_pt))
913            {
914                j += 1;
915                continue;
916            }
917
918            // To get here we're either splitting or rejoining
919            let is_rejoining = self.arena[p1_root].owner_idx != self.arena[p2_root].owner_idx;
920
921            if is_rejoining {
922                let p2_owner = self.arena[p2].owner_idx;
923                let p1_owner = self.arena[p1].owner_idx;
924                self.results[p2_owner] = None;
925                self.set_new_owner(p2, p1_owner);
926            }
927
928            // Do the split or re-join
929            if cw_is_toward_larger {
930                self.arena[p1].next = p2;
931                self.arena[p2].prev = p1;
932                self.arena[p1a].prev = p2a;
933                self.arena[p2a].next = p1a;
934            } else {
935                self.arena[p1].prev = p2;
936                self.arena[p2].next = p1;
937                self.arena[p1a].next = p2a;
938                self.arena[p2a].prev = p1a;
939            }
940
941            if !is_rejoining {
942                let new_idx = self.results.len();
943                self.results.push(Some(p1a));
944                self.set_new_owner(p1a, new_idx);
945            }
946
947            let (op, op2);
948            if cw_is_toward_larger {
949                op = p2;
950                op2 = p1a;
951            } else {
952                op = p1;
953                op2 = p2a;
954            }
955
956            let op_owner = self.arena[op].owner_idx;
957            let op2_owner = self.arena[op2].owner_idx;
958            self.results[op_owner] = Some(op);
959            self.results[op2_owner] = Some(op2);
960
961            // Get ready for the next loop
962            let op_is_larger;
963            let op2_is_larger;
964            if is_horz {
965                let op_prev = self.arena[op].prev;
966                let op2_prev = self.arena[op2].prev;
967                op_is_larger = self.arena[op].pt.x > self.arena[op_prev].pt.x;
968                op2_is_larger = self.arena[op2].pt.x > self.arena[op2_prev].pt.x;
969            } else {
970                let op_prev = self.arena[op].prev;
971                let op2_prev = self.arena[op2].prev;
972                op_is_larger = self.arena[op].pt.y > self.arena[op_prev].pt.y;
973                op2_is_larger = self.arena[op2].pt.y > self.arena[op2_prev].pt.y;
974            }
975
976            let op_next = self.arena[op].next;
977            let op_prev = self.arena[op].prev;
978            let op2_next = self.arena[op2].next;
979            let op2_prev = self.arena[op2].prev;
980
981            if (op_next == op_prev) || (self.arena[op].pt == self.arena[op_prev].pt) {
982                if op2_is_larger == cw_is_toward_larger {
983                    self.edges[cw_idx][i] = Some(op2);
984                    self.edges[ccw_idx][j] = None;
985                    j += 1;
986                } else {
987                    self.edges[ccw_idx][j] = Some(op2);
988                    self.edges[cw_idx][i] = None;
989                    i += 1;
990                }
991            } else if (op2_next == op2_prev) || (self.arena[op2].pt == self.arena[op2_prev].pt) {
992                if op_is_larger == cw_is_toward_larger {
993                    self.edges[cw_idx][i] = Some(op);
994                    self.edges[ccw_idx][j] = None;
995                    j += 1;
996                } else {
997                    self.edges[ccw_idx][j] = Some(op);
998                    self.edges[cw_idx][i] = None;
999                    i += 1;
1000                }
1001            } else if op_is_larger == op2_is_larger {
1002                if op_is_larger == cw_is_toward_larger {
1003                    self.edges[cw_idx][i] = Some(op);
1004                    self.uncouple_edge(op2);
1005                    self.add_to_edge(cw_idx, op2);
1006                    self.edges[ccw_idx][j] = None;
1007                    j += 1;
1008                } else {
1009                    self.edges[cw_idx][i] = None;
1010                    i += 1;
1011                    self.edges[ccw_idx][j] = Some(op2);
1012                    self.uncouple_edge(op);
1013                    self.add_to_edge(ccw_idx, op);
1014                    j = 0;
1015                }
1016            } else {
1017                if op_is_larger == cw_is_toward_larger {
1018                    self.edges[cw_idx][i] = Some(op);
1019                } else {
1020                    self.edges[ccw_idx][j] = Some(op);
1021                }
1022                if op2_is_larger == cw_is_toward_larger {
1023                    self.edges[cw_idx][i] = Some(op2);
1024                } else {
1025                    self.edges[ccw_idx][j] = Some(op2);
1026                }
1027            }
1028        }
1029    }
1030
1031    /// Extract a path from the circular linked list
1032    /// Direct port from clipper.rectclip.cpp line 843
1033    fn get_path(&mut self, op_idx_ref: &mut Option<usize>) -> Path64 {
1034        let op_start = match *op_idx_ref {
1035            Some(idx) => idx,
1036            None => return Path64::new(),
1037        };
1038
1039        // Check if degenerate
1040        if self.arena[op_start].next == self.arena[op_start].prev {
1041            *op_idx_ref = None;
1042            return Path64::new();
1043        }
1044
1045        // Remove collinear points
1046        let mut op_idx = op_start;
1047        let mut op2_idx = self.arena[op_start].next;
1048        while op2_idx != op_idx {
1049            let prev_idx = self.arena[op2_idx].prev;
1050            let next_idx = self.arena[op2_idx].next;
1051            let prev_pt = self.arena[prev_idx].pt;
1052            let op2_pt = self.arena[op2_idx].pt;
1053            let next_pt = self.arena[next_idx].pt;
1054
1055            if is_collinear(prev_pt, op2_pt, next_pt) {
1056                op_idx = self.arena[op2_idx].prev;
1057                match self.unlink_op(op2_idx) {
1058                    Some(new_idx) => op2_idx = new_idx,
1059                    None => {
1060                        *op_idx_ref = None;
1061                        return Path64::new();
1062                    }
1063                }
1064            } else {
1065                op2_idx = self.arena[op2_idx].next;
1066            }
1067        }
1068
1069        *op_idx_ref = Some(op2_idx);
1070        if self.arena[op2_idx].next == self.arena[op2_idx].prev {
1071            *op_idx_ref = None;
1072            return Path64::new();
1073        }
1074
1075        let mut result = Path64::new();
1076        let start = op2_idx;
1077        result.push(self.arena[start].pt);
1078        let mut curr = self.arena[start].next;
1079        while curr != start {
1080            result.push(self.arena[curr].pt);
1081            curr = self.arena[curr].next;
1082        }
1083        result
1084    }
1085
1086    /// Execute clipping operation on multiple paths
1087    /// Direct port from clipper.rectclip.cpp line 873
1088    pub fn execute(&mut self, paths: &Paths64) -> Paths64 {
1089        let mut result = Paths64::new();
1090        if self.rect.is_empty() {
1091            return result;
1092        }
1093
1094        for path in paths {
1095            if path.len() < 3 {
1096                continue;
1097            }
1098            self.path_bounds = get_bounds_path(path);
1099            if !self.rect.intersects(&self.path_bounds) {
1100                continue;
1101            } else if self.rect.contains_rect(&self.path_bounds) {
1102                result.push(path.clone());
1103                continue;
1104            }
1105
1106            self.execute_internal(path);
1107            self.check_edges();
1108            for edge_i in 0..4usize {
1109                self.tidy_edges(edge_i, edge_i * 2, edge_i * 2 + 1);
1110            }
1111
1112            for ri in 0..self.results.len() {
1113                let mut op_ref = self.results[ri];
1114                let tmp = self.get_path(&mut op_ref);
1115                self.results[ri] = op_ref;
1116                if !tmp.is_empty() {
1117                    result.push(tmp);
1118                }
1119            }
1120
1121            // Clean up after every loop
1122            self.clear();
1123        }
1124        result
1125    }
1126}
1127
1128// ============================================================================
1129// RectClipLines64 - Rectangular line clipper for line segments
1130// Direct port from clipper.rectclip.h line 70
1131// ============================================================================
1132
1133/// Rectangular line clipper class for line segment clipping
1134/// Direct port from clipper.rectclip.h line 70
1135pub struct RectClipLines64 {
1136    rect: Rect64,
1137    rect_as_path: Path64,
1138    #[allow(dead_code)]
1139    rect_mp: Point64,
1140    arena: Vec<OutPt2>,
1141    results: Vec<Option<usize>>,
1142    start_locs: Vec<Location>,
1143}
1144
1145impl RectClipLines64 {
1146    /// Create new line clipper
1147    /// Direct port from clipper.rectclip.h line 75
1148    pub fn new(rect: Rect64) -> Self {
1149        let rect_as_path = rect.as_path();
1150        let rect_mp = rect.mid_point();
1151        Self {
1152            rect,
1153            rect_as_path,
1154            rect_mp,
1155            arena: Vec::new(),
1156            results: Vec::new(),
1157            start_locs: Vec::new(),
1158        }
1159    }
1160
1161    fn clear(&mut self) {
1162        self.arena.clear();
1163        self.results.clear();
1164        self.start_locs.clear();
1165    }
1166
1167    /// Add a point (same logic as RectClip64::Add)
1168    fn add(&mut self, pt: Point64, start_new: bool) -> usize {
1169        let curr_idx = self.results.len();
1170        if curr_idx == 0 || start_new {
1171            let new_idx = self.arena.len();
1172            let mut op = OutPt2::new(pt);
1173            op.next = new_idx;
1174            op.prev = new_idx;
1175            self.arena.push(op);
1176            self.results.push(Some(new_idx));
1177            new_idx
1178        } else {
1179            let result_idx = curr_idx - 1;
1180            let prev_op_idx = self.results[result_idx].unwrap();
1181            if self.arena[prev_op_idx].pt == pt {
1182                return prev_op_idx;
1183            }
1184            let new_idx = self.arena.len();
1185            let mut op = OutPt2::new(pt);
1186            op.owner_idx = result_idx;
1187
1188            let prev_next = self.arena[prev_op_idx].next;
1189            op.next = prev_next;
1190            op.prev = prev_op_idx;
1191            self.arena.push(op);
1192
1193            self.arena[prev_next].prev = new_idx;
1194            self.arena[prev_op_idx].next = new_idx;
1195
1196            self.results[result_idx] = Some(new_idx);
1197            new_idx
1198        }
1199    }
1200
1201    /// Get next location (same logic as RectClip64::GetNextLocation but without add for Inside)
1202    fn get_next_location(
1203        &mut self,
1204        path: &Path64,
1205        loc: &mut Location,
1206        i: &mut usize,
1207        high_i: usize,
1208    ) {
1209        match *loc {
1210            Location::Left => {
1211                while *i <= high_i && path[*i].x <= self.rect.left {
1212                    *i += 1;
1213                }
1214                if *i > high_i {
1215                    return;
1216                }
1217                if path[*i].x >= self.rect.right {
1218                    *loc = Location::Right;
1219                } else if path[*i].y <= self.rect.top {
1220                    *loc = Location::Top;
1221                } else if path[*i].y >= self.rect.bottom {
1222                    *loc = Location::Bottom;
1223                } else {
1224                    *loc = Location::Inside;
1225                }
1226            }
1227            Location::Top => {
1228                while *i <= high_i && path[*i].y <= self.rect.top {
1229                    *i += 1;
1230                }
1231                if *i > high_i {
1232                    return;
1233                }
1234                if path[*i].y >= self.rect.bottom {
1235                    *loc = Location::Bottom;
1236                } else if path[*i].x <= self.rect.left {
1237                    *loc = Location::Left;
1238                } else if path[*i].x >= self.rect.right {
1239                    *loc = Location::Right;
1240                } else {
1241                    *loc = Location::Inside;
1242                }
1243            }
1244            Location::Right => {
1245                while *i <= high_i && path[*i].x >= self.rect.right {
1246                    *i += 1;
1247                }
1248                if *i > high_i {
1249                    return;
1250                }
1251                if path[*i].x <= self.rect.left {
1252                    *loc = Location::Left;
1253                } else if path[*i].y <= self.rect.top {
1254                    *loc = Location::Top;
1255                } else if path[*i].y >= self.rect.bottom {
1256                    *loc = Location::Bottom;
1257                } else {
1258                    *loc = Location::Inside;
1259                }
1260            }
1261            Location::Bottom => {
1262                while *i <= high_i && path[*i].y >= self.rect.bottom {
1263                    *i += 1;
1264                }
1265                if *i > high_i {
1266                    return;
1267                }
1268                if path[*i].y <= self.rect.top {
1269                    *loc = Location::Top;
1270                } else if path[*i].x <= self.rect.left {
1271                    *loc = Location::Left;
1272                } else if path[*i].x >= self.rect.right {
1273                    *loc = Location::Right;
1274                } else {
1275                    *loc = Location::Inside;
1276                }
1277            }
1278            Location::Inside => {
1279                while *i <= high_i {
1280                    if path[*i].x < self.rect.left {
1281                        *loc = Location::Left;
1282                    } else if path[*i].x > self.rect.right {
1283                        *loc = Location::Right;
1284                    } else if path[*i].y > self.rect.bottom {
1285                        *loc = Location::Bottom;
1286                    } else if path[*i].y < self.rect.top {
1287                        *loc = Location::Top;
1288                    } else {
1289                        let pt = path[*i];
1290                        self.add(pt, false);
1291                        *i += 1;
1292                        continue;
1293                    }
1294                    break;
1295                }
1296            }
1297        }
1298    }
1299
1300    /// Internal execution for line clipping
1301    /// Direct port from clipper.rectclip.cpp line 942
1302    fn execute_internal(&mut self, path: &Path64) {
1303        if self.rect.is_empty() || path.len() < 2 {
1304            return;
1305        }
1306
1307        self.clear();
1308
1309        let high_i = path.len() - 1;
1310        let mut i = 1usize;
1311        let mut prev = Location::Inside;
1312        let mut loc = Location::Inside;
1313
1314        if !get_location(&self.rect, &path[0], &mut loc) {
1315            while i <= high_i && !get_location(&self.rect, &path[i], &mut prev) {
1316                i += 1;
1317            }
1318            if i > high_i {
1319                // all of path must be inside rect
1320                for pt in path {
1321                    self.add(*pt, false);
1322                }
1323                return;
1324            }
1325            if prev == Location::Inside {
1326                loc = Location::Inside;
1327            }
1328            i = 1;
1329        }
1330        if loc == Location::Inside {
1331            self.add(path[0], false);
1332        }
1333
1334        while i <= high_i {
1335            prev = loc;
1336            self.get_next_location(path, &mut loc, &mut i, high_i);
1337            if i > high_i {
1338                break;
1339            }
1340            let mut ip = Point64::new(0, 0);
1341            let mut ip2 = Point64::new(0, 0);
1342            let prev_pt = path[i - 1];
1343
1344            let mut crossing_loc = loc;
1345            if !get_intersection(
1346                &self.rect_as_path.clone(),
1347                path[i],
1348                prev_pt,
1349                &mut crossing_loc,
1350                &mut ip,
1351            ) {
1352                i += 1;
1353                continue;
1354            }
1355
1356            if loc == Location::Inside {
1357                // path must be entering rect
1358                self.add(ip, true);
1359            } else if prev != Location::Inside {
1360                // passing right through rect
1361                crossing_loc = prev;
1362                let rect_as_path = self.rect_as_path.clone();
1363                get_intersection(&rect_as_path, prev_pt, path[i], &mut crossing_loc, &mut ip2);
1364                self.add(ip2, true);
1365                self.add(ip, false);
1366            } else {
1367                // path must be exiting rect
1368                self.add(ip, false);
1369            }
1370        }
1371    }
1372
1373    /// Extract a path from the circular linked list (line version)
1374    /// Direct port from clipper.rectclip.cpp line 1012
1375    fn get_path(&self, op_idx_ref: &mut Option<usize>) -> Path64 {
1376        let op_start = match *op_idx_ref {
1377            Some(idx) => idx,
1378            None => return Path64::new(),
1379        };
1380
1381        if self.arena[op_start].next == op_start {
1382            return Path64::new();
1383        }
1384
1385        // Start at path beginning (next from start sentinel)
1386        let start = self.arena[op_start].next;
1387        let mut result = Path64::new();
1388        result.push(self.arena[start].pt);
1389        let mut op2 = self.arena[start].next;
1390        while op2 != start {
1391            result.push(self.arena[op2].pt);
1392            op2 = self.arena[op2].next;
1393        }
1394        result
1395    }
1396
1397    /// Execute line clipping
1398    /// Direct port from clipper.rectclip.cpp line 916
1399    pub fn execute(&mut self, paths: &Paths64) -> Paths64 {
1400        let mut result = Paths64::new();
1401        if self.rect.is_empty() {
1402            return result;
1403        }
1404
1405        for path in paths {
1406            let path_rec = get_bounds_path(path);
1407            if !self.rect.intersects(&path_rec) {
1408                continue;
1409            }
1410
1411            self.execute_internal(path);
1412
1413            for ri in 0..self.results.len() {
1414                let mut op_ref = self.results[ri];
1415                let tmp = self.get_path(&mut op_ref);
1416                if !tmp.is_empty() {
1417                    result.push(tmp);
1418                }
1419            }
1420            self.clear();
1421        }
1422        result
1423    }
1424}
1425
1426// Include tests from separate file
1427#[cfg(test)]
1428#[path = "rectclip_tests.rs"]
1429mod tests;