mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-27 17:51:59 +01:00
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:
co-authored by
Claude Opus 5.5
parent
2e1f3b0a41
commit
4d7db6dabe
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
@@ -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 출력 위치
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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 收缩
|
||||
|
||||
|
@@ -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.
|
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user