1use crate::{Diagnostic, Limits, Machine, Snapshot, Span, Value, compile};
6use konjure_sdk::{self as sdk, EntityId, Frame, Geometry, Mesh, SceneDocument};
7use rapier3d::{
8 glamx::{DQuat, EulerRot},
9 prelude::*,
10};
11use serde::Serialize;
12use std::collections::{BTreeMap, BTreeSet};
13
14pub const FIXED_DT: f64 = 1.0 / 60.0;
16pub const MAX_STEP_COUNT: u32 = 600;
18pub const MAX_CLOUD_POINTS: usize = 256;
20const MAX_MESH_VERTICES: usize = 16_384;
21const MAX_MESH_INDICES: usize = 98_304;
22const MAX_SCENE_VERTICES: usize = 262_144;
23const GEOMETRIES: &[&str] = &[
24 "Sphere",
25 "Box",
26 "Cylinder",
27 "Quad",
28 "Line",
29 "Ray",
30 "Mesh",
31 "PointCloud",
32];
33
34#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
36pub struct Contact {
37 pub entity_a: u64,
39 pub entity_b: u64,
41}
42
43#[derive(Clone, Debug, Serialize)]
45pub struct RenderOutput {
46 pub ui: konjure_sdk::presentation::UiDocument,
48 pub document: SceneDocument,
50 pub meshes: BTreeMap<EntityId, Mesh>,
52 pub frame: Frame,
54 pub contacts: Vec<Contact>,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59enum BodyKind {
60 Dynamic,
61 Fixed,
62 Kinematic,
63}
64#[derive(Clone, Debug, PartialEq)]
65enum CollisionShape {
66 Sphere(f32),
67 Box(Vector),
68}
69#[derive(Clone, Debug, PartialEq)]
70struct BodyConfig {
71 kind: BodyKind,
72 shape: CollisionShape,
73 mass: f32,
74 restitution: f32,
75 friction: f32,
76}
77#[derive(Clone)]
78struct Binding {
79 handle: RigidBodyHandle,
80 config: BodyConfig,
81 transform: Value,
82 rigid_body: Value,
83}
84
85pub struct SpatialRuntime {
87 machine: Machine,
88 world: PhysicsWorld,
89 bindings: BTreeMap<u64, Binding>,
90 contacts: Vec<Contact>,
91}
92
93impl Clone for SpatialRuntime {
94 fn clone(&self) -> Self {
95 Self {
98 machine: self.machine.clone(),
99 world: PhysicsWorld {
100 gravity: self.world.gravity,
101 integration_parameters: self.world.integration_parameters,
102 physics_pipeline: PhysicsPipeline::new(),
103 islands: self.world.islands.clone(),
104 broad_phase: self.world.broad_phase.clone(),
105 narrow_phase: self.world.narrow_phase.clone(),
106 bodies: self.world.bodies.clone(),
107 colliders: self.world.colliders.clone(),
108 impulse_joints: self.world.impulse_joints.clone(),
109 multibody_joints: self.world.multibody_joints.clone(),
110 ccd_solver: CCDSolver::new(),
111 },
112 bindings: self.bindings.clone(),
113 contacts: self.contacts.clone(),
114 }
115 }
116}
117
118fn accept_action_result(value: &Value) -> Result<(), Diagnostic> {
119 if let Value::Enum(value) = value
120 && value.case == "Err"
121 && matches!(&value.ty,crate::Type::Applied{name,..}if name=="Res")
122 {
123 let cause = value
124 .payload
125 .first()
126 .map(ToString::to_string)
127 .unwrap_or_else(|| "Unknown".into());
128 let code = match cause.as_str() {
129 "Overflow" => "numeric_overflow",
130 "DivisionByZero" => "division_by_zero",
131 "InexactConversion" => "inexact_conversion",
132 "Bounds" => "index_out_of_bounds",
133 "InvalidUtf8" => "invalid_utf8",
134 "InvalidShape" | "ShapeMismatch" => "shape_mismatch",
135 _ => "data_error",
136 };
137 return Err(Diagnostic::new(
138 code,
139 format!("action returned Err({cause})"),
140 value
141 .origin
142 .clone()
143 .unwrap_or_else(|| Span::new("main", 0, 0)),
144 ));
145 }
146 Ok(())
147}
148impl SpatialRuntime {
149 pub fn new(source: &str, modules: &BTreeMap<String, String>) -> Result<Self, Vec<Diagnostic>> {
152 let program = compile(source, modules)?;
153 let machine = Machine::new(program, Limits::default()).map_err(|error| vec![error])?;
154 let mut runtime = Self {
155 machine,
156 world: PhysicsWorld::default(),
157 bindings: BTreeMap::new(),
158 contacts: Vec::new(),
159 };
160 runtime.world.integration_parameters.dt = FIXED_DT as f32;
161 runtime.normalize().map_err(|error| vec![error])?;
162 runtime.render().map_err(|error| vec![error])?;
163 runtime.reconcile(false).map_err(|error| vec![error])?;
164 Ok(runtime)
165 }
166
167 #[must_use]
170 pub const fn execution_state(&self) -> (u64, bool) {
171 self.machine.execution_state()
172 }
173
174 pub fn step(&mut self, count: u32) -> Result<Snapshot, Diagnostic> {
177 if count > MAX_STEP_COUNT {
178 return Err(error(
179 &Span::default(),
180 "step_limit",
181 format!("step count exceeds {MAX_STEP_COUNT}"),
182 ));
183 }
184 if count == 0 {
185 return Ok(self.snapshot());
186 }
187 let mut candidate = self.clone();
188 for _ in 0..count {
189 candidate.machine.tick(FIXED_DT)?;
190 candidate.normalize()?;
191 candidate.render()?;
192 candidate.reconcile(true)?;
193 candidate.world.step();
194 candidate.write_back()?;
195 candidate.render()?;
196 candidate.reconcile(false)?;
197 }
198 *self = candidate;
199 Ok(self.snapshot())
200 }
201
202 pub fn finish(&mut self) -> Result<Snapshot, Diagnostic> {
208 if self.snapshot().finished {
209 return Ok(self.snapshot());
210 }
211 let mut candidate = self.clone();
212 candidate.machine.finish()?;
213 candidate.normalize()?;
214 candidate.render()?;
215 candidate.reconcile(false)?;
216 *self = candidate;
217 Ok(self.snapshot())
218 }
219
220 pub fn invoke(&mut self, name: &str, arguments: &[Value]) -> Result<Value, Diagnostic> {
223 let mut candidate = self.clone();
224 let result = candidate.machine.invoke(name, arguments.to_vec())?;
225 accept_action_result(&result)?;
226 candidate.normalize()?;
227 candidate.render()?;
228 candidate.reconcile(false)?;
229 *self = candidate;
230 Ok(result)
231 }
232
233 pub fn invoke_component(
236 &mut self,
237 id: u64,
238 component: &str,
239 field: &str,
240 arguments: &[Value],
241 ) -> Result<Value, Diagnostic> {
242 let mut candidate = self.clone();
243 let result =
244 candidate
245 .machine
246 .invoke_component(id, component, field, arguments.to_vec())?;
247 accept_action_result(&result)?;
248 candidate.normalize()?;
249 candidate.render()?;
250 candidate.reconcile(false)?;
251 *self = candidate;
252 Ok(result)
253 }
254
255 pub fn invoke_entity(
258 &mut self,
259 id: u64,
260 method: &str,
261 arguments: &[Value],
262 ) -> Result<Value, Diagnostic> {
263 let mut candidate = self.clone();
264 let result = candidate
265 .machine
266 .invoke_entity(id, method, arguments.to_vec())?;
267 accept_action_result(&result)?;
268 candidate.normalize()?;
269 candidate.render()?;
270 candidate.reconcile(false)?;
271 *self = candidate;
272 Ok(result)
273 }
274
275 pub fn snapshot(&self) -> Snapshot {
277 self.machine.snapshot()
278 }
279
280 pub fn render(&self) -> Result<RenderOutput, Diagnostic> {
282 let snapshot = self.snapshot();
283 let ui = crate::ui::document(&snapshot, 0)
284 .map_err(|problem| error(&Span::default(), "invalid_ui", problem.to_string()))?;
285 let mut document = SceneDocument {
286 format: "konjure.scene".into(),
287 version: sdk::FORMAT_VERSION,
288 id: sdk::SceneId("language_scene".into()),
289 title: "Konjure language scene".into(),
290 entities: Vec::new(),
291 conversation: Vec::new(),
292 };
293 let mut meshes = BTreeMap::new();
294 let mut vertex_count = 0;
295 for entity in &snapshot.entities {
296 let span = &entity.source;
297 let transform = entity
299 .components
300 .get("Transform")
301 .map(|value| transform(value, span))
302 .transpose()?
303 .unwrap_or_default();
304 let (color, material) = entity
305 .components
306 .get("Material")
307 .map(|value| material(value, span))
308 .transpose()?
309 .unwrap_or((
310 sdk::Color {
311 r: 111,
312 g: 227,
313 b: 255,
314 a: 255,
315 },
316 sdk::Material {
317 roughness: 0.6,
318 emissive: 0.0,
319 },
320 ));
321 let Some((name, value)) = geometry_component(&entity.components, span)? else {
322 continue;
323 };
324 let id = EntityId(format!("entity_{}", entity.id));
325 let (geometry, mesh) = geometry(name, value, &id, span)?;
326 vertex_count += mesh.positions.len();
327 if vertex_count > MAX_SCENE_VERTICES {
328 return Err(error(
329 span,
330 "mesh_limit",
331 "scene exceeds its generated vertex budget",
332 ));
333 }
334 meshes.insert(id.clone(), mesh);
335 document.entities.push(sdk::Entity {
336 id,
337 parent: None,
338 transform,
339 geometry,
340 color,
341 material,
342 animations: Vec::new(),
343 actions: BTreeMap::new(),
344 });
345 }
346 sdk::validate(&document).map_err(|diagnostic| {
347 error(&Span::default(), "invalid_scene", diagnostic.to_string())
348 })?;
349 let mut sampling_document = document.clone();
351 for entity in &mut sampling_document.entities {
352 entity.geometry = Geometry::Group;
353 }
354 let evaluator = sdk::Runtime::new(sampling_document).map_err(|diagnostic| {
355 error(&Span::default(), "invalid_scene", diagnostic.to_string())
356 })?;
357 let mut frame = evaluator.sample(snapshot.time).map_err(|diagnostic| {
358 error(&Span::default(), "invalid_scene", diagnostic.to_string())
359 })?;
360 for draw in &mut frame.draws {
361 draw.mesh_entity = Some(draw.entity.clone());
362 }
363 Ok(RenderOutput {
364 ui,
365 document,
366 meshes,
367 frame,
368 contacts: self.contacts.clone(),
369 })
370 }
371
372 fn normalize(&mut self) -> Result<(), Diagnostic> {
373 let mut updates = Vec::new();
374 for entity in self.snapshot().entities {
375 if geometry_component(&entity.components, &entity.source)?.is_some() {
376 if !entity.components.contains_key("Transform") {
377 updates.push((entity.id, default_transform()));
378 }
379 if !entity.components.contains_key("Material") {
380 updates.push((entity.id, default_material()));
381 }
382 }
383 }
384 self.machine.upsert_components_for_adapter(&updates)
385 }
386
387 fn reconcile(&mut self, apply_forces: bool) -> Result<(), Diagnostic> {
388 let snapshot = self.snapshot();
389 let present: BTreeSet<_> = snapshot
390 .entities
391 .iter()
392 .filter(|entity| entity.components.contains_key("RigidBody"))
393 .map(|entity| entity.id)
394 .collect();
395 let removed: Vec<_> = self
396 .bindings
397 .keys()
398 .filter(|id| !present.contains(id))
399 .copied()
400 .collect();
401 for id in removed {
402 if let Some(binding) = self.bindings.remove(&id) {
403 self.world.remove_body(binding.handle);
404 }
405 }
406 let mut spatial_change = false;
407 for entity in &snapshot.entities {
408 let span = &entity.source;
409 let force = entity
410 .components
411 .get("Force")
412 .map(|value| vector(field(value, "value", span)?, span))
413 .transpose()?;
414 let impulse = entity
415 .components
416 .get("Impulse")
417 .map(|value| vector(field(value, "value", span)?, span))
418 .transpose()?;
419 let Some(body_value) = entity.components.get("RigidBody") else {
420 if force.is_some() || impulse.is_some() {
421 return Err(error(
422 span,
423 "missing_body",
424 "Force and Impulse require a dynamic RigidBody",
425 ));
426 }
427 continue;
428 };
429 let transform_value = entity.components.get("Transform").ok_or_else(|| {
430 error(
431 span,
432 "missing_shape",
433 "RigidBody requires a Sphere or Box geometry",
434 )
435 })?;
436 let transform = transform(transform_value, span)?;
437 let config = body_config(body_value, &entity.components, transform.scale, span)?;
438 if config.kind != BodyKind::Dynamic && (force.is_some() || impulse.is_some()) {
439 return Err(error(
440 span,
441 "invalid_force",
442 "Force and Impulse require a dynamic RigidBody",
443 ));
444 }
445 let velocity = vector(field(body_value, "velocity", span)?, span)?;
446 let angular_velocity = vector(field(body_value, "angular_velocity", span)?, span)?;
447 if config.kind == BodyKind::Fixed
448 && (velocity != sdk::Vec3::ZERO || angular_velocity != sdk::Vec3::ZERO)
449 {
450 return Err(error(
451 span,
452 "fixed_velocity",
453 "a fixed RigidBody cannot have nonzero velocity",
454 ));
455 }
456 let mut old = self.bindings.get(&entity.id).cloned();
457 if let Some(binding) = &old {
458 if binding.config.kind == BodyKind::Dynamic
459 && (binding.transform.field("position") != transform_value.field("position")
460 || binding.transform.field("rotation") != transform_value.field("rotation"))
461 {
462 return Err(error(
463 span,
464 "dynamic_pose_edit",
465 "dynamic body position and rotation are owned by physics; use velocity, Force, or Impulse",
466 ));
467 }
468 if binding.config != config {
469 self.world.remove_body(binding.handle);
470 old = None;
471 }
472 }
473 let pose = pose(transform);
474 let handle = if let Some(binding) = old {
475 let body =
476 self.world.bodies.get_mut(binding.handle).ok_or_else(|| {
477 error(span, "physics_state", "missing persistent rigid body")
478 })?;
479 if binding.transform != *transform_value {
480 body.set_position(pose, true);
481 spatial_change = true;
482 }
483 if binding.rigid_body.field("velocity") != body_value.field("velocity") {
484 body.set_linvel(to_vector(velocity), true);
485 }
486 if binding.rigid_body.field("angular_velocity")
487 != body_value.field("angular_velocity")
488 {
489 body.set_angvel(to_vector(angular_velocity), true);
490 }
491 binding.handle
492 } else {
493 let builder = match config.kind {
494 BodyKind::Dynamic => RigidBodyBuilder::dynamic(),
495 BodyKind::Fixed => RigidBodyBuilder::fixed(),
496 BodyKind::Kinematic => RigidBodyBuilder::kinematic_velocity_based(),
497 };
498 let collider = match config.shape {
499 CollisionShape::Sphere(radius) => ColliderBuilder::ball(radius),
500 CollisionShape::Box(size) => {
501 ColliderBuilder::cuboid(size.x / 2.0, size.y / 2.0, size.z / 2.0)
502 }
503 };
504 let (handle, _) = self.world.insert(
505 builder
506 .pose(pose)
507 .linvel(to_vector(velocity))
508 .angvel(to_vector(angular_velocity))
509 .ccd_enabled(true)
510 .user_data(u128::from(entity.id)),
511 collider
512 .mass(config.mass)
513 .restitution(config.restitution)
514 .friction(config.friction)
515 .user_data(u128::from(entity.id)),
516 );
517 spatial_change = true;
518 handle
519 };
520 if apply_forces {
521 let body = self.world.bodies.get_mut(handle).ok_or_else(|| {
522 error(
523 span,
524 "physics_state",
525 "missing rigid body during force application",
526 )
527 })?;
528 body.reset_forces(false);
529 if let Some(force) = force {
530 body.add_force(to_vector(force), true);
531 }
532 if let Some(impulse) = impulse {
533 body.apply_impulse(to_vector(impulse), true);
534 }
535 }
536 self.bindings.insert(
537 entity.id,
538 Binding {
539 handle,
540 config,
541 transform: transform_value.clone(),
542 rigid_body: body_value.clone(),
543 },
544 );
545 }
546 if spatial_change {
548 self.contacts.clear();
549 }
550 self.contacts
551 .retain(|pair| present.contains(&pair.entity_a) && present.contains(&pair.entity_b));
552 Ok(())
553 }
554
555 fn write_back(&mut self) -> Result<(), Diagnostic> {
556 let mut updates = Vec::new();
557 let snapshot = self.snapshot();
558 for entity in &snapshot.entities {
559 let Some(binding) = self.bindings.get_mut(&entity.id) else {
560 continue;
561 };
562 let body = self.world.bodies.get(binding.handle).ok_or_else(|| {
563 error(
564 &entity.source,
565 "physics_state",
566 "missing simulated rigid body",
567 )
568 })?;
569 let position = body.translation();
570 let (x, y, z) = body.rotation().to_euler(EulerRot::XYZ);
571 let mut transform_value = binding.transform.clone();
572 replace_field(
573 &mut transform_value,
574 "position",
575 vector_value(from_vector(position)),
576 &entity.source,
577 )?;
578 replace_field(
579 &mut transform_value,
580 "rotation",
581 vector_value(sdk::Vec3 {
582 x: f64::from(x),
583 y: f64::from(y),
584 z: f64::from(z),
585 }),
586 &entity.source,
587 )?;
588 let mut rigid_body = binding.rigid_body.clone();
589 replace_field(
590 &mut rigid_body,
591 "velocity",
592 vector_value(from_vector(body.linvel())),
593 &entity.source,
594 )?;
595 replace_field(
596 &mut rigid_body,
597 "angular_velocity",
598 vector_value(from_vector(body.angvel())),
599 &entity.source,
600 )?;
601 binding.transform = transform_value.clone();
602 binding.rigid_body = rigid_body.clone();
603 updates.push((entity.id, transform_value));
604 updates.push((entity.id, rigid_body));
605 if entity.components.contains_key("Impulse") {
606 updates.push((
607 entity.id,
608 record("Impulse", [("value", vector_value(sdk::Vec3::ZERO))]),
609 ));
610 }
611 }
612 self.machine.patch_components_for_adapter(&updates)?;
613 let mut pairs = BTreeSet::new();
614 for contact in self
615 .world
616 .contact_pairs()
617 .filter(|contact| contact.has_any_active_contact())
618 {
619 let Some(a) = self.world.colliders.get(contact.collider1) else {
620 continue;
621 };
622 let Some(b) = self.world.colliders.get(contact.collider2) else {
623 continue;
624 };
625 let a = u64::try_from(a.user_data).map_err(|_| {
626 error(
627 &Span::default(),
628 "physics_state",
629 "invalid contact entity ID",
630 )
631 })?;
632 let b = u64::try_from(b.user_data).map_err(|_| {
633 error(
634 &Span::default(),
635 "physics_state",
636 "invalid contact entity ID",
637 )
638 })?;
639 pairs.insert(Contact {
640 entity_a: a.min(b),
641 entity_b: a.max(b),
642 });
643 }
644 self.contacts = pairs.into_iter().collect();
645 Ok(())
646 }
647}
648
649fn error(span: &Span, code: &str, message: impl Into<String>) -> Diagnostic {
650 Diagnostic::new(code, message, span.clone())
651}
652fn field<'a>(value: &'a Value, name: &str, span: &Span) -> Result<&'a Value, Diagnostic> {
653 value.field(name).ok_or_else(|| {
654 error(
655 span,
656 "spatial_field",
657 format!("{} requires field `{name}`", value.type_name()),
658 )
659 })
660}
661fn number(value: &Value, span: &Span) -> Result<f64, Diagnostic> {
662 match value {
663 Value::Number(number) if number.is_finite() && number.abs() <= sdk::MAX_COORDINATE_M => {
664 Ok(*number)
665 }
666 _ => Err(error(
667 span,
668 "spatial_number",
669 "spatial number must be finite and within +/-1,000,000",
670 )),
671 }
672}
673fn numeric_field(value: &Value, name: &str, span: &Span) -> Result<f64, Diagnostic> {
674 number(field(value, name, span)?, span)
675}
676fn positive(value: f64, span: &Span) -> Result<f64, Diagnostic> {
677 if value.is_finite() && (1e-6..=sdk::MAX_COORDINATE_M).contains(&value) {
678 Ok(value)
679 } else {
680 Err(error(
681 span,
682 "spatial_dimension",
683 "dimensions must be between 0.000001 and 1,000,000",
684 ))
685 }
686}
687fn text<'a>(value: &'a Value, span: &Span) -> Result<&'a str, Diagnostic> {
688 if let Value::Text(text) = value {
689 Ok(text)
690 } else {
691 Err(error(span, "spatial_type", "expected a Str"))
692 }
693}
694fn vector(value: &Value, span: &Span) -> Result<sdk::Vec3, Diagnostic> {
695 if value.type_name() != "Vec3" {
696 return Err(error(span, "spatial_type", "expected a Vec3"));
697 }
698 Ok(sdk::Vec3 {
699 x: numeric_field(value, "x", span)?,
700 y: numeric_field(value, "y", span)?,
701 z: numeric_field(value, "z", span)?,
702 })
703}
704fn tensor<'a>(value: &'a Value, span: &Span) -> Result<&'a sdk::data::Tensor, Diagnostic> {
705 if let Value::Tensor(value) = value {
706 Ok(value)
707 } else {
708 Err(error(span, "spatial_type", "expected a Tensor"))
709 }
710}
711
712fn tensor_positions(
714 value: &Value,
715 maximum: usize,
716 span: &Span,
717) -> Result<Vec<sdk::Vec3>, Diagnostic> {
718 let tensor = tensor(value, span)?;
719 let shape = tensor.shape();
720 if tensor.dtype() != sdk::data::DType::F32
721 || shape.len() != 2
722 || shape[1] != 3
723 || shape[0] > maximum
724 {
725 return Err(error(
726 span,
727 "spatial_shape",
728 "positions require a bounded N by 3 Tensor[f32]",
729 ));
730 }
731 (0..shape[0])
732 .map(|row| {
733 let coordinate = |axis| {
734 let value = tensor
735 .get(&[row as isize, axis])
736 .and_then(|value| value.to_f64())
737 .map_err(|e| error(span, "spatial_type", e.to_string()))?;
738 if value.abs() > sdk::MAX_COORDINATE_M {
739 return Err(error(
740 span,
741 "spatial_dimension",
742 "position exceeds coordinate bounds",
743 ));
744 }
745 Ok(value)
746 };
747 Ok(sdk::Vec3 {
748 x: coordinate(0)?,
749 y: coordinate(1)?,
750 z: coordinate(2)?,
751 })
752 })
753 .collect()
754}
755fn record<const N: usize>(class: &str, fields: [(&str, Value); N]) -> Value {
756 Value::record(
757 class,
758 fields.into_iter().map(|(name, value)| (name.into(), value)),
759 )
760}
761fn vector_value(vector: sdk::Vec3) -> Value {
762 record(
763 "Vec3",
764 [
765 ("x", Value::Number(vector.x)),
766 ("y", Value::Number(vector.y)),
767 ("z", Value::Number(vector.z)),
768 ],
769 )
770}
771fn default_transform() -> Value {
772 record(
773 "Transform",
774 [
775 ("position", vector_value(sdk::Vec3::ZERO)),
776 ("rotation", vector_value(sdk::Vec3::ZERO)),
777 (
778 "scale",
779 vector_value(sdk::Vec3 {
780 x: 1.0,
781 y: 1.0,
782 z: 1.0,
783 }),
784 ),
785 ],
786 )
787}
788fn default_material() -> Value {
789 record(
790 "Material",
791 [
792 ("color", Value::Text("#6fe3ff".into())),
793 ("roughness", Value::Number(0.6)),
794 ("emissive", Value::Number(0.0)),
795 ],
796 )
797}
798fn replace_field(
799 value: &mut Value,
800 key: &str,
801 replacement: Value,
802 span: &Span,
803) -> Result<(), Diagnostic> {
804 if let Value::Record(record) = value {
805 record.fields.insert(key.into(), replacement);
806 Ok(())
807 } else {
808 Err(error(span, "spatial_type", "component must be a record"))
809 }
810}
811fn to_vector(value: sdk::Vec3) -> Vector {
812 Vector::new(value.x as f32, value.y as f32, value.z as f32)
813}
814fn from_vector(value: Vector) -> sdk::Vec3 {
815 sdk::Vec3 {
816 x: f64::from(value.x),
817 y: f64::from(value.y),
818 z: f64::from(value.z),
819 }
820}
821fn pose(transform: sdk::Transform) -> Pose {
822 let q = transform.rotation;
823 Pose::from_parts(
824 to_vector(transform.translation),
825 Rotation::from_xyzw(q.x as f32, q.y as f32, q.z as f32, q.w as f32).normalize(),
826 )
827}
828fn transform(value: &Value, span: &Span) -> Result<sdk::Transform, Diagnostic> {
829 let translation = vector(field(value, "position", span)?, span)?;
830 let angles = vector(field(value, "rotation", span)?, span)?;
831 let scale = vector(field(value, "scale", span)?, span)?;
832 for dimension in [scale.x, scale.y, scale.z] {
833 positive(dimension, span)?;
834 }
835 let q = DQuat::from_euler(EulerRot::XYZ, angles.x, angles.y, angles.z);
836 Ok(sdk::Transform {
837 translation,
838 rotation: sdk::Quaternion {
839 x: q.x,
840 y: q.y,
841 z: q.z,
842 w: q.w,
843 },
844 scale,
845 })
846}
847fn material(value: &Value, span: &Span) -> Result<(sdk::Color, sdk::Material), Diagnostic> {
848 let color = text(field(value, "color", span)?, span)?;
849 let digits = color
850 .strip_prefix('#')
851 .filter(|digits| {
852 matches!(digits.len(), 6 | 8)
853 && digits.is_ascii()
854 && digits.bytes().all(|byte| byte.is_ascii_hexdigit())
855 })
856 .ok_or_else(|| {
857 error(
858 span,
859 "invalid_color",
860 "Material.color must be #RRGGBB or #RRGGBBAA",
861 )
862 })?;
863 let channel = |offset: usize| {
864 u8::from_str_radix(&digits[offset..offset + 2], 16)
865 .map_err(|_| error(span, "invalid_color", "invalid hexadecimal color"))
866 };
867 let roughness = numeric_field(value, "roughness", span)?;
868 let emissive = numeric_field(value, "emissive", span)?;
869 if !(0.0..=1.0).contains(&roughness) || !(0.0..=100.0).contains(&emissive) {
870 return Err(error(
871 span,
872 "invalid_material",
873 "roughness must be 0..=1 and emissive must be 0..=100",
874 ));
875 }
876 Ok((
877 sdk::Color {
878 r: channel(0)?,
879 g: channel(2)?,
880 b: channel(4)?,
881 a: if digits.len() == 8 { channel(6)? } else { 255 },
882 },
883 sdk::Material {
884 roughness: roughness as f32,
885 emissive: emissive as f32,
886 },
887 ))
888}
889fn geometry_component<'a>(
890 components: &'a BTreeMap<String, Value>,
891 span: &Span,
892) -> Result<Option<(&'static str, &'a Value)>, Diagnostic> {
893 let mut present = GEOMETRIES
894 .iter()
895 .filter_map(|name| components.get(*name).map(|value| (*name, value)));
896 let first = present.next();
897 if present.next().is_some() {
898 return Err(error(
899 span,
900 "multiple_geometry",
901 "an entity may have only one geometry component",
902 ));
903 }
904 Ok(first)
905}
906fn body_config(
907 value: &Value,
908 components: &BTreeMap<String, Value>,
909 scale: sdk::Vec3,
910 span: &Span,
911) -> Result<BodyConfig, Diagnostic> {
912 let kind = match text(field(value, "kind", span)?, span)? {
913 "dynamic" => BodyKind::Dynamic,
914 "fixed" => BodyKind::Fixed,
915 "kinematic" => BodyKind::Kinematic,
916 _ => {
917 return Err(error(
918 span,
919 "invalid_body",
920 "RigidBody.kind must be dynamic, fixed, or kinematic",
921 ));
922 }
923 };
924 let shape = match geometry_component(components, span)? {
925 Some(("Sphere", sphere)) => {
926 if scale.x != scale.y || scale.x != scale.z {
927 return Err(error(
928 span,
929 "unsupported_shape",
930 "a physical Sphere requires uniform scale",
931 ));
932 }
933 CollisionShape::Sphere(
934 positive(numeric_field(sphere, "radius", span)? * scale.x, span)? as f32,
935 )
936 }
937 Some(("Box", box_value)) => {
938 let size = vector(field(box_value, "size", span)?, span)?;
939 CollisionShape::Box(Vector::new(
940 positive(size.x * scale.x, span)? as f32,
941 positive(size.y * scale.y, span)? as f32,
942 positive(size.z * scale.z, span)? as f32,
943 ))
944 }
945 _ => {
946 return Err(error(
947 span,
948 "unsupported_shape",
949 "RigidBody collision geometry must be Sphere or Box",
950 ));
951 }
952 };
953 let mass = positive(numeric_field(value, "mass", span)?, span)? as f32;
954 let restitution = numeric_field(value, "restitution", span)?;
955 let friction = numeric_field(value, "friction", span)?;
956 if !(0.0..=1.0).contains(&restitution) || !(0.0..=1.0).contains(&friction) {
957 return Err(error(
958 span,
959 "invalid_body",
960 "restitution and friction must be 0..=1",
961 ));
962 }
963 Ok(BodyConfig {
964 kind,
965 shape,
966 mass,
967 restitution: restitution as f32,
968 friction: friction as f32,
969 })
970}
971
972fn geometry(
973 name: &str,
974 value: &Value,
975 id: &EntityId,
976 span: &Span,
977) -> Result<(Geometry, Mesh), Diagnostic> {
978 let geometry = match name {
979 "Sphere" => Geometry::Sphere {
980 radius_m: positive(numeric_field(value, "radius", span)?, span)?,
981 },
982 "Box" => Geometry::Box {
983 size_m: vector(field(value, "size", span)?, span)?,
984 },
985 "Cylinder" => Geometry::Cylinder {
986 radius_m: positive(numeric_field(value, "radius", span)?, span)?,
987 height_m: positive(numeric_field(value, "height", span)?, span)?,
988 },
989 "Line" => Geometry::Line {
990 from_m: vector(field(value, "from", span)?, span)?,
991 to_m: vector(field(value, "to", span)?, span)?,
992 },
993 "Ray" => {
994 let origin = vector(field(value, "origin", span)?, span)?;
995 let direction = vector(field(value, "direction", span)?, span)?;
996 let length = positive(numeric_field(value, "length", span)?, span)?;
997 let magnitude = direction.x.hypot(direction.y).hypot(direction.z);
998 if magnitude <= 1e-12 {
999 return Err(error(span, "invalid_ray", "Ray.direction must be nonzero"));
1000 }
1001 Geometry::Line {
1002 from_m: origin,
1003 to_m: sdk::Vec3 {
1004 x: origin.x + direction.x / magnitude * length,
1005 y: origin.y + direction.y / magnitude * length,
1006 z: origin.z + direction.z / magnitude * length,
1007 },
1008 }
1009 }
1010 "Quad" => {
1011 let x = positive(numeric_field(value, "width", span)?, span)? / 2.0;
1012 let y = positive(numeric_field(value, "height", span)?, span)? / 2.0;
1013 return Ok((
1014 Geometry::Mesh {
1015 asset: id.0.clone(),
1016 },
1017 Mesh {
1018 positions: vec![
1019 sdk::Vec3 {
1020 x: -x,
1021 y: -y,
1022 z: 0.0,
1023 },
1024 sdk::Vec3 { x, y: -y, z: 0.0 },
1025 sdk::Vec3 { x, y, z: 0.0 },
1026 sdk::Vec3 { x: -x, y, z: 0.0 },
1027 ],
1028 normals: vec![
1029 sdk::Vec3 {
1030 x: 0.0,
1031 y: 0.0,
1032 z: 1.0
1033 };
1034 4
1035 ],
1036 indices: vec![0, 1, 2, 0, 2, 3],
1037 },
1038 ));
1039 }
1040 "Mesh" => {
1041 return Ok((
1042 Geometry::Mesh {
1043 asset: id.0.clone(),
1044 },
1045 custom_mesh(value, span)?,
1046 ));
1047 }
1048 "PointCloud" => {
1049 return Ok((
1050 Geometry::Mesh {
1051 asset: id.0.clone(),
1052 },
1053 point_cloud(value, span)?,
1054 ));
1055 }
1056 _ => {
1057 return Err(error(
1058 span,
1059 "unsupported_geometry",
1060 "unsupported geometry component",
1061 ));
1062 }
1063 };
1064 let mesh = sdk::tessellate(&geometry)
1065 .map_err(|diagnostic| error(span, "invalid_geometry", diagnostic.to_string()))?;
1066 Ok((geometry, mesh))
1067}
1068
1069fn custom_mesh(value: &Value, span: &Span) -> Result<Mesh, Diagnostic> {
1070 let positions = tensor_positions(field(value, "positions", span)?, MAX_MESH_VERTICES, span)?;
1071 let index_data = tensor(field(value, "indices", span)?, span)?;
1072 let shape = index_data.shape();
1073 if positions.len() < 3
1074 || index_data.dtype() != sdk::data::DType::U32
1075 || shape.len() != 2
1076 || shape[1] != 3
1077 || shape[0] == 0
1078 || shape[0] > MAX_MESH_INDICES / 3
1079 {
1080 return Err(error(
1081 span,
1082 "invalid_mesh",
1083 "Mesh requires N by 3 positions and M by 3 triangle indices within the mesh limits",
1084 ));
1085 }
1086 let mut indices = Vec::with_capacity(shape[0] * 3);
1087 for triangle in 0..shape[0] {
1088 for corner in 0..3 {
1089 let index = index_data
1090 .get(&[triangle as isize, corner])
1091 .map_err(|e| error(span, "invalid_mesh_index", e.to_string()))?
1092 .as_u128()
1093 .ok_or_else(|| error(span, "spatial_type", "mesh indices must be u32"))?;
1094 if index >= positions.len() as u128 {
1095 return Err(error(
1096 span,
1097 "invalid_mesh_index",
1098 "mesh indices must address an existing vertex",
1099 ));
1100 }
1101 indices.push(index as u32);
1102 }
1103 }
1104 let mut normals = vec![sdk::Vec3::ZERO; positions.len()];
1105 for triangle in indices.as_chunks::<3>().0 {
1106 let a = positions[triangle[0] as usize];
1107 let b = positions[triangle[1] as usize];
1108 let c = positions[triangle[2] as usize];
1109 let ab = sdk::Vec3 {
1110 x: b.x - a.x,
1111 y: b.y - a.y,
1112 z: b.z - a.z,
1113 };
1114 let ac = sdk::Vec3 {
1115 x: c.x - a.x,
1116 y: c.y - a.y,
1117 z: c.z - a.z,
1118 };
1119 let normal = sdk::Vec3 {
1120 x: ab.y * ac.z - ab.z * ac.y,
1121 y: ab.z * ac.x - ab.x * ac.z,
1122 z: ab.x * ac.y - ab.y * ac.x,
1123 };
1124 if normal.x.hypot(normal.y).hypot(normal.z) <= 1e-18 {
1125 return Err(error(
1126 span,
1127 "degenerate_mesh",
1128 "mesh triangles must have nonzero area",
1129 ));
1130 }
1131 for &index in triangle {
1132 let n = &mut normals[index as usize];
1133 n.x += normal.x;
1134 n.y += normal.y;
1135 n.z += normal.z;
1136 }
1137 }
1138 for normal in &mut normals {
1139 let length = normal.x.hypot(normal.y).hypot(normal.z);
1140 if length <= 1e-18 {
1141 return Err(error(
1142 span,
1143 "invalid_mesh_normals",
1144 "mesh vertices must have a nonzero accumulated normal; remove unused vertices and split opposing faces",
1145 ));
1146 }
1147 normal.x /= length;
1148 normal.y /= length;
1149 normal.z /= length;
1150 }
1151 Ok(Mesh {
1152 positions,
1153 normals,
1154 indices,
1155 })
1156}
1157
1158fn point_cloud(value: &Value, span: &Span) -> Result<Mesh, Diagnostic> {
1159 let points = tensor_positions(field(value, "points", span)?, MAX_CLOUD_POINTS, span)?;
1160 if points.is_empty() || points.len() > MAX_CLOUD_POINTS {
1161 return Err(error(
1162 span,
1163 "point_cloud_limit",
1164 format!("PointCloud requires 1..={MAX_CLOUD_POINTS} points"),
1165 ));
1166 }
1167 let radius = positive(numeric_field(value, "radius", span)?, span)?;
1168 let sphere = sdk::tessellate(&Geometry::Sphere { radius_m: radius })
1169 .map_err(|diagnostic| error(span, "invalid_geometry", diagnostic.to_string()))?;
1170 let mut mesh = Mesh {
1171 positions: Vec::with_capacity(sphere.positions.len() * points.len()),
1172 normals: Vec::with_capacity(sphere.normals.len() * points.len()),
1173 indices: Vec::with_capacity(sphere.indices.len() * points.len()),
1174 };
1175 for point in points {
1176 let offset = u32::try_from(mesh.positions.len())
1177 .map_err(|_| error(span, "mesh_limit", "point cloud exceeds index range"))?;
1178 for vertex in &sphere.positions {
1179 let vertex = sdk::Vec3 {
1180 x: vertex.x + point.x,
1181 y: vertex.y + point.y,
1182 z: vertex.z + point.z,
1183 };
1184 if [vertex.x, vertex.y, vertex.z]
1185 .iter()
1186 .any(|coordinate| coordinate.abs() > sdk::MAX_COORDINATE_M)
1187 {
1188 return Err(error(
1189 span,
1190 "spatial_dimension",
1191 "point cloud surface exceeds coordinate bounds",
1192 ));
1193 }
1194 mesh.positions.push(vertex);
1195 }
1196 mesh.normals.extend_from_slice(&sphere.normals);
1197 mesh.indices
1198 .extend(sphere.indices.iter().map(|index| index + offset));
1199 }
1200 Ok(mesh)
1201}