Brush panel: update-current-brush button, identity-based preset selection, tip-grid fixes (#2007)
* update brush button * ui brush duplicated highlight fix * formatting fix + locale * i18n: translate "Update the current brush with these settings" in remaining complete catalogs * Tip Shape picks the tip only; set_brush records the preset once accepted - A click in the Brush Tip Shape grid copies the tip fields only (main's Photoshop behaviour: the dynamics sections stay); the picked preset is marked current with brush.presets.setCurrent instead of a whole-preset tools.setBrush, so identity-based selection still tracks it. - tools.setBrush sets current_preset only after the brush merged and validated, so a failed call leaves no state behind; a reset still wins over a preset in the same call. Tested. - brush.presets.save / brush.defineFromSelection append at the end again (insert-after-the-copied-brush had no Photoshop reference). * Brush presets: forget the picked preset when the tool's brush swaps; no indexing in brush.presets.update Each painting tool keeps its own brush (#218), so Update Brush could overwrite the preset picked with one tool with another tool's brush. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: JJ <jj@dot.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
917fbcb006
commit
35f832f03a
|
|
@ -595,6 +595,7 @@ fn name_param(p: &Value, cmd: &str) -> Result<String> {
|
|||
p.get("name").and_then(Value::as_str).map(str::trim).filter(|n| !n.is_empty()).map(str::to_string).ok_or_else(|| bad(cmd, "missing `name`"))
|
||||
}
|
||||
|
||||
/// Replace the preset of that name, or append it at the end of the list.
|
||||
fn upsert(s: &mut Session, preset: BrushPreset) {
|
||||
match s.tools.presets.iter_mut().find(|x| x.name.eq_ignore_ascii_case(&preset.name)) {
|
||||
Some(x) => *x = preset,
|
||||
|
|
@ -611,6 +612,8 @@ fn presets_save(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
None => s.tools.brush.clone(),
|
||||
};
|
||||
upsert(s, BrushPreset { name: name.clone(), brush, builtin: false, group: String::new(), folder: Vec::new() });
|
||||
// The saved brush is the current brush now (Photoshop selects the brush it just saved).
|
||||
s.tools.current_preset = Some(name.clone());
|
||||
Ok(json!({ "name": name, "count": s.tools.presets.len() }))
|
||||
}
|
||||
|
||||
|
|
@ -622,10 +625,48 @@ fn presets_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if s.tools.presets.len() == before {
|
||||
return Err(bad(cmd, format!("no brush preset named `{name}`")));
|
||||
}
|
||||
if s.tools.current_preset.as_ref().is_some_and(|c| c.eq_ignore_ascii_case(&name)) {
|
||||
s.tools.current_preset = None;
|
||||
}
|
||||
s.brush_presets_changed();
|
||||
Ok(json!({ "count": s.tools.presets.len() }))
|
||||
}
|
||||
|
||||
/// Overwrite an existing preset with the current brush (or the current brush plus a `brush`
|
||||
/// patch), in place: no new preset, the position, name and group stay. The preset defaults to the
|
||||
/// one the current brush was picked from. A built-in that is updated becomes the user's preset
|
||||
/// (built-ins regenerate with their factory settings).
|
||||
fn presets_update(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "brush.presets.update";
|
||||
let name = match p.get("name").filter(|v| !v.is_null()) {
|
||||
Some(_) => name_param(p, cmd)?,
|
||||
None => s.tools.current_preset.clone().ok_or_else(|| bad(cmd, "the current brush was not picked from a preset"))?,
|
||||
};
|
||||
let brush = match p.get("brush").filter(|v| v.is_object()) {
|
||||
Some(patch) => merge_brush(&s.tools.brush, patch, cmd)?,
|
||||
None => s.tools.brush.clone(),
|
||||
};
|
||||
let pr = s.tools.presets.iter_mut().find(|x| x.name.eq_ignore_ascii_case(&name)).ok_or_else(|| bad(cmd, format!("no brush preset named `{name}`")))?;
|
||||
pr.brush = brush;
|
||||
pr.builtin = false;
|
||||
let (name, group) = (pr.name.clone(), pr.group.clone());
|
||||
s.brush_presets_changed();
|
||||
Ok(json!({ "name": name, "group": group }))
|
||||
}
|
||||
|
||||
/// Mark a preset as the one the current brush was picked from. The tip grids' click copies only
|
||||
/// the tip fields (the rest of the brush stays), so the engine cannot infer the preset from the
|
||||
/// brush: the UI names it. Selection is by identity, so look-alike duplicates stay distinct.
|
||||
fn presets_set_current(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "brush.presets.setCurrent";
|
||||
let name = name_param(p, cmd)?;
|
||||
if !s.tools.presets.iter().any(|x| x.name.eq_ignore_ascii_case(&name)) {
|
||||
return Err(bad(cmd, format!("no brush preset named `{name}`")));
|
||||
}
|
||||
s.tools.current_preset = Some(name.clone());
|
||||
Ok(json!({ "name": name }))
|
||||
}
|
||||
|
||||
fn has_selection_and_pixels(s: &Session) -> std::result::Result<(), String> {
|
||||
let d = s.active().ok_or("no document")?;
|
||||
if d.doc.selection.is_none() {
|
||||
|
|
@ -688,16 +729,20 @@ fn define_from_selection(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let brush = BrushSettings { tip: TipShape::Sampled(tip), size: tw.max(th) as f32, spacing: 0.25, pressure_size: false, ..BrushSettings::default() };
|
||||
upsert(s, BrushPreset { name: name.clone(), brush: brush.clone(), builtin: false, group: String::new(), folder: Vec::new() });
|
||||
s.tools.brush = brush;
|
||||
s.tools.current_preset = Some(name.clone());
|
||||
Ok(json!({ "name": name, "width": tw, "height": th }))
|
||||
}
|
||||
|
||||
pub(crate) fn set_brush(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "tools.setBrush";
|
||||
let mut b = s.tools.brush.clone();
|
||||
let mut picked = None;
|
||||
if let Some(name) = p.get("preset").and_then(Value::as_str) {
|
||||
b = find_preset(s, name, cmd)?.brush.clone().picked_over(&s.tools.brush);
|
||||
picked = Some(name.to_string());
|
||||
}
|
||||
if flag(p, "reset", false) {
|
||||
let reset = flag(p, "reset", false);
|
||||
if reset {
|
||||
b = BrushSettings::default();
|
||||
}
|
||||
let mut patch = p.clone();
|
||||
|
|
@ -715,6 +760,14 @@ pub(crate) fn set_brush(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
b = merge_brush(&b, &patch, cmd)?;
|
||||
validate_brush(&b, cmd)?;
|
||||
// The picked preset stays "the current brush" across later edits, so `brush.presets.update`
|
||||
// knows what to overwrite. Only once the brush is accepted: a failed call leaves no state
|
||||
// behind (a `reset` wins over a `preset`, as it replaces the whole brush).
|
||||
if reset {
|
||||
s.tools.current_preset = None;
|
||||
} else if let Some(name) = picked {
|
||||
s.tools.current_preset = Some(name);
|
||||
}
|
||||
// The tool's brush always paints with full tips: load a library preset's from the store.
|
||||
s.load_brush_tips(&mut b).map_err(|e| bad(cmd, e))?;
|
||||
let before = std::mem::replace(&mut s.tools.brush, b);
|
||||
|
|
@ -835,7 +888,16 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
),
|
||||
spec!("brush.presets.list", "List Brush Presets", r##"{"full":bool=false}"##, always, presets_list, false),
|
||||
spec!("brush.presets.save", "Save Brush Preset", r##"{"name":string,"brush":{…BrushSettings}?=current brush}"##, always, presets_save, true),
|
||||
spec!(
|
||||
"brush.presets.update",
|
||||
"Update Brush Preset",
|
||||
r##"{"name":string?=the preset the current brush was picked from,"brush":{…BrushSettings}?=current brush}"##,
|
||||
always,
|
||||
presets_update,
|
||||
true
|
||||
),
|
||||
spec!("brush.presets.delete", "Delete Brush Preset", r##"{"name":string}"##, always, presets_delete, true),
|
||||
spec!("brush.presets.setCurrent", "Set Current Brush Preset", r##"{"name":string}"##, always, presets_set_current, true),
|
||||
spec!("brush.defineFromSelection", "Define Brush Preset…", r##"{"name":string}"##, has_selection_and_pixels, define_from_selection, true),
|
||||
spec!("brush.get", "Get Brush", "{}", always, |s, _| Ok(brush_json(&s.tools.brush)), false),
|
||||
spec!("tools.setBrush", "Set Brush", r##"{"preset":name?,"reset":bool?,…BrushSettings fields (camelCase, deep-merged)}"##, always, set_brush, true),
|
||||
|
|
|
|||
|
|
@ -183,6 +183,62 @@ fn presets_save_load_delete_round_trip() {
|
|||
assert!(s.execute("brush.presets.save", json!({})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presets_save_appends_at_the_end_and_update_overwrites_in_place() {
|
||||
let mut s = session(10, 10);
|
||||
s.execute("tools.setBrush", json!({"preset": "Chalk"})).unwrap();
|
||||
assert_eq!(s.tools.current_preset.as_deref(), Some("Chalk"));
|
||||
let n = s.tools.presets.len();
|
||||
let chalk_at = s.tools.presets.iter().position(|p| p.name == "Chalk").unwrap();
|
||||
// A new preset appends at the end, picked preset or not.
|
||||
s.execute("brush.presets.save", json!({"name": "Mine"})).unwrap();
|
||||
assert_eq!(s.tools.presets.last().unwrap().name, "Mine");
|
||||
s.execute("brush.presets.save", json!({"name": "Tail"})).unwrap();
|
||||
assert_eq!(s.tools.presets.last().unwrap().name, "Tail");
|
||||
// A reset clears the picked preset.
|
||||
s.execute("tools.setBrush", json!({"reset": true})).unwrap();
|
||||
assert_eq!(s.tools.current_preset, None);
|
||||
// Editing the brush keeps the picked preset current; update overwrites it in place.
|
||||
s.execute("tools.setBrush", json!({"preset": "Chalk"})).unwrap();
|
||||
s.execute("tools.setBrush", json!({"size": 55})).unwrap();
|
||||
assert_eq!(s.tools.current_preset.as_deref(), Some("Chalk"));
|
||||
let r = s.execute("brush.presets.update", json!({})).unwrap();
|
||||
assert_eq!(r["name"], "Chalk");
|
||||
assert_eq!(s.tools.presets.len(), n + 2, "no new preset (the two saves added two)");
|
||||
assert_eq!(s.tools.presets.iter().position(|p| p.name == "Chalk"), Some(chalk_at));
|
||||
let chalk = s.tools.presets.iter().find(|p| p.name == "Chalk").unwrap();
|
||||
assert_eq!(chalk.brush.size, 55.0);
|
||||
assert!(!chalk.builtin, "an updated built-in becomes the user's preset");
|
||||
// A named update of another preset.
|
||||
s.execute("brush.presets.update", json!({"name": "Mine", "brush": {"size": 9}})).unwrap();
|
||||
assert_eq!(s.tools.presets.iter().find(|p| p.name == "Mine").unwrap().brush.size, 9.0);
|
||||
// Errors: no such preset, and no preset to update after a reset.
|
||||
assert!(s.execute("brush.presets.update", json!({"name": "Nope"})).is_err());
|
||||
s.execute("tools.setBrush", json!({"reset": true})).unwrap();
|
||||
assert!(s.execute("brush.presets.update", json!({})).is_err());
|
||||
// Deleting the current preset clears it, so a later update fails rather than guessing.
|
||||
s.execute("tools.setBrush", json!({"preset": "Chalk"})).unwrap();
|
||||
s.execute("brush.presets.delete", json!({"name": "Chalk"})).unwrap();
|
||||
assert_eq!(s.tools.current_preset, None);
|
||||
assert!(s.execute("brush.presets.update", json!({})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_set_brush_leaves_no_current_preset_behind() {
|
||||
let mut s = session(10, 10);
|
||||
let before = s.tools.brush.clone();
|
||||
// A rejected brush (a wrong-typed size) must not record the preset it came with.
|
||||
assert!(s.execute("tools.setBrush", json!({"preset": "Chalk", "brush": {"size": "big"}})).is_err());
|
||||
assert_eq!(s.tools.current_preset, None);
|
||||
assert_eq!(s.tools.brush, before);
|
||||
// Once the brush is accepted, the preset is recorded.
|
||||
s.execute("tools.setBrush", json!({"preset": "Chalk"})).unwrap();
|
||||
assert_eq!(s.tools.current_preset.as_deref(), Some("Chalk"));
|
||||
// A `reset` wins over a `preset` in the same call.
|
||||
s.execute("tools.setBrush", json!({"preset": "Chalk", "reset": true})).unwrap();
|
||||
assert_eq!(s.tools.current_preset, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_brush_merges_fields() {
|
||||
let mut s = session(10, 10);
|
||||
|
|
|
|||
|
|
@ -112,8 +112,12 @@ fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
let pr = s.tools.presets.get_mut(i).ok_or_else(|| bad(cmd, "no such preset"))?;
|
||||
if pr.name != new {
|
||||
let was_current = s.tools.current_preset.as_ref().is_some_and(|c| c.eq_ignore_ascii_case(&pr.name));
|
||||
pr.name = new.clone();
|
||||
pr.builtin = false;
|
||||
if was_current {
|
||||
s.tools.current_preset = Some(new.clone());
|
||||
}
|
||||
s.brush_presets_changed();
|
||||
}
|
||||
Ok(json!({ "name": new }))
|
||||
|
|
@ -318,6 +322,9 @@ fn delete_group(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let (g, f) = target_folder(s, p, cmd)?;
|
||||
let before = s.tools.presets.len();
|
||||
s.tools.presets.retain(|x| !in_folder(x, &g, &f));
|
||||
if s.tools.current_preset.as_ref().is_some_and(|c| !s.tools.presets.iter().any(|x| x.name.eq_ignore_ascii_case(c))) {
|
||||
s.tools.current_preset = None;
|
||||
}
|
||||
s.brush_presets_changed();
|
||||
Ok(json!({ "deleted": before - s.tools.presets.len(), "count": s.tools.presets.len() }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,6 +252,10 @@ pub struct ToolState {
|
|||
/// The coalescing key of the running `tools.setBrush` gesture and the brush before it, so the
|
||||
/// gesture journals as one call ([`brush_cmds::coalesce_journal`]).
|
||||
pub brush_gesture: Option<(String, photocraft_paint::BrushSettings)>,
|
||||
/// Name of the preset the current brush was last picked from. Kept across later edits (the
|
||||
/// preset stays "the current brush" so `brush.presets.update` can overwrite it); cleared when
|
||||
/// the brush is reset or the preset is gone.
|
||||
pub current_preset: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ToolState {
|
||||
|
|
@ -268,6 +272,7 @@ impl Default for ToolState {
|
|||
presets_rev: 0,
|
||||
mixer: Default::default(),
|
||||
brush_gesture: None,
|
||||
current_preset: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,23 +190,6 @@ pub fn grouped_presets(presets: &[paint::BrushPreset]) -> Vec<(String, Vec<usize
|
|||
out
|
||||
}
|
||||
|
||||
/// Does the current brush match `preset` (everything but colour, size and the tool state that
|
||||
/// picking a preset keeps: smoothing and the section locks)? Cheap fields first: the full
|
||||
/// comparison clones the preset and its tip.
|
||||
pub fn is_current(preset: &BrushSettings, brush: &BrushSettings) -> bool {
|
||||
preset.hardness == brush.hardness
|
||||
&& preset.spacing == brush.spacing
|
||||
&& preset.tip == brush.tip
|
||||
&& BrushSettings {
|
||||
color: brush.color,
|
||||
size: brush.size,
|
||||
background: brush.background,
|
||||
smoothing: brush.smoothing.clone(),
|
||||
locks: brush.locks.clone(),
|
||||
..preset.clone()
|
||||
} == *brush
|
||||
}
|
||||
|
||||
/// A fresh "Brush N" name.
|
||||
pub(crate) fn new_preset_name(presets: &[paint::BrushPreset]) -> String {
|
||||
(1..).map(|n| format!("Brush {n}")).find(|n| !presets.iter().any(|p| &p.name == n)).unwrap_or_else(|| "Brush".into())
|
||||
|
|
@ -316,8 +299,14 @@ fn settings_tab(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
let on = section_flag(&mut b, section).is_none_or(|f| *f);
|
||||
egui::ScrollArea::vertical().id_salt(("brush-section", section)).max_height(450.0).auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.set_width(WIDTH - 204.0);
|
||||
// A section that's off shows its options greyed out (Photoshop).
|
||||
ui.add_enabled_ui(on, |ui| section_body(ui, &mut b, section, &app.session.tools.presets));
|
||||
// A section that's off shows its options greyed out (Photoshop). A tip picked in
|
||||
// the grid copies the tip fields only — the dynamics sections stay (Photoshop).
|
||||
// The picked preset is recorded by name (`brush.presets.setCurrent`, identity, so
|
||||
// look-alike duplicates stay distinct), since the engine can't infer it from the
|
||||
// brush afterwards.
|
||||
ui.add_enabled_ui(on, |ui| section_body(ui, &mut b, section, &app.session.tools.presets, app.session.tools.current_preset.as_deref()))
|
||||
.inner
|
||||
.inspect(|name| run_or_status(app, "brush.presets.setCurrent", json!({ "name": name })));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -337,6 +326,15 @@ fn settings_tab(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
if icons::button(ui, "undo-2", 24.0, false, tl!("Reset the brush to the defaults")).clicked() {
|
||||
b = BrushSettings { color: b.color, background: b.background, smoothing: b.smoothing.clone(), locks: b.locks.clone(), ..Default::default() };
|
||||
}
|
||||
// Overwrite the preset the current brush was picked from (Photoshop's "update the
|
||||
// selected brush"): no new preset, the brush keeps its place in the list.
|
||||
let current = app.session.tools.current_preset.clone().filter(|n| app.session.tools.presets.iter().any(|p| p.name.eq_ignore_ascii_case(n)));
|
||||
let update =
|
||||
ui.add_enabled_ui(current.is_some(), |ui| icons::button(ui, "check", 24.0, false, tl!("Update the current brush with these settings"))).inner;
|
||||
update.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, current.is_some(), "Update the current brush with these settings"));
|
||||
if update.clicked() {
|
||||
run_or_status(app, "brush.presets.update", json!({ "brush": serde_json::to_value(&b).unwrap_or(Value::Null) }));
|
||||
}
|
||||
});
|
||||
});
|
||||
commit_gesture(app, ui.ctx(), &before, &b);
|
||||
|
|
|
|||
|
|
@ -696,6 +696,49 @@ fn f5_edits_the_default_brush_with_every_section() {
|
|||
assert!(!h.state().ui.panels.brush_settings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_current_brush_button_overwrites_the_picked_preset_in_place() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let mut h = harness(0, 0);
|
||||
// Nothing picked yet: the button is disabled and clicking it does nothing.
|
||||
assert!(h.state().session.tools.current_preset.is_none());
|
||||
h.get_by_label("Update the current brush with these settings").click();
|
||||
h.run_steps(3);
|
||||
assert!(!h.state().session.journal.iter().any(|(id, _)| id == "brush.presets.update"));
|
||||
// Pick a preset, then turn a section on.
|
||||
h.state_mut().run("tools.setBrush", json!({ "preset": "Chalk" })).unwrap();
|
||||
h.run_steps(2);
|
||||
let n = h.state().session.tools.presets.len();
|
||||
let at = h.state().session.tools.presets.iter().position(|p| p.name == "Chalk").unwrap();
|
||||
h.get_by_label("Enable Scattering").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().session.tools.brush.scattering.enabled);
|
||||
// The update button overwrites the picked preset in place: no new preset, position kept.
|
||||
h.get_by_label("Update the current brush with these settings").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().session.journal.iter().any(|(id, p)| id == "brush.presets.update" && p["brush"]["scattering"]["enabled"] == json!(true)));
|
||||
assert_eq!(h.state().session.tools.presets.len(), n);
|
||||
let chalk = h.state().session.tools.presets.get(at).unwrap();
|
||||
assert_eq!(chalk.name, "Chalk");
|
||||
assert!(chalk.brush.scattering.enabled);
|
||||
assert!(!chalk.builtin, "an updated built-in becomes the user's preset");
|
||||
}
|
||||
|
||||
/// Each painting tool keeps its own brush (#218): the preset picked for one tool is not the
|
||||
/// current preset of the next, so Update can't overwrite it with the other tool's brush.
|
||||
#[test]
|
||||
fn switching_tools_forgets_the_picked_preset() {
|
||||
let mut app = app();
|
||||
app.ui.tool = crate::state::Tool::Brush;
|
||||
crate::paint_mouse::sync_tool_brush(&mut app);
|
||||
app.run("tools.setBrush", json!({ "preset": "Chalk" })).unwrap();
|
||||
assert_eq!(app.session.tools.current_preset.as_deref(), Some("Chalk"));
|
||||
app.ui.tool = crate::state::Tool::Eraser;
|
||||
crate::paint_mouse::sync_tool_brush(&mut app);
|
||||
assert_eq!(app.session.tools.current_preset, None);
|
||||
assert!(app.run("brush.presets.update", json!({})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_bar_reaches_brush_settings_and_the_preset_library() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
|
|
@ -719,11 +762,11 @@ fn options_bar_reaches_brush_settings_and_the_preset_library() {
|
|||
assert!(h.query_by_label(&group).is_some(), "group {group}");
|
||||
}
|
||||
let target = presets.iter().find(|p| p.group == "Dry Media").or(presets.last()).unwrap().name.clone();
|
||||
assert!(!is_current(&paint::presets::find(&presets, &target).unwrap().brush, &h.state().session.tools.brush));
|
||||
assert_ne!(h.state().session.tools.current_preset.as_deref(), Some(target.as_str()));
|
||||
h.get_by_label(&target).click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(last_journal(h.state()).unwrap(), ("tools.setBrush".to_string(), json!({ "preset": target })));
|
||||
assert!(is_current(&paint::presets::find(&presets, &target).unwrap().brush, &h.state().session.tools.brush));
|
||||
assert_eq!(h.state().session.tools.current_preset.as_deref(), Some(target.as_str()));
|
||||
// A click picks and keeps the picker open; the chip's click closes it again.
|
||||
assert!(h.state().ui.brush_picker.is_some());
|
||||
h.get_by_label("Brush Preset picker").click();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use photocraft_engine::BrushSettings;
|
|||
use photocraft_engine::paint::{BrushPreset, MAX_BRUSH_SIZE, TipShape};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::brush_panel::{BrushesPanelState, BrushesView, Renaming, is_current, new_preset_name, run_or_status};
|
||||
use crate::brush_panel::{BrushesPanelState, BrushesView, Renaming, new_preset_name, run_or_status};
|
||||
use crate::brushes_tab::{self, Action, ListLayout};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{PhotocraftApp, icons, widgets};
|
||||
|
|
@ -121,8 +121,9 @@ pub(crate) fn named(resp: egui::Response, label: &str) -> egui::Response {
|
|||
}
|
||||
|
||||
/// The picker's contents: Size, Hardness (round tips only: a sampled tip has none), a search
|
||||
/// field with the Brush Settings, New Preset and gear buttons, and the preset list.
|
||||
pub fn body(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset], st: &mut BrushesPanelState) -> Vec<Pick> {
|
||||
/// field with the Brush Settings, New Preset and gear buttons, and the preset list. `current` is
|
||||
/// the selected preset's name (by identity, so edits don't deselect).
|
||||
pub fn body(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset], current: Option<&str>, st: &mut BrushesPanelState) -> Vec<Pick> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut picks = Vec::new();
|
||||
ui.set_width(WIDTH);
|
||||
|
|
@ -154,7 +155,7 @@ pub fn body(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset], s
|
|||
ui.add_space(20.0);
|
||||
ui.add(egui::TextEdit::singleline(&mut st.filter).hint_text(tl!("Search Brushes")).desired_width(WIDTH - 118.0).id_salt("brush-picker-search"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
gear_menu(ui, b, presets, st, &mut picks);
|
||||
gear_menu(ui, current, presets, st, &mut picks);
|
||||
if named(icons::button(ui, "square-plus", 24.0, false, "Create new brush preset from the current settings"), "New Brush Preset").clicked() {
|
||||
picks.push(Pick::NewPreset);
|
||||
}
|
||||
|
|
@ -165,13 +166,13 @@ pub fn body(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset], s
|
|||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
picks.extend(brushes_tab::preset_list(ui, presets, b, st, LIST).into_iter().map(Pick::List));
|
||||
picks.extend(brushes_tab::preset_list(ui, presets, current, st, LIST).into_iter().map(Pick::List));
|
||||
picks
|
||||
}
|
||||
|
||||
/// The gear: Photoshop's picker menu. A new preset, rename or delete the current one, the list's
|
||||
/// view, and Import Brushes.
|
||||
fn gear_menu(ui: &mut egui::Ui, b: &BrushSettings, presets: &[BrushPreset], st: &mut BrushesPanelState, picks: &mut Vec<Pick>) {
|
||||
fn gear_menu(ui: &mut egui::Ui, current: Option<&str>, presets: &[BrushPreset], st: &mut BrushesPanelState, picks: &mut Vec<Pick>) {
|
||||
let gear = named(icons::button(ui, "settings", 24.0, false, tl!("Brush Preset Options")), "Brush Preset Options");
|
||||
egui::Popup::menu(&gear).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
if ui.button(tl!("New Brush Preset…")).clicked() {
|
||||
|
|
@ -179,7 +180,7 @@ fn gear_menu(ui: &mut egui::Ui, b: &BrushSettings, presets: &[BrushPreset], st:
|
|||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
let current = presets.iter().find(|p| is_current(&p.brush, b)).map(|p| p.name.clone());
|
||||
let current = current.filter(|n| presets.iter().any(|p| p.name.eq_ignore_ascii_case(n))).map(str::to_string);
|
||||
ui.add_enabled_ui(current.is_some(), |ui| {
|
||||
if ui.button(tl!("Rename Brush…")).clicked()
|
||||
&& let Some(name) = current.clone()
|
||||
|
|
|
|||
|
|
@ -226,21 +226,9 @@ pub fn thumb_scale(size: f32) -> f32 {
|
|||
(size / 40.0).clamp(0.3, 1.0)
|
||||
}
|
||||
|
||||
/// Does `b`'s tip look like `p`'s (same bitmap or round hardness, and size)?
|
||||
fn same_tip(tip: &TipShape, size: f32, hardness: f32, p: &BrushPreset) -> bool {
|
||||
let (pt, ps, ph, ..) = preset_tip(p);
|
||||
(size - ps).abs() < 0.5
|
||||
&& match (tip, pt) {
|
||||
(TipShape::Round, TipShape::Round) => (hardness - ph).abs() < 0.01,
|
||||
(TipShape::Sampled(a), TipShape::Sampled(b)) => a.width == b.width && a.height == b.height && a.data == b.data,
|
||||
// Stored tips (#1843) match by key: the bitmap is the same one.
|
||||
(TipShape::Stored(a), TipShape::Stored(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Photoshop's scrolling grid of preset tips with their sizes. Returns the clicked preset index.
|
||||
fn tip_grid(ui: &mut egui::Ui, id: &str, presets: &[BrushPreset], selected: impl Fn(&BrushPreset) -> bool) -> Option<usize> {
|
||||
/// Photoshop's scrolling grid of preset tips with their sizes; `highlight` is the selected
|
||||
/// preset (by identity: look-alike duplicates must stay distinct). Returns the clicked index.
|
||||
fn tip_grid(ui: &mut egui::Ui, id: &str, presets: &[BrushPreset], highlight: Option<usize>) -> Option<usize> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut clicked = None;
|
||||
let frame = egui::Frame::NONE.fill(t.field).stroke(Stroke::new(1.0, t.field_border)).corner_radius(t.radius_sm as u8).inner_margin(egui::Margin::same(3));
|
||||
|
|
@ -253,7 +241,7 @@ fn tip_grid(ui: &mut egui::Ui, id: &str, presets: &[BrushPreset], selected: impl
|
|||
if !ui.is_rect_visible(cell) {
|
||||
continue;
|
||||
}
|
||||
if selected(p) {
|
||||
if highlight == Some(i) {
|
||||
ui.painter().rect_filled(cell, 3.0, t.accent_soft);
|
||||
ui.painter().rect_stroke(cell, 3.0, Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
} else if resp.hovered() {
|
||||
|
|
@ -281,10 +269,14 @@ fn tip_grid(ui: &mut egui::Ui, id: &str, presets: &[BrushPreset], selected: impl
|
|||
clicked
|
||||
}
|
||||
|
||||
fn tip_shape(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset]) {
|
||||
let (tip, size, hardness) = (b.tip.clone(), b.size, b.hardness);
|
||||
if let Some(p) = tip_grid(ui, "brush-tip-grid", presets, |p| same_tip(&tip, size, hardness, p)).and_then(|i| presets.get(i)) {
|
||||
// Picking a tip changes the tip only: the dynamics sections stay (Photoshop).
|
||||
/// The Brush Tip Shape section: the tip grid and the tip's own controls. Picking a tip copies the
|
||||
/// tip fields only — the dynamics sections stay as they are (Photoshop). The clicked preset's name
|
||||
/// is returned so the caller can mark it current with `brush.presets.setCurrent` (by identity:
|
||||
/// look-alike duplicates stay distinct; the engine can't infer it from the brush). The tip fields
|
||||
/// are also copied into the working brush so this frame's controls start from them.
|
||||
fn tip_shape(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset], current: Option<&str>) -> Option<String> {
|
||||
let highlight = current.and_then(|n| presets.iter().position(|p| p.name.eq_ignore_ascii_case(n)));
|
||||
if let Some(p) = tip_grid(ui, "brush-tip-grid", presets, highlight).and_then(|i| presets.get(i)) {
|
||||
let (pt, ps, ph, pa, pr, psp) = preset_tip(p);
|
||||
b.tip = pt.clone();
|
||||
b.size = ps;
|
||||
|
|
@ -292,6 +284,7 @@ fn tip_shape(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset])
|
|||
b.angle = pa;
|
||||
b.roundness = pr;
|
||||
b.spacing = psp;
|
||||
return Some(p.name.clone());
|
||||
}
|
||||
let orig = b.tip.bitmap_size().map(|(w, h)| w.max(h) as f32);
|
||||
size_row(ui, tl!("Size"), &mut b.size, MAX_BRUSH_SIZE, orig);
|
||||
|
|
@ -332,6 +325,7 @@ fn tip_shape(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset])
|
|||
ui.add_space(2.0);
|
||||
let on = b.spacing_enabled;
|
||||
ui.add_enabled_ui(on, |ui| signed_pct(ui, tl!("Spacing"), &mut b.spacing, 0.01, 10.0));
|
||||
None
|
||||
}
|
||||
|
||||
fn shape_dynamics(ui: &mut egui::Ui, b: &mut BrushSettings) {
|
||||
|
|
@ -490,8 +484,9 @@ fn dual_brush(ui: &mut egui::Ui, b: &mut BrushSettings, presets: &[BrushPreset])
|
|||
widgets::checkbox(ui, &mut d.flip, tl!("Flip"));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let (tip, size, hardness) = (d.tip.clone(), d.size, d.hardness);
|
||||
if let Some(p) = tip_grid(ui, "brush-dual-grid", presets, |p| same_tip(&tip, size, hardness, p)).and_then(|i| presets.get(i)) {
|
||||
// The dual tip is a sub-setting, not a selection: no persistent highlight (a look-alike match
|
||||
// would highlight an arbitrary duplicate).
|
||||
if let Some(p) = tip_grid(ui, "brush-dual-grid", presets, None).and_then(|i| presets.get(i)) {
|
||||
let (pt, ps, ph, pa, pr, psp) = preset_tip(p);
|
||||
d.tip = pt.clone();
|
||||
d.size = ps;
|
||||
|
|
@ -582,13 +577,17 @@ fn smoothing(ui: &mut egui::Ui, b: &mut BrushSettings) {
|
|||
}
|
||||
|
||||
/// Controls for section `i` (see [`crate::brush_panel::SECTIONS`]). `presets` feed the tip grids.
|
||||
pub fn section_body(ui: &mut egui::Ui, b: &mut BrushSettings, i: usize, presets: &[BrushPreset]) {
|
||||
/// Returns the name of a preset whose tip was picked in the Brush Tip Shape grid, for
|
||||
/// `brush.presets.setCurrent`.
|
||||
pub fn section_body(ui: &mut egui::Ui, b: &mut BrushSettings, i: usize, presets: &[BrushPreset], current: Option<&str>) -> Option<String> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let note = |ui: &mut egui::Ui, s: &str| {
|
||||
ui.label(RichText::new(s).color(t.text_faint).font(theme::medium(11.5)));
|
||||
};
|
||||
if i == 0 {
|
||||
return tip_shape(ui, b, presets, current);
|
||||
}
|
||||
match i {
|
||||
0 => tip_shape(ui, b, presets),
|
||||
1 => shape_dynamics(ui, b),
|
||||
2 => scattering(ui, b),
|
||||
3 => texture(ui, b),
|
||||
|
|
@ -609,6 +608,7 @@ pub fn section_body(ui: &mut egui::Ui, b: &mut BrushSettings, i: usize, presets:
|
|||
11 => smoothing(ui, b),
|
||||
_ => note(ui, "Keeps the current pattern and scale when you switch to another textured brush preset."),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use photocraft_engine::paint::{BrushPreset, MAX_BRUSH_SIZE};
|
|||
use serde_json::json;
|
||||
|
||||
use crate::brush_panel::{
|
||||
BrushesPanelState, BrushesView, Renaming, UNGROUPED, WIDTH, commit_gesture, full_uv, grouped_presets, is_current, new_preset_name, run_or_status,
|
||||
BrushesPanelState, BrushesView, Renaming, UNGROUPED, WIDTH, commit_gesture, full_uv, grouped_presets, new_preset_name, run_or_status,
|
||||
};
|
||||
use crate::brush_preview;
|
||||
use crate::theme::{self, Tokens};
|
||||
|
|
@ -212,9 +212,7 @@ pub fn apply(app: &mut PhotocraftApp, acts: Vec<Action>) {
|
|||
Action::Select(name) => run_or_status(app, "tools.setBrush", json!({ "preset": name })),
|
||||
Action::Choose(name) => {
|
||||
// The double-click's first click already picked it.
|
||||
let picked =
|
||||
photocraft_engine::paint::presets::find(&app.session.tools.presets, &name).is_some_and(|p| is_current(&p.brush, &app.session.tools.brush));
|
||||
if !picked {
|
||||
if app.session.tools.current_preset.as_deref() != Some(name.as_str()) {
|
||||
run_or_status(app, "tools.setBrush", json!({ "preset": name }));
|
||||
}
|
||||
}
|
||||
|
|
@ -469,7 +467,8 @@ struct Draw<'a> {
|
|||
label: &'a str,
|
||||
key: &'a str,
|
||||
presets: &'a [BrushPreset],
|
||||
brush: &'a photocraft_engine::BrushSettings,
|
||||
/// The selected preset's name (by identity, so look-alike duplicates stay distinct).
|
||||
current: Option<&'a str>,
|
||||
collapsed: &'a [String],
|
||||
filtering: bool,
|
||||
grid: bool,
|
||||
|
|
@ -496,14 +495,14 @@ fn draw_nodes(ui: &mut egui::Ui, d: &Draw, nodes: &[Node], depth: usize, acts: &
|
|||
ui.horizontal(|ui| {
|
||||
ui.add_space(left);
|
||||
for p in row {
|
||||
grid_cell(ui, p, is_current(&p.brush, d.brush), d.presets, d.layout.cell, acts);
|
||||
grid_cell(ui, p, d.current.is_some_and(|n| p.name.eq_ignore_ascii_case(n)), d.presets, d.layout.cell, acts);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for p in run.iter() {
|
||||
list_row(ui, p, is_current(&p.brush, d.brush), d.presets, indent, acts);
|
||||
list_row(ui, p, d.current.is_some_and(|n| p.name.eq_ignore_ascii_case(n)), d.presets, indent, acts);
|
||||
}
|
||||
}
|
||||
run.clear();
|
||||
|
|
@ -562,15 +561,10 @@ fn rename_bar(ui: &mut egui::Ui, st: &mut BrushesPanelState, acts: &mut Vec<Acti
|
|||
}
|
||||
|
||||
/// The presets in their groups, filtered by `st.filter`, as rows or tip cells (`st.view`), with
|
||||
/// the rename bar above them while a rename is open. Group toggles and renames update `st`; the
|
||||
/// returned actions are commands for [`apply`].
|
||||
pub fn preset_list(
|
||||
ui: &mut egui::Ui,
|
||||
presets: &[BrushPreset],
|
||||
brush: &photocraft_engine::BrushSettings,
|
||||
st: &mut BrushesPanelState,
|
||||
layout: ListLayout,
|
||||
) -> Vec<Action> {
|
||||
/// the rename bar above them while a rename is open. `current` is the selected preset's name:
|
||||
/// selection is by identity, so it survives edits and tells look-alike duplicates apart. Group
|
||||
/// toggles and renames update `st`; the returned actions are commands for [`apply`].
|
||||
pub fn preset_list(ui: &mut egui::Ui, presets: &[BrushPreset], current: Option<&str>, st: &mut BrushesPanelState, layout: ListLayout) -> Vec<Action> {
|
||||
let mut acts = Vec::new();
|
||||
rename_bar(ui, st, &mut acts);
|
||||
let filter = st.filter.trim().to_lowercase();
|
||||
|
|
@ -590,7 +584,7 @@ pub fn preset_list(
|
|||
if !open {
|
||||
continue;
|
||||
}
|
||||
let d = Draw { label: &label, key: &key, presets, brush, collapsed, filtering: !filter.is_empty(), grid, layout };
|
||||
let d = Draw { label: &label, key: &key, presets, current, collapsed, filtering: !filter.is_empty(), grid, layout };
|
||||
draw_nodes(ui, &d, &folder_tree(&items), 0, &mut acts);
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
|
|
@ -662,7 +656,7 @@ pub fn show(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
let acts = preset_list(ui, &app.session.tools.presets, &app.session.tools.brush, &mut app.ui.brushes_panel, PANEL_LIST);
|
||||
let acts = preset_list(ui, &app.session.tools.presets, app.session.tools.current_preset.as_deref(), &mut app.ui.brushes_panel, PANEL_LIST);
|
||||
apply(app, acts);
|
||||
ui.add_space(4.0);
|
||||
widgets::hairline(ui);
|
||||
|
|
@ -670,7 +664,8 @@ pub fn show(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new(format!("{} presets", app.session.tools.presets.len())).color(t.text_faint));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let current = app.session.tools.presets.iter().find(|p| is_current(&p.brush, &app.session.tools.brush)).map(|p| p.name.clone());
|
||||
// The current preset by identity: editing the brush after picking must not deselect it.
|
||||
let current = app.session.tools.current_preset.clone().filter(|n| app.session.tools.presets.iter().any(|p| p.name.eq_ignore_ascii_case(n)));
|
||||
if ui.add_enabled_ui(current.is_some(), |ui| icons::button(ui, "trash", 24.0, false, "Delete brush")).inner.clicked()
|
||||
&& let Some(name) = current
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1938,6 +1938,7 @@
|
|||
Collapse group ({key}-click: all groups) Sbalit skupinu ({key}+kliknutí: všechny skupiny)
|
||||
Create new brush from the current settings Vytvořit nový štětec z aktuálního nastavení
|
||||
Create new brush from these settings Vytvořit nový štětec z tohoto nastavení
|
||||
Update the current brush with these settings Aktualizovat aktuální štětec těmito nastaveními
|
||||
Delete brush Odstranit štětec
|
||||
Drag the arrow to set the angle, the dots to set the roundness Tažením šipky nastavíte úhel, tažením bodů kulatost
|
||||
Expand Rozbalit
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1912,6 +1912,7 @@
|
|||
Collapse group ({key}-click: all groups) Gruppe einklappen ({key}-Klick: alle Gruppen)
|
||||
Create new brush from the current settings Neuen Pinsel aus den aktuellen Einstellungen erstellen
|
||||
Create new brush from these settings Neuen Pinsel aus diesen Einstellungen erstellen
|
||||
Update the current brush with these settings Aktuellen Pinsel mit diesen Einstellungen aktualisieren
|
||||
Delete brush Pinsel löschen
|
||||
Drag the arrow to set the angle, the dots to set the roundness Ziehe den Pfeil für den Winkel, die Punkte für die Rundung
|
||||
Expand Ausklappen
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1463,6 +1463,7 @@
|
|||
Collapse Panel Group Σύμπτυξη ομάδας πλαισίων
|
||||
Create new brush from the current settings Δημιουργία νέου πινέλου από τις τρέχουσες ρυθμίσεις
|
||||
Create new brush from these settings Δημιουργία νέου πινέλου από αυτές τις ρυθμίσεις
|
||||
Update the current brush with these settings Ενημέρωση του τρέχοντος πινέλου με αυτές τις ρυθμίσεις
|
||||
Delete brush Διαγραφή πινέλου
|
||||
Drag the arrow to set the angle, the dots to set the roundness Σύρετε το βέλος για τη γωνία και τις κουκκίδες για τη στρογγυλότητα
|
||||
Expand Panel Group Ανάπτυξη ομάδας πλαισίων
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 5.
|
|
|
@ -1922,6 +1922,7 @@
|
|||
Collapse group ({key}-click: all groups) Contraer grupo ({key}-clic: todos los grupos)
|
||||
Create new brush from the current settings Crear un pincel nuevo con los ajustes actuales
|
||||
Create new brush from these settings Crear un pincel nuevo con estos ajustes
|
||||
Update the current brush with these settings Actualizar el pincel actual con estos ajustes
|
||||
Delete brush Eliminar pincel
|
||||
Drag the arrow to set the angle, the dots to set the roundness Arrastre la flecha para fijar el ángulo y los puntos para fijar la redondez
|
||||
Expand Expandir
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -1891,6 +1891,7 @@
|
|||
Collapse group ({key}-click: all groups) Réduire le groupe ({key}-clic : tous les groupes)
|
||||
Create new brush from the current settings Créer un nouveau pinceau à partir des paramètres actuels
|
||||
Create new brush from these settings Créer un nouveau pinceau à partir de ces paramètres
|
||||
Update the current brush with these settings Mettre à jour le pinceau actuel avec ces paramètres
|
||||
Delete brush Supprimer le pinceau
|
||||
Drag the arrow to set the angle, the dots to set the roundness Faites glisser la flèche pour régler l'angle, les points pour régler l'arrondi
|
||||
Expand Développer
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1885,6 +1885,7 @@
|
|||
Collapse group ({key}-click: all groups) Ciutkan grup ({key}-klik: semua grup)
|
||||
Create new brush from the current settings Buat kuas baru dari pengaturan saat ini
|
||||
Create new brush from these settings Buat kuas baru dari pengaturan ini
|
||||
Update the current brush with these settings Perbarui kuas saat ini dengan pengaturan ini
|
||||
Delete brush Hapus kuas
|
||||
Drag the arrow to set the angle, the dots to set the roundness Seret panah untuk mengatur sudut, titik untuk mengatur kebulatan
|
||||
Expand Bentangkan
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -1920,6 +1920,7 @@
|
|||
Collapse group ({key}-click: all groups) Comprimi gruppo ({key}-click: tutti i gruppi)
|
||||
Create new brush from the current settings Crea nuovo pennello da impostazioni correnti
|
||||
Create new brush from these settings Crea nuovo pennello da queste impostazioni
|
||||
Update the current brush with these settings Aggiorna il pennello attuale con queste impostazioni
|
||||
Delete brush Elimina pennello
|
||||
Drag the arrow to set the angle, the dots to set the roundness Trascina la freccia per impostare l'angolo, i puntini per la rotondità
|
||||
Expand Espandi
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -1928,6 +1928,7 @@
|
|||
Create new brush from the current settings 現在の設定から新規ブラシを作成
|
||||
Create new brush from these settings この設定から新規ブラシを作成
|
||||
Delete brush ブラシを削除
|
||||
Update the current brush with these settings この設定で現在のブラシを更新
|
||||
Drag the arrow to set the angle, the dots to set the roundness 矢印をドラッグして角度、点をドラッグして真円率を設定
|
||||
Expand 展開
|
||||
Expand Panel Group パネルグループを展開
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1887,6 +1887,7 @@
|
|||
Collapse group ({key}-click: all groups) 그룹 접기 ({key} 키를 누른 채 클릭: 모든 그룹)
|
||||
Create new brush from the current settings 현재 설정으로 새 브러시 만들기
|
||||
Create new brush from these settings 이 설정으로 새 브러시 만들기
|
||||
Update the current brush with these settings 이 설정으로 현재 브러시 업데이트
|
||||
Delete brush 브러시 삭제
|
||||
Drag the arrow to set the angle, the dots to set the roundness 화살표를 드래그하여 각도를, 점을 드래그하여 원형률을 설정하세요
|
||||
Expand 펼치기
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 10.
|
|
|
@ -1902,6 +1902,7 @@
|
|||
Collapse group ({key}-click: all groups) Groep samenvouwen ({key}-klik: alle groepen)
|
||||
Create new brush from the current settings Nieuw penseel maken van de huidige instellingen
|
||||
Create new brush from these settings Nieuw penseel maken van deze instellingen
|
||||
Update the current brush with these settings Huidig penseel bijwerken met deze instellingen
|
||||
Delete brush Penseel verwijderen
|
||||
Drag the arrow to set the angle, the dots to set the roundness Sleep de pijl om de hoek in te stellen, de stippen om de rondheid in te stellen
|
||||
Expand Uitvouwen
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
|
|
@ -1918,6 +1918,7 @@
|
|||
Collapse group ({key}-click: all groups) Zwiń grupę (kliknięcie z {key}: wszystkie grupy)
|
||||
Create new brush from the current settings Utwórz nowy pędzel z bieżących ustawień
|
||||
Create new brush from these settings Utwórz nowy pędzel z tych ustawień
|
||||
Update the current brush with these settings Zaktualizuj bieżący pędzel tymi ustawieniami
|
||||
Delete brush Usuń pędzel
|
||||
Drag the arrow to set the angle, the dots to set the roundness Przeciągnij strzałkę, aby ustawić kąt, a punkty, aby ustawić zaokrąglenie
|
||||
Expand Rozwiń
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1890,6 +1890,7 @@
|
|||
Collapse group ({key}-click: all groups) Recolher grupo ({key}-clique: todos os grupos)
|
||||
Create new brush from the current settings Criar novo pincel a partir das configurações atuais
|
||||
Create new brush from these settings Criar novo pincel a partir destas configurações
|
||||
Update the current brush with these settings Atualizar o pincel atual com estas configurações
|
||||
Delete brush Excluir pincel
|
||||
Drag the arrow to set the angle, the dots to set the roundness Arraste a seta para definir o ângulo e os pontos para definir o arredondamento
|
||||
Expand Expandir
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -958,6 +958,7 @@
|
|||
Brush Preset Options Параметры наборов кистей
|
||||
New Brush Новая кисть
|
||||
Create new brush from these settings Создать новую кисть из этих параметров
|
||||
Update the current brush with these settings Обновить текущую кисть этими параметрами
|
||||
Reset the brush to the defaults Сбросить кисть к значениям по умолчанию
|
||||
No brush presets Нет наборов кистей
|
||||
Override Pressure Переопределить нажим
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -949,6 +949,7 @@
|
|||
Brush Preset Options Параметри набору пензля
|
||||
New Brush Новий пензель
|
||||
Create new brush from these settings Створити новий пензель із цими параметрами
|
||||
Update the current brush with these settings Оновити поточний пензель цими параметрами
|
||||
Reset the brush to the defaults Скинути пензель до типових значень
|
||||
No brush presets Немає наборів пензлів
|
||||
Override Pressure Перевизначити натиск
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 12.
|
|
|
@ -1929,6 +1929,7 @@
|
|||
Collapse group (⌥-click: all groups) 折叠组 (⌥-单击:所有组)
|
||||
Create new brush from the current settings 根据当前设置创建新画笔
|
||||
Create new brush from these settings 根据这些设置创建新画笔
|
||||
Update the current brush with these settings 用这些设置更新当前画笔
|
||||
Delete brush 删除画笔
|
||||
Drag the arrow to set the angle, the dots to set the roundness 拖动箭头设置角度,拖动圆点设置圆度
|
||||
Expand 展开
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -1922,6 +1922,7 @@
|
|||
Collapse group ({key}-click: all groups) 收合群組 ({key}+按一下:全部群組)
|
||||
Create new brush from the current settings 以目前設定建立新筆刷
|
||||
Create new brush from these settings 以這些設定建立新筆刷
|
||||
Update the current brush with these settings 以這些設定更新目前筆刷
|
||||
Delete brush 刪除筆刷
|
||||
Drag the arrow to set the angle, the dots to set the roundness 拖曳箭頭設定角度,拖曳圓點設定圓度
|
||||
Expand 展開
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -34,6 +34,9 @@ pub fn sync_tool_brush(app: &mut PhotocraftApp) {
|
|||
// A tool seen for the first time takes what the brush has, at Photoshop's smoothing.
|
||||
None => BrushSettings { smoothing: first_smoothing(), ..current },
|
||||
};
|
||||
// The picked preset was the outgoing tool's brush: forget it, so Update Brush can't overwrite
|
||||
// that preset with this tool's brush.
|
||||
app.session.tools.current_preset = None;
|
||||
app.ui.brush_tool = tool;
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +158,9 @@ pub fn show_picker(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
|||
let before = app.session.tools.brush.clone();
|
||||
let mut b = before.clone();
|
||||
let list = &mut app.ui.brush_picker_list;
|
||||
let picks = egui::Frame::popup(ui.style()).show(ui, |ui| crate::brush_picker::body(ui, &mut b, &app.session.tools.presets, list)).inner;
|
||||
let picks = egui::Frame::popup(ui.style())
|
||||
.show(ui, |ui| crate::brush_picker::body(ui, &mut b, &app.session.tools.presets, app.session.tools.current_preset.as_deref(), list))
|
||||
.inner;
|
||||
crate::brush_panel::commit_gesture(app, ui.ctx(), &before, &b);
|
||||
let closes = picks.iter().any(crate::brush_picker::Pick::closes);
|
||||
crate::brush_picker::apply(app, ui.ctx(), picks);
|
||||
|
|
|
|||
Loading…
Reference in New Issue