vector-editor-core 0.4.1

Core structures for editing point based structures
Documentation
//! Interaction state for editing a [`Layer`] of point objects.
//!
//! [`LayerEditor`] tracks which point is currently being placed and whether
//! snapping is active. It is render- and backend-agnostic: positions are passed
//! in already snapped by the caller, and the grab radius is given as a scalar,
//! so this module depends on no vector or rendering crate.

use crate::Layer;

use inner_space::{InnerSpace, VectorSpace};

use std::ops::Sub;

/// Identifies a single point: the object within a layer and the point within it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PointIndex {
    /// Index of the object within the layer.
    pub object: usize,
    /// Index of the point within the object.
    pub point: usize,
}

/// Editing state for a single layer.
///
/// Holds whether snapping is enabled and which point is currently being placed.
#[derive(Default)]
pub struct LayerEditor {
    /// Whether snapping is enabled. The caller reads this to decide whether to
    /// snap positions before passing them in, and to render a grid.
    pub snapping: bool,
    editing: Option<PointIndex>,
}

impl LayerEditor {
    /// Creates a new editor with snapping disabled and nothing being edited.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Returns the point currently being placed, if any.
    #[must_use]
    pub const fn editing(&self) -> Option<PointIndex> {
        self.editing
    }

    /// Toggles snapping.
    pub const fn toggle_snapping(&mut self) {
        self.snapping = !self.snapping;
    }

    /// Stops placing the current point.
    pub const fn finish(&mut self) {
        self.editing = None;
    }

    /// Moves the point currently being placed to `position`.
    ///
    /// Does nothing if no point is being placed. `position` is expected to be
    /// already snapped by the caller.
    pub fn update<P: Copy, O>(&self, layer: &mut Layer<P, O>, position: P) {
        let Some(index) = self.editing else {
            return;
        };
        layer.objects[index.object].points[index.point] = position;
    }

    /// Places a point at the cursor.
    ///
    /// - While placing an object, inserts a new point after the current one.
    /// - Otherwise grabs the nearest existing point within `grab_distance` of
    ///   `grab_position`.
    /// - Otherwise starts a new object with `options` at `placement`.
    ///
    /// `placement` is the (snapped) position for new points; `grab_position` is
    /// the (raw) cursor used only for hit-testing existing points.
    pub fn select<P, O>(
        &mut self,
        layer: &mut Layer<P, O>,
        placement: P,
        grab_position: P,
        grab_distance: <P::Output as VectorSpace>::Scalar,
        options: O,
    ) where
        P: Copy + Sub,
        P::Output: InnerSpace,
        <P::Output as VectorSpace>::Scalar: PartialOrd + Copy,
    {
        if let Some(index) = &mut self.editing {
            index.point += 1;
            layer.objects[index.object]
                .points
                .insert(index.point, placement);
            return;
        }

        let mut nearest: Option<(PointIndex, <P::Output as VectorSpace>::Scalar)> = None;
        for (object, ground) in layer.objects.iter().enumerate() {
            if let Some((point, current)) = ground.nearest(grab_position, grab_distance)
                && nearest.is_none_or(|(_, last)| current < last)
            {
                nearest = Some((PointIndex { object, point }, current));
            }
        }

        if let Some((index, _)) = nearest {
            self.editing = Some(index);
            return;
        }

        let object = layer.objects.len();
        layer
            .objects
            .push(crate::PointObject::new(vec![placement, placement], options));
        self.editing = Some(PointIndex { object, point: 1 });
    }

    /// Removes the last placed point, or the whole object if only its initial
    /// point remains, or the last object when not currently placing.
    pub fn try_undo<P, O>(&mut self, layer: &mut Layer<P, O>) {
        let Some(index) = &mut self.editing else {
            layer.objects.pop();
            return;
        };

        let ground = &mut layer.objects[index.object];
        if index.point > 1 {
            ground.points.remove(index.point);
            index.point -= 1;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{LayerEditor, PointIndex};
    use crate::Layer;

    #[test]
    fn select_extend_grab_undo() {
        let mut editor = LayerEditor::new();
        let mut layer: Layer<f32, ()> = Layer::new();

        editor.select(&mut layer, 0.0, 0.0, 0.5, ());
        assert_eq!(layer.objects.len(), 1);
        assert_eq!(layer.objects[0].points, vec![0.0, 0.0]);
        assert_eq!(
            editor.editing(),
            Some(PointIndex {
                object: 0,
                point: 1
            })
        );

        editor.update(&mut layer, 5.0);
        assert_eq!(layer.objects[0].points[1], 5.0);

        editor.select(&mut layer, 10.0, 10.0, 0.5, ());
        assert_eq!(layer.objects[0].points, vec![0.0, 5.0, 10.0]);
        assert_eq!(
            editor.editing(),
            Some(PointIndex {
                object: 0,
                point: 2
            })
        );

        editor.try_undo(&mut layer);
        assert_eq!(layer.objects[0].points, vec![0.0, 5.0]);

        editor.finish();
        editor.select(&mut layer, 100.0, 5.1, 0.5, ());
        assert_eq!(layer.objects.len(), 1);
        assert_eq!(
            editor.editing(),
            Some(PointIndex {
                object: 0,
                point: 1
            })
        );
    }
}