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