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}