diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 0d9edd9c..06b27576 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -20,9 +20,10 @@ next to them. | `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `assets/fonts/Inter-Regular.ttf`, `Inter-Medium.ttf`, `Inter-SemiBold.ttf` | Inter 4.001 (UI font) | The Inter Project Authors (Rasmus Andersson) | | SIL OFL 1.1, [`assets/fonts/OFL-Inter.txt`](assets/fonts/OFL-Inter.txt) | | `assets/fonts/JetBrainsMono-Regular.ttf` | JetBrains Mono 2.305 (numeric font) | The JetBrains Mono Project Authors | | SIL OFL 1.1, [`assets/fonts/OFL-JetBrainsMono.txt`](assets/fonts/OFL-JetBrainsMono.txt) | -| `assets/icons/*.svg` except `slice-knife.svg`, `artboard.svg`, `perspective-crop.svg`, `frame-placeholder.svg`, `type-mask.svg`, `type-mask-vertical.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `brush-replace.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `pen-add-anchor.svg`, `pen-delete-anchor.svg`, `pen-freeform.svg`, `pen-curvature.svg`, `bandage-sparkle.svg`, `painting-symmetry.svg`, `symmetry-*.svg` and `marquee-{row,column}.svg` (113 files; `star-filled.svg` is `star.svg` with a solid fill) | Lucide icons | Lucide Icons and Contributors | (`icons/.svg`; `align-*` are now named `text-align-*` upstream) | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) | +| `assets/icons/*.svg` except `slice-knife.svg`, `artboard.svg`, `perspective-crop.svg`, `frame-placeholder.svg`, `type-mask.svg`, `type-mask-vertical.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `brush-replace.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `convert-point.svg`, `pen-add-anchor.svg`, `pen-delete-anchor.svg`, `pen-freeform.svg`, `pen-curvature.svg`, `bandage-sparkle.svg`, `painting-symmetry.svg`, `symmetry-*.svg` and `marquee-{row,column}.svg` (113 files; `star-filled.svg` is `star.svg` with a solid fill) | Lucide icons | Lucide Icons and Contributors | (`icons/.svg`; `align-*` are now named `text-align-*` upstream) | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) | | `assets/icons/{check,chevron-down,chevron-left,chevron-right,chevron-up,chevrons-left,chevrons-right,circle,clock,compass,info,link,link-2,lock,minus,monitor,moon,move,navigation,plus,search,square,trash,triangle,type,x,zoom-in}.svg` | Lucide icons derived from Feather (subset of the row above) | Cole Bemis (Feather), Lucide Contributors | via Lucide | MIT (Feather) and ISC (Lucide), [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) | | `assets/icons/slice-knife.svg` | Slice tool glyph | PhotoCraft contributors | Original work | MIT OR Apache-2.0 | +| `assets/icons/convert-point.svg` | Convert Point tool glyph: an angle with a square anchor on its apex, drawn on Lucide's 24 px grid and stroke | PhotoCraft contributors | Original work | MIT OR Apache-2.0 | | `assets/icons/artboard.svg` | Artboard tool glyph: a board with a tick at each end of its edges | PhotoCraft contributors | Original work | MIT OR Apache-2.0 | | `assets/icons/perspective-crop.svg` | Perspective Crop tool glyph: a crop frame in perspective with its grid and crop-mark ticks | PhotoCraft contributors | Original work | MIT OR Apache-2.0 | | `assets/icons/type-mask.svg`, `assets/icons/type-mask-vertical.svg` | Horizontal and Vertical Type Mask tool glyphs: a T drawn as marching ants (the vertical one beside a downward arrow), on Lucide's 24 px grid and stroke | PhotoCraft contributors | Original work | MIT OR Apache-2.0 | diff --git a/README.md b/README.md index 249d165c..9f593143 100644 --- a/README.md +++ b/README.md @@ -178,8 +178,8 @@ Every screenshot here is the real app at work on public-domain art, rendered off
-

🧰 65 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 · 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 · 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 · Horizontal and Vertical Type · Horizontal and Vertical Type Mask · Path Selection · Direct Selection · Rectangle · Ellipse · Triangle · Polygon · Line · Custom Shape · Hand · Rotate View · Zoom +

🧰 66 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 · 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 · 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

🖌️ A real brush engine

diff --git a/ROADMAP.md b/ROADMAP.md index 9548c63d..98e8c118 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~30 points (ready for real work ~45% → ~75%, with reliable PSD) and ~1,100–1,800 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first version: full re-measure against Photoshop 2026 27.11.0) · **Target:** Adobe Photoshop 2026 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (toolbox count 56 of 68, 12 missing) · **Target:** Adobe Photoshop 2026 PhotoCraft's one-page summary of how close it is to Adobe Photoshop 2026 (27.11.0, measured from the installed app on 2026-10-10) at version 0.6.0. Detail: [target-app parity](docs/target-app-parity.md) @@ -19,7 +19,7 @@ About 80% of the work parallelizes. | | Value | Kind | |---|---|---| -| **Feature breadth** | **~76%** | partly measured: menus 628/628, tools 53/68, panels 30/35, formats 13+4 of 31 | +| **Feature breadth** | **~76%** | partly measured: menus 628/628, tools 56/68, panels 30/35, formats 13+4 of 31 | | **Ready for real work** | **~45%** (40–50%) | estimated: weighted by dimension below | | Mainstream practitioner | ~43% (38–48%) | estimated: weekly areas of a typical pro, with discounts ([method](docs/target-app-parity.md#mainstream-practitioner-43)) | | Essentials user | ~61% (55–67%) | estimated: core features only ([method](docs/target-app-parity.md#essentials-user-61)) | @@ -114,7 +114,7 @@ Also shipped: Traditional Chinese, Russian, Ukrainian, Czech, Polish, Greek, Dut | 2 | Performance budgets: the 22 over-budget scenarios and the 150-layer GPU crash | 200–350 h | | 3 | User bug backlog to under 100 open | 250–400 h | | 4 | Brush and transform feel (brush-parity, ui-parity rows) | 160–290 h | -| 5 | The 15 missing tools | 60–100 h | +| 5 | The 12 missing tools | 60–100 h | Detail and the next releases: [docs/roadmap.md](docs/roadmap.md). @@ -135,6 +135,7 @@ Detail and the next releases: [docs/roadmap.md](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Toolbox count 56 of 68 (12 missing) with the Convert Point tool (#3204) | | 2026-10-10 | minor | Readiness by audience with hours per tier | | 2026-10-10 | minor | Mainstream-practitioner and essentials-user numbers added | | 2026-10-10 | minor | Alpha gate result (passes; stays alpha) in the stage explanation | diff --git a/assets/icons/convert-point.svg b/assets/icons/convert-point.svg new file mode 100644 index 00000000..fe1088a3 --- /dev/null +++ b/assets/icons/convert-point.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/crates/ui-egui/src/anchor_tools_tests.rs b/crates/ui-egui/src/anchor_tools_tests.rs index 550d82ad..c88f1a2b 100644 --- a/crates/ui-egui/src/anchor_tools_tests.rs +++ b/crates/ui-egui/src/anchor_tools_tests.rs @@ -48,8 +48,8 @@ fn both_tools_join_the_pen_group() { let group: Vec = Tool::ALL.iter().copied().filter(|t| t.key() == 'P').collect(); assert_eq!( group, - vec![Tool::Pen, Tool::FreeformPen, Tool::CurvaturePen, Tool::AddAnchorPoint, Tool::DeleteAnchorPoint], - "P cycles Pen → Freeform → Curvature → Add → Delete" + vec![Tool::Pen, Tool::FreeformPen, Tool::CurvaturePen, Tool::AddAnchorPoint, Tool::DeleteAnchorPoint, Tool::ConvertPoint], + "P cycles Pen → Freeform → Curvature → Add → Delete → Convert Point" ); let mut app = app(Tool::Brush); app.ui.tool = Tool::from_name("addAnchorPoint").unwrap(); diff --git a/crates/ui-egui/src/direct_select.rs b/crates/ui-egui/src/direct_select.rs index 96c0f148..6cc2a293 100644 --- a/crates/ui-egui/src/direct_select.rs +++ b/crates/ui-egui/src/direct_select.rs @@ -1,13 +1,15 @@ -//! Direct Selection tool (A, #790), and the Pen's ⌘/Ctrl (Direct Selection) and ⌥/Alt (Convert -//! Point) modifiers, as in Photoshop. It edits the paths the canvas shows: the targeted vector -//! mask or the active shape layer's path, and the work path. +//! Direct Selection tool (A, #790), the Convert Point tool (#3204), and the Pen's ⌘/Ctrl (Direct +//! Selection) and ⌥/Alt (Convert Point) modifiers, as in Photoshop. It edits the paths the canvas +//! shows: the targeted vector mask or the active shape layer's path, and the work path. //! //! * Click an anchor to select it (⇧ adds or removes, ⌥ selects its whole subpath), drag on empty //! canvas to marquee-select anchors, click empty canvas to deselect. //! * Drag selected anchors to move them, a direction handle to turn it (a smooth point keeps its //! handles collinear; ⌥ breaks them apart), or a segment to bend it (straight segments move). -//! * Pen + ⌥: click an anchor to make it a corner, drag from it to pull out smooth handles, drag a -//! handle to move it alone. +//! * Convert Point (the tool, or Pen + ⌥): click an anchor to make it a corner, drag from it to +//! pull out smooth handles, drag a handle to move it alone. The tool selects the anchor it +//! converts, so its handles show; a press on empty canvas or a segment does nothing, and ⌘/Ctrl +//! is Direct Selection, as with the Pen. //! //! A drag previews with `photocraft_vector::edit` and commits once on release through the engine //! (`path.moveAnchors`, `path.moveHandle`, `path.bendSegment`, `path.convertPoint`): one history @@ -104,16 +106,29 @@ impl Edit { } } -/// Is Direct Selection what the pointer does now: its own tool, or the Pen or an anchor point -/// tool with ⌘/Ctrl held. +/// Is Direct Selection what the pointer does now: its own tool, or the Pen, an anchor point tool +/// or Convert Point with ⌘/Ctrl held. fn direct(tool: Tool, mods: Modifiers) -> bool { - tool == Tool::DirectSelection || (matches!(tool, Tool::Pen | Tool::AddAnchorPoint | Tool::DeleteAnchorPoint) && mods.command) + tool == Tool::DirectSelection || (matches!(tool, Tool::Pen | Tool::AddAnchorPoint | Tool::DeleteAnchorPoint | Tool::ConvertPoint) && mods.command) +} + +/// Is Convert Point what the pointer does now: its own tool, or the Pen with ⌥ held while no path +/// is being drawn. ⌘/Ctrl wins over both (Direct Selection). +fn converting(tool: Tool, mods: Modifiers, pen_idle: bool) -> bool { + !mods.command && (tool == Tool::ConvertPoint || (tool == Tool::Pen && mods.alt && pen_idle)) +} + +/// The tools that take every pointer event on the canvas, not just a press that hits a path. +fn owns(tool: Tool) -> bool { + matches!(tool, Tool::DirectSelection | Tool::ConvertPoint) } /// Does the canvas draw Direct Selection's overlay instead of the plain path outlines? The Add / /// Delete Anchor Point tools show it too: they edit the same paths. pub fn shows(app: &PhotocraftApp, mods: Modifiers) -> bool { - direct(app.ui.tool, mods) || app.ui.direct_selection.drag.is_some() || matches!(app.ui.tool, Tool::AddAnchorPoint | Tool::DeleteAnchorPoint) + direct(app.ui.tool, mods) + || app.ui.direct_selection.drag.is_some() + || matches!(app.ui.tool, Tool::AddAnchorPoint | Tool::DeleteAnchorPoint | Tool::ConvertPoint) } /// The paths Direct Selection edits, front first: the targeted vector mask or the active shape @@ -154,7 +169,7 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: Modifiers) -> bool ToolEvent::Down { x, y, .. } => { let pen_idle = app.ui.pen.is_none(); // Pen + ⌥ is Convert Point only while no path is being drawn. - let convert = tool == Tool::Pen && mods.alt && !mods.command && pen_idle; + let convert = converting(tool, mods, pen_idle); if !(direct(tool, mods) || convert) { return false; } @@ -172,7 +187,7 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: Modifiers) -> bool *preview = edit.preview(base, *start, *end); } Some(Drag::Marquee { end, .. }) => *end = [x, y], - None => return tool == Tool::DirectSelection, + None => return owns(tool), } true } @@ -185,14 +200,15 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: Modifiers) -> bool marquee(app, start, [x, y], add); true } - None => tool == Tool::DirectSelection, + None => owns(tool), }, } } /// A press: pick what's under the pointer and start a drag. False lets the Pen handle a ⌥-press -/// that hit nothing. +/// that hit nothing; the Convert Point tool has nothing else to do, so it keeps the press. fn press(app: &mut PhotocraftApp, p: [f64; 2], mods: Modifiers, convert: bool) -> bool { + let own_tool = app.ui.tool == Tool::ConvertPoint; let tol = 6.0 / f64::from(app.point_zoom().max(0.01)); let ds = &app.ui.direct_selection; let found = candidates(app).into_iter().find_map(|(r, path)| { @@ -202,7 +218,7 @@ fn press(app: &mut PhotocraftApp, p: [f64; 2], mods: Modifiers, convert: bool) - let ds = &mut app.ui.direct_selection; let Some((target, base, hit)) = found else { if convert { - return false; + return own_tool; } ds.drag = Some(Drag::Marquee { start: p, end: p, add: mods.shift }); return true; @@ -214,9 +230,17 @@ fn press(app: &mut PhotocraftApp, p: [f64; 2], mods: Modifiers, convert: bool) - Hit::Handle(k, h) => { let kn = base.subpaths.get(k[0]).and_then(|s| s.knots.get(k[1])); let from = kn.map_or(Point::new(p[0], p[1]), |kn| if h == Handle::In { kn.in_ctrl } else { kn.out_ctrl }); - Edit::Handle(k, h, from, mods.alt) + // Converting moves a handle on its own, as ⌥ does with Direct Selection. + Edit::Handle(k, h, from, mods.alt || convert) + } + Hit::Anchor(k) if convert => { + if own_tool { + // The tool selects the anchor it works on, so the anchor's handles show. + ds.segment = None; + ds.anchors = vec![k]; + } + Edit::Convert(k) } - Hit::Anchor(k) if convert => Edit::Convert(k), // Pen + ⌥ on a segment does nothing. Hit::Segment(..) if convert => return true, Hit::Anchor(k) => { diff --git a/crates/ui-egui/src/direct_select_tests.rs b/crates/ui-egui/src/direct_select_tests.rs index f7aefd05..17334a16 100644 --- a/crates/ui-egui/src/direct_select_tests.rs +++ b/crates/ui-egui/src/direct_select_tests.rs @@ -1,5 +1,6 @@ //! #790: paths can be reshaped after they're drawn, as in Photoshop: the Direct Selection tool -//! (A), and the Pen with ⌘/Ctrl (Direct Selection) or ⌥/Alt (Convert Point). +//! (A), and the Pen with ⌘/Ctrl (Direct Selection) or ⌥/Alt (Convert Point). #3204: Convert Point +//! is a tool of its own, in the Pen's flyout. use egui::Modifiers; use photocraft_doc::vector::Path; @@ -160,3 +161,67 @@ fn shape_layer_paths_are_edited_too() { let b: [i64; 4] = serde_json::from_value(info["bounds"].clone()).unwrap(); assert!(b[0] + b[2] >= 100 && b[1] + b[3] >= 90, "the shape re-rendered: {b:?}"); } + +#[test] +fn the_convert_point_tool_converts_points_without_a_modifier() { + let mut app = app(Tool::ConvertPoint); + with_path(&mut app); + let n = steps(&app); + click(&mut app, [120.0, 40.0], Modifiers::NONE); + assert_eq!(work(&app).subpaths[0].knots[1], photocraft_doc::Knot::corner(120.0, 40.0), "a click makes a corner"); + assert_eq!(steps(&app), n + 1); + assert_eq!(app.ui.direct_selection.anchors, vec![[0, 1]], "the converted anchor is selected"); + drag(&mut app, [40.0, 120.0], [40.0, 140.0], Modifiers::NONE); + let k = work(&app).subpaths[0].knots[3]; + assert_eq!((k.in_ctrl, k.out_ctrl, k.smooth), (Point::new(40.0, 100.0), Point::new(40.0, 140.0), true), "a drag pulls out smooth handles"); + assert_eq!(steps(&app), n + 2, "one history step per press"); + assert_eq!(app.ui.direct_selection.anchors, vec![[0, 3]]); + assert!(app.ui.pen.is_none(), "the tool never starts a pen path"); + assert_eq!(app.ui.tool, Tool::ConvertPoint); + assert!(app.session.undo() && app.session.undo()); + assert!(work(&app).subpaths[0].knots[1].smooth, "undo restores the smooth point"); +} + +#[test] +fn the_convert_point_tool_moves_a_handle_alone_and_ignores_empty_canvas_and_segments() { + let mut app = app(Tool::DirectSelection); + with_path(&mut app); + // Select the smooth anchor with Direct Selection so its handles show. + click(&mut app, [120.0, 40.0], Modifiers::NONE); + app.ui.tool = Tool::ConvertPoint; + drag(&mut app, [140.0, 40.0], [140.0, 70.0], Modifiers::NONE); + let k = work(&app).subpaths[0].knots[1]; + assert_eq!((k.out_ctrl, k.in_ctrl, k.smooth), (Point::new(140.0, 70.0), Point::new(100.0, 40.0), false), "the handle moved without its twin"); + // Nothing happens on empty canvas or on a segment, and the tool keeps the event. + let (before, n) = (work(&app), steps(&app)); + click(&mut app, [80.0, 80.0], Modifiers::NONE); + drag(&mut app, [120.0, 80.0], [120.0, 100.0], Modifiers::NONE); + assert!(app.ui.direct_selection.drag.is_none(), "no marquee"); + assert_eq!(app.ui.direct_selection.anchors, vec![[0, 1]], "the selection stays"); + assert_eq!((work(&app), steps(&app)), (before, n)); + assert!(app.ui.pen.is_none() && app.drag.is_none(), "the press was not handed to another tool"); +} + +#[test] +fn the_convert_point_tool_with_command_is_direct_selection() { + let mut app = app(Tool::ConvertPoint); + with_path(&mut app); + drag(&mut app, [40.5, 120.5], [50.5, 130.5], Modifiers::COMMAND); + assert_eq!(anchor(&app, 3), Point::new(50.0, 130.0), "⌘-drag moves the anchor"); + assert!(work(&app).subpaths[0].knots[3].in_ctrl == work(&app).subpaths[0].knots[3].anchor, "and does not pull out handles"); + // A handle drag with ⌘ turns the smooth pair together, like Direct Selection. + click(&mut app, [120.0, 40.0], Modifiers::COMMAND); + assert_eq!(app.ui.direct_selection.anchors, vec![[0, 1]]); + drag(&mut app, [140.0, 40.0], [120.0, 60.0], Modifiers::COMMAND); + let k = work(&app).subpaths[0].knots[1]; + assert!((k.in_ctrl.x - 120.0).abs() < 1e-9 && (k.in_ctrl.y - 20.0).abs() < 1e-9, "the twin handle turned with it: {:?}", k.in_ctrl); +} + +#[test] +fn the_convert_point_tool_shows_the_path_overlay() { + let mut app = app(Tool::ConvertPoint); + assert!(super::shows(&app, Modifiers::NONE), "anchors are drawn before any press"); + app.ui.tool = Tool::Pen; + assert!(!super::shows(&app, Modifiers::NONE)); + assert!(super::shows(&app, Modifiers::COMMAND)); +} diff --git a/crates/ui-egui/src/hold_keys.rs b/crates/ui-egui/src/hold_keys.rs index 57a7db41..5aba48c4 100644 --- a/crates/ui-egui/src/hold_keys.rs +++ b/crates/ui-egui/src/hold_keys.rs @@ -87,6 +87,7 @@ pub fn cmd_moves(t: Tool) -> bool { | Tool::CurvaturePen | Tool::AddAnchorPoint | Tool::DeleteAnchorPoint + | Tool::ConvertPoint | Tool::RectMarquee | Tool::EllipseMarquee | Tool::Lasso diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 162909fe..d60f0a82 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1720,6 +1720,7 @@ Pen Pressure Přítlak pera Pen Tilt Náklon pera Pen Tool Nástroj pero + Convert Point Tool Nástroj převést bod Photo Fotografie PhotoCraft website Web PhotoCraft Picas Pica diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index b4ce4f46..5a66da4b 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1695,6 +1695,7 @@ Pen Pressure Stiftdruck Pen Tilt Stiftneigung Pen Tool Stift-Werkzeug + Convert Point Tool Punkt-umwandeln-Werkzeug Photo Foto PhotoCraft website PhotoCraft-Website Picas Pica diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 729a3104..cdb89c1a 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3287,6 +3287,7 @@ cameraRaw Yellow Κίτρινο Click a segment to add an anchor point · {alt}-click an anchor to delete it Κάντε κλικ σε ένα τμήμα για να προσθέσετε σημείο αγκύρωσης · {alt}-κλικ σε σημείο αγκύρωσης για διαγραφή Click an anchor point to delete it · {alt}-click a segment to add one Κάντε κλικ σε ένα σημείο αγκύρωσης για να το διαγράψετε · {alt}-κλικ σε τμήμα για προσθήκη Pen Tool Πένα (Pen Tool) + Convert Point Tool Μετατροπή σημείου (Convert Point Tool) Polygon Tool Πολύγωνο (Polygon Tool) Polygonal Lasso Tool Πολυγωνικό λάσο (Polygonal Lasso Tool) Quick Selection Tool Γρήγορη επιλογή (Quick Selection Tool) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index c269fdc6..5bb31c7e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1704,6 +1704,7 @@ Pen Pressure Presión de la pluma Pen Tilt Inclinación de la pluma Pen Tool Herramienta Pluma + Convert Point Tool Herramienta Convertir punto Photo Fotografía PhotoCraft website Sitio web de PhotoCraft Picas Picas diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index f487c4fb..c3b4be8d 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1681,6 +1681,7 @@ Pen Pressure Pression du stylet Pen Tilt Inclinaison du stylet Pen Tool Outil Plume + Convert Point Tool Outil Conversion de point Photo Photo PhotoCraft website Site web de PhotoCraft Picas Picas diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index cf74582b..b5453ff8 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1675,6 +1675,7 @@ Pen Pressure Tekanan Pena Pen Tilt Kemiringan Pena Pen Tool Alat Pen + Convert Point Tool Alat Convert Point Photo Foto PhotoCraft website Situs web PhotoCraft Picas Pica diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 7fb8e586..50cc663a 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1702,6 +1702,7 @@ Pen Pressure Pressione penna Pen Tilt Inclinazione penna Pen Tool Strumento Penna + Convert Point Tool Strumento Converti punto Photo Foto PhotoCraft website Sito web PhotoCraft Picas Pica diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index fb0775a6..7916f4a7 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1704,6 +1704,7 @@ Pen Pressure ペンの筆圧 Pen Tilt ペンの傾き Pen Tool ペンツール + Convert Point Tool アンカーポイントの変換ツール Photo 写真 PhotoCraft website PhotoCraft の Web サイト Picas パイカ diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 29a6bbdc..423234d9 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1673,6 +1673,7 @@ Pen Pressure 펜 압력 Pen Tilt 펜 기울기 Pen Tool 펜 도구 + Convert Point Tool 기준점 변환 도구 Photo 사진 PhotoCraft website PhotoCraft 웹사이트 Picas 파이카 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index d11991cb..ec885830 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1682,6 +1682,7 @@ Pen Pressure Pendruk Pen Tilt Penkanteling Pen Tool Pengereedschap + Convert Point Tool Punt-converteergereedschap Photo Foto PhotoCraft website Website van PhotoCraft Picas Pica's diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index ae47472a..64e6e5b7 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1697,6 +1697,7 @@ Pen Pressure Nacisk pióra Pen Tilt Nachylenie pióra Pen Tool Narzędzie Pióro + Convert Point Tool Narzędzie Konwertuj punkt Photo Zdjęcie PhotoCraft website Strona internetowa PhotoCraft Picas Pikasy diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 83999108..1e3d568a 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1679,6 +1679,7 @@ Pen Pressure Pressão da caneta Pen Tilt Inclinação da caneta Pen Tool Ferramenta Caneta + Convert Point Tool Ferramenta Converter ponto Photo Foto PhotoCraft website Site do PhotoCraft Picas Paicas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 01881284..7e0b6288 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1588,6 +1588,7 @@ Quick Selection Tool Быстрое выделение Object Selection Tool Выделение объектов Pen Tool Перо + Convert Point Tool Преобразование узла Path Selection Tool Выделение контура Direct Selection Tool Выделение узла Add Anchor Point Tool Добавить узел diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 421c0279..141dcd49 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1569,6 +1569,7 @@ Quick Selection Tool Інструмент «Швидке виділення» Object Selection Tool Інструмент «Виділення об'єкта» Pen Tool Інструмент «Перо» + Convert Point Tool Інструмент «Перетворення вузла» Path Selection Tool Інструмент «Виділення контуру» Direct Selection Tool Інструмент «Пряме виділення» Add Anchor Point Tool Інструмент «Додати вузол» diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 6332b3e0..65117cba 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1718,6 +1718,7 @@ Pen Pressure 钢笔压力 Pen Tilt 钢笔倾斜 Pen Tool 钢笔工具 + Convert Point Tool 转换点工具 Photo 照片 PhotoCraft website PhotoCraft 网站 Picas 派卡 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 2c5a5dd0..5a9d8c01 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1710,6 +1710,7 @@ Pen Pressure 筆壓 Pen Tilt 筆傾斜 Pen Tool 筆型工具 + Convert Point Tool 轉換錨點工具 Photo 相片 PhotoCraft website PhotoCraft 網站 Picas 派卡 diff --git a/crates/ui-egui/src/icon_data.rs b/crates/ui-egui/src/icon_data.rs index 11658b1d..d8709a49 100644 --- a/crates/ui-egui/src/icon_data.rs +++ b/crates/ui-egui/src/icon_data.rs @@ -32,6 +32,8 @@ pub static ICONS: &[(&str, &[u8])] = &[ ("cloud", include_bytes!("../../../assets/icons/cloud.svg")), ("compass", include_bytes!("../../../assets/icons/compass.svg")), ("contrast", include_bytes!("../../../assets/icons/contrast.svg")), + // PhotoCraft original: the Convert Point tool glyph. + ("convert-point", include_bytes!("../../../assets/icons/convert-point.svg")), ("copy", include_bytes!("../../../assets/icons/copy.svg")), ("crop", include_bytes!("../../../assets/icons/crop.svg")), ("diamond", include_bytes!("../../../assets/icons/diamond.svg")), diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 0675b2e6..ebd020a9 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -139,6 +139,7 @@ pub fn tool_icon(t: Tool) -> &'static str { Tool::CurvaturePen => "pen-curvature", Tool::AddAnchorPoint => "pen-add-anchor", Tool::DeleteAnchorPoint => "pen-delete-anchor", + Tool::ConvertPoint => "convert-point", Tool::PathSelection => "mouse-pointer-2", Tool::DirectSelection => "direct-select", Tool::Rectangle => "rectangle-horizontal", diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 4d128f5c..49318f84 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -35,7 +35,7 @@ const TOOL_SECTIONS: &[&[&[Tool]]] = &[ &[Tool::Dodge, Tool::Burn, Tool::Sponge], ], &[ - &[Tool::Pen, Tool::FreeformPen, Tool::CurvaturePen, Tool::AddAnchorPoint, Tool::DeleteAnchorPoint], + &[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], diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 9e311241..5432425d 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -120,6 +120,9 @@ pub enum Tool { CurvaturePen, AddAnchorPoint, DeleteAnchorPoint, + /// Converts an anchor between smooth and corner, and pulls handles out of a corner + /// (Photoshop's Convert Point tool; also the Pen's ⌥ mode, `direct_select.rs`). + ConvertPoint, PathSelection, DirectSelection, Rectangle, @@ -134,7 +137,7 @@ pub enum Tool { } impl Tool { - pub const ALL: [Tool; 65] = [ + pub const ALL: [Tool; 66] = [ Tool::Move, Tool::Artboard, Tool::RectMarquee, @@ -189,6 +192,7 @@ impl Tool { Tool::CurvaturePen, Tool::AddAnchorPoint, Tool::DeleteAnchorPoint, + Tool::ConvertPoint, Tool::PathSelection, Tool::DirectSelection, Tool::Rectangle, @@ -261,6 +265,7 @@ impl Tool { Tool::CurvaturePen => "Curvature Pen Tool", Tool::AddAnchorPoint => "Add Anchor Point Tool", Tool::DeleteAnchorPoint => "Delete Anchor Point Tool", + Tool::ConvertPoint => "Convert Point Tool", Tool::PathSelection => "Path Selection Tool", Tool::DirectSelection => "Direct Selection Tool", Tool::Rectangle => "Rectangle Tool", @@ -332,7 +337,7 @@ impl Tool { Tool::SingleRowMarquee | Tool::SingleColumnMarquee => '\0', Tool::Dodge | Tool::Burn | Tool::Sponge => 'O', Tool::QuickSelection | Tool::ObjectSelection => 'W', - Tool::Pen | Tool::FreeformPen | Tool::CurvaturePen | Tool::AddAnchorPoint | Tool::DeleteAnchorPoint => 'P', + Tool::Pen | Tool::FreeformPen | Tool::CurvaturePen | Tool::AddAnchorPoint | Tool::DeleteAnchorPoint | Tool::ConvertPoint => 'P', Tool::PathSelection | Tool::DirectSelection => 'A', Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Line | Tool::CustomShape => 'U', } @@ -1300,11 +1305,15 @@ 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(), 65); + assert_eq!(Tool::ALL.len(), 66); assert_eq!(Tool::from_name("Vertical Type Mask Tool"), Some(Tool::VerticalTypeMask)); assert_eq!(Tool::from_name("addAnchorPoint"), Some(Tool::AddAnchorPoint)); assert_eq!(Tool::from_name("Delete Anchor Point Tool"), Some(Tool::DeleteAnchorPoint)); assert_eq!(Tool::DeleteAnchorPoint.key(), 'P'); + assert_eq!(Tool::from_name("convertPoint"), Some(Tool::ConvertPoint)); + assert_eq!(Tool::from_name("Convert Point Tool"), Some(Tool::ConvertPoint)); + assert_eq!(Tool::ConvertPoint.key(), 'P', "it shares the Pen's key"); + assert!(!Tool::ConvertPoint.is_brushlike()); assert_eq!(Tool::from_name("Remove Tool"), Some(Tool::Remove)); assert_eq!(Tool::Remove.key(), 'J'); assert!(Tool::Remove.is_brushlike()); diff --git a/crates/ui-egui/src/tool_reset.rs b/crates/ui-egui/src/tool_reset.rs index 17bfdad1..5d95df62 100644 --- a/crates/ui-egui/src/tool_reset.rs +++ b/crates/ui-egui/src/tool_reset.rs @@ -181,6 +181,7 @@ pub fn reset_options(o: &mut ToolOptions, tool: Tool) { | Tool::SliceSelect | Tool::AddAnchorPoint | Tool::DeleteAnchorPoint + | Tool::ConvertPoint | Tool::Artboard | Tool::Frame => {} } diff --git a/docs/gaps.md b/docs/gaps.md index c4417fbf..e638534c 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -54,8 +54,8 @@ what an agent can't finish alone. scripted check there) is the only real oracle. ### G6. Missing tools -- **Missing (6 of 68):** Color Sampler (#1046), Color Replacement (engine command exists), Art - History Brush, Add Anchor Point, Delete Anchor Point, Convert Point (only as Pen + ⌥). Freeform +- **Missing (5 of 68):** Color Sampler (#1046), Color Replacement (engine command exists), Art + History Brush, Add Anchor Point, Delete Anchor Point. Convert Point landed (#3204). Freeform Pen and Curvature Pen landed (#1054); Horizontal and Vertical Type Mask landed (#1055, `type.createSelection`); Perspective Crop landed (#1043, `image.perspectiveCrop`); Frame landed (#2914). diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 0f946016..7951232f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -61,7 +61,7 @@ architecture and the PSD writer are the serial parts. | Component | Weight | Ours / Photoshop | % | Kind | |---|---:|---|---:|---| | Menu commands | 30% | 628 / 628 catalog items dispatch a live command | 100% | measured (`cargo xtask parity`) | -| Toolbox tools | 15% | 58 / 68 (Photoshop's 67 `.kys` tools plus Remove, Triangle, Curvature Pen; minus legacy Rounded Rectangle and Targeted Adjustment) | 85% | measured (`Tool` enum in `ui-egui/src/state.rs`, `TOOL_SECTIONS` in `panels.rs`) | +| Toolbox tools | 15% | 59 / 68 (Photoshop's 67 `.kys` tools plus Remove, Triangle, Curvature Pen; minus legacy Rounded Rectangle and Targeted Adjustment) | 87% | measured (`Tool` enum in `ui-egui/src/state.rs`, `TOOL_SECTIONS` in `panels.rs`) | | Panels | 10% | 30 / 35 Window-menu panels | 86% | measured (`view_cmds.rs`, panel modules) | | Filters and adjustments | 10% | every Filter and Image › Adjustments item live; Filter Gallery 47 looks | 100% | measured (menu catalog) | | File formats (usage-weighted) | 15% | 13 of 31 Photoshop formats supported in Photoshop's directions, 4 partial; the common ones (PSD/PSB, TIFF, JPEG, PNG, WebP) all present | ~80% (48% by count) | measured list, estimated weights ([file-format-parity.md](file-format-parity.md)) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 91afa00d..6fd9b839 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -40,7 +40,7 @@ Triangle and Curvature Pen (minus legacy Rounded Rectangle and Targeted Adjustme | Eraser | Eraser, Background Eraser, Magic Eraser | | | Fill | Gradient, Paint Bucket | | | Focus, toning | Blur, Sharpen, Smudge, Dodge, Burn, Sponge | | -| Pen | Pen, Freeform Pen, Curvature Pen (#1054) | Add Anchor Point, Delete Anchor Point, Convert Point (⌥ with Pen only) | +| Pen | Pen, Freeform Pen, Curvature Pen (#1054), Convert Point (#3204; also ⌥ with Pen) | Add Anchor Point, Delete Anchor Point | | Type | Horizontal, Vertical, Vertical Type Mask, Horizontal Type Mask | | | Path selection | Path Selection, Direct Selection | | | Shapes | Rectangle, Ellipse, Triangle, Polygon, Line, Custom Shape | (Star is a Polygon option in Photoshop) |