810 lines
28 KiB
Rust
810 lines
28 KiB
Rust
/// 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}"
|
|
);
|
|
}
|
|
}
|