diff --git a/README.md b/README.md index b1b453a3180..c1789ab7d22 100644 --- a/README.md +++ b/README.md @@ -185,8 +185,8 @@ Every screenshot here is the real app at work on public-domain art, rendered off
-

🧰 68 tools

- Move · Artboard · Rectangular and Elliptical Marquee · Single Row Marquee · Single Column Marquee · Lasso · Polygonal Lasso · Magnetic Lasso · Object Selection · Quick Selection · Magic Wand · Crop · Perspective Crop · Slice · Slice Select · Frame · Eyedropper · Color Sampler · Ruler · Note · Count · Remove · Spot Healing Brush · Healing Brush · Patch · Content-Aware Move · Red Eye · Brush · Pencil · Mixer Brush · Color Replacement · Clone Stamp · Pattern Stamp · History Brush · Art History Brush · Eraser · Background Eraser · Magic Eraser · Gradient · Paint Bucket · Blur · Sharpen · Smudge · Dodge · Burn · Sponge · Pen · Freeform Pen · Curvature Pen · Add Anchor Point · Delete Anchor Point · Convert Point · Horizontal and Vertical Type · Horizontal and Vertical Type Mask · Path Selection · Direct Selection · Rectangle · Ellipse · Triangle · Polygon · Line · Custom Shape · Hand · Rotate View · Zoom +

🧰 69 tools

+ Move · Artboard · Rectangular and Elliptical Marquee · Single Row Marquee · Single Column Marquee · Lasso · Polygonal Lasso · Magnetic Lasso · Object Selection · Quick Selection · Magic Wand · Crop · Perspective Crop · Slice · Slice Select · Frame · Eyedropper · Color Sampler · Ruler · Note · Count · Remove · Spot Healing Brush · Healing Brush · Patch · Content-Aware Move · Red Eye · Brush · Pencil · Mixer Brush · Color Replacement · Clone Stamp · Pattern Stamp · History Brush · Art History Brush · Eraser · Background Eraser · Magic Eraser · Gradient · Paint Bucket · Blur · Sharpen · Smudge · Dodge · Burn · Sponge · Pen · Freeform Pen · Curvature Pen · Add Anchor Point · Delete Anchor Point · Convert Point · Horizontal and Vertical Type · Horizontal and Vertical Type Mask · Path Selection · Direct Selection · Rectangle · Ellipse · Triangle · Polygon · Star · Line · Custom Shape · Hand · Rotate View · Zoom

🖌️ A real brush engine

diff --git a/crates/engine/src/shell_commands.rs b/crates/engine/src/shell_commands.rs index 49db9509e52..7aaf58ffcd6 100644 --- a/crates/engine/src/shell_commands.rs +++ b/crates/engine/src/shell_commands.rs @@ -169,6 +169,7 @@ pub const TOOLS: &[(&str, &str, Option<&str>)] = &[ ("tools.select.ellipseShape", "Ellipse Tool", Some("U")), ("tools.select.triangle", "Triangle Tool", Some("U")), ("tools.select.polygon", "Polygon Tool", Some("U")), + ("tools.select.star", "Star Tool", Some("U")), ("tools.select.line", "Line Tool", Some("U")), ("tools.select.customShape", "Custom Shape Tool", Some("U")), ("tools.select.slice", "Slice Tool", Some("C")), diff --git a/crates/ui-egui/src/canvas/snap_ui.rs b/crates/ui-egui/src/canvas/snap_ui.rs index 989af47e1c9..f1a8917b637 100644 --- a/crates/ui-egui/src/canvas/snap_ui.rs +++ b/crates/ui-egui/src/canvas/snap_ui.rs @@ -146,6 +146,7 @@ fn is_point_tool(t: Tool) -> bool { | Tool::EllipseShape | Tool::Triangle | Tool::Polygon + | Tool::Star | Tool::Line | Tool::Type | Tool::VerticalType @@ -334,7 +335,10 @@ pub(crate) fn snap_click(app: &PhotocraftApp, p: [f64; 2], mods: egui::Modifiers /// the pixel grid. fn pixel_round(app: &PhotocraftApp, p: [f64; 2]) -> [f64; 2] { if app.session.prefs().tools.snap_vector_tools_and_transforms_to_pixel_grid - && matches!(app.ui.tool, Tool::Pen | Tool::CurvaturePen | Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Line) + && matches!( + app.ui.tool, + Tool::Pen | Tool::CurvaturePen | Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Star | Tool::Line + ) { [p[0].round(), p[1].round()] } else { diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index c102064a7f1..0a40f7b0343 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -179,6 +179,7 @@ pub fn tool_icon(t: Tool) -> &'static str { Tool::EllipseShape => "circle", Tool::Triangle => "triangle", Tool::Polygon => "pentagon", + Tool::Star => "star", Tool::Line => "slash", Tool::CustomShape => "cloud", } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 7d5d2a90e86..2388c9d29dd 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -865,6 +865,9 @@ mod transform_reference_point_tests; #[cfg(test)] mod transform_relative_position_tests; +#[cfg(test)] +mod star_tool_tests; + #[cfg(test)] mod transform_several_tests; diff --git a/crates/ui-egui/src/panels/toolbar.rs b/crates/ui-egui/src/panels/toolbar.rs index 12f85de2c10..da8500f5351 100644 --- a/crates/ui-egui/src/panels/toolbar.rs +++ b/crates/ui-egui/src/panels/toolbar.rs @@ -30,7 +30,7 @@ pub(crate) const TOOL_SECTIONS: &[&[&[Tool]]] = &[ &[Tool::Pen, Tool::FreeformPen, Tool::CurvaturePen, Tool::AddAnchorPoint, Tool::DeleteAnchorPoint, Tool::ConvertPoint], &[Tool::Type, Tool::VerticalType, Tool::VerticalTypeMask, Tool::HorizontalTypeMask], &[Tool::PathSelection, Tool::DirectSelection], - &[Tool::Rectangle, Tool::EllipseShape, Tool::Triangle, Tool::Polygon, Tool::Line, Tool::CustomShape], + &[Tool::Rectangle, Tool::EllipseShape, Tool::Triangle, Tool::Polygon, Tool::Star, Tool::Line, Tool::CustomShape], ], &[&[Tool::Hand, Tool::RotateView], &[Tool::Zoom]], ]; diff --git a/crates/ui-egui/src/star_tool_tests.rs b/crates/ui-egui/src/star_tool_tests.rs new file mode 100644 index 00000000000..e79fa9f4c33 --- /dev/null +++ b/crates/ui-egui/src/star_tool_tests.rs @@ -0,0 +1,103 @@ +//! The Star Tool (#11377): in the shape slot between Polygon and Line, on U, and a drag makes a +//! live star shape layer with PhotoCraft's star defaults (5 points, star ratio 0.5). + +use egui::{Key, Modifiers}; +use photocraft_doc::{LayerContent, LiveShape}; +use serde_json::json; + +use crate::PhotocraftApp; +use crate::canvas::{ToolEvent, tool_event}; +use crate::panels::toolbar::TOOL_SECTIONS; +use crate::state::Tool; + +const SHAPES: [Tool; 7] = [Tool::Rectangle, Tool::EllipseShape, Tool::Triangle, Tool::Polygon, Tool::Star, Tool::Line, Tool::CustomShape]; + +fn app() -> PhotocraftApp { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + app.sync_views(); + app +} + +fn press(app: &mut PhotocraftApp, key: Key, modifiers: Modifiers) { + let ctx = egui::Context::default(); + let raw = egui::RawInput { + events: vec![egui::Event::ModifiersChanged(modifiers), egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }], + ..Default::default() + }; + ctx.begin_pass(raw); + crate::shortcuts::handle(app, &ctx); + ctx.end_pass().textures_delta.clear(); +} + +fn drag(app: &mut PhotocraftApp, from: [f64; 2], to: [f64; 2], mods: Modifiers) { + tool_event(app, ToolEvent::Down { x: from[0], y: from[1], pressure: 1.0 }, mods); + tool_event(app, ToolEvent::Move { x: (from[0] + to[0]) / 2.0, y: (from[1] + to[1]) / 2.0, pressure: 1.0 }, mods); + tool_event(app, ToolEvent::Move { x: to[0], y: to[1], pressure: 1.0 }, mods); + tool_event(app, ToolEvent::Up { x: to[0], y: to[1] }, mods); +} + +fn live_shape(app: &PhotocraftApp) -> Option { + let st = app.session.active()?; + match &st.doc.layer(st.active_layer?)?.content { + LayerContent::Shape(s) => s.live.clone(), + _ => None, + } +} + +#[test] +fn star_sits_in_the_shape_slot_after_polygon() { + let slot = TOOL_SECTIONS.iter().flat_map(|s| s.iter()).find(|slot| slot.contains(&Tool::Star)).copied(); + assert_eq!(slot, Some(&SHAPES[..])); + let pos = |t: Tool| Tool::ALL.iter().position(|x| *x == t); + assert_eq!(pos(Tool::Star), pos(Tool::Polygon).map(|p| p + 1), "Tool::ALL keeps the slot order"); +} + +#[test] +fn star_is_a_shape_tool_on_u_with_its_label_icon_and_name() { + assert!(crate::tools::vector_ui::is_shape_tool(Tool::Star)); + assert_eq!(Tool::Star.key(), 'U'); + assert_eq!(Tool::Star.label(), "Star Tool"); + assert_eq!(Tool::from_name("Star Tool"), Some(Tool::Star)); + assert_eq!(Tool::from_name("star"), Some(Tool::Star)); + assert_eq!(crate::icons::tool_icon(Tool::Star), "star"); +} + +#[test] +fn shift_u_steps_from_polygon_to_star_to_line() { + let mut app = app(); + app.ui.tool = Tool::Polygon; + press(&mut app, Key::U, Modifiers::SHIFT); + assert_eq!(app.ui.tool, Tool::Star); + press(&mut app, Key::U, Modifiers::SHIFT); + assert_eq!(app.ui.tool, Tool::Line); +} + +#[test] +fn a_drag_makes_a_live_star_shape_layer_with_one_undo_step() { + let mut app = app(); + app.ui.tool = Tool::Star; + let before = app.session.active().unwrap().history.past_len(); + drag(&mut app, [20.0, 30.0], [120.0, 90.0], Modifiers::NONE); + match live_shape(&app) { + Some(LiveShape::Polygon { rect, sides, star_ratio, .. }) => { + assert_eq!((sides, star_ratio), (5, 0.5)); + assert_eq!(rect, [20.0, 30.0, 100.0, 60.0]); + } + other => panic!("a live star shape, got {other:?}"), + } + assert_eq!(app.session.active().unwrap().history.past_len(), before + 1, "one history step"); + app.run("edit.undo", json!({})).unwrap(); + assert!(live_shape(&app).is_none(), "undo takes the star away"); +} + +#[test] +fn shift_and_alt_constrain_and_centre_the_star_like_the_polygon_tool() { + let mut app = app(); + app.ui.tool = Tool::Star; + drag(&mut app, [100.0, 100.0], [140.0, 120.0], Modifiers::SHIFT | Modifiers::ALT); + match live_shape(&app) { + Some(LiveShape::Polygon { rect, .. }) => assert_eq!(rect, [60.0, 60.0, 80.0, 80.0], "a square box drawn from the centre"), + other => panic!("a live star shape, got {other:?}"), + } +} diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 256a88bb38d..5b793318190 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -769,7 +769,7 @@ mod tests { assert!(!Tool::RedEye.is_brushlike()); assert_eq!(Tool::from_name("patternStamp"), Some(Tool::PatternStamp)); assert_eq!(Tool::from_name("Pattern Stamp Tool"), Some(Tool::PatternStamp)); - assert_eq!(Tool::ALL.len(), 68); + assert_eq!(Tool::ALL.len(), 69); } fn vector_tool_names_parse() { diff --git a/crates/ui-egui/src/state/tool.rs b/crates/ui-egui/src/state/tool.rs index 8dda7ce2319..66aa7d53f68 100644 --- a/crates/ui-egui/src/state/tool.rs +++ b/crates/ui-egui/src/state/tool.rs @@ -71,6 +71,8 @@ pub enum Tool { EllipseShape, Triangle, Polygon, + /// The shape flyout's Star Tool (#11377): a live polygon shape with a star ratio. + Star, Line, CustomShape, Slice, @@ -79,7 +81,7 @@ pub enum Tool { } impl Tool { - pub const ALL: [Tool; 68] = [ + pub const ALL: [Tool; 69] = [ Tool::Move, Tool::Artboard, Tool::RectMarquee, @@ -143,6 +145,7 @@ impl Tool { Tool::EllipseShape, Tool::Triangle, Tool::Polygon, + Tool::Star, Tool::Line, Tool::CustomShape, Tool::Slice, @@ -218,6 +221,7 @@ impl Tool { Tool::EllipseShape => "Ellipse Tool", Tool::Triangle => "Triangle Tool", Tool::Polygon => "Polygon Tool", + Tool::Star => "Star Tool", Tool::Line => "Line Tool", Tool::CustomShape => "Custom Shape Tool", } @@ -288,7 +292,7 @@ impl Tool { // In the Pen flyout, but P never reaches them. Tool::AddAnchorPoint | Tool::DeleteAnchorPoint | Tool::ConvertPoint => '', Tool::PathSelection | Tool::DirectSelection => 'A', - Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Line | Tool::CustomShape => 'U', + Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Star | Tool::Line | Tool::CustomShape => 'U', } } /// Glyph drawn in the toolbar (vector icons come later). diff --git a/crates/ui-egui/src/tools/corner_radius.rs b/crates/ui-egui/src/tools/corner_radius.rs index 6358ffd1394..f5ec3192e9e 100644 --- a/crates/ui-egui/src/tools/corner_radius.rs +++ b/crates/ui-egui/src/tools/corner_radius.rs @@ -150,7 +150,7 @@ pub struct Drag { fn shows(app: &PhotocraftApp) -> bool { app.ui.transform.is_none() && match app.ui.tool { - Tool::PathSelection | Tool::Rectangle | Tool::Triangle | Tool::Polygon => true, + Tool::PathSelection | Tool::Rectangle | Tool::Triangle | Tool::Polygon | Tool::Star => true, Tool::Move => app.ui.tool_options.move_show_transform, _ => false, } diff --git a/crates/ui-egui/src/tools/shape_bounds_box.rs b/crates/ui-egui/src/tools/shape_bounds_box.rs index ee137bc5418..d56508bf969 100644 --- a/crates/ui-egui/src/tools/shape_bounds_box.rs +++ b/crates/ui-egui/src/tools/shape_bounds_box.rs @@ -62,7 +62,7 @@ pub struct Drag { fn shows(app: &PhotocraftApp) -> bool { app.ui.transform.is_none() && app.ui.view.shows(app.ui.view.show.target_path) - && matches!(app.ui.tool, Tool::PathSelection | Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Line) + && matches!(app.ui.tool, Tool::PathSelection | Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Star | Tool::Line) } /// The selected shape layers and the box round their paths. `None` unless at least one layer is diff --git a/crates/ui-egui/src/tools/tool_reset.rs b/crates/ui-egui/src/tools/tool_reset.rs index 5ae380a4be8..6c19b9e42bb 100644 --- a/crates/ui-egui/src/tools/tool_reset.rs +++ b/crates/ui-egui/src/tools/tool_reset.rs @@ -257,7 +257,7 @@ pub fn option_keys(tool: Tool) -> &'static [&'static str] { arrow_length, arrow_concavity, ), - Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::CustomShape => { + Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Star | Tool::CustomShape => { reset!(shape_mode, shape_fill, stroke_width, shape_stroke, shape_fill_gradient, shape_stroke_gradient, corner_radius, polygon_sides, line_weight) } Tool::ColorSampler diff --git a/crates/ui-egui/src/tools/vector_ui/shapes.rs b/crates/ui-egui/src/tools/vector_ui/shapes.rs index 7b67544721d..562e700bc3b 100644 --- a/crates/ui-egui/src/tools/vector_ui/shapes.rs +++ b/crates/ui-egui/src/tools/vector_ui/shapes.rs @@ -3,7 +3,7 @@ use super::overlay::path_lines; use super::{Affine, Color32, PhotocraftApp, Tokens, Tool, ToolOptions, Value, ViewXform, json, paint_param}; pub fn is_shape_tool(t: Tool) -> bool { - matches!(t, Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Line | Tool::CustomShape) + matches!(t, Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Star | Tool::Line | Tool::CustomShape) } fn rgb32(c: [f32; 4]) -> Color32 { @@ -80,6 +80,8 @@ pub(super) fn shape_geometry(o: &ToolOptions, tool: Tool, start: [f64; 2], end: Tool::Rectangle | Tool::CustomShape => json!({"kind": "rect", "rect": rect}), Tool::EllipseShape => json!({"kind": "ellipse", "rect": rect}), Tool::Triangle => json!({"kind": "polygon", "rect": rect, "sides": 3}), + // PhotoCraft's star defaults; the Star Tool's own options are not measured yet (#11377). + Tool::Star => json!({"kind": "star", "rect": rect, "sides": 5, "starRatio": 0.5}), _ => json!({"kind": "polygon", "rect": rect, "sides": o.polygon_sides.max(3)}), } })