aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-terrain/src/contour.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-18 07:59:36 +0300
committerValentin Popov <valentin@popov.link>2026-10-18 07:59:42 +0300
commitc8543d7365329fa97d31ac9b0114a1ee9095dd74 (patch)
tree3cb6c12749b659b99a6b8a2c6efb68aa79ff4626 /crates/fparkan-terrain/src/contour.rs
parent737e429cb109446ec008c4dcf91d4f04faccd64a (diff)
downloadfparkan-c8543d7365329fa97d31ac9b0114a1ee9095dd74.tar.xz
fparkan-c8543d7365329fa97d31ac9b0114a1ee9095dd74.zip
feat(terrain): build BAS basement foundations
Diffstat (limited to 'crates/fparkan-terrain/src/contour.rs')
-rw-r--r--crates/fparkan-terrain/src/contour.rs809
1 files changed, 809 insertions, 0 deletions
diff --git a/crates/fparkan-terrain/src/contour.rs b/crates/fparkan-terrain/src/contour.rs
new file mode 100644
index 0000000..de5077a
--- /dev/null
+++ b/crates/fparkan-terrain/src/contour.rs
@@ -0,0 +1,809 @@
+/// One triangle fragment retained after subtracting a building contour.
+/// `source_triangle` indexes the triangle array passed to
+/// [`subtract_contour`].
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub(crate) struct TerrainTriangleFragment {
+ /// Source triangle index, for preserving its material and vertex channels.
+ pub source_triangle: usize,
+ /// Fragment triangle vertices in the input coordinate space.
+ pub positions: [[f32; 3]; 3],
+}
+
+/// Error returned when a terrain contour cannot be subtracted safely.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub(crate) enum TerrainContourError {
+ /// The contour is non-finite, degenerate, or self-intersecting.
+ UnsupportedContour,
+ /// A source triangle has non-finite coordinates or a degenerate XY plane.
+ InvalidSourceTriangle {
+ /// Index of the source triangle.
+ triangle: usize,
+ },
+}
+
+impl std::fmt::Display for TerrainContourError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::UnsupportedContour => write!(
+ f,
+ "terrain contour must be finite, simple, and non-degenerate"
+ ),
+ Self::InvalidSourceTriangle { triangle } => {
+ write!(
+ f,
+ "terrain source triangle {triangle} is not a finite XY surface"
+ )
+ }
+ }
+ }
+}
+
+impl std::error::Error for TerrainContourError {}
+
+/// Subtracts a finite simple contour using the native per-source-face point set:
+/// the three source vertices plus the actual contour segments clipped to that
+/// source triangle. It does not extend cuts across the triangle or triangulate
+/// the contour into unrelated internal edges.
+///
+/// # Errors
+///
+/// Returns [`TerrainContourError::UnsupportedContour`] for malformed or
+/// self-intersecting contours, and
+/// [`TerrainContourError::InvalidSourceTriangle`] when a source face cannot be
+/// triangulated or lifted onto its original plane.
+pub(crate) fn subtract_contour(
+ triangles: &[[[f32; 3]; 3]],
+ contour: &[[f32; 2]],
+) -> Result<Vec<TerrainTriangleFragment>, TerrainContourError> {
+ let ring = normalize_ring(contour)?;
+ let contour_min: [f64; 2] = std::array::from_fn(|axis| {
+ ring.iter()
+ .map(|point| point[axis])
+ .fold(f64::INFINITY, f64::min)
+ });
+ let contour_max: [f64; 2] = std::array::from_fn(|axis| {
+ ring.iter()
+ .map(|point| point[axis])
+ .fold(f64::NEG_INFINITY, f64::max)
+ });
+ let mut output = Vec::new();
+
+ for (source_triangle, source) in triangles.iter().copied().enumerate() {
+ if !source.iter().flatten().all(|value| value.is_finite()) {
+ return Err(TerrainContourError::InvalidSourceTriangle {
+ triangle: source_triangle,
+ });
+ }
+ let source_xy = source.map(|point| [f64::from(point[0]), f64::from(point[1])]);
+ let source_area = cross2(source_xy[0], source_xy[1], source_xy[2]);
+ if source_area.abs() <= 1.0e-10 {
+ return Err(TerrainContourError::InvalidSourceTriangle {
+ triangle: source_triangle,
+ });
+ }
+ let source_min: [f64; 2] = std::array::from_fn(|axis| {
+ source_xy
+ .iter()
+ .map(|point| point[axis])
+ .fold(f64::INFINITY, f64::min)
+ });
+ let source_max: [f64; 2] = std::array::from_fn(|axis| {
+ source_xy
+ .iter()
+ .map(|point| point[axis])
+ .fold(f64::NEG_INFINITY, f64::max)
+ });
+ if (0..2).any(|axis| {
+ source_max[axis] < contour_min[axis] || contour_max[axis] < source_min[axis]
+ }) {
+ output.push(TerrainTriangleFragment {
+ source_triangle,
+ positions: source,
+ });
+ continue;
+ }
+
+ let mut vertices = source_xy.to_vec();
+ let initial = if source_area > 0.0 {
+ [0, 1, 2]
+ } else {
+ [0, 2, 1]
+ };
+ let mut triangulation = vec![initial];
+ let mut clipped_segments = Vec::new();
+ for index in 0..ring.len() {
+ let first = ring[index];
+ let second = ring[(index + 1) % ring.len()];
+ if let Some((start, end)) = clip_segment_to_triangle(first, second, source_xy) {
+ let start_id = insert_unique_point(&mut vertices, start);
+ let end_id = insert_unique_point(&mut vertices, end);
+ if start_id != end_id {
+ clipped_segments.push((start_id, end_id));
+ }
+ }
+ }
+
+ if clipped_segments.is_empty() {
+ let center = [
+ (source_xy[0][0] + source_xy[1][0] + source_xy[2][0]) / 3.0,
+ (source_xy[0][1] + source_xy[1][1] + source_xy[2][1]) / 3.0,
+ ];
+ if !point_in_ring(center, &ring) {
+ output.push(TerrainTriangleFragment {
+ source_triangle,
+ positions: source,
+ });
+ }
+ continue;
+ }
+
+ for point_id in 3..vertices.len() {
+ insert_triangulation_point(point_id, &vertices, &mut triangulation).map_err(|()| {
+ TerrainContourError::InvalidSourceTriangle {
+ triangle: source_triangle,
+ }
+ })?;
+ }
+
+ let mut constraints = source_boundary_constraints(&vertices, source_xy);
+ for (first, second) in clipped_segments {
+ let mut on_segment = vertices
+ .iter()
+ .copied()
+ .enumerate()
+ .filter_map(|(index, point)| {
+ point_on_segment(point, vertices[first], vertices[second]).then_some((
+ segment_parameter(point, vertices[first], vertices[second]),
+ index,
+ ))
+ })
+ .collect::<Vec<_>>();
+ on_segment.sort_by(|left, right| left.0.total_cmp(&right.0));
+ on_segment.dedup_by_key(|(_, index)| *index);
+ for pair in on_segment.windows(2) {
+ let edge = canonical_edge(pair[0].1, pair[1].1);
+ if edge.0 == edge.1 {
+ continue;
+ }
+ recover_constraint(edge, &mut triangulation, &vertices, &constraints).map_err(
+ |()| TerrainContourError::InvalidSourceTriangle {
+ triangle: source_triangle,
+ },
+ )?;
+ constraints.insert(edge);
+ }
+ }
+ legalize_constrained_delaunay(&mut triangulation, &vertices, &constraints).map_err(
+ |()| TerrainContourError::InvalidSourceTriangle {
+ triangle: source_triangle,
+ },
+ )?;
+
+ for triangle in triangulation {
+ let center = triangle.map(|index| vertices[index]);
+ let center = [
+ (center[0][0] + center[1][0] + center[2][0]) / 3.0,
+ (center[0][1] + center[1][1] + center[2][1]) / 3.0,
+ ];
+ if point_in_ring(center, &ring) {
+ continue;
+ }
+ let mut positions: [[f32; 3]; 3] = triangle
+ .map(|index| lift_to_source_plane(vertices[index], source))
+ .into_iter()
+ .collect::<Option<Vec<_>>>()
+ .and_then(|points| points.try_into().ok())
+ .ok_or(TerrainContourError::InvalidSourceTriangle {
+ triangle: source_triangle,
+ })?;
+ if source_area < 0.0 {
+ positions.swap(1, 2);
+ }
+ output.push(TerrainTriangleFragment {
+ source_triangle,
+ positions,
+ });
+ }
+ }
+ Ok(output)
+}
+
+fn normalize_ring(contour: &[[f32; 2]]) -> Result<Vec<[f64; 2]>, TerrainContourError> {
+ let mut ring = contour
+ .iter()
+ .map(|point| [f64::from(point[0]), f64::from(point[1])])
+ .collect::<Vec<_>>();
+ if ring.iter().flatten().any(|value| !value.is_finite()) {
+ return Err(TerrainContourError::UnsupportedContour);
+ }
+ if ring.len() > 1 && distance_squared(ring[0], *ring.last().expect("nonempty")) <= 1.0e-16 {
+ ring.pop();
+ }
+ let mut normalized = Vec::with_capacity(ring.len());
+ for point in ring {
+ if normalized
+ .last()
+ .is_none_or(|previous| distance_squared(*previous, point) > 1.0e-16)
+ {
+ normalized.push(point);
+ }
+ }
+ if normalized.len() > 1
+ && distance_squared(normalized[0], *normalized.last().expect("nonempty")) <= 1.0e-16
+ {
+ normalized.pop();
+ }
+ if normalized.len() < 3 {
+ return Err(TerrainContourError::UnsupportedContour);
+ }
+ let signed_area = polygon_area(&normalized);
+ if !signed_area.is_finite() || signed_area.abs() <= 1.0e-10 {
+ return Err(TerrainContourError::UnsupportedContour);
+ }
+ for first in 0..normalized.len() {
+ let first_next = (first + 1) % normalized.len();
+ for second in (first + 1)..normalized.len() {
+ let second_next = (second + 1) % normalized.len();
+ if first_next == second || second_next == first {
+ continue;
+ }
+ if segments_intersect(
+ normalized[first],
+ normalized[first_next],
+ normalized[second],
+ normalized[second_next],
+ ) {
+ return Err(TerrainContourError::UnsupportedContour);
+ }
+ }
+ }
+ if signed_area < 0.0 {
+ normalized.reverse();
+ }
+ Ok(normalized)
+}
+
+fn clip_segment_to_triangle(
+ first: [f64; 2],
+ second: [f64; 2],
+ triangle: [[f64; 2]; 3],
+) -> Option<([f64; 2], [f64; 2])> {
+ let mut triangle = triangle;
+ if cross2(triangle[0], triangle[1], triangle[2]) < 0.0 {
+ triangle.swap(1, 2);
+ }
+ let mut start = 0.0_f64;
+ let mut end = 1.0_f64;
+ for edge in 0..3 {
+ let edge_first = triangle[edge];
+ let edge_second = triangle[(edge + 1) % 3];
+ let at_start = cross2(edge_first, edge_second, first);
+ let at_end = cross2(edge_first, edge_second, second);
+ let change = at_end - at_start;
+ if change.abs() <= 1.0e-14 {
+ if at_start < -1.0e-10 {
+ return None;
+ }
+ continue;
+ }
+ let crossing = -at_start / change;
+ if change > 0.0 {
+ start = start.max(crossing);
+ } else {
+ end = end.min(crossing);
+ }
+ if start > end + 1.0e-12 {
+ return None;
+ }
+ }
+ start = start.clamp(0.0, 1.0);
+ end = end.clamp(0.0, 1.0);
+ if end - start <= 1.0e-12 {
+ return None;
+ }
+ Some((
+ interpolate(first, second, start),
+ interpolate(first, second, end),
+ ))
+}
+
+fn insert_unique_point(vertices: &mut Vec<[f64; 2]>, point: [f64; 2]) -> usize {
+ // Terrain positions are f32 throughout the native data and renderer. Make
+ // clipped crossings use that same representable coordinate before building
+ // the local CDT, so distinct f64 intersections cannot collapse to one XY
+ // vertex only after fragment emission.
+ let point = [f64::from(point[0] as f32), f64::from(point[1] as f32)];
+ if let Some(index) = vertices
+ .iter()
+ .position(|candidate| distance_squared(*candidate, point) <= 1.0e-18)
+ {
+ return index;
+ }
+ let index = vertices.len();
+ vertices.push(point);
+ index
+}
+
+fn insert_triangulation_point(
+ point_id: usize,
+ vertices: &[[f64; 2]],
+ triangles: &mut Vec<[usize; 3]>,
+) -> Result<(), ()> {
+ let point = vertices[point_id];
+ let edge = triangulation_edges(triangles)
+ .keys()
+ .copied()
+ .find(|&(first, second)| point_on_open_segment(point, vertices[first], vertices[second]));
+ if let Some(edge) = edge {
+ let mut split = Vec::new();
+ triangles.retain(|triangle| {
+ if triangle_has_edge(*triangle, edge) {
+ let opposite = triangle
+ .iter()
+ .copied()
+ .find(|index| *index != edge.0 && *index != edge.1)
+ .expect("triangle has opposite vertex");
+ split.push(opposite);
+ false
+ } else {
+ true
+ }
+ });
+ if split.is_empty() {
+ return Err(());
+ }
+ for opposite in split {
+ let first = make_ccw([edge.0, point_id, opposite], vertices).ok_or(())?;
+ let second = make_ccw([point_id, edge.1, opposite], vertices).ok_or(())?;
+ triangles.extend([first, second]);
+ }
+ return Ok(());
+ }
+
+ let containing = triangles.iter().position(|triangle| {
+ point_in_triangle(
+ point,
+ vertices[triangle[0]],
+ vertices[triangle[1]],
+ vertices[triangle[2]],
+ )
+ });
+ let Some(containing) = containing else {
+ return Err(());
+ };
+ let triangle = triangles.swap_remove(containing);
+ for edge in 0..3 {
+ let split = make_ccw(
+ [triangle[edge], triangle[(edge + 1) % 3], point_id],
+ vertices,
+ )
+ .ok_or(())?;
+ triangles.push(split);
+ }
+ Ok(())
+}
+
+fn source_boundary_constraints(
+ vertices: &[[f64; 2]],
+ triangle: [[f64; 2]; 3],
+) -> std::collections::BTreeSet<(usize, usize)> {
+ let mut constraints = std::collections::BTreeSet::new();
+ for edge in 0..3 {
+ let first = triangle[edge];
+ let second = triangle[(edge + 1) % 3];
+ let mut points = vertices
+ .iter()
+ .copied()
+ .enumerate()
+ .filter_map(|(index, point)| {
+ point_on_segment(point, first, second)
+ .then_some((segment_parameter(point, first, second), index))
+ })
+ .collect::<Vec<_>>();
+ points.sort_by(|left, right| left.0.total_cmp(&right.0));
+ points.dedup_by_key(|(_, index)| *index);
+ for pair in points.windows(2) {
+ constraints.insert(canonical_edge(pair[0].1, pair[1].1));
+ }
+ }
+ constraints
+}
+
+fn recover_constraint(
+ target: (usize, usize),
+ triangles: &mut [[usize; 3]],
+ vertices: &[[f64; 2]],
+ constraints: &std::collections::BTreeSet<(usize, usize)>,
+) -> Result<(), ()> {
+ let limit = vertices
+ .len()
+ .saturating_mul(vertices.len())
+ .saturating_mul(8)
+ .max(1);
+ for _ in 0..limit {
+ if edge_exists(triangles, target) {
+ return Ok(());
+ }
+ let mut crossed = triangulation_edges(triangles)
+ .into_iter()
+ .filter_map(|(edge, adjacent)| {
+ if adjacent.len() != 2
+ || edge.0 == target.0
+ || edge.0 == target.1
+ || edge.1 == target.0
+ || edge.1 == target.1
+ {
+ return None;
+ }
+ proper_intersection_parameter(
+ vertices[target.0],
+ vertices[target.1],
+ vertices[edge.0],
+ vertices[edge.1],
+ )
+ .map(|parameter| (parameter, edge, adjacent))
+ })
+ .collect::<Vec<_>>();
+ crossed.sort_by(|left, right| left.0.total_cmp(&right.0));
+ let mut flipped = false;
+ for (_, edge, adjacent) in crossed {
+ if constraints.contains(&edge) {
+ return Err(());
+ }
+ let first_index = adjacent[0].0;
+ let second_index = adjacent[1].0;
+ let first_opposite = adjacent[0].1;
+ let second_opposite = adjacent[1].1;
+ if !convex_quadrilateral(
+ vertices[edge.0],
+ vertices[edge.1],
+ vertices[first_opposite],
+ vertices[second_opposite],
+ ) {
+ continue;
+ }
+ let replacement = canonical_edge(first_opposite, second_opposite);
+ if constraints.contains(&replacement)
+ || proper_intersection_parameter(
+ vertices[target.0],
+ vertices[target.1],
+ vertices[replacement.0],
+ vertices[replacement.1],
+ )
+ .is_some()
+ {
+ continue;
+ }
+ let (a, b) = directed_edge(triangles[first_index], edge).ok_or(())?;
+ triangles[first_index] =
+ make_ccw([first_opposite, second_opposite, b], vertices).ok_or(())?;
+ triangles[second_index] =
+ make_ccw([second_opposite, first_opposite, a], vertices).ok_or(())?;
+ flipped = true;
+ break;
+ }
+ if !flipped {
+ return Err(());
+ }
+ }
+ Err(())
+}
+
+fn legalize_constrained_delaunay(
+ triangles: &mut [[usize; 3]],
+ vertices: &[[f64; 2]],
+ constraints: &std::collections::BTreeSet<(usize, usize)>,
+) -> Result<(), ()> {
+ let limit = triangles
+ .len()
+ .saturating_mul(triangles.len())
+ .saturating_mul(8)
+ .max(1);
+ for _ in 0..limit {
+ let edges = triangulation_edges(triangles);
+ let mut flipped = false;
+ for (edge, adjacent) in edges {
+ if adjacent.len() != 2 || constraints.contains(&edge) {
+ continue;
+ }
+ let first_index = adjacent[0].0;
+ let second_index = adjacent[1].0;
+ let c = adjacent[0].1;
+ let d = adjacent[1].1;
+ if !convex_quadrilateral(vertices[edge.0], vertices[edge.1], vertices[c], vertices[d]) {
+ continue;
+ }
+ let (a, b) = directed_edge(triangles[first_index], edge).ok_or(())?;
+ if in_circle(vertices[a], vertices[b], vertices[c], vertices[d]) <= 1.0e-10 {
+ continue;
+ }
+ let replacement = canonical_edge(c, d);
+ if constraints.iter().any(|constraint| {
+ proper_intersection_parameter(
+ vertices[replacement.0],
+ vertices[replacement.1],
+ vertices[constraint.0],
+ vertices[constraint.1],
+ )
+ .is_some()
+ }) {
+ continue;
+ }
+ triangles[first_index] = make_ccw([c, d, b], vertices).ok_or(())?;
+ triangles[second_index] = make_ccw([d, c, a], vertices).ok_or(())?;
+ flipped = true;
+ break;
+ }
+ if !flipped {
+ return Ok(());
+ }
+ }
+ Err(())
+}
+
+fn triangulation_edges(
+ triangles: &[[usize; 3]],
+) -> std::collections::BTreeMap<(usize, usize), Vec<(usize, usize)>> {
+ let mut edges = std::collections::BTreeMap::new();
+ for (triangle_index, triangle) in triangles.iter().copied().enumerate() {
+ for edge in 0..3 {
+ let first = triangle[edge];
+ let second = triangle[(edge + 1) % 3];
+ let opposite = triangle[(edge + 2) % 3];
+ edges
+ .entry(canonical_edge(first, second))
+ .or_insert_with(Vec::new)
+ .push((triangle_index, opposite));
+ }
+ }
+ edges
+}
+
+fn triangle_has_edge(triangle: [usize; 3], edge: (usize, usize)) -> bool {
+ triangle.contains(&edge.0) && triangle.contains(&edge.1)
+}
+
+fn edge_exists(triangles: &[[usize; 3]], edge: (usize, usize)) -> bool {
+ triangles
+ .iter()
+ .copied()
+ .any(|triangle| triangle_has_edge(triangle, edge))
+}
+
+fn directed_edge(triangle: [usize; 3], edge: (usize, usize)) -> Option<(usize, usize)> {
+ (0..3).find_map(|index| {
+ let first = triangle[index];
+ let second = triangle[(index + 1) % 3];
+ (canonical_edge(first, second) == edge).then_some((first, second))
+ })
+}
+
+fn canonical_edge(first: usize, second: usize) -> (usize, usize) {
+ if first < second {
+ (first, second)
+ } else {
+ (second, first)
+ }
+}
+
+fn make_ccw(triangle: [usize; 3], vertices: &[[f64; 2]]) -> Option<[usize; 3]> {
+ let area = cross2(
+ vertices[triangle[0]],
+ vertices[triangle[1]],
+ vertices[triangle[2]],
+ );
+ if !area.is_finite() || area.abs() <= 1.0e-12 {
+ return None;
+ }
+ Some(if area > 0.0 {
+ triangle
+ } else {
+ [triangle[0], triangle[2], triangle[1]]
+ })
+}
+
+fn point_in_triangle(point: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
+ let edges = [(a, b), (b, c), (c, a)];
+ edges.into_iter().all(|(first, second)| {
+ let scale = distance_squared(first, second).sqrt();
+ cross2(first, second, point) >= -linear_tolerance(first, second, point) * scale
+ })
+}
+
+fn point_on_open_segment(point: [f64; 2], first: [f64; 2], second: [f64; 2]) -> bool {
+ if !point_on_segment(point, first, second) {
+ return false;
+ }
+ let t = segment_parameter(point, first, second);
+ t > 1.0e-9 && t < 1.0 - 1.0e-9
+}
+
+fn point_on_segment(point: [f64; 2], first: [f64; 2], second: [f64; 2]) -> bool {
+ let length_squared = distance_squared(first, second);
+ if length_squared <= 1.0e-20 {
+ return distance_squared(point, first) <= 1.0e-18;
+ }
+ let cross = cross2(first, second, point).abs();
+ let dot = (point[0] - first[0]) * (second[0] - first[0])
+ + (point[1] - first[1]) * (second[1] - first[1]);
+ let tolerance = linear_tolerance(first, second, point);
+ let length = length_squared.sqrt();
+ cross <= tolerance * length
+ && dot >= -tolerance * length
+ && dot <= length_squared + tolerance * length
+}
+
+fn linear_tolerance(first: [f64; 2], second: [f64; 2], point: [f64; 2]) -> f64 {
+ let coordinate_scale = first
+ .into_iter()
+ .chain(second)
+ .chain(point)
+ .map(f64::abs)
+ .fold(1.0_f64, f64::max);
+ 2.0 * f64::from(f32::EPSILON) * coordinate_scale
+}
+
+fn segment_parameter(point: [f64; 2], first: [f64; 2], second: [f64; 2]) -> f64 {
+ let delta = [second[0] - first[0], second[1] - first[1]];
+ let length_squared = delta[0] * delta[0] + delta[1] * delta[1];
+ if length_squared <= 1.0e-20 {
+ 0.0
+ } else {
+ ((point[0] - first[0]) * delta[0] + (point[1] - first[1]) * delta[1]) / length_squared
+ }
+}
+
+fn proper_intersection_parameter(
+ first: [f64; 2],
+ second: [f64; 2],
+ edge_first: [f64; 2],
+ edge_second: [f64; 2],
+) -> Option<f64> {
+ let first_delta = [second[0] - first[0], second[1] - first[1]];
+ let edge_delta = [
+ edge_second[0] - edge_first[0],
+ edge_second[1] - edge_first[1],
+ ];
+ let denominator = first_delta[0] * edge_delta[1] - first_delta[1] * edge_delta[0];
+ if denominator.abs() <= 1.0e-12 {
+ return None;
+ }
+ let offset = [edge_first[0] - first[0], edge_first[1] - first[1]];
+ let t = (offset[0] * edge_delta[1] - offset[1] * edge_delta[0]) / denominator;
+ let u = (offset[0] * first_delta[1] - offset[1] * first_delta[0]) / denominator;
+ (t > 1.0e-9 && t < 1.0 - 1.0e-9 && u > 1.0e-9 && u < 1.0 - 1.0e-9).then_some(t)
+}
+
+fn convex_quadrilateral(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool {
+ cross2(a, b, c) * cross2(a, b, d) < -1.0e-12 && cross2(c, d, a) * cross2(c, d, b) < -1.0e-12
+}
+
+fn in_circle(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> f64 {
+ let a = [a[0] - d[0], a[1] - d[1]];
+ let b = [b[0] - d[0], b[1] - d[1]];
+ let c = [c[0] - d[0], c[1] - d[1]];
+ let a2 = a[0] * a[0] + a[1] * a[1];
+ let b2 = b[0] * b[0] + b[1] * b[1];
+ let c2 = c[0] * c[0] + c[1] * c[1];
+ a2 * (b[0] * c[1] - b[1] * c[0]) - b2 * (a[0] * c[1] - a[1] * c[0])
+ + c2 * (a[0] * b[1] - a[1] * b[0])
+}
+
+fn segments_intersect(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool {
+ let ab_c = cross2(a, b, c);
+ let ab_d = cross2(a, b, d);
+ let cd_a = cross2(c, d, a);
+ let cd_b = cross2(c, d, b);
+ let eps = 1.0e-8;
+ (ab_c.abs() <= eps && point_on_segment(c, a, b))
+ || (ab_d.abs() <= eps && point_on_segment(d, a, b))
+ || (cd_a.abs() <= eps && point_on_segment(a, c, d))
+ || (cd_b.abs() <= eps && point_on_segment(b, c, d))
+ || (ab_c * ab_d < 0.0 && cd_a * cd_b < 0.0)
+}
+
+fn point_in_ring(point: [f64; 2], ring: &[[f64; 2]]) -> bool {
+ let mut winding = 0_i32;
+ for index in 0..ring.len() {
+ let first = ring[index];
+ let second = ring[(index + 1) % ring.len()];
+ let side = cross2(first, second, point);
+ if first[1] <= point[1] {
+ if second[1] > point[1] && side > 0.0 {
+ winding += 1;
+ }
+ } else if second[1] <= point[1] && side < 0.0 {
+ winding -= 1;
+ }
+ }
+ winding != 0
+}
+
+fn polygon_area(points: &[[f64; 2]]) -> f64 {
+ (0..points.len())
+ .map(|index| {
+ let first = points[index];
+ let second = points[(index + 1) % points.len()];
+ first[0] * second[1] - second[0] * first[1]
+ })
+ .sum::<f64>()
+ * 0.5
+}
+
+fn lift_to_source_plane(point: [f64; 2], source: [[f32; 3]; 3]) -> Option<[f32; 3]> {
+ for vertex in source {
+ if distance_squared(point, [f64::from(vertex[0]), f64::from(vertex[1])]) <= 1.0e-18 {
+ return Some(vertex);
+ }
+ }
+ let a = source[0].map(f64::from);
+ let b = source[1].map(f64::from);
+ let c = source[2].map(f64::from);
+ let denominator = cross2([a[0], a[1]], [b[0], b[1]], [c[0], c[1]]);
+ if denominator.abs() <= 1.0e-10 {
+ return None;
+ }
+ let second_weight = cross2([a[0], a[1]], point, [c[0], c[1]]) / denominator;
+ let third_weight = cross2([a[0], a[1]], [b[0], b[1]], point) / denominator;
+ let first_weight = 1.0 - second_weight - third_weight;
+ let z = first_weight * a[2] + second_weight * b[2] + third_weight * c[2];
+ let position = [point[0] as f32, point[1] as f32, z as f32];
+ position
+ .iter()
+ .all(|value| value.is_finite())
+ .then_some(position)
+}
+
+fn cross2(first: [f64; 2], second: [f64; 2], third: [f64; 2]) -> f64 {
+ (second[0] - first[0]) * (third[1] - first[1]) - (second[1] - first[1]) * (third[0] - first[0])
+}
+
+fn interpolate(first: [f64; 2], second: [f64; 2], amount: f64) -> [f64; 2] {
+ [
+ first[0] + (second[0] - first[0]) * amount,
+ first[1] + (second[1] - first[1]) * amount,
+ ]
+}
+
+fn distance_squared(first: [f64; 2], second: [f64; 2]) -> f64 {
+ let delta_x = first[0] - second[0];
+ let delta_y = first[1] - second[1];
+ delta_x * delta_x + delta_y * delta_y
+}
+
+#[cfg(test)]
+mod tests {
+ use super::subtract_contour;
+
+ #[test]
+ fn translated_thin_outside_strip_keeps_its_area() {
+ let base = 10_000.0_f32;
+ let source = [[
+ [base, base, 0.0],
+ [base + 10.0, base, 0.0],
+ [base, base + 10.0, 0.0],
+ ]];
+ let outer = [
+ [base + 0.003, base + 0.003],
+ [base + 1.0, base + 0.003],
+ [base + 1.0, base + 0.997],
+ [base + 0.003, base + 0.997],
+ ];
+ let output = subtract_contour(&source, &outer).expect("simple contour");
+ let retained_area = output
+ .iter()
+ .map(|fragment| {
+ let [a, b, c] = fragment.positions;
+ let ab = [f64::from(b[0] - a[0]), f64::from(b[1] - a[1])];
+ let ac = [f64::from(c[0] - a[0]), f64::from(c[1] - a[1])];
+ (ab[0] * ac[1] - ab[1] * ac[0]).abs() * 0.5
+ })
+ .sum::<f64>();
+ let expected_area =
+ 50.0 - (f64::from(outer[1][0] - outer[0][0]) * f64::from(outer[2][1] - outer[1][1]));
+ assert!(
+ (retained_area - expected_area).abs() < 0.001,
+ "retained area {retained_area}, expected {expected_area}"
+ );
+ }
+}