1806 lines
64 KiB
Rust
1806 lines
64 KiB
Rust
//! Native-shaped building basement foundation geometry.
|
|||
|
|
|
||
|
|
use crate::contour::subtract_contour;
|
||
|
|
use fparkan_terrain_format::{FullSurfaceMask, LandMeshDocument, TerrainLod};
|
||
|
|
|
||
|
|
const LINE_EPSILON: f64 = 1.0e-5;
|
||
|
|
const POINT_EPSILON_SQUARED: f64 = 1.0e-12;
|
||
|
|
|
||
|
|
/// A terrain or generated basement triangle shared by preview consumers.
|
||
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
|
|
pub struct BasementFoundationFragment {
|
||
|
|
/// Original Land.msh face for terrain fragments; `None` for generated band faces.
|
||
|
|
pub source_face: Option<usize>,
|
||
|
|
/// Raw terrain or generated basement face flags.
|
||
|
|
pub surface_mask: FullSurfaceMask,
|
||
|
|
/// Triangle coordinates shared by rendering, shadows, and terrain queries.
|
||
|
|
pub positions: [[f32; 3]; 3],
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Canonical output of the native-shaped terrain basement preparation.
|
||
|
|
#[derive(Clone, Debug, PartialEq)]
|
||
|
|
pub struct BasementFoundation {
|
||
|
|
pub fragments: Vec<BasementFoundationFragment>,
|
||
|
|
/// Native placement-height inputs in footprint order.
|
||
|
|
pub placements: Vec<BasementPlacement>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// One building footprint in native preparation order.
|
||
|
|
#[derive(Clone, Copy, Debug)]
|
||
|
|
pub struct BasementFootprint<'a> {
|
||
|
|
/// Authored outside ring in world XY coordinates.
|
||
|
|
pub outer: &'a [[f32; 2]],
|
||
|
|
/// Authored inner ring, retaining its source Z for placement calculations.
|
||
|
|
pub inner: &'a [[f32; 3]],
|
||
|
|
/// Whether native getter mode flattens the generated inner boundary to mean outer Z.
|
||
|
|
pub flatten_inner_to_mean_outer_z: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Native placement-height inputs captured for one building.
|
||
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
|
|
pub struct BasementPlacement {
|
||
|
|
/// Native transform adjustment: first authored inner Z minus mean sampled outer terrain Z.
|
||
|
|
pub placement_delta: f32,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Error returned while preparing a basement foundation.
|
||
|
|
#[derive(Clone, Debug, PartialEq)]
|
||
|
|
pub enum BasementFoundationError {
|
||
|
|
/// Terrain Node38/Slot68 data cannot resolve LOD0 faces.
|
||
|
|
InvalidLodMapping(fparkan_terrain_format::TerrainLodError),
|
||
|
|
/// A selected LOD0 face refers to a missing vertex.
|
||
|
|
InvalidTerrainFace { face: usize, vertex: u16 },
|
||
|
|
/// A contour, nested-ring relationship, or generated annulus is invalid.
|
||
|
|
InvalidContour,
|
||
|
|
/// The LOD0 mesh has no usable terrain faces.
|
||
|
|
EmptyLod0,
|
||
|
|
/// The outer contour did not cross any LOD0 face.
|
||
|
|
NoOuterCrossings,
|
||
|
|
/// A point of the authored outer contour lies outside the LOD0 terrain.
|
||
|
|
OuterPointOutsideLod0 { point: usize },
|
||
|
|
/// An authored outer segment leaves the current terrain surface.
|
||
|
|
UncoveredOuterSegment { segment: usize },
|
||
|
|
/// Footprint outer contours overlap; native overlap semantics remain unverified.
|
||
|
|
OverlappingFootprints,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::fmt::Display for BasementFoundationError {
|
||
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
match self {
|
||
|
|
Self::InvalidLodMapping(error) => write!(f, "invalid terrain LOD mapping: {error}"),
|
||
|
|
Self::InvalidTerrainFace { face, vertex } => {
|
||
|
|
write!(f, "terrain face {face} references missing vertex {vertex}")
|
||
|
|
}
|
||
|
|
Self::InvalidContour => write!(f, "invalid basement contour geometry"),
|
||
|
|
Self::EmptyLod0 => write!(f, "terrain has no LOD0 faces"),
|
||
|
|
Self::NoOuterCrossings => write!(f, "outer contour does not cross LOD0 terrain"),
|
||
|
|
Self::OuterPointOutsideLod0 { point } => {
|
||
|
|
write!(f, "outer contour point {point} is outside LOD0 terrain")
|
||
|
|
}
|
||
|
|
Self::UncoveredOuterSegment { segment } => {
|
||
|
|
write!(
|
||
|
|
f,
|
||
|
|
"outer contour segment {segment} leaves the terrain surface"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
Self::OverlappingFootprints => {
|
||
|
|
write!(f, "overlapping basement footprints are not supported")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::error::Error for BasementFoundationError {
|
||
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||
|
|
match self {
|
||
|
|
Self::InvalidLodMapping(error) => Some(error),
|
||
|
|
Self::InvalidTerrainFace { .. }
|
||
|
|
| Self::InvalidContour
|
||
|
|
| Self::EmptyLod0
|
||
|
|
| Self::NoOuterCrossings => None,
|
||
|
|
Self::OuterPointOutsideLod0 { .. } | Self::UncoveredOuterSegment { .. } => None,
|
||
|
|
Self::OverlappingFootprints => None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Строит основания ландшафта в порядке нативной подготовки размещений.
|
||
|
|
///
|
||
|
|
/// Вершины внешнего контура нормализуются по топологии LOD0: сначала проверяются
|
||
|
|
/// вершины граней и соседних граней, затем применяется проекция, используемая
|
||
|
|
/// нативным `FUN_10006240`. Это косинусная формула с отдельной проверкой
|
||
|
|
/// расстояния, а не обычная ортогональная проекция. При обходе сегмента
|
||
|
|
/// вставляются только вершины LOD0, прошедшие тот же нативный предикат и
|
||
|
|
/// находящиеся не ближе 0.5 мировых единиц к обоим концам; геометрическое
|
||
|
|
/// совпадение с отрезком само по себе недостаточно.
|
||
|
|
///
|
||
|
|
/// Для каждого контура функция упорядочивает пересечения с плоскостями текущих
|
||
|
|
/// канонических треугольников, вычисляет среднюю высоту внешней границы и строит
|
||
|
|
/// ограниченную триангуляцию кольца. Явный флаг выбирает нативную ветвь высоты
|
||
|
|
/// внутренней границы. Непересекающиеся контуры последовательно вырезаются из
|
||
|
|
/// текущей поверхности; пересекающиеся контуры отклоняются, поскольку их
|
||
|
|
/// взаимодействие в нативном коде пока не установлено.
|
||
|
|
///
|
||
|
|
/// Возвращённый набор фрагментов является общей геометрией для рендеринга,
|
||
|
|
/// теней и запросов поверхности. Фрагменты исходной поверхности сохраняют
|
||
|
|
/// номера и маски исходных граней; у сгенерированных граней маска соответствует
|
||
|
|
/// записи `AddBasementFaceProc`.
|
||
|
|
///
|
||
|
|
/// The returned fragment vector is the canonical geometry for rendering,
|
||
|
|
/// shadows, and terrain queries. Source faces preserve their original face IDs
|
||
|
|
/// and masks. Generated faces use the raw mask written by AddBasementFaceProc.
|
||
|
|
pub fn build_basement_foundations(
|
||
|
|
mesh: &LandMeshDocument,
|
||
|
|
footprints: &[BasementFootprint<'_>],
|
||
|
|
) -> Result<BasementFoundation, BasementFoundationError> {
|
||
|
|
let assignments = mesh
|
||
|
|
.face_lod_assignments()
|
||
|
|
.map_err(BasementFoundationError::InvalidLodMapping)?;
|
||
|
|
let mut lod0_face_indices = Vec::new();
|
||
|
|
let mut terrain = Vec::new();
|
||
|
|
for assignment in assignments
|
||
|
|
.iter()
|
||
|
|
.filter(|assignment| assignment.lod == TerrainLod::Lod0)
|
||
|
|
{
|
||
|
|
let face = &mesh.faces[assignment.face_index];
|
||
|
|
let positions = terrain_face_positions(mesh, assignment.face_index)?;
|
||
|
|
terrain.push(BasementFoundationFragment {
|
||
|
|
source_face: Some(assignment.face_index),
|
||
|
|
surface_mask: face.flags,
|
||
|
|
positions,
|
||
|
|
});
|
||
|
|
lod0_face_indices.push(assignment.face_index);
|
||
|
|
}
|
||
|
|
if terrain.is_empty() {
|
||
|
|
return Err(BasementFoundationError::EmptyLod0);
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut bands = Vec::new();
|
||
|
|
let mut placements = Vec::with_capacity(footprints.len());
|
||
|
|
let mut previous_outer_rings: Vec<Vec<[f64; 2]>> = Vec::with_capacity(footprints.len());
|
||
|
|
for footprint in footprints {
|
||
|
|
let (outer_points, outer_closed) = normalize_ring(footprint.outer)?;
|
||
|
|
let snapped_outer = normalize_outer_points(&outer_points, &lod0_face_indices, mesh)?;
|
||
|
|
if !simple_ring(&snapped_outer) {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
if previous_outer_rings
|
||
|
|
.iter()
|
||
|
|
.any(|previous| rings_overlap(previous, &snapped_outer))
|
||
|
|
{
|
||
|
|
return Err(BasementFoundationError::OverlappingFootprints);
|
||
|
|
}
|
||
|
|
previous_outer_rings.push(snapped_outer.clone());
|
||
|
|
let mut outer_ring = snapped_outer
|
||
|
|
.iter()
|
||
|
|
.map(|point| [point[0] as f32, point[1] as f32])
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
if outer_closed {
|
||
|
|
outer_ring.push(outer_ring[0]);
|
||
|
|
}
|
||
|
|
let samples = terrain
|
||
|
|
.iter()
|
||
|
|
.map(|fragment| {
|
||
|
|
(
|
||
|
|
fragment
|
||
|
|
.source_face
|
||
|
|
.expect("terrain fragments have source faces"),
|
||
|
|
fragment.surface_mask,
|
||
|
|
fragment.positions,
|
||
|
|
)
|
||
|
|
})
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let mut outer_crossings = contour_crossings(&snapped_outer, &samples)?;
|
||
|
|
if outer_crossings.is_empty() {
|
||
|
|
return Err(BasementFoundationError::NoOuterCrossings);
|
||
|
|
}
|
||
|
|
// Crossings are open here; restore the authored duplicate only afterward.
|
||
|
|
let mean_outer_z = outer_crossings
|
||
|
|
.iter()
|
||
|
|
.map(|crossing| f64::from(crossing[2]))
|
||
|
|
.sum::<f64>()
|
||
|
|
/ outer_crossings.len() as f64;
|
||
|
|
if !mean_outer_z.is_finite() {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
let mean_outer_z = mean_outer_z as f32;
|
||
|
|
if outer_closed {
|
||
|
|
let closing = *outer_crossings
|
||
|
|
.first()
|
||
|
|
.ok_or(BasementFoundationError::NoOuterCrossings)?;
|
||
|
|
outer_crossings.push(closing);
|
||
|
|
}
|
||
|
|
|
||
|
|
let inner_xy = footprint
|
||
|
|
.inner
|
||
|
|
.iter()
|
||
|
|
.map(|point| [point[0], point[1]])
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let (inner_points, inner_closed) = normalize_ring(&inner_xy)?;
|
||
|
|
if footprint
|
||
|
|
.inner
|
||
|
|
.iter()
|
||
|
|
.flatten()
|
||
|
|
.any(|value| !value.is_finite())
|
||
|
|
|| !nested_rings(&snapped_outer, &inner_points)
|
||
|
|
{
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
let authored_inner_z = footprint
|
||
|
|
.inner
|
||
|
|
.first()
|
||
|
|
.ok_or(BasementFoundationError::InvalidContour)?[2];
|
||
|
|
let placement_delta = authored_inner_z - mean_outer_z;
|
||
|
|
if !placement_delta.is_finite() {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
placements.push(BasementPlacement { placement_delta });
|
||
|
|
let inner_vertex_count = if inner_closed {
|
||
|
|
footprint.inner.len() - 1
|
||
|
|
} else {
|
||
|
|
footprint.inner.len()
|
||
|
|
};
|
||
|
|
let mut inner_ring = footprint.inner[..inner_vertex_count]
|
||
|
|
.iter()
|
||
|
|
.map(|point| {
|
||
|
|
[
|
||
|
|
point[0],
|
||
|
|
point[1],
|
||
|
|
if footprint.flatten_inner_to_mean_outer_z {
|
||
|
|
mean_outer_z
|
||
|
|
} else {
|
||
|
|
point[2]
|
||
|
|
},
|
||
|
|
]
|
||
|
|
})
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
if inner_closed {
|
||
|
|
inner_ring.push(inner_ring[0]);
|
||
|
|
}
|
||
|
|
let outer_surface = crossings_as_ring(&outer_crossings, outer_closed)?;
|
||
|
|
let inner_surface = inner_ring_without_closure(&inner_ring);
|
||
|
|
for positions in triangulate_annulus(&outer_surface, inner_surface)? {
|
||
|
|
bands.push(BasementFoundationFragment {
|
||
|
|
source_face: None,
|
||
|
|
surface_mask: basement_surface_mask(positions),
|
||
|
|
positions,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
let source_positions = terrain
|
||
|
|
.iter()
|
||
|
|
.map(|fragment| fragment.positions)
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let outside = subtract_contour(&source_positions, &outer_ring)
|
||
|
|
.map_err(|_| BasementFoundationError::InvalidContour)?;
|
||
|
|
let clipped = outside
|
||
|
|
.into_iter()
|
||
|
|
.map(|fragment| {
|
||
|
|
let source = terrain[fragment.source_triangle];
|
||
|
|
BasementFoundationFragment {
|
||
|
|
source_face: source.source_face,
|
||
|
|
surface_mask: source.surface_mask,
|
||
|
|
positions: fragment.positions,
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
terrain = clipped;
|
||
|
|
}
|
||
|
|
terrain.extend(bands);
|
||
|
|
Ok(BasementFoundation {
|
||
|
|
fragments: terrain,
|
||
|
|
placements,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn terrain_face_positions(
|
||
|
|
mesh: &LandMeshDocument,
|
||
|
|
face_index: usize,
|
||
|
|
) -> Result<[[f32; 3]; 3], BasementFoundationError> {
|
||
|
|
let face = &mesh.faces[face_index];
|
||
|
|
let resolve = |vertex: u16| {
|
||
|
|
mesh.positions.get(usize::from(vertex)).copied().ok_or(
|
||
|
|
BasementFoundationError::InvalidTerrainFace {
|
||
|
|
face: face_index,
|
||
|
|
vertex,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
};
|
||
|
|
Ok([
|
||
|
|
resolve(face.vertices[0])?,
|
||
|
|
resolve(face.vertices[1])?,
|
||
|
|
resolve(face.vertices[2])?,
|
||
|
|
])
|
||
|
|
}
|
||
|
|
|
||
|
|
fn normalize_ring(ring: &[[f32; 2]]) -> Result<(Vec<[f64; 2]>, bool), BasementFoundationError> {
|
||
|
|
if ring.iter().flatten().any(|value| !value.is_finite()) {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
let mut points = ring
|
||
|
|
.iter()
|
||
|
|
.map(|point| [f64::from(point[0]), f64::from(point[1])])
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let closed = points.len() > 1
|
||
|
|
&& distance_squared(points[0], *points.last().expect("nonempty")) <= POINT_EPSILON_SQUARED;
|
||
|
|
if closed {
|
||
|
|
points.pop();
|
||
|
|
}
|
||
|
|
if points.len() < 3 || !simple_ring(&points) {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
Ok((points, closed))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn normalize_outer_points(
|
||
|
|
points: &[[f64; 2]],
|
||
|
|
lod0_faces: &[usize],
|
||
|
|
mesh: &LandMeshDocument,
|
||
|
|
) -> Result<Vec<[f64; 2]>, BasementFoundationError> {
|
||
|
|
// shortcut: first-face lookup and neighbor ordering still use source LOD0
|
||
|
|
// IDs for every placement. The crossing sampler and cutter use the current
|
||
|
|
// canonical fragment list, but dynamic neighbor-ID reconstruction is not
|
||
|
|
// modeled yet, so later placements are not claimed as native-identical.
|
||
|
|
let mut output = points
|
||
|
|
.iter()
|
||
|
|
.enumerate()
|
||
|
|
.map(|(point_index, point)| {
|
||
|
|
native_normalize_outer_point(
|
||
|
|
mesh,
|
||
|
|
lod0_faces,
|
||
|
|
[point[0] as f32, point[1] as f32],
|
||
|
|
point_index,
|
||
|
|
)
|
||
|
|
})
|
||
|
|
.collect::<Result<Vec<_>, _>>()?
|
||
|
|
.into_iter()
|
||
|
|
.map(|point| [f64::from(point[0]), f64::from(point[1])])
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
|
||
|
|
// The native contour pass asks for terrain faces overlapping the current
|
||
|
|
// segment's AABB, then tries their three vertices in face order. On one
|
||
|
|
// accepted vertex it inserts the full terrain point and resumes from the
|
||
|
|
// shortened segment at the same index.
|
||
|
|
let mut segment_index = 0;
|
||
|
|
while segment_index < output.len() {
|
||
|
|
let first = output[segment_index].map(|value| value as f32);
|
||
|
|
let second = output[(segment_index + 1) % output.len()].map(|value| value as f32);
|
||
|
|
let min = [first[0].min(second[0]), first[1].min(second[1])];
|
||
|
|
let max = [first[0].max(second[0]), first[1].max(second[1])];
|
||
|
|
let mut inserted = false;
|
||
|
|
'faces: for &face_index in lod0_faces {
|
||
|
|
let Some(face) = mesh.faces.get(face_index) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
let triangle = face
|
||
|
|
.vertices
|
||
|
|
.map(|vertex| mesh.positions.get(usize::from(vertex)).copied());
|
||
|
|
let [Some(a), Some(b), Some(c)] = triangle else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
let face_min = [a[0].min(b[0]).min(c[0]), a[1].min(b[1]).min(c[1])];
|
||
|
|
let face_max = [a[0].max(b[0]).max(c[0]), a[1].max(b[1]).max(c[1])];
|
||
|
|
if (0..2).any(|axis| face_max[axis] < min[axis] || max[axis] < face_min[axis]) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
for vertex in [a, b, c] {
|
||
|
|
if !native_segment_inserts_vertex(first, second, vertex) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let vertex_xy = [f64::from(vertex[0]), f64::from(vertex[1])];
|
||
|
|
if output
|
||
|
|
.iter()
|
||
|
|
.any(|existing| distance_squared(*existing, vertex_xy) <= POINT_EPSILON_SQUARED)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
output.insert(segment_index + 1, vertex_xy);
|
||
|
|
inserted = true;
|
||
|
|
break 'faces;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !inserted {
|
||
|
|
segment_index += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(output)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn native_normalize_outer_point(
|
||
|
|
mesh: &LandMeshDocument,
|
||
|
|
lod0_faces: &[usize],
|
||
|
|
point: [f32; 2],
|
||
|
|
point_index: usize,
|
||
|
|
) -> Result<[f32; 2], BasementFoundationError> {
|
||
|
|
let containing_face = lod0_faces.iter().copied().find(|&face_index| {
|
||
|
|
let Some(face) = mesh.faces.get(face_index) else {
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
let triangle = face
|
||
|
|
.vertices
|
||
|
|
.map(|vertex| mesh.positions[usize::from(vertex)]);
|
||
|
|
point_in_triangle_xy(point, triangle)
|
||
|
|
});
|
||
|
|
let containing_face = containing_face
|
||
|
|
.ok_or(BasementFoundationError::OuterPointOutsideLod0 { point: point_index })?;
|
||
|
|
let mut candidates = vec![containing_face];
|
||
|
|
for neighbor in mesh.faces[containing_face].neighbors.iter().flatten() {
|
||
|
|
let neighbor = usize::from(*neighbor);
|
||
|
|
if lod0_faces.contains(&neighbor) && !candidates.contains(&neighbor) {
|
||
|
|
candidates.push(neighbor);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let normalized = point;
|
||
|
|
for face_index in candidates {
|
||
|
|
let face = &mesh.faces[face_index];
|
||
|
|
let vertices = face
|
||
|
|
.vertices
|
||
|
|
.map(|vertex| mesh.positions.get(usize::from(vertex)).copied());
|
||
|
|
let [Some(a), Some(b), Some(c)] = vertices else {
|
||
|
|
return Err(BasementFoundationError::InvalidTerrainFace {
|
||
|
|
face: face_index,
|
||
|
|
vertex: face.vertices[0],
|
||
|
|
});
|
||
|
|
};
|
||
|
|
let vertices = [a, b, c];
|
||
|
|
for (first, second) in [(0, 1), (1, 2), (2, 0)] {
|
||
|
|
if let Some(candidate) =
|
||
|
|
native_normalize_edge(normalized, vertices[first], vertices[second])?
|
||
|
|
{
|
||
|
|
return Ok(candidate);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(normalized)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn point_in_triangle_xy(point: [f32; 2], triangle: [[f32; 3]; 3]) -> bool {
|
||
|
|
let sign = |a: [f32; 3], b: [f32; 3]| {
|
||
|
|
(point[0] - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point[1] - b[1])
|
||
|
|
};
|
||
|
|
let d1 = sign(triangle[0], triangle[1]);
|
||
|
|
let d2 = sign(triangle[1], triangle[2]);
|
||
|
|
let d3 = sign(triangle[2], triangle[0]);
|
||
|
|
let has_negative = d1 < -1.0e-4 || d2 < -1.0e-4 || d3 < -1.0e-4;
|
||
|
|
let has_positive = d1 > 1.0e-4 || d2 > 1.0e-4 || d3 > 1.0e-4;
|
||
|
|
!(has_negative && has_positive)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn native_normalize_edge(
|
||
|
|
point: [f32; 2],
|
||
|
|
first: [f32; 3],
|
||
|
|
second: [f32; 3],
|
||
|
|
) -> Result<Option<[f32; 2]>, BasementFoundationError> {
|
||
|
|
if distance_xy(point, [first[0], first[1]]) < 0.5 {
|
||
|
|
let point = [first[0], first[1]];
|
||
|
|
return Ok(Some(point));
|
||
|
|
}
|
||
|
|
if distance_xy(point, [second[0], second[1]]) < 0.5 {
|
||
|
|
let point = [second[0], second[1]];
|
||
|
|
return Ok(Some(point));
|
||
|
|
}
|
||
|
|
let Some((offset, _)) = native_edge_projection(point, first, second) else {
|
||
|
|
return Ok(None);
|
||
|
|
};
|
||
|
|
let mut projected = [point[0] + offset[0], point[1] + offset[1]];
|
||
|
|
let Some((check_offset, _)) = native_edge_projection(projected, first, second) else {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
};
|
||
|
|
if check_offset[0].hypot(check_offset[1]) >= 0.0001 {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
if distance_xy(projected, [first[0], first[1]]) < 0.5 {
|
||
|
|
projected = [first[0], first[1]];
|
||
|
|
}
|
||
|
|
if distance_xy(projected, [second[0], second[1]]) < 0.5 {
|
||
|
|
projected = [second[0], second[1]];
|
||
|
|
}
|
||
|
|
Ok(Some(projected))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn native_segment_inserts_vertex(first: [f32; 2], second: [f32; 2], point: [f32; 3]) -> bool {
|
||
|
|
if distance_xy([point[0], point[1]], first) < 0.5
|
||
|
|
|| distance_xy([point[0], point[1]], second) < 0.5
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
native_edge_projection(
|
||
|
|
[point[0], point[1]],
|
||
|
|
[first[0], first[1], 0.0],
|
||
|
|
[second[0], second[1], 0.0],
|
||
|
|
)
|
||
|
|
.is_some()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn native_edge_projection(
|
||
|
|
point: [f32; 2],
|
||
|
|
first: [f32; 3],
|
||
|
|
second: [f32; 3],
|
||
|
|
) -> Option<([f32; 2], f32)> {
|
||
|
|
let ux = point[0] - first[0];
|
||
|
|
let uy = point[1] - first[1];
|
||
|
|
let ex = second[0] - first[0];
|
||
|
|
let ey = second[1] - first[1];
|
||
|
|
let edge_length = (ex * ex + ey * ey).sqrt();
|
||
|
|
let point_length = (ux * ux + uy * uy).sqrt();
|
||
|
|
if edge_length == 0.0 || point_length == 0.0 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let t = ((ex * ux + ey * uy) / edge_length) / point_length;
|
||
|
|
if !(0.0..=1.0).contains(&t) {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let offset = [
|
||
|
|
(t * ex + first[0]) - point[0],
|
||
|
|
(t * ey + first[1]) - point[1],
|
||
|
|
];
|
||
|
|
(offset[0].hypot(offset[1]) < 0.5).then_some((offset, t))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn distance_xy(first: [f32; 2], second: [f32; 2]) -> f32 {
|
||
|
|
((first[0] - second[0]).powi(2) + (first[1] - second[1]).powi(2)).sqrt()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn contour_crossings(
|
||
|
|
ring: &[[f64; 2]],
|
||
|
|
terrain_faces: &[(usize, FullSurfaceMask, [[f32; 3]; 3])],
|
||
|
|
) -> Result<Vec<[f32; 3]>, BasementFoundationError> {
|
||
|
|
let mut crossings = Vec::new();
|
||
|
|
for segment_index in 0..ring.len() {
|
||
|
|
let first = ring[segment_index];
|
||
|
|
let second = ring[(segment_index + 1) % ring.len()];
|
||
|
|
let mut segment_crossings = Vec::new();
|
||
|
|
let mut coverage = Vec::new();
|
||
|
|
for (source_face, _, triangle) in terrain_faces {
|
||
|
|
let Some((start, end)) = segment_triangle_interval(first, second, *triangle) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
coverage.push((start, end));
|
||
|
|
for t in [start, end] {
|
||
|
|
let point = [
|
||
|
|
first[0] + (second[0] - first[0]) * t,
|
||
|
|
first[1] + (second[1] - first[1]) * t,
|
||
|
|
];
|
||
|
|
let Some(position) = lift_to_face(point, *triangle) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
segment_crossings.push((t, *source_face, position));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
segment_crossings.sort_by(|left, right| {
|
||
|
|
left.0
|
||
|
|
.total_cmp(&right.0)
|
||
|
|
.then_with(|| left.1.cmp(&right.1))
|
||
|
|
});
|
||
|
|
coverage.sort_by(|left, right| {
|
||
|
|
left.0
|
||
|
|
.total_cmp(&right.0)
|
||
|
|
.then_with(|| left.1.total_cmp(&right.1))
|
||
|
|
});
|
||
|
|
let mut covered_until = 0.0_f64;
|
||
|
|
for (start, end) in coverage {
|
||
|
|
if start > covered_until + 1.0e-8 {
|
||
|
|
return Err(BasementFoundationError::UncoveredOuterSegment {
|
||
|
|
segment: segment_index,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
covered_until = covered_until.max(end);
|
||
|
|
}
|
||
|
|
if covered_until < 1.0 - 1.0e-8 {
|
||
|
|
return Err(BasementFoundationError::UncoveredOuterSegment {
|
||
|
|
segment: segment_index,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
for (_, _, position) in segment_crossings {
|
||
|
|
let duplicate_of_previous = crossings.last().is_some_and(|previous: &[f32; 3]| {
|
||
|
|
distance_squared(
|
||
|
|
[f64::from(previous[0]), f64::from(previous[1])],
|
||
|
|
[f64::from(position[0]), f64::from(position[1])],
|
||
|
|
) <= POINT_EPSILON_SQUARED
|
||
|
|
});
|
||
|
|
if duplicate_of_previous {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
crossings.push(position);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if crossings.len() > 1
|
||
|
|
&& distance_squared(
|
||
|
|
[f64::from(crossings[0][0]), f64::from(crossings[0][1])],
|
||
|
|
[
|
||
|
|
f64::from(crossings.last().expect("nonempty")[0]),
|
||
|
|
f64::from(crossings.last().expect("nonempty")[1]),
|
||
|
|
],
|
||
|
|
) <= POINT_EPSILON_SQUARED
|
||
|
|
{
|
||
|
|
crossings.pop();
|
||
|
|
}
|
||
|
|
Ok(crossings)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn segment_triangle_interval(
|
||
|
|
first: [f64; 2],
|
||
|
|
second: [f64; 2],
|
||
|
|
triangle: [[f32; 3]; 3],
|
||
|
|
) -> Option<(f64, f64)> {
|
||
|
|
let vertices = triangle.map(|point| [f64::from(point[0]), f64::from(point[1])]);
|
||
|
|
let area = cross2(vertices[0], vertices[1], vertices[2]);
|
||
|
|
if !area.is_finite() || area.abs() <= 1.0e-12 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let orientation = area.signum();
|
||
|
|
let delta = [second[0] - first[0], second[1] - first[1]];
|
||
|
|
let mut lower: f64 = 0.0;
|
||
|
|
let mut upper: f64 = 1.0;
|
||
|
|
for edge in 0..3 {
|
||
|
|
let edge_first = vertices[edge];
|
||
|
|
let edge_second = vertices[(edge + 1) % 3];
|
||
|
|
let start_distance = orientation * cross2(edge_first, edge_second, first);
|
||
|
|
let end = [first[0] + delta[0], first[1] + delta[1]];
|
||
|
|
let end_distance = orientation * cross2(edge_first, edge_second, end);
|
||
|
|
let slope = end_distance - start_distance;
|
||
|
|
if slope.abs() <= 1.0e-15 {
|
||
|
|
if start_distance < -LINE_EPSILON {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
// Tolerance decides whether a nearly outside interval is accepted;
|
||
|
|
// the actual boundary sample remains on the geometric edge (distance 0).
|
||
|
|
let crossing = -start_distance / slope;
|
||
|
|
if slope > 0.0 {
|
||
|
|
lower = lower.max(crossing);
|
||
|
|
} else {
|
||
|
|
upper = upper.min(crossing);
|
||
|
|
}
|
||
|
|
if lower > upper {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
(upper >= 0.0 && lower <= 1.0).then_some((lower.clamp(0.0, 1.0), upper.clamp(0.0, 1.0)))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn lift_to_face(point: [f64; 2], triangle: [[f32; 3]; 3]) -> Option<[f32; 3]> {
|
||
|
|
let vertices = triangle.map(|vertex| vertex.map(f64::from));
|
||
|
|
let denominator = cross2(
|
||
|
|
[vertices[0][0], vertices[0][1]],
|
||
|
|
[vertices[1][0], vertices[1][1]],
|
||
|
|
[vertices[2][0], vertices[2][1]],
|
||
|
|
);
|
||
|
|
if denominator.abs() <= 1.0e-12 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let weight_b = cross2(
|
||
|
|
[vertices[0][0], vertices[0][1]],
|
||
|
|
point,
|
||
|
|
[vertices[2][0], vertices[2][1]],
|
||
|
|
) / denominator;
|
||
|
|
let weight_c = cross2(
|
||
|
|
[vertices[0][0], vertices[0][1]],
|
||
|
|
[vertices[1][0], vertices[1][1]],
|
||
|
|
point,
|
||
|
|
) / denominator;
|
||
|
|
let weight_a = 1.0 - weight_b - weight_c;
|
||
|
|
let z = weight_a * vertices[0][2] + weight_b * vertices[1][2] + weight_c * vertices[2][2];
|
||
|
|
let result = [point[0] as f32, point[1] as f32, z as f32];
|
||
|
|
result
|
||
|
|
.iter()
|
||
|
|
.all(|value| value.is_finite())
|
||
|
|
.then_some(result)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn crossings_as_ring(
|
||
|
|
crossings: &[[f32; 3]],
|
||
|
|
closed: bool,
|
||
|
|
) -> Result<Vec<[f32; 3]>, BasementFoundationError> {
|
||
|
|
let length = crossings.len().saturating_sub(usize::from(closed));
|
||
|
|
let ring = crossings.iter().take(length).copied().collect::<Vec<_>>();
|
||
|
|
(ring.len() >= 3)
|
||
|
|
.then_some(ring)
|
||
|
|
.ok_or(BasementFoundationError::InvalidContour)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn inner_ring_without_closure(ring: &[[f32; 3]]) -> Vec<[f32; 3]> {
|
||
|
|
let mut result = ring.to_vec();
|
||
|
|
if result.len() > 1 && same_xy(result[0], *result.last().expect("nonempty")) {
|
||
|
|
result.pop();
|
||
|
|
}
|
||
|
|
result
|
||
|
|
}
|
||
|
|
|
||
|
|
fn triangulate_annulus(
|
||
|
|
outer: &[[f32; 3]],
|
||
|
|
inner: Vec<[f32; 3]>,
|
||
|
|
) -> Result<Vec<[[f32; 3]; 3]>, BasementFoundationError> {
|
||
|
|
let mut outer = remove_consecutive_duplicates(outer);
|
||
|
|
let mut inner = remove_consecutive_duplicates(&inner);
|
||
|
|
if outer.len() < 3 || inner.len() < 3 || !nested_rings_3d(&outer, &inner) {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
if signed_area_3d(&outer) < 0.0 {
|
||
|
|
outer.reverse();
|
||
|
|
}
|
||
|
|
if signed_area_3d(&inner) > 0.0 {
|
||
|
|
inner.reverse();
|
||
|
|
}
|
||
|
|
let polygon = bridge_hole(&outer, &inner)?;
|
||
|
|
let output =
|
||
|
|
legalize_constrained_edges(ear_clip_weakly_simple_polygon(&polygon)?, &outer, &inner)?;
|
||
|
|
if output.iter().any(|triangle| {
|
||
|
|
!triangle.iter().flatten().all(|value| value.is_finite())
|
||
|
|
|| cross2(xy(triangle[0]), xy(triangle[1]), xy(triangle[2])).abs() <= 1.0e-9
|
||
|
|
}) {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
Ok(output)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn bridge_hole(
|
||
|
|
outer: &[[f32; 3]],
|
||
|
|
inner: &[[f32; 3]],
|
||
|
|
) -> Result<Vec<[f32; 3]>, BasementFoundationError> {
|
||
|
|
let hole_index = (0..inner.len())
|
||
|
|
.max_by(|left, right| {
|
||
|
|
inner[*left][0]
|
||
|
|
.total_cmp(&inner[*right][0])
|
||
|
|
.then_with(|| inner[*right][1].total_cmp(&inner[*left][1]))
|
||
|
|
})
|
||
|
|
.ok_or(BasementFoundationError::InvalidContour)?;
|
||
|
|
let hole = inner[hole_index];
|
||
|
|
let mut candidates = (0..outer.len())
|
||
|
|
.map(|index| (distance_squared(xy(hole), xy(outer[index])), index))
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
candidates.sort_by(|left, right| {
|
||
|
|
left.0
|
||
|
|
.total_cmp(&right.0)
|
||
|
|
.then_with(|| outer[right.1][0].total_cmp(&outer[left.1][0]))
|
||
|
|
});
|
||
|
|
let mut bridged_outer = None;
|
||
|
|
for (_, candidate_index) in candidates {
|
||
|
|
let candidate = outer[candidate_index];
|
||
|
|
if visible_bridge(xy(hole), xy(candidate), outer, inner) {
|
||
|
|
bridged_outer = Some((outer.to_vec(), candidate_index));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let (outer, outer_index) = bridged_outer.ok_or(BasementFoundationError::InvalidContour)?;
|
||
|
|
let mut polygon = Vec::with_capacity(outer.len() + inner.len() + 2);
|
||
|
|
for step in 0..outer.len() {
|
||
|
|
let index = (outer_index + step) % outer.len();
|
||
|
|
polygon.push(outer[index]);
|
||
|
|
if step == 0 {
|
||
|
|
polygon.push(inner[hole_index]);
|
||
|
|
for hole_step in 1..inner.len() {
|
||
|
|
polygon.push(inner[(hole_index + hole_step) % inner.len()]);
|
||
|
|
}
|
||
|
|
polygon.push(inner[hole_index]);
|
||
|
|
polygon.push(outer[outer_index]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(polygon)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn visible_bridge(
|
||
|
|
hole: [f64; 2],
|
||
|
|
outer: [f64; 2],
|
||
|
|
outer_ring: &[[f32; 3]],
|
||
|
|
inner_ring: &[[f32; 3]],
|
||
|
|
) -> bool {
|
||
|
|
let midpoint = [(hole[0] + outer[0]) * 0.5, (hole[1] + outer[1]) * 0.5];
|
||
|
|
let outer_xy = outer_ring.iter().copied().map(xy).collect::<Vec<_>>();
|
||
|
|
let inner_xy = inner_ring.iter().copied().map(xy).collect::<Vec<_>>();
|
||
|
|
if !point_in_ring(midpoint, &outer_xy) || point_in_ring(midpoint, &inner_xy) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (0..outer_xy.len()).any(|index| {
|
||
|
|
segment_crosses_open_edge(
|
||
|
|
hole,
|
||
|
|
outer,
|
||
|
|
outer_xy[index],
|
||
|
|
outer_xy[(index + 1) % outer_xy.len()],
|
||
|
|
)
|
||
|
|
}) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
!(0..inner_xy.len()).any(|index| {
|
||
|
|
segment_crosses_open_edge(
|
||
|
|
hole,
|
||
|
|
outer,
|
||
|
|
inner_xy[index],
|
||
|
|
inner_xy[(index + 1) % inner_xy.len()],
|
||
|
|
)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn segment_crosses_open_edge(
|
||
|
|
first: [f64; 2],
|
||
|
|
second: [f64; 2],
|
||
|
|
edge_first: [f64; 2],
|
||
|
|
edge_second: [f64; 2],
|
||
|
|
) -> bool {
|
||
|
|
if same_point(first, edge_first)
|
||
|
|
|| same_point(first, edge_second)
|
||
|
|
|| same_point(second, edge_first)
|
||
|
|
|| same_point(second, edge_second)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
segments_intersect(first, second, edge_first, edge_second)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn same_point(first: [f64; 2], second: [f64; 2]) -> bool {
|
||
|
|
distance_squared(first, second) <= POINT_EPSILON_SQUARED
|
||
|
|
}
|
||
|
|
|
||
|
|
fn ear_clip_weakly_simple_polygon(
|
||
|
|
polygon: &[[f32; 3]],
|
||
|
|
) -> Result<Vec<[[f32; 3]; 3]>, BasementFoundationError> {
|
||
|
|
let mut remaining = (0..polygon.len()).collect::<Vec<_>>();
|
||
|
|
let mut triangles = Vec::with_capacity(polygon.len().saturating_sub(2));
|
||
|
|
while remaining.len() > 3 {
|
||
|
|
let mut ear = None;
|
||
|
|
for slot in 0..remaining.len() {
|
||
|
|
let previous = remaining[(slot + remaining.len() - 1) % remaining.len()];
|
||
|
|
let current = remaining[slot];
|
||
|
|
let next = remaining[(slot + 1) % remaining.len()];
|
||
|
|
let a = xy(polygon[previous]);
|
||
|
|
let b = xy(polygon[current]);
|
||
|
|
let c = xy(polygon[next]);
|
||
|
|
if cross2(a, b, c) <= 1.0e-9 || !diagonal_is_clear(previous, next, &remaining, polygon)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let contains_point = remaining.iter().copied().any(|candidate| {
|
||
|
|
candidate != previous
|
||
|
|
&& candidate != current
|
||
|
|
&& candidate != next
|
||
|
|
&& !same_xy(polygon[candidate], polygon[previous])
|
||
|
|
&& !same_xy(polygon[candidate], polygon[current])
|
||
|
|
&& !same_xy(polygon[candidate], polygon[next])
|
||
|
|
&& point_in_or_on_triangle(xy(polygon[candidate]), a, b, c)
|
||
|
|
});
|
||
|
|
if !contains_point {
|
||
|
|
ear = Some((slot, [polygon[previous], polygon[current], polygon[next]]));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if let Some((slot, triangle)) = ear {
|
||
|
|
triangles.push(triangle);
|
||
|
|
remaining.remove(slot);
|
||
|
|
} else {
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if remaining.len() == 3 {
|
||
|
|
let triangle = [
|
||
|
|
polygon[remaining[0]],
|
||
|
|
polygon[remaining[1]],
|
||
|
|
polygon[remaining[2]],
|
||
|
|
];
|
||
|
|
if cross2(xy(triangle[0]), xy(triangle[1]), xy(triangle[2])) > 1.0e-9 {
|
||
|
|
triangles.push(triangle);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
(!triangles.is_empty())
|
||
|
|
.then_some(triangles)
|
||
|
|
.ok_or(BasementFoundationError::InvalidContour)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn diagonal_is_clear(
|
||
|
|
first_index: usize,
|
||
|
|
second_index: usize,
|
||
|
|
polygon_indices: &[usize],
|
||
|
|
polygon: &[[f32; 3]],
|
||
|
|
) -> bool {
|
||
|
|
let first = xy(polygon[first_index]);
|
||
|
|
let second = xy(polygon[second_index]);
|
||
|
|
for edge in 0..polygon_indices.len() {
|
||
|
|
let edge_first_index = polygon_indices[edge];
|
||
|
|
let edge_second_index = polygon_indices[(edge + 1) % polygon_indices.len()];
|
||
|
|
if edge_first_index == first_index
|
||
|
|
|| edge_second_index == first_index
|
||
|
|
|| edge_first_index == second_index
|
||
|
|
|| edge_second_index == second_index
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if segment_crosses_open_edge(
|
||
|
|
first,
|
||
|
|
second,
|
||
|
|
xy(polygon[edge_first_index]),
|
||
|
|
xy(polygon[edge_second_index]),
|
||
|
|
) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
true
|
||
|
|
}
|
||
|
|
|
||
|
|
fn point_in_or_on_triangle(point: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
|
||
|
|
let epsilon = 1.0e-9;
|
||
|
|
cross2(a, b, point) >= -epsilon
|
||
|
|
&& cross2(b, c, point) >= -epsilon
|
||
|
|
&& cross2(c, a, point) >= -epsilon
|
||
|
|
}
|
||
|
|
|
||
|
|
fn nested_rings(outer: &[[f64; 2]], inner: &[[f64; 2]]) -> bool {
|
||
|
|
if !simple_ring(outer) || !simple_ring(inner) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if rings_intersect(outer, inner) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
point_in_ring(inner[0], outer)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn nested_rings_3d(outer: &[[f32; 3]], inner: &[[f32; 3]]) -> bool {
|
||
|
|
let outer_xy = outer.iter().copied().map(xy).collect::<Vec<_>>();
|
||
|
|
let inner_xy = inner.iter().copied().map(xy).collect::<Vec<_>>();
|
||
|
|
nested_rings(&outer_xy, &inner_xy)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn remove_consecutive_duplicates(ring: &[[f32; 3]]) -> Vec<[f32; 3]> {
|
||
|
|
let mut result = Vec::with_capacity(ring.len());
|
||
|
|
for point in ring {
|
||
|
|
if result
|
||
|
|
.last()
|
||
|
|
.is_none_or(|previous| !same_xy(*previous, *point))
|
||
|
|
{
|
||
|
|
result.push(*point);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if result.len() > 1 && same_xy(result[0], *result.last().expect("nonempty")) {
|
||
|
|
result.pop();
|
||
|
|
}
|
||
|
|
result
|
||
|
|
}
|
||
|
|
|
||
|
|
fn simple_ring(ring: &[[f64; 2]]) -> bool {
|
||
|
|
if ring.len() < 3 || ring.iter().flatten().any(|value| !value.is_finite()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
let area = ring_area(ring);
|
||
|
|
if !area.is_finite() || area.abs() <= 1.0e-10 {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
for first in 0..ring.len() {
|
||
|
|
let first_next = (first + 1) % ring.len();
|
||
|
|
if distance_squared(ring[first], ring[first_next]) <= POINT_EPSILON_SQUARED {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
for second in (first + 1)..ring.len() {
|
||
|
|
let second_next = (second + 1) % ring.len();
|
||
|
|
if first == second || first_next == second || second_next == first {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if segments_intersect(
|
||
|
|
ring[first],
|
||
|
|
ring[first_next],
|
||
|
|
ring[second],
|
||
|
|
ring[second_next],
|
||
|
|
) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
true
|
||
|
|
}
|
||
|
|
|
||
|
|
fn rings_intersect(first: &[[f64; 2]], second: &[[f64; 2]]) -> bool {
|
||
|
|
(0..first.len()).any(|first_index| {
|
||
|
|
(0..second.len()).any(|second_index| {
|
||
|
|
segments_intersect(
|
||
|
|
first[first_index],
|
||
|
|
first[(first_index + 1) % first.len()],
|
||
|
|
second[second_index],
|
||
|
|
second[(second_index + 1) % second.len()],
|
||
|
|
)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn rings_overlap(first: &[[f64; 2]], second: &[[f64; 2]]) -> bool {
|
||
|
|
rings_intersect(first, second)
|
||
|
|
|| point_in_ring(first[0], second)
|
||
|
|
|| point_in_ring(second[0], first)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn point_in_ring(point: [f64; 2], ring: &[[f64; 2]]) -> bool {
|
||
|
|
let mut inside = false;
|
||
|
|
for index in 0..ring.len() {
|
||
|
|
let first = ring[index];
|
||
|
|
let second = ring[(index + 1) % ring.len()];
|
||
|
|
if point_on_segment(point, first, second) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (first[1] > point[1]) != (second[1] > point[1]) {
|
||
|
|
let intersection_x =
|
||
|
|
first[0] + (point[1] - first[1]) * (second[0] - first[0]) / (second[1] - first[1]);
|
||
|
|
if point[0] < intersection_x {
|
||
|
|
inside = !inside;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
inside
|
||
|
|
}
|
||
|
|
|
||
|
|
fn point_on_segment(point: [f64; 2], first: [f64; 2], second: [f64; 2]) -> bool {
|
||
|
|
let dx = second[0] - first[0];
|
||
|
|
let dy = second[1] - first[1];
|
||
|
|
let length = (dx * dx + dy * dy).sqrt();
|
||
|
|
cross2(first, second, point).abs() <= LINE_EPSILON * length
|
||
|
|
&& point[0] >= first[0].min(second[0]) - LINE_EPSILON
|
||
|
|
&& point[0] <= first[0].max(second[0]) + LINE_EPSILON
|
||
|
|
&& point[1] >= first[1].min(second[1]) - LINE_EPSILON
|
||
|
|
&& point[1] <= first[1].max(second[1]) + LINE_EPSILON
|
||
|
|
}
|
||
|
|
|
||
|
|
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 epsilon = 1.0e-8;
|
||
|
|
(ab_c.abs() <= epsilon && on_segment(c, a, b, epsilon))
|
||
|
|
|| (ab_d.abs() <= epsilon && on_segment(d, a, b, epsilon))
|
||
|
|
|| (cd_a.abs() <= epsilon && on_segment(a, c, d, epsilon))
|
||
|
|
|| (cd_b.abs() <= epsilon && on_segment(b, c, d, epsilon))
|
||
|
|
|| (ab_c * ab_d < 0.0 && cd_a * cd_b < 0.0)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn on_segment(point: [f64; 2], first: [f64; 2], second: [f64; 2], epsilon: f64) -> bool {
|
||
|
|
point[0] >= first[0].min(second[0]) - epsilon
|
||
|
|
&& point[0] <= first[0].max(second[0]) + epsilon
|
||
|
|
&& point[1] >= first[1].min(second[1]) - epsilon
|
||
|
|
&& point[1] <= first[1].max(second[1]) + epsilon
|
||
|
|
}
|
||
|
|
|
||
|
|
fn ring_area(ring: &[[f64; 2]]) -> f64 {
|
||
|
|
(0..ring.len())
|
||
|
|
.map(|index| {
|
||
|
|
let first = ring[index];
|
||
|
|
let second = ring[(index + 1) % ring.len()];
|
||
|
|
first[0] * second[1] - second[0] * first[1]
|
||
|
|
})
|
||
|
|
.sum::<f64>()
|
||
|
|
* 0.5
|
||
|
|
}
|
||
|
|
|
||
|
|
fn signed_area_3d(ring: &[[f32; 3]]) -> f64 {
|
||
|
|
ring_area(&ring.iter().copied().map(xy).collect::<Vec<_>>())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn xy(position: [f32; 3]) -> [f64; 2] {
|
||
|
|
[f64::from(position[0]), f64::from(position[1])]
|
||
|
|
}
|
||
|
|
|
||
|
|
fn same_xy(first: [f32; 3], second: [f32; 3]) -> bool {
|
||
|
|
distance_squared(xy(first), xy(second)) <= POINT_EPSILON_SQUARED
|
||
|
|
}
|
||
|
|
|
||
|
|
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 distance_squared(first: [f64; 2], second: [f64; 2]) -> f64 {
|
||
|
|
let dx = first[0] - second[0];
|
||
|
|
let dy = first[1] - second[1];
|
||
|
|
dx * dx + dy * dy
|
||
|
|
}
|
||
|
|
|
||
|
|
pub(crate) fn basement_surface_mask(positions: [[f32; 3]; 3]) -> FullSurfaceMask {
|
||
|
|
let first = positions[1];
|
||
|
|
let origin = positions[0];
|
||
|
|
let second = positions[2];
|
||
|
|
let normal_z = (first[0] - origin[0]) * (second[1] - origin[1])
|
||
|
|
- (first[1] - origin[1]) * (second[0] - origin[0]);
|
||
|
|
FullSurfaceMask(if normal_z < 0.0 { 0x0300 } else { 0x8300 })
|
||
|
|
}
|
||
|
|
|
||
|
|
fn legalize_constrained_edges(
|
||
|
|
mut triangles: Vec<[[f32; 3]; 3]>,
|
||
|
|
outer: &[[f32; 3]],
|
||
|
|
inner: &[[f32; 3]],
|
||
|
|
) -> Result<Vec<[[f32; 3]; 3]>, BasementFoundationError> {
|
||
|
|
let max_flips = triangles
|
||
|
|
.len()
|
||
|
|
.saturating_mul(triangles.len())
|
||
|
|
.saturating_mul(4)
|
||
|
|
.max(1);
|
||
|
|
for _ in 0..max_flips {
|
||
|
|
let mut flipped = false;
|
||
|
|
'pairs: for first in 0..triangles.len() {
|
||
|
|
for second in (first + 1)..triangles.len() {
|
||
|
|
let Some((a, b, c, d)) = shared_edge(triangles[first], triangles[second]) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
if ring_has_edge(outer, a, b) || ring_has_edge(inner, a, b) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if !convex_quadrilateral(a, b, c, d) || in_circle(a, b, c, d) <= 1.0e-8 {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let first_triangle = orient_ccw([c, d, b]);
|
||
|
|
let second_triangle = orient_ccw([d, c, a]);
|
||
|
|
if cross2(
|
||
|
|
xy(first_triangle[0]),
|
||
|
|
xy(first_triangle[1]),
|
||
|
|
xy(first_triangle[2]),
|
||
|
|
) <= 1.0e-9
|
||
|
|
|| cross2(
|
||
|
|
xy(second_triangle[0]),
|
||
|
|
xy(second_triangle[1]),
|
||
|
|
xy(second_triangle[2]),
|
||
|
|
) <= 1.0e-9
|
||
|
|
{
|
||
|
|
return Err(BasementFoundationError::InvalidContour);
|
||
|
|
}
|
||
|
|
triangles[first] = first_triangle;
|
||
|
|
triangles[second] = second_triangle;
|
||
|
|
flipped = true;
|
||
|
|
break 'pairs;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !flipped {
|
||
|
|
return Ok(triangles);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Err(BasementFoundationError::InvalidContour)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn shared_edge(
|
||
|
|
first: [[f32; 3]; 3],
|
||
|
|
second: [[f32; 3]; 3],
|
||
|
|
) -> Option<([f32; 3], [f32; 3], [f32; 3], [f32; 3])> {
|
||
|
|
for edge in 0..3 {
|
||
|
|
let a = first[edge];
|
||
|
|
let b = first[(edge + 1) % 3];
|
||
|
|
if let Some(opposite) = second
|
||
|
|
.iter()
|
||
|
|
.copied()
|
||
|
|
.find(|point| !same_xy(*point, a) && !same_xy(*point, b))
|
||
|
|
{
|
||
|
|
let has_a = second.iter().any(|point| same_xy(*point, a));
|
||
|
|
let has_b = second.iter().any(|point| same_xy(*point, b));
|
||
|
|
if has_a && has_b {
|
||
|
|
let first_opposite = first[(edge + 2) % 3];
|
||
|
|
return Some((a, b, first_opposite, opposite));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
fn ring_has_edge(ring: &[[f32; 3]], a: [f32; 3], b: [f32; 3]) -> bool {
|
||
|
|
(0..ring.len()).any(|index| {
|
||
|
|
let first = ring[index];
|
||
|
|
let second = ring[(index + 1) % ring.len()];
|
||
|
|
(same_xy(first, a) && same_xy(second, b)) || (same_xy(first, b) && same_xy(second, a))
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn convex_quadrilateral(a: [f32; 3], b: [f32; 3], c: [f32; 3], d: [f32; 3]) -> bool {
|
||
|
|
let epsilon = 1.0e-9;
|
||
|
|
let c_side = cross2(xy(a), xy(b), xy(c));
|
||
|
|
let d_side = cross2(xy(a), xy(b), xy(d));
|
||
|
|
let a_side = cross2(xy(c), xy(d), xy(a));
|
||
|
|
let b_side = cross2(xy(c), xy(d), xy(b));
|
||
|
|
c_side * d_side < -epsilon && a_side * b_side < -epsilon
|
||
|
|
}
|
||
|
|
|
||
|
|
fn in_circle(a: [f32; 3], b: [f32; 3], c: [f32; 3], d: [f32; 3]) -> f64 {
|
||
|
|
let a = [
|
||
|
|
f64::from(a[0]) - f64::from(d[0]),
|
||
|
|
f64::from(a[1]) - f64::from(d[1]),
|
||
|
|
];
|
||
|
|
let b = [
|
||
|
|
f64::from(b[0]) - f64::from(d[0]),
|
||
|
|
f64::from(b[1]) - f64::from(d[1]),
|
||
|
|
];
|
||
|
|
let c = [
|
||
|
|
f64::from(c[0]) - f64::from(d[0]),
|
||
|
|
f64::from(c[1]) - f64::from(d[1]),
|
||
|
|
];
|
||
|
|
(a[0] * a[0] + a[1] * a[1]) * (b[0] * c[1] - b[1] * c[0])
|
||
|
|
- (b[0] * b[0] + b[1] * b[1]) * (a[0] * c[1] - a[1] * c[0])
|
||
|
|
+ (c[0] * c[0] + c[1] * c[1]) * (a[0] * b[1] - a[1] * b[0])
|
||
|
|
}
|
||
|
|
|
||
|
|
fn orient_ccw(mut triangle: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
|
||
|
|
if cross2(xy(triangle[0]), xy(triangle[1]), xy(triangle[2])) < 0.0 {
|
||
|
|
triangle.swap(1, 2);
|
||
|
|
}
|
||
|
|
triangle
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use fparkan_terrain_format::{TerrainFace28, TerrainSlotTable};
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn foundation_samples_lod0_planes_and_flattens_or_preserves_authored_inner_z() {
|
||
|
|
let mesh = two_lod_mesh();
|
||
|
|
let outer = [[0.4, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.4, 0.0]];
|
||
|
|
let inner = [
|
||
|
|
[1.0, 1.0, 10.0],
|
||
|
|
[3.0, 1.0, 11.0],
|
||
|
|
[3.0, 3.0, 12.0],
|
||
|
|
[1.0, 3.0, 13.0],
|
||
|
|
[1.0, 1.0, 10.0],
|
||
|
|
];
|
||
|
|
let (open_outer, _) = normalize_ring(&outer).expect("outer ring");
|
||
|
|
let snapped = normalize_outer_points(&open_outer, &[0, 1, 2], &mesh)
|
||
|
|
.expect("outer points lie within LOD0");
|
||
|
|
assert_eq!(snapped.first(), Some(&[0.0, 0.0]));
|
||
|
|
assert!(!snapped.contains(&[2.0, 0.0]));
|
||
|
|
assert!(native_segment_inserts_vertex(
|
||
|
|
[0.0, 0.0],
|
||
|
|
[4.0, 0.0],
|
||
|
|
[3.96875, 0.4991, 12.0]
|
||
|
|
));
|
||
|
|
assert!(!native_segment_inserts_vertex(
|
||
|
|
[0.0, 0.0],
|
||
|
|
[4.0, 0.0],
|
||
|
|
[2.0, 0.0, 12.0]
|
||
|
|
));
|
||
|
|
let terrain = [
|
||
|
|
(
|
||
|
|
0,
|
||
|
|
mesh.faces[0].flags,
|
||
|
|
terrain_face_positions(&mesh, 0).unwrap(),
|
||
|
|
),
|
||
|
|
(
|
||
|
|
1,
|
||
|
|
mesh.faces[1].flags,
|
||
|
|
terrain_face_positions(&mesh, 1).unwrap(),
|
||
|
|
),
|
||
|
|
(
|
||
|
|
2,
|
||
|
|
mesh.faces[2].flags,
|
||
|
|
terrain_face_positions(&mesh, 2).unwrap(),
|
||
|
|
),
|
||
|
|
];
|
||
|
|
let crossings = contour_crossings(&snapped, &terrain).expect("LOD0 crossings");
|
||
|
|
assert!(crossings.len() > 4);
|
||
|
|
assert_eq!(crossings.first().unwrap()[2], 0.0);
|
||
|
|
assert!(crossings
|
||
|
|
.iter()
|
||
|
|
.any(|crossing| same_xy(*crossing, [2.0, 0.0, 2.0])));
|
||
|
|
let expected_mean =
|
||
|
|
crossings.iter().map(|crossing| crossing[2]).sum::<f32>() / crossings.len() as f32;
|
||
|
|
|
||
|
|
let flat = build_basement_foundations(
|
||
|
|
&mesh,
|
||
|
|
&[BasementFootprint {
|
||
|
|
outer: &outer,
|
||
|
|
inner: &inner,
|
||
|
|
flatten_inner_to_mean_outer_z: true,
|
||
|
|
}],
|
||
|
|
)
|
||
|
|
.expect("flat foundation");
|
||
|
|
let placement = flat.placements[0];
|
||
|
|
assert!((placement.placement_delta - (10.0 - expected_mean)).abs() < 1.0e-6);
|
||
|
|
let flat_band = flat
|
||
|
|
.fragments
|
||
|
|
.iter()
|
||
|
|
.filter(|fragment| fragment.source_face.is_none())
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
assert!(flat
|
||
|
|
.fragments
|
||
|
|
.iter()
|
||
|
|
.filter_map(|fragment| fragment.source_face)
|
||
|
|
.all(|face| face < 3));
|
||
|
|
assert!(!flat_band.is_empty());
|
||
|
|
assert!(flat_band
|
||
|
|
.iter()
|
||
|
|
.all(|fragment| fragment.surface_mask == FullSurfaceMask(0x8300)));
|
||
|
|
for point in &inner[..4] {
|
||
|
|
assert!(flat_band
|
||
|
|
.iter()
|
||
|
|
.flat_map(|fragment| fragment.positions)
|
||
|
|
.any(|vertex| {
|
||
|
|
same_xy(vertex, *point) && (vertex[2] - expected_mean).abs() < 1.0e-6
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
let flat_area = flat_band
|
||
|
|
.iter()
|
||
|
|
.map(|fragment| {
|
||
|
|
let [a, b, c] = fragment.positions;
|
||
|
|
cross2(xy(a), xy(b), xy(c)).abs() * 0.5
|
||
|
|
})
|
||
|
|
.sum::<f64>();
|
||
|
|
assert!((flat_area - 12.0).abs() < 1.0e-5);
|
||
|
|
|
||
|
|
let authored = build_basement_foundations(
|
||
|
|
&mesh,
|
||
|
|
&[BasementFootprint {
|
||
|
|
outer: &outer,
|
||
|
|
inner: &inner,
|
||
|
|
flatten_inner_to_mean_outer_z: false,
|
||
|
|
}],
|
||
|
|
)
|
||
|
|
.expect("authored-height foundation");
|
||
|
|
let authored_band = authored
|
||
|
|
.fragments
|
||
|
|
.iter()
|
||
|
|
.filter(|fragment| fragment.source_face.is_none())
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
for point in &inner[..4] {
|
||
|
|
assert!(authored_band
|
||
|
|
.iter()
|
||
|
|
.flat_map(|fragment| fragment.positions)
|
||
|
|
.any(|vertex| {
|
||
|
|
same_xy(vertex, *point) && (vertex[2] - point[2]).abs() < 1.0e-6
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn foundation_rejects_outer_contours_outside_or_across_uncovered_terrain() {
|
||
|
|
let mesh = two_lod_mesh();
|
||
|
|
let outside = [[5.0, 1.0], [5.5, 1.0], [5.0, 1.5]];
|
||
|
|
let inner = [[5.1, 1.1, 0.0], [5.3, 1.1, 0.0], [5.1, 1.3, 0.0]];
|
||
|
|
assert_eq!(
|
||
|
|
build_basement_foundations(
|
||
|
|
&mesh,
|
||
|
|
&[BasementFootprint {
|
||
|
|
outer: &outside,
|
||
|
|
inner: &inner,
|
||
|
|
flatten_inner_to_mean_outer_z: true,
|
||
|
|
}],
|
||
|
|
),
|
||
|
|
Err(BasementFoundationError::OuterPointOutsideLod0 { point: 0 })
|
||
|
|
);
|
||
|
|
|
||
|
|
let mut islands = two_lod_mesh();
|
||
|
|
islands.positions = vec![
|
||
|
|
[0.0, 0.0, 0.0],
|
||
|
|
[1.0, 0.0, 0.0],
|
||
|
|
[0.0, 1.0, 0.0],
|
||
|
|
[2.0, 2.0, 0.0],
|
||
|
|
[3.0, 2.0, 0.0],
|
||
|
|
[2.0, 3.0, 0.0],
|
||
|
|
];
|
||
|
|
islands.faces.truncate(2);
|
||
|
|
islands.faces[0].vertices = [0, 1, 2];
|
||
|
|
islands.faces[1].vertices = [3, 4, 5];
|
||
|
|
islands.slots.slots_raw[0][2..4].copy_from_slice(&2_u16.to_le_bytes());
|
||
|
|
islands.slots.slots_raw[1][0..2].copy_from_slice(&2_u16.to_le_bytes());
|
||
|
|
islands.slots.slots_raw[1][2..4].copy_from_slice(&0_u16.to_le_bytes());
|
||
|
|
let outer = [[0.1, 0.1], [0.7, 0.1], [2.1, 2.1]];
|
||
|
|
let inner = [[0.2, 0.2, 0.0], [0.4, 0.2, 0.0], [0.2, 0.4, 0.0]];
|
||
|
|
assert_eq!(
|
||
|
|
build_basement_foundations(
|
||
|
|
&islands,
|
||
|
|
&[BasementFootprint {
|
||
|
|
outer: &outer,
|
||
|
|
inner: &inner,
|
||
|
|
flatten_inner_to_mean_outer_z: true,
|
||
|
|
}],
|
||
|
|
),
|
||
|
|
Err(BasementFoundationError::UncoveredOuterSegment { segment: 1 })
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn shared_diagonal_crossing_is_exact_and_counted_once() {
|
||
|
|
let first = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
|
||
|
|
let second = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [0.0, 1.0, 2.0]];
|
||
|
|
let edge_start = [0.8, 0.5];
|
||
|
|
let edge_end = [0.5, 0.8];
|
||
|
|
let first_interval =
|
||
|
|
segment_triangle_interval(edge_start, edge_end, first).expect("first face interval");
|
||
|
|
let second_interval =
|
||
|
|
segment_triangle_interval(edge_start, edge_end, second).expect("second face interval");
|
||
|
|
assert!((first_interval.1 - 0.5).abs() < 1.0e-9);
|
||
|
|
assert!((second_interval.0 - 0.5).abs() < 1.0e-9);
|
||
|
|
|
||
|
|
let ring = [[0.5, 0.2], [0.8, 0.5], [0.5, 0.8], [0.2, 0.5]];
|
||
|
|
let crossings = contour_crossings(
|
||
|
|
&ring,
|
||
|
|
&[
|
||
|
|
(0, FullSurfaceMask(1), first),
|
||
|
|
(1, FullSurfaceMask(2), second),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
.expect("shared-edge crossings");
|
||
|
|
let shared = crossings
|
||
|
|
.iter()
|
||
|
|
.filter(|crossing| {
|
||
|
|
distance_squared(
|
||
|
|
[f64::from(crossing[0]), f64::from(crossing[1])],
|
||
|
|
[0.65, 0.65],
|
||
|
|
) <= POINT_EPSILON_SQUARED
|
||
|
|
})
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
assert_eq!(
|
||
|
|
shared.len(),
|
||
|
|
1,
|
||
|
|
"shared-edge crossing duplicated: {shared:?}"
|
||
|
|
);
|
||
|
|
assert!((shared[0][2] - 0.65).abs() < 1.0e-6);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn annulus_preserves_collinear_outer_height_breakpoints_and_is_constrained_delaunay() {
|
||
|
|
let outer = [
|
||
|
|
[0.0, 0.0, 0.0],
|
||
|
|
[2.0, 0.0, 3.0],
|
||
|
|
[4.0, 0.0, 10.0],
|
||
|
|
[4.0, 4.0, 12.0],
|
||
|
|
[0.0, 4.0, 0.0],
|
||
|
|
];
|
||
|
|
let inner = vec![
|
||
|
|
[1.0, 1.0, 5.0],
|
||
|
|
[3.0, 1.0, 5.0],
|
||
|
|
[3.0, 3.0, 5.0],
|
||
|
|
[1.0, 3.0, 5.0],
|
||
|
|
];
|
||
|
|
let triangles = triangulate_annulus(&outer, inner.clone()).expect("annulus");
|
||
|
|
for edge in outer.windows(2) {
|
||
|
|
assert!(
|
||
|
|
triangles
|
||
|
|
.iter()
|
||
|
|
.any(|triangle| has_xy_edge(*triangle, edge[0], edge[1])),
|
||
|
|
"missing outer boundary segment {:?} -> {:?}",
|
||
|
|
edge[0],
|
||
|
|
edge[1]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
for point in outer {
|
||
|
|
assert!(
|
||
|
|
triangles
|
||
|
|
.iter()
|
||
|
|
.flat_map(|triangle| triangle.iter())
|
||
|
|
.any(|vertex| {
|
||
|
|
same_xy(*vertex, point) && (vertex[2] - point[2]).abs() < 1.0e-6
|
||
|
|
}),
|
||
|
|
"lost outer breakpoint {point:?}"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
assert_locally_delaunay(&triangles, &outer, &inner);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn constrained_annulus_handles_concave_rings_with_different_vertex_counts() {
|
||
|
|
let outer = [
|
||
|
|
[0.0, 0.0, 1.0],
|
||
|
|
[10.0, 0.0, 2.0],
|
||
|
|
[10.0, 3.0, 3.0],
|
||
|
|
[4.0, 3.0, 4.0],
|
||
|
|
[4.0, 10.0, 5.0],
|
||
|
|
[0.0, 10.0, 6.0],
|
||
|
|
];
|
||
|
|
let inner = vec![
|
||
|
|
[1.0, 1.0, 3.0],
|
||
|
|
[3.0, 1.0, 3.0],
|
||
|
|
[3.0, 1.8, 3.0],
|
||
|
|
[2.2, 1.8, 3.0],
|
||
|
|
[2.2, 2.4, 3.0],
|
||
|
|
[3.0, 2.4, 3.0],
|
||
|
|
[3.0, 3.0, 3.0],
|
||
|
|
[1.0, 3.0, 3.0],
|
||
|
|
];
|
||
|
|
let band = triangulate_annulus(&outer, inner.clone()).expect("constrained annulus");
|
||
|
|
let area = band
|
||
|
|
.iter()
|
||
|
|
.map(|triangle| cross2(xy(triangle[0]), xy(triangle[1]), xy(triangle[2])).abs() * 0.5)
|
||
|
|
.sum::<f64>();
|
||
|
|
let expected = ring_area(&outer.iter().copied().map(xy).collect::<Vec<_>>()).abs()
|
||
|
|
- ring_area(&inner.iter().copied().map(xy).collect::<Vec<_>>()).abs();
|
||
|
|
assert!(
|
||
|
|
(area - expected).abs() < 1.0e-5,
|
||
|
|
"area {area}, expected {expected}"
|
||
|
|
);
|
||
|
|
|
||
|
|
let outer_xy = outer.iter().copied().map(xy).collect::<Vec<_>>();
|
||
|
|
let inner_xy = inner.iter().copied().map(xy).collect::<Vec<_>>();
|
||
|
|
for triangle in &band {
|
||
|
|
let centroid = triangle_centroid(*triangle);
|
||
|
|
assert!(point_in_ring(centroid, &outer_xy));
|
||
|
|
assert!(!point_in_ring(centroid, &inner_xy));
|
||
|
|
}
|
||
|
|
for first in 0..band.len() {
|
||
|
|
for second in (first + 1)..band.len() {
|
||
|
|
assert!(!positive_area_overlap(band[first], band[second]));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert_locally_delaunay(&band, &outer, &inner);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn multiple_disjoint_footprints_clip_canonical_outside_once() {
|
||
|
|
let mesh = two_lod_mesh();
|
||
|
|
let first_outer = [[0.6, 0.6], [1.7, 0.6], [1.7, 1.7], [0.6, 1.7]];
|
||
|
|
let first_inner = [
|
||
|
|
[0.9, 0.9, 5.0],
|
||
|
|
[1.4, 0.9, 5.0],
|
||
|
|
[1.4, 1.4, 5.0],
|
||
|
|
[0.9, 1.4, 5.0],
|
||
|
|
];
|
||
|
|
let second_outer = [[2.3, 2.3], [3.4, 2.3], [3.4, 3.4], [2.3, 3.4]];
|
||
|
|
let second_inner = [
|
||
|
|
[2.6, 2.6, 7.0],
|
||
|
|
[3.1, 2.6, 7.0],
|
||
|
|
[3.1, 3.1, 7.0],
|
||
|
|
[2.6, 3.1, 7.0],
|
||
|
|
];
|
||
|
|
let output = build_basement_foundations(
|
||
|
|
&mesh,
|
||
|
|
&[
|
||
|
|
BasementFootprint {
|
||
|
|
outer: &first_outer,
|
||
|
|
inner: &first_inner,
|
||
|
|
flatten_inner_to_mean_outer_z: true,
|
||
|
|
},
|
||
|
|
BasementFootprint {
|
||
|
|
outer: &second_outer,
|
||
|
|
inner: &second_inner,
|
||
|
|
flatten_inner_to_mean_outer_z: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
)
|
||
|
|
.expect("two disjoint foundations");
|
||
|
|
assert_eq!(output.placements.len(), 2);
|
||
|
|
let terrain = output
|
||
|
|
.fragments
|
||
|
|
.iter()
|
||
|
|
.filter(|fragment| fragment.source_face.is_some())
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let bands = output
|
||
|
|
.fragments
|
||
|
|
.iter()
|
||
|
|
.filter(|fragment| fragment.source_face.is_none())
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
assert!(!terrain.is_empty());
|
||
|
|
assert!(!bands.is_empty());
|
||
|
|
for fragment in &terrain {
|
||
|
|
let centroid = triangle_centroid(fragment.positions);
|
||
|
|
assert!(!point_strictly_in_ring(centroid, &ring_f64(&first_outer)));
|
||
|
|
assert!(!point_strictly_in_ring(centroid, &ring_f64(&second_outer)));
|
||
|
|
}
|
||
|
|
for first in 0..terrain.len() {
|
||
|
|
for second in (first + 1)..terrain.len() {
|
||
|
|
assert!(!positive_area_overlap(
|
||
|
|
terrain[first].positions,
|
||
|
|
terrain[second].positions
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for band in bands {
|
||
|
|
let centroid = triangle_centroid(band.positions);
|
||
|
|
assert!(
|
||
|
|
point_in_ring(centroid, &ring_f64(&first_outer))
|
||
|
|
|| point_in_ring(centroid, &ring_f64(&second_outer))
|
||
|
|
);
|
||
|
|
assert!(!point_in_ring(
|
||
|
|
centroid,
|
||
|
|
&ring_f64(&first_inner.iter().map(|p| [p[0], p[1]]).collect::<Vec<_>>())
|
||
|
|
));
|
||
|
|
assert!(!point_in_ring(
|
||
|
|
centroid,
|
||
|
|
&ring_f64(
|
||
|
|
&second_inner
|
||
|
|
.iter()
|
||
|
|
.map(|p| [p[0], p[1]])
|
||
|
|
.collect::<Vec<_>>()
|
||
|
|
)
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn overlapping_footprints_are_explicitly_rejected() {
|
||
|
|
let mesh = two_lod_mesh();
|
||
|
|
let outer_a = [[0.5, 0.5], [2.5, 0.5], [2.5, 2.5], [0.5, 2.5]];
|
||
|
|
let inner_a = [
|
||
|
|
[1.0, 1.0, 3.0],
|
||
|
|
[2.0, 1.0, 3.0],
|
||
|
|
[2.0, 2.0, 3.0],
|
||
|
|
[1.0, 2.0, 3.0],
|
||
|
|
];
|
||
|
|
let outer_b = [[1.5, 1.5], [3.5, 1.5], [3.5, 3.5], [1.5, 3.5]];
|
||
|
|
let inner_b = [
|
||
|
|
[2.0, 2.0, 3.0],
|
||
|
|
[3.0, 2.0, 3.0],
|
||
|
|
[3.0, 3.0, 3.0],
|
||
|
|
[2.0, 3.0, 3.0],
|
||
|
|
];
|
||
|
|
assert_eq!(
|
||
|
|
build_basement_foundations(
|
||
|
|
&mesh,
|
||
|
|
&[
|
||
|
|
BasementFootprint {
|
||
|
|
outer: &outer_a,
|
||
|
|
inner: &inner_a,
|
||
|
|
flatten_inner_to_mean_outer_z: true
|
||
|
|
},
|
||
|
|
BasementFootprint {
|
||
|
|
outer: &outer_b,
|
||
|
|
inner: &inner_b,
|
||
|
|
flatten_inner_to_mean_outer_z: true
|
||
|
|
},
|
||
|
|
],
|
||
|
|
),
|
||
|
|
Err(BasementFoundationError::OverlappingFootprints)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn generated_flags_follow_native_normal_z_branch() {
|
||
|
|
let positive = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
|
||
|
|
let negative = [positive[0], positive[2], positive[1]];
|
||
|
|
assert_eq!(basement_surface_mask(positive), FullSurfaceMask(0x8300));
|
||
|
|
assert_eq!(basement_surface_mask(negative), FullSurfaceMask(0x0300));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn foundation_rejects_inner_ring_that_is_not_nested() {
|
||
|
|
let mesh = two_lod_mesh();
|
||
|
|
let outer = [[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
|
||
|
|
let inner = [
|
||
|
|
[3.0, 1.0, 0.0],
|
||
|
|
[5.0, 1.0, 0.0],
|
||
|
|
[5.0, 3.0, 0.0],
|
||
|
|
[3.0, 3.0, 0.0],
|
||
|
|
];
|
||
|
|
assert_eq!(
|
||
|
|
build_basement_foundations(
|
||
|
|
&mesh,
|
||
|
|
&[BasementFootprint {
|
||
|
|
outer: &outer,
|
||
|
|
inner: &inner,
|
||
|
|
flatten_inner_to_mean_outer_z: true
|
||
|
|
}],
|
||
|
|
),
|
||
|
|
Err(BasementFoundationError::InvalidContour)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn assert_locally_delaunay(
|
||
|
|
triangles: &[[[f32; 3]; 3]],
|
||
|
|
outer: &[[f32; 3]],
|
||
|
|
inner: &[[f32; 3]],
|
||
|
|
) {
|
||
|
|
for first in 0..triangles.len() {
|
||
|
|
for second in (first + 1)..triangles.len() {
|
||
|
|
let Some((a, b, c, d)) = shared_edge(triangles[first], triangles[second]) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
if ring_has_edge(outer, a, b)
|
||
|
|
|| ring_has_edge(inner, a, b)
|
||
|
|
|| !convex_quadrilateral(a, b, c, d)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
assert!(
|
||
|
|
in_circle(a, b, c, d) <= 1.0e-8,
|
||
|
|
"illegal diagonal {a:?}->{b:?}"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn has_xy_edge(triangle: [[f32; 3]; 3], first: [f32; 3], second: [f32; 3]) -> bool {
|
||
|
|
(0..3).any(|index| {
|
||
|
|
(same_xy(triangle[index], first) && same_xy(triangle[(index + 1) % 3], second))
|
||
|
|
|| (same_xy(triangle[index], second) && same_xy(triangle[(index + 1) % 3], first))
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn triangle_centroid(triangle: [[f32; 3]; 3]) -> [f64; 2] {
|
||
|
|
[
|
||
|
|
triangle
|
||
|
|
.iter()
|
||
|
|
.map(|point| f64::from(point[0]))
|
||
|
|
.sum::<f64>()
|
||
|
|
/ 3.0,
|
||
|
|
triangle
|
||
|
|
.iter()
|
||
|
|
.map(|point| f64::from(point[1]))
|
||
|
|
.sum::<f64>()
|
||
|
|
/ 3.0,
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
fn point_strictly_in_ring(point: [f64; 2], ring: &[[f64; 2]]) -> bool {
|
||
|
|
!((0..ring.len())
|
||
|
|
.any(|index| point_on_segment(point, ring[index], ring[(index + 1) % ring.len()])))
|
||
|
|
&& point_in_ring(point, ring)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn ring_f64(ring: &[[f32; 2]]) -> Vec<[f64; 2]> {
|
||
|
|
ring.iter()
|
||
|
|
.map(|point| [f64::from(point[0]), f64::from(point[1])])
|
||
|
|
.collect()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn two_lod_mesh() -> LandMeshDocument {
|
||
|
|
let positions = vec![
|
||
|
|
[0.0, 0.0, 0.0],
|
||
|
|
[2.0, 0.0, 2.0],
|
||
|
|
[4.0, 0.0, 4.0],
|
||
|
|
[0.0, 4.0, 4.0],
|
||
|
|
[4.0, 4.0, 8.0],
|
||
|
|
[0.0, 0.0, 100.0],
|
||
|
|
[4.0, 0.0, 100.0],
|
||
|
|
[0.0, 4.0, 100.0],
|
||
|
|
];
|
||
|
|
let face = |flags, vertices| TerrainFace28 {
|
||
|
|
flags: FullSurfaceMask(flags),
|
||
|
|
material_tag: 0,
|
||
|
|
aux_tag: 0,
|
||
|
|
vertices,
|
||
|
|
neighbors: [None; 3],
|
||
|
|
tail_raw: [0; 8],
|
||
|
|
raw: [0; 28],
|
||
|
|
};
|
||
|
|
let faces = vec![
|
||
|
|
face(1, [0, 1, 3]),
|
||
|
|
face(2, [1, 4, 3]),
|
||
|
|
face(4, [1, 2, 4]),
|
||
|
|
face(8, [5, 6, 7]),
|
||
|
|
];
|
||
|
|
let mut nodes_raw = vec![u8::MAX; 38];
|
||
|
|
nodes_raw[8..10].copy_from_slice(&0_u16.to_le_bytes());
|
||
|
|
nodes_raw[10..12].copy_from_slice(&1_u16.to_le_bytes());
|
||
|
|
let mut slots_raw = vec![[0_u8; 68]; 2];
|
||
|
|
slots_raw[0][2..4].copy_from_slice(&3_u16.to_le_bytes());
|
||
|
|
slots_raw[1][0..2].copy_from_slice(&3_u16.to_le_bytes());
|
||
|
|
slots_raw[1][2..4].copy_from_slice(&1_u16.to_le_bytes());
|
||
|
|
LandMeshDocument {
|
||
|
|
streams: Vec::new(),
|
||
|
|
nodes_raw,
|
||
|
|
slots: TerrainSlotTable {
|
||
|
|
header_raw: Vec::new(),
|
||
|
|
slots_raw,
|
||
|
|
},
|
||
|
|
positions,
|
||
|
|
normals: Vec::new(),
|
||
|
|
uv0: Vec::new(),
|
||
|
|
accelerator: Vec::new(),
|
||
|
|
aux14: Vec::new(),
|
||
|
|
aux18: Vec::new(),
|
||
|
|
faces,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn positive_area_overlap(first: [[f32; 3]; 3], second: [[f32; 3]; 3]) -> bool {
|
||
|
|
for triangle in [first, second] {
|
||
|
|
for edge in 0..3 {
|
||
|
|
let a = xy(triangle[edge]);
|
||
|
|
let b = xy(triangle[(edge + 1) % 3]);
|
||
|
|
let axis = [b[1] - a[1], a[0] - b[0]];
|
||
|
|
let project = |vertices: [[f32; 3]; 3]| {
|
||
|
|
vertices.map(|vertex| {
|
||
|
|
let point = xy(vertex);
|
||
|
|
point[0] * axis[0] + point[1] * axis[1]
|
||
|
|
})
|
||
|
|
};
|
||
|
|
let first_projection = project(first);
|
||
|
|
let second_projection = project(second);
|
||
|
|
let first_min = first_projection
|
||
|
|
.iter()
|
||
|
|
.copied()
|
||
|
|
.fold(f64::INFINITY, f64::min);
|
||
|
|
let first_max = first_projection
|
||
|
|
.iter()
|
||
|
|
.copied()
|
||
|
|
.fold(f64::NEG_INFINITY, f64::max);
|
||
|
|
let second_min = second_projection
|
||
|
|
.iter()
|
||
|
|
.copied()
|
||
|
|
.fold(f64::INFINITY, f64::min);
|
||
|
|
let second_max = second_projection
|
||
|
|
.iter()
|
||
|
|
.copied()
|
||
|
|
.fold(f64::NEG_INFINITY, f64::max);
|
||
|
|
if first_max <= second_min + 1.0e-8 || second_max <= first_min + 1.0e-8 {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
true
|
||
|
|
}
|
||
|
|
}
|