1pub mod camera_follow;
43pub mod context;
44pub mod debug_draw;
46pub mod events;
47pub mod gpu_plugin;
49pub mod guide;
51pub mod jobs;
53pub mod mode;
54pub mod output;
55pub mod plugin;
56pub mod plugins;
58pub mod resources;
59pub mod snapshot;
60pub mod systems;
62pub mod timestep;
63
64pub use camera_follow::CameraFollow;
65pub use context::{RuntimeFrameContext, RuntimeStepContext, SimulationStepContext};
66pub use debug_draw::{DebugDraw, DebugLayer, DebugPrim};
67pub use events::RuntimeEventBus;
68pub use gpu_plugin::{gpu_phase, GpuFrameContext, GpuPlugin, PostPaintTargets};
69pub use jobs::{JobPoll, JobSender, JobSlot};
70pub use mode::SceneRuntimeMode;
71pub use output::{CameraCommand, ContactEvent, NodeTransformOp, RuntimeOutput, SelectionOp, SkinnedMeshUpdate, SkinnedPoseUpdate, TransformWriteback};
72pub use plugin::{phase, RuntimeEvent, RuntimePhase, RuntimePlugin};
73pub use plugins::{
74 AnimationClip, AnimationPlugin, AnimationTrack, Channel, ClipPlayerPlugin, Constraint,
75 ConstraintPlugin, Interpolation, Joint, JointMatrices, Keyframe, PhysicsBody,
76 PhysicsLitePlugin, Pose, Sampler, Skeleton, SkeletonPlugin, SkinnedActor, SkinnedActorPart,
77 SkinnedActorPlugin, SkinningPath, Track, TrackValue, TrackValues, apply_skin,
78};
79pub use resources::RuntimeResources;
80pub use snapshot::{TransformSnapshot, TransformSnapshotTable};
81pub use systems::{ManipulationSystem, SelectionSystem};
82pub use timestep::{FixedStepIter, FixedTimestep};
83
84use crate::interaction::selection::Selection;
85use crate::scene::scene::Scene;
86
87fn run_range(
92 plugins: &mut Vec<Box<dyn RuntimePlugin>>,
93 min: i32,
94 max: i32,
95 dt: f32,
96 frame: &RuntimeFrameContext,
97 scene: &Scene,
98 writeback: &mut TransformWriteback,
99 output: &mut RuntimeOutput,
100 resources: &mut RuntimeResources,
101) {
102 for plugin in plugins.iter_mut() {
103 let p = plugin.priority();
104 if p >= min && p < max {
105 let mut ctx = RuntimeStepContext {
106 priority: p,
107 dt,
108 scene,
109 writeback,
110 output,
111 pick_hit: frame.pick_hit,
112 resources,
113 };
114 plugin.step(&mut ctx);
115 }
116 }
117}
118
119#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::camera::camera::Camera;
125 use crate::interaction::input::ActionFrame;
126 use crate::interaction::selection::{NodeId, Selection};
127 use crate::scene::material::Material;
128 use crate::scene::scene::Scene;
129 use std::sync::{Arc, Mutex};
130
131 fn make_frame(camera: &Camera, input: &ActionFrame, dt: f32) -> RuntimeFrameContext {
132 RuntimeFrameContext {
133 dt,
134 camera: camera.clone(),
135 viewport_size: glam::Vec2::new(800.0, 600.0),
136 input: input.clone(),
137 pick_hit: None,
138 clicked: false,
139 drag_started: false,
140 dragging: false,
141 pointer_delta: glam::Vec2::ZERO,
142 cursor_viewport: None,
143 shift_held: false,
144 }
145 }
146
147 struct OrderTracker {
149 priority: i32,
150 id: u32,
151 log: Arc<Mutex<Vec<(i32, u32)>>>,
152 }
153
154 impl RuntimePlugin for OrderTracker {
155 fn priority(&self) -> i32 {
156 self.priority
157 }
158 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
159 self.log.lock().unwrap().push((self.priority, self.id));
160 }
161 }
162
163 struct CallCounter {
165 priority: i32,
166 count: Arc<Mutex<u32>>,
167 }
168
169 impl RuntimePlugin for CallCounter {
170 fn priority(&self) -> i32 {
171 self.priority
172 }
173 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
174 *self.count.lock().unwrap() += 1;
175 }
176 }
177
178 struct WritebackPlugin {
180 node_id: NodeId,
181 transform: glam::Affine3A,
182 }
183
184 impl RuntimePlugin for WritebackPlugin {
185 fn priority(&self) -> i32 {
186 phase::SIMULATE
187 }
188 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
189 ctx.writeback.set(self.node_id, self.transform);
190 }
191 }
192
193 struct DtRecorder {
195 dts: Arc<Mutex<Vec<f32>>>,
196 }
197
198 impl RuntimePlugin for DtRecorder {
199 fn priority(&self) -> i32 {
200 phase::SIMULATE
201 }
202 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
203 self.dts.lock().unwrap().push(ctx.dt);
204 }
205 }
206
207 #[test]
208 fn test_phase_execution_order() {
209 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
211 let mut runtime = ViewportRuntime::new()
212 .with_plugin(OrderTracker { priority: phase::WRITEBACK, id: 3, log: log.clone() })
213 .with_plugin(OrderTracker { priority: phase::SIMULATE, id: 2, log: log.clone() })
214 .with_plugin(OrderTracker { priority: phase::ANIMATE, id: 1, log: log.clone() })
215 .with_plugin(OrderTracker { priority: phase::PREPARE, id: 0, log: log.clone() });
216
217 let camera = Camera::default();
218 let input = ActionFrame::default();
219 let mut scene = Scene::new();
220 let mut sel = Selection::new();
221 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
222
223 let calls = log.lock().unwrap();
224 let priorities: Vec<i32> = calls.iter().map(|(p, _)| *p).collect();
225 for w in priorities.windows(2) {
227 assert!(w[0] <= w[1], "expected ascending order, got {:?}", priorities);
228 }
229 assert_eq!(priorities.len(), 4);
230 }
231
232 #[test]
233 fn test_registration_order_within_phase() {
234 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
236 let mut runtime = ViewportRuntime::new()
237 .with_plugin(OrderTracker { priority: phase::SIMULATE, id: 0, log: log.clone() })
238 .with_plugin(OrderTracker { priority: phase::SIMULATE, id: 1, log: log.clone() });
239
240 let camera = Camera::default();
241 let input = ActionFrame::default();
242 let mut scene = Scene::new();
243 let mut sel = Selection::new();
244 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
245
246 let calls = log.lock().unwrap();
247 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
248 assert_eq!(ids, vec![0, 1]);
249 }
250
251 #[test]
252 fn test_simulate_runs_once_without_fixed_timestep() {
253 let count = Arc::new(Mutex::new(0u32));
254 let mut runtime = ViewportRuntime::new()
255 .with_plugin(CallCounter { priority: phase::SIMULATE, count: count.clone() });
256
257 let camera = Camera::default();
258 let input = ActionFrame::default();
259 let mut scene = Scene::new();
260 let mut sel = Selection::new();
261 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
262
263 assert_eq!(*count.lock().unwrap(), 1);
264 }
265
266 #[test]
267 fn test_simulate_runs_n_times_with_fixed_timestep() {
268 let count = Arc::new(Mutex::new(0u32));
269 let hz = 60.0_f32;
270 let step_dt = 1.0 / hz;
271 let mut runtime = ViewportRuntime::new()
272 .with_fixed_timestep(FixedTimestep::new(hz))
273 .with_plugin(CallCounter { priority: phase::SIMULATE, count: count.clone() });
274
275 let camera = Camera::default();
276 let input = ActionFrame::default();
277 let mut scene = Scene::new();
278 let mut sel = Selection::new();
279
280 let dt = step_dt * 3.0 + 0.0001;
282 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
283
284 assert_eq!(*count.lock().unwrap(), 3);
285 }
286
287 #[test]
288 fn test_simulate_dt_equals_step_dt_with_fixed_timestep() {
289 let dts = Arc::new(Mutex::new(Vec::<f32>::new()));
290 let hz = 60.0_f32;
291 let step_dt = 1.0 / hz;
292 let mut runtime = ViewportRuntime::new()
293 .with_fixed_timestep(FixedTimestep::new(hz))
294 .with_plugin(DtRecorder { dts: dts.clone() });
295
296 let camera = Camera::default();
297 let input = ActionFrame::default();
298 let mut scene = Scene::new();
299 let mut sel = Selection::new();
300
301 let dt = step_dt * 2.0 + 0.0001;
303 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
304
305 let recorded = dts.lock().unwrap();
306 assert_eq!(recorded.len(), 2);
307 for &d in recorded.iter() {
308 assert!((d - step_dt).abs() < 1e-6, "expected {step_dt}, got {d}");
309 }
310 }
311
312 #[test]
313 fn test_writeback_flushes_to_scene() {
314 let target = glam::Affine3A::from_translation(glam::Vec3::new(3.0, 4.0, 5.0));
315 let mut scene = Scene::new();
316 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
317
318 let mut runtime = ViewportRuntime::new()
319 .with_plugin(WritebackPlugin { node_id, transform: target });
320
321 let camera = Camera::default();
322 let input = ActionFrame::default();
323 let mut sel = Selection::new();
324 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
325
326 let node = scene.node(node_id).expect("node not found");
327 let pos = node.world_transform().col(3).truncate();
328 assert!((pos - glam::Vec3::new(3.0, 4.0, 5.0)).length() < 1e-5, "pos was {pos:?}");
329 }
330
331 #[test]
332 fn test_writeback_ops_in_output() {
333 let target = glam::Affine3A::from_translation(glam::Vec3::new(1.0, 0.0, 0.0));
334 let mut scene = Scene::new();
335 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
336
337 let mut runtime = ViewportRuntime::new()
338 .with_plugin(WritebackPlugin { node_id, transform: target });
339
340 let camera = Camera::default();
341 let input = ActionFrame::default();
342 let mut sel = Selection::new();
343 let output = runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
344
345 assert_eq!(output.node_transform_ops.len(), 1);
346 assert_eq!(output.node_transform_ops[0].id, node_id);
347 }
348
349 #[test]
350 fn test_step_index_increments_each_simulate() {
351 let mut runtime = ViewportRuntime::new();
352 let camera = Camera::default();
353 let input = ActionFrame::default();
354 let mut scene = Scene::new();
355 let mut sel = Selection::new();
356
357 assert_eq!(runtime.step_index(), 0);
358 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
359 assert_eq!(runtime.step_index(), 1);
360 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
361 assert_eq!(runtime.step_index(), 2);
362 }
363
364 #[test]
365 fn test_step_index_increments_n_times_with_fixed_timestep() {
366 let hz = 60.0_f32;
367 let step_dt = 1.0 / hz;
368 let mut runtime = ViewportRuntime::new()
369 .with_fixed_timestep(FixedTimestep::new(hz));
370
371 let camera = Camera::default();
372 let input = ActionFrame::default();
373 let mut scene = Scene::new();
374 let mut sel = Selection::new();
375
376 let dt = step_dt * 3.0 + 0.0001;
378 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, dt));
379 assert_eq!(runtime.step_index(), 3);
380 }
381
382 #[test]
383 fn test_snapshot_updated_after_writeback() {
384 let target = glam::Affine3A::from_translation(glam::Vec3::new(7.0, 0.0, 0.0));
385 let mut scene = Scene::new();
386 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
387
388 let mut runtime = ViewportRuntime::new()
389 .with_plugin(WritebackPlugin { node_id, transform: target });
390
391 let camera = Camera::default();
392 let input = ActionFrame::default();
393 let mut sel = Selection::new();
394 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
395
396 let snap = runtime.snapshots().get(node_id).expect("snapshot missing");
397 assert!((snap.curr.translation.x - 7.0).abs() < 1e-5);
398 }
399
400 #[test]
403 fn test_post_sim_runs_after_simulate() {
404 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
405 let mut runtime = ViewportRuntime::new()
406 .with_plugin(OrderTracker { priority: phase::POST_SIM, id: 1, log: log.clone() })
407 .with_plugin(OrderTracker { priority: phase::SIMULATE, id: 0, log: log.clone() });
408
409 let camera = Camera::default();
410 let input = ActionFrame::default();
411 let mut scene = Scene::new();
412 let mut sel = Selection::new();
413 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
414
415 let calls = log.lock().unwrap();
416 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
417 assert_eq!(ids, vec![0, 1], "simulate must run before post_sim");
418 }
419
420 #[test]
421 fn test_plugin_between_bands() {
422 let log = Arc::new(Mutex::new(Vec::<(i32, u32)>::new()));
423 let mid = phase::ANIMATE + 50;
424 let mut runtime = ViewportRuntime::new()
425 .with_plugin(OrderTracker { priority: phase::SIMULATE, id: 2, log: log.clone() })
426 .with_plugin(OrderTracker { priority: mid, id: 1, log: log.clone() })
427 .with_plugin(OrderTracker { priority: phase::ANIMATE, id: 0, log: log.clone() });
428
429 let camera = Camera::default();
430 let input = ActionFrame::default();
431 let mut scene = Scene::new();
432 let mut sel = Selection::new();
433 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
434
435 let calls = log.lock().unwrap();
436 let ids: Vec<u32> = calls.iter().map(|(_, id)| *id).collect();
437 assert_eq!(ids, vec![0, 1, 2], "plugins must execute in priority order");
438 }
439
440 struct EventRecorder {
442 added: Arc<Mutex<Vec<NodeId>>>,
443 removed: Arc<Mutex<Vec<NodeId>>>,
444 }
445
446 impl RuntimePlugin for EventRecorder {
447 fn priority(&self) -> i32 {
448 phase::PREPARE
449 }
450 fn on_event(&mut self, event: &RuntimeEvent, _ctx: &mut RuntimeStepContext<'_>) {
451 match event {
452 RuntimeEvent::NodeAdded(id) => self.added.lock().unwrap().push(*id),
453 RuntimeEvent::NodeRemoved(id) => self.removed.lock().unwrap().push(*id),
454 }
455 }
456 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
457 }
458
459 #[test]
460 fn test_lifecycle_events_node_added() {
461 let added = Arc::new(Mutex::new(Vec::<NodeId>::new()));
462 let removed = Arc::new(Mutex::new(Vec::<NodeId>::new()));
463 let mut runtime = ViewportRuntime::new()
464 .with_plugin(EventRecorder { added: added.clone(), removed: removed.clone() });
465
466 let camera = Camera::default();
467 let input = ActionFrame::default();
468 let mut scene = Scene::new();
469 let mut sel = Selection::new();
470
471 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
473 assert!(added.lock().unwrap().is_empty(), "no events on first step");
474
475 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
477 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
478
479 let a = added.lock().unwrap();
480 assert!(a.contains(&node_id), "expected NodeAdded event for {:?}", node_id);
481 assert!(removed.lock().unwrap().is_empty());
482 }
483
484 #[test]
485 fn test_lifecycle_events_node_removed() {
486 let added = Arc::new(Mutex::new(Vec::<NodeId>::new()));
487 let removed = Arc::new(Mutex::new(Vec::<NodeId>::new()));
488 let mut runtime = ViewportRuntime::new()
489 .with_plugin(EventRecorder { added: added.clone(), removed: removed.clone() });
490
491 let camera = Camera::default();
492 let input = ActionFrame::default();
493 let mut scene = Scene::new();
494 let mut sel = Selection::new();
495
496 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
497
498 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
500 added.lock().unwrap().clear();
501
502 scene.remove(node_id);
504 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
505
506 let r = removed.lock().unwrap();
507 assert!(r.contains(&node_id), "expected NodeRemoved event for {:?}", node_id);
508 }
509
510 #[derive(Default)]
512 struct LifecycleRecorder {
513 log: Arc<Mutex<Vec<&'static str>>>,
514 }
515
516 impl RuntimePlugin for LifecycleRecorder {
517 fn priority(&self) -> i32 {
518 phase::PREPARE
519 }
520 fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
521 self.log.lock().unwrap().push("submit");
522 }
523 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
524 self.log.lock().unwrap().push("step");
525 }
526 fn collect(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
527 self.log.lock().unwrap().push("collect");
528 }
529 }
530
531 #[test]
532 fn test_submit_collect_called() {
533 let log = Arc::new(Mutex::new(Vec::<&'static str>::new()));
534 let mut runtime = ViewportRuntime::new()
535 .with_plugin(LifecycleRecorder { log: log.clone() });
536
537 let camera = Camera::default();
538 let input = ActionFrame::default();
539 let mut scene = Scene::new();
540 let mut sel = Selection::new();
541
542 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
543
544 let calls = log.lock().unwrap();
545 assert!(calls.contains(&"submit"), "submit must be called");
546 assert!(calls.contains(&"step"), "step must be called");
547 assert!(calls.contains(&"collect"), "collect must be called");
548 let si = calls.iter().position(|&s| s == "submit").unwrap();
550 let st = calls.iter().position(|&s| s == "step").unwrap();
551 let co = calls.iter().position(|&s| s == "collect").unwrap();
552 assert!(si < st, "submit must come before step");
553 assert!(st < co, "step must come before collect");
554 }
555
556 #[test]
559 fn test_resource_insert_get() {
560 let mut res = RuntimeResources::new();
561 res.insert(42u32);
562 assert_eq!(res.get::<u32>(), Some(&42));
563 assert!(res.get::<u64>().is_none());
564 }
565
566 #[test]
567 fn test_resource_insert_overwrites() {
568 let mut res = RuntimeResources::new();
569 res.insert(1u32);
570 res.insert(2u32);
571 assert_eq!(res.get::<u32>(), Some(&2));
572 }
573
574 #[test]
575 fn test_resource_get_mut() {
576 let mut res = RuntimeResources::new();
577 res.insert(0u32);
578 *res.get_mut::<u32>().unwrap() = 99;
579 assert_eq!(res.get::<u32>(), Some(&99));
580 }
581
582 #[test]
583 fn test_resource_remove() {
584 let mut res = RuntimeResources::new();
585 res.insert(7u32);
586 assert!(res.contains::<u32>());
587 let val = res.remove::<u32>();
588 assert_eq!(val, Some(7));
589 assert!(!res.contains::<u32>());
590 assert!(res.remove::<u32>().is_none());
592 }
593
594 #[test]
595 fn test_resource_missing_returns_none() {
596 let res = RuntimeResources::new();
597 assert!(res.get::<u32>().is_none());
598 assert!(!res.contains::<u32>());
599 }
600
601 struct CounterWriter {
603 value: u32,
604 }
605
606 impl RuntimePlugin for CounterWriter {
607 fn priority(&self) -> i32 { phase::ANIMATE }
608 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
609 ctx.resources.insert(self.value);
610 }
611 }
612
613 struct CounterReader {
615 recorded: Arc<Mutex<Vec<u32>>>,
616 }
617
618 impl RuntimePlugin for CounterReader {
619 fn priority(&self) -> i32 { phase::POST_SIM }
620 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
621 if let Some(&v) = ctx.resources.get::<u32>() {
622 self.recorded.lock().unwrap().push(v);
623 }
624 }
625 }
626
627 #[test]
628 fn test_resource_shared_across_plugins_same_frame() {
629 let recorded = Arc::new(Mutex::new(Vec::<u32>::new()));
632 let mut runtime = ViewportRuntime::new()
633 .with_plugin(CounterWriter { value: 42 })
634 .with_plugin(CounterReader { recorded: recorded.clone() });
635
636 let camera = Camera::default();
637 let input = ActionFrame::default();
638 let mut scene = Scene::new();
639 let mut sel = Selection::new();
640 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
641
642 let vals = recorded.lock().unwrap();
643 assert_eq!(vals.as_slice(), &[42], "reader must see value written by writer in the same frame");
644 }
645
646 #[test]
647 fn test_resource_persists_across_frames() {
648 let mut runtime = ViewportRuntime::new()
650 .with_plugin(CounterWriter { value: 10 });
651
652 let camera = Camera::default();
653 let input = ActionFrame::default();
654 let mut scene = Scene::new();
655 let mut sel = Selection::new();
656
657 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
658 assert!(runtime.resources().contains::<u32>(), "resource must persist after step");
659 }
660
661 #[test]
662 fn test_runtime_works_without_resources() {
663 let mut runtime = ViewportRuntime::new();
665 let camera = Camera::default();
666 let input = ActionFrame::default();
667 let mut scene = Scene::new();
668 let mut sel = Selection::new();
669 let output = runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0));
670 assert!(output.contact_events.is_empty());
671 assert!(output.node_transform_ops.is_empty());
672 }
673
674 #[derive(Debug, PartialEq)]
678 struct GameplayEvent { id: u32 }
679
680 #[derive(Debug, PartialEq)]
681 struct DiagnosticsEvent { frame_ms: f32 }
682
683 struct GameplayEmitter { count: u32 }
685
686 impl RuntimePlugin for GameplayEmitter {
687 fn priority(&self) -> i32 { phase::SIMULATE }
688 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
689 for i in 0..self.count {
690 ctx.output.events.emit(GameplayEvent { id: i });
691 }
692 }
693 }
694
695 struct DiagnosticsEmitter;
697
698 impl RuntimePlugin for DiagnosticsEmitter {
699 fn priority(&self) -> i32 { phase::POST_SIM }
700 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
701 ctx.output.events.emit(DiagnosticsEvent { frame_ms: ctx.dt * 1000.0 });
702 }
703 }
704
705 struct GameplayReader {
707 seen: Arc<Mutex<Vec<u32>>>,
708 }
709
710 impl RuntimePlugin for GameplayReader {
711 fn priority(&self) -> i32 { phase::WRITEBACK }
712 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
713 for ev in ctx.output.events.read::<GameplayEvent>() {
714 self.seen.lock().unwrap().push(ev.id);
715 }
716 }
717 }
718
719 fn run_one_frame(runtime: &mut ViewportRuntime) -> RuntimeOutput {
720 let camera = Camera::default();
721 let input = ActionFrame::default();
722 let mut scene = Scene::new();
723 let mut sel = Selection::new();
724 runtime.step(&mut scene, &mut sel, &make_frame(&camera, &input, 1.0 / 60.0))
725 }
726
727 #[test]
728 fn test_event_bus_emit_and_read() {
729 let mut bus = RuntimeEventBus::new();
730 bus.emit(GameplayEvent { id: 1 });
731 bus.emit(GameplayEvent { id: 2 });
732 let ids: Vec<u32> = bus.read::<GameplayEvent>().map(|e| e.id).collect();
733 assert_eq!(ids, vec![1, 2]);
734 }
735
736 #[test]
737 fn test_event_bus_typed_isolation() {
738 let mut bus = RuntimeEventBus::new();
740 bus.emit(GameplayEvent { id: 42 });
741 assert!(!bus.has::<DiagnosticsEvent>());
742 assert_eq!(bus.count::<DiagnosticsEvent>(), 0);
743 let diag: Vec<_> = bus.read::<DiagnosticsEvent>().collect();
744 assert!(diag.is_empty());
745 }
746
747 #[test]
748 fn test_event_bus_drain() {
749 let mut bus = RuntimeEventBus::new();
750 bus.emit(GameplayEvent { id: 7 });
751 bus.emit(GameplayEvent { id: 8 });
752 let drained = bus.drain::<GameplayEvent>();
753 assert_eq!(drained, vec![GameplayEvent { id: 7 }, GameplayEvent { id: 8 }]);
754 assert!(bus.drain::<GameplayEvent>().is_empty());
756 assert!(!bus.has::<GameplayEvent>());
757 }
758
759 #[test]
760 fn test_event_bus_has_and_count() {
761 let mut bus = RuntimeEventBus::new();
762 assert!(!bus.has::<GameplayEvent>());
763 assert_eq!(bus.count::<GameplayEvent>(), 0);
764 bus.emit(GameplayEvent { id: 1 });
765 bus.emit(GameplayEvent { id: 2 });
766 assert!(bus.has::<GameplayEvent>());
767 assert_eq!(bus.count::<GameplayEvent>(), 2);
768 }
769
770 #[test]
771 fn test_event_bus_is_empty() {
772 let mut bus = RuntimeEventBus::new();
773 assert!(bus.is_empty());
774 bus.emit(GameplayEvent { id: 0 });
775 assert!(!bus.is_empty());
776 }
777
778 #[test]
779 fn test_events_emitted_by_plugin_visible_in_output() {
780 let mut runtime = ViewportRuntime::new()
781 .with_plugin(GameplayEmitter { count: 3 });
782 let output = run_one_frame(&mut runtime);
783 assert_eq!(output.events.count::<GameplayEvent>(), 3);
784 let ids: Vec<u32> = output.events.read::<GameplayEvent>().map(|e| e.id).collect();
785 assert_eq!(ids, vec![0, 1, 2]);
786 }
787
788 #[test]
789 fn test_events_typed_routing_across_plugins() {
790 let seen = Arc::new(Mutex::new(Vec::<u32>::new()));
793 let mut runtime = ViewportRuntime::new()
794 .with_plugin(GameplayEmitter { count: 2 })
795 .with_plugin(GameplayReader { seen: seen.clone() });
796 run_one_frame(&mut runtime);
797 let ids = seen.lock().unwrap();
798 assert_eq!(ids.as_slice(), &[0, 1]);
799 }
800
801 #[test]
802 fn test_multiple_event_types_do_not_interfere() {
803 let mut runtime = ViewportRuntime::new()
805 .with_plugin(GameplayEmitter { count: 2 })
806 .with_plugin(DiagnosticsEmitter);
807 let output = run_one_frame(&mut runtime);
808 assert_eq!(output.events.count::<GameplayEvent>(), 2);
809 assert_eq!(output.events.count::<DiagnosticsEvent>(), 1);
810 }
811
812 #[test]
813 fn test_events_cleared_each_frame() {
814 let mut runtime = ViewportRuntime::new()
816 .with_plugin(GameplayEmitter { count: 1 });
817 run_one_frame(&mut runtime);
818 let output2 = run_one_frame(&mut runtime);
819 assert_eq!(output2.events.count::<GameplayEvent>(), 1);
821 }
822
823 #[test]
824 fn test_existing_output_fields_unaffected() {
825 let mut runtime = ViewportRuntime::new();
828 let output = run_one_frame(&mut runtime);
829 assert!(output.contact_events.is_empty());
830 assert!(output.selection_ops.is_empty());
831 assert!(output.node_transform_ops.is_empty());
832 assert!(output.camera_follow_target.is_none());
833 assert!(output.events.is_empty());
834 }
835
836 use crate::runtime::jobs::{JobPoll, JobSlot};
839
840 #[test]
841 fn test_job_slot_empty_by_default() {
842 let slot: JobSlot<u32> = JobSlot::empty();
843 assert!(slot.is_empty());
844 assert!(!slot.is_pending());
845 assert!(!slot.is_ready());
846 }
847
848 #[test]
849 fn test_job_slot_pending_after_new() {
850 let (slot, _sender) = JobSlot::<u32>::new();
851 assert!(slot.is_pending());
852 assert!(!slot.is_empty());
853 assert!(!slot.is_ready());
854 }
855
856 #[test]
857 fn test_job_handoff_complete() {
858 let (mut slot, sender) = JobSlot::<u32>::new();
859 assert!(slot.is_pending());
860 sender.complete(42);
861 assert!(slot.is_ready());
862 match slot.take() {
863 JobPoll::Ready(v) => assert_eq!(v, 42),
864 _ => panic!("expected Ready, got other variant"),
865 }
866 assert!(slot.is_empty());
868 }
869
870 #[test]
871 fn test_job_handoff_fail() {
872 let (mut slot, sender) = JobSlot::<u32>::new();
873 sender.fail("disk error");
874 match slot.take() {
875 JobPoll::Failed(msg) => assert_eq!(msg, "disk error"),
876 _ => panic!("expected Failed"),
877 }
878 assert!(slot.is_empty());
879 }
880
881 #[test]
882 fn test_job_handoff_cancel() {
883 let (mut slot, sender) = JobSlot::<u32>::new();
884 sender.cancel();
885 match slot.take() {
886 JobPoll::Cancelled => {}
887 _ => panic!("expected Cancelled"),
888 }
889 assert!(slot.is_empty());
890 }
891
892 #[test]
893 fn test_job_sender_drop_cancels() {
894 let (mut slot, sender) = JobSlot::<u32>::new();
895 drop(sender);
896 match slot.take() {
897 JobPoll::Cancelled => {}
898 _ => panic!("expected Cancelled after sender drop"),
899 }
900 assert!(slot.is_empty());
901 }
902
903 #[test]
904 fn test_job_take_on_empty_returns_empty() {
905 let mut slot: JobSlot<u32> = JobSlot::empty();
906 match slot.take() {
907 JobPoll::Empty => {}
908 _ => panic!("expected Empty"),
909 }
910 }
911
912 #[test]
913 fn test_job_take_while_pending_returns_pending() {
914 let (mut slot, _sender) = JobSlot::<u32>::new();
915 match slot.take() {
916 JobPoll::Pending => {}
917 _ => panic!("expected Pending"),
918 }
919 assert!(slot.is_pending());
921 }
922
923 #[test]
924 fn test_job_async_handoff_across_frames() {
925 struct LoaderPlugin {
927 slot: JobSlot<u32>,
928 result: Arc<Mutex<Option<u32>>>,
929 }
930
931 impl RuntimePlugin for LoaderPlugin {
932 fn priority(&self) -> i32 { phase::PREPARE }
933
934 fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
935 if self.slot.is_empty() {
936 let (new_slot, sender) = JobSlot::new();
937 self.slot = new_slot;
938 sender.complete(99);
940 }
941 }
942
943 fn collect(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
944 if let JobPoll::Ready(v) = self.slot.take() {
945 *self.result.lock().unwrap() = Some(v);
946 }
947 }
948
949 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
950 }
951
952 let result = Arc::new(Mutex::new(None::<u32>));
953 let mut runtime = ViewportRuntime::new()
954 .with_plugin(LoaderPlugin { slot: JobSlot::empty(), result: result.clone() });
955
956 run_one_frame(&mut runtime);
957 assert_eq!(*result.lock().unwrap(), Some(99));
958 }
959
960 #[test]
961 fn test_job_staged_resource_update_ordering() {
962 struct Integrator {
967 slot: JobSlot<u32>,
968 }
969
970 impl RuntimePlugin for Integrator {
971 fn priority(&self) -> i32 { phase::PREPARE } fn submit(&mut self, _ctx: &RuntimeStepContext<'_>) {
974 if self.slot.is_empty() {
975 let (s, sender) = JobSlot::new();
976 self.slot = s;
977 sender.complete(77);
978 }
979 }
980
981 fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {
982 if let JobPoll::Ready(v) = self.slot.take() {
983 ctx.resources.insert(v);
984 }
985 }
986
987 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
988 }
989
990 struct Reader {
991 seen: Arc<Mutex<Option<u32>>>,
992 }
993
994 impl RuntimePlugin for Reader {
995 fn priority(&self) -> i32 { phase::POST_SIM } fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {
998 *self.seen.lock().unwrap() = ctx.resources.get::<u32>().copied();
999 }
1000
1001 fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {}
1002 }
1003
1004 let seen = Arc::new(Mutex::new(None::<u32>));
1005 let mut runtime = ViewportRuntime::new()
1006 .with_plugin(Integrator { slot: JobSlot::empty() })
1007 .with_plugin(Reader { seen: seen.clone() });
1008
1009 run_one_frame(&mut runtime);
1010 assert_eq!(*seen.lock().unwrap(), Some(77));
1012 }
1013
1014 #[test]
1015 fn test_job_slot_in_resources() {
1016 let (slot, sender) = JobSlot::<u32>::new();
1018 let mut res = RuntimeResources::new();
1019 res.insert(slot);
1020 sender.complete(55);
1021 let found = res.get_mut::<JobSlot<u32>>().unwrap();
1022 match found.take() {
1023 JobPoll::Ready(v) => assert_eq!(v, 55),
1024 _ => panic!("expected Ready"),
1025 }
1026 }
1027
1028 use crate::resources::SkinWeights;
1031 use crate::runtime::plugins::skeleton_plugin::{
1032 Joint, JointMatrices, Pose, Skeleton, SkeletonPlugin, apply_skin,
1033 };
1034
1035 fn two_joint_skeleton() -> Skeleton {
1036 Skeleton::new(vec![
1037 Joint {
1038 name: "root".into(),
1039 parent: None,
1040 inverse_bind: glam::Affine3A::IDENTITY,
1041 },
1042 Joint {
1043 name: "child".into(),
1044 parent: Some(0),
1045 inverse_bind: glam::Affine3A::from_translation(-glam::Vec3::Y),
1046 },
1047 ])
1048 }
1049
1050 fn single_vertex_weights(joint: u8, weight: f32) -> SkinWeights {
1051 SkinWeights {
1052 joint_indices: vec![[joint, 0, 0, 0]],
1053 joint_weights: vec![[weight, 0.0, 0.0, 0.0]],
1054 }
1055 }
1056
1057 #[test]
1058 fn test_skeleton_joint_count() {
1059 let sk = two_joint_skeleton();
1060 assert_eq!(sk.joint_count(), 2);
1061 }
1062
1063 #[test]
1064 fn test_skeleton_find_joint() {
1065 let sk = two_joint_skeleton();
1066 assert_eq!(sk.find_joint("root"), Some(0));
1067 assert_eq!(sk.find_joint("child"), Some(1));
1068 assert_eq!(sk.find_joint("missing"), None);
1069 }
1070
1071 #[test]
1072 fn test_pose_identity_transforms() {
1073 let pose = Pose::identity(3);
1074 assert_eq!(pose.joint_count(), 3);
1075 for t in &pose.local_transforms {
1076 assert_eq!(*t, glam::Affine3A::IDENTITY);
1077 }
1078 }
1079
1080 #[test]
1081 fn test_joint_matrices_single_root_identity() {
1082 let sk = Skeleton::new(vec![Joint {
1083 name: "root".into(),
1084 parent: None,
1085 inverse_bind: glam::Affine3A::IDENTITY,
1086 }]);
1087 let pose = Pose::identity(1);
1088 let mats = JointMatrices::compute(&sk, &pose);
1089 let m = mats.as_slice()[0];
1090 assert!(m.matrix3.col(0).abs_diff_eq(glam::Vec3A::X, 1e-5));
1092 assert!(m.translation.abs_diff_eq(glam::Vec3A::ZERO, 1e-5));
1093 }
1094
1095 #[test]
1096 fn test_joint_matrices_parent_child_chain() {
1097 let sk = two_joint_skeleton();
1103 let mut pose = Pose::identity(2);
1104 pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::X);
1105 let mats = JointMatrices::compute(&sk, &pose);
1106 let child = mats.as_slice()[1];
1107 let expected_translation = glam::Vec3A::new(1.0, -1.0, 0.0);
1108 assert!(
1109 child.translation.abs_diff_eq(expected_translation, 1e-5),
1110 "child translation was {:?}, expected {:?}",
1111 child.translation,
1112 expected_translation,
1113 );
1114 }
1115
1116 #[test]
1117 fn test_apply_skin_identity_pose() {
1118 let sk = Skeleton::new(vec![Joint {
1119 name: "root".into(),
1120 parent: None,
1121 inverse_bind: glam::Affine3A::IDENTITY,
1122 }]);
1123 let pose = Pose::identity(1);
1124 let mats = JointMatrices::compute(&sk, &pose);
1125
1126 let positions = vec![[1.0f32, 2.0, 3.0]];
1127 let normals = vec![[0.0f32, 1.0, 0.0]];
1128 let weights = single_vertex_weights(0, 1.0);
1129 let (out_pos, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1130
1131 assert!((out_pos[0][0] - 1.0).abs() < 1e-5);
1132 assert!((out_pos[0][1] - 2.0).abs() < 1e-5);
1133 assert!((out_pos[0][2] - 3.0).abs() < 1e-5);
1134 assert!((out_nrm[0][1] - 1.0).abs() < 1e-5);
1135 }
1136
1137 #[test]
1138 fn test_apply_skin_single_joint_full_weight() {
1139 let sk = Skeleton::new(vec![Joint {
1141 name: "root".into(),
1142 parent: None,
1143 inverse_bind: glam::Affine3A::IDENTITY,
1144 }]);
1145 let mut pose = Pose::identity(1);
1146 pose.local_transforms[0] = glam::Affine3A::from_translation(glam::Vec3::new(5.0, 0.0, 0.0));
1147 let mats = JointMatrices::compute(&sk, &pose);
1148
1149 let positions = vec![[0.0f32, 0.0, 0.0]];
1150 let normals = vec![[1.0f32, 0.0, 0.0]];
1151 let weights = single_vertex_weights(0, 1.0);
1152 let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1153 assert!((out_pos[0][0] - 5.0).abs() < 1e-5);
1154 assert!(out_pos[0][1].abs() < 1e-5);
1155 }
1156
1157 #[test]
1158 fn test_apply_skin_two_joint_blend() {
1159 let sk = Skeleton::new(vec![
1162 Joint { name: "a".into(), parent: None, inverse_bind: glam::Affine3A::IDENTITY },
1163 Joint { name: "b".into(), parent: None, inverse_bind: glam::Affine3A::IDENTITY },
1164 ]);
1165 let mut pose = Pose::identity(2);
1166 pose.local_transforms[1] = glam::Affine3A::from_translation(glam::Vec3::new(10.0, 0.0, 0.0));
1167 let mats = JointMatrices::compute(&sk, &pose);
1168
1169 let positions = vec![[0.0f32, 0.0, 0.0]];
1170 let normals = vec![[1.0f32, 0.0, 0.0]];
1171 let weights = SkinWeights {
1172 joint_indices: vec![[0, 1, 0, 0]],
1173 joint_weights: vec![[0.5, 0.5, 0.0, 0.0]],
1174 };
1175 let (out_pos, _) = apply_skin(&positions, &normals, &weights, &mats);
1176 assert!((out_pos[0][0] - 5.0).abs() < 1e-5, "expected 5.0, got {}", out_pos[0][0]);
1177 }
1178
1179 #[test]
1180 fn test_apply_skin_normal_renormalized() {
1181 let sk = Skeleton::new(vec![Joint {
1183 name: "root".into(),
1184 parent: None,
1185 inverse_bind: glam::Affine3A::IDENTITY,
1186 }]);
1187 let mut pose = Pose::identity(1);
1188 pose.local_transforms[0] = glam::Affine3A::from_scale(glam::Vec3::splat(2.0));
1189 let mats = JointMatrices::compute(&sk, &pose);
1190
1191 let positions = vec![[1.0f32, 0.0, 0.0]];
1192 let normals = vec![[1.0f32, 0.0, 0.0]];
1193 let weights = single_vertex_weights(0, 1.0);
1194 let (_, out_nrm) = apply_skin(&positions, &normals, &weights, &mats);
1195 let len = (out_nrm[0][0].powi(2) + out_nrm[0][1].powi(2) + out_nrm[0][2].powi(2)).sqrt();
1196 assert!((len - 1.0).abs() < 1e-5, "normal length was {len}");
1197 }
1198
1199 #[test]
1200 fn test_skeleton_plugin_writes_update_to_output() {
1201 let sk = Skeleton::new(vec![Joint {
1202 name: "root".into(),
1203 parent: None,
1204 inverse_bind: glam::Affine3A::IDENTITY,
1205 }]);
1206 let positions = vec![[0.0f32, 0.0, 0.0]];
1207 let normals = vec![[0.0f32, 1.0, 0.0]];
1208 let weights = single_vertex_weights(0, 1.0);
1209 let mesh_id = crate::resources::mesh_store::MeshId(0);
1210
1211 let mut runtime = ViewportRuntime::new()
1212 .with_plugin(SkeletonPlugin::new(sk, mesh_id, positions, normals, weights));
1213
1214 runtime.resources_mut().insert(Pose::identity(1));
1216
1217 let output = run_one_frame(&mut runtime);
1218 assert_eq!(output.skinned_mesh_updates.len(), 1);
1219 assert_eq!(output.skinned_mesh_updates[0].mesh_id, mesh_id);
1220 }
1221
1222 #[test]
1223 fn test_skinned_mesh_updates_empty_without_pose() {
1224 let sk = Skeleton::new(vec![Joint {
1225 name: "root".into(),
1226 parent: None,
1227 inverse_bind: glam::Affine3A::IDENTITY,
1228 }]);
1229 let mesh_id = crate::resources::mesh_store::MeshId(0);
1230 let mut runtime = ViewportRuntime::new()
1231 .with_plugin(SkeletonPlugin::new(
1232 sk,
1233 mesh_id,
1234 vec![[0.0f32; 3]],
1235 vec![[0.0f32, 1.0, 0.0]],
1236 single_vertex_weights(0, 1.0),
1237 ));
1238 let output = run_one_frame(&mut runtime);
1240 assert!(output.skinned_mesh_updates.is_empty());
1241 }
1242
1243 #[test]
1244 fn test_skinned_mesh_updates_cleared_each_frame() {
1245 let sk = Skeleton::new(vec![Joint {
1246 name: "root".into(),
1247 parent: None,
1248 inverse_bind: glam::Affine3A::IDENTITY,
1249 }]);
1250 let mesh_id = crate::resources::mesh_store::MeshId(0);
1251 let mut runtime = ViewportRuntime::new()
1252 .with_plugin(SkeletonPlugin::new(
1253 sk,
1254 mesh_id,
1255 vec![[0.0f32; 3]],
1256 vec![[0.0f32, 1.0, 0.0]],
1257 single_vertex_weights(0, 1.0),
1258 ));
1259 runtime.resources_mut().insert(Pose::identity(1));
1260
1261 run_one_frame(&mut runtime);
1262 let output2 = run_one_frame(&mut runtime);
1263 assert_eq!(output2.skinned_mesh_updates.len(), 1);
1265 }
1266}
1267
1268pub struct ViewportRuntime {
1283 mode: SceneRuntimeMode,
1284 plugins: Vec<Box<dyn RuntimePlugin>>,
1285 gpu_plugins: Vec<Box<dyn GpuPlugin>>,
1286 gpu_initialized: bool,
1289 fixed_timestep: Option<FixedTimestep>,
1290 snapshots: TransformSnapshotTable,
1291 step_index: u64,
1292 selection_system: Option<SelectionSystem>,
1293 manipulation_system: Option<ManipulationSystem>,
1294 camera_follow: Option<CameraFollow>,
1295 resources: RuntimeResources,
1297 prev_node_ids: std::collections::HashSet<crate::interaction::selection::NodeId>,
1299 scene_initialized: bool,
1301}
1302
1303impl Default for ViewportRuntime {
1304 fn default() -> Self {
1305 Self::new()
1306 }
1307}
1308
1309impl ViewportRuntime {
1310 pub fn new() -> Self {
1312 Self {
1313 mode: SceneRuntimeMode::default(),
1314 plugins: Vec::new(),
1315 gpu_plugins: Vec::new(),
1316 gpu_initialized: false,
1317 fixed_timestep: None,
1318 snapshots: TransformSnapshotTable::new(),
1319 step_index: 0,
1320 selection_system: None,
1321 manipulation_system: None,
1322 camera_follow: None,
1323 resources: RuntimeResources::new(),
1324 prev_node_ids: std::collections::HashSet::new(),
1325 scene_initialized: false,
1326 }
1327 }
1328
1329 pub fn with_selection_system(mut self) -> Self {
1335 self.selection_system = Some(SelectionSystem::new());
1336 self
1337 }
1338
1339 pub fn with_manipulation_system(mut self) -> Self {
1345 self.manipulation_system = Some(ManipulationSystem::new());
1346 self
1347 }
1348
1349 pub fn is_manipulating(&self) -> bool {
1353 self.manipulation_system
1354 .as_ref()
1355 .map_or(false, |m| m.is_active())
1356 }
1357
1358 pub fn manipulation_system(&self) -> Option<&ManipulationSystem> {
1360 self.manipulation_system.as_ref()
1361 }
1362
1363 pub fn with_mode(mut self, mode: SceneRuntimeMode) -> Self {
1365 self.mode = mode;
1366 self
1367 }
1368
1369 pub fn with_plugin(mut self, plugin: impl RuntimePlugin) -> Self {
1372 self.plugins.push(Box::new(plugin));
1373 self
1374 }
1375
1376 pub fn with_gpu_plugin(mut self, plugin: impl GpuPlugin) -> Self {
1385 self.gpu_plugins.push(Box::new(plugin));
1386 self.gpu_initialized = false;
1387 self
1388 }
1389
1390 pub fn with_gpu_plugin_for_viewport(
1402 mut self,
1403 viewport: crate::renderer::ViewportId,
1404 plugin: impl GpuPlugin,
1405 ) -> Self {
1406 let scoped = gpu_plugin::ViewportScopedPlugin {
1407 viewport,
1408 inner: plugin,
1409 };
1410 self.gpu_plugins.push(Box::new(scoped));
1411 self.gpu_initialized = false;
1412 self
1413 }
1414
1415 pub fn notify_device_recreated(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
1424 for p in self.gpu_plugins.iter_mut() {
1425 p.on_device_recreated(device, queue);
1426 }
1427 self.gpu_initialized = false;
1428 }
1429
1430 pub fn pre_prepare(
1449 &mut self,
1450 device: &wgpu::Device,
1451 queue: &wgpu::Queue,
1452 ctx: &GpuFrameContext<'_>,
1453 ) -> Vec<wgpu::CommandBuffer> {
1454 if !self.gpu_initialized {
1455 for p in self.gpu_plugins.iter_mut() {
1456 p.init_gpu(device);
1457 }
1458 self.gpu_initialized = true;
1459 }
1460 self.gpu_plugins.sort_by_key(|p| p.priority());
1463
1464 let mut out = Vec::new();
1465 for p in self.gpu_plugins.iter_mut() {
1466 out.extend(p.pre_prepare(device, queue, ctx));
1467 }
1468 out
1469 }
1470
1471 pub fn post_paint(
1484 &mut self,
1485 device: &wgpu::Device,
1486 queue: &wgpu::Queue,
1487 targets: &PostPaintTargets<'_>,
1488 ctx: &GpuFrameContext<'_>,
1489 ) -> Vec<wgpu::CommandBuffer> {
1490 self.gpu_plugins.sort_by_key(|p| p.priority());
1494
1495 let mut out = Vec::new();
1496 for p in self.gpu_plugins.iter_mut() {
1497 out.extend(p.post_paint(device, queue, targets, ctx));
1498 }
1499 out
1500 }
1501
1502 pub fn with_fixed_timestep(mut self, ts: FixedTimestep) -> Self {
1508 self.fixed_timestep = Some(ts);
1509 self
1510 }
1511
1512 pub fn set_fixed_timestep(&mut self, ts: FixedTimestep) {
1517 self.fixed_timestep = Some(ts);
1518 }
1519
1520 pub fn clear_fixed_timestep(&mut self) {
1522 self.fixed_timestep = None;
1523 }
1524
1525 pub fn mode(&self) -> SceneRuntimeMode {
1527 self.mode
1528 }
1529
1530 pub fn snapshots(&self) -> &TransformSnapshotTable {
1535 &self.snapshots
1536 }
1537
1538 pub fn resources(&self) -> &RuntimeResources {
1543 &self.resources
1544 }
1545
1546 pub fn resources_mut(&mut self) -> &mut RuntimeResources {
1551 &mut self.resources
1552 }
1553
1554 pub fn alpha(&self) -> f32 {
1559 self.fixed_timestep.as_ref().map_or(1.0, |ts| ts.alpha())
1560 }
1561
1562 pub fn step_index(&self) -> u64 {
1567 self.step_index
1568 }
1569
1570 pub fn with_camera_follow(mut self, follow: CameraFollow) -> Self {
1575 self.camera_follow = Some(follow);
1576 self
1577 }
1578
1579 pub fn set_camera_follow(&mut self, follow: CameraFollow) {
1581 self.camera_follow = Some(follow);
1582 }
1583
1584 pub fn clear_camera_follow(&mut self) {
1587 self.camera_follow = None;
1588 }
1589
1590 pub fn camera_follow(&self) -> Option<&CameraFollow> {
1592 self.camera_follow.as_ref()
1593 }
1594
1595 pub fn step(
1606 &mut self,
1607 scene: &mut Scene,
1608 selection: &mut Selection,
1609 frame: &RuntimeFrameContext,
1610 ) -> RuntimeOutput {
1611 let mut output = RuntimeOutput::default();
1612 let mut writeback = TransformWriteback::default();
1613
1614 let current_ids: std::collections::HashSet<crate::interaction::selection::NodeId> =
1617 scene.nodes().map(|n| n.id()).collect();
1618
1619 if self.scene_initialized {
1620 let mut events: Vec<RuntimeEvent> = Vec::new();
1622 for &id in current_ids.difference(&self.prev_node_ids) {
1623 events.push(RuntimeEvent::NodeAdded(id));
1624 }
1625 for &id in self.prev_node_ids.difference(¤t_ids) {
1626 events.push(RuntimeEvent::NodeRemoved(id));
1627 }
1628
1629 if !events.is_empty() {
1630 let mut plugins = std::mem::take(&mut self.plugins);
1632 for event in &events {
1633 for plugin in plugins.iter_mut() {
1634 let mut ctx = RuntimeStepContext {
1635 priority: plugin.priority(),
1636 dt: frame.dt,
1637 scene,
1638 writeback: &mut writeback,
1639 output: &mut output,
1640 pick_hit: frame.pick_hit,
1641 resources: &mut self.resources,
1642 };
1643 plugin.on_event(event, &mut ctx);
1644 }
1645 }
1646 self.plugins = plugins;
1647 }
1648 } else {
1649 self.scene_initialized = true;
1650 }
1651 self.prev_node_ids = current_ids;
1652
1653 self.plugins.sort_by_key(|p| p.priority());
1655
1656 let mut plugins = std::mem::take(&mut self.plugins);
1658
1659 for plugin in plugins.iter_mut() {
1661 let ctx = RuntimeStepContext {
1662 priority: plugin.priority(),
1663 dt: frame.dt,
1664 scene,
1665 writeback: &mut writeback,
1666 output: &mut output,
1667 pick_hit: frame.pick_hit,
1668 resources: &mut self.resources,
1669 };
1670 plugin.submit(&ctx);
1671 }
1672
1673 run_range(
1677 &mut plugins,
1678 i32::MIN,
1679 phase::SELECT,
1680 frame.dt,
1681 frame,
1682 scene,
1683 &mut writeback,
1684 &mut output,
1685 &mut self.resources,
1686 );
1687
1688 if let Some(sel_sys) = &self.selection_system {
1690 sel_sys.step(frame, &mut output);
1691 }
1692 run_range(
1694 &mut plugins,
1695 phase::SELECT,
1696 phase::MANIPULATE,
1697 frame.dt,
1698 frame,
1699 scene,
1700 &mut writeback,
1701 &mut output,
1702 &mut self.resources,
1703 );
1704
1705 if self.manipulation_system.is_some() {
1707 let mut manip_sys = self.manipulation_system.take().unwrap();
1708 manip_sys.step(frame, scene, selection, &mut writeback, &mut output);
1709 self.manipulation_system = Some(manip_sys);
1710 }
1711 run_range(
1713 &mut plugins,
1714 phase::MANIPULATE,
1715 phase::SIMULATE,
1716 frame.dt,
1717 frame,
1718 scene,
1719 &mut writeback,
1720 &mut output,
1721 &mut self.resources,
1722 );
1723
1724 if self.fixed_timestep.is_some() {
1726 let mut ts = self.fixed_timestep.take().unwrap();
1727 for step_dt in ts.advance(frame.dt) {
1728 run_range(
1729 &mut plugins,
1730 phase::SIMULATE,
1731 phase::POST_SIM,
1732 step_dt,
1733 frame,
1734 scene,
1735 &mut writeback,
1736 &mut output,
1737 &mut self.resources,
1738 );
1739 self.step_index = self.step_index.wrapping_add(1);
1740 }
1741 self.fixed_timestep = Some(ts);
1742 } else {
1743 run_range(
1744 &mut plugins,
1745 phase::SIMULATE,
1746 phase::POST_SIM,
1747 frame.dt,
1748 frame,
1749 scene,
1750 &mut writeback,
1751 &mut output,
1752 &mut self.resources,
1753 );
1754 self.step_index = self.step_index.wrapping_add(1);
1755 }
1756
1757 run_range(
1759 &mut plugins,
1760 phase::POST_SIM,
1761 i32::MAX,
1762 frame.dt,
1763 frame,
1764 scene,
1765 &mut writeback,
1766 &mut output,
1767 &mut self.resources,
1768 );
1769
1770 for plugin in plugins.iter_mut() {
1772 let mut ctx = RuntimeStepContext {
1773 priority: plugin.priority(),
1774 dt: frame.dt,
1775 scene,
1776 writeback: &mut writeback,
1777 output: &mut output,
1778 pick_hit: frame.pick_hit,
1779 resources: &mut self.resources,
1780 };
1781 plugin.collect(&mut ctx);
1782 }
1783
1784 self.plugins = plugins;
1786
1787 let ops = writeback.into_ops();
1789 for op in &ops {
1790 scene.set_local_transform(op.id, glam::Mat4::from(op.transform));
1791 self.snapshots.update(op.id, op.transform);
1792 }
1793 if !ops.is_empty() {
1794 scene.update_transforms();
1795 }
1796 output.node_transform_ops = ops;
1797
1798 for op in &output.selection_ops {
1800 op.apply_to(selection);
1801 }
1802
1803 if let Some(CameraFollow::Node { id, offset, .. }) = &self.camera_follow {
1805 let id = *id;
1806 let offset = *offset;
1807 let alpha = self.alpha();
1808 let pos = self
1809 .snapshots
1810 .interpolated(id, alpha)
1811 .map(|t| glam::Vec3::from(t.translation))
1812 .or_else(|| {
1813 scene
1814 .node(id)
1815 .map(|n| n.world_transform().col(3).truncate())
1816 });
1817 if let Some(pos) = pos {
1818 output.camera_follow_target = Some(pos + offset);
1819 }
1820 }
1821
1822 output
1823 }
1824}