1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//! Plugin authoring guide.
//!
//! This module documents how to write a [`super::RuntimePlugin`] and register it
//! with [`super::ViewportRuntime`].
//!
//! # The RuntimePlugin trait
//!
//! A plugin is any type that implements [`super::RuntimePlugin`]:
//!
//! ```rust,ignore
//! pub trait RuntimePlugin: Send + 'static {
//! fn priority(&self) -> i32;
//! fn submit(&mut self, ctx: &RuntimeStepContext<'_>) {}
//! fn collect(&mut self, ctx: &mut RuntimeStepContext<'_>) {}
//! fn on_event(&mut self, event: &RuntimeEvent, ctx: &mut RuntimeStepContext<'_>) {}
//! fn step(&mut self, ctx: &mut RuntimeStepContext<'_>);
//! }
//! ```
//!
//! `step` is the only required method. The others have default no-op implementations.
//!
//! Hook call order each frame:
//!
//! 1. `on_event` -- called once per lifecycle event (node added or removed) before
//! the step loop begins. Only fired when the scene's node set changes.
//! 2. `submit` -- called once per frame before the step loop, in priority order.
//! Use this to kick off background work (e.g. spawning a compute task) that will
//! be collected on the same or next frame. The context is `&RuntimeStepContext`
//! (shared reference), so only `get` and `contains` on resources are available.
//! 3. `step` -- called once per phase execution, in priority order. For simulate-range
//! plugins with a fixed timestep this is called once per accumulated step
//! (possibly multiple times per frame). For all other plugins it is called once
//! per frame. This is where the main per-frame or per-step work happens.
//! 4. `collect` -- called once per frame after the step loop, in priority order.
//! Use this to read results from background work started in `submit`.
//!
//! # RuntimePhase ordering
//!
//! Phase constants are plain `i32` values in the [`super::plugin::phase`] module.
//! Plugins execute in ascending numeric order each frame:
//!
//! | Phase | Value | When it runs |
//! |--------------|-------|----------------------------------------------------|
//! | Prepare | 100 | First. Update time-dependent state before queries. |
//! | Pick | 200 | Ray-cast and object picking. |
//! | Select | 300 | Selection updates driven by pick results. |
//! | Manipulate | 400 | Gizmo drag and keyboard transform sessions. |
//! | Animate | 500 | Procedural or keyframe animation. |
//! | Simulate | 600 | Physics or simulation. Fixed timestep: runs N times per frame. |
//! | PostSimulate | 700 | After all Simulate iterations, before Writeback. |
//! | Writeback | 800 | Flush accumulated transform ops to the scene. |
//!
//! You can use values between bands to place a plugin at a specific sub-position.
//! For example `phase::ANIMATE + 50` runs after standard Animate plugins but before
//! any plugin at `phase::SIMULATE`. Two plugins at the same priority run in
//! registration order (stable sort).
//!
//! The Simulate phase is special: with a fixed timestep configured, plugins in the
//! `[SIMULATE, POST_SIM)` range may be called zero or more times per rendered frame
//! depending on how much wall time accumulated since the last frame. All other phase
//! ranges always run exactly once per frame.
//!
//! # Hook registration
//!
//! Build the runtime and add plugins with `with_plugin`:
//!
//! ```rust,ignore
//! use viewport_lib::runtime::{ViewportRuntime, FixedTimestep};
//!
//! let mut runtime = ViewportRuntime::new()
//! .with_fixed_timestep(FixedTimestep::new(60.0))
//! .with_plugin(MyPhysicsPlugin::new())
//! .with_plugin(MyAudioPlugin::new());
//! ```
//!
//! Multiple plugins can share the same priority. They run in registration order
//! within the same band. Registration order is preserved by a stable sort on
//! `priority()` at the start of each frame.
//!
//! Call `runtime.step(&mut scene, &mut selection, &frame_ctx)` once per frame to
//! drive all plugins. It returns a [`super::output::RuntimeOutput`] with selection
//! ops, contact events, transform ops, and any application-defined events.
//!
//! # Accessing scene data
//!
//! Each hook receives a [`super::context::RuntimeStepContext`] with the following fields:
//!
//! - `ctx.scene` -- read-only reference to the scene. Use this to query node
//! transforms, materials, and spatial structure.
//! - `ctx.writeback` -- write transforms here. Call `ctx.writeback.set(node_id, affine)`.
//! The runtime flushes accumulated writes to the scene after the writeback phase.
//! - `ctx.output` -- accumulate selection operations, contact events, camera commands,
//! and typed application events here.
//! - `ctx.resources` -- shared typed resource registry. Use this to pass data between
//! plugins within the same frame or across frames.
//! - `ctx.pick_hit` -- forwarded from `RuntimeFrameContext`. The pick result under the
//! cursor for this frame, if picking was done by the caller.
//! - `ctx.dt` -- delta time for this step. For simulate-range plugins with a fixed
//! timestep this is the fixed step size, not wall dt.
//!
//! # Resource sharing between plugins
//!
//! [`super::RuntimeResources`] is a typed map keyed by `TypeId`. Any `Send + 'static`
//! type can be stored as a resource. One value per type: inserting again overwrites.
//!
//! Typical pattern: one plugin writes, another reads in the same frame.
//!
//! ```rust,ignore
//! // PhysicsPlugin at SIMULATE inserts contact data.
//! fn step(&mut self, ctx: &mut RuntimeStepContext) {
//! let contacts = self.solver.solve(ctx.scene, ctx.dt);
//! ctx.resources.insert(contacts);
//! }
//!
//! // AudioPlugin at POST_SIM reads it.
//! fn step(&mut self, ctx: &mut RuntimeStepContext) {
//! if let Some(contacts) = ctx.resources.get::<ContactList>() {
//! for c in contacts.iter() {
//! self.audio.play_impact(c.impulse);
//! }
//! }
//! }
//! ```
//!
//! Resources persist across frames until explicitly removed or the runtime is dropped.
//! Use `runtime.resources_mut().insert(...)` before the first `step` to pre-populate
//! resources that plugins expect to find on frame one.
//!
//! Available methods on `RuntimeResources`:
//! - `insert(value)` -- store a value, replacing any existing value of that type
//! - `get::<T>()` -- shared reference, returns `None` if not present
//! - `get_mut::<T>()` -- mutable reference
//! - `remove::<T>()` -- take ownership and remove
//! - `contains::<T>()` -- presence check without borrowing the value
//!
//! In `submit` (called with `&RuntimeStepContext`), only `get` and `contains` are
//! accessible. In `step`, `collect`, and `on_event` (called with `&mut RuntimeStepContext`),
//! full insert/get_mut/remove access is available.
//!
//! # Debug visualization with DebugDraw
//!
//! [`super::DebugDraw`] is a resource, not a renderer type. Plugins draw to it by
//! calling `ctx.resources.get_mut::<DebugDraw>()` in their `step` implementations.
//! After the frame, the host converts the accumulated primitives into render items.
//!
//! Setup pattern:
//!
//! ```rust,ignore
//! use viewport_lib::runtime::debug_draw::DebugDraw;
//!
//! // At startup: insert a DebugDraw resource.
//! runtime.resources_mut().insert(DebugDraw::new());
//!
//! // Each frame, before runtime.step():
//! if let Some(dd) = runtime.resources_mut().get_mut::<DebugDraw>() {
//! dd.begin_frame(); // clears transient (one-frame) primitives
//! }
//! let output = runtime.step(&mut scene, &mut selection, &frame_ctx);
//!
//! // After step: convert accumulated primitives to render items.
//! if let Some(dd) = runtime.resources().get::<DebugDraw>() {
//! frame_data.scene.polylines.extend(dd.to_polylines());
//! if let Some(pc) = dd.to_point_cloud() {
//! frame_data.scene.point_clouds.push(pc);
//! }
//! }
//! ```
//!
//! Inside a plugin's `step`:
//!
//! ```rust,ignore
//! fn step(&mut self, ctx: &mut RuntimeStepContext) {
//! if let Some(dd) = ctx.resources.get_mut::<DebugDraw>() {
//! dd.line(contact.point_a, contact.point_b, [1.0, 0.0, 0.0, 1.0]);
//! }
//! }
//! ```
//!
//! Primitives submitted with `line`, `point`, `aabb`, `sphere`, and `label` are
//! transient: `begin_frame` clears them. Persistent primitives are added with
//! `add_persistent(key, prim)` keyed by a `u64` and stay until `remove_persistent(key)`
//! is called. See the [`super::debug_draw`] module for the full API.
//!
//! # Fixed timestep and the Simulate phase
//!
//! Configure a fixed timestep with `ViewportRuntime::with_fixed_timestep`:
//!
//! ```rust,ignore
//! let mut runtime = ViewportRuntime::new()
//! .with_fixed_timestep(FixedTimestep::new(60.0)); // 60 Hz
//! ```
//!
//! The runtime accumulates wall time. Each call to `runtime.step(...)` advances the
//! accumulator by `frame_ctx.dt`. If the accumulated time exceeds the step size, the
//! runtime calls `step()` on simulate-range plugins once per step. If wall dt is less
//! than the step size, `step()` may not be called at all for that frame.
//!
//! Inside simulate-range plugins, `ctx.dt` is the fixed step size (e.g. `1.0/60.0`),
//! not wall dt. This makes physics integration deterministic regardless of frame rate.
//!
//! For rendering, use `runtime.alpha()` to interpolate between the previous and current
//! simulation state:
//!
//! ```rust,ignore
//! let alpha = runtime.alpha();
//! if let Some(t) = runtime.snapshots().interpolated(node_id, alpha) {
//! // use t as the render transform instead of the scene node transform
//! }
//! ```
//!
//! `alpha` is in `[0.0, 1.0]`: the fractional position between the last completed step
//! and the next. This eliminates the jitter that appears when frame rate and step rate
//! are out of phase. When no fixed timestep is configured, `alpha()` returns `1.0`.
//!
//! # File layout for new plugins
//!
//! New plugins live in a subdirectory under `src/runtime/plugins/`, not as a single
//! file. The [`super::plugins::skeleton_plugin`] module is the reference template:
//!
//! ```text
//! plugins/
//! my_plugin/
//! mod.rs -- re-exports the public surface
//! plugin.rs -- the RuntimePlugin impl
//! <feature>.rs -- substrate types, math, helpers, internal state
//! ```
//!
//! Using a subdirectory from the start avoids a rename-and-rewire when the plugin
//! grows. Re-export all public types from `mod.rs` and add the module to
//! `src/runtime/plugins/mod.rs`.
//!
//! # Minimal plugin example
//!
//! A plugin that counts frames and logs every 60 steps:
//!
//! ```rust,ignore
//! use viewport_lib::runtime::{RuntimePlugin, RuntimeStepContext, ViewportRuntime};
//! use viewport_lib::runtime::plugin::phase;
//!
//! struct FrameCounter {
//! count: u64,
//! }
//!
//! impl FrameCounter {
//! fn new() -> Self {
//! Self { count: 0 }
//! }
//! }
//!
//! impl RuntimePlugin for FrameCounter {
//! fn priority(&self) -> i32 {
//! phase::PREPARE
//! }
//!
//! fn step(&mut self, _ctx: &mut RuntimeStepContext<'_>) {
//! self.count += 1;
//! if self.count % 60 == 0 {
//! println!("frame {}", self.count);
//! }
//! }
//! }
//!
//! // Registration:
//! let mut runtime = ViewportRuntime::new()
//! .with_plugin(FrameCounter::new());
//! ```
//!
//! This plugin runs once per frame at the Prepare phase. It owns its state as a
//! struct field. To share state with another plugin, insert it into `ctx.resources`
//! instead of keeping it in the struct.