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

245 lines
6.8 KiB
Rust
Raw Permalink 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.

//! 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);
}
}