//! Dependency-light renderer scene intermediate representation. //! //! The scene is semantic data: quads, conditional-line controls, provenance, and //! occurrence transforms remain intact until the renderer uploads geometry. use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use std::sync::Arc; macro_rules! id_type { ($name:ident) => { #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] #[serde(transparent)] pub struct $name(pub u64); }; } id_type!(SceneId); id_type!(NodeId); id_type!(OccurrenceId); id_type!(GeometryId); id_type!(MaterialId); id_type!(TextureId); id_type!(ProvenanceId); id_type!(DiagnosticId); #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct SceneBounds { pub min: [f64; 3], pub max: [f64; 3], } impl SceneBounds { pub fn from_points(points: impl IntoIterator) -> Option { let mut iter = points.into_iter(); let first = iter.next()?; if first.iter().any(|value| !value.is_finite()) { return None; } let mut bounds = Self { min: first, max: first, }; for point in iter { if point.iter().any(|value| !value.is_finite()) { return None; } for (axis, value) in point.into_iter().enumerate() { bounds.min[axis] = bounds.min[axis].min(value); bounds.max[axis] = bounds.max[axis].max(value); } } Some(bounds) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct SceneStats { pub points: u64, pub triangles: u64, pub lines: u64, pub conditional_lines: u64, pub occurrences: u64, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SceneTexmapProjectionMode { Planar, Cylindrical, Spherical, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct SceneTexmapAssociation { pub mode: SceneTexmapProjectionMode, pub parameters: Vec, pub pngfile: SmolStr, pub glossmap: Option, pub fallback: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ScenePrimitiveRange { pub point_start: u32, pub point_count: u32, pub topology: SmolStr, pub bfc_state: SmolStr, pub bfc_cull: bool, pub bfc_ccw: bool, pub bfc_inverted: bool, pub texmap: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SceneGeometry { pub id: GeometryId, pub provenance: ProvenanceId, pub points: Box<[[f64; 3]]>, /// Optional UV coordinates aligned one-to-one with `points`. #[serde(default)] pub uvs: Box<[[f32; 2]]>, pub triangles: Box<[[u32; 3]]>, pub quads: Box<[[u32; 4]]>, pub lines: Box<[[u32; 2]]>, pub conditional_lines: Box<[[u32; 4]]>, pub primitives: Box<[ScenePrimitiveRange]>, } fn opaque_alpha() -> u8 { u8::MAX } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SceneMaterial { pub id: MaterialId, pub colour: u32, /// Resolved LDraw colour RGB; omitted by older scene payloads. #[serde(default)] pub colour_rgb: Option<[u8; 3]>, /// Resolved LDraw colour alpha; omitted by older scene payloads. #[serde(default = "opaque_alpha")] pub alpha: u8, pub edge_colour: Option, pub finish: Option, pub local_mapping: Option, #[serde(default)] pub albedo_texture: Option, #[serde(default)] pub metallic_roughness_texture: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SceneTexture { pub id: TextureId, pub provenance: ProvenanceId, pub uri: SmolStr, #[serde(default)] pub resource_key: Option, #[serde(default)] pub sha256: Option<[u8; 32]>, #[serde(default)] pub byte_length: Option, #[serde(default)] pub role: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SceneOccurrence { pub id: OccurrenceId, pub canonical_occurrence_id: SmolStr, #[serde(default)] pub parent_occurrence_id: Option, #[serde(default)] pub logical_part_occurrence_id: Option, #[serde(default)] pub logical_part_path: Option, pub node: NodeId, /// Optional per-occurrence UV overlay; the referenced geometry remains shared. #[serde(default)] pub uvs: Option>, pub definition_id: SmolStr, pub canonical_path: SmolStr, pub source_class: SmolStr, pub source: SmolStr, pub target: SmolStr, pub source_span: Option<[u32; 4]>, pub direct_colour: u32, pub effective_colour: u32, pub primitive_colour: u32, pub material: Option, pub bfc_state: SmolStr, pub bfc_cull: bool, pub bfc_ccw: bool, pub bfc_inverted: bool, pub transform: [[f64; 4]; 4], } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SceneProvenance { pub id: ProvenanceId, pub source: SmolStr, pub span: Option<[u32; 4]>, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SceneDiagnostic { pub id: DiagnosticId, pub message: SmolStr, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Scene { pub id: SceneId, pub units: Units, pub source: SmolStr, pub provenance: Box<[SceneProvenance]>, pub diagnostics: Box<[SceneDiagnostic]>, pub root: NodeId, pub geometries: Box<[SceneGeometry]>, pub materials: Box<[SceneMaterial]>, pub textures: Box<[SceneTexture]>, pub occurrences: Box<[SceneOccurrence]>, pub bounds: Option, pub stats: SceneStats, } impl Scene { pub fn recompute_stats(&mut self) -> Result<(), &'static str> { use std::collections::HashMap; let mut geometries = HashMap::with_capacity(self.geometries.len()); for geometry in &self.geometries { geometries.insert(geometry.id.0, geometry); } let mut stats = SceneStats { occurrences: u64::try_from(self.occurrences.len()) .map_err(|_| "occurrence count overflow")?, ..SceneStats::default() }; for occurrence in &self.occurrences { let geometry = geometries .get(&occurrence.node.0) .ok_or("unknown geometry")?; let points = u64::try_from(geometry.points.len()).map_err(|_| "point count overflow")?; let triangles = u64::try_from(geometry.triangles.len()).map_err(|_| "triangle count overflow")?; let quads = u64::try_from(geometry.quads.len()).map_err(|_| "quad count overflow")?; let lines = u64::try_from(geometry.lines.len()).map_err(|_| "line count overflow")?; let conditional_lines = u64::try_from(geometry.conditional_lines.len()) .map_err(|_| "conditional-line count overflow")?; stats.points = stats .points .checked_add(points) .ok_or("point count overflow")?; stats.triangles = stats .triangles .checked_add(triangles) .and_then(|count| count.checked_add(quads.checked_mul(2)?)) .ok_or("triangle count overflow")?; stats.lines = stats .lines .checked_add(lines) .ok_or("line count overflow")?; stats.conditional_lines = stats .conditional_lines .checked_add(conditional_lines) .ok_or("conditional-line count overflow")?; } self.stats = stats; Ok(()) } } pub use super::{LengthUnit, Units}; #[cfg(test)] mod tests { use super::*; #[test] fn bounds_reject_non_finite_points() { assert!(SceneBounds::from_points([[0.0, 1.0, 2.0], [f64::NAN, 0.0, 0.0]]).is_none()); } #[test] fn scene_postcard_roundtrip_is_deterministic() { let mut scene = Scene { id: SceneId(1), units: Units::declared(LengthUnit::LdrawUnit), source: "omr-10030-1.mpd".into(), provenance: Box::default(), diagnostics: Box::default(), root: NodeId(1), geometries: Box::new([SceneGeometry { id: GeometryId(1), provenance: ProvenanceId(1), points: Box::new([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]), uvs: Box::default(), triangles: Box::new([[0, 1, 2]]), quads: Box::default(), lines: Box::default(), conditional_lines: Box::default(), primitives: Box::default(), }]), materials: Box::default(), textures: Box::default(), occurrences: Box::new([SceneOccurrence { id: OccurrenceId(1), canonical_occurrence_id: "root".into(), parent_occurrence_id: None, logical_part_occurrence_id: None, logical_part_path: None, node: NodeId(1), uvs: None, definition_id: "root".into(), canonical_path: "omr-10030-1.mpd".into(), source_class: "project".into(), source: "omr-10030-1.mpd".into(), target: "omr-10030-1.mpd".into(), source_span: None, direct_colour: 16, effective_colour: 16, primitive_colour: 16, material: None, bfc_state: "certified".into(), bfc_cull: true, bfc_ccw: true, bfc_inverted: false, transform: [ [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], ], }]), bounds: SceneBounds::from_points([[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]]), stats: SceneStats::default(), }; scene.recompute_stats().unwrap(); let bytes = postcard::to_allocvec(&scene).unwrap(); assert_eq!(postcard::from_bytes::(&bytes).unwrap(), scene); assert_eq!(scene.stats.triangles, 1); } }