- 🧰 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)}),
}
})
|