mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 02:02:04 +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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user