Skip to main content

flow_gate_core/gate/
polygon.rs

1use crate::error::FlowGateError;
2use crate::traits::{Gate, GateId, ParameterName};
3use crate::transform::TransformKind;
4
5#[derive(Debug, Clone)]
6pub struct PolygonDimension {
7    pub parameter: ParameterName,
8    pub transform: Option<TransformKind>,
9}
10
11#[derive(Debug, Clone)]
12pub struct PolygonGate {
13    id: GateId,
14    parent_id: Option<GateId>,
15    x_dim: PolygonDimension,
16    y_dim: PolygonDimension,
17    dim_names: [ParameterName; 2],
18    vertices: Vec<(f64, f64)>,
19    aabb_x: (f64, f64),
20    aabb_y: (f64, f64),
21}
22
23impl PolygonGate {
24    pub fn new(
25        id: GateId,
26        parent_id: Option<GateId>,
27        x_dim: PolygonDimension,
28        y_dim: PolygonDimension,
29        vertices: Vec<(f64, f64)>,
30    ) -> Result<Self, FlowGateError> {
31        if vertices.len() < 3 {
32            return Err(FlowGateError::InvalidGate(
33                "PolygonGate requires at least 3 vertices".to_string(),
34            ));
35        }
36        let aabb_x = (
37            vertices
38                .iter()
39                .map(|(x, _)| *x)
40                .fold(f64::INFINITY, f64::min),
41            vertices
42                .iter()
43                .map(|(x, _)| *x)
44                .fold(f64::NEG_INFINITY, f64::max),
45        );
46        let aabb_y = (
47            vertices
48                .iter()
49                .map(|(_, y)| *y)
50                .fold(f64::INFINITY, f64::min),
51            vertices
52                .iter()
53                .map(|(_, y)| *y)
54                .fold(f64::NEG_INFINITY, f64::max),
55        );
56        let dim_names = [x_dim.parameter.clone(), y_dim.parameter.clone()];
57        Ok(Self {
58            id,
59            parent_id,
60            x_dim,
61            y_dim,
62            dim_names,
63            vertices,
64            aabb_x,
65            aabb_y,
66        })
67    }
68
69    pub fn x_dim(&self) -> &PolygonDimension {
70        &self.x_dim
71    }
72
73    pub fn y_dim(&self) -> &PolygonDimension {
74        &self.y_dim
75    }
76
77    pub fn vertices(&self) -> &[(f64, f64)] {
78        &self.vertices
79    }
80}
81
82impl Gate for PolygonGate {
83    fn dimensions(&self) -> &[ParameterName] {
84        &self.dim_names
85    }
86
87    fn contains(&self, coords: &[f64]) -> bool {
88        if coords.len() != 2 {
89            return false;
90        }
91        let (px, py) = (coords[0], coords[1]);
92        if !px.is_finite() || !py.is_finite() {
93            return false;
94        }
95        if px < self.aabb_x.0 || px > self.aabb_x.1 || py < self.aabb_y.0 || py > self.aabb_y.1 {
96            return false;
97        }
98
99        for i in 0..self.vertices.len() {
100            let (x0, y0) = self.vertices[i];
101            let (x1, y1) = self.vertices[(i + 1) % self.vertices.len()];
102            if point_on_segment(px, py, x0, y0, x1, y1) {
103                return true;
104            }
105        }
106
107        // Use winding-number crossing parity, matching flowCore/flowutils
108        // behavior for non-simple polygons in the official corpus.
109        winding_number(px, py, &self.vertices).abs() % 2 == 1
110    }
111
112    fn gate_id(&self) -> &GateId {
113        &self.id
114    }
115
116    fn parent_id(&self) -> Option<&GateId> {
117        self.parent_id.as_ref()
118    }
119}
120
121#[inline]
122pub fn is_left(x0: f64, y0: f64, x1: f64, y1: f64, px: f64, py: f64) -> f64 {
123    (x1 - x0) * (py - y0) - (px - x0) * (y1 - y0)
124}
125
126/// Epsilon tolerance for polygon edge classification.
127/// Polygon gates in the Gating-ML test corpus use transformed coordinates
128/// (Logicle, ASinH, etc.) where floating-point precision loss on the
129/// coordinate transform causes boundary points to drift by ~1e-10–1e-8.
130/// Using a generous tolerance avoids misclassifying near-boundary events.
131const POLYGON_EDGE_EPS: f64 = 1e-9;
132
133pub fn winding_number(px: f64, py: f64, vertices: &[(f64, f64)]) -> i32 {
134    let n = vertices.len();
135    let mut wn = 0_i32;
136    for i in 0..n {
137        let (x0, y0) = vertices[i];
138        let (x1, y1) = vertices[(i + 1) % n];
139
140        // Points extremely close to an edge are classified as inside
141        // to avoid floating-point flip-flop on transformed coordinates.
142        let cp = is_left(x0, y0, x1, y1, px, py);
143        if cp.abs() < POLYGON_EDGE_EPS {
144            return 1;
145        }
146
147        if y0 <= py {
148            if y1 > py && cp > 0.0 {
149                wn += 1;
150            }
151        } else if y1 <= py && cp < 0.0 {
152            wn -= 1;
153        }
154    }
155    wn
156}
157
158/// Checks whether a point lies on a line segment, with tolerance.
159fn point_on_segment(px: f64, py: f64, x0: f64, y0: f64, x1: f64, y1: f64) -> bool {
160    let cross = is_left(x0, y0, x1, y1, px, py).abs();
161    if cross > POLYGON_EDGE_EPS {
162        return false;
163    }
164    let min_x = x0.min(x1) - POLYGON_EDGE_EPS;
165    let max_x = x0.max(x1) + POLYGON_EDGE_EPS;
166    let min_y = y0.min(y1) - POLYGON_EDGE_EPS;
167    let max_y = y0.max(y1) + POLYGON_EDGE_EPS;
168    px >= min_x && px <= max_x && py >= min_y && py <= max_y
169}