rest of DCEL migration
This commit is contained in:
parent
2739391257
commit
05966ed271
|
|
@ -9,7 +9,6 @@ pub mod add_layer;
|
|||
pub mod add_shape;
|
||||
pub mod modify_shape_path;
|
||||
pub mod move_clip_instances;
|
||||
pub mod move_objects;
|
||||
pub mod paint_bucket;
|
||||
pub mod remove_effect;
|
||||
pub mod set_document_properties;
|
||||
|
|
@ -18,7 +17,6 @@ pub mod set_layer_properties;
|
|||
pub mod set_shape_properties;
|
||||
pub mod split_clip_instance;
|
||||
pub mod transform_clip_instances;
|
||||
pub mod transform_objects;
|
||||
pub mod trim_clip_instances;
|
||||
pub mod create_folder;
|
||||
pub mod rename_folder;
|
||||
|
|
@ -27,7 +25,6 @@ pub mod move_asset_to_folder;
|
|||
pub mod update_midi_notes;
|
||||
pub mod loop_clip_instances;
|
||||
pub mod remove_clip_instances;
|
||||
pub mod remove_shapes;
|
||||
pub mod set_keyframe;
|
||||
pub mod group_shapes;
|
||||
pub mod convert_to_movie_clip;
|
||||
|
|
@ -39,16 +36,14 @@ pub use add_layer::AddLayerAction;
|
|||
pub use add_shape::AddShapeAction;
|
||||
pub use modify_shape_path::ModifyDcelAction;
|
||||
pub use move_clip_instances::MoveClipInstancesAction;
|
||||
pub use move_objects::MoveShapeInstancesAction;
|
||||
pub use paint_bucket::PaintBucketAction;
|
||||
pub use remove_effect::RemoveEffectAction;
|
||||
pub use set_document_properties::SetDocumentPropertiesAction;
|
||||
pub use set_instance_properties::{InstancePropertyChange, SetInstancePropertiesAction};
|
||||
pub use set_layer_properties::{LayerProperty, SetLayerPropertiesAction};
|
||||
pub use set_shape_properties::{SetShapePropertiesAction, ShapePropertyChange};
|
||||
pub use set_shape_properties::SetShapePropertiesAction;
|
||||
pub use split_clip_instance::SplitClipInstanceAction;
|
||||
pub use transform_clip_instances::TransformClipInstancesAction;
|
||||
pub use transform_objects::TransformShapeInstancesAction;
|
||||
pub use trim_clip_instances::{TrimClipInstancesAction, TrimData, TrimType};
|
||||
pub use create_folder::CreateFolderAction;
|
||||
pub use rename_folder::RenameFolderAction;
|
||||
|
|
@ -57,7 +52,6 @@ pub use move_asset_to_folder::MoveAssetToFolderAction;
|
|||
pub use update_midi_notes::UpdateMidiNotesAction;
|
||||
pub use loop_clip_instances::LoopClipInstancesAction;
|
||||
pub use remove_clip_instances::RemoveClipInstancesAction;
|
||||
pub use remove_shapes::RemoveShapesAction;
|
||||
pub use set_keyframe::SetKeyframeAction;
|
||||
pub use group_shapes::GroupAction;
|
||||
pub use convert_to_movie_clip::ConvertToMovieClipAction;
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
//! Move shapes action — STUB: needs DCEL rewrite
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::document::Document;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
use vello::kurbo::Point;
|
||||
|
||||
/// Action that moves shapes to new positions within a keyframe
|
||||
/// TODO: Replace with DCEL vertex translation
|
||||
pub struct MoveShapeInstancesAction {
|
||||
layer_id: Uuid,
|
||||
time: f64,
|
||||
shape_positions: HashMap<Uuid, (Point, Point)>,
|
||||
}
|
||||
|
||||
impl MoveShapeInstancesAction {
|
||||
pub fn new(layer_id: Uuid, time: f64, shape_positions: HashMap<Uuid, (Point, Point)>) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
time,
|
||||
shape_positions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for MoveShapeInstancesAction {
|
||||
fn execute(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
let _ = (&self.layer_id, self.time, &self.shape_positions);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
let count = self.shape_positions.len();
|
||||
if count == 1 {
|
||||
"Move shape".to_string()
|
||||
} else {
|
||||
format!("Move {} shapes", count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
//! Remove shapes action — STUB: needs DCEL rewrite
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::document::Document;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Action that removes shapes from a vector layer's keyframe
|
||||
/// TODO: Replace with DCEL edge/face removal actions
|
||||
pub struct RemoveShapesAction {
|
||||
layer_id: Uuid,
|
||||
shape_ids: Vec<Uuid>,
|
||||
time: f64,
|
||||
}
|
||||
|
||||
impl RemoveShapesAction {
|
||||
pub fn new(layer_id: Uuid, shape_ids: Vec<Uuid>, time: f64) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
shape_ids,
|
||||
time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for RemoveShapesAction {
|
||||
fn execute(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
let _ = (&self.layer_id, &self.shape_ids, self.time);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
let count = self.shape_ids.len();
|
||||
if count == 1 {
|
||||
"Delete shape".to_string()
|
||||
} else {
|
||||
format!("Delete {} shapes", count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +1,170 @@
|
|||
//! Set shape properties action — STUB: needs DCEL rewrite
|
||||
//! Set shape properties action — operates on DCEL edge/face IDs.
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::dcel::{EdgeId, FaceId};
|
||||
use crate::document::Document;
|
||||
use crate::layer::AnyLayer;
|
||||
use crate::shape::ShapeColor;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Property change for a shape
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ShapePropertyChange {
|
||||
FillColor(Option<ShapeColor>),
|
||||
StrokeColor(Option<ShapeColor>),
|
||||
StrokeWidth(f64),
|
||||
}
|
||||
|
||||
/// Action that sets properties on a shape
|
||||
/// TODO: Replace with DCEL face/edge property changes
|
||||
/// Action that sets fill/stroke properties on DCEL elements.
|
||||
pub struct SetShapePropertiesAction {
|
||||
layer_id: Uuid,
|
||||
shape_id: Uuid,
|
||||
time: f64,
|
||||
new_value: ShapePropertyChange,
|
||||
old_value: Option<ShapePropertyChange>,
|
||||
change: PropertyChange,
|
||||
old_edge_values: Vec<(EdgeId, Option<ShapeColor>, Option<f64>)>,
|
||||
old_face_values: Vec<(FaceId, Option<ShapeColor>)>,
|
||||
}
|
||||
|
||||
enum PropertyChange {
|
||||
FillColor {
|
||||
face_ids: Vec<FaceId>,
|
||||
color: Option<ShapeColor>,
|
||||
},
|
||||
StrokeColor {
|
||||
edge_ids: Vec<EdgeId>,
|
||||
color: Option<ShapeColor>,
|
||||
},
|
||||
StrokeWidth {
|
||||
edge_ids: Vec<EdgeId>,
|
||||
width: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl SetShapePropertiesAction {
|
||||
pub fn new(layer_id: Uuid, shape_id: Uuid, time: f64, new_value: ShapePropertyChange) -> Self {
|
||||
pub fn set_fill_color(
|
||||
layer_id: Uuid,
|
||||
time: f64,
|
||||
face_ids: Vec<FaceId>,
|
||||
color: Option<ShapeColor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
shape_id,
|
||||
time,
|
||||
new_value,
|
||||
old_value: None,
|
||||
change: PropertyChange::FillColor { face_ids, color },
|
||||
old_edge_values: Vec::new(),
|
||||
old_face_values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_fill_color(layer_id: Uuid, shape_id: Uuid, time: f64, color: Option<ShapeColor>) -> Self {
|
||||
Self::new(layer_id, shape_id, time, ShapePropertyChange::FillColor(color))
|
||||
pub fn set_stroke_color(
|
||||
layer_id: Uuid,
|
||||
time: f64,
|
||||
edge_ids: Vec<EdgeId>,
|
||||
color: Option<ShapeColor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
time,
|
||||
change: PropertyChange::StrokeColor { edge_ids, color },
|
||||
old_edge_values: Vec::new(),
|
||||
old_face_values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_stroke_color(layer_id: Uuid, shape_id: Uuid, time: f64, color: Option<ShapeColor>) -> Self {
|
||||
Self::new(layer_id, shape_id, time, ShapePropertyChange::StrokeColor(color))
|
||||
pub fn set_stroke_width(
|
||||
layer_id: Uuid,
|
||||
time: f64,
|
||||
edge_ids: Vec<EdgeId>,
|
||||
width: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
time,
|
||||
change: PropertyChange::StrokeWidth { edge_ids, width },
|
||||
old_edge_values: Vec::new(),
|
||||
old_face_values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_stroke_width(layer_id: Uuid, shape_id: Uuid, time: f64, width: f64) -> Self {
|
||||
Self::new(layer_id, shape_id, time, ShapePropertyChange::StrokeWidth(width))
|
||||
fn get_dcel_mut<'a>(
|
||||
document: &'a mut Document,
|
||||
layer_id: &Uuid,
|
||||
time: f64,
|
||||
) -> Result<&'a mut crate::dcel::Dcel, String> {
|
||||
let layer = document
|
||||
.get_layer_mut(layer_id)
|
||||
.ok_or_else(|| format!("Layer {} not found", layer_id))?;
|
||||
let vl = match layer {
|
||||
AnyLayer::Vector(vl) => vl,
|
||||
_ => return Err("Not a vector layer".to_string()),
|
||||
};
|
||||
vl.dcel_at_time_mut(time)
|
||||
.ok_or_else(|| format!("No keyframe at time {}", time))
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for SetShapePropertiesAction {
|
||||
fn execute(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
let _ = (&self.layer_id, &self.shape_id, self.time, &self.new_value);
|
||||
fn execute(&mut self, document: &mut Document) -> Result<(), String> {
|
||||
let dcel = Self::get_dcel_mut(document, &self.layer_id, self.time)?;
|
||||
|
||||
match &self.change {
|
||||
PropertyChange::FillColor { face_ids, color } => {
|
||||
self.old_face_values.clear();
|
||||
for &fid in face_ids {
|
||||
let face = dcel.face(fid);
|
||||
self.old_face_values.push((fid, face.fill_color));
|
||||
dcel.face_mut(fid).fill_color = *color;
|
||||
}
|
||||
}
|
||||
PropertyChange::StrokeColor { edge_ids, color } => {
|
||||
self.old_edge_values.clear();
|
||||
for &eid in edge_ids {
|
||||
let edge = dcel.edge(eid);
|
||||
let old_width = edge.stroke_style.as_ref().map(|s| s.width);
|
||||
self.old_edge_values.push((eid, edge.stroke_color, old_width));
|
||||
dcel.edge_mut(eid).stroke_color = *color;
|
||||
}
|
||||
}
|
||||
PropertyChange::StrokeWidth { edge_ids, width } => {
|
||||
self.old_edge_values.clear();
|
||||
for &eid in edge_ids {
|
||||
let edge = dcel.edge(eid);
|
||||
let old_width = edge.stroke_style.as_ref().map(|s| s.width);
|
||||
self.old_edge_values.push((eid, edge.stroke_color, old_width));
|
||||
if let Some(ref mut style) = dcel.edge_mut(eid).stroke_style {
|
||||
style.width = *width;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
let _ = &self.old_value;
|
||||
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
||||
let dcel = Self::get_dcel_mut(document, &self.layer_id, self.time)?;
|
||||
|
||||
match &self.change {
|
||||
PropertyChange::FillColor { .. } => {
|
||||
for &(fid, old_color) in &self.old_face_values {
|
||||
dcel.face_mut(fid).fill_color = old_color;
|
||||
}
|
||||
}
|
||||
PropertyChange::StrokeColor { .. } => {
|
||||
for &(eid, old_color, _) in &self.old_edge_values {
|
||||
dcel.edge_mut(eid).stroke_color = old_color;
|
||||
}
|
||||
}
|
||||
PropertyChange::StrokeWidth { .. } => {
|
||||
for &(eid, _, old_width) in &self.old_edge_values {
|
||||
if let Some(w) = old_width {
|
||||
if let Some(ref mut style) = dcel.edge_mut(eid).stroke_style {
|
||||
style.width = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
match &self.new_value {
|
||||
ShapePropertyChange::FillColor(_) => "Set fill color".to_string(),
|
||||
ShapePropertyChange::StrokeColor(_) => "Set stroke color".to_string(),
|
||||
ShapePropertyChange::StrokeWidth(_) => "Set stroke width".to_string(),
|
||||
match &self.change {
|
||||
PropertyChange::FillColor { .. } => "Set fill color".to_string(),
|
||||
PropertyChange::StrokeColor { .. } => "Set stroke color".to_string(),
|
||||
PropertyChange::StrokeWidth { .. } => "Set stroke width".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
//! Transform shapes action — STUB: needs DCEL rewrite
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::document::Document;
|
||||
use crate::object::Transform;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Action to transform multiple shapes in a keyframe
|
||||
/// TODO: Replace with DCEL-based transforms (affine on vertices/edges)
|
||||
pub struct TransformShapeInstancesAction {
|
||||
layer_id: Uuid,
|
||||
time: f64,
|
||||
shape_transforms: HashMap<Uuid, (Transform, Transform)>,
|
||||
}
|
||||
|
||||
impl TransformShapeInstancesAction {
|
||||
pub fn new(
|
||||
layer_id: Uuid,
|
||||
time: f64,
|
||||
shape_transforms: HashMap<Uuid, (Transform, Transform)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
time,
|
||||
shape_transforms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for TransformShapeInstancesAction {
|
||||
fn execute(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
let _ = (&self.layer_id, self.time, &self.shape_transforms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
format!("Transform {} shape(s)", self.shape_transforms.len())
|
||||
}
|
||||
}
|
||||
|
|
@ -186,13 +186,15 @@ pub struct VectorLayer {
|
|||
/// Base layer properties
|
||||
pub layer: Layer,
|
||||
|
||||
/// Shapes defined in this layer (indexed by UUID for O(1) lookup)
|
||||
/// Legacy shapes — kept for old .beam file compat, not written to new files.
|
||||
#[serde(default, skip_serializing)]
|
||||
pub shapes: HashMap<Uuid, Shape>,
|
||||
|
||||
/// Shape instances (references to shapes with transforms)
|
||||
/// Legacy shape instances — kept for old .beam file compat, not written to new files.
|
||||
#[serde(default, skip_serializing)]
|
||||
pub shape_instances: Vec<ShapeInstance>,
|
||||
|
||||
/// Shape keyframes (sorted by time) — replaces shapes/shape_instances
|
||||
/// Shape keyframes (sorted by time)
|
||||
#[serde(default)]
|
||||
pub keyframes: Vec<ShapeKeyframe>,
|
||||
|
||||
|
|
|
|||
|
|
@ -1875,8 +1875,7 @@ impl EditorApp {
|
|||
}
|
||||
};
|
||||
|
||||
// TODO: DCEL - paste shapes disabled during migration
|
||||
// (was: push shapes into kf.shapes, select pasted shapes)
|
||||
// TODO: DCEL - paste shapes not yet implemented
|
||||
let _ = (vector_layer, shapes);
|
||||
}
|
||||
ClipboardContent::MidiNotes { .. } => {
|
||||
|
|
@ -2624,8 +2623,7 @@ impl EditorApp {
|
|||
let mut rect_shape = Shape::new(rect_path);
|
||||
rect_shape.fill_color = Some(ShapeColor::rgb(0, 0, 255));
|
||||
|
||||
// TODO: DCEL - test shape creation disabled during migration
|
||||
// (was: push shapes into kf.shapes)
|
||||
// TODO: DCEL - test shape creation not yet implemented
|
||||
let _ = (circle_shape, rect_shape);
|
||||
|
||||
// Add the layer to the clip
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
/// - Document settings (when nothing is selected)
|
||||
|
||||
use eframe::egui::{self, DragValue, Ui};
|
||||
use lightningbeam_core::actions::SetDocumentPropertiesAction;
|
||||
use lightningbeam_core::actions::{SetDocumentPropertiesAction, SetShapePropertiesAction};
|
||||
use lightningbeam_core::layer::AnyLayer;
|
||||
use lightningbeam_core::shape::ShapeColor;
|
||||
use lightningbeam_core::tool::{SimplifyMode, Tool};
|
||||
|
|
@ -283,9 +283,18 @@ impl InfopanelPane {
|
|||
&mut self,
|
||||
ui: &mut Ui,
|
||||
path: &NodePath,
|
||||
_shared: &mut SharedPaneState,
|
||||
shared: &mut SharedPaneState,
|
||||
info: &SelectionInfo,
|
||||
) {
|
||||
// Clone IDs and values we need before borrowing shared mutably
|
||||
let layer_id = match info.layer_id {
|
||||
Some(id) => id,
|
||||
None => return,
|
||||
};
|
||||
let time = *shared.playback_time;
|
||||
let face_ids: Vec<_> = shared.selection.selected_faces().iter().copied().collect();
|
||||
let edge_ids: Vec<_> = shared.selection.selected_edges().iter().copied().collect();
|
||||
|
||||
egui::CollapsingHeader::new("Shape")
|
||||
.id_salt(("shape", path))
|
||||
.default_open(self.shape_section_open)
|
||||
|
|
@ -293,19 +302,30 @@ impl InfopanelPane {
|
|||
self.shape_section_open = true;
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Fill color (read-only display for now)
|
||||
// Fill color
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Fill:");
|
||||
match info.fill_color {
|
||||
Some(Some(color)) => {
|
||||
let egui_color = egui::Color32::from_rgba_unmultiplied(
|
||||
let mut egui_color = egui::Color32::from_rgba_unmultiplied(
|
||||
color.r, color.g, color.b, color.a,
|
||||
);
|
||||
let (rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(20.0, 20.0),
|
||||
egui::Sense::hover(),
|
||||
);
|
||||
ui.painter().rect_filled(rect, 2.0, egui_color);
|
||||
if egui::color_picker::color_edit_button_srgba(
|
||||
ui,
|
||||
&mut egui_color,
|
||||
egui::color_picker::Alpha::OnlyBlend,
|
||||
).changed() {
|
||||
let new_color = ShapeColor {
|
||||
r: egui_color.r(),
|
||||
g: egui_color.g(),
|
||||
b: egui_color.b(),
|
||||
a: egui_color.a(),
|
||||
};
|
||||
let action = SetShapePropertiesAction::set_fill_color(
|
||||
layer_id, time, face_ids.clone(), Some(new_color),
|
||||
);
|
||||
shared.pending_actions.push(Box::new(action));
|
||||
}
|
||||
}
|
||||
Some(None) => {
|
||||
ui.label("None");
|
||||
|
|
@ -316,19 +336,30 @@ impl InfopanelPane {
|
|||
}
|
||||
});
|
||||
|
||||
// Stroke color (read-only display for now)
|
||||
// Stroke color
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Stroke:");
|
||||
match info.stroke_color {
|
||||
Some(Some(color)) => {
|
||||
let egui_color = egui::Color32::from_rgba_unmultiplied(
|
||||
let mut egui_color = egui::Color32::from_rgba_unmultiplied(
|
||||
color.r, color.g, color.b, color.a,
|
||||
);
|
||||
let (rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(20.0, 20.0),
|
||||
egui::Sense::hover(),
|
||||
);
|
||||
ui.painter().rect_filled(rect, 2.0, egui_color);
|
||||
if egui::color_picker::color_edit_button_srgba(
|
||||
ui,
|
||||
&mut egui_color,
|
||||
egui::color_picker::Alpha::OnlyBlend,
|
||||
).changed() {
|
||||
let new_color = ShapeColor {
|
||||
r: egui_color.r(),
|
||||
g: egui_color.g(),
|
||||
b: egui_color.b(),
|
||||
a: egui_color.a(),
|
||||
};
|
||||
let action = SetShapePropertiesAction::set_stroke_color(
|
||||
layer_id, time, edge_ids.clone(), Some(new_color),
|
||||
);
|
||||
shared.pending_actions.push(Box::new(action));
|
||||
}
|
||||
}
|
||||
Some(None) => {
|
||||
ui.label("None");
|
||||
|
|
@ -339,12 +370,21 @@ impl InfopanelPane {
|
|||
}
|
||||
});
|
||||
|
||||
// Stroke width (read-only display for now)
|
||||
// Stroke width
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Stroke Width:");
|
||||
match info.stroke_width {
|
||||
Some(width) => {
|
||||
ui.label(format!("{:.1}", width));
|
||||
Some(mut width) => {
|
||||
if ui.add(
|
||||
DragValue::new(&mut width)
|
||||
.speed(0.1)
|
||||
.range(0.1..=100.0),
|
||||
).changed() {
|
||||
let action = SetShapePropertiesAction::set_stroke_width(
|
||||
layer_id, time, edge_ids.clone(), width,
|
||||
);
|
||||
shared.pending_actions.push(Box::new(action));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ui.label("--");
|
||||
|
|
|
|||
|
|
@ -1476,7 +1476,77 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
// For multiple objects: use axis-aligned bounding box (simpler for now)
|
||||
|
||||
let total_selected = self.ctx.selection.clip_instances().len();
|
||||
if total_selected == 1 {
|
||||
if self.ctx.selection.has_dcel_selection() {
|
||||
// DCEL selection: compute bbox from selected vertices
|
||||
if let Some(dcel) = vector_layer.dcel_at_time(self.ctx.playback_time) {
|
||||
let mut min_x = f64::INFINITY;
|
||||
let mut min_y = f64::INFINITY;
|
||||
let mut max_x = f64::NEG_INFINITY;
|
||||
let mut max_y = f64::NEG_INFINITY;
|
||||
let mut found_any = false;
|
||||
|
||||
for &vid in self.ctx.selection.selected_vertices() {
|
||||
let v = dcel.vertex(vid);
|
||||
if v.deleted { continue; }
|
||||
min_x = min_x.min(v.position.x);
|
||||
min_y = min_y.min(v.position.y);
|
||||
max_x = max_x.max(v.position.x);
|
||||
max_y = max_y.max(v.position.y);
|
||||
found_any = true;
|
||||
}
|
||||
|
||||
if found_any {
|
||||
let bbox = KurboRect::new(min_x, min_y, max_x, max_y);
|
||||
let handle_size = (8.0 / self.ctx.zoom.max(0.5) as f64).max(6.0);
|
||||
let handle_color = Color::from_rgb8(0, 120, 255);
|
||||
let rotation_handle_offset = 20.0 / self.ctx.zoom.max(0.5) as f64;
|
||||
|
||||
scene.stroke(&Stroke::new(stroke_width), overlay_transform, handle_color, None, &bbox);
|
||||
|
||||
let corners = [
|
||||
vello::kurbo::Point::new(bbox.x0, bbox.y0),
|
||||
vello::kurbo::Point::new(bbox.x1, bbox.y0),
|
||||
vello::kurbo::Point::new(bbox.x1, bbox.y1),
|
||||
vello::kurbo::Point::new(bbox.x0, bbox.y1),
|
||||
];
|
||||
|
||||
for corner in &corners {
|
||||
let handle_rect = KurboRect::new(
|
||||
corner.x - handle_size / 2.0, corner.y - handle_size / 2.0,
|
||||
corner.x + handle_size / 2.0, corner.y + handle_size / 2.0,
|
||||
);
|
||||
scene.fill(Fill::NonZero, overlay_transform, handle_color, None, &handle_rect);
|
||||
scene.stroke(&Stroke::new(1.0), overlay_transform, Color::from_rgb8(255, 255, 255), None, &handle_rect);
|
||||
}
|
||||
|
||||
let edges = [
|
||||
vello::kurbo::Point::new(bbox.center().x, bbox.y0),
|
||||
vello::kurbo::Point::new(bbox.x1, bbox.center().y),
|
||||
vello::kurbo::Point::new(bbox.center().x, bbox.y1),
|
||||
vello::kurbo::Point::new(bbox.x0, bbox.center().y),
|
||||
];
|
||||
|
||||
for edge in &edges {
|
||||
let edge_circle = Circle::new(*edge, handle_size / 2.0);
|
||||
scene.fill(Fill::NonZero, overlay_transform, handle_color, None, &edge_circle);
|
||||
scene.stroke(&Stroke::new(1.0), overlay_transform, Color::from_rgb8(255, 255, 255), None, &edge_circle);
|
||||
}
|
||||
|
||||
let rotation_handle_pos = vello::kurbo::Point::new(bbox.center().x, bbox.y0 - rotation_handle_offset);
|
||||
let rotation_circle = Circle::new(rotation_handle_pos, handle_size / 2.0);
|
||||
scene.fill(Fill::NonZero, overlay_transform, Color::from_rgb8(50, 200, 50), None, &rotation_circle);
|
||||
scene.stroke(&Stroke::new(1.0), overlay_transform, Color::from_rgb8(255, 255, 255), None, &rotation_circle);
|
||||
|
||||
let line_path = {
|
||||
let mut path = vello::kurbo::BezPath::new();
|
||||
path.move_to(rotation_handle_pos);
|
||||
path.line_to(vello::kurbo::Point::new(bbox.center().x, bbox.y0));
|
||||
path
|
||||
};
|
||||
scene.stroke(&Stroke::new(1.0), overlay_transform, Color::from_rgb8(50, 200, 50), None, &line_path);
|
||||
}
|
||||
}
|
||||
} else if total_selected == 1 {
|
||||
// Single clip instance - draw rotated bounding box
|
||||
let object_id = *self.ctx.selection.clip_instances().iter().next().unwrap();
|
||||
|
||||
|
|
@ -4239,6 +4309,260 @@ impl StagePane {
|
|||
None
|
||||
}
|
||||
|
||||
/// Handle transform tool for DCEL elements (vertices/edges).
|
||||
/// Uses snapshot-based undo via ModifyDcelAction.
|
||||
fn handle_transform_dcel(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
response: &egui::Response,
|
||||
point: vello::kurbo::Point,
|
||||
active_layer_id: &uuid::Uuid,
|
||||
shared: &mut SharedPaneState,
|
||||
) {
|
||||
use lightningbeam_core::tool::ToolState;
|
||||
use lightningbeam_core::layer::AnyLayer;
|
||||
|
||||
let time = *shared.playback_time;
|
||||
|
||||
// Calculate bounding box of selected DCEL vertices
|
||||
let selected_verts: Vec<lightningbeam_core::dcel::VertexId> =
|
||||
shared.selection.selected_vertices().iter().copied().collect();
|
||||
|
||||
if selected_verts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let bbox = {
|
||||
let document = shared.action_executor.document();
|
||||
if let Some(AnyLayer::Vector(vl)) = document.get_layer(active_layer_id) {
|
||||
if let Some(dcel) = vl.dcel_at_time(time) {
|
||||
let mut min_x = f64::MAX;
|
||||
let mut min_y = f64::MAX;
|
||||
let mut max_x = f64::MIN;
|
||||
let mut max_y = f64::MIN;
|
||||
for &vid in &selected_verts {
|
||||
let v = dcel.vertex(vid);
|
||||
if v.deleted { continue; }
|
||||
min_x = min_x.min(v.position.x);
|
||||
min_y = min_y.min(v.position.y);
|
||||
max_x = max_x.max(v.position.x);
|
||||
max_y = max_y.max(v.position.y);
|
||||
}
|
||||
if min_x > max_x { return; }
|
||||
vello::kurbo::Rect::new(min_x, min_y, max_x, max_y)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// If already transforming, handle drag and release
|
||||
match shared.tool_state.clone() {
|
||||
ToolState::Transforming { mode, start_mouse, original_bbox, .. } => {
|
||||
// Drag: apply transform preview to DCEL
|
||||
if response.dragged() {
|
||||
*shared.tool_state = ToolState::Transforming {
|
||||
mode: mode.clone(),
|
||||
original_transforms: std::collections::HashMap::new(),
|
||||
pivot: original_bbox.center(),
|
||||
start_mouse,
|
||||
current_mouse: point,
|
||||
original_bbox,
|
||||
};
|
||||
|
||||
if let Some(ref cache) = self.dcel_editing_cache {
|
||||
let original_dcel = cache.dcel_before.clone();
|
||||
let selected_verts_set: std::collections::HashSet<lightningbeam_core::dcel::VertexId> =
|
||||
selected_verts.iter().copied().collect();
|
||||
let selected_edges: std::collections::HashSet<lightningbeam_core::dcel::EdgeId> =
|
||||
shared.selection.selected_edges().iter().copied().collect();
|
||||
|
||||
let affine = Self::compute_transform_affine(
|
||||
&mode, start_mouse, point, &original_bbox,
|
||||
);
|
||||
|
||||
let document = shared.action_executor.document_mut();
|
||||
if let Some(AnyLayer::Vector(vl)) = document.get_layer_mut(active_layer_id) {
|
||||
if let Some(dcel) = vl.dcel_at_time_mut(time) {
|
||||
Self::apply_dcel_transform(
|
||||
dcel, &original_dcel, &selected_verts_set, &selected_edges, affine,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release: finalize
|
||||
if response.drag_stopped() || (ui.input(|i| i.pointer.any_released()) && matches!(*shared.tool_state, ToolState::Transforming { .. })) {
|
||||
if let Some(cache) = self.dcel_editing_cache.take() {
|
||||
let dcel_after = {
|
||||
let document = shared.action_executor.document();
|
||||
match document.get_layer(active_layer_id) {
|
||||
Some(AnyLayer::Vector(vl)) => vl.dcel_at_time(time).cloned(),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
if let Some(dcel_after) = dcel_after {
|
||||
use lightningbeam_core::actions::ModifyDcelAction;
|
||||
let action = ModifyDcelAction::new(
|
||||
cache.layer_id, cache.time, cache.dcel_before, dcel_after, "Transform",
|
||||
);
|
||||
shared.pending_actions.push(Box::new(action));
|
||||
}
|
||||
}
|
||||
*shared.tool_state = ToolState::Idle;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Idle: check for handle clicks to start a transform
|
||||
if response.drag_started() || response.clicked() {
|
||||
let tolerance = 10.0;
|
||||
if let Some(mode) = Self::hit_test_transform_handle(point, bbox, tolerance) {
|
||||
// Snapshot DCEL for undo
|
||||
let document = shared.action_executor.document();
|
||||
if let Some(AnyLayer::Vector(vl)) = document.get_layer(active_layer_id) {
|
||||
if let Some(dcel) = vl.dcel_at_time(time) {
|
||||
self.dcel_editing_cache = Some(DcelEditingCache {
|
||||
layer_id: *active_layer_id,
|
||||
time,
|
||||
dcel_before: dcel.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
*shared.tool_state = ToolState::Transforming {
|
||||
mode,
|
||||
original_transforms: std::collections::HashMap::new(),
|
||||
pivot: bbox.center(),
|
||||
start_mouse: point,
|
||||
current_mouse: point,
|
||||
original_bbox: bbox,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute an Affine transform from a TransformMode, start mouse, and current mouse position.
|
||||
fn compute_transform_affine(
|
||||
mode: &lightningbeam_core::tool::TransformMode,
|
||||
start_mouse: vello::kurbo::Point,
|
||||
current_mouse: vello::kurbo::Point,
|
||||
original_bbox: &vello::kurbo::Rect,
|
||||
) -> vello::kurbo::Affine {
|
||||
use lightningbeam_core::tool::{TransformMode, Axis};
|
||||
use vello::kurbo::Affine;
|
||||
|
||||
match mode {
|
||||
TransformMode::ScaleCorner { origin } => {
|
||||
let start_vec = start_mouse - *origin;
|
||||
let current_vec = current_mouse - *origin;
|
||||
let sx = if start_vec.x.abs() > 0.001 { current_vec.x / start_vec.x } else { 1.0 };
|
||||
let sy = if start_vec.y.abs() > 0.001 { current_vec.y / start_vec.y } else { 1.0 };
|
||||
Affine::translate((origin.x, origin.y))
|
||||
* Affine::scale_non_uniform(sx, sy)
|
||||
* Affine::translate((-origin.x, -origin.y))
|
||||
}
|
||||
TransformMode::ScaleEdge { axis, origin } => {
|
||||
let (sx, sy) = match axis {
|
||||
Axis::Horizontal => {
|
||||
let sd = start_mouse.x - origin.x;
|
||||
let cd = current_mouse.x - origin.x;
|
||||
(if sd.abs() > 0.001 { cd / sd } else { 1.0 }, 1.0)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let sd = start_mouse.y - origin.y;
|
||||
let cd = current_mouse.y - origin.y;
|
||||
(1.0, if sd.abs() > 0.001 { cd / sd } else { 1.0 })
|
||||
}
|
||||
};
|
||||
Affine::translate((origin.x, origin.y))
|
||||
* Affine::scale_non_uniform(sx, sy)
|
||||
* Affine::translate((-origin.x, -origin.y))
|
||||
}
|
||||
TransformMode::Rotate { center } => {
|
||||
let start_angle = (start_mouse.y - center.y).atan2(start_mouse.x - center.x);
|
||||
let current_angle = (current_mouse.y - center.y).atan2(current_mouse.x - center.x);
|
||||
let delta = current_angle - start_angle;
|
||||
Affine::translate((center.x, center.y))
|
||||
* Affine::rotate(delta)
|
||||
* Affine::translate((-center.x, -center.y))
|
||||
}
|
||||
TransformMode::Skew { axis, origin } => {
|
||||
let center = original_bbox.center();
|
||||
let skew_radians = match axis {
|
||||
Axis::Horizontal => {
|
||||
let edge_y = if (origin.y - original_bbox.y0).abs() < 0.1 {
|
||||
original_bbox.y1
|
||||
} else {
|
||||
original_bbox.y0
|
||||
};
|
||||
let distance = edge_y - center.y;
|
||||
if distance.abs() > 0.1 {
|
||||
((current_mouse.x - start_mouse.x) / distance).atan()
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let edge_x = if (origin.x - original_bbox.x0).abs() < 0.1 {
|
||||
original_bbox.x1
|
||||
} else {
|
||||
original_bbox.x0
|
||||
};
|
||||
let distance = edge_x - center.x;
|
||||
if distance.abs() > 0.1 {
|
||||
((current_mouse.y - start_mouse.y) / distance).atan()
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
};
|
||||
let tan_s = skew_radians.tan();
|
||||
let (kx, ky) = match axis {
|
||||
Axis::Horizontal => (tan_s, 0.0),
|
||||
Axis::Vertical => (0.0, tan_s),
|
||||
};
|
||||
// Skew around center: translate to center, skew, translate back
|
||||
let skew = Affine::new([1.0, ky, kx, 1.0, 0.0, 0.0]);
|
||||
Affine::translate((center.x, center.y))
|
||||
* skew
|
||||
* Affine::translate((-center.x, -center.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply an affine transform to selected DCEL vertices and their connected edge control points.
|
||||
/// Reads original positions from `original_dcel` and writes transformed positions to `dcel`.
|
||||
fn apply_dcel_transform(
|
||||
dcel: &mut lightningbeam_core::dcel::Dcel,
|
||||
original_dcel: &lightningbeam_core::dcel::Dcel,
|
||||
selected_verts: &std::collections::HashSet<lightningbeam_core::dcel::VertexId>,
|
||||
selected_edges: &std::collections::HashSet<lightningbeam_core::dcel::EdgeId>,
|
||||
affine: vello::kurbo::Affine,
|
||||
) {
|
||||
// Transform selected vertex positions
|
||||
for &vid in selected_verts {
|
||||
let original_pos = original_dcel.vertex(vid).position;
|
||||
dcel.vertex_mut(vid).position = affine * original_pos;
|
||||
}
|
||||
|
||||
// Transform edge curves for selected edges
|
||||
for &eid in selected_edges {
|
||||
let original_curve = original_dcel.edge(eid).curve;
|
||||
let edge = dcel.edge_mut(eid);
|
||||
edge.curve.p0 = affine * original_curve.p0;
|
||||
edge.curve.p1 = affine * original_curve.p1;
|
||||
edge.curve.p2 = affine * original_curve.p2;
|
||||
edge.curve.p3 = affine * original_curve.p3;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_transform_tool(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
|
|
@ -4285,6 +4609,12 @@ impl StagePane {
|
|||
return;
|
||||
}
|
||||
|
||||
// For vector layers with DCEL selection, use DCEL-specific transform path
|
||||
if shared.selection.has_dcel_selection() {
|
||||
self.handle_transform_dcel(ui, response, point, &active_layer_id, shared);
|
||||
return;
|
||||
}
|
||||
|
||||
// For vector layers: single object uses rotated bbox, multiple objects use axis-aligned bbox
|
||||
let total_selected = shared.selection.clip_instances().len();
|
||||
if total_selected == 1 {
|
||||
|
|
@ -4451,16 +4781,13 @@ impl StagePane {
|
|||
if response.drag_stopped() || (ui.input(|i| i.pointer.any_released()) && matches!(shared.tool_state, ToolState::Transforming { .. })) {
|
||||
if let ToolState::Transforming { original_transforms, .. } = shared.tool_state.clone() {
|
||||
use std::collections::HashMap;
|
||||
use lightningbeam_core::actions::{TransformShapeInstancesAction, TransformClipInstancesAction};
|
||||
use lightningbeam_core::actions::TransformClipInstancesAction;
|
||||
|
||||
let shape_instance_transforms = HashMap::new();
|
||||
let mut clip_instance_transforms = HashMap::new();
|
||||
|
||||
// Get current transforms and pair with originals
|
||||
if let Some(AnyLayer::Vector(vector_layer)) = shared.action_executor.document().get_layer(&active_layer_id) {
|
||||
for (object_id, original) in original_transforms {
|
||||
// TODO: DCEL - shape instance transform lookup disabled during migration
|
||||
// Try clip instance
|
||||
if let Some(clip_instance) = vector_layer.clip_instances.iter().find(|ci| ci.id == object_id) {
|
||||
let new_transform = clip_instance.transform.clone();
|
||||
clip_instance_transforms.insert(object_id, (original, new_transform));
|
||||
|
|
@ -4468,12 +4795,6 @@ impl StagePane {
|
|||
}
|
||||
}
|
||||
|
||||
// Create action for shape instances
|
||||
if !shape_instance_transforms.is_empty() {
|
||||
let action = TransformShapeInstancesAction::new(active_layer_id, *shared.playback_time, shape_instance_transforms);
|
||||
shared.pending_actions.push(Box::new(action));
|
||||
}
|
||||
|
||||
// Create action for clip instances
|
||||
if !clip_instance_transforms.is_empty() {
|
||||
let action = TransformClipInstancesAction::new(active_layer_id, *shared.playback_time, clip_instance_transforms);
|
||||
|
|
@ -5195,21 +5516,17 @@ impl StagePane {
|
|||
if response.drag_stopped() || (ui.input(|i| i.pointer.any_released()) && matches!(shared.tool_state, ToolState::Transforming { .. })) {
|
||||
if let ToolState::Transforming { original_transforms, .. } = shared.tool_state.clone() {
|
||||
use std::collections::HashMap;
|
||||
use lightningbeam_core::actions::{TransformShapeInstancesAction, TransformClipInstancesAction};
|
||||
use lightningbeam_core::actions::TransformClipInstancesAction;
|
||||
|
||||
let shape_instance_transforms = HashMap::new();
|
||||
let mut clip_instance_transforms = HashMap::new();
|
||||
|
||||
if let Some(AnyLayer::Vector(vector_layer)) = shared.action_executor.document().get_layer(&active_layer_id) {
|
||||
for (obj_id, original) in original_transforms {
|
||||
// TODO: DCEL - shape instance transform lookup disabled during migration
|
||||
// Try clip instance
|
||||
if let Some(clip_instance) = vector_layer.clip_instances.iter().find(|ci| ci.id == obj_id) {
|
||||
clip_instance_transforms.insert(obj_id, (original, clip_instance.transform.clone()));
|
||||
}
|
||||
}
|
||||
} else if let Some(AnyLayer::Video(video_layer)) = shared.action_executor.document().get_layer(&active_layer_id) {
|
||||
// Handle Video layer clip instances
|
||||
for (obj_id, original) in original_transforms {
|
||||
if let Some(clip_instance) = video_layer.clip_instances.iter().find(|ci| ci.id == obj_id) {
|
||||
clip_instance_transforms.insert(obj_id, (original, clip_instance.transform.clone()));
|
||||
|
|
@ -5217,12 +5534,6 @@ impl StagePane {
|
|||
}
|
||||
}
|
||||
|
||||
// Create action for shape instances
|
||||
if !shape_instance_transforms.is_empty() {
|
||||
let action = TransformShapeInstancesAction::new(*active_layer_id, *shared.playback_time, shape_instance_transforms);
|
||||
shared.pending_actions.push(Box::new(action));
|
||||
}
|
||||
|
||||
// Create action for clip instances
|
||||
if !clip_instance_transforms.is_empty() {
|
||||
let action = TransformClipInstancesAction::new(*active_layer_id, *shared.playback_time, clip_instance_transforms);
|
||||
|
|
@ -5463,6 +5774,71 @@ impl StagePane {
|
|||
}
|
||||
}
|
||||
|
||||
// Delete/Backspace: remove selected DCEL elements
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) {
|
||||
if shared.selection.has_dcel_selection() {
|
||||
if let Some(active_layer_id) = *shared.active_layer_id {
|
||||
let time = *shared.playback_time;
|
||||
|
||||
// Collect selected edge IDs before mutating
|
||||
let selected_edges: Vec<lightningbeam_core::dcel::EdgeId> =
|
||||
shared.selection.selected_edges().iter().copied().collect();
|
||||
|
||||
if !selected_edges.is_empty() {
|
||||
// Snapshot before
|
||||
let dcel_before = {
|
||||
let document = shared.action_executor.document();
|
||||
match document.get_layer(&active_layer_id) {
|
||||
Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) => {
|
||||
vl.dcel_at_time(time).cloned()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(dcel_before) = dcel_before {
|
||||
// Remove selected edges
|
||||
{
|
||||
let document = shared.action_executor.document_mut();
|
||||
if let Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) = document.get_layer_mut(&active_layer_id) {
|
||||
if let Some(dcel) = vl.dcel_at_time_mut(time) {
|
||||
for eid in &selected_edges {
|
||||
dcel.remove_edge(*eid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot after
|
||||
let dcel_after = {
|
||||
let document = shared.action_executor.document();
|
||||
match document.get_layer(&active_layer_id) {
|
||||
Some(lightningbeam_core::layer::AnyLayer::Vector(vl)) => {
|
||||
vl.dcel_at_time(time).cloned()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(dcel_after) = dcel_after {
|
||||
use lightningbeam_core::actions::ModifyDcelAction;
|
||||
let action = ModifyDcelAction::new(
|
||||
active_layer_id,
|
||||
time,
|
||||
dcel_before,
|
||||
dcel_after,
|
||||
"Delete",
|
||||
);
|
||||
shared.pending_actions.push(Box::new(action));
|
||||
}
|
||||
|
||||
shared.selection.clear_dcel_selection();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distinguish between mouse wheel (discrete) and trackpad (smooth)
|
||||
let mut handled = false;
|
||||
ui.input(|i| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue