Toolbar: Star Tool in the shape slot between Polygon and Line (#11377)

This commit is contained in:
Hector Villarreal committed 2026-11-07 17:32:45 -06:00
1 parent 153fcff8c3
commit de9e11df9b
13 files changed
+129 -11

No files matched your search

+2 -2
View File
@@ -185,8 +185,8 @@ Every screenshot here is the real app at work on public-domain art, rendered off
<table>
<tr>
<td width="33%" valign="top">
<h4>🧰 68 tools</h4>
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
<h4>🧰 69 tools</h4>
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
</td>
<td width="33%" valign="top">
<h4>🖌️ A real brush engine</h4>
+1
View File
@@ -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")),
+5 -1
View File
@@ -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 {
+1
View File
@@ -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",
}
+3
View File
@@ -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;
+1 -1
View File
@@ -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]],
];
+103
View File
@@ -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<LiveShape> {
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:?}"),
}
}
+1 -1
View File
@@ -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() {
+6 -2
View File
@@ -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).
+1 -1
View File
@@ -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,
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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)}),
}
})