715 lines
18 KiB
Rust
715 lines
18 KiB
Rust
//! Layer system for Lightningbeam
|
|
//!
|
|
//! Layers organize objects and shapes, and contain animation data.
|
|
|
|
use crate::animation::AnimationData;
|
|
use crate::clip::ClipInstance;
|
|
use crate::object::ShapeInstance;
|
|
use crate::shape::Shape;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use uuid::Uuid;
|
|
|
|
/// Layer type
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum LayerType {
|
|
/// Vector graphics layer (shapes and objects)
|
|
Vector,
|
|
/// Audio track
|
|
Audio,
|
|
/// Video clip
|
|
Video,
|
|
/// Generic automation layer
|
|
Automation,
|
|
}
|
|
|
|
/// Common trait for all layer types
|
|
///
|
|
/// Provides uniform access to common layer properties across VectorLayer,
|
|
/// AudioLayer, VideoLayer, and their wrapper AnyLayer enum.
|
|
pub trait LayerTrait {
|
|
// Identity
|
|
fn id(&self) -> Uuid;
|
|
fn name(&self) -> &str;
|
|
fn set_name(&mut self, name: String);
|
|
fn has_custom_name(&self) -> bool;
|
|
fn set_has_custom_name(&mut self, custom: bool);
|
|
|
|
// Visual properties
|
|
fn visible(&self) -> bool;
|
|
fn set_visible(&mut self, visible: bool);
|
|
fn opacity(&self) -> f64;
|
|
fn set_opacity(&mut self, opacity: f64);
|
|
|
|
// Audio properties (all layers can affect audio through nesting)
|
|
fn volume(&self) -> f64;
|
|
fn set_volume(&mut self, volume: f64);
|
|
fn muted(&self) -> bool;
|
|
fn set_muted(&mut self, muted: bool);
|
|
|
|
// Editor state
|
|
fn soloed(&self) -> bool;
|
|
fn set_soloed(&mut self, soloed: bool);
|
|
fn locked(&self) -> bool;
|
|
fn set_locked(&mut self, locked: bool);
|
|
}
|
|
|
|
/// Base layer structure
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct Layer {
|
|
/// Unique identifier
|
|
pub id: Uuid,
|
|
|
|
/// Layer type
|
|
pub layer_type: LayerType,
|
|
|
|
/// Layer name
|
|
pub name: String,
|
|
|
|
/// Whether the name was set by user (vs auto-generated)
|
|
pub has_custom_name: bool,
|
|
|
|
/// Whether the layer is visible
|
|
pub visible: bool,
|
|
|
|
/// Layer opacity (0.0 to 1.0)
|
|
pub opacity: f64,
|
|
|
|
/// Audio volume (1.0 = 100%, affects nested audio layers/clips)
|
|
pub volume: f64,
|
|
|
|
/// Audio mute state
|
|
pub muted: bool,
|
|
|
|
/// Solo state (for isolating layers)
|
|
pub soloed: bool,
|
|
|
|
/// Lock state (prevents editing)
|
|
pub locked: bool,
|
|
|
|
/// Animation data for this layer
|
|
pub animation_data: AnimationData,
|
|
}
|
|
|
|
impl Layer {
|
|
/// Create a new layer
|
|
pub fn new(layer_type: LayerType, name: impl Into<String>) -> Self {
|
|
Self {
|
|
id: Uuid::new_v4(),
|
|
layer_type,
|
|
name: name.into(),
|
|
has_custom_name: false, // Auto-generated by default
|
|
visible: true,
|
|
opacity: 1.0,
|
|
volume: 1.0, // 100% volume
|
|
muted: false,
|
|
soloed: false,
|
|
locked: false,
|
|
animation_data: AnimationData::new(),
|
|
}
|
|
}
|
|
|
|
/// Create with a specific ID
|
|
pub fn with_id(id: Uuid, layer_type: LayerType, name: impl Into<String>) -> Self {
|
|
Self {
|
|
id,
|
|
layer_type,
|
|
name: name.into(),
|
|
has_custom_name: false,
|
|
visible: true,
|
|
opacity: 1.0,
|
|
volume: 1.0,
|
|
muted: false,
|
|
soloed: false,
|
|
locked: false,
|
|
animation_data: AnimationData::new(),
|
|
}
|
|
}
|
|
|
|
/// Set visibility
|
|
pub fn with_visibility(mut self, visible: bool) -> Self {
|
|
self.visible = visible;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Vector layer containing shapes and objects
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct VectorLayer {
|
|
/// Base layer properties
|
|
pub layer: Layer,
|
|
|
|
/// Shapes defined in this layer (indexed by UUID for O(1) lookup)
|
|
pub shapes: HashMap<Uuid, Shape>,
|
|
|
|
/// Shape instances (references to shapes with transforms)
|
|
pub shape_instances: Vec<ShapeInstance>,
|
|
|
|
/// Clip instances (references to vector clips with transforms)
|
|
/// VectorLayer can contain instances of VectorClips for nested compositions
|
|
pub clip_instances: Vec<ClipInstance>,
|
|
}
|
|
|
|
impl LayerTrait for VectorLayer {
|
|
fn id(&self) -> Uuid {
|
|
self.layer.id
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
&self.layer.name
|
|
}
|
|
|
|
fn set_name(&mut self, name: String) {
|
|
self.layer.name = name;
|
|
}
|
|
|
|
fn has_custom_name(&self) -> bool {
|
|
self.layer.has_custom_name
|
|
}
|
|
|
|
fn set_has_custom_name(&mut self, custom: bool) {
|
|
self.layer.has_custom_name = custom;
|
|
}
|
|
|
|
fn visible(&self) -> bool {
|
|
self.layer.visible
|
|
}
|
|
|
|
fn set_visible(&mut self, visible: bool) {
|
|
self.layer.visible = visible;
|
|
}
|
|
|
|
fn opacity(&self) -> f64 {
|
|
self.layer.opacity
|
|
}
|
|
|
|
fn set_opacity(&mut self, opacity: f64) {
|
|
self.layer.opacity = opacity;
|
|
}
|
|
|
|
fn volume(&self) -> f64 {
|
|
self.layer.volume
|
|
}
|
|
|
|
fn set_volume(&mut self, volume: f64) {
|
|
self.layer.volume = volume;
|
|
}
|
|
|
|
fn muted(&self) -> bool {
|
|
self.layer.muted
|
|
}
|
|
|
|
fn set_muted(&mut self, muted: bool) {
|
|
self.layer.muted = muted;
|
|
}
|
|
|
|
fn soloed(&self) -> bool {
|
|
self.layer.soloed
|
|
}
|
|
|
|
fn set_soloed(&mut self, soloed: bool) {
|
|
self.layer.soloed = soloed;
|
|
}
|
|
|
|
fn locked(&self) -> bool {
|
|
self.layer.locked
|
|
}
|
|
|
|
fn set_locked(&mut self, locked: bool) {
|
|
self.layer.locked = locked;
|
|
}
|
|
}
|
|
|
|
impl VectorLayer {
|
|
/// Create a new vector layer
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
Self {
|
|
layer: Layer::new(LayerType::Vector, name),
|
|
shapes: HashMap::new(),
|
|
shape_instances: Vec::new(),
|
|
clip_instances: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Add a shape to this layer
|
|
pub fn add_shape(&mut self, shape: Shape) -> Uuid {
|
|
let id = shape.id;
|
|
self.shapes.insert(id, shape);
|
|
id
|
|
}
|
|
|
|
/// Add an object to this layer
|
|
pub fn add_object(&mut self, object: ShapeInstance) -> Uuid {
|
|
let id = object.id;
|
|
self.shape_instances.push(object);
|
|
id
|
|
}
|
|
|
|
/// Find a shape by ID
|
|
pub fn get_shape(&self, id: &Uuid) -> Option<&Shape> {
|
|
self.shapes.get(id)
|
|
}
|
|
|
|
/// Find a mutable shape by ID
|
|
pub fn get_shape_mut(&mut self, id: &Uuid) -> Option<&mut Shape> {
|
|
self.shapes.get_mut(id)
|
|
}
|
|
|
|
/// Find an object by ID
|
|
pub fn get_object(&self, id: &Uuid) -> Option<&ShapeInstance> {
|
|
self.shape_instances.iter().find(|o| &o.id == id)
|
|
}
|
|
|
|
/// Find a mutable object by ID
|
|
pub fn get_object_mut(&mut self, id: &Uuid) -> Option<&mut ShapeInstance> {
|
|
self.shape_instances.iter_mut().find(|o| &o.id == id)
|
|
}
|
|
|
|
// === MUTATION METHODS (pub(crate) - only accessible to action module) ===
|
|
|
|
/// Add a shape to this layer (internal, for actions only)
|
|
///
|
|
/// This method is intentionally `pub(crate)` to ensure mutations
|
|
/// only happen through the action system.
|
|
pub(crate) fn add_shape_internal(&mut self, shape: Shape) -> Uuid {
|
|
let id = shape.id;
|
|
self.shapes.insert(id, shape);
|
|
id
|
|
}
|
|
|
|
/// Add an object to this layer (internal, for actions only)
|
|
///
|
|
/// This method is intentionally `pub(crate)` to ensure mutations
|
|
/// only happen through the action system.
|
|
pub(crate) fn add_object_internal(&mut self, object: ShapeInstance) -> Uuid {
|
|
let id = object.id;
|
|
self.shape_instances.push(object);
|
|
id
|
|
}
|
|
|
|
/// Remove a shape from this layer (internal, for actions only)
|
|
///
|
|
/// Returns the removed shape if found.
|
|
/// This method is intentionally `pub(crate)` to ensure mutations
|
|
/// only happen through the action system.
|
|
pub(crate) fn remove_shape_internal(&mut self, id: &Uuid) -> Option<Shape> {
|
|
self.shapes.remove(id)
|
|
}
|
|
|
|
/// Remove an object from this layer (internal, for actions only)
|
|
///
|
|
/// Returns the removed object if found.
|
|
/// This method is intentionally `pub(crate)` to ensure mutations
|
|
/// only happen through the action system.
|
|
pub(crate) fn remove_object_internal(&mut self, id: &Uuid) -> Option<ShapeInstance> {
|
|
if let Some(index) = self.shape_instances.iter().position(|o| &o.id == id) {
|
|
Some(self.shape_instances.remove(index))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Modify an object in place (internal, for actions only)
|
|
///
|
|
/// Applies the given function to the object if found.
|
|
/// This method is intentionally `pub(crate)` to ensure mutations
|
|
/// only happen through the action system.
|
|
pub fn modify_object_internal<F>(&mut self, id: &Uuid, f: F)
|
|
where
|
|
F: FnOnce(&mut ShapeInstance),
|
|
{
|
|
if let Some(object) = self.get_object_mut(id) {
|
|
f(object);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Audio layer containing audio clips
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct AudioLayer {
|
|
/// Base layer properties
|
|
pub layer: Layer,
|
|
|
|
/// Clip instances (references to audio clips)
|
|
/// AudioLayer can contain instances of AudioClips (sampled or MIDI)
|
|
pub clip_instances: Vec<ClipInstance>,
|
|
}
|
|
|
|
impl LayerTrait for AudioLayer {
|
|
fn id(&self) -> Uuid {
|
|
self.layer.id
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
&self.layer.name
|
|
}
|
|
|
|
fn set_name(&mut self, name: String) {
|
|
self.layer.name = name;
|
|
}
|
|
|
|
fn has_custom_name(&self) -> bool {
|
|
self.layer.has_custom_name
|
|
}
|
|
|
|
fn set_has_custom_name(&mut self, custom: bool) {
|
|
self.layer.has_custom_name = custom;
|
|
}
|
|
|
|
fn visible(&self) -> bool {
|
|
self.layer.visible
|
|
}
|
|
|
|
fn set_visible(&mut self, visible: bool) {
|
|
self.layer.visible = visible;
|
|
}
|
|
|
|
fn opacity(&self) -> f64 {
|
|
self.layer.opacity
|
|
}
|
|
|
|
fn set_opacity(&mut self, opacity: f64) {
|
|
self.layer.opacity = opacity;
|
|
}
|
|
|
|
fn volume(&self) -> f64 {
|
|
self.layer.volume
|
|
}
|
|
|
|
fn set_volume(&mut self, volume: f64) {
|
|
self.layer.volume = volume;
|
|
}
|
|
|
|
fn muted(&self) -> bool {
|
|
self.layer.muted
|
|
}
|
|
|
|
fn set_muted(&mut self, muted: bool) {
|
|
self.layer.muted = muted;
|
|
}
|
|
|
|
fn soloed(&self) -> bool {
|
|
self.layer.soloed
|
|
}
|
|
|
|
fn set_soloed(&mut self, soloed: bool) {
|
|
self.layer.soloed = soloed;
|
|
}
|
|
|
|
fn locked(&self) -> bool {
|
|
self.layer.locked
|
|
}
|
|
|
|
fn set_locked(&mut self, locked: bool) {
|
|
self.layer.locked = locked;
|
|
}
|
|
}
|
|
|
|
impl AudioLayer {
|
|
/// Create a new audio layer
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
Self {
|
|
layer: Layer::new(LayerType::Audio, name),
|
|
clip_instances: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Video layer containing video clips
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct VideoLayer {
|
|
/// Base layer properties
|
|
pub layer: Layer,
|
|
|
|
/// Clip instances (references to video clips)
|
|
/// VideoLayer can contain instances of VideoClips
|
|
pub clip_instances: Vec<ClipInstance>,
|
|
}
|
|
|
|
impl LayerTrait for VideoLayer {
|
|
fn id(&self) -> Uuid {
|
|
self.layer.id
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
&self.layer.name
|
|
}
|
|
|
|
fn set_name(&mut self, name: String) {
|
|
self.layer.name = name;
|
|
}
|
|
|
|
fn has_custom_name(&self) -> bool {
|
|
self.layer.has_custom_name
|
|
}
|
|
|
|
fn set_has_custom_name(&mut self, custom: bool) {
|
|
self.layer.has_custom_name = custom;
|
|
}
|
|
|
|
fn visible(&self) -> bool {
|
|
self.layer.visible
|
|
}
|
|
|
|
fn set_visible(&mut self, visible: bool) {
|
|
self.layer.visible = visible;
|
|
}
|
|
|
|
fn opacity(&self) -> f64 {
|
|
self.layer.opacity
|
|
}
|
|
|
|
fn set_opacity(&mut self, opacity: f64) {
|
|
self.layer.opacity = opacity;
|
|
}
|
|
|
|
fn volume(&self) -> f64 {
|
|
self.layer.volume
|
|
}
|
|
|
|
fn set_volume(&mut self, volume: f64) {
|
|
self.layer.volume = volume;
|
|
}
|
|
|
|
fn muted(&self) -> bool {
|
|
self.layer.muted
|
|
}
|
|
|
|
fn set_muted(&mut self, muted: bool) {
|
|
self.layer.muted = muted;
|
|
}
|
|
|
|
fn soloed(&self) -> bool {
|
|
self.layer.soloed
|
|
}
|
|
|
|
fn set_soloed(&mut self, soloed: bool) {
|
|
self.layer.soloed = soloed;
|
|
}
|
|
|
|
fn locked(&self) -> bool {
|
|
self.layer.locked
|
|
}
|
|
|
|
fn set_locked(&mut self, locked: bool) {
|
|
self.layer.locked = locked;
|
|
}
|
|
}
|
|
|
|
impl VideoLayer {
|
|
/// Create a new video layer
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
Self {
|
|
layer: Layer::new(LayerType::Video, name),
|
|
clip_instances: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Unified layer enum for polymorphic handling
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub enum AnyLayer {
|
|
Vector(VectorLayer),
|
|
Audio(AudioLayer),
|
|
Video(VideoLayer),
|
|
}
|
|
|
|
impl LayerTrait for AnyLayer {
|
|
fn id(&self) -> Uuid {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.id(),
|
|
AnyLayer::Audio(l) => l.id(),
|
|
AnyLayer::Video(l) => l.id(),
|
|
}
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.name(),
|
|
AnyLayer::Audio(l) => l.name(),
|
|
AnyLayer::Video(l) => l.name(),
|
|
}
|
|
}
|
|
|
|
fn set_name(&mut self, name: String) {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.set_name(name),
|
|
AnyLayer::Audio(l) => l.set_name(name),
|
|
AnyLayer::Video(l) => l.set_name(name),
|
|
}
|
|
}
|
|
|
|
fn has_custom_name(&self) -> bool {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.has_custom_name(),
|
|
AnyLayer::Audio(l) => l.has_custom_name(),
|
|
AnyLayer::Video(l) => l.has_custom_name(),
|
|
}
|
|
}
|
|
|
|
fn set_has_custom_name(&mut self, custom: bool) {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.set_has_custom_name(custom),
|
|
AnyLayer::Audio(l) => l.set_has_custom_name(custom),
|
|
AnyLayer::Video(l) => l.set_has_custom_name(custom),
|
|
}
|
|
}
|
|
|
|
fn visible(&self) -> bool {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.visible(),
|
|
AnyLayer::Audio(l) => l.visible(),
|
|
AnyLayer::Video(l) => l.visible(),
|
|
}
|
|
}
|
|
|
|
fn set_visible(&mut self, visible: bool) {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.set_visible(visible),
|
|
AnyLayer::Audio(l) => l.set_visible(visible),
|
|
AnyLayer::Video(l) => l.set_visible(visible),
|
|
}
|
|
}
|
|
|
|
fn opacity(&self) -> f64 {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.opacity(),
|
|
AnyLayer::Audio(l) => l.opacity(),
|
|
AnyLayer::Video(l) => l.opacity(),
|
|
}
|
|
}
|
|
|
|
fn set_opacity(&mut self, opacity: f64) {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.set_opacity(opacity),
|
|
AnyLayer::Audio(l) => l.set_opacity(opacity),
|
|
AnyLayer::Video(l) => l.set_opacity(opacity),
|
|
}
|
|
}
|
|
|
|
fn volume(&self) -> f64 {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.volume(),
|
|
AnyLayer::Audio(l) => l.volume(),
|
|
AnyLayer::Video(l) => l.volume(),
|
|
}
|
|
}
|
|
|
|
fn set_volume(&mut self, volume: f64) {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.set_volume(volume),
|
|
AnyLayer::Audio(l) => l.set_volume(volume),
|
|
AnyLayer::Video(l) => l.set_volume(volume),
|
|
}
|
|
}
|
|
|
|
fn muted(&self) -> bool {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.muted(),
|
|
AnyLayer::Audio(l) => l.muted(),
|
|
AnyLayer::Video(l) => l.muted(),
|
|
}
|
|
}
|
|
|
|
fn set_muted(&mut self, muted: bool) {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.set_muted(muted),
|
|
AnyLayer::Audio(l) => l.set_muted(muted),
|
|
AnyLayer::Video(l) => l.set_muted(muted),
|
|
}
|
|
}
|
|
|
|
fn soloed(&self) -> bool {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.soloed(),
|
|
AnyLayer::Audio(l) => l.soloed(),
|
|
AnyLayer::Video(l) => l.soloed(),
|
|
}
|
|
}
|
|
|
|
fn set_soloed(&mut self, soloed: bool) {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.set_soloed(soloed),
|
|
AnyLayer::Audio(l) => l.set_soloed(soloed),
|
|
AnyLayer::Video(l) => l.set_soloed(soloed),
|
|
}
|
|
}
|
|
|
|
fn locked(&self) -> bool {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.locked(),
|
|
AnyLayer::Audio(l) => l.locked(),
|
|
AnyLayer::Video(l) => l.locked(),
|
|
}
|
|
}
|
|
|
|
fn set_locked(&mut self, locked: bool) {
|
|
match self {
|
|
AnyLayer::Vector(l) => l.set_locked(locked),
|
|
AnyLayer::Audio(l) => l.set_locked(locked),
|
|
AnyLayer::Video(l) => l.set_locked(locked),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AnyLayer {
|
|
/// Get a reference to the base layer
|
|
pub fn layer(&self) -> &Layer {
|
|
match self {
|
|
AnyLayer::Vector(l) => &l.layer,
|
|
AnyLayer::Audio(l) => &l.layer,
|
|
AnyLayer::Video(l) => &l.layer,
|
|
}
|
|
}
|
|
|
|
/// Get a mutable reference to the base layer
|
|
pub fn layer_mut(&mut self) -> &mut Layer {
|
|
match self {
|
|
AnyLayer::Vector(l) => &mut l.layer,
|
|
AnyLayer::Audio(l) => &mut l.layer,
|
|
AnyLayer::Video(l) => &mut l.layer,
|
|
}
|
|
}
|
|
|
|
/// Get the layer ID
|
|
pub fn id(&self) -> Uuid {
|
|
self.layer().id
|
|
}
|
|
|
|
/// Get the layer name
|
|
pub fn name(&self) -> &str {
|
|
&self.layer().name
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_layer_creation() {
|
|
let layer = Layer::new(LayerType::Vector, "Test Layer");
|
|
assert_eq!(layer.layer_type, LayerType::Vector);
|
|
assert_eq!(layer.name, "Test Layer");
|
|
assert_eq!(layer.opacity, 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_layer() {
|
|
let vector_layer = VectorLayer::new("My Layer");
|
|
assert_eq!(vector_layer.shapes.len(), 0);
|
|
assert_eq!(vector_layer.shape_instances.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_layer_time_range() {
|
|
let layer = Layer::new(LayerType::Vector, "Test")
|
|
.with_time_range(5.0, 15.0);
|
|
|
|
assert_eq!(layer.duration(), 10.0);
|
|
assert!(layer.contains_time(10.0));
|
|
assert!(!layer.contains_time(3.0));
|
|
assert!(!layer.contains_time(20.0));
|
|
}
|
|
}
|