Skip to main content

clipper2_rust/
engine.rs

1//! Main polygon clipping engine
2//!
3//! Direct port from clipper.engine.h and clipper.engine.cpp
4//! Copyright (c) Angus Johnson 2010-2025
5//! This is the core clipping module implementing the Vatti sweep-line algorithm
6
7use crate::core::*;
8use crate::engine_fns::*;
9use crate::engine_public::PolyTree64;
10use std::collections::BinaryHeap;
11
12// ============================================================================
13// Sentinel value for null indices in arena-based structures
14// ============================================================================
15pub const NONE: usize = usize::MAX;
16
17// ============================================================================
18// Enums - Direct port from clipper.engine.h lines 29-36
19// ============================================================================
20
21/// Type of clipping operation
22/// Direct port from clipper.engine.h line 29
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24#[repr(C)]
25pub enum ClipType {
26    #[default]
27    NoClip,
28    Intersection,
29    Union,
30    Difference,
31    Xor,
32}
33
34/// Whether a path is Subject or Clip
35/// Direct port from clipper.engine.h line 31
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37#[repr(C)]
38pub enum PathType {
39    Subject,
40    Clip,
41}
42
43/// How an edge joins with another (for horizontal processing)
44/// Direct port from clipper.engine.h line 32
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
46#[repr(C)]
47pub enum JoinWith {
48    #[default]
49    NoJoin,
50    Left,
51    Right,
52}
53
54/// Vertex flags (bitflags)
55/// Direct port from clipper.engine.h line 34
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
57pub struct VertexFlags(u32);
58
59impl VertexFlags {
60    pub const EMPTY: VertexFlags = VertexFlags(0);
61    pub const OPEN_START: VertexFlags = VertexFlags(1);
62    pub const OPEN_END: VertexFlags = VertexFlags(2);
63    pub const LOCAL_MAX: VertexFlags = VertexFlags(4);
64    pub const LOCAL_MIN: VertexFlags = VertexFlags(8);
65}
66
67impl std::ops::BitAnd for VertexFlags {
68    type Output = Self;
69    fn bitand(self, rhs: Self) -> Self {
70        VertexFlags(self.0 & rhs.0)
71    }
72}
73
74impl std::ops::BitOr for VertexFlags {
75    type Output = Self;
76    fn bitor(self, rhs: Self) -> Self {
77        VertexFlags(self.0 | rhs.0)
78    }
79}
80
81impl std::ops::BitAndAssign for VertexFlags {
82    fn bitand_assign(&mut self, rhs: Self) {
83        self.0 &= rhs.0;
84    }
85}
86
87impl std::ops::BitOrAssign for VertexFlags {
88    fn bitor_assign(&mut self, rhs: Self) {
89        self.0 |= rhs.0;
90    }
91}
92
93// ============================================================================
94// Core Data Structures - Arena-indexed
95// ============================================================================
96
97/// Input polygon vertex (circular doubly-linked list via arena indices)
98/// Direct port from clipper.engine.h line 48
99#[derive(Debug, Clone)]
100pub struct Vertex {
101    pub pt: Point64,
102    pub next: usize, // index into vertex arena
103    pub prev: usize, // index into vertex arena
104    pub flags: VertexFlags,
105}
106
107impl Vertex {
108    pub fn new(pt: Point64) -> Self {
109        Self {
110            pt,
111            next: NONE,
112            prev: NONE,
113            flags: VertexFlags::EMPTY,
114        }
115    }
116}
117
118/// Output point in the clipping result (circular doubly-linked list)
119/// Direct port from clipper.engine.h line 55
120#[derive(Debug, Clone)]
121pub struct OutPt {
122    pub pt: Point64,
123    pub next: usize,         // index into outpt arena
124    pub prev: usize,         // index into outpt arena
125    pub outrec: usize,       // index into outrec list
126    pub horz: Option<usize>, // index into horz_seg_list
127}
128
129impl OutPt {
130    pub fn new(pt: Point64, outrec_idx: usize) -> Self {
131        Self {
132            pt,
133            next: NONE,
134            prev: NONE,
135            outrec: outrec_idx,
136            horz: None,
137        }
138    }
139}
140
141/// Output polygon record
142/// Direct port from clipper.engine.h line 79
143#[derive(Debug, Clone)]
144pub struct OutRec {
145    pub idx: usize,
146    pub owner: Option<usize>,           // index into outrec list
147    pub front_edge: Option<usize>,      // index into active arena
148    pub back_edge: Option<usize>,       // index into active arena
149    pub pts: Option<usize>,             // index into outpt arena
150    pub polypath: Option<usize>,        // index into polypath list
151    pub splits: Option<Vec<usize>>,     // indices of split outrecs
152    pub recursive_split: Option<usize>, // index into outrec list
153    pub bounds: Rect64,
154    pub path: Path64,
155    pub is_open: bool,
156}
157
158impl OutRec {
159    pub fn new(idx: usize) -> Self {
160        Self {
161            idx,
162            owner: None,
163            front_edge: None,
164            back_edge: None,
165            pts: None,
166            polypath: None,
167            splits: None,
168            recursive_split: None,
169            bounds: Rect64::new(0, 0, 0, 0),
170            path: Path64::new(),
171            is_open: false,
172        }
173    }
174}
175
176/// Active edge in the sweep line
177/// Direct port from clipper.engine.h line 104
178#[derive(Debug, Clone)]
179pub struct Active {
180    pub bot: Point64,
181    pub top: Point64,
182    pub curr_x: i64,
183    pub dx: f64,
184    pub wind_dx: i32, // 1 or -1 depending on winding direction
185    pub wind_cnt: i32,
186    pub wind_cnt2: i32,        // winding count of the opposite polytype
187    pub outrec: Option<usize>, // index into outrec list
188    // AEL: active edge list
189    pub prev_in_ael: Option<usize>, // index into active arena
190    pub next_in_ael: Option<usize>, // index into active arena
191    // SEL: sorted edge list
192    pub prev_in_sel: Option<usize>, // index into active arena
193    pub next_in_sel: Option<usize>, // index into active arena
194    pub jump: Option<usize>,        // index into active arena
195    pub vertex_top: usize,          // index into vertex arena
196    pub local_min: usize,           // index into local_minima list
197    pub is_left_bound: bool,
198    pub join_with: JoinWith,
199}
200
201impl Active {
202    pub fn new() -> Self {
203        Self {
204            bot: Point64::new(0, 0),
205            top: Point64::new(0, 0),
206            curr_x: 0,
207            dx: 0.0,
208            wind_dx: 1,
209            wind_cnt: 0,
210            wind_cnt2: 0,
211            outrec: None,
212            prev_in_ael: None,
213            next_in_ael: None,
214            prev_in_sel: None,
215            next_in_sel: None,
216            jump: None,
217            vertex_top: NONE,
218            local_min: NONE,
219            is_left_bound: false,
220            join_with: JoinWith::NoJoin,
221        }
222    }
223}
224
225impl Default for Active {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231/// Local minimum vertex where ascending and descending bounds meet
232/// Direct port from clipper.engine.h line 131
233#[derive(Debug, Clone)]
234pub struct LocalMinima {
235    pub vertex: usize, // index into vertex arena
236    pub polytype: PathType,
237    pub is_open: bool,
238}
239
240impl LocalMinima {
241    pub fn new(vertex: usize, polytype: PathType, is_open: bool) -> Self {
242        Self {
243            vertex,
244            polytype,
245            is_open,
246        }
247    }
248}
249
250/// Intersection between two active edges
251/// Direct port from clipper.engine.h line 139
252#[derive(Debug, Clone)]
253pub struct IntersectNode {
254    pub pt: Point64,
255    pub edge1: usize, // index into active arena
256    pub edge2: usize, // index into active arena
257}
258
259impl IntersectNode {
260    pub fn new() -> Self {
261        Self {
262            pt: Point64::new(0, 0),
263            edge1: NONE,
264            edge2: NONE,
265        }
266    }
267
268    pub fn with_edges(edge1: usize, edge2: usize, pt: Point64) -> Self {
269        Self { pt, edge1, edge2 }
270    }
271}
272
273impl Default for IntersectNode {
274    fn default() -> Self {
275        Self::new()
276    }
277}
278
279/// Horizontal segment for horizontal edge processing
280/// Direct port from clipper.engine.h line 148
281#[derive(Debug, Clone)]
282pub struct HorzSegment {
283    pub left_op: Option<usize>,  // index into outpt arena
284    pub right_op: Option<usize>, // index into outpt arena
285    pub left_to_right: bool,
286}
287
288impl HorzSegment {
289    pub fn new() -> Self {
290        Self {
291            left_op: None,
292            right_op: None,
293            left_to_right: true,
294        }
295    }
296
297    pub fn with_op(op_idx: usize) -> Self {
298        Self {
299            left_op: Some(op_idx),
300            right_op: None,
301            left_to_right: true,
302        }
303    }
304}
305
306impl Default for HorzSegment {
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312/// Horizontal join between two output points
313/// Direct port from clipper.engine.h line 156
314#[derive(Debug, Clone)]
315pub struct HorzJoin {
316    pub op1: Option<usize>, // index into outpt arena
317    pub op2: Option<usize>, // index into outpt arena
318}
319
320impl HorzJoin {
321    pub fn new() -> Self {
322        Self {
323            op1: None,
324            op2: None,
325        }
326    }
327
328    pub fn with_ops(ltr: usize, rtl: usize) -> Self {
329        Self {
330            op1: Some(ltr),
331            op2: Some(rtl),
332        }
333    }
334}
335
336impl Default for HorzJoin {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342// ============================================================================
343// ClipperBase - Main clipping engine struct
344// Direct port from clipper.engine.h line 192
345// ============================================================================
346
347/// Main clipping engine. Manages all arenas and the sweep-line algorithm state.
348/// Direct port from clipper.engine.h line 192
349pub struct ClipperBase {
350    // Configuration
351    pub cliptype: ClipType,
352    pub fillrule: FillRule,
353    pub preserve_collinear: bool,
354    pub reverse_solution: bool,
355    pub error_code: i32,
356    pub has_open_paths: bool,
357    pub succeeded: bool,
358    using_polytree: bool,
359
360    // Sweep-line state
361    bot_y: i64,
362    minima_list_sorted: bool,
363
364    // Arenas - all nodes are stored here, referenced by indices
365    pub vertex_arena: Vec<Vertex>,
366    pub active_arena: Vec<Active>,
367    pub outpt_arena: Vec<OutPt>,
368
369    // Lists referencing arena indices
370    pub outrec_list: Vec<OutRec>,
371    pub minima_list: Vec<LocalMinima>,
372    current_locmin_idx: usize,
373    vertex_lists: Vec<Vec<usize>>, // groups of vertex indices
374
375    // Active edge list head
376    pub actives: Option<usize>, // index into active_arena
377    pub sel: Option<usize>,     // index into active_arena
378
379    // Scanline priority queue (min-heap using Reverse)
380    scanline_list: BinaryHeap<i64>,
381
382    // Intersection and horizontal processing
383    pub intersect_nodes: Vec<IntersectNode>,
384    pub horz_seg_list: Vec<HorzSegment>,
385    pub horz_join_list: Vec<HorzJoin>,
386}
387
388impl ClipperBase {
389    pub fn new() -> Self {
390        Self {
391            cliptype: ClipType::NoClip,
392            fillrule: FillRule::EvenOdd,
393            preserve_collinear: true,
394            reverse_solution: false,
395            error_code: 0,
396            has_open_paths: false,
397            succeeded: true,
398            using_polytree: false,
399            bot_y: 0,
400            minima_list_sorted: false,
401            vertex_arena: Vec::new(),
402            active_arena: Vec::new(),
403            outpt_arena: Vec::new(),
404            outrec_list: Vec::new(),
405            minima_list: Vec::new(),
406            current_locmin_idx: 0,
407            vertex_lists: Vec::new(),
408            actives: None,
409            sel: None,
410            scanline_list: BinaryHeap::new(),
411            intersect_nodes: Vec::new(),
412            horz_seg_list: Vec::new(),
413            horz_join_list: Vec::new(),
414        }
415    }
416
417    /// Clear all data
418    /// Direct port from clipper.engine.h line 284
419    pub fn clear(&mut self) {
420        self.vertex_arena.clear();
421        self.active_arena.clear();
422        self.outpt_arena.clear();
423        self.outrec_list.clear();
424        self.minima_list.clear();
425        self.current_locmin_idx = 0;
426        self.vertex_lists.clear();
427        self.actives = None;
428        self.sel = None;
429        self.scanline_list.clear();
430        self.intersect_nodes.clear();
431        self.horz_seg_list.clear();
432        self.horz_join_list.clear();
433        self.minima_list_sorted = false;
434        self.has_open_paths = false;
435        self.succeeded = true;
436    }
437
438    /// Insert a scanline y-value
439    /// Direct port from clipper.engine.cpp InsertScanline
440    #[inline]
441    pub fn insert_scanline(&mut self, y: i64) {
442        self.scanline_list.push(y);
443    }
444
445    /// Pop the next scanline y-value
446    /// Direct port from clipper.engine.cpp PopScanline
447    #[inline]
448    pub fn pop_scanline(&mut self) -> Option<i64> {
449        if self.scanline_list.is_empty() {
450            return None;
451        }
452        let y = self.scanline_list.pop().unwrap();
453        // Skip duplicates
454        while !self.scanline_list.is_empty() && *self.scanline_list.peek().unwrap() == y {
455            self.scanline_list.pop();
456        }
457        Some(y)
458    }
459
460    /// Pop a local minimum at a given y-value
461    /// Direct port from clipper.engine.cpp PopLocalMinima
462    #[inline]
463    pub fn pop_local_minima(&mut self, y: i64) -> Option<usize> {
464        if self.current_locmin_idx < self.minima_list.len() {
465            let vertex_idx = self.minima_list[self.current_locmin_idx].vertex;
466            if self.vertex_arena[vertex_idx].pt.y == y {
467                let idx = self.current_locmin_idx;
468                self.current_locmin_idx += 1;
469                return Some(idx);
470            }
471        }
472        None
473    }
474
475    /// Create a new OutRec
476    /// Direct port from clipper.engine.cpp NewOutRec
477    pub fn new_out_rec(&mut self) -> usize {
478        let idx = self.outrec_list.len();
479        self.outrec_list.push(OutRec::new(idx));
480        idx
481    }
482
483    /// Create a new OutPt in the arena
484    pub fn new_out_pt(&mut self, pt: Point64, outrec_idx: usize) -> usize {
485        let idx = self.outpt_arena.len();
486        let mut op = OutPt::new(pt, outrec_idx);
487        op.next = idx;
488        op.prev = idx;
489        self.outpt_arena.push(op);
490        idx
491    }
492
493    /// Create a new Active in the arena
494    pub fn new_active(&mut self) -> usize {
495        let idx = self.active_arena.len();
496        self.active_arena.push(Active::new());
497        idx
498    }
499
500    /// Add a local minimum
501    /// Direct port from clipper.engine.cpp AddLocMin
502    pub fn add_loc_min(&mut self, vert_idx: usize, polytype: PathType, is_open: bool) {
503        // Check that the vertex is a local minima
504        self.vertex_arena[vert_idx].flags |= VertexFlags::LOCAL_MIN;
505        self.minima_list
506            .push(LocalMinima::new(vert_idx, polytype, is_open));
507    }
508
509    /// Add a path for clipping (converts to vertex representation)
510    /// Direct port from clipper.engine.cpp AddPath / AddPaths_
511    pub fn add_path(&mut self, path: &Path64, polytype: PathType, is_open: bool) {
512        let mut path = path.clone();
513
514        // Validate path
515        let cnt = path.len();
516        if cnt < 2 || (!is_open && cnt < 3) {
517            return;
518        }
519
520        // Remove consecutive duplicates
521        strip_duplicates_path(&mut path, !is_open);
522
523        if path.len() < 2 || (!is_open && path.len() < 3) {
524            return;
525        }
526
527        if is_open {
528            self.has_open_paths = true;
529        }
530
531        // Build vertices
532        let first_vert_idx = self.vertex_arena.len();
533        let vert_count = path.len();
534
535        for pt in &path {
536            self.vertex_arena.push(Vertex::new(*pt));
537        }
538
539        // Link vertices into circular list
540        for i in 0..vert_count {
541            let abs_idx = first_vert_idx + i;
542            self.vertex_arena[abs_idx].next = first_vert_idx + (i + 1) % vert_count;
543            self.vertex_arena[abs_idx].prev = first_vert_idx + (i + vert_count - 1) % vert_count;
544        }
545
546        // Record this vertex list
547        let vert_indices: Vec<usize> = (first_vert_idx..first_vert_idx + vert_count).collect();
548        self.vertex_lists.push(vert_indices);
549
550        if is_open {
551            // For open paths, mark start and end
552            self.vertex_arena[first_vert_idx].flags |= VertexFlags::OPEN_START;
553            self.vertex_arena[first_vert_idx + vert_count - 1].flags |= VertexFlags::OPEN_END;
554
555            // Find local minima for open paths
556            let mut i = first_vert_idx;
557            let last = first_vert_idx + vert_count - 1;
558
559            // Skip ascending from start
560            while i < last && self.vertex_arena[i].pt.y <= self.vertex_arena[i + 1].pt.y {
561                i += 1;
562            }
563            // We need to handle ascending/descending sequences and add local minima
564            // The first local min for an open path is the start if it's lower than next,
565            // or wherever we first start going up after going down
566            self.find_and_add_local_minima_open(first_vert_idx, vert_count, polytype);
567        } else {
568            self.find_and_add_local_minima_closed(first_vert_idx, vert_count, polytype);
569        }
570    }
571
572    /// Find and add local minima for a closed path
573    fn find_and_add_local_minima_closed(
574        &mut self,
575        first_vert_idx: usize,
576        vert_count: usize,
577        polytype: PathType,
578    ) {
579        // Find all local minima in the closed path
580        // A local minimum is where the path changes from descending to ascending
581        // (in screen coordinates where Y increases downward)
582
583        // First, find any vertex that is NOT a local minimum to start from
584        // We need to find a vertex where prev.y != curr.y to know direction
585        let mut start = first_vert_idx;
586        let end = first_vert_idx + vert_count;
587
588        // Skip vertices where prev has same y
589        let mut found_start = false;
590        for i in first_vert_idx..end {
591            let prev = self.vertex_arena[i].prev;
592            if self.vertex_arena[prev].pt.y != self.vertex_arena[i].pt.y {
593                start = i;
594                found_start = true;
595                break;
596            }
597        }
598        if !found_start {
599            return; // All vertices at same y - degenerate
600        }
601
602        // Determine initial direction
603        let prev = self.vertex_arena[start].prev;
604        let mut going_up = self.vertex_arena[prev].pt.y > self.vertex_arena[start].pt.y;
605
606        let mut curr = start;
607        loop {
608            let next = self.vertex_arena[curr].next;
609            let curr_pt = self.vertex_arena[curr].pt;
610            let next_pt = self.vertex_arena[next].pt;
611
612            if next_pt.y > curr_pt.y && going_up {
613                // Was going up, now going down => local max
614                self.vertex_arena[curr].flags |= VertexFlags::LOCAL_MAX;
615                going_up = false;
616            } else if next_pt.y < curr_pt.y && !going_up {
617                // Was going down, now going up => local min
618                going_up = true;
619                self.add_loc_min(curr, polytype, false);
620            }
621
622            // Skip horizontal edges (same y)
623            if next_pt.y == curr_pt.y && next != start {
624                curr = next;
625                continue;
626            }
627
628            curr = next;
629            if curr == start {
630                break;
631            }
632        }
633    }
634
635    /// Find and add local minima for an open path
636    fn find_and_add_local_minima_open(
637        &mut self,
638        first_vert_idx: usize,
639        vert_count: usize,
640        polytype: PathType,
641    ) {
642        let last_vert_idx = first_vert_idx + vert_count - 1;
643
644        // For open paths, the first and last vertices are potential local minima
645        let first_pt = self.vertex_arena[first_vert_idx].pt;
646        let second_pt = self.vertex_arena[first_vert_idx + 1].pt;
647
648        // The start is a local min if it goes up (in screen coords, y decreases)
649        // or if horizontal then the next segment goes up
650        let mut going_up;
651        if first_pt.y > second_pt.y {
652            // Going up from first vertex - first is a local min
653            self.add_loc_min(first_vert_idx, polytype, true);
654            going_up = true;
655        } else if first_pt.y < second_pt.y {
656            // Going down - first is NOT a local min, it's a local max
657            self.vertex_arena[first_vert_idx].flags |= VertexFlags::LOCAL_MAX;
658            going_up = false;
659        } else {
660            // Horizontal at start - need to keep scanning
661            let mut i = first_vert_idx + 1;
662            while i < last_vert_idx && self.vertex_arena[i].pt.y == first_pt.y {
663                i += 1;
664            }
665            if i >= last_vert_idx {
666                // Completely horizontal open path - still add as local minimum
667                // so it participates in the sweep line (matches C++ Clipper2 behavior)
668                self.add_loc_min(first_vert_idx, polytype, true);
669                going_up = true;
670            } else {
671                going_up = self.vertex_arena[i].pt.y < first_pt.y;
672                if going_up {
673                    self.add_loc_min(first_vert_idx, polytype, true);
674                } else {
675                    self.vertex_arena[first_vert_idx].flags |= VertexFlags::LOCAL_MAX;
676                }
677            }
678        }
679
680        // Scan interior vertices
681        for i in (first_vert_idx + 1)..last_vert_idx {
682            let curr_pt = self.vertex_arena[i].pt;
683            let next_pt = self.vertex_arena[i + 1].pt;
684
685            if next_pt.y > curr_pt.y && going_up {
686                self.vertex_arena[i].flags |= VertexFlags::LOCAL_MAX;
687                going_up = false;
688            } else if next_pt.y < curr_pt.y && !going_up {
689                going_up = true;
690                self.add_loc_min(i, polytype, true);
691            }
692        }
693
694        // Handle last vertex
695        let last_pt = self.vertex_arena[last_vert_idx].pt;
696        let prev_pt = self.vertex_arena[last_vert_idx - 1].pt;
697        if !going_up {
698            // Was descending - last vertex is a local min
699            self.add_loc_min(last_vert_idx, polytype, true);
700        } else if last_pt.y < prev_pt.y || (last_pt.y == prev_pt.y && going_up) {
701            self.vertex_arena[last_vert_idx].flags |= VertexFlags::LOCAL_MAX;
702        }
703    }
704
705    /// Add multiple paths for clipping
706    /// Direct port from clipper.engine.h AddPaths
707    pub fn add_paths(&mut self, paths: &Paths64, polytype: PathType, is_open: bool) {
708        for path in paths {
709            self.add_path(path, polytype, is_open);
710        }
711    }
712
713    /// Sort the local minima list
714    pub fn sort_minima_list(&mut self) {
715        if !self.minima_list_sorted {
716            // Sort by y descending (bottom first), then by x ascending
717            let vertex_arena = &self.vertex_arena;
718            self.minima_list.sort_by(|a, b| {
719                let a_pt = vertex_arena[a.vertex].pt;
720                let b_pt = vertex_arena[b.vertex].pt;
721                if a_pt.y != b_pt.y {
722                    b_pt.y.cmp(&a_pt.y) // descending by y (larger y = lower on screen)
723                } else {
724                    a_pt.x.cmp(&b_pt.x) // ascending by x
725                }
726            });
727            self.minima_list_sorted = true;
728        }
729    }
730
731    /// Duplicate an OutPt, inserting after or before
732    /// Direct port from clipper.engine.cpp DuplicateOp
733    pub fn duplicate_op(&mut self, op_idx: usize, insert_after: bool) -> usize {
734        let pt = self.outpt_arena[op_idx].pt;
735        let outrec = self.outpt_arena[op_idx].outrec;
736        let new_idx = self.outpt_arena.len();
737        let mut result = OutPt::new(pt, outrec);
738
739        if insert_after {
740            let next = self.outpt_arena[op_idx].next;
741            result.next = next;
742            result.prev = op_idx;
743            self.outpt_arena.push(result);
744            self.outpt_arena[next].prev = new_idx;
745            self.outpt_arena[op_idx].next = new_idx;
746        } else {
747            let prev = self.outpt_arena[op_idx].prev;
748            result.prev = prev;
749            result.next = op_idx;
750            self.outpt_arena.push(result);
751            self.outpt_arena[prev].next = new_idx;
752            self.outpt_arena[op_idx].prev = new_idx;
753        }
754
755        new_idx
756    }
757
758    /// Dispose (unlink) an OutPt, return the next
759    /// Direct port from clipper.engine.cpp DisposeOutPt
760    pub fn dispose_out_pt(&mut self, op_idx: usize) -> usize {
761        let result = self.outpt_arena[op_idx].next;
762        let prev = self.outpt_arena[op_idx].prev;
763        self.outpt_arena[prev].next = result;
764        self.outpt_arena[result].prev = prev;
765        // Note: we don't actually free the arena slot, just unlink
766        result
767    }
768
769    /// Dispose all OutPts in a circular list, setting outrec.pts to None
770    /// Direct port from clipper.engine.cpp DisposeOutPts
771    pub fn dispose_out_pts(&mut self, outrec_idx: usize) {
772        if let Some(pts_idx) = self.outrec_list[outrec_idx].pts {
773            // Unlink the circular list (don't actually free, arena-based)
774            let prev = self.outpt_arena[pts_idx].prev;
775            self.outpt_arena[prev].next = NONE; // break the circle
776                                                // Walk and mark as disposed
777            let mut op = Some(pts_idx);
778            while let Some(idx) = op {
779                if self.outpt_arena[idx].next == NONE {
780                    break;
781                }
782                let next = self.outpt_arena[idx].next;
783                if next == NONE || next == idx {
784                    break;
785                }
786                op = Some(next);
787            }
788        }
789        self.outrec_list[outrec_idx].pts = None;
790    }
791
792    /// Set the front and back edges of an OutRec
793    /// Direct port from clipper.engine.cpp SetSides
794    #[inline]
795    pub fn set_sides(&mut self, outrec_idx: usize, start_edge: usize, end_edge: usize) {
796        self.outrec_list[outrec_idx].front_edge = Some(start_edge);
797        self.outrec_list[outrec_idx].back_edge = Some(end_edge);
798    }
799
800    /// Swap OutRecs between two active edges
801    /// Direct port from clipper.engine.cpp SwapOutrecs
802    pub fn swap_outrecs(&mut self, e1_idx: usize, e2_idx: usize) {
803        let or1 = self.active_arena[e1_idx].outrec;
804        let or2 = self.active_arena[e2_idx].outrec;
805
806        if or1 == or2 {
807            if let Some(or_idx) = or1 {
808                let fe = self.outrec_list[or_idx].front_edge;
809                let be = self.outrec_list[or_idx].back_edge;
810                self.outrec_list[or_idx].front_edge = be;
811                self.outrec_list[or_idx].back_edge = fe;
812            }
813            return;
814        }
815
816        if let Some(or1_idx) = or1 {
817            if self.outrec_list[or1_idx].front_edge == Some(e1_idx) {
818                self.outrec_list[or1_idx].front_edge = Some(e2_idx);
819            } else {
820                self.outrec_list[or1_idx].back_edge = Some(e2_idx);
821            }
822        }
823
824        if let Some(or2_idx) = or2 {
825            if self.outrec_list[or2_idx].front_edge == Some(e2_idx) {
826                self.outrec_list[or2_idx].front_edge = Some(e1_idx);
827            } else {
828                self.outrec_list[or2_idx].back_edge = Some(e1_idx);
829            }
830        }
831
832        self.active_arena[e1_idx].outrec = or2;
833        self.active_arena[e2_idx].outrec = or1;
834    }
835
836    /// Check if an active edge is the front edge of its outrec
837    /// Direct port from clipper.engine.cpp IsFront
838    #[inline]
839    pub fn is_front(&self, e_idx: usize) -> bool {
840        if let Some(or_idx) = self.active_arena[e_idx].outrec {
841            self.outrec_list[or_idx].front_edge == Some(e_idx)
842        } else {
843            false
844        }
845    }
846
847    /// Get the previous hot edge in AEL
848    /// Direct port from clipper.engine.cpp GetPrevHotEdge
849    pub fn get_prev_hot_edge(&self, e_idx: usize) -> Option<usize> {
850        let mut prev = self.active_arena[e_idx].prev_in_ael;
851        while let Some(p_idx) = prev {
852            if is_open_active(&self.active_arena[p_idx], &self.minima_list)
853                || !is_hot_edge(&self.active_arena[p_idx])
854            {
855                prev = self.active_arena[p_idx].prev_in_ael;
856            } else {
857                return Some(p_idx);
858            }
859        }
860        None
861    }
862
863    /// Add a trial horizontal join
864    /// Direct port from clipper.engine.cpp AddTrialHorzJoin
865    #[inline]
866    pub fn add_trial_horz_join(&mut self, op_idx: usize) {
867        // Match C++: check is_open, then just push to the list.
868        // Do NOT check or set op.horz here - that is only done inside
869        // convert_horz_segs_to_joins (via UpdateHorzSegment logic).
870        let or_idx = self.outpt_arena[op_idx].outrec;
871        if self.outrec_list[or_idx].is_open {
872            return;
873        }
874        self.horz_seg_list.push(HorzSegment::with_op(op_idx));
875    }
876
877    /// Push a horizontal edge onto the horz stack
878    /// Direct port from clipper.engine.cpp PushHorz
879    #[inline]
880    pub fn push_horz(&mut self, e_idx: usize) {
881        self.active_arena[e_idx].next_in_sel = self.sel;
882        self.sel = Some(e_idx);
883    }
884
885    /// Pop a horizontal edge from the horz stack
886    /// Direct port from clipper.engine.cpp PopHorz
887    #[inline]
888    pub fn pop_horz(&mut self) -> Option<usize> {
889        let e = self.sel;
890        if let Some(e_idx) = e {
891            self.sel = self.active_arena[e_idx].next_in_sel;
892        }
893        e
894    }
895
896    /// Build Path64 output from OutRec
897    /// Direct port from clipper.engine.cpp BuildPath64
898    pub fn build_path64(&self, outrec: &OutRec) -> Option<Path64> {
899        let op_start = outrec.pts?;
900        let cnt = point_count(op_start, &self.outpt_arena);
901        if cnt < 2 {
902            return None;
903        }
904
905        let reverse = if outrec.is_open {
906            false
907        } else {
908            let area = area_outpt(op_start, &self.outpt_arena);
909            if area == 0.0 {
910                return None;
911            }
912            (area < 0.0) != self.reverse_solution
913        };
914
915        let mut result = Path64::with_capacity(cnt as usize);
916        if reverse {
917            let mut op = op_start;
918            loop {
919                result.push(self.outpt_arena[op].pt);
920                op = self.outpt_arena[op].prev;
921                if op == op_start {
922                    break;
923                }
924            }
925        } else {
926            let op_next = self.outpt_arena[op_start].next;
927            let mut op = op_next;
928            loop {
929                result.push(self.outpt_arena[op].pt);
930                op = self.outpt_arena[op].next;
931                if op == op_next {
932                    break;
933                }
934            }
935        }
936
937        // Remove collinear if not preserving
938        if !self.preserve_collinear {
939            // Strip collinear points
940            let mut i = 0;
941            while i < result.len() && result.len() > 2 {
942                let prev = if i == 0 { result.len() - 1 } else { i - 1 };
943                let next = (i + 1) % result.len();
944                if is_collinear(result[prev], result[i], result[next]) {
945                    result.remove(i);
946                    i = i.saturating_sub(1);
947                } else {
948                    i += 1;
949                }
950            }
951        }
952
953        if result.len() < 2 {
954            None
955        } else {
956            Some(result)
957        }
958    }
959}
960
961impl Default for ClipperBase {
962    fn default() -> Self {
963        Self::new()
964    }
965}
966
967// ============================================================================
968// ClipperBase - Sweep-line algorithm methods
969// Direct port from clipper.engine.cpp
970// ============================================================================
971
972impl ClipperBase {
973    // ---- Helper methods for arena-based access ----
974
975    /// Check if an active edge is part of an open path
976    #[inline]
977    fn is_open_idx(&self, e_idx: usize) -> bool {
978        self.minima_list[self.active_arena[e_idx].local_min].is_open
979    }
980
981    /// Check if an active edge is at a local maximum
982    #[inline]
983    fn is_maxima_idx(&self, e_idx: usize) -> bool {
984        is_maxima_vertex(&self.vertex_arena[self.active_arena[e_idx].vertex_top])
985    }
986
987    /// Check if an active edge is horizontal
988    #[inline]
989    fn is_horizontal_idx(&self, e_idx: usize) -> bool {
990        self.active_arena[e_idx].top.y == self.active_arena[e_idx].bot.y
991    }
992
993    /// Check if an active edge is the open end
994    #[inline]
995    fn is_open_end_idx(&self, e_idx: usize) -> bool {
996        is_open_end_vertex(&self.vertex_arena[self.active_arena[e_idx].vertex_top])
997    }
998
999    /// Get polytype for an active edge
1000    #[inline]
1001    fn get_poly_type_idx(&self, e_idx: usize) -> PathType {
1002        self.minima_list[self.active_arena[e_idx].local_min].polytype
1003    }
1004
1005    /// Check if two edges have same polytype
1006    #[inline]
1007    fn is_same_poly_type_idx(&self, e1: usize, e2: usize) -> bool {
1008        self.minima_list[self.active_arena[e1].local_min].polytype
1009            == self.minima_list[self.active_arena[e2].local_min].polytype
1010    }
1011
1012    /// Get next vertex index for an active
1013    #[inline]
1014    fn next_vertex_idx(&self, e_idx: usize) -> usize {
1015        if self.active_arena[e_idx].wind_dx > 0 {
1016            self.vertex_arena[self.active_arena[e_idx].vertex_top].next
1017        } else {
1018            self.vertex_arena[self.active_arena[e_idx].vertex_top].prev
1019        }
1020    }
1021
1022    // ---- Reset ----
1023
1024    /// Reset for a new execution
1025    /// Direct port from clipper.engine.cpp Reset (line 786)
1026    pub fn reset(&mut self) {
1027        self.sort_minima_list();
1028        // Insert all local minima y-values as scanlines
1029        for i in (0..self.minima_list.len()).rev() {
1030            let y = self.vertex_arena[self.minima_list[i].vertex].pt.y;
1031            self.insert_scanline(y);
1032        }
1033        self.current_locmin_idx = 0;
1034        self.actives = None;
1035        self.sel = None;
1036        self.succeeded = true;
1037    }
1038
1039    /// Clean up after execution
1040    pub fn clean_up(&mut self) {
1041        // In arena-based approach, just clear the runtime state
1042        self.active_arena.clear();
1043        self.scanline_list.clear();
1044        self.intersect_nodes.clear();
1045        // Dispose all output
1046        for i in 0..self.outrec_list.len() {
1047            self.outrec_list[i].pts = None;
1048        }
1049        self.outpt_arena.clear();
1050        self.outrec_list.clear();
1051        self.horz_seg_list.clear();
1052        self.horz_join_list.clear();
1053        self.actives = None;
1054        self.sel = None;
1055    }
1056
1057    // ---- Wind count and contributing checks ----
1058
1059    /// Check if a closed path edge is contributing to the solution
1060    /// Direct port from clipper.engine.cpp IsContributingClosed (line 908)
1061    fn is_contributing_closed(&self, e_idx: usize) -> bool {
1062        let e = &self.active_arena[e_idx];
1063        match self.fillrule {
1064            FillRule::EvenOdd => {}
1065            FillRule::NonZero => {
1066                if e.wind_cnt.abs() != 1 {
1067                    return false;
1068                }
1069            }
1070            FillRule::Positive => {
1071                if e.wind_cnt != 1 {
1072                    return false;
1073                }
1074            }
1075            FillRule::Negative => {
1076                if e.wind_cnt != -1 {
1077                    return false;
1078                }
1079            }
1080        }
1081
1082        match self.cliptype {
1083            ClipType::NoClip => false,
1084            ClipType::Intersection => match self.fillrule {
1085                FillRule::Positive => e.wind_cnt2 > 0,
1086                FillRule::Negative => e.wind_cnt2 < 0,
1087                _ => e.wind_cnt2 != 0,
1088            },
1089            ClipType::Union => match self.fillrule {
1090                FillRule::Positive => e.wind_cnt2 <= 0,
1091                FillRule::Negative => e.wind_cnt2 >= 0,
1092                _ => e.wind_cnt2 == 0,
1093            },
1094            ClipType::Difference => {
1095                let result = match self.fillrule {
1096                    FillRule::Positive => e.wind_cnt2 <= 0,
1097                    FillRule::Negative => e.wind_cnt2 >= 0,
1098                    _ => e.wind_cnt2 == 0,
1099                };
1100                if self.get_poly_type_idx(e_idx) == PathType::Subject {
1101                    result
1102                } else {
1103                    !result
1104                }
1105            }
1106            ClipType::Xor => true,
1107        }
1108    }
1109
1110    /// Check if an open path edge is contributing to the solution
1111    /// Direct port from clipper.engine.cpp IsContributingOpen (line 984)
1112    fn is_contributing_open(&self, e_idx: usize) -> bool {
1113        let e = &self.active_arena[e_idx];
1114        let (is_in_clip, is_in_subj) = match self.fillrule {
1115            FillRule::Positive => (e.wind_cnt2 > 0, e.wind_cnt > 0),
1116            FillRule::Negative => (e.wind_cnt2 < 0, e.wind_cnt < 0),
1117            _ => (e.wind_cnt2 != 0, e.wind_cnt != 0),
1118        };
1119
1120        match self.cliptype {
1121            ClipType::Intersection => is_in_clip,
1122            ClipType::Union => !is_in_subj && !is_in_clip,
1123            _ => !is_in_clip,
1124        }
1125    }
1126
1127    /// Set wind count for a closed path edge
1128    /// Direct port from clipper.engine.cpp SetWindCountForClosedPathEdge (line 1011)
1129    fn set_wind_count_for_closed_path_edge(&mut self, e_idx: usize) {
1130        let pt = self.get_poly_type_idx(e_idx);
1131        let e_wind_dx = self.active_arena[e_idx].wind_dx;
1132
1133        // Find nearest closed path edge of same PolyType in AEL (heading left)
1134        let mut e2_opt = self.active_arena[e_idx].prev_in_ael;
1135        while let Some(e2_idx) = e2_opt {
1136            if self.get_poly_type_idx(e2_idx) == pt && !self.is_open_idx(e2_idx) {
1137                break;
1138            }
1139            e2_opt = self.active_arena[e2_idx].prev_in_ael;
1140        }
1141
1142        let e2_start; // where to start scanning for wind_cnt2
1143        if let Some(e2_idx) = e2_opt {
1144            if self.fillrule == FillRule::EvenOdd {
1145                self.active_arena[e_idx].wind_cnt = e_wind_dx;
1146                self.active_arena[e_idx].wind_cnt2 = self.active_arena[e2_idx].wind_cnt2;
1147                e2_start = self.active_arena[e2_idx].next_in_ael;
1148            } else {
1149                let e2_wind_cnt = self.active_arena[e2_idx].wind_cnt;
1150                let e2_wind_dx = self.active_arena[e2_idx].wind_dx;
1151                // NonZero, Positive, or Negative filling
1152                if e2_wind_cnt * e2_wind_dx < 0 {
1153                    // opposite directions: 'e' is outside 'e2'
1154                    if e2_wind_cnt.abs() > 1 {
1155                        if e2_wind_dx * e_wind_dx < 0 {
1156                            self.active_arena[e_idx].wind_cnt = e2_wind_cnt;
1157                        } else {
1158                            self.active_arena[e_idx].wind_cnt = e2_wind_cnt + e_wind_dx;
1159                        }
1160                    } else {
1161                        self.active_arena[e_idx].wind_cnt = if self.is_open_idx(e_idx) {
1162                            1
1163                        } else {
1164                            e_wind_dx
1165                        };
1166                    }
1167                } else {
1168                    // 'e' must be inside 'e2'
1169                    if e2_wind_dx * e_wind_dx < 0 {
1170                        self.active_arena[e_idx].wind_cnt = e2_wind_cnt;
1171                    } else {
1172                        self.active_arena[e_idx].wind_cnt = e2_wind_cnt + e_wind_dx;
1173                    }
1174                }
1175                self.active_arena[e_idx].wind_cnt2 = self.active_arena[e2_idx].wind_cnt2;
1176                e2_start = self.active_arena[e2_idx].next_in_ael;
1177            }
1178        } else {
1179            self.active_arena[e_idx].wind_cnt = e_wind_dx;
1180            e2_start = self.actives;
1181        }
1182
1183        // Update wind_cnt2
1184        let mut e2_cur = e2_start;
1185        if self.fillrule == FillRule::EvenOdd {
1186            while e2_cur != Some(e_idx) {
1187                if let Some(e2i) = e2_cur {
1188                    if self.get_poly_type_idx(e2i) != pt && !self.is_open_idx(e2i) {
1189                        let wc2 = self.active_arena[e_idx].wind_cnt2;
1190                        self.active_arena[e_idx].wind_cnt2 = if wc2 == 0 { 1 } else { 0 };
1191                    }
1192                    e2_cur = self.active_arena[e2i].next_in_ael;
1193                } else {
1194                    break;
1195                }
1196            }
1197        } else {
1198            while e2_cur != Some(e_idx) {
1199                if let Some(e2i) = e2_cur {
1200                    if self.get_poly_type_idx(e2i) != pt && !self.is_open_idx(e2i) {
1201                        let e2_wd = self.active_arena[e2i].wind_dx;
1202                        self.active_arena[e_idx].wind_cnt2 += e2_wd;
1203                    }
1204                    e2_cur = self.active_arena[e2i].next_in_ael;
1205                } else {
1206                    break;
1207                }
1208            }
1209        }
1210    }
1211
1212    /// Set wind count for an open path edge
1213    /// Direct port from clipper.engine.cpp SetWindCountForOpenPathEdge (line 1089)
1214    fn set_wind_count_for_open_path_edge(&mut self, e_idx: usize) {
1215        let mut e2_opt = self.actives;
1216        if self.fillrule == FillRule::EvenOdd {
1217            let mut cnt1 = 0i32;
1218            let mut cnt2 = 0i32;
1219            while e2_opt != Some(e_idx) {
1220                if let Some(e2i) = e2_opt {
1221                    if self.get_poly_type_idx(e2i) == PathType::Clip {
1222                        cnt2 += 1;
1223                    } else if !self.is_open_idx(e2i) {
1224                        cnt1 += 1;
1225                    }
1226                    e2_opt = self.active_arena[e2i].next_in_ael;
1227                } else {
1228                    break;
1229                }
1230            }
1231            self.active_arena[e_idx].wind_cnt = if is_odd(cnt1) { 1 } else { 0 };
1232            self.active_arena[e_idx].wind_cnt2 = if is_odd(cnt2) { 1 } else { 0 };
1233        } else {
1234            while e2_opt != Some(e_idx) {
1235                if let Some(e2i) = e2_opt {
1236                    if self.get_poly_type_idx(e2i) == PathType::Clip {
1237                        self.active_arena[e_idx].wind_cnt2 += self.active_arena[e2i].wind_dx;
1238                    } else if !self.is_open_idx(e2i) {
1239                        self.active_arena[e_idx].wind_cnt += self.active_arena[e2i].wind_dx;
1240                    }
1241                    e2_opt = self.active_arena[e2i].next_in_ael;
1242                } else {
1243                    break;
1244                }
1245            }
1246        }
1247    }
1248
1249    // ---- Edge insertion ----
1250
1251    /// Insert a left-bound edge into the AEL
1252    /// Direct port from clipper.engine.cpp InsertLeftEdge (line 1157)
1253    fn insert_left_edge(&mut self, e_idx: usize) {
1254        if self.actives.is_none() {
1255            self.active_arena[e_idx].prev_in_ael = None;
1256            self.active_arena[e_idx].next_in_ael = None;
1257            self.actives = Some(e_idx);
1258        } else {
1259            let actives_idx = self.actives.unwrap();
1260            if !is_valid_ael_order(
1261                &self.active_arena[actives_idx],
1262                &self.active_arena[e_idx],
1263                &self.vertex_arena,
1264                &self.minima_list,
1265            ) {
1266                self.active_arena[e_idx].prev_in_ael = None;
1267                self.active_arena[e_idx].next_in_ael = self.actives;
1268                self.active_arena[actives_idx].prev_in_ael = Some(e_idx);
1269                self.actives = Some(e_idx);
1270            } else {
1271                let mut e2 = actives_idx;
1272                while let Some(next) = self.active_arena[e2].next_in_ael {
1273                    if !is_valid_ael_order(
1274                        &self.active_arena[next],
1275                        &self.active_arena[e_idx],
1276                        &self.vertex_arena,
1277                        &self.minima_list,
1278                    ) {
1279                        break;
1280                    }
1281                    e2 = next;
1282                }
1283                if self.active_arena[e2].join_with == JoinWith::Right {
1284                    if let Some(next) = self.active_arena[e2].next_in_ael {
1285                        e2 = next;
1286                    } else {
1287                        return;
1288                    }
1289                }
1290                let next = self.active_arena[e2].next_in_ael;
1291                self.active_arena[e_idx].next_in_ael = next;
1292                if let Some(next_idx) = next {
1293                    self.active_arena[next_idx].prev_in_ael = Some(e_idx);
1294                }
1295                self.active_arena[e_idx].prev_in_ael = Some(e2);
1296                self.active_arena[e2].next_in_ael = Some(e_idx);
1297            }
1298        }
1299    }
1300
1301    /// Insert a right-bound edge into AEL after its left-bound partner
1302    /// Direct port from clipper.engine.cpp InsertRightEdge (line 1189)
1303    fn insert_right_edge(&mut self, e_idx: usize, e2_idx: usize) {
1304        let next = self.active_arena[e_idx].next_in_ael;
1305        self.active_arena[e2_idx].next_in_ael = next;
1306        if let Some(next_idx) = next {
1307            self.active_arena[next_idx].prev_in_ael = Some(e2_idx);
1308        }
1309        self.active_arena[e2_idx].prev_in_ael = Some(e_idx);
1310        self.active_arena[e_idx].next_in_ael = Some(e2_idx);
1311    }
1312
1313    // ---- Output management ----
1314
1315    /// Add an output point for an active edge
1316    /// Direct port from clipper.engine.cpp AddOutPt (line 1497)
1317    fn add_out_pt(&mut self, e_idx: usize, pt: Point64) -> usize {
1318        let or_idx = self.active_arena[e_idx].outrec.unwrap();
1319        let to_front = self.is_front(e_idx);
1320        let op_front = self.outrec_list[or_idx].pts.unwrap();
1321        let op_back = self.outpt_arena[op_front].next;
1322
1323        if to_front {
1324            if pt == self.outpt_arena[op_front].pt {
1325                return op_front;
1326            }
1327        } else if pt == self.outpt_arena[op_back].pt {
1328            return op_back;
1329        }
1330
1331        let new_idx = self.outpt_arena.len();
1332        let mut new_op = OutPt::new(pt, or_idx);
1333        new_op.prev = op_front;
1334        new_op.next = op_back;
1335        self.outpt_arena.push(new_op);
1336        self.outpt_arena[op_back].prev = new_idx;
1337        self.outpt_arena[op_front].next = new_idx;
1338
1339        if to_front {
1340            self.outrec_list[or_idx].pts = Some(new_idx);
1341        }
1342        new_idx
1343    }
1344
1345    /// Start an open path output
1346    /// Direct port from clipper.engine.cpp StartOpenPath (line 1696)
1347    fn start_open_path(&mut self, e_idx: usize, pt: Point64) -> usize {
1348        let outrec_idx = self.new_out_rec();
1349        self.outrec_list[outrec_idx].is_open = true;
1350
1351        if self.active_arena[e_idx].wind_dx > 0 {
1352            self.outrec_list[outrec_idx].front_edge = Some(e_idx);
1353            self.outrec_list[outrec_idx].back_edge = None;
1354        } else {
1355            self.outrec_list[outrec_idx].front_edge = None;
1356            self.outrec_list[outrec_idx].back_edge = Some(e_idx);
1357        }
1358
1359        self.active_arena[e_idx].outrec = Some(outrec_idx);
1360
1361        let op_idx = self.new_out_pt(pt, outrec_idx);
1362        self.outrec_list[outrec_idx].pts = Some(op_idx);
1363        op_idx
1364    }
1365
1366    /// Add a local minimum polygon
1367    /// Direct port from clipper.engine.cpp AddLocalMinPoly (line 1332)
1368    fn add_local_min_poly(
1369        &mut self,
1370        e1_idx: usize,
1371        e2_idx: usize,
1372        pt: Point64,
1373        is_new: bool,
1374    ) -> usize {
1375        let outrec_idx = self.new_out_rec();
1376        self.active_arena[e1_idx].outrec = Some(outrec_idx);
1377        self.active_arena[e2_idx].outrec = Some(outrec_idx);
1378
1379        if self.is_open_idx(e1_idx) {
1380            self.outrec_list[outrec_idx].owner = None;
1381            self.outrec_list[outrec_idx].is_open = true;
1382            if self.active_arena[e1_idx].wind_dx > 0 {
1383                self.set_sides(outrec_idx, e1_idx, e2_idx);
1384            } else {
1385                self.set_sides(outrec_idx, e2_idx, e1_idx);
1386            }
1387        } else {
1388            let prev_hot = self.get_prev_hot_edge(e1_idx);
1389            if let Some(prev_hot_idx) = prev_hot {
1390                if self.using_polytree {
1391                    if let Some(prev_or) = self.active_arena[prev_hot_idx].outrec {
1392                        set_owner(&mut self.outrec_list, outrec_idx, prev_or);
1393                    }
1394                }
1395                if outrec_is_ascending(prev_hot_idx, &self.outrec_list, &self.active_arena)
1396                    == is_new
1397                {
1398                    self.set_sides(outrec_idx, e2_idx, e1_idx);
1399                } else {
1400                    self.set_sides(outrec_idx, e1_idx, e2_idx);
1401                }
1402            } else {
1403                self.outrec_list[outrec_idx].owner = None;
1404                if is_new {
1405                    self.set_sides(outrec_idx, e1_idx, e2_idx);
1406                } else {
1407                    self.set_sides(outrec_idx, e2_idx, e1_idx);
1408                }
1409            }
1410        }
1411
1412        let op_idx = self.new_out_pt(pt, outrec_idx);
1413        self.outrec_list[outrec_idx].pts = Some(op_idx);
1414        op_idx
1415    }
1416
1417    /// Add a local maximum polygon (close a polygon)
1418    /// Direct port from clipper.engine.cpp AddLocalMaxPoly (line 1380)
1419    fn add_local_max_poly(&mut self, e1_idx: usize, e2_idx: usize, pt: Point64) -> Option<usize> {
1420        if is_joined(&self.active_arena[e1_idx]) {
1421            self.split(e1_idx, pt);
1422        }
1423        if is_joined(&self.active_arena[e2_idx]) {
1424            self.split(e2_idx, pt);
1425        }
1426
1427        if self.is_front(e1_idx) == self.is_front(e2_idx) {
1428            if self.is_open_end_idx(e1_idx) {
1429                let or_idx = self.active_arena[e1_idx].outrec.unwrap();
1430                swap_front_back_sides(or_idx, &mut self.outrec_list, &self.outpt_arena);
1431            } else if self.is_open_end_idx(e2_idx) {
1432                let or_idx = self.active_arena[e2_idx].outrec.unwrap();
1433                swap_front_back_sides(or_idx, &mut self.outrec_list, &self.outpt_arena);
1434            } else {
1435                self.succeeded = false;
1436                return None;
1437            }
1438        }
1439
1440        let result = self.add_out_pt(e1_idx, pt);
1441        let or1 = self.active_arena[e1_idx].outrec;
1442        let or2 = self.active_arena[e2_idx].outrec;
1443
1444        if or1 == or2 {
1445            let or_idx = or1.unwrap();
1446            self.outrec_list[or_idx].pts = Some(result);
1447
1448            if self.using_polytree {
1449                let prev_hot = self.get_prev_hot_edge(e1_idx);
1450                if prev_hot.is_none() {
1451                    self.outrec_list[or_idx].owner = None;
1452                } else if let Some(prev_hot_idx) = prev_hot {
1453                    if let Some(prev_or) = self.active_arena[prev_hot_idx].outrec {
1454                        set_owner(&mut self.outrec_list, or_idx, prev_or);
1455                    }
1456                }
1457            }
1458
1459            uncouple_outrec(e1_idx, &mut self.active_arena, &mut self.outrec_list);
1460            let final_result = self.outrec_list[or_idx].pts;
1461            if let Some(owner) = self.outrec_list[or_idx].owner {
1462                if self.outrec_list[owner].front_edge.is_none() {
1463                    self.outrec_list[or_idx].owner = get_real_outrec(&self.outrec_list, owner);
1464                }
1465            }
1466            return final_result;
1467        } else if self.is_open_idx(e1_idx) {
1468            if self.active_arena[e1_idx].wind_dx < 0 {
1469                self.join_outrec_paths(e1_idx, e2_idx);
1470            } else {
1471                self.join_outrec_paths(e2_idx, e1_idx);
1472            }
1473        } else {
1474            let or1_idx = or1.unwrap();
1475            let or2_idx = or2.unwrap();
1476            if self.outrec_list[or1_idx].idx < self.outrec_list[or2_idx].idx {
1477                self.join_outrec_paths(e1_idx, e2_idx);
1478            } else {
1479                self.join_outrec_paths(e2_idx, e1_idx);
1480            }
1481        }
1482        Some(result)
1483    }
1484
1485    /// Join two outrec paths
1486    /// Direct port from clipper.engine.cpp JoinOutrecPaths (line 1435)
1487    fn join_outrec_paths(&mut self, e1_idx: usize, e2_idx: usize) {
1488        let or1 = self.active_arena[e1_idx].outrec.unwrap();
1489        let or2 = self.active_arena[e2_idx].outrec.unwrap();
1490
1491        let p1_st = self.outrec_list[or1].pts.unwrap();
1492        let p2_st = self.outrec_list[or2].pts.unwrap();
1493        let p1_end = self.outpt_arena[p1_st].next;
1494        let p2_end = self.outpt_arena[p2_st].next;
1495
1496        if self.is_front(e1_idx) {
1497            self.outpt_arena[p2_end].prev = p1_st;
1498            self.outpt_arena[p1_st].next = p2_end;
1499            self.outpt_arena[p2_st].next = p1_end;
1500            self.outpt_arena[p1_end].prev = p2_st;
1501            self.outrec_list[or1].pts = Some(p2_st);
1502            self.outrec_list[or1].front_edge = self.outrec_list[or2].front_edge;
1503            if let Some(fe) = self.outrec_list[or1].front_edge {
1504                self.active_arena[fe].outrec = Some(or1);
1505            }
1506        } else {
1507            self.outpt_arena[p1_end].prev = p2_st;
1508            self.outpt_arena[p2_st].next = p1_end;
1509            self.outpt_arena[p1_st].next = p2_end;
1510            self.outpt_arena[p2_end].prev = p1_st;
1511            self.outrec_list[or1].back_edge = self.outrec_list[or2].back_edge;
1512            if let Some(be) = self.outrec_list[or1].back_edge {
1513                self.active_arena[be].outrec = Some(or1);
1514            }
1515        }
1516
1517        self.outrec_list[or2].front_edge = None;
1518        self.outrec_list[or2].back_edge = None;
1519        self.outrec_list[or2].pts = None;
1520
1521        if self.is_open_end_idx(e1_idx) {
1522            self.outrec_list[or2].pts = self.outrec_list[or1].pts;
1523            self.outrec_list[or1].pts = None;
1524        } else {
1525            set_owner(&mut self.outrec_list, or2, or1);
1526        }
1527
1528        self.active_arena[e1_idx].outrec = None;
1529        self.active_arena[e2_idx].outrec = None;
1530    }
1531
1532    /// Split joined edges
1533    /// Direct port from clipper.engine.cpp Split (line 2796)
1534    fn split(&mut self, e_idx: usize, pt: Point64) {
1535        if self.active_arena[e_idx].join_with == JoinWith::Right {
1536            self.active_arena[e_idx].join_with = JoinWith::NoJoin;
1537            let next = self.active_arena[e_idx].next_in_ael.unwrap();
1538            self.active_arena[next].join_with = JoinWith::NoJoin;
1539            self.add_local_min_poly(e_idx, next, pt, true);
1540        } else {
1541            self.active_arena[e_idx].join_with = JoinWith::NoJoin;
1542            let prev = self.active_arena[e_idx].prev_in_ael.unwrap();
1543            self.active_arena[prev].join_with = JoinWith::NoJoin;
1544            self.add_local_min_poly(prev, e_idx, pt, true);
1545        }
1546    }
1547
1548    // ---- AEL management ----
1549
1550    /// Delete an active from the AEL
1551    /// Direct port from clipper.engine.cpp DeleteFromAEL (line 2099)
1552    fn delete_from_ael(&mut self, e_idx: usize) {
1553        let prev = self.active_arena[e_idx].prev_in_ael;
1554        let next = self.active_arena[e_idx].next_in_ael;
1555        if prev.is_none() && next.is_none() && self.actives != Some(e_idx) {
1556            return; // already deleted
1557        }
1558        if let Some(prev_idx) = prev {
1559            self.active_arena[prev_idx].next_in_ael = next;
1560        } else {
1561            self.actives = next;
1562        }
1563        if let Some(next_idx) = next {
1564            self.active_arena[next_idx].prev_in_ael = prev;
1565        }
1566        // Mark as unlinked
1567        self.active_arena[e_idx].prev_in_ael = None;
1568        self.active_arena[e_idx].next_in_ael = None;
1569    }
1570
1571    /// Swap positions of two adjacent active edges in the AEL
1572    /// Direct port from clipper.engine.cpp SwapPositionsInAEL (line 2482)
1573    fn swap_positions_in_ael(&mut self, e1_idx: usize, e2_idx: usize) {
1574        // precondition: e1 must be immediately to the left of e2
1575        let next = self.active_arena[e2_idx].next_in_ael;
1576        if let Some(next_idx) = next {
1577            self.active_arena[next_idx].prev_in_ael = Some(e1_idx);
1578        }
1579        let prev = self.active_arena[e1_idx].prev_in_ael;
1580        if let Some(prev_idx) = prev {
1581            self.active_arena[prev_idx].next_in_ael = Some(e2_idx);
1582        }
1583        self.active_arena[e2_idx].prev_in_ael = prev;
1584        self.active_arena[e2_idx].next_in_ael = Some(e1_idx);
1585        self.active_arena[e1_idx].prev_in_ael = Some(e2_idx);
1586        self.active_arena[e1_idx].next_in_ael = next;
1587        if self.active_arena[e2_idx].prev_in_ael.is_none() {
1588            self.actives = Some(e2_idx);
1589        }
1590    }
1591
1592    /// Copy AEL to SEL and update curr_x for top_y
1593    /// Direct port from clipper.engine.cpp AdjustCurrXAndCopyToSEL (line 2113)
1594    fn adjust_curr_x_and_copy_to_sel(&mut self, top_y: i64) {
1595        let mut e_opt = self.actives;
1596        self.sel = e_opt;
1597        while let Some(e_idx) = e_opt {
1598            self.active_arena[e_idx].prev_in_sel = self.active_arena[e_idx].prev_in_ael;
1599            self.active_arena[e_idx].next_in_sel = self.active_arena[e_idx].next_in_ael;
1600            self.active_arena[e_idx].jump = self.active_arena[e_idx].next_in_sel;
1601            self.active_arena[e_idx].curr_x = top_x(&self.active_arena[e_idx], top_y);
1602            e_opt = self.active_arena[e_idx].next_in_ael;
1603        }
1604    }
1605
1606    // ---- Trim horizontal ----
1607
1608    /// Trim horizontal edge
1609    /// Direct port from clipper.engine.cpp TrimHorz (line 1719)
1610    fn trim_horz(&mut self, e_idx: usize) {
1611        let mut was_trimmed = false;
1612        let nv = self.next_vertex_idx(e_idx);
1613        let mut pt = self.vertex_arena[nv].pt;
1614
1615        while pt.y == self.active_arena[e_idx].top.y {
1616            if self.preserve_collinear
1617                && (pt.x < self.active_arena[e_idx].top.x)
1618                    != (self.active_arena[e_idx].bot.x < self.active_arena[e_idx].top.x)
1619            {
1620                break;
1621            }
1622
1623            self.active_arena[e_idx].vertex_top = self.next_vertex_idx(e_idx);
1624            self.active_arena[e_idx].top = pt;
1625            was_trimmed = true;
1626            if self.is_maxima_idx(e_idx) {
1627                break;
1628            }
1629            let nv2 = self.next_vertex_idx(e_idx);
1630            pt = self.vertex_arena[nv2].pt;
1631        }
1632
1633        if was_trimmed {
1634            set_dx(&mut self.active_arena[e_idx]);
1635        }
1636    }
1637
1638    /// Update edge into AEL (advance to next segment)
1639    /// Direct port from clipper.engine.cpp UpdateEdgeIntoAEL (line 1742)
1640    fn update_edge_into_ael(&mut self, e_idx: usize) {
1641        self.active_arena[e_idx].bot = self.active_arena[e_idx].top;
1642        self.active_arena[e_idx].vertex_top = self.next_vertex_idx(e_idx);
1643        self.active_arena[e_idx].top = self.vertex_arena[self.active_arena[e_idx].vertex_top].pt;
1644        self.active_arena[e_idx].curr_x = self.active_arena[e_idx].bot.x;
1645        set_dx(&mut self.active_arena[e_idx]);
1646
1647        if is_joined(&self.active_arena[e_idx]) {
1648            let bot = self.active_arena[e_idx].bot;
1649            self.split(e_idx, bot);
1650        }
1651
1652        if self.is_horizontal_idx(e_idx) {
1653            if !self.is_open_idx(e_idx) {
1654                self.trim_horz(e_idx);
1655            }
1656            return;
1657        }
1658
1659        let top_y = self.active_arena[e_idx].top.y;
1660        self.insert_scanline(top_y);
1661        let bot = self.active_arena[e_idx].bot;
1662        self.check_join_left(e_idx, bot, false);
1663        self.check_join_right(e_idx, bot, true);
1664    }
1665
1666    /// Find edge with matching local min
1667    /// Direct port from clipper.engine.cpp FindEdgeWithMatchingLocMin (line 1763)
1668    fn find_edge_with_matching_loc_min(&self, e_idx: usize) -> Option<usize> {
1669        let local_min = self.active_arena[e_idx].local_min;
1670
1671        // Search forward
1672        let mut result = self.active_arena[e_idx].next_in_ael;
1673        while let Some(r_idx) = result {
1674            if self.active_arena[r_idx].local_min == local_min {
1675                return Some(r_idx);
1676            }
1677            if !self.is_horizontal_idx(r_idx)
1678                && self.active_arena[e_idx].bot != self.active_arena[r_idx].bot
1679            {
1680                result = None;
1681                break;
1682            }
1683            result = self.active_arena[r_idx].next_in_ael;
1684        }
1685
1686        // Search backward
1687        if result.is_none() {
1688            result = self.active_arena[e_idx].prev_in_ael;
1689            while let Some(r_idx) = result {
1690                if self.active_arena[r_idx].local_min == local_min {
1691                    return Some(r_idx);
1692                }
1693                if !self.is_horizontal_idx(r_idx)
1694                    && self.active_arena[e_idx].bot != self.active_arena[r_idx].bot
1695                {
1696                    return None;
1697                }
1698                result = self.active_arena[r_idx].prev_in_ael;
1699            }
1700        }
1701        result
1702    }
1703
1704    // ---- Check Join ----
1705
1706    /// Check join left
1707    /// Direct port from clipper.engine.cpp CheckJoinLeft (line 2812)
1708    fn check_join_left(&mut self, e_idx: usize, pt: Point64, check_curr_x: bool) {
1709        let prev = self.active_arena[e_idx].prev_in_ael;
1710        let prev_idx = match prev {
1711            Some(p) => p,
1712            None => return,
1713        };
1714
1715        if !is_hot_edge(&self.active_arena[e_idx])
1716            || !is_hot_edge(&self.active_arena[prev_idx])
1717            || self.is_horizontal_idx(e_idx)
1718            || self.is_horizontal_idx(prev_idx)
1719            || self.is_open_idx(e_idx)
1720            || self.is_open_idx(prev_idx)
1721        {
1722            return;
1723        }
1724
1725        let e_top = self.active_arena[e_idx].top;
1726        let p_top = self.active_arena[prev_idx].top;
1727        if (pt.y < e_top.y + 2 || pt.y < p_top.y + 2)
1728            && (self.active_arena[e_idx].bot.y > pt.y || self.active_arena[prev_idx].bot.y > pt.y)
1729        {
1730            return;
1731        }
1732
1733        if check_curr_x {
1734            if perpendic_dist_from_line_sqrd(
1735                pt,
1736                self.active_arena[prev_idx].bot,
1737                self.active_arena[prev_idx].top,
1738            ) > 0.25
1739            {
1740                return;
1741            }
1742        } else if self.active_arena[e_idx].curr_x != self.active_arena[prev_idx].curr_x {
1743            return;
1744        }
1745
1746        if !is_collinear(e_top, pt, p_top) {
1747            return;
1748        }
1749
1750        let e_or = self.active_arena[e_idx].outrec.unwrap();
1751        let p_or = self.active_arena[prev_idx].outrec.unwrap();
1752
1753        if self.outrec_list[e_or].idx == self.outrec_list[p_or].idx {
1754            self.add_local_max_poly(prev_idx, e_idx, pt);
1755        } else if self.outrec_list[e_or].idx < self.outrec_list[p_or].idx {
1756            self.join_outrec_paths(e_idx, prev_idx);
1757        } else {
1758            self.join_outrec_paths(prev_idx, e_idx);
1759        }
1760        self.active_arena[prev_idx].join_with = JoinWith::Right;
1761        self.active_arena[e_idx].join_with = JoinWith::Left;
1762    }
1763
1764    /// Check join right
1765    /// Direct port from clipper.engine.cpp CheckJoinRight (line 2840)
1766    fn check_join_right(&mut self, e_idx: usize, pt: Point64, check_curr_x: bool) {
1767        let next = self.active_arena[e_idx].next_in_ael;
1768        let next_idx = match next {
1769            Some(n) => n,
1770            None => return,
1771        };
1772
1773        if !is_hot_edge(&self.active_arena[e_idx])
1774            || !is_hot_edge(&self.active_arena[next_idx])
1775            || self.is_horizontal_idx(e_idx)
1776            || self.is_horizontal_idx(next_idx)
1777            || self.is_open_idx(e_idx)
1778            || self.is_open_idx(next_idx)
1779        {
1780            return;
1781        }
1782
1783        let e_top = self.active_arena[e_idx].top;
1784        let n_top = self.active_arena[next_idx].top;
1785        if (pt.y < e_top.y + 2 || pt.y < n_top.y + 2)
1786            && (self.active_arena[e_idx].bot.y > pt.y || self.active_arena[next_idx].bot.y > pt.y)
1787        {
1788            return;
1789        }
1790
1791        if check_curr_x {
1792            if perpendic_dist_from_line_sqrd(
1793                pt,
1794                self.active_arena[next_idx].bot,
1795                self.active_arena[next_idx].top,
1796            ) > 0.35
1797            {
1798                return;
1799            }
1800        } else if self.active_arena[e_idx].curr_x != self.active_arena[next_idx].curr_x {
1801            return;
1802        }
1803
1804        if !is_collinear(e_top, pt, n_top) {
1805            return;
1806        }
1807
1808        let e_or = self.active_arena[e_idx].outrec.unwrap();
1809        let n_or = self.active_arena[next_idx].outrec.unwrap();
1810
1811        if self.outrec_list[e_or].idx == self.outrec_list[n_or].idx {
1812            self.add_local_max_poly(e_idx, next_idx, pt);
1813        } else if self.outrec_list[e_or].idx < self.outrec_list[n_or].idx {
1814            self.join_outrec_paths(e_idx, next_idx);
1815        } else {
1816            self.join_outrec_paths(next_idx, e_idx);
1817        }
1818
1819        self.active_arena[e_idx].join_with = JoinWith::Right;
1820        self.active_arena[next_idx].join_with = JoinWith::Left;
1821    }
1822
1823    // ---- Intersect edges ----
1824
1825    /// Process intersection between two edges
1826    /// Direct port from clipper.engine.cpp IntersectEdges (line 1783)
1827    fn intersect_edges(&mut self, e1_idx: usize, e2_idx: usize, pt: Point64) {
1828        // MANAGE OPEN PATH INTERSECTIONS SEPARATELY
1829        if self.has_open_paths && (self.is_open_idx(e1_idx) || self.is_open_idx(e2_idx)) {
1830            if self.is_open_idx(e1_idx) && self.is_open_idx(e2_idx) {
1831                return;
1832            }
1833            let (edge_o, edge_c) = if self.is_open_idx(e1_idx) {
1834                (e1_idx, e2_idx)
1835            } else {
1836                (e2_idx, e1_idx)
1837            };
1838
1839            if is_joined(&self.active_arena[edge_c]) {
1840                self.split(edge_c, pt);
1841            }
1842
1843            if self.active_arena[edge_c].wind_cnt.abs() != 1 {
1844                return;
1845            }
1846
1847            match self.cliptype {
1848                ClipType::Union => {
1849                    if !is_hot_edge(&self.active_arena[edge_c]) {
1850                        return;
1851                    }
1852                }
1853                _ => {
1854                    if self.minima_list[self.active_arena[edge_c].local_min].polytype
1855                        == PathType::Subject
1856                    {
1857                        return;
1858                    }
1859                }
1860            }
1861
1862            match self.fillrule {
1863                FillRule::Positive => {
1864                    if self.active_arena[edge_c].wind_cnt != 1 {
1865                        return;
1866                    }
1867                }
1868                FillRule::Negative => {
1869                    if self.active_arena[edge_c].wind_cnt != -1 {
1870                        return;
1871                    }
1872                }
1873                _ => {
1874                    if self.active_arena[edge_c].wind_cnt.abs() != 1 {
1875                        return;
1876                    }
1877                }
1878            }
1879
1880            // toggle contribution
1881            if is_hot_edge(&self.active_arena[edge_o]) {
1882                self.add_out_pt(edge_o, pt);
1883                if self.is_front(edge_o) {
1884                    let or = self.active_arena[edge_o].outrec.unwrap();
1885                    self.outrec_list[or].front_edge = None;
1886                } else {
1887                    let or = self.active_arena[edge_o].outrec.unwrap();
1888                    self.outrec_list[or].back_edge = None;
1889                }
1890                self.active_arena[edge_o].outrec = None;
1891            } else if pt == self.active_arena[edge_o].bot
1892                && pt
1893                    == self.vertex_arena
1894                        [self.minima_list[self.active_arena[edge_o].local_min].vertex]
1895                        .pt
1896                && !is_open_end_vertex(
1897                    &self.vertex_arena
1898                        [self.minima_list[self.active_arena[edge_o].local_min].vertex],
1899                )
1900            {
1901                let e3 = self.find_edge_with_matching_loc_min(edge_o);
1902                if let Some(e3_idx) = e3 {
1903                    if is_hot_edge(&self.active_arena[e3_idx]) {
1904                        let e3_or = self.active_arena[e3_idx].outrec.unwrap();
1905                        self.active_arena[edge_o].outrec = Some(e3_or);
1906                        if self.active_arena[edge_o].wind_dx > 0 {
1907                            self.set_sides(e3_or, edge_o, e3_idx);
1908                        } else {
1909                            self.set_sides(e3_or, e3_idx, edge_o);
1910                        }
1911                        return;
1912                    }
1913                }
1914                self.start_open_path(edge_o, pt);
1915            } else {
1916                self.start_open_path(edge_o, pt);
1917            }
1918            return;
1919        }
1920
1921        // MANAGING CLOSED PATHS FROM HERE ON
1922        if is_joined(&self.active_arena[e1_idx]) {
1923            self.split(e1_idx, pt);
1924        }
1925        if is_joined(&self.active_arena[e2_idx]) {
1926            self.split(e2_idx, pt);
1927        }
1928
1929        // UPDATE WINDING COUNTS
1930        let old_e1_windcnt;
1931        let old_e2_windcnt;
1932
1933        if self.is_same_poly_type_idx(e1_idx, e2_idx) {
1934            if self.fillrule == FillRule::EvenOdd {
1935                let tmp = self.active_arena[e1_idx].wind_cnt;
1936                self.active_arena[e1_idx].wind_cnt = self.active_arena[e2_idx].wind_cnt;
1937                self.active_arena[e2_idx].wind_cnt = tmp;
1938            } else {
1939                let e1_wc = self.active_arena[e1_idx].wind_cnt;
1940                let e2_wd = self.active_arena[e2_idx].wind_dx;
1941                let e1_wd = self.active_arena[e1_idx].wind_dx;
1942                if e1_wc + e2_wd == 0 {
1943                    self.active_arena[e1_idx].wind_cnt = -e1_wc;
1944                } else {
1945                    self.active_arena[e1_idx].wind_cnt = e1_wc + e2_wd;
1946                }
1947                let e2_wc = self.active_arena[e2_idx].wind_cnt;
1948                if e2_wc - e1_wd == 0 {
1949                    self.active_arena[e2_idx].wind_cnt = -e2_wc;
1950                } else {
1951                    self.active_arena[e2_idx].wind_cnt = e2_wc - e1_wd;
1952                }
1953            }
1954        } else if self.fillrule != FillRule::EvenOdd {
1955            self.active_arena[e1_idx].wind_cnt2 += self.active_arena[e2_idx].wind_dx;
1956            self.active_arena[e2_idx].wind_cnt2 -= self.active_arena[e1_idx].wind_dx;
1957        } else {
1958            let wc2_1 = self.active_arena[e1_idx].wind_cnt2;
1959            self.active_arena[e1_idx].wind_cnt2 = if wc2_1 == 0 { 1 } else { 0 };
1960            let wc2_2 = self.active_arena[e2_idx].wind_cnt2;
1961            self.active_arena[e2_idx].wind_cnt2 = if wc2_2 == 0 { 1 } else { 0 };
1962        }
1963
1964        let fillpos = FillRule::Positive;
1965        match self.fillrule {
1966            FillRule::EvenOdd | FillRule::NonZero => {
1967                old_e1_windcnt = self.active_arena[e1_idx].wind_cnt.abs();
1968                old_e2_windcnt = self.active_arena[e2_idx].wind_cnt.abs();
1969            }
1970            _ => {
1971                if self.fillrule == fillpos {
1972                    old_e1_windcnt = self.active_arena[e1_idx].wind_cnt;
1973                    old_e2_windcnt = self.active_arena[e2_idx].wind_cnt;
1974                } else {
1975                    old_e1_windcnt = -self.active_arena[e1_idx].wind_cnt;
1976                    old_e2_windcnt = -self.active_arena[e2_idx].wind_cnt;
1977                }
1978            }
1979        }
1980
1981        let e1_wc_in_01 = old_e1_windcnt == 0 || old_e1_windcnt == 1;
1982        let e2_wc_in_01 = old_e2_windcnt == 0 || old_e2_windcnt == 1;
1983
1984        if (!is_hot_edge(&self.active_arena[e1_idx]) && !e1_wc_in_01)
1985            || (!is_hot_edge(&self.active_arena[e2_idx]) && !e2_wc_in_01)
1986        {
1987            return;
1988        }
1989
1990        // NOW PROCESS THE INTERSECTION
1991        if is_hot_edge(&self.active_arena[e1_idx]) && is_hot_edge(&self.active_arena[e2_idx]) {
1992            if (old_e1_windcnt != 0 && old_e1_windcnt != 1)
1993                || (old_e2_windcnt != 0 && old_e2_windcnt != 1)
1994                || (!self.is_same_poly_type_idx(e1_idx, e2_idx) && self.cliptype != ClipType::Xor)
1995            {
1996                self.add_local_max_poly(e1_idx, e2_idx, pt);
1997            } else if self.is_front(e1_idx)
1998                || self.active_arena[e1_idx].outrec == self.active_arena[e2_idx].outrec
1999            {
2000                self.add_local_max_poly(e1_idx, e2_idx, pt);
2001                self.add_local_min_poly(e1_idx, e2_idx, pt, false);
2002            } else {
2003                self.add_out_pt(e1_idx, pt);
2004                self.add_out_pt(e2_idx, pt);
2005                self.swap_outrecs(e1_idx, e2_idx);
2006            }
2007        } else if is_hot_edge(&self.active_arena[e1_idx]) {
2008            self.add_out_pt(e1_idx, pt);
2009            self.swap_outrecs(e1_idx, e2_idx);
2010        } else if is_hot_edge(&self.active_arena[e2_idx]) {
2011            self.add_out_pt(e2_idx, pt);
2012            self.swap_outrecs(e1_idx, e2_idx);
2013        } else {
2014            // neither edge is hot
2015            let e1wc2;
2016            let e2wc2;
2017            match self.fillrule {
2018                FillRule::EvenOdd | FillRule::NonZero => {
2019                    e1wc2 = self.active_arena[e1_idx].wind_cnt2.abs() as i64;
2020                    e2wc2 = self.active_arena[e2_idx].wind_cnt2.abs() as i64;
2021                }
2022                _ => {
2023                    if self.fillrule == fillpos {
2024                        e1wc2 = self.active_arena[e1_idx].wind_cnt2 as i64;
2025                        e2wc2 = self.active_arena[e2_idx].wind_cnt2 as i64;
2026                    } else {
2027                        e1wc2 = -(self.active_arena[e1_idx].wind_cnt2 as i64);
2028                        e2wc2 = -(self.active_arena[e2_idx].wind_cnt2 as i64);
2029                    }
2030                }
2031            }
2032
2033            if !self.is_same_poly_type_idx(e1_idx, e2_idx) {
2034                self.add_local_min_poly(e1_idx, e2_idx, pt, false);
2035            } else if old_e1_windcnt == 1 && old_e2_windcnt == 1 {
2036                match self.cliptype {
2037                    ClipType::Union => {
2038                        if e1wc2 <= 0 && e2wc2 <= 0 {
2039                            self.add_local_min_poly(e1_idx, e2_idx, pt, false);
2040                        }
2041                    }
2042                    ClipType::Difference => {
2043                        if (self.get_poly_type_idx(e1_idx) == PathType::Clip
2044                            && e1wc2 > 0
2045                            && e2wc2 > 0)
2046                            || (self.get_poly_type_idx(e1_idx) == PathType::Subject
2047                                && e1wc2 <= 0
2048                                && e2wc2 <= 0)
2049                        {
2050                            self.add_local_min_poly(e1_idx, e2_idx, pt, false);
2051                        }
2052                    }
2053                    ClipType::Xor => {
2054                        self.add_local_min_poly(e1_idx, e2_idx, pt, false);
2055                    }
2056                    _ => {
2057                        // Intersection
2058                        if e1wc2 > 0 && e2wc2 > 0 {
2059                            self.add_local_min_poly(e1_idx, e2_idx, pt, false);
2060                        }
2061                    }
2062                }
2063            }
2064        }
2065    }
2066
2067    // ---- Insert local minima into AEL ----
2068
2069    /// Insert local minima into AEL at a given y
2070    /// Direct port from clipper.engine.cpp InsertLocalMinimaIntoAEL (line 1198)
2071    fn insert_local_minima_into_ael(&mut self, bot_y: i64) {
2072        while let Some(loc_min_idx) = self.pop_local_minima(bot_y) {
2073            let vert_idx = self.minima_list[loc_min_idx].vertex;
2074            let vert_flags = self.vertex_arena[vert_idx].flags;
2075            let vert_pt = self.vertex_arena[vert_idx].pt;
2076
2077            // Create left bound if not open start
2078            let left_bound_opt = if (vert_flags & VertexFlags::OPEN_START) != VertexFlags::EMPTY {
2079                None
2080            } else {
2081                let lb_idx = self.active_arena.len();
2082                let mut lb = Active::new();
2083                lb.bot = vert_pt;
2084                lb.curr_x = vert_pt.x;
2085                lb.wind_dx = -1;
2086                lb.vertex_top = self.vertex_arena[vert_idx].prev; // descending
2087                lb.top = self.vertex_arena[lb.vertex_top].pt;
2088                lb.local_min = loc_min_idx;
2089                set_dx(&mut lb);
2090                self.active_arena.push(lb);
2091                Some(lb_idx)
2092            };
2093
2094            // Create right bound if not open end
2095            let right_bound_opt = if (vert_flags & VertexFlags::OPEN_END) != VertexFlags::EMPTY {
2096                None
2097            } else {
2098                let rb_idx = self.active_arena.len();
2099                let mut rb = Active::new();
2100                rb.bot = vert_pt;
2101                rb.curr_x = vert_pt.x;
2102                rb.wind_dx = 1;
2103                rb.vertex_top = self.vertex_arena[vert_idx].next; // ascending
2104                rb.top = self.vertex_arena[rb.vertex_top].pt;
2105                rb.local_min = loc_min_idx;
2106                set_dx(&mut rb);
2107                self.active_arena.push(rb);
2108                Some(rb_idx)
2109            };
2110
2111            // Determine which is actually left and which is right
2112            let (mut left_bound, mut right_bound) = (left_bound_opt, right_bound_opt);
2113
2114            if let (Some(lb), Some(rb)) = (left_bound, right_bound) {
2115                if self.is_horizontal_idx(lb) {
2116                    if is_heading_right_horz(&self.active_arena[lb]) {
2117                        std::mem::swap(&mut left_bound, &mut right_bound);
2118                    }
2119                } else if self.is_horizontal_idx(rb) {
2120                    if is_heading_left_horz(&self.active_arena[rb]) {
2121                        std::mem::swap(&mut left_bound, &mut right_bound);
2122                    }
2123                } else if self.active_arena[lb].dx < self.active_arena[rb].dx {
2124                    std::mem::swap(&mut left_bound, &mut right_bound);
2125                }
2126            } else if left_bound.is_none() {
2127                left_bound = right_bound;
2128                right_bound = None;
2129            }
2130
2131            let lb_idx = left_bound.unwrap();
2132            self.active_arena[lb_idx].is_left_bound = true;
2133            self.insert_left_edge(lb_idx);
2134
2135            let contributing = if self.is_open_idx(lb_idx) {
2136                self.set_wind_count_for_open_path_edge(lb_idx);
2137                self.is_contributing_open(lb_idx)
2138            } else {
2139                self.set_wind_count_for_closed_path_edge(lb_idx);
2140                self.is_contributing_closed(lb_idx)
2141            };
2142
2143            if let Some(rb_idx) = right_bound {
2144                self.active_arena[rb_idx].is_left_bound = false;
2145                self.active_arena[rb_idx].wind_cnt = self.active_arena[lb_idx].wind_cnt;
2146                self.active_arena[rb_idx].wind_cnt2 = self.active_arena[lb_idx].wind_cnt2;
2147                self.insert_right_edge(lb_idx, rb_idx);
2148
2149                if contributing {
2150                    let bot = self.active_arena[lb_idx].bot;
2151                    self.add_local_min_poly(lb_idx, rb_idx, bot, true);
2152                    if !self.is_horizontal_idx(lb_idx) {
2153                        let bot = self.active_arena[lb_idx].bot;
2154                        self.check_join_left(lb_idx, bot, false);
2155                    }
2156                }
2157
2158                // Process any right-bound edge intersections
2159                while let Some(next) = self.active_arena[rb_idx].next_in_ael {
2160                    if !is_valid_ael_order(
2161                        &self.active_arena[next],
2162                        &self.active_arena[rb_idx],
2163                        &self.vertex_arena,
2164                        &self.minima_list,
2165                    ) {
2166                        break;
2167                    }
2168                    let bot = self.active_arena[rb_idx].bot;
2169                    self.intersect_edges(rb_idx, next, bot);
2170                    self.swap_positions_in_ael(rb_idx, next);
2171                }
2172
2173                if self.is_horizontal_idx(rb_idx) {
2174                    self.push_horz(rb_idx);
2175                } else {
2176                    let bot = self.active_arena[rb_idx].bot;
2177                    self.check_join_right(rb_idx, bot, false);
2178                    let top_y = self.active_arena[rb_idx].top.y;
2179                    self.insert_scanline(top_y);
2180                }
2181            } else if contributing {
2182                let bot = self.active_arena[lb_idx].bot;
2183                self.start_open_path(lb_idx, bot);
2184            }
2185
2186            if self.is_horizontal_idx(lb_idx) {
2187                self.push_horz(lb_idx);
2188            } else {
2189                let top_y = self.active_arena[lb_idx].top.y;
2190                self.insert_scanline(top_y);
2191            }
2192        }
2193    }
2194
2195    // ---- Horizontal edge processing ----
2196
2197    /// Reset horizontal direction for a horizontal edge
2198    /// Direct port from clipper.engine.cpp ResetHorzDirection (line 2511)
2199    fn reset_horz_direction(&self, horz_idx: usize, max_vertex: Option<usize>) -> (i64, i64, bool) {
2200        let horz = &self.active_arena[horz_idx];
2201        if horz.bot.x == horz.top.x {
2202            let horz_left = horz.curr_x;
2203            let horz_right = horz.curr_x;
2204            let mut e = horz.next_in_ael;
2205            while let Some(e_idx) = e {
2206                if Some(self.active_arena[e_idx].vertex_top) == max_vertex {
2207                    return (horz_left, horz_right, true);
2208                }
2209                e = self.active_arena[e_idx].next_in_ael;
2210            }
2211            (horz_left, horz_right, false)
2212        } else if horz.curr_x < horz.top.x {
2213            (horz.curr_x, horz.top.x, true)
2214        } else {
2215            (horz.top.x, horz.curr_x, false)
2216        }
2217    }
2218
2219    /// Process a horizontal edge
2220    /// Direct port from clipper.engine.cpp DoHorizontal (line 2537)
2221    fn do_horizontal(&mut self, horz_idx: usize) {
2222        let horz_is_open = self.is_open_idx(horz_idx);
2223        let y = self.active_arena[horz_idx].bot.y;
2224
2225        let vertex_max = if horz_is_open {
2226            get_curr_y_maxima_vertex_open(&self.active_arena[horz_idx], &self.vertex_arena)
2227        } else {
2228            get_curr_y_maxima_vertex(&self.active_arena[horz_idx], &self.vertex_arena)
2229        };
2230
2231        let (mut horz_left, mut horz_right, mut is_left_to_right) =
2232            self.reset_horz_direction(horz_idx, vertex_max);
2233
2234        if is_hot_edge(&self.active_arena[horz_idx]) {
2235            let curr_x = self.active_arena[horz_idx].curr_x;
2236            let op = self.add_out_pt(horz_idx, Point64::new(curr_x, y));
2237            let or = self.outpt_arena[op].outrec;
2238            if !self.outrec_list[or].is_open {
2239                self.add_trial_horz_join(op);
2240            }
2241        }
2242
2243        loop {
2244            let e_start = if is_left_to_right {
2245                self.active_arena[horz_idx].next_in_ael
2246            } else {
2247                self.active_arena[horz_idx].prev_in_ael
2248            };
2249
2250            let mut e_opt = e_start;
2251            while let Some(e_idx) = e_opt {
2252                // Check if we've reached the maxima vertex pair
2253                if Some(self.active_arena[e_idx].vertex_top) == vertex_max {
2254                    if is_hot_edge(&self.active_arena[horz_idx])
2255                        && is_joined(&self.active_arena[e_idx])
2256                    {
2257                        let top = self.active_arena[e_idx].top;
2258                        self.split(e_idx, top);
2259                    }
2260
2261                    if is_hot_edge(&self.active_arena[horz_idx]) {
2262                        while Some(self.active_arena[horz_idx].vertex_top) != vertex_max {
2263                            let top = self.active_arena[horz_idx].top;
2264                            self.add_out_pt(horz_idx, top);
2265                            self.update_edge_into_ael(horz_idx);
2266                        }
2267                        if is_left_to_right {
2268                            let top = self.active_arena[horz_idx].top;
2269                            self.add_local_max_poly(horz_idx, e_idx, top);
2270                        } else {
2271                            let top = self.active_arena[horz_idx].top;
2272                            self.add_local_max_poly(e_idx, horz_idx, top);
2273                        }
2274                    }
2275                    self.delete_from_ael(e_idx);
2276                    self.delete_from_ael(horz_idx);
2277                    return;
2278                }
2279
2280                // Check break conditions for non-maxima
2281                if vertex_max != Some(self.active_arena[horz_idx].vertex_top)
2282                    || self.is_open_end_idx(horz_idx)
2283                {
2284                    if (is_left_to_right && self.active_arena[e_idx].curr_x > horz_right)
2285                        || (!is_left_to_right && self.active_arena[e_idx].curr_x < horz_left)
2286                    {
2287                        break;
2288                    }
2289
2290                    if self.active_arena[e_idx].curr_x == self.active_arena[horz_idx].top.x
2291                        && !self.is_horizontal_idx(e_idx)
2292                    {
2293                        let nv_idx = self.next_vertex_idx(horz_idx);
2294                        let nv_pt = self.vertex_arena[nv_idx].pt;
2295
2296                        if is_left_to_right {
2297                            if self.is_open_idx(e_idx)
2298                                && !self.is_same_poly_type_idx(e_idx, horz_idx)
2299                                && !is_hot_edge(&self.active_arena[e_idx])
2300                            {
2301                                if top_x(&self.active_arena[e_idx], nv_pt.y) > nv_pt.x {
2302                                    break;
2303                                }
2304                            } else if top_x(&self.active_arena[e_idx], nv_pt.y) >= nv_pt.x {
2305                                break;
2306                            }
2307                        } else if self.is_open_idx(e_idx)
2308                            && !self.is_same_poly_type_idx(e_idx, horz_idx)
2309                            && !is_hot_edge(&self.active_arena[e_idx])
2310                        {
2311                            if top_x(&self.active_arena[e_idx], nv_pt.y) < nv_pt.x {
2312                                break;
2313                            }
2314                        } else if top_x(&self.active_arena[e_idx], nv_pt.y) <= nv_pt.x {
2315                            break;
2316                        }
2317                    }
2318                }
2319
2320                let pt = Point64::new(
2321                    self.active_arena[e_idx].curr_x,
2322                    self.active_arena[horz_idx].bot.y,
2323                );
2324                if is_left_to_right {
2325                    self.intersect_edges(horz_idx, e_idx, pt);
2326                    self.swap_positions_in_ael(horz_idx, e_idx);
2327                    let pt2 = pt;
2328                    self.check_join_left(e_idx, pt2, false);
2329                    self.active_arena[horz_idx].curr_x = self.active_arena[e_idx].curr_x;
2330                    e_opt = self.active_arena[horz_idx].next_in_ael;
2331                } else {
2332                    self.intersect_edges(e_idx, horz_idx, pt);
2333                    self.swap_positions_in_ael(e_idx, horz_idx);
2334                    let pt2 = pt;
2335                    self.check_join_right(e_idx, pt2, false);
2336                    self.active_arena[horz_idx].curr_x = self.active_arena[e_idx].curr_x;
2337                    e_opt = self.active_arena[horz_idx].prev_in_ael;
2338                }
2339
2340                if self.active_arena[horz_idx].outrec.is_some() {
2341                    if let Some(last_op) = get_last_op(
2342                        horz_idx,
2343                        &self.active_arena,
2344                        &self.outrec_list,
2345                        &self.outpt_arena,
2346                    ) {
2347                        self.add_trial_horz_join(last_op);
2348                    }
2349                }
2350            }
2351
2352            // Check if finished with consecutive horizontals
2353            if horz_is_open && self.is_open_end_idx(horz_idx) {
2354                if is_hot_edge(&self.active_arena[horz_idx]) {
2355                    let top = self.active_arena[horz_idx].top;
2356                    self.add_out_pt(horz_idx, top);
2357                    if self.is_front(horz_idx) {
2358                        let or = self.active_arena[horz_idx].outrec.unwrap();
2359                        self.outrec_list[or].front_edge = None;
2360                    } else {
2361                        let or = self.active_arena[horz_idx].outrec.unwrap();
2362                        self.outrec_list[or].back_edge = None;
2363                    }
2364                    self.active_arena[horz_idx].outrec = None;
2365                }
2366                self.delete_from_ael(horz_idx);
2367                return;
2368            }
2369
2370            let nv_idx = self.next_vertex_idx(horz_idx);
2371            if self.vertex_arena[nv_idx].pt.y != self.active_arena[horz_idx].top.y {
2372                break;
2373            }
2374
2375            // Still more horizontals in bound
2376            if is_hot_edge(&self.active_arena[horz_idx]) {
2377                let top = self.active_arena[horz_idx].top;
2378                self.add_out_pt(horz_idx, top);
2379            }
2380            self.update_edge_into_ael(horz_idx);
2381
2382            let result = self.reset_horz_direction(horz_idx, vertex_max);
2383            horz_left = result.0;
2384            horz_right = result.1;
2385            is_left_to_right = result.2;
2386        }
2387
2388        if is_hot_edge(&self.active_arena[horz_idx]) {
2389            let top = self.active_arena[horz_idx].top;
2390            let op = self.add_out_pt(horz_idx, top);
2391            self.add_trial_horz_join(op);
2392        }
2393
2394        self.update_edge_into_ael(horz_idx);
2395    }
2396
2397    // ---- Intersection list processing ----
2398
2399    /// Add a new intersect node
2400    /// Direct port from clipper.engine.cpp AddNewIntersectNode (line 2356)
2401    fn add_new_intersect_node(&mut self, e1_idx: usize, e2_idx: usize, top_y: i64) {
2402        let e1 = &self.active_arena[e1_idx];
2403        let e2 = &self.active_arena[e2_idx];
2404        let mut ip = Point64::new(e1.curr_x, top_y);
2405        get_segment_intersect_pt(e1.bot, e1.top, e2.bot, e2.top, &mut ip);
2406
2407        if ip.y > self.bot_y || ip.y < top_y {
2408            let abs_dx1 = e1.dx.abs();
2409            let abs_dx2 = e2.dx.abs();
2410            if abs_dx1 > 100.0 && abs_dx2 > 100.0 {
2411                if abs_dx1 > abs_dx2 {
2412                    ip = get_closest_point_on_segment(ip, e1.bot, e1.top);
2413                } else {
2414                    ip = get_closest_point_on_segment(ip, e2.bot, e2.top);
2415                }
2416            } else if abs_dx1 > 100.0 {
2417                ip = get_closest_point_on_segment(ip, e1.bot, e1.top);
2418            } else if abs_dx2 > 100.0 {
2419                ip = get_closest_point_on_segment(ip, e2.bot, e2.top);
2420            } else {
2421                if ip.y < top_y {
2422                    ip.y = top_y;
2423                } else {
2424                    ip.y = self.bot_y;
2425                }
2426                if abs_dx1 < abs_dx2 {
2427                    ip.x = top_x(&self.active_arena[e1_idx], ip.y);
2428                } else {
2429                    ip.x = top_x(&self.active_arena[e2_idx], ip.y);
2430                }
2431            }
2432        }
2433        self.intersect_nodes
2434            .push(IntersectNode::with_edges(e1_idx, e2_idx, ip));
2435    }
2436
2437    /// Build the intersection list
2438    /// Direct port from clipper.engine.cpp BuildIntersectList (line 2390)
2439    fn build_intersect_list(&mut self, top_y: i64) -> bool {
2440        if self.actives.is_none() {
2441            return false;
2442        }
2443        let actives_idx = self.actives.unwrap();
2444        if self.active_arena[actives_idx].next_in_ael.is_none() {
2445            return false;
2446        }
2447
2448        self.adjust_curr_x_and_copy_to_sel(top_y);
2449
2450        let mut left_opt = self.sel;
2451        // Check if we have a jump
2452        if left_opt.is_none() || self.active_arena[left_opt.unwrap()].jump.is_none() {
2453            return !self.intersect_nodes.is_empty();
2454        }
2455
2456        while let Some(left_idx) = left_opt {
2457            if self.active_arena[left_idx].jump.is_none() {
2458                break;
2459            }
2460
2461            let mut prev_base: Option<usize> = None;
2462            let mut left_inner = left_opt;
2463
2464            while let Some(li) = left_inner {
2465                if self.active_arena[li].jump.is_none() {
2466                    break;
2467                }
2468
2469                let mut curr_base = li;
2470                let right_idx = self.active_arena[li].jump.unwrap();
2471                let mut l_end = right_idx;
2472                let r_end = self.active_arena[right_idx].jump;
2473                self.active_arena[li].jump = r_end;
2474
2475                let mut left_scan = li;
2476                let mut right_scan = right_idx;
2477
2478                while left_scan != l_end && right_scan != r_end.unwrap_or(NONE) {
2479                    if self.active_arena[right_scan].curr_x < self.active_arena[left_scan].curr_x {
2480                        let mut tmp = self.active_arena[right_scan].prev_in_sel;
2481                        while let Some(tmp_idx) = tmp {
2482                            self.add_new_intersect_node(tmp_idx, right_scan, top_y);
2483                            if tmp_idx == left_scan {
2484                                break;
2485                            }
2486                            tmp = self.active_arena[tmp_idx].prev_in_sel;
2487                        }
2488
2489                        let tmp_idx = right_scan;
2490                        right_scan =
2491                            extract_from_sel(tmp_idx, &mut self.active_arena).unwrap_or(NONE);
2492                        l_end = right_scan;
2493
2494                        insert1_before2_in_sel(tmp_idx, left_scan, &mut self.active_arena);
2495                        if left_scan == curr_base {
2496                            curr_base = tmp_idx;
2497                            self.active_arena[curr_base].jump = r_end;
2498                            if let Some(pb) = prev_base {
2499                                self.active_arena[pb].jump = Some(curr_base);
2500                            } else {
2501                                self.sel = Some(curr_base);
2502                            }
2503                        }
2504                    } else {
2505                        left_scan = self.active_arena[left_scan].next_in_sel.unwrap_or(NONE);
2506                    }
2507                }
2508
2509                prev_base = Some(curr_base);
2510                left_inner = r_end;
2511            }
2512
2513            left_opt = self.sel;
2514        }
2515
2516        !self.intersect_nodes.is_empty()
2517    }
2518
2519    /// Do intersections
2520    /// Direct port from clipper.engine.cpp DoIntersections (line 2347)
2521    fn do_intersections(&mut self, top_y: i64) {
2522        if self.build_intersect_list(top_y) {
2523            self.process_intersect_list();
2524            self.intersect_nodes.clear();
2525        }
2526    }
2527
2528    /// Process the intersection list
2529    /// Direct port from clipper.engine.cpp ProcessIntersectList (line 2448)
2530    fn process_intersect_list(&mut self) {
2531        // Sort by y descending (bottom up), then x ascending
2532        self.intersect_nodes.sort_by(intersect_list_sort);
2533
2534        let len = self.intersect_nodes.len();
2535        for i in 0..len {
2536            // Ensure edges are adjacent
2537            if !edges_adjacent_in_ael(&self.intersect_nodes[i], &self.active_arena) {
2538                let mut j = i + 1;
2539                while j < len
2540                    && !edges_adjacent_in_ael(&self.intersect_nodes[j], &self.active_arena)
2541                {
2542                    j += 1;
2543                }
2544                if j < len {
2545                    self.intersect_nodes.swap(i, j);
2546                }
2547            }
2548
2549            let e1 = self.intersect_nodes[i].edge1;
2550            let e2 = self.intersect_nodes[i].edge2;
2551            let pt = self.intersect_nodes[i].pt;
2552
2553            self.intersect_edges(e1, e2, pt);
2554            self.swap_positions_in_ael(e1, e2);
2555
2556            self.active_arena[e1].curr_x = pt.x;
2557            self.active_arena[e2].curr_x = pt.x;
2558            self.check_join_left(e2, pt, true);
2559            self.check_join_right(e1, pt, true);
2560        }
2561    }
2562
2563    // ---- Top of scanbeam ----
2564
2565    /// Process the top of a scanbeam
2566    /// Direct port from clipper.engine.cpp DoTopOfScanbeam (line 2708)
2567    fn do_top_of_scanbeam(&mut self, y: i64) {
2568        self.sel = None;
2569        let mut e_opt = self.actives;
2570        while let Some(e_idx) = e_opt {
2571            if self.active_arena[e_idx].top.y == y {
2572                self.active_arena[e_idx].curr_x = self.active_arena[e_idx].top.x;
2573                if self.is_maxima_idx(e_idx) {
2574                    e_opt = self.do_maxima(e_idx);
2575                    continue;
2576                } else {
2577                    if is_hot_edge(&self.active_arena[e_idx]) {
2578                        let top = self.active_arena[e_idx].top;
2579                        self.add_out_pt(e_idx, top);
2580                    }
2581                    self.update_edge_into_ael(e_idx);
2582                    if self.is_horizontal_idx(e_idx) {
2583                        self.push_horz(e_idx);
2584                    }
2585                }
2586            } else {
2587                self.active_arena[e_idx].curr_x = top_x(&self.active_arena[e_idx], y);
2588            }
2589            e_opt = self.active_arena[e_idx].next_in_ael;
2590        }
2591    }
2592
2593    /// Process a maxima edge
2594    /// Direct port from clipper.engine.cpp DoMaxima (line 2740)
2595    fn do_maxima(&mut self, e_idx: usize) -> Option<usize> {
2596        let prev_e = self.active_arena[e_idx].prev_in_ael;
2597        let mut next_e = self.active_arena[e_idx].next_in_ael;
2598
2599        if self.is_open_end_idx(e_idx) {
2600            if is_hot_edge(&self.active_arena[e_idx]) {
2601                let top = self.active_arena[e_idx].top;
2602                self.add_out_pt(e_idx, top);
2603            }
2604            if !self.is_horizontal_idx(e_idx) {
2605                if is_hot_edge(&self.active_arena[e_idx]) {
2606                    if self.is_front(e_idx) {
2607                        let or = self.active_arena[e_idx].outrec.unwrap();
2608                        self.outrec_list[or].front_edge = None;
2609                    } else {
2610                        let or = self.active_arena[e_idx].outrec.unwrap();
2611                        self.outrec_list[or].back_edge = None;
2612                    }
2613                    self.active_arena[e_idx].outrec = None;
2614                }
2615                self.delete_from_ael(e_idx);
2616            }
2617            return next_e;
2618        }
2619
2620        let max_pair = get_maxima_pair(&self.active_arena[e_idx], &self.active_arena);
2621        if max_pair.is_none() {
2622            return next_e;
2623        }
2624        let max_pair_idx = max_pair.unwrap();
2625
2626        if is_joined(&self.active_arena[e_idx]) {
2627            let top = self.active_arena[e_idx].top;
2628            self.split(e_idx, top);
2629        }
2630        if is_joined(&self.active_arena[max_pair_idx]) {
2631            let top = self.active_arena[max_pair_idx].top;
2632            self.split(max_pair_idx, top);
2633        }
2634
2635        // Process edges between maxima pair
2636        while next_e != Some(max_pair_idx) {
2637            if let Some(ne_idx) = next_e {
2638                let top = self.active_arena[e_idx].top;
2639                self.intersect_edges(e_idx, ne_idx, top);
2640                self.swap_positions_in_ael(e_idx, ne_idx);
2641                next_e = self.active_arena[e_idx].next_in_ael;
2642            } else {
2643                break;
2644            }
2645        }
2646
2647        if self.is_open_idx(e_idx) {
2648            if is_hot_edge(&self.active_arena[e_idx]) {
2649                let top = self.active_arena[e_idx].top;
2650                self.add_local_max_poly(e_idx, max_pair_idx, top);
2651            }
2652            self.delete_from_ael(max_pair_idx);
2653            self.delete_from_ael(e_idx);
2654            return if let Some(pe) = prev_e {
2655                self.active_arena[pe].next_in_ael
2656            } else {
2657                self.actives
2658            };
2659        }
2660
2661        // non-open maxima
2662        if is_hot_edge(&self.active_arena[e_idx]) {
2663            let top = self.active_arena[e_idx].top;
2664            self.add_local_max_poly(e_idx, max_pair_idx, top);
2665        }
2666
2667        self.delete_from_ael(e_idx);
2668        self.delete_from_ael(max_pair_idx);
2669        if let Some(pe) = prev_e {
2670            self.active_arena[pe].next_in_ael
2671        } else {
2672            self.actives
2673        }
2674    }
2675
2676    // ---- Horizontal join processing ----
2677
2678    /// Set horizontal segment heading forward
2679    #[allow(dead_code)]
2680    fn set_horz_seg_heading_forward(&mut self, hs_idx: usize, op_p: usize, op_n: usize) -> bool {
2681        if self.outpt_arena[op_p].pt.x == self.outpt_arena[op_n].pt.x {
2682            return false;
2683        }
2684        if self.outpt_arena[op_p].pt.x < self.outpt_arena[op_n].pt.x {
2685            self.horz_seg_list[hs_idx].left_op = Some(op_p);
2686            self.horz_seg_list[hs_idx].right_op = Some(op_n);
2687            self.horz_seg_list[hs_idx].left_to_right = true;
2688        } else {
2689            self.horz_seg_list[hs_idx].left_op = Some(op_n);
2690            self.horz_seg_list[hs_idx].right_op = Some(op_p);
2691            self.horz_seg_list[hs_idx].left_to_right = false;
2692        }
2693        true
2694    }
2695
2696    /// Convert horizontal segments to joins
2697    /// Direct port from clipper.engine.cpp ConvertHorzSegsToJoins (line 2218)
2698    fn convert_horz_segs_to_joins(&mut self) {
2699        // Update horizontal segments
2700        let mut valid_count = 0;
2701        for i in 0..self.horz_seg_list.len() {
2702            let op = match self.horz_seg_list[i].left_op {
2703                Some(op) => op,
2704                None => {
2705                    continue;
2706                }
2707            };
2708            let or_idx = self.outpt_arena[op].outrec;
2709            let real_or = get_real_outrec(&self.outrec_list, or_idx);
2710            if real_or.is_none() {
2711                continue;
2712            }
2713            let real_or_idx = real_or.unwrap();
2714            let has_edges = self.outrec_list[real_or_idx].front_edge.is_some();
2715            let curr_y = self.outpt_arena[op].pt.y;
2716
2717            let mut op_p = op;
2718            let mut op_n = op;
2719
2720            if has_edges {
2721                let op_a = self.outrec_list[real_or_idx].pts.unwrap();
2722                let op_z = self.outpt_arena[op_a].next;
2723                while op_p != op_z && self.outpt_arena[self.outpt_arena[op_p].prev].pt.y == curr_y {
2724                    op_p = self.outpt_arena[op_p].prev;
2725                }
2726                while op_n != op_a && self.outpt_arena[self.outpt_arena[op_n].next].pt.y == curr_y {
2727                    op_n = self.outpt_arena[op_n].next;
2728                }
2729            } else {
2730                while self.outpt_arena[op_p].prev != op_n
2731                    && self.outpt_arena[self.outpt_arena[op_p].prev].pt.y == curr_y
2732                {
2733                    op_p = self.outpt_arena[op_p].prev;
2734                }
2735                while self.outpt_arena[op_n].next != op_p
2736                    && self.outpt_arena[self.outpt_arena[op_n].next].pt.y == curr_y
2737                {
2738                    op_n = self.outpt_arena[op_n].next;
2739                }
2740            }
2741
2742            if self.outpt_arena[op_p].pt.x == self.outpt_arena[op_n].pt.x {
2743                self.horz_seg_list[i].right_op = None;
2744                continue;
2745            }
2746
2747            // Set heading
2748            if self.outpt_arena[op_p].pt.x < self.outpt_arena[op_n].pt.x {
2749                self.horz_seg_list[i].left_op = Some(op_p);
2750                self.horz_seg_list[i].right_op = Some(op_n);
2751                self.horz_seg_list[i].left_to_right = true;
2752            } else {
2753                self.horz_seg_list[i].left_op = Some(op_n);
2754                self.horz_seg_list[i].right_op = Some(op_p);
2755                self.horz_seg_list[i].left_to_right = false;
2756            }
2757
2758            // Check if left_op already has a horz
2759            let left_op = self.horz_seg_list[i].left_op.unwrap();
2760            if self.outpt_arena[left_op].horz.is_some() {
2761                self.horz_seg_list[i].right_op = None;
2762                continue;
2763            }
2764
2765            self.outpt_arena[left_op].horz = Some(i);
2766            valid_count += 1;
2767        }
2768
2769        if valid_count < 2 {
2770            return;
2771        }
2772
2773        // Sort by left_op x coordinate
2774        self.horz_seg_list.sort_by(|a, b| {
2775            let a_valid = a.right_op.is_some();
2776            let b_valid = b.right_op.is_some();
2777            if a_valid != b_valid {
2778                return if a_valid {
2779                    std::cmp::Ordering::Less
2780                } else {
2781                    std::cmp::Ordering::Greater
2782                };
2783            }
2784            if !a_valid {
2785                return std::cmp::Ordering::Equal;
2786            }
2787            let a_x = self.outpt_arena[a.left_op.unwrap()].pt.x;
2788            let b_x = self.outpt_arena[b.left_op.unwrap()].pt.x;
2789            a_x.cmp(&b_x)
2790        });
2791
2792        // Find pairs and create joins
2793        for i in 0..self.horz_seg_list.len() {
2794            if self.horz_seg_list[i].right_op.is_none() {
2795                break;
2796            }
2797            for j in (i + 1)..self.horz_seg_list.len() {
2798                if self.horz_seg_list[j].right_op.is_none() {
2799                    break;
2800                }
2801
2802                let hs1_left_x = self.outpt_arena[self.horz_seg_list[i].left_op.unwrap()]
2803                    .pt
2804                    .x;
2805                let hs1_right_x = self.outpt_arena[self.horz_seg_list[i].right_op.unwrap()]
2806                    .pt
2807                    .x;
2808                let hs2_left_x = self.outpt_arena[self.horz_seg_list[j].left_op.unwrap()]
2809                    .pt
2810                    .x;
2811                let hs2_right_x = self.outpt_arena[self.horz_seg_list[j].right_op.unwrap()]
2812                    .pt
2813                    .x;
2814
2815                if hs2_left_x >= hs1_right_x
2816                    || self.horz_seg_list[j].left_to_right == self.horz_seg_list[i].left_to_right
2817                    || hs2_right_x <= hs1_left_x
2818                {
2819                    continue;
2820                }
2821
2822                let curr_y = self.outpt_arena[self.horz_seg_list[i].left_op.unwrap()]
2823                    .pt
2824                    .y;
2825                let hs1_ltr = self.horz_seg_list[i].left_to_right;
2826
2827                if hs1_ltr {
2828                    // C++ modifies hs1->left_op persistently
2829                    let mut lo1 = self.horz_seg_list[i].left_op.unwrap();
2830                    while self.outpt_arena[self.outpt_arena[lo1].next].pt.y == curr_y
2831                        && self.outpt_arena[self.outpt_arena[lo1].next].pt.x <= hs2_left_x
2832                    {
2833                        lo1 = self.outpt_arena[lo1].next;
2834                    }
2835                    self.horz_seg_list[i].left_op = Some(lo1);
2836                    let mut lo2 = self.horz_seg_list[j].left_op.unwrap();
2837                    while self.outpt_arena[self.outpt_arena[lo2].prev].pt.y == curr_y
2838                        && self.outpt_arena[self.outpt_arena[lo2].prev].pt.x
2839                            <= self.outpt_arena[lo1].pt.x
2840                    {
2841                        lo2 = self.outpt_arena[lo2].prev;
2842                    }
2843                    self.horz_seg_list[j].left_op = Some(lo2);
2844                    let dup1 = self.duplicate_op(lo1, true);
2845                    let dup2 = self.duplicate_op(lo2, false);
2846                    self.horz_join_list.push(HorzJoin::with_ops(dup1, dup2));
2847                } else {
2848                    // C++ modifies hs1->left_op persistently
2849                    let mut lo1 = self.horz_seg_list[i].left_op.unwrap();
2850                    while self.outpt_arena[self.outpt_arena[lo1].prev].pt.y == curr_y
2851                        && self.outpt_arena[self.outpt_arena[lo1].prev].pt.x <= hs2_left_x
2852                    {
2853                        lo1 = self.outpt_arena[lo1].prev;
2854                    }
2855                    self.horz_seg_list[i].left_op = Some(lo1);
2856                    let mut lo2 = self.horz_seg_list[j].left_op.unwrap();
2857                    while self.outpt_arena[self.outpt_arena[lo2].next].pt.y == curr_y
2858                        && self.outpt_arena[self.outpt_arena[lo2].next].pt.x
2859                            <= self.outpt_arena[lo1].pt.x
2860                    {
2861                        lo2 = self.outpt_arena[lo2].next;
2862                    }
2863                    self.horz_seg_list[j].left_op = Some(lo2);
2864                    let dup1 = self.duplicate_op(lo2, true);
2865                    let dup2 = self.duplicate_op(lo1, false);
2866                    self.horz_join_list.push(HorzJoin::with_ops(dup1, dup2));
2867                }
2868            }
2869        }
2870    }
2871
2872    /// Process horizontal joins
2873    /// Direct port from clipper.engine.cpp ProcessHorzJoins (line 2279)
2874    fn process_horz_joins(&mut self) {
2875        for idx in 0..self.horz_join_list.len() {
2876            let op1 = match self.horz_join_list[idx].op1 {
2877                Some(op) => op,
2878                None => continue,
2879            };
2880            let op2 = match self.horz_join_list[idx].op2 {
2881                Some(op) => op,
2882                None => continue,
2883            };
2884
2885            let or1 = get_real_outrec(&self.outrec_list, self.outpt_arena[op1].outrec);
2886            let or2 = get_real_outrec(&self.outrec_list, self.outpt_arena[op2].outrec);
2887            if or1.is_none() || or2.is_none() {
2888                continue;
2889            }
2890            let or1 = or1.unwrap();
2891            let or2_orig = or2.unwrap();
2892
2893            let op1b = self.outpt_arena[op1].next;
2894            let op2b = self.outpt_arena[op2].prev;
2895            self.outpt_arena[op1].next = op2;
2896            self.outpt_arena[op2].prev = op1;
2897            self.outpt_arena[op1b].prev = op2b;
2898            self.outpt_arena[op2b].next = op1b;
2899
2900            if or1 == or2_orig {
2901                // 'join' is really a split
2902                let or2_new = self.new_out_rec();
2903                self.outrec_list[or2_new].pts = Some(op1b);
2904                fix_outrec_pts(or2_new, &self.outrec_list, &mut self.outpt_arena);
2905
2906                if self.outrec_list[or1]
2907                    .pts
2908                    .map(|p| self.outpt_arena[p].outrec)
2909                    == Some(or2_new)
2910                {
2911                    self.outrec_list[or1].pts = Some(op1);
2912                    self.outpt_arena[op1].outrec = or1;
2913                }
2914
2915                if self.using_polytree {
2916                    if path2_contains_path1_outpt(
2917                        self.outrec_list[or1].pts.unwrap(),
2918                        self.outrec_list[or2_new].pts.unwrap(),
2919                        &self.outpt_arena,
2920                    ) {
2921                        let or1_pts = self.outrec_list[or1].pts;
2922                        let or2_pts = self.outrec_list[or2_new].pts;
2923                        self.outrec_list[or1].pts = or2_pts;
2924                        self.outrec_list[or2_new].pts = or1_pts;
2925                        fix_outrec_pts(or1, &self.outrec_list, &mut self.outpt_arena);
2926                        fix_outrec_pts(or2_new, &self.outrec_list, &mut self.outpt_arena);
2927                        self.outrec_list[or2_new].owner = Some(or1);
2928                    } else if path2_contains_path1_outpt(
2929                        self.outrec_list[or2_new].pts.unwrap(),
2930                        self.outrec_list[or1].pts.unwrap(),
2931                        &self.outpt_arena,
2932                    ) {
2933                        self.outrec_list[or2_new].owner = Some(or1);
2934                    } else {
2935                        self.outrec_list[or2_new].owner = self.outrec_list[or1].owner;
2936                    }
2937
2938                    if self.outrec_list[or1].splits.is_none() {
2939                        self.outrec_list[or1].splits = Some(Vec::new());
2940                    }
2941                    self.outrec_list[or1].splits.as_mut().unwrap().push(or2_new);
2942                } else {
2943                    self.outrec_list[or2_new].owner = Some(or1);
2944                }
2945            } else {
2946                // joining, not splitting
2947                self.outrec_list[or2_orig].pts = None;
2948                if self.using_polytree {
2949                    set_owner(&mut self.outrec_list, or2_orig, or1);
2950                    if self.outrec_list[or2_orig].splits.is_some() {
2951                        move_splits(&mut self.outrec_list, or2_orig, or1);
2952                    }
2953                } else {
2954                    self.outrec_list[or2_orig].owner = Some(or1);
2955                }
2956            }
2957        }
2958    }
2959
2960    // ---- Clean collinear and self-intersection fixing ----
2961
2962    /// Clean collinear points from an OutRec
2963    /// Direct port from clipper.engine.cpp CleanCollinear (line 1525)
2964    pub fn clean_collinear(&mut self, outrec_idx: usize) {
2965        let real_or = get_real_outrec(&self.outrec_list, outrec_idx);
2966        let or_idx = match real_or {
2967            Some(idx) => idx,
2968            None => return,
2969        };
2970        if self.outrec_list[or_idx].is_open {
2971            return;
2972        }
2973        if !is_valid_closed_path(self.outrec_list[or_idx].pts, &self.outpt_arena) {
2974            self.dispose_out_pts(or_idx);
2975            return;
2976        }
2977
2978        let start_op = self.outrec_list[or_idx].pts.unwrap();
2979        let mut op2 = start_op;
2980        loop {
2981            let prev = self.outpt_arena[op2].prev;
2982            let next = self.outpt_arena[op2].next;
2983            let prev_pt = self.outpt_arena[prev].pt;
2984            let op2_pt = self.outpt_arena[op2].pt;
2985            let next_pt = self.outpt_arena[next].pt;
2986
2987            if is_collinear(prev_pt, op2_pt, next_pt)
2988                && (op2_pt == prev_pt
2989                    || op2_pt == next_pt
2990                    || !self.preserve_collinear
2991                    || dot_product_three_points(prev_pt, op2_pt, next_pt) < 0.0)
2992            {
2993                if Some(op2) == self.outrec_list[or_idx].pts {
2994                    self.outrec_list[or_idx].pts = Some(prev);
2995                }
2996                op2 = self.dispose_out_pt(op2);
2997                if !is_valid_closed_path(Some(op2), &self.outpt_arena) {
2998                    self.dispose_out_pts(or_idx);
2999                    return;
3000                }
3001                // Reset start
3002                continue;
3003            }
3004            op2 = next;
3005            if op2 == start_op
3006                || (self.outrec_list[or_idx].pts.is_some()
3007                    && op2 == self.outrec_list[or_idx].pts.unwrap())
3008            {
3009                break;
3010            }
3011        }
3012        self.fix_self_intersects(or_idx);
3013    }
3014
3015    /// Fix self-intersections in an OutRec
3016    /// Direct port from clipper.engine.cpp FixSelfIntersects (line 1646)
3017    fn fix_self_intersects(&mut self, outrec_idx: usize) {
3018        let start_op = match self.outrec_list[outrec_idx].pts {
3019            Some(op) => op,
3020            None => return,
3021        };
3022
3023        let prev = self.outpt_arena[start_op].prev;
3024        let next = self.outpt_arena[start_op].next;
3025        if prev == self.outpt_arena[next].next {
3026            return; // triangles can't self-intersect
3027        }
3028
3029        let mut op2 = start_op;
3030        loop {
3031            let prev = self.outpt_arena[op2].prev;
3032            let next = self.outpt_arena[op2].next;
3033            let next_next = self.outpt_arena[next].next;
3034
3035            if segments_intersect(
3036                self.outpt_arena[prev].pt,
3037                self.outpt_arena[op2].pt,
3038                self.outpt_arena[next].pt,
3039                self.outpt_arena[next_next].pt,
3040                false,
3041            ) {
3042                let next_next_next = self.outpt_arena[next_next].next;
3043                if segments_intersect(
3044                    self.outpt_arena[prev].pt,
3045                    self.outpt_arena[op2].pt,
3046                    self.outpt_arena[next_next].pt,
3047                    self.outpt_arena[next_next_next].pt,
3048                    false,
3049                ) {
3050                    // Adjacent intersections (micro self-intersection)
3051                    op2 = self.duplicate_op(op2, false);
3052                    let nnext = self.outpt_arena[self.outpt_arena[op2].next].next;
3053                    let nnn = self.outpt_arena[nnext].next;
3054                    self.outpt_arena[op2].pt = self.outpt_arena[nnn].pt;
3055                    op2 = self.outpt_arena[op2].next;
3056                } else {
3057                    if Some(op2) == self.outrec_list[outrec_idx].pts
3058                        || Some(next) == self.outrec_list[outrec_idx].pts
3059                    {
3060                        let pts = self.outrec_list[outrec_idx].pts.unwrap();
3061                        self.outrec_list[outrec_idx].pts = Some(self.outpt_arena[pts].prev);
3062                    }
3063                    self.do_split_op(outrec_idx, op2);
3064                    if self.outrec_list[outrec_idx].pts.is_none() {
3065                        break;
3066                    }
3067                    op2 = self.outrec_list[outrec_idx].pts.unwrap();
3068                    let prev = self.outpt_arena[op2].prev;
3069                    let next = self.outpt_arena[op2].next;
3070                    if prev == self.outpt_arena[next].next {
3071                        break;
3072                    }
3073                    continue;
3074                }
3075            } else {
3076                op2 = self.outpt_arena[op2].next;
3077            }
3078            if op2 == start_op
3079                || (self.outrec_list[outrec_idx].pts.is_some()
3080                    && op2 == self.outrec_list[outrec_idx].pts.unwrap())
3081            {
3082                break;
3083            }
3084        }
3085    }
3086
3087    /// Do split operation on self-intersecting polygon
3088    /// Direct port from clipper.engine.cpp DoSplitOp (line 1562)
3089    fn do_split_op(&mut self, outrec_idx: usize, split_op: usize) {
3090        let prev_op = self.outpt_arena[split_op].prev;
3091        let next_op = self.outpt_arena[split_op].next;
3092        let next_next_op = self.outpt_arena[next_op].next;
3093        self.outrec_list[outrec_idx].pts = Some(prev_op);
3094
3095        let mut ip = self.outpt_arena[prev_op].pt;
3096        get_segment_intersect_pt(
3097            self.outpt_arena[prev_op].pt,
3098            self.outpt_arena[split_op].pt,
3099            self.outpt_arena[next_op].pt,
3100            self.outpt_arena[next_next_op].pt,
3101            &mut ip,
3102        );
3103
3104        let area1 = area_outpt(self.outrec_list[outrec_idx].pts.unwrap(), &self.outpt_arena);
3105        let abs_area1 = area1.abs();
3106        if abs_area1 < 2.0 {
3107            self.dispose_out_pts(outrec_idx);
3108            return;
3109        }
3110
3111        let area2 = area_triangle(
3112            ip,
3113            self.outpt_arena[split_op].pt,
3114            self.outpt_arena[next_op].pt,
3115        );
3116        let abs_area2 = area2.abs();
3117
3118        // De-link split_op and next_op, inserting intersection point
3119        if ip == self.outpt_arena[prev_op].pt || ip == self.outpt_arena[next_next_op].pt {
3120            self.outpt_arena[next_next_op].prev = prev_op;
3121            self.outpt_arena[prev_op].next = next_next_op;
3122        } else {
3123            let new_op2 = self.new_out_pt(ip, self.outpt_arena[prev_op].outrec);
3124            self.outpt_arena[new_op2].prev = prev_op;
3125            self.outpt_arena[new_op2].next = next_next_op;
3126            self.outpt_arena[next_next_op].prev = new_op2;
3127            self.outpt_arena[prev_op].next = new_op2;
3128        }
3129
3130        if abs_area2 >= 1.0 && (abs_area2 > abs_area1 || (area2 > 0.0) == (area1 > 0.0)) {
3131            let new_or = self.new_out_rec();
3132            self.outrec_list[new_or].owner = self.outrec_list[outrec_idx].owner;
3133
3134            self.outpt_arena[split_op].outrec = new_or;
3135            self.outpt_arena[next_op].outrec = new_or;
3136
3137            let new_op = self.new_out_pt(ip, new_or);
3138            self.outpt_arena[new_op].prev = next_op;
3139            self.outpt_arena[new_op].next = split_op;
3140            self.outrec_list[new_or].pts = Some(new_op);
3141            self.outpt_arena[split_op].prev = new_op;
3142            self.outpt_arena[next_op].next = new_op;
3143
3144            if self.using_polytree {
3145                if path2_contains_path1_outpt(prev_op, new_op, &self.outpt_arena) {
3146                    self.outrec_list[new_or].splits = Some(vec![outrec_idx]);
3147                } else {
3148                    if self.outrec_list[outrec_idx].splits.is_none() {
3149                        self.outrec_list[outrec_idx].splits = Some(Vec::new());
3150                    }
3151                    self.outrec_list[outrec_idx]
3152                        .splits
3153                        .as_mut()
3154                        .unwrap()
3155                        .push(new_or);
3156                }
3157            }
3158        }
3159        // Otherwise the split triangle is too small, just discard split_op and next_op
3160    }
3161
3162    // ---- Check bounds and recursive owner checks (for polytree) ----
3163
3164    /// Check bounds of an outrec
3165    /// Direct port from clipper.engine.cpp CheckBounds (line 2929)
3166    pub fn check_bounds(&mut self, outrec_idx: usize) -> bool {
3167        if self.outrec_list[outrec_idx].pts.is_none() {
3168            return false;
3169        }
3170        if !self.outrec_list[outrec_idx].bounds.is_empty() {
3171            return true;
3172        }
3173        self.clean_collinear(outrec_idx);
3174        if self.outrec_list[outrec_idx].pts.is_none() {
3175            return false;
3176        }
3177
3178        let op_start = self.outrec_list[outrec_idx].pts.unwrap();
3179        let path =
3180            build_path64_from_outpt(op_start, self.reverse_solution, false, &self.outpt_arena);
3181        match path {
3182            None => {
3183                self.outrec_list[outrec_idx].path = Path64::new();
3184                false
3185            }
3186            Some(p) => {
3187                self.outrec_list[outrec_idx].bounds = get_bounds(&p);
3188                self.outrec_list[outrec_idx].path = p;
3189                true
3190            }
3191        }
3192    }
3193
3194    /// Check if a split outrec should own this outrec
3195    /// Direct port from clipper.engine.cpp CheckSplitOwner (line 2941)
3196    fn check_split_owner(&mut self, outrec_idx: usize, splits: &[usize]) -> bool {
3197        for &split_idx in splits {
3198            if self.outrec_list[split_idx].pts.is_none() {
3199                if let Some(ref sub_splits) = self.outrec_list[split_idx].splits.clone() {
3200                    if self.check_split_owner(outrec_idx, sub_splits) {
3201                        return true;
3202                    }
3203                }
3204            }
3205            let real_split = get_real_outrec(&self.outrec_list, split_idx);
3206            let split = match real_split {
3207                Some(s) if s != outrec_idx => s,
3208                _ => continue,
3209            };
3210
3211            if self.outrec_list[split].recursive_split == Some(outrec_idx) {
3212                continue;
3213            }
3214            self.outrec_list[split].recursive_split = Some(outrec_idx);
3215
3216            if let Some(ref sub_splits) = self.outrec_list[split].splits.clone() {
3217                if self.check_split_owner(outrec_idx, sub_splits) {
3218                    return true;
3219                }
3220            }
3221
3222            if !self.check_bounds(split) {
3223                continue;
3224            }
3225            let or_bounds = self.outrec_list[outrec_idx].bounds;
3226            if !self.outrec_list[split].bounds.contains_rect(&or_bounds) {
3227                continue;
3228            }
3229
3230            let or_pts = self.outrec_list[outrec_idx].pts.unwrap();
3231            let split_pts = self.outrec_list[split].pts.unwrap();
3232            if !path2_contains_path1_outpt(or_pts, split_pts, &self.outpt_arena) {
3233                continue;
3234            }
3235
3236            if !is_valid_owner(&self.outrec_list, outrec_idx, split) {
3237                self.outrec_list[split].owner = self.outrec_list[outrec_idx].owner;
3238            }
3239
3240            self.outrec_list[outrec_idx].owner = Some(split);
3241            return true;
3242        }
3243        false
3244    }
3245
3246    /// Recursively check and set owners for polytree building
3247    /// Direct port from clipper.engine.cpp RecursiveCheckOwners (line 2967)
3248    pub fn recursive_check_owners(&mut self, outrec_idx: usize, polytree: &mut PolyTree64) {
3249        if self.outrec_list[outrec_idx].polypath.is_some()
3250            || self.outrec_list[outrec_idx].bounds.is_empty()
3251        {
3252            return;
3253        }
3254
3255        while let Some(owner) = self.outrec_list[outrec_idx].owner {
3256            if let Some(ref splits) = self.outrec_list[owner].splits.clone() {
3257                if self.check_split_owner(outrec_idx, splits) {
3258                    break;
3259                }
3260            }
3261            if self.outrec_list[owner].pts.is_some() && self.check_bounds(owner) {
3262                let or_bounds = self.outrec_list[outrec_idx].bounds;
3263                let owner_bounds = self.outrec_list[owner].bounds;
3264                let contains = owner_bounds.contains_rect(&or_bounds);
3265                if contains {
3266                    let or_pts = self.outrec_list[outrec_idx].pts.unwrap();
3267                    let owner_pts = self.outrec_list[owner].pts.unwrap();
3268                    let p2cp1 = path2_contains_path1_outpt(or_pts, owner_pts, &self.outpt_arena);
3269                    if p2cp1 {
3270                        break;
3271                    }
3272                }
3273            }
3274            self.outrec_list[outrec_idx].owner = self.outrec_list[owner].owner;
3275        }
3276
3277        let path = self.outrec_list[outrec_idx].path.clone();
3278        if let Some(owner) = self.outrec_list[outrec_idx].owner {
3279            if self.outrec_list[owner].polypath.is_none() {
3280                self.recursive_check_owners(owner, polytree);
3281            }
3282            let parent_pp = self.outrec_list[owner].polypath.unwrap_or(0);
3283            let pp = polytree.add_child(parent_pp, path);
3284            self.outrec_list[outrec_idx].polypath = Some(pp);
3285        } else {
3286            let pp = polytree.add_child(0, path);
3287            self.outrec_list[outrec_idx].polypath = Some(pp);
3288        }
3289    }
3290
3291    // ---- ExecuteInternal ----
3292
3293    /// Main execution loop of the sweep-line algorithm
3294    /// Direct port from clipper.engine.cpp ExecuteInternal (line 2129)
3295    pub fn execute_internal(
3296        &mut self,
3297        ct: ClipType,
3298        fillrule: FillRule,
3299        use_polytrees: bool,
3300    ) -> bool {
3301        self.cliptype = ct;
3302        self.fillrule = fillrule;
3303        self.using_polytree = use_polytrees;
3304        self.reset();
3305
3306        if ct == ClipType::NoClip {
3307            return true;
3308        }
3309
3310        let y = match self.pop_scanline() {
3311            Some(y) => y,
3312            None => return true,
3313        };
3314
3315        let mut y = y;
3316        while self.succeeded {
3317            self.insert_local_minima_into_ael(y);
3318
3319            while let Some(e) = self.pop_horz() {
3320                self.do_horizontal(e);
3321            }
3322
3323            if !self.horz_seg_list.is_empty() {
3324                self.convert_horz_segs_to_joins();
3325                self.horz_seg_list.clear();
3326            }
3327
3328            self.bot_y = y;
3329
3330            match self.pop_scanline() {
3331                Some(new_y) => y = new_y,
3332                None => break,
3333            }
3334
3335            self.do_intersections(y);
3336            self.do_top_of_scanbeam(y);
3337
3338            while let Some(e) = self.pop_horz() {
3339                self.do_horizontal(e);
3340            }
3341        }
3342
3343        if self.succeeded {
3344            self.process_horz_joins();
3345        }
3346
3347        self.succeeded
3348    }
3349}
3350
3351// ============================================================================
3352// Tests
3353// ============================================================================
3354
3355#[cfg(test)]
3356#[path = "engine_tests.rs"]
3357mod tests;