Skip to main content

clipper2_rust/
engine_fns.rs

1//! Engine free functions - standalone utility functions for the sweep-line algorithm
2//!
3//! Direct port from clipper.engine.cpp
4//! Copyright (c) Angus Johnson 2010-2025
5
6use crate::core::*;
7use crate::engine::*;
8
9// ============================================================================
10// Engine Free Functions - Direct port from clipper.engine.cpp
11// ============================================================================
12
13/// Check if a value is odd
14/// Direct port from clipper.engine.cpp line 61
15#[inline]
16pub fn is_odd(val: i32) -> bool {
17    val & 1 != 0
18}
19
20/// Check if an active edge is "hot" (has an output record)
21/// Direct port from clipper.engine.cpp line 67
22#[inline]
23pub fn is_hot_edge(e: &Active) -> bool {
24    e.outrec.is_some()
25}
26
27/// Check if an active edge is part of an open path
28/// Direct port from clipper.engine.cpp line 73
29#[inline]
30pub fn is_open_active(e: &Active, minima_list: &[LocalMinima]) -> bool {
31    minima_list[e.local_min].is_open
32}
33
34/// Check if a vertex is an open end
35/// Direct port from clipper.engine.cpp line 79
36#[inline]
37pub fn is_open_end_vertex(v: &Vertex) -> bool {
38    (v.flags & (VertexFlags::OPEN_START | VertexFlags::OPEN_END)) != VertexFlags::EMPTY
39}
40
41/// Check if an active edge's vertex_top is an open end
42/// Direct port from clipper.engine.cpp line 87
43#[inline]
44pub fn is_open_end_active(e: &Active, vertex_arena: &[Vertex]) -> bool {
45    is_open_end_vertex(&vertex_arena[e.vertex_top])
46}
47
48/// Calculate slope dx for an edge defined by two points
49/// Direct port from clipper.engine.cpp line 116
50#[inline]
51pub fn get_dx(pt1: Point64, pt2: Point64) -> f64 {
52    let dy = (pt2.y - pt1.y) as f64;
53    if dy != 0.0 {
54        (pt2.x - pt1.x) as f64 / dy
55    } else if pt2.x > pt1.x {
56        -f64::MAX
57    } else {
58        f64::MAX
59    }
60}
61
62/// Get the x coordinate of an active edge at a given y
63/// Direct port from clipper.engine.cpp line 127
64#[inline]
65pub fn top_x(ae: &Active, current_y: i64) -> i64 {
66    if current_y == ae.top.y || ae.top.x == ae.bot.x {
67        ae.top.x
68    } else if current_y == ae.bot.y {
69        ae.bot.x
70    } else {
71        ae.bot.x + (ae.dx * (current_y - ae.bot.y) as f64).round() as i64
72    }
73}
74
75/// Check if an active edge is horizontal
76/// Direct port from clipper.engine.cpp line 137
77#[inline]
78pub fn is_horizontal_active(e: &Active) -> bool {
79    e.top.y == e.bot.y
80}
81
82/// Check if a horizontal edge is heading right
83/// Direct port from clipper.engine.cpp line 143
84#[inline]
85pub fn is_heading_right_horz(e: &Active) -> bool {
86    e.dx == -f64::MAX
87}
88
89/// Check if a horizontal edge is heading left
90/// Direct port from clipper.engine.cpp line 149
91#[inline]
92pub fn is_heading_left_horz(e: &Active) -> bool {
93    e.dx == f64::MAX
94}
95
96/// Get the polytype of an active edge
97/// Direct port from clipper.engine.cpp line 162
98#[inline]
99pub fn get_poly_type(e: &Active, minima_list: &[LocalMinima]) -> PathType {
100    minima_list[e.local_min].polytype
101}
102
103/// Check if two active edges are the same polytype
104/// Direct port from clipper.engine.cpp line 167
105#[inline]
106pub fn is_same_poly_type(e1: &Active, e2: &Active, minima_list: &[LocalMinima]) -> bool {
107    minima_list[e1.local_min].polytype == minima_list[e2.local_min].polytype
108}
109
110/// Set dx for an active edge from its bot/top points
111/// Direct port from clipper.engine.cpp line 172
112#[inline]
113pub fn set_dx(e: &mut Active) {
114    e.dx = get_dx(e.bot, e.top);
115}
116
117/// Get next vertex for an active edge (depends on winding direction)
118/// Direct port from clipper.engine.cpp line 177
119#[inline]
120pub fn next_vertex(e: &Active, vertex_arena: &[Vertex]) -> usize {
121    if e.wind_dx > 0 {
122        vertex_arena[e.vertex_top].next
123    } else {
124        vertex_arena[e.vertex_top].prev
125    }
126}
127
128/// Get the vertex two steps back (useful for alternate edge top)
129/// Direct port from clipper.engine.cpp line 187
130#[inline]
131pub fn prev_prev_vertex(ae: &Active, vertex_arena: &[Vertex]) -> usize {
132    if ae.wind_dx > 0 {
133        let prev = vertex_arena[ae.vertex_top].prev;
134        vertex_arena[prev].prev
135    } else {
136        let next = vertex_arena[ae.vertex_top].next;
137        vertex_arena[next].next
138    }
139}
140
141/// Check if a vertex is a local maximum
142/// Direct port from clipper.engine.cpp line 215
143#[inline]
144pub fn is_maxima_vertex(v: &Vertex) -> bool {
145    (v.flags & VertexFlags::LOCAL_MAX) != VertexFlags::EMPTY
146}
147
148/// Check if an active edge is at a local maximum
149/// Direct port from clipper.engine.cpp line 221
150#[inline]
151pub fn is_maxima_active(e: &Active, vertex_arena: &[Vertex]) -> bool {
152    is_maxima_vertex(&vertex_arena[e.vertex_top])
153}
154
155/// Count points in an OutPt circular list
156/// Direct port from clipper.engine.cpp line 266
157pub fn point_count(op_start: usize, outpt_arena: &[OutPt]) -> i32 {
158    let mut op2 = op_start;
159    let mut cnt = 0;
160    loop {
161        op2 = outpt_arena[op2].next;
162        cnt += 1;
163        if op2 == op_start {
164            break;
165        }
166    }
167    cnt
168}
169
170/// Check if an OutPt path is invalid (single point or None)
171/// Direct port from clipper.engine.cpp line 105
172#[inline]
173pub fn is_invalid_path(op: Option<usize>, outpt_arena: &[OutPt]) -> bool {
174    match op {
175        None => true,
176        Some(idx) => outpt_arena[idx].next == idx,
177    }
178}
179
180/// Calculate area of an OutPt circular list
181/// Direct port from clipper.engine.cpp line 366
182pub fn area_outpt(op_start: usize, outpt_arena: &[OutPt]) -> f64 {
183    let mut result = 0.0;
184    let mut op2 = op_start;
185    loop {
186        let prev_idx = outpt_arena[op2].prev;
187        result += (outpt_arena[prev_idx].pt.y + outpt_arena[op2].pt.y) as f64
188            * (outpt_arena[prev_idx].pt.x - outpt_arena[op2].pt.x) as f64;
189        op2 = outpt_arena[op2].next;
190        if op2 == op_start {
191            break;
192        }
193    }
194    result * 0.5
195}
196
197/// Calculate area of a triangle formed by three points
198/// Direct port from clipper.engine.cpp line 380
199#[inline]
200pub fn area_triangle(pt1: Point64, pt2: Point64, pt3: Point64) -> f64 {
201    (pt3.y + pt1.y) as f64 * (pt3.x - pt1.x) as f64
202        + (pt1.y + pt2.y) as f64 * (pt1.x - pt2.x) as f64
203        + (pt2.y + pt3.y) as f64 * (pt2.x - pt3.x) as f64
204}
205
206/// Reverse the direction of an OutPt circular list
207/// Direct port from clipper.engine.cpp line 388
208pub fn reverse_out_pts(op_start: usize, outpt_arena: &mut [OutPt]) {
209    let mut op1 = op_start;
210    loop {
211        std::mem::swap(&mut outpt_arena[op1].next, &mut outpt_arena[op1].prev);
212        op1 = outpt_arena[op1].prev; // advance to original next (now in prev)
213        if op1 == op_start {
214            break;
215        }
216    }
217}
218
219/// Sort comparator for IntersectNodes
220/// Direct port from clipper.engine.cpp line 322
221pub fn intersect_list_sort(a: &IntersectNode, b: &IntersectNode) -> std::cmp::Ordering {
222    if a.pt.y == b.pt.y {
223        a.pt.x.cmp(&b.pt.x)
224    } else {
225        b.pt.y.cmp(&a.pt.y)
226    }
227}
228
229/// Get the maxima pair for an active edge
230/// Direct port from clipper.engine.cpp line 254
231pub fn get_maxima_pair(e: &Active, active_arena: &[Active]) -> Option<usize> {
232    let mut e2_idx = e.next_in_ael;
233    while let Some(idx) = e2_idx {
234        if active_arena[idx].vertex_top == e.vertex_top {
235            return Some(idx);
236        }
237        e2_idx = active_arena[idx].next_in_ael;
238    }
239    None
240}
241
242/// Get current Y maxima vertex for open paths
243/// Direct port from clipper.engine.cpp line 226
244pub fn get_curr_y_maxima_vertex_open(e: &Active, vertex_arena: &[Vertex]) -> Option<usize> {
245    let mut result = e.vertex_top;
246    if e.wind_dx > 0 {
247        while vertex_arena[vertex_arena[result].next].pt.y == vertex_arena[result].pt.y
248            && (vertex_arena[result].flags & (VertexFlags::OPEN_END | VertexFlags::LOCAL_MAX))
249                == VertexFlags::EMPTY
250        {
251            result = vertex_arena[result].next;
252        }
253    } else {
254        while vertex_arena[vertex_arena[result].prev].pt.y == vertex_arena[result].pt.y
255            && (vertex_arena[result].flags & (VertexFlags::OPEN_END | VertexFlags::LOCAL_MAX))
256                == VertexFlags::EMPTY
257        {
258            result = vertex_arena[result].prev;
259        }
260    }
261    if is_maxima_vertex(&vertex_arena[result]) {
262        Some(result)
263    } else {
264        None
265    }
266}
267
268/// Get current Y maxima vertex for closed paths
269/// Direct port from clipper.engine.cpp line 243
270pub fn get_curr_y_maxima_vertex(e: &Active, vertex_arena: &[Vertex]) -> Option<usize> {
271    let mut result = e.vertex_top;
272    if e.wind_dx > 0 {
273        while vertex_arena[vertex_arena[result].next].pt.y == vertex_arena[result].pt.y {
274            result = vertex_arena[result].next;
275        }
276    } else {
277        while vertex_arena[vertex_arena[result].prev].pt.y == vertex_arena[result].pt.y {
278            result = vertex_arena[result].prev;
279        }
280    }
281    if is_maxima_vertex(&vertex_arena[result]) {
282        Some(result)
283    } else {
284        None
285    }
286}
287
288/// Check if the AEL ordering is valid between two active edges
289/// Direct port from clipper.engine.cpp IsValidAelOrder (line 1119)
290pub fn is_valid_ael_order(
291    resident: &Active,
292    newcomer: &Active,
293    vertex_arena: &[Vertex],
294    minima_list: &[LocalMinima],
295) -> bool {
296    if newcomer.curr_x != resident.curr_x {
297        return newcomer.curr_x > resident.curr_x;
298    }
299
300    // get the turning direction: resident.top, newcomer.bot, newcomer.top
301    let i = cross_product_sign(
302        vertex_arena[resident.vertex_top].pt,
303        newcomer.bot,
304        vertex_arena[newcomer.vertex_top].pt,
305    );
306    if i != 0 {
307        return i < 0;
308    }
309
310    // edges must be collinear to get here
311    // for starting open paths, place them according to
312    // the direction they're about to turn
313    if !is_maxima_active(resident, vertex_arena)
314        && vertex_arena[resident.vertex_top].pt.y > vertex_arena[newcomer.vertex_top].pt.y
315    {
316        let nv = next_vertex(resident, vertex_arena);
317        return cross_product_sign(
318            newcomer.bot,
319            vertex_arena[resident.vertex_top].pt,
320            vertex_arena[nv].pt,
321        ) <= 0;
322    } else if !is_maxima_active(newcomer, vertex_arena)
323        && vertex_arena[newcomer.vertex_top].pt.y > vertex_arena[resident.vertex_top].pt.y
324    {
325        let nv = next_vertex(newcomer, vertex_arena);
326        return cross_product_sign(
327            newcomer.bot,
328            vertex_arena[newcomer.vertex_top].pt,
329            vertex_arena[nv].pt,
330        ) >= 0;
331    }
332
333    let y = newcomer.bot.y;
334    let newcomer_is_left = newcomer.is_left_bound;
335
336    if resident.bot.y != y || vertex_arena[minima_list[resident.local_min].vertex].pt.y != y {
337        newcomer.is_left_bound
338    } else if resident.is_left_bound != newcomer_is_left {
339        newcomer_is_left
340    } else if is_collinear(
341        vertex_arena[prev_prev_vertex(resident, vertex_arena)].pt,
342        resident.bot,
343        vertex_arena[resident.vertex_top].pt,
344    ) {
345        true
346    } else {
347        // compare turning direction of the alternate bound
348        let ppv_r = prev_prev_vertex(resident, vertex_arena);
349        let ppv_n = prev_prev_vertex(newcomer, vertex_arena);
350        (cross_product_sign(vertex_arena[ppv_r].pt, newcomer.bot, vertex_arena[ppv_n].pt) > 0)
351            == newcomer_is_left
352    }
353}
354
355// ============================================================================
356// Path building functions
357// ============================================================================
358
359/// Build a Path64 from an OutRec
360/// Direct port from clipper.engine.cpp GetCleanPath equivalent
361pub fn get_clean_path(op_start: usize, outpt_arena: &[OutPt]) -> Path64 {
362    let mut result = Path64::new();
363    let mut op = op_start;
364    loop {
365        let prev = outpt_arena[op].prev;
366        let next = outpt_arena[op].next;
367        if outpt_arena[op].pt == outpt_arena[prev].pt
368            || is_collinear(
369                outpt_arena[prev].pt,
370                outpt_arena[op].pt,
371                outpt_arena[next].pt,
372            )
373        {
374            op = next;
375            if op == op_start {
376                break;
377            }
378            continue;
379        }
380        result.push(outpt_arena[op].pt);
381        op = next;
382        if op == op_start {
383            break;
384        }
385    }
386    result
387}
388
389// ============================================================================
390// Free helper functions for sweep-line algorithm
391// Direct port from clipper.engine.cpp
392// ============================================================================
393
394/// Get the real (non-disposed) OutRec by following owner chain
395/// Direct port from clipper.engine.cpp GetRealOutRec (line 412)
396pub fn get_real_outrec(outrec_list: &[OutRec], idx: usize) -> Option<usize> {
397    let mut i = Some(idx);
398    while let Some(cur) = i {
399        if outrec_list[cur].pts.is_some() {
400            return Some(cur);
401        }
402        i = outrec_list[cur].owner;
403    }
404    None
405}
406
407/// Check if an owner is valid (not circular)
408/// Direct port from clipper.engine.cpp IsValidOwner (line 418)
409pub fn is_valid_owner(outrec_list: &[OutRec], outrec_idx: usize, test_owner_idx: usize) -> bool {
410    let mut tmp = Some(test_owner_idx);
411    while let Some(idx) = tmp {
412        if idx == outrec_idx {
413            return false;
414        }
415        tmp = outrec_list[idx].owner;
416    }
417    true
418}
419
420/// Check if two points are really close (within 2 units)
421/// Direct port from clipper.engine.cpp PtsReallyClose (line 436)
422#[inline]
423pub fn pts_really_close(pt1: Point64, pt2: Point64) -> bool {
424    (pt1.x - pt2.x).abs() < 2 && (pt1.y - pt2.y).abs() < 2
425}
426
427/// Check if an OutPt triangle is very small
428/// Direct port from clipper.engine.cpp IsVerySmallTriangle (line 441)
429pub fn is_very_small_triangle(op_idx: usize, outpt_arena: &[OutPt]) -> bool {
430    let next = outpt_arena[op_idx].next;
431    let prev = outpt_arena[op_idx].prev;
432    if outpt_arena[next].next != prev {
433        return false;
434    }
435    pts_really_close(outpt_arena[prev].pt, outpt_arena[next].pt)
436        || pts_really_close(outpt_arena[op_idx].pt, outpt_arena[next].pt)
437        || pts_really_close(outpt_arena[op_idx].pt, outpt_arena[prev].pt)
438}
439
440/// Check if a closed path is valid
441/// Direct port from clipper.engine.cpp IsValidClosedPath (line 449)
442pub fn is_valid_closed_path(op: Option<usize>, outpt_arena: &[OutPt]) -> bool {
443    match op {
444        None => false,
445        Some(idx) => {
446            let next = outpt_arena[idx].next;
447            if next == idx {
448                return false;
449            }
450            let prev = outpt_arena[idx].prev;
451            if next == prev {
452                return false;
453            }
454            !is_very_small_triangle(idx, outpt_arena)
455        }
456    }
457}
458
459/// Check if the hot edge is ascending (front edge of its outrec)
460/// Direct port from clipper.engine.cpp OutrecIsAscending (line 455)
461#[inline]
462pub fn outrec_is_ascending(
463    hot_edge_idx: usize,
464    outrec_list: &[OutRec],
465    active_arena: &[Active],
466) -> bool {
467    if let Some(or_idx) = active_arena[hot_edge_idx].outrec {
468        outrec_list[or_idx].front_edge == Some(hot_edge_idx)
469    } else {
470        false
471    }
472}
473
474/// Swap front and back sides of an OutRec
475/// Direct port from clipper.engine.cpp SwapFrontBackSides (line 460)
476pub fn swap_front_back_sides(outrec_idx: usize, outrec_list: &mut [OutRec], outpt_arena: &[OutPt]) {
477    std::mem::swap(
478        &mut outrec_list[outrec_idx].front_edge,
479        &mut outrec_list[outrec_idx].back_edge,
480    );
481    if let Some(pts) = outrec_list[outrec_idx].pts {
482        outrec_list[outrec_idx].pts = Some(outpt_arena[pts].next);
483    }
484}
485
486/// Check if intersect node edges are adjacent in AEL
487/// Direct port from clipper.engine.cpp EdgesAdjacentInAEL (line 468)
488#[inline]
489pub fn edges_adjacent_in_ael(inode: &IntersectNode, active_arena: &[Active]) -> bool {
490    active_arena[inode.edge1].next_in_ael == Some(inode.edge2)
491        || active_arena[inode.edge1].prev_in_ael == Some(inode.edge2)
492}
493
494/// Check if an edge is joined
495/// Direct port from clipper.engine.cpp IsJoined (line 473)
496#[inline]
497pub fn is_joined(e: &Active) -> bool {
498    e.join_with != JoinWith::NoJoin
499}
500
501/// Set the owner of an OutRec
502/// Direct port from clipper.engine.cpp SetOwner (line 478)
503pub fn set_owner(outrec_list: &mut [OutRec], outrec_idx: usize, new_owner_idx: usize) {
504    // precondition: new_owner_idx is valid
505    // Direct port from C++: new_owner->owner = GetRealOutRec(new_owner->owner);
506    // When owner is None, GetRealOutRec returns None (C++ handles nullptr correctly)
507    outrec_list[new_owner_idx].owner = match outrec_list[new_owner_idx].owner {
508        Some(owner_idx) => get_real_outrec(outrec_list, owner_idx),
509        None => None,
510    };
511    let mut tmp = Some(new_owner_idx);
512    while let Some(t) = tmp {
513        if t == outrec_idx {
514            outrec_list[new_owner_idx].owner = outrec_list[outrec_idx].owner;
515            break;
516        }
517        tmp = outrec_list[t].owner;
518    }
519    outrec_list[outrec_idx].owner = Some(new_owner_idx);
520}
521
522/// Point in polygon test for OutPt-based polygons
523/// Direct port from clipper.engine.cpp PointInOpPolygon (line 488)
524pub fn point_in_op_polygon(
525    pt: Point64,
526    op_start: usize,
527    outpt_arena: &[OutPt],
528) -> PointInPolygonResult {
529    let next = outpt_arena[op_start].next;
530    if next == op_start || outpt_arena[op_start].prev == next {
531        return PointInPolygonResult::IsOutside;
532    }
533
534    let mut op = op_start;
535    loop {
536        if outpt_arena[op].pt.y != pt.y {
537            break;
538        }
539        op = outpt_arena[op].next;
540        if op == op_start {
541            break;
542        }
543    }
544    if outpt_arena[op].pt.y == pt.y {
545        return PointInPolygonResult::IsOutside;
546    }
547
548    let mut is_above = outpt_arena[op].pt.y < pt.y;
549    let starting_above = is_above;
550    let mut val = 0;
551    let mut op2 = outpt_arena[op].next;
552
553    while op2 != op {
554        if is_above {
555            while op2 != op && outpt_arena[op2].pt.y < pt.y {
556                op2 = outpt_arena[op2].next;
557            }
558        } else {
559            while op2 != op && outpt_arena[op2].pt.y > pt.y {
560                op2 = outpt_arena[op2].next;
561            }
562        }
563        if op2 == op {
564            break;
565        }
566
567        if outpt_arena[op2].pt.y == pt.y {
568            let prev = outpt_arena[op2].prev;
569            if outpt_arena[op2].pt.x == pt.x
570                || (outpt_arena[op2].pt.y == outpt_arena[prev].pt.y
571                    && (pt.x < outpt_arena[prev].pt.x) != (pt.x < outpt_arena[op2].pt.x))
572            {
573                return PointInPolygonResult::IsOn;
574            }
575            op2 = outpt_arena[op2].next;
576            if op2 == op {
577                break;
578            }
579            continue;
580        }
581
582        let prev = outpt_arena[op2].prev;
583        if pt.x < outpt_arena[op2].pt.x && pt.x < outpt_arena[prev].pt.x {
584            // do nothing
585        } else if pt.x > outpt_arena[prev].pt.x && pt.x > outpt_arena[op2].pt.x {
586            val = 1 - val;
587        } else {
588            let i = cross_product_sign(outpt_arena[prev].pt, outpt_arena[op2].pt, pt);
589            if i == 0 {
590                return PointInPolygonResult::IsOn;
591            }
592            if (i < 0) == is_above {
593                val = 1 - val;
594            }
595        }
596        is_above = !is_above;
597        op2 = outpt_arena[op2].next;
598    }
599
600    if is_above != starting_above {
601        let prev = outpt_arena[op2].prev;
602        let i = cross_product_sign(outpt_arena[prev].pt, outpt_arena[op2].pt, pt);
603        if i == 0 {
604            return PointInPolygonResult::IsOn;
605        }
606        if (i < 0) == is_above {
607            val = 1 - val;
608        }
609    }
610
611    if val == 0 {
612        PointInPolygonResult::IsOutside
613    } else {
614        PointInPolygonResult::IsInside
615    }
616}
617
618/// Check if path1 (as OutPt list) is contained within path2 (as OutPt list)
619/// Direct port from clipper.engine.cpp Path2ContainsPath1 (line 576)
620pub fn path2_contains_path1_outpt(
621    op1_start: usize,
622    op2_start: usize,
623    outpt_arena: &[OutPt],
624) -> bool {
625    let mut pip = PointInPolygonResult::IsOn;
626    let mut op = op1_start;
627    loop {
628        match point_in_op_polygon(outpt_arena[op].pt, op2_start, outpt_arena) {
629            PointInPolygonResult::IsOutside => {
630                if pip == PointInPolygonResult::IsOutside {
631                    return false;
632                }
633                pip = PointInPolygonResult::IsOutside;
634            }
635            PointInPolygonResult::IsInside => {
636                if pip == PointInPolygonResult::IsInside {
637                    return true;
638                }
639                pip = PointInPolygonResult::IsInside;
640            }
641            _ => {}
642        }
643        op = outpt_arena[op].next;
644        if op == op1_start {
645            break;
646        }
647    }
648    // result unclear, try using cleaned paths
649    let clean1 = get_clean_path(op1_start, outpt_arena);
650    let clean2 = get_clean_path(op2_start, outpt_arena);
651    path2_contains_path1(&clean1, &clean2)
652}
653
654/// Check if path1 is contained within path2 using Path64 vectors
655/// Direct port from clipper.h line 717 - template Path2ContainsPath1<T>
656/// precondition: paths must not intersect, except for transient micro intersections
657pub fn path2_contains_path1(path1: &Path64, path2: &Path64) -> bool {
658    let mut pip = PointInPolygonResult::IsOn;
659    for pt in path1 {
660        match point_in_polygon(*pt, path2) {
661            PointInPolygonResult::IsOutside => {
662                if pip == PointInPolygonResult::IsOutside {
663                    return false;
664                }
665                pip = PointInPolygonResult::IsOutside;
666            }
667            PointInPolygonResult::IsInside => {
668                if pip == PointInPolygonResult::IsInside {
669                    return true;
670                }
671                pip = PointInPolygonResult::IsInside;
672            }
673            _ => {}
674        }
675    }
676    if pip != PointInPolygonResult::IsInside {
677        return false;
678    }
679    // result is likely true but check midpoint
680    let mp1 = get_bounds_path(path1).mid_point();
681    point_in_polygon(mp1, path2) == PointInPolygonResult::IsInside
682}
683
684/// Build a Path64 from OutPt circular list
685/// Direct port from clipper.engine.cpp BuildPath64 (line 2891)
686pub fn build_path64_from_outpt(
687    op_start: usize,
688    reverse: bool,
689    is_open: bool,
690    outpt_arena: &[OutPt],
691) -> Option<Path64> {
692    let next = outpt_arena[op_start].next;
693    if next == op_start || (!is_open && next == outpt_arena[op_start].prev) {
694        return None;
695    }
696
697    let mut path = Path64::new();
698    let (mut last_pt, mut op2);
699
700    if reverse {
701        last_pt = outpt_arena[op_start].pt;
702        op2 = outpt_arena[op_start].prev;
703    } else {
704        let op_next = outpt_arena[op_start].next;
705        last_pt = outpt_arena[op_next].pt;
706        op2 = outpt_arena[op_next].next;
707        path.push(last_pt);
708        while op2 != outpt_arena[op_start].next {
709            if outpt_arena[op2].pt != last_pt {
710                last_pt = outpt_arena[op2].pt;
711                path.push(last_pt);
712            }
713            op2 = outpt_arena[op2].next;
714        }
715        if !is_open
716            && path.len() == 3
717            && is_very_small_triangle(outpt_arena[op_start].next, outpt_arena)
718        {
719            return None;
720        }
721        return if path.len() >= 2 { Some(path) } else { None };
722    }
723
724    // reverse case
725    path.push(last_pt);
726    while op2 != op_start {
727        if outpt_arena[op2].pt != last_pt {
728            last_pt = outpt_arena[op2].pt;
729            path.push(last_pt);
730        }
731        op2 = outpt_arena[op2].prev;
732    }
733
734    if !is_open && path.len() == 3 && is_very_small_triangle(op_start, outpt_arena) {
735        return None;
736    }
737
738    if path.len() >= 2 {
739        Some(path)
740    } else {
741        None
742    }
743}
744
745/// Build a PathD from OutPt circular list
746/// Direct port from clipper.engine.cpp BuildPathD (line 3055)
747pub fn build_path_d_from_outpt(
748    op_start: usize,
749    reverse: bool,
750    is_open: bool,
751    outpt_arena: &[OutPt],
752    inv_scale: f64,
753) -> Option<PathD> {
754    let next = outpt_arena[op_start].next;
755    if next == op_start || (!is_open && next == outpt_arena[op_start].prev) {
756        return None;
757    }
758
759    let mut path = PathD::new();
760    let (mut last_pt, mut op2);
761
762    if reverse {
763        last_pt = outpt_arena[op_start].pt;
764        op2 = outpt_arena[op_start].prev;
765    } else {
766        let op_next = outpt_arena[op_start].next;
767        last_pt = outpt_arena[op_next].pt;
768        op2 = outpt_arena[op_next].next;
769        path.push(PointD::new(
770            last_pt.x as f64 * inv_scale,
771            last_pt.y as f64 * inv_scale,
772        ));
773        while op2 != outpt_arena[op_start].next {
774            if outpt_arena[op2].pt != last_pt {
775                last_pt = outpt_arena[op2].pt;
776                path.push(PointD::new(
777                    last_pt.x as f64 * inv_scale,
778                    last_pt.y as f64 * inv_scale,
779                ));
780            }
781            op2 = outpt_arena[op2].next;
782        }
783        if path.len() == 3 && is_very_small_triangle(outpt_arena[op_start].next, outpt_arena) {
784            return None;
785        }
786        return if path.len() >= 2 { Some(path) } else { None };
787    }
788
789    // reverse case
790    path.push(PointD::new(
791        last_pt.x as f64 * inv_scale,
792        last_pt.y as f64 * inv_scale,
793    ));
794    while op2 != op_start {
795        if outpt_arena[op2].pt != last_pt {
796            last_pt = outpt_arena[op2].pt;
797            path.push(PointD::new(
798                last_pt.x as f64 * inv_scale,
799                last_pt.y as f64 * inv_scale,
800            ));
801        }
802        op2 = outpt_arena[op2].prev;
803    }
804
805    if path.len() == 3 && is_very_small_triangle(op_start, outpt_arena) {
806        return None;
807    }
808
809    if path.len() >= 2 {
810        Some(path)
811    } else {
812        None
813    }
814}
815
816/// Get the last output point for a hot edge
817/// Direct port from clipper.engine.cpp GetLastOp (line 2496)
818#[inline]
819pub fn get_last_op(
820    hot_edge_idx: usize,
821    active_arena: &[Active],
822    outrec_list: &[OutRec],
823    outpt_arena: &[OutPt],
824) -> Option<usize> {
825    if let Some(or_idx) = active_arena[hot_edge_idx].outrec {
826        let result = outrec_list[or_idx].pts?;
827        if outrec_list[or_idx].front_edge != Some(hot_edge_idx) {
828            Some(outpt_arena[result].next)
829        } else {
830            Some(result)
831        }
832    } else {
833        None
834    }
835}
836
837/// Fix all OutPt outrec references in an OutRec
838/// Direct port from clipper.engine.cpp FixOutRecPts (line 2158)
839pub fn fix_outrec_pts(outrec_idx: usize, outrec_list: &[OutRec], outpt_arena: &mut [OutPt]) {
840    if let Some(pts) = outrec_list[outrec_idx].pts {
841        let mut op = pts;
842        loop {
843            outpt_arena[op].outrec = outrec_idx;
844            op = outpt_arena[op].next;
845            if op == pts {
846                break;
847            }
848        }
849    }
850}
851
852/// Update all OutPt outrec references in an OutRec
853/// Direct port from clipper.engine.cpp UpdateOutrecOwner (line 1684)
854pub fn update_outrec_owner(outrec_idx: usize, outrec_list: &[OutRec], outpt_arena: &mut [OutPt]) {
855    fix_outrec_pts(outrec_idx, outrec_list, outpt_arena);
856}
857
858/// Move splits from one OutRec to another
859/// Direct port from clipper.engine.cpp MoveSplits (line 2269)
860pub fn move_splits(outrec_list: &mut [OutRec], from_or: usize, to_or: usize) {
861    let from_splits = outrec_list[from_or].splits.take().unwrap_or_default();
862    if outrec_list[to_or].splits.is_none() {
863        outrec_list[to_or].splits = Some(Vec::new());
864    }
865    if let Some(ref mut to_splits) = outrec_list[to_or].splits {
866        for s in &from_splits {
867            if *s != to_or {
868                to_splits.push(*s);
869            }
870        }
871    }
872}
873
874/// Uncouple an OutRec from its edges
875/// Direct port from clipper.engine.cpp UncoupleOutRec (line 425)
876pub fn uncouple_outrec(e_idx: usize, active_arena: &mut [Active], outrec_list: &mut [OutRec]) {
877    if let Some(or_idx) = active_arena[e_idx].outrec {
878        if let Some(fe) = outrec_list[or_idx].front_edge {
879            active_arena[fe].outrec = None;
880        }
881        if let Some(be) = outrec_list[or_idx].back_edge {
882            active_arena[be].outrec = None;
883        }
884        outrec_list[or_idx].front_edge = None;
885        outrec_list[or_idx].back_edge = None;
886    }
887}
888
889/// Extract an active from the SEL (sorted edge list)
890/// Direct port from clipper.engine.cpp ExtractFromSEL
891pub fn extract_from_sel(e_idx: usize, active_arena: &mut [Active]) -> Option<usize> {
892    let next = active_arena[e_idx].next_in_sel;
893    if let Some(next_idx) = next {
894        active_arena[next_idx].prev_in_sel = active_arena[e_idx].prev_in_sel;
895    }
896    if let Some(prev_idx) = active_arena[e_idx].prev_in_sel {
897        active_arena[prev_idx].next_in_sel = next;
898    }
899    active_arena[e_idx].prev_in_sel = None;
900    active_arena[e_idx].next_in_sel = None;
901    next
902}
903
904/// Insert tmp before left in SEL
905/// Direct port from clipper.engine.cpp Insert1Before2InSEL
906pub fn insert1_before2_in_sel(tmp_idx: usize, left_idx: usize, active_arena: &mut [Active]) {
907    let prev = active_arena[left_idx].prev_in_sel;
908    active_arena[tmp_idx].prev_in_sel = prev;
909    if let Some(prev_idx) = prev {
910        active_arena[prev_idx].next_in_sel = Some(tmp_idx);
911    }
912    active_arena[tmp_idx].next_in_sel = Some(left_idx);
913    active_arena[left_idx].prev_in_sel = Some(tmp_idx);
914}