Content-Aware Fill workspace, as Photoshop's

Edit › Content-Aware Fill… opens Photoshop's workspace (measured on 25.4)
instead of the generic dialog: a full-window dialog like Liquify's.

- Tools: Sampling Brush (B), Lasso (L), Hand (H), Zoom (Z), each with its
  options bar.
- Document view: the sampling area tinted green at 50 % and the fill area's
  marching ants.
- Preview panel: the fill computed in the background after each change,
  opening framed on the sampling window.
- Content-Aware Fill panel: Sampling Area Overlay (Show, Opacity, Color,
  Indicates), Sampling Area Options (Auto, Rectangular, Custom, Sample All
  Layers), Fill Settings, Output Settings, and the reset buttons.
- OK, Apply, Cancel.

As Photoshop's:
- The Sampling Brush subtracts in Auto and Rectangular and adds in Custom,
  which starts empty; Alt reverses. Its first size is a tenth of the
  window's side. Painting keeps the option, and added strokes may lie past
  the window.
- The lasso edits the fill area (New, Add, Subtract, Intersect, Expand,
  Contract); the window and the Auto area follow.
- Apply fills and stays open; Cancel keeps what was applied. Ctrl+Z undoes
  a stroke or an edit. Output opens on New Layer; settings are remembered.

The workspace holds plain data. `edit.contentAwareFill` takes it as
`samplingStrokes`, `fillEdits`, `sampleAllLayers` and `layer`, and both
the preview and OK run one plan (`content_aware_plan`), so the preview is
what gets written. New strings are translated in every catalog.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Unemars
2026-10-10 10:05:40 +02:00
co-authored by Claude Opus 5.5
parent 2e1f3b0a41
commit 4d7db6dabe
25 changed files with 2397 additions and 72 deletions
+105
View File
@@ -466,6 +466,111 @@ pub fn level_transform(ch: usize, from: &[f32], to: &[f32], a: Adapt) -> Vec<(f3
.collect()
}
/// One stroke of the Content-Aware Fill workspace's Sampling Brush: a hard round brush of
/// diameter `size` along `points` (document px), adding to the sampling area or taking from it.
#[derive(Clone, Debug, PartialEq)]
pub struct SamplingStroke {
pub add: bool,
pub size: f64,
pub points: Vec<[f64; 2]>,
}
impl SamplingStroke {
/// Bounds of the pixels the stroke covers.
pub fn bounds(&self) -> Rect {
let r = (self.size / 2.0).max(0.5);
self.points.iter().fold(Rect::EMPTY, |acc, p| {
let b = Rect::new((p[0] - r).floor() as i32, (p[1] - r).floor() as i32, (p[0] + r).ceil() as i32 + 1, (p[1] + r).ceil() as i32 + 1);
if acc.is_empty() { b } else { acc.union(&b) }
})
}
}
/// Squared distance from `p` to the segment `a`–`b`.
fn seg_dist2(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let l2 = dx * dx + dy * dy;
let t = if l2 > 0.0 { (((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l2).clamp(0.0, 1.0) } else { 0.0 };
let (qx, qy) = (a[0] + t * dx - p[0], a[1] + t * dy - p[1]);
qx * qx + qy * qy
}
/// Paint `strokes` in order into `mask` (row-major over `area`): a pixel whose centre lies within
/// half the brush size of a stroke's path is set (adding) or cleared (subtracting).
pub fn paint_strokes(mask: &mut [bool], area: Rect, strokes: &[SamplingStroke]) {
let w = area.width() as usize;
for s in strokes {
let b = s.bounds().intersect(&area);
if b.is_empty() || s.points.is_empty() {
continue;
}
let r2 = (s.size / 2.0).max(0.5).powi(2);
let segs: Vec<([f64; 2], [f64; 2])> =
if s.points.len() == 1 { vec![(s.points[0], s.points[0])] } else { s.points.windows(2).map(|p| (p[0], p[1])).collect() };
for y in b.y0..b.y1 {
for x in b.x0..b.x1 {
let c = [f64::from(x) + 0.5, f64::from(y) + 0.5];
if segs.iter().any(|(a, bb)| seg_dist2(c, *a, *bb) <= r2) {
mask[(y - area.y0) as usize * w + (x - area.x0) as usize] = s.add;
}
}
}
}
}
/// The pixels of `area` (row-major) whose centres lie inside the polygon `pts` (even-odd rule):
/// what a lasso outlines.
pub fn polygon_mask(area: Rect, pts: &[[f64; 2]]) -> Vec<bool> {
let (w, h) = (area.width() as usize, area.height() as usize);
let mut out = vec![false; w * h];
if pts.len() < 3 {
return out;
}
let mut xs: Vec<f64> = Vec::new();
for row in 0..h {
let cy = f64::from(area.y0) + row as f64 + 0.5;
xs.clear();
for i in 0..pts.len() {
let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
if (a[1] <= cy) != (b[1] <= cy) {
xs.push(a[0] + (cy - a[1]) / (b[1] - a[1]) * (b[0] - a[0]));
}
}
xs.sort_by(f64::total_cmp);
for pair in xs.as_chunks::<2>().0 {
// Pixel centres x + 0.5 within [pair0, pair1).
let x0 = ((pair[0] - 0.5).ceil() as i64 - i64::from(area.x0)).clamp(0, w as i64) as usize;
let x1 = ((pair[1] - 0.5).ceil() as i64 - i64::from(area.x0)).clamp(0, w as i64) as usize;
out[row * w + x0..row * w + x1.max(x0)].iter_mut().for_each(|v| *v = true);
}
}
out
}
/// Grow (`by` > 0) or shrink (`by` < 0) a mask by `|by|` pixels (Euclidean): the lasso's Expand
/// and Contract.
pub fn grow_mask(mask: &[bool], w: usize, h: usize, by: i32) -> Vec<bool> {
if by == 0 || w == 0 || h == 0 {
return mask.to_vec();
}
let r = f32::from(u16::try_from(by.unsigned_abs()).unwrap_or(u16::MAX));
if by > 0 {
let d = crate::selection::edt(mask, w, h);
d.iter().map(|v| *v <= r).collect()
} else {
let outside: Vec<bool> = mask.iter().map(|v| !v).collect();
let d = crate::selection::edt(&outside, w, h);
d.iter().map(|v| *v > r).collect()
}
}
/// The Sampling Brush's starting size for a selection with bounds `hole`: a tenth of the sampling
/// window's side, as Photoshop opens the workspace (20 px for a 50 px selection, 40 for 100).
pub fn default_brush_size(hole: Rect) -> f64 {
let (w, h) = (f64::from(hole.width().max(50)), f64::from(hole.height().max(50)));
(0.4 * (w * h).sqrt()).round()
}
#[cfg(test)]
mod tests {
use super::*;
+257 -51
View File
@@ -110,6 +110,12 @@ fn always(_: &Session) -> std::result::Result<(), String> {
fn pixel_layer(s: &Session) -> std::result::Result<LayerId, String> {
let d = s.active().ok_or("no document open")?;
let id = d.active_layer.ok_or("no active layer")?;
pixel_layer_id(s, id)
}
/// `id` if it is an unlocked pixel layer of the active document.
fn pixel_layer_id(s: &Session, id: LayerId) -> std::result::Result<LayerId, String> {
let d = s.active().ok_or("no document open")?;
let l = d.doc.layer(id).ok_or("no active layer")?;
if !matches!(l.content, LayerContent::Raster(_)) {
return Err(format!("active layer is {} {} layer, not a pixel layer", l.content.article(), l.content.kind_name()));
@@ -337,38 +343,186 @@ fn delete_and_fill(s: &mut Session, _: &Value) -> Result<Value> {
content_aware_fill_as(s, &json!({"sampling": "rectangular"}), "edit.deleteAndFillSelection", "Delete and Fill Selection")
}
fn content_aware_fill_as(s: &mut Session, p: &Value, cmd: &'static str, label: &'static str) -> Result<Value> {
use photocraft_algo::content_aware::{FillOptions, auto_sampling, color_level, fill_with, rotation_level};
let id = pixel_layer(s).map_err(EngineError::Other)?;
let st = s.active().ok_or(EngineError::NoDocument)?;
let doc = st.doc.clone();
let sel = doc.selection.clone().ok_or_else(|| bad(cmd, "no selection"))?;
let hb = sel.content_bounds().intersect(&doc.bounds());
if hb.is_empty() {
/// Everything one Content-Aware Fill needs, worked out from the document and the command's
/// params. The Content-Aware Fill workspace runs exactly this for its preview, so what it shows
/// is what OK and Apply write.
pub struct CafPlan {
pub layer: LayerId,
/// The rectangle worked on: the sampling window, and any sampling-brush strokes or custom
/// area beyond it.
pub rect: Rect,
/// Photoshop's sampling window around the fill area (`content_aware::sampling_window`).
pub window: Rect,
pub fmt: PixelFormat,
/// The target's pixels over `rect`, and what is sampled (the same, or with Sample All Layers
/// the visible composite in the target's encoding).
pub layer_px: Vec<f32>,
pub img: Vec<f32>,
/// The fill area (selection coverage, after the workspace's lasso edits) over `rect`.
pub cover: Vec<f32>,
pub hole: Vec<bool>,
/// What may be copied from: the sampling option's area before the brush strokes, and after.
pub base: Vec<bool>,
pub source: Vec<bool>,
pub opts: photocraft_algo::content_aware::FillOptions,
pub output: String,
}
/// The workspace's lasso edits of the fill area: `{"op": "new|add|subtract|intersect", "points":
/// [[x, y], ...]}` or `{"op": "expand|contract", "by": px}`, applied in order.
#[derive(Clone, Debug, PartialEq)]
pub enum FillEdit {
Lasso { op: SelOp, points: Vec<[f64; 2]> },
Grow(i32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SelOp {
New,
Add,
Subtract,
Intersect,
}
fn points_param(v: &Value, cmd: &str, what: &str) -> Result<Vec<[f64; 2]>> {
let a = v.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, format!("{what} needs `points` [[x, y], ...]")))?;
a.iter()
.map(|p| {
let x = p.get(0).and_then(Value::as_f64).filter(|v| v.is_finite() && v.abs() < 1e7);
let y = p.get(1).and_then(Value::as_f64).filter(|v| v.is_finite() && v.abs() < 1e7);
match (x, y) {
(Some(x), Some(y)) => Ok([x, y]),
_ => Err(bad(cmd, format!("{what} points must be pairs of finite numbers"))),
}
})
.collect()
}
fn fill_edits(p: &Value, cmd: &str) -> Result<Vec<FillEdit>> {
let Some(a) = p.get("fillEdits") else { return Ok(Vec::new()) };
let a = a.as_array().ok_or_else(|| bad(cmd, "`fillEdits` must be an array"))?;
a.iter()
.map(|e| {
let op = e.get("op").and_then(Value::as_str).unwrap_or("");
Ok(match op {
"expand" | "contract" => {
let by = e
.get("by")
.and_then(Value::as_f64)
.filter(|v| v.is_finite() && (0.0..=1000.0).contains(v))
.ok_or_else(|| bad(cmd, "`by` must be 0..1000 px"))?;
FillEdit::Grow(if op == "expand" { by.round() as i32 } else { -(by.round() as i32) })
}
"new" | "add" | "subtract" | "intersect" => FillEdit::Lasso {
op: match op {
"new" => SelOp::New,
"add" => SelOp::Add,
"subtract" => SelOp::Subtract,
_ => SelOp::Intersect,
},
points: points_param(e, cmd, "a lasso edit")?,
},
o => return Err(bad(cmd, format!("unknown fill edit `{o}` (new|add|subtract|intersect|expand|contract)"))),
})
})
.collect()
}
fn sampling_strokes(p: &Value, cmd: &str) -> Result<Vec<photocraft_algo::content_aware::SamplingStroke>> {
let Some(a) = p.get("samplingStrokes") else { return Ok(Vec::new()) };
let a = a.as_array().ok_or_else(|| bad(cmd, "`samplingStrokes` must be an array"))?;
a.iter()
.map(|e| {
let add = match e.get("mode").and_then(Value::as_str).unwrap_or("add") {
"add" => true,
"subtract" => false,
o => return Err(bad(cmd, format!("unknown stroke mode `{o}` (add|subtract)"))),
};
let size = e
.get("size")
.and_then(Value::as_f64)
.filter(|v| v.is_finite() && (1.0..=5000.0).contains(v))
.ok_or_else(|| bad(cmd, "a stroke's `size` must be 1..5000 px"))?;
Ok(photocraft_algo::content_aware::SamplingStroke { add, size, points: points_param(e, cmd, "a sampling stroke")? })
})
.collect()
}
/// Work out a Content-Aware Fill (see [`CafPlan`]) on layer `id` of `doc`.
pub fn content_aware_plan(doc: &Document, id: LayerId, p: &Value, cmd: &str) -> Result<CafPlan> {
use photocraft_algo::content_aware::{FillOptions, auto_sampling, color_level, grow_mask, paint_strokes, polygon_mask, rotation_level};
let canvas = doc.bounds();
let sel = doc.selection.as_ref().ok_or_else(|| bad(cmd, "no selection"))?;
let sb = sel.content_bounds().intersect(&canvas);
if sb.is_empty() {
return Err(bad(cmd, "the selection is outside the canvas"));
}
let edits = fill_edits(p, cmd)?;
let strokes = sampling_strokes(p, cmd)?;
// The fill area: the selection, then the lasso edits, over a rectangle that holds them all.
let grow: i32 = edits.iter().map(|e| if let FillEdit::Grow(b) = e { b.max(&0) } else { &0 }).sum();
let reach = edits.iter().fold(sb, |r, e| match e {
FillEdit::Lasso { points, .. } => {
points.iter().fold(r, |r, q| r.union(&Rect::new(q[0].floor() as i32, q[1].floor() as i32, q[0].floor() as i32 + 1, q[1].floor() as i32 + 1)))
}
FillEdit::Grow(_) => r,
});
let er = reach.inflate(grow.saturating_add(1)).intersect(&canvas);
let (ew, eh) = crate::fill_cmds::window_size(er)?;
let mut ecover = vec![0.0f32; ew * eh];
for (i, c) in ecover.iter_mut().enumerate() {
*c = sel.sample_channel(er.x0 + (i % ew) as i32, er.y0 + (i / ew) as i32, 0);
}
let mut ehole: Vec<bool> = ecover.iter().map(|c| *c > 0.0).collect();
for e in &edits {
match e {
FillEdit::Lasso { op, points } => {
let m = polygon_mask(er, points);
for (h, v) in ehole.iter_mut().zip(&m) {
*h = match op {
SelOp::New => *v,
SelOp::Add => *h || *v,
SelOp::Subtract => *h && !*v,
SelOp::Intersect => *h && *v,
};
}
}
FillEdit::Grow(by) => ehole = grow_mask(&ehole, ew, eh, *by),
}
}
let hb = ehole.iter().enumerate().filter(|(_, h)| **h).fold(Rect::EMPTY, |r, (i, _)| {
let (x, y) = (er.x0 + (i % ew) as i32, er.y0 + (i / ew) as i32);
r.union(&Rect::new(x, y, x + 1, y + 1))
});
if hb.is_empty() {
return Err(bad(cmd, "the fill area is empty"));
}
let sampling = str_or(p, "sampling", "auto").to_string();
let custom_mask: Option<Surface> = p.get("channel").and_then(|v| channel_mask(&doc, v)).cloned();
let custom_mask: Option<&Surface> = p.get("channel").and_then(|v| channel_mask(doc, v));
let custom_rect = if p.get("area").is_some() { Some(rect_param(p, "area", cmd)?) } else { None };
// Photoshop's window (`content_aware::sampling_window`) for Auto and Rectangular; `margin`
// asks for a rectangle that far around the selection instead.
// asks for a rectangle that far around the fill area instead.
let window = match sampling.as_str() {
"auto" => crate::fill_cmds::sampling_window(hb, doc.bounds()),
"auto" => crate::fill_cmds::sampling_window(hb, canvas),
"rectangular" => match int(p, "margin") {
Some(m) => hb.inflate(m.clamp(0, 100_000) as i32),
None => crate::fill_cmds::sampling_window(hb, doc.bounds()),
None => crate::fill_cmds::sampling_window(hb, canvas),
},
"custom" => {
let r = match (custom_rect, custom_mask.as_ref()) {
let r = match (custom_rect, custom_mask) {
(Some(r), _) => r,
(None, Some(m)) => m.content_bounds(),
(None, None) => return Err(bad(cmd, "custom sampling needs `area` [x,y,w,h] or `channel` (alpha channel index or name)")),
// The workspace's Custom: only what the Sampling Brush adds.
(None, None) if strokes.iter().any(|s| s.add) => hb,
(None, None) => return Err(bad(cmd, "custom sampling needs `area` [x,y,w,h], `channel` (alpha channel index or name) or `samplingStrokes`")),
};
r.union(&hb).inflate(4)
}
other => return Err(bad(cmd, format!("unknown sampling `{other}` (auto|rectangular|custom)"))),
}
.intersect(&doc.bounds());
.intersect(&canvas);
// Strokes that add to the sampling area may reach past the window.
let rect = strokes.iter().filter(|s| s.add).fold(window, |r, s| r.union(&s.bounds())).intersect(&canvas);
let opts = FillOptions {
color_adaptation: color_level(str_or(p, "colorAdaptation", "default")),
rotations: rotation_level(str_or(p, "rotationAdaptation", "none")),
@@ -384,48 +538,100 @@ fn content_aware_fill_as(s: &mut Session, p: &Value, cmd: &'static str, label: &
let fmt = surf.format();
let n = fmt.channels();
// Refuse a window too large to read before allocating for it, whatever the sampling (#963).
let (w, h) = crate::fill_cmds::window_size(window)?;
// A background job when started with `Session::start` (#210): reading the window and the
// PatchMatch fill run on a worker against the document snapshot, cancellable per row band.
let (w, h) = crate::fill_cmds::window_size(rect)?;
let layer_px = surf.read_region(rect);
let img = if bool_or(p, "sampleAllLayers", false) {
crate::retouch_cmds::composite_region(doc, Some(id), crate::retouch_cmds::SampleLayers::All, rect, fmt).data
} else {
layer_px.clone()
};
let mut cover = vec![0.0f32; w * h];
let mut hole = vec![false; w * h];
for y in 0..h {
for x in 0..w {
let (dx, dy) = (rect.x0 + x as i32, rect.y0 + y as i32);
if er.contains(dx, dy) {
let j = (dy - er.y0) as usize * ew + (dx - er.x0) as usize;
hole[y * w + x] = ehole[j];
// Pixels the lasso added are filled outright; selected ones by their coverage.
cover[y * w + x] = if ehole[j] { if ecover[j] > 0.0 { ecover[j] } else { 1.0 } } else { 0.0 };
}
}
}
// The sampling option's area: Auto keeps what looks like the surroundings within the window,
// Rectangular the whole window, Custom the given area (or nothing, for the brush to add to).
let mut base = vec![false; w * h];
match sampling.as_str() {
"auto" | "rectangular" => {
let (ww, wh) = (window.width() as usize, window.height() as usize);
let (ox, oy) = ((window.x0 - rect.x0) as usize, (window.y0 - rect.y0) as usize);
let area: Vec<bool> = if sampling == "auto" {
let mut wimg = vec![0.0f32; ww * wh * n];
let mut whole = vec![false; ww * wh];
for y in 0..wh {
let s = ((oy + y) * w + ox) * n;
wimg[y * ww * n..(y + 1) * ww * n].copy_from_slice(&img[s..s + ww * n]);
whole[y * ww..(y + 1) * ww].copy_from_slice(&hole[(oy + y) * w + ox..(oy + y) * w + ox + ww]);
}
auto_sampling(ww, wh, n, &wimg, &whole)
} else {
vec![true; ww * wh]
};
for y in 0..wh {
base[(oy + y) * w + ox..(oy + y) * w + ox + ww].copy_from_slice(&area[y * ww..(y + 1) * ww]);
}
}
_ => {
for y in 0..h {
for x in 0..w {
let (dx, dy) = (rect.x0 + x as i32, rect.y0 + y as i32);
base[y * w + x] = match (custom_rect, custom_mask) {
(Some(r), _) => r.contains(dx, dy),
(None, Some(m)) => m.sample_channel(dx, dy, 0) > 0.5,
_ => false,
};
}
}
}
}
base.iter_mut().zip(&hole).for_each(|(b, h)| *b &= !*h);
let mut source = base.clone();
paint_strokes(&mut source, rect, &strokes);
source.iter_mut().zip(&hole).for_each(|(s, h)| *s &= !*h);
Ok(CafPlan { layer: id, rect, window, fmt, layer_px, img, cover, hole, base, source, opts, output })
}
fn content_aware_fill_as(s: &mut Session, p: &Value, cmd: &'static str, label: &'static str) -> Result<Value> {
use photocraft_algo::content_aware::fill_with;
// The workspace names the layer it opened on: after an Apply to a new layer that one is
// active, but the fill still samples the original.
let id = match p.get("layer").and_then(Value::as_u64) {
Some(l) => pixel_layer_id(s, LayerId(l)),
None => pixel_layer(s),
}
.map_err(EngineError::Other)?;
let st = s.active().ok_or(EngineError::NoDocument)?;
let doc = st.doc.clone();
// Check everything (and refuse absurd windows, #963) before the job starts.
let plan = content_aware_plan(&doc, id, p, cmd)?;
// A background job when started with `Session::start` (#210): the PatchMatch fill runs on a
// worker against the plan, cancellable per row band.
crate::jobs::run(
s,
label,
true,
move |ctx| {
ctx.progress(0.0, label);
let surf = doc.layer(id).and_then(|l| l.surface()).ok_or(EngineError::NoLayer(id))?;
let img = surf.read_region(window);
let mut cover = vec![0.0f32; w * h];
let mut hole = vec![false; w * h];
let mut source = vec![true; w * h];
for y in 0..h {
if y % 64 == 0 {
ctx.check()?;
}
for x in 0..w {
let (dx, dy) = (window.x0 + x as i32, window.y0 + y as i32);
let i = y * w + x;
cover[i] = sel.sample_channel(dx, dy, 0);
hole[i] = cover[i] > 0.0;
if sampling == "custom" {
source[i] = match (custom_rect, custom_mask.as_ref()) {
(Some(r), _) => r.contains(dx, dy),
(None, Some(m)) => m.sample_channel(dx, dy, 0) > 0.5,
_ => true,
};
}
}
}
ctx.check()?;
// Auto: only what looks like the selection's surroundings.
if sampling == "auto" {
source = auto_sampling(w, h, n, &img, &hole);
}
ctx.check()?;
let filled = ctx.stage(0.05, 1.0, label, |ctl| fill_with(w, h, n, &img, &hole, &source, &opts, ctl)).map_err(|_| EngineError::Cancelled)?;
Ok((img, cover, hole, filled))
let (w, h) = (plan.rect.width() as usize, plan.rect.height() as usize);
let n = plan.fmt.channels();
let filled = ctx
.stage(0.05, 1.0, label, |ctl| fill_with(w, h, n, &plan.img, &plan.hole, &plan.source, &plan.opts, ctl))
.map_err(|_| EngineError::Cancelled)?;
Ok((plan, filled))
},
move |s, (plan, filled)| {
apply_content_aware_fill(s, label, plan.layer, &plan.output, plan.fmt, plan.rect, &plan.layer_px, &plan.cover, &plan.hole, &filled)
},
move |s, (img, cover, hole, filled)| apply_content_aware_fill(s, label, id, &output, fmt, window, &img, &cover, &hole, &filled),
)
}
@@ -983,7 +1189,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Content-Aware Fill…",
["Edit"],
None,
r##"{"sampling":"auto|rectangular|custom","margin":px?,"area":[x,y,w,h]?,"channel":index|name?,"colorAdaptation":"default|none|high|veryHigh","rotationAdaptation":"none|low|medium|high|full","scale":bool=false,"mirror":bool=false,"output":"current|new|duplicate","seed":u64=1}"##,
r##"{"sampling":"auto|rectangular|custom","margin":px?,"area":[x,y,w,h]?,"channel":index|name?,"samplingStrokes":[{"mode":"add|subtract","size":px,"points":[[x,y],…]}]? (the workspace's Sampling Brush, after the sampling option's area; added strokes may lie outside the window),"fillEdits":[{"op":"new|add|subtract|intersect","points":[[x,y],…]}|{"op":"expand|contract","by":px}]? (the workspace's lasso edits of the fill area, in order),"sampleAllLayers":bool=false,"colorAdaptation":"default|none|high|veryHigh","rotationAdaptation":"none|low|medium|high|full","scale":bool=false,"mirror":bool=false,"output":"current|new|duplicate","layer":id?=active (the layer filled and sampled),"seed":u64=1}"##,
can_caf,
content_aware_fill
),
+46
View File
@@ -234,6 +234,52 @@ fn content_aware_fill_samples_photoshops_window() {
}
}
#[test]
fn content_aware_fill_takes_the_workspace_edits() {
// The plan the workspace previews: lasso edits change the fill area (and the window follows),
// sampling-brush strokes change what may be copied, also outside the window.
let mut s = Session::new();
s.execute("file.new", json!({"width": 600, "height": 400, "background": "white"})).unwrap();
s.execute("select.rect", json!({"x": 275, "y": 175, "width": 50, "height": 50})).unwrap();
let st = s.active().unwrap();
let (doc, id) = (st.doc.clone(), st.active_layer.unwrap());
let plan = |p: Value| content_aware_plan(&doc, id, &p, "edit.contentAwareFill");
let count = |m: &[bool]| m.iter().filter(|v| **v).count();
let base = plan(json!({"sampling": "rectangular"})).unwrap();
assert_eq!((base.window, count(&base.hole)), (Rect::new(200, 100, 400, 300), 2500));
assert_eq!(count(&base.source), 200 * 200 - 2500);
// A lasso square added beside the selection: a bigger fill area, a window around both.
let p = plan(json!({"sampling": "rectangular", "fillEdits": [{"op": "add", "points": [[340, 175], [390, 175], [390, 225], [340, 225]]}]})).unwrap();
assert_eq!(count(&p.hole), 5000);
assert!(p.window.width() > 200 && p.window.contains_rect(&Rect::new(275, 175, 390, 225)), "{:?}", p.window);
// Expand by 2 grows the 50 px square to about 54 px (rounded corners).
let p = plan(json!({"sampling": "rectangular", "fillEdits": [{"op": "expand", "by": 2}]})).unwrap();
assert!((54 * 54 - 20..=54 * 54).contains(&count(&p.hole)), "{}", count(&p.hole));
// Subtracting it all leaves nothing to fill.
assert!(plan(json!({"fillEdits": [{"op": "subtract", "points": [[0, 0], [600, 0], [600, 400], [0, 400]]}]})).is_err());
// A subtracting stroke takes a band out of the sampling area; an adding one far away reaches
// past the window, and the plan's rectangle grows to hold it.
let p = plan(json!({"sampling": "rectangular", "samplingStrokes": [{"mode": "subtract", "size": 10, "points": [[210, 110], [390, 110]]}]})).unwrap();
assert!((count(&base.source) - count(&p.source)).abs_diff(10 * 190) < 60);
let p = plan(json!({"sampling": "rectangular", "samplingStrokes": [{"mode": "add", "size": 20, "points": [[540, 350], [580, 350]]}]})).unwrap();
assert!(p.rect.contains(560, 350) && !p.window.contains(560, 350));
assert!(count(&p.source) > count(&base.source) + 600);
// Custom from strokes alone; bad edits are errors.
let p = plan(json!({"sampling": "custom", "samplingStrokes": [{"mode": "add", "size": 20, "points": [[100, 100], [160, 100]]}]})).unwrap();
assert!((1450..1600).contains(&count(&p.source)), "a 20 px brush along 60 px: {}", count(&p.source));
for bad in [
json!({"samplingStrokes": [{"mode": "paint", "size": 10, "points": [[0, 0]]}]}),
json!({"samplingStrokes": [{"size": 0, "points": [[0, 0]]}]}),
json!({"fillEdits": [{"op": "add", "points": [[0, "x"]]}]}),
json!({"fillEdits": [{"op": "expand", "by": -3}]}),
] {
assert!(plan(bad.clone()).is_err(), "{bad}");
}
// The command fills with the same plan; `layer` names the layer even when another is active.
let r = s.execute("edit.contentAwareFill", json!({"sampling": "rectangular", "fillEdits": [{"op": "expand", "by": 2}], "layer": id.0})).unwrap();
assert!(r["filled"].as_u64().unwrap() > 2800);
}
#[test]
fn content_aware_fill_outputs_and_sampling() {
let mut s = blob_session(8);
+2 -2
View File
@@ -137,7 +137,7 @@ fn damage_json(r: Rect) -> Value {
/// Which pixels Clone Stamp / Healing Brush sample.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SampleLayers {
pub(crate) enum SampleLayers {
Current,
CurrentAndBelow,
All,
@@ -154,7 +154,7 @@ fn sample_layers(p: &Value, cmd: &str) -> Result<SampleLayers> {
/// Composite pixels of `rect` in `fmt` (native channels): the whole document, or the target layer
/// and everything below it.
fn composite_region(pre: &Document, id: Option<LayerId>, which: SampleLayers, rect: Rect, fmt: PixelFormat) -> Region {
pub(crate) fn composite_region(pre: &Document, id: Option<LayerId>, which: SampleLayers, rect: Rect, fmt: PixelFormat) -> Region {
let mut doc = pre.clone();
if which == SampleLayers::CurrentAndBelow
&& let Some(id) = id
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -21,6 +21,8 @@ use crate::canvas::{ToolEvent, ViewXform};
#[derive(Default)]
pub struct Distort {
pub liquify: Option<crate::liquify_ui::LiquifyDialog>,
/// Edit › Content-Aware Fill… (caf_ui).
pub caf: Option<crate::caf_ui::CafWorkspace>,
pub puppet: Option<crate::puppet_ui::PuppetSession>,
pub perspective: Option<crate::perspective_ui::PerspSession>,
/// Filter Gallery dialog (gallery_ui).
@@ -29,13 +31,14 @@ pub struct Distort {
impl Distort {
pub fn active(&self) -> bool {
self.liquify.is_some() || self.puppet.is_some() || self.perspective.is_some()
self.liquify.is_some() || self.caf.is_some() || self.puppet.is_some() || self.perspective.is_some()
}
/// Short description for `ui.inspect`.
pub fn describe(&self) -> Value {
json!({
"liquify": self.liquify.as_ref().map(|d| d.describe()),
"contentAwareFill": self.caf.as_ref().map(|d| d.describe()),
"puppet": self.puppet.as_ref().map(|p| p.describe()),
"perspective": self.perspective.as_ref().map(|p| p.describe()),
"gallery": self.gallery.as_ref().map(|g| g.describe()),
@@ -106,6 +109,7 @@ pub fn menu(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: &Val
"filter.filterGallery" if ui.is_some() && app.distort.gallery.is_some() => Some(crate::gallery_ui::control(app, ui.unwrap_or(&Value::Null))),
"filter.liquify" if empty => Some(crate::liquify_ui::open(app, ctx).map(|_| json!({"liquify": app.distort.describe()["liquify"]}))),
"filter.liquify" if ui.is_some() && app.distort.liquify.is_some() => Some(crate::liquify_ui::control(app, ui.unwrap_or(&Value::Null))),
"edit.contentAwareFill" if ui.is_some() && app.distort.caf.is_some() => Some(crate::caf_ui::control(app, ui.unwrap_or(&Value::Null))),
"edit.puppetWarp" | "layer.smartObjects.puppetWarp" if empty => {
Some(crate::puppet_ui::begin(app, ctx, id).map(|_| json!({"puppet": app.distort.describe()["puppet"]})))
}
@@ -124,6 +128,10 @@ pub fn menu(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: &Val
/// Pointer events (document coordinates) while a mode is active. True when consumed.
pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) -> bool {
if app.distort.caf.is_some() {
crate::caf_ui::pointer(app, ev, mods);
return true;
}
if app.distort.liquify.is_some() {
crate::liquify_ui::pointer(app, ev, mods);
return true;
@@ -153,6 +161,14 @@ pub fn keys(app: &mut PhotocraftApp, ctx: &egui::Context) -> bool {
}
return true;
}
if app.distort.caf.is_some() {
if enter(ctx) {
crate::caf_ui::commit(app);
} else if esc(ctx) {
crate::caf_ui::cancel(app);
}
return true;
}
if app.distort.liquify.is_some() {
if enter(ctx) {
crate::liquify_ui::commit(app);
@@ -205,8 +221,11 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> bool {
false
}
/// Full-window dialogs (Liquify, Filter Gallery).
/// Full-window dialogs (Liquify, Content-Aware Fill, Filter Gallery).
pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
if app.distort.caf.is_some() {
crate::caf_ui::show(app, ctx);
}
if app.distort.gallery.is_some() {
crate::gallery_ui::show(app, ctx);
}
+21
View File
@@ -2293,3 +2293,24 @@ fontWeight Italic Kurzíva
Cycle Orientation Přepnout orientaci
Set the overlay options for the Crop tool Nastavit volby překryvu pro nástroj Oříznutí
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt+klik převzalo pracovní prostředí, které pomocí Alt+tažení přesouvá okna. Nastavte jeho klávesu pro přesun oken na Super, nebo zamkněte Alt v nabídce Okno › Modifikační klávesy.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Nástroj Štětec vzorkování
Sampling Area Overlay Překrytí oblasti vzorkování
Show Sampling Area Zobrazit oblast vzorkování
Indicates Označuje
Sampling Area Oblast vzorkování
Excluded Area Vyloučená oblast
Sampling Area Options Volby oblasti vzorkování
Rectangular Obdélníková
Fill Settings Nastavení výplně
Rotation Adaptation Přizpůsobení otočení
Very High Velmi vysoká
High Vysoká
Low Nízká
Full Úplná
Mirror Zrcadlit
Output Settings Nastavení výstupu
Output To Výstup do
New Layer Nová vrstva
Duplicate Layer Duplikovat vrstvu
Contract Zúžit
Can't render this file because it contains an unexpected character in line 6 and column 47.
+21
View File
@@ -2331,3 +2331,24 @@ fontWeight Italic Kursiv
Cycle Orientation Ausrichtung wechseln
Set the overlay options for the Crop tool Überlagerungsoptionen für das Freistellungswerkzeug festlegen
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Der Alt-Klick ging an die Arbeitsumgebung, die Fenster mit Alt-Ziehen verschiebt. Stelle dort die Taste zum Verschieben von Fenstern auf Super oder raste Alt unter Fenster › Modifikatortasten ein.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Aufnahmepinsel-Werkzeug
Sampling Area Overlay Überlagerung des Aufnahmebereichs
Show Sampling Area Aufnahmebereich einblenden
Indicates Kennzeichnet
Sampling Area Aufnahmebereich
Excluded Area Ausgeschlossener Bereich
Sampling Area Options Optionen für Aufnahmebereich
Rectangular Rechteckig
Fill Settings Füllungseinstellungen
Rotation Adaptation Drehungsanpassung
Very High Sehr hoch
High Hoch
Low Niedrig
Full Voll
Mirror Spiegeln
Output Settings Ausgabeeinstellungen
Output To Ausgabe in
New Layer Neue Ebene
Duplicate Layer Ebene duplizieren
Contract Verkleinern
Can't render this file because it contains an unexpected character in line 6 and column 47.
+16
View File
@@ -3402,3 +3402,19 @@ fontWeight Thin Πολύ λεπτά
Cycle Orientation Εναλλαγή προσανατολισμού
Set the overlay options for the Crop tool Ορισμός επιλογών επικάλυψης για το εργαλείο Περικοπή
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Το Alt-κλικ πήγε στο περιβάλλον εργασίας, που μετακινεί παράθυρα με Alt-σύρσιμο. Ορίστε το πλήκτρο μετακίνησης παραθύρων του σε Super ή κλειδώστε το Alt στο Παράθυρο › Πλήκτρα τροποποίησης.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Εργαλείο πινέλου δειγματοληψίας
Sampling Area Overlay Επικάλυψη περιοχής δειγματοληψίας
Show Sampling Area Εμφάνιση περιοχής δειγματοληψίας
Indicates Υποδεικνύει
Sampling Area Περιοχή δειγματοληψίας
Excluded Area Εξαιρούμενη περιοχή
Sampling Area Options Επιλογές περιοχής δειγματοληψίας
Rectangular Ορθογώνια
Fill Settings Ρυθμίσεις γεμίσματος
Rotation Adaptation Προσαρμογή περιστροφής
Very High Πολύ υψηλή
Full Πλήρης
Mirror Κατοπτρισμός
Output Settings Ρυθμίσεις εξόδου
Output To Έξοδος σε
Can't render this file because it contains an unexpected character in line 965 and column 18.
+21
View File
@@ -2361,3 +2361,24 @@ fontWeight Italic Cursiva
Cycle Orientation Alternar orientación
Set the overlay options for the Crop tool Definir las opciones de superposición de la herramienta Recortar
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. El Alt+clic lo recibió el escritorio, que mueve ventanas con Alt+arrastrar. Cambia su tecla para mover ventanas a Super o bloquea Alt en Ventana › Teclas modificadoras.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Herramienta Pincel de muestreo
Sampling Area Overlay Superposición del área de muestreo
Show Sampling Area Mostrar área de muestreo
Indicates Indica
Sampling Area Área de muestreo
Excluded Area Área excluida
Sampling Area Options Opciones del área de muestreo
Rectangular Rectangular
Fill Settings Ajustes de relleno
Rotation Adaptation Adaptación de rotación
Very High Muy alta
High Alta
Low Baja
Full Completa
Mirror Reflejar
Output Settings Ajustes de salida
Output To Salida a
New Layer Capa nueva
Duplicate Layer Duplicar capa
Contract Contraer
Can't render this file because it contains an unexpected character in line 4 and column 47.
+19
View File
@@ -2714,3 +2714,22 @@ fontWeight Italic Italique
Cycle Orientation Changer d’orientation
Set the overlay options for the Crop tool Définir les options de superposition de l’outil Recadrage
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. L’Alt-clic a été intercepté par le bureau, qui déplace les fenêtres avec Alt-glisser. Réglez sa touche de déplacement des fenêtres sur Super, ou verrouillez Alt dans Fenêtre › Touches de modification.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Outil Pinceau d’échantillonnage
Sampling Area Overlay Incrustation de la zone d’échantillonnage
Show Sampling Area Afficher la zone d’échantillonnage
Indicates Indique
Sampling Area Zone d’échantillonnage
Excluded Area Zone exclue
Sampling Area Options Options de la zone d’échantillonnage
Rectangular Rectangulaire
Fill Settings Paramètres de remplissage
Rotation Adaptation Adaptation de la rotation
Very High Très élevée
Full Totale
Mirror Miroir
Output Settings Paramètres de sortie
Output To Sortie vers
New Layer Nouveau calque
Duplicate Layer Dupliquer le calque
Contract Contracter
Can't render this file because it contains an unexpected character in line 6 and column 47.
+19
View File
@@ -2701,3 +2701,22 @@ fontWeight Italic Miring
Cycle Orientation Ganti Orientasi
Set the overlay options for the Crop tool Atur opsi overlay untuk alat Crop
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt-klik diambil oleh desktop, yang memindahkan jendela dengan Alt-seret. Atur tombol pemindah jendelanya ke Super, atau kunci Alt di Jendela › Tombol Pengubah.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Alat Kuas Pengambilan Sampel
Sampling Area Overlay Hamparan Area Pengambilan Sampel
Show Sampling Area Tampilkan Area Pengambilan Sampel
Indicates Menandai
Sampling Area Area Pengambilan Sampel
Excluded Area Area yang Dikecualikan
Sampling Area Options Opsi Area Pengambilan Sampel
Rectangular Persegi Panjang
Fill Settings Pengaturan Isian
Rotation Adaptation Adaptasi Rotasi
Very High Sangat Tinggi
Full Penuh
Mirror Cermin
Output Settings Pengaturan Keluaran
Output To Keluaran Ke
New Layer Layer Baru
Duplicate Layer Duplikat Layer
Contract Ciutkan
Can't render this file because it contains an unexpected character in line 4 and column 47.
+21
View File
@@ -2362,3 +2362,24 @@ fontWeight Italic Corsivo
Cycle Orientation Cambia orientamento
Set the overlay options for the Crop tool Imposta le opzioni di sovrapposizione dello strumento Taglierina
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. L’Alt-click è andato al desktop, che sposta le finestre con Alt-trascina. Imposta il suo tasto per spostare le finestre su Super, oppure blocca Alt in Finestra › Tasti modificatori.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Strumento Pennello di campionamento
Sampling Area Overlay Sovrapposizione area di campionamento
Show Sampling Area Mostra area di campionamento
Indicates Indica
Sampling Area Area di campionamento
Excluded Area Area esclusa
Sampling Area Options Opzioni area di campionamento
Rectangular Rettangolare
Fill Settings Impostazioni di riempimento
Rotation Adaptation Adattamento rotazione
Very High Molto alta
High Alta
Low Bassa
Full Completa
Mirror Rifletti
Output Settings Impostazioni di output
Output To Output in
New Layer Nuovo livello
Duplicate Layer Duplica livello
Contract Contrai
1 # Italian (it) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
+16
View File
@@ -3317,3 +3317,19 @@ fontWeight Italic 斜体
Cycle Orientation 方向を切り替え
Set the overlay options for the Crop tool 切り抜きツールのオーバーレイオプションを設定
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt+クリックはデスクトップに取られました(Alt+ドラッグでウィンドウを移動する設定です)。デスクトップのウィンドウ移動キーを Super に変更するか、ウィンドウ › 修飾キーで Alt を固定してください。
# Edit › Content-Aware Fill workspace
Sampling Brush Tool サンプリングブラシツール
Sampling Area Overlay サンプリング領域のオーバーレイ
Show Sampling Area サンプリング領域を表示
Indicates 表示対象
Sampling Area サンプリング領域
Excluded Area 除外された領域
Sampling Area Options サンプリング領域オプション
Rectangular 長方形
Fill Settings 塗りつぶし設定
Rotation Adaptation 回転への適応
Very High 非常に高い
Full フル
Mirror ミラー
Output Settings 出力設定
Output To 出力先
Can't render this file because it contains an unexpected character in line 6 and column 47.
+16
View File
@@ -2871,3 +2871,19 @@ fontWeight Italic 기울임
Cycle Orientation 방향 순환
Set the overlay options for the Crop tool 자르기 도구의 오버레이 옵션 설정
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt 클릭이 데스크톱으로 전달되었습니다. 데스크톱이 Alt 드래그로 창을 이동합니다. 창 이동 키를 Super로 바꾸거나 창 › 보조 키에서 Alt를 고정하세요.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool 샘플링 브러시 도구
Sampling Area Overlay 샘플링 영역 오버레이
Show Sampling Area 샘플링 영역 표시
Indicates 표시 대상
Sampling Area 샘플링 영역
Excluded Area 제외 영역
Sampling Area Options 샘플링 영역 옵션
Rectangular 사각형
Fill Settings 채우기 설정
Rotation Adaptation 회전 적응
Very High 매우 높음
Full 전체
Mirror 미러
Output Settings 출력 설정
Output To 출력 위치
1 # Korean (ko) catalog. UTF-8: context<TAB>English source<TAB>translation.
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
+16
View File
@@ -3477,3 +3477,19 @@ cameraRaw Whites Witten
Cycle Orientation Oriëntatie wisselen
Set the overlay options for the Crop tool Overlay-opties voor het gereedschap Uitsnijden instellen
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. De Alt-klik ging naar het bureaublad, dat vensters verplaatst met Alt-slepen. Stel daar de toets voor het verplaatsen van vensters in op Super, of vergrendel Alt via Venster › Wijzigingstoetsen.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Gereedschap Monsterpenseel
Sampling Area Overlay Overlay van monstergebied
Show Sampling Area Monstergebied tonen
Indicates Geeft aan
Sampling Area Monstergebied
Excluded Area Uitgesloten gebied
Sampling Area Options Opties voor monstergebied
Rectangular Rechthoekig
Fill Settings Vulinstellingen
Rotation Adaptation Rotatieaanpassing
Very High Zeer hoog
Full Volledig
Mirror Spiegelen
Output Settings Uitvoerinstellingen
Output To Uitvoer naar
Can't render this file because it contains an unexpected character in line 8 and column 34.
+16
View File
@@ -3315,3 +3315,19 @@ cameraRaw Roughness Chropowatość
Cycle Orientation Przełącz orientację
Set the overlay options for the Crop tool Ustaw opcje nakładki narzędzia Kadrowanie
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt+kliknięcie przejęło środowisko pulpitu, które przesuwa okna przez Alt+przeciąganie. Ustaw w nim klawisz przesuwania okien na Super lub zablokuj Alt w Okno › Klawisze modyfikujące.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Narzędzie Pędzel próbkowania
Sampling Area Overlay Nakładka obszaru próbkowania
Show Sampling Area Pokaż obszar próbkowania
Indicates Wskazuje
Sampling Area Obszar próbkowania
Excluded Area Obszar wykluczony
Sampling Area Options Opcje obszaru próbkowania
Rectangular Prostokątny
Fill Settings Ustawienia wypełnienia
Rotation Adaptation Dopasowanie obrotu
Very High Bardzo wysoka
Full Pełna
Mirror Odbicie lustrzane
Output Settings Ustawienia wyjścia
Output To Wyjście do
Can't render this file because it contains an unexpected character in line 6 and column 47.
+19
View File
@@ -2721,3 +2721,22 @@ fontWeight Italic Itálico
Cycle Orientation Alternar orientação
Set the overlay options for the Crop tool Definir as opções de sobreposição da ferramenta Corte demarcado
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. O Alt-clique foi para o desktop, que move janelas com Alt-arrastar. Mude a tecla de mover janelas dele para Super ou trave o Alt em Janela › Teclas modificadoras.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Ferramenta Pincel de amostragem
Sampling Area Overlay Sobreposição da área de amostragem
Show Sampling Area Mostrar área de amostragem
Indicates Indica
Sampling Area Área de amostragem
Excluded Area Área excluída
Sampling Area Options Opções da área de amostragem
Rectangular Retangular
Fill Settings Configurações de preenchimento
Rotation Adaptation Adaptação de rotação
Very High Muito alta
Full Completa
Mirror Espelhar
Output Settings Configurações de saída
Output To Saída para
New Layer Nova camada
Duplicate Layer Duplicar camada
Contract Contrair
Can't render this file because it contains an unexpected character in line 6 and column 47.
+23
View File
@@ -2567,3 +2567,26 @@ fontWeight Italic Курсив
Cycle Orientation Сменить ориентацию
Set the overlay options for the Crop tool Задать параметры перекрытия для инструмента «Рамка»
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt+щелчок перехватило окружение рабочего стола: оно перемещает окна через Alt+перетаскивание. Назначьте для перемещения окон клавишу Super или зафиксируйте Alt в меню Окно › Клавиши-модификаторы.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Кисть области выборки
Sampling Area Overlay Наложение области выборки
Show Sampling Area Показать область выборки
Indicates Обозначает
Sampling Area Область выборки
Excluded Area Исключённая область
Sampling Area Options Параметры области выборки
Rectangular Прямоугольная
Fill Settings Параметры заливки
Rotation Adaptation Адаптация поворота
Very High Очень высокая
Full Полная
Mirror Отражение
Output Settings Параметры вывода
Output To Вывод в
New Layer Новый слой
Duplicate Layer Дубликат слоя
Contract Сжать
New selection Новое выделение
Add to selection ({key}) Добавить к выделению ({key})
Subtract from selection ({key}) Вычесть из выделения ({key})
Intersect with selection ({key}) Пересечь с выделением ({key})
Can't render this file because it contains an unexpected character in line 6 and column 47.
+16
View File
@@ -3402,3 +3402,19 @@ cameraRaw Masking Маскування
Cycle Orientation Змінити орієнтацію
Set the overlay options for the Crop tool Задати параметри накладання для інструмента «Рамка»
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt-клік перехопило робоче середовище, яке переміщує вікна через Alt-перетягування. Призначте для переміщення вікон клавішу Super або зафіксуйте Alt у меню Вікно › Клавіші-модифікатори.
# Edit › Content-Aware Fill workspace
Sampling Brush Tool Пензель області вибірки
Sampling Area Overlay Накладання області вибірки
Show Sampling Area Показати область вибірки
Indicates Позначає
Sampling Area Область вибірки
Excluded Area Виключена область
Sampling Area Options Параметри області вибірки
Rectangular Прямокутна
Fill Settings Параметри заливки
Rotation Adaptation Адаптація повороту
Very High Дуже висока
Full Повна
Mirror Віддзеркалення
Output Settings Параметри виводу
Output To Вивести в
Can't render this file because it contains an unexpected character in line 1305 and column 18.
+21
View File
@@ -2301,3 +2301,24 @@ fontWeight Italic 斜体
Cycle Orientation 循环切换取向
Set the overlay options for the Crop tool 设置裁剪工具的叠加选项
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt 单击被桌面环境截获了,它用 Alt 拖动来移动窗口。请把桌面的窗口移动键改为 Super,或在 窗口 › 修饰键 中锁定 Alt。
# Edit › Content-Aware Fill workspace
Sampling Brush Tool 取样画笔工具
Sampling Area Overlay 取样区域叠加
Show Sampling Area 显示取样区域
Indicates 指示
Sampling Area 取样区域
Excluded Area 排除区域
Sampling Area Options 取样区域选项
Rectangular 矩形
Fill Settings 填充设置
Rotation Adaptation 旋转适应
Very High 非常高
High 高
Low 低
Full 完全
Mirror 镜像
Output Settings 输出设置
Output To 输出到
New Layer 新建图层
Duplicate Layer 复制图层
Contract 收缩
1 # Simplified Chinese (zh-hans) catalog. UTF-8, TAB-separated columns:
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
+21
View File
@@ -2270,3 +2270,24 @@ fontWeight Italic 斜體
Cycle Orientation 循環切換方向
Set the overlay options for the Crop tool 設定裁切工具的覆蓋選項
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt 按一下被桌面環境攔截了,它用 Alt 拖曳來移動視窗。請把桌面的視窗移動鍵改為 Super,或在 視窗 › 輔助鍵 中鎖定 Alt。
# Edit › Content-Aware Fill workspace
Sampling Brush Tool 取樣筆刷工具
Sampling Area Overlay 取樣區域覆蓋
Show Sampling Area 顯示取樣區域
Indicates 指示
Sampling Area 取樣區域
Excluded Area 排除區域
Sampling Area Options 取樣區域選項
Rectangular 矩形
Fill Settings 填色設定
Rotation Adaptation 旋轉調整
Very High 非常高
High 高
Low 低
Full 完全
Mirror 鏡像
Output Settings 輸出設定
Output To 輸出至
New Layer 新增圖層
Duplicate Layer 複製圖層
Contract 縮減
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -30,6 +30,7 @@ pub mod brush_preview;
pub mod brush_resize;
pub mod brush_sections;
pub mod brushes_tab;
pub mod caf_ui;
mod camera_raw_detail_ui;
mod camera_raw_preview_ui;
mod camera_raw_scope_ui;
+8 -16
View File
@@ -614,19 +614,9 @@ pub fn invoke(app: &mut PhotocraftApp, _ctx: &egui::Context, id: &str, params: &
}
dialog(d)
}
"edit.contentAwareFill" => {
if !app.session.is_enabled(id) {
return Some(Err("Content-Aware Fill needs a selection on a pixel layer".into()));
}
let d = crate::filter_dialog::open(app, id);
if let Some(dm) = d.and_then(|d| app.ui.dialog_mut(d)) {
dm.fields.insert("__preview".into(), json!(true));
for k in ["margin", "seed", "channel"] {
dm.fields.remove(k);
}
}
dialog(d)
}
// Photoshop's Content-Aware Fill workspace (caf_ui); the engine command with params
// still runs directly.
"edit.contentAwareFill" => Some(crate::caf_ui::open(app).map(|_| json!({"contentAwareFill": app.distort.describe()["contentAwareFill"]}))),
"edit.contentAwareScale" => {
let st = app.session.active()?;
let b = match &st.doc.selection {
@@ -2285,9 +2275,11 @@ mod tests {
assert!(note.starts_with("Monitor profile in use: sRGB") && note.contains("fallback"), "{note}");
assert!(crate::menus::invoke(&mut app, &ctx, "edit.fade", json!({})).is_err());
app.run("select.rect", json!({"x": 4, "y": 4, "width": 8, "height": 8})).unwrap();
let d = crate::menus::invoke(&mut app, &ctx, "edit.contentAwareFill", json!({})).unwrap()["dialog"].as_u64().unwrap();
assert_eq!(app.ui.dialogs.iter().find(|x| x.id == d).unwrap().fields["__preview"], true);
crate::dialogs::confirm(&mut app, d).unwrap();
// Content-Aware Fill opens its workspace (caf_ui); OK fills.
let r = crate::menus::invoke(&mut app, &ctx, "edit.contentAwareFill", json!({})).unwrap();
assert!(r["contentAwareFill"]["window"].is_array(), "{r}");
crate::caf_ui::commit(&mut app);
assert!(app.distort.caf.is_none());
let d = crate::menus::invoke(&mut app, &ctx, "edit.contentAwareScale", json!({})).unwrap()["dialog"].as_u64().unwrap();
assert_eq!(app.ui.dialogs.iter().find(|x| x.id == d).unwrap().fields["width"], 8);
let d = crate::menus::invoke(&mut app, &ctx, "edit.presets.presetManager", json!({})).unwrap()["dialog"].as_u64().unwrap();
+5 -1
View File
@@ -263,11 +263,15 @@ pub fn handle(app: &mut PhotocraftApp, ctx: &egui::Context) {
if app.distort.liquify.is_some() && !ctx.text_edit_focused() {
crate::liquify_ui::keys(app, ctx);
}
// The Content-Aware Fill workspace likewise.
if app.distort.caf.is_some() && !ctx.text_edit_focused() {
crate::caf_ui::keys(app, ctx);
}
use crate::shortcut_dispatch::{Focus, dispatch_pressed};
let focus = Focus::of(ctx);
// Dialogs (and Liquify's panel while one of its controls has focus) keep canvas zoom; a
// focused text field keeps its keys.
let liquify_focus = app.distort.liquify.is_some() && focus != Focus::None;
let liquify_focus = (app.distort.liquify.is_some() || app.distort.caf.is_some()) && focus != Focus::None;
if liquify_focus || !app.ui.dialogs.is_empty() || app.discard.is_some() {
if focus != Focus::Text {
nav_keys(app, ctx);