Skip to main content

konjure_sdk/
interchange.rs

1//! Bounded, renderer-neutral scene exchange with embedded geometry.
2//!
3//! Bundles carry evaluated geometry, not executable code or external asset URLs.
4//! Adapters own file I/O and native objects; this module owns admission checks.
5use crate::{Diagnostic, EntityId, Geometry, MAX_COORDINATE_M, Mesh, SceneDocument};
6use alloc::{collections::BTreeMap, format, string::String};
7use serde::{Deserialize, Serialize};
8
9/// Maximum encoded bundle size admitted by file and network adapters.
10pub const MAX_BUNDLE_BYTES: usize = 8 * 1024 * 1024;
11/// Maximum vertices in one mesh.
12pub const MAX_BUNDLE_MESH_VERTICES: usize = 16_384;
13/// Maximum triangle indices in one mesh.
14pub const MAX_BUNDLE_MESH_INDICES: usize = 98_304;
15/// Maximum total vertices in a scene bundle.
16pub const MAX_BUNDLE_VERTICES: usize = 262_144;
17
18/// An independently editable scene snapshot with all local meshes embedded.
19#[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    /// Wire-format name, `konjure.scene-bundle`.
25    pub format: String,
26    /// Wire-format version, currently one.
27    pub version: u16,
28    /// Local transforms, hierarchy, and materials in SDK conventions.
29    pub document: SceneDocument,
30    /// Local meshes keyed by entity ID. Groups have no mesh entry.
31    pub meshes: BTreeMap<EntityId, Mesh>,
32}
33
34impl SceneBundle {
35    /// Constructs and validates an owned scene snapshot.
36    ///
37    /// # Errors
38    /// Rejects invalid scenes, missing meshes, malformed buffers and exceeded budgets.
39    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    /// Checks topology and every embedded buffer before a host applies a scene.
54    ///
55    /// # Errors
56    /// Rejects unknown formats, dangling meshes, nonfinite values, invalid normals,
57    /// invalid indices, missing geometry, or excessive work/storage.
58    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}