1use crate::*;
2use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
3use serde::{Deserialize, Serialize};
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
5#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
6#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct Draw {
10 pub entity: EntityId,
12 pub mesh_entity: Option<EntityId>,
14 pub transform: Transform,
16 pub matrix: [f64; 16],
18 pub color: Color,
20}
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
23#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct Frame {
27 pub time_seconds: f64,
29 pub draws: Vec<Draw>,
31 pub events: Vec<RuntimeEvent>,
33}
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38#[serde(rename_all = "snake_case")]
39pub enum RuntimeEvent {
41 ActionApplied {
43 entity: EntityId,
45 action: ActionName,
47 },
48 EffectProposed {
50 entity: EntityId,
52 effect: String,
54 },
55}
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
58#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60#[serde(tag = "kind", rename_all = "snake_case")]
61pub enum Event {
63 InvokeAction {
65 entity: EntityId,
67 action: ActionName,
69 },
70}
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
73#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(deny_unknown_fields)]
75#[serde(rename_all = "snake_case")]
76pub enum ReceiptStatus {
78 Applied,
80 Proposed,
82}
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
85#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(deny_unknown_fields)]
87pub struct Receipt {
89 pub status: ReceiptStatus,
91 pub event: RuntimeEvent,
93}
94fn local_matrix(t: Transform) -> [f64; 16] {
95 let q = t.rotation;
96 let (x2, y2, z2) = (q.x + q.x, q.y + q.y, q.z + q.z);
97 let (xx, xy, xz) = (q.x * x2, q.x * y2, q.x * z2);
98 let (yy, yz, zz) = (q.y * y2, q.y * z2, q.z * z2);
99 let (wx, wy, wz) = (q.w * x2, q.w * y2, q.w * z2);
100 [
101 (1. - yy - zz) * t.scale.x,
102 (xy + wz) * t.scale.x,
103 (xz - wy) * t.scale.x,
104 0.,
105 (xy - wz) * t.scale.y,
106 (1. - xx - zz) * t.scale.y,
107 (yz + wx) * t.scale.y,
108 0.,
109 (xz + wy) * t.scale.z,
110 (yz - wx) * t.scale.z,
111 (1. - xx - yy) * t.scale.z,
112 0.,
113 t.translation.x,
114 t.translation.y,
115 t.translation.z,
116 1.,
117 ]
118}
119fn mul_matrix(a: [f64; 16], b: [f64; 16]) -> Result<[f64; 16], Diagnostic> {
120 let mut out = [0.; 16];
121 for column in 0..4 {
122 for row in 0..4 {
123 for k in 0..4 {
124 let term = a[k * 4 + row] * b[column * 4 + k];
125 out[column * 4 + row] += term;
126 if !out[column * 4 + row].is_finite()
127 || out[column * 4 + row].abs() > MAX_COORDINATE_M
128 {
129 return Err(Diagnostic::plain(
130 "world transform exceeds renderable bounds",
131 ));
132 }
133 }
134 }
135 }
136 Ok(out)
137}
138fn world_matrix(
139 scene: &SceneDocument,
140 local: &BTreeMap<EntityId, Transform>,
141 id: &EntityId,
142) -> Result<[f64; 16], Diagnostic> {
143 let mut chain = Vec::new();
144 let mut cursor = id.clone();
145 loop {
146 let entity = scene
147 .entities
148 .iter()
149 .find(|e| e.id == cursor)
150 .ok_or_else(|| Diagnostic::plain("entity disappeared while sampling"))?;
151 chain.push(cursor.clone());
152 if let Some(parent) = &entity.parent {
153 cursor = parent.clone()
154 } else {
155 break;
156 }
157 }
158 let mut matrix = [
159 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
160 ];
161 for entity in chain.iter().rev() {
162 matrix = mul_matrix(
163 matrix,
164 local_matrix(
165 *local
166 .get(entity)
167 .expect("topology validation preserves transforms"),
168 ),
169 )?
170 }
171 let columns = [
174 [matrix[0], matrix[1], matrix[2]],
175 [matrix[4], matrix[5], matrix[6]],
176 [matrix[8], matrix[9], matrix[10]],
177 ];
178 let lengths = columns.map(|v| libm::sqrt(v.iter().map(|n| n * n).sum()));
179 if lengths
180 .iter()
181 .any(|length| !(1e-6..=MAX_COORDINATE_M).contains(length))
182 {
183 return Err(Diagnostic::plain(
184 "world transform scale exceeds renderable bounds",
185 ));
186 }
187 let [a, b, c] = columns;
188 let determinant = a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
189 + a[2] * (b[0] * c[1] - b[1] * c[0]);
190 if determinant.abs() / lengths.iter().product::<f64>() < 1e-6 {
191 return Err(Diagnostic::plain(
192 "world transform basis is too close to singular for rendering",
193 ));
194 }
195 Ok(matrix)
196}
197pub struct Runtime {
203 scene: SceneDocument,
204 meshes: BTreeMap<EntityId, Mesh>,
205 motion_enabled: BTreeMap<EntityId, bool>,
206 colors: BTreeMap<EntityId, Color>,
207 events: Vec<RuntimeEvent>,
208}
209impl Runtime {
210 pub fn new(scene: SceneDocument) -> Result<Self, Diagnostic> {
220 validate(&scene)?;
221 let mut meshes = BTreeMap::new();
222 let mut motion_enabled = BTreeMap::new();
223 let mut colors = BTreeMap::new();
224 for e in &scene.entities {
225 match &e.geometry {
226 Geometry::Group => {}
227 Geometry::Mesh { asset } => {
228 return Err(Diagnostic::plain(format!(
229 "mesh asset `{asset}` is unresolved; provide it through a platform adapter"
230 )));
231 }
232 geometry => {
233 meshes.insert(e.id.clone(), tessellate(geometry)?);
234 }
235 }
236 motion_enabled.insert(e.id.clone(), true);
237 colors.insert(e.id.clone(), e.color);
238 }
239 Ok(Self {
240 scene,
241 meshes,
242 motion_enabled,
243 colors,
244 events: Vec::new(),
245 })
246 }
247 pub fn scene(&self) -> &SceneDocument {
249 &self.scene
250 }
251 pub fn meshes(&self) -> &BTreeMap<EntityId, Mesh> {
253 &self.meshes
254 }
255 pub fn sample(&self, time_seconds: f64) -> Result<Frame, Diagnostic> {
266 if !time_seconds.is_finite() || !(0.0..=MAX_SAMPLE_SECONDS).contains(&time_seconds) {
267 return Err(Diagnostic::plain(
268 "sample time must be finite and non-negative",
269 ));
270 }
271 let mut local = BTreeMap::new();
272 for e in &self.scene.entities {
273 let mut t = e.transform;
274 if self.motion_enabled.get(&e.id).copied().unwrap_or(true) {
275 for a in &e.animations {
276 match a {
277 Animation::Orbit { radius_m, period_s } => {
278 let x = 2. * core::f64::consts::PI * time_seconds / period_s;
279 t.translation.x += radius_m * libm::cos(x);
280 t.translation.z += radius_m * libm::sin(x)
281 }
282 Animation::Bob {
283 amplitude_m,
284 period_s,
285 } => {
286 t.translation.y += amplitude_m
287 * libm::sin(2. * core::f64::consts::PI * time_seconds / period_s)
288 }
289 Animation::Spin { degrees_per_second } => {
290 let r = degrees_per_second * core::f64::consts::PI / 180.
291 * time_seconds
292 / 2.;
293 let spin = Quaternion {
294 x: 0.,
295 y: libm::sin(r),
296 z: 0.,
297 w: libm::cos(r),
298 };
299 t.rotation = Quaternion {
300 x: spin.w * t.rotation.x
301 + spin.x * t.rotation.w
302 + spin.y * t.rotation.z
303 - spin.z * t.rotation.y,
304 y: spin.w * t.rotation.y - spin.x * t.rotation.z
305 + spin.y * t.rotation.w
306 + spin.z * t.rotation.x,
307 z: spin.w * t.rotation.z + spin.x * t.rotation.y
308 - spin.y * t.rotation.x
309 + spin.z * t.rotation.w,
310 w: spin.w * t.rotation.w
311 - spin.x * t.rotation.x
312 - spin.y * t.rotation.y
313 - spin.z * t.rotation.z,
314 };
315 }
316 }
317 }
318 }
319 local.insert(e.id.clone(), t);
320 }
321 let mut draws = Vec::with_capacity(self.scene.entities.len());
322 for e in &self.scene.entities {
323 let t = *local
324 .get(&e.id)
325 .expect("every entity has a local transform");
326 let matrix = world_matrix(&self.scene, &local, &e.id)?;
327 draws.push(Draw {
328 entity: e.id.clone(),
329 mesh_entity: self.meshes.contains_key(&e.id).then(|| e.id.clone()),
330 transform: t,
331 matrix,
332 color: *self.colors.get(&e.id).unwrap_or(&e.color),
333 });
334 }
335 draws.sort_by(|a, b| a.entity.cmp(&b.entity));
336 Ok(Frame {
337 time_seconds,
338 draws,
339 events: self.events.clone(),
340 })
341 }
342 pub fn dispatch(&mut self, event: Event) -> Result<Receipt, Diagnostic> {
353 match event {
354 Event::InvokeAction { entity, action } => {
355 let entity_def = self
356 .scene
357 .entities
358 .iter()
359 .find(|e| e.id == entity)
360 .ok_or_else(|| Diagnostic::plain("event references an unknown entity"))?;
361 let action_def = entity_def
362 .actions
363 .get(&action)
364 .ok_or_else(|| Diagnostic::plain("action is not declared on this entity"))?;
365 let runtime_event = match action_def {
366 Action::ToggleMotion => {
367 let current = self.motion_enabled.get(&entity).copied().unwrap_or(true);
368 self.motion_enabled.insert(entity.clone(), !current);
369 RuntimeEvent::ActionApplied {
370 entity: entity.clone(),
371 action: action.clone(),
372 }
373 }
374 Action::ChangeColor { color } => {
375 self.colors.insert(entity.clone(), *color);
376 RuntimeEvent::ActionApplied {
377 entity: entity.clone(),
378 action: action.clone(),
379 }
380 }
381 Action::ProposeEffect { effect } => RuntimeEvent::EffectProposed {
382 entity: entity.clone(),
383 effect: effect.clone(),
384 },
385 };
386 let status = if matches!(runtime_event, RuntimeEvent::EffectProposed { .. }) {
387 ReceiptStatus::Proposed
388 } else {
389 ReceiptStatus::Applied
390 };
391 if self.events.len() == MAX_RUNTIME_EVENTS {
392 self.events.remove(0);
393 }
394 self.events.push(runtime_event.clone());
395 Ok(Receipt {
396 status,
397 event: runtime_event,
398 })
399 }
400 }
401 }
402}