use crate::curve::interior_knot_count;
use super::*;
fn point_segment_distance(point: Vec2, start: Vec2, end: Vec2) -> f64 {
let segment = end.sub(start);
let length_squared = segment.dot(segment);
if length_squared <= 1e-30 {
return point.sub(start).length();
}
let parameter = (point.sub(start).dot(segment) / length_squared).clamp(0.0, 1.0);
point.sub(start.add(segment.scale(parameter))).length()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PolygonClass {
Inside,
Outside,
Boundary,
}
fn point_in_polygon(point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
for index in 0..polygon.len() {
if point_segment_distance(point, polygon[index], polygon[(index + 1) % polygon.len()])
<= tolerance
{
return PolygonClass::Boundary;
}
}
let mut inside = false;
for index in 0..polygon.len() {
let a = polygon[index];
let b = polygon[(index + 1) % polygon.len()];
if (a.y > point.y) != (b.y > point.y) {
let crossing = a.x + (point.y - a.y) / (b.y - a.y) * (b.x - a.x);
if crossing > point.x {
inside = !inside;
}
}
}
if inside {
PolygonClass::Inside
} else {
PolygonClass::Outside
}
}
struct SegmentReference<'a> {
start: Vec2,
end: Vec2,
curve: &'a NurbsCurve,
parameter_start: f64,
parameter_end: f64,
}
fn seam_band_point_in_face(
face: &FaceRecord,
point: Vec2,
tolerance: f64,
) -> Result<Option<PolygonClass>, String> {
if face.surface.closed_directions()? != (true, true) {
return Ok(None);
}
let [u0, u1] = face.surface.domain_u()?;
let [v0, v1] = face.surface.domain_v()?;
if crate::topology::doubly_periodic_has_only_collapsed_loops(face)? {
return Ok(Some(PolygonClass::Inside));
}
let u_span = (u1 - u0).abs().max(1e-30);
let v_span = (v1 - v0).abs().max(1e-30);
let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
for loop_record in &face.loops {
let mut points: Vec<[f64; 2]> = Vec::new();
for coedge in &loop_record.coedges {
let [d0, d1] = coedge.pcurve.domain()?;
let samples = 24;
for k in 0..=samples {
let t = d0 + (d1 - d0) * k as f64 / samples as f64;
let p = coedge.pcurve.evaluate(t)?;
points.push([p.x, p.y]);
}
}
let (mut umin, mut umax, mut vmin, mut vmax) = (
f64::INFINITY,
f64::NEG_INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
);
for p in &points {
umin = umin.min(p[0]);
umax = umax.max(p[0]);
vmin = vmin.min(p[1]);
vmax = vmax.max(p[1]);
}
if face.loops.len() > 2 && (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span
{
continue;
}
loops_uv.push(points);
}
if face.loops.len() == 1
&& loops_uv.len() == 1
&& std::env::var("BREP_SEAM_BAND_MERGED").as_deref() != Ok("0")
{
let coedges = &face.loops[0].coedges;
let offsets = crate::topology::loop_seam_offsets(coedges, true, true, u_span, v_span)?;
if offsets.iter().any(|o| o[0] != 0.0 || o[1] != 0.0) {
let mut polygon: Vec<Vec2> = Vec::new();
for (coedge_index, coedge) in coedges.iter().enumerate() {
let [d0, d1] = coedge.pcurve.domain()?;
let samples = 24;
for k in 0..samples {
let t = d0 + (d1 - d0) * k as f64 / samples as f64;
let p = coedge.pcurve.evaluate(t)?;
polygon.push(Vec2 {
x: p.x + offsets[coedge_index][0],
y: p.y + offsets[coedge_index][1],
});
}
}
let mut best = PolygonClass::Outside;
'images: for du in [-1.0, 0.0, 1.0] {
for dv in [-1.0, 0.0, 1.0] {
let image = Vec2 {
x: point.x + du * u_span,
y: point.y + dv * v_span,
};
match point_in_polygon(image, &polygon, tolerance) {
PolygonClass::Boundary => {
best = PolygonClass::Boundary;
break 'images;
}
PolygonClass::Inside => best = PolygonClass::Inside,
PolygonClass::Outside => {}
}
}
}
return Ok(Some(best));
}
}
if loops_uv.len() != 2 {
return Ok(None);
}
if let Some(band) = crate::topology::analyze_doubly_periodic_seam_band(
&loops_uv,
[u0, u1, v0, v1],
face.same_sense,
) {
let polygon: Vec<Vec2> =
crate::topology::seam_band_uv_polygon(&loops_uv, [u0, u1, v0, v1], &band)
.into_iter()
.map(|p| Vec2 { x: p[0], y: p[1] })
.collect();
return Ok(Some(point_in_polygon(point, &polygon, tolerance)));
}
for p_is_u in [true, false] {
let (period, q_extent) = if p_is_u {
(u1 - u0, v_span)
} else {
(v1 - v0, u_span)
};
if !(period > 0.0) {
continue;
}
let coord = |p: &[f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
let mut rings = Vec::with_capacity(2);
for points in &loops_uv {
let (mut pmin, mut pmax, mut qmin, mut qmax) = (
f64::INFINITY,
f64::NEG_INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
);
for p in points {
let (periodic, cross) = coord(p);
pmin = pmin.min(periodic);
pmax = pmax.max(periodic);
qmin = qmin.min(cross);
qmax = qmax.max(cross);
}
if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
rings.clear();
break;
}
let mut net = 0.0;
for pair in points.windows(2) {
let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
if delta > 0.5 * period {
delta -= period;
} else if delta < -0.5 * period {
delta += period;
}
net += delta;
}
let direction = if net > 0.25 * period {
1
} else if net < -0.25 * period {
-1
} else {
0
};
rings.push((0.5 * (qmin + qmax), direction));
}
if rings.len() != 2 {
continue;
}
rings.sort_by(|a, b| a.0.total_cmp(&b.0));
let [(q_lo, lower_direction), (q_hi, upper_direction)] = rings.as_slice() else {
unreachable!()
};
let inconclusive =
*lower_direction == 0 || *upper_direction == 0 || lower_direction == upper_direction;
let between_is_ccw_uv = if p_is_u {
*lower_direction > 0
} else {
*lower_direction < 0
};
let complement = !inconclusive && between_is_ccw_uv != face.same_sense;
let q = if p_is_u { point.y } else { point.x };
if (q - q_lo).abs() <= tolerance || (q - q_hi).abs() <= tolerance {
return Ok(Some(PolygonClass::Boundary));
}
let between = q > *q_lo && q < *q_hi;
return Ok(Some(if between != complement {
PolygonClass::Inside
} else {
PolygonClass::Outside
}));
}
Ok(None)
}
fn wrapped_horizon_point_in_face(
face: &FaceRecord,
point: Vec2,
tolerance: f64,
) -> Result<Option<PolygonClass>, String> {
if face.surface.closed_directions()? != (true, false) || face.loops.is_empty() {
return Ok(None);
}
if std::env::var("BREP_HORIZON_CONTAINMENT").as_deref() == Ok("0") {
return Ok(None);
}
if face.loops.len() <= 2 && std::env::var("BREP_HORIZON_SINGLE_LOOP").as_deref() == Ok("0") {
return Ok(None);
}
let [u0, u1] = face.surface.domain_u()?;
let u_period = u1 - u0;
if u_period <= 0.0 {
return Ok(None);
}
let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
let mut loops: Vec<Vec<Vec2>> = Vec::new();
let mut straddling = false;
for loop_record in &face.loops {
let mut points: Vec<Vec2> = Vec::new();
for coedge in &loop_record.coedges {
let curve = &coedge.pcurve;
let [start, end] = curve.domain()?;
let sample_count =
2usize.max((interior_knot_count(&curve.knots, curve.degree) + 1) * (curve.degree + 1) * 4);
for index in 0..sample_count {
let parameter = start + (end - start) * index as f64 / sample_count as f64;
let evaluated = curve.evaluate(parameter)?;
points.push(Vec2 {
x: evaluated.x,
y: evaluated.y,
});
}
}
if points.len() < 3 {
continue;
}
let mut unwrapped = Vec::with_capacity(points.len());
let mut jumps = 0usize;
let mut cursor = points[0];
unwrapped.push(cursor);
for pair in points.windows(2) {
let mut du = pair[1].x - pair[0].x;
let folded = du - u_period * (du / u_period).round();
if (du - folded).abs() > 0.25 * u_period {
jumps += 1;
}
du = folded;
cursor = Vec2 {
x: cursor.x + du,
y: pair[1].y,
};
unwrapped.push(cursor);
}
let closure = (unwrapped[0].x - unwrapped[unwrapped.len() - 1].x).abs();
if closure > 0.25 * u_period {
if debug {
eprintln!(
"horizon: face {} loop winds the period (closure {closure:.3}) — bail",
face.id
);
}
return Ok(None); }
if jumps > 0 {
straddling = true;
}
loops.push(unwrapped);
}
if !straddling || loops.is_empty() {
return Ok(None);
}
if std::env::var("BREP_HORIZON_CROSS_FRAME").as_deref() == Ok("0") {
let mut best: Option<PolygonClass> = None;
for shift in [-u_period, 0.0, u_period] {
let image = Vec2 {
x: point.x + shift,
y: point.y,
};
let mut crossings = 0usize;
for polygon in &loops {
match point_in_polygon(image, polygon, tolerance) {
PolygonClass::Boundary => return Ok(Some(PolygonClass::Boundary)),
PolygonClass::Inside => crossings += 1,
PolygonClass::Outside => {}
}
}
if crossings % 2 == 1 {
best = Some(PolygonClass::Inside);
} else if best.is_none() {
best = Some(PolygonClass::Outside);
}
}
if debug {
eprintln!(
"horizon: face {} loops={} straddling (legacy per-image) -> {:?}",
face.id,
loops.len(),
best
);
}
return Ok(best);
}
let mut crossings = 0usize;
for polygon in &loops {
let mut image_hits = 0usize;
for shift in [-u_period, 0.0, u_period] {
let image = Vec2 {
x: point.x + shift,
y: point.y,
};
match point_in_polygon(image, polygon, tolerance) {
PolygonClass::Boundary => return Ok(Some(PolygonClass::Boundary)),
PolygonClass::Inside => image_hits += 1,
PolygonClass::Outside => {}
}
}
crossings += image_hits % 2;
}
let class = if crossings % 2 == 1 {
PolygonClass::Inside
} else {
PolygonClass::Outside
};
if debug {
eprintln!(
"horizon: face {} loops={} straddling -> {:?}",
face.id,
loops.len(),
class
);
}
Ok(Some(class))
}
fn winding_sphere_cap_point_in_face(
face: &FaceRecord,
point: Vec2,
tolerance: f64,
) -> Result<Option<PolygonClass>, String> {
if !matches!(
face.surface.analytic(),
Some(crate::AnalyticSurface::Sphere { .. })
) || face.surface.closed_directions()? != (true, false)
|| face.loops.len() != 2
{
return Ok(None);
}
let [u0, u1] = face.surface.domain_u()?;
let [v0, v1] = face.surface.domain_v()?;
let period = u1 - u0;
let v_span = v1 - v0;
if !(period > 0.0 && v_span > 0.0) {
return Ok(None);
}
struct WindingLoop {
points: Vec<Vec2>,
winding: f64,
vmin: f64,
vmax: f64,
}
let mut loops = Vec::with_capacity(2);
for loop_record in &face.loops {
let mut points = Vec::new();
for coedge in &loop_record.coedges {
let [start, end] = coedge.pcurve.domain()?;
let samples = 2usize
.max((interior_knot_count(&coedge.pcurve.knots, coedge.pcurve.degree) + 1) * (coedge.pcurve.degree + 1) * 4);
for index in 0..samples {
let parameter = start + (end - start) * index as f64 / samples as f64;
let p = coedge.pcurve.evaluate(parameter)?;
points.push(Vec2 { x: p.x, y: p.y });
}
}
if points.len() < 2 {
return Ok(None);
}
let first = points[0];
let mut cursor = first;
let mut unwrapped = vec![cursor];
for next in points.iter().skip(1) {
let du = next.x - cursor.x;
let folded = du - period * (du / period).round();
cursor = Vec2 {
x: cursor.x + folded,
y: next.y,
};
unwrapped.push(cursor);
}
let last_raw = *points.last().unwrap();
let closing_du = first.x - last_raw.x;
let closing_folded = closing_du - period * (closing_du / period).round();
let winding = cursor.x + closing_folded - first.x;
let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
for p in &unwrapped {
vmin = vmin.min(p.y);
vmax = vmax.max(p.y);
}
loops.push(WindingLoop {
points: unwrapped,
winding,
vmin,
vmax,
});
}
if loops
.iter()
.any(|loop_data| (loop_data.winding.abs() - period).abs() > 0.05 * period)
|| loops[0].winding * loops[1].winding >= 0.0
{
return Ok(None);
}
let flat = |loop_data: &WindingLoop| loop_data.vmax - loop_data.vmin <= 1e-6 * v_span;
let (pole, rim) = match (flat(&loops[0]), flat(&loops[1])) {
(true, false) => (&loops[0], &loops[1]),
(false, true) => (&loops[1], &loops[0]),
_ => return Ok(None),
};
let pole_v = 0.5 * (pole.vmin + pole.vmax);
if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
return Ok(None);
}
let window_start = rim.points[0].x.min(rim.points[rim.points.len() - 1].x);
let query_u = window_start + (point.x - window_start).rem_euclid(period);
let mut crossings = Vec::new();
for pair in rim.points.windows(2) {
let (a, b) = (pair[0], pair[1]);
if (a.x > query_u) != (b.x > query_u) {
crossings.push(a.y + (query_u - a.x) / (b.x - a.x) * (b.y - a.y));
}
for image in [-period, 0.0, period] {
if point_segment_distance(
Vec2 {
x: point.x + image,
y: point.y,
},
a,
b,
) <= tolerance
{
return Ok(Some(PolygonClass::Boundary));
}
}
}
let Some(rim_v) = crossings
.into_iter()
.min_by(|a, b| (a - point.y).abs().total_cmp(&(b - point.y).abs()))
else {
return Ok(None);
};
let inside = if pole_v < rim_v {
point.y <= rim_v + tolerance
} else {
point.y >= rim_v - tolerance
};
Ok(Some(if inside {
PolygonClass::Inside
} else {
PolygonClass::Outside
}))
}
fn covering_rim_strip_point_in_face(
face: &FaceRecord,
point: Vec2,
tolerance: f64,
) -> Result<Option<PolygonClass>, String> {
if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
return Ok(None);
}
if std::env::var("BREP_COVERING_RIM_STRIP").as_deref() == Ok("0") {
return Ok(None);
}
let [u0, u1] = face.surface.domain_u()?;
let [v0, v1] = face.surface.domain_v()?;
let period = u1 - u0;
if !(period > 0.0) {
return Ok(None);
}
let v_span = (v1 - v0).abs().max(1e-30);
struct Rim {
points: Vec<Vec2>,
vmin: f64,
vmax: f64,
ascending: bool,
}
let mut rims: Vec<Rim> = Vec::with_capacity(2);
let mut out_of_domain = false;
for loop_record in &face.loops {
let mut points: Vec<Vec2> = Vec::new();
for coedge in &loop_record.coedges {
let curve = &coedge.pcurve;
let [start, end] = curve.domain()?;
let sample_count =
2usize.max((interior_knot_count(&curve.knots, curve.degree) + 1) * (curve.degree + 1) * 4);
for index in 0..=sample_count {
let parameter = start + (end - start) * index as f64 / sample_count as f64;
let evaluated = curve.evaluate(parameter)?;
points.push(Vec2 {
x: evaluated.x,
y: evaluated.y,
});
}
}
if points.len() < 3 {
return Ok(None);
}
for p in &points {
if p.x < u0 - 1e-3 * period || p.x > u1 + 1e-3 * period {
out_of_domain = true;
}
}
let mut unwrapped: Vec<Vec2> = Vec::with_capacity(points.len());
let mut cursor = points[0];
unwrapped.push(cursor);
for pair in points.windows(2) {
let du = pair[1].x - pair[0].x;
let folded = du - period * (du / period).round();
cursor = Vec2 {
x: cursor.x + folded,
y: pair[1].y,
};
unwrapped.push(cursor);
}
let net = unwrapped[unwrapped.len() - 1].x - unwrapped[0].x;
if (net.abs() - period).abs() > 2e-2 * period {
return Ok(None); }
if (unwrapped[unwrapped.len() - 1].y - unwrapped[0].y).abs() > 1e-3 * v_span {
return Ok(None); }
let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
for p in &unwrapped {
vmin = vmin.min(p.y);
vmax = vmax.max(p.y);
}
rims.push(Rim {
points: unwrapped,
vmin,
vmax,
ascending: net > 0.0,
});
}
if !out_of_domain {
return Ok(None);
}
if rims[0].ascending == rims[1].ascending {
return Ok(None); }
let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
(&rims[0], &rims[1])
} else if rims[1].vmax <= rims[0].vmin {
(&rims[1], &rims[0])
} else {
return Ok(None); };
if upper.vmin - lower.vmax <= 1e-6 * v_span {
return Ok(None);
}
if (lower.ascending) != face.same_sense {
return Ok(None);
}
for rim in [lower, upper] {
for shift in [-period, 0.0, period] {
let image = Vec2 {
x: point.x + shift,
y: point.y,
};
for pair in rim.points.windows(2) {
if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
return Ok(Some(PolygonClass::Boundary));
}
}
}
}
let mut crossings = 0usize;
for rim in [lower, upper] {
let window_base = rim.points[0].x.min(rim.points[rim.points.len() - 1].x);
let x = window_base + (point.x - window_base).rem_euclid(period);
for pair in rim.points.windows(2) {
let (a, b) = (pair[0], pair[1]);
if (a.x > x) != (b.x > x) {
let v_cross = a.y + (x - a.x) / (b.x - a.x) * (b.y - a.y);
if v_cross > point.y {
crossings += 1;
}
}
}
}
Ok(Some(if crossings % 2 == 1 {
PolygonClass::Inside
} else {
PolygonClass::Outside
}))
}
struct SphereRegionCache {
scratch: Vec<f64>,
entries: Vec<(Vec<f64>, crate::sphere_chart::SphericalRegion)>,
}
const SPHERE_REGION_CACHE_ENTRIES: usize = 4;
thread_local! {
static SPHERE_REGIONS: std::cell::RefCell<SphereRegionCache> = const {
std::cell::RefCell::new(SphereRegionCache {
scratch: Vec::new(),
entries: Vec::new(),
})
};
}
struct LoopSamples {
polygon: Vec<Vec2>,
coedges: Vec<(u64, usize, usize)>,
}
fn sphere_chart_point_in_face(
face: &FaceRecord,
point: Vec2,
sampled: &[LoopSamples],
) -> Result<Option<PolygonClass>, String> {
if std::env::var("BREP_NO_SPHERE_CHARTS").is_ok()
|| std::env::var("BREP_NO_SPHERE_CHART_TRIM").is_ok()
{
return Ok(None);
}
let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
return Ok(None);
};
let mut uses: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
for coedge in face.loops.iter().flat_map(|record| record.coedges.iter()) {
*uses.entry(coedge.edge_id).or_insert(0) += 1;
}
let outward_face_normal =
face.same_sense == atlas.parameterization_is_outward(&face.surface)?;
SPHERE_REGIONS.with(|cache| {
let SphereRegionCache { scratch, entries } = &mut *cache.borrow_mut();
scratch.clear();
scratch.extend_from_slice(&[
atlas.centre.x,
atlas.centre.y,
atlas.centre.z,
atlas.radius,
if outward_face_normal { 1.0 } else { 0.0 },
]);
for axis in atlas.basis {
scratch.extend_from_slice(&[axis.x, axis.y, axis.z]);
}
let header = scratch.len();
for samples in sampled.iter() {
let count = samples.polygon.len();
if count < 2 {
continue;
}
for &(edge_id, first, span) in &samples.coedges {
if span == 0 || uses.get(&edge_id).copied().unwrap_or(0) >= 2 {
continue;
}
scratch.push(span as f64);
for offset in 0..=span {
let uv = samples.polygon[(first + offset) % count];
scratch.push(uv.x);
scratch.push(uv.y);
}
}
}
if let Some(index) = entries.iter().position(|(signature, _)| signature == scratch) {
if index != 0 {
entries.swap(0, index);
}
} else {
let mut points: Vec<Vec3> = Vec::new();
let mut spans: Vec<(usize, usize)> = Vec::new();
let mut cursor = header;
while cursor < scratch.len() {
let span = scratch[cursor] as usize;
cursor += 1;
let start = points.len();
for index in 0..=span {
let uv = (scratch[cursor + 2 * index], scratch[cursor + 2 * index + 1]);
points.push(face.surface.evaluate(uv.0, uv.1)?);
}
cursor += 2 * (span + 1);
spans.push((start, points.len()));
}
crate::sphere_chart::canonicalize_points(&mut points, 1e-9);
let mut boundary: Vec<(Vec3, Vec3)> = Vec::new();
for (first, last) in spans {
for index in first..last.saturating_sub(1) {
boundary.push((points[index], points[index + 1]));
}
}
let region = crate::sphere_chart::SphericalRegion::from_segments(
atlas.centre,
&boundary,
outward_face_normal,
);
entries.insert(0, (scratch.clone(), region));
entries.truncate(SPHERE_REGION_CACHE_ENTRIES);
}
let region = &entries[0].1;
if region.is_whole_sphere() {
return Ok(Some(PolygonClass::Inside));
}
if !region.is_decidable() {
return Ok(None);
}
let probe = face.surface.evaluate(point.x, point.y)?;
Ok(Some(if region.contains(atlas.centre, probe) {
PolygonClass::Inside
} else {
PolygonClass::Outside
}))
})
}
pub fn parameter_point_in_face(
face: &FaceRecord,
point: Vec2,
tolerance: f64,
) -> Result<PolygonClass, String> {
if let Some(class) = seam_band_point_in_face(face, point, tolerance)? {
return Ok(class);
}
if let Some(class) = winding_sphere_cap_point_in_face(face, point, tolerance)? {
return Ok(class);
}
if let Some(class) = wrapped_horizon_point_in_face(face, point, tolerance)? {
return Ok(class);
}
if let Some(class) = covering_rim_strip_point_in_face(face, point, tolerance)? {
return Ok(class);
}
let mut crossings = 0;
let mut boundary = false;
let mut nearest: Option<(SegmentReference<'_>, f64)> = None;
let mut sampled: Vec<LoopSamples> = Vec::with_capacity(face.loops.len());
for loop_record in &face.loops {
let mut polygon = Vec::new();
let mut segments = Vec::new();
let mut coedges = Vec::with_capacity(loop_record.coedges.len());
for coedge in &loop_record.coedges {
let first = polygon.len();
let curve = &coedge.pcurve;
let [start, end] = curve.domain()?;
let sample_count =
2usize.max((interior_knot_count(&curve.knots, curve.degree) + 1) * (curve.degree + 1) * 4);
for index in 0..sample_count {
let parameter = start + (end - start) * index as f64 / sample_count as f64;
let evaluated = curve.evaluate(parameter)?;
polygon.push(Vec2 {
x: evaluated.x,
y: evaluated.y,
});
let parameter_end = if index + 1 < sample_count {
start + (end - start) * (index + 1) as f64 / sample_count as f64
} else {
end
};
segments.push(SegmentReference {
start: Vec2 {
x: evaluated.x,
y: evaluated.y,
},
end: Vec2 { x: 0.0, y: 0.0 },
curve,
parameter_start: parameter,
parameter_end,
});
}
coedges.push((coedge.edge_id, first, polygon.len() - first));
}
for index in 0..polygon.len() {
segments[index].end = polygon[(index + 1) % polygon.len()];
}
match point_in_polygon(point, &polygon, tolerance) {
PolygonClass::Boundary => boundary = true,
PolygonClass::Inside => crossings += 1,
PolygonClass::Outside => {}
}
for segment in segments {
let distance = point_segment_distance(point, segment.start, segment.end);
if nearest
.as_ref()
.is_none_or(|(_, nearest_distance)| distance < *nearest_distance)
{
nearest = Some((segment, distance));
}
}
sampled.push(LoopSamples { polygon, coedges });
}
if boundary {
return Ok(PolygonClass::Boundary);
}
let parity = if crossings % 2 == 1 {
PolygonClass::Inside
} else {
PolygonClass::Outside
};
let parity_decides = match nearest.as_ref() {
None => true,
Some((segment, distance)) => {
let length = segment.end.sub(segment.start).length();
*distance > length || length <= 0.0
}
};
if parity_decides {
if let Some(class) = sphere_chart_point_in_face(face, point, &sampled)? {
return Ok(class);
}
}
let Some((segment, distance)) = nearest else {
return Ok(parity);
};
let segment_length = segment.end.sub(segment.start).length();
if distance > segment_length || segment_length <= 0.0 {
return Ok(parity);
}
let chord_parameter = segment.parameter_start
+ (segment.parameter_end - segment.parameter_start)
* (point.sub(segment.start).dot(segment.end.sub(segment.start))
/ (segment_length * segment_length))
.clamp(0.0, 1.0);
let [domain_start, domain_end] = segment.curve.domain()?;
let mut parameter = chord_parameter;
for _ in 0..12 {
let derivatives = segment.curve.derivatives(parameter, 2)?;
let on_curve = Vec2 {
x: derivatives[0].x,
y: derivatives[0].y,
};
let tangent = Vec2 {
x: derivatives[1].x,
y: derivatives[1].y,
};
let second = Vec2 {
x: derivatives[2].x,
y: derivatives[2].y,
};
let residual = on_curve.sub(point);
let denominator = tangent.dot(tangent) + residual.dot(second);
if denominator.abs() < 1e-30 {
break;
}
let step = -residual.dot(tangent) / denominator;
parameter = (parameter + step).clamp(domain_start, domain_end);
if step.abs() < 1e-14 * (domain_end - domain_start + 1.0) {
break;
}
}
let margin = 1e-9 * (domain_end - domain_start);
if parameter > domain_start + margin && parameter < domain_end - margin {
let derivatives = segment.curve.derivatives(parameter, 1)?;
let on_curve = Vec2 {
x: derivatives[0].x,
y: derivatives[0].y,
};
let tangent = Vec2 {
x: derivatives[1].x,
y: derivatives[1].y,
};
let offset = point.sub(on_curve);
if offset.length() <= tolerance {
return Ok(PolygonClass::Boundary);
}
let cross = tangent.x * offset.y - tangent.y * offset.x;
if cross.abs() > 1e-30 {
return Ok(if (cross > 0.0) == face.same_sense {
PolygonClass::Inside
} else {
PolygonClass::Outside
});
}
}
Ok(parity)
}