diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-18 07:59:36 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-18 07:59:42 +0300 |
| commit | c8543d7365329fa97d31ac9b0114a1ee9095dd74 (patch) | |
| tree | 3cb6c12749b659b99a6b8a2c6efb68aa79ff4626 /crates/fparkan-terrain/src/contour.rs | |
| parent | 737e429cb109446ec008c4dcf91d4f04faccd64a (diff) | |
| download | fparkan-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.rs | 809 |
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}" + ); + } +} |
