mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-15 03:36:48 -04:00
* Blur Gallery: Field Blur pins on the image (#3373) Field Blur opened a parameter dialog with one centre, though the engine already blends any number of pins. It now works as Photoshop's Blur Gallery workspace: the image under the dialog takes the pointer. A click adds a pin and selects it, a drag moves a pin, and a drag around the selected pin's ring turns its blur up or down (one turn is the 0-500 px range; the arc shows the amount). Delete or Backspace removes the selected pin. The dialog shows the selected pin's Blur and Preview, and opens at the right edge, beside the image, so the pins stay in view. The pins live in the dialog's fields (`pins`, `__selected`), so the existing filter preview follows every change, OK applies filter.blurGallery.fieldBlur with them in one step, and automation drives it with ui.dialog.set. Pins are fractions of the selection's bounds when there is one, else the canvas, as the engine reads them. Proxy previews now scale Blur Gallery pins' pixel blur like other pixel params. Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n(vi): translate the Field Blur pin strings main added the Vietnamese catalog after this branch was cut, so every_tl_literal_is_translated failed on the merge for "No pin selected" and the pin hint. Terms follow the catalog's own: ghim (pin), Bấm (click), Delete kept as the key name. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * Blur Gallery: Alt+Cancel Reset starts again from one pin main's Alt+Cancel → Reset (#3871) restores a filter dialog's schema defaults, and the schema has none for Field Blur's pins, so Reset left the workspace with no pins. blur_pins::reset now puts back the opening pin (in the middle, selected), and reset_fields runs it for the Blur Gallery. init is reset plus capturing the bounds, which Reset keeps. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(hu, tr): translate the Field Blur pin strings main added complete Hungarian and Turkish catalogs after this branch was cut. Terms follow each catalog: tű / Mezőelmosás (informal Hungarian), pim / Alan Bulanıklığı (formal Turkish). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(sr): translate the Field Blur pin strings Main gained a complete Serbian catalogue (#4436); every_tl_literal_is_translated needs these strings there too. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
50631b54f7
commit
be7a1f80fc
27 files changed
+592
-3
No files matched your search
@@ -0,0 +1,311 @@
|
||||
//! Filter › Blur Gallery › Field Blur… as Photoshop's workspace (#3373): pins on the image, each
|
||||
//! with its own blur, interpolated between them. While the dialog is open the image under it takes
|
||||
//! the pointer: a click on the image adds a pin (and selects it), a drag moves a pin, and a drag on
|
||||
//! the selected pin's ring turns its blur up or down (one turn = the full 0–500 px; the arc shows
|
||||
//! the amount). Delete or Backspace removes the selected pin. The dialog shows the selected pin's
|
||||
//! Blur and the Preview switch; OK runs `filter.blurGallery.fieldBlur` with the `pins`.
|
||||
//!
|
||||
//! Everything lives in the dialog's fields, so automation drives it with `ui.dialog.set`: `pins`
|
||||
//! (`[{x, y, blur}]`, x and y as fractions of the filter's bounds) and `__selected`. The bounds are
|
||||
//! the engine's: the selection's when there is one, else the canvas.
|
||||
|
||||
use egui::{Color32, Pos2, Stroke, vec2};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::PhotocraftApp;
|
||||
use crate::canvas::ViewXform;
|
||||
|
||||
pub const COMMAND: &str = "filter.blurGallery.fieldBlur";
|
||||
/// Field Blur's range, in pixels.
|
||||
const MAX_BLUR: f64 = 500.0;
|
||||
/// Photoshop's first pin.
|
||||
const DEFAULT_BLUR: f64 = 15.0;
|
||||
/// More pins than anyone places by hand; keeps the params and the per-pixel loop bounded.
|
||||
pub const MAX_PINS: usize = 64;
|
||||
/// Screen radii (points): the pin's dot, how near a press must be to grab it, and its ring.
|
||||
const DOT: f32 = 5.0;
|
||||
const GRAB: f32 = 9.0;
|
||||
const RING: f32 = 24.0;
|
||||
const RING_BAND: f32 = 5.0;
|
||||
|
||||
pub fn owns(fields: &Map<String, Value>) -> bool {
|
||||
fields.get("__command").and_then(Value::as_str) == Some(COMMAND)
|
||||
}
|
||||
|
||||
/// Turn a freshly opened Field Blur dialog into the workspace: one pin in the middle of the
|
||||
/// filter's bounds with the remembered blur, selected.
|
||||
pub fn init(app: &mut PhotocraftApp, fields: &mut Map<String, Value>) {
|
||||
reset(fields);
|
||||
let bounds = app.session.active().map(|st| (st.doc.id, st.doc.bounds(), st.doc.selection.clone()));
|
||||
if let Some((doc, canvas, selection)) = bounds {
|
||||
let b = selection.map(|s| app.cached_bounds(u64::MAX - doc.0, &s)).filter(|b| !b.is_empty()).unwrap_or(canvas);
|
||||
fields.insert("__bounds".into(), json!([b.x0, b.y0, b.x1, b.y1]));
|
||||
}
|
||||
}
|
||||
|
||||
/// One pin in the middle with the dialog's blur, selected. Alt+Cancel (Reset) runs this alone, so
|
||||
/// the bounds captured on opening stay.
|
||||
pub fn reset(fields: &mut Map<String, Value>) {
|
||||
let blur = fields.get("blur").and_then(Value::as_f64).filter(|b| b.is_finite()).unwrap_or(DEFAULT_BLUR).clamp(0.0, MAX_BLUR);
|
||||
fields.remove("centerX");
|
||||
fields.remove("centerY");
|
||||
fields.insert("blur".into(), json!(blur));
|
||||
fields.insert("pins".into(), json!([{"x": 0.5, "y": 0.5, "blur": blur}]));
|
||||
fields.insert("__selected".into(), json!(0));
|
||||
}
|
||||
|
||||
/// Where the dialog first opens (offset from the window's top-left): at the right edge, beside the
|
||||
/// image, as Photoshop docks its Blur Tools panel, so the pins under it stay in view. Moved, it
|
||||
/// reopens where it was left, like any dialog.
|
||||
pub fn first_place(fields: &Map<String, Value>, ctx: &egui::Context) -> Option<egui::Vec2> {
|
||||
owns(fields).then(|| vec2((ctx.content_rect().width() - 480.0).max(0.0), 96.0))
|
||||
}
|
||||
|
||||
/// One pin: position as fractions of the bounds, blur in pixels.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Pin {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub blur: f64,
|
||||
}
|
||||
|
||||
/// The pins in `fields`, cleaned (finite, in range, at most [`MAX_PINS`]).
|
||||
pub fn pins(fields: &Map<String, Value>) -> Vec<Pin> {
|
||||
let Some(list) = fields.get("pins").and_then(Value::as_array) else { return Vec::new() };
|
||||
let num = |v: &Value, k: &str, d: f64| v.get(k).and_then(Value::as_f64).filter(|x| x.is_finite()).unwrap_or(d);
|
||||
list.iter()
|
||||
.take(MAX_PINS)
|
||||
.map(|v| Pin { x: num(v, "x", 0.5).clamp(0.0, 1.0), y: num(v, "y", 0.5).clamp(0.0, 1.0), blur: num(v, "blur", DEFAULT_BLUR).clamp(0.0, MAX_BLUR) })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn set_pins(fields: &mut Map<String, Value>, pins: &[Pin]) {
|
||||
let list: Vec<Value> = pins.iter().map(|p| json!({"x": p.x, "y": p.y, "blur": p.blur})).collect();
|
||||
fields.insert("pins".into(), Value::Array(list));
|
||||
}
|
||||
|
||||
/// The selected pin's index, if it exists.
|
||||
pub fn selected(fields: &Map<String, Value>) -> Option<usize> {
|
||||
let n = pins(fields).len();
|
||||
fields.get("__selected").and_then(Value::as_u64).map(|i| i as usize).filter(|i| *i < n)
|
||||
}
|
||||
|
||||
fn select(fields: &mut Map<String, Value>, i: Option<usize>) {
|
||||
fields.insert("__selected".into(), i.map_or(Value::Null, |i| json!(i)));
|
||||
// The dialog's Blur (remembered for the next Field Blur) follows the selected pin.
|
||||
if let Some(p) = i.and_then(|i| pins(fields).get(i).copied()) {
|
||||
fields.insert("blur".into(), json!(p.blur));
|
||||
}
|
||||
}
|
||||
|
||||
/// The filter's bounds in document pixels `[x0, y0, x1, y1]`.
|
||||
fn bounds(fields: &Map<String, Value>) -> Option<[f64; 4]> {
|
||||
let b = fields.get("__bounds")?.as_array()?;
|
||||
let v = |i: usize| b.get(i).and_then(Value::as_f64);
|
||||
let r = [v(0)?, v(1)?, v(2)?, v(3)?];
|
||||
(r[2] > r[0] && r[3] > r[1]).then_some(r)
|
||||
}
|
||||
|
||||
fn to_doc(b: [f64; 4], p: Pin) -> [f64; 2] {
|
||||
[b[0] + p.x * (b[2] - b[0]), b[1] + p.y * (b[3] - b[1])]
|
||||
}
|
||||
|
||||
fn from_doc(b: [f64; 4], d: [f64; 2]) -> (f64, f64) {
|
||||
(((d[0] - b[0]) / (b[2] - b[0])).clamp(0.0, 1.0), ((d[1] - b[1]) / (b[3] - b[1])).clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// Remove the selected pin; the one before it (or the new first) is selected.
|
||||
pub fn remove_selected(fields: &mut Map<String, Value>) -> bool {
|
||||
let Some(i) = selected(fields) else { return false };
|
||||
let mut list = pins(fields);
|
||||
list.remove(i);
|
||||
set_pins(fields, &list);
|
||||
select(fields, if list.is_empty() { None } else { Some(i.saturating_sub(1)) });
|
||||
true
|
||||
}
|
||||
|
||||
/// The dialog body: the selected pin's Blur, how to place pins, and Preview.
|
||||
pub fn body(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
|
||||
let t = crate::theme::Tokens::get(ui.ctx());
|
||||
match selected(f) {
|
||||
Some(i) => {
|
||||
let mut list = pins(f);
|
||||
if let Some(pin) = list.get_mut(i) {
|
||||
let mut v = pin.blur as f32;
|
||||
if crate::widgets::slider_row(ui, tl!("Blur"), &mut v, 0.0..=MAX_BLUR as f32, "px", None).changed() {
|
||||
pin.blur = f64::from(v.round()).clamp(0.0, MAX_BLUR);
|
||||
set_pins(f, &list);
|
||||
select(f, Some(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ui.label(egui::RichText::new(tl!("No pin selected")).color(t.text_dim));
|
||||
}
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
egui::RichText::new(tl!("Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin."))
|
||||
.color(t.text_dim)
|
||||
.size(11.5),
|
||||
)
|
||||
.wrap(),
|
||||
);
|
||||
// Delete removes the selected pin, unless a field is being typed in.
|
||||
let typing = ui.ctx().memory(|m| m.focused().is_some());
|
||||
if !typing && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Delete) || i.consume_key(egui::Modifiers::NONE, egui::Key::Backspace)) {
|
||||
remove_selected(f);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
if let Some(mut preview) = f.get("__preview").and_then(Value::as_bool) {
|
||||
ui.add_space(4.0);
|
||||
crate::widgets::checkbox(ui, &mut preview, tl!("Preview"));
|
||||
f.insert("__preview".into(), json!(preview));
|
||||
}
|
||||
}
|
||||
|
||||
/// What a press at screen point `at` grabs: the selected pin's ring, a pin, or nothing (a new pin).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Grab {
|
||||
Ring(usize),
|
||||
Pin(usize),
|
||||
New,
|
||||
}
|
||||
|
||||
fn grab(fields: &Map<String, Value>, xf: &ViewXform, at: Pos2) -> Grab {
|
||||
let Some(b) = bounds(fields) else { return Grab::New };
|
||||
let list = pins(fields);
|
||||
let screen = |p: Pin| {
|
||||
let d = to_doc(b, p);
|
||||
xf.to_screen(d[0] as f32, d[1] as f32)
|
||||
};
|
||||
if let Some(i) = selected(fields)
|
||||
&& let Some(p) = list.get(i)
|
||||
&& (screen(*p).distance(at) - RING).abs() <= RING_BAND
|
||||
{
|
||||
return Grab::Ring(i);
|
||||
}
|
||||
list.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| (i, screen(*p).distance(at)))
|
||||
.filter(|(_, d)| *d <= GRAB)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map_or(Grab::New, |(i, _)| Grab::Pin(i))
|
||||
}
|
||||
|
||||
/// The pointer on the free image under the dialog: `over` is where it is, `press` where the
|
||||
/// primary button is held (`dialogs::free_press`), both in screen points. Returns whether the
|
||||
/// workspace took it.
|
||||
pub fn canvas(app: &mut PhotocraftApp, ctx: &egui::Context, xf: &ViewXform, over: Pos2, press: Option<Pos2>) -> bool {
|
||||
let Some(d) = app.ui.dialogs.last_mut().filter(|d| owns(&d.fields)) else { return false };
|
||||
let f = &mut d.fields;
|
||||
let Some(b) = bounds(f) else { return false };
|
||||
let (start, down) = ctx.input(|i| (i.pointer.primary_pressed(), i.pointer.primary_down()));
|
||||
ctx.set_cursor_icon(match grab(f, xf, over) {
|
||||
Grab::Ring(_) => egui::CursorIcon::Alias,
|
||||
Grab::Pin(_) => egui::CursorIcon::Grab,
|
||||
Grab::New => egui::CursorIcon::Crosshair,
|
||||
});
|
||||
if !down && press.is_none() {
|
||||
f.remove("__drag");
|
||||
return true;
|
||||
}
|
||||
let Some(at) = press else { return true };
|
||||
let doc = xf.to_doc(at);
|
||||
let mut list = pins(f);
|
||||
if start {
|
||||
let drag = match grab(f, xf, at) {
|
||||
Grab::Ring(i) => {
|
||||
let c = list.get(i).map(|p| to_doc(b, *p)).unwrap_or(doc);
|
||||
json!({"ring": i, "angle": (doc[1] - c[1]).atan2(doc[0] - c[0])})
|
||||
}
|
||||
Grab::Pin(i) => {
|
||||
select(f, Some(i));
|
||||
let c = list.get(i).map(|p| to_doc(b, *p)).unwrap_or(doc);
|
||||
json!({"pin": i, "dx": c[0] - doc[0], "dy": c[1] - doc[1]})
|
||||
}
|
||||
Grab::New if list.len() < MAX_PINS && (b[0]..=b[2]).contains(&doc[0]) && (b[1]..=b[3]).contains(&doc[1]) => {
|
||||
// A new pin takes the selected pin's blur, as the dialog's Blur shows it.
|
||||
let blur = f.get("blur").and_then(Value::as_f64).filter(|v| v.is_finite()).unwrap_or(DEFAULT_BLUR).clamp(0.0, MAX_BLUR);
|
||||
let (x, y) = from_doc(b, doc);
|
||||
list.push(Pin { x, y, blur });
|
||||
set_pins(f, &list);
|
||||
select(f, Some(list.len() - 1));
|
||||
json!({"pin": list.len() - 1, "dx": 0.0, "dy": 0.0})
|
||||
}
|
||||
Grab::New => return true,
|
||||
};
|
||||
f.insert("__drag".into(), drag);
|
||||
ctx.request_repaint();
|
||||
return true;
|
||||
}
|
||||
let Some(drag) = f.get("__drag").cloned() else { return true };
|
||||
if let Some(i) = drag.get("pin").and_then(Value::as_u64).map(|i| i as usize)
|
||||
&& let Some(p) = list.get_mut(i)
|
||||
{
|
||||
let num = |k: &str| drag.get(k).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
(p.x, p.y) = from_doc(b, [doc[0] + num("dx"), doc[1] + num("dy")]);
|
||||
set_pins(f, &list);
|
||||
} else if let Some(i) = drag.get("ring").and_then(Value::as_u64).map(|i| i as usize)
|
||||
&& let Some(p) = list.get_mut(i)
|
||||
{
|
||||
// The angle turned since the last frame, the short way round: clockwise raises the blur.
|
||||
let c = to_doc(b, *p);
|
||||
let a = (doc[1] - c[1]).atan2(doc[0] - c[0]);
|
||||
let last = drag.get("angle").and_then(Value::as_f64).unwrap_or(a);
|
||||
let turn = (a - last + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI;
|
||||
p.blur = (p.blur + turn / std::f64::consts::TAU * MAX_BLUR).clamp(0.0, MAX_BLUR);
|
||||
set_pins(f, &list);
|
||||
select(f, Some(i));
|
||||
f.insert("__drag".into(), json!({"ring": i, "angle": a}));
|
||||
}
|
||||
ctx.request_repaint();
|
||||
true
|
||||
}
|
||||
|
||||
/// Draw the pins over the image while the workspace is open: a dot per pin (filled when
|
||||
/// selected) and, round the selected one, its ring with the blur as an arc from the top.
|
||||
pub fn draw(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform) {
|
||||
let Some(d) = app.ui.dialogs.last().filter(|d| owns(&d.fields)) else { return };
|
||||
let Some(b) = bounds(&d.fields) else { return };
|
||||
let sel = selected(&d.fields);
|
||||
let (dark, light) = (Stroke::new(3.0, Color32::from_black_alpha(150)), Stroke::new(1.5, Color32::from_white_alpha(235)));
|
||||
for (i, p) in pins(&d.fields).into_iter().enumerate() {
|
||||
let c = to_doc(b, p);
|
||||
let c = xf.to_screen(c[0] as f32, c[1] as f32);
|
||||
if sel == Some(i) {
|
||||
painter.circle_stroke(c, RING, dark);
|
||||
painter.circle_stroke(c, RING, Stroke::new(1.0, Color32::from_white_alpha(140)));
|
||||
let frac = (p.blur / MAX_BLUR) as f32;
|
||||
let steps = (frac * 64.0).ceil().max(1.0) as usize;
|
||||
let arc: Vec<Pos2> = (0..=steps)
|
||||
.map(|k| {
|
||||
let a = -std::f32::consts::FRAC_PI_2 + std::f32::consts::TAU * frac * k as f32 / steps as f32;
|
||||
c + vec2(a.cos(), a.sin()) * RING
|
||||
})
|
||||
.collect();
|
||||
if frac > 0.0 {
|
||||
painter.add(egui::Shape::line(arc.clone(), Stroke::new(4.5, Color32::from_black_alpha(150))));
|
||||
painter.add(egui::Shape::line(arc, Stroke::new(2.5, Color32::WHITE)));
|
||||
}
|
||||
painter.circle(c, DOT, Color32::WHITE, Stroke::new(1.0, Color32::from_black_alpha(180)));
|
||||
} else {
|
||||
painter.circle_stroke(c, DOT, dark);
|
||||
painter.circle_stroke(c, DOT, light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Preview proxies run at 1/`k` of the document: the pins' pixel blur scales with them.
|
||||
pub fn scale_for_proxy(params: &mut Value, k: u32) {
|
||||
if k <= 1 {
|
||||
return;
|
||||
}
|
||||
let Some(list) = params.get_mut("pins").and_then(Value::as_array_mut) else { return };
|
||||
for pin in list {
|
||||
if let Some(b) = pin.get("blur").and_then(Value::as_f64) {
|
||||
pin["blur"] = json!(b / f64::from(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
//! #3373: Field Blur's pins are placed, dragged and tuned on the image under the dialog, the
|
||||
//! preview follows them, and OK applies them.
|
||||
|
||||
use egui::{Modifiers, PointerButton, Pos2, pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::PhotocraftApp;
|
||||
use crate::blur_pins::{COMMAND, Pin, pins, selected};
|
||||
use crate::canvas::ViewXform;
|
||||
|
||||
/// A 400×300 document at 400 %, so it fills the window round the dialog, with Field Blur open.
|
||||
fn harness(selection: bool) -> Harness<'static, PhotocraftApp> {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
if selection {
|
||||
app.run("select.rect", json!({"x": 100, "y": 50, "width": 200, "height": 100})).unwrap();
|
||||
}
|
||||
app.sync_views();
|
||||
app.ui.extras.rulers = false;
|
||||
let mut h = Harness::builder().with_size(vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut PhotocraftApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
if !ctx.fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) {
|
||||
return;
|
||||
}
|
||||
crate::shortcuts::handle(app, &ctx);
|
||||
egui::CentralPanel::default().show(ui, |ui| crate::canvas::document_area(app, ui));
|
||||
crate::dialogs::show(app, &ctx);
|
||||
},
|
||||
app,
|
||||
);
|
||||
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
|
||||
h.run_steps(4);
|
||||
let v = &mut h.state_mut().ui.views[0];
|
||||
(v.zoom, v.center, v.fit_pending) = (4.0, [200.0, 150.0], false);
|
||||
crate::filter_dialog::open(h.state_mut(), COMMAND).unwrap();
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn fields(h: &Harness<'static, PhotocraftApp>) -> serde_json::Map<String, serde_json::Value> {
|
||||
h.state().ui.dialogs.last().unwrap().fields.clone()
|
||||
}
|
||||
|
||||
fn xf(h: &Harness<'static, PhotocraftApp>) -> ViewXform {
|
||||
let app = h.state();
|
||||
let v = &app.ui.views[0];
|
||||
ViewXform {
|
||||
rect: crate::rulers::content_rect(app, app.last_canvas_rect),
|
||||
zoom: v.zoom,
|
||||
center: v.center,
|
||||
flip: false,
|
||||
rotation: v.rotation,
|
||||
aspect: app.ui.view.display_aspect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn button(h: &mut Harness<'static, PhotocraftApp>, p: Pos2, down: bool) {
|
||||
h.event(egui::Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: down, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
}
|
||||
|
||||
fn move_to(h: &mut Harness<'static, PhotocraftApp>, p: Pos2) {
|
||||
h.event(egui::Event::PointerMoved(p));
|
||||
h.run_steps(1);
|
||||
}
|
||||
|
||||
fn drag(h: &mut Harness<'static, PhotocraftApp>, path: &[Pos2]) {
|
||||
move_to(h, path[0]);
|
||||
button(h, path[0], true);
|
||||
for p in &path[1..] {
|
||||
move_to(h, *p);
|
||||
}
|
||||
button(h, *path.last().unwrap(), false);
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// Where pin `p` is on screen.
|
||||
fn on_screen(h: &Harness<'static, PhotocraftApp>, p: Pin, bounds: [f64; 4]) -> Pos2 {
|
||||
let (x, y) = (bounds[0] + p.x * (bounds[2] - bounds[0]), bounds[1] + p.y * (bounds[3] - bounds[1]));
|
||||
xf(h).to_screen(x as f32, y as f32)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_opens_with_one_selected_pin_in_the_middle_of_the_bounds() {
|
||||
let h = harness(false);
|
||||
let f = fields(&h);
|
||||
assert_eq!(pins(&f), vec![Pin { x: 0.5, y: 0.5, blur: 15.0 }]);
|
||||
assert_eq!(selected(&f), Some(0));
|
||||
assert_eq!(f["__bounds"], json!([0, 0, 400, 300]));
|
||||
assert!(!f.contains_key("centerX") && !f.contains_key("centerY"), "the pins replace the single centre");
|
||||
// With a selection the pins are fractions of its bounds, as the engine reads them.
|
||||
let h = harness(true);
|
||||
assert_eq!(fields(&h)["__bounds"], json!([100, 50, 300, 150]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pins_are_added_moved_tuned_and_removed_on_the_image() {
|
||||
let mut h = harness(false);
|
||||
let r = h.state().last_canvas_rect;
|
||||
// A click on the free image (left of the dialog) adds a pin there, selected.
|
||||
let a = pos2(r.left() + 80.0, r.bottom() - 80.0);
|
||||
move_to(&mut h, a);
|
||||
button(&mut h, a, true);
|
||||
button(&mut h, a, false);
|
||||
h.run_steps(2);
|
||||
let f = fields(&h);
|
||||
let list = pins(&f);
|
||||
assert_eq!(list.len(), 2, "{list:?}");
|
||||
assert_eq!(selected(&f), Some(1));
|
||||
let at = xf(&h).to_doc(a);
|
||||
assert!((list[1].x - at[0] / 400.0).abs() < 1e-3 && (list[1].y - at[1] / 300.0).abs() < 1e-3, "{:?} vs {at:?}", list[1]);
|
||||
assert_eq!(list[1].blur, 15.0, "a new pin takes the selected pin's blur");
|
||||
// Dragging the pin moves it by the pointer's movement (4 screen points per pixel at 400 %).
|
||||
let b = a + vec2(40.0, -20.0);
|
||||
drag(&mut h, &[a, a + vec2(20.0, -10.0), b]);
|
||||
let moved = pins(&fields(&h))[1];
|
||||
assert!((moved.x - (list[1].x + 10.0 / 400.0)).abs() < 1e-3 && (moved.y - (list[1].y - 5.0 / 300.0)).abs() < 1e-3, "{moved:?}");
|
||||
assert!(fields(&h).get("__drag").is_none(), "the drag ends on release");
|
||||
// A quarter turn clockwise on its ring adds a quarter of the 0–500 px range.
|
||||
let c = on_screen(&h, moved, [0.0, 0.0, 400.0, 300.0]);
|
||||
let ring: Vec<Pos2> = (0..=6).map(|k| c + vec2(24.0 * (k as f32 * 0.2618).cos(), 24.0 * (k as f32 * 0.2618).sin())).collect();
|
||||
drag(&mut h, &ring);
|
||||
let tuned = pins(&fields(&h))[1];
|
||||
assert!((tuned.blur - 140.0).abs() < 2.0, "15 + 125 px: {}", tuned.blur);
|
||||
assert!((tuned.x - moved.x).abs() < 1e-9, "the ring doesn't move the pin");
|
||||
assert_eq!(fields(&h)["blur"], json!(tuned.blur), "the dialog's Blur follows the selected pin");
|
||||
// The preview runs the filter with the pins (computed by the canvas's GPU path in the app;
|
||||
// asked for directly here, as the headless harness has no GPU).
|
||||
assert!(crate::canvas::ensure_filter_preview(h.state_mut(), 0, &egui::Context::default()).is_some());
|
||||
let preview = h.state().filter_preview.as_ref().expect("a live preview");
|
||||
assert_eq!(preview.key.params["pins"].as_array().map(Vec::len), Some(2));
|
||||
// Delete removes the selected pin; OK applies the rest in one step.
|
||||
h.key_press(egui::Key::Delete);
|
||||
h.run_steps(2);
|
||||
assert_eq!(pins(&fields(&h)).len(), 1);
|
||||
assert_eq!(selected(&fields(&h)), Some(0));
|
||||
let id = h.state().ui.dialogs.last().unwrap().id;
|
||||
let steps = h.state().session.active().unwrap().history.past_len();
|
||||
crate::dialogs::confirm(h.state_mut(), id).unwrap();
|
||||
let (cmd, p) = h.state().session.journal.last().cloned().unwrap();
|
||||
assert_eq!(cmd, COMMAND);
|
||||
assert_eq!(p["pins"], json!([{"x": 0.5, "y": 0.5, "blur": 15.0}]), "{p}");
|
||||
assert_eq!(h.state().session.active().unwrap().history.past_len(), steps + 1);
|
||||
}
|
||||
|
||||
/// A click outside the bounds (beside a selection) adds no pin; pins are capped.
|
||||
#[test]
|
||||
fn pins_stay_inside_the_bounds_and_are_capped() {
|
||||
let mut h = harness(true);
|
||||
let r = h.state().last_canvas_rect;
|
||||
let outside = pos2(r.left() + 30.0, r.top() + 30.0);
|
||||
assert!(xf(&h).to_doc(outside)[0] < 100.0, "left of the selection");
|
||||
move_to(&mut h, outside);
|
||||
button(&mut h, outside, true);
|
||||
button(&mut h, outside, false);
|
||||
h.run_steps(2);
|
||||
assert_eq!(pins(&fields(&h)).len(), 1);
|
||||
let many: Vec<_> = (0..200).map(|i| json!({"x": f64::from(i) / 200.0, "y": 0.5, "blur": 1e9})).collect();
|
||||
h.state_mut().ui.dialogs.last_mut().unwrap().fields.insert("pins".into(), json!(many));
|
||||
let list = pins(&fields(&h));
|
||||
assert_eq!(list.len(), crate::blur_pins::MAX_PINS);
|
||||
assert!(list.iter().all(|p| p.blur == 500.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_previews_scale_the_pins_blur() {
|
||||
let mut p = json!({"pins": [{"x": 0.2, "y": 0.3, "blur": 40.0}, {"x": 0.9, "y": 0.9, "blur": 0.0}]});
|
||||
crate::blur_pins::scale_for_proxy(&mut p, 4);
|
||||
assert_eq!(p, json!({"pins": [{"x": 0.2, "y": 0.3, "blur": 10.0}, {"x": 0.9, "y": 0.9, "blur": 0.0}]}));
|
||||
crate::blur_pins::scale_for_proxy(&mut p, 1);
|
||||
assert_eq!(p["pins"][0]["blur"], json!(10.0));
|
||||
}
|
||||
|
||||
/// Alt+Cancel → Reset (#3871) starts the workspace again from one pin in the middle, as when it
|
||||
/// opened, instead of leaving it with no pins; the bounds captured on opening stay.
|
||||
#[test]
|
||||
fn alt_reset_starts_again_from_one_pin() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
let id = crate::filter_dialog::open(&mut app, COMMAND).unwrap();
|
||||
let mut fields = app.ui.dialog_mut(id).unwrap().fields.clone();
|
||||
let bounds = fields["__bounds"].clone();
|
||||
fields.insert("pins".into(), json!([{"x": 0.1, "y": 0.2, "blur": 40.0}, {"x": 0.8, "y": 0.7, "blur": 90.0}]));
|
||||
fields.insert("__selected".into(), json!(1));
|
||||
assert!(crate::dialog_alt_reset::supported(&fields));
|
||||
crate::dialog_alt_reset::apply(&mut app, &egui::Context::default(), &mut fields);
|
||||
assert_eq!(pins(&fields), [Pin { x: 0.5, y: 0.5, blur: 15.0 }]);
|
||||
assert_eq!(selected(&fields), Some(0));
|
||||
assert_eq!(fields["__bounds"], bounds);
|
||||
assert!(!fields.contains_key("centerX") && !fields.contains_key("centerY"), "{fields:?}");
|
||||
}
|
||||
@@ -3214,7 +3214,9 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
||||
// Select and Mask's Refine Edge Brush paints on the image.
|
||||
let refine_brushing =
|
||||
primary && app.ui.dialogs.last().is_some_and(|d| crate::select_mask_view::owns(&d.fields) && crate::select_mask_view::brush_on(&d.fields));
|
||||
let hand = app.ui.tool == Tool::Hand && !picking && !range_picking && !refine_brushing;
|
||||
// Field Blur's pins are placed and dragged on the image (`blur_pins`).
|
||||
let pinning = primary && app.ui.dialogs.last().is_some_and(|d| crate::blur_pins::owns(&d.fields));
|
||||
let hand = app.ui.tool == Tool::Hand && !picking && !range_picking && !refine_brushing && !pinning;
|
||||
if let Some(d) = crate::dialogs::pan_delta(&ctx, rect, hand) {
|
||||
let d = xf.unmap_vec(d) / (view.zoom / ppp);
|
||||
view.center[0] -= d.x;
|
||||
@@ -3238,6 +3240,12 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
||||
crate::color_picker_ui::sample_at(app, d[0], d[1]);
|
||||
}
|
||||
}
|
||||
} else if pinning
|
||||
&& !middle
|
||||
&& let Some(p) = crate::dialogs::free_pointer_over(&ctx, rect)
|
||||
{
|
||||
let press = crate::dialogs::free_press(&ctx, rect);
|
||||
crate::blur_pins::canvas(app, &ctx, &xf, p, press);
|
||||
} else if refine_brushing
|
||||
&& !middle
|
||||
&& let Some(p) = crate::dialogs::free_pointer_over(&ctx, rect)
|
||||
@@ -4445,6 +4453,7 @@ fn draw_tool_state(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform,
|
||||
crate::tool_feedback::draw_ants(painter, &pts, false);
|
||||
}
|
||||
crate::crop_straighten::draw(app, painter, |p| xf.to_screen(p[0] as f32, p[1] as f32), hover);
|
||||
crate::blur_pins::draw(app, painter, xf);
|
||||
if let Some(c) = app.ui.crop_rect {
|
||||
let deg = crate::crop_ui::angle(app);
|
||||
let shield = crate::crop_shield::current(app, painter.ctx());
|
||||
|
||||
@@ -157,7 +157,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
// effects, extends down and right instead of re-centring.
|
||||
let pinned: Option<egui::Vec2> = ctx.data(|m| m.get_temp(id));
|
||||
let place = place_id(&d);
|
||||
let start = pinned.or_else(|| ctx.data_mut(|m| m.get_persisted::<egui::Vec2>(place)));
|
||||
let start = pinned.or_else(|| ctx.data_mut(|m| m.get_persisted::<egui::Vec2>(place))).or_else(|| crate::blur_pins::first_place(&d.fields, ctx));
|
||||
let mut drag = egui::Vec2::ZERO;
|
||||
let mut sizing = false;
|
||||
let area = match start {
|
||||
@@ -305,6 +305,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
// Image Size and Canvas Size open on Width, selected (#2329).
|
||||
DialogKind::Command if fields.contains_key("__sizing") => first_field_focused(ui, id, |ui| crate::sizing::body(ui, &mut fields)),
|
||||
DialogKind::Command if crate::adjust_dialog::owns(&fields) => crate::adjust_dialog::body(app, ui, &mut fields),
|
||||
DialogKind::Command if crate::blur_pins::owns(&fields) => crate::blur_pins::body(ui, &mut fields),
|
||||
DialogKind::Command if fields.contains_key("__filter") => {
|
||||
// Color Settings: the monitor profile in use can change while it is open.
|
||||
if fields.get("__command").and_then(Value::as_str) == Some("edit.colorSettings") {
|
||||
|
||||
@@ -435,6 +435,10 @@ pub fn reset_fields(app: &PhotocraftApp, fields: &mut Map<String, Value>) {
|
||||
}
|
||||
}
|
||||
apply_command_defaults(app, &command, fields);
|
||||
// The schema has no default for the Blur Gallery's pins: start again from the opening one.
|
||||
if command == crate::blur_pins::COMMAND {
|
||||
crate::blur_pins::reset(fields);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(app: &mut PhotocraftApp, command: &str) -> Option<u64> {
|
||||
@@ -454,6 +458,9 @@ pub fn open(app: &mut PhotocraftApp, command: &str) -> Option<u64> {
|
||||
}
|
||||
restore_remembered(app, command, spec.params, &mut fields);
|
||||
apply_command_defaults(app, command, &mut fields);
|
||||
if command == crate::blur_pins::COMMAND {
|
||||
crate::blur_pins::init(app, &mut fields);
|
||||
}
|
||||
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<String> = app.session.documents().iter().map(|d| d.doc.name.clone()).collect();
|
||||
@@ -906,6 +913,10 @@ pub(crate) fn preview_document_for_target(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Blur Gallery pins carry their blur in document pixels too.
|
||||
if command.starts_with("filter.blurGallery.") {
|
||||
crate::blur_pins::scale_for_proxy(&mut p, run_k);
|
||||
}
|
||||
s.execute(command, p).ok()?;
|
||||
s.active().map(|d| crate::proxy::proxy_document(&d.doc, reduce_k))
|
||||
}
|
||||
|
||||
@@ -559,6 +559,9 @@
|
||||
Blur Rozostření
|
||||
Blur Gallery Galerie rozostření
|
||||
Field Blur… Rozostření pole…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Není vybrán žádný špendlík
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Kliknutím do obrazu přidáte špendlík; tažením špendlík přesunete, tažením jeho prstence změníte rozostření. Delete odstraní vybraný špendlík.
|
||||
Iris Blur… Rozostření clonou…
|
||||
Tilt-Shift… Tilt-shift…
|
||||
Path Blur… Rozostření podél cesty…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -558,6 +558,9 @@
|
||||
Blur Weichzeichnen
|
||||
Blur Gallery Weichzeichnen-Galerie
|
||||
Field Blur… Feldweichzeichner…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Kein Pin ausgewählt
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Klicken Sie ins Bild, um einen Pin hinzuzufügen; ziehen Sie einen Pin, um ihn zu verschieben, oder seinen Ring, um die Unschärfe zu ändern. Entf entfernt den ausgewählten Pin.
|
||||
Iris Blur… Irisweichzeichner…
|
||||
Tilt-Shift… Tilt-Shift…
|
||||
Path Blur… Pfadweichzeichner…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2398,6 +2398,9 @@
|
||||
Fibers… Ίνες…
|
||||
Field Blur Θόλωση πεδίου
|
||||
Field Blur… Θόλωση πεδίου…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Δεν έχει επιλεγεί καρφίτσα
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Κάντε κλικ στην εικόνα για να προσθέσετε καρφίτσα· σύρετε μια καρφίτσα για να τη μετακινήσετε ή τον δακτύλιό της για να αλλάξετε τη θόλωση. Το Delete αφαιρεί την επιλεγμένη καρφίτσα.
|
||||
File Info Πληροφορίες αρχείου
|
||||
File Settings Ρυθμίσεις αρχείου
|
||||
Fill Path Γέμισμα διαδρομής
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1029 and column 18.
|
@@ -555,6 +555,9 @@
|
||||
Blur Desenfocar
|
||||
Blur Gallery Galería de desenfoque
|
||||
Field Blur… Desenfoque de campo…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Ningún pin seleccionado
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Haga clic en la imagen para añadir un pin; arrastre un pin para moverlo o su anillo para cambiar el desenfoque. Suprimir elimina el pin seleccionado.
|
||||
Iris Blur… Desenfoque de iris…
|
||||
Tilt-Shift… Cambio de inclinación…
|
||||
Path Blur… Desenfoque de trazado…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -543,6 +543,9 @@
|
||||
Blur Flou
|
||||
Blur Gallery Galerie de flous
|
||||
Field Blur… Flou de champ…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Aucune épingle sélectionnée
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Cliquez dans l’image pour ajouter une épingle ; faites glisser une épingle pour la déplacer, ou son anneau pour modifier le flou. Suppr supprime l’épingle sélectionnée.
|
||||
Iris Blur… Flou d'iris…
|
||||
Tilt-Shift… Inclinaison-décalage…
|
||||
Path Blur… Flou de tracé…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -561,6 +561,8 @@
|
||||
Facet Lapolás
|
||||
Fibers… Szálak…
|
||||
Field Blur… Mezőelmosás…
|
||||
No pin selected Nincs kijelölt tű
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Kattints a képre tű hozzáadásához; húzd a tűt az áthelyezéséhez, vagy a gyűrűjét az elmosás módosításához. A Delete törli a kijelölt tűt.
|
||||
Fill Path… Útvonal kitöltése…
|
||||
Fill Screen Képernyő kitöltése
|
||||
Filter Szűrő
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -536,6 +536,9 @@
|
||||
Blur Blur
|
||||
Blur Gallery Galeri Blur
|
||||
Field Blur… Field Blur…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Tidak ada pin yang dipilih
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Klik gambar untuk menambah pin; seret pin untuk memindahkannya, atau cincinnya untuk mengubah blur. Delete menghapus pin yang dipilih.
|
||||
Iris Blur… Iris Blur…
|
||||
Tilt-Shift… Tilt-Shift…
|
||||
Path Blur… Path Blur…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -551,6 +551,9 @@
|
||||
Blur Sfocatura
|
||||
Blur Gallery Galleria sfocatura
|
||||
Field Blur… Sfocatura campo…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Nessuna puntina selezionata
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Fai clic sull’immagine per aggiungere una puntina; trascina una puntina per spostarla o il suo anello per cambiare la sfocatura. Canc rimuove la puntina selezionata.
|
||||
Iris Blur… Sfocatura diaframma…
|
||||
Tilt-Shift… Miniatura…
|
||||
Path Blur… Sfocatura tracciato…
|
||||
|
||||
|
@@ -553,6 +553,9 @@
|
||||
Blur ぼかし
|
||||
Blur Gallery ぼかしギャラリー
|
||||
Field Blur… フィールドぼかし…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected ピンが選択されていません
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. 画像をクリックしてピンを追加します。ピンをドラッグして移動し、リングをドラッグしてぼかしを変更します。Delete キーで選択したピンを削除します。
|
||||
Iris Blur… 虹彩絞りぼかし…
|
||||
Tilt-Shift… チルトシフト…
|
||||
Path Blur… パスぼかし…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -540,6 +540,9 @@
|
||||
Blur 흐림 효과
|
||||
Blur Gallery 흐림 효과 갤러리
|
||||
Field Blur… 필드 흐림 효과…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected 선택한 핀 없음
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. 이미지를 클릭하여 핀을 추가합니다. 핀을 드래그하여 이동하고, 링을 드래그하여 흐림 정도를 바꿉니다. Delete 키로 선택한 핀을 삭제합니다.
|
||||
Iris Blur… 조리개 흐림 효과…
|
||||
Tilt-Shift… 기울기-이동…
|
||||
Path Blur… 패스 흐림 효과…
|
||||
|
||||
|
@@ -543,6 +543,9 @@
|
||||
Blur Vervagen
|
||||
Blur Gallery Vervagingsgalerie
|
||||
Field Blur… Veldvervaging…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Geen speld geselecteerd
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Klik in de afbeelding om een speld toe te voegen; sleep een speld om hem te verplaatsen, of zijn ring om de vervaging te wijzigen. Delete verwijdert de geselecteerde speld.
|
||||
Iris Blur… Irisvervaging…
|
||||
Tilt-Shift… Tilt-shift…
|
||||
Path Blur… Padvervaging…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -552,6 +552,9 @@
|
||||
Blur Rozmycie
|
||||
Blur Gallery Galeria rozmyć
|
||||
Field Blur… Rozmycie pola…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Nie zaznaczono pinezki
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Kliknij obraz, aby dodać pinezkę; przeciągnij pinezkę, aby ją przesunąć, lub jej pierścień, aby zmienić rozmycie. Delete usuwa zaznaczoną pinezkę.
|
||||
Iris Blur… Rozmycie irysowe…
|
||||
Tilt-Shift… Tilt-shift…
|
||||
Path Blur… Rozmycie ścieżki…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -537,6 +537,9 @@
|
||||
Blur Desfoque
|
||||
Blur Gallery Galeria de desfoque
|
||||
Field Blur… Desfoque de campo…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Nenhum alfinete selecionado
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Clique na imagem para adicionar um alfinete; arraste um alfinete para movê-lo ou o anel dele para alterar o desfoque. Delete remove o alfinete selecionado.
|
||||
Iris Blur… Desfoque de íris…
|
||||
Tilt-Shift… Tilt-shift…
|
||||
Path Blur… Desfoque de demarcador…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -343,6 +343,9 @@
|
||||
Oil Paint… Масляная живопись…
|
||||
Blur Gallery Галерея размытия
|
||||
Field Blur… Размытие по полю…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Булавка не выбрана
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Щелкните изображение, чтобы добавить булавку; перетащите булавку, чтобы переместить ее, или ее кольцо, чтобы изменить размытие. Delete удаляет выбранную булавку.
|
||||
Iris Blur… Диафрагменное размытие…
|
||||
Tilt-Shift… Наклон-сдвиг…
|
||||
Path Blur… Размытие по контуру…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -533,6 +533,8 @@
|
||||
Blur Zamućenje
|
||||
Blur Gallery Galerija zamućenja
|
||||
Field Blur… Zamućenje polja…
|
||||
No pin selected Nijedna čioda nije izabrana
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Kliknite na sliku da dodate čiodu; prevucite čiodu da je pomerite ili njen prsten da promenite zamućenje. Delete uklanja izabranu čiodu.
|
||||
Iris Blur… Iris zamućenje…
|
||||
Tilt-Shift… Tilt-shift…
|
||||
Path Blur… Zamućenje duž putanje…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -532,6 +532,8 @@
|
||||
Blur Bulanıklaştırma
|
||||
Blur Gallery Bulanıklaştırma Galerisi
|
||||
Field Blur… Alan Bulanıklığı…
|
||||
No pin selected Seçili pim yok
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Pim eklemek için görüntüye tıklayın; taşımak için pimi, bulanıklığını değiştirmek için halkasını sürükleyin. Delete seçili pimi siler.
|
||||
Iris Blur… İris Bulanıklığı…
|
||||
Tilt-Shift… Tilt-Shift…
|
||||
Path Blur… Yol Bulanıklığı…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -321,6 +321,9 @@
|
||||
Oil Paint… Олійний живопис…
|
||||
Blur Gallery Галерея розмиття
|
||||
Field Blur… Розмиття поля…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Шпильку не вибрано
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Клацніть зображення, щоб додати шпильку; перетягніть шпильку, щоб перемістити її, або її кільце, щоб змінити розмиття. Delete видаляє вибрану шпильку.
|
||||
Iris Blur… Розмиття діафрагми…
|
||||
Tilt-Shift… Нахил і зсув…
|
||||
Path Blur… Розмиття вздовж контуру…
|
||||
|
||||
|
@@ -508,6 +508,9 @@
|
||||
Facet Mặt cắt
|
||||
Fibers… Sợi…
|
||||
Field Blur… Làm mờ vùng…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected Chưa chọn ghim nào
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. Bấm vào ảnh để thêm ghim; kéo ghim để di chuyển, hoặc kéo vòng của ghim để đổi độ mờ. Delete xóa ghim đã chọn.
|
||||
Filter Gallery… Thư viện bộ lọc…
|
||||
Find Edges Tìm cạnh
|
||||
Flame… Ngọn lửa…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -558,6 +558,9 @@
|
||||
Blur 模糊
|
||||
Blur Gallery 模糊库
|
||||
Field Blur… 场景模糊…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected 未选择图钉
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. 单击图像以添加图钉;拖动图钉可移动它,拖动其圆环可更改模糊程度。按 Delete 键删除所选图钉。
|
||||
Iris Blur… 光圈模糊…
|
||||
Tilt-Shift… 移轴模糊…
|
||||
Path Blur… 路径模糊…
|
||||
|
||||
|
@@ -563,6 +563,9 @@
|
||||
Blur 模糊
|
||||
Blur Gallery 模糊收藏館
|
||||
Field Blur… 區域模糊…
|
||||
# Field Blur pins (#3373)
|
||||
No pin selected 未選取圖釘
|
||||
Click the image to add a pin; drag a pin to move it, or its ring to change its blur. Delete removes the selected pin. 按一下影像即可新增圖釘;拖移圖釘可移動它,拖移其圓環可變更模糊程度。按 Delete 鍵刪除所選圖釘。
|
||||
Iris Blur… 光圈模糊…
|
||||
Tilt-Shift… 傾斜偏移…
|
||||
Path Blur… 路徑模糊…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -27,6 +27,7 @@ pub mod artboard_ui;
|
||||
pub(crate) mod background_redraw;
|
||||
mod blend_keys;
|
||||
pub(crate) mod blend_preview;
|
||||
pub mod blur_pins;
|
||||
mod brand;
|
||||
pub(crate) mod brush_cursor;
|
||||
pub mod brush_panel;
|
||||
@@ -2520,6 +2521,9 @@ mod new_layer_button_tests;
|
||||
#[cfg(test)]
|
||||
mod smart_filter_rows_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod blur_pins_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod move_outline_tests;
|
||||
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
# Where PhotoCraft falls short of Photoshop
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
|
||||
on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
|
||||
@@ -70,6 +70,8 @@ what an agent can't finish alone.
|
||||
(TOOL-214-15); Vanishing Point plane editor (TOOL-214-14); Content-Aware Move's live preview
|
||||
of the result (TOOL-213-4; Transform On Drop and Content-Aware Scale through the transform box,
|
||||
TOOL-214-7, are done).
|
||||
Blur Gallery: Field Blur has its on-canvas pins (2026-10-11, #3373); Iris, Tilt-Shift, Spin and
|
||||
Path Blur still edit their pins as parameter dialogs.
|
||||
- **Evidence:** scorecard rows above, all partial or missing.
|
||||
- **Impact:** these are the tools retouchers and compositors use daily; a slider dialog is not a
|
||||
substitute.
|
||||
@@ -273,6 +275,7 @@ what an agent can't finish alone.
|
||||
| 2026-10-12 | minor | G4: checkbox-only Brush Settings rows preserve the open controls and Smoothing settings (#4283); no readiness percentage or estimate changed |
|
||||
| 2026-10-12 | minor | G11: Load/Save Actions `.atn` landed (AUTO-219-2 partial); remaining panel work listed |
|
||||
| 2026-10-12 | minor | Refreshed against main: G5 items fixed by #2480, #2327, #2609, #2684, #2675, #2556, #2396, #2527, #2625 and TOOL-214-5/#2442; G7 TOOL-214-7 and Transform On Drop done; G2 #2377 fixed (#2417); G9 UI scale (UI-217-9) and Stamp Visible (UI-217-22) done, prefs 27 of 156; G10 #2225 done (#2482); G13 Khmer, Lao, Myanmar fallback (#3259); G14 CR3 line; G17 Windows ARM64 done (DIST-220-2) |
|
||||
| 2026-10-11 | minor | G7: Field Blur pins are placed, dragged and tuned on the image (#3373); the other Blur Gallery filters remain dialogs |
|
||||
| 2026-10-11 | minor | D5100 image area and Camera Raw tuning validation; G14 remains open |
|
||||
| 2026-10-11 | minor | G13: Czech dynamic UI text localized; engine messages and native review remain open |
|
||||
| 2026-10-11 | minor | G8: triple/quadruple/five-click type selection done (TYPE-218-8, #3493); Type evidence recounted |
|
||||
|
||||
Reference in new issue
Block a user