This commit is contained in:
parent
c046337ddf
commit
b6e9ec97e5
|
|
@ -1155,14 +1155,17 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
),
|
||||
spec!("path.delete", "Delete Path", [], r##"{"name":str|"work"="work"}"##, has_doc, path_delete),
|
||||
spec!("path.rename", "Rename Path", [], r##"{"name":str|"work"="work","to":str} (renaming the work path saves it)"##, has_doc, path_rename),
|
||||
spec!(
|
||||
"path.toSelection",
|
||||
"Make Selection from Path",
|
||||
[],
|
||||
r##"{"name":str|"work"|"layer"="work","feather":px=0,"antiAlias":bool=true,"mode":"replace|add|subtract|intersect"="replace"}"##,
|
||||
has_doc,
|
||||
path_to_selection
|
||||
),
|
||||
// ⌘↩ / Ctrl+Enter, as in Photoshop (the UI loads the path selected in the Paths panel, #306).
|
||||
CommandSpec {
|
||||
id: "path.toSelection",
|
||||
label: "Make Selection from Path",
|
||||
menu: &[],
|
||||
shortcut: Some("Cmd+Enter"),
|
||||
params: r##"{"name":str|"work"|"layer"="work","feather":px=0,"antiAlias":bool=true,"mode":"replace|add|subtract|intersect"="replace"}"##,
|
||||
enabled: has_doc,
|
||||
run: path_to_selection,
|
||||
journal: true,
|
||||
},
|
||||
spec!("select.toWorkPath", "Make Work Path", [], r##"{"tolerance":0.5..10 px=2} → {subpaths,knots}"##, has_selection, select_to_work_path),
|
||||
spec!(
|
||||
"path.fill",
|
||||
|
|
|
|||
|
|
@ -1964,3 +1964,4 @@
|
|||
OpenGL OpenGL
|
||||
Use tablet pressure タブレットの筆圧を使用
|
||||
Vulkan Vulkan
|
||||
No path to make a selection from: draw one with the Pen tool or select one in the Paths panel. 選択範囲を作成するパスがありません。ペンツールで描くか、パスパネルでパスを選択してください。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -198,6 +198,9 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
|
|||
return Ok(json!({"workspace": app.ui.workspace}));
|
||||
}
|
||||
match id {
|
||||
// ⌘↩ / Ctrl+Enter (#306): load the path selected in the Paths panel (or the one being
|
||||
// drawn) as a selection.
|
||||
"path.toSelection" if params.get("name").is_none() => crate::vector_ui::path_to_selection(app, params),
|
||||
// Layer › Rename Layer from a menu starts in-place renaming in the Layers panel.
|
||||
"layer.renameLayer" if params.get("name").is_none() => {
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
//! #306: ⌘↩ (Ctrl+Enter off the Mac) loads a path as a selection, as in Photoshop: the path
|
||||
//! being drawn with the Pen, the path selected in the Paths panel, or the work path. The key is
|
||||
//! rebindable in Edit › Keyboard Shortcuts, and with no path it says why instead of doing nothing.
|
||||
|
||||
use egui::{Event, Key, Modifiers, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::PhotocraftApp;
|
||||
use crate::state::Tool;
|
||||
use crate::vector_ui::PenPath;
|
||||
|
||||
fn harness() -> Harness<'static, PhotocraftApp> {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
s.execute("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).with_max_steps(64).build_eframe(move |cc| {
|
||||
PhotocraftApp::setup_context(&cc.egui_ctx, Default::default());
|
||||
PhotocraftApp::new(s, crate::Services::default())
|
||||
});
|
||||
h.run_steps(6);
|
||||
h
|
||||
}
|
||||
|
||||
/// ⌘ on the Mac, Ctrl elsewhere, as egui-winit reports them.
|
||||
fn command() -> Modifiers {
|
||||
if cfg!(target_os = "macos") {
|
||||
Modifiers { command: true, mac_cmd: true, ..Default::default() }
|
||||
} else {
|
||||
Modifiers { command: true, ctrl: true, ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
fn press(h: &mut Harness<'_, PhotocraftApp>, key: Key, m: Modifiers) {
|
||||
h.event(Event::ModifiersChanged(m));
|
||||
h.event(Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: m });
|
||||
h.event(Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers: m });
|
||||
h.event(Event::ModifiersChanged(Modifiers::NONE));
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn rect_path(x: f64, y: f64, w: f64, h: f64) -> serde_json::Value {
|
||||
let k = |x: f64, y: f64| json!({"anchor": [x, y]});
|
||||
json!({"subpaths": [{"closed": true, "knots": [k(x, y), k(x + w, y), k(x + w, y + h), k(x, y + h)]}]})
|
||||
}
|
||||
|
||||
/// The selection's bounds (x, y, width, height), if any.
|
||||
fn selection(h: &Harness<'_, PhotocraftApp>) -> Option<[i64; 4]> {
|
||||
let r = h.state().session.active()?.doc.selection.as_ref()?.content_bounds();
|
||||
Some([r.x0, r.y0, r.x1 - r.x0, r.y1 - r.y0].map(i64::from))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_enter_with_the_pen_loads_the_work_path() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("path.set", json!({"name": "work", "path": rect_path(10.0, 10.0, 50.0, 40.0)})).unwrap();
|
||||
h.state_mut().ui.tool = Tool::Pen;
|
||||
h.run_steps(2);
|
||||
assert!(h.state().session.active().unwrap().doc.selection.is_none());
|
||||
press(&mut h, Key::Enter, command());
|
||||
assert!(h.state().session.active().unwrap().doc.selection.is_some(), "⌘↩ made a selection: {}", h.state().ui.status);
|
||||
assert_eq!(selection(&h), Some([10, 10, 50, 40]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_enter_while_drawing_finishes_the_path_and_loads_it() {
|
||||
let mut h = harness();
|
||||
h.state_mut().ui.tool = Tool::Pen;
|
||||
let p = |x: f64, y: f64| [[x, y], [x, y], [x, y]];
|
||||
h.state_mut().ui.pen = Some(PenPath { knots: vec![p(20.0, 20.0), p(120.0, 20.0), p(120.0, 80.0)], dragging: false });
|
||||
h.run_steps(2);
|
||||
press(&mut h, Key::Enter, command());
|
||||
let app = h.state();
|
||||
assert!(app.ui.pen.is_none(), "the pen path was finished");
|
||||
assert!(app.session.active().unwrap().doc.work_path.is_some(), "into the work path");
|
||||
assert!(app.session.active().unwrap().doc.selection.is_some(), "and loaded as a selection: {}", app.ui.status);
|
||||
// Plain ↩ still only finishes a path.
|
||||
let mut h = harness();
|
||||
h.state_mut().ui.tool = Tool::Pen;
|
||||
h.state_mut().ui.pen = Some(PenPath { knots: vec![p(20.0, 20.0), p(120.0, 20.0), p(120.0, 80.0)], dragging: false });
|
||||
h.run_steps(2);
|
||||
press(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert!(h.state().session.active().unwrap().doc.work_path.is_some());
|
||||
assert!(h.state().session.active().unwrap().doc.selection.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_enter_loads_the_path_selected_in_the_paths_panel() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("path.set", json!({"name": "work", "path": rect_path(0.0, 0.0, 30.0, 30.0)})).unwrap();
|
||||
h.state_mut().run("path.set", json!({"name": "Path 1", "path": rect_path(100.0, 40.0, 60.0, 50.0)})).unwrap();
|
||||
// Any tool, with a path selected in the Paths panel (as in Photoshop).
|
||||
h.state_mut().ui.tool = Tool::Brush;
|
||||
h.state_mut().ui.selected_path = Some("Path 1".into());
|
||||
h.run_steps(2);
|
||||
press(&mut h, Key::Enter, command());
|
||||
assert_eq!(selection(&h), Some([100, 40, 60, 50]), "{}", h.state().ui.status);
|
||||
// A selected path that went away falls back to the work path.
|
||||
h.state_mut().run("path.delete", json!({"name": "Path 1"})).unwrap();
|
||||
h.state_mut().run("select.deselect", json!({})).unwrap();
|
||||
press(&mut h, Key::Enter, command());
|
||||
assert_eq!(selection(&h), Some([0, 0, 30, 30]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_path_it_says_why_and_the_key_can_be_rebound() {
|
||||
let mut h = harness();
|
||||
h.state_mut().ui.tool = Tool::Pen;
|
||||
press(&mut h, Key::Enter, command());
|
||||
let app = h.state();
|
||||
assert!(app.session.active().unwrap().doc.selection.is_none());
|
||||
assert!(app.ui.status_error && app.ui.status.contains("No path"), "status: {}", app.ui.status);
|
||||
// It's a listed, rebindable shortcut.
|
||||
let cmd_enter = crate::shortcuts::parse("Cmd+Enter").unwrap();
|
||||
assert!(crate::shortcut_dispatch::bindings(app).iter().any(|(id, sc)| id == "path.toSelection" && *sc == cmd_enter));
|
||||
h.state_mut().run("edit.keyboardShortcuts", json!({"set": {"path.toSelection": "Cmd+Shift+Y"}})).unwrap();
|
||||
h.state_mut().run("path.set", json!({"name": "work", "path": rect_path(10.0, 10.0, 20.0, 20.0)})).unwrap();
|
||||
press(&mut h, Key::Enter, command());
|
||||
assert!(h.state().session.active().unwrap().doc.selection.is_none(), "⌘↩ was moved");
|
||||
let mut m = command();
|
||||
m.shift = true;
|
||||
press(&mut h, Key::Y, m);
|
||||
assert_eq!(selection(&h), Some([10, 10, 20, 20]), "{}", h.state().ui.status);
|
||||
}
|
||||
|
|
@ -182,6 +182,37 @@ pub fn pen_commit(app: &mut PhotocraftApp, closed: bool) {
|
|||
}
|
||||
}
|
||||
|
||||
/// The path that path commands act on without a name: the one selected in the Paths panel, else
|
||||
/// the work path, else the selected layer's shape path or vector mask; `None` when there's none.
|
||||
pub fn active_path_name(app: &PhotocraftApp) -> Option<String> {
|
||||
let st = app.session.active()?;
|
||||
let rows = path_rows(&st.doc, st.active_layer);
|
||||
let key = |r: &PathEntry| match r.kind {
|
||||
PathRow::Work => "work".to_string(),
|
||||
PathRow::Layer => "layer".to_string(),
|
||||
PathRow::Saved => r.name.clone(),
|
||||
};
|
||||
let selected = app.ui.selected_path.as_deref().filter(|s| rows.iter().any(|r| key(r) == *s)).map(str::to_string);
|
||||
selected.or_else(|| rows.iter().find(|r| r.kind == PathRow::Work).map(key)).or_else(|| rows.iter().find(|r| r.kind == PathRow::Layer).map(key))
|
||||
}
|
||||
|
||||
/// ⌘↩ / Ctrl+Enter (#306): load a path as a selection, like Photoshop with a Pen or Path
|
||||
/// Selection tool or a path selected in the Paths panel. A path being drawn with the Pen is
|
||||
/// finished first and loads; otherwise [`active_path_name`]. `params` may set feather, mode…
|
||||
pub fn path_to_selection(app: &mut PhotocraftApp, params: Value) -> Result<Value, String> {
|
||||
if app.ui.pen.as_ref().is_some_and(|p| p.knots.len() >= 2) {
|
||||
// The finished path becomes the work path, or a shape layer's path or a vector mask.
|
||||
let on_layer = app.ui.tool_options.vector_mode == "shape" || targeted_vector_mask(app).is_some();
|
||||
pen_commit(app, false);
|
||||
app.ui.selected_path = Some(if on_layer { "layer" } else { "work" }.into());
|
||||
}
|
||||
let name = active_path_name(app)
|
||||
.ok_or_else(|| tl!("No path to make a selection from: draw one with the Pen tool or select one in the Paths panel.").to_string())?;
|
||||
let mut p = params.as_object().cloned().unwrap_or_default();
|
||||
p.insert("name".into(), json!(name));
|
||||
app.run("path.toSelection", Value::Object(p))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Path Selection
|
||||
|
||||
|
|
@ -816,3 +847,7 @@ mod tests {
|
|||
assert_eq!((wp.subpaths[0].knots[0].anchor.x, wp.subpaths[0].knots[0].anchor.y), (30.0, 25.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "path_selection_tests.rs"]
|
||||
mod path_selection_tests;
|
||||
|
|
|
|||
Loading…
Reference in New Issue