mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-29 04:17:11 -06:00
* 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>
73 lines
3.9 KiB
Rust
73 lines
3.9 KiB
Rust
//! 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());
|
||
}
|
||
}
|