424 lines
13 KiB
Rust
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());
|
|
}
|
|
}
|