feat(terrain): build BAS basement foundations
Docs Deploy / Build and Deploy MkDocs (push) Successful in 31s

This commit is contained in:
Valentin Popov committed 2026-10-18 08:59:42 +04:00
1 parent 737e429cb1
commit c8543d7365
4 files changed
+2970 -24

No files matched your search

+809
View File
@@ -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}"
);
}
}
File diff suppressed because it is too large. Load diff
+338 -24
View File
@@ -4,6 +4,15 @@
use fparkan_terrain_format::{LandMapDocument, LandMeshDocument};
use std::collections::VecDeque;
mod contour;
mod foundation;
use foundation::basement_surface_mask;
pub use foundation::{
build_basement_foundations, BasementFootprint, BasementFoundation, BasementFoundationError,
BasementFoundationFragment, BasementPlacement,
};
/// Terrain material-selector contract preserved from the decoded map mesh.
///
/// Applications access this semantic terrain API through this terrain crate
@@ -21,6 +30,7 @@ pub struct TerrainWorld {
surfaces: Vec<RuntimeTriangle>,
surface_bvh: SurfaceBvh,
source_mesh: Option<LandMeshDocument>,
basement_foundation: Option<BasementFoundation>,
}
/// Surface hit.
@@ -91,6 +101,13 @@ pub enum TerrainError {
/// Position count.
position_count: usize,
},
/// A basement fragment does not preserve a valid LOD0 source face and mask.
InvalidFoundationFragment {
/// Fragment index.
fragment: usize,
},
/// Foundation fragments require a retained source Land.msh document.
FoundationRequiresMesh,
/// Terrain Node38/Slot68 records do not assign every face to one LOD.
InvalidLodMapping(TerrainLodError),
}
@@ -119,6 +136,12 @@ impl std::fmt::Display for TerrainError {
f,
"terrain face {face} references vertex {vertex} outside {position_count} positions"
),
Self::InvalidFoundationFragment { fragment } => {
write!(f, "invalid basement foundation fragment {fragment}")
}
Self::FoundationRequiresMesh => {
write!(f, "basement foundation requires a source Land.msh")
}
Self::InvalidLodMapping(error) => write!(f, "invalid terrain LOD mapping: {error}"),
}
}
@@ -132,7 +155,9 @@ impl std::error::Error for TerrainError {
| Self::TooManyAreals { .. }
| Self::InvalidGridReference { .. }
| Self::InvalidArealReference { .. }
| Self::InvalidSurfaceVertex { .. } => None,
| Self::InvalidSurfaceVertex { .. }
| Self::InvalidFoundationFragment { .. }
| Self::FoundationRequiresMesh => None,
}
}
}
@@ -254,6 +279,7 @@ impl TerrainWorld {
surfaces: Vec::new(),
surface_bvh: SurfaceBvh::default(),
source_mesh: None,
basement_foundation: None,
})
}
@@ -290,6 +316,87 @@ impl TerrainWorld {
Ok(world)
}
/// Returns a new world whose surface queries use exactly the canonical
/// source and generated triangles from a basement foundation.
///
/// # Errors
///
/// Returns [`TerrainError`] if this world has no source mesh or any
/// fragment does not preserve valid LOD0 identity, flags, and geometry.
pub fn with_basement_foundation(
mut self,
foundation: BasementFoundation,
) -> Result<Self, TerrainError> {
let mesh = self
.source_mesh
.as_ref()
.ok_or(TerrainError::FoundationRequiresMesh)?;
let assignments = mesh
.face_lod_assignments()
.map_err(TerrainError::InvalidLodMapping)?;
let is_lod0 = assignments
.iter()
.map(|assignment| assignment.lod == TerrainLod::Lod0)
.collect::<Vec<_>>();
let mut surfaces = Vec::with_capacity(foundation.fragments.len());
let mut generated = 0_usize;
for (fragment_index, fragment) in foundation.fragments.iter().enumerate() {
if !valid_foundation_triangle(fragment.positions) {
return Err(TerrainError::InvalidFoundationFragment {
fragment: fragment_index,
});
}
let face = if let Some(source_face) = fragment.source_face {
let Some(source) = mesh.faces.get(source_face) else {
return Err(TerrainError::InvalidFoundationFragment {
fragment: fragment_index,
});
};
if !is_lod0.get(source_face).copied().unwrap_or(false)
|| source.flags != fragment.surface_mask
{
return Err(TerrainError::InvalidFoundationFragment {
fragment: fragment_index,
});
}
source_face
} else {
if basement_surface_mask(fragment.positions) != fragment.surface_mask {
return Err(TerrainError::InvalidFoundationFragment {
fragment: fragment_index,
});
}
let face = mesh.faces.len().checked_add(generated).ok_or(
TerrainError::InvalidFoundationFragment {
fragment: fragment_index,
},
)?;
generated =
generated
.checked_add(1)
.ok_or(TerrainError::InvalidFoundationFragment {
fragment: fragment_index,
})?;
face
};
surfaces.push(RuntimeTriangle {
face,
mask: fragment.surface_mask,
vertices: fragment.positions,
});
}
self.surfaces = surfaces;
self.surface_bvh = SurfaceBvh::from_surfaces(&self.surfaces);
self.basement_foundation = Some(foundation);
Ok(self)
}
/// Returns the canonical basement geometry used by the surface query index.
#[must_use]
pub fn basement_foundation(&self) -> Option<&BasementFoundation> {
self.basement_foundation.as_ref()
}
/// Returns the number of navigation areas.
#[must_use]
pub fn areal_count(&self) -> usize {
@@ -740,26 +847,31 @@ impl RuntimeTriangle {
}
fn raycast(&self, origin: [f32; 3], direction: [f32; 3]) -> Option<f32> {
let edge1 = sub3(self.vertices[1], self.vertices[0]);
let edge2 = sub3(self.vertices[2], self.vertices[0]);
let pvec = cross3(direction, edge2);
let det = dot3(edge1, pvec);
if det.abs() <= 1.0e-6 {
let edge1 = sub3_f64(self.vertices[1], self.vertices[0]);
let edge2 = sub3_f64(self.vertices[2], self.vertices[0]);
let direction = direction.map(f64::from);
let pvec = cross3_f64(direction, edge2);
let det = dot3_f64(edge1, pvec);
if det == 0.0 || !det.is_finite() {
return None;
}
let inv_det = 1.0 / det;
let tvec = sub3(origin, self.vertices[0]);
let u = dot3(tvec, pvec) * inv_det;
let tvec = sub3_f64(origin, self.vertices[0]);
let u = dot3_f64(tvec, pvec) * inv_det;
if !(-1.0e-5..=1.00001).contains(&u) {
return None;
}
let qvec = cross3(tvec, edge1);
let v = dot3(direction, qvec) * inv_det;
let qvec = cross3_f64(tvec, edge1);
let v = dot3_f64(direction, qvec) * inv_det;
if v < -1.0e-5 || u + v > 1.00001 {
return None;
}
let distance = dot3(edge2, qvec) * inv_det;
(distance >= 0.0).then_some(distance)
let distance = dot3_f64(edge2, qvec) * inv_det;
if distance >= 0.0 && distance.is_finite() && distance <= f64::from(f32::MAX) {
Some(distance as f32)
} else {
None
}
}
}
@@ -919,6 +1031,21 @@ fn build_surfaces(mesh: &LandMeshDocument) -> Result<Vec<RuntimeTriangle>, Terra
Ok(triangles)
}
fn valid_foundation_triangle(vertices: [[f32; 3]; 3]) -> bool {
if !vertices.iter().flatten().all(|value| value.is_finite()) {
return false;
}
let first = [
f64::from(vertices[1][0]) - f64::from(vertices[0][0]),
f64::from(vertices[1][1]) - f64::from(vertices[0][1]),
];
let second = [
f64::from(vertices[2][0]) - f64::from(vertices[0][0]),
f64::from(vertices[2][1]) - f64::from(vertices[0][1]),
];
first[0] * second[1] - first[1] * second[0] != 0.0
}
fn surface_vertex(
mesh: &LandMeshDocument,
face: usize,
@@ -940,25 +1067,41 @@ fn barycentric_2d(
second: [f32; 2],
third: [f32; 2],
) -> Option<[f32; 3]> {
let edge_second = [second[0] - first[0], second[1] - first[1]];
let edge_third = [third[0] - first[0], third[1] - first[1]];
let point_delta = [point[0] - first[0], point[1] - first[1]];
let edge_second = [
f64::from(second[0]) - f64::from(first[0]),
f64::from(second[1]) - f64::from(first[1]),
];
let edge_third = [
f64::from(third[0]) - f64::from(first[0]),
f64::from(third[1]) - f64::from(first[1]),
];
let point_delta = [
f64::from(point[0]) - f64::from(first[0]),
f64::from(point[1]) - f64::from(first[1]),
];
let denom = edge_second[0] * edge_third[1] - edge_third[0] * edge_second[1];
if denom.abs() <= 1.0e-6 {
if denom == 0.0 {
return None;
}
let inv = 1.0 / denom;
let second_weight = (point_delta[0] * edge_third[1] - edge_third[0] * point_delta[1]) * inv;
let third_weight = (edge_second[0] * point_delta[1] - point_delta[0] * edge_second[1]) * inv;
let second_weight = (point_delta[0] * edge_third[1] - edge_third[0] * point_delta[1]) / denom;
let third_weight = (edge_second[0] * point_delta[1] - point_delta[0] * edge_second[1]) / denom;
let first_weight = 1.0 - second_weight - third_weight;
Some([first_weight, second_weight, third_weight])
let weights = [first_weight, second_weight, third_weight];
if weights.iter().any(|weight| !weight.is_finite()) {
return None;
}
Some(weights.map(|weight| weight as f32))
}
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
fn sub3_f64(a: [f32; 3], b: [f32; 3]) -> [f64; 3] {
[
f64::from(a[0]) - f64::from(b[0]),
f64::from(a[1]) - f64::from(b[1]),
f64::from(a[2]) - f64::from(b[2]),
]
}
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
fn cross3_f64(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
@@ -966,7 +1109,7 @@ fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
]
}
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
fn dot3_f64(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
@@ -1126,6 +1269,177 @@ mod tests {
);
}
#[test]
fn foundation_world_queries_use_the_canonical_cut_and_generated_band() {
use crate::{build_basement_foundations, BasementFootprint};
let mesh = synthetic_land_mesh();
let outer = [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]];
let inner = [
[0.4, 0.4, 1.0],
[0.6, 0.4, 1.0],
[0.6, 0.6, 1.0],
[0.4, 0.6, 1.0],
];
let foundation = build_basement_foundations(
&mesh,
&[BasementFootprint {
outer: &outer,
inner: &inner,
flatten_inner_to_mean_outer_z: true,
}],
)
.expect("foundation");
let expected_fragments = foundation.fragments.clone();
let expected_fragment_count = foundation.fragments.len();
let world = TerrainWorld::from_land_msh(&mesh)
.expect("source world")
.with_basement_foundation(foundation)
.expect("foundation world");
assert_eq!(world.surface_count(), expected_fragment_count);
assert_eq!(
world
.basement_foundation()
.map(|value| value.fragments.as_slice()),
Some(expected_fragments.as_slice())
);
assert!(world
.height_at([0.05, 0.05])
.expect("band height")
.is_some());
assert!(world.height_at([0.3, 0.3]).expect("band height").is_some());
assert_eq!(world.height_at([0.5, 0.5]).expect("hole height"), None);
assert_eq!(
world
.raycast([0.5, 0.5, 4.0], [0.0, 0.0, -1.0], FullSurfaceMask(0),)
.expect("hole ray"),
None
);
let mut generated_face = mesh.faces.len();
for (runtime, fragment) in world.surfaces.iter().zip(&expected_fragments) {
assert_eq!(runtime.mask, fragment.surface_mask);
assert_eq!(runtime.vertices, fragment.positions);
if let Some(source_face) = fragment.source_face {
assert_eq!(runtime.face, source_face);
assert_eq!(runtime.mask, mesh.faces[source_face].flags);
} else {
assert_eq!(runtime.face, generated_face);
generated_face += 1;
}
}
assert!(generated_face > mesh.faces.len());
}
#[test]
fn foundation_install_accepts_thin_nonzero_fragments_for_surface_queries() {
let mesh = synthetic_land_mesh();
let fragment = BasementFoundationFragment {
source_face: Some(0),
surface_mask: mesh.faces[0].flags,
positions: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.000_000_5, 0.0]],
};
let foundation = BasementFoundation {
fragments: vec![fragment],
placements: Vec::new(),
};
let world = TerrainWorld::from_land_msh(&mesh)
.expect("source world")
.with_basement_foundation(foundation)
.expect("thin but non-degenerate triangle");
assert_eq!(world.surface_count(), 1);
assert_eq!(
world.height_at([0.25, 0.000_000_125]).expect("height"),
Some(0.0)
);
assert!(world
.raycast(
[0.25, 0.000_000_125, 1.0],
[0.0, 0.0, -1.0],
FullSurfaceMask(0),
)
.expect("raycast")
.is_some());
}
#[test]
fn foundation_install_rejects_invalid_source_identity_masks_and_generated_flags() {
let mesh = synthetic_land_mesh();
let source_triangle = [[0.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]];
let generated_triangle = [[0.0, 0.0, 0.0], [0.25, 0.0, 0.0], [0.0, 0.25, 0.0]];
let invalid = [
BasementFoundationFragment {
source_face: Some(mesh.faces.len()),
surface_mask: mesh.faces[0].flags,
positions: source_triangle,
},
BasementFoundationFragment {
source_face: Some(0),
surface_mask: FullSurfaceMask(mesh.faces[0].flags.0 ^ 1),
positions: source_triangle,
},
BasementFoundationFragment {
source_face: None,
surface_mask: FullSurfaceMask(0x0300),
positions: generated_triangle,
},
];
for (index, fragment) in invalid.into_iter().enumerate() {
let foundation = BasementFoundation {
fragments: vec![fragment],
placements: Vec::new(),
};
assert!(
matches!(
TerrainWorld::from_land_msh(&mesh)
.expect("source world")
.with_basement_foundation(foundation),
Err(TerrainError::InvalidFoundationFragment { fragment: 0 })
),
"invalid fragment case {index}"
);
}
}
#[test]
fn foundation_install_rejects_lod1_source_faces() {
use fparkan_terrain_format::TerrainLod;
let mut mesh = synthetic_land_mesh();
mesh.positions.push([0.0, 0.0, 2.0]);
mesh.positions.push([1.0, 0.0, 2.0]);
mesh.positions.push([0.0, 1.0, 2.0]);
mesh.faces
.push(terrain_face(FullSurfaceMask(0x0000_0003), [4, 5, 6]));
mesh.nodes_raw[10..12].copy_from_slice(&1_u16.to_le_bytes());
let mut lod1_slot = [0_u8; 68];
lod1_slot[0..2].copy_from_slice(&2_u16.to_le_bytes());
lod1_slot[2..4].copy_from_slice(&1_u16.to_le_bytes());
mesh.slots.slots_raw.push(lod1_slot);
assert_eq!(
mesh.face_lod_assignments().expect("LOD assignment")[2].lod,
TerrainLod::Lod1
);
let foundation = BasementFoundation {
fragments: vec![BasementFoundationFragment {
source_face: Some(2),
surface_mask: mesh.faces[2].flags,
positions: [[0.0, 0.0, 2.0], [1.0, 0.0, 2.0], [0.0, 1.0, 2.0]],
}],
placements: Vec::new(),
};
assert!(matches!(
TerrainWorld::from_land_msh(&mesh)
.expect("source world")
.with_basement_foundation(foundation),
Err(TerrainError::InvalidFoundationFragment { fragment: 0 })
));
}
#[test]
fn surface_bvh_reduces_candidates_without_changing_surface_math() {
let surfaces: Vec<_> = (0_u16..16)