Lightningbeam/lightningbeam-ui/lightningbeam-core/src/selection.rs

424 lines
13 KiB
Rust

//! Selection state management
//!
//! Tracks selected shape instances, clip instances, and shapes for editing operations.
use crate::shape::Shape;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use vello::kurbo::BezPath;
/// Selection state for the editor
///
/// Maintains sets of selected shape instances, clip instances, and shapes.
/// This is separate from the document to make it easy to
/// pass around for UI rendering without needing mutable access.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Selection {
/// Currently selected shape instances
selected_shape_instances: Vec<Uuid>,
/// Currently selected shapes (definitions)
selected_shapes: Vec<Uuid>,
/// Currently selected clip instances
selected_clip_instances: Vec<Uuid>,
}
impl Selection {
/// Create a new empty selection
pub fn new() -> Self {
Self {
selected_shape_instances: Vec::new(),
selected_shapes: Vec::new(),
selected_clip_instances: Vec::new(),
}
}
/// Add a shape instance to the selection
pub fn add_shape_instance(&mut self, id: Uuid) {
if !self.selected_shape_instances.contains(&id) {
self.selected_shape_instances.push(id);
}
}
/// Add a shape definition to the selection
pub fn add_shape(&mut self, id: Uuid) {
if !self.selected_shapes.contains(&id) {
self.selected_shapes.push(id);
}
}
/// Remove a shape instance from the selection
pub fn remove_shape_instance(&mut self, id: &Uuid) {
self.selected_shape_instances.retain(|&x| x != *id);
}
/// Remove a shape definition from the selection
pub fn remove_shape(&mut self, id: &Uuid) {
self.selected_shapes.retain(|&x| x != *id);
}
/// Toggle a shape instance's selection state
pub fn toggle_shape_instance(&mut self, id: Uuid) {
if self.contains_shape_instance(&id) {
self.remove_shape_instance(&id);
} else {
self.add_shape_instance(id);
}
}
/// Toggle a shape's selection state
pub fn toggle_shape(&mut self, id: Uuid) {
if self.contains_shape(&id) {
self.remove_shape(&id);
} else {
self.add_shape(id);
}
}
/// Add a clip instance to the selection
pub fn add_clip_instance(&mut self, id: Uuid) {
if !self.selected_clip_instances.contains(&id) {
self.selected_clip_instances.push(id);
}
}
/// Remove a clip instance from the selection
pub fn remove_clip_instance(&mut self, id: &Uuid) {
self.selected_clip_instances.retain(|&x| x != *id);
}
/// Toggle a clip instance's selection state
pub fn toggle_clip_instance(&mut self, id: Uuid) {
if self.contains_clip_instance(&id) {
self.remove_clip_instance(&id);
} else {
self.add_clip_instance(id);
}
}
/// Clear all selections
pub fn clear(&mut self) {
self.selected_shape_instances.clear();
self.selected_shapes.clear();
self.selected_clip_instances.clear();
}
/// Clear only object selections
pub fn clear_shape_instances(&mut self) {
self.selected_shape_instances.clear();
}
/// Clear only shape selections
pub fn clear_shapes(&mut self) {
self.selected_shapes.clear();
}
/// Clear only clip instance selections
pub fn clear_clip_instances(&mut self) {
self.selected_clip_instances.clear();
}
/// Check if an object is selected
pub fn contains_shape_instance(&self, id: &Uuid) -> bool {
self.selected_shape_instances.contains(id)
}
/// Check if a shape is selected
pub fn contains_shape(&self, id: &Uuid) -> bool {
self.selected_shapes.contains(id)
}
/// Check if a clip instance is selected
pub fn contains_clip_instance(&self, id: &Uuid) -> bool {
self.selected_clip_instances.contains(id)
}
/// Check if selection is empty
pub fn is_empty(&self) -> bool {
self.selected_shape_instances.is_empty()
&& self.selected_shapes.is_empty()
&& self.selected_clip_instances.is_empty()
}
/// Get the selected objects
pub fn shape_instances(&self) -> &[Uuid] {
&self.selected_shape_instances
}
/// Get the selected shapes
pub fn shapes(&self) -> &[Uuid] {
&self.selected_shapes
}
/// Get the number of selected objects
pub fn shape_instance_count(&self) -> usize {
self.selected_shape_instances.len()
}
/// Get the number of selected shapes
pub fn shape_count(&self) -> usize {
self.selected_shapes.len()
}
/// Get the selected clip instances
pub fn clip_instances(&self) -> &[Uuid] {
&self.selected_clip_instances
}
/// Get the number of selected clip instances
pub fn clip_instance_count(&self) -> usize {
self.selected_clip_instances.len()
}
/// Set selection to a single object (clears previous selection)
pub fn select_only_shape_instance(&mut self, id: Uuid) {
self.clear();
self.add_shape_instance(id);
}
/// Set selection to a single shape (clears previous selection)
pub fn select_only_shape(&mut self, id: Uuid) {
self.clear();
self.add_shape(id);
}
/// Set selection to a single clip instance (clears previous selection)
pub fn select_only_clip_instance(&mut self, id: Uuid) {
self.clear();
self.add_clip_instance(id);
}
/// Set selection to multiple objects (clears previous selection)
pub fn select_shape_instances(&mut self, ids: &[Uuid]) {
self.clear_shape_instances();
for &id in ids {
self.add_shape_instance(id);
}
}
/// Set selection to multiple shapes (clears previous selection)
pub fn select_shapes(&mut self, ids: &[Uuid]) {
self.clear_shapes();
for &id in ids {
self.add_shape(id);
}
}
/// Set selection to multiple clip instances (clears previous selection)
pub fn select_clip_instances(&mut self, ids: &[Uuid]) {
self.clear_clip_instances();
for &id in ids {
self.add_clip_instance(id);
}
}
}
/// Represents a temporary region-based split of shapes.
///
/// When a region select is active, shapes that cross the region boundary
/// are temporarily split into "inside" and "outside" parts. The inside
/// parts are selected. If the user performs an operation, the split is
/// committed; if they deselect, the original shapes are restored.
#[derive(Clone, Debug)]
pub struct RegionSelection {
/// The clipping region as a closed BezPath (polygon or rect)
pub region_path: BezPath,
/// Layer containing the affected shapes
pub layer_id: Uuid,
/// Keyframe time
pub time: f64,
/// Per-shape split results
pub splits: Vec<ShapeSplit>,
/// Shape IDs that were fully inside the region (not split, just selected)
pub fully_inside_ids: Vec<Uuid>,
/// Whether the split has been committed (via an operation on the selection)
pub committed: bool,
}
/// One shape's split result from a region selection
#[derive(Clone, Debug)]
pub struct ShapeSplit {
/// The original shape (stored for reverting)
pub original_shape: Shape,
/// UUID for the "inside" portion shape
pub inside_shape_id: Uuid,
/// The clipped path inside the region
pub inside_path: BezPath,
/// UUID for the "outside" portion shape
pub outside_shape_id: Uuid,
/// The clipped path outside the region
pub outside_path: BezPath,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_selection_creation() {
let selection = Selection::new();
assert!(selection.is_empty());
assert_eq!(selection.shape_instance_count(), 0);
assert_eq!(selection.shape_count(), 0);
}
#[test]
fn test_add_remove_objects() {
let mut selection = Selection::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
selection.add_shape_instance(id1);
assert_eq!(selection.shape_instance_count(), 1);
assert!(selection.contains_shape_instance(&id1));
selection.add_shape_instance(id2);
assert_eq!(selection.shape_instance_count(), 2);
selection.remove_shape_instance(&id1);
assert_eq!(selection.shape_instance_count(), 1);
assert!(!selection.contains_shape_instance(&id1));
assert!(selection.contains_shape_instance(&id2));
}
#[test]
fn test_toggle() {
let mut selection = Selection::new();
let id = Uuid::new_v4();
selection.toggle_shape_instance(id);
assert!(selection.contains_shape_instance(&id));
selection.toggle_shape_instance(id);
assert!(!selection.contains_shape_instance(&id));
}
#[test]
fn test_select_only() {
let mut selection = Selection::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
selection.add_shape_instance(id1);
selection.add_shape_instance(id2);
assert_eq!(selection.shape_instance_count(), 2);
selection.select_only_shape_instance(id1);
assert_eq!(selection.shape_instance_count(), 1);
assert!(selection.contains_shape_instance(&id1));
assert!(!selection.contains_shape_instance(&id2));
}
#[test]
fn test_clear() {
let mut selection = Selection::new();
selection.add_shape_instance(Uuid::new_v4());
selection.add_shape(Uuid::new_v4());
assert!(!selection.is_empty());
selection.clear();
assert!(selection.is_empty());
}
#[test]
fn test_add_remove_clip_instances() {
let mut selection = Selection::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
selection.add_clip_instance(id1);
assert_eq!(selection.clip_instance_count(), 1);
assert!(selection.contains_clip_instance(&id1));
selection.add_clip_instance(id2);
assert_eq!(selection.clip_instance_count(), 2);
selection.remove_clip_instance(&id1);
assert_eq!(selection.clip_instance_count(), 1);
assert!(!selection.contains_clip_instance(&id1));
assert!(selection.contains_clip_instance(&id2));
}
#[test]
fn test_toggle_clip_instance() {
let mut selection = Selection::new();
let id = Uuid::new_v4();
selection.toggle_clip_instance(id);
assert!(selection.contains_clip_instance(&id));
selection.toggle_clip_instance(id);
assert!(!selection.contains_clip_instance(&id));
}
#[test]
fn test_select_only_clip_instance() {
let mut selection = Selection::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
selection.add_clip_instance(id1);
selection.add_clip_instance(id2);
assert_eq!(selection.clip_instance_count(), 2);
selection.select_only_clip_instance(id1);
assert_eq!(selection.clip_instance_count(), 1);
assert!(selection.contains_clip_instance(&id1));
assert!(!selection.contains_clip_instance(&id2));
}
#[test]
fn test_clear_clip_instances() {
let mut selection = Selection::new();
selection.add_clip_instance(Uuid::new_v4());
selection.add_clip_instance(Uuid::new_v4());
selection.add_shape_instance(Uuid::new_v4());
assert_eq!(selection.clip_instance_count(), 2);
assert_eq!(selection.shape_instance_count(), 1);
selection.clear_clip_instances();
assert_eq!(selection.clip_instance_count(), 0);
assert_eq!(selection.shape_instance_count(), 1);
}
#[test]
fn test_clip_instances_getter() {
let mut selection = Selection::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
selection.add_clip_instance(id1);
selection.add_clip_instance(id2);
let clip_instances = selection.clip_instances();
assert_eq!(clip_instances.len(), 2);
assert!(clip_instances.contains(&id1));
assert!(clip_instances.contains(&id2));
}
#[test]
fn test_mixed_selection() {
let mut selection = Selection::new();
let shape_instance_id = Uuid::new_v4();
let clip_instance_id = Uuid::new_v4();
selection.add_shape_instance(shape_instance_id);
selection.add_clip_instance(clip_instance_id);
assert_eq!(selection.shape_instance_count(), 1);
assert_eq!(selection.clip_instance_count(), 1);
assert!(!selection.is_empty());
selection.clear_shape_instances();
assert_eq!(selection.shape_instance_count(), 0);
assert_eq!(selection.clip_instance_count(), 1);
assert!(!selection.is_empty());
selection.clear();
assert!(selection.is_empty());
}
}