//! Layer system for Lightningbeam //! //! Layers organize objects and shapes, and contain animation data. use crate::animation::AnimationData; use crate::clip::ClipInstance; use crate::effect_layer::EffectLayer; 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, /// Visual effects layer Effect, } /// 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) -> 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) -> 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 } } /// Tween type between keyframes #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TweenType { /// Hold shapes until next keyframe (no interpolation) None, /// Shape tween — morph geometry between keyframes Shape, } impl Default for TweenType { fn default() -> Self { TweenType::None } } /// A keyframe containing all shapes at a point in time #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ShapeKeyframe { /// Time in seconds pub time: f64, /// All shapes at this keyframe pub shapes: Vec, /// What happens between this keyframe and the next #[serde(default)] pub tween_after: TweenType, /// Clip instance IDs visible in this keyframe region. /// Groups are only rendered in regions where they appear in the left keyframe. #[serde(default)] pub clip_instance_ids: Vec, } impl ShapeKeyframe { /// Create a new empty keyframe at a given time pub fn new(time: f64) -> Self { Self { time, shapes: Vec::new(), tween_after: TweenType::None, clip_instance_ids: Vec::new(), } } /// Create a keyframe with shapes pub fn with_shapes(time: f64, shapes: Vec) -> Self { Self { time, shapes, tween_after: TweenType::None, clip_instance_ids: Vec::new(), } } } /// 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, /// Shape instances (references to shapes with transforms) pub shape_instances: Vec, /// Shape keyframes (sorted by time) — replaces shapes/shape_instances #[serde(default)] pub keyframes: Vec, /// Clip instances (references to vector clips with transforms) /// VectorLayer can contain instances of VectorClips for nested compositions pub clip_instances: Vec, } 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) -> Self { Self { layer: Layer::new(LayerType::Vector, name), shapes: HashMap::new(), shape_instances: Vec::new(), keyframes: 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) === /// 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(&mut self, id: &Uuid, f: F) where F: FnOnce(&mut ShapeInstance), { if let Some(object) = self.get_object_mut(id) { f(object); } } // === KEYFRAME METHODS === /// Find the keyframe at-or-before the given time pub fn keyframe_at(&self, time: f64) -> Option<&ShapeKeyframe> { // keyframes are sorted by time; find the last one <= time let idx = self.keyframes.partition_point(|kf| kf.time <= time); if idx > 0 { Some(&self.keyframes[idx - 1]) } else { None } } /// Find the mutable keyframe at-or-before the given time pub fn keyframe_at_mut(&mut self, time: f64) -> Option<&mut ShapeKeyframe> { let idx = self.keyframes.partition_point(|kf| kf.time <= time); if idx > 0 { Some(&mut self.keyframes[idx - 1]) } else { None } } /// Find the index of a keyframe at the exact time (within tolerance) pub fn keyframe_index_at_exact(&self, time: f64, tolerance: f64) -> Option { self.keyframes.iter().position(|kf| (kf.time - time).abs() < tolerance) } /// Get shapes visible at a given time (from the keyframe at-or-before time) pub fn shapes_at_time(&self, time: f64) -> &[Shape] { match self.keyframe_at(time) { Some(kf) => &kf.shapes, None => &[], } } /// Get the duration of the keyframe span starting at-or-before `time`. /// Returns the time from `time` to the next keyframe, or to `fallback_end` if there is no next keyframe. pub fn keyframe_span_duration(&self, time: f64, fallback_end: f64) -> f64 { // Find the next keyframe after `time` let next_idx = self.keyframes.partition_point(|kf| kf.time <= time); let end = if next_idx < self.keyframes.len() { self.keyframes[next_idx].time } else { fallback_end }; (end - time).max(0.0) } /// Check if a clip instance (group) is visible at the given time. /// Returns true if the keyframe at-or-before `time` contains `clip_instance_id`. pub fn is_clip_instance_visible_at(&self, clip_instance_id: &Uuid, time: f64) -> bool { self.keyframe_at(time) .map_or(false, |kf| kf.clip_instance_ids.contains(clip_instance_id)) } /// Get the visibility end time for a group clip instance starting from `time`. /// A group is visible in regions bounded on the left by a keyframe that contains it /// and on the right by any keyframe. Walks forward through keyframe regions, /// extending as long as consecutive left-keyframes contain the clip instance. /// If the last containing keyframe has no next keyframe (no right border), /// the region is just one frame long. pub fn group_visibility_end(&self, clip_instance_id: &Uuid, time: f64, frame_duration: f64) -> f64 { let start_idx = self.keyframes.partition_point(|kf| kf.time <= time); // start_idx is the index of the first keyframe AFTER time // Walk forward: each keyframe that contains the group extends visibility to the NEXT keyframe for idx in start_idx..self.keyframes.len() { if !self.keyframes[idx].clip_instance_ids.contains(clip_instance_id) { // This keyframe doesn't contain the group — it's the right border of the last region return self.keyframes[idx].time; } // This keyframe contains the group — check if there's a next one to form a right border } // No more keyframes after the last containing one — last region is one frame if let Some(last_kf) = self.keyframes.last() { if last_kf.clip_instance_ids.contains(clip_instance_id) { return last_kf.time + frame_duration; } } time + frame_duration } /// Get mutable shapes at a given time pub fn shapes_at_time_mut(&mut self, time: f64) -> Option<&mut Vec> { self.keyframe_at_mut(time).map(|kf| &mut kf.shapes) } /// Find a shape by ID within the keyframe active at the given time pub fn get_shape_in_keyframe(&self, shape_id: &Uuid, time: f64) -> Option<&Shape> { self.keyframe_at(time) .and_then(|kf| kf.shapes.iter().find(|s| &s.id == shape_id)) } /// Find a mutable shape by ID within the keyframe active at the given time pub fn get_shape_in_keyframe_mut(&mut self, shape_id: &Uuid, time: f64) -> Option<&mut Shape> { self.keyframe_at_mut(time) .and_then(|kf| kf.shapes.iter_mut().find(|s| &s.id == shape_id)) } /// Ensure a keyframe exists at the exact time, creating an empty one if needed. /// Returns a mutable reference to the keyframe. pub fn ensure_keyframe_at(&mut self, time: f64) -> &mut ShapeKeyframe { let tolerance = 0.001; if let Some(idx) = self.keyframe_index_at_exact(time, tolerance) { return &mut self.keyframes[idx]; } // Insert in sorted position let insert_idx = self.keyframes.partition_point(|kf| kf.time < time); self.keyframes.insert(insert_idx, ShapeKeyframe::new(time)); &mut self.keyframes[insert_idx] } /// Insert a new keyframe at time by copying shapes from the active keyframe. /// Shape UUIDs are regenerated (no cross-keyframe identity). /// If a keyframe already exists at the exact time, does nothing and returns it. pub fn insert_keyframe_from_current(&mut self, time: f64) -> &mut ShapeKeyframe { let tolerance = 0.001; if let Some(idx) = self.keyframe_index_at_exact(time, tolerance) { return &mut self.keyframes[idx]; } // Clone shapes and clip instance IDs from the active keyframe let (cloned_shapes, cloned_clip_ids) = self .keyframe_at(time) .map(|kf| { let shapes: Vec = kf.shapes .iter() .map(|s| { let mut new_shape = s.clone(); new_shape.id = Uuid::new_v4(); new_shape }) .collect(); let clip_ids = kf.clip_instance_ids.clone(); (shapes, clip_ids) }) .unwrap_or_default(); let insert_idx = self.keyframes.partition_point(|kf| kf.time < time); let mut kf = ShapeKeyframe::with_shapes(time, cloned_shapes); kf.clip_instance_ids = cloned_clip_ids; self.keyframes.insert(insert_idx, kf); &mut self.keyframes[insert_idx] } /// Add a shape to the keyframe at the given time. /// Creates a keyframe if none exists at that time. pub(crate) fn add_shape_to_keyframe(&mut self, shape: Shape, time: f64) { let kf = self.ensure_keyframe_at(time); kf.shapes.push(shape); } /// Remove a shape from the keyframe at the given time. /// Returns the removed shape if found. pub(crate) fn remove_shape_from_keyframe(&mut self, shape_id: &Uuid, time: f64) -> Option { let kf = self.keyframe_at_mut(time)?; let idx = kf.shapes.iter().position(|s| &s.id == shape_id)?; Some(kf.shapes.remove(idx)) } /// Remove a keyframe at the exact time (within tolerance). /// Returns the removed keyframe if found. pub(crate) fn remove_keyframe_at(&mut self, time: f64, tolerance: f64) -> Option { if let Some(idx) = self.keyframe_index_at_exact(time, tolerance) { Some(self.keyframes.remove(idx)) } else { None } } } /// Audio layer subtype - distinguishes sampled audio from MIDI #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum AudioLayerType { /// Sampled audio (WAV, MP3, etc.) Sampled, /// MIDI sequence Midi, } impl Default for AudioLayerType { fn default() -> Self { AudioLayerType::Sampled } } /// 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, /// Audio layer subtype (sampled vs MIDI) #[serde(default)] pub audio_layer_type: AudioLayerType, } 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 sampled audio layer pub fn new(name: impl Into) -> Self { Self { layer: Layer::new(LayerType::Audio, name), clip_instances: Vec::new(), audio_layer_type: AudioLayerType::Sampled, } } /// Create a new sampled audio layer (explicit) pub fn new_sampled(name: impl Into) -> Self { Self::new(name) } /// Create a new MIDI layer pub fn new_midi(name: impl Into) -> Self { Self { layer: Layer::new(LayerType::Audio, name), clip_instances: Vec::new(), audio_layer_type: AudioLayerType::Midi, } } } /// 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, } 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) -> 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), Effect(EffectLayer), } 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(), AnyLayer::Effect(l) => l.id(), } } fn name(&self) -> &str { match self { AnyLayer::Vector(l) => l.name(), AnyLayer::Audio(l) => l.name(), AnyLayer::Video(l) => l.name(), AnyLayer::Effect(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), AnyLayer::Effect(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(), AnyLayer::Effect(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), AnyLayer::Effect(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(), AnyLayer::Effect(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), AnyLayer::Effect(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(), AnyLayer::Effect(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), AnyLayer::Effect(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(), AnyLayer::Effect(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), AnyLayer::Effect(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(), AnyLayer::Effect(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), AnyLayer::Effect(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(), AnyLayer::Effect(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), AnyLayer::Effect(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(), AnyLayer::Effect(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), AnyLayer::Effect(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, AnyLayer::Effect(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, AnyLayer::Effect(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_basic_properties() { let layer = Layer::new(LayerType::Vector, "Test"); assert_eq!(layer.name, "Test"); assert_eq!(layer.visible, true); assert_eq!(layer.opacity, 1.0); assert_eq!(layer.volume, 1.0); assert_eq!(layer.muted, false); assert_eq!(layer.soloed, false); assert_eq!(layer.locked, false); } }