konjure_sdk/
interchange.rs1use crate::{Diagnostic, EntityId, Geometry, MAX_COORDINATE_M, Mesh, SceneDocument};
6use alloc::{collections::BTreeMap, format, string::String};
7use serde::{Deserialize, Serialize};
8
9pub const MAX_BUNDLE_BYTES: usize = 8 * 1024 * 1024;
11pub const MAX_BUNDLE_MESH_VERTICES: usize = 16_384;
13pub const MAX_BUNDLE_MESH_INDICES: usize = 98_304;
15pub const MAX_BUNDLE_VERTICES: usize = 262_144;
17
18#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct SceneBundle {
24 pub format: String,
26 pub version: u16,
28 pub document: SceneDocument,
30 pub meshes: BTreeMap<EntityId, Mesh>,
32}
33
34impl SceneBundle {
35 pub fn new(
40 document: SceneDocument,
41 meshes: BTreeMap<EntityId, Mesh>,
42 ) -> Result<Self, Diagnostic> {
43 let bundle = Self {
44 format: "konjure.scene-bundle".into(),
45 version: 1,
46 document,
47 meshes,
48 };
49 bundle.validate()?;
50 Ok(bundle)
51 }
52
53 pub fn validate(&self) -> Result<(), Diagnostic> {
59 if self.format != "konjure.scene-bundle" || self.version != 1 {
60 return Err(Diagnostic::plain(
61 "unsupported scene bundle format or version",
62 ));
63 }
64 crate::validate(&self.document)?;
65 let mut vertices = 0usize;
66 for entity in &self.document.entities {
67 if matches!(entity.geometry, Geometry::Group) {
68 if self.meshes.contains_key(&entity.id) {
69 return Err(Diagnostic::plain("a group must not contain a mesh"));
70 }
71 continue;
72 }
73 let mesh = self
74 .meshes
75 .get(&entity.id)
76 .ok_or_else(|| Diagnostic::plain(format!("missing mesh for {}", entity.id.0)))?;
77 let line = matches!(entity.geometry, Geometry::Line { .. });
78 validate_mesh(mesh, line)?;
79 if let Geometry::Line { from_m, to_m } = entity.geometry
80 && (mesh.positions != [from_m, to_m]
81 || mesh.indices != [0, 1]
82 || mesh.normals != [crate::Vec3::ZERO, crate::Vec3::ZERO])
83 {
84 return Err(Diagnostic::plain(
85 "line mesh must match its endpoints with indices [0,1] and zero normals",
86 ));
87 }
88 vertices = vertices
89 .checked_add(mesh.positions.len())
90 .ok_or_else(|| Diagnostic::plain("scene mesh budget exceeded"))?;
91 if vertices > MAX_BUNDLE_VERTICES {
92 return Err(Diagnostic::plain("scene mesh budget exceeded"));
93 }
94 }
95 for id in self.meshes.keys() {
96 if !self.document.entities.iter().any(|entity| &entity.id == id) {
97 return Err(Diagnostic::plain(format!(
98 "mesh {} has no scene entity",
99 id.0
100 )));
101 }
102 }
103 Ok(())
104 }
105}
106
107fn validate_mesh(mesh: &Mesh, line: bool) -> Result<(), Diagnostic> {
108 if mesh.positions.is_empty()
109 || mesh.positions.len() > MAX_BUNDLE_MESH_VERTICES
110 || mesh.indices.len() > MAX_BUNDLE_MESH_INDICES
111 {
112 return Err(Diagnostic::plain(
113 "mesh size is outside the exchange limits",
114 ));
115 }
116 if mesh.normals.len() != mesh.positions.len()
117 || mesh.indices.is_empty()
118 || (line && mesh.indices.len() != 2)
119 || (!line && !mesh.indices.len().is_multiple_of(3))
120 {
121 return Err(Diagnostic::plain("mesh buffers have incompatible lengths"));
122 }
123 if mesh
124 .indices
125 .iter()
126 .any(|&index| index as usize >= mesh.positions.len())
127 {
128 return Err(Diagnostic::plain("mesh index is out of bounds"));
129 }
130 for point in &mesh.positions {
131 if [point.x, point.y, point.z]
132 .iter()
133 .any(|value| !value.is_finite() || value.abs() > MAX_COORDINATE_M)
134 {
135 return Err(Diagnostic::plain(
136 "mesh positions must be finite and within coordinate bounds",
137 ));
138 }
139 }
140 for normal in &mesh.normals {
141 let length2 = normal.x * normal.x + normal.y * normal.y + normal.z * normal.z;
142 if !length2.is_finite() || (!line && (length2 - 1.0).abs() > 0.01) {
143 return Err(Diagnostic::plain(
144 "triangle mesh normals must be finite unit vectors",
145 ));
146 }
147 }
148 Ok(())
149}