use crate::appearance::{MaterialReference, TextureReference};
use crate::error::{CjseqError, Result};
use crate::semantics::Semantics;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub type Ring = Vec<usize>;
pub type Surface = Vec<Ring>;
pub type Shell = Vec<Surface>;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeometryType {
MultiPoint,
MultiLineString,
MultiSurface,
CompositeSurface,
Solid,
MultiSolid,
CompositeSolid,
GeometryInstance,
}
impl GeometryType {
pub fn boundary_depth(self) -> usize {
match self {
GeometryType::MultiPoint | GeometryType::GeometryInstance => 1,
GeometryType::MultiLineString => 2,
GeometryType::MultiSurface | GeometryType::CompositeSurface => 3,
GeometryType::Solid => 4,
GeometryType::MultiSolid | GeometryType::CompositeSolid => 5,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub struct GeometryCommon {
#[serde(skip_serializing_if = "Option::is_none")]
pub semantics: Option<Semantics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub material: Option<HashMap<String, MaterialReference>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub texture: Option<HashMap<String, TextureReference>>,
}
fn nesting_depth(v: &Value) -> usize {
match v {
Value::Array(a) => 1 + a.iter().map(nesting_depth).max().unwrap_or(0),
_ => 0,
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(tag = "type")]
pub enum Geometry {
MultiPoint {
#[serde(skip_serializing_if = "Option::is_none")]
lod: Option<String>,
boundaries: Ring,
#[serde(flatten)]
common: GeometryCommon,
},
MultiLineString {
#[serde(skip_serializing_if = "Option::is_none")]
lod: Option<String>,
boundaries: Vec<Ring>,
#[serde(flatten)]
common: GeometryCommon,
},
MultiSurface {
#[serde(skip_serializing_if = "Option::is_none")]
lod: Option<String>,
boundaries: Vec<Surface>,
#[serde(flatten)]
common: GeometryCommon,
},
CompositeSurface {
#[serde(skip_serializing_if = "Option::is_none")]
lod: Option<String>,
boundaries: Vec<Surface>,
#[serde(flatten)]
common: GeometryCommon,
},
Solid {
#[serde(skip_serializing_if = "Option::is_none")]
lod: Option<String>,
boundaries: Vec<Shell>,
#[serde(flatten)]
common: GeometryCommon,
},
MultiSolid {
#[serde(skip_serializing_if = "Option::is_none")]
lod: Option<String>,
boundaries: Vec<Vec<Shell>>,
#[serde(flatten)]
common: GeometryCommon,
},
CompositeSolid {
#[serde(skip_serializing_if = "Option::is_none")]
lod: Option<String>,
boundaries: Vec<Vec<Shell>>,
#[serde(flatten)]
common: GeometryCommon,
},
GeometryInstance {
boundaries: Ring,
template: usize,
#[serde(rename = "transformationMatrix")]
transformation_matrix: [f64; 16],
},
}
impl Geometry {
pub fn from_json_value(v: Value) -> Result<Geometry> {
let err = match Geometry::deserialize(&v) {
Ok(g) => return Ok(g),
Err(e) => e,
};
let thetype = v
.get("type")
.and_then(|t| GeometryType::deserialize(t).ok());
if let (Some(thetype), Some(boundaries)) = (thetype, v.get("boundaries")) {
let expected = thetype.boundary_depth();
let found = nesting_depth(boundaries);
if found != expected {
return Err(CjseqError::GeometryDepth {
geometry_type: thetype,
expected,
found,
});
}
}
Err(CjseqError::Json(err))
}
pub fn geometry_type(&self) -> GeometryType {
match self {
Geometry::MultiPoint { .. } => GeometryType::MultiPoint,
Geometry::MultiLineString { .. } => GeometryType::MultiLineString,
Geometry::MultiSurface { .. } => GeometryType::MultiSurface,
Geometry::CompositeSurface { .. } => GeometryType::CompositeSurface,
Geometry::Solid { .. } => GeometryType::Solid,
Geometry::MultiSolid { .. } => GeometryType::MultiSolid,
Geometry::CompositeSolid { .. } => GeometryType::CompositeSolid,
Geometry::GeometryInstance { .. } => GeometryType::GeometryInstance,
}
}
pub fn lod(&self) -> Option<&str> {
match self {
Geometry::MultiPoint { lod, .. }
| Geometry::MultiLineString { lod, .. }
| Geometry::MultiSurface { lod, .. }
| Geometry::CompositeSurface { lod, .. }
| Geometry::Solid { lod, .. }
| Geometry::MultiSolid { lod, .. }
| Geometry::CompositeSolid { lod, .. } => lod.as_deref(),
Geometry::GeometryInstance { .. } => None,
}
}
pub fn common(&self) -> Option<&GeometryCommon> {
match self {
Geometry::MultiPoint { common, .. }
| Geometry::MultiLineString { common, .. }
| Geometry::MultiSurface { common, .. }
| Geometry::CompositeSurface { common, .. }
| Geometry::Solid { common, .. }
| Geometry::MultiSolid { common, .. }
| Geometry::CompositeSolid { common, .. } => Some(common),
Geometry::GeometryInstance { .. } => None,
}
}
pub fn common_mut(&mut self) -> Option<&mut GeometryCommon> {
match self {
Geometry::MultiPoint { common, .. }
| Geometry::MultiLineString { common, .. }
| Geometry::MultiSurface { common, .. }
| Geometry::CompositeSurface { common, .. }
| Geometry::Solid { common, .. }
| Geometry::MultiSolid { common, .. }
| Geometry::CompositeSolid { common, .. } => Some(common),
Geometry::GeometryInstance { .. } => None,
}
}
fn boundary_indices_mut(&mut self) -> Box<dyn Iterator<Item = &mut usize> + '_> {
match self {
Geometry::MultiPoint { boundaries, .. }
| Geometry::GeometryInstance { boundaries, .. } => Box::new(boundaries.iter_mut()),
Geometry::MultiLineString { boundaries, .. } => {
Box::new(boundaries.iter_mut().flatten())
}
Geometry::MultiSurface { boundaries, .. }
| Geometry::CompositeSurface { boundaries, .. } => {
Box::new(boundaries.iter_mut().flatten().flatten())
}
Geometry::Solid { boundaries, .. } => {
Box::new(boundaries.iter_mut().flatten().flatten().flatten())
}
Geometry::MultiSolid { boundaries, .. }
| Geometry::CompositeSolid { boundaries, .. } => Box::new(
boundaries
.iter_mut()
.flatten()
.flatten()
.flatten()
.flatten(),
),
}
}
pub(crate) fn update_geometry_boundaries(&mut self, violdnew: &mut HashMap<usize, usize>) {
for vi in self.boundary_indices_mut() {
let next = violdnew.len();
*vi = *violdnew.entry(*vi).or_insert(next);
}
}
pub(crate) fn offset_geometry_boundaries(&mut self, offset: usize) {
for vi in self.boundary_indices_mut() {
*vi += offset;
}
}
pub(crate) fn update_material(&mut self, m_oldnew: &mut HashMap<usize, usize>) {
let Some(common) = self.common_mut() else {
return;
};
let Some(materials) = common.material.as_mut() else {
return;
};
for mat in materials.values_mut() {
if let Some(thevalue) = mat.value {
let l = m_oldnew.len();
mat.value = Some(*m_oldnew.entry(thevalue).or_insert(l));
continue;
}
let Some(values) = mat.values.as_mut().and_then(|v| v.as_mut()) else {
continue;
};
for x in values.indices_mut() {
let l = m_oldnew.len();
*x = *m_oldnew.entry(*x).or_insert(l);
}
}
}
pub(crate) fn update_texture(
&mut self,
t_oldnew: &mut HashMap<usize, usize>,
t_v_oldnew: &mut HashMap<usize, usize>,
offset: usize,
) {
let Some(common) = self.common_mut() else {
return;
};
let Some(textures) = common.texture.as_mut() else {
return;
};
let mut remap = |is_texture: bool, thevalue: usize| -> usize {
if is_texture {
let l = t_oldnew.len();
*t_oldnew.entry(thevalue).or_insert(l)
} else {
match t_v_oldnew.get(&thevalue) {
Some(y) => *y,
None => {
let l = t_v_oldnew.len();
t_v_oldnew.insert(thevalue, l + offset);
l
}
}
}
};
for tex in textures.values_mut() {
let Some(values) = tex.values.as_mut() else {
continue;
};
for ring in values.rings_mut() {
for (k, z) in ring.iter_mut().enumerate() {
if let Some(thevalue) = *z {
*z = Some(remap(k == 0, thevalue));
}
}
}
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct GeometryTemplates {
pub templates: Vec<Geometry>,
#[serde(rename = "vertices-templates")]
pub vertices_templates: Value,
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(v: serde_json::Value) -> std::result::Result<Geometry, serde_json::Error> {
serde_json::from_value(v)
}
#[test]
fn each_variant_has_its_own_depth() {
let cases = [
(GeometryType::MultiPoint, serde_json::json!([0, 1])),
(GeometryType::MultiLineString, serde_json::json!([[0, 1]])),
(GeometryType::MultiSurface, serde_json::json!([[[0, 1, 2]]])),
(
GeometryType::CompositeSurface,
serde_json::json!([[[0, 1, 2]]]),
),
(GeometryType::Solid, serde_json::json!([[[[0, 1, 2]]]])),
(
GeometryType::MultiSolid,
serde_json::json!([[[[[0, 1, 2]]]]]),
),
(
GeometryType::CompositeSolid,
serde_json::json!([[[[[0, 1, 2]]]]]),
),
];
for (thetype, boundaries) in cases {
let name = serde_json::to_value(thetype).unwrap();
let g = parse(serde_json::json!({
"type": name, "lod": "1", "boundaries": boundaries.clone()
}))
.unwrap_or_else(|e| panic!("{name} must accept its own depth: {e}"));
assert_eq!(g.geometry_type(), thetype);
assert_eq!(g.lod(), Some("1"));
assert_eq!(
serde_json::to_value(&g).unwrap()["boundaries"],
boundaries,
"{name} must round-trip its boundaries"
);
let too_deep = serde_json::json!([boundaries]);
assert!(
parse(serde_json::json!({
"type": name, "lod": "1", "boundaries": too_deep
}))
.is_err(),
"{name} must reject boundaries one level too deep"
);
let too_shallow = boundaries[0].clone();
assert!(
parse(serde_json::json!({
"type": name, "lod": "1", "boundaries": too_shallow
}))
.is_err(),
"{name} must reject boundaries one level too shallow"
);
}
}
#[test]
fn geometry_instance_roundtrips() {
let input = serde_json::json!({
"type": "GeometryInstance",
"boundaries": [372],
"template": 0,
"transformationMatrix": [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]
});
let g: Geometry = serde_json::from_value(input.clone()).unwrap();
assert_eq!(g.geometry_type(), GeometryType::GeometryInstance);
assert!(g.common().is_none());
assert_eq!(serde_json::to_value(&g).unwrap(), input);
}
#[test]
fn offset_walks_every_depth() {
let mut g: Geometry = serde_json::from_value(serde_json::json!({
"type": "CompositeSolid", "lod": "2",
"boundaries": [[[[[0, 1]]]], [[[[2, 3]]]]]
}))
.unwrap();
g.offset_geometry_boundaries(10);
assert_eq!(
serde_json::to_value(&g).unwrap()["boundaries"],
serde_json::json!([[[[[10, 11]]]], [[[[12, 13]]]]])
);
}
#[test]
fn update_boundaries_compacts_indices() {
let mut g: Geometry = serde_json::from_value(serde_json::json!({
"type": "MultiSurface", "lod": "2",
"boundaries": [[[7, 9, 7]], [[9, 4, 4]]]
}))
.unwrap();
let mut map: HashMap<usize, usize> = HashMap::new();
g.update_geometry_boundaries(&mut map);
assert_eq!(
serde_json::to_value(&g).unwrap()["boundaries"],
serde_json::json!([[[0, 1, 0]], [[1, 2, 2]]])
);
assert_eq!(map.len(), 3);
}
#[test]
fn update_boundaries_compacts_indices_at_max_depth() {
let mut g: Geometry = serde_json::from_value(serde_json::json!({
"type": "CompositeSolid", "lod": "2",
"boundaries": [[[[[7, 9, 7]]]], [[[[9, 4, 4]]]]]
}))
.unwrap();
let mut map: HashMap<usize, usize> = HashMap::new();
g.update_geometry_boundaries(&mut map);
assert_eq!(
serde_json::to_value(&g).unwrap()["boundaries"],
serde_json::json!([[[[[0, 1, 0]]]], [[[[1, 2, 2]]]]])
);
let mut entries: Vec<(usize, usize)> = map.into_iter().collect();
entries.sort();
assert_eq!(entries, vec![(4, 2), (7, 0), (9, 1)]);
}
#[test]
fn update_texture_renumbers_textures_and_texture_vertices() {
let mut g: Geometry = serde_json::from_value(serde_json::json!({
"type": "MultiSurface", "lod": "2",
"boundaries": [[[0, 1, 2, 3]], [[4, 5, 6, 7]], [[8, 9, 10, 11]]],
"texture": {"winter": {"values": [
[[5, 20, 21, 22, 20]],
[[5, 22, 21, 30, 22]],
[[null]]
]}}
}))
.unwrap();
let mut t_oldnew: HashMap<usize, usize> = HashMap::new();
let mut t_v_oldnew: HashMap<usize, usize> = HashMap::new();
g.update_texture(&mut t_oldnew, &mut t_v_oldnew, 100);
let values =
serde_json::to_value(&g.common().unwrap().texture.as_ref().unwrap()["winter"].values)
.unwrap();
assert_eq!(
values,
serde_json::json!([[[0, 0, 1, 2, 100]], [[0, 102, 101, 3, 102]], [[null]]]),
"texture values must renumber exactly as the old implementation did"
);
let mut t: Vec<(usize, usize)> = t_oldnew.into_iter().collect();
t.sort();
assert_eq!(t, vec![(5, 0)], "the repeated texture index 5 maps once");
let mut tv: Vec<(usize, usize)> = t_v_oldnew.into_iter().collect();
tv.sort();
assert_eq!(
tv,
vec![(20, 100), (21, 101), (22, 102), (30, 103)],
"texture vertices are stored offset by `offset`"
);
}
#[test]
fn update_material_preserves_null_holes() {
let mut g: Geometry = serde_json::from_value(serde_json::json!({
"type": "Solid", "lod": "2",
"boundaries": [[[[0, 1, 2, 3]], [[4, 5, 6, 7]], [[8, 9, 10, 11]]]],
"material": {"irradiation": {"values": [[0, null, 2], [2, null, 0]]}}
}))
.unwrap();
let mut m_oldnew: HashMap<usize, usize> = HashMap::new();
g.update_material(&mut m_oldnew);
let values = serde_json::to_value(
g.common().unwrap().material.as_ref().unwrap()["irradiation"]
.values
.as_ref()
.unwrap(),
)
.unwrap();
assert_eq!(
values,
serde_json::json!([[0, null, 1], [1, null, 0]]),
"null holes must survive and indices must compact in first-encounter order"
);
let mut m: Vec<(usize, usize)> = m_oldnew.into_iter().collect();
m.sort();
assert_eq!(m, vec![(0, 0), (2, 1)]);
}
#[test]
fn wrong_depth_reports_which_geometry_and_which_depth() {
let err = Geometry::from_json_value(serde_json::json!({
"type": "MultiSurface", "lod": "2",
"boundaries": [[[[0, 1, 2]]]]
}))
.unwrap_err();
match &err {
CjseqError::GeometryDepth {
geometry_type,
expected,
found,
} => {
assert_eq!(*geometry_type, GeometryType::MultiSurface);
assert_eq!(*expected, 3, "a MultiSurface nests boundaries 3 deep");
assert_eq!(
*found, 4,
"the depth actually found must be reported as a number, not prose"
);
}
other => panic!("expected a GeometryDepth error, got {other:?}"),
}
let msg = err.to_string();
assert!(
msg.contains("MultiSurface") && msg.contains('3'),
"the message must name the geometry and the depth it expected, got {msg:?}"
);
}
#[test]
fn a_non_depth_failure_stays_a_json_error() {
let err = Geometry::from_json_value(serde_json::json!({
"type": "MultiSurface", "lod": "2",
"boundaries": [[["not an index"]]]
}))
.unwrap_err();
assert!(
matches!(err, CjseqError::Json(_)),
"expected a Json error, got {err:?}"
);
}
#[test]
fn from_json_value_accepts_a_good_geometry() {
let g = Geometry::from_json_value(serde_json::json!({
"type": "Solid", "lod": "2", "boundaries": [[[[0, 1, 2]]]]
}))
.unwrap();
assert_eq!(g.geometry_type(), GeometryType::Solid);
}
#[test]
fn absent_members_are_not_serialized_as_null() {
let g: Geometry = serde_json::from_value(serde_json::json!({
"type": "MultiSurface", "boundaries": [[[0, 1, 2]]]
}))
.unwrap();
assert_eq!(
serde_json::to_value(&g).unwrap(),
serde_json::json!({"type": "MultiSurface", "boundaries": [[[0, 1, 2]]]})
);
}
}