Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
11 kB · 327 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328//! 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<Item = [f64; 3]>) -> Option<Self> { 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<SmolStr>, pub pngfile: SmolStr, pub glossmap: Option<SmolStr>, 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<SceneTexmapAssociation>,}
#[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<u32>, pub finish: Option<SmolStr>, pub local_mapping: Option<SmolStr>, #[serde(default)] pub albedo_texture: Option<TextureId>, #[serde(default)] pub metallic_roughness_texture: Option<TextureId>,}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct SceneTexture { pub id: TextureId, pub provenance: ProvenanceId, pub uri: SmolStr, #[serde(default)] pub resource_key: Option<SmolStr>, #[serde(default)] pub sha256: Option<[u8; 32]>, #[serde(default)] pub byte_length: Option<u64>, #[serde(default)] pub role: Option<SmolStr>,}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct SceneOccurrence { pub id: OccurrenceId, pub canonical_occurrence_id: SmolStr, #[serde(default)] pub parent_occurrence_id: Option<SmolStr>, #[serde(default)] pub logical_part_occurrence_id: Option<SmolStr>, #[serde(default)] pub logical_part_path: Option<SmolStr>, pub node: NodeId, /// Optional per-occurrence UV overlay; the referenced geometry remains shared. #[serde(default)] pub uvs: Option<Arc<[[f32; 2]]>>, 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<MaterialId>, 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<SceneBounds>, 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::<Scene>(&bytes).unwrap(), scene); assert_eq!(scene.stats.triangles, 1); }}