230 lines
6.0 KiB
Rust
230 lines
6.0 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()
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
}
|