use crate::backend::default::geometry::{GeometryCore, GeometryType};
use crate::cityjson::core::boundary::Boundary;
use crate::cityjson::core::vertex::VertexRef;
use crate::error::{Error, Result};
use crate::resources::id::ResourceId;
use crate::resources::mapping::SemanticOrMaterialMap;
use crate::resources::mapping::textures::TextureMapCore;
use crate::resources::storage::StringStorage;
use crate::v2_0::boundary::BoundaryType;
use crate::v2_0::vertex::VertexIndex;
pub(crate) trait GeometryValidationContext<VR: VertexRef, RR: ResourceId> {
fn semantic_exists(&self, id: RR) -> bool;
fn material_exists(&self, id: RR) -> bool;
fn texture_exists(&self, id: RR) -> bool;
fn uv_exists(&self, id: VertexIndex<VR>) -> bool;
fn regular_vertex_exists(&self, id: VertexIndex<VR>) -> bool;
fn template_vertex_exists(&self, id: VertexIndex<VR>) -> bool;
fn template_geometry_exists(&self, id: RR) -> bool;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum BoundaryVertexSource {
Regular,
Template,
}
pub(crate) fn validate_stored_geometry<VR, RR, SS, C>(
geometry: &GeometryCore<VR, RR, SS>,
context: &C,
) -> Result<()>
where
VR: VertexRef,
RR: ResourceId,
SS: StringStorage,
C: GeometryValidationContext<VR, RR>,
{
validate_stored_geometry_for_boundary_source(geometry, context, BoundaryVertexSource::Regular)
}
pub(crate) fn validate_stored_geometry_for_boundary_source<VR, RR, SS, C>(
geometry: &GeometryCore<VR, RR, SS>,
context: &C,
boundary_vertex_source: BoundaryVertexSource,
) -> Result<()>
where
VR: VertexRef,
RR: ResourceId,
SS: StringStorage,
C: GeometryValidationContext<VR, RR>,
{
if *geometry.type_geometry() == GeometryType::GeometryInstance {
return validate_instance_isolation(geometry, context);
}
if geometry.instance().is_some() {
return Err(Error::InvalidGeometry(format!(
"{} must not carry GeometryInstance payload",
geometry.type_geometry()
)));
}
let boundary = validate_boundary_present(geometry)?;
validate_boundary_consistent(boundary)?;
validate_boundary_kind(*geometry.type_geometry(), boundary)?;
validate_boundary_vertices(boundary, context, boundary_vertex_source)?;
if let Some(semantics) = geometry.semantics() {
validate_semantic_kind(*geometry.type_geometry(), semantics, boundary, context)?;
}
if let Some(themes) = geometry.materials() {
for (theme_name, material_map) in themes {
validate_material_kind(
*geometry.type_geometry(),
material_map,
boundary,
context,
theme_name,
)?;
}
}
if let Some(themes) = geometry.textures() {
for (theme_name, texture_map) in themes {
validate_texture_map(theme_name, texture_map, boundary, context)?;
}
}
Ok(())
}
fn validate_boundary_vertices<VR, RR, C>(
boundary: &Boundary<VR>,
context: &C,
boundary_vertex_source: BoundaryVertexSource,
) -> Result<()>
where
VR: VertexRef,
RR: ResourceId,
C: GeometryValidationContext<VR, RR>,
{
let pool_name = match boundary_vertex_source {
BoundaryVertexSource::Regular => "regular",
BoundaryVertexSource::Template => "template",
};
for (index, vertex_ref) in boundary.vertices().iter().enumerate() {
let exists = match boundary_vertex_source {
BoundaryVertexSource::Regular => context.regular_vertex_exists(*vertex_ref),
BoundaryVertexSource::Template => context.template_vertex_exists(*vertex_ref),
};
if !exists {
return Err(Error::InvalidGeometry(format!(
"boundary vertex {index} references missing {pool_name} vertex {vertex_ref}"
)));
}
}
Ok(())
}
pub(crate) fn validate_instance_isolation<VR, RR, SS, C>(
geometry: &GeometryCore<VR, RR, SS>,
context: &C,
) -> Result<()>
where
VR: VertexRef,
RR: ResourceId,
SS: StringStorage,
C: GeometryValidationContext<VR, RR>,
{
if geometry.boundaries().is_some() {
return Err(Error::InvalidGeometry(
"GeometryInstance must not contain a boundary".to_string(),
));
}
if geometry.semantics().is_some() {
return Err(Error::InvalidGeometry(
"GeometryInstance must not contain semantics".to_string(),
));
}
if geometry.materials().is_some() {
return Err(Error::InvalidGeometry(
"GeometryInstance must not contain materials".to_string(),
));
}
if geometry.textures().is_some() {
return Err(Error::InvalidGeometry(
"GeometryInstance must not contain textures".to_string(),
));
}
let instance = geometry.instance().ok_or_else(|| {
Error::IncompleteGeometry(
"GeometryInstance must have instance data (template + reference + transform)"
.to_string(),
)
})?;
if !context.template_geometry_exists(*instance.template()) {
return Err(Error::InvalidGeometry(format!(
"GeometryInstance references missing template {}",
instance.template()
)));
}
if !context.regular_vertex_exists(*instance.reference_point()) {
return Err(Error::InvalidGeometry(format!(
"GeometryInstance references missing reference point {}",
instance.reference_point()
)));
}
Ok(())
}
pub(crate) fn validate_boundary_present<VR, RR, SS>(
geometry: &GeometryCore<VR, RR, SS>,
) -> Result<&Boundary<VR>>
where
VR: VertexRef,
RR: ResourceId,
SS: StringStorage,
{
geometry.boundaries().ok_or_else(|| {
Error::IncompleteGeometry(format!(
"{} geometry must have a boundary",
geometry.type_geometry()
))
})
}
pub(crate) fn validate_boundary_consistent<VR: VertexRef>(boundary: &Boundary<VR>) -> Result<()> {
if !boundary.is_consistent() {
return Err(Error::InvalidGeometry(
"boundary offsets are inconsistent".to_string(),
));
}
Ok(())
}
pub(crate) fn validate_boundary_kind<VR: VertexRef>(
type_geometry: GeometryType,
boundary: &Boundary<VR>,
) -> Result<()> {
validate_boundary_shape(type_geometry, boundary)?;
let boundary_type = boundary.check_type();
if !boundary_type_matches(type_geometry, boundary_type) {
return Err(Error::InvalidGeometryType {
expected: format!("{type_geometry}"),
found: format!("{boundary_type}"),
});
}
Ok(())
}
pub(crate) fn validate_semantic_kind<VR, RR, C>(
type_geometry: GeometryType,
semantics: &SemanticOrMaterialMap<VR, RR>,
boundary: &Boundary<VR>,
context: &C,
) -> Result<()>
where
VR: VertexRef,
RR: ResourceId,
C: GeometryValidationContext<VR, RR>,
{
validate_resource_map("semantic", type_geometry, semantics, boundary, |id| {
context.semantic_exists(id)
})
}
pub(crate) fn validate_material_kind<VR, RR, C>(
type_geometry: GeometryType,
materials: &SemanticOrMaterialMap<VR, RR>,
boundary: &Boundary<VR>,
context: &C,
theme_name: &impl std::fmt::Display,
) -> Result<()>
where
VR: VertexRef,
RR: ResourceId,
C: GeometryValidationContext<VR, RR>,
{
validate_resource_map(
&format!("material theme '{theme_name}'"),
type_geometry,
materials,
boundary,
|id| context.material_exists(id),
)
}
fn validate_resource_map<VR, RR>(
map_name: &str,
type_geometry: GeometryType,
map: &SemanticOrMaterialMap<VR, RR>,
boundary: &Boundary<VR>,
resource_exists: impl Fn(RR) -> bool,
) -> Result<()>
where
VR: VertexRef,
RR: ResourceId,
{
let expected_bucket = expected_bucket_name(type_geometry)?;
let populated_bucket_count = usize::from(!map.points().is_empty())
+ usize::from(!map.linestrings().is_empty())
+ usize::from(!map.surfaces().is_empty());
if populated_bucket_count != 1 {
return Err(Error::InvalidGeometry(format!(
"{map_name} must populate exactly one assignment bucket"
)));
}
let (actual_bucket, assignments) = if !map.points().is_empty() {
("points", map.points())
} else if !map.linestrings().is_empty() {
("linestrings", map.linestrings())
} else if !map.surfaces().is_empty() {
("surfaces", map.surfaces())
} else {
unreachable!("populated_bucket_count == 1 guarantees one populated bucket");
};
let expected_count = match type_geometry {
GeometryType::MultiPoint => boundary.vertices().len(),
GeometryType::MultiLineString => boundary.rings().len(),
GeometryType::MultiSurface
| GeometryType::CompositeSurface
| GeometryType::Solid
| GeometryType::MultiSolid
| GeometryType::CompositeSolid => boundary.surfaces().len(),
GeometryType::GeometryInstance => {
return Err(Error::InvalidGeometry(
"GeometryInstance must not have semantic or material mappings".to_string(),
));
}
};
if actual_bucket != expected_bucket {
return Err(Error::InvalidGeometry(format!(
"{map_name} for {type_geometry} must only use the '{expected_bucket}' bucket"
)));
}
if assignments.len() != expected_count {
return Err(Error::InvalidGeometry(format!(
"{map_name} {actual_bucket} bucket length {} does not match expected primitive count {expected_count}",
assignments.len()
)));
}
for (index, assignment) in assignments.iter().enumerate() {
if let Some(resource_id) = assignment
&& !resource_exists(*resource_id)
{
return Err(Error::InvalidGeometry(format!(
"{map_name} assignment {index} references missing resource {resource_id}"
)));
}
}
Ok(())
}
fn validate_texture_map<VR, RR, C>(
theme_name: &impl std::fmt::Display,
texture_map: &TextureMapCore<VR, RR>,
boundary: &Boundary<VR>,
context: &C,
) -> Result<()>
where
VR: VertexRef,
RR: ResourceId,
C: GeometryValidationContext<VR, RR>,
{
if texture_map.rings().len() != boundary.rings().len() {
return Err(Error::InvalidGeometry(format!(
"texture theme '{theme_name}' ring count {} does not match boundary ring count {}",
texture_map.rings().len(),
boundary.rings().len()
)));
}
if texture_map.ring_textures().len() != boundary.rings().len() {
return Err(Error::InvalidGeometry(format!(
"texture theme '{theme_name}' ring_textures count {} does not match boundary ring count {}",
texture_map.ring_textures().len(),
boundary.rings().len()
)));
}
if texture_map.vertices().len() != boundary.vertices().len() {
return Err(Error::InvalidGeometry(format!(
"texture theme '{theme_name}' UV vertex count {} does not match boundary vertex count {}",
texture_map.vertices().len(),
boundary.vertices().len()
)));
}
for (ring_index, ring_start) in texture_map.rings().iter().enumerate() {
if *ring_start != boundary.rings()[ring_index] {
return Err(Error::InvalidGeometry(format!(
"texture theme '{theme_name}' ring {ring_index} start {ring_start} does not match boundary ring start {}",
boundary.rings()[ring_index]
)));
}
let slice_start = ring_start.to_usize();
let slice_end = boundary
.rings()
.get(ring_index + 1)
.map_or(boundary.vertices().len(), VertexIndex::to_usize);
let uv_slice = &texture_map.vertices()[slice_start..slice_end];
match texture_map.ring_textures()[ring_index] {
Some(texture_id) => {
if !context.texture_exists(texture_id) {
return Err(Error::InvalidGeometry(format!(
"texture theme '{theme_name}' ring {ring_index} references missing texture {texture_id}"
)));
}
for (uv_index, uv_ref) in uv_slice.iter().enumerate() {
let uv_ref = uv_ref.ok_or_else(|| {
Error::InvalidGeometry(format!(
"texture theme '{theme_name}' ring {ring_index} has null UV at boundary occurrence {uv_index}"
))
})?;
if !context.uv_exists(uv_ref) {
return Err(Error::InvalidGeometry(format!(
"texture theme '{theme_name}' ring {ring_index} references missing UV {uv_ref}"
)));
}
}
}
None => {
if uv_slice.iter().any(Option::is_some) {
return Err(Error::InvalidGeometry(format!(
"texture theme '{theme_name}' ring {ring_index} is untextured but carries UV payload"
)));
}
}
}
}
Ok(())
}
fn expected_bucket_name(type_geometry: GeometryType) -> Result<&'static str> {
match type_geometry {
GeometryType::MultiPoint => Ok("points"),
GeometryType::MultiLineString => Ok("linestrings"),
GeometryType::MultiSurface
| GeometryType::CompositeSurface
| GeometryType::Solid
| GeometryType::MultiSolid
| GeometryType::CompositeSolid => Ok("surfaces"),
GeometryType::GeometryInstance => Err(Error::InvalidGeometry(
"GeometryInstance must not carry semantic or material mappings".to_string(),
)),
}
}
fn validate_boundary_shape<VR: VertexRef>(
type_geometry: GeometryType,
boundary: &Boundary<VR>,
) -> Result<()> {
match type_geometry {
GeometryType::MultiPoint => {
require_non_empty(type_geometry, "vertices", boundary.vertices().is_empty())?;
require_empty(type_geometry, "rings", !boundary.rings().is_empty())?;
require_empty(type_geometry, "surfaces", !boundary.surfaces().is_empty())?;
require_empty(type_geometry, "shells", !boundary.shells().is_empty())?;
require_empty(type_geometry, "solids", !boundary.solids().is_empty())?;
}
GeometryType::MultiLineString => {
require_non_empty(type_geometry, "vertices", boundary.vertices().is_empty())?;
require_non_empty(type_geometry, "rings", boundary.rings().is_empty())?;
require_empty(type_geometry, "surfaces", !boundary.surfaces().is_empty())?;
require_empty(type_geometry, "shells", !boundary.shells().is_empty())?;
require_empty(type_geometry, "solids", !boundary.solids().is_empty())?;
}
GeometryType::MultiSurface | GeometryType::CompositeSurface => {
require_non_empty(type_geometry, "vertices", boundary.vertices().is_empty())?;
require_non_empty(type_geometry, "rings", boundary.rings().is_empty())?;
require_non_empty(type_geometry, "surfaces", boundary.surfaces().is_empty())?;
require_empty(type_geometry, "shells", !boundary.shells().is_empty())?;
require_empty(type_geometry, "solids", !boundary.solids().is_empty())?;
}
GeometryType::Solid => {
require_non_empty(type_geometry, "vertices", boundary.vertices().is_empty())?;
require_non_empty(type_geometry, "rings", boundary.rings().is_empty())?;
require_non_empty(type_geometry, "surfaces", boundary.surfaces().is_empty())?;
require_non_empty(type_geometry, "shells", boundary.shells().is_empty())?;
require_empty(type_geometry, "solids", !boundary.solids().is_empty())?;
}
GeometryType::MultiSolid | GeometryType::CompositeSolid => {
require_non_empty(type_geometry, "vertices", boundary.vertices().is_empty())?;
require_non_empty(type_geometry, "rings", boundary.rings().is_empty())?;
require_non_empty(type_geometry, "surfaces", boundary.surfaces().is_empty())?;
require_non_empty(type_geometry, "shells", boundary.shells().is_empty())?;
require_non_empty(type_geometry, "solids", boundary.solids().is_empty())?;
}
GeometryType::GeometryInstance => unreachable!("GeometryInstance has no boundary"),
}
validate_non_empty_segments(
"ring",
"vertex",
boundary.rings(),
boundary.vertices().len(),
)?;
validate_non_empty_segments(
"surface",
"ring",
boundary.surfaces(),
boundary.rings().len(),
)?;
validate_non_empty_segments(
"shell",
"surface",
boundary.shells(),
boundary.surfaces().len(),
)?;
validate_non_empty_segments("solid", "shell", boundary.solids(), boundary.shells().len())?;
Ok(())
}
fn require_non_empty(type_geometry: GeometryType, level_name: &str, missing: bool) -> Result<()> {
if missing {
return Err(Error::InvalidGeometry(format!(
"{type_geometry} requires non-empty {level_name}"
)));
}
Ok(())
}
fn require_empty(type_geometry: GeometryType, level_name: &str, present: bool) -> Result<()> {
if present {
return Err(Error::InvalidGeometry(format!(
"{type_geometry} must not carry {level_name}"
)));
}
Ok(())
}
fn validate_non_empty_segments<VR: VertexRef>(
parent_name: &str,
child_name: &str,
offsets: &[VertexIndex<VR>],
child_len: usize,
) -> Result<()> {
for (index, start) in offsets.iter().enumerate() {
let end = offsets
.get(index + 1)
.map_or(child_len, VertexIndex::to_usize);
if start.to_usize() == end {
return Err(Error::InvalidGeometry(format!(
"{parent_name} {index} must own at least one {child_name}"
)));
}
}
Ok(())
}
fn boundary_type_matches(type_geometry: GeometryType, boundary_type: BoundaryType) -> bool {
matches!(
(type_geometry, boundary_type),
(GeometryType::MultiPoint, BoundaryType::MultiPoint)
| (GeometryType::MultiLineString, BoundaryType::MultiLineString)
| (
GeometryType::MultiSurface | GeometryType::CompositeSurface,
BoundaryType::MultiOrCompositeSurface
)
| (GeometryType::Solid, BoundaryType::Solid)
| (
GeometryType::MultiSolid | GeometryType::CompositeSolid,
BoundaryType::MultiOrCompositeSolid
)
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::default::geometry::{AffineTransform3D, GeometryInstanceData, LoD};
use crate::backend::default::geometry::{ThemedMaterials, ThemedTextures};
use crate::cityjson::core::appearance::ThemeName;
use crate::resources::id::ResourceId32;
use crate::resources::storage::OwnedStringStorage;
use crate::v2_0::boundary::nested::{
BoundaryNestedMultiOrCompositeSurface32, BoundaryNestedMultiPoint32,
};
type B = Boundary<u32>;
type G = GeometryCore<u32, ResourceId32, OwnedStringStorage>;
struct TestContext {
semantics: u32,
materials: u32,
textures: u32,
uvs: u32,
vertices: u32,
template_vertices: u32,
templates: u32,
}
impl Default for TestContext {
fn default() -> Self {
Self {
semantics: 0,
materials: 0,
textures: 0,
uvs: 0,
vertices: 8,
template_vertices: 8,
templates: 0,
}
}
}
impl GeometryValidationContext<u32, ResourceId32> for TestContext {
fn semantic_exists(&self, id: ResourceId32) -> bool {
id.generation() == 0 && id.index() < self.semantics
}
fn material_exists(&self, id: ResourceId32) -> bool {
id.generation() == 0 && id.index() < self.materials
}
fn texture_exists(&self, id: ResourceId32) -> bool {
id.generation() == 0 && id.index() < self.textures
}
fn uv_exists(&self, id: VertexIndex<u32>) -> bool {
id.value() < self.uvs
}
fn regular_vertex_exists(&self, id: VertexIndex<u32>) -> bool {
id.value() < self.vertices
}
fn template_vertex_exists(&self, id: VertexIndex<u32>) -> bool {
id.value() < self.template_vertices
}
fn template_geometry_exists(&self, id: ResourceId32) -> bool {
id.generation() == 0 && id.index() < self.templates
}
}
fn rid(index: u32) -> ResourceId32 {
ResourceId32::new(index, 0)
}
fn vi(index: u32) -> VertexIndex<u32> {
VertexIndex::new(index)
}
fn theme(value: &str) -> ThemeName<OwnedStringStorage> {
ThemeName::new(value.to_string())
}
fn s1_boundary() -> B {
let nested: BoundaryNestedMultiOrCompositeSurface32 = vec![
vec![vec![0u32, 1, 4], vec![0, 2, 1]],
vec![vec![2, 3, 4, 5]],
];
nested.try_into().unwrap()
}
fn multipoint_boundary() -> B {
let nested: BoundaryNestedMultiPoint32 = vec![0u32, 1, 2];
nested.into()
}
fn surface_map(
assignments: &[Option<ResourceId32>],
) -> SemanticOrMaterialMap<u32, ResourceId32> {
let mut map = SemanticOrMaterialMap::new();
for assignment in assignments {
map.add_surface(*assignment);
}
map
}
fn dense_texture_map(boundary: &B) -> TextureMapCore<u32, ResourceId32> {
let mut texture_map = TextureMapCore::default();
for ring_start in boundary.rings() {
texture_map.add_ring(*ring_start);
}
texture_map.add_ring_texture(Some(rid(0)));
texture_map.add_ring_texture(None);
texture_map.add_ring_texture(Some(rid(0)));
texture_map.add_vertex(Some(vi(0)));
texture_map.add_vertex(Some(vi(1)));
texture_map.add_vertex(Some(vi(2)));
texture_map.add_vertex(None);
texture_map.add_vertex(None);
texture_map.add_vertex(None);
texture_map.add_vertex(Some(vi(3)));
texture_map.add_vertex(Some(vi(4)));
texture_map.add_vertex(Some(vi(5)));
texture_map.add_vertex(Some(vi(6)));
texture_map
}
fn geometry_of_type(
type_geometry: GeometryType,
boundary: B,
semantics: Option<SemanticOrMaterialMap<u32, ResourceId32>>,
materials: Option<ThemedMaterials<u32, ResourceId32, OwnedStringStorage>>,
textures: Option<ThemedTextures<u32, ResourceId32, OwnedStringStorage>>,
) -> G {
GeometryCore::new(
type_geometry,
Some(LoD::LoD2),
Some(boundary),
semantics,
materials,
textures,
None,
)
}
fn regular_geometry(
boundary: B,
semantics: Option<SemanticOrMaterialMap<u32, ResourceId32>>,
materials: Option<ThemedMaterials<u32, ResourceId32, OwnedStringStorage>>,
textures: Option<ThemedTextures<u32, ResourceId32, OwnedStringStorage>>,
) -> G {
geometry_of_type(
GeometryType::MultiSurface,
boundary,
semantics,
materials,
textures,
)
}
fn boundary_with_empty_surface() -> B {
Boundary {
vertices: vec![vi(0), vi(1), vi(2)],
rings: vec![vi(0)],
surfaces: vec![vi(0), vi(1)],
shells: Vec::new(),
solids: Vec::new(),
}
}
fn boundary_with_empty_shell() -> B {
Boundary {
vertices: vec![vi(0), vi(1), vi(2)],
rings: vec![vi(0)],
surfaces: vec![vi(0)],
shells: vec![vi(0), vi(1)],
solids: Vec::new(),
}
}
fn multisurface_with_shells() -> B {
Boundary {
vertices: vec![vi(0), vi(1), vi(2)],
rings: vec![vi(0)],
surfaces: vec![vi(0)],
shells: vec![vi(0)],
solids: Vec::new(),
}
}
fn multisurface_with_solids() -> B {
Boundary {
vertices: vec![vi(0), vi(1), vi(2)],
rings: vec![vi(0)],
surfaces: vec![vi(0)],
shells: vec![vi(0)],
solids: vec![vi(0)],
}
}
fn solid_with_solids() -> B {
multisurface_with_solids()
}
fn multisolid_missing_solids() -> B {
Boundary {
vertices: vec![vi(0), vi(1), vi(2)],
rings: vec![vi(0)],
surfaces: vec![vi(0)],
shells: vec![vi(0)],
solids: Vec::new(),
}
}
fn multisolid_missing_shells() -> B {
Boundary {
vertices: vec![vi(0), vi(1), vi(2)],
rings: vec![vi(0)],
surfaces: vec![vi(0)],
shells: Vec::new(),
solids: vec![vi(0)],
}
}
fn multisolid_missing_surfaces() -> B {
Boundary {
vertices: vec![vi(0), vi(1), vi(2)],
rings: vec![vi(0)],
surfaces: Vec::new(),
shells: vec![vi(0)],
solids: vec![vi(0)],
}
}
fn multisolid_missing_rings() -> B {
Boundary {
vertices: vec![vi(0), vi(1), vi(2)],
rings: Vec::new(),
surfaces: vec![vi(0)],
shells: vec![vi(0)],
solids: vec![vi(0)],
}
}
fn multisolid_missing_vertices() -> B {
Boundary {
vertices: Vec::new(),
rings: vec![vi(0)],
surfaces: vec![vi(0)],
shells: vec![vi(0)],
solids: vec![vi(0)],
}
}
#[test]
fn valid_regular_geometry_passes_full_shared_validator() {
let boundary = s1_boundary();
let semantics = Some(surface_map(&[Some(rid(0)), Some(rid(1))]));
let materials = Some(vec![(theme("theme-a"), surface_map(&[Some(rid(0)), None]))]);
let textures = Some(vec![(theme("theme-a"), dense_texture_map(&boundary))]);
let geometry = regular_geometry(boundary, semantics, materials, textures);
let context = TestContext {
semantics: 2,
materials: 1,
textures: 1,
uvs: 7,
..TestContext::default()
};
assert!(validate_stored_geometry(&geometry, &context).is_ok());
}
#[test]
fn missing_regular_boundary_vertex_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiPoint,
multipoint_boundary(),
None,
None,
None,
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
vertices: 2,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("missing regular vertex"));
}
#[test]
fn missing_template_boundary_vertex_is_rejected_for_template_validation() {
let geometry = geometry_of_type(
GeometryType::MultiPoint,
multipoint_boundary(),
None,
None,
None,
);
let err = validate_stored_geometry_for_boundary_source(
&geometry,
&TestContext {
template_vertices: 2,
..TestContext::default()
},
BoundaryVertexSource::Template,
)
.unwrap_err();
assert!(format!("{err}").contains("missing template vertex"));
}
#[test]
fn regular_geometry_without_boundary_fails() {
let geometry: G = GeometryCore::new(
GeometryType::MultiSurface,
Some(LoD::LoD2),
None,
None,
None::<ThemedMaterials<u32, ResourceId32, OwnedStringStorage>>,
None::<ThemedTextures<u32, ResourceId32, OwnedStringStorage>>,
None,
);
assert!(validate_stored_geometry(&geometry, &TestContext::default()).is_err());
}
#[test]
fn semantic_map_with_wrong_bucket_is_rejected() {
let mut semantics = SemanticOrMaterialMap::new();
semantics.add_linestring(Some(rid(0)));
semantics.add_linestring(Some(rid(1)));
let geometry = regular_geometry(s1_boundary(), Some(semantics), None, None);
let err = validate_stored_geometry(
&geometry,
&TestContext {
semantics: 2,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("surfaces"));
}
#[test]
fn shortened_dense_surface_map_is_rejected() {
let geometry = regular_geometry(
s1_boundary(),
Some(surface_map(&[Some(rid(0))])),
None,
None,
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
semantics: 1,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("primitive count"));
}
#[test]
fn invalid_semantic_reference_is_rejected() {
let geometry = regular_geometry(
s1_boundary(),
Some(surface_map(&[Some(rid(9)), Some(rid(0))])),
None,
None,
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
semantics: 1,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("missing resource"));
}
#[test]
fn invalid_material_reference_is_rejected() {
let geometry = regular_geometry(
s1_boundary(),
None,
Some(vec![(theme("theme-a"), surface_map(&[Some(rid(2)), None]))]),
None,
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
materials: 1,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("missing resource"));
}
#[test]
fn invalid_texture_reference_is_rejected() {
let boundary = s1_boundary();
let mut textures = dense_texture_map(&boundary);
textures.ring_textures_mut()[0] = Some(rid(9));
let geometry = regular_geometry(
boundary,
None,
None,
Some(vec![(theme("theme-a"), textures)]),
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
textures: 1,
uvs: 7,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("missing texture"));
}
#[test]
fn invalid_uv_reference_is_rejected() {
let boundary = s1_boundary();
let mut textures = dense_texture_map(&boundary);
textures.vertices_mut()[0] = Some(vi(99));
let geometry = regular_geometry(
boundary,
None,
None,
Some(vec![(theme("theme-a"), textures)]),
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
textures: 1,
uvs: 7,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("missing UV"));
}
#[test]
fn texture_map_requires_dense_ring_alignment() {
let boundary = s1_boundary();
let mut textures = dense_texture_map(&boundary);
textures.rings_mut()[1] = boundary.rings()[0];
let geometry = regular_geometry(
boundary,
None,
None,
Some(vec![(theme("theme-a"), textures)]),
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
textures: 1,
uvs: 7,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("does not match boundary ring start"));
}
#[test]
fn untextured_ring_must_not_carry_uv_payload() {
let boundary = s1_boundary();
let mut textures = dense_texture_map(&boundary);
textures.vertices_mut()[3] = Some(vi(3));
let geometry = regular_geometry(
boundary,
None,
None,
Some(vec![(theme("theme-a"), textures)]),
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
textures: 1,
uvs: 7,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("untextured"));
}
#[test]
fn textured_ring_must_have_uv_for_every_boundary_occurrence() {
let boundary = s1_boundary();
let mut textures = dense_texture_map(&boundary);
textures.vertices_mut()[0] = None;
let geometry = regular_geometry(
boundary,
None,
None,
Some(vec![(theme("theme-a"), textures)]),
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
textures: 1,
uvs: 7,
..TestContext::default()
},
)
.unwrap_err();
assert!(format!("{err}").contains("null UV"));
}
#[test]
fn instance_template_and_reference_must_resolve() {
let geometry: G = GeometryCore::new(
GeometryType::GeometryInstance,
None,
None,
None,
None::<ThemedMaterials<u32, ResourceId32, OwnedStringStorage>>,
None::<ThemedTextures<u32, ResourceId32, OwnedStringStorage>>,
Some(GeometryInstanceData::new(
rid(1),
vi(2),
AffineTransform3D::identity(),
)),
);
let err = validate_stored_geometry(
&geometry,
&TestContext {
templates: 1,
vertices: 2,
..TestContext::default()
},
)
.unwrap_err();
assert!(
format!("{err}").contains("missing template")
|| format!("{err}").contains("missing reference point")
);
}
#[test]
fn multipoint_boundary_is_rejected_for_multisurface() {
let geometry = regular_geometry(multipoint_boundary(), None, None, None);
assert!(validate_stored_geometry(&geometry, &TestContext::default()).is_err());
}
#[test]
fn surface_with_no_ring_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiSurface,
boundary_with_empty_surface(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("surface 1"));
}
#[test]
fn shell_with_no_surface_is_rejected() {
let geometry = geometry_of_type(
GeometryType::Solid,
boundary_with_empty_shell(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("shell 1"));
}
#[test]
fn multisurface_with_shells_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiSurface,
multisurface_with_shells(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("must not carry shells"));
}
#[test]
fn multisurface_with_solids_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiSurface,
multisurface_with_solids(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("must not carry"));
}
#[test]
fn solid_with_solids_is_rejected() {
let geometry = geometry_of_type(GeometryType::Solid, solid_with_solids(), None, None, None);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("must not carry solids"));
}
#[test]
fn multisolid_missing_solids_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiSolid,
multisolid_missing_solids(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("requires non-empty solids"));
}
#[test]
fn multisolid_missing_shells_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiSolid,
multisolid_missing_shells(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("requires non-empty shells"));
}
#[test]
fn multisolid_missing_surfaces_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiSolid,
multisolid_missing_surfaces(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("requires non-empty surfaces"));
}
#[test]
fn multisolid_missing_rings_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiSolid,
multisolid_missing_rings(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("requires non-empty rings"));
}
#[test]
fn multisolid_missing_vertices_is_rejected() {
let geometry = geometry_of_type(
GeometryType::MultiSolid,
multisolid_missing_vertices(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("requires non-empty vertices"));
}
#[test]
fn template_geometry_uses_the_same_shape_rules() {
let geometry = geometry_of_type(
GeometryType::MultiSurface,
boundary_with_empty_surface(),
None,
None,
None,
);
let err = validate_stored_geometry(&geometry, &TestContext::default()).unwrap_err();
assert!(format!("{err}").contains("surface 1"));
}
}