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

524 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Selection state management
//!
//! Tracks selected DCEL elements (edges, faces, vertices) and clip instances for editing operations.
use crate::dcel::{Dcel, EdgeId, FaceId, VertexId};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use uuid::Uuid;
use vello::kurbo::{Affine, BezPath};
/// Shape of a raster pixel selection, in canvas pixel coordinates.
#[derive(Clone, Debug)]
pub enum RasterSelection {
/// Axis-aligned rectangle: (x0, y0, x1, y1), x1 >= x0, y1 >= y0.
Rect(i32, i32, i32, i32),
/// Closed freehand lasso polygon.
Lasso(Vec<(i32, i32)>),
}
impl RasterSelection {
/// Bounding box as (x0, y0, x1, y1).
pub fn bounding_rect(&self) -> (i32, i32, i32, i32) {
match self {
Self::Rect(x0, y0, x1, y1) => (*x0, *y0, *x1, *y1),
Self::Lasso(pts) => {
let x0 = pts.iter().map(|p| p.0).min().unwrap_or(0);
let y0 = pts.iter().map(|p| p.1).min().unwrap_or(0);
let x1 = pts.iter().map(|p| p.0).max().unwrap_or(0);
let y1 = pts.iter().map(|p| p.1).max().unwrap_or(0);
(x0, y0, x1, y1)
}
}
}
/// Returns true if the given canvas pixel is inside the selection.
pub fn contains_pixel(&self, px: i32, py: i32) -> bool {
match self {
Self::Rect(x0, y0, x1, y1) => px >= *x0 && px < *x1 && py >= *y0 && py < *y1,
Self::Lasso(pts) => point_in_polygon(px, py, pts),
}
}
}
/// Even-odd point-in-polygon test for integer coordinates.
fn point_in_polygon(px: i32, py: i32, polygon: &[(i32, i32)]) -> bool {
let n = polygon.len();
if n < 3 { return false; }
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = (polygon[i].0 as f64, polygon[i].1 as f64);
let (xj, yj) = (polygon[j].0 as f64, polygon[j].1 as f64);
let x = px as f64;
let y = py as f64;
if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
inside = !inside;
}
j = i;
}
inside
}
/// A pasted or cut selection that floats above the canvas until committed.
///
/// While a floating selection is alive `raw_pixels` on the target keyframe is
/// left in a "pre-composite" state (hole punched for cut, unchanged for copy).
/// The floating pixels are rendered as an overlay. Committing composites them
/// into `raw_pixels` and records a `RasterStrokeAction` for undo.
#[derive(Clone, Debug)]
pub struct RasterFloatingSelection {
/// sRGB-encoded premultiplied RGBA, width × height × 4 bytes.
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
/// Top-left position in canvas pixel coordinates.
pub x: i32,
pub y: i32,
/// Which raster layer and keyframe this float belongs to.
pub layer_id: Uuid,
pub time: f64,
/// Snapshot of `raw_pixels` before the cut/paste was initiated, used for
/// undo (via `RasterStrokeAction`) when the float is committed, and for
/// Cancel (Escape) to restore the canvas without creating an undo entry.
pub canvas_before: Vec<u8>,
/// Key for this float's GPU canvas in `GpuBrushEngine::canvases`.
/// Allows painting strokes directly onto the float buffer (B) without
/// touching the layer canvas (A).
pub canvas_id: Uuid,
}
/// Tracks the most recently selected thing(s) across the entire document.
///
/// Lightweight overlay on top of per-domain selection state. Tells consumers
/// "the user's attention is on this kind of thing" — for properties panels,
/// delete/copy/paste dispatch, group commands, etc.
#[derive(Clone, Debug, Default)]
pub enum FocusSelection {
#[default]
None,
/// One or more layers selected (by UUID)
Layers(Vec<Uuid>),
/// One or more clip instances selected (by UUID)
ClipInstances(Vec<Uuid>),
/// DCEL geometry selected on a specific layer at a specific time
Geometry { layer_id: Uuid, time: f64 },
/// MIDI notes selected in piano roll
Notes { layer_id: Uuid, midi_clip_id: u32, indices: Vec<usize> },
/// Node graph nodes selected (backend node indices)
Nodes(Vec<u32>),
/// Assets selected in asset library (by UUID)
Assets(Vec<Uuid>),
}
impl FocusSelection {
pub fn is_none(&self) -> bool {
matches!(self, FocusSelection::None)
}
pub fn layer_ids(&self) -> Option<&[Uuid]> {
match self {
FocusSelection::Layers(ids) => Some(ids),
_ => Option::None,
}
}
pub fn clip_instance_ids(&self) -> Option<&[Uuid]> {
match self {
FocusSelection::ClipInstances(ids) => Some(ids),
_ => Option::None,
}
}
}
/// Selection state for the editor
///
/// Maintains sets of selected DCEL elements and clip instances.
/// The vertex/edge/face sets implicitly represent a subgraph of the DCEL —
/// connectivity is determined by shared vertices between edges.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Selection {
/// Currently selected vertices
selected_vertices: HashSet<VertexId>,
/// Currently selected edges
selected_edges: HashSet<EdgeId>,
/// Currently selected faces
selected_faces: HashSet<FaceId>,
/// Currently selected clip instances
selected_clip_instances: Vec<Uuid>,
/// Active raster pixel selection (marquee or lasso outline).
/// Transient UI state — not persisted.
#[serde(skip)]
pub raster_selection: Option<RasterSelection>,
/// Floating raster selection waiting to be committed or cancelled.
/// Transient UI state — not persisted.
#[serde(skip)]
pub raster_floating: Option<RasterFloatingSelection>,
}
impl Selection {
/// Create a new empty selection
pub fn new() -> Self {
Self {
selected_vertices: HashSet::new(),
selected_edges: HashSet::new(),
selected_faces: HashSet::new(),
selected_clip_instances: Vec::new(),
raster_selection: None,
raster_floating: None,
}
}
// -----------------------------------------------------------------------
// DCEL element selection
// -----------------------------------------------------------------------
/// Select an edge and its endpoint vertices, forming/extending a subgraph.
pub fn select_edge(&mut self, edge_id: EdgeId, dcel: &Dcel) {
if edge_id.is_none() || dcel.edge(edge_id).deleted {
return;
}
self.selected_edges.insert(edge_id);
// Add both endpoint vertices
let [he_fwd, he_bwd] = dcel.edge(edge_id).half_edges;
if !he_fwd.is_none() {
let v = dcel.half_edge(he_fwd).origin;
if !v.is_none() {
self.selected_vertices.insert(v);
}
}
if !he_bwd.is_none() {
let v = dcel.half_edge(he_bwd).origin;
if !v.is_none() {
self.selected_vertices.insert(v);
}
}
}
/// Select a face and all its boundary edges + vertices.
pub fn select_face(&mut self, face_id: FaceId, dcel: &Dcel) {
if face_id.is_none() || face_id.0 == 0 || dcel.face(face_id).deleted {
return;
}
self.selected_faces.insert(face_id);
// Add all boundary edges and vertices
let boundary = dcel.face_boundary(face_id);
for he_id in boundary {
let he = dcel.half_edge(he_id);
let edge_id = he.edge;
if !edge_id.is_none() {
self.selected_edges.insert(edge_id);
// Add endpoints
let [he_fwd, he_bwd] = dcel.edge(edge_id).half_edges;
if !he_fwd.is_none() {
let v = dcel.half_edge(he_fwd).origin;
if !v.is_none() {
self.selected_vertices.insert(v);
}
}
if !he_bwd.is_none() {
let v = dcel.half_edge(he_bwd).origin;
if !v.is_none() {
self.selected_vertices.insert(v);
}
}
}
}
}
/// Deselect an edge and its vertices (if they have no other selected edges).
pub fn deselect_edge(&mut self, edge_id: EdgeId, dcel: &Dcel) {
self.selected_edges.remove(&edge_id);
// Remove endpoint vertices only if they're not used by other selected edges
let [he_fwd, he_bwd] = dcel.edge(edge_id).half_edges;
for he_id in [he_fwd, he_bwd] {
if he_id.is_none() {
continue;
}
let v = dcel.half_edge(he_id).origin;
if v.is_none() {
continue;
}
// Check if any other selected edge uses this vertex
let used = self.selected_edges.iter().any(|&eid| {
let e = dcel.edge(eid);
let [a, b] = e.half_edges;
(!a.is_none() && dcel.half_edge(a).origin == v)
|| (!b.is_none() && dcel.half_edge(b).origin == v)
});
if !used {
self.selected_vertices.remove(&v);
}
}
}
/// Deselect a face (edges/vertices stay if still referenced by other selections).
pub fn deselect_face(&mut self, face_id: FaceId) {
self.selected_faces.remove(&face_id);
}
/// Toggle an edge's selection state.
pub fn toggle_edge(&mut self, edge_id: EdgeId, dcel: &Dcel) {
if self.selected_edges.contains(&edge_id) {
self.deselect_edge(edge_id, dcel);
} else {
self.select_edge(edge_id, dcel);
}
}
/// Toggle a face's selection state.
pub fn toggle_face(&mut self, face_id: FaceId, dcel: &Dcel) {
if self.selected_faces.contains(&face_id) {
self.deselect_face(face_id);
} else {
self.select_face(face_id, dcel);
}
}
/// Check if an edge is selected.
pub fn contains_edge(&self, edge_id: &EdgeId) -> bool {
self.selected_edges.contains(edge_id)
}
/// Check if a face is selected.
pub fn contains_face(&self, face_id: &FaceId) -> bool {
self.selected_faces.contains(face_id)
}
/// Check if a vertex is selected.
pub fn contains_vertex(&self, vertex_id: &VertexId) -> bool {
self.selected_vertices.contains(vertex_id)
}
/// Clear DCEL element selections (edges, faces, vertices).
pub fn clear_dcel_selection(&mut self) {
self.selected_vertices.clear();
self.selected_edges.clear();
self.selected_faces.clear();
}
/// Check if any DCEL elements are selected.
pub fn has_dcel_selection(&self) -> bool {
!self.selected_edges.is_empty() || !self.selected_faces.is_empty()
}
/// Get selected edges.
pub fn selected_edges(&self) -> &HashSet<EdgeId> {
&self.selected_edges
}
/// Get selected faces.
pub fn selected_faces(&self) -> &HashSet<FaceId> {
&self.selected_faces
}
/// Get selected vertices.
pub fn selected_vertices(&self) -> &HashSet<VertexId> {
&self.selected_vertices
}
// -----------------------------------------------------------------------
// Clip instance selection (unchanged)
// -----------------------------------------------------------------------
/// 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);
}
}
/// Check if a clip instance is selected
pub fn contains_clip_instance(&self, id: &Uuid) -> bool {
self.selected_clip_instances.contains(id)
}
/// Clear only clip instance selections
pub fn clear_clip_instances(&mut self) {
self.selected_clip_instances.clear();
}
/// 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 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 clip instances (clears previous clip selection)
pub fn select_clip_instances(&mut self, ids: &[Uuid]) {
self.clear_clip_instances();
for &id in ids {
self.add_clip_instance(id);
}
}
// -----------------------------------------------------------------------
// General
// -----------------------------------------------------------------------
/// Clear all selections
pub fn clear(&mut self) {
self.selected_vertices.clear();
self.selected_edges.clear();
self.selected_faces.clear();
self.selected_clip_instances.clear();
self.raster_selection = None;
self.raster_floating = None;
}
/// Check if selection is empty
pub fn is_empty(&self) -> bool {
self.selected_edges.is_empty()
&& self.selected_faces.is_empty()
&& self.selected_clip_instances.is_empty()
}
}
/// Represents a temporary region-based selection.
///
/// When a region select is active, the region boundary is inserted into the
/// DCEL as invisible edges, splitting existing geometry. Faces inside the
/// region are added to the normal `Selection`. If the user performs an
/// operation, the selection is committed; if they deselect, the DCEL is
/// restored from the snapshot.
#[derive(Clone, Debug)]
pub struct RegionSelection {
/// The clipping region as a closed BezPath (polygon or rect)
pub region_path: BezPath,
/// Layer containing the affected elements
pub layer_id: Uuid,
/// Keyframe time
pub time: f64,
/// Snapshot of the DCEL before region boundary insertion, for revert
pub dcel_snapshot: Dcel,
/// The extracted DCEL containing geometry inside the region
pub selected_dcel: Dcel,
/// Transform applied to the selected DCEL (e.g. from dragging)
pub transform: Affine,
/// Whether the selection has been committed (via an operation on the selection)
pub committed: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_selection_creation() {
let selection = Selection::new();
assert!(selection.is_empty());
assert_eq!(selection.clip_instance_count(), 0);
}
#[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() {
let mut selection = Selection::new();
selection.add_clip_instance(Uuid::new_v4());
assert!(!selection.is_empty());
selection.clear();
assert!(selection.is_empty());
}
#[test]
fn test_dcel_selection_basics() {
let selection = Selection::new();
assert!(!selection.has_dcel_selection());
assert!(selection.selected_edges().is_empty());
assert!(selection.selected_faces().is_empty());
assert!(selection.selected_vertices().is_empty());
}
#[test]
fn test_clear_dcel_selection() {
let mut selection = Selection::new();
// Manually insert for unit test (no DCEL needed)
selection.selected_edges.insert(EdgeId(0));
selection.selected_vertices.insert(VertexId(0));
assert!(selection.has_dcel_selection());
selection.clear_dcel_selection();
assert!(!selection.has_dcel_selection());
}
}