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

990 lines
32 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.

//! Rendering system for Lightningbeam documents
//!
//! Renders documents to Vello scenes for GPU-accelerated display.
use crate::animation::TransformProperty;
use crate::clip::ImageAsset;
use crate::document::Document;
use crate::layer::{AnyLayer, LayerTrait, VectorLayer};
use kurbo::{Affine, Shape};
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
use vello::kurbo::Rect;
use vello::peniko::{Blob, Fill, Image, ImageFormat};
use vello::Scene;
/// Cache for decoded image data to avoid re-decoding every frame
pub struct ImageCache {
cache: HashMap<Uuid, Arc<Image>>,
}
impl ImageCache {
/// Create a new empty image cache
pub fn new() -> Self {
Self {
cache: HashMap::new(),
}
}
/// Get or decode an image, caching the result
pub fn get_or_decode(&mut self, asset: &ImageAsset) -> Option<Arc<Image>> {
if let Some(cached) = self.cache.get(&asset.id) {
return Some(Arc::clone(cached));
}
// Decode and cache
let image = decode_image_asset(asset)?;
let arc_image = Arc::new(image);
self.cache.insert(asset.id, Arc::clone(&arc_image));
Some(arc_image)
}
/// Clear cache entry when an image asset is deleted or modified
pub fn invalidate(&mut self, id: &Uuid) {
self.cache.remove(id);
}
/// Clear all cached images
pub fn clear(&mut self) {
self.cache.clear();
}
}
impl Default for ImageCache {
fn default() -> Self {
Self::new()
}
}
/// Decode an image asset to peniko Image
fn decode_image_asset(asset: &ImageAsset) -> Option<Image> {
// Get the raw file data
let data = asset.data.as_ref()?;
// Decode using the image crate
let img = image::load_from_memory(data).ok()?;
let rgba = img.to_rgba8();
// Create peniko Image
Some(Image::new(
Blob::from(rgba.into_raw()),
ImageFormat::Rgba8,
asset.width,
asset.height,
))
}
/// Render a document to a Vello scene
pub fn render_document(
document: &Document,
scene: &mut Scene,
image_cache: &mut ImageCache,
video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
) {
render_document_with_transform(document, scene, Affine::IDENTITY, image_cache, video_manager);
}
/// Render a document to a Vello scene with a base transform
/// The base transform is composed with all object transforms (useful for camera zoom/pan)
pub fn render_document_with_transform(
document: &Document,
scene: &mut Scene,
base_transform: Affine,
image_cache: &mut ImageCache,
video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
) {
// 1. Draw background
render_background(document, scene, base_transform);
// 2. Recursively render the root graphics object at current time
let time = document.current_time;
render_graphics_object(document, time, scene, base_transform, image_cache, video_manager);
}
/// Draw the document background
fn render_background(document: &Document, scene: &mut Scene, base_transform: Affine) {
let background_rect = Rect::new(0.0, 0.0, document.width, document.height);
// Convert our ShapeColor to vello's peniko Color
let background_color = document.background_color.to_peniko();
scene.fill(
Fill::NonZero,
base_transform,
background_color,
None,
&background_rect,
);
}
/// Recursively render the root graphics object and its children
fn render_graphics_object(
document: &Document,
time: f64,
scene: &mut Scene,
base_transform: Affine,
image_cache: &mut ImageCache,
video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
) {
// Check if any layers are soloed
let any_soloed = document.visible_layers().any(|layer| layer.soloed());
// Render layers based on solo state
// If any layer is soloed, only render soloed layers
// Otherwise, render all visible layers
// Start with full opacity (1.0)
for layer in document.visible_layers() {
if any_soloed {
// Only render soloed layers when solo is active
if layer.soloed() {
render_layer(document, time, layer, scene, base_transform, 1.0, image_cache, video_manager);
}
} else {
// Render all visible layers when no solo is active
render_layer(document, time, layer, scene, base_transform, 1.0, image_cache, video_manager);
}
}
}
/// Render a single layer
fn render_layer(
document: &Document,
time: f64,
layer: &AnyLayer,
scene: &mut Scene,
base_transform: Affine,
parent_opacity: f64,
image_cache: &mut ImageCache,
video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
) {
match layer {
AnyLayer::Vector(vector_layer) => {
render_vector_layer(document, time, vector_layer, scene, base_transform, parent_opacity, image_cache, video_manager)
}
AnyLayer::Audio(_) => {
// Audio layers don't render visually
}
AnyLayer::Video(video_layer) => {
let mut video_mgr = video_manager.lock().unwrap();
render_video_layer(document, time, video_layer, scene, base_transform, parent_opacity, &mut video_mgr);
}
}
}
/// Render a clip instance (recursive rendering for nested compositions)
fn render_clip_instance(
document: &Document,
time: f64,
clip_instance: &crate::clip::ClipInstance,
parent_opacity: f64,
scene: &mut Scene,
base_transform: Affine,
animation_data: &crate::animation::AnimationData,
image_cache: &mut ImageCache,
video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
) {
// Try to find the clip in the document's clip libraries
// For now, only handle VectorClips (VideoClip and AudioClip rendering not yet implemented)
let Some(vector_clip) = document.vector_clips.get(&clip_instance.clip_id) else {
return; // Clip not found or not a vector clip
};
// Remap timeline time to clip's internal time
let Some(clip_time) = clip_instance.remap_time(time, vector_clip.duration) else {
return; // Clip instance not active at this time
};
// Evaluate animated transform properties
let transform = &clip_instance.transform;
let x = animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::X,
},
time,
transform.x,
);
let y = animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::Y,
},
time,
transform.y,
);
let rotation = animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::Rotation,
},
time,
transform.rotation,
);
let scale_x = animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::ScaleX,
},
time,
transform.scale_x,
);
let scale_y = animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::ScaleY,
},
time,
transform.scale_y,
);
let skew_x = animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::SkewX,
},
time,
transform.skew_x,
);
let skew_y = animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::SkewY,
},
time,
transform.skew_y,
);
// Build transform matrix (similar to shape instances)
// For clip instances, we don't have a path to calculate center from,
// so we use the clip's center point (width/2, height/2)
let center_x = vector_clip.width / 2.0;
let center_y = vector_clip.height / 2.0;
// Build skew transforms (applied around clip center)
let skew_transform = if skew_x != 0.0 || skew_y != 0.0 {
let skew_x_affine = if skew_x != 0.0 {
let tan_skew = skew_x.to_radians().tan();
Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
} else {
Affine::IDENTITY
};
let skew_y_affine = if skew_y != 0.0 {
let tan_skew = skew_y.to_radians().tan();
Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
} else {
Affine::IDENTITY
};
// Skew around center: translate to origin, skew, translate back
Affine::translate((center_x, center_y))
* skew_x_affine
* skew_y_affine
* Affine::translate((-center_x, -center_y))
} else {
Affine::IDENTITY
};
let clip_transform = Affine::translate((x, y))
* Affine::rotate(rotation.to_radians())
* Affine::scale_non_uniform(scale_x, scale_y)
* skew_transform;
let instance_transform = base_transform * clip_transform;
// Evaluate animated opacity
let opacity = animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::Opacity,
},
time,
clip_instance.opacity,
);
// Cascade opacity: parent_opacity × animated opacity
let clip_opacity = parent_opacity * opacity;
// Recursively render all root layers in the clip at the remapped time
for layer_node in vector_clip.layers.iter() {
// Skip invisible layers for performance
if !layer_node.data.visible() {
continue;
}
render_layer(document, clip_time, &layer_node.data, scene, instance_transform, clip_opacity, image_cache, video_manager);
}
}
/// Render a video layer with all its clip instances
fn render_video_layer(
document: &Document,
time: f64,
layer: &crate::layer::VideoLayer,
scene: &mut Scene,
base_transform: Affine,
parent_opacity: f64,
video_manager: &mut crate::video::VideoManager,
) {
use crate::animation::TransformProperty;
// Cascade opacity: parent_opacity × layer.opacity
let layer_opacity = parent_opacity * layer.layer.opacity;
// Render each video clip instance
for clip_instance in &layer.clip_instances {
// Get the video clip from the document
let Some(video_clip) = document.video_clips.get(&clip_instance.clip_id) else {
continue; // Clip not found
};
// Remap timeline time to clip's internal time
let Some(clip_time) = clip_instance.remap_time(time, video_clip.duration) else {
continue; // Clip instance not active at this time
};
// Get video frame from VideoManager
let Some(frame) = video_manager.get_frame(&clip_instance.clip_id, clip_time) else {
continue; // Frame not available
};
// Evaluate animated transform properties
let transform = &clip_instance.transform;
let x = layer.layer.animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::X,
},
time,
transform.x,
);
let y = layer.layer.animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::Y,
},
time,
transform.y,
);
let rotation = layer.layer.animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::Rotation,
},
time,
transform.rotation,
);
let scale_x = layer.layer.animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::ScaleX,
},
time,
transform.scale_x,
);
let scale_y = layer.layer.animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::ScaleY,
},
time,
transform.scale_y,
);
let skew_x = layer.layer.animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::SkewX,
},
time,
transform.skew_x,
);
let skew_y = layer.layer.animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::SkewY,
},
time,
transform.skew_y,
);
// Build skew transform (applied around center)
let center_x = video_clip.width / 2.0;
let center_y = video_clip.height / 2.0;
let skew_transform = if skew_x != 0.0 || skew_y != 0.0 {
let skew_x_affine = if skew_x != 0.0 {
let tan_skew = skew_x.to_radians().tan();
Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
} else {
Affine::IDENTITY
};
let skew_y_affine = if skew_y != 0.0 {
let tan_skew = skew_y.to_radians().tan();
Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
} else {
Affine::IDENTITY
};
// Skew around center
Affine::translate((center_x, center_y))
* skew_x_affine
* skew_y_affine
* Affine::translate((-center_x, -center_y))
} else {
Affine::IDENTITY
};
let clip_transform = Affine::translate((x, y))
* Affine::rotate(rotation.to_radians())
* Affine::scale_non_uniform(scale_x, scale_y)
* skew_transform;
let instance_transform = base_transform * clip_transform;
// Evaluate animated opacity
let opacity = layer.layer.animation_data.eval(
&crate::animation::AnimationTarget::Object {
id: clip_instance.id,
property: TransformProperty::Opacity,
},
time,
clip_instance.opacity,
);
// Cascade opacity: layer_opacity × animated opacity
let final_opacity = (layer_opacity * opacity) as f32;
// Create peniko Image from video frame data (zero-copy via Arc clone)
// Coerce Arc<Vec<u8>> to Arc<dyn AsRef<[u8]> + Send + Sync>
let blob_data: Arc<dyn AsRef<[u8]> + Send + Sync> = frame.rgba_data.clone();
let image = Image::new(
vello::peniko::Blob::new(blob_data),
vello::peniko::ImageFormat::Rgba8,
frame.width,
frame.height,
);
// Apply opacity
let image_with_alpha = image.with_alpha(final_opacity);
// Create rectangle path for the video frame
let video_rect = Rect::new(0.0, 0.0, video_clip.width, video_clip.height);
// Render video frame as image fill
scene.fill(
Fill::NonZero,
instance_transform,
&image_with_alpha,
None,
&video_rect,
);
}
}
/// Render a vector layer with all its clip instances and shape instances
fn render_vector_layer(
document: &Document,
time: f64,
layer: &VectorLayer,
scene: &mut Scene,
base_transform: Affine,
parent_opacity: f64,
image_cache: &mut ImageCache,
video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
) {
// Cascade opacity: parent_opacity × layer.opacity
let layer_opacity = parent_opacity * layer.layer.opacity;
// Render clip instances first (they appear under shape instances)
for clip_instance in &layer.clip_instances {
render_clip_instance(document, time, clip_instance, layer_opacity, scene, base_transform, &layer.layer.animation_data, image_cache, video_manager);
}
// Render each shape instance in the layer
for shape_instance in &layer.shape_instances {
// Get the shape for this instance
let Some(shape) = layer.get_shape(&shape_instance.shape_id) else {
continue;
};
// Evaluate animated properties
let transform = &shape_instance.transform;
let x = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Object {
id: shape_instance.id,
property: TransformProperty::X,
},
time,
transform.x,
);
let y = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Object {
id: shape_instance.id,
property: TransformProperty::Y,
},
time,
transform.y,
);
let rotation = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Object {
id: shape_instance.id,
property: TransformProperty::Rotation,
},
time,
transform.rotation,
);
let scale_x = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Object {
id: shape_instance.id,
property: TransformProperty::ScaleX,
},
time,
transform.scale_x,
);
let scale_y = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Object {
id: shape_instance.id,
property: TransformProperty::ScaleY,
},
time,
transform.scale_y,
);
let skew_x = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Object {
id: shape_instance.id,
property: TransformProperty::SkewX,
},
time,
transform.skew_x,
);
let skew_y = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Object {
id: shape_instance.id,
property: TransformProperty::SkewY,
},
time,
transform.skew_y,
);
let opacity = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Object {
id: shape_instance.id,
property: TransformProperty::Opacity,
},
time,
shape_instance.opacity,
);
// Check if shape has morphing animation
let shape_index = layer
.layer
.animation_data
.eval(
&crate::animation::AnimationTarget::Shape {
id: shape.id,
property: crate::animation::ShapeProperty::ShapeIndex,
},
time,
0.0,
);
// Get the morphed path
let path = shape.get_morphed_path(shape_index);
// Build transform matrix (compose with base transform for camera)
// Get shape center for skewing around center
let shape_bbox = shape.path().bounding_box();
let center_x = (shape_bbox.x0 + shape_bbox.x1) / 2.0;
let center_y = (shape_bbox.y0 + shape_bbox.y1) / 2.0;
// Build skew transforms (applied around shape center)
let skew_transform = if skew_x != 0.0 || skew_y != 0.0 {
let skew_x_affine = if skew_x != 0.0 {
let tan_skew = skew_x.to_radians().tan();
Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
} else {
Affine::IDENTITY
};
let skew_y_affine = if skew_y != 0.0 {
let tan_skew = skew_y.to_radians().tan();
Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
} else {
Affine::IDENTITY
};
// Skew around center: translate to origin, skew, translate back
Affine::translate((center_x, center_y))
* skew_x_affine
* skew_y_affine
* Affine::translate((-center_x, -center_y))
} else {
Affine::IDENTITY
};
let object_transform = Affine::translate((x, y))
* Affine::rotate(rotation.to_radians())
* Affine::scale_non_uniform(scale_x, scale_y)
* skew_transform;
let affine = base_transform * object_transform;
// Calculate final opacity (cascaded from parent → layer → shape instance)
// layer_opacity already includes parent_opacity from render_vector_layer
let final_opacity = (layer_opacity * opacity) as f32;
// Determine fill rule
let fill_rule = match shape.fill_rule {
crate::shape::FillRule::NonZero => Fill::NonZero,
crate::shape::FillRule::EvenOdd => Fill::EvenOdd,
};
// Render fill - prefer image fill over color fill
let mut filled = false;
// Check for image fill first
if let Some(image_asset_id) = shape.image_fill {
if let Some(image_asset) = document.get_image_asset(&image_asset_id) {
if let Some(image) = image_cache.get_or_decode(image_asset) {
// Apply opacity to image (clone is cheap - Image uses Arc<Blob> internally)
let image_with_alpha = (*image).clone().with_alpha(final_opacity);
// The image is rendered as a fill for the shape path
// Since the shape path is a rectangle matching the image dimensions,
// the image should fill the shape perfectly
scene.fill(fill_rule, affine, &image_with_alpha, None, &path);
filled = true;
}
}
}
// Fall back to color fill if no image fill (or image failed to load)
if !filled {
if let Some(fill_color) = &shape.fill_color {
// Apply opacity to color
let alpha = ((fill_color.a as f32 / 255.0) * final_opacity * 255.0) as u8;
let adjusted_color = crate::shape::ShapeColor::rgba(
fill_color.r,
fill_color.g,
fill_color.b,
alpha,
);
scene.fill(
fill_rule,
affine,
adjusted_color.to_peniko(),
None,
&path,
);
}
}
// Render stroke if present
if let (Some(stroke_color), Some(stroke_style)) = (&shape.stroke_color, &shape.stroke_style)
{
// Apply opacity to color
let alpha = ((stroke_color.a as f32 / 255.0) * final_opacity * 255.0) as u8;
let adjusted_color = crate::shape::ShapeColor::rgba(
stroke_color.r,
stroke_color.g,
stroke_color.b,
alpha,
);
scene.stroke(
&stroke_style.to_stroke(),
affine,
adjusted_color.to_peniko(),
None,
&path,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Document;
use crate::layer::{AnyLayer, LayerTrait, VectorLayer};
use crate::object::ShapeInstance;
use crate::shape::{Shape, ShapeColor};
use kurbo::{Circle, Shape as KurboShape};
#[test]
fn test_render_empty_document() {
let doc = Document::new("Test");
let mut scene = Scene::new();
let mut image_cache = ImageCache::new();
render_document(&doc, &mut scene, &mut image_cache);
// Should render background without errors
}
#[test]
fn test_render_document_with_shape() {
let mut doc = Document::new("Test");
// Create a simple circle shape
let circle = Circle::new((100.0, 100.0), 50.0);
let path = circle.to_path(0.1);
let shape = Shape::new(path).with_fill(ShapeColor::rgb(255, 0, 0));
// Create a shape instance for the shape
let shape_instance = ShapeInstance::new(shape.id);
// Create a vector layer
let mut vector_layer = VectorLayer::new("Layer 1");
vector_layer.add_shape(shape);
vector_layer.add_object(shape_instance);
// Add to document
doc.root.add_child(AnyLayer::Vector(vector_layer));
// Render
let mut scene = Scene::new();
let mut image_cache = ImageCache::new();
render_document(&doc, &mut scene, &mut image_cache);
// Should render without errors
}
// === Solo Rendering Tests ===
/// Helper to check if any layer is soloed in document
fn has_soloed_layer(doc: &Document) -> bool {
doc.visible_layers().any(|layer| layer.soloed())
}
/// Helper to count visible layers for rendering (respecting solo)
fn count_layers_to_render(doc: &Document) -> usize {
let any_soloed = has_soloed_layer(doc);
doc.visible_layers()
.filter(|layer| {
if any_soloed {
layer.soloed()
} else {
true
}
})
.count()
}
#[test]
fn test_no_solo_all_layers_render() {
let mut doc = Document::new("Test");
// Add two visible layers, neither soloed
let layer1 = VectorLayer::new("Layer 1");
let layer2 = VectorLayer::new("Layer 2");
doc.root.add_child(AnyLayer::Vector(layer1));
doc.root.add_child(AnyLayer::Vector(layer2));
// Both should be rendered
assert_eq!(has_soloed_layer(&doc), false);
assert_eq!(count_layers_to_render(&doc), 2);
// Render should work without errors
let mut scene = Scene::new();
let mut image_cache = ImageCache::new();
render_document(&doc, &mut scene, &mut image_cache);
}
#[test]
fn test_one_layer_soloed() {
let mut doc = Document::new("Test");
// Add two layers
let mut layer1 = VectorLayer::new("Layer 1");
let layer2 = VectorLayer::new("Layer 2");
// Solo layer 1
layer1.layer.soloed = true;
doc.root.add_child(AnyLayer::Vector(layer1));
doc.root.add_child(AnyLayer::Vector(layer2));
// Only soloed layer should be rendered
assert_eq!(has_soloed_layer(&doc), true);
assert_eq!(count_layers_to_render(&doc), 1);
// Verify the soloed layer is the one that would render
let any_soloed = has_soloed_layer(&doc);
let soloed_count: usize = doc.visible_layers()
.filter(|l| any_soloed && l.soloed())
.count();
assert_eq!(soloed_count, 1);
// Render should work
let mut scene = Scene::new();
let mut image_cache = ImageCache::new();
render_document(&doc, &mut scene, &mut image_cache);
}
#[test]
fn test_multiple_layers_soloed() {
let mut doc = Document::new("Test");
// Add three layers
let mut layer1 = VectorLayer::new("Layer 1");
let mut layer2 = VectorLayer::new("Layer 2");
let layer3 = VectorLayer::new("Layer 3");
// Solo layers 1 and 2
layer1.layer.soloed = true;
layer2.layer.soloed = true;
doc.root.add_child(AnyLayer::Vector(layer1));
doc.root.add_child(AnyLayer::Vector(layer2));
doc.root.add_child(AnyLayer::Vector(layer3));
// Only soloed layers (1 and 2) should render
assert_eq!(has_soloed_layer(&doc), true);
assert_eq!(count_layers_to_render(&doc), 2);
// Render
let mut scene = Scene::new();
let mut image_cache = ImageCache::new();
render_document(&doc, &mut scene, &mut image_cache);
}
#[test]
fn test_hidden_layer_not_rendered() {
let mut doc = Document::new("Test");
let layer1 = VectorLayer::new("Layer 1");
let mut layer2 = VectorLayer::new("Layer 2");
// Hide layer 2
layer2.layer.visible = false;
doc.root.add_child(AnyLayer::Vector(layer1));
doc.root.add_child(AnyLayer::Vector(layer2));
// Only visible layer (1) should be considered
assert_eq!(doc.visible_layers().count(), 1);
// Render
let mut scene = Scene::new();
let mut image_cache = ImageCache::new();
render_document(&doc, &mut scene, &mut image_cache);
}
#[test]
fn test_hidden_but_soloed_layer() {
// A hidden layer that is soloed shouldn't render
// because visible_layers() filters out hidden layers first
let mut doc = Document::new("Test");
let layer1 = VectorLayer::new("Layer 1");
let mut layer2 = VectorLayer::new("Layer 2");
// Layer 2: soloed but hidden
layer2.layer.soloed = true;
layer2.layer.visible = false;
doc.root.add_child(AnyLayer::Vector(layer1));
doc.root.add_child(AnyLayer::Vector(layer2));
// visible_layers only returns layer 1 (layer 2 is hidden)
// Since layer 1 isn't soloed and no visible layers are soloed,
// all visible layers render
let any_soloed = has_soloed_layer(&doc);
assert_eq!(any_soloed, false); // No *visible* layer is soloed
// Both visible layers render (only 1 is visible)
assert_eq!(count_layers_to_render(&doc), 1);
// Render
let mut scene = Scene::new();
let mut image_cache = ImageCache::new();
render_document(&doc, &mut scene, &mut image_cache);
}
#[test]
fn test_solo_with_layer_opacity() {
let mut doc = Document::new("Test");
// Create layers with different opacities
let mut layer1 = VectorLayer::new("Layer 1");
let mut layer2 = VectorLayer::new("Layer 2");
layer1.layer.opacity = 0.5;
layer1.layer.soloed = true;
layer2.layer.opacity = 0.8;
// Add circle shapes for visible rendering
let circle = Circle::new((50.0, 50.0), 20.0);
let path = circle.to_path(0.1);
let shape = Shape::new(path).with_fill(ShapeColor::rgb(255, 0, 0));
let shape_instance = ShapeInstance::new(shape.id);
layer1.add_shape(shape.clone());
layer1.add_object(shape_instance);
let shape2 = Shape::new(circle.to_path(0.1)).with_fill(ShapeColor::rgb(0, 255, 0));
let shape_instance2 = ShapeInstance::new(shape2.id);
layer2.add_shape(shape2);
layer2.add_object(shape_instance2);
doc.root.add_child(AnyLayer::Vector(layer1));
doc.root.add_child(AnyLayer::Vector(layer2));
// Only layer 1 (soloed with 0.5 opacity) should render
assert_eq!(has_soloed_layer(&doc), true);
assert_eq!(count_layers_to_render(&doc), 1);
// Render
let mut scene = Scene::new();
let mut image_cache = ImageCache::new();
render_document(&doc, &mut scene, &mut image_cache);
}
#[test]
fn test_unsolo_returns_to_normal() {
let mut doc = Document::new("Test");
let mut layer1 = VectorLayer::new("Layer 1");
let mut layer2 = VectorLayer::new("Layer 2");
// First, solo layer 1
layer1.layer.soloed = true;
let id1 = doc.root.add_child(AnyLayer::Vector(layer1));
let id2 = doc.root.add_child(AnyLayer::Vector(layer2));
// Only 1 layer renders when soloed
assert_eq!(count_layers_to_render(&doc), 1);
// Now unsolo layer 1
if let Some(layer) = doc.root.get_child_mut(&id1) {
layer.set_soloed(false);
}
// Now both should render again
assert_eq!(has_soloed_layer(&doc), false);
assert_eq!(count_layers_to_render(&doc), 2);
}
}