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

354 lines
9.8 KiB
Rust

//! Document structure for Lightningbeam
//!
//! The Document represents a complete animation project with settings
//! and a root graphics object containing the scene graph.
use crate::clip::{AudioClip, ImageAsset, VideoClip, VectorClip};
use crate::layer::AnyLayer;
use crate::shape::ShapeColor;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
/// Root graphics object containing all layers in the scene
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GraphicsObject {
/// Unique identifier
pub id: Uuid,
/// Name of this graphics object
pub name: String,
/// Child layers
pub children: Vec<AnyLayer>,
}
impl GraphicsObject {
/// Create a new graphics object
pub fn new(name: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
children: Vec::new(),
}
}
/// Add a layer as a child
pub fn add_child(&mut self, layer: AnyLayer) -> Uuid {
let id = layer.id();
self.children.push(layer);
id
}
/// Get a child layer by ID
pub fn get_child(&self, id: &Uuid) -> Option<&AnyLayer> {
self.children.iter().find(|l| &l.id() == id)
}
/// Get a mutable child layer by ID
pub fn get_child_mut(&mut self, id: &Uuid) -> Option<&mut AnyLayer> {
self.children.iter_mut().find(|l| &l.id() == id)
}
/// Remove a child layer by ID
pub fn remove_child(&mut self, id: &Uuid) -> Option<AnyLayer> {
if let Some(index) = self.children.iter().position(|l| &l.id() == id) {
Some(self.children.remove(index))
} else {
None
}
}
}
impl Default for GraphicsObject {
fn default() -> Self {
Self::new("Root")
}
}
/// Document settings and scene
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Document {
/// Unique identifier for this document
pub id: Uuid,
/// Document name
pub name: String,
/// Background color
pub background_color: ShapeColor,
/// Canvas width in pixels
pub width: f64,
/// Canvas height in pixels
pub height: f64,
/// Framerate (frames per second)
pub framerate: f64,
/// Duration in seconds
pub duration: f64,
/// Root graphics object containing all layers
pub root: GraphicsObject,
/// Clip libraries - reusable clip definitions
/// VectorClips can be instantiated multiple times with different transforms/timing
pub vector_clips: HashMap<Uuid, VectorClip>,
/// Video clip library - references to video files
pub video_clips: HashMap<Uuid, VideoClip>,
/// Audio clip library - sampled audio and MIDI clips
pub audio_clips: HashMap<Uuid, AudioClip>,
/// Image asset library - static images for fill textures
pub image_assets: HashMap<Uuid, ImageAsset>,
/// Current playback time in seconds
#[serde(skip)]
pub current_time: f64,
}
impl Default for Document {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
name: "Untitled".to_string(),
background_color: ShapeColor::rgb(255, 255, 255), // White background
width: 1920.0,
height: 1080.0,
framerate: 60.0,
duration: 10.0,
root: GraphicsObject::default(),
vector_clips: HashMap::new(),
video_clips: HashMap::new(),
audio_clips: HashMap::new(),
image_assets: HashMap::new(),
current_time: 0.0,
}
}
}
impl Document {
/// Create a new document with default settings
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
/// Create a document with custom dimensions
pub fn with_size(name: impl Into<String>, width: f64, height: f64) -> Self {
Self {
name: name.into(),
width,
height,
..Default::default()
}
}
/// Set the background color
pub fn with_background(mut self, color: ShapeColor) -> Self {
self.background_color = color;
self
}
/// Set the framerate
pub fn with_framerate(mut self, framerate: f64) -> Self {
self.framerate = framerate;
self
}
/// Set the duration
pub fn with_duration(mut self, duration: f64) -> Self {
self.duration = duration;
self
}
/// Get the aspect ratio
pub fn aspect_ratio(&self) -> f64 {
self.width / self.height
}
/// Set the current playback time
pub fn set_time(&mut self, time: f64) {
self.current_time = time.max(0.0).min(self.duration);
}
/// Get visible layers from the root graphics object
pub fn visible_layers(&self) -> impl Iterator<Item = &AnyLayer> {
self.root
.children
.iter()
.filter(|layer| layer.layer().visible)
}
/// Get a layer by ID
pub fn get_layer(&self, id: &Uuid) -> Option<&AnyLayer> {
self.root.get_child(id)
}
// === MUTATION METHODS (pub(crate) - only accessible to action module) ===
/// Get mutable access to the root graphics object
///
/// This method is intentionally `pub(crate)` to ensure mutations
/// only happen through the action system.
pub(crate) fn root_mut(&mut self) -> &mut GraphicsObject {
&mut self.root
}
/// Get mutable access to a layer by ID
///
/// This method is intentionally `pub(crate)` to ensure mutations
/// only happen through the action system.
pub fn get_layer_mut(&mut self, id: &Uuid) -> Option<&mut AnyLayer> {
self.root.get_child_mut(id)
}
// === CLIP LIBRARY METHODS ===
/// Add a vector clip to the library
pub fn add_vector_clip(&mut self, clip: VectorClip) -> Uuid {
let id = clip.id;
self.vector_clips.insert(id, clip);
id
}
/// Add a video clip to the library
pub fn add_video_clip(&mut self, clip: VideoClip) -> Uuid {
let id = clip.id;
self.video_clips.insert(id, clip);
id
}
/// Add an audio clip to the library
pub fn add_audio_clip(&mut self, clip: AudioClip) -> Uuid {
let id = clip.id;
self.audio_clips.insert(id, clip);
id
}
/// Get a vector clip by ID
pub fn get_vector_clip(&self, id: &Uuid) -> Option<&VectorClip> {
self.vector_clips.get(id)
}
/// Get a video clip by ID
pub fn get_video_clip(&self, id: &Uuid) -> Option<&VideoClip> {
self.video_clips.get(id)
}
/// Get an audio clip by ID
pub fn get_audio_clip(&self, id: &Uuid) -> Option<&AudioClip> {
self.audio_clips.get(id)
}
/// Get a mutable vector clip by ID
pub fn get_vector_clip_mut(&mut self, id: &Uuid) -> Option<&mut VectorClip> {
self.vector_clips.get_mut(id)
}
/// Get a mutable video clip by ID
pub fn get_video_clip_mut(&mut self, id: &Uuid) -> Option<&mut VideoClip> {
self.video_clips.get_mut(id)
}
/// Get a mutable audio clip by ID
pub fn get_audio_clip_mut(&mut self, id: &Uuid) -> Option<&mut AudioClip> {
self.audio_clips.get_mut(id)
}
/// Remove a vector clip from the library
pub fn remove_vector_clip(&mut self, id: &Uuid) -> Option<VectorClip> {
self.vector_clips.remove(id)
}
/// Remove a video clip from the library
pub fn remove_video_clip(&mut self, id: &Uuid) -> Option<VideoClip> {
self.video_clips.remove(id)
}
/// Remove an audio clip from the library
pub fn remove_audio_clip(&mut self, id: &Uuid) -> Option<AudioClip> {
self.audio_clips.remove(id)
}
// === IMAGE ASSET METHODS ===
/// Add an image asset to the library
pub fn add_image_asset(&mut self, asset: ImageAsset) -> Uuid {
let id = asset.id;
self.image_assets.insert(id, asset);
id
}
/// Get an image asset by ID
pub fn get_image_asset(&self, id: &Uuid) -> Option<&ImageAsset> {
self.image_assets.get(id)
}
/// Get a mutable image asset by ID
pub fn get_image_asset_mut(&mut self, id: &Uuid) -> Option<&mut ImageAsset> {
self.image_assets.get_mut(id)
}
/// Remove an image asset from the library
pub fn remove_image_asset(&mut self, id: &Uuid) -> Option<ImageAsset> {
self.image_assets.remove(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layer::{LayerTrait, VectorLayer};
#[test]
fn test_document_creation() {
let doc = Document::new("Test Project");
assert_eq!(doc.name, "Test Project");
assert_eq!(doc.width, 1920.0);
assert_eq!(doc.height, 1080.0);
assert_eq!(doc.root.children.len(), 0);
}
#[test]
fn test_graphics_object() {
let mut root = GraphicsObject::new("Root");
let vector_layer = VectorLayer::new("Layer 1");
let layer_id = root.add_child(AnyLayer::Vector(vector_layer));
assert_eq!(root.children.len(), 1);
assert!(root.get_child(&layer_id).is_some());
}
#[test]
fn test_document_with_layers() {
let mut doc = Document::new("Test");
let layer1 = VectorLayer::new("Layer 1");
let mut layer2 = VectorLayer::new("Layer 2");
// Hide layer2 to test visibility filtering
layer2.layer.visible = false;
doc.root.add_child(AnyLayer::Vector(layer1));
doc.root.add_child(AnyLayer::Vector(layer2));
// Only visible layers should be returned
assert_eq!(doc.visible_layers().count(), 1);
// Update layer2 to be visible via root access
let ids: Vec<_> = doc.root.children.iter().map(|n| n.id()).collect();
if let Some(layer) = doc.root.get_child_mut(&ids[1]) {
layer.set_visible(true);
}
assert_eq!(doc.visible_layers().count(), 2);
}
}