245 lines
6.8 KiB
Rust
245 lines
6.8 KiB
Rust
//! Object system for Lightningbeam
|
||
//!
|
||
//! An Object represents an instance of a Shape with transform properties.
|
||
//! Objects can be animated via the animation system.
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use uuid::Uuid;
|
||
use vello::kurbo::Shape as KurboShape;
|
||
|
||
/// 2D transform for an object
|
||
/// Contains only geometric transformations (position, rotation, scale, skew)
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct Transform {
|
||
/// X position
|
||
pub x: f64,
|
||
/// Y position
|
||
pub y: f64,
|
||
/// Rotation in degrees
|
||
pub rotation: f64,
|
||
/// X scale factor
|
||
pub scale_x: f64,
|
||
/// Y scale factor
|
||
pub scale_y: f64,
|
||
/// X skew in degrees
|
||
pub skew_x: f64,
|
||
/// Y skew in degrees
|
||
pub skew_y: f64,
|
||
}
|
||
|
||
impl Default for Transform {
|
||
fn default() -> Self {
|
||
Self {
|
||
x: 0.0,
|
||
y: 0.0,
|
||
rotation: 0.0,
|
||
scale_x: 1.0,
|
||
scale_y: 1.0,
|
||
skew_x: 0.0,
|
||
skew_y: 0.0,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Transform {
|
||
/// Create a new default transform
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
/// Set scale + position so `content_w × content_h` is fit **uniformly and centered** within
|
||
/// `doc_w × doc_h`, preserving aspect ratio (letterbox/pillarbox). The single shared way to
|
||
/// place a media clip (video/image) on the document — both import paths use this so a clip
|
||
/// looks identical however it was added. No-op for non-positive content dimensions.
|
||
pub fn fit_centered(&mut self, content_w: f64, content_h: f64, doc_w: f64, doc_h: f64) {
|
||
if content_w <= 0.0 || content_h <= 0.0 {
|
||
return;
|
||
}
|
||
let scale = (doc_w / content_w).min(doc_h / content_h);
|
||
self.scale_x = scale;
|
||
self.scale_y = scale;
|
||
self.x = (doc_w - content_w * scale) / 2.0;
|
||
self.y = (doc_h - content_h * scale) / 2.0;
|
||
}
|
||
|
||
/// Create a transform with position
|
||
pub fn with_position(x: f64, y: f64) -> Self {
|
||
Self {
|
||
x,
|
||
y,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// Create a transform with rotation
|
||
pub fn with_rotation(rotation: f64) -> Self {
|
||
Self {
|
||
rotation,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// Set position
|
||
pub fn set_position(&mut self, x: f64, y: f64) {
|
||
self.x = x;
|
||
self.y = y;
|
||
}
|
||
|
||
/// Set rotation
|
||
pub fn set_rotation(&mut self, rotation: f64) {
|
||
self.rotation = rotation;
|
||
}
|
||
|
||
/// Set scale
|
||
pub fn set_scale(&mut self, scale_x: f64, scale_y: f64) {
|
||
self.scale_x = scale_x;
|
||
self.scale_y = scale_y;
|
||
}
|
||
|
||
/// Set uniform scale
|
||
pub fn set_uniform_scale(&mut self, scale: f64) {
|
||
self.scale_x = scale;
|
||
self.scale_y = scale;
|
||
}
|
||
|
||
/// Convert to an affine transform matrix
|
||
/// Note: Skew is applied in local space. For proper centering, the shape's
|
||
/// bounding box center should be used (see renderer.rs for the full implementation).
|
||
pub fn to_affine(&self) -> kurbo::Affine {
|
||
use kurbo::Affine;
|
||
|
||
// Build transform: translate * rotate * scale * skew
|
||
let translate = Affine::translate((self.x, self.y));
|
||
let rotate = Affine::rotate(self.rotation.to_radians());
|
||
let scale = Affine::scale_non_uniform(self.scale_x, self.scale_y);
|
||
|
||
// Skew transforms (applied in local space)
|
||
let skew_x = if self.skew_x != 0.0 {
|
||
let tan_skew = self.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 = if self.skew_y != 0.0 {
|
||
let tan_skew = self.skew_y.to_radians().tan();
|
||
Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
|
||
} else {
|
||
Affine::IDENTITY
|
||
};
|
||
|
||
translate * rotate * scale * skew_x * skew_y
|
||
}
|
||
}
|
||
|
||
/// A shape instance (shape with transform)
|
||
/// Represents an instance of a Shape with its own transform properties.
|
||
/// Multiple instances can reference the same shape.
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct ShapeInstance {
|
||
/// Unique identifier
|
||
pub id: Uuid,
|
||
|
||
/// Reference to the shape this object uses
|
||
pub shape_id: Uuid,
|
||
|
||
/// Transform properties (position, rotation, scale, skew)
|
||
pub transform: Transform,
|
||
|
||
/// Opacity (0.0 to 1.0, separate from geometric transform)
|
||
pub opacity: f64,
|
||
|
||
/// Name for display in UI
|
||
pub name: Option<String>,
|
||
}
|
||
|
||
impl ShapeInstance {
|
||
/// Create a new shape instance for a shape
|
||
pub fn new(shape_id: Uuid) -> Self {
|
||
Self {
|
||
id: Uuid::new_v4(),
|
||
shape_id,
|
||
transform: Transform::default(),
|
||
opacity: 1.0,
|
||
name: None,
|
||
}
|
||
}
|
||
|
||
/// Create a new shape instance with a specific ID
|
||
pub fn with_id(id: Uuid, shape_id: Uuid) -> Self {
|
||
Self {
|
||
id,
|
||
shape_id,
|
||
transform: Transform::default(),
|
||
opacity: 1.0,
|
||
name: None,
|
||
}
|
||
}
|
||
|
||
/// Set the name
|
||
pub fn with_name(mut self, name: impl Into<String>) -> Self {
|
||
self.name = Some(name.into());
|
||
self
|
||
}
|
||
|
||
/// Set the transform
|
||
pub fn with_transform(mut self, transform: Transform) -> Self {
|
||
self.transform = transform;
|
||
self
|
||
}
|
||
|
||
/// Set position
|
||
pub fn with_position(mut self, x: f64, y: f64) -> Self {
|
||
self.transform.set_position(x, y);
|
||
self
|
||
}
|
||
|
||
/// Convert shape instance transform to affine matrix
|
||
pub fn to_affine(&self) -> kurbo::Affine {
|
||
self.transform.to_affine()
|
||
}
|
||
|
||
/// Get the bounding box of this shape instance given its shape
|
||
///
|
||
/// Returns the bounding box in the instance's parent coordinate space
|
||
/// (i.e., with the instance's transform applied).
|
||
pub fn bounding_box(&self, shape: &crate::shape::Shape) -> kurbo::Rect {
|
||
let path_bbox = shape.path().bounding_box();
|
||
self.to_affine().transform_rect_bbox(path_bbox)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_transform_default() {
|
||
let transform = Transform::default();
|
||
assert_eq!(transform.x, 0.0);
|
||
assert_eq!(transform.y, 0.0);
|
||
assert_eq!(transform.scale_x, 1.0);
|
||
assert_eq!(transform.scale_y, 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_transform_affine() {
|
||
let mut transform = Transform::default();
|
||
transform.set_position(100.0, 200.0);
|
||
transform.set_rotation(45.0);
|
||
|
||
let affine = transform.to_affine();
|
||
// Just verify it doesn't panic
|
||
let _ = affine.as_coeffs();
|
||
}
|
||
|
||
#[test]
|
||
fn test_shape_instance_creation() {
|
||
let shape_id = Uuid::new_v4();
|
||
let shape_instance = ShapeInstance::new(shape_id);
|
||
|
||
assert_eq!(shape_instance.shape_id, shape_id);
|
||
assert_eq!(shape_instance.transform.x, 0.0);
|
||
}
|
||
}
|