feat(terrain): build BAS basement foundations
Docs Deploy / Build and Deploy MkDocs (push) Successful in 31s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 31s
This commit is contained in:
4 files changed
+2970
-24
No files matched your search
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user