1053 lines
31 KiB
Rust
1053 lines
31 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::dcel::Dcel;
|
||
use crate::effect_layer::EffectLayer;
|
||
use crate::object::ShapeInstance;
|
||
use crate::raster_layer::RasterLayer;
|
||
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,
|
||
/// Group layer containing child layers (e.g. video + audio)
|
||
Group,
|
||
/// Raster pixel-buffer painting layer
|
||
Raster,
|
||
}
|
||
|
||
/// 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);
|
||
}
|
||
|
||
fn default_input_gain() -> f64 { 1.0 }
|
||
|
||
/// 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,
|
||
|
||
/// Input gain for recording (1.0 = unity, range 0.0–4.0)
|
||
#[serde(default = "default_input_gain")]
|
||
pub input_gain: 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
|
||
input_gain: 1.0,
|
||
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,
|
||
input_gain: 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 vector artwork as a DCEL planar subdivision.
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct ShapeKeyframe {
|
||
/// Time in seconds
|
||
pub time: f64,
|
||
/// DCEL planar subdivision containing all vector artwork
|
||
pub dcel: Dcel,
|
||
/// 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<Uuid>,
|
||
}
|
||
|
||
impl ShapeKeyframe {
|
||
/// Create a new empty keyframe at a given time
|
||
pub fn new(time: f64) -> Self {
|
||
Self {
|
||
time,
|
||
dcel: Dcel::new(),
|
||
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,
|
||
|
||
/// Legacy shapes — kept for old .beam file compat, not written to new files.
|
||
#[serde(default, skip_serializing)]
|
||
pub shapes: HashMap<Uuid, Shape>,
|
||
|
||
/// Legacy shape instances — kept for old .beam file compat, not written to new files.
|
||
#[serde(default, skip_serializing)]
|
||
pub shape_instances: Vec<ShapeInstance>,
|
||
|
||
/// Shape keyframes (sorted by time)
|
||
#[serde(default)]
|
||
pub keyframes: Vec<ShapeKeyframe>,
|
||
|
||
/// 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(),
|
||
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<F>(&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<usize> {
|
||
self.keyframes.iter().position(|kf| (kf.time - time).abs() < tolerance)
|
||
}
|
||
|
||
/// Get the DCEL at a given time (from the keyframe at-or-before time)
|
||
pub fn dcel_at_time(&self, time: f64) -> Option<&Dcel> {
|
||
self.keyframe_at(time).map(|kf| &kf.dcel)
|
||
}
|
||
|
||
/// Get a mutable DCEL at a given time
|
||
pub fn dcel_at_time_mut(&mut self, time: f64) -> Option<&mut Dcel> {
|
||
self.keyframe_at_mut(time).map(|kf| &mut kf.dcel)
|
||
}
|
||
|
||
/// 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
|
||
}
|
||
|
||
// Shape-based methods removed — use DCEL methods instead.
|
||
// - shapes_at_time_mut → dcel_at_time_mut
|
||
// - get_shape_in_keyframe → use DCEL vertex/edge/face accessors
|
||
// - get_shape_in_keyframe_mut → use DCEL vertex/edge/face accessors
|
||
|
||
/// 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 cloning the DCEL from the active keyframe.
|
||
/// 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 DCEL and clip instance IDs from the active keyframe
|
||
let (cloned_dcel, cloned_clip_ids) = self
|
||
.keyframe_at(time)
|
||
.map(|kf| {
|
||
(kf.dcel.clone(), kf.clip_instance_ids.clone())
|
||
})
|
||
.unwrap_or_else(|| (Dcel::new(), Vec::new()));
|
||
|
||
let insert_idx = self.keyframes.partition_point(|kf| kf.time < time);
|
||
let mut kf = ShapeKeyframe::new(time);
|
||
kf.dcel = cloned_dcel;
|
||
kf.clip_instance_ids = cloned_clip_ids;
|
||
self.keyframes.insert(insert_idx, kf);
|
||
&mut self.keyframes[insert_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<ShapeKeyframe> {
|
||
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<ClipInstance>,
|
||
|
||
/// 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<String>) -> 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<String>) -> Self {
|
||
Self::new(name)
|
||
}
|
||
|
||
/// Create a new MIDI layer
|
||
pub fn new_midi(name: impl Into<String>) -> 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<ClipInstance>,
|
||
|
||
/// When true, the live webcam feed is shown in the stage for this
|
||
/// layer (at times when no clip instance is active).
|
||
#[serde(default)]
|
||
pub camera_enabled: bool,
|
||
}
|
||
|
||
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(),
|
||
camera_enabled: false,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Group layer containing child layers (e.g. video + audio).
|
||
/// Collapsible in the timeline; when collapsed shows a merged clip view.
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct GroupLayer {
|
||
/// Base layer properties
|
||
pub layer: Layer,
|
||
|
||
/// Child layers in this group (typically one VideoLayer + one AudioLayer)
|
||
pub children: Vec<AnyLayer>,
|
||
|
||
/// Whether the group is expanded in the timeline
|
||
#[serde(default = "default_true")]
|
||
pub expanded: bool,
|
||
}
|
||
|
||
fn default_true() -> bool {
|
||
true
|
||
}
|
||
|
||
impl LayerTrait for GroupLayer {
|
||
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 GroupLayer {
|
||
/// Create a new group layer
|
||
pub fn new(name: impl Into<String>) -> Self {
|
||
Self {
|
||
layer: Layer::new(LayerType::Group, name),
|
||
children: Vec::new(),
|
||
expanded: true,
|
||
}
|
||
}
|
||
|
||
/// Add a child layer to this group
|
||
pub fn add_child(&mut self, layer: AnyLayer) {
|
||
self.children.push(layer);
|
||
}
|
||
|
||
/// Get clip instances from all child layers as (child_layer_id, &ClipInstance) pairs
|
||
pub fn all_child_clip_instances(&self) -> Vec<(Uuid, &ClipInstance)> {
|
||
let mut result = Vec::new();
|
||
for child in &self.children {
|
||
let child_id = child.id();
|
||
let instances: &[ClipInstance] = match child {
|
||
AnyLayer::Audio(l) => &l.clip_instances,
|
||
AnyLayer::Video(l) => &l.clip_instances,
|
||
AnyLayer::Vector(l) => &l.clip_instances,
|
||
AnyLayer::Effect(l) => &l.clip_instances,
|
||
AnyLayer::Group(_) => &[], // no nested groups
|
||
AnyLayer::Raster(_) => &[], // raster layers have no clip instances
|
||
};
|
||
for ci in instances {
|
||
result.push((child_id, ci));
|
||
}
|
||
}
|
||
result
|
||
}
|
||
}
|
||
|
||
/// Unified layer enum for polymorphic handling
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub enum AnyLayer {
|
||
Vector(VectorLayer),
|
||
Audio(AudioLayer),
|
||
Video(VideoLayer),
|
||
Effect(EffectLayer),
|
||
Group(GroupLayer),
|
||
Raster(RasterLayer),
|
||
}
|
||
|
||
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(),
|
||
AnyLayer::Group(l) => l.id(),
|
||
AnyLayer::Raster(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(),
|
||
AnyLayer::Group(l) => l.name(),
|
||
AnyLayer::Raster(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),
|
||
AnyLayer::Group(l) => l.set_name(name),
|
||
AnyLayer::Raster(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(),
|
||
AnyLayer::Group(l) => l.has_custom_name(),
|
||
AnyLayer::Raster(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),
|
||
AnyLayer::Group(l) => l.set_has_custom_name(custom),
|
||
AnyLayer::Raster(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(),
|
||
AnyLayer::Group(l) => l.visible(),
|
||
AnyLayer::Raster(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),
|
||
AnyLayer::Group(l) => l.set_visible(visible),
|
||
AnyLayer::Raster(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(),
|
||
AnyLayer::Group(l) => l.opacity(),
|
||
AnyLayer::Raster(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),
|
||
AnyLayer::Group(l) => l.set_opacity(opacity),
|
||
AnyLayer::Raster(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(),
|
||
AnyLayer::Group(l) => l.volume(),
|
||
AnyLayer::Raster(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),
|
||
AnyLayer::Group(l) => l.set_volume(volume),
|
||
AnyLayer::Raster(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(),
|
||
AnyLayer::Group(l) => l.muted(),
|
||
AnyLayer::Raster(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),
|
||
AnyLayer::Group(l) => l.set_muted(muted),
|
||
AnyLayer::Raster(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(),
|
||
AnyLayer::Group(l) => l.soloed(),
|
||
AnyLayer::Raster(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),
|
||
AnyLayer::Group(l) => l.set_soloed(soloed),
|
||
AnyLayer::Raster(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(),
|
||
AnyLayer::Group(l) => l.locked(),
|
||
AnyLayer::Raster(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),
|
||
AnyLayer::Group(l) => l.set_locked(locked),
|
||
AnyLayer::Raster(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,
|
||
AnyLayer::Group(l) => &l.layer,
|
||
AnyLayer::Raster(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,
|
||
AnyLayer::Group(l) => &mut l.layer,
|
||
AnyLayer::Raster(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);
|
||
}
|
||
}
|