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 let parent_targets: BTreeSet<u64> = snapshot
296 .entities
297 .iter()
298 .filter_map(|entity| entity.components.get("Parent"))
299 .filter_map(|value| field(value, "entity", &Span::default()).ok())
300 .filter_map(|value| match value {
301 Value::Entity(id) => Some(*id),
302 _ => None,
303 })
304 .collect();
305 for entity in &snapshot.entities {
306 let span = &entity.source;
307 let transform = entity
309 .components
310 .get("Transform")
311 .map(|value| transform(value, span))
312 .transpose()?
313 .unwrap_or_default();
314 let (color, material) = entity
315 .components
316 .get("Material")
317 .map(|value| material(value, span))
318 .transpose()?
319 .unwrap_or((
320 sdk::Color {
321 r: 111,
322 g: 227,
323 b: 255,
324 a: 255,
325 },
326 sdk::Material {
327 roughness: 0.6,
328 emissive: 0.0,
329 },
330 ));
331 let geometry_value = geometry_component(&entity.components, span)?;
332 if geometry_value.is_none()
333 && !entity.components.contains_key("Parent")
334 && !parent_targets.contains(&entity.id)
335 {
336 continue;
337 }
338 let id = EntityId(format!("entity_{}", entity.id));
339 let (geometry, mesh) = match geometry_value {
340 Some((name, value)) => geometry(name, value, &id, span)?,
341 None => (
342 Geometry::Group,
343 Mesh {
344 positions: Vec::new(),
345 normals: Vec::new(),
346 indices: Vec::new(),
347 },
348 ),
349 };
350 if !mesh.positions.is_empty() {
351 vertex_count += mesh.positions.len();
352 if vertex_count > MAX_SCENE_VERTICES {
353 return Err(error(
354 span,
355 "mesh_limit",
356 "scene exceeds its generated vertex budget",
357 ));
358 }
359 meshes.insert(id.clone(), mesh);
360 }
361 let parent = entity
362 .components
363 .get("Parent")
364 .map(|value| {
365 let Value::Entity(parent) = field(value, "entity", span)? else {
366 return Err(error(
367 span,
368 "invalid_parent",
369 "Parent.entity must be an Entity",
370 ));
371 };
372 if !snapshot
373 .entities
374 .iter()
375 .any(|candidate| candidate.id == *parent)
376 {
377 return Err(error(
378 span,
379 "unknown_parent",
380 "Parent.entity must reference an existing entity",
381 ));
382 }
383 Ok(EntityId(format!("entity_{parent}")))
384 })
385 .transpose()?;
386 document.entities.push(sdk::Entity {
387 id,
388 parent,
389 transform,
390 geometry,
391 color,
392 material,
393 animations: Vec::new(),
394 actions: BTreeMap::new(),
395 });
396 }
397 sdk::validate(&document).map_err(|diagnostic| {
398 error(&Span::default(), "invalid_scene", diagnostic.to_string())
399 })?;
400 let mut sampling_document = document.clone();
402 for entity in &mut sampling_document.entities {
403 entity.geometry = Geometry::Group;
404 }
405 let evaluator = sdk::Runtime::new(sampling_document).map_err(|diagnostic| {
406 error(&Span::default(), "invalid_scene", diagnostic.to_string())
407 })?;
408 let mut frame = evaluator.sample(snapshot.time).map_err(|diagnostic| {
409 error(&Span::default(), "invalid_scene", diagnostic.to_string())
410 })?;
411 for draw in &mut frame.draws {
412 draw.mesh_entity = Some(draw.entity.clone());
413 }
414 Ok(RenderOutput {
415 ui,
416 document,
417 meshes,
418 frame,
419 contacts: self.contacts.clone(),
420 })
421 }
422
423 fn normalize(&mut self) -> Result<(), Diagnostic> {
424 let mut updates = Vec::new();
425 for entity in self.snapshot().entities {
426 if geometry_component(&entity.components, &entity.source)?.is_some() {
427 if !entity.components.contains_key("Transform") {
428 updates.push((entity.id, default_transform()));
429 }
430 if !entity.components.contains_key("Material") {
431 updates.push((entity.id, default_material()));
432 }
433 }
434 }
435 self.machine.upsert_components_for_adapter(&updates)
436 }
437
438 fn reconcile(&mut self, apply_forces: bool) -> Result<(), Diagnostic> {
439 let snapshot = self.snapshot();
440 let present: BTreeSet<_> = snapshot
441 .entities
442 .iter()
443 .filter(|entity| entity.components.contains_key("RigidBody"))
444 .map(|entity| entity.id)
445 .collect();
446 let removed: Vec<_> = self
447 .bindings
448 .keys()
449 .filter(|id| !present.contains(id))
450 .copied()
451 .collect();
452 for id in removed {
453 if let Some(binding) = self.bindings.remove(&id) {
454 self.world.remove_body(binding.handle);
455 }
456 }
457 let mut spatial_change = false;
458 for entity in &snapshot.entities {
459 let span = &entity.source;
460 let force = entity
461 .components
462 .get("Force")
463 .map(|value| vector(field(value, "value", span)?, span))
464 .transpose()?;
465 let impulse = entity
466 .components
467 .get("Impulse")
468 .map(|value| vector(field(value, "value", span)?, span))
469 .transpose()?;
470 let Some(body_value) = entity.components.get("RigidBody") else {
471 if force.is_some() || impulse.is_some() {
472 return Err(error(
473 span,
474 "missing_body",
475 "Force and Impulse require a dynamic RigidBody",
476 ));
477 }
478 continue;
479 };
480 let transform_value = entity.components.get("Transform").ok_or_else(|| {
481 error(
482 span,
483 "missing_shape",
484 "RigidBody requires a Sphere or Box geometry",
485 )
486 })?;
487 let transform = transform(transform_value, span)?;
488 let config = body_config(body_value, &entity.components, transform.scale, span)?;
489 if config.kind != BodyKind::Dynamic && (force.is_some() || impulse.is_some()) {
490 return Err(error(
491 span,
492 "invalid_force",
493 "Force and Impulse require a dynamic RigidBody",
494 ));
495 }
496 let velocity = vector(field(body_value, "velocity", span)?, span)?;
497 let angular_velocity = vector(field(body_value, "angular_velocity", span)?, span)?;
498 if config.kind == BodyKind::Fixed
499 && (velocity != sdk::Vec3::ZERO || angular_velocity != sdk::Vec3::ZERO)
500 {
501 return Err(error(
502 span,
503 "fixed_velocity",
504 "a fixed RigidBody cannot have nonzero velocity",
505 ));
506 }
507 let mut old = self.bindings.get(&entity.id).cloned();
508 if let Some(binding) = &old {
509 if binding.config.kind == BodyKind::Dynamic
510 && (binding.transform.field("position") != transform_value.field("position")
511 || binding.transform.field("rotation") != transform_value.field("rotation"))
512 {
513 return Err(error(
514 span,
515 "dynamic_pose_edit",
516 "dynamic body position and rotation are owned by physics; use velocity, Force, or Impulse",
517 ));
518 }
519 if binding.config != config {
520 self.world.remove_body(binding.handle);
521 old = None;
522 }
523 }
524 let pose = pose(transform);
525 let handle = if let Some(binding) = old {
526 let body =
527 self.world.bodies.get_mut(binding.handle).ok_or_else(|| {
528 error(span, "physics_state", "missing persistent rigid body")
529 })?;
530 if binding.transform != *transform_value {
531 body.set_position(pose, true);
532 spatial_change = true;
533 }
534 if binding.rigid_body.field("velocity") != body_value.field("velocity") {
535 body.set_linvel(to_vector(velocity), true);
536 }
537 if binding.rigid_body.field("angular_velocity")
538 != body_value.field("angular_velocity")
539 {
540 body.set_angvel(to_vector(angular_velocity), true);
541 }
542 binding.handle
543 } else {
544 let builder = match config.kind {
545 BodyKind::Dynamic => RigidBodyBuilder::dynamic(),
546 BodyKind::Fixed => RigidBodyBuilder::fixed(),
547 BodyKind::Kinematic => RigidBodyBuilder::kinematic_velocity_based(),
548 };
549 let collider = match config.shape {
550 CollisionShape::Sphere(radius) => ColliderBuilder::ball(radius),
551 CollisionShape::Box(size) => {
552 ColliderBuilder::cuboid(size.x / 2.0, size.y / 2.0, size.z / 2.0)
553 }
554 };
555 let (handle, _) = self.world.insert(
556 builder
557 .pose(pose)
558 .linvel(to_vector(velocity))
559 .angvel(to_vector(angular_velocity))
560 .ccd_enabled(true)
561 .user_data(u128::from(entity.id)),
562 collider
563 .mass(config.mass)
564 .restitution(config.restitution)
565 .friction(config.friction)
566 .user_data(u128::from(entity.id)),
567 );
568 spatial_change = true;
569 handle
570 };
571 if apply_forces {
572 let body = self.world.bodies.get_mut(handle).ok_or_else(|| {
573 error(
574 span,
575 "physics_state",
576 "missing rigid body during force application",
577 )
578 })?;
579 body.reset_forces(false);
580 if let Some(force) = force {
581 body.add_force(to_vector(force), true);
582 }
583 if let Some(impulse) = impulse {
584 body.apply_impulse(to_vector(impulse), true);
585 }
586 }
587 self.bindings.insert(
588 entity.id,
589 Binding {
590 handle,
591 config,
592 transform: transform_value.clone(),
593 rigid_body: body_value.clone(),
594 },
595 );
596 }
597 if spatial_change {
599 self.contacts.clear();
600 }
601 self.contacts
602 .retain(|pair| present.contains(&pair.entity_a) && present.contains(&pair.entity_b));
603 Ok(())
604 }
605
606 fn write_back(&mut self) -> Result<(), Diagnostic> {
607 let mut updates = Vec::new();
608 let snapshot = self.snapshot();
609 for entity in &snapshot.entities {
610 let Some(binding) = self.bindings.get_mut(&entity.id) else {
611 continue;
612 };
613 let body = self.world.bodies.get(binding.handle).ok_or_else(|| {
614 error(
615 &entity.source,
616 "physics_state",
617 "missing simulated rigid body",
618 )
619 })?;
620 let position = body.translation();
621 let (x, y, z) = body.rotation().to_euler(EulerRot::XYZ);
622 let mut transform_value = binding.transform.clone();
623 replace_field(
624 &mut transform_value,
625 "position",
626 vector_value(from_vector(position)),
627 &entity.source,
628 )?;
629 replace_field(
630 &mut transform_value,
631 "rotation",
632 vector_value(sdk::Vec3 {
633 x: f64::from(x),
634 y: f64::from(y),
635 z: f64::from(z),
636 }),
637 &entity.source,
638 )?;
639 let mut rigid_body = binding.rigid_body.clone();
640 replace_field(
641 &mut rigid_body,
642 "velocity",
643 vector_value(from_vector(body.linvel())),
644 &entity.source,
645 )?;
646 replace_field(
647 &mut rigid_body,
648 "angular_velocity",
649 vector_value(from_vector(body.angvel())),
650 &entity.source,
651 )?;
652 binding.transform = transform_value.clone();
653 binding.rigid_body = rigid_body.clone();
654 updates.push((entity.id, transform_value));
655 updates.push((entity.id, rigid_body));
656 if entity.components.contains_key("Impulse") {
657 updates.push((
658 entity.id,
659 record("Impulse", [("value", vector_value(sdk::Vec3::ZERO))]),
660 ));
661 }
662 }
663 self.machine.patch_components_for_adapter(&updates)?;
664 let mut pairs = BTreeSet::new();
665 for contact in self
666 .world
667 .contact_pairs()
668 .filter(|contact| contact.has_any_active_contact())
669 {
670 let Some(a) = self.world.colliders.get(contact.collider1) else {
671 continue;
672 };
673 let Some(b) = self.world.colliders.get(contact.collider2) else {
674 continue;
675 };
676 let a = u64::try_from(a.user_data).map_err(|_| {
677 error(
678 &Span::default(),
679 "physics_state",
680 "invalid contact entity ID",
681 )
682 })?;
683 let b = u64::try_from(b.user_data).map_err(|_| {
684 error(
685 &Span::default(),
686 "physics_state",
687 "invalid contact entity ID",
688 )
689 })?;
690 pairs.insert(Contact {
691 entity_a: a.min(b),
692 entity_b: a.max(b),
693 });
694 }
695 self.contacts = pairs.into_iter().collect();
696 Ok(())
697 }
698}
699
700fn error(span: &Span, code: &str, message: impl Into<String>) -> Diagnostic {
701 Diagnostic::new(code, message, span.clone())
702}
703fn field<'a>(value: &'a Value, name: &str, span: &Span) -> Result<&'a Value, Diagnostic> {
704 value.field(name).ok_or_else(|| {
705 error(
706 span,
707 "spatial_field",
708 format!("{} requires field `{name}`", value.type_name()),
709 )
710 })
711}
712fn number(value: &Value, span: &Span) -> Result<f64, Diagnostic> {
713 match value {
714 Value::Number(number) if number.is_finite() && number.abs() <= sdk::MAX_COORDINATE_M => {
715 Ok(*number)
716 }
717 _ => Err(error(
718 span,
719 "spatial_number",
720 "spatial number must be finite and within +/-1,000,000",
721 )),
722 }
723}
724fn numeric_field(value: &Value, name: &str, span: &Span) -> Result<f64, Diagnostic> {
725 number(field(value, name, span)?, span)
726}
727fn positive(value: f64, span: &Span) -> Result<f64, Diagnostic> {
728 if value.is_finite() && (1e-6..=sdk::MAX_COORDINATE_M).contains(&value) {
729 Ok(value)
730 } else {
731 Err(error(
732 span,
733 "spatial_dimension",
734 "dimensions must be between 0.000001 and 1,000,000",
735 ))
736 }
737}
738fn text<'a>(value: &'a Value, span: &Span) -> Result<&'a str, Diagnostic> {
739 if let Value::Text(text) = value {
740 Ok(text)
741 } else {
742 Err(error(span, "spatial_type", "expected a Str"))
743 }
744}
745fn vector(value: &Value, span: &Span) -> Result<sdk::Vec3, Diagnostic> {
746 if value.type_name() != "Vec3" {
747 return Err(error(span, "spatial_type", "expected a Vec3"));
748 }
749 Ok(sdk::Vec3 {
750 x: numeric_field(value, "x", span)?,
751 y: numeric_field(value, "y", span)?,
752 z: numeric_field(value, "z", span)?,
753 })
754}
755fn tensor<'a>(value: &'a Value, span: &Span) -> Result<&'a sdk::data::Tensor, Diagnostic> {
756 if let Value::Tensor(value) = value {
757 Ok(value)
758 } else {
759 Err(error(span, "spatial_type", "expected a Tensor"))
760 }
761}
762
763fn tensor_positions(
765 value: &Value,
766 maximum: usize,
767 span: &Span,
768) -> Result<Vec<sdk::Vec3>, Diagnostic> {
769 let tensor = tensor(value, span)?;
770 let shape = tensor.shape();
771 if tensor.dtype() != sdk::data::DType::F32
772 || shape.len() != 2
773 || shape[1] != 3
774 || shape[0] > maximum
775 {
776 return Err(error(
777 span,
778 "spatial_shape",
779 "positions require a bounded N by 3 Tensor[f32]",
780 ));
781 }
782 (0..shape[0])
783 .map(|row| {
784 let coordinate = |axis| {
785 let value = tensor
786 .get(&[row as isize, axis])
787 .and_then(|value| value.to_f64())
788 .map_err(|e| error(span, "spatial_type", e.to_string()))?;
789 if value.abs() > sdk::MAX_COORDINATE_M {
790 return Err(error(
791 span,
792 "spatial_dimension",
793 "position exceeds coordinate bounds",
794 ));
795 }
796 Ok(value)
797 };
798 Ok(sdk::Vec3 {
799 x: coordinate(0)?,
800 y: coordinate(1)?,
801 z: coordinate(2)?,
802 })
803 })
804 .collect()
805}
806fn record<const N: usize>(class: &str, fields: [(&str, Value); N]) -> Value {
807 Value::record(
808 class,
809 fields.into_iter().map(|(name, value)| (name.into(), value)),
810 )
811}
812fn vector_value(vector: sdk::Vec3) -> Value {
813 record(
814 "Vec3",
815 [
816 ("x", Value::Number(vector.x)),
817 ("y", Value::Number(vector.y)),
818 ("z", Value::Number(vector.z)),
819 ],
820 )
821}
822fn default_transform() -> Value {
823 record(
824 "Transform",
825 [
826 ("position", vector_value(sdk::Vec3::ZERO)),
827 ("rotation", vector_value(sdk::Vec3::ZERO)),
828 (
829 "scale",
830 vector_value(sdk::Vec3 {
831 x: 1.0,
832 y: 1.0,
833 z: 1.0,
834 }),
835 ),
836 ],
837 )
838}
839fn default_material() -> Value {
840 record(
841 "Material",
842 [
843 ("color", Value::Text("#6fe3ff".into())),
844 ("roughness", Value::Number(0.6)),
845 ("emissive", Value::Number(0.0)),
846 ],
847 )
848}
849fn replace_field(
850 value: &mut Value,
851 key: &str,
852 replacement: Value,
853 span: &Span,
854) -> Result<(), Diagnostic> {
855 if let Value::Record(record) = value {
856 record.fields.insert(key.into(), replacement);
857 Ok(())
858 } else {
859 Err(error(span, "spatial_type", "component must be a record"))
860 }
861}
862fn to_vector(value: sdk::Vec3) -> Vector {
863 Vector::new(value.x as f32, value.y as f32, value.z as f32)
864}
865fn from_vector(value: Vector) -> sdk::Vec3 {
866 sdk::Vec3 {
867 x: f64::from(value.x),
868 y: f64::from(value.y),
869 z: f64::from(value.z),
870 }
871}
872fn pose(transform: sdk::Transform) -> Pose {
873 let q = transform.rotation;
874 Pose::from_parts(
875 to_vector(transform.translation),
876 Rotation::from_xyzw(q.x as f32, q.y as f32, q.z as f32, q.w as f32).normalize(),
877 )
878}
879fn transform(value: &Value, span: &Span) -> Result<sdk::Transform, Diagnostic> {
880 let translation = vector(field(value, "position", span)?, span)?;
881 let angles = vector(field(value, "rotation", span)?, span)?;
882 let scale = vector(field(value, "scale", span)?, span)?;
883 for dimension in [scale.x, scale.y, scale.z] {
884 positive(dimension, span)?;
885 }
886 let q = DQuat::from_euler(EulerRot::XYZ, angles.x, angles.y, angles.z);
887 Ok(sdk::Transform {
888 translation,
889 rotation: sdk::Quaternion {
890 x: q.x,
891 y: q.y,
892 z: q.z,
893 w: q.w,
894 },
895 scale,
896 })
897}
898fn material(value: &Value, span: &Span) -> Result<(sdk::Color, sdk::Material), Diagnostic> {
899 let color = text(field(value, "color", span)?, span)?;
900 let digits = color
901 .strip_prefix('#')
902 .filter(|digits| {
903 matches!(digits.len(), 6 | 8)
904 && digits.is_ascii()
905 && digits.bytes().all(|byte| byte.is_ascii_hexdigit())
906 })
907 .ok_or_else(|| {
908 error(
909 span,
910 "invalid_color",
911 "Material.color must be #RRGGBB or #RRGGBBAA",
912 )
913 })?;
914 let channel = |offset: usize| {
915 u8::from_str_radix(&digits[offset..offset + 2], 16)
916 .map_err(|_| error(span, "invalid_color", "invalid hexadecimal color"))
917 };
918 let roughness = numeric_field(value, "roughness", span)?;
919 let emissive = numeric_field(value, "emissive", span)?;
920 if !(0.0..=1.0).contains(&roughness) || !(0.0..=100.0).contains(&emissive) {
921 return Err(error(
922 span,
923 "invalid_material",
924 "roughness must be 0..=1 and emissive must be 0..=100",
925 ));
926 }
927 Ok((
928 sdk::Color {
929 r: channel(0)?,
930 g: channel(2)?,
931 b: channel(4)?,
932 a: if digits.len() == 8 { channel(6)? } else { 255 },
933 },
934 sdk::Material {
935 roughness: roughness as f32,
936 emissive: emissive as f32,
937 },
938 ))
939}
940fn geometry_component<'a>(
941 components: &'a BTreeMap<String, Value>,
942 span: &Span,
943) -> Result<Option<(&'static str, &'a Value)>, Diagnostic> {
944 let mut present = GEOMETRIES
945 .iter()
946 .filter_map(|name| components.get(*name).map(|value| (*name, value)));
947 let first = present.next();
948 if present.next().is_some() {
949 return Err(error(
950 span,
951 "multiple_geometry",
952 "an entity may have only one geometry component",
953 ));
954 }
955 Ok(first)
956}
957fn body_config(
958 value: &Value,
959 components: &BTreeMap<String, Value>,
960 scale: sdk::Vec3,
961 span: &Span,
962) -> Result<BodyConfig, Diagnostic> {
963 let kind = match text(field(value, "kind", span)?, span)? {
964 "dynamic" => BodyKind::Dynamic,
965 "fixed" => BodyKind::Fixed,
966 "kinematic" => BodyKind::Kinematic,
967 _ => {
968 return Err(error(
969 span,
970 "invalid_body",
971 "RigidBody.kind must be dynamic, fixed, or kinematic",
972 ));
973 }
974 };
975 let shape = match geometry_component(components, span)? {
976 Some(("Sphere", sphere)) => {
977 if scale.x != scale.y || scale.x != scale.z {
978 return Err(error(
979 span,
980 "unsupported_shape",
981 "a physical Sphere requires uniform scale",
982 ));
983 }
984 CollisionShape::Sphere(
985 positive(numeric_field(sphere, "radius", span)? * scale.x, span)? as f32,
986 )
987 }
988 Some(("Box", box_value)) => {
989 let size = vector(field(box_value, "size", span)?, span)?;
990 CollisionShape::Box(Vector::new(
991 positive(size.x * scale.x, span)? as f32,
992 positive(size.y * scale.y, span)? as f32,
993 positive(size.z * scale.z, span)? as f32,
994 ))
995 }
996 _ => {
997 return Err(error(
998 span,
999 "unsupported_shape",
1000 "RigidBody collision geometry must be Sphere or Box",
1001 ));
1002 }
1003 };
1004 let mass = positive(numeric_field(value, "mass", span)?, span)? as f32;
1005 let restitution = numeric_field(value, "restitution", span)?;
1006 let friction = numeric_field(value, "friction", span)?;
1007 if !(0.0..=1.0).contains(&restitution) || !(0.0..=1.0).contains(&friction) {
1008 return Err(error(
1009 span,
1010 "invalid_body",
1011 "restitution and friction must be 0..=1",
1012 ));
1013 }
1014 Ok(BodyConfig {
1015 kind,
1016 shape,
1017 mass,
1018 restitution: restitution as f32,
1019 friction: friction as f32,
1020 })
1021}
1022
1023fn geometry(
1024 name: &str,
1025 value: &Value,
1026 id: &EntityId,
1027 span: &Span,
1028) -> Result<(Geometry, Mesh), Diagnostic> {
1029 let geometry = match name {
1030 "Sphere" => Geometry::Sphere {
1031 radius_m: positive(numeric_field(value, "radius", span)?, span)?,
1032 },
1033 "Box" => Geometry::Box {
1034 size_m: vector(field(value, "size", span)?, span)?,
1035 },
1036 "Cylinder" => Geometry::Cylinder {
1037 radius_m: positive(numeric_field(value, "radius", span)?, span)?,
1038 height_m: positive(numeric_field(value, "height", span)?, span)?,
1039 },
1040 "Line" => Geometry::Line {
1041 from_m: vector(field(value, "from", span)?, span)?,
1042 to_m: vector(field(value, "to", span)?, span)?,
1043 },
1044 "Ray" => {
1045 let origin = vector(field(value, "origin", span)?, span)?;
1046 let direction = vector(field(value, "direction", span)?, span)?;
1047 let length = positive(numeric_field(value, "length", span)?, span)?;
1048 let magnitude = direction.x.hypot(direction.y).hypot(direction.z);
1049 if magnitude <= 1e-12 {
1050 return Err(error(span, "invalid_ray", "Ray.direction must be nonzero"));
1051 }
1052 Geometry::Line {
1053 from_m: origin,
1054 to_m: sdk::Vec3 {
1055 x: origin.x + direction.x / magnitude * length,
1056 y: origin.y + direction.y / magnitude * length,
1057 z: origin.z + direction.z / magnitude * length,
1058 },
1059 }
1060 }
1061 "Quad" => {
1062 let x = positive(numeric_field(value, "width", span)?, span)? / 2.0;
1063 let y = positive(numeric_field(value, "height", span)?, span)? / 2.0;
1064 return Ok((
1065 Geometry::Mesh {
1066 asset: id.0.clone(),
1067 },
1068 Mesh {
1069 positions: vec![
1070 sdk::Vec3 {
1071 x: -x,
1072 y: -y,
1073 z: 0.0,
1074 },
1075 sdk::Vec3 { x, y: -y, z: 0.0 },
1076 sdk::Vec3 { x, y, z: 0.0 },
1077 sdk::Vec3 { x: -x, y, z: 0.0 },
1078 ],
1079 normals: vec![
1080 sdk::Vec3 {
1081 x: 0.0,
1082 y: 0.0,
1083 z: 1.0
1084 };
1085 4
1086 ],
1087 indices: vec![0, 1, 2, 0, 2, 3],
1088 },
1089 ));
1090 }
1091 "Mesh" => {
1092 return Ok((
1093 Geometry::Mesh {
1094 asset: id.0.clone(),
1095 },
1096 custom_mesh(value, span)?,
1097 ));
1098 }
1099 "PointCloud" => {
1100 return Ok((
1101 Geometry::Mesh {
1102 asset: id.0.clone(),
1103 },
1104 point_cloud(value, span)?,
1105 ));
1106 }
1107 _ => {
1108 return Err(error(
1109 span,
1110 "unsupported_geometry",
1111 "unsupported geometry component",
1112 ));
1113 }
1114 };
1115 let mesh = sdk::tessellate(&geometry)
1116 .map_err(|diagnostic| error(span, "invalid_geometry", diagnostic.to_string()))?;
1117 Ok((geometry, mesh))
1118}
1119
1120pub(crate) fn custom_mesh(value: &Value, span: &Span) -> Result<Mesh, Diagnostic> {
1121 let positions = tensor_positions(field(value, "positions", span)?, MAX_MESH_VERTICES, span)?;
1122 let index_data = tensor(field(value, "indices", span)?, span)?;
1123 let shape = index_data.shape();
1124 if positions.len() < 3
1125 || index_data.dtype() != sdk::data::DType::U32
1126 || shape.len() != 2
1127 || shape[1] != 3
1128 || shape[0] == 0
1129 || shape[0] > MAX_MESH_INDICES / 3
1130 {
1131 return Err(error(
1132 span,
1133 "invalid_mesh",
1134 "Mesh requires N by 3 positions and M by 3 triangle indices within the mesh limits",
1135 ));
1136 }
1137 let mut indices = Vec::with_capacity(shape[0] * 3);
1138 for triangle in 0..shape[0] {
1139 for corner in 0..3 {
1140 let index = index_data
1141 .get(&[triangle as isize, corner])
1142 .map_err(|e| error(span, "invalid_mesh_index", e.to_string()))?
1143 .as_u128()
1144 .ok_or_else(|| error(span, "spatial_type", "mesh indices must be u32"))?;
1145 if index >= positions.len() as u128 {
1146 return Err(error(
1147 span,
1148 "invalid_mesh_index",
1149 "mesh indices must address an existing vertex",
1150 ));
1151 }
1152 indices.push(index as u32);
1153 }
1154 }
1155 let supplied_normals =
1156 tensor_positions(field(value, "normals", span)?, MAX_MESH_VERTICES, span)?;
1157 if !supplied_normals.is_empty() && supplied_normals.len() != positions.len() {
1158 return Err(error(
1159 span,
1160 "invalid_mesh_normals",
1161 "Mesh.normals must be empty or have one N by 3 normal for every position",
1162 ));
1163 }
1164 let mut normals = vec![sdk::Vec3::ZERO; positions.len()];
1165 for triangle in indices.as_chunks::<3>().0 {
1166 let a = positions[triangle[0] as usize];
1167 let b = positions[triangle[1] as usize];
1168 let c = positions[triangle[2] as usize];
1169 let ab = sdk::Vec3 {
1170 x: b.x - a.x,
1171 y: b.y - a.y,
1172 z: b.z - a.z,
1173 };
1174 let ac = sdk::Vec3 {
1175 x: c.x - a.x,
1176 y: c.y - a.y,
1177 z: c.z - a.z,
1178 };
1179 let normal = sdk::Vec3 {
1180 x: ab.y * ac.z - ab.z * ac.y,
1181 y: ab.z * ac.x - ab.x * ac.z,
1182 z: ab.x * ac.y - ab.y * ac.x,
1183 };
1184 if normal.x.hypot(normal.y).hypot(normal.z) <= 1e-18 {
1185 if !supplied_normals.is_empty() {
1186 continue;
1187 }
1188 return Err(error(
1189 span,
1190 "degenerate_mesh",
1191 "mesh triangles must have nonzero area",
1192 ));
1193 }
1194 for &index in triangle {
1195 let n = &mut normals[index as usize];
1196 n.x += normal.x;
1197 n.y += normal.y;
1198 n.z += normal.z;
1199 }
1200 }
1201 if !supplied_normals.is_empty() {
1202 normals = supplied_normals;
1203 for normal in &mut normals {
1204 let length = normal.x.hypot(normal.y).hypot(normal.z);
1205 if !length.is_finite() || length <= 1e-18 {
1206 return Err(error(
1207 span,
1208 "invalid_mesh_normals",
1209 "Mesh.normals must be finite and nonzero",
1210 ));
1211 }
1212 normal.x /= length;
1213 normal.y /= length;
1214 normal.z /= length;
1215 }
1216 return Ok(Mesh {
1217 positions,
1218 normals,
1219 indices,
1220 });
1221 }
1222 for normal in &mut normals {
1223 let length = normal.x.hypot(normal.y).hypot(normal.z);
1224 if length <= 1e-18 {
1225 return Err(error(
1226 span,
1227 "invalid_mesh_normals",
1228 "mesh vertices must have a nonzero accumulated normal; remove unused vertices and split opposing faces",
1229 ));
1230 }
1231 normal.x /= length;
1232 normal.y /= length;
1233 normal.z /= length;
1234 }
1235 Ok(Mesh {
1236 positions,
1237 normals,
1238 indices,
1239 })
1240}
1241
1242fn point_cloud(value: &Value, span: &Span) -> Result<Mesh, Diagnostic> {
1243 let points = tensor_positions(field(value, "points", span)?, MAX_CLOUD_POINTS, span)?;
1244 if points.is_empty() || points.len() > MAX_CLOUD_POINTS {
1245 return Err(error(
1246 span,
1247 "point_cloud_limit",
1248 format!("PointCloud requires 1..={MAX_CLOUD_POINTS} points"),
1249 ));
1250 }
1251 let radius = positive(numeric_field(value, "radius", span)?, span)?;
1252 let sphere = sdk::tessellate(&Geometry::Sphere { radius_m: radius })
1253 .map_err(|diagnostic| error(span, "invalid_geometry", diagnostic.to_string()))?;
1254 let mut mesh = Mesh {
1255 positions: Vec::with_capacity(sphere.positions.len() * points.len()),
1256 normals: Vec::with_capacity(sphere.normals.len() * points.len()),
1257 indices: Vec::with_capacity(sphere.indices.len() * points.len()),
1258 };
1259 for point in points {
1260 let offset = u32::try_from(mesh.positions.len())
1261 .map_err(|_| error(span, "mesh_limit", "point cloud exceeds index range"))?;
1262 for vertex in &sphere.positions {
1263 let vertex = sdk::Vec3 {
1264 x: vertex.x + point.x,
1265 y: vertex.y + point.y,
1266 z: vertex.z + point.z,
1267 };
1268 if [vertex.x, vertex.y, vertex.z]
1269 .iter()
1270 .any(|coordinate| coordinate.abs() > sdk::MAX_COORDINATE_M)
1271 {
1272 return Err(error(
1273 span,
1274 "spatial_dimension",
1275 "point cloud surface exceeds coordinate bounds",
1276 ));
1277 }
1278 mesh.positions.push(vertex);
1279 }
1280 mesh.normals.extend_from_slice(&sphere.normals);
1281 mesh.indices
1282 .extend(sphere.indices.iter().map(|index| index + offset));
1283 }
1284 Ok(mesh)
1285}