Skip to main content

scenix_scene/
editor.rs

1use alloc::{string::String, vec::Vec};
2
3use scenix_core::NodeId;
4use scenix_math::Vec3;
5
6/// A bit mask used to filter scene interaction layers.
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct LayerMask(u32);
10
11impl LayerMask {
12    /// No layers.
13    pub const NONE: Self = Self(0);
14    /// Every layer bit.
15    pub const ALL: Self = Self(u32::MAX);
16
17    /// Creates a mask from raw bits.
18    #[inline]
19    pub const fn from_bits(bits: u32) -> Self {
20        Self(bits)
21    }
22
23    /// Returns raw mask bits.
24    #[inline]
25    pub const fn bits(self) -> u32 {
26        self.0
27    }
28
29    /// Returns whether any bits overlap.
30    #[inline]
31    pub const fn intersects(self, other: Self) -> bool {
32        self.0 & other.0 != 0
33    }
34
35    /// Returns whether this mask includes the node's layer mask.
36    #[inline]
37    pub const fn matches_node(self, node_layers: u32) -> bool {
38        self.0 & node_layers != 0
39    }
40
41    /// Adds bits to the mask.
42    #[inline]
43    pub fn insert(&mut self, other: Self) {
44        self.0 |= other.0;
45    }
46
47    /// Removes bits from the mask.
48    #[inline]
49    pub fn remove(&mut self, other: Self) {
50        self.0 &= !other.0;
51    }
52}
53
54/// Layer filters applied by editor interactions.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct LayerPolicy {
58    /// Layers eligible for selection.
59    pub selectable: LayerMask,
60    /// Layers eligible for dragging.
61    pub draggable: LayerMask,
62    /// Layers eligible for transform tools.
63    pub transformable: LayerMask,
64}
65
66impl Default for LayerPolicy {
67    fn default() -> Self {
68        Self {
69            selectable: LayerMask::ALL,
70            draggable: LayerMask::ALL,
71            transformable: LayerMask::ALL,
72        }
73    }
74}
75
76/// Sparse editor-only metadata associated with a scene node.
77#[derive(Clone, Debug, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct NodeEditorMetadata {
80    /// Node can be selected.
81    pub selectable: bool,
82    /// Node can be moved by drag tools.
83    pub draggable: bool,
84    /// Node can be modified by transform tools.
85    pub transformable: bool,
86    /// Node is protected from editor mutations.
87    pub locked: bool,
88    /// Node appears in inspector snapshots.
89    pub visible_in_inspector: bool,
90    /// Optional editor label overriding the scene name.
91    pub label: Option<String>,
92    /// Application-defined editor tags.
93    pub tags: Vec<String>,
94}
95
96impl Default for NodeEditorMetadata {
97    fn default() -> Self {
98        Self {
99            selectable: true,
100            draggable: true,
101            transformable: true,
102            locked: false,
103            visible_in_inspector: true,
104            label: None,
105            tags: Vec::new(),
106        }
107    }
108}
109
110/// How a selection command combines with the existing selection.
111#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
113pub enum SelectionMode {
114    /// Replace the complete selection.
115    #[default]
116    Replace,
117    /// Add the node if absent.
118    Add,
119    /// Toggle membership.
120    Toggle,
121    /// Remove the node if present.
122    Remove,
123}
124
125/// Current graph-local editor selection.
126#[derive(Clone, Debug, Default, PartialEq, Eq)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128pub struct SelectionState {
129    /// Node under the pointer, if any.
130    pub hovered: Option<NodeId>,
131    /// Node receiving the active interaction, if any.
132    pub active: Option<NodeId>,
133    selected: Vec<NodeId>,
134}
135
136impl SelectionState {
137    /// Selected IDs in ascending deterministic order.
138    #[inline]
139    pub fn selected(&self) -> &[NodeId] {
140        &self.selected
141    }
142
143    /// Returns whether a node is selected.
144    #[inline]
145    pub fn contains(&self, id: NodeId) -> bool {
146        self.selected.binary_search(&id).is_ok()
147    }
148}
149
150/// Exact selection differences produced by one operation.
151#[derive(Clone, Debug, Default, PartialEq, Eq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct SelectionChange {
154    /// IDs added by the operation.
155    pub added: Vec<NodeId>,
156    /// IDs removed by the operation.
157    pub removed: Vec<NodeId>,
158}
159
160/// Active transform tool.
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub enum TransformMode {
164    /// Move nodes.
165    #[default]
166    Translate,
167    /// Rotate nodes.
168    Rotate,
169    /// Scale nodes.
170    Scale,
171}
172
173/// Coordinate system used by a transform tool.
174#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
176pub enum TransformSpace {
177    /// World axes.
178    #[default]
179    World,
180    /// Node-local axes.
181    Local,
182}
183
184/// Axis or plane constraint used by a transform operation.
185#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
187pub enum TransformConstraint {
188    /// No axis constraint.
189    #[default]
190    Free,
191    /// X axis.
192    X,
193    /// Y axis.
194    Y,
195    /// Z axis.
196    Z,
197    /// XY plane.
198    XY,
199    /// XZ plane.
200    XZ,
201    /// YZ plane.
202    YZ,
203}
204
205impl TransformConstraint {
206    /// Returns a component mask for translation and scale constraints.
207    pub const fn component_mask(self) -> Vec3 {
208        match self {
209            Self::Free => Vec3::ONE,
210            Self::X => Vec3::X,
211            Self::Y => Vec3::Y,
212            Self::Z => Vec3::Z,
213            Self::XY => Vec3::new(1.0, 1.0, 0.0),
214            Self::XZ => Vec3::new(1.0, 0.0, 1.0),
215            Self::YZ => Vec3::new(0.0, 1.0, 1.0),
216        }
217    }
218
219    /// Returns the single constrained axis, when applicable.
220    pub const fn axis(self) -> Option<Vec3> {
221        match self {
222            Self::X => Some(Vec3::X),
223            Self::Y => Some(Vec3::Y),
224            Self::Z => Some(Vec3::Z),
225            _ => None,
226        }
227    }
228}
229
230/// Quantization increments for editor transforms. Zero disables a component.
231#[derive(Clone, Copy, Debug, Default, PartialEq)]
232#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
233pub struct SnapSettings {
234    /// Translation increments per axis.
235    pub translation: Vec3,
236    /// Rotation increment in radians.
237    pub rotation_radians: f32,
238    /// Scale increments per axis.
239    pub scale: Vec3,
240}
241
242impl SnapSettings {
243    /// Quantizes a translation relative to the operation start.
244    pub fn snap_translation(self, delta: Vec3) -> Vec3 {
245        snap_vec3(delta, self.translation)
246    }
247
248    /// Quantizes a rotation angle relative to the operation start.
249    pub fn snap_rotation(self, radians: f32) -> f32 {
250        snap_scalar(radians, self.rotation_radians)
251    }
252
253    /// Quantizes a scale delta relative to the operation start.
254    pub fn snap_scale(self, delta: Vec3) -> Vec3 {
255        snap_vec3(delta, self.scale)
256    }
257}
258
259fn snap_scalar(value: f32, increment: f32) -> f32 {
260    if increment.is_finite() && increment.abs() > 1.0e-6 {
261        let increment = increment.abs();
262        let scaled = value / increment;
263        if !scaled.is_finite() {
264            return value;
265        }
266        let rounded = if scaled >= 0.0 {
267            (scaled + 0.5) as i64
268        } else {
269            (scaled - 0.5) as i64
270        };
271        rounded as f32 * increment
272    } else {
273        value
274    }
275}
276
277fn snap_vec3(value: Vec3, increments: Vec3) -> Vec3 {
278    Vec3::new(
279        snap_scalar(value.x, increments.x),
280        snap_scalar(value.y, increments.y),
281        snap_scalar(value.z, increments.z),
282    )
283}
284
285pub(crate) fn apply_selection(
286    state: &mut SelectionState,
287    id: NodeId,
288    mode: SelectionMode,
289) -> SelectionChange {
290    let previous = state.selected.clone();
291    match mode {
292        SelectionMode::Replace => {
293            state.selected.clear();
294            state.selected.push(id);
295        }
296        SelectionMode::Add => {
297            if let Err(index) = state.selected.binary_search(&id) {
298                state.selected.insert(index, id);
299            }
300        }
301        SelectionMode::Toggle => match state.selected.binary_search(&id) {
302            Ok(index) => {
303                state.selected.remove(index);
304            }
305            Err(index) => state.selected.insert(index, id),
306        },
307        SelectionMode::Remove => {
308            if let Ok(index) = state.selected.binary_search(&id) {
309                state.selected.remove(index);
310            }
311        }
312    }
313    selection_diff(&previous, &state.selected)
314}
315
316pub(crate) fn replace_selection(
317    state: &mut SelectionState,
318    mut selected: Vec<NodeId>,
319) -> SelectionChange {
320    selected.sort_unstable();
321    selected.dedup();
322    let previous = core::mem::replace(&mut state.selected, selected);
323    selection_diff(&previous, &state.selected)
324}
325
326fn selection_diff(previous: &[NodeId], current: &[NodeId]) -> SelectionChange {
327    SelectionChange {
328        added: current
329            .iter()
330            .copied()
331            .filter(|id| previous.binary_search(id).is_err())
332            .collect(),
333        removed: previous
334            .iter()
335            .copied()
336            .filter(|id| current.binary_search(id).is_err())
337            .collect(),
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn snap_settings_quantize_relative_deltas() {
347        let snap = SnapSettings {
348            translation: Vec3::new(0.5, 1.0, 0.0),
349            rotation_radians: 0.25,
350            scale: Vec3::new(0.1, 0.0, 0.5),
351        };
352        assert_eq!(
353            snap.snap_translation(Vec3::new(0.74, 1.6, 0.3)),
354            Vec3::new(0.5, 2.0, 0.3)
355        );
356        assert_eq!(snap.snap_rotation(0.38), 0.5);
357    }
358}