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::skeleton::{
1219 Joint, JointMatrices, Pose, Skeleton, SkeletonPlugin, apply_skin,
1220 };
1221 use crate::plugins::skinning::{SkinWeights, SkinnedMeshUpdate};
1222
1223 fn two_joint_skeleton() -> Skeleton {
1224 Skeleton::new(vec![
1225 Joint {
1226 name: "root".into(),
1227 parent: None,
1228 inverse_bind: glam::Affine3A::IDENTITY,
1229 },
1230 Joint {
1231 name: "child".into(),
1232 parent: Some(0),
1233 inverse_bind: glam::Affine3A::from_translation(-glam::Vec3::Y),
1234 },
1235 ])
1236 }
1237
1238 fn single_vertex_weights(joint: u8, weight: f32) -> SkinWeights {
1239 SkinWeights {
1240 joint_indices: vec![[joint, 0, 0, 0]],
1241 joint_weights: vec![[weight, 0.0, 0.0, 0.0]],
1242 }
1243 }
1244
1245 #[test]
1246 fn test_skeleton_joint_count() {
1247 let sk = two_joint_skeleton();
1248 assert_eq!(sk.joint_count(), 2);
1249 }
1250
1251 #[test]
1252 fn test_skeleton_find_joint() {
1253 let sk = two_joint_skeleton();
1254 assert_eq!(sk.find_joint("root"), Some(0));
1255 assert_eq!(sk.find_joint("child"), Some(1));
1256 assert_eq!(sk.find_joint("missing"), None);
1257 }
1258
1259 #[test]
1260 fn test_pose_identity_transforms() {
1261 let pose = Pose::identity(3);
1262 assert_eq!(pose.joint_count(), 3);
1263 for t in &pose.local_transforms {
1264 assert_eq!(*t, glam::Affine3A::IDENTITY);
1265 }
1266 }
1267
1268 #[test]
1269 fn test_joint_matrices_single_root_identity() {
1270 let sk = Skeleton::new(vec![Joint {
1271 name: "root".into(),
1272 parent: None,
1273 inverse_bind: glam::Affine3A::IDENTITY,
1274 }]);
1275 let pose = Pose::identity(1);
1276 let mats = JointMatrices::compute(&sk, &pose);
1277 let m = mats.as_slice()[0];
1278 assert!(m.matrix3.col(0).abs_diff_eq(glam::Vec3A::X, 1e-5));
1280 assert!(m.translation.abs_diff_eq(glam::Vec3A::ZERO, 1e-5));
1281 }
1282
1283 #[test]
1284 fn test_joint_matrices_parent_child_chain() {
1285 let sk = two_joint_skeleton();
1291 let mut pose = Pose::identity(2);
1292 pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::X);
1293 let mats = JointMatrices::compute(&sk, &pose);
1294 let child = mats.as_slice()[1];
1295 let expected_translation = glam::Vec3A::new(1.0, -1.0, 0.0);
1296 assert!(
1297 child.translation.abs_diff_eq(expected_translation, 1e-5),
1298 "child translation was {:?}, expected {:?}",
1299 child.translation,
1300 expected_translation,
1301 );
1302 }
1303
1304 #[test]
1305 fn test_apply_skin_identity_pose() {
1306 let sk = Skeleton::new(vec![Joint {
1307 name: "root".into(),
1308 parent: None,
1309 inverse_bind: glam::Affine3A::IDENTITY,
1310 }]);
1311 let pose = Pose::identity(1);
1312 let mats = JointMatrices::compute(&sk, &pose);
1313
1314 let positions = vec![[1.0f32, 2.0, 3.0]];
1315 let normals = vec![[0.0f32, 1.0, 0.0]];
1316 let weights = single_vertex_weights(0, 1.0);
1317 let (out_pos, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1318
1319 assert!((out_pos[0][0] - 1.0).abs() < 1e-5);
1320 assert!((out_pos[0][1] - 2.0).abs() < 1e-5);
1321 assert!((out_pos[0][2] - 3.0).abs() < 1e-5);
1322 assert!((out_nrm[0][1] - 1.0).abs() < 1e-5);
1323 }
1324
1325 #[test]
1326 fn test_apply_skin_single_joint_full_weight() {
1327 let sk = Skeleton::new(vec![Joint {
1329 name: "root".into(),
1330 parent: None,
1331 inverse_bind: glam::Affine3A::IDENTITY,
1332 }]);
1333 let mut pose = Pose::identity(1);
1334 pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::new(5.0, 0.0, 0.0));
1335 let mats = JointMatrices::compute(&sk, &pose);
1336
1337 let positions = vec![[0.0f32, 0.0, 0.0]];
1338 let normals = vec![[1.0f32, 0.0, 0.0]];
1339 let weights = single_vertex_weights(0, 1.0);
1340 let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1341 assert!((out_pos[0][0] - 5.0).abs() < 1e-5);
1342 assert!(out_pos[0][1].abs() < 1e-5);
1343 }
1344
1345 #[test]
1346 fn test_apply_skin_two_joint_blend() {
1347 let sk = Skeleton::new(vec![
1350 Joint {
1351 name: "a".into(),
1352 parent: None,
1353 inverse_bind: glam::Affine3A::IDENTITY,
1354 },
1355 Joint {
1356 name: "b".into(),
1357 parent: None,
1358 inverse_bind: glam::Affine3A::IDENTITY,
1359 },
1360 ]);
1361 let mut pose = Pose::identity(2);
1362 pose.local_transforms[1] =
1363 glam::Affine3A::from_translation(glam::Vec3::new(10.0, 0.0, 0.0));
1364 let mats = JointMatrices::compute(&sk, &pose);
1365
1366 let positions = vec![[0.0f32, 0.0, 0.0]];
1367 let normals = vec![[1.0f32, 0.0, 0.0]];
1368 let weights = SkinWeights {
1369 joint_indices: vec![[0, 1, 0, 0]],
1370 joint_weights: vec![[0.5, 0.5, 0.0, 0.0]],
1371 };
1372 let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1373 assert!(
1374 (out_pos[0][0] - 5.0).abs() < 1e-5,
1375 "expected 5.0, got {}",
1376 out_pos[0][0]
1377 );
1378 }
1379
1380 #[test]
1381 fn test_apply_skin_normal_renormalized() {
1382 let sk = Skeleton::new(vec![Joint {
1384 name: "root".into(),
1385 parent: None,
1386 inverse_bind: glam::Affine3A::IDENTITY,
1387 }]);
1388 let mut pose = Pose::identity(1);
1389 pose.local_transforms[0] = glam::Affine3A::from_scale(glam::Vec3::splat(2.0));
1390 let mats = JointMatrices::compute(&sk, &pose);
1391
1392 let positions = vec![[1.0f32, 0.0, 0.0]];
1393 let normals = vec![[1.0f32, 0.0, 0.0]];
1394 let weights = single_vertex_weights(0, 1.0);
1395 let (_, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1396 let len = (out_nrm[0][0].powi(2) + out_nrm[0][1].powi(2) + out_nrm[0][2].powi(2)).sqrt();
1397 assert!((len - 1.0).abs() < 1e-5, "normal length was {len}");
1398 }
1399
1400 #[test]
1401 fn test_skeleton_plugin_writes_update_to_output() {
1402 let sk = Skeleton::new(vec![Joint {
1403 name: "root".into(),
1404 parent: None,
1405 inverse_bind: glam::Affine3A::IDENTITY,
1406 }]);
1407 let positions = vec![[0.0f32, 0.0, 0.0]];
1408 let normals = vec![[0.0f32, 1.0, 0.0]];
1409 let weights = single_vertex_weights(0, 1.0);
1410 let mesh_id = crate::resources::mesh_store::MeshId(0);
1411
1412 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1413 sk, mesh_id, positions, normals, weights,
1414 ));
1415
1416 runtime.resources_mut().insert(Pose::identity(1));
1418
1419 let output = run_one_frame(&mut runtime);
1420 let updates: Vec<&SkinnedMeshUpdate> = output.events.read::<SkinnedMeshUpdate>().collect();
1421 assert_eq!(updates.len(), 1);
1422 assert_eq!(updates[0].mesh_id, mesh_id);
1423 }
1424
1425 #[test]
1426 fn test_skinned_mesh_updates_empty_without_pose() {
1427 let sk = Skeleton::new(vec![Joint {
1428 name: "root".into(),
1429 parent: None,
1430 inverse_bind: glam::Affine3A::IDENTITY,
1431 }]);
1432 let mesh_id = crate::resources::mesh_store::MeshId(0);
1433 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1434 sk,
1435 mesh_id,
1436 vec![[0.0f32; 3]],
1437 vec![[0.0f32, 1.0, 0.0]],
1438 single_vertex_weights(0, 1.0),
1439 ));
1440 let output = run_one_frame(&mut runtime);
1442 assert_eq!(output.events.read::<SkinnedMeshUpdate>().count(), 0);
1443 }
1444
1445 #[test]
1446 fn test_skinned_mesh_updates_cleared_each_frame() {
1447 let sk = Skeleton::new(vec![Joint {
1448 name: "root".into(),
1449 parent: None,
1450 inverse_bind: glam::Affine3A::IDENTITY,
1451 }]);
1452 let mesh_id = crate::resources::mesh_store::MeshId(0);
1453 let mut runtime = ViewportRuntime::new().with_plugin(SkeletonPlugin::new(
1454 sk,
1455 mesh_id,
1456 vec![[0.0f32; 3]],
1457 vec![[0.0f32, 1.0, 0.0]],
1458 single_vertex_weights(0, 1.0),
1459 ));
1460 runtime.resources_mut().insert(Pose::identity(1));
1461
1462 run_one_frame(&mut runtime);
1463 let output2 = run_one_frame(&mut runtime);
1464 assert_eq!(output2.events.read::<SkinnedMeshUpdate>().count(), 1);
1466 }
1467}
1468
1469pub struct ViewportRuntime {
1484 mode: SceneRuntimeMode,
1485 plugins: Vec<Box<dyn RuntimePlugin>>,
1486 gpu_plugins: Vec<Box<dyn GpuPlugin>>,
1487 gpu_initialized: bool,
1490 fixed_timestep: Option<FixedTimestep>,
1491 snapshots: TransformSnapshotTable,
1492 step_index: u64,
1493 selection_system: Option<SelectionSystem>,
1494 manipulation_system: Option<ManipulationSystem>,
1495 camera_follow: Option<CameraFollow>,
1496 resources: RuntimeResources,
1498 prev_node_ids: std::collections::HashSet<crate::interaction::selection::NodeId>,
1500 scene_initialized: bool,
1502}
1503
1504impl Default for ViewportRuntime {
1505 fn default() -> Self {
1506 Self::new()
1507 }
1508}
1509
1510impl ViewportRuntime {
1511 pub fn new() -> Self {
1513 Self {
1514 mode: SceneRuntimeMode::default(),
1515 plugins: Vec::new(),
1516 gpu_plugins: Vec::new(),
1517 gpu_initialized: false,
1518 fixed_timestep: None,
1519 snapshots: TransformSnapshotTable::new(),
1520 step_index: 0,
1521 selection_system: None,
1522 manipulation_system: None,
1523 camera_follow: None,
1524 resources: RuntimeResources::new(),
1525 prev_node_ids: std::collections::HashSet::new(),
1526 scene_initialized: false,
1527 }
1528 }
1529
1530 pub fn with_selection_system(mut self) -> Self {
1536 self.selection_system = Some(SelectionSystem::new());
1537 self
1538 }
1539
1540 pub fn with_manipulation_system(mut self) -> Self {
1546 self.manipulation_system = Some(ManipulationSystem::new());
1547 self
1548 }
1549
1550 pub fn is_manipulating(&self) -> bool {
1554 self.manipulation_system
1555 .as_ref()
1556 .map_or(false, |m| m.is_active())
1557 }
1558
1559 pub fn manipulation_system(&self) -> Option<&ManipulationSystem> {
1561 self.manipulation_system.as_ref()
1562 }
1563
1564 pub fn with_mode(mut self, mode: SceneRuntimeMode) -> Self {
1566 self.mode = mode;
1567 self
1568 }
1569
1570 pub fn with_plugin(mut self, plugin: impl RuntimePlugin) -> Self {
1573 self.plugins.push(Box::new(plugin));
1574 self
1575 }
1576
1577 pub fn add_plugin(&mut self, plugin: impl RuntimePlugin) {
1583 self.plugins.push(Box::new(plugin));
1584 }
1585
1586 pub fn with_gpu_plugin(mut self, plugin: impl GpuPlugin) -> Self {
1595 self.gpu_plugins.push(Box::new(plugin));
1596 self.gpu_initialized = false;
1597 self
1598 }
1599
1600 pub fn add_gpu_plugin(&mut self, plugin: impl GpuPlugin) {
1605 self.gpu_plugins.push(Box::new(plugin));
1606 self.gpu_initialized = false;
1607 }
1608
1609 pub fn with_gpu_plugin_for_viewport(
1621 mut self,
1622 viewport: crate::renderer::ViewportId,
1623 plugin: impl GpuPlugin,
1624 ) -> Self {
1625 let scoped = gpu_plugin::ViewportScopedPlugin {
1626 viewport,
1627 inner: plugin,
1628 };
1629 self.gpu_plugins.push(Box::new(scoped));
1630 self.gpu_initialized = false;
1631 self
1632 }
1633
1634 pub fn notify_device_recreated(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
1643 for p in self.gpu_plugins.iter_mut() {
1644 p.on_device_recreated(device, queue);
1645 }
1646 self.gpu_initialized = false;
1647 }
1648
1649 pub fn pre_prepare(
1668 &mut self,
1669 device: &wgpu::Device,
1670 queue: &wgpu::Queue,
1671 ctx: &GpuFrameContext<'_>,
1672 ) -> Vec<wgpu::CommandBuffer> {
1673 if !self.gpu_initialized {
1674 for p in self.gpu_plugins.iter_mut() {
1675 p.init_gpu(device);
1676 }
1677 self.gpu_initialized = true;
1678 }
1679 self.gpu_plugins.sort_by_key(|p| p.priority());
1682
1683 let mut out = Vec::new();
1684 for p in self.gpu_plugins.iter_mut() {
1685 out.extend(p.pre_prepare(device, queue, ctx));
1686 }
1687 out
1688 }
1689
1690 pub fn post_paint(
1703 &mut self,
1704 device: &wgpu::Device,
1705 queue: &wgpu::Queue,
1706 targets: &PostPaintTargets<'_>,
1707 ctx: &GpuFrameContext<'_>,
1708 ) -> Vec<wgpu::CommandBuffer> {
1709 self.gpu_plugins.sort_by_key(|p| p.priority());
1713
1714 let mut out = Vec::new();
1715 for p in self.gpu_plugins.iter_mut() {
1716 out.extend(p.post_paint(device, queue, targets, ctx));
1717 }
1718 out
1719 }
1720
1721 pub fn with_fixed_timestep(mut self, ts: FixedTimestep) -> Self {
1727 self.fixed_timestep = Some(ts);
1728 self
1729 }
1730
1731 pub fn set_fixed_timestep(&mut self, ts: FixedTimestep) {
1736 self.fixed_timestep = Some(ts);
1737 }
1738
1739 pub fn clear_fixed_timestep(&mut self) {
1741 self.fixed_timestep = None;
1742 }
1743
1744 pub fn mode(&self) -> SceneRuntimeMode {
1746 self.mode
1747 }
1748
1749 pub fn snapshots(&self) -> &TransformSnapshotTable {
1754 &self.snapshots
1755 }
1756
1757 pub fn resources(&self) -> &RuntimeResources {
1762 &self.resources
1763 }
1764
1765 pub fn resources_mut(&mut self) -> &mut RuntimeResources {
1770 &mut self.resources
1771 }
1772
1773 pub fn alpha(&self) -> f32 {
1778 self.fixed_timestep.as_ref().map_or(1.0, |ts| ts.alpha())
1779 }
1780
1781 pub fn step_index(&self) -> u64 {
1786 self.step_index
1787 }
1788
1789 pub fn with_camera_follow(mut self, follow: CameraFollow) -> Self {
1795 self.camera_follow = Some(follow);
1796 self
1797 }
1798
1799 pub fn set_camera_follow(&mut self, follow: CameraFollow) {
1801 self.camera_follow = Some(follow);
1802 }
1803
1804 pub fn clear_camera_follow(&mut self) {
1807 self.camera_follow = None;
1808 }
1809
1810 pub fn camera_follow(&self) -> Option<&CameraFollow> {
1812 self.camera_follow.as_ref()
1813 }
1814
1815 pub fn step(
1826 &mut self,
1827 scene: &mut Scene,
1828 selection: &mut Selection,
1829 frame: &RuntimeFrameContext,
1830 ) -> RuntimeOutput {
1831 let mut output = RuntimeOutput::default();
1832 let mut writeback = TransformWriteback::default();
1833
1834 let current_ids: std::collections::HashSet<crate::interaction::selection::NodeId> =
1837 scene.nodes().map(|n| n.id()).collect();
1838
1839 if self.scene_initialized {
1840 let mut events: Vec<RuntimeEvent> = Vec::new();
1842 for &id in current_ids.difference(&self.prev_node_ids) {
1843 events.push(RuntimeEvent::NodeAdded(id));
1844 }
1845 for &id in self.prev_node_ids.difference(¤t_ids) {
1846 events.push(RuntimeEvent::NodeRemoved(id));
1847 }
1848
1849 if !events.is_empty() {
1850 let mut plugins = std::mem::take(&mut self.plugins);
1852 for event in &events {
1853 for plugin in plugins.iter_mut() {
1854 let mut ctx = RuntimeStepContext {
1855 priority: plugin.priority(),
1856 dt: frame.dt,
1857 scene,
1858 writeback: &mut writeback,
1859 output: &mut output,
1860 pick_hit: frame.pick_hit,
1861 resources: &mut self.resources,
1862 };
1863 plugin.on_event(event, &mut ctx);
1864 }
1865 }
1866 self.plugins = plugins;
1867 }
1868 } else {
1869 self.scene_initialized = true;
1870 }
1871 self.prev_node_ids = current_ids;
1872
1873 self.plugins.sort_by_key(|p| p.priority());
1875
1876 let mut plugins = std::mem::take(&mut self.plugins);
1878
1879 for plugin in plugins.iter_mut() {
1881 let ctx = RuntimeStepContext {
1882 priority: plugin.priority(),
1883 dt: frame.dt,
1884 scene,
1885 writeback: &mut writeback,
1886 output: &mut output,
1887 pick_hit: frame.pick_hit,
1888 resources: &mut self.resources,
1889 };
1890 plugin.submit(&ctx);
1891 }
1892
1893 run_range(
1897 &mut plugins,
1898 i32::MIN,
1899 phase::SELECT,
1900 frame.dt,
1901 frame,
1902 scene,
1903 &mut writeback,
1904 &mut output,
1905 &mut self.resources,
1906 );
1907
1908 if let Some(sel_sys) = &self.selection_system {
1910 sel_sys.step(frame, &mut output);
1911 }
1912 run_range(
1914 &mut plugins,
1915 phase::SELECT,
1916 phase::MANIPULATE,
1917 frame.dt,
1918 frame,
1919 scene,
1920 &mut writeback,
1921 &mut output,
1922 &mut self.resources,
1923 );
1924
1925 if self.manipulation_system.is_some() {
1927 let mut manip_sys = self.manipulation_system.take().unwrap();
1928 manip_sys.step(frame, scene, selection, &mut writeback, &mut output);
1929 self.manipulation_system = Some(manip_sys);
1930 }
1931 run_range(
1933 &mut plugins,
1934 phase::MANIPULATE,
1935 phase::SIMULATE,
1936 frame.dt,
1937 frame,
1938 scene,
1939 &mut writeback,
1940 &mut output,
1941 &mut self.resources,
1942 );
1943
1944 if self.fixed_timestep.is_some() {
1946 let mut ts = self.fixed_timestep.take().unwrap();
1947 for step_dt in ts.advance(frame.dt) {
1948 run_range(
1949 &mut plugins,
1950 phase::SIMULATE,
1951 phase::POST_SIM,
1952 step_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 self.fixed_timestep = Some(ts);
1962 } else {
1963 run_range(
1964 &mut plugins,
1965 phase::SIMULATE,
1966 phase::POST_SIM,
1967 frame.dt,
1968 frame,
1969 scene,
1970 &mut writeback,
1971 &mut output,
1972 &mut self.resources,
1973 );
1974 self.step_index = self.step_index.wrapping_add(1);
1975 }
1976
1977 run_range(
1979 &mut plugins,
1980 phase::POST_SIM,
1981 i32::MAX,
1982 frame.dt,
1983 frame,
1984 scene,
1985 &mut writeback,
1986 &mut output,
1987 &mut self.resources,
1988 );
1989
1990 for plugin in plugins.iter_mut() {
1992 let mut ctx = RuntimeStepContext {
1993 priority: plugin.priority(),
1994 dt: frame.dt,
1995 scene,
1996 writeback: &mut writeback,
1997 output: &mut output,
1998 pick_hit: frame.pick_hit,
1999 resources: &mut self.resources,
2000 };
2001 plugin.collect(&mut ctx);
2002 }
2003
2004 self.plugins = plugins;
2006
2007 let ops = writeback.into_ops();
2009 for op in &ops {
2010 let new_local = if op.preserve_scale {
2011 let incoming = glam::Mat4::from(op.transform);
2016 let (_, rot, trans) = incoming.to_scale_rotation_translation();
2017 let existing_scale = scene
2018 .node(op.id)
2019 .map(|n| n.local_transform().to_scale_rotation_translation().0)
2020 .unwrap_or(glam::Vec3::ONE);
2021 glam::Mat4::from_scale_rotation_translation(existing_scale, rot, trans)
2022 } else {
2023 glam::Mat4::from(op.transform)
2024 };
2025 scene.set_local_transform(op.id, new_local);
2026 self.snapshots.update(op.id, op.transform);
2027 }
2028 if !ops.is_empty() {
2029 scene.update_transforms();
2030 }
2031 output.node_transform_ops = ops;
2032
2033 for op in &output.selection_ops {
2035 op.apply_to(selection);
2036 }
2037
2038 if let Some(CameraFollow::Node { id, offset, .. }) = &self.camera_follow {
2040 let id = *id;
2041 let offset = *offset;
2042 let alpha = self.alpha();
2043 let pos = self
2044 .snapshots
2045 .interpolated(id, alpha)
2046 .map(|t| glam::Vec3::from(t.translation))
2047 .or_else(|| {
2048 scene
2049 .node(id)
2050 .map(|n| n.world_transform().col(3).truncate())
2051 });
2052 if let Some(pos) = pos {
2053 output
2054 .events
2055 .emit(crate::runtime::output::CameraFollowTarget(pos + offset));
2056 }
2057 }
2058
2059 output
2060 }
2061}