use crate::curve::interior_knot_count;
use super::*;
pub fn trim_sample_count(curve: &NurbsCurve) -> usize {
2usize.max((interior_knot_count(&curve.knots, curve.degree) + 1) * (curve.degree + 1) * 4)
}
pub fn trim_station(start: f64, end: f64, index: usize, count: usize) -> f64 {
start + (end - start) * index as f64 / count as f64
}
pub const SEAM_BAND_MERGED_SWITCH: &str = "BREP_SEAM_BAND_MERGED";
pub const HORIZON_CONTAINMENT_SWITCH: &str = "BREP_HORIZON_CONTAINMENT";
pub const HORIZON_SINGLE_LOOP_SWITCH: &str = "BREP_HORIZON_SINGLE_LOOP";
pub const HORIZON_CROSS_FRAME_SWITCH: &str = "BREP_HORIZON_CROSS_FRAME";
pub const COVERING_RIM_STRIP_SWITCH: &str = "BREP_COVERING_RIM_STRIP";
pub const NO_SPHERE_CHART_TRIM_SWITCH: &str = "BREP_NO_SPHERE_CHART_TRIM";
pub fn horizon_cross_frame() -> bool {
switch!(HORIZON_CROSS_FRAME_SWITCH, true)
}
fn sphere_chart_trim_enabled() -> bool {
env_read!("BREP_NO_SPHERE_CHARTS").is_none()
&& std::env::var(NO_SPHERE_CHART_TRIM_SWITCH).is_err()
}
fn point_segment_distance(point: Vec2, start: Vec2, end: Vec2) -> f64 {
point.distance_to_segment(start, end, 1e-30)
}
pub use crate::polygon::PolygonClass;
fn point_in_polygon(point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
crate::polygon::classify(point, polygon, tolerance, 1e-30)
}
fn polygon_parity_inside(point: Vec2, polygon: &[Vec2]) -> bool {
crate::polygon::parity_inside([point.x, point.y], polygon.len(), |i| [polygon[i].x, polygon[i].y])
}
#[derive(Clone, Copy)]
struct SegmentPrep {
coedge_index: usize,
parameter_start: f64,
parameter_end: f64,
}
pub(crate) fn lane_switches() -> f64 {
let mut bits = 0u32;
if !switch!(SEAM_BAND_MERGED_SWITCH, true) {
bits |= 1;
}
if !switch!(HORIZON_CONTAINMENT_SWITCH, true) {
bits |= 2;
}
if !switch!(HORIZON_SINGLE_LOOP_SWITCH, true) {
bits |= 4;
}
if !switch!(COVERING_RIM_STRIP_SWITCH, true) {
bits |= 8;
}
bits as f64
}
enum SeamBandPrep {
NotApplicable,
AlwaysInside,
Merged { polygon: Vec<Vec2>, u_span: f64, v_span: f64 },
Analyzed { polygon: Vec<Vec2> },
Rings { p_is_u: bool, q_lo: f64, q_hi: f64, complement: bool },
Declined,
}
struct SphereCapPrep {
rim: Vec<Vec2>,
pole_v: f64,
period: f64,
}
struct HorizonPrep {
loops: Vec<Vec<Vec2>>,
u_period: f64,
index: std::cell::OnceCell<Option<ChordIndex>>,
}
struct RimStripPrep {
lower: Vec<Vec2>,
upper: Vec<Vec2>,
period: f64,
}
struct GenericPrep {
loops: Vec<LoopSamples>,
segments: Vec<Vec<SegmentPrep>>,
chord_count: usize,
index: std::cell::OnceCell<Option<ChordIndex>>,
}
const CHORD_INDEX_MIN_CHORDS: usize = 64;
const CHORD_INDEX_LEAF: usize = 6;
const CHORD_INDEX_MAX_COORDINATE: f64 = 1e100;
fn chord_index_enabled() -> bool {
switch!("BREP_CONTAINMENT_INDEX", true)
}
#[derive(Clone, Copy)]
struct ChordNode {
low: Vec2,
high: Vec2,
loop_index: u32,
first: u32,
last: u32,
children: Option<(u32, u32)>,
}
struct ChordIndex {
nodes: Vec<ChordNode>,
roots: Vec<u32>,
slack: f64,
}
trait ChordLoops {
fn loop_count(&self) -> usize;
fn polygon(&self, index: usize) -> &[Vec2];
}
impl ChordLoops for [LoopSamples] {
fn loop_count(&self) -> usize {
self.len()
}
fn polygon(&self, index: usize) -> &[Vec2] {
&self[index].polygon
}
}
impl ChordLoops for [Vec<Vec2>] {
fn loop_count(&self) -> usize {
self.len()
}
fn polygon(&self, index: usize) -> &[Vec2] {
&self[index]
}
}
impl ChordIndex {
fn build<L: ChordLoops + ?Sized>(loops: &L) -> Option<ChordIndex> {
let mut nodes: Vec<ChordNode> = Vec::new();
let mut roots = Vec::new();
let mut largest: f64 = 0.0;
for loop_index in 0..loops.loop_count() {
let polygon = loops.polygon(loop_index);
let len = polygon.len();
if len == 0 {
continue;
}
u32::try_from(len).ok()?;
for sample in polygon {
if !(sample.x.abs() <= CHORD_INDEX_MAX_COORDINATE && sample.y.abs() <= CHORD_INDEX_MAX_COORDINATE) {
return None;
}
largest = largest.max(sample.x.abs()).max(sample.y.abs());
}
let loop_index = u32::try_from(loop_index).ok()?;
let mut level: Vec<u32> = Vec::with_capacity(len / CHORD_INDEX_LEAF + 1);
for first in (0..len).step_by(CHORD_INDEX_LEAF) {
let last = (first + CHORD_INDEX_LEAF).min(len);
let (mut low, mut high) = (polygon[first], polygon[first]);
for index in first..last {
let end = polygon[(index + 1) % len];
low = Vec2 { x: low.x.min(end.x), y: low.y.min(end.y) };
high = Vec2 { x: high.x.max(end.x), y: high.y.max(end.y) };
let start = polygon[index];
low = Vec2 { x: low.x.min(start.x), y: low.y.min(start.y) };
high = Vec2 { x: high.x.max(start.x), y: high.y.max(start.y) };
}
level.push(u32::try_from(nodes.len()).ok()?);
nodes.push(ChordNode {
low,
high,
loop_index,
first: first as u32,
last: last as u32,
children: None,
});
}
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len() / 2 + 1);
for pair in level.chunks(2) {
let &[left, right] = pair else {
next.push(pair[0]);
continue;
};
let (a, b) = (nodes[left as usize], nodes[right as usize]);
next.push(u32::try_from(nodes.len()).ok()?);
nodes.push(ChordNode {
low: Vec2 { x: a.low.x.min(b.low.x), y: a.low.y.min(b.low.y) },
high: Vec2 { x: a.high.x.max(b.high.x), y: a.high.y.max(b.high.y) },
loop_index,
first: a.first,
last: b.last,
children: Some((left, right)),
});
}
level = next;
}
roots.push(level[0]);
}
if roots.is_empty() {
return None;
}
Some(ChordIndex {
nodes,
roots,
slack: 1e-12 * (1.0 + largest),
})
}
fn slack_at(&self, point: Vec2) -> f64 {
self.slack + 1e-12 * point.x.abs().max(point.y.abs())
}
fn box_distance(node: &ChordNode, point: Vec2) -> f64 {
let dx = (node.low.x - point.x).max(point.x - node.high.x).max(0.0);
let dy = (node.low.y - point.y).max(point.y - node.high.y).max(0.0);
(dx * dx + dy * dy).sqrt()
}
fn nearest<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2) -> Option<(usize, usize, f64)> {
let slack = self.slack_at(point);
let mut best: Option<(f64, u32, u32)> = None;
let mut stack: Vec<(u32, f64)> = Vec::with_capacity(64);
for &root in &self.roots {
stack.push((root, Self::box_distance(&self.nodes[root as usize], point)));
}
while let Some((node_index, bound)) = stack.pop() {
if best.is_some_and(|(distance, _, _)| bound > distance + slack + 1e-12 * distance) {
continue;
}
let node = self.nodes[node_index as usize];
match node.children {
None => {
let polygon = loops.polygon(node.loop_index as usize);
for index in node.first..node.last {
let start = polygon[index as usize];
let end = polygon[(index as usize + 1) % polygon.len()];
let distance = point_segment_distance(point, start, end);
let better = best.is_none_or(|(least, loop_index, chord_index)| {
distance < least
|| (distance == least && (node.loop_index, index) < (loop_index, chord_index))
});
if better {
best = Some((distance, node.loop_index, index));
}
}
}
Some((left, right)) => {
let left_bound = Self::box_distance(&self.nodes[left as usize], point);
let right_bound = Self::box_distance(&self.nodes[right as usize], point);
if left_bound <= right_bound {
stack.push((right, right_bound));
stack.push((left, left_bound));
} else {
stack.push((left, left_bound));
stack.push((right, right_bound));
}
}
}
}
best.map(|(distance, loop_index, chord_index)| {
(loop_index as usize, chord_index as usize, distance)
})
}
fn any_within<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2, tolerance: f64) -> bool {
let reach = tolerance + self.slack_at(point) + 1e-12 * tolerance.abs();
let mut stack: Vec<u32> = self.roots.clone();
while let Some(node_index) = stack.pop() {
let node = self.nodes[node_index as usize];
if Self::box_distance(&node, point) > reach {
continue;
}
match node.children {
None => {
let polygon = loops.polygon(node.loop_index as usize);
for index in node.first as usize..node.last as usize {
let (start, end) = (polygon[index], polygon[(index + 1) % polygon.len()]);
if point_segment_distance(point, start, end) <= tolerance {
return true;
}
}
}
Some((left, right)) => {
stack.push(right);
stack.push(left);
}
}
}
false
}
fn flip_crossings<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2, odd: &mut [bool]) {
let slack = self.slack_at(point);
let mut stack: Vec<u32> = self.roots.clone();
while let Some(node_index) = stack.pop() {
let node = self.nodes[node_index as usize];
if !(node.low.y <= point.y && node.high.y > point.y) || node.high.x + slack < point.x {
continue;
}
match node.children {
None => {
let polygon = loops.polygon(node.loop_index as usize);
for index in node.first as usize..node.last as usize {
let a = polygon[index];
let b = polygon[(index + 1) % polygon.len()];
if crate::polygon::crosses_ray([point.x, point.y], [a.x, a.y], [b.x, b.y]) {
let parity = &mut odd[node.loop_index as usize];
*parity = !*parity;
}
}
}
Some((left, right)) => {
stack.push(right);
stack.push(left);
}
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ChordScan {
Auto,
Indexed,
Linear,
}
impl ChordScan {
fn indexes(self, chords: usize) -> bool {
match self {
ChordScan::Linear => false,
ChordScan::Indexed => true,
ChordScan::Auto => chord_index_enabled() && chords >= CHORD_INDEX_MIN_CHORDS,
}
}
}
fn in_index_range(point: Vec2) -> bool {
point.x.abs() <= CHORD_INDEX_MAX_COORDINATE && point.y.abs() <= CHORD_INDEX_MAX_COORDINATE
}
fn linear_scan(
generic: &GenericPrep,
point: Vec2,
tolerance: f64,
) -> (usize, bool, Option<(usize, usize, f64)>) {
let mut crossings = 0;
let mut boundary = false;
let mut nearest: Option<(usize, usize, f64)> = None;
let fused = fused_scan_enabled();
for (loop_index, (samples, chords)) in generic.loops.iter().zip(&generic.segments).enumerate() {
let polygon = &samples.polygon;
if fused {
let mut on_boundary = false;
for index in 0..polygon.len() {
let distance = point_segment_distance(point, polygon[index], polygon[(index + 1) % polygon.len()]);
if distance <= tolerance {
on_boundary = true;
}
if index < chords.len()
&& nearest
.as_ref()
.is_none_or(|(_, _, nearest_distance)| distance < *nearest_distance)
{
nearest = Some((loop_index, index, distance));
}
}
if on_boundary {
boundary = true;
} else if polygon_parity_inside(point, polygon) {
crossings += 1;
}
continue;
}
match point_in_polygon(point, polygon, tolerance) {
PolygonClass::Boundary => boundary = true,
PolygonClass::Inside => crossings += 1,
PolygonClass::Outside => {}
}
for index in 0..chords.len() {
let start = polygon[index];
let end = polygon[(index + 1) % polygon.len()];
let distance = point_segment_distance(point, start, end);
if nearest
.as_ref()
.is_none_or(|(_, _, nearest_distance)| distance < *nearest_distance)
{
nearest = Some((loop_index, index, distance));
}
}
}
(crossings, boundary, nearest)
}
fn fused_scan_enabled() -> bool {
switch!("BREP_CONTAINMENT_FUSED_SCAN", true)
}
fn generic_scan(
generic: &GenericPrep,
point: Vec2,
tolerance: f64,
scan: ChordScan,
) -> (usize, bool, Option<(usize, usize, f64)>) {
if scan.indexes(generic.chord_count) && in_index_range(point) {
if let Some(index) = generic.index.get_or_init(|| {
let started = containment_profile_started();
let built = ChordIndex::build(generic.loops.as_slice());
let ms = containment_elapsed_ms(started);
containment_profile(|p| {
p.index_builds += 1;
p.index_ms += ms;
});
built
}) {
let loops = generic.loops.as_slice();
let nearest = index.nearest(loops, point);
let boundary = nearest.is_some_and(|(_, _, distance)| distance <= tolerance);
let crossings = if boundary {
0
} else {
let mut odd = vec![false; loops.len()];
index.flip_crossings(loops, point, &mut odd);
odd.into_iter().filter(|&parity| parity).count()
};
return (crossings, boundary, nearest);
}
}
linear_scan(generic, point, tolerance)
}
struct PreparedFace {
seam_band: std::cell::OnceCell<Result<SeamBandPrep, String>>,
sphere_cap: std::cell::OnceCell<Result<Option<SphereCapPrep>, String>>,
horizon: std::cell::OnceCell<Result<Option<HorizonPrep>, String>>,
rim_strip: std::cell::OnceCell<Result<Option<RimStripPrep>, String>>,
generic: std::cell::OnceCell<Result<GenericPrep, String>>,
sphere_region: std::cell::OnceCell<Result<crate::sphere_chart::SphericalRegion, String>>,
loop_samples: std::cell::RefCell<Vec<Option<Result<std::rc::Rc<Vec<Vec2>>, String>>>>,
}
impl PreparedFace {
fn new() -> Self {
Self {
seam_band: std::cell::OnceCell::new(),
sphere_cap: std::cell::OnceCell::new(),
horizon: std::cell::OnceCell::new(),
rim_strip: std::cell::OnceCell::new(),
generic: std::cell::OnceCell::new(),
sphere_region: std::cell::OnceCell::new(),
loop_samples: std::cell::RefCell::new(Vec::new()),
}
}
}
fn loop_samples(prepared: &PreparedFace, face: &FaceRecord, loop_index: usize) -> Result<std::rc::Rc<Vec<Vec2>>, String> {
if let Some(Some(sampled)) = prepared.loop_samples.borrow().get(loop_index) {
return sampled.clone();
}
let sampled = (|| {
let mut points: Vec<Vec2> = Vec::new();
for coedge in &face.loops[loop_index].coedges {
let curve = &coedge.pcurve;
let [start, end] = curve.domain()?;
let sample_count = trim_sample_count(curve);
for index in 0..sample_count {
let parameter = trim_station(start, end, index, sample_count);
let evaluated = curve.evaluate(parameter)?;
points.push(Vec2 {
x: evaluated.x,
y: evaluated.y,
});
}
}
Ok(std::rc::Rc::new(points))
})();
let mut cells = prepared.loop_samples.borrow_mut();
if cells.len() < face.loops.len() {
cells.resize(face.loops.len(), None);
}
cells[loop_index] = Some(sampled.clone());
sampled
}
#[derive(Default, Clone, Copy)]
struct ContainmentProfile {
enabled: bool,
cache_misses: u64,
seam_band_preps: u64,
seam_band_ms: f64,
sphere_cap_preps: u64,
sphere_cap_ms: f64,
horizon_preps: u64,
horizon_ms: f64,
rim_strip_preps: u64,
rim_strip_ms: f64,
generic_preps: u64,
generic_ms: f64,
generic_chords: u64,
index_builds: u64,
index_ms: f64,
region_builds: u64,
region_evaluate_ms: f64,
region_canonical_ms: f64,
region_segments_ms: f64,
}
thread_local! {
static CONTAINMENT_PROFILE: std::cell::Cell<ContainmentProfile> =
const { std::cell::Cell::new(ContainmentProfile {
enabled: false, cache_misses: 0, seam_band_preps: 0, seam_band_ms: 0.0,
sphere_cap_preps: 0, sphere_cap_ms: 0.0, horizon_preps: 0, horizon_ms: 0.0,
rim_strip_preps: 0, rim_strip_ms: 0.0, generic_preps: 0, generic_ms: 0.0,
generic_chords: 0, index_builds: 0, index_ms: 0.0, region_builds: 0,
region_evaluate_ms: 0.0, region_canonical_ms: 0.0, region_segments_ms: 0.0,
}) };
}
impl crate::profile::Counters for ContainmentProfile {
fn armed(&self) -> bool {
self.enabled
}
}
fn containment_profile(update: impl FnOnce(&mut ContainmentProfile)) {
crate::profile::update(&CONTAINMENT_PROFILE, update)
}
fn containment_profile_started() -> Option<web_time::Instant> {
crate::profile::started(&CONTAINMENT_PROFILE)
}
use crate::profile::elapsed_ms as containment_elapsed_ms;
pub(crate) fn containment_profile_begin() {
let enabled = crate::profile::profile_enabled();
CONTAINMENT_PROFILE.with(|cell| {
cell.set(ContainmentProfile {
enabled,
..ContainmentProfile::default()
})
});
}
pub(crate) fn containment_profile_report(label: &str) {
let p = CONTAINMENT_PROFILE.with(|cell| cell.get());
if !p.enabled {
return;
}
profile_line!(
"containment.profile {label} cache_misses={} seam_band: preps={} ms={:.2} sphere_cap: preps={} ms={:.2} horizon: preps={} ms={:.2} rim_strip: preps={} ms={:.2} generic: preps={} ms={:.2} chords={} index: builds={} ms={:.2} region: builds={} evaluate_ms={:.2} canonical_ms={:.2} segments_ms={:.2}",
p.cache_misses, p.seam_band_preps, p.seam_band_ms, p.sphere_cap_preps, p.sphere_cap_ms,
p.horizon_preps, p.horizon_ms, p.rim_strip_preps, p.rim_strip_ms, p.generic_preps,
p.generic_ms, p.generic_chords, p.index_builds, p.index_ms, p.region_builds,
p.region_evaluate_ms, p.region_canonical_ms, p.region_segments_ms,
);
CONTAINMENT_PROFILE.with(|cell| cell.set(ContainmentProfile::default()));
}
fn timed_lane<'a, T>(
cell: &'a std::cell::OnceCell<Result<T, String>>,
build: impl FnOnce() -> Result<T, String>,
record: fn(&mut ContainmentProfile, f64),
) -> Result<&'a T, String> {
lane(cell, || {
let started = containment_profile_started();
let built = build();
let ms = containment_elapsed_ms(started);
containment_profile(|p| record(p, ms));
built
})
}
fn lane<'a, T>(
cell: &'a std::cell::OnceCell<Result<T, String>>,
build: impl FnOnce() -> Result<T, String>,
) -> Result<&'a T, String> {
match cell.get_or_init(build) {
Ok(value) => Ok(value),
Err(error) => Err(error.clone()),
}
}
struct PreparedFaceCache {
entries: Vec<(u64, Vec<f64>, std::rc::Rc<PreparedFace>)>,
}
const PREPARED_FACE_CACHE_ENTRIES: usize = 64;
thread_local! {
static PREPARED_FACES: std::cell::RefCell<PreparedFaceCache> = const {
std::cell::RefCell::new(PreparedFaceCache {
entries: Vec::new(),
})
};
}
fn coedge_slots(face: &FaceRecord) -> Vec<usize> {
let ids: Vec<u64> = face.loops.iter().flat_map(|record| record.coedges.iter().map(|coedge| coedge.edge_id)).collect();
ids.iter().enumerate().map(|(at, id)| ids[..at].iter().position(|earlier| earlier == id).unwrap_or(at)).collect()
}
fn walk_face_signature<F: FnMut(f64) -> bool>(face: &FaceRecord, switches: f64, visit: &mut F) -> bool {
let slots = coedge_slots(face);
let mut flat = 0usize;
let surface = &face.surface;
let point = |visit: &mut F, p: &crate::Vec4| visit(p.x) && visit(p.y) && visit(p.z) && visit(p.w);
if !(visit(switches)
&& visit(if face.same_sense { 1.0 } else { 0.0 })
&& visit(surface.degree_u as f64)
&& visit(surface.degree_v as f64)
&& visit(surface.knots_u.len() as f64))
{
return false;
}
if !(surface.knots_u.iter().all(|&knot| visit(knot)) && visit(surface.knots_v.len() as f64)) {
return false;
}
if !(surface.knots_v.iter().all(|&knot| visit(knot)) && visit(surface.control_points.len() as f64)) {
return false;
}
for row in &surface.control_points {
if !(visit(row.len() as f64) && row.iter().all(|p| point(visit, p))) {
return false;
}
}
if !visit(face.loops.len() as f64) {
return false;
}
for loop_record in &face.loops {
if !visit(loop_record.coedges.len() as f64) {
return false;
}
for coedge in &loop_record.coedges {
let curve = &coedge.pcurve;
let slot = slots[flat];
flat += 1;
if !(visit(slot as f64)
&& visit(if coedge.forward { 1.0 } else { 0.0 })
&& visit(curve.degree as f64)
&& visit(curve.knots.len() as f64))
{
return false;
}
if !(curve.knots.iter().all(|&knot| visit(knot)) && visit(curve.control_points.len() as f64)) {
return false;
}
if !curve.control_points.iter().all(|p| point(visit, p)) {
return false;
}
}
}
true
}
fn face_key_hash(face: &FaceRecord, switches: f64) -> u64 {
use std::hash::Hasher;
let slots = coedge_slots(face);
let mut flat = 0usize;
let mut hasher = rustc_hash::FxHasher::default();
let mut write = |value: f64| hasher.write_u64(value.to_bits());
let surface = &face.surface;
write(switches);
write(if face.same_sense { 1.0 } else { 0.0 });
write(surface.degree_u as f64);
write(surface.degree_v as f64);
write(surface.knots_u.len() as f64);
write(surface.knots_v.len() as f64);
write(surface.control_points.len() as f64);
let corners = [surface.control_points.first().and_then(|row| row.first()), surface.control_points.last().and_then(|row| row.last())];
for p in corners.into_iter().flatten() {
[p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
}
write(face.loops.len() as f64);
for loop_record in &face.loops {
write(loop_record.coedges.len() as f64);
for coedge in &loop_record.coedges {
let curve = &coedge.pcurve;
write(slots[flat] as f64);
flat += 1;
write(if coedge.forward { 1.0 } else { 0.0 });
write(curve.degree as f64);
write(curve.knots.len() as f64);
write(curve.control_points.len() as f64);
for knot in [curve.knots.first(), curve.knots.last()].into_iter().flatten() {
write(*knot);
}
for p in [curve.control_points.first(), curve.control_points.last()].into_iter().flatten() {
[p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
}
}
}
hasher.finish()
}
fn cached_entry(
entries: &[(u64, Vec<f64>, std::rc::Rc<PreparedFace>)],
face: &FaceRecord,
switches: f64,
hash: u64,
) -> Option<usize> {
entries.iter().position(|(entry_hash, signature, _)| {
if *entry_hash != hash {
return false;
}
let mut at = 0;
let walked = walk_face_signature(face, switches, &mut |value: f64| {
let same = signature
.get(at)
.is_some_and(|stored| stored.to_bits() == value.to_bits() && !value.is_nan());
at += 1;
same
});
walked && at == signature.len()
})
}
pub(crate) fn forget_prepared_faces() {
PREPARED_FACES.with(|cache| cache.borrow_mut().entries.clear());
}
fn cached_prepared_face(face: &FaceRecord) -> Option<std::rc::Rc<PreparedFace>> {
PREPARED_FACES.with(|cache| {
let PreparedFaceCache { entries } = &*cache.borrow();
let switches = lane_switches();
let hash = face_key_hash(face, switches);
cached_entry(entries, face, switches, hash).map(|index| entries[index].2.clone())
})
}
fn prepared_face(face: &FaceRecord) -> std::rc::Rc<PreparedFace> {
PREPARED_FACES.with(|cache| {
let PreparedFaceCache { entries } = &mut *cache.borrow_mut();
let switches = lane_switches();
let hash = face_key_hash(face, switches);
if let Some(index) = cached_entry(entries, face, switches, hash) {
if index != 0 {
let entry = entries.remove(index);
entries.insert(0, entry);
}
return entries[0].2.clone();
}
let mut signature = Vec::new();
walk_face_signature(face, switches, &mut |value: f64| {
signature.push(value);
true
});
let prepared = std::rc::Rc::new(PreparedFace::new());
entries.insert(0, (hash, signature, prepared.clone()));
entries.truncate(PREPARED_FACE_CACHE_ENTRIES);
containment_profile(|p| p.cache_misses += 1);
prepared
})
}
fn prepare_seam_band(face: &FaceRecord) -> Result<SeamBandPrep, String> {
if face.surface.closed_directions()? != (true, true) {
return Ok(SeamBandPrep::NotApplicable);
}
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(SeamBandPrep::AlwaysInside);
}
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());
let mut retained: Vec<usize> = Vec::with_capacity(face.loops.len());
for (loop_index, loop_record) in face.loops.iter().enumerate() {
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 = trim_station(d0, d1, k, samples);
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 (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span {
continue;
}
retained.push(loop_index);
loops_uv.push(points);
}
if loops_uv.len() == 1 && switch!(SEAM_BAND_MERGED_SWITCH, true) {
let coedges = &face.loops[retained[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 = trim_station(d0, d1, k, samples);
let p = coedge.pcurve.evaluate(t)?;
polygon.push(Vec2 {
x: p.x + offsets[coedge_index][0],
y: p.y + offsets[coedge_index][1],
});
}
}
return Ok(SeamBandPrep::Merged { polygon, u_span, v_span });
}
}
if loops_uv.len() != 2 {
return Ok(SeamBandPrep::Declined);
}
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(SeamBandPrep::Analyzed { polygon });
}
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;
return Ok(SeamBandPrep::Rings {
p_is_u,
q_lo: *q_lo,
q_hi: *q_hi,
complement,
});
}
Ok(SeamBandPrep::Declined)
}
fn seam_band_point_in_face(
prepared: &SeamBandPrep,
point: Vec2,
tolerance: f64,
) -> Option<PolygonClass> {
match prepared {
SeamBandPrep::NotApplicable | SeamBandPrep::Declined => None,
SeamBandPrep::AlwaysInside => Some(PolygonClass::Inside),
SeamBandPrep::Merged { polygon, u_span, v_span } => {
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 => {}
}
}
}
Some(best)
}
SeamBandPrep::Analyzed { polygon } => Some(point_in_polygon(point, polygon, tolerance)),
SeamBandPrep::Rings { p_is_u, q_lo, q_hi, complement } => {
let q = if *p_is_u { point.y } else { point.x };
if (q - q_lo).abs() <= tolerance || (q - q_hi).abs() <= tolerance {
return Some(PolygonClass::Boundary);
}
let between = q > *q_lo && q < *q_hi;
Some(if between != *complement {
PolygonClass::Inside
} else {
PolygonClass::Outside
})
}
}
}
fn prepare_wrapped_horizon(face: &FaceRecord, prepared: &PreparedFace) -> Result<Option<HorizonPrep>, String> {
if face.surface.closed_directions()? != (true, false) || face.loops.is_empty() {
return Ok(None);
}
if !switch!(HORIZON_CONTAINMENT_SWITCH, true) {
return Ok(None);
}
if face.loops.len() <= 2 && !switch!(HORIZON_SINGLE_LOOP_SWITCH, true) {
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_index in 0..face.loops.len() {
let points = loop_samples(prepared, face, loop_index)?;
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);
}
Ok(Some(HorizonPrep {
loops,
u_period,
index: std::cell::OnceCell::new(),
}))
}
fn wrapped_horizon_point_in_face(
face: &FaceRecord,
prepared: &HorizonPrep,
point: Vec2,
tolerance: f64,
scan: ChordScan,
) -> Option<PolygonClass> {
let HorizonPrep { loops, u_period, index } = prepared;
let u_period = *u_period;
let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
if !horizon_cross_frame() {
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 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 best;
}
let images = [-u_period, 0.0, u_period].map(|shift| Vec2 {
x: point.x + shift,
y: point.y,
});
let chords: usize = loops.iter().map(Vec::len).sum();
let indexed = (scan.indexes(chords) && images.iter().all(|image| in_index_range(*image)))
.then(|| index.get_or_init(|| ChordIndex::build(loops.as_slice())).as_ref())
.flatten();
let mut crossings = 0usize;
if let Some(index) = indexed {
let loops = loops.as_slice();
if images.iter().any(|image| index.any_within(loops, *image, tolerance)) {
return Some(PolygonClass::Boundary);
}
let mut odd = vec![false; loops.len()];
for image in images {
index.flip_crossings(loops, image, &mut odd);
}
crossings = odd.into_iter().filter(|&parity| parity).count();
} else {
for polygon in loops {
let mut image_hits = 0usize;
for image in images {
match point_in_polygon(image, polygon, tolerance) {
PolygonClass::Boundary => return 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
);
}
Some(class)
}
fn prepare_winding_sphere_cap(face: &FaceRecord, prepared: &PreparedFace) -> Result<Option<SphereCapPrep>, 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_index in 0..face.loops.len() {
let points = loop_samples(prepared, face, loop_index)?;
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);
}
Ok(Some(SphereCapPrep {
rim: rim.points.clone(),
pole_v,
period,
}))
}
fn winding_sphere_cap_point_in_face(
prepared: &SphereCapPrep,
point: Vec2,
tolerance: f64,
) -> Option<PolygonClass> {
let SphereCapPrep { rim, pole_v, period } = prepared;
let (pole_v, period) = (*pole_v, *period);
let window_start = rim[0].x.min(rim[rim.len() - 1].x);
let query_u = window_start + (point.x - window_start).rem_euclid(period);
let mut crossings = Vec::new();
for pair in rim.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 Some(PolygonClass::Boundary);
}
}
}
let rim_v = crossings
.into_iter()
.min_by(|a, b| (a - point.y).abs().total_cmp(&(b - point.y).abs()))?;
let inside = if pole_v < rim_v {
point.y <= rim_v + tolerance
} else {
point.y >= rim_v - tolerance
};
Some(if inside {
PolygonClass::Inside
} else {
PolygonClass::Outside
})
}
fn prepare_covering_rim_strip(face: &FaceRecord) -> Result<Option<RimStripPrep>, String> {
if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
return Ok(None);
}
if !switch!(COVERING_RIM_STRIP_SWITCH, true) {
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 = trim_sample_count(curve);
for index in 0..=sample_count {
let parameter = trim_station(start, end, index, sample_count);
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);
}
Ok(Some(RimStripPrep {
lower: lower.points.clone(),
upper: upper.points.clone(),
period,
}))
}
fn covering_rim_strip_point_in_face(
prepared: &RimStripPrep,
point: Vec2,
tolerance: f64,
) -> Option<PolygonClass> {
let RimStripPrep { lower, upper, period } = prepared;
let period = *period;
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.windows(2) {
if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
return Some(PolygonClass::Boundary);
}
}
}
}
let mut crossings = 0usize;
for rim in [lower, upper] {
let window_base = rim[0].x.min(rim[rim.len() - 1].x);
let x = window_base + (point.x - window_base).rem_euclid(period);
for pair in rim.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;
}
}
}
}
Some(if crossings % 2 == 1 {
PolygonClass::Inside
} else {
PolygonClass::Outside
})
}
fn prepare_generic(face: &FaceRecord, prepared: &PreparedFace) -> Result<GenericPrep, String> {
let slots = coedge_slots(face);
let mut flat = 0usize;
let mut loops: Vec<LoopSamples> = Vec::with_capacity(face.loops.len());
let mut segments: Vec<Vec<SegmentPrep>> = Vec::with_capacity(face.loops.len());
let mut chord_count = 0;
for (loop_index, loop_record) in face.loops.iter().enumerate() {
let polygon = loop_samples(prepared, face, loop_index)?;
let mut chords = Vec::with_capacity(polygon.len());
let mut coedges = Vec::with_capacity(loop_record.coedges.len());
let mut first = 0;
for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
let curve = &coedge.pcurve;
let [start, end] = curve.domain()?;
let sample_count = trim_sample_count(curve);
for index in 0..sample_count {
let parameter = trim_station(start, end, index, sample_count);
let parameter_end = if index + 1 < sample_count {
trim_station(start, end, index + 1, sample_count)
} else {
end
};
chords.push(SegmentPrep {
coedge_index,
parameter_start: parameter,
parameter_end,
});
}
coedges.push((slots[flat] as u64, first, sample_count));
flat += 1;
first += sample_count;
}
debug_assert_eq!(first, polygon.len());
chord_count += chords.len();
loops.push(LoopSamples { polygon, coedges });
segments.push(chords);
}
containment_profile(|p| p.generic_chords += chord_count as u64);
Ok(GenericPrep {
loops,
segments,
chord_count,
index: std::cell::OnceCell::new(),
})
}
struct LoopSamples {
polygon: std::rc::Rc<Vec<Vec2>>,
coedges: Vec<(u64, usize, usize)>,
}
fn sphere_chart_point_in_face(
face: &FaceRecord,
point: Vec2,
prepared: &PreparedFace,
sampled: &[LoopSamples],
) -> Result<Option<PolygonClass>, String> {
let Some(atlas) = sphere_chart_atlas(face) else {
return Ok(None);
};
let region = lane(&prepared.sphere_region, || build_sphere_region(face, &atlas, sampled))?;
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
}))
}
fn build_sphere_region(
face: &FaceRecord,
atlas: &crate::sphere_chart::SphereAtlas,
sampled: &[LoopSamples],
) -> Result<crate::sphere_chart::SphericalRegion, String> {
let mut uses: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
for slot in coedge_slots(face) {
*uses.entry(slot as u64).or_insert(0) += 1;
}
let outward_face_normal =
face.same_sense == atlas.parameterization_is_outward(&face.surface)?;
let evaluate_started = containment_profile_started();
let mut points: Vec<Vec3> = Vec::new();
let mut spans: Vec<(usize, usize)> = Vec::new();
for samples in sampled {
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;
}
let start = points.len();
for offset in 0..=span {
let uv = samples.polygon[(first + offset) % count];
points.push(face.surface.evaluate(uv.x, uv.y)?);
}
spans.push((start, points.len()));
}
}
let evaluate_ms = containment_elapsed_ms(evaluate_started);
let canonical_started = containment_profile_started();
crate::sphere_chart::canonicalize_points(&mut points, 1e-9);
let canonical_ms = containment_elapsed_ms(canonical_started);
let segments_started = containment_profile_started();
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,
);
let segments_ms = containment_elapsed_ms(segments_started);
containment_profile(|p| {
p.region_builds += 1;
p.region_evaluate_ms += evaluate_ms;
p.region_canonical_ms += canonical_ms;
p.region_segments_ms += segments_ms;
});
Ok(region)
}
fn sphere_chart_atlas(face: &FaceRecord) -> Option<crate::sphere_chart::SphereAtlas> {
if !sphere_chart_trim_enabled() {
return None;
}
crate::sphere_chart::SphereAtlas::of_surface(&face.surface)
}
fn containment_trace_enabled() -> bool {
debug_flag!("BREP_DEBUG_CONTAINMENT")
}
fn trace_lane(face: &FaceRecord, point: Vec2, lane: &str, class: PolygonClass) {
if containment_trace_enabled() {
eprintln!(
"containment face={} uv=({:.8},{:.8}) lane={lane} -> {class:?}",
face.id, point.x, point.y
);
}
}
enum Lane<'a> {
SeamBand(&'a SeamBandPrep),
SphereCap(&'a SphereCapPrep),
Horizon(&'a HorizonPrep),
RimStrip(&'a RimStripPrep),
Generic,
}
fn lane_from<'a>(
face: &FaceRecord,
prepared: &'a PreparedFace,
from: usize,
) -> Result<(usize, Lane<'a>), String> {
if from == 0 {
let band = timed_lane(&prepared.seam_band, || prepare_seam_band(face), |p, ms| {
p.seam_band_preps += 1;
p.seam_band_ms += ms;
})?;
if !matches!(band, SeamBandPrep::NotApplicable | SeamBandPrep::Declined) {
return Ok((0, Lane::SeamBand(band)));
}
}
if from <= 1 {
if let Some(cap) = timed_lane(&prepared.sphere_cap, || prepare_winding_sphere_cap(face, prepared), |p, ms| {
p.sphere_cap_preps += 1;
p.sphere_cap_ms += ms;
})? {
return Ok((1, Lane::SphereCap(cap)));
}
}
if from <= 2 {
if let Some(horizon) = timed_lane(&prepared.horizon, || prepare_wrapped_horizon(face, prepared), |p, ms| {
p.horizon_preps += 1;
p.horizon_ms += ms;
})? {
return Ok((2, Lane::Horizon(horizon)));
}
}
if from <= 3 {
if let Some(strip) = timed_lane(&prepared.rim_strip, || prepare_covering_rim_strip(face), |p, ms| {
p.rim_strip_preps += 1;
p.rim_strip_ms += ms;
})? {
return Ok((3, Lane::RimStrip(strip)));
}
}
Ok((4, Lane::Generic))
}
pub fn containment_lane(face: &FaceRecord) -> Result<ContainmentLane, String> {
let prepared = cached_prepared_face(face).unwrap_or_else(|| std::rc::Rc::new(PreparedFace::new()));
Ok(match lane_from(face, &prepared, 0)?.1 {
Lane::SeamBand(_) => ContainmentLane::SeamBand,
Lane::SphereCap(_) => ContainmentLane::SphereCap,
Lane::Horizon(horizon) => ContainmentLane::WrappedHorizon {
loops: horizon.loops.clone(),
u_period: horizon.u_period,
cross_frame: horizon_cross_frame(),
},
Lane::RimStrip(_) => ContainmentLane::CoveringRimStrip,
Lane::Generic => ContainmentLane::Generic {
sphere_chart: sphere_chart_atlas(face).is_some(),
},
})
}
#[derive(Clone, Debug)]
pub enum ContainmentLane {
SeamBand,
SphereCap,
WrappedHorizon {
loops: Vec<Vec<Vec2>>,
u_period: f64,
cross_frame: bool,
},
CoveringRimStrip,
Generic {
sphere_chart: bool,
},
}
pub fn parameter_point_in_face(
face: &FaceRecord,
point: Vec2,
tolerance: f64,
) -> Result<PolygonClass, String> {
point_in_face_scanned(face, point, tolerance, ChordScan::Auto)
}
#[doc(hidden)]
pub fn parameter_point_in_face_scan(
face: &FaceRecord,
point: Vec2,
tolerance: f64,
indexed: bool,
) -> Result<PolygonClass, String> {
let scan = if indexed {
ChordScan::Indexed
} else {
ChordScan::Linear
};
point_in_face_scanned(face, point, tolerance, scan)
}
fn point_in_face_scanned(
face: &FaceRecord,
point: Vec2,
tolerance: f64,
scan: ChordScan,
) -> Result<PolygonClass, String> {
let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::CLASSIFY);
let prepared = prepared_face(face);
let mut from = 0;
loop {
let (position, lane) = lane_from(face, &prepared, from)?;
let answer = match lane {
Lane::SeamBand(band) => {
seam_band_point_in_face(band, point, tolerance).map(|class| ("seam_band", class))
}
Lane::SphereCap(cap) => winding_sphere_cap_point_in_face(cap, point, tolerance)
.map(|class| ("winding_sphere_cap", class)),
Lane::Horizon(horizon) => wrapped_horizon_point_in_face(face, horizon, point, tolerance, scan)
.map(|class| ("wrapped_horizon", class)),
Lane::RimStrip(strip) => covering_rim_strip_point_in_face(strip, point, tolerance)
.map(|class| ("covering_rim_strip", class)),
Lane::Generic => break,
};
if let Some((name, class)) = answer {
trace_lane(face, point, name, class);
return Ok(class);
}
from = position + 1;
}
let generic = timed_lane(&prepared.generic, || prepare_generic(face, &prepared), |p, ms| {
p.generic_preps += 1;
p.generic_ms += ms;
})?;
let (crossings, boundary, nearest) = generic_scan(generic, point, tolerance, scan);
if boundary {
trace_lane(face, point, "polygon_boundary", PolygonClass::Boundary);
return Ok(PolygonClass::Boundary);
}
let parity = if crossings % 2 == 1 {
PolygonClass::Inside
} else {
PolygonClass::Outside
};
let chord = nearest.map(|(loop_index, index, distance)| {
let polygon = &generic.loops[loop_index].polygon;
let prep = generic.segments[loop_index][index];
(
polygon[index],
polygon[(index + 1) % polygon.len()],
&face.loops[loop_index].coedges[prep.coedge_index].pcurve,
prep,
distance,
)
});
if containment_trace_enabled() {
let (distance, length) = match chord.as_ref() {
Some((start, end, _, _, distance)) => (*distance, end.sub(*start).length()),
None => (f64::NAN, f64::NAN),
};
eprintln!(
"containment face={} uv=({:.8},{:.8}) crossings={crossings} parity={parity:?} \
nearest distance={distance:.3e} segment length={length:.3e}",
face.id, point.x, point.y
);
}
let parity_decides = match chord.as_ref() {
None => true,
Some((start, end, _, _, distance)) => {
let length = end.sub(*start).length();
*distance > length || length <= 0.0
}
};
if parity_decides {
if let Some(class) = sphere_chart_point_in_face(face, point, &prepared, &generic.loops)? {
trace_lane(face, point, "sphere_chart", class);
return Ok(class);
}
}
let Some((segment_start, segment_end, curve, prep, distance)) = chord else {
trace_lane(face, point, "parity (no segment)", parity);
return Ok(parity);
};
let segment_length = segment_end.sub(segment_start).length();
if distance > segment_length || segment_length <= 0.0 {
trace_lane(face, point, "parity (far from every segment)", parity);
return Ok(parity);
}
let chord_parameter = prep.parameter_start
+ (prep.parameter_end - prep.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] = curve.domain()?;
let small = crate::curve::small_curve_derivatives_enabled();
let read = |parameter: f64, count: usize| -> Result<[Vec3; 3], String> {
if small {
return curve.derivatives_small(parameter, count);
}
let heap = curve.derivatives(parameter, count)?;
let mut grid = [Vec3::default(); 3];
grid[..heap.len()].copy_from_slice(&heap);
Ok(grid)
};
use crate::projection::{curve_foot_newton, CurveFootPolicy, FootDomain, FootMode};
let (parameter, _) = curve_foot_newton(curve, Vec3::new(point.x, point.y, 0.0),
chord_parameter, [domain_start, domain_end], CurveFootPolicy {
domain: FootDomain::Clamp,
iterations: 12,
slope_guard: 1e-30,
step_limit: f64::INFINITY,
parameter_tolerance: 1e-14 * (domain_end - domain_start + 1.0),
mode: FootMode::Planar,
})?;
let margin = 1e-9 * (domain_end - domain_start);
if parameter > domain_start + margin && parameter < domain_end - margin {
let derivatives = read(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 {
trace_lane(face, point, "nearest curve (on it)", PolygonClass::Boundary);
return Ok(PolygonClass::Boundary);
}
let cross = tangent.x * offset.y - tangent.y * offset.x;
if cross.abs() > 1e-30 {
let class = if (cross > 0.0) == face.same_sense {
PolygonClass::Inside
} else {
PolygonClass::Outside
};
trace_lane(face, point, "nearest curve (side)", class);
return Ok(class);
}
}
trace_lane(face, point, "parity (nearest curve declined)", parity);
Ok(parity)
}