add line and polygon tools

This commit is contained in:
Skyler Lehmkuhl 2025-11-19 09:27:13 -05:00
parent 08f3c30b29
commit a0875b1bc0
3 changed files with 334 additions and 1 deletions

View File

@ -78,6 +78,11 @@ impl Action for PaintBucketAction {
for object in vector_layer.objects.iter().rev() { for object in vector_layer.objects.iter().rev() {
// Find the corresponding shape // Find the corresponding shape
if let Some(shape) = vector_layer.shapes.iter().find(|s| s.id == object.shape_id) { if let Some(shape) = vector_layer.shapes.iter().find(|s| s.id == object.shape_id) {
// Skip shapes without fill color (e.g., lines with only stroke)
if shape.fill_color.is_none() {
continue;
}
// Apply the object's transform to get the transformed path // Apply the object's transform to get the transformed path
let transform_affine = object.transform.to_affine(); let transform_affine = object.transform.to_affine();

View File

@ -85,6 +85,19 @@ pub enum ToolState {
current_mouse: Point, // Current mouse position during drag current_mouse: Point, // Current mouse position during drag
original_bbox: vello::kurbo::Rect, // Bounding box at start of transform (fixed) original_bbox: vello::kurbo::Rect, // Bounding box at start of transform (fixed)
}, },
/// Creating a line
CreatingLine {
start_point: Point, // Starting point of the line
current_point: Point, // Current mouse position (end point)
},
/// Creating a polygon
CreatingPolygon {
center: Point, // Center point of the polygon
current_point: Point, // Current mouse position (determines radius)
num_sides: u32, // Number of sides (from properties, default 5)
},
} }
/// Path simplification mode for the draw tool /// Path simplification mode for the draw tool

View File

@ -554,7 +554,88 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
} }
} }
// 5. Draw path drawing preview // 5. Draw line creation preview
if let lightningbeam_core::tool::ToolState::CreatingLine { ref start_point, ref current_point, .. } = self.tool_state {
use vello::kurbo::Line;
// Calculate line length
let dx = current_point.x - start_point.x;
let dy = current_point.y - start_point.y;
let length = (dx * dx + dy * dy).sqrt();
if length > 0.0 {
// Use actual stroke color for line preview
let stroke_color = Color::rgba8(
self.stroke_color.r(),
self.stroke_color.g(),
self.stroke_color.b(),
self.stroke_color.a(),
);
// Draw the line directly
let line = Line::new(*start_point, *current_point);
scene.stroke(
&Stroke::new(2.0),
camera_transform,
stroke_color,
None,
&line,
);
}
}
// 6. Draw polygon creation preview
if let lightningbeam_core::tool::ToolState::CreatingPolygon { ref center, ref current_point, num_sides, .. } = self.tool_state {
use vello::kurbo::{BezPath, Point};
use std::f64::consts::PI;
// Calculate radius
let dx = current_point.x - center.x;
let dy = current_point.y - center.y;
let radius = (dx * dx + dy * dy).sqrt();
if radius > 5.0 && num_sides >= 3 {
let preview_transform = camera_transform * Affine::translate((center.x, center.y));
// Use actual fill color (same as final shape)
let fill_color = Color::rgba8(
self.fill_color.r(),
self.fill_color.g(),
self.fill_color.b(),
self.fill_color.a(),
);
// Create the polygon path inline
let mut path = BezPath::new();
let angle_step = 2.0 * PI / num_sides as f64;
let start_angle = -PI / 2.0;
// First vertex
let first_x = radius * start_angle.cos();
let first_y = radius * start_angle.sin();
path.move_to(Point::new(first_x, first_y));
// Add remaining vertices
for i in 1..num_sides {
let angle = start_angle + angle_step * i as f64;
let x = radius * angle.cos();
let y = radius * angle.sin();
path.line_to(Point::new(x, y));
}
path.close_path();
scene.fill(
Fill::NonZero,
preview_transform,
fill_color,
None,
&path,
);
}
}
// 7. Draw path drawing preview
if let lightningbeam_core::tool::ToolState::DrawingPath { ref points, .. } = self.tool_state { if let lightningbeam_core::tool::ToolState::DrawingPath { ref points, .. } = self.tool_state {
use vello::kurbo::{BezPath, Point}; use vello::kurbo::{BezPath, Point};
@ -1362,6 +1443,179 @@ impl StagePane {
} }
} }
fn handle_line_tool(
&mut self,
ui: &mut egui::Ui,
response: &egui::Response,
world_pos: egui::Vec2,
_shift_held: bool,
_ctrl_held: bool,
shared: &mut SharedPaneState,
) {
use lightningbeam_core::tool::ToolState;
use lightningbeam_core::layer::AnyLayer;
use vello::kurbo::Point;
// Check if we have an active vector layer
let active_layer_id = match shared.active_layer_id {
Some(id) => id,
None => return,
};
let active_layer = match shared.action_executor.document().get_layer(active_layer_id) {
Some(layer) => layer,
None => return,
};
// Only work on VectorLayer
if !matches!(active_layer, AnyLayer::Vector(_)) {
return;
}
let point = Point::new(world_pos.x as f64, world_pos.y as f64);
// Mouse down: start creating line
if response.drag_started() || response.clicked() {
*shared.tool_state = ToolState::CreatingLine {
start_point: point,
current_point: point,
};
}
// Mouse drag: update line
if response.dragged() {
if let ToolState::CreatingLine { start_point, .. } = shared.tool_state {
*shared.tool_state = ToolState::CreatingLine {
start_point: *start_point,
current_point: point,
};
}
}
// Mouse up: create the line shape
if response.drag_stopped() || (ui.input(|i| i.pointer.any_released()) && matches!(shared.tool_state, ToolState::CreatingLine { .. })) {
if let ToolState::CreatingLine { start_point, current_point } = shared.tool_state.clone() {
// Calculate line length to ensure it's not too small
let dx = current_point.x - start_point.x;
let dy = current_point.y - start_point.y;
let length = (dx * dx + dy * dy).sqrt();
// Only create shape if line has reasonable length
if length > 1.0 {
use lightningbeam_core::shape::{Shape, ShapeColor, StrokeStyle};
use lightningbeam_core::object::Object;
use lightningbeam_core::actions::AddShapeAction;
// Create shape with line path
let path = Self::create_line_path(dx, dy);
// Lines should have stroke by default, not fill
let shape = Shape::new(path)
.with_stroke(
ShapeColor::from_egui(*shared.stroke_color),
StrokeStyle {
width: 2.0,
..Default::default()
}
);
// Create object at the start point
let object = Object::new(shape.id).with_position(start_point.x, start_point.y);
// Create and execute action immediately
let action = AddShapeAction::new(*active_layer_id, shape, object);
shared.action_executor.execute(Box::new(action));
// Clear tool state to stop preview rendering
*shared.tool_state = ToolState::Idle;
}
}
}
}
fn handle_polygon_tool(
&mut self,
ui: &mut egui::Ui,
response: &egui::Response,
world_pos: egui::Vec2,
_shift_held: bool,
_ctrl_held: bool,
shared: &mut SharedPaneState,
) {
use lightningbeam_core::tool::ToolState;
use lightningbeam_core::layer::AnyLayer;
use vello::kurbo::Point;
// Check if we have an active vector layer
let active_layer_id = match shared.active_layer_id {
Some(id) => id,
None => return,
};
let active_layer = match shared.action_executor.document().get_layer(active_layer_id) {
Some(layer) => layer,
None => return,
};
// Only work on VectorLayer
if !matches!(active_layer, AnyLayer::Vector(_)) {
return;
}
let point = Point::new(world_pos.x as f64, world_pos.y as f64);
// Mouse down: start creating polygon (center point)
if response.drag_started() || response.clicked() {
*shared.tool_state = ToolState::CreatingPolygon {
center: point,
current_point: point,
num_sides: 5, // Default to 5 sides (pentagon)
};
}
// Mouse drag: update polygon radius
if response.dragged() {
if let ToolState::CreatingPolygon { center, num_sides, .. } = shared.tool_state {
*shared.tool_state = ToolState::CreatingPolygon {
center: *center,
current_point: point,
num_sides: *num_sides,
};
}
}
// Mouse up: create the polygon shape
if response.drag_stopped() || (ui.input(|i| i.pointer.any_released()) && matches!(shared.tool_state, ToolState::CreatingPolygon { .. })) {
if let ToolState::CreatingPolygon { center, current_point, num_sides } = shared.tool_state.clone() {
// Calculate radius
let dx = current_point.x - center.x;
let dy = current_point.y - center.y;
let radius = (dx * dx + dy * dy).sqrt();
// Only create shape if polygon has reasonable size
if radius > 5.0 {
use lightningbeam_core::shape::{Shape, ShapeColor};
use lightningbeam_core::object::Object;
use lightningbeam_core::actions::AddShapeAction;
// Create shape with polygon path
let path = Self::create_polygon_path(num_sides, radius);
let shape = Shape::new(path).with_fill(ShapeColor::from_egui(*shared.fill_color));
// Create object at the center point
let object = Object::new(shape.id).with_position(center.x, center.y);
// Create and execute action immediately
let action = AddShapeAction::new(*active_layer_id, shape, object);
shared.action_executor.execute(Box::new(action));
// Clear tool state to stop preview rendering
*shared.tool_state = ToolState::Idle;
}
}
}
}
/// Create a rectangle path from lines (easier for curve editing later) /// Create a rectangle path from lines (easier for curve editing later)
fn create_rectangle_path(width: f64, height: f64) -> vello::kurbo::BezPath { fn create_rectangle_path(width: f64, height: f64) -> vello::kurbo::BezPath {
use vello::kurbo::{BezPath, Point}; use vello::kurbo::{BezPath, Point};
@ -1436,6 +1690,61 @@ impl StagePane {
path path
} }
/// Create a line path from start to end point
fn create_line_path(dx: f64, dy: f64) -> vello::kurbo::BezPath {
use vello::kurbo::{BezPath, Point};
let mut path = BezPath::new();
// Start at origin (object position will be the start point)
path.move_to(Point::new(0.0, 0.0));
// Line to end point
path.line_to(Point::new(dx, dy));
path
}
/// Create a regular polygon path centered at origin
///
/// # Arguments
/// * `num_sides` - Number of sides for the polygon (must be >= 3)
/// * `radius` - Radius from center to vertices
fn create_polygon_path(num_sides: u32, radius: f64) -> vello::kurbo::BezPath {
use vello::kurbo::{BezPath, Point};
use std::f64::consts::PI;
let mut path = BezPath::new();
if num_sides < 3 {
return path;
}
// Calculate angle between vertices
let angle_step = 2.0 * PI / num_sides as f64;
// Start at top (angle = -PI/2 so first vertex is at top)
let start_angle = -PI / 2.0;
// First vertex
let first_x = radius * (start_angle).cos();
let first_y = radius * (start_angle).sin();
path.move_to(Point::new(first_x, first_y));
// Add remaining vertices
for i in 1..num_sides {
let angle = start_angle + angle_step * i as f64;
let x = radius * angle.cos();
let y = radius * angle.sin();
path.line_to(Point::new(x, y));
}
// Close the path back to first vertex
path.close_path();
path
}
fn handle_draw_tool( fn handle_draw_tool(
&mut self, &mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -2565,6 +2874,12 @@ impl StagePane {
Tool::PaintBucket => { Tool::PaintBucket => {
self.handle_paint_bucket_tool(&response, world_pos, shared); self.handle_paint_bucket_tool(&response, world_pos, shared);
} }
Tool::Line => {
self.handle_line_tool(ui, &response, world_pos, shift_held, ctrl_held, shared);
}
Tool::Polygon => {
self.handle_polygon_tool(ui, &response, world_pos, shift_held, ctrl_held, shared);
}
_ => { _ => {
// Other tools not implemented yet // Other tools not implemented yet
} }