///|
/// Immutable filled screen geometry with the SVG nonzero or even-odd rule.
/// Input and inspection arrays are independent; Drawing can safely share this
/// opaque value. Validation and bounds are cached once at construction.
pub struct FilledRegion {
priv rings : Array[Array[(Double, Double)]]
priv even_odd : Bool
priv valid : Bool
priv filled_rings : Array[Bool]
priv bounds : (Double, Double, Double, Double)
} derive(Eq, Debug)
///|
pub extend FilledRegion with Eq::{equal, not_equal}
///|
pub extend FilledRegion with @debug.Debug::{to_repr}
///|
pub fn filled_region(
rings : Array[Array[(Double, Double)]],
even_odd~ : Bool = false,
) -> FilledRegion {
let copied = rings.map(ring => ring.copy())
let mut valid = true
let mut area = false
let filled_rings = []
let mut xmin = 1.0 / 0.0
let mut xmax = -1.0 / 0.0
let mut ymin = 1.0 / 0.0
let mut ymax = -1.0 / 0.0
for ring in copied {
if ring.length() < 3 {
filled_rings.push(false)
continue
}
let first = ring[0]
let mut second = first
let mut ring_xmin = 1.0 / 0.0
let mut ring_xmax = -1.0 / 0.0
let mut ring_ymin = 1.0 / 0.0
let mut ring_ymax = -1.0 / 0.0
for p in ring {
if !finite(p.0) || !finite(p.1) {
valid = false
}
ring_xmin = ring_xmin.min(p.0)
ring_xmax = ring_xmax.max(p.0)
ring_ymin = ring_ymin.min(p.1)
ring_ymax = ring_ymax.max(p.1)
if second == first && p != first {
second = p
}
}
// Signed area may cancel for a real self-crossing fill. Reject only an
// entirely collinear ring, not a bow tie or opposite-oriented components.
let mut ring_area = false
for p in ring {
if quad_cross(first, second, p) != 0.0 {
ring_area = true
}
}
filled_rings.push(ring_area)
area = area || ring_area
if ring_area {
xmin = xmin.min(ring_xmin)
xmax = xmax.max(ring_xmax)
ymin = ymin.min(ring_ymin)
ymax = ymax.max(ring_ymax)
}
}
// Store extrema, not origin+size: reconstructing max from a remote min
// and rounded span can exclude a local component by many screen pixels.
let bounds = (xmin, ymin, xmax, ymax)
valid = valid &&
area &&
finite(xmin) &&
finite(xmax) &&
finite(ymin) &&
finite(ymax) &&
xmin < xmax &&
ymin < ymax &&
finite(xmax - xmin) &&
finite(ymax - ymin)
{ rings: copied, even_odd, valid, filled_rings, bounds, }
}
///|
pub fn FilledRegion::rings(
self : FilledRegion,
) -> Array[Array[(Double, Double)]] {
self.rings.map(ring => ring.copy())
}
///|
pub fn FilledRegion::even_odd(self : FilledRegion) -> Bool {
self.even_odd
}
///|
fn FilledRegion::contains(self : FilledRegion, x : Double, y : Double) -> Bool {
let (xmin, ymin, xmax, ymax) = self.bounds
if !self.valid ||
!finite(x) ||
!finite(y) ||
x < xmin ||
x > xmax ||
y < ymin ||
y > ymax {
return false
}
let mut winding = 0
let mut parity = false
for ring_index, ring in self.rings {
if !self.filled_rings[ring_index] {
continue
}
for i, a in ring {
let b = ring[(i + 1) % ring.length()]
let cross = quad_cross(a, b, (x, y))
if cross == 0.0 &&
x >= a.0.min(b.0) &&
x <= a.0.max(b.0) &&
y >= a.1.min(b.1) &&
y <= a.1.max(b.1) {
return true
}
if a.1 <= y && b.1 > y && cross > 0.0 {
parity = !parity
winding += 1
} else if a.1 > y && b.1 <= y && cross < 0.0 {
parity = !parity
winding -= 1
}
}
}
if self.even_odd {
parity
} else {
winding != 0
}
}