Skip to main content

viewport_lib/runtime/
output.rs

1//! Runtime output types: transform ops, selection ops, contact events, and generic events.
2
3use super::events::RuntimeEventBus;
4use crate::camera::camera::{Camera, CameraTarget};
5use crate::interaction::select::selection::{NodeId, Selection};
6
7/// Write buffer for transform changes produced by plugins.
8///
9/// Passed into [`super::context::RuntimeStepContext`] so plugins can record
10/// transform changes without directly mutating the scene. The runtime flushes
11/// all ops to the scene after the `Writeback` phase.
12#[derive(Default)]
13pub struct TransformWriteback {
14    ops: Vec<NodeTransformOp>,
15}
16
17impl TransformWriteback {
18    /// Record a new local-space transform for a scene node.
19    ///
20    /// For physics-driven nodes with no parent, local space equals world space.
21    /// If the same node is written more than once, all ops are kept and applied
22    /// in order (last write wins after scene propagation). The full transform,
23    /// including scale embedded in the `Affine3A`, replaces the node's current
24    /// local transform.
25    pub fn set(&mut self, id: NodeId, transform: glam::Affine3A) {
26        self.ops.push(NodeTransformOp {
27            id,
28            transform,
29            preserve_scale: false,
30        });
31    }
32
33    /// Record a transform that should preserve the node's existing scale.
34    ///
35    /// The rotation and translation from `transform` are applied; the scale
36    /// component (if any) is ignored and the scene node's current scale is
37    /// kept. Use this for physics or animation writebacks: those systems don't
38    /// model scale, so they shouldn't clobber a scale the user set
39    /// independently (e.g. for visual stretching, ellipsoid bones, or
40    /// art-time unit conversion).
41    pub fn set_preserve_scale(&mut self, id: NodeId, transform: glam::Affine3A) {
42        self.ops.push(NodeTransformOp {
43            id,
44            transform,
45            preserve_scale: true,
46        });
47    }
48
49    /// Consume the buffer and return all recorded ops.
50    pub(super) fn into_ops(self) -> Vec<NodeTransformOp> {
51        self.ops
52    }
53}
54
55/// A transform write targeting one scene node.
56#[derive(Debug, Clone)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct NodeTransformOp {
59    /// Target scene node.
60    pub id: NodeId,
61    /// New local-space transform for the node. For physics-driven root nodes,
62    /// this is the world-space transform.
63    pub transform: glam::Affine3A,
64    /// When `true`, the runtime flush replaces only the rotation and
65    /// translation, keeping the node's existing scale. Set via
66    /// [`TransformWriteback::set_preserve_scale`].
67    #[cfg_attr(feature = "serde", serde(default))]
68    pub preserve_scale: bool,
69}
70
71/// A change to the selection state.
72///
73/// Produced by runtime plugins and returned in [`RuntimeOutput::selection_ops`].
74/// The runtime applies these to the app-owned [`Selection`] during each step.
75#[derive(Debug, Clone)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub enum SelectionOp {
78    /// Clear all selected nodes and select one.
79    SelectOne(NodeId),
80    /// Toggle a node's selected state.
81    Toggle(NodeId),
82    /// Add a node to the selection.
83    Add(NodeId),
84    /// Remove a node from the selection.
85    Remove(NodeId),
86    /// Add multiple nodes to the selection.
87    Extend(Vec<NodeId>),
88    /// Clear the selection.
89    Clear,
90    /// Replace the selection with the given set.
91    SelectAll(Vec<NodeId>),
92}
93
94impl SelectionOp {
95    /// Apply this operation to a [`Selection`].
96    pub fn apply_to(&self, selection: &mut Selection) {
97        match self {
98            SelectionOp::SelectOne(id) => selection.select_one(*id),
99            SelectionOp::Toggle(id) => selection.toggle(*id),
100            SelectionOp::Add(id) => selection.add(*id),
101            SelectionOp::Remove(id) => selection.remove(*id),
102            SelectionOp::Extend(ids) => selection.extend(ids.iter().copied()),
103            SelectionOp::Clear => selection.clear(),
104            SelectionOp::SelectAll(ids) => selection.select_all(ids.iter().copied()),
105        }
106    }
107}
108
109/// A camera state change produced by a runtime plugin.
110///
111/// Emitted by plugins onto [`RuntimeOutput::events`]; apply with
112/// [`apply_camera_commands`].
113///
114/// Commands are applied in the order they were emitted (plugin priority order,
115/// then registration order within the same priority). Each command builds on the
116/// result of the previous one in the same frame.
117///
118/// This is independent of [`super::CameraFollow`]: camera-follow targets and
119/// `CameraCommand`s coexist and the app decides which to apply.
120#[derive(Debug, Clone)]
121pub enum CameraCommand {
122    /// Set the orbit center (pivot point) to an absolute world position.
123    SetCenter(glam::Vec3),
124    /// Add a world-space delta to the orbit center.
125    OffsetCenter(glam::Vec3),
126    /// Set the camera distance from the center. Clamped to a small positive value.
127    SetDistance(f32),
128    /// Set the camera orientation.
129    SetOrientation(glam::Quat),
130    /// Blend center, distance, and orientation toward a target state.
131    ///
132    /// `weight` is in `[0, 1]`. At `1.0` the camera snaps to `target` immediately.
133    /// Smaller values produce smooth motion when emitted every frame.
134    BlendToward {
135        /// Target camera state to blend toward.
136        target: CameraTarget,
137        /// Blend weight in `[0, 1]`.
138        weight: f32,
139    },
140}
141
142/// Suggested camera center computed from the active [`super::CameraFollow`]
143/// binding. Emitted onto [`RuntimeOutput::events`] when a follow target is
144/// resolved this step. Apply to `camera.center` for orbit-camera follow
145/// behavior.
146#[derive(Debug, Clone, Copy)]
147pub struct CameraFollowTarget(pub glam::Vec3);
148
149/// Apply every [`CameraCommand`] currently in the event bus to `camera`, in
150/// emission order. Drains the bus.
151///
152/// Commands are applied sequentially: each one builds on the result of the
153/// previous. A `BlendToward` command blends from whatever state the camera is
154/// in after all prior commands, not from the frame-start state.
155pub fn apply_camera_commands(events: &mut RuntimeEventBus, camera: &mut Camera) {
156    for cmd in events.drain::<CameraCommand>() {
157        match cmd {
158            CameraCommand::SetCenter(c) => {
159                camera.center = c;
160            }
161            CameraCommand::OffsetCenter(d) => {
162                camera.center += d;
163            }
164            CameraCommand::SetDistance(d) => {
165                camera.set_distance(d);
166            }
167            CameraCommand::SetOrientation(q) => {
168                camera.orientation = q.normalize();
169            }
170            CameraCommand::BlendToward { target, weight } => {
171                let w = weight.clamp(0.0, 1.0);
172                camera.center = camera.center.lerp(target.center, w);
173                camera.distance = camera.distance + (target.distance - camera.distance) * w;
174                camera.distance = camera.distance.max(0.001);
175                camera.orientation = camera
176                    .orientation
177                    .slerp(target.orientation.normalize(), w)
178                    .normalize();
179            }
180        }
181    }
182}
183
184/// Output produced by one call to [`super::ViewportRuntime::step`].
185///
186/// `node_transform_ops` have already been applied to the scene and the
187/// snapshot table when this is returned; `selection_ops` are applied to the
188/// `Selection` argument. Everything else flows through the typed `events`
189/// bus: plugins emit via `ctx.output.events.emit(MyEvent { .. })`, and the
190/// app reads them after `step()` via `output.events.read::<MyEvent>()` or
191/// `output.events.drain::<MyEvent>()`.
192#[derive(Default)]
193#[non_exhaustive]
194pub struct RuntimeOutput {
195    /// Transform ops applied to the scene during this step.
196    pub node_transform_ops: Vec<NodeTransformOp>,
197    /// Selection changes produced by runtime plugins, already applied to the
198    /// app-owned [`Selection`].
199    pub selection_ops: Vec<SelectionOp>,
200    /// Generic typed event bus. Events are cleared each frame because
201    /// `RuntimeOutput` is constructed fresh on every `step` call.
202    pub events: RuntimeEventBus,
203}