1use konjure_sdk::{
6 Diagnostic, Geometry, Vec3,
7 interchange::{MAX_BUNDLE_BYTES, SceneBundle},
8};
9use rapier3d::glamx::{DQuat, EulerRot};
10use serde::Serialize;
11use std::{collections::BTreeMap, fmt::Write};
12
13pub const MAX_IMPORTED_MODULES: usize = 64;
15pub const MAX_IMPORTED_SOURCE_BYTES: usize = 1024 * 1024;
17pub const MAX_MESH_JSON_CHUNK_BYTES: usize = 16 * 1024;
19
20fn problem(message: impl Into<String>) -> Diagnostic {
21 Diagnostic {
22 message: message.into(),
23 span: None,
24 }
25}
26
27pub fn parse_bundle(input: &str) -> Result<SceneBundle, Diagnostic> {
32 if input.len() > MAX_BUNDLE_BYTES {
33 return Err(problem("scene bundle exceeds 8 MiB"));
34 }
35 let bundle: SceneBundle = serde_json::from_str(input).map_err(|e| problem(e.to_string()))?;
36 bundle.validate()?;
37 Ok(bundle)
38}
39
40fn vector(value: Vec3) -> String {
41 format!("Vec3 {{ x: {}, y: {}, z: {} }}", value.x, value.y, value.z)
42}
43
44pub fn to_source(bundle: &SceneBundle) -> Result<String, Diagnostic> {
52 bundle.validate()?;
53 let mut output = String::from(
54 "// Imported scene. Geometry is baked; transforms and materials remain editable.\n",
55 );
56 let ids: BTreeMap<_, _> = bundle
57 .document
58 .entities
59 .iter()
60 .enumerate()
61 .map(|(i, e)| (&e.id, i))
62 .collect();
63 for (i, entity) in bundle.document.entities.iter().enumerate() {
64 if !entity.animations.is_empty() || !entity.actions.is_empty() {
65 return Err(problem(
66 "exchange requires an evaluated scene without actions or animations",
67 ));
68 }
69 let q = entity.transform.rotation;
70 let (x, y, z) = DQuat::from_xyzw(q.x, q.y, q.z, q.w).to_euler(EulerRot::XYZ);
71 let transform = format!(
72 "Transform {{ position: {}, rotation: {}, scale: {} }}",
73 vector(entity.transform.translation),
74 vector(Vec3 { x, y, z }),
75 vector(entity.transform.scale)
76 );
77 writeln!(
78 output,
79 "\n// {}\nlet object_{i} = spawn({transform});",
80 entity.id.0
81 )
82 .map_err(|e| problem(e.to_string()))?;
83 if !matches!(entity.geometry, Geometry::Group) {
84 if let Geometry::Line { from_m, to_m } = entity.geometry {
85 writeln!(
86 output,
87 "add(object_{i}, Line {{ from: {}, to: {} }});",
88 vector(from_m),
89 vector(to_m)
90 )
91 .map_err(|e| problem(e.to_string()))?;
92 } else {
93 let mesh = &bundle.meshes[&entity.id];
94 write!(output, "add(object_{i}, Mesh {{ positions: [")
95 .map_err(|e| problem(e.to_string()))?;
96 for (index, p) in mesh.positions.iter().enumerate() {
97 if index > 0 {
98 output.push(',');
99 }
100 write!(output, "[{},{},{}]", p.x as f32, p.y as f32, p.z as f32)
101 .map_err(|e| problem(e.to_string()))?;
102 }
103 output.push_str("]:f32, normals: [");
104 for (index, p) in mesh.normals.iter().enumerate() {
105 if index > 0 {
106 output.push(',');
107 }
108 write!(output, "[{},{},{}]", p.x as f32, p.y as f32, p.z as f32)
109 .map_err(|e| problem(e.to_string()))?;
110 }
111 output.push_str("]:f32, indices: [");
112 for (index, triangle) in mesh.indices.as_chunks::<3>().0.iter().enumerate() {
113 if index > 0 {
114 output.push(',');
115 }
116 write!(output, "[{},{},{}]", triangle[0], triangle[1], triangle[2])
117 .map_err(|e| problem(e.to_string()))?;
118 }
119 output.push_str("]:u32 });\n");
120 }
121 }
122 let c = entity.color;
123 writeln!(output,"add(object_{i}, Material {{ color: \"#{:02x}{:02x}{:02x}{:02x}\", roughness: {}, emissive: {} }});",c.r,c.g,c.b,c.a,entity.material.roughness,entity.material.emissive).map_err(|e|problem(e.to_string()))?;
124 if output.len() > 1024 * 1024 {
125 return Err(problem(
126 "imported scene exceeds the 1 MiB source limit; simplify meshes in Blender",
127 ));
128 }
129 }
130 for (i, entity) in bundle.document.entities.iter().enumerate() {
131 if let Some(parent) = &entity.parent {
132 writeln!(
133 output,
134 "add(object_{i}, Parent {{ entity: object_{} }});",
135 ids[parent]
136 )
137 .map_err(|e| problem(e.to_string()))?;
138 }
139 }
140 Ok(output)
141}
142
143#[derive(Serialize)]
144struct MeshF32Json {
145 positions: Vec<PointF32>,
146 normals: Vec<PointF32>,
147 indices: Vec<u32>,
148}
149
150#[derive(Serialize)]
151struct PointF32 {
152 x: f32,
153 y: f32,
154 z: f32,
155}
156
157fn mesh_decode_call(output: &mut String, mesh: &konjure_sdk::Mesh) -> Result<(), Diagnostic> {
158 let mesh = MeshF32Json {
162 positions: mesh
163 .positions
164 .iter()
165 .map(|point| PointF32 {
166 x: point.x as f32,
167 y: point.y as f32,
168 z: point.z as f32,
169 })
170 .collect(),
171 normals: mesh
172 .normals
173 .iter()
174 .map(|normal| PointF32 {
175 x: normal.x as f32,
176 y: normal.y as f32,
177 z: normal.z as f32,
178 })
179 .collect(),
180 indices: mesh.indices.clone(),
181 };
182 let encoded = serde_json::to_string(&mesh).map_err(|error| problem(error.to_string()))?;
183 output.push_str("Mesh {}.decode_json([");
184 for (index, chunk) in encoded
185 .as_bytes()
186 .chunks(MAX_MESH_JSON_CHUNK_BYTES)
187 .enumerate()
188 {
189 if index > 0 {
190 output.push(',');
191 }
192 let chunk = std::str::from_utf8(chunk).map_err(|error| problem(error.to_string()))?;
193 output.push_str(&serde_json::to_string(chunk).map_err(|error| problem(error.to_string()))?);
197 }
198 output.push_str("])");
199 Ok(())
200}
201
202pub fn to_sources(bundle: &SceneBundle) -> Result<BTreeMap<String, String>, Diagnostic> {
215 bundle.validate()?;
216 let mesh_count = bundle
217 .document
218 .entities
219 .iter()
220 .filter(|entity| !matches!(entity.geometry, Geometry::Group | Geometry::Line { .. }))
221 .count();
222 if mesh_count + 1 > MAX_IMPORTED_MODULES {
223 return Err(problem(
224 "imported scene requires more than 64 source modules; simplify or combine meshes in Blender",
225 ));
226 }
227 let ids: BTreeMap<_, _> = bundle
228 .document
229 .entities
230 .iter()
231 .enumerate()
232 .map(|(index, entity)| (&entity.id, index))
233 .collect();
234 let mut sources = BTreeMap::new();
235 let mut main = String::from(
236 "// Imported scene.\n// Geometry is baked in geometry_N modules; transforms and materials remain editable.\n",
237 );
238 let mut next_geometry = 0usize;
239 for (index, entity) in bundle.document.entities.iter().enumerate() {
240 if !entity.animations.is_empty() || !entity.actions.is_empty() {
241 return Err(problem(
242 "exchange requires an evaluated scene without actions or animations",
243 ));
244 }
245 let q = entity.transform.rotation;
246 let (x, y, z) = DQuat::from_xyzw(q.x, q.y, q.z, q.w).to_euler(EulerRot::XYZ);
247 writeln!(
248 main,
249 "\n// {}\nlet object_{index} = spawn(\n Transform {{\n position: {},\n rotation: {},\n scale: {}\n }}\n);",
250 entity.id.0,
251 vector(entity.transform.translation),
252 vector(Vec3 { x, y, z }),
253 vector(entity.transform.scale),
254 )
255 .map_err(|e| problem(e.to_string()))?;
256 match entity.geometry {
257 Geometry::Group => {}
258 Geometry::Line { from_m, to_m } => writeln!(
259 main,
260 "add(object_{index}, Line {{ from: {}, to: {} }});",
261 vector(from_m),
262 vector(to_m)
263 )
264 .map_err(|e| problem(e.to_string()))?,
265 _ => {
266 let module = format!("geometry_{next_geometry}");
267 next_geometry += 1;
268 let mesh = bundle
269 .meshes
270 .get(&entity.id)
271 .ok_or_else(|| problem("validated bundle is missing geometry"))?;
272 let mut source = format!(
273 "// Baked immutable mesh data for {}. Edit transforms/material in main.\nfunc create() -> Res[Mesh, DataError] {{ ret ",
274 entity.id.0
275 );
276 mesh_decode_call(&mut source, mesh)?;
277 source.push_str("; }\nexport create;\n");
278 sources.insert(module.clone(), source);
279 let call = format!("{module}.create()?");
280 writeln!(main, "import {module};\nadd(object_{index}, {call});")
281 .map_err(|e| problem(e.to_string()))?;
282 }
283 }
284 let color = entity.color;
285 writeln!(
286 main,
287 "add(object_{index}, Material {{\n color: \"#{:02x}{:02x}{:02x}{:02x}\",\n roughness: {},\n emissive: {}\n}});",
288 color.r,
289 color.g,
290 color.b,
291 color.a,
292 entity.material.roughness,
293 entity.material.emissive
294 )
295 .map_err(|e| problem(e.to_string()))?;
296 }
297 for (index, entity) in bundle.document.entities.iter().enumerate() {
298 if let Some(parent) = &entity.parent {
299 writeln!(
300 main,
301 "add(object_{index}, Parent {{ entity: object_{} }});",
302 ids[parent]
303 )
304 .map_err(|e| problem(e.to_string()))?;
305 }
306 }
307 let bytes = main.len() + sources.values().map(String::len).sum::<usize>();
308 if bytes > MAX_IMPORTED_SOURCE_BYTES {
309 return Err(problem(
310 "imported source exceeds the 1 MiB project limit; simplify meshes in Blender",
311 ));
312 }
313 sources.insert("main".into(), main.clone());
314 let mut imports = sources.clone();
315 imports.remove("main");
316 crate::spatial::SpatialRuntime::new(&main, &imports)
317 .and_then(|runtime| runtime.render().map_err(|error| vec![error]))
318 .map_err(|errors| {
319 problem(
320 errors
321 .into_iter()
322 .map(|error| error.to_string())
323 .collect::<Vec<_>>()
324 .join("\n"),
325 )
326 })?;
327 Ok(sources)
328}
329
330pub fn from_runtime(runtime: &crate::spatial::SpatialRuntime) -> Result<SceneBundle, Diagnostic> {
335 let render = runtime.render().map_err(|e| problem(e.to_string()))?;
336 SceneBundle::new(render.document, render.meshes)
337}