Files
photocraft/crates/engine/examples/retouch_bench.rs
T
b4f9d56c93 Add the Patch tool with a live preview (#213) (#381)
* Add the Patch tool (#213)

paint.patch heals the selected area with texture from where the
selection is dragged (Source) or copies the selection onto the
dragged-to area (Destination), seamless-cloned like the Healing Brush.
The solve is clipped to the canvas, so patches at the edge stay opaque.

Tool::Patch joins the J group: outside the selection it draws a lasso
(Shift/Alt add/subtract), inside it drags the patch, the outline follows
the pointer and the offset is clamped to the canvas. Options bar:
Source / Destination. Strings translated in all six catalogs.

Tests at 8/16/32 bit, a UI gesture test, panic_hunt green. Scorecard
TOOL-213-3 is partial: the drag previews the outline, not the pixels.
retouch_bench: patch 250x250 22 ms, 1000x1000 362 ms (release).

generated with Claude Code

* Patch tool: live preview of the heal while dragging (#213)

The canvas shows the document healed at the pointer's offset
(patch_preview.rs, modelled on the Move tool's preview) and
recomposites only the healed area; release commits the exact
paint.patch and its damage rect refreshes the same area.

The preview (retouch_cmds::patch_preview) solves the colour fit on a
coarse grid (at most 128x128 cells) and interpolates it back: the
Poisson result is source + a harmonic correction, so the texture stays
exact and only the smooth correction is approximated (worst pixel 0.022
at 8x coarser in the test). Selection coverage is read tile-wise and
the interpolation runs per row in parallel.

retouch_bench (release, 6016x6016): preview 250x250 7 ms, 500x500
17 ms, 1000x1000 52 ms (exact patch: 21 / 90 / 354 ms). Scorecard
TOOL-213-3 done. Also applies rustfmt to the first commit's code.

generated with Claude Code

* Patch tool: cap patches at 16M pixels, drop product names from comments (#213)

The solve needs roughly 150 bytes per pixel, so the old 64M cap allowed
about 10 GB and risked an allocation abort instead of an error.

generated with Claude Code

* Regenerate docs/scorecard.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tool::ALL holds both the Patch tool and the Mixer Brush (rebase onto #433)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:42:30 -04:00

73 lines
3.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Timing: Healing Brush and Spot Healing dab cost, and Patch cost, on a 6016×6016 document.
//! `cargo run --release -p photocraft-engine --example retouch_bench [side] [depth]`
//!
//! Each measurement is one single-dab stroke through the full command path (`Session::execute`:
//! undo snapshot, source sampling, solve/fill, composite), on a textured area of the canvas.
use std::time::Instant;
use photocraft_engine::Session;
use photocraft_geom::Rect;
use serde_json::json;
fn main() {
let side: u32 = std::env::args().nth(1).and_then(|v| v.parse().ok()).unwrap_or(6016);
let depth: u64 = std::env::args().nth(2).and_then(|v| v.parse().ok()).unwrap_or(8);
let mut s = Session::new();
s.execute("file.new", json!({"width": side, "height": side, "depth": depth})).unwrap();
// Texture a 2048² area in the middle (the rest stays white) with a gradient, noise and blemishes.
let area = Rect::new(2000, 2000, 4048, 4048).intersect(&Rect::new(0, 0, side as i32, side as i32));
s.edit("texture", |doc, active| {
let surf = doc.layer_mut(active.unwrap()).unwrap().surface_mut().unwrap();
let fmt = surf.format();
let mut data = Vec::with_capacity(area.width() as usize * area.height() as usize * 4);
for y in area.y0..area.y1 {
for x in area.x0..area.x1 {
let n = ((x.wrapping_mul(73_856_093) ^ y.wrapping_mul(19_349_663)) as u32 % 1000) as f32 / 1000.0 * 0.08 - 0.04;
let blemish = (x % 400 - 200).abs() < 8 && (y % 400 - 200).abs() < 8;
let v =
if blemish { [0.05, 0.05, 0.05, 1.0] } else { [0.3 + 0.0002 * x as f32 + n, 0.4 + ((x + y) as f32 * 0.01).sin() * 0.1 + n, 0.5 + n, 1.0] };
data.extend(photocraft_raster::from_rgba(&fmt, v));
}
}
surf.write_region(area, &data);
Ok(())
})
.unwrap();
println!("document {side}x{side}, {depth}-bit RGB");
let mut k = 0;
let mut spot = || {
k += 1;
(2200 + (k % 4) * 400, 2200 + (k / 4 % 4) * 400)
};
for size in [50.0, 100.0, 200.0, 300.0] {
let (x, y) = spot();
let t = Instant::now();
s.execute("paint.healingBrush", json!({"points": [[x, y]], "offset": [-150, -130], "size": size, "hardness": 50})).unwrap();
println!("healingBrush size {size:>4}: {:>8.1?}", t.elapsed());
}
for kind in ["contentAware", "proximityMatch", "createTexture"] {
for size in [50.0, 100.0, 200.0, 300.0] {
let (x, y) = spot();
let t = Instant::now();
s.execute("paint.spotHealing", json!({"points": [[x, y]], "size": size, "hardness": 50, "type": kind})).unwrap();
println!("spotHealing {kind:<15} size {size:>4}: {:>8.1?}", t.elapsed());
}
}
// A short multi-dab heal stroke (spacing 25 %): the whole stroke is solved at once.
let t = Instant::now();
s.execute("paint.healingBrush", json!({"points": [[2300, 3500], [2700, 3500]], "offset": [0, -300], "size": 100, "hardness": 50})).unwrap();
println!("healingBrush stroke 400 px long, size 100: {:.1?}", t.elapsed());
// Patch: an elliptical selection of each size, dragged 300 px; the whole selection is solved.
for size in [100, 250, 500, 1000] {
let (x, y) = (2300, 2300);
s.execute("select.rect", json!({"x": x, "y": y, "width": size, "height": size, "ellipse": true})).unwrap();
let t = Instant::now();
s.execute("paint.patch", json!({"offset": [300, 200]})).unwrap();
println!("patch ellipse {size:>4}×{size:<4}: {:>8.1?}", t.elapsed());
// The Patch Tool's live preview of the same patch (coarse solve, 128² cells, at 100 % zoom).
let t = Instant::now();
photocraft_engine::retouch_cmds::patch_preview(&s, &json!({"offset": [300, 200]}), 128 * 128, 1).unwrap();
println!("patch preview {size:>4}×{size:<4}: {:>8.1?}", t.elapsed());
}
}