1pub mod camera_follow;
43pub mod context;
44pub mod debug_draw;
46pub mod events;
47pub mod gpu_plugin;
49pub mod guide;
51pub mod jobs;
53pub mod mode;
54pub mod output;
55pub mod plugin;
56pub mod plugins;
58pub mod resources;
59pub mod snapshot;
60pub mod systems;
62pub mod timestep;
63
64pub use camera_follow::CameraFollow;
65pub use context::{RuntimeFrameContext, RuntimeStepContext, SimulationStepContext};
66pub use debug_draw::{DebugDraw, DebugLayer, DebugPrim};
67pub use events::RuntimeEventBus;
68pub use gpu_plugin::{GpuFrameContext, GpuPlugin, PostPaintTargets, gpu_phase};
69pub use jobs::{JobPoll, JobSender, JobSlot};
70pub use mode::SceneRuntimeMode;
71pub use output::{
72 CameraCommand, ContactEvent, NodeTransformOp, RuntimeOutput, SelectionOp, SkinnedMeshUpdate,
73 SkinnedPoseUpdate, TransformWriteback,
74};
75pub use plugin::{RuntimeEvent, RuntimePhase, RuntimePlugin, phase};
76pub use plugins::{
77 AnimationClip, AnimationPlugin, AnimationTrack, Channel, ClipPlayerPlugin, Constraint,
78 ConstraintPlugin, Interpolation, Joint, JointMatrices, Keyframe, PhysicsBody,
79 PhysicsLitePlugin, Pose, Sampler, Skeleton, SkeletonPlugin, SkinnedActor, SkinnedActorPart,
80 SkinnedActorPlugin, SkinningPath, Track, TrackValue, TrackValues, apply_skin,
81};
82pub use resources::RuntimeResources;
83pub use snapshot::{TransformSnapshot, TransformSnapshotTable};
84pub use systems::{ManipulationSystem, SelectionSystem};
85pub use timestep::{FixedStepIter, FixedTimestep};
86
87use crate::interaction::selection::Selection;
88use crate::scene::scene::Scene;
89
90fn run_range(
95 plugins: &mut Vec<Box<dyn RuntimePlugin>>,
96 min: i32,
97 max: i32,
98 dt: f32,
99 frame: &RuntimeFrameContext,
100 scene: &Scene,
101 writeback: &mut TransformWriteback,
102 output: &mut RuntimeOutput,
103 resources: &mut RuntimeResources,
104) {
105 for plugin in plugins.iter_mut() {
106 let p = plugin.priority();
107 if p >= min && p < max {
108 let mut ctx = RuntimeStepContext {
109 priority: p,
110 dt,
111 scene,
112 writeback,
113 output,
114 pick_hit: frame.pick_hit,
115 resources,
116 };
117 plugin.step(&mut ctx);
118 }
119 }
120}
121
122#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::camera::camera::Camera;
128 use crate::interaction::input::ActionFrame;
129 use crate::interaction::selection::{NodeId, Selection};
130 use crate::scene::material::Material;
131 use crate::scene::scene::Scene;
132 use std::sync::{Arc, Mutex};
133
134 fn make_frame(camera: &Camera, input: &ActionFrame, dt: f32) -> RuntimeFrameContext {
135 RuntimeFrameContext {
136 dt,
137 camera: camera.clone(),
138 viewport_size: glam::Vec2::new(800.0, 600.0),
139 input: input.clone(),
140 pick_hit: None,
141 clicked: false,
142 drag_started: false,
143 dragging: false,
144 pointer_delta: glam::Vec2::ZERO,
145 cursor_viewport: None,
146 shift_held: false,
147 }
148 }
149
150 struct OrderTracker {
152 priority: i32,
153 id: u32,
154 log: Arc<Mutex<Vec<(i32, u32)>>>,
155 }
156
157 impl RuntimePlugin for OrderTracker {
158 fn priority(&self) -> i32 {
159 self.priority
160 }
161 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
162 self.log.lock().unwrap().push((self.priority, self.id));
163 }
164 }
165
166 struct CallCounter {
168 priority: i32,
169 count: Arc<Mutex<u32>>,
170 }
171
172 impl RuntimePlugin for CallCounter {
173 fn priority(&self) -> i32 {
174 self.priority
175 }
176 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
177 *self.count.lock().unwrap() += 1;
178 }
179 }
180
181 struct WritebackPlugin {
183 node_id: NodeId,
184 transform: glam::Affine3A,
185 }
186
187 impl RuntimePlugin for WritebackPlugin {
188 fn priority(&self) -> i32 {
189 phase::SIMULATE
190 }
191 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
192 ctx.writeback.set(self.node_id, self.transform);
193 }
194 }
195
196 struct DtRecorder {
198 dts: Arc<Mutex<Vec<f32>>>,
199 }
200
201 impl RuntimePlugin for DtRecorder {
202 fn priority(&self) -> i32 {
203 phase::SIMULATE
204 }
205 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
206 self.dts.lock().unwrap().push(ctx.dt);
207 }
208 }
209
210 #[test]
211 fn test_phase_execution_order() {
212 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
214 let mut runtime = ViewportRuntime::new()
215 .with_plugin(OrderTracker {
216 priority: phase::WRITEBACK,
217 id: 3,
218 log: log.clone(),
219 })
220 .with_plugin(OrderTracker {
221 priority: phase::SIMULATE,
222 id: 2,
223 log: log.clone(),
224 })
225 .with_plugin(OrderTracker {
226 priority: phase::ANIMATE,
227 id: 1,
228 log: log.clone(),
229 })
230 .with_plugin(OrderTracker {
231 priority: phase::PREPARE,
232 id: 0,
233 log: log.clone(),
234 });
235
236 let camera = Camera::default();
237 let input = ActionFrame::default();
238 let mut scene = Scene::new();
239 let mut sel = Selection::new();
240 runtime.step(
241 &mut scene,
242 &mut sel,
243 &make_frame(&camera, &input, 1.0 / 60.0),
244 );
245
246 let calls = log.lock().unwrap();
247 let priorities: Vec<i32> = calls.iter().map(|(p, _)| *p).collect();
248 for w in priorities.windows(2) {
250 assert!(
251 w[0] <= w[1],
252 "expected ascending order, got {:?}",
253 priorities
254 );
255 }
256 assert_eq!(priorities.len(), 4);
257 }
258
259 #[test]
260 fn test_registration_order_within_phase() {
261 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
263 let mut runtime = ViewportRuntime::new()
264 .with_plugin(OrderTracker {
265 priority: phase::SIMULATE,
266 id: 0,
267 log: log.clone(),
268 })
269 .with_plugin(OrderTracker {
270 priority: phase::SIMULATE,
271 id: 1,
272 log: log.clone(),
273 });
274
275 let camera = Camera::default();
276 let input = ActionFrame::default();
277 let mut scene = Scene::new();
278 let mut sel = Selection::new();
279 runtime.step(
280 &mut scene,
281 &mut sel,
282 &make_frame(&camera, &input, 1.0 / 60.0),
283 );
284
285 let calls = log.lock().unwrap();
286 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
287 assert_eq!(ids, vec![0, 1]);
288 }
289
290 #[test]
291 fn test_simulate_runs_once_without_fixed_timestep() {
292 let count = Arc::new(Mutex::new(0u32));
293 let mut runtime = ViewportRuntime::new().with_plugin(CallCounter {
294 priority: phase::SIMULATE,
295 count: count.clone(),
296 });
297
298 let camera = Camera::default();
299 let input = ActionFrame::default();
300 let mut scene = Scene::new();
301 let mut sel = Selection::new();
302 runtime.step(
303 &mut scene,
304 &mut sel,
305 &make_frame(&camera, &input, 1.0 / 60.0),
306 );
307
308 assert_eq!(*count.lock().unwrap(), 1);
309 }
310
311 #[test]
312 fn test_simulate_runs_n_times_with_fixed_timestep() {
313 let count = Arc::new(Mutex::new(0u32));
314 let hz = 60.0_f32;
315 let step_dt = 1.0 / hz;
316 let mut runtime = ViewportRuntime::new()
317 .with_fixed_timestep(FixedTimestep::new(hz))
318 .with_plugin(CallCounter {
319 priority: phase::SIMULATE,
320 count: count.clone(),
321 });
322
323 let camera = Camera::default();
324 let input = ActionFrame::default();
325 let mut scene = Scene::new();
326 let mut sel = Selection::new();
327
328 let dt = step_dt * 3.0 + 0.0001;
330 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
331
332 assert_eq!(*count.lock().unwrap(), 3);
333 }
334
335 #[test]
336 fn test_simulate_dt_equals_step_dt_with_fixed_timestep() {
337 let dts = Arc::new(Mutex::new(Vec::<f32>::new()));
338 let hz = 60.0_f32;
339 let step_dt = 1.0 / hz;
340 let mut runtime = ViewportRuntime::new()
341 .with_fixed_timestep(FixedTimestep::new(hz))
342 .with_plugin(DtRecorder { dts: dts.clone() });
343
344 let camera = Camera::default();
345 let input = ActionFrame::default();
346 let mut scene = Scene::new();
347 let mut sel = Selection::new();
348
349 let dt = step_dt * 2.0 + 0.0001;
351 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
352
353 let recorded = dts.lock().unwrap();
354 assert_eq!(recorded.len(), 2);
355 for &d in recorded.iter() {
356 assert!((d - step_dt).abs() < 1e-6, "expected {step_dt}, got {d}");
357 }
358 }
359
360 #[test]
361 fn test_writeback_flushes_to_scene() {
362 let target = glam::Affine3A::from_translation(glam::Vec3::new(3.0, 4.0, 5.0));
363 let mut scene = Scene::new();
364 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
365
366 let mut runtime = ViewportRuntime::new().with_plugin(WritebackPlugin {
367 node_id,
368 transform: target,
369 });
370
371 let camera = Camera::default();
372 let input = ActionFrame::default();
373 let mut sel = Selection::new();
374 runtime.step(
375 &mut scene,
376 &mut sel,
377 &make_frame(&camera, &input, 1.0 / 60.0),
378 );
379
380 let node = scene.node(node_id).expect("node not found");
381 let pos = node.world_transform().col(3).truncate();
382 assert!(
383 (pos - glam::Vec3::new(3.0, 4.0, 5.0)).length() < 1e-5,
384 "pos was {pos:?}"
385 );
386 }
387
388 #[test]
389 fn test_writeback_ops_in_output() {
390 let target = glam::Affine3A::from_translation(glam::Vec3::new(1.0, 0.0, 0.0));
391 let mut scene = Scene::new();
392 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
393
394 let mut runtime = ViewportRuntime::new().with_plugin(WritebackPlugin {
395 node_id,
396 transform: target,
397 });
398
399 let camera = Camera::default();
400 let input = ActionFrame::default();
401 let mut sel = Selection::new();
402 let output = runtime.step(
403 &mut scene,
404 &mut sel,
405 &make_frame(&camera, &input, 1.0 / 60.0),
406 );
407
408 assert_eq!(output.node_transform_ops.len(), 1);
409 assert_eq!(output.node_transform_ops[0].id, node_id);
410 }
411
412 #[test]
413 fn test_step_index_increments_each_simulate() {
414 let mut runtime = ViewportRuntime::new();
415 let camera = Camera::default();
416 let input = ActionFrame::default();
417 let mut scene = Scene::new();
418 let mut sel = Selection::new();
419
420 assert_eq!(runtime.step_index(), 0);
421 runtime.step(
422 &mut scene,
423 &mut sel,
424 &make_frame(&camera, &input, 1.0 / 60.0),
425 );
426 assert_eq!(runtime.step_index(), 1);
427 runtime.step(
428 &mut scene,
429 &mut sel,
430 &make_frame(&camera, &input, 1.0 / 60.0),
431 );
432 assert_eq!(runtime.step_index(), 2);
433 }
434
435 #[test]
436 fn test_step_index_increments_n_times_with_fixed_timestep() {
437 let hz = 60.0_f32;
438 let step_dt = 1.0 / hz;
439 let mut runtime = ViewportRuntime::new().with_fixed_timestep(FixedTimestep::new(hz));
440
441 let camera = Camera::default();
442 let input = ActionFrame::default();
443 let mut scene = Scene::new();
444 let mut sel = Selection::new();
445
446 let dt = step_dt * 3.0 + 0.0001;
448 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
449 assert_eq!(runtime.step_index(), 3);
450 }
451
452 #[test]
453 fn test_snapshot_updated_after_writeback() {
454 let target = glam::Affine3A::from_translation(glam::Vec3::new(7.0, 0.0, 0.0));
455 let mut scene = Scene::new();
456 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
457
458 let mut runtime = ViewportRuntime::new().with_plugin(WritebackPlugin {
459 node_id,
460 transform: target,
461 });
462
463 let camera = Camera::default();
464 let input = ActionFrame::default();
465 let mut sel = Selection::new();
466 runtime.step(
467 &mut scene,
468 &mut sel,
469 &make_frame(&camera, &input, 1.0 / 60.0),
470 );
471
472 let snap = runtime.snapshots().get(node_id).expect("snapshot missing");
473 assert!((snap.curr.translation.x - 7.0).abs() < 1e-5);
474 }
475
476 #[test]
479 fn test_post_sim_runs_after_simulate() {
480 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
481 let mut runtime = ViewportRuntime::new()
482 .with_plugin(OrderTracker {
483 priority: phase::POST_SIM,
484 id: 1,
485 log: log.clone(),
486 })
487 .with_plugin(OrderTracker {
488 priority: phase::SIMULATE,
489 id: 0,
490 log: log.clone(),
491 });
492
493 let camera = Camera::default();
494 let input = ActionFrame::default();
495 let mut scene = Scene::new();
496 let mut sel = Selection::new();
497 runtime.step(
498 &mut scene,
499 &mut sel,
500 &make_frame(&camera, &input, 1.0 / 60.0),
501 );
502
503 let calls = log.lock().unwrap();
504 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
505 assert_eq!(ids, vec![0, 1], "simulate must run before post_sim");
506 }
507
508 #[test]
509 fn test_plugin_between_bands() {
510 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
511 let mid = phase::ANIMATE + 50;
512 let mut runtime = ViewportRuntime::new()
513 .with_plugin(OrderTracker {
514 priority: phase::SIMULATE,
515 id: 2,
516 log: log.clone(),
517 })
518 .with_plugin(OrderTracker {
519 priority: mid,
520 id: 1,
521 log: log.clone(),
522 })
523 .with_plugin(OrderTracker {
524 priority: phase::ANIMATE,
525 id: 0,
526 log: log.clone(),
527 });
528
529 let camera = Camera::default();
530 let input = ActionFrame::default();
531 let mut scene = Scene::new();
532 let mut sel = Selection::new();
533 runtime.step(
534 &mut scene,
535 &mut sel,
536 &make_frame(&camera, &input, 1.0 / 60.0),
537 );
538
539 let calls = log.lock().unwrap();
540 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
541 assert_eq!(ids, vec![0, 1, 2], "plugins must execute in priority order");
542 }
543
544 struct EventRecorder {
546 added: Arc<Mutex<Vec<NodeId>>>,
547 removed: Arc<Mutex<Vec<NodeId>>>,
548 }
549
550 impl RuntimePlugin for EventRecorder {
551 fn priority(&self) -> i32 {
552 phase::PREPARE
553 }
554 fn on_event(&mut self, event: &RuntimeEvent, _ctx: &mut RuntimeStepContext<'_>) {
555 match event {
556 RuntimeEvent::NodeAdded(id) => self.added.lock().unwrap().push(*id),
557 RuntimeEvent::NodeRemoved(id) => self.removed.lock().unwrap().push(*id),
558 }
559 }
560 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
561 }
562
563 #[test]
564 fn test_lifecycle_events_node_added() {
565 let added = Arc::new(Mutex::new(Vec::<NodeId>::new()));
566 let removed = Arc::new(Mutex::new(Vec::<NodeId>::new()));
567 let mut runtime = ViewportRuntime::new().with_plugin(EventRecorder {
568 added: added.clone(),
569 removed: removed.clone(),
570 });
571
572 let camera = Camera::default();
573 let input = ActionFrame::default();
574 let mut scene = Scene::new();
575 let mut sel = Selection::new();
576
577 runtime.step(
579 &mut scene,
580 &mut sel,
581 &make_frame(&camera, &input, 1.0 / 60.0),
582 );
583 assert!(added.lock().unwrap().is_empty(), "no events on first step");
584
585 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
587 runtime.step(
588 &mut scene,
589 &mut sel,
590 &make_frame(&camera, &input, 1.0 / 60.0),
591 );
592
593 let a = added.lock().unwrap();
594 assert!(
595 a.contains(&node_id),
596 "expected NodeAdded event for {:?}",
597 node_id
598 );
599 assert!(removed.lock().unwrap().is_empty());
600 }
601
602 #[test]
603 fn test_lifecycle_events_node_removed() {
604 let added = Arc::new(Mutex::new(Vec::<NodeId>::new()));
605 let removed = Arc::new(Mutex::new(Vec::<NodeId>::new()));
606 let mut runtime = ViewportRuntime::new().with_plugin(EventRecorder {
607 added: added.clone(),
608 removed: removed.clone(),
609 });
610
611 let camera = Camera::default();
612 let input = ActionFrame::default();
613 let mut scene = Scene::new();
614 let mut sel = Selection::new();
615
616 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
617
618 runtime.step(
620 &mut scene,
621 &mut sel,
622 &make_frame(&camera, &input, 1.0 / 60.0),
623 );
624 added.lock().unwrap().clear();
625
626 scene.remove(node_id);
628 runtime.step(
629 &mut scene,
630 &mut sel,
631 &make_frame(&camera, &input, 1.0 / 60.0),
632 );
633
634 let r = removed.lock().unwrap();
635 assert!(
636 r.contains(&node_id),
637 "expected NodeRemoved event for {:?}",
638 node_id
639 );
640 }
641
642 #[derive(Default)]
644 struct LifecycleRecorder {
645 log: Arc<Mutex<Vec<&'static str>>>,
646 }
647
648 impl RuntimePlugin for LifecycleRecorder {
649 fn priority(&self) -> i32 {
650 phase::PREPARE
651 }
652 fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
653 self.log.lock().unwrap().push("submit");
654 }
655 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
656 self.log.lock().unwrap().push("step");
657 }
658 fn collect(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
659 self.log.lock().unwrap().push("collect");
660 }
661 }
662
663 #[test]
664 fn test_submit_collect_called() {
665 let log = Arc::new(Mutex::new(Vec::<&'static str>::new()));
666 let mut runtime =
667 ViewportRuntime::new().with_plugin(LifecycleRecorder { log: log.clone() });
668
669 let camera = Camera::default();
670 let input = ActionFrame::default();
671 let mut scene = Scene::new();
672 let mut sel = Selection::new();
673
674 runtime.step(
675 &mut scene,
676 &mut sel,
677 &make_frame(&camera, &input, 1.0 / 60.0),
678 );
679
680 let calls = log.lock().unwrap();
681 assert!(calls.contains(&"submit"), "submit must be called");
682 assert!(calls.contains(&"step"), "step must be called");
683 assert!(calls.contains(&"collect"), "collect must be called");
684 let si = calls.iter().position(|&s| s == "submit").unwrap();
686 let st = calls.iter().position(|&s| s == "step").unwrap();
687 let co = calls.iter().position(|&s| s == "collect").unwrap();
688 assert!(si < st, "submit must come before step");
689 assert!(st < co, "step must come before collect");
690 }
691
692 #[test]
695 fn test_resource_insert_get() {
696 let mut res = RuntimeResources::new();
697 res.insert(42u32);
698 assert_eq!(res.get::<u32>(), Some(&42));
699 assert!(res.get::<u64>().is_none());
700 }
701
702 #[test]
703 fn test_resource_insert_overwrites() {
704 let mut res = RuntimeResources::new();
705 res.insert(1u32);
706 res.insert(2u32);
707 assert_eq!(res.get::<u32>(), Some(&2));
708 }
709
710 #[test]
711 fn test_resource_get_mut() {
712 let mut res = RuntimeResources::new();
713 res.insert(0u32);
714 *res.get_mut::<u32>().unwrap() = 99;
715 assert_eq!(res.get::<u32>(), Some(&99));
716 }
717
718 #[test]
719 fn test_resource_remove() {
720 let mut res = RuntimeResources::new();
721 res.insert(7u32);
722 assert!(res.contains::<u32>());
723 let val = res.remove::<u32>();
724 assert_eq!(val, Some(7));
725 assert!(!res.contains::<u32>());
726 assert!(res.remove::<u32>().is_none());
728 }
729
730 #[test]
731 fn test_resource_missing_returns_none() {
732 let res = RuntimeResources::new();
733 assert!(res.get::<u32>().is_none());
734 assert!(!res.contains::<u32>());
735 }
736
737 struct CounterWriter {
739 value: u32,
740 }
741
742 impl RuntimePlugin for CounterWriter {
743 fn priority(&self) -> i32 {
744 phase::ANIMATE
745 }
746 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
747 ctx.resources.insert(self.value);
748 }
749 }
750
751 struct CounterReader {
753 recorded: Arc<Mutex<Vec<u32>>>,
754 }
755
756 impl RuntimePlugin for CounterReader {
757 fn priority(&self) -> i32 {
758 phase::POST_SIM
759 }
760 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
761 if let Some(&v) = ctx.resources.get::<u32>() {
762 self.recorded.lock().unwrap().push(v);
763 }
764 }
765 }
766
767 #[test]
768 fn test_resource_shared_across_plugins_same_frame() {
769 let recorded = Arc::new(Mutex::new(Vec::<u32>::new()));
772 let mut runtime = ViewportRuntime::new()
773 .with_plugin(CounterWriter { value: 42 })
774 .with_plugin(CounterReader {
775 recorded: recorded.clone(),
776 });
777
778 let camera = Camera::default();
779 let input = ActionFrame::default();
780 let mut scene = Scene::new();
781 let mut sel = Selection::new();
782 runtime.step(
783 &mut scene,
784 &mut sel,
785 &make_frame(&camera, &input, 1.0 / 60.0),
786 );
787
788 let vals = recorded.lock().unwrap();
789 assert_eq!(
790 vals.as_slice(),
791 &[42],
792 "reader must see value written by writer in the same frame"
793 );
794 }
795
796 #[test]
797 fn test_resource_persists_across_frames() {
798 let mut runtime = ViewportRuntime::new().with_plugin(CounterWriter { value: 10 });
800
801 let camera = Camera::default();
802 let input = ActionFrame::default();
803 let mut scene = Scene::new();
804 let mut sel = Selection::new();
805
806 runtime.step(
807 &mut scene,
808 &mut sel,
809 &make_frame(&camera, &input, 1.0 / 60.0),
810 );
811 assert!(
812 runtime.resources().contains::<u32>(),
813 "resource must persist after step"
814 );
815 }
816
817 #[test]
818 fn test_runtime_works_without_resources() {
819 let mut runtime = ViewportRuntime::new();
821 let camera = Camera::default();
822 let input = ActionFrame::default();
823 let mut scene = Scene::new();
824 let mut sel = Selection::new();
825 let output = runtime.step(
826 &mut scene,
827 &mut sel,
828 &make_frame(&camera, &input, 1.0 / 60.0),
829 );
830 assert!(output.contact_events.is_empty());
831 assert!(output.node_transform_ops.is_empty());
832 }
833
834 #[derive(Debug, PartialEq)]
838 struct GameplayEvent {
839 id: u32,
840 }
841
842 #[derive(Debug, PartialEq)]
843 struct DiagnosticsEvent {
844 frame_ms: f32,
845 }
846
847 struct GameplayEmitter {
849 count: u32,
850 }
851
852 impl RuntimePlugin for GameplayEmitter {
853 fn priority(&self) -> i32 {
854 phase::SIMULATE
855 }
856 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
857 for i in 0..self.count {
858 ctx.output.events.emit(GameplayEvent { id: i });
859 }
860 }
861 }
862
863 struct DiagnosticsEmitter;
865
866 impl RuntimePlugin for DiagnosticsEmitter {
867 fn priority(&self) -> i32 {
868 phase::POST_SIM
869 }
870 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
871 ctx.output.events.emit(DiagnosticsEvent {
872 frame_ms: ctx.dt * 1000.0,
873 });
874 }
875 }
876
877 struct GameplayReader {
879 seen: Arc<Mutex<Vec<u32>>>,
880 }
881
882 impl RuntimePlugin for GameplayReader {
883 fn priority(&self) -> i32 {
884 phase::WRITEBACK
885 }
886 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
887 for ev in ctx.output.events.read::<GameplayEvent>() {
888 self.seen.lock().unwrap().push(ev.id);
889 }
890 }
891 }
892
893 fn run_one_frame(runtime: &mut ViewportRuntime) -> RuntimeOutput {
894 let camera = Camera::default();
895 let input = ActionFrame::default();
896 let mut scene = Scene::new();
897 let mut sel = Selection::new();
898 runtime.step(
899 &mut scene,
900 &mut sel,
901 &make_frame(&camera, &input, 1.0 / 60.0),
902 )
903 }
904
905 #[test]
906 fn test_event_bus_emit_and_read() {
907 let mut bus = RuntimeEventBus::new();
908 bus.emit(GameplayEvent { id: 1 });
909 bus.emit(GameplayEvent { id: 2 });
910 let ids: Vec<u32> = bus.read::<GameplayEvent>().map(|e| e.id).collect();
911 assert_eq!(ids, vec![1, 2]);
912 }
913
914 #[test]
915 fn test_event_bus_typed_isolation() {
916 let mut bus = RuntimeEventBus::new();
918 bus.emit(GameplayEvent { id: 42 });
919 assert!(!bus.has::<DiagnosticsEvent>());
920 assert_eq!(bus.count::<DiagnosticsEvent>(), 0);
921 let diag: Vec<_> = bus.read::<DiagnosticsEvent>().collect();
922 assert!(diag.is_empty());
923 }
924
925 #[test]
926 fn test_event_bus_drain() {
927 let mut bus = RuntimeEventBus::new();
928 bus.emit(GameplayEvent { id: 7 });
929 bus.emit(GameplayEvent { id: 8 });
930 let drained = bus.drain::<GameplayEvent>();
931 assert_eq!(
932 drained,
933 vec![GameplayEvent { id: 7 }, GameplayEvent { id: 8 }]
934 );
935 assert!(bus.drain::<GameplayEvent>().is_empty());
937 assert!(!bus.has::<GameplayEvent>());
938 }
939
940 #[test]
941 fn test_event_bus_has_and_count() {
942 let mut bus = RuntimeEventBus::new();
943 assert!(!bus.has::<GameplayEvent>());
944 assert_eq!(bus.count::<GameplayEvent>(), 0);
945 bus.emit(GameplayEvent { id: 1 });
946 bus.emit(GameplayEvent { id: 2 });
947 assert!(bus.has::<GameplayEvent>());
948 assert_eq!(bus.count::<GameplayEvent>(), 2);
949 }
950
951 #[test]
952 fn test_event_bus_is_empty() {
953 let mut bus = RuntimeEventBus::new();
954 assert!(bus.is_empty());
955 bus.emit(GameplayEvent { id: 0 });
956 assert!(!bus.is_empty());
957 }
958
959 #[test]
960 fn test_events_emitted_by_plugin_visible_in_output() {
961 let mut runtime = ViewportRuntime::new().with_plugin(GameplayEmitter { count: 3 });
962 let output = run_one_frame(&mut runtime);
963 assert_eq!(output.events.count::<GameplayEvent>(), 3);
964 let ids: Vec<u32> = output
965 .events
966 .read::<GameplayEvent>()
967 .map(|e| e.id)
968 .collect();
969 assert_eq!(ids, vec![0, 1, 2]);
970 }
971
972 #[test]
973 fn test_events_typed_routing_across_plugins() {
974 let seen = Arc::new(Mutex::new(Vec::<u32>::new()));
977 let mut runtime = ViewportRuntime::new()
978 .with_plugin(GameplayEmitter { count: 2 })
979 .with_plugin(GameplayReader { seen: seen.clone() });
980 run_one_frame(&mut runtime);
981 let ids = seen.lock().unwrap();
982 assert_eq!(ids.as_slice(), &[0, 1]);
983 }
984
985 #[test]
986 fn test_multiple_event_types_do_not_interfere() {
987 let mut runtime = ViewportRuntime::new()
989 .with_plugin(GameplayEmitter { count: 2 })
990 .with_plugin(DiagnosticsEmitter);
991 let output = run_one_frame(&mut runtime);
992 assert_eq!(output.events.count::<GameplayEvent>(), 2);
993 assert_eq!(output.events.count::<DiagnosticsEvent>(), 1);
994 }
995
996 #[test]
997 fn test_events_cleared_each_frame() {
998 let mut runtime = ViewportRuntime::new().with_plugin(GameplayEmitter { count: 1 });
1000 run_one_frame(&mut runtime);
1001 let output2 = run_one_frame(&mut runtime);
1002 assert_eq!(output2.events.count::<GameplayEvent>(), 1);
1004 }
1005
1006 #[test]
1007 fn test_existing_output_fields_unaffected() {
1008 let mut runtime = ViewportRuntime::new();
1011 let output = run_one_frame(&mut runtime);
1012 assert!(output.contact_events.is_empty());
1013 assert!(output.selection_ops.is_empty());
1014 assert!(output.node_transform_ops.is_empty());
1015 assert!(output.camera_follow_target.is_none());
1016 assert!(output.events.is_empty());
1017 }
1018
1019 use crate::runtime::jobs::{JobPoll, JobSlot};
1022
1023 #[test]
1024 fn test_job_slot_empty_by_default() {
1025 let slot: JobSlot<u32> = JobSlot::empty();
1026 assert!(slot.is_empty());
1027 assert!(!slot.is_pending());
1028 assert!(!slot.is_ready());
1029 }
1030
1031 #[test]
1032 fn test_job_slot_pending_after_new() {
1033 let (slot, _sender) = JobSlot::<u32>::new();
1034 assert!(slot.is_pending());
1035 assert!(!slot.is_empty());
1036 assert!(!slot.is_ready());
1037 }
1038
1039 #[test]
1040 fn test_job_handoff_complete() {
1041 let (mut slot, sender) = JobSlot::<u32>::new();
1042 assert!(slot.is_pending());
1043 sender.complete(42);
1044 assert!(slot.is_ready());
1045 match slot.take() {
1046 JobPoll::Ready(v) => assert_eq!(v, 42),
1047 _ => panic!("expected Ready, got other variant"),
1048 }
1049 assert!(slot.is_empty());
1051 }
1052
1053 #[test]
1054 fn test_job_handoff_fail() {
1055 let (mut slot, sender) = JobSlot::<u32>::new();
1056 sender.fail("disk error");
1057 match slot.take() {
1058 JobPoll::Failed(msg) => assert_eq!(msg, "disk error"),
1059 _ => panic!("expected Failed"),
1060 }
1061 assert!(slot.is_empty());
1062 }
1063
1064 #[test]
1065 fn test_job_handoff_cancel() {
1066 let (mut slot, sender) = JobSlot::<u32>::new();
1067 sender.cancel();
1068 match slot.take() {
1069 JobPoll::Cancelled => {}
1070 _ => panic!("expected Cancelled"),
1071 }
1072 assert!(slot.is_empty());
1073 }
1074
1075 #[test]
1076 fn test_job_sender_drop_cancels() {
1077 let (mut slot, sender) = JobSlot::<u32>::new();
1078 drop(sender);
1079 match slot.take() {
1080 JobPoll::Cancelled => {}
1081 _ => panic!("expected Cancelled after sender drop"),
1082 }
1083 assert!(slot.is_empty());
1084 }
1085
1086 #[test]
1087 fn test_job_take_on_empty_returns_empty() {
1088 let mut slot: JobSlot<u32> = JobSlot::empty();
1089 match slot.take() {
1090 JobPoll::Empty => {}
1091 _ => panic!("expected Empty"),
1092 }
1093 }
1094
1095 #[test]
1096 fn test_job_take_while_pending_returns_pending() {
1097 let (mut slot, _sender) = JobSlot::<u32>::new();
1098 match slot.take() {
1099 JobPoll::Pending => {}
1100 _ => panic!("expected Pending"),
1101 }
1102 assert!(slot.is_pending());
1104 }
1105
1106 #[test]
1107 fn test_job_async_handoff_across_frames() {
1108 struct LoaderPlugin {
1110 slot: JobSlot<u32>,
1111 result: Arc<Mutex<Option<u32>>>,
1112 }
1113
1114 impl RuntimePlugin for LoaderPlugin {
1115 fn priority(&self) -> i32 {
1116 phase::PREPARE
1117 }
1118
1119 fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
1120 if self.slot.is_empty() {
1121 let (new_slot, sender) = JobSlot::new();
1122 self.slot = new_slot;
1123 sender.complete(99);
1125 }
1126 }
1127
1128 fn collect(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
1129 if let JobPoll::Ready(v) = self.slot.take() {
1130 *self.result.lock().unwrap() = Some(v);
1131 }
1132 }
1133
1134 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
1135 }
1136
1137 let result = Arc::new(Mutex::new(None::<u32>));
1138 let mut runtime = ViewportRuntime::new().with_plugin(LoaderPlugin {
1139 slot: JobSlot::empty(),
1140 result: result.clone(),
1141 });
1142
1143 run_one_frame(&mut runtime);
1144 assert_eq!(*result.lock().unwrap(), Some(99));
1145 }
1146
1147 #[test]
1148 fn test_job_staged_resource_update_ordering() {
1149 struct Integrator {
1154 slot: JobSlot<u32>,
1155 }
1156
1157 impl RuntimePlugin for Integrator {
1158 fn priority(&self) -> i32 {
1159 phase::PREPARE
1160 } fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
1163 if self.slot.is_empty() {
1164 let (s, sender) = JobSlot::new();
1165 self.slot = s;
1166 sender.complete(77);
1167 }
1168 }
1169
1170 fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {
1171 if let JobPoll::Ready(v) = self.slot.take() {
1172 ctx.resources.insert(v);
1173 }
1174 }
1175
1176 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
1177 }
1178
1179 struct Reader {
1180 seen: Arc<Mutex<Option<u32>>>,
1181 }
1182
1183 impl RuntimePlugin for Reader {
1184 fn priority(&self) -> i32 {
1185 phase::POST_SIM
1186 } fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {
1189 *self.seen.lock().unwrap() = ctx.resources.get::<u32>().copied();
1190 }
1191
1192 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
1193 }
1194
1195 let seen = Arc::new(Mutex::new(None::<u32>));
1196 let mut runtime = ViewportRuntime::new()
1197 .with_plugin(Integrator {
1198 slot: JobSlot::empty(),
1199 })
1200 .with_plugin(Reader { seen: seen.clone() });
1201
1202 run_one_frame(&mut runtime);
1203 assert_eq!(*seen.lock().unwrap(), Some(77));
1205 }
1206
1207 #[test]
1208 fn test_job_slot_in_resources() {
1209 let (slot, sender) = JobSlot::<u32>::new();
1211 let mut res = RuntimeResources::new();
1212 res.insert(slot);
1213 sender.complete(55);
1214 let found = res.get_mut::<JobSlot<u32>>().unwrap();
1215 match found.take() {
1216 JobPoll::Ready(v) => assert_eq!(v, 55),
1217 _ => panic!("expected Ready"),
1218 }
1219 }
1220
1221 use crate::resources::SkinWeights;
1224 use crate::runtime::plugins::skeleton_plugin::{
1225 Joint, JointMatrices, Pose, Skeleton, SkeletonPlugin, apply_skin,
1226 };
1227
1228 fn two_joint_skeleton() -> Skeleton {
1229 Skeleton::new(vec![
1230 Joint {
1231 name: "root".into(),
1232 parent: None,
1233 inverse_bind: glam::Affine3A::IDENTITY,
1234 },
1235 Joint {
1236 name: "child".into(),
1237 parent: Some(0),
1238 inverse_bind: glam::Affine3A::from_translation(-glam::Vec3::Y),
1239 },
1240 ])
1241 }
1242
1243 fn single_vertex_weights(joint: u8, weight: f32) -> SkinWeights {
1244 SkinWeights {
1245 joint_indices: vec![[joint, 0, 0, 0]],
1246 joint_weights: vec![[weight, 0.0, 0.0, 0.0]],
1247 }
1248 }
1249
1250 #[test]
1251 fn test_skeleton_joint_count() {
1252 let sk = two_joint_skeleton();
1253 assert_eq!(sk.joint_count(), 2);
1254 }
1255
1256 #[test]
1257 fn test_skeleton_find_joint() {
1258 let sk = two_joint_skeleton();
1259 assert_eq!(sk.find_joint("root"), Some(0));
1260 assert_eq!(sk.find_joint("child"), Some(1));
1261 assert_eq!(sk.find_joint("missing"), None);
1262 }
1263
1264 #[test]
1265 fn test_pose_identity_transforms() {
1266 let pose = Pose::identity(3);
1267 assert_eq!(pose.joint_count(), 3);
1268 for t in &pose.local_transforms {
1269 assert_eq!(*t, glam::Affine3A::IDENTITY);
1270 }
1271 }
1272
1273 #[test]
1274 fn test_joint_matrices_single_root_identity() {
1275 let sk = Skeleton::new(vec![Joint {
1276 name: "root".into(),
1277 parent: None,
1278 inverse_bind: glam::Affine3A::IDENTITY,
1279 }]);
1280 let pose = Pose::identity(1);
1281 let mats = JointMatrices::compute(&sk, &pose);
1282 let m = mats.as_slice()[0];
1283 assert!(m.matrix3.col(0).abs_diff_eq(glam::Vec3A::X, 1e-5));
1285 assert!(m.translation.abs_diff_eq(glam::Vec3A::ZERO, 1e-5));
1286 }
1287
1288 #[test]
1289 fn test_joint_matrices_parent_child_chain() {
1290 let sk = two_joint_skeleton();
1296 let mut pose = Pose::identity(2);
1297 pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::X);
1298 let mats = JointMatrices::compute(&sk, &pose);
1299 let child = mats.as_slice()[1];
1300 let expected_translation = glam::Vec3A::new(1.0, -1.0, 0.0);
1301 assert!(
1302 child.translation.abs_diff_eq(expected_translation, 1e-5),
1303 "child translation was {:?}, expected {:?}",
1304 child.translation,
1305 expected_translation,
1306 );
1307 }
1308
1309 #[test]
1310 fn test_apply_skin_identity_pose() {
1311 let sk = Skeleton::new(vec![Joint {
1312 name: "root".into(),
1313 parent: None,
1314 inverse_bind: glam::Affine3A::IDENTITY,
1315 }]);
1316 let pose = Pose::identity(1);
1317 let mats = JointMatrices::compute(&sk, &pose);
1318
1319 let positions = vec![[1.0f32, 2.0, 3.0]];
1320 let normals = vec![[0.0f32, 1.0, 0.0]];
1321 let weights = single_vertex_weights(0, 1.0);
1322 let (out_pos, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1323
1324 assert!((out_pos[0][0] - 1.0).abs() < 1e-5);
1325 assert!((out_pos[0][1] - 2.0).abs() < 1e-5);
1326 assert!((out_pos[0][2] - 3.0).abs() < 1e-5);
1327 assert!((out_nrm[0][1] - 1.0).abs() < 1e-5);
1328 }
1329
1330 #[test]
1331 fn test_apply_skin_single_joint_full_weight() {
1332 let sk = Skeleton::new(vec![Joint {
1334 name: "root".into(),
1335 parent: None,
1336 inverse_bind: glam::Affine3A::IDENTITY,
1337 }]);
1338 let mut pose = Pose::identity(1);
1339 pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::new(5.0, 0.0, 0.0));
1340 let mats = JointMatrices::compute(&sk, &pose);
1341
1342 let positions = vec![[0.0f32, 0.0, 0.0]];
1343 let normals = vec![[1.0f32, 0.0, 0.0]];
1344 let weights = single_vertex_weights(0, 1.0);
1345 let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1346 assert!((out_pos[0][0] - 5.0).abs() < 1e-5);
1347 assert!(out_pos[0][1].abs() < 1e-5);
1348 }
1349
1350 #[test]
1351 fn test_apply_skin_two_joint_blend() {
1352 let sk = Skeleton::new(vec![
1355 Joint {
1356 name: "a".into(),
1357 parent: None,
1358 inverse_bind: glam::Affine3A::IDENTITY,
1359 },
1360 Joint {
1361 name: "b".into(),
1362 parent: None,
1363 inverse_bind: glam::Affine3A::IDENTITY,
1364 },
1365 ]);
1366 let mut pose = Pose::identity(2);
1367 pose.local_transforms[1] =
1368 glam::Affine3A::from_translation(glam::Vec3::new(10.0, 0.0, 0.0));
1369 let mats = JointMatrices::compute(&sk, &pose);
1370
1371 let positions = vec![[0.0f32, 0.0, 0.0]];
1372 let normals = vec![[1.0f32, 0.0, 0.0]];
1373 let weights = SkinWeights {
1374 joint_indices: vec![[0, 1, 0, 0]],
1375 joint_weights: vec![[0.5, 0.5, 0.0, 0.0]],
1376 };
1377 let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1378 assert!(
1379 (out_pos[0][0] - 5.0).abs() < 1e-5,
1380 "expected 5.0, got {}",
1381 out_pos[0][0]
1382 );
1383 }
1384
1385 #[test]
1386 fn test_apply_skin_normal_renormalized() {
1387 let sk = Skeleton::new(vec![Joint {
1389 name: "root".into(),
1390 parent: None,
1391 inverse_bind: glam::Affine3A::IDENTITY,
1392 }]);
1393 let mut pose = Pose::identity(1);
1394 pose.local_transforms[0] = glam::Affine3A::from_scale(glam::Vec3::splat(2.0));
1395 let mats = JointMatrices::compute(&sk, &pose);
1396
1397 let positions = vec![[1.0f32, 0.0, 0.0]];
1398 let normals = vec![[1.0f32, 0.0, 0.0]];
1399 let weights = single_vertex_weights(0, 1.0);
1400 let (_, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1401 let len = (out_nrm[0][0].powi(2) + out_nrm[0][1].powi(2) + out_nrm[0][2].powi(2)).sqrt();
1402 assert!((len - 1.0).abs() < 1e-5, "normal length was {len}");
1403 }
1404
1405 #[test]
1406 fn test_skeleton_plugin_writes_update_to_output() {
1407 let sk = Skeleton::new(vec![Joint {
1408 name: "root".into(),
1409 parent: None,
1410 inverse_bind: glam::Affine3A::IDENTITY,
1411 }]);
1412 let positions = vec![[0.0f32, 0.0, 0.0]];
1413 let normals = vec![[0.0f32, 1.0, 0.0]];
1414 let weights = single_vertex_weights(0, 1.0);
1415 let mesh_id = crate::resources::mesh_store::MeshId(0);
1416
1417 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1418 sk, mesh_id, positions, normals, weights,
1419 ));
1420
1421 runtime.resources_mut().insert(Pose::identity(1));
1423
1424 let output = run_one_frame(&mut runtime);
1425 assert_eq!(output.skinned_mesh_updates.len(), 1);
1426 assert_eq!(output.skinned_mesh_updates[0].mesh_id, mesh_id);
1427 }
1428
1429 #[test]
1430 fn test_skinned_mesh_updates_empty_without_pose() {
1431 let sk = Skeleton::new(vec![Joint {
1432 name: "root".into(),
1433 parent: None,
1434 inverse_bind: glam::Affine3A::IDENTITY,
1435 }]);
1436 let mesh_id = crate::resources::mesh_store::MeshId(0);
1437 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1438 sk,
1439 mesh_id,
1440 vec![[0.0f32; 3]],
1441 vec![[0.0f32, 1.0, 0.0]],
1442 single_vertex_weights(0, 1.0),
1443 ));
1444 let output = run_one_frame(&mut runtime);
1446 assert!(output.skinned_mesh_updates.is_empty());
1447 }
1448
1449 #[test]
1450 fn test_skinned_mesh_updates_cleared_each_frame() {
1451 let sk = Skeleton::new(vec![Joint {
1452 name: "root".into(),
1453 parent: None,
1454 inverse_bind: glam::Affine3A::IDENTITY,
1455 }]);
1456 let mesh_id = crate::resources::mesh_store::MeshId(0);
1457 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1458 sk,
1459 mesh_id,
1460 vec![[0.0f32; 3]],
1461 vec![[0.0f32, 1.0, 0.0]],
1462 single_vertex_weights(0, 1.0),
1463 ));
1464 runtime.resources_mut().insert(Pose::identity(1));
1465
1466 run_one_frame(&mut runtime);
1467 let output2 = run_one_frame(&mut runtime);
1468 assert_eq!(output2.skinned_mesh_updates.len(), 1);
1470 }
1471}
1472
1473pub struct ViewportRuntime {
1488 mode: SceneRuntimeMode,
1489 plugins: Vec<Box<dyn RuntimePlugin>>,
1490 gpu_plugins: Vec<Box<dyn GpuPlugin>>,
1491 gpu_initialized: bool,
1494 fixed_timestep: Option<FixedTimestep>,
1495 snapshots: TransformSnapshotTable,
1496 step_index: u64,
1497 selection_system: Option<SelectionSystem>,
1498 manipulation_system: Option<ManipulationSystem>,
1499 camera_follow: Option<CameraFollow>,
1500 resources: RuntimeResources,
1502 prev_node_ids: std::collections::HashSet<crate::interaction::selection::NodeId>,
1504 scene_initialized: bool,
1506}
1507
1508impl Default for ViewportRuntime {
1509 fn default() -> Self {
1510 Self::new()
1511 }
1512}
1513
1514impl ViewportRuntime {
1515 pub fn new() -> Self {
1517 Self {
1518 mode: SceneRuntimeMode::default(),
1519 plugins: Vec::new(),
1520 gpu_plugins: Vec::new(),
1521 gpu_initialized: false,
1522 fixed_timestep: None,
1523 snapshots: TransformSnapshotTable::new(),
1524 step_index: 0,
1525 selection_system: None,
1526 manipulation_system: None,
1527 camera_follow: None,
1528 resources: RuntimeResources::new(),
1529 prev_node_ids: std::collections::HashSet::new(),
1530 scene_initialized: false,
1531 }
1532 }
1533
1534 pub fn with_selection_system(mut self) -> Self {
1540 self.selection_system = Some(SelectionSystem::new());
1541 self
1542 }
1543
1544 pub fn with_manipulation_system(mut self) -> Self {
1550 self.manipulation_system = Some(ManipulationSystem::new());
1551 self
1552 }
1553
1554 pub fn is_manipulating(&self) -> bool {
1558 self.manipulation_system
1559 .as_ref()
1560 .map_or(false, |m| m.is_active())
1561 }
1562
1563 pub fn manipulation_system(&self) -> Option<&ManipulationSystem> {
1565 self.manipulation_system.as_ref()
1566 }
1567
1568 pub fn with_mode(mut self, mode: SceneRuntimeMode) -> Self {
1570 self.mode = mode;
1571 self
1572 }
1573
1574 pub fn with_plugin(mut self, plugin: impl RuntimePlugin) -> Self {
1577 self.plugins.push(Box::new(plugin));
1578 self
1579 }
1580
1581 pub fn with_gpu_plugin(mut self, plugin: impl GpuPlugin) -> Self {
1590 self.gpu_plugins.push(Box::new(plugin));
1591 self.gpu_initialized = false;
1592 self
1593 }
1594
1595 pub fn with_gpu_plugin_for_viewport(
1607 mut self,
1608 viewport: crate::renderer::ViewportId,
1609 plugin: impl GpuPlugin,
1610 ) -> Self {
1611 let scoped = gpu_plugin::ViewportScopedPlugin {
1612 viewport,
1613 inner: plugin,
1614 };
1615 self.gpu_plugins.push(Box::new(scoped));
1616 self.gpu_initialized = false;
1617 self
1618 }
1619
1620 pub fn notify_device_recreated(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
1629 for p in self.gpu_plugins.iter_mut() {
1630 p.on_device_recreated(device, queue);
1631 }
1632 self.gpu_initialized = false;
1633 }
1634
1635 pub fn pre_prepare(
1654 &mut self,
1655 device: &wgpu::Device,
1656 queue: &wgpu::Queue,
1657 ctx: &GpuFrameContext<'_>,
1658 ) -> Vec<wgpu::CommandBuffer> {
1659 if !self.gpu_initialized {
1660 for p in self.gpu_plugins.iter_mut() {
1661 p.init_gpu(device);
1662 }
1663 self.gpu_initialized = true;
1664 }
1665 self.gpu_plugins.sort_by_key(|p| p.priority());
1668
1669 let mut out = Vec::new();
1670 for p in self.gpu_plugins.iter_mut() {
1671 out.extend(p.pre_prepare(device, queue, ctx));
1672 }
1673 out
1674 }
1675
1676 pub fn post_paint(
1689 &mut self,
1690 device: &wgpu::Device,
1691 queue: &wgpu::Queue,
1692 targets: &PostPaintTargets<'_>,
1693 ctx: &GpuFrameContext<'_>,
1694 ) -> Vec<wgpu::CommandBuffer> {
1695 self.gpu_plugins.sort_by_key(|p| p.priority());
1699
1700 let mut out = Vec::new();
1701 for p in self.gpu_plugins.iter_mut() {
1702 out.extend(p.post_paint(device, queue, targets, ctx));
1703 }
1704 out
1705 }
1706
1707 pub fn with_fixed_timestep(mut self, ts: FixedTimestep) -> Self {
1713 self.fixed_timestep = Some(ts);
1714 self
1715 }
1716
1717 pub fn set_fixed_timestep(&mut self, ts: FixedTimestep) {
1722 self.fixed_timestep = Some(ts);
1723 }
1724
1725 pub fn clear_fixed_timestep(&mut self) {
1727 self.fixed_timestep = None;
1728 }
1729
1730 pub fn mode(&self) -> SceneRuntimeMode {
1732 self.mode
1733 }
1734
1735 pub fn snapshots(&self) -> &TransformSnapshotTable {
1740 &self.snapshots
1741 }
1742
1743 pub fn resources(&self) -> &RuntimeResources {
1748 &self.resources
1749 }
1750
1751 pub fn resources_mut(&mut self) -> &mut RuntimeResources {
1756 &mut self.resources
1757 }
1758
1759 pub fn alpha(&self) -> f32 {
1764 self.fixed_timestep.as_ref().map_or(1.0, |ts| ts.alpha())
1765 }
1766
1767 pub fn step_index(&self) -> u64 {
1772 self.step_index
1773 }
1774
1775 pub fn with_camera_follow(mut self, follow: CameraFollow) -> Self {
1780 self.camera_follow = Some(follow);
1781 self
1782 }
1783
1784 pub fn set_camera_follow(&mut self, follow: CameraFollow) {
1786 self.camera_follow = Some(follow);
1787 }
1788
1789 pub fn clear_camera_follow(&mut self) {
1792 self.camera_follow = None;
1793 }
1794
1795 pub fn camera_follow(&self) -> Option<&CameraFollow> {
1797 self.camera_follow.as_ref()
1798 }
1799
1800 pub fn step(
1811 &mut self,
1812 scene: &mut Scene,
1813 selection: &mut Selection,
1814 frame: &RuntimeFrameContext,
1815 ) -> RuntimeOutput {
1816 let mut output = RuntimeOutput::default();
1817 let mut writeback = TransformWriteback::default();
1818
1819 let current_ids: std::collections::HashSet<crate::interaction::selection::NodeId> =
1822 scene.nodes().map(|n| n.id()).collect();
1823
1824 if self.scene_initialized {
1825 let mut events: Vec<RuntimeEvent> = Vec::new();
1827 for &id in current_ids.difference(&self.prev_node_ids) {
1828 events.push(RuntimeEvent::NodeAdded(id));
1829 }
1830 for &id in self.prev_node_ids.difference(¤t_ids) {
1831 events.push(RuntimeEvent::NodeRemoved(id));
1832 }
1833
1834 if !events.is_empty() {
1835 let mut plugins = std::mem::take(&mut self.plugins);
1837 for event in &events {
1838 for plugin in plugins.iter_mut() {
1839 let mut ctx = RuntimeStepContext {
1840 priority: plugin.priority(),
1841 dt: frame.dt,
1842 scene,
1843 writeback: &mut writeback,
1844 output: &mut output,
1845 pick_hit: frame.pick_hit,
1846 resources: &mut self.resources,
1847 };
1848 plugin.on_event(event, &mut ctx);
1849 }
1850 }
1851 self.plugins = plugins;
1852 }
1853 } else {
1854 self.scene_initialized = true;
1855 }
1856 self.prev_node_ids = current_ids;
1857
1858 self.plugins.sort_by_key(|p| p.priority());
1860
1861 let mut plugins = std::mem::take(&mut self.plugins);
1863
1864 for plugin in plugins.iter_mut() {
1866 let ctx = RuntimeStepContext {
1867 priority: plugin.priority(),
1868 dt: frame.dt,
1869 scene,
1870 writeback: &mut writeback,
1871 output: &mut output,
1872 pick_hit: frame.pick_hit,
1873 resources: &mut self.resources,
1874 };
1875 plugin.submit(&ctx);
1876 }
1877
1878 run_range(
1882 &mut plugins,
1883 i32::MIN,
1884 phase::SELECT,
1885 frame.dt,
1886 frame,
1887 scene,
1888 &mut writeback,
1889 &mut output,
1890 &mut self.resources,
1891 );
1892
1893 if let Some(sel_sys) = &self.selection_system {
1895 sel_sys.step(frame, &mut output);
1896 }
1897 run_range(
1899 &mut plugins,
1900 phase::SELECT,
1901 phase::MANIPULATE,
1902 frame.dt,
1903 frame,
1904 scene,
1905 &mut writeback,
1906 &mut output,
1907 &mut self.resources,
1908 );
1909
1910 if self.manipulation_system.is_some() {
1912 let mut manip_sys = self.manipulation_system.take().unwrap();
1913 manip_sys.step(frame, scene, selection, &mut writeback, &mut output);
1914 self.manipulation_system = Some(manip_sys);
1915 }
1916 run_range(
1918 &mut plugins,
1919 phase::MANIPULATE,
1920 phase::SIMULATE,
1921 frame.dt,
1922 frame,
1923 scene,
1924 &mut writeback,
1925 &mut output,
1926 &mut self.resources,
1927 );
1928
1929 if self.fixed_timestep.is_some() {
1931 let mut ts = self.fixed_timestep.take().unwrap();
1932 for step_dt in ts.advance(frame.dt) {
1933 run_range(
1934 &mut plugins,
1935 phase::SIMULATE,
1936 phase::POST_SIM,
1937 step_dt,
1938 frame,
1939 scene,
1940 &mut writeback,
1941 &mut output,
1942 &mut self.resources,
1943 );
1944 self.step_index = self.step_index.wrapping_add(1);
1945 }
1946 self.fixed_timestep = Some(ts);
1947 } else {
1948 run_range(
1949 &mut plugins,
1950 phase::SIMULATE,
1951 phase::POST_SIM,
1952 frame.dt,
1953 frame,
1954 scene,
1955 &mut writeback,
1956 &mut output,
1957 &mut self.resources,
1958 );
1959 self.step_index = self.step_index.wrapping_add(1);
1960 }
1961
1962 run_range(
1964 &mut plugins,
1965 phase::POST_SIM,
1966 i32::MAX,
1967 frame.dt,
1968 frame,
1969 scene,
1970 &mut writeback,
1971 &mut output,
1972 &mut self.resources,
1973 );
1974
1975 for plugin in plugins.iter_mut() {
1977 let mut ctx = RuntimeStepContext {
1978 priority: plugin.priority(),
1979 dt: frame.dt,
1980 scene,
1981 writeback: &mut writeback,
1982 output: &mut output,
1983 pick_hit: frame.pick_hit,
1984 resources: &mut self.resources,
1985 };
1986 plugin.collect(&mut ctx);
1987 }
1988
1989 self.plugins = plugins;
1991
1992 let ops = writeback.into_ops();
1994 for op in &ops {
1995 let new_local = if op.preserve_scale {
1996 let incoming = glam::Mat4::from(op.transform);
2001 let (_, rot, trans) = incoming.to_scale_rotation_translation();
2002 let existing_scale = scene
2003 .node(op.id)
2004 .map(|n| n.local_transform().to_scale_rotation_translation().0)
2005 .unwrap_or(glam::Vec3::ONE);
2006 glam::Mat4::from_scale_rotation_translation(existing_scale, rot, trans)
2007 } else {
2008 glam::Mat4::from(op.transform)
2009 };
2010 scene.set_local_transform(op.id, new_local);
2011 self.snapshots.update(op.id, op.transform);
2012 }
2013 if !ops.is_empty() {
2014 scene.update_transforms();
2015 }
2016 output.node_transform_ops = ops;
2017
2018 for op in &output.selection_ops {
2020 op.apply_to(selection);
2021 }
2022
2023 if let Some(CameraFollow::Node { id, offset, .. }) = &self.camera_follow {
2025 let id = *id;
2026 let offset = *offset;
2027 let alpha = self.alpha();
2028 let pos = self
2029 .snapshots
2030 .interpolated(id, alpha)
2031 .map(|t| glam::Vec3::from(t.translation))
2032 .or_else(|| {
2033 scene
2034 .node(id)
2035 .map(|n| n.world_transform().col(3).truncate())
2036 });
2037 if let Some(pos) = pos {
2038 output.camera_follow_target = Some(pos + offset);
2039 }
2040 }
2041
2042 output
2043 }
2044}