248 lines
9.3 KiB
Rust
248 lines
9.3 KiB
Rust
//! Raster (pixel-buffer) layer for Lightningbeam
|
||
//!
|
||
//! Each keyframe holds a PNG-compressed pixel buffer stored in the .beam ZIP
|
||
//! under `media/raster/<uuid>.png`. A brush engine renders dabs along strokes
|
||
//! and the resulting RGBA image is composited into the Vello scene.
|
||
|
||
use crate::brush_settings::BrushSettings;
|
||
use crate::layer::{Layer, LayerTrait, LayerType};
|
||
use serde::{Deserialize, Serialize};
|
||
use uuid::Uuid;
|
||
|
||
/// How a raster stroke blends onto the layer buffer
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||
pub enum RasterBlendMode {
|
||
/// Normal alpha-compositing (paint over)
|
||
Normal,
|
||
/// Erase pixels (reduce alpha)
|
||
Erase,
|
||
/// Smudge / blend surrounding pixels
|
||
Smudge,
|
||
/// Clone stamp: copy pixels from a source region
|
||
CloneStamp,
|
||
/// Healing brush: color-corrected clone stamp (preserves source texture, shifts color to match destination)
|
||
Healing,
|
||
/// Pattern stamp: paint with a repeating procedural tile pattern
|
||
PatternStamp,
|
||
/// Dodge / Burn: lighten (dodge) or darken (burn) existing pixels
|
||
DodgeBurn,
|
||
/// Sponge: saturate or desaturate existing pixels
|
||
Sponge,
|
||
/// Blur / Sharpen: soften or crisp up existing pixels
|
||
BlurSharpen,
|
||
}
|
||
|
||
impl Default for RasterBlendMode {
|
||
fn default() -> Self {
|
||
Self::Normal
|
||
}
|
||
}
|
||
|
||
impl RasterBlendMode {
|
||
/// Returns false for blend modes that operate on existing pixels and don't
|
||
/// use the brush color at all (clone, heal, dodge/burn, sponge).
|
||
/// Used by brush_engine.rs to decide whether color_a should be 1.0 or stroke.color[3].
|
||
pub fn uses_brush_color(self) -> bool {
|
||
!matches!(self, Self::CloneStamp | Self::Healing | Self::DodgeBurn | Self::Sponge | Self::BlurSharpen)
|
||
}
|
||
}
|
||
|
||
/// A single point along a stroke
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct StrokePoint {
|
||
pub x: f32,
|
||
pub y: f32,
|
||
/// Pen/tablet pressure 0.0–1.0 (mouse uses 0.5)
|
||
pub pressure: f32,
|
||
/// Pen tilt X in radians
|
||
pub tilt_x: f32,
|
||
/// Pen tilt Y in radians
|
||
pub tilt_y: f32,
|
||
/// Seconds elapsed since start of this stroke
|
||
pub timestamp: f64,
|
||
}
|
||
|
||
/// Record of a single brush stroke applied to a keyframe
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct StrokeRecord {
|
||
pub brush_settings: BrushSettings,
|
||
/// RGBA linear color [r, g, b, a]
|
||
pub color: [f32; 4],
|
||
pub blend_mode: RasterBlendMode,
|
||
/// Generic tool parameters — encoding depends on blend_mode:
|
||
/// - CloneStamp / Healing: [offset_x, offset_y, 0, 0]
|
||
/// - PatternStamp: [pattern_type, pattern_scale, 0, 0]
|
||
/// - DodgeBurn / Sponge: [mode, 0, 0, 0]
|
||
/// - all others: [0, 0, 0, 0]
|
||
#[serde(default)]
|
||
pub tool_params: [f32; 4],
|
||
pub points: Vec<StrokePoint>,
|
||
}
|
||
|
||
/// Specifies how the raster content transitions to the next keyframe
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||
pub enum TweenType {
|
||
/// Hold the pixel buffer until the next keyframe
|
||
Hold,
|
||
}
|
||
|
||
impl Default for TweenType {
|
||
fn default() -> Self {
|
||
Self::Hold
|
||
}
|
||
}
|
||
|
||
/// A single keyframe of a raster layer
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct RasterKeyframe {
|
||
/// Unique ID for this keyframe (used as pixel-cache key)
|
||
pub id: Uuid,
|
||
/// Time position in seconds
|
||
pub time: f64,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
/// ZIP-relative path: `"media/raster/<uuid>.png"`
|
||
pub media_path: String,
|
||
/// Stroke history (for potential replay / future non-destructive editing)
|
||
pub stroke_log: Vec<StrokeRecord>,
|
||
pub tween_after: TweenType,
|
||
/// Raw RGBA pixel buffer (width × height × 4 bytes).
|
||
///
|
||
/// This is the working in-memory representation used by the brush engine and renderer.
|
||
/// NOT serialized to the document JSON — populated from the ZIP's PNG on load,
|
||
/// and encoded back to PNG on save. An empty Vec means the canvas is blank (transparent).
|
||
#[serde(skip)]
|
||
pub raw_pixels: Vec<u8>,
|
||
/// Set to `true` whenever `raw_pixels` changes so the GPU texture cache can re-upload.
|
||
/// Always `true` after load; cleared by the renderer after uploading.
|
||
#[serde(skip, default = "default_true")]
|
||
pub texture_dirty: bool,
|
||
/// Phase 3 paging: the keyframe's pixels live in the container and must be
|
||
/// faulted in (`raw_pixels` empty *and* this true ⇒ page in from the store).
|
||
/// A *new* keyframe is `false` (intentionally blank/resident, nothing to load);
|
||
/// set true on load and again when evicted. Never serialized.
|
||
#[serde(skip)]
|
||
pub needs_fault_in: bool,
|
||
/// Phase 3a eviction: set `true` whenever user editing mutates `raw_pixels`
|
||
/// (brush, fill, paint-bucket, floating-selection commit/lift, undo/redo of
|
||
/// those). A dirty keyframe's current pixels are NOT yet persisted in the
|
||
/// container, so it must NEVER be evicted (doing so would silently lose the
|
||
/// unsaved edit). Cleared on a successful save. Never serialized.
|
||
#[serde(skip)]
|
||
pub dirty: bool,
|
||
}
|
||
|
||
fn default_true() -> bool { true }
|
||
|
||
impl RasterKeyframe {
|
||
/// Returns true when the pixel buffer has been initialised (non-blank).
|
||
pub fn has_pixels(&self) -> bool {
|
||
!self.raw_pixels.is_empty()
|
||
}
|
||
|
||
pub fn new(time: f64, width: u32, height: u32) -> Self {
|
||
let id = Uuid::new_v4();
|
||
let media_path = format!("media/raster/{}.png", id);
|
||
Self {
|
||
id,
|
||
time,
|
||
width,
|
||
height,
|
||
media_path,
|
||
stroke_log: Vec::new(),
|
||
tween_after: TweenType::Hold,
|
||
raw_pixels: Vec::new(),
|
||
texture_dirty: true,
|
||
needs_fault_in: false,
|
||
dirty: false,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A pixel-buffer painting layer
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct RasterLayer {
|
||
/// Base layer properties (id, name, opacity, visibility, …)
|
||
pub layer: Layer,
|
||
/// Keyframes sorted by time
|
||
pub keyframes: Vec<RasterKeyframe>,
|
||
}
|
||
|
||
impl RasterLayer {
|
||
/// Create a new raster layer with the given name
|
||
pub fn new(name: impl Into<String>) -> Self {
|
||
Self {
|
||
layer: Layer::new(LayerType::Raster, name),
|
||
keyframes: Vec::new(),
|
||
}
|
||
}
|
||
|
||
// === Keyframe accessors ===
|
||
|
||
/// Get the active keyframe at-or-before `time`
|
||
pub fn keyframe_at(&self, time: f64) -> Option<&RasterKeyframe> {
|
||
let idx = self.keyframes.partition_point(|kf| kf.time <= time);
|
||
if idx > 0 { Some(&self.keyframes[idx - 1]) } else { None }
|
||
}
|
||
|
||
/// Get a mutable reference to the active keyframe at-or-before `time`
|
||
pub fn keyframe_at_mut(&mut self, time: f64) -> Option<&mut RasterKeyframe> {
|
||
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 exactly `time` (within tolerance)
|
||
fn keyframe_index_at_exact(&self, time: f64, tolerance: f64) -> Option<usize> {
|
||
self.keyframes.iter().position(|kf| (kf.time - time).abs() < tolerance)
|
||
}
|
||
|
||
/// Ensure a keyframe exists at `time`; create one (with given dimensions) if not.
|
||
///
|
||
/// If `width`/`height` are 0 the new keyframe inherits dimensions from the
|
||
/// previous active keyframe, falling back to 1920×1080.
|
||
pub fn ensure_keyframe_at(&mut self, time: f64, width: u32, height: u32) -> &mut RasterKeyframe {
|
||
let tolerance = 0.001;
|
||
if let Some(idx) = self.keyframe_index_at_exact(time, tolerance) {
|
||
return &mut self.keyframes[idx];
|
||
}
|
||
|
||
let (w, h) = if width == 0 || height == 0 {
|
||
self.keyframe_at(time)
|
||
.map(|kf| (kf.width, kf.height))
|
||
.unwrap_or((1920, 1080))
|
||
} else {
|
||
(width, height)
|
||
};
|
||
|
||
let insert_idx = self.keyframes.partition_point(|kf| kf.time < time);
|
||
self.keyframes.insert(insert_idx, RasterKeyframe::new(time, w, h));
|
||
&mut self.keyframes[insert_idx]
|
||
}
|
||
|
||
/// Return the ZIP-relative PNG path for the active keyframe at `time`, or `None`.
|
||
pub fn buffer_path_at_time(&self, time: f64) -> Option<&str> {
|
||
self.keyframe_at(time).map(|kf| kf.media_path.as_str())
|
||
}
|
||
}
|
||
|
||
// Delegate all LayerTrait methods to self.layer
|
||
impl LayerTrait for RasterLayer {
|
||
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; }
|
||
}
|