Photoshop's Artboard tool in the Move flyout (V, ⇧V cycles Move ↔ Artboard): dragging on an empty spot draws a new board (`layer.new.artboard`), clicking a board selects it, dragging it moves it with its contents, and the selected board's edge and corner handles resize it (`layer.artboard.set`, moveContents: false). Each gesture is one undo step. Options bar: Size preset, W/H, Make Portrait, Make Landscape, Add New Artboard. Original toolbar glyph (assets/icons/artboard.svg, ATTRIBUTION.md row). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
1f64daeba4
commit
2e0c426692
|
|
@ -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) | <https://github.com/rsms/inter> | 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 | <https://github.com/JetBrains/JetBrainsMono> | SIL OFL 1.1, [`assets/fonts/OFL-JetBrainsMono.txt`](assets/fonts/OFL-JetBrainsMono.txt) |
|
||||
| `assets/icons/*.svg` except `slice-knife.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` (112 files; `star-filled.svg` is `star.svg` with a solid fill) | Lucide icons | Lucide Icons and Contributors | <https://github.com/lucide-icons/lucide> (`icons/<name>.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`, `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` (112 files; `star-filled.svg` is `star.svg` with a solid fill) | Lucide icons | Lucide Icons and Contributors | <https://github.com/lucide-icons/lucide> (`icons/<name>.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-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 | <https://github.com/feathericons/feather> 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/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/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 |
|
||||
| `assets/icons/marquee-row.svg`, `assets/icons/marquee-column.svg` | Single Row and Single Column Marquee tool glyphs: a dashed row (column) between two edge bars, drawn on Lucide's 24 px grid and stroke | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
|
||||
| `assets/icons/clip-below.svg`, `assets/icons/clip-release.svg` | Clipping-mask cursors (⌥ over the line between two layers): a hooked arrow onto the layer below; the same with a small "no" badge to release | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
|
||||
|
|
|
|||
|
|
@ -178,8 +178,8 @@ Every screenshot here is the real app at work on public-domain art, rendered off
|
|||
<table>
|
||||
<tr>
|
||||
<td width="33%" valign="top">
|
||||
<h4>🧰 62 tools</h4>
|
||||
Move · Rectangular and Elliptical Marquee · Single Row Marquee · Single Column Marquee · Lasso · Polygonal Lasso · Magnetic Lasso · Object Selection · Quick Selection · Magic Wand · Crop · Slice · Slice Select · 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
|
||||
<h4>🧰 63 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 · Slice · Slice Select · 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
|
||||
</td>
|
||||
<td width="33%" valign="top">
|
||||
<h4>🖌️ A real brush engine</h4>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<!-- PhotoCraft's own Artboard tool glyph (not from an icon set): a board with a tick at each end of its edges. -->
|
||||
<rect x="7" y="7" width="10" height="10" rx="1" />
|
||||
<path d="M7 2v2.5M17 2v2.5M7 19.5V22M17 19.5V22" />
|
||||
<path d="M2 7h2.5M2 17h2.5M19.5 7H22M19.5 17H22" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 487 B |
|
|
@ -0,0 +1,338 @@
|
|||
//! Artboard tool (Move group, V / ⇧V cycles with the Move tool): drag on an empty spot to draw a
|
||||
//! new artboard, click a board to select it, drag it to move it with its contents, and drag the
|
||||
//! selected board's edge and corner handles to resize it. The options bar sets the selected board's
|
||||
//! size (preset, W/H, portrait/landscape) and adds a board of the same size beside the last one.
|
||||
//! Every change is one engine command (`layer.new.artboard`, `layer.artboard.set`), so each gesture
|
||||
//! is one undo step; this module only turns pointer gestures into them.
|
||||
|
||||
use egui::{CursorIcon, Pos2, Rect, Stroke, vec2};
|
||||
use photocraft_doc::{Document, Layer, LayerId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::PhotocraftApp;
|
||||
use crate::canvas::{ToolEvent, ViewXform};
|
||||
use crate::crop_ui::{Hit, hit};
|
||||
use crate::state::Tool;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// How far from an edge (screen points) a press still grabs its handle.
|
||||
const HANDLE_PX: f64 = 6.0;
|
||||
/// A press that moves less than this (screen points) is a click, not a drawn board.
|
||||
const CLICK_PX: f64 = 3.0;
|
||||
|
||||
/// Artboard tool state (serde: readable through the control channel).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ArtboardToolUi {
|
||||
#[serde(skip)]
|
||||
pub drag: Option<ArtboardDrag>,
|
||||
}
|
||||
|
||||
/// A gesture in progress. Board rects are `[x0, y0, x1, y1]` in document pixels.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ArtboardDrag {
|
||||
/// Drawing a new board from `start`.
|
||||
New { start: [f64; 2], cur: [f64; 2] },
|
||||
/// Moving board `id`, whose rect was `rect` at the press.
|
||||
Move { id: LayerId, rect: [i64; 4], start: [f64; 2], cur: [f64; 2] },
|
||||
/// Dragging handle (`hx`, `hy`) of board `id`: −1 the left/top edge, 1 the right/bottom, 0 neither.
|
||||
Resize { id: LayerId, rect: [i64; 4], hx: i8, hy: i8, start: [f64; 2], cur: [f64; 2] },
|
||||
}
|
||||
|
||||
fn edges(r: photocraft_geom::Rect) -> [i64; 4] {
|
||||
[i64::from(r.x0), i64::from(r.y0), i64::from(r.x1), i64::from(r.y1)]
|
||||
}
|
||||
|
||||
fn as_f64(r: [i64; 4]) -> [f64; 4] {
|
||||
r.map(|v| v as f64)
|
||||
}
|
||||
|
||||
/// `[x, y, width, height]` for the engine's `rect` param.
|
||||
fn xywh(r: [i64; 4]) -> [i64; 4] {
|
||||
[r[0], r[1], r[2].saturating_sub(r[0]), r[3].saturating_sub(r[1])]
|
||||
}
|
||||
|
||||
/// The selected board: the active layer's artboard, with its edges.
|
||||
fn active_board(app: &PhotocraftApp) -> Option<(LayerId, [i64; 4])> {
|
||||
let st = app.session.active()?;
|
||||
let id = st.doc.artboard_of(st.active_layer?)?;
|
||||
Some((id, edges(st.doc.layer(id).and_then(Layer::artboard)?.rect)))
|
||||
}
|
||||
|
||||
/// The topmost board under a document point.
|
||||
fn board_at(doc: &Document, p: [f64; 2]) -> Option<(LayerId, [i64; 4])> {
|
||||
let inside = |r: [f64; 4]| p[0] >= r[0] && p[0] < r[2] && p[1] >= r[1] && p[1] < r[3];
|
||||
doc.artboards().iter().rev().map(|b| (b.0, edges(b.2.rect))).find(|b| inside(as_f64(b.1)))
|
||||
}
|
||||
|
||||
fn tolerance(app: &PhotocraftApp) -> f64 {
|
||||
HANDLE_PX / f64::from(app.point_zoom()).max(0.01)
|
||||
}
|
||||
|
||||
/// The selected board's handle under a document point.
|
||||
fn handle_at(app: &PhotocraftApp, p: [f64; 2]) -> Option<(LayerId, [i64; 4], i8, i8)> {
|
||||
let (id, r) = active_board(app)?;
|
||||
match hit(as_f64(r), p, tolerance(app)) {
|
||||
Hit::Handle(hx, hy) => Some((id, r, hx, hy)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The board drawn from `a` to `b`, edges on whole pixels.
|
||||
fn drawn(a: [f64; 2], b: [f64; 2]) -> [i64; 4] {
|
||||
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])].map(|v| v.round() as i64)
|
||||
}
|
||||
|
||||
/// `d` rounded to whole pixels.
|
||||
fn offset(start: [f64; 2], cur: [f64; 2]) -> [i64; 2] {
|
||||
[(cur[0] - start[0]).round() as i64, (cur[1] - start[1]).round() as i64]
|
||||
}
|
||||
|
||||
/// `r` with handle (`hx`, `hy`) dragged by `d`: an edge dragged past the opposite one flips the
|
||||
/// board, which never gets narrower than one pixel.
|
||||
fn resized(r: [i64; 4], hx: i8, hy: i8, d: [i64; 2]) -> [i64; 4] {
|
||||
let mut e = r;
|
||||
for (h, lo, hi, dv) in [(hx, 0, 2, d[0]), (hy, 1, 3, d[1])] {
|
||||
match h {
|
||||
-1 => e[lo] = e[lo].saturating_add(dv),
|
||||
1 => e[hi] = e[hi].saturating_add(dv),
|
||||
_ => {}
|
||||
}
|
||||
let (a, b) = (e[lo].min(e[hi]), e[lo].max(e[hi]));
|
||||
(e[lo], e[hi]) = (a, b.max(a.saturating_add(1)));
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
/// The board a gesture would leave, for the preview.
|
||||
fn preview(d: &ArtboardDrag) -> [i64; 4] {
|
||||
match *d {
|
||||
ArtboardDrag::New { start, cur } => drawn(start, cur),
|
||||
ArtboardDrag::Move { rect, start, cur, .. } => {
|
||||
let o = offset(start, cur);
|
||||
[rect[0].saturating_add(o[0]), rect[1].saturating_add(o[1]), rect[2].saturating_add(o[0]), rect[3].saturating_add(o[1])]
|
||||
}
|
||||
ArtboardDrag::Resize { rect, hx, hy, start, cur, .. } => resized(rect, hx, hy, offset(start, cur)),
|
||||
}
|
||||
}
|
||||
|
||||
fn report(app: &mut PhotocraftApp, r: Result<Value, String>) {
|
||||
if let Err(e) = r {
|
||||
app.ui.status = e;
|
||||
app.ui.status_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pointer input for the Artboard tool. Returns true when the event was its own.
|
||||
pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, _mods: egui::Modifiers) -> bool {
|
||||
if app.ui.tool != Tool::Artboard {
|
||||
return false;
|
||||
}
|
||||
let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return true };
|
||||
match ev {
|
||||
ToolEvent::Down { x, y, .. } => {
|
||||
let p = [x, y];
|
||||
app.ui.artboard_tool.drag = Some(if let Some((id, rect, hx, hy)) = handle_at(app, p) {
|
||||
ArtboardDrag::Resize { id, rect, hx, hy, start: p, cur: p }
|
||||
} else if let Some((id, rect)) = board_at(&doc, p) {
|
||||
if app.session.active().and_then(|st| st.active_layer) != Some(id) {
|
||||
let r = app.run("layer.select", json!({"layer": id.0}));
|
||||
report(app, r);
|
||||
}
|
||||
ArtboardDrag::Move { id, rect, start: p, cur: p }
|
||||
} else {
|
||||
ArtboardDrag::New { start: p, cur: p }
|
||||
});
|
||||
}
|
||||
ToolEvent::Move { x, y, .. } => {
|
||||
if let Some(ArtboardDrag::New { cur, .. } | ArtboardDrag::Move { cur, .. } | ArtboardDrag::Resize { cur, .. }) = &mut app.ui.artboard_tool.drag {
|
||||
*cur = [x, y];
|
||||
}
|
||||
}
|
||||
ToolEvent::Up { x, y } => {
|
||||
let Some(mut d) = app.ui.artboard_tool.drag.take() else { return true };
|
||||
let (ArtboardDrag::New { cur, .. } | ArtboardDrag::Move { cur, .. } | ArtboardDrag::Resize { cur, .. }) = &mut d;
|
||||
*cur = [x, y];
|
||||
let r = preview(&d);
|
||||
let result = match d {
|
||||
ArtboardDrag::New { start, cur } => {
|
||||
let screen = (cur[0] - start[0]).hypot(cur[1] - start[1]) * f64::from(app.point_zoom());
|
||||
if screen < CLICK_PX || r[2] <= r[0] || r[3] <= r[1] {
|
||||
return true;
|
||||
}
|
||||
app.run("layer.new.artboard", json!({"rect": xywh(r)}))
|
||||
}
|
||||
ArtboardDrag::Move { id, rect, .. } if r != rect => app.run("layer.artboard.set", json!({"layer": id.0, "x": r[0], "y": r[1]})),
|
||||
// Resizing leaves the contents where they are, as in Photoshop.
|
||||
ArtboardDrag::Resize { id, rect, .. } if r != rect => {
|
||||
app.run("layer.artboard.set", json!({"layer": id.0, "rect": xywh(r), "moveContents": false}))
|
||||
}
|
||||
_ => return true,
|
||||
};
|
||||
report(app, result);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The resize pointer for handle (`hx`, `hy`), or none away from the selected board's handles.
|
||||
pub fn cursor(app: &PhotocraftApp, p: [f64; 2]) -> Option<CursorIcon> {
|
||||
let (hx, hy) = match app.ui.artboard_tool.drag {
|
||||
Some(ArtboardDrag::Resize { hx, hy, .. }) => (hx, hy),
|
||||
Some(_) => return None,
|
||||
None => handle_at(app, p).map(|h| (h.2, h.3))?,
|
||||
};
|
||||
Some(match (hx, hy) {
|
||||
(_, 0) => CursorIcon::ResizeHorizontal,
|
||||
(0, _) => CursorIcon::ResizeVertical,
|
||||
_ if hx == hy => CursorIcon::ResizeNwSe,
|
||||
_ => CursorIcon::ResizeNeSw,
|
||||
})
|
||||
}
|
||||
|
||||
/// The selected board's handles and the board a gesture in progress would leave.
|
||||
pub fn draw_overlay(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform) {
|
||||
if app.ui.tool != Tool::Artboard {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(painter.ctx());
|
||||
let pt = |x: i64, y: i64| xf.to_screen(x as f32, y as f32);
|
||||
let outline = |r: [i64; 4]| -> Vec<Pos2> { vec![pt(r[0], r[1]), pt(r[2], r[1]), pt(r[2], r[3]), pt(r[0], r[3]), pt(r[0], r[1])] };
|
||||
let drag = app.ui.artboard_tool.drag;
|
||||
if let Some(d) = drag.as_ref() {
|
||||
painter.add(egui::Shape::line(outline(preview(d)), Stroke::new(1.5, t.accent)));
|
||||
}
|
||||
let Some((_, r)) = active_board(app).filter(|_| !matches!(drag, Some(ArtboardDrag::New { .. }))) else { return };
|
||||
let r = drag.as_ref().map_or(r, preview);
|
||||
let (mx, my) = (r[0].saturating_add(r[2]) / 2, r[1].saturating_add(r[3]) / 2);
|
||||
for (x, y) in [(r[0], r[1]), (mx, r[1]), (r[2], r[1]), (r[2], my), (r[2], r[3]), (mx, r[3]), (r[0], r[3]), (r[0], my)] {
|
||||
let h = Rect::from_center_size(pt(x, y), vec2(7.0, 7.0));
|
||||
painter.rect_filled(h, 0.0, egui::Color32::WHITE);
|
||||
painter.rect_stroke(h, 0.0, Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Artboard tool's options bar: the selected board's size preset, W/H, Make Portrait / Make
|
||||
/// Landscape, and Add New Artboard.
|
||||
pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, tool: Tool) -> bool {
|
||||
if tool != Tool::Artboard {
|
||||
return false;
|
||||
}
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let board = active_board(app);
|
||||
let preset = board.and_then(|(id, _)| app.session.active()?.doc.layer(id)?.artboard().map(|a| a.preset.clone()));
|
||||
let mut edit: Option<Value> = None;
|
||||
ui.add_enabled_ui(board.is_some(), |ui| {
|
||||
let (id, r) = board.unwrap_or((LayerId(0), [0, 0, 0, 0]));
|
||||
ui.label(egui::RichText::new(tl!("Size")).color(t.text_dim));
|
||||
let mut preset = preset.unwrap_or_default();
|
||||
let mut opts: Vec<(String, &str)> = vec![(String::new(), tl!("Custom"))];
|
||||
opts.extend(photocraft_engine::artboard_cmds::PRESETS.iter().map(|(n, _, _)| (n.to_string(), *n)));
|
||||
if crate::widgets::dropdown(ui, "artboard-tool-preset", &mut preset, &opts, 150.0) && !preset.is_empty() {
|
||||
edit = Some(json!({"layer": id.0, "preset": preset}));
|
||||
}
|
||||
let (mut w, mut h) = ((r[2] - r[0]) as f32, (r[3] - r[1]) as f32);
|
||||
ui.label(egui::RichText::new(tl!("W")).color(t.text_dim));
|
||||
let cw = crate::widgets::value_field(ui, &mut w, 1.0..=300_000.0, "px", 64.0).changed();
|
||||
ui.label(egui::RichText::new(tl!("H")).color(t.text_dim));
|
||||
let ch = crate::widgets::value_field(ui, &mut h, 1.0..=300_000.0, "px", 64.0).changed();
|
||||
if cw || ch {
|
||||
edit = Some(json!({"layer": id.0, "width": w.round(), "height": h.round(), "coalesce": format!("artboard-tool-wh-{}", id.0)}));
|
||||
}
|
||||
if crate::widgets::secondary_button(ui, tl!("Make Portrait"), 0.0).clicked() && w > h {
|
||||
edit = Some(json!({"layer": id.0, "width": h.round(), "height": w.round()}));
|
||||
}
|
||||
if crate::widgets::secondary_button(ui, tl!("Make Landscape"), 0.0).clicked() && h > w {
|
||||
edit = Some(json!({"layer": id.0, "width": h.round(), "height": w.round()}));
|
||||
}
|
||||
});
|
||||
if let Some(p) = edit {
|
||||
let r = app.run("layer.artboard.set", p);
|
||||
report(app, r);
|
||||
}
|
||||
crate::widgets::vline(ui, 22.0);
|
||||
let has_doc = app.session.active().is_some();
|
||||
if ui.add_enabled_ui(has_doc, |ui| crate::widgets::secondary_button(ui, tl!("Add New Artboard"), 0.0)).inner.clicked() {
|
||||
// A board the size of the selected one, right of the rightmost board (the canvas size
|
||||
// without a selected board).
|
||||
let p = board.map_or(json!({}), |(_, r)| json!({"width": r[2] - r[0], "height": r[3] - r[1]}));
|
||||
let r = app.run("layer.new.artboard", p);
|
||||
report(app, r);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::canvas::tool_event;
|
||||
|
||||
fn app() -> PhotocraftApp {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app.sync_views();
|
||||
app.ui.extras.snap = false;
|
||||
app.ui.view.show.smart_guides = false;
|
||||
app.ui.tool = Tool::Artboard;
|
||||
app
|
||||
}
|
||||
|
||||
fn drag(app: &mut PhotocraftApp, from: [f64; 2], to: [f64; 2]) {
|
||||
let m = egui::Modifiers::NONE;
|
||||
tool_event(app, ToolEvent::Down { x: from[0], y: from[1], pressure: 1.0 }, m);
|
||||
tool_event(app, ToolEvent::Move { x: (from[0] + to[0]) / 2.0, y: (from[1] + to[1]) / 2.0, pressure: 1.0 }, m);
|
||||
tool_event(app, ToolEvent::Move { x: to[0], y: to[1], pressure: 1.0 }, m);
|
||||
tool_event(app, ToolEvent::Up { x: to[0], y: to[1] }, m);
|
||||
}
|
||||
|
||||
fn boards(app: &PhotocraftApp) -> Vec<photocraft_geom::Rect> {
|
||||
app.session.active().unwrap().doc.artboards().iter().map(|b| b.2.rect).collect()
|
||||
}
|
||||
|
||||
/// Drawing on empty canvas makes a board; its right-edge handle resizes it in one undo step;
|
||||
/// dragging it from inside moves it.
|
||||
#[test]
|
||||
fn artboard_tool_draws_resizes_and_moves_boards() {
|
||||
let mut app = app();
|
||||
drag(&mut app, [20.0, 30.0], [120.0, 110.0]);
|
||||
assert_eq!(boards(&app), vec![photocraft_geom::Rect::new(20, 30, 120, 110)]);
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.active_layer, Some(st.doc.artboards()[0].0));
|
||||
// The right edge's handle.
|
||||
drag(&mut app, [120.0, 70.0], [160.0, 75.0]);
|
||||
assert_eq!(boards(&app), vec![photocraft_geom::Rect::new(20, 30, 160, 110)]);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(boards(&app), vec![photocraft_geom::Rect::new(20, 30, 120, 110)]);
|
||||
// From inside: the board moves.
|
||||
drag(&mut app, [60.0, 60.0], [70.0, 50.0]);
|
||||
assert_eq!(boards(&app), vec![photocraft_geom::Rect::new(30, 20, 130, 100)]);
|
||||
// A click on empty canvas draws nothing.
|
||||
drag(&mut app, [300.0, 250.0], [300.0, 250.0]);
|
||||
assert_eq!(boards(&app).len(), 1);
|
||||
}
|
||||
|
||||
/// V picks the Move group, ⇧V cycles Move ↔ Artboard as in Photoshop.
|
||||
#[test]
|
||||
fn shift_v_cycles_move_and_artboard() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
let press = |app: &mut PhotocraftApp, modifiers| {
|
||||
let key = egui::Key::V;
|
||||
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();
|
||||
};
|
||||
app.ui.tool = Tool::Brush;
|
||||
press(&mut app, egui::Modifiers::NONE);
|
||||
assert_eq!(app.ui.tool, Tool::Move);
|
||||
press(&mut app, egui::Modifiers::SHIFT);
|
||||
assert_eq!(app.ui.tool, Tool::Artboard);
|
||||
press(&mut app, egui::Modifiers::SHIFT);
|
||||
assert_eq!(app.ui.tool, Tool::Move);
|
||||
}
|
||||
}
|
||||
|
|
@ -3146,6 +3146,7 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
|||
crate::analysis_ui::draw_overlay(app, &painter, &xf);
|
||||
crate::gradient_ui::draw_overlay(app, &painter, &xf);
|
||||
crate::slice_ui::draw_overlay(app, &painter, &xf);
|
||||
crate::artboard_tool::draw_overlay(app, &painter, &xf);
|
||||
// Tool cursors (Photoshop-style).
|
||||
let guide_hover = response.hover_pos().and_then(|p| {
|
||||
let d = xf.to_doc(p);
|
||||
|
|
@ -3169,6 +3170,8 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
|||
crate::type_transform::cursor(app, &ctx, &xf, p, mods)
|
||||
}) {
|
||||
ui.ctx().set_cursor_icon(c);
|
||||
} else if let Some(c) = response.hover_pos().filter(|_| tool == Tool::Artboard).and_then(|p| crate::artboard_tool::cursor(app, xf.to_doc(p))) {
|
||||
ui.ctx().set_cursor_icon(c);
|
||||
} else if let Some((p, c)) = response.hover_pos().filter(|_| tool == Tool::Crop).and_then(|p| Some((p, crate::crop_ui::cursor(app, xf.to_doc(p))?))) {
|
||||
ui.ctx().set_cursor_icon(c);
|
||||
let d = xf.to_doc(p);
|
||||
|
|
@ -4184,6 +4187,10 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers)
|
|||
if crate::slice_ui::pointer(app, ev, mods) {
|
||||
return;
|
||||
}
|
||||
// Artboard tool: draw, select, move and resize boards.
|
||||
if crate::artboard_tool::pointer(app, ev, mods) {
|
||||
return;
|
||||
}
|
||||
// Crop tool: draw, move and resize the frame.
|
||||
if crate::crop_ui::pointer(app, ev, mods) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -2490,6 +2490,12 @@ cameraData Lens Objektiv
|
|||
Previous Document Předchozí dokument
|
||||
W x H x Resolution Š x V x Rozlišení
|
||||
Front Image Přední obraz
|
||||
Artboard Tool Nástroj kreslicí plocha
|
||||
Make Portrait Na výšku
|
||||
Make Landscape Na šířku
|
||||
Add New Artboard Přidat novou kreslicí plochu
|
||||
W Š
|
||||
H V
|
||||
Freeform Pen Tool Nástroj pero od ruky
|
||||
Curvature Pen Tool Nástroj pero křivosti
|
||||
Curve Fit: Přizpůsobení křivky:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2523,6 +2523,10 @@ cameraData Lens Objektiv
|
|||
Previous Document Vorheriges Dokument
|
||||
W x H x Resolution B x H x Auflösung
|
||||
Front Image Vorderes Bild
|
||||
Artboard Tool Zeichenbereich-Werkzeug
|
||||
Make Portrait Hochformat
|
||||
Make Landscape Querformat
|
||||
Add New Artboard Neuen Zeichenbereich hinzufügen
|
||||
Freeform Pen Tool Freiform-Stift-Werkzeug
|
||||
Curvature Pen Tool Rundungsstift-Werkzeug
|
||||
Curve Fit: Kurvenanpassung:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3587,6 +3587,12 @@ cameraData Lens Φακός
|
|||
Previous Document Προηγούμενο έγγραφο
|
||||
W x H x Resolution Π x Υ x Ανάλυση
|
||||
Front Image Μπροστινή εικόνα
|
||||
Artboard Tool Πίνακας σχεδίασης (Artboard Tool)
|
||||
Make Portrait Κατακόρυφος προσανατολισμός
|
||||
Make Landscape Οριζόντιος προσανατολισμός
|
||||
Add New Artboard Προσθήκη νέου πίνακα σχεδίασης
|
||||
W Π
|
||||
H Υ
|
||||
Freeform Pen Tool Πένα ελεύθερης σχεδίασης
|
||||
Curvature Pen Tool Πένα καμπυλότητας
|
||||
Curve Fit: Προσαρμογή καμπύλης:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 5.
|
|
|
@ -2564,6 +2564,10 @@ cameraData Lens Objetivo
|
|||
Previous Document Documento anterior
|
||||
W x H x Resolution An. x Al. x Resolución
|
||||
Front Image Imagen frontal
|
||||
Artboard Tool Herramienta Lienzo
|
||||
Make Portrait Hacer vertical
|
||||
Make Landscape Hacer horizontal
|
||||
Add New Artboard Añadir nuevo lienzo
|
||||
Freeform Pen Tool Herramienta Pluma de forma libre
|
||||
Curvature Pen Tool Herramienta Pluma de curvatura
|
||||
Curve Fit: Ajuste de curva:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -2901,6 +2901,10 @@ cameraData Lens Objectif
|
|||
Previous Document Document précédent
|
||||
W x H x Resolution L x H x Résolution
|
||||
Front Image Image de premier plan
|
||||
Artboard Tool Outil Plan de travail
|
||||
Make Portrait Format portrait
|
||||
Make Landscape Format paysage
|
||||
Add New Artboard Ajouter un plan de travail
|
||||
Freeform Pen Tool Outil Plume libre
|
||||
Curvature Pen Tool Outil Plume de courbure
|
||||
Curve Fit: Ajustement de courbe :
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2888,6 +2888,10 @@ cameraData Lens Lensa
|
|||
Previous Document Dokumen Sebelumnya
|
||||
W x H x Resolution L x T x Resolusi
|
||||
Front Image Gambar Depan
|
||||
Artboard Tool Alat Artboard
|
||||
Make Portrait Jadikan Potret
|
||||
Make Landscape Jadikan Lanskap
|
||||
Add New Artboard Tambah Artboard Baru
|
||||
Freeform Pen Tool Alat Pen Bebas
|
||||
Curvature Pen Tool Alat Pen Kelengkungan
|
||||
Curve Fit: Penyesuaian Kurva:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -2554,6 +2554,10 @@ cameraData Lens Obiettivo
|
|||
Previous Document Documento precedente
|
||||
W x H x Resolution L x A x Risoluzione
|
||||
Front Image Immagine in primo piano
|
||||
Artboard Tool Strumento Tavola da disegno
|
||||
Make Portrait Rendi verticale
|
||||
Make Landscape Rendi orizzontale
|
||||
Add New Artboard Aggiungi nuova tavola da disegno
|
||||
Freeform Pen Tool Strumento Penna a mano libera
|
||||
Curvature Pen Tool Strumento Penna curvatura
|
||||
Curve Fit: Adattamento curva:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -3502,6 +3502,10 @@ cameraData Lens レンズ
|
|||
Previous Document 前のドキュメント
|
||||
W x H x Resolution 幅 x 高さ x 解像度
|
||||
Front Image 前面の画像
|
||||
Artboard Tool アートボードツール
|
||||
Make Portrait 縦向きにする
|
||||
Make Landscape 横向きにする
|
||||
Add New Artboard 新規アートボードを追加
|
||||
Freeform Pen Tool フリーフォームペンツール
|
||||
Curvature Pen Tool 曲線ペンツール
|
||||
Curve Fit: カーブフィット:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3068,6 +3068,12 @@ cameraData Lens 렌즈
|
|||
Previous Document 이전 문서
|
||||
W x H x Resolution 폭 x 높이 x 해상도
|
||||
Front Image 전면 이미지
|
||||
Artboard Tool 아트보드 도구
|
||||
Make Portrait 세로 방향으로
|
||||
Make Landscape 가로 방향으로
|
||||
Add New Artboard 새 아트보드 추가
|
||||
W 폭
|
||||
H 높이
|
||||
Freeform Pen Tool 자유 형태 펜 도구
|
||||
Curvature Pen Tool 곡률 펜 도구
|
||||
Curve Fit: 곡선 맞춤:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 10.
|
|
|
@ -3655,6 +3655,10 @@ cameraData Lens Lens
|
|||
Previous Document Vorig document
|
||||
W x H x Resolution B x H x Resolutie
|
||||
Front Image Voorste afbeelding
|
||||
Artboard Tool Tekengebiedgereedschap
|
||||
Make Portrait Staand maken
|
||||
Make Landscape Liggend maken
|
||||
Add New Artboard Nieuw tekengebied toevoegen
|
||||
Freeform Pen Tool Pengereedschap Vrije vorm
|
||||
Curvature Pen Tool Pengereedschap Kromming
|
||||
Curve Fit: Curve aanpassen:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
|
|
@ -3503,6 +3503,10 @@ cameraData Lens Obiektyw
|
|||
Previous Document Poprzedni dokument
|
||||
W x H x Resolution Szer. x Wys. x Rozdzielczość
|
||||
Front Image Obraz na wierzchu
|
||||
Artboard Tool Narzędzie Obszar roboczy
|
||||
Make Portrait Ustaw pionowo
|
||||
Make Landscape Ustaw poziomo
|
||||
Add New Artboard Dodaj nowy obszar roboczy
|
||||
Freeform Pen Tool Narzędzie Pióro dowolne
|
||||
Curvature Pen Tool Narzędzie Pióro krzywizny
|
||||
Curve Fit: Dopasowanie krzywej:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2909,6 +2909,10 @@ cameraData Lens Lente
|
|||
Previous Document Documento anterior
|
||||
W x H x Resolution L x A x Resolução
|
||||
Front Image Imagem frontal
|
||||
Artboard Tool Ferramenta Prancheta
|
||||
Make Portrait Tornar retrato
|
||||
Make Landscape Tornar paisagem
|
||||
Add New Artboard Adicionar nova prancheta
|
||||
Freeform Pen Tool Ferramenta Caneta de forma livre
|
||||
Curvature Pen Tool Ferramenta Caneta de curvatura
|
||||
Curve Fit: Ajuste de curva:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2830,6 +2830,10 @@ cameraData Lens Объектив
|
|||
Previous Document Предыдущий документ
|
||||
W x H x Resolution Ш x В x Разрешение
|
||||
Front Image Изображение переднего плана
|
||||
Artboard Tool Монтажная область (инструмент)
|
||||
Make Portrait Сделать книжной
|
||||
Make Landscape Сделать альбомной
|
||||
Add New Artboard Добавить монтажную область
|
||||
Freeform Pen Tool Свободное перо
|
||||
Curvature Pen Tool Перо кривизны
|
||||
Curve Fit: Точность кривой:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3588,6 +3588,10 @@ cameraData Lens Об'єктив
|
|||
Previous Document Попередній документ
|
||||
W x H x Resolution Ш x В x Роздільна здатність
|
||||
Front Image Зображення переднього плану
|
||||
Artboard Tool Інструмент «Монтажна область»
|
||||
Make Portrait Зробити портретною
|
||||
Make Landscape Зробити альбомною
|
||||
Add New Artboard Додати монтажну область
|
||||
Freeform Pen Tool Інструмент «Довільне перо»
|
||||
Curvature Pen Tool Інструмент «Перо кривизни»
|
||||
Curve Fit: Точність кривої:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 12.
|
|
|
@ -2497,6 +2497,12 @@ cameraData Lens 镜头
|
|||
Previous Document 上一个文档
|
||||
W x H x Resolution 宽 x 高 x 分辨率
|
||||
Front Image 前面的图像
|
||||
Artboard Tool 画板工具
|
||||
Make Portrait 设为纵向
|
||||
Make Landscape 设为横向
|
||||
Add New Artboard 添加新画板
|
||||
W 宽
|
||||
H 高
|
||||
Freeform Pen Tool 自由钢笔工具
|
||||
Curvature Pen Tool 弯度钢笔工具
|
||||
Curve Fit: 曲线拟合:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -2467,6 +2467,12 @@ cameraData Lens 鏡頭
|
|||
Previous Document 上一個文件
|
||||
W x H x Resolution 寬 x 高 x 解析度
|
||||
Front Image 前面的影像
|
||||
Artboard Tool 工作區域工具
|
||||
Make Portrait 設為直向
|
||||
Make Landscape 設為橫向
|
||||
Add New Artboard 新增工作區域
|
||||
W 寬
|
||||
H 高
|
||||
Freeform Pen Tool 自由筆型工具
|
||||
Curvature Pen Tool 曲線筆工具
|
||||
Curve Fit: 曲線擬合:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -7,6 +7,7 @@ pub static ICONS: &[(&str, &[u8])] = &[
|
|||
("align-left", include_bytes!("../../../assets/icons/align-left.svg")),
|
||||
("align-right", include_bytes!("../../../assets/icons/align-right.svg")),
|
||||
("app-window", include_bytes!("../../../assets/icons/app-window.svg")),
|
||||
("artboard", include_bytes!("../../../assets/icons/artboard.svg")),
|
||||
("arrow-left-right", include_bytes!("../../../assets/icons/arrow-left-right.svg")),
|
||||
("ban", include_bytes!("../../../assets/icons/ban.svg")),
|
||||
("bandage", include_bytes!("../../../assets/icons/bandage.svg")),
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ fn badge(ctx: &egui::Context, name: &str, p: egui::Pos2, doubled: bool) {
|
|||
pub fn tool_icon(t: Tool) -> &'static str {
|
||||
match t {
|
||||
Tool::Move => "move",
|
||||
Tool::Artboard => "artboard",
|
||||
Tool::RectMarquee => "square-dashed",
|
||||
Tool::EllipseMarquee => "circle-dashed",
|
||||
Tool::SingleRowMarquee => "marquee-row",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ pub mod adjust_ui;
|
|||
mod alt_grab;
|
||||
pub mod analysis_ui;
|
||||
pub mod anchor_tools;
|
||||
pub mod artboard_tool;
|
||||
pub mod artboard_ui;
|
||||
pub(crate) mod blend_preview;
|
||||
mod brand;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::{PhotocraftApp, icons, widgets};
|
|||
/// Photoshop toolbar: divider-separated sections of slots; each slot is a flyout group whose button
|
||||
/// shows the group's last-used tool.
|
||||
const TOOL_SECTIONS: &[&[&[Tool]]] = &[
|
||||
&[&[Tool::Move]],
|
||||
&[&[Tool::Move, Tool::Artboard]],
|
||||
&[
|
||||
&[Tool::RectMarquee, Tool::EllipseMarquee, Tool::SingleRowMarquee, Tool::SingleColumnMarquee],
|
||||
&[Tool::Lasso, Tool::PolygonLasso, Tool::MagneticLasso],
|
||||
|
|
@ -888,6 +888,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
|| crate::vector_ui::options_bar(app, ui, tool)
|
||||
|| crate::analysis_ui::options_bar(app, ui, tool)
|
||||
|| crate::slice_ui::options_bar(app, ui, tool)
|
||||
|| crate::artboard_tool::options_bar(app, ui, tool)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -4291,14 +4292,14 @@ mod toolbar_hidden_tests {
|
|||
|
||||
#[test]
|
||||
fn a_slot_keeps_only_its_visible_tools_and_its_index() {
|
||||
let sections = visible_sections(&["Sponge".to_string(), "Move".to_string()]);
|
||||
let sections = visible_sections(&["Sponge".to_string(), "Move".to_string(), "Artboard".to_string()]);
|
||||
assert_eq!(sections.len(), TOOL_SECTIONS.len() - 1, "the Move section has no tools left");
|
||||
let index = TOOL_SECTIONS.iter().flat_map(|section| section.iter()).position(|slot| slot.contains(&Tool::Sponge)).unwrap();
|
||||
let slot = sections.iter().flatten().find(|(i, _)| *i == index).map(|(_, tools)| tools.clone());
|
||||
assert_eq!(slot, Some(vec![Tool::Dodge, Tool::Burn]));
|
||||
assert!(sections.iter().flatten().all(|(_, tools)| !tools.contains(&Tool::Move) && !tools.contains(&Tool::Sponge)));
|
||||
assert_eq!(visible_sections(&[]).iter().map(Vec::len).sum::<usize>(), TOOL_SECTIONS.iter().map(|section| section.len()).sum::<usize>());
|
||||
let by_command = visible_sections(&["sponge".to_string(), "move".to_string()]);
|
||||
let by_command = visible_sections(&["sponge".to_string(), "move".to_string(), "artboard".to_string()]);
|
||||
assert_eq!(by_command, sections, "edit.toolbar names are read like `ui.set {{tool}}` names");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ impl Default for CameraRawPreviewState {
|
|||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Tool {
|
||||
Move,
|
||||
Artboard,
|
||||
RectMarquee,
|
||||
EllipseMarquee,
|
||||
/// Selects one pixel row across the whole canvas at the clicked y (Photoshop's flyout only:
|
||||
|
|
@ -131,8 +132,9 @@ pub enum Tool {
|
|||
}
|
||||
|
||||
impl Tool {
|
||||
pub const ALL: [Tool; 62] = [
|
||||
pub const ALL: [Tool; 63] = [
|
||||
Tool::Move,
|
||||
Tool::Artboard,
|
||||
Tool::RectMarquee,
|
||||
Tool::EllipseMarquee,
|
||||
Tool::SingleRowMarquee,
|
||||
|
|
@ -199,6 +201,7 @@ impl Tool {
|
|||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Tool::Move => "Move Tool",
|
||||
Tool::Artboard => "Artboard Tool",
|
||||
Tool::RectMarquee => "Rectangular Marquee Tool",
|
||||
Tool::EllipseMarquee => "Elliptical Marquee Tool",
|
||||
Tool::SingleRowMarquee => "Single Row Marquee Tool",
|
||||
|
|
@ -301,7 +304,7 @@ impl Tool {
|
|||
/// Photoshop default single-key shortcut (`'\0'` = none, e.g. the Blur group).
|
||||
pub fn key(self) -> char {
|
||||
match self {
|
||||
Tool::Move => 'V',
|
||||
Tool::Move | Tool::Artboard => 'V',
|
||||
Tool::RectMarquee | Tool::EllipseMarquee => 'M',
|
||||
Tool::Brush | Tool::Pencil | Tool::MixerBrush | Tool::ColorReplacement => 'B',
|
||||
Tool::Eraser | Tool::BackgroundEraser | Tool::MagicEraser => 'E',
|
||||
|
|
@ -1041,6 +1044,9 @@ pub struct UiState {
|
|||
/// Slice and Slice Select tools (see `slice_ui`).
|
||||
#[serde(default)]
|
||||
pub slices: crate::slice_ui::SliceUi,
|
||||
/// Artboard tool (see `artboard_tool`).
|
||||
#[serde(default)]
|
||||
pub artboard_tool: crate::artboard_tool::ArtboardToolUi,
|
||||
/// Modifier Keys panel, custom pixel aspect ratios, workspace dialogs (see `workspace_ui`).
|
||||
#[serde(default)]
|
||||
pub shell: crate::workspace_ui::ShellUi,
|
||||
|
|
@ -1168,6 +1174,7 @@ impl Default for UiState {
|
|||
analysis: Default::default(),
|
||||
timeline: Default::default(),
|
||||
slices: Default::default(),
|
||||
artboard_tool: Default::default(),
|
||||
shell: Default::default(),
|
||||
layer_filter: Vec::new(),
|
||||
pen: None,
|
||||
|
|
@ -1275,7 +1282,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(), 62);
|
||||
assert_eq!(Tool::ALL.len(), 63);
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -179,7 +179,8 @@ pub fn reset_options(o: &mut ToolOptions, tool: Tool) {
|
|||
| Tool::Slice
|
||||
| Tool::SliceSelect
|
||||
| Tool::AddAnchorPoint
|
||||
| Tool::DeleteAnchorPoint => {}
|
||||
| Tool::DeleteAnchorPoint
|
||||
| Tool::Artboard => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ what an agent can't finish alone.
|
|||
| G3 | User-reported bug backlog | stability, UI | blocks beta | 250–400 h | [target-app-parity](target-app-parity.md) |
|
||||
| G4 | Brush feel | UI / painting | high | 100–180 h | [brush-parity](brush-parity.md) |
|
||||
| G5 | Transform, handles and modifiers | UI | high | 60–110 h | [ui-parity](ui-parity.md) |
|
||||
| G6 | Missing tools (15) | features | high | 60–100 h | [ui-parity](ui-parity.md#tools) |
|
||||
| G6 | Missing tools (12) | features | high | 60–100 h | [ui-parity](ui-parity.md#tools) |
|
||||
| G7 | Workspaces shown as parameter dialogs | features | high | 80–140 h | [target-app-parity](target-app-parity.md#feature-areas) |
|
||||
| G8 | Type depth | features | high | 120–200 h | [target-app-parity](target-app-parity.md#feature-areas) |
|
||||
| G9 | Panels, preferences and workspace | UI | medium-high | 150–250 h | [ui-parity](ui-parity.md#panels-and-workspace) |
|
||||
|
|
@ -54,7 +54,7 @@ what an agent can't finish alone.
|
|||
scripted check there) is the only real oracle.
|
||||
|
||||
### G6. Missing tools
|
||||
- **Missing (9 of 68):** Artboard, Perspective Crop, Frame, Color Sampler (#1046), Color
|
||||
- **Missing (8 of 68):** Perspective Crop, Frame, Color Sampler (#1046), Color
|
||||
Replacement (engine command exists), Art History Brush, Add Anchor Point, Delete Anchor Point,
|
||||
Convert Point (only as Pen + ⌥). Freeform Pen and Curvature Pen landed (#1054); Horizontal and
|
||||
Vertical Type Mask landed (#1055, `type.createSelection`).
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ Ranked; each item links to its gap entry. Pick from the top unless an issue is a
|
|||
4. **Brush and transform feel** ([G4](gaps.md#g4-brush-feel), [G5](gaps.md#g5-transform-handles-and-modifiers)):
|
||||
[`brush-parity.md`](brush-parity.md) and [`ui-parity.md`](ui-parity.md) rows marked partial
|
||||
or missing. 150–250 h.
|
||||
5. **Missing tools** ([G6](gaps.md#g6-missing-tools)): 15 of Photoshop's 68. 60–100 h.
|
||||
5. **Missing tools** ([G6](gaps.md#g6-missing-tools)): 14 of Photoshop's 68. 60–100 h.
|
||||
|
||||
## What's next, with estimates
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
|
|||
| FILE-215-17 | CMYK/RGB documents preview spot inks; spot plates export (TIFF extra samples, PDF) | missing | [#215](https://github.com/storytold/photocraft/issues/215) | compose multichannel.rs:1 spot preview for Multichannel only |
|
||||
| FILE-215-18 | Save for Web per-slice settings remembered; Export As remembers and offers nearest scaling | partial | [#215](https://github.com/storytold/photocraft/issues/215) | file_ui.rs:188 last_web in-session only; export_dialog.rs:16 fixed defaults |
|
||||
| FILE-215-19 | Save As JPEG opens a JPEG Options dialog (quality 0-12, format options) | missing | [#215](https://github.com/storytold/photocraft/issues/215) | ExportSettings has jpeg_quality 1-100 only (lib.rs:118) |
|
||||
| FILE-215-20 | Artboard tool with handles and + buttons, Duplicate Artboard beside, vector PDF pages | partial | [#215](https://github.com/storytold/photocraft/issues/215) | artboard_ui.rs:1 names/properties only; no tool, + buttons or duplicate |
|
||||
| FILE-215-20 | Artboard tool with handles and + buttons, Duplicate Artboard beside, vector PDF pages | partial | [#215](https://github.com/storytold/photocraft/issues/215) | artboard_tool.rs: Artboard tool draws, selects, moves and resizes boards by their handles; Duplicate Artboard places the copy beside (#1584); no + buttons, raster PDF pages only |
|
||||
| FILE-215-21 | Local libraries of colours, patterns, images and layers, as linked items or copies | missing | [#215](https://github.com/storytold/photocraft/issues/215) | Only preset libraries (preset_store.rs) |
|
||||
| FILE-216-1 | Every corpus fetched and enforced in CI, cached by pin | missing | [#216](https://github.com/storytold/photocraft/issues/216) | No corpus step in ci.yml; in progress in #190 |
|
||||
| FILE-216-2 | Large-file fixtures: 20 MP, 150-layer, generated 2 GB PSD and 4-5 GB PSB stress tests | partial | [#216](https://github.com/storytold/photocraft/issues/216) | 4-5 GB PSB fixtures: psd example big_psb writes them (raw and RLE, planes past 2 GiB), io example big_psb_check opens and checks them; opt-in io tests/large_psb.rs (2.5 GB merged image). No 2 GB PSD, 150-layer or 20 MP fixture, nothing in CI |
|
||||
|
|
|
|||
|
|
@ -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% | 55 / 68 (Photoshop's 67 `.kys` tools plus Remove, Triangle, Curvature Pen; minus legacy Rounded Rectangle and Targeted Adjustment) | 81% | measured (`Tool` enum in `ui-egui/src/state.rs`, `TOOL_SECTIONS` in `panels.rs`) |
|
||||
| Toolbox tools | 15% | 56 / 68 (Photoshop's 67 `.kys` tools plus Remove, Triangle, Curvature Pen; minus legacy Rounded Rectangle and Targeted Adjustment) | 82% | 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)) |
|
||||
|
|
@ -75,7 +75,7 @@ Weights reflect what a working Photoshop user depends on (photographers, retouch
|
|||
|
||||
| Dimension | Weight | % ready | Remaining (agent-h) | Evidence | Detail |
|
||||
|---|---:|---:|---:|---|---|
|
||||
| Features in depth (tools, layers, selections, type, filters, transforms) | 25% | 55% | 600–1,000 | Scorecard checklists: Tools 20 done / 15 partial / 6 missing, Type 0 / 3 / 6, Automation 3 / 2 / 6. 15 tools missing; Select and Mask, Vanishing Point and Content-Aware Fill are parameter dialogs, not workspaces; type on a path, manual kerning, RTL paragraphs missing | [gaps.md](gaps.md) |
|
||||
| Features in depth (tools, layers, selections, type, filters, transforms) | 25% | 55% | 600–1,000 | Scorecard checklists: Tools 20 done / 15 partial / 6 missing, Type 0 / 3 / 6, Automation 3 / 2 / 6. 14 tools missing; Select and Mask, Vanishing Point and Content-Aware Fill are parameter dialogs, not workspaces; type on a path, manual kerning, RTL paragraphs missing | [gaps.md](gaps.md) |
|
||||
| UI / UX fidelity and feel | 15% | 40% | 350–600 | ~150 open UI issues; Workspace checklist 5 / 7 / 11; 46 of 149 preferences do nothing; no floating panel groups, thin Color panel, no transform context menu, brush lag and pressure jumps reported | [ui-parity.md](ui-parity.md), [brush-parity.md](brush-parity.md) |
|
||||
| File formats and fidelity | 15% | 60% | 300–500 | PSD oracle on Photoshop-authored files 133/256 (52%); io corpus 146/170 (86%); psd-tools 236/309 (76%); round trips ~100% of corpus; but users report Photoshop refusing our files (#1281, #2469; a text-layer case, #2374, was fixed 2026-10-10); no Photoshop PDF, EPS, JPEG 2000, JPEG XL, AVIF read, CR3 | [file-format-parity.md](file-format-parity.md) |
|
||||
| Performance | 10% | 25% | 200–350 | 3 of 25 budgeted scenarios meet budget (baseline 2026-10-05); 150-layer nudge crashes the GPU path (P12); layer move on 15000×10000 16-bit 2.7 s p50; users report brush and cursor lag (#2619, #2566, #1994) | [scorecard.md](scorecard.md#performance) |
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ Status: **done** (behaves as in Photoshop, with a test), **partial** (exists, di
|
|||
**missing**. Evidence is a file under `crates/ui-egui/src/` unless another path is given, a
|
||||
scorecard id, or an issue. Modifier names are macOS first (⌘ ⌥ ⇧); Windows/Linux use Ctrl and Alt.
|
||||
|
||||
**Summary (estimated, weights = how often the gesture is used):** ~40% ready. Tools 53/68
|
||||
present (78%, measured). Free Transform modifiers and Crop are close to Photoshop; the gaps are
|
||||
**Summary (estimated, weights = how often the gesture is used):** ~40% ready. Tools 56/68
|
||||
present (82%, measured). Free Transform modifiers and Crop are close to Photoshop; the gaps are
|
||||
in the long tail of modifiers, in-transform UI, spring-loaded tools, panels and preferences.
|
||||
Remaining: 60–110 h for transforms and modifiers (gap G5), 60–100 h for tools (G6), 150–250 h
|
||||
for panels and workspace (G9).
|
||||
|
|
@ -26,7 +26,7 @@ Triangle and Curvature Pen (minus legacy Rounded Rectangle and Targeted Adjustme
|
|||
|
||||
| Group | Present | Missing |
|
||||
|---|---|---|
|
||||
| Move, artboard | Move | Artboard (commands `layer.new.artboard*` exist) |
|
||||
| Move, artboard | Move, Artboard | |
|
||||
| Marquee | Rectangular, Elliptical, Single Row, Single Column | |
|
||||
| Lasso | Lasso, Polygonal, Magnetic | |
|
||||
| Selection | Object Selection, Quick Selection, Magic Wand | |
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ id = "FILE-215-20"
|
|||
target = "Artboard tool with handles and + buttons, Duplicate Artboard beside, vector PDF pages"
|
||||
status = "partial"
|
||||
issue = 215
|
||||
note = "artboard_ui.rs:1 names/properties only; no tool, + buttons or duplicate"
|
||||
note = "artboard_tool.rs: Artboard tool draws, selects, moves and resizes boards by their handles; Duplicate Artboard places the copy beside (#1584); no + buttons, raster PDF pages only"
|
||||
|
||||
[[item]]
|
||||
id = "FILE-215-21"
|
||||
|
|
|
|||
Loading…
Reference in New Issue