Files
photocraft/crates/io/examples/bench_abr.rs
Brandon ThomasandClaude Opus 5.5 d881785fde Photoshop brushes: .abr import, stylus pressure/tilt, 36 grouped presets, .grd import (#74)
* psd: .abr brush (v1/v2/v6+) and .grd gradient (v5) parsers and writers

Clean-room from public format descriptions: legacy computed/sampled brushes,
8BIM-sectioned samp/patt/desc files, 8/16-bit raw and PackBits tips. Bounded
(MAX_EDGE, MAX_TOTAL_BYTES), damaged brushes are skipped with warnings, hostile
input returns Err. Adds an abr fuzz target.

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

* paint: 36 grouped built-in brush presets and new procedural tips

General / Dry Media / Wet Media / Special Effects groups (pressure size/opacity
variants, airbrush, pencils, charcoal, pastel, crayon, rake, watercolour, ink,
oil, foliage, grass, stipple, sparkle, halftone…). Leaf, grass, charcoal,
sponge, star and rake tips are generated in code. BrushPreset gains a group.

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

* io: map .abr brushes onto BrushSettings

Tip shape, shape dynamics, scattering, texture (embedded patt patterns),
dual brush, colour dynamics, transfer, pose, noise, wet edges, build-up and
captured tool options; unmapped settings are reported as warnings. Includes
bench_abr (100-brush import, 5000-dab stroke).

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

* engine: brush.presets.importAbr and gradient.presets.importGrd

Path or base64 data, group per file (re-import replaces), unique names next to
the built-ins; graceful-failure tests.

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

* ui: real stylus pressure/tilt, grouped Brushes panel, Import Brushes

Pen force from egui Touch events (Windows WM_POINTER via winit), Pointer Events
pressure/tilt/twist on the web, tilt via ui.pointer; mouse stays 1.0. .abr/.grd
open from File > Open, drag-and-drop, Brushes panel and Preset Manager. Preset
previews keyed on a cheap signature instead of the full brush JSON.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:26:16 -04:00

124 lines
5.5 KiB
Rust
Raw Permalink 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.
//! Brush import and painting benchmark: parse + map a synthetic 100-brush ABR v6 file (256 px
//! RLE-compressed tips, full dynamics descriptors), then paint a 5000-dab stroke with an imported
//! brush on a 6000×6000 RGBA8 layer.
//!
//! `cargo run --release -p photocraft-io --example bench_abr`
#![allow(clippy::unwrap_used)]
use std::time::Instant;
use photocraft_color::PixelFormat;
use photocraft_paint::{StrokePoint, render_stroke};
use photocraft_psd::abr::{AbrSample, write_v6};
use photocraft_psd::descriptor::{Descriptor, UnicodeString, Value};
use photocraft_raster::Surface;
fn prc(v: f64) -> Value {
Value::UnitFloat { unit: *b"#Prc", value: v }
}
fn var(control: i32, jitter: f64) -> Value {
Value::Descriptor(Descriptor::new("brVr").with("bVTy", Value::Integer(control)).with("jitter", prc(jitter)))
}
fn main() {
let n = 256u32;
let samples: Vec<AbrSample> = (0..100)
.map(|i| {
let data = (0..n * n)
.map(|k| {
let (x, y) = ((k % n) as f32 - 128.0, (k / n) as f32 - 128.0);
let r = (x * x + y * y).sqrt() / 128.0;
(((1.0 - r) * 255.0).clamp(0.0, 255.0) as u8) & if (k + i) % 7 == 0 { 0x7f } else { 0xff }
})
.collect();
AbrSample { id: format!("$tip-{i}"), width: n, height: n, depth: 8, data }
})
.collect();
let presets: Vec<Descriptor> = (0..100)
.map(|i| {
Descriptor::new("brushPreset")
.with("Nm ", Value::Text(UnicodeString::new_nul(&format!("Brush {i}"))))
.with(
"Brsh",
Value::Descriptor(
Descriptor::new("sampledBrush")
.with("Dmtr", Value::UnitFloat { unit: *b"#Pxl", value: 60.0 })
.with("Spcn", prc(25.0))
.with("sampledData", Value::Text(UnicodeString::new_nul(&format!("$tip-{i}")))),
),
)
.with("useTipDynamics", Value::Boolean(true))
.with("szVr", var(2, 20.0))
.with("angleDynamics", var(0, 100.0))
.with("useScatter", Value::Boolean(true))
.with("scatterDynamics", var(0, 120.0))
.with("Cnt ", Value::Integer(2))
.with("usePaintDynamics", Value::Boolean(true))
.with("opVr", var(2, 0.0))
})
.collect();
let bytes = write_v6(2, &samples, &[], &presets, true).unwrap();
// `--write <path>`: also save a small showcase file (procedural tips) for UI snapshots.
let args: Vec<String> = std::env::args().collect();
if let Some(path) = args.iter().position(|a| a == "--write").and_then(|i| args.get(i + 1)) {
use photocraft_paint::procedural::*;
let tips = [("Leafy", leaf_tip(96)), ("Charcoal Stick", charcoal_tip(96, 3)), ("Sea Sponge", sponge_tip(96, 5)), ("Star Burst", star_tip(96))];
let samples: Vec<AbrSample> = tips
.iter()
.enumerate()
.map(|(i, (_, t))| AbrSample {
id: format!("$show-{i}"),
width: t.width,
height: t.height,
depth: 16,
data: t.data.iter().flat_map(|v| v.to_be_bytes()).collect(),
})
.collect();
let presets: Vec<Descriptor> = tips
.iter()
.enumerate()
.map(|(i, (name, _))| {
let mut d = presets[i].clone();
d.items.retain(|(k, _)| !k.is("Nm ") && !k.is("Brsh"));
d.with("Nm ", Value::Text(UnicodeString::new_nul(name))).with(
"Brsh",
Value::Descriptor(
Descriptor::new("sampledBrush")
.with("Dmtr", Value::UnitFloat { unit: *b"#Pxl", value: 50.0 })
.with("Spcn", prc(if i == 1 { 10.0 } else { 45.0 }))
.with("sampledData", Value::Text(UnicodeString::new_nul(&format!("$show-{i}")))),
),
)
})
.collect();
std::fs::write(path, write_v6(2, &samples, &[], &presets, true).unwrap()).unwrap();
println!("wrote {path}");
}
let mut best = f64::MAX;
let mut imp = None;
for _ in 0..5 {
let t = Instant::now();
imp = Some(photocraft_io::abr_map::read_abr(&bytes, "Bench").unwrap());
best = best.min(t.elapsed().as_secs_f64() * 1000.0);
}
let imp = imp.unwrap();
println!("ABR import: {} brushes, {:.1} MB file, best of 5: {best:.1} ms", imp.presets.len(), bytes.len() as f64 / 1e6);
// A long pressure-varied stroke with the first imported brush (size, angle, scatter, opacity dynamics).
let mut brush = imp.presets[0].brush.clone();
brush.seed = 1;
let mut s = Surface::new(PixelFormat::RGBA8);
let size = 6000.0;
let pts: Vec<StrokePoint> = (0..=2000)
.map(|i| {
let t = i as f64 / 2000.0;
StrokePoint::new(200.0 + t * (size - 400.0), size / 2.0 + (t * 24.0).sin() * 1500.0, (0.3 + 0.7 * (t * 25.0).sin().abs()) as f32)
})
.collect();
let dabs = photocraft_paint::dabs(&photocraft_paint::Stroke { brush: brush.clone(), points: pts.clone() }).len();
let t = Instant::now();
render_stroke(&mut s, &brush, &pts, None, false, 1.0);
println!("Stroke: {dabs} primary dabs (scatter count 2), {:.1} ms", t.elapsed().as_secs_f64() * 1000.0);
}