maps-engine-rust 0.1.0

Zero-dependency map engine core in Rust — Web Mercator projection, tiles, geofencing, clustering, navigation math.
Documentation
//! Drawing tools with undo/redo history. Port of `draw.ts` + `edit.ts`.

use crate::geo::LonLat;

/// A drawable shape.
#[derive(Debug, Clone, PartialEq)]
pub enum Shape {
    Marker(LonLat),
    Polyline(Vec<LonLat>),
    Polygon(Vec<LonLat>),
    Circle { center: LonLat, radius_m: f64 },
    Rectangle { sw: LonLat, ne: LonLat },
}

impl Shape {
    /// Number of vertices (markers/circles count as 1).
    pub fn vertex_count(&self) -> usize {
        match self {
            Shape::Marker(_) => 1,
            Shape::Polyline(v) | Shape::Polygon(v) => v.len(),
            Shape::Circle { .. } => 1,
            Shape::Rectangle { .. } => 2,
        }
    }
}

/// Drawing session holding shapes plus undo/redo stacks.
#[derive(Debug, Default)]
pub struct DrawSession {
    shapes: Vec<Shape>,
    undo_stack: Vec<DrawOp>,
    redo_stack: Vec<DrawOp>,
}

#[derive(Debug, Clone)]
enum DrawOp {
    Add { index: usize },
    Remove { index: usize, shape: Shape },
    Clear { shapes: Vec<Shape> },
}

impl DrawSession {
    pub fn new() -> Self {
        DrawSession::default()
    }

    pub fn shapes(&self) -> &[Shape] {
        &self.shapes
    }

    /// Add a shape; clears the redo stack.
    pub fn add(&mut self, shape: Shape) {
        let index = self.shapes.len();
        self.shapes.push(shape);
        self.undo_stack.push(DrawOp::Add { index });
        self.redo_stack.clear();
    }

    /// Remove shape at `index`.
    pub fn remove(&mut self, index: usize) -> bool {
        if index >= self.shapes.len() {
            return false;
        }
        let shape = self.shapes.remove(index);
        self.undo_stack.push(DrawOp::Remove { index, shape });
        self.redo_stack.clear();
        true
    }

    /// Remove all shapes.
    pub fn clear(&mut self) {
        if self.shapes.is_empty() {
            return;
        }
        let shapes = std::mem::take(&mut self.shapes);
        self.undo_stack.push(DrawOp::Clear { shapes });
        self.redo_stack.clear();
    }

    pub fn can_undo(&self) -> bool {
        !self.undo_stack.is_empty()
    }
    pub fn can_redo(&self) -> bool {
        !self.redo_stack.is_empty()
    }

    /// Undo the last operation. Returns false if nothing to undo.
    pub fn undo(&mut self) -> bool {
        let op = match self.undo_stack.pop() {
            Some(op) => op,
            None => return false,
        };
        match op {
            DrawOp::Add { index } => {
                if index < self.shapes.len() {
                    let shape = self.shapes.remove(index);
                    self.redo_stack.push(DrawOp::Remove { index, shape });
                }
            }
            DrawOp::Remove { index, shape } => {
                let idx = index.min(self.shapes.len());
                self.shapes.insert(idx, shape);
                self.redo_stack.push(DrawOp::Add { index: idx });
            }
            DrawOp::Clear { shapes } => {
                let current = std::mem::replace(&mut self.shapes, shapes);
                self.redo_stack.push(DrawOp::Clear { shapes: current });
            }
        }
        true
    }

    /// Redo the last undone operation. Returns false if nothing to redo.
    pub fn redo(&mut self) -> bool {
        let op = match self.redo_stack.pop() {
            Some(op) => op,
            None => return false,
        };
        // Redo stack holds inverse ops recorded during undo():
        //   Remove{index, shape}  -> re-insert the shape
        //   Add{index}            -> remove the shape again
        //   Clear{shapes}         -> clear again
        match op {
            DrawOp::Remove { index, shape } => {
                let idx = index.min(self.shapes.len());
                self.shapes.insert(idx, shape);
                self.undo_stack.push(DrawOp::Add { index: idx });
            }
            DrawOp::Add { index } => {
                if index < self.shapes.len() {
                    let shape = self.shapes.remove(index);
                    self.undo_stack.push(DrawOp::Remove { index, shape });
                }
            }
            DrawOp::Clear { .. } => {
                let shapes = std::mem::take(&mut self.shapes);
                self.undo_stack.push(DrawOp::Clear { shapes });
            }
        }
        true
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn marker() -> Shape {
        Shape::Marker(LonLat::new(105.85, 21.02))
    }

    #[test]
    fn add_undo_redo() {
        let mut s = DrawSession::new();
        s.add(marker());
        assert_eq!(s.shapes().len(), 1);
        assert!(s.undo());
        assert_eq!(s.shapes().len(), 0);
        assert!(s.redo());
        assert_eq!(s.shapes().len(), 1);
        assert_eq!(s.shapes()[0], marker());
    }

    #[test]
    fn remove_undo_restores_position() {
        let mut s = DrawSession::new();
        s.add(marker());
        s.add(Shape::Marker(LonLat::new(106.0, 21.0)));
        assert!(s.remove(0));
        assert_eq!(s.shapes().len(), 1);
        assert!(s.undo());
        assert_eq!(s.shapes().len(), 2);
        assert_eq!(s.shapes()[0], marker());
    }

    #[test]
    fn clear_undo_restores_all() {
        let mut s = DrawSession::new();
        s.add(marker());
        s.add(marker());
        s.clear();
        assert!(s.shapes().is_empty());
        assert!(s.undo());
        assert_eq!(s.shapes().len(), 2);
    }

    #[test]
    fn new_action_clears_redo() {
        let mut s = DrawSession::new();
        s.add(marker());
        s.undo();
        assert!(s.can_redo());
        s.add(marker());
        assert!(!s.can_redo());
    }

    #[test]
    fn undo_empty_returns_false() {
        let mut s = DrawSession::new();
        assert!(!s.undo());
        assert!(!s.redo());
        assert!(!s.remove(5));
    }
}