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