Add tool skeletons

This commit is contained in:
Skyler Lehmkuhl 2026-03-06 07:22:50 -05:00
parent 37ac9b6abe
commit 2c9d8c1589
10 changed files with 298 additions and 78 deletions

View File

@ -11,9 +11,10 @@ use vello::kurbo::Point;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Tool {
// ── Vector / shared tools ──────────────────────────────────────────────
/// Selection tool - select and move objects
Select,
/// Draw/Pen tool - freehand drawing
/// Draw/Brush tool - freehand drawing (vector) / paintbrush (raster)
Draw,
/// Transform tool - scale, rotate, skew
Transform,
@ -37,12 +38,48 @@ pub enum Tool {
RegionSelect,
/// Split tool - split audio/video clips at a point
Split,
// ── Raster brush tools ────────────────────────────────────────────────
/// Pencil tool - hard-edged raster brush
Pencil,
/// Pen tool - pressure-sensitive raster pen
Pen,
/// Airbrush tool - soft spray raster brush
Airbrush,
/// Erase tool - erase raster pixels
Erase,
/// Smudge tool - smudge/blend raster pixels
Smudge,
/// Lasso select tool - freehand selection on raster layers
/// Clone Stamp - copy pixels from a source point
CloneStamp,
/// Healing Brush - content-aware pixel repair
HealingBrush,
/// Pattern Stamp - paint with a repeating pattern
PatternStamp,
/// Dodge/Burn - lighten or darken pixels
DodgeBurn,
/// Sponge - saturate or desaturate pixels
Sponge,
/// Blur/Sharpen - blur or sharpen pixel regions
BlurSharpen,
// ── Raster fill / shape ───────────────────────────────────────────────
/// Gradient tool - fill with a gradient
Gradient,
/// Custom Shape tool - draw from a shape library
CustomShape,
// ── Raster selection tools ────────────────────────────────────────────
/// Elliptical marquee selection
SelectEllipse,
/// Lasso select tool - freehand / polygonal / magnetic selection
SelectLasso,
/// Magic Wand - select by colour similarity
MagicWand,
/// Quick Select - brush-based smart selection
QuickSelect,
// ── Raster transform tools ────────────────────────────────────────────
/// Warp / perspective transform
Warp,
/// Liquify - freeform pixel warping
Liquify,
}
/// Region select mode
@ -60,6 +97,23 @@ impl Default for RegionSelectMode {
}
}
/// Lasso selection sub-mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LassoMode {
/// Freehand lasso (existing, implemented)
Freehand,
/// Click-to-place polygonal lasso
Polygonal,
/// Magnetically snaps to edges
Magnetic,
}
impl Default for LassoMode {
fn default() -> Self {
Self::Freehand
}
}
/// Tool state tracking for interactive operations
#[derive(Debug, Clone)]
pub enum ToolState {
@ -229,44 +283,77 @@ impl Tool {
/// Get display name for the tool
pub fn display_name(self) -> &'static str {
match self {
Tool::Select => "Select",
Tool::Draw => "Draw",
Tool::Transform => "Transform",
Tool::Rectangle => "Rectangle",
Tool::Ellipse => "Ellipse",
Tool::PaintBucket => "Paint Bucket",
Tool::Eyedropper => "Eyedropper",
Tool::Line => "Line",
Tool::Polygon => "Polygon",
Tool::BezierEdit => "Bezier Edit",
Tool::Text => "Text",
Tool::RegionSelect => "Region Select",
Tool::Split => "Split",
Tool::Erase => "Erase",
Tool::Smudge => "Smudge",
Tool::SelectLasso => "Lasso Select",
Tool::Select => "Select",
Tool::Draw => "Brush",
Tool::Transform => "Transform",
Tool::Rectangle => "Rectangle",
Tool::Ellipse => "Ellipse",
Tool::PaintBucket => "Paint Bucket",
Tool::Eyedropper => "Eyedropper",
Tool::Line => "Line",
Tool::Polygon => "Polygon",
Tool::BezierEdit => "Bezier Edit",
Tool::Text => "Text",
Tool::RegionSelect => "Region Select",
Tool::Split => "Split",
Tool::Pencil => "Pencil",
Tool::Pen => "Pen",
Tool::Airbrush => "Airbrush",
Tool::Erase => "Eraser",
Tool::Smudge => "Smudge",
Tool::CloneStamp => "Clone Stamp",
Tool::HealingBrush => "Healing Brush",
Tool::PatternStamp => "Pattern Stamp",
Tool::DodgeBurn => "Dodge / Burn",
Tool::Sponge => "Sponge",
Tool::BlurSharpen => "Blur / Sharpen",
Tool::Gradient => "Gradient",
Tool::CustomShape => "Custom Shape",
Tool::SelectEllipse => "Elliptical Select",
Tool::SelectLasso => "Lasso Select",
Tool::MagicWand => "Magic Wand",
Tool::QuickSelect => "Quick Select",
Tool::Warp => "Warp",
Tool::Liquify => "Liquify",
}
}
/// Get SVG icon file name for the tool
pub fn icon_file(self) -> &'static str {
match self {
Tool::Select => "select.svg",
Tool::Draw => "draw.svg",
Tool::Transform => "transform.svg",
Tool::Rectangle => "rectangle.svg",
Tool::Ellipse => "ellipse.svg",
Tool::PaintBucket => "paint_bucket.svg",
Tool::Eyedropper => "eyedropper.svg",
Tool::Line => "line.svg",
Tool::Polygon => "polygon.svg",
Tool::BezierEdit => "bezier_edit.svg",
Tool::Text => "text.svg",
Tool::RegionSelect => "region_select.svg",
Tool::Split => "split.svg",
Tool::Erase => "erase.svg",
Tool::Smudge => "smudge.svg",
Tool::SelectLasso => "lasso.svg",
Tool::Select => "select.svg",
Tool::Draw => "draw.svg",
Tool::Transform => "transform.svg",
Tool::Rectangle => "rectangle.svg",
Tool::Ellipse => "ellipse.svg",
Tool::PaintBucket => "paint_bucket.svg",
Tool::Eyedropper => "eyedropper.svg",
Tool::Line => "line.svg",
Tool::Polygon => "polygon.svg",
Tool::BezierEdit => "bezier_edit.svg",
Tool::Text => "text.svg",
Tool::RegionSelect => "region_select.svg",
Tool::Split => "split.svg",
Tool::Erase => "erase.svg",
Tool::Smudge => "smudge.svg",
Tool::SelectLasso => "lasso.svg",
// Not yet implemented — use the placeholder icon
Tool::Pencil
| Tool::Pen
| Tool::Airbrush
| Tool::CloneStamp
| Tool::HealingBrush
| Tool::PatternStamp
| Tool::DodgeBurn
| Tool::Sponge
| Tool::BlurSharpen
| Tool::Gradient
| Tool::CustomShape
| Tool::SelectEllipse
| Tool::MagicWand
| Tool::QuickSelect
| Tool::Warp
| Tool::Liquify => "todo.svg",
}
}
@ -294,7 +381,23 @@ impl Tool {
match layer_type {
None | Some(LayerType::Vector) => Tool::all(),
Some(LayerType::Audio) | Some(LayerType::Video) => &[Tool::Select, Tool::Split],
Some(LayerType::Raster) => &[Tool::Select, Tool::SelectLasso, Tool::Draw, Tool::Erase, Tool::Smudge, Tool::Eyedropper],
Some(LayerType::Raster) => &[
// Brush tools
Tool::Draw, Tool::Pencil, Tool::Pen, Tool::Airbrush,
Tool::Erase, Tool::Smudge,
Tool::CloneStamp, Tool::HealingBrush, Tool::PatternStamp,
Tool::DodgeBurn, Tool::Sponge, Tool::BlurSharpen,
// Fill / shape
Tool::PaintBucket, Tool::Gradient,
Tool::Rectangle, Tool::Ellipse, Tool::Polygon, Tool::Line, Tool::CustomShape,
// Selection
Tool::Select, Tool::SelectEllipse, Tool::SelectLasso,
Tool::MagicWand, Tool::QuickSelect,
// Transform
Tool::Transform, Tool::Warp, Tool::Liquify,
// Utility
Tool::Eyedropper,
],
_ => &[Tool::Select],
}
}

View File

@ -124,7 +124,7 @@
"children": [
{
"type": "vertical-grid",
"percent": 30,
"percent": 67,
"children": [
{ "type": "pane", "name": "toolbar" },
{ "type": "pane", "name": "infopanel" }

View File

@ -32,22 +32,41 @@ impl CustomCursor {
/// Convert a Tool enum to the corresponding custom cursor
pub fn from_tool(tool: Tool) -> Self {
match tool {
Tool::Select => CustomCursor::Select,
Tool::Draw => CustomCursor::Draw,
Tool::Transform => CustomCursor::Transform,
Tool::Rectangle => CustomCursor::Rectangle,
Tool::Ellipse => CustomCursor::Ellipse,
Tool::PaintBucket => CustomCursor::PaintBucket,
Tool::Eyedropper => CustomCursor::Eyedropper,
Tool::Line => CustomCursor::Line,
Tool::Polygon => CustomCursor::Polygon,
Tool::BezierEdit => CustomCursor::BezierEdit,
Tool::Text => CustomCursor::Text,
Tool::RegionSelect => CustomCursor::Select, // Reuse select cursor for now
Tool::Split => CustomCursor::Select, // Reuse select cursor for now
Tool::Erase => CustomCursor::Draw, // Reuse draw cursor for raster erase
Tool::Smudge => CustomCursor::Draw, // Reuse draw cursor for raster smudge
Tool::SelectLasso => CustomCursor::Select, // Reuse select cursor for lasso
Tool::Select => CustomCursor::Select,
Tool::Draw => CustomCursor::Draw,
Tool::Transform => CustomCursor::Transform,
Tool::Rectangle => CustomCursor::Rectangle,
Tool::Ellipse => CustomCursor::Ellipse,
Tool::PaintBucket => CustomCursor::PaintBucket,
Tool::Eyedropper => CustomCursor::Eyedropper,
Tool::Line => CustomCursor::Line,
Tool::Polygon => CustomCursor::Polygon,
Tool::BezierEdit => CustomCursor::BezierEdit,
Tool::Text => CustomCursor::Text,
Tool::RegionSelect => CustomCursor::Select,
Tool::Split => CustomCursor::Select,
Tool::Erase => CustomCursor::Draw,
Tool::Smudge => CustomCursor::Draw,
Tool::SelectLasso => CustomCursor::Select,
// Raster brush tools — use draw cursor until implemented
Tool::Pencil
| Tool::Pen
| Tool::Airbrush
| Tool::CloneStamp
| Tool::HealingBrush
| Tool::PatternStamp
| Tool::DodgeBurn
| Tool::Sponge
| Tool::BlurSharpen => CustomCursor::Draw,
// Selection tools — use select cursor until implemented
Tool::SelectEllipse
| Tool::MagicWand
| Tool::QuickSelect => CustomCursor::Select,
// Other tools — use select cursor until implemented
Tool::Gradient
| Tool::CustomShape
| Tool::Warp
| Tool::Liquify => CustomCursor::Select,
}
}

View File

@ -429,24 +429,26 @@ impl AppAction {
/// `Tool::Split` has no tool-shortcut action (it's triggered via the menu).
pub fn tool_app_action(tool: lightningbeam_core::tool::Tool) -> Option<AppAction> {
use lightningbeam_core::tool::Tool;
Some(match tool {
Tool::Select => AppAction::ToolSelect,
Tool::Draw => AppAction::ToolDraw,
Tool::Transform => AppAction::ToolTransform,
Tool::Rectangle => AppAction::ToolRectangle,
Tool::Ellipse => AppAction::ToolEllipse,
Tool::PaintBucket => AppAction::ToolPaintBucket,
Tool::Eyedropper => AppAction::ToolEyedropper,
Tool::Line => AppAction::ToolLine,
Tool::Polygon => AppAction::ToolPolygon,
Tool::BezierEdit => AppAction::ToolBezierEdit,
Tool::Text => AppAction::ToolText,
Tool::RegionSelect => AppAction::ToolRegionSelect,
Tool::Erase => AppAction::ToolErase,
Tool::Smudge => AppAction::ToolSmudge,
Tool::SelectLasso => AppAction::ToolSelectLasso,
Tool::Split => AppAction::ToolSplit,
})
match tool {
Tool::Select => Some(AppAction::ToolSelect),
Tool::Draw => Some(AppAction::ToolDraw),
Tool::Transform => Some(AppAction::ToolTransform),
Tool::Rectangle => Some(AppAction::ToolRectangle),
Tool::Ellipse => Some(AppAction::ToolEllipse),
Tool::PaintBucket => Some(AppAction::ToolPaintBucket),
Tool::Eyedropper => Some(AppAction::ToolEyedropper),
Tool::Line => Some(AppAction::ToolLine),
Tool::Polygon => Some(AppAction::ToolPolygon),
Tool::BezierEdit => Some(AppAction::ToolBezierEdit),
Tool::Text => Some(AppAction::ToolText),
Tool::RegionSelect => Some(AppAction::ToolRegionSelect),
Tool::Erase => Some(AppAction::ToolErase),
Tool::Smudge => Some(AppAction::ToolSmudge),
Tool::SelectLasso => Some(AppAction::ToolSelectLasso),
Tool::Split => Some(AppAction::ToolSplit),
// New tools have no keybinding yet
_ => None,
}
}
// === Default bindings ===

View File

@ -332,6 +332,7 @@ mod tool_icons {
pub static ERASE: &[u8] = include_bytes!("../../../src/assets/erase.svg");
pub static SMUDGE: &[u8] = include_bytes!("../../../src/assets/smudge.svg");
pub static LASSO: &[u8] = include_bytes!("../../../src/assets/lasso.svg");
pub static TODO: &[u8] = include_bytes!("../../../src/assets/todo.svg");
}
/// Embedded focus icon SVGs
@ -399,11 +400,28 @@ impl ToolIconCache {
Tool::Polygon => tool_icons::POLYGON,
Tool::BezierEdit => tool_icons::BEZIER_EDIT,
Tool::Text => tool_icons::TEXT,
Tool::RegionSelect => tool_icons::SELECT, // Reuse select icon for now
Tool::RegionSelect => tool_icons::SELECT,
Tool::Split => tool_icons::SPLIT,
Tool::Erase => tool_icons::ERASE,
Tool::Smudge => tool_icons::SMUDGE,
Tool::SelectLasso => tool_icons::LASSO,
// Not yet implemented — use placeholder icon
Tool::Pencil
| Tool::Pen
| Tool::Airbrush
| Tool::CloneStamp
| Tool::HealingBrush
| Tool::PatternStamp
| Tool::DodgeBurn
| Tool::Sponge
| Tool::BlurSharpen
| Tool::Gradient
| Tool::CustomShape
| Tool::SelectEllipse
| Tool::MagicWand
| Tool::QuickSelect
| Tool::Warp
| Tool::Liquify => tool_icons::TODO,
};
if let Some(texture) = rasterize_svg(svg_data, tool.icon_file(), 180, ctx) {
self.icons.insert(tool, texture);
@ -856,6 +874,7 @@ struct EditorApp {
// Region select state
region_selection: Option<lightningbeam_core::selection::RegionSelection>,
region_select_mode: lightningbeam_core::tool::RegionSelectMode,
lasso_mode: lightningbeam_core::tool::LassoMode,
// VU meter levels
input_level: f32,
@ -1127,6 +1146,7 @@ impl EditorApp {
polygon_sides: 5, // Default to pentagon
region_selection: None,
region_select_mode: lightningbeam_core::tool::RegionSelectMode::default(),
lasso_mode: lightningbeam_core::tool::LassoMode::default(),
input_level: 0.0,
output_level: (0.0, 0.0),
track_levels: HashMap::new(),
@ -5590,6 +5610,7 @@ impl eframe::App for EditorApp {
script_saved: &mut self.script_saved,
region_selection: &mut self.region_selection,
region_select_mode: &mut self.region_select_mode,
lasso_mode: &mut self.lasso_mode,
pending_graph_loads: &self.pending_graph_loads,
clipboard_consumed: &mut clipboard_consumed,
keymap: &self.keymap,

View File

@ -169,7 +169,10 @@ impl InfopanelPane {
.and_then(|id| shared.action_executor.document().get_layer(&id))
.map_or(false, |l| matches!(l, AnyLayer::Raster(_)));
let is_raster_paint_tool = active_is_raster && matches!(tool, Tool::Draw | Tool::Erase | Tool::Smudge);
let is_raster_paint_tool = active_is_raster && matches!(
tool,
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush | Tool::Erase | Tool::Smudge
);
// Only show tool options for tools that have options
let is_vector_tool = !active_is_raster && matches!(
@ -319,7 +322,7 @@ impl InfopanelPane {
}
// Raster paint tools
Tool::Draw | Tool::Erase if is_raster_paint_tool => {
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush | Tool::Erase if is_raster_paint_tool => {
self.render_raster_tool_options(ui, shared, matches!(tool, Tool::Erase));
}
@ -513,6 +516,11 @@ impl InfopanelPane {
*shared.brush_hardness = s.hardness.clamp(0.0, 1.0);
*shared.brush_spacing = s.dabs_per_radius;
*shared.active_brush_settings = s.clone();
// If the user was on a preset-backed tool (Pencil/Pen/Airbrush)
// and manually picked a different brush, revert to the generic tool.
if matches!(*shared.selected_tool, Tool::Pencil | Tool::Pen | Tool::Airbrush) {
*shared.selected_tool = Tool::Draw;
}
}
}
}

View File

@ -283,6 +283,8 @@ pub struct SharedPaneState<'a> {
pub region_selection: &'a mut Option<lightningbeam_core::selection::RegionSelection>,
/// Region select mode (Rectangle or Lasso)
pub region_select_mode: &'a mut lightningbeam_core::tool::RegionSelectMode,
/// Lasso select sub-mode (Freehand / Polygonal / Magnetic)
pub lasso_mode: &'a mut lightningbeam_core::tool::LassoMode,
/// Counter for in-flight graph preset loads — increment when sending a
/// GraphLoadPreset command so the repaint loop stays alive until the
/// audio thread sends GraphPresetLoaded back

View File

@ -7518,6 +7518,9 @@ impl StagePane {
self.handle_draw_tool(ui, &response, world_pos, shared);
}
}
Tool::Pencil | Tool::Pen | Tool::Airbrush => {
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Normal, shared);
}
Tool::Erase => {
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Erase, shared);
}
@ -8001,8 +8004,11 @@ impl StagePane {
// Compute semi-axes (world pixels) and dab rotation angle.
let (a_world, b_world, dab_angle_rad) = match *shared.selected_tool {
Tool::Erase => (*shared.eraser_radius, *shared.eraser_radius, 0.0_f32),
Tool::Smudge => (*shared.smudge_radius, *shared.smudge_radius, 0.0_f32),
Tool::Erase => (*shared.eraser_radius, *shared.eraser_radius, 0.0_f32),
Tool::Smudge
| Tool::BlurSharpen
| Tool::DodgeBurn
| Tool::Sponge => (*shared.smudge_radius, *shared.smudge_radius, 0.0_f32),
_ => {
let bs = &shared.active_brush_settings;
let r = *shared.brush_radius;
@ -8637,7 +8643,10 @@ impl PaneRenderer for StagePane {
use lightningbeam_core::tool::Tool;
let is_raster_paint = matches!(
*shared.selected_tool,
Tool::Draw | Tool::Erase | Tool::Smudge
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush
| Tool::Erase | Tool::Smudge
| Tool::CloneStamp | Tool::HealingBrush | Tool::PatternStamp
| Tool::DodgeBurn | Tool::Sponge | Tool::BlurSharpen
) && shared.active_layer_id.and_then(|id| {
shared.action_executor.document().get_layer(&id)
}).map_or(false, |l| matches!(l, lightningbeam_core::layer::AnyLayer::Raster(_)));

View File

@ -5,7 +5,8 @@
use eframe::egui;
use lightningbeam_core::layer::{AnyLayer, LayerType};
use lightningbeam_core::tool::{Tool, RegionSelectMode};
use lightningbeam_core::tool::{Tool, RegionSelectMode, LassoMode};
use lightningbeam_core::brush_settings::bundled_brushes;
use crate::keymap::tool_app_action;
use super::{NodePath, PaneRenderer, SharedPaneState};
@ -101,7 +102,7 @@ impl PaneRenderer for ToolbarPane {
}
// Draw sub-tool arrow indicator for tools with modes
let has_sub_tools = matches!(tool, Tool::RegionSelect);
let has_sub_tools = matches!(tool, Tool::RegionSelect | Tool::SelectLasso);
if has_sub_tools {
let arrow_size = 6.0;
let margin = 4.0;
@ -125,6 +126,22 @@ impl PaneRenderer for ToolbarPane {
// Check for click first
if response.clicked() {
*shared.selected_tool = *tool;
// Preset-backed tools: auto-select the matching bundled brush.
let preset_name = match tool {
Tool::Pencil => Some("Pencil"),
Tool::Pen => Some("Pen"),
Tool::Airbrush => Some("Airbrush"),
_ => None,
};
if let Some(name) = preset_name {
if let Some(preset) = bundled_brushes().iter().find(|p| p.name == name) {
let s = &preset.settings;
*shared.brush_opacity = s.opaque.clamp(0.0, 1.0);
*shared.brush_hardness = s.hardness.clamp(0.0, 1.0);
*shared.brush_spacing = s.dabs_per_radius;
*shared.active_brush_settings = s.clone();
}
}
}
// Right-click context menu for tools with sub-options
@ -150,6 +167,33 @@ impl PaneRenderer for ToolbarPane {
ui.close();
}
}
Tool::SelectLasso => {
ui.set_min_width(130.0);
if ui.selectable_label(
*shared.lasso_mode == LassoMode::Freehand,
"Freehand",
).clicked() {
*shared.lasso_mode = LassoMode::Freehand;
*shared.selected_tool = Tool::SelectLasso;
ui.close();
}
if ui.selectable_label(
*shared.lasso_mode == LassoMode::Polygonal,
"Polygonal",
).clicked() {
*shared.lasso_mode = LassoMode::Polygonal;
*shared.selected_tool = Tool::SelectLasso;
ui.close();
}
if ui.selectable_label(
*shared.lasso_mode == LassoMode::Magnetic,
"Magnetic",
).clicked() {
*shared.lasso_mode = LassoMode::Magnetic;
*shared.selected_tool = Tool::SelectLasso;
ui.close();
}
}
_ => {}
}
});
@ -176,6 +220,13 @@ impl PaneRenderer for ToolbarPane {
RegionSelectMode::Lasso => "Lasso",
};
format!("{} - {}{}\nRight-click for options", tool.display_name(), mode, hint)
} else if *tool == Tool::SelectLasso {
let mode = match *shared.lasso_mode {
LassoMode::Freehand => "Freehand",
LassoMode::Polygonal => "Polygonal",
LassoMode::Magnetic => "Magnetic",
};
format!("{} - {}{}\nRight-click for options", tool.display_name(), mode, hint)
} else {
format!("{}{}", tool.display_name(), hint)
};

5
src/assets/todo.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B