Transform shapes
This commit is contained in:
parent
67724c944c
commit
9204308033
|
|
@ -2737,6 +2737,7 @@ dependencies = [
|
|||
name = "lightningbeam-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"kurbo 0.11.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ edition = "2021"
|
|||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# UI framework (for Color32 conversion)
|
||||
egui = "0.29"
|
||||
|
||||
# Geometry and rendering
|
||||
kurbo = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -64,6 +64,13 @@ impl ActionExecutor {
|
|||
&self.document
|
||||
}
|
||||
|
||||
/// Get mutable access to the document
|
||||
/// Note: This should only be used for live previews. Permanent changes
|
||||
/// should go through the execute() method to support undo/redo.
|
||||
pub fn document_mut(&mut self) -> &mut Document {
|
||||
&mut self.document
|
||||
}
|
||||
|
||||
/// Execute an action and add it to the undo stack
|
||||
///
|
||||
/// This clears the redo stack since we're creating a new timeline branch.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
//! Add shape action
|
||||
//!
|
||||
//! Handles adding a new shape and object to a vector layer.
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::document::Document;
|
||||
use crate::layer::AnyLayer;
|
||||
use crate::object::Object;
|
||||
use crate::shape::Shape;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Action that adds a shape and object to a vector layer
|
||||
///
|
||||
/// This action creates both a Shape (the path/geometry) and an Object
|
||||
/// (the instance with transform). Both are added to the layer.
|
||||
pub struct AddShapeAction {
|
||||
/// Layer ID to add the shape to
|
||||
layer_id: Uuid,
|
||||
|
||||
/// The shape to add (contains path and styling)
|
||||
shape: Shape,
|
||||
|
||||
/// The object to add (references the shape with transform)
|
||||
object: Object,
|
||||
|
||||
/// ID of the created shape (set after execution)
|
||||
created_shape_id: Option<Uuid>,
|
||||
|
||||
/// ID of the created object (set after execution)
|
||||
created_object_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl AddShapeAction {
|
||||
/// Create a new add shape action
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `layer_id` - The layer to add the shape to
|
||||
/// * `shape` - The shape to add
|
||||
/// * `object` - The object instance referencing the shape
|
||||
pub fn new(layer_id: Uuid, shape: Shape, object: Object) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
shape,
|
||||
object,
|
||||
created_shape_id: None,
|
||||
created_object_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for AddShapeAction {
|
||||
fn execute(&mut self, document: &mut Document) {
|
||||
let layer = match document.get_layer_mut(&self.layer_id) {
|
||||
Some(l) => l,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if let AnyLayer::Vector(vector_layer) = layer {
|
||||
// Add shape and object to the layer
|
||||
let shape_id = vector_layer.add_shape_internal(self.shape.clone());
|
||||
let object_id = vector_layer.add_object_internal(self.object.clone());
|
||||
|
||||
// Store the IDs for rollback
|
||||
self.created_shape_id = Some(shape_id);
|
||||
self.created_object_id = Some(object_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn rollback(&mut self, document: &mut Document) {
|
||||
// Remove the created shape and object if they exist
|
||||
if let (Some(shape_id), Some(object_id)) = (self.created_shape_id, self.created_object_id) {
|
||||
let layer = match document.get_layer_mut(&self.layer_id) {
|
||||
Some(l) => l,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if let AnyLayer::Vector(vector_layer) = layer {
|
||||
// Remove in reverse order: object first, then shape
|
||||
vector_layer.remove_object_internal(&object_id);
|
||||
vector_layer.remove_shape_internal(&shape_id);
|
||||
}
|
||||
|
||||
// Clear the stored IDs
|
||||
self.created_shape_id = None;
|
||||
self.created_object_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Add shape".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::layer::VectorLayer;
|
||||
use crate::shape::ShapeColor;
|
||||
use vello::kurbo::{Circle, Rect, Shape as KurboShape};
|
||||
|
||||
#[test]
|
||||
fn test_add_shape_action_rectangle() {
|
||||
// Create a document with a vector layer
|
||||
let mut document = Document::new("Test");
|
||||
let vector_layer = VectorLayer::new("Layer 1");
|
||||
let layer_id = document.root.add_child(AnyLayer::Vector(vector_layer));
|
||||
|
||||
// Create a rectangle shape
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 50.0);
|
||||
let path = rect.to_path(0.1);
|
||||
let shape = Shape::new(path).with_fill(ShapeColor::rgb(255, 0, 0));
|
||||
let object = Object::new(shape.id).with_position(50.0, 50.0);
|
||||
|
||||
// Create and execute action
|
||||
let mut action = AddShapeAction::new(layer_id, shape, object);
|
||||
action.execute(&mut document);
|
||||
|
||||
// Verify shape and object were added
|
||||
if let Some(AnyLayer::Vector(layer)) = document.get_layer(&layer_id) {
|
||||
assert_eq!(layer.shapes.len(), 1);
|
||||
assert_eq!(layer.objects.len(), 1);
|
||||
|
||||
let added_object = &layer.objects[0];
|
||||
assert_eq!(added_object.transform.x, 50.0);
|
||||
assert_eq!(added_object.transform.y, 50.0);
|
||||
} else {
|
||||
panic!("Layer not found or not a vector layer");
|
||||
}
|
||||
|
||||
// Rollback
|
||||
action.rollback(&mut document);
|
||||
|
||||
// Verify shape and object were removed
|
||||
if let Some(AnyLayer::Vector(layer)) = document.get_layer(&layer_id) {
|
||||
assert_eq!(layer.shapes.len(), 0);
|
||||
assert_eq!(layer.objects.len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_shape_action_circle() {
|
||||
let mut document = Document::new("Test");
|
||||
let vector_layer = VectorLayer::new("Layer 1");
|
||||
let layer_id = document.root.add_child(AnyLayer::Vector(vector_layer));
|
||||
|
||||
// Create a circle shape
|
||||
let circle = Circle::new((50.0, 50.0), 25.0);
|
||||
let path = circle.to_path(0.1);
|
||||
let shape = Shape::new(path)
|
||||
.with_fill(ShapeColor::rgb(0, 255, 0));
|
||||
let object = Object::new(shape.id);
|
||||
|
||||
let mut action = AddShapeAction::new(layer_id, shape, object);
|
||||
|
||||
// Test description
|
||||
assert_eq!(action.description(), "Add shape");
|
||||
|
||||
// Execute
|
||||
action.execute(&mut document);
|
||||
|
||||
if let Some(AnyLayer::Vector(layer)) = document.get_layer(&layer_id) {
|
||||
assert_eq!(layer.shapes.len(), 1);
|
||||
assert_eq!(layer.objects.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_shape_action_multiple_execute() {
|
||||
let mut document = Document::new("Test");
|
||||
let vector_layer = VectorLayer::new("Layer 1");
|
||||
let layer_id = document.root.add_child(AnyLayer::Vector(vector_layer));
|
||||
|
||||
let rect = Rect::new(0.0, 0.0, 50.0, 50.0);
|
||||
let path = rect.to_path(0.1);
|
||||
let shape = Shape::new(path);
|
||||
let object = Object::new(shape.id);
|
||||
|
||||
let mut action = AddShapeAction::new(layer_id, shape, object);
|
||||
|
||||
// Execute twice (should add duplicate)
|
||||
action.execute(&mut document);
|
||||
action.execute(&mut document);
|
||||
|
||||
if let Some(AnyLayer::Vector(layer)) = document.get_layer(&layer_id) {
|
||||
// Should have 2 shapes and 2 objects
|
||||
assert_eq!(layer.shapes.len(), 2);
|
||||
assert_eq!(layer.objects.len(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,10 @@
|
|||
//! This module contains all the concrete action types that can be executed
|
||||
//! through the action system.
|
||||
|
||||
pub mod add_shape;
|
||||
pub mod move_objects;
|
||||
pub mod transform_objects;
|
||||
|
||||
pub use add_shape::AddShapeAction;
|
||||
pub use move_objects::MoveObjectsAction;
|
||||
pub use transform_objects::TransformObjectsAction;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
//! Transform objects action
|
||||
//!
|
||||
//! Applies scale, rotation, and other transformations to objects with undo/redo support.
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::document::Document;
|
||||
use crate::layer::AnyLayer;
|
||||
use crate::object::Transform;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Action to transform multiple objects
|
||||
pub struct TransformObjectsAction {
|
||||
layer_id: Uuid,
|
||||
/// Map of object ID to (old transform, new transform)
|
||||
object_transforms: HashMap<Uuid, (Transform, Transform)>,
|
||||
}
|
||||
|
||||
impl TransformObjectsAction {
|
||||
/// Create a new transform action
|
||||
pub fn new(
|
||||
layer_id: Uuid,
|
||||
object_transforms: HashMap<Uuid, (Transform, Transform)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
object_transforms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for TransformObjectsAction {
|
||||
fn execute(&mut self, document: &mut Document) {
|
||||
if let Some(layer) = document.get_layer_mut(&self.layer_id) {
|
||||
if let AnyLayer::Vector(vector_layer) = layer {
|
||||
for (object_id, (_old, new)) in &self.object_transforms {
|
||||
vector_layer.modify_object_internal(object_id, |obj| {
|
||||
obj.transform = new.clone();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rollback(&mut self, document: &mut Document) {
|
||||
if let Some(layer) = document.get_layer_mut(&self.layer_id) {
|
||||
if let AnyLayer::Vector(vector_layer) = layer {
|
||||
for (object_id, (old, _new)) in &self.object_transforms {
|
||||
vector_layer.modify_object_internal(object_id, |obj| {
|
||||
obj.transform = old.clone();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
format!("Transform {} object(s)", self.object_transforms.len())
|
||||
}
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ impl Document {
|
|||
///
|
||||
/// This method is intentionally `pub(crate)` to ensure mutations
|
||||
/// only happen through the action system.
|
||||
pub(crate) fn get_layer_mut(&mut self, id: &Uuid) -> Option<&mut AnyLayer> {
|
||||
pub fn get_layer_mut(&mut self, id: &Uuid) -> Option<&mut AnyLayer> {
|
||||
self.root.get_child_mut(id)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ impl VectorLayer {
|
|||
/// Applies the given function to the object if found.
|
||||
/// This method is intentionally `pub(crate)` to ensure mutations
|
||||
/// only happen through the action system.
|
||||
pub(crate) fn modify_object_internal<F>(&mut self, id: &Uuid, f: F)
|
||||
pub fn modify_object_internal<F>(&mut self, id: &Uuid, f: F)
|
||||
where
|
||||
F: FnOnce(&mut Object),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ pub mod pane;
|
|||
pub mod tool;
|
||||
pub mod animation;
|
||||
pub mod path_interpolation;
|
||||
pub mod path_fitting;
|
||||
pub mod shape;
|
||||
pub mod object;
|
||||
pub mod layer;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,613 @@
|
|||
//! Path fitting algorithms for converting raw points to smooth curves
|
||||
//!
|
||||
//! Provides two main algorithms:
|
||||
//! - Ramer-Douglas-Peucker (RDP) simplification for corner detection
|
||||
//! - Schneider curve fitting for smooth Bezier curves
|
||||
//!
|
||||
//! Based on:
|
||||
//! - simplify.js by Vladimir Agafonkin
|
||||
//! - fit-curve by Philip J. Schneider (Graphics Gems, 1990)
|
||||
|
||||
use kurbo::{BezPath, Point, Vec2};
|
||||
|
||||
/// Configuration for RDP simplification
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RdpConfig {
|
||||
/// Tolerance for simplification (default: 10.0)
|
||||
/// Higher values = more simplification (fewer points)
|
||||
pub tolerance: f64,
|
||||
/// Whether to use highest quality (skip radial distance filter)
|
||||
pub highest_quality: bool,
|
||||
}
|
||||
|
||||
impl Default for RdpConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tolerance: 10.0,
|
||||
highest_quality: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for Schneider curve fitting
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SchneiderConfig {
|
||||
/// Maximum error tolerance (default: 30.0)
|
||||
/// Lower values = more accurate curves (more segments)
|
||||
pub max_error: f64,
|
||||
}
|
||||
|
||||
impl Default for SchneiderConfig {
|
||||
fn default() -> Self {
|
||||
Self { max_error: 30.0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Simplify a polyline using Ramer-Douglas-Peucker algorithm
|
||||
///
|
||||
/// This is a two-stage process:
|
||||
/// 1. Radial distance filter (unless highest_quality is true)
|
||||
/// 2. Douglas-Peucker recursive simplification
|
||||
pub fn simplify_rdp(points: &[Point], config: RdpConfig) -> Vec<Point> {
|
||||
if points.len() <= 2 {
|
||||
return points.to_vec();
|
||||
}
|
||||
|
||||
let sq_tolerance = config.tolerance * config.tolerance;
|
||||
|
||||
let mut simplified = if config.highest_quality {
|
||||
points.to_vec()
|
||||
} else {
|
||||
simplify_radial_dist(points, sq_tolerance)
|
||||
};
|
||||
|
||||
simplified = simplify_douglas_peucker(&simplified, sq_tolerance);
|
||||
|
||||
simplified
|
||||
}
|
||||
|
||||
/// First stage: Remove points that are too close to the previous point
|
||||
fn simplify_radial_dist(points: &[Point], sq_tolerance: f64) -> Vec<Point> {
|
||||
if points.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut result = vec![points[0]];
|
||||
let mut prev_point = points[0];
|
||||
|
||||
for &point in &points[1..] {
|
||||
if sq_dist(point, prev_point) > sq_tolerance {
|
||||
result.push(point);
|
||||
prev_point = point;
|
||||
}
|
||||
}
|
||||
|
||||
// Always include the last point if it's different from the previous one
|
||||
if let Some(&last) = points.last() {
|
||||
if last != prev_point {
|
||||
result.push(last);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Second stage: Douglas-Peucker recursive simplification
|
||||
fn simplify_douglas_peucker(points: &[Point], sq_tolerance: f64) -> Vec<Point> {
|
||||
if points.len() < 2 {
|
||||
return points.to_vec();
|
||||
}
|
||||
|
||||
let last = points.len() - 1;
|
||||
let mut simplified = vec![points[0]];
|
||||
simplify_dp_step(points, 0, last, sq_tolerance, &mut simplified);
|
||||
simplified.push(points[last]);
|
||||
|
||||
simplified
|
||||
}
|
||||
|
||||
/// Recursive Douglas-Peucker step
|
||||
fn simplify_dp_step(
|
||||
points: &[Point],
|
||||
first: usize,
|
||||
last: usize,
|
||||
sq_tolerance: f64,
|
||||
simplified: &mut Vec<Point>,
|
||||
) {
|
||||
let mut max_sq_dist = sq_tolerance;
|
||||
let mut index = 0;
|
||||
|
||||
for i in first + 1..last {
|
||||
let sq_dist = sq_seg_dist(points[i], points[first], points[last]);
|
||||
|
||||
if sq_dist > max_sq_dist {
|
||||
index = i;
|
||||
max_sq_dist = sq_dist;
|
||||
}
|
||||
}
|
||||
|
||||
if max_sq_dist > sq_tolerance {
|
||||
if index - first > 1 {
|
||||
simplify_dp_step(points, first, index, sq_tolerance, simplified);
|
||||
}
|
||||
simplified.push(points[index]);
|
||||
if last - index > 1 {
|
||||
simplify_dp_step(points, index, last, sq_tolerance, simplified);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Square distance between two points
|
||||
#[inline]
|
||||
fn sq_dist(p1: Point, p2: Point) -> f64 {
|
||||
let dx = p1.x - p2.x;
|
||||
let dy = p1.y - p2.y;
|
||||
dx * dx + dy * dy
|
||||
}
|
||||
|
||||
/// Square distance from a point to a line segment
|
||||
fn sq_seg_dist(p: Point, p1: Point, p2: Point) -> f64 {
|
||||
let mut x = p1.x;
|
||||
let mut y = p1.y;
|
||||
let dx = p2.x - x;
|
||||
let dy = p2.y - y;
|
||||
|
||||
if dx != 0.0 || dy != 0.0 {
|
||||
let t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
|
||||
|
||||
if t > 1.0 {
|
||||
x = p2.x;
|
||||
y = p2.y;
|
||||
} else if t > 0.0 {
|
||||
x += dx * t;
|
||||
y += dy * t;
|
||||
}
|
||||
}
|
||||
|
||||
let dx = p.x - x;
|
||||
let dy = p.y - y;
|
||||
dx * dx + dy * dy
|
||||
}
|
||||
|
||||
/// Fit Bezier curves to a set of points using Schneider's algorithm
|
||||
///
|
||||
/// Returns a BezPath containing the fitted cubic Bezier curves
|
||||
pub fn fit_bezier_curves(points: &[Point], config: SchneiderConfig) -> BezPath {
|
||||
if points.len() < 2 {
|
||||
return BezPath::new();
|
||||
}
|
||||
|
||||
// Remove duplicate points
|
||||
let mut unique_points = Vec::new();
|
||||
unique_points.push(points[0]);
|
||||
for i in 1..points.len() {
|
||||
if points[i] != points[i - 1] {
|
||||
unique_points.push(points[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if unique_points.len() < 2 {
|
||||
return BezPath::new();
|
||||
}
|
||||
|
||||
let len = unique_points.len();
|
||||
let left_tangent = create_tangent(unique_points[1], unique_points[0]);
|
||||
let right_tangent = create_tangent(unique_points[len - 2], unique_points[len - 1]);
|
||||
|
||||
let curves = fit_cubic(&unique_points, left_tangent, right_tangent, config.max_error);
|
||||
|
||||
// Convert curves to BezPath
|
||||
let mut path = BezPath::new();
|
||||
if curves.is_empty() {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Start at the first point
|
||||
path.move_to(curves[0][0]);
|
||||
|
||||
// Add all the curves
|
||||
for curve in curves {
|
||||
path.curve_to(curve[1], curve[2], curve[3]);
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Fit a cubic Bezier curve to a set of points
|
||||
///
|
||||
/// Returns an array of Bezier curves, where each curve is [p0, p1, p2, p3]
|
||||
fn fit_cubic(
|
||||
points: &[Point],
|
||||
left_tangent: Vec2,
|
||||
right_tangent: Vec2,
|
||||
error: f64,
|
||||
) -> Vec<[Point; 4]> {
|
||||
const MAX_ITERATIONS: usize = 20;
|
||||
|
||||
// Use heuristic if region only has two points
|
||||
if points.len() == 2 {
|
||||
let dist = (points[1] - points[0]).hypot() / 3.0;
|
||||
let bez_curve = [
|
||||
points[0],
|
||||
points[0] + left_tangent * dist,
|
||||
points[1] + right_tangent * dist,
|
||||
points[1],
|
||||
];
|
||||
return vec![bez_curve];
|
||||
}
|
||||
|
||||
// Parameterize points and attempt to fit curve
|
||||
let u = chord_length_parameterize(points);
|
||||
let (mut bez_curve, mut max_error, mut split_point) =
|
||||
generate_and_report(points, &u, &u, left_tangent, right_tangent);
|
||||
|
||||
if max_error < error {
|
||||
return vec![bez_curve];
|
||||
}
|
||||
|
||||
// If error not too large, try reparameterization and iteration
|
||||
if max_error < error * error {
|
||||
let mut u_prime = u.clone();
|
||||
let mut prev_err = max_error;
|
||||
let mut prev_split = split_point;
|
||||
|
||||
for _ in 0..MAX_ITERATIONS {
|
||||
u_prime = reparameterize(&bez_curve, points, &u_prime);
|
||||
|
||||
let result = generate_and_report(points, &u, &u_prime, left_tangent, right_tangent);
|
||||
bez_curve = result.0;
|
||||
max_error = result.1;
|
||||
split_point = result.2;
|
||||
|
||||
if max_error < error {
|
||||
return vec![bez_curve];
|
||||
}
|
||||
|
||||
// If development grinds to a halt, abort
|
||||
if split_point == prev_split {
|
||||
let err_change = max_error / prev_err;
|
||||
if err_change > 0.9999 && err_change < 1.0001 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prev_err = max_error;
|
||||
prev_split = split_point;
|
||||
}
|
||||
}
|
||||
|
||||
// Fitting failed -- split at max error point and fit recursively
|
||||
let mut beziers = Vec::new();
|
||||
|
||||
// Calculate tangent at split point
|
||||
let mut center_vector = points[split_point - 1] - points[split_point + 1];
|
||||
|
||||
// Handle case where points are the same
|
||||
if center_vector.hypot() == 0.0 {
|
||||
center_vector = points[split_point - 1] - points[split_point];
|
||||
center_vector = Vec2::new(-center_vector.y, center_vector.x);
|
||||
}
|
||||
|
||||
let to_center_tangent = normalize(center_vector);
|
||||
let from_center_tangent = -to_center_tangent;
|
||||
|
||||
// Recursively fit curves
|
||||
beziers.extend(fit_cubic(
|
||||
&points[0..=split_point],
|
||||
left_tangent,
|
||||
to_center_tangent,
|
||||
error,
|
||||
));
|
||||
beziers.extend(fit_cubic(
|
||||
&points[split_point..],
|
||||
from_center_tangent,
|
||||
right_tangent,
|
||||
error,
|
||||
));
|
||||
|
||||
beziers
|
||||
}
|
||||
|
||||
/// Generate a Bezier curve and compute its error
|
||||
fn generate_and_report(
|
||||
points: &[Point],
|
||||
params_orig: &[f64],
|
||||
params_prime: &[f64],
|
||||
left_tangent: Vec2,
|
||||
right_tangent: Vec2,
|
||||
) -> ([Point; 4], f64, usize) {
|
||||
let bez_curve = generate_bezier(points, params_prime, left_tangent, right_tangent);
|
||||
let (max_error, split_point) = compute_max_error(points, &bez_curve, params_orig);
|
||||
|
||||
(bez_curve, max_error, split_point)
|
||||
}
|
||||
|
||||
/// Use least-squares method to find Bezier control points
|
||||
fn generate_bezier(
|
||||
points: &[Point],
|
||||
parameters: &[f64],
|
||||
left_tangent: Vec2,
|
||||
right_tangent: Vec2,
|
||||
) -> [Point; 4] {
|
||||
let first_point = points[0];
|
||||
let last_point = points[points.len() - 1];
|
||||
|
||||
// Compute the A matrix
|
||||
let mut a = Vec::new();
|
||||
for &u in parameters {
|
||||
let ux = 1.0 - u;
|
||||
let a0 = left_tangent * (3.0 * u * ux * ux);
|
||||
let a1 = right_tangent * (3.0 * ux * u * u);
|
||||
a.push([a0, a1]);
|
||||
}
|
||||
|
||||
// Create C and X matrices
|
||||
let mut c = [[0.0, 0.0], [0.0, 0.0]];
|
||||
let mut x = [0.0, 0.0];
|
||||
|
||||
for i in 0..points.len() {
|
||||
let u = parameters[i];
|
||||
let ai = a[i];
|
||||
|
||||
c[0][0] += dot(ai[0], ai[0]);
|
||||
c[0][1] += dot(ai[0], ai[1]);
|
||||
c[1][0] += dot(ai[0], ai[1]);
|
||||
c[1][1] += dot(ai[1], ai[1]);
|
||||
|
||||
let tmp = points[i] - bezier_q(&[first_point, first_point, last_point, last_point], u);
|
||||
|
||||
x[0] += dot(ai[0], tmp);
|
||||
x[1] += dot(ai[1], tmp);
|
||||
}
|
||||
|
||||
// Compute determinants
|
||||
let det_c0_c1 = c[0][0] * c[1][1] - c[1][0] * c[0][1];
|
||||
let det_c0_x = c[0][0] * x[1] - c[1][0] * x[0];
|
||||
let det_x_c1 = x[0] * c[1][1] - x[1] * c[0][1];
|
||||
|
||||
// Derive alpha values
|
||||
let alpha_l = if det_c0_c1 == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
det_x_c1 / det_c0_c1
|
||||
};
|
||||
let alpha_r = if det_c0_c1 == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
det_c0_x / det_c0_c1
|
||||
};
|
||||
|
||||
// If alpha is negative or too small, use heuristic
|
||||
let seg_length = (last_point - first_point).hypot();
|
||||
let epsilon = 1.0e-6 * seg_length;
|
||||
|
||||
let (p1, p2) = if alpha_l < epsilon || alpha_r < epsilon {
|
||||
// Fall back on standard formula
|
||||
(
|
||||
first_point + left_tangent * (seg_length / 3.0),
|
||||
last_point + right_tangent * (seg_length / 3.0),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
first_point + left_tangent * alpha_l,
|
||||
last_point + right_tangent * alpha_r,
|
||||
)
|
||||
};
|
||||
|
||||
[first_point, p1, p2, last_point]
|
||||
}
|
||||
|
||||
/// Reparameterize points using Newton-Raphson
|
||||
fn reparameterize(bezier: &[Point; 4], points: &[Point], parameters: &[f64]) -> Vec<f64> {
|
||||
parameters
|
||||
.iter()
|
||||
.zip(points.iter())
|
||||
.map(|(&p, &point)| newton_raphson_root_find(bezier, point, p))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Use Newton-Raphson iteration to find better root
|
||||
fn newton_raphson_root_find(bez: &[Point; 4], point: Point, u: f64) -> f64 {
|
||||
let d = bezier_q(bez, u) - point;
|
||||
let qprime = bezier_qprime(bez, u);
|
||||
let numerator = dot(d, qprime);
|
||||
let qprimeprime = bezier_qprimeprime(bez, u);
|
||||
let denominator = dot(qprime, qprime) + 2.0 * dot(d, qprimeprime);
|
||||
|
||||
if denominator == 0.0 {
|
||||
u
|
||||
} else {
|
||||
u - numerator / denominator
|
||||
}
|
||||
}
|
||||
|
||||
/// Assign parameter values using chord length
|
||||
fn chord_length_parameterize(points: &[Point]) -> Vec<f64> {
|
||||
let mut u = Vec::new();
|
||||
let mut curr_u = 0.0;
|
||||
|
||||
u.push(0.0);
|
||||
|
||||
for i in 1..points.len() {
|
||||
curr_u += (points[i] - points[i - 1]).hypot();
|
||||
u.push(curr_u);
|
||||
}
|
||||
|
||||
let total_length = u[u.len() - 1];
|
||||
u.iter().map(|&x| x / total_length).collect()
|
||||
}
|
||||
|
||||
/// Find maximum squared distance of points to fitted curve
|
||||
fn compute_max_error(points: &[Point], bez: &[Point; 4], parameters: &[f64]) -> (f64, usize) {
|
||||
let mut max_dist = 0.0;
|
||||
let mut split_point = points.len() / 2;
|
||||
|
||||
let t_dist_map = map_t_to_relative_distances(bez, 10);
|
||||
|
||||
for i in 0..points.len() {
|
||||
let point = points[i];
|
||||
let t = find_t(bez, parameters[i], &t_dist_map, 10);
|
||||
|
||||
let v = bezier_q(bez, t) - point;
|
||||
let dist = v.x * v.x + v.y * v.y;
|
||||
|
||||
if dist > max_dist {
|
||||
max_dist = dist;
|
||||
split_point = i;
|
||||
}
|
||||
}
|
||||
|
||||
(max_dist, split_point)
|
||||
}
|
||||
|
||||
/// Sample t values and map to relative distances along curve
|
||||
fn map_t_to_relative_distances(bez: &[Point; 4], b_parts: usize) -> Vec<f64> {
|
||||
let mut b_t_dist = vec![0.0];
|
||||
let mut b_t_prev = bez[0];
|
||||
let mut sum_len = 0.0;
|
||||
|
||||
for i in 1..=b_parts {
|
||||
let b_t_curr = bezier_q(bez, i as f64 / b_parts as f64);
|
||||
sum_len += (b_t_curr - b_t_prev).hypot();
|
||||
b_t_dist.push(sum_len);
|
||||
b_t_prev = b_t_curr;
|
||||
}
|
||||
|
||||
// Normalize to 0..1
|
||||
b_t_dist.iter().map(|&x| x / sum_len).collect()
|
||||
}
|
||||
|
||||
/// Find t value for a given parameter distance
|
||||
fn find_t(bez: &[Point; 4], param: f64, t_dist_map: &[f64], b_parts: usize) -> f64 {
|
||||
if param < 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
if param > 1.0 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
for i in 1..=b_parts {
|
||||
if param <= t_dist_map[i] {
|
||||
let t_min = (i - 1) as f64 / b_parts as f64;
|
||||
let t_max = i as f64 / b_parts as f64;
|
||||
let len_min = t_dist_map[i - 1];
|
||||
let len_max = t_dist_map[i];
|
||||
|
||||
let t = (param - len_min) / (len_max - len_min) * (t_max - t_min) + t_min;
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Evaluate cubic Bezier at parameter t
|
||||
fn bezier_q(ctrl_poly: &[Point; 4], t: f64) -> Point {
|
||||
let tx = 1.0 - t;
|
||||
let p_a = ctrl_poly[0].to_vec2() * (tx * tx * tx);
|
||||
let p_b = ctrl_poly[1].to_vec2() * (3.0 * tx * tx * t);
|
||||
let p_c = ctrl_poly[2].to_vec2() * (3.0 * tx * t * t);
|
||||
let p_d = ctrl_poly[3].to_vec2() * (t * t * t);
|
||||
|
||||
(p_a + p_b + p_c + p_d).to_point()
|
||||
}
|
||||
|
||||
/// Evaluate first derivative of cubic Bezier at parameter t
|
||||
fn bezier_qprime(ctrl_poly: &[Point; 4], t: f64) -> Vec2 {
|
||||
let tx = 1.0 - t;
|
||||
let p_a = (ctrl_poly[1] - ctrl_poly[0]) * (3.0 * tx * tx);
|
||||
let p_b = (ctrl_poly[2] - ctrl_poly[1]) * (6.0 * tx * t);
|
||||
let p_c = (ctrl_poly[3] - ctrl_poly[2]) * (3.0 * t * t);
|
||||
|
||||
p_a + p_b + p_c
|
||||
}
|
||||
|
||||
/// Evaluate second derivative of cubic Bezier at parameter t
|
||||
fn bezier_qprimeprime(ctrl_poly: &[Point; 4], t: f64) -> Vec2 {
|
||||
let v0 = ctrl_poly[2].to_vec2() - ctrl_poly[1].to_vec2() * 2.0 + ctrl_poly[0].to_vec2();
|
||||
let v1 = ctrl_poly[3].to_vec2() - ctrl_poly[2].to_vec2() * 2.0 + ctrl_poly[1].to_vec2();
|
||||
v0 * (6.0 * (1.0 - t)) + v1 * (6.0 * t)
|
||||
}
|
||||
|
||||
/// Create a unit tangent vector from A to B
|
||||
fn create_tangent(point_a: Point, point_b: Point) -> Vec2 {
|
||||
normalize(point_a - point_b)
|
||||
}
|
||||
|
||||
/// Normalize a vector to unit length
|
||||
fn normalize(v: Vec2) -> Vec2 {
|
||||
let len = v.hypot();
|
||||
if len == 0.0 {
|
||||
Vec2::ZERO
|
||||
} else {
|
||||
v / len
|
||||
}
|
||||
}
|
||||
|
||||
/// Dot product of two vectors
|
||||
fn dot(v1: Vec2, v2: Vec2) -> f64 {
|
||||
v1.x * v2.x + v1.y * v2.y
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rdp_simplification() {
|
||||
let points = vec![
|
||||
Point::new(0.0, 0.0),
|
||||
Point::new(1.0, 0.1),
|
||||
Point::new(2.0, 0.0),
|
||||
Point::new(3.0, 0.0),
|
||||
Point::new(4.0, 0.0),
|
||||
Point::new(5.0, 0.0),
|
||||
];
|
||||
|
||||
let config = RdpConfig {
|
||||
tolerance: 0.5,
|
||||
highest_quality: false,
|
||||
};
|
||||
|
||||
let simplified = simplify_rdp(&points, config);
|
||||
|
||||
// Should simplify the nearly-straight line
|
||||
assert!(simplified.len() < points.len());
|
||||
assert_eq!(simplified[0], points[0]);
|
||||
assert_eq!(simplified[simplified.len() - 1], points[points.len() - 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schneider_curve_fitting() {
|
||||
let points = vec![
|
||||
Point::new(0.0, 0.0),
|
||||
Point::new(50.0, 100.0),
|
||||
Point::new(100.0, 50.0),
|
||||
Point::new(150.0, 100.0),
|
||||
];
|
||||
|
||||
let config = SchneiderConfig { max_error: 30.0 };
|
||||
|
||||
let path = fit_bezier_curves(&points, config);
|
||||
|
||||
// Should create a valid BezPath
|
||||
assert!(!path.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chord_length_parameterization() {
|
||||
let points = vec![
|
||||
Point::new(0.0, 0.0),
|
||||
Point::new(1.0, 0.0),
|
||||
Point::new(2.0, 0.0),
|
||||
];
|
||||
|
||||
let params = chord_length_parameterize(&points);
|
||||
|
||||
// Should start at 0 and end at 1
|
||||
assert_eq!(params[0], 0.0);
|
||||
assert_eq!(params[params.len() - 1], 1.0);
|
||||
// Should be evenly spaced for uniform spacing
|
||||
assert!((params[1] - 0.5).abs() < 0.01);
|
||||
}
|
||||
}
|
||||
|
|
@ -177,6 +177,16 @@ impl ShapeColor {
|
|||
pub fn to_brush(&self) -> Brush {
|
||||
Brush::Solid(self.to_peniko())
|
||||
}
|
||||
|
||||
/// Create from egui Color32
|
||||
pub fn from_egui(color: egui::Color32) -> Self {
|
||||
Self {
|
||||
r: color.r(),
|
||||
g: color.g(),
|
||||
b: color.b(),
|
||||
a: color.a(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShapeColor {
|
||||
|
|
|
|||
|
|
@ -54,14 +54,18 @@ pub enum ToolState {
|
|||
|
||||
/// Creating a rectangle shape
|
||||
CreatingRectangle {
|
||||
start_corner: Point,
|
||||
current_corner: Point,
|
||||
start_point: Point, // Starting point (corner or center depending on modifiers)
|
||||
current_point: Point, // Current mouse position
|
||||
centered: bool, // If true, start_point is center; if false, it's a corner
|
||||
constrain_square: bool, // If true, constrain to square (equal width/height)
|
||||
},
|
||||
|
||||
/// Creating an ellipse shape
|
||||
CreatingEllipse {
|
||||
center: Point,
|
||||
current_point: Point,
|
||||
start_point: Point, // Starting point (center or corner depending on modifiers)
|
||||
current_point: Point, // Current mouse position
|
||||
corner_mode: bool, // If true, start is corner; if false, start is center
|
||||
constrain_circle: bool, // If true, constrain to circle (equal radii)
|
||||
},
|
||||
|
||||
/// Transforming selected objects (scale, rotate)
|
||||
|
|
@ -69,6 +73,9 @@ pub enum ToolState {
|
|||
mode: TransformMode,
|
||||
original_transforms: HashMap<Uuid, crate::object::Transform>,
|
||||
pivot: Point,
|
||||
start_mouse: Point, // Mouse position when transform started
|
||||
current_mouse: Point, // Current mouse position during drag
|
||||
original_bbox: vello::kurbo::Rect, // Bounding box at start of transform (fixed)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Lightningbeam Editor
|
||||
Comment=Animation and video editing software
|
||||
Exec=lightningbeam-editor
|
||||
Icon=lightningbeam-editor
|
||||
Terminal=false
|
||||
Categories=Graphics;AudioVideo;Video;
|
||||
StartupWMClass=lightningbeam-editor
|
||||
|
|
@ -68,10 +68,36 @@ fn main() -> eframe::Result {
|
|||
}
|
||||
}
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
// Load window icon
|
||||
let icon_data = include_bytes!("../../../src-tauri/icons/icon.png");
|
||||
let icon_image = match image::load_from_memory(icon_data) {
|
||||
Ok(img) => {
|
||||
let rgba = img.to_rgba8();
|
||||
let (width, height) = (rgba.width(), rgba.height());
|
||||
println!("✅ Loaded window icon: {}x{}", width, height);
|
||||
Some(egui::IconData {
|
||||
rgba: rgba.into_raw(),
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to load window icon: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mut viewport_builder = egui::ViewportBuilder::default()
|
||||
.with_inner_size([1920.0, 1080.0])
|
||||
.with_title("Lightningbeam Editor"),
|
||||
.with_title("Lightningbeam Editor")
|
||||
.with_app_id("lightningbeam-editor"); // Set app_id for Wayland
|
||||
|
||||
if let Some(icon) = icon_image {
|
||||
viewport_builder = viewport_builder.with_icon(icon);
|
||||
}
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: viewport_builder,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
|
@ -235,6 +261,10 @@ struct EditorApp {
|
|||
active_layer_id: Option<Uuid>, // Currently active layer for editing
|
||||
selection: lightningbeam_core::selection::Selection, // Current selection state
|
||||
tool_state: lightningbeam_core::tool::ToolState, // Current tool interaction state
|
||||
// Draw tool configuration
|
||||
draw_simplify_mode: lightningbeam_core::tool::SimplifyMode, // Current simplification mode for draw tool
|
||||
rdp_tolerance: f64, // RDP simplification tolerance (default: 10.0)
|
||||
schneider_max_error: f64, // Schneider curve fitting max error (default: 30.0)
|
||||
}
|
||||
|
||||
impl EditorApp {
|
||||
|
|
@ -289,6 +319,9 @@ impl EditorApp {
|
|||
active_layer_id: Some(layer_id),
|
||||
selection: lightningbeam_core::selection::Selection::new(),
|
||||
tool_state: lightningbeam_core::tool::ToolState::default(),
|
||||
draw_simplify_mode: lightningbeam_core::tool::SimplifyMode::Smooth, // Default to smooth curves
|
||||
rdp_tolerance: 10.0, // Default RDP tolerance
|
||||
schneider_max_error: 30.0, // Default Schneider max error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -610,11 +643,14 @@ impl eframe::App for EditorApp {
|
|||
&mut fallback_pane_priority,
|
||||
&mut pending_handlers,
|
||||
&self.theme,
|
||||
self.action_executor.document(),
|
||||
&mut self.action_executor,
|
||||
&mut self.selection,
|
||||
&self.active_layer_id,
|
||||
&mut self.tool_state,
|
||||
&mut pending_actions,
|
||||
&mut self.draw_simplify_mode,
|
||||
&mut self.rdp_tolerance,
|
||||
&mut self.schneider_max_error,
|
||||
);
|
||||
|
||||
// Execute action on the best handler (two-phase dispatch)
|
||||
|
|
@ -697,15 +733,18 @@ fn render_layout_node(
|
|||
fallback_pane_priority: &mut Option<u32>,
|
||||
pending_handlers: &mut Vec<panes::ViewActionHandler>,
|
||||
theme: &Theme,
|
||||
document: &lightningbeam_core::document::Document,
|
||||
action_executor: &mut lightningbeam_core::action::ActionExecutor,
|
||||
selection: &mut lightningbeam_core::selection::Selection,
|
||||
active_layer_id: &Option<Uuid>,
|
||||
tool_state: &mut lightningbeam_core::tool::ToolState,
|
||||
pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>,
|
||||
draw_simplify_mode: &mut lightningbeam_core::tool::SimplifyMode,
|
||||
rdp_tolerance: &mut f64,
|
||||
schneider_max_error: &mut f64,
|
||||
) {
|
||||
match node {
|
||||
LayoutNode::Pane { name } => {
|
||||
render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, icon_cache, tool_icon_cache, selected_tool, fill_color, stroke_color, pane_instances, path, pending_view_action, fallback_pane_priority, pending_handlers, theme, document, selection, active_layer_id, tool_state, pending_actions);
|
||||
render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, icon_cache, tool_icon_cache, selected_tool, fill_color, stroke_color, pane_instances, path, pending_view_action, fallback_pane_priority, pending_handlers, theme, action_executor, selection, active_layer_id, tool_state, pending_actions, draw_simplify_mode, rdp_tolerance, schneider_max_error);
|
||||
}
|
||||
LayoutNode::HorizontalGrid { percent, children } => {
|
||||
// Handle dragging
|
||||
|
|
@ -749,11 +788,14 @@ fn render_layout_node(
|
|||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
theme,
|
||||
document,
|
||||
action_executor,
|
||||
selection,
|
||||
active_layer_id,
|
||||
tool_state,
|
||||
pending_actions,
|
||||
draw_simplify_mode,
|
||||
rdp_tolerance,
|
||||
schneider_max_error,
|
||||
);
|
||||
|
||||
let mut right_path = path.clone();
|
||||
|
|
@ -778,11 +820,14 @@ fn render_layout_node(
|
|||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
theme,
|
||||
document,
|
||||
action_executor,
|
||||
selection,
|
||||
active_layer_id,
|
||||
tool_state,
|
||||
pending_actions,
|
||||
draw_simplify_mode,
|
||||
rdp_tolerance,
|
||||
schneider_max_error,
|
||||
);
|
||||
|
||||
// Draw divider with interaction
|
||||
|
|
@ -899,11 +944,14 @@ fn render_layout_node(
|
|||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
theme,
|
||||
document,
|
||||
action_executor,
|
||||
selection,
|
||||
active_layer_id,
|
||||
tool_state,
|
||||
pending_actions,
|
||||
draw_simplify_mode,
|
||||
rdp_tolerance,
|
||||
schneider_max_error,
|
||||
);
|
||||
|
||||
let mut bottom_path = path.clone();
|
||||
|
|
@ -928,11 +976,14 @@ fn render_layout_node(
|
|||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
theme,
|
||||
document,
|
||||
action_executor,
|
||||
selection,
|
||||
active_layer_id,
|
||||
tool_state,
|
||||
pending_actions,
|
||||
draw_simplify_mode,
|
||||
rdp_tolerance,
|
||||
schneider_max_error,
|
||||
);
|
||||
|
||||
// Draw divider with interaction
|
||||
|
|
@ -1029,11 +1080,14 @@ fn render_pane(
|
|||
fallback_pane_priority: &mut Option<u32>,
|
||||
pending_handlers: &mut Vec<panes::ViewActionHandler>,
|
||||
theme: &Theme,
|
||||
document: &lightningbeam_core::document::Document,
|
||||
action_executor: &mut lightningbeam_core::action::ActionExecutor,
|
||||
selection: &mut lightningbeam_core::selection::Selection,
|
||||
active_layer_id: &Option<Uuid>,
|
||||
tool_state: &mut lightningbeam_core::tool::ToolState,
|
||||
pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>,
|
||||
draw_simplify_mode: &mut lightningbeam_core::tool::SimplifyMode,
|
||||
rdp_tolerance: &mut f64,
|
||||
schneider_max_error: &mut f64,
|
||||
) {
|
||||
let pane_type = PaneType::from_name(pane_name);
|
||||
|
||||
|
|
@ -1208,11 +1262,14 @@ fn render_pane(
|
|||
fallback_pane_priority,
|
||||
theme,
|
||||
pending_handlers,
|
||||
document,
|
||||
action_executor,
|
||||
selection,
|
||||
active_layer_id,
|
||||
tool_state,
|
||||
pending_actions,
|
||||
draw_simplify_mode,
|
||||
rdp_tolerance,
|
||||
schneider_max_error,
|
||||
};
|
||||
pane_instance.render_header(&mut header_ui, &mut shared);
|
||||
}
|
||||
|
|
@ -1248,11 +1305,14 @@ fn render_pane(
|
|||
fallback_pane_priority,
|
||||
theme,
|
||||
pending_handlers,
|
||||
document,
|
||||
action_executor,
|
||||
selection,
|
||||
active_layer_id,
|
||||
tool_state,
|
||||
pending_actions,
|
||||
draw_simplify_mode,
|
||||
rdp_tolerance,
|
||||
schneider_max_error,
|
||||
};
|
||||
|
||||
// Render pane content (header was already rendered above)
|
||||
|
|
|
|||
|
|
@ -43,8 +43,9 @@ pub struct SharedPaneState<'a> {
|
|||
/// Registry of handlers for the current pending action
|
||||
/// Panes register themselves here during render, execution happens after
|
||||
pub pending_handlers: &'a mut Vec<ViewActionHandler>,
|
||||
/// Active document being edited (read-only, mutations go through actions)
|
||||
pub document: &'a lightningbeam_core::document::Document,
|
||||
/// Action executor for immediate action execution (for shape tools to avoid flicker)
|
||||
/// Also provides read-only access to the document via action_executor.document()
|
||||
pub action_executor: &'a mut lightningbeam_core::action::ActionExecutor,
|
||||
/// Current selection state (mutable for tools to modify)
|
||||
pub selection: &'a mut lightningbeam_core::selection::Selection,
|
||||
/// Currently active layer ID
|
||||
|
|
@ -53,6 +54,10 @@ pub struct SharedPaneState<'a> {
|
|||
pub tool_state: &'a mut lightningbeam_core::tool::ToolState,
|
||||
/// Actions to execute after rendering completes (two-phase dispatch)
|
||||
pub pending_actions: &'a mut Vec<Box<dyn lightningbeam_core::action::Action>>,
|
||||
/// Draw tool configuration
|
||||
pub draw_simplify_mode: &'a mut lightningbeam_core::tool::SimplifyMode,
|
||||
pub rdp_tolerance: &'a mut f64,
|
||||
pub schneider_max_error: &'a mut f64,
|
||||
}
|
||||
|
||||
/// Trait for pane rendering
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue