From 749109e6fb3bbbc78236dd02fc27fda72e497b87 Mon Sep 17 00:00:00 2001 From: kayrockscreenprinting Date: Fri, 9 Oct 2026 09:22:50 -0700 Subject: [PATCH] Define Pattern asks for the pattern's name (#1850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edit > Define Pattern… now opens a Pattern Name dialog, prefilled with the name the pattern would get anyway (pattern_cmds::default_name, now shared with the command so the two can't drift), as Photoshop does. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/pattern_cmds.rs | 28 +++++++++++++----------- crates/ui-egui/src/filter_dialog.rs | 33 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/crates/engine/src/pattern_cmds.rs b/crates/engine/src/pattern_cmds.rs index 5ddd359694..2cf3cf144a 100644 --- a/crates/engine/src/pattern_cmds.rs +++ b/crates/engine/src/pattern_cmds.rs @@ -183,6 +183,20 @@ fn composite_surface(doc: &Document, r: Rect) -> Surface { s } +/// The name Define Pattern gives a pattern without one: the active document's name without its +/// extension ("Pattern" without a document), numbered past the library's existing names. The +/// Define Pattern dialog starts from it. +pub fn default_name(s: &Session) -> String { + let doc_name = s.active().map(|st| st.doc.name.clone()).unwrap_or_default(); + let base = + std::path::Path::new(&doc_name).file_stem().map(|x| x.to_string_lossy().to_string()).filter(|b| !b.is_empty()).unwrap_or_else(|| "Pattern".into()); + let taken = |n: &str| s.patterns.items.iter().any(|q| q.name == n); + if !taken(&base) { + return base; + } + (2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or(base) +} + fn define(s: &mut Session, p: &Value) -> Result { let cmd = "edit.definePattern"; let st = s.active().ok_or(EngineError::NoDocument)?; @@ -210,17 +224,7 @@ fn define(s: &mut Session, p: &Value) -> Result { } let name = match p.get("name").and_then(Value::as_str) { Some(n) if !n.trim().is_empty() => n.to_string(), - _ => { - let base = std::path::Path::new(&doc.name).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| "Pattern".into()); - let mut i = 1; - loop { - let n = if i == 1 { base.clone() } else { format!("{base} {i}") }; - if !s.patterns.items.iter().any(|q| q.name == n) { - break n; - } - i += 1; - } - } + _ => default_name(s), }; let pat = Pattern::new(name, surf, r.width(), r.height()); let id = pat.id.clone(); @@ -354,7 +358,7 @@ pub fn specs() -> Vec { label: "Define Pattern…", menu: &["Edit"], shortcut: None, - params: r##"{"name":str?,"rect":[x0,y0,x1,y1]? (default: selection bounds, else the canvas)} → {"pattern":id} (added to the library; samples the visible composite)"##, + params: r##"{"name":text,"rect":[x0,y0,x1,y1]?} → {"pattern":id} (name: default the document's name numbered past the library's names; rect: default the selection bounds or else the canvas; added to the library from the visible composite)"##, enabled: has_doc, journal: true, run: define, diff --git a/crates/ui-egui/src/filter_dialog.rs b/crates/ui-egui/src/filter_dialog.rs index 27192e564a..8ea3a6774c 100644 --- a/crates/ui-egui/src/filter_dialog.rs +++ b/crates/ui-egui/src/filter_dialog.rs @@ -169,6 +169,7 @@ pub fn has_dialog(command: &str) -> bool { | "view.proofSetup" | "layer.layerStyle.globalLight" | "image.mode.colorTable" + | "edit.definePattern" )) && photocraft_engine::commands::find(command).is_some_and(|c| !parse_spec(c.params).is_empty()) } @@ -205,6 +206,10 @@ pub fn open(app: &mut PhotocraftApp, command: &str) -> Option { if command == "image.rotation.arbitrary" { straighten_defaults(app, &mut fields); } + // Photoshop's Pattern Name dialog starts from the name the pattern would get anyway. + if command == "edit.definePattern" { + fields.insert("name".into(), json!(photocraft_engine::pattern_cmds::default_name(&app.session))); + } if parse_spec(spec.params).iter().any(|p| p.kind == Kind::Document) { // The document picker lists every open document (params refer to them by index). let names: Vec = app.session.documents().iter().map(|d| d.doc.name.clone()).collect(); @@ -679,4 +684,32 @@ mod tests { } } } + + #[test] + fn define_pattern_asks_for_a_name() { + // Edit › Define Pattern… named the pattern after the document without asking ("ask", then + // "ask 2"). Photoshop's Pattern Name dialog starts from that name and lets you change it. + let spec = photocraft_engine::commands::find("edit.definePattern").unwrap(); + assert_eq!(parse_spec(spec.params), vec![Param { key: "name".into(), kind: Kind::Text }, Param { key: "rect".into(), kind: Kind::Json }]); + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 8, "height": 8, "name": "ask.jpg"})).unwrap(); + let ctx = egui::Context::default(); + let define = |app: &mut PhotocraftApp, name: Option<&str>| { + crate::menus::invoke(app, &ctx, "edit.definePattern", Value::Null).unwrap(); + let d = app.ui.dialogs.last().expect("Define Pattern opens a dialog").clone(); + let shown = d.fields["name"].as_str().unwrap().to_string(); + if let Some(n) = name { + app.ui.dialog_mut(d.id).unwrap().fields.insert("name".into(), json!(n)); + } + crate::dialogs::confirm(app, d.id).unwrap(); + shown + }; + assert_eq!(define(&mut app, Some("Bricks")), "ask", "prefilled with the document's name"); + assert_eq!(define(&mut app, None), "ask", "Bricks didn't take it"); + assert_eq!(define(&mut app, None), "ask 2", "numbered past the library"); + let names: Vec<&str> = app.session.patterns.items.iter().map(|p| p.name.as_str()).collect(); + for n in ["Bricks", "ask", "ask 2"] { + assert!(names.contains(&n), "{n} in {names:?}"); + } + } }