readme, misc
This commit is contained in:
parent
b00b24739a
commit
14c5c1bc51
|
|
@ -22,7 +22,7 @@ cargo test --workspace
|
|||
|
||||
Start with [`AGENTS.md`](AGENTS.md), then [`docs/`](docs/).
|
||||
|
||||
## The artcraft suite
|
||||
## Crafting Apps
|
||||
|
||||
Open-source, clean-room, pure-Rust creative apps that share the same conventions: native on macOS, Windows and Linux, in the browser via WebAssembly, and fully drivable by agents.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
//! Multi-dimensional colour lookup tables (ICC CLUTs) with tetrahedral interpolation.
|
||||
//!
|
||||
//! Grid layout follows ICC.1: the first input channel varies slowest, output values of one
|
||||
//! node are stored together. 3-input tables use tetrahedral interpolation (Sakamoto 1980 /
|
||||
//! Kasson et al. 1995); 4-input tables interpolate linearly along the first input between
|
||||
//! two tetrahedral evaluations; other input counts use multilinear interpolation.
|
||||
|
||||
/// Maximum number of input or output channels of a CLUT.
|
||||
pub const MAX_CHANNELS: usize = 15;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Clut {
|
||||
pub inputs: usize,
|
||||
pub outputs: usize,
|
||||
/// Grid points per input dimension (each ≥ 2, or 1 for a degenerate dimension).
|
||||
pub grid: Vec<usize>,
|
||||
/// Normalised node values, `outputs` per node.
|
||||
pub data: Vec<f32>,
|
||||
strides: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Clut {
|
||||
/// Builds a table from node data. Panics if `data` has the wrong length.
|
||||
pub fn new(grid: Vec<usize>, outputs: usize, data: Vec<f32>) -> Self {
|
||||
let inputs = grid.len();
|
||||
assert!((1..=MAX_CHANNELS).contains(&inputs) && (1..=MAX_CHANNELS).contains(&outputs), "bad CLUT dimensions");
|
||||
let mut strides = vec![0usize; inputs];
|
||||
let mut s = outputs;
|
||||
for i in (0..inputs).rev() {
|
||||
strides[i] = s;
|
||||
s *= grid[i].max(1);
|
||||
}
|
||||
assert_eq!(data.len(), s, "CLUT data length");
|
||||
Clut { inputs, outputs, grid, data, strides }
|
||||
}
|
||||
|
||||
/// Number of nodes.
|
||||
pub fn nodes(&self) -> usize {
|
||||
self.data.len() / self.outputs
|
||||
}
|
||||
|
||||
/// Builds a table by sampling `f` at every node (inputs normalised to `[0, 1]`).
|
||||
pub fn sample(grid: Vec<usize>, outputs: usize, mut f: impl FnMut(&[f32], &mut [f32])) -> Self {
|
||||
let inputs = grid.len();
|
||||
let total: usize = grid.iter().product();
|
||||
let mut data = vec![0.0f32; total * outputs];
|
||||
let mut idx = vec![0usize; inputs];
|
||||
let mut inp = vec![0.0f32; inputs];
|
||||
for node in 0..total {
|
||||
let mut rem = node;
|
||||
for d in (0..inputs).rev() {
|
||||
idx[d] = rem % grid[d];
|
||||
rem /= grid[d];
|
||||
}
|
||||
for d in 0..inputs {
|
||||
inp[d] = if grid[d] > 1 { idx[d] as f32 / (grid[d] - 1) as f32 } else { 0.0 };
|
||||
}
|
||||
f(&inp, &mut data[node * outputs..(node + 1) * outputs]);
|
||||
}
|
||||
Clut::new(grid, outputs, data)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn locate(&self, d: usize, v: f32) -> (usize, f32, usize) {
|
||||
let n = self.grid[d];
|
||||
if n < 2 {
|
||||
return (0, 0.0, 0);
|
||||
}
|
||||
let p = v.clamp(0.0, 1.0) * (n - 1) as f32;
|
||||
let i = (p as usize).min(n - 2);
|
||||
(i * self.strides[d], p - i as f32, self.strides[d])
|
||||
}
|
||||
|
||||
/// Interpolates at `input` (`inputs` values) into `out` (`outputs` values).
|
||||
#[inline]
|
||||
pub fn eval(&self, input: &[f32], out: &mut [f32]) {
|
||||
match self.inputs {
|
||||
1 => self.eval1(input[0], out),
|
||||
3 => {
|
||||
let (b0, r0, s0) = self.locate(0, input[0]);
|
||||
let (b1, r1, s1) = self.locate(1, input[1]);
|
||||
let (b2, r2, s2) = self.locate(2, input[2]);
|
||||
self.tetra(b0 + b1 + b2, [s0, s1, s2], [r0, r1, r2], out);
|
||||
}
|
||||
4 => {
|
||||
let (b0, r0, s0) = self.locate(0, input[0]);
|
||||
let (b1, r1, s1) = self.locate(1, input[1]);
|
||||
let (b2, r2, s2) = self.locate(2, input[2]);
|
||||
let (b3, r3, s3) = self.locate(3, input[3]);
|
||||
let base = b0 + b1 + b2 + b3;
|
||||
let mut lo = [0.0f32; MAX_CHANNELS];
|
||||
self.tetra(base, [s1, s2, s3], [r1, r2, r3], &mut lo[..self.outputs]);
|
||||
if r0 > 0.0 && s0 > 0 {
|
||||
let mut hi = [0.0f32; MAX_CHANNELS];
|
||||
self.tetra(base + s0, [s1, s2, s3], [r1, r2, r3], &mut hi[..self.outputs]);
|
||||
for (o, (l, h)) in out.iter_mut().zip(lo.iter().zip(&hi)) {
|
||||
*o = l + (h - l) * r0;
|
||||
}
|
||||
} else {
|
||||
out[..self.outputs].copy_from_slice(&lo[..self.outputs]);
|
||||
}
|
||||
}
|
||||
_ => self.eval_multilinear(input, out),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval1(&self, v: f32, out: &mut [f32]) {
|
||||
let (b, r, s) = self.locate(0, v);
|
||||
for (k, o) in out.iter_mut().enumerate().take(self.outputs) {
|
||||
let a = self.data[b + k];
|
||||
let c = if s > 0 { self.data[b + s + k] } else { a };
|
||||
*o = a + (c - a) * r;
|
||||
}
|
||||
}
|
||||
|
||||
/// Tetrahedral interpolation inside one cube of three dimensions.
|
||||
#[inline]
|
||||
fn tetra(&self, base: usize, s: [usize; 3], r: [f32; 3], out: &mut [f32]) {
|
||||
let d = &self.data;
|
||||
let [rx, ry, rz] = r;
|
||||
let [sx, sy, sz] = s;
|
||||
let c000 = base;
|
||||
let c100 = base + sx;
|
||||
let c010 = base + sy;
|
||||
let c001 = base + sz;
|
||||
let c110 = base + sx + sy;
|
||||
let c101 = base + sx + sz;
|
||||
let c011 = base + sy + sz;
|
||||
let c111 = base + sx + sy + sz;
|
||||
// Pick the tetrahedron containing (rx, ry, rz) and the path through the cube corners.
|
||||
let (p1, p2, p3, w1, w2, w3) = if rx >= ry {
|
||||
if ry >= rz {
|
||||
(c100, c110, c111, rx, ry, rz)
|
||||
} else if rx >= rz {
|
||||
(c100, c101, c111, rx, rz, ry)
|
||||
} else {
|
||||
(c001, c101, c111, rz, rx, ry)
|
||||
}
|
||||
} else if rx >= rz {
|
||||
(c010, c110, c111, ry, rx, rz)
|
||||
} else if ry >= rz {
|
||||
(c010, c011, c111, ry, rz, rx)
|
||||
} else {
|
||||
(c001, c011, c111, rz, ry, rx)
|
||||
};
|
||||
for (k, o) in out.iter_mut().enumerate().take(self.outputs) {
|
||||
let v0 = d[c000 + k];
|
||||
let v1 = d[p1 + k];
|
||||
let v2 = d[p2 + k];
|
||||
let v3 = d[p3 + k];
|
||||
*o = v0 + (v1 - v0) * w1 + (v2 - v1) * w2 + (v3 - v2) * w3;
|
||||
}
|
||||
}
|
||||
|
||||
/// Trilinear interpolation (3 inputs), kept as a reference for accuracy comparisons.
|
||||
pub fn eval_trilinear(&self, input: &[f32], out: &mut [f32]) {
|
||||
assert_eq!(self.inputs, 3);
|
||||
self.eval_multilinear(input, out);
|
||||
}
|
||||
|
||||
/// Multilinear interpolation over all 2^n cube corners.
|
||||
pub fn eval_multilinear(&self, input: &[f32], out: &mut [f32]) {
|
||||
let n = self.inputs;
|
||||
let mut base = 0usize;
|
||||
let mut r = [0.0f32; MAX_CHANNELS];
|
||||
let mut s = [0usize; MAX_CHANNELS];
|
||||
for d in 0..n {
|
||||
let (b, rr, ss) = self.locate(d, input[d]);
|
||||
base += b;
|
||||
r[d] = rr;
|
||||
s[d] = ss;
|
||||
}
|
||||
let o = self.outputs;
|
||||
out[..o].iter_mut().for_each(|v| *v = 0.0);
|
||||
for corner in 0..(1usize << n) {
|
||||
let mut w = 1.0f32;
|
||||
let mut off = base;
|
||||
for d in 0..n {
|
||||
if corner & (1 << d) != 0 {
|
||||
w *= r[d];
|
||||
off += s[d];
|
||||
} else {
|
||||
w *= 1.0 - r[d];
|
||||
}
|
||||
}
|
||||
if w == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for k in 0..o {
|
||||
out[k] += w * self.data[off + k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn f3(p: &[f32], o: &mut [f32]) {
|
||||
o[0] = p[0] * 0.5 + p[1] * 0.3 + p[2] * 0.2;
|
||||
o[1] = p[0] * p[1];
|
||||
o[2] = (p[2] * 3.0).sin() * 0.5 + 0.5;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_on_nodes_and_linear_functions() {
|
||||
let c = Clut::sample(vec![5, 5, 5], 3, f3);
|
||||
let mut o = [0.0; 3];
|
||||
c.eval(&[0.25, 0.5, 0.75], &mut o);
|
||||
let mut w = [0.0; 3];
|
||||
f3(&[0.25, 0.5, 0.75], &mut w);
|
||||
for i in 0..3 {
|
||||
assert!((o[i] - w[i]).abs() < 1e-6);
|
||||
}
|
||||
// Linear function is reproduced exactly anywhere.
|
||||
c.eval(&[0.13, 0.77, 0.41], &mut o);
|
||||
assert!((o[0] - (0.13 * 0.5 + 0.77 * 0.3 + 0.41 * 0.2)).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_d_and_multilinear_agree_on_linear() {
|
||||
let f = |p: &[f32], o: &mut [f32]| o[0] = 0.1 * p[0] + 0.2 * p[1] + 0.3 * p[2] + 0.4 * p[3];
|
||||
let c = Clut::sample(vec![3, 4, 5, 6], 1, f);
|
||||
let mut a = [0.0];
|
||||
let mut b = [0.0];
|
||||
for p in [[0.1f32, 0.9, 0.3, 0.7], [1.0, 0.0, 0.5, 0.5], [0.33, 0.66, 0.99, 0.01]] {
|
||||
c.eval(&p, &mut a);
|
||||
c.eval_multilinear(&p, &mut b);
|
||||
let want = 0.1 * p[0] + 0.2 * p[1] + 0.3 * p[2] + 0.4 * p[3];
|
||||
assert!((a[0] - want).abs() < 1e-5 && (b[0] - want).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_and_two_d() {
|
||||
let c = Clut::sample(vec![3], 2, |p, o| {
|
||||
o[0] = p[0];
|
||||
o[1] = 1.0 - p[0];
|
||||
});
|
||||
let mut o = [0.0; 2];
|
||||
c.eval(&[0.25], &mut o);
|
||||
assert!((o[0] - 0.25).abs() < 1e-6 && (o[1] - 0.75).abs() < 1e-6);
|
||||
let c = Clut::sample(vec![2, 2], 1, |p, o| o[0] = p[0] + p[1]);
|
||||
c.eval(&[0.5, 0.25], &mut o);
|
||||
assert!((o[0] - 0.75).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
//! Float evaluation pipelines: a list of stages mapping normalised device values to the
|
||||
//! profile connection space (or back). This is the exact reference path; [`crate::Transform`]
|
||||
//! builds faster lookup tables from it.
|
||||
|
||||
use crate::clut::Clut;
|
||||
use crate::curve::Curve;
|
||||
use crate::math;
|
||||
|
||||
/// Largest channel count flowing through a pipeline.
|
||||
pub const MAX: usize = 16;
|
||||
|
||||
/// One processing element.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Stage {
|
||||
/// Per-channel curves.
|
||||
Curves(Vec<Curve>),
|
||||
/// Per-channel inverse curves.
|
||||
InvCurves(Vec<Curve>),
|
||||
/// `out[r] = Σ m[r][c]·in[c] + offset[r]`, `rows × cols`.
|
||||
Matrix { rows: usize, cols: usize, m: Vec<f64>, offset: Vec<f64> },
|
||||
Clut(Clut),
|
||||
/// Float CIE Lab (L 0–100) → XYZ (D50, Y = 1).
|
||||
LabToXyz,
|
||||
XyzToLab,
|
||||
/// ICC v4 / 8-bit Lab encoding (normalised) → float Lab, and back.
|
||||
DecodeLabV4,
|
||||
EncodeLabV4,
|
||||
/// ICC v2 legacy 16-bit Lab encoding (used by `lut16Type`) → float Lab, and back.
|
||||
DecodeLabV2,
|
||||
EncodeLabV2,
|
||||
/// u1Fixed15 XYZ encoding (normalised over 0..=0xFFFF) → XYZ, and back.
|
||||
DecodeXyz,
|
||||
EncodeXyz,
|
||||
/// Clamp every channel to `[0, 1]`.
|
||||
Clamp01,
|
||||
}
|
||||
|
||||
const XYZ_SCALE: f32 = 65535.0 / 32768.0;
|
||||
const V2_L: f32 = 100.0 * 65535.0 / 65280.0;
|
||||
const V2_AB: f32 = 255.0 * 65535.0 / 65280.0;
|
||||
|
||||
impl Stage {
|
||||
/// Output channel count given `n` inputs.
|
||||
pub fn outputs(&self, n: usize) -> usize {
|
||||
match self {
|
||||
Stage::Matrix { rows, .. } => *rows,
|
||||
Stage::Clut(c) => c.outputs,
|
||||
_ => n,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matrix3(m: &math::Mat3) -> Stage {
|
||||
Stage::Matrix { rows: 3, cols: 3, m: m.iter().flatten().copied().collect(), offset: vec![0.0; 3] }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn run(&self, v: &mut [f32; MAX], n: usize) -> usize {
|
||||
match self {
|
||||
Stage::Curves(cs) => {
|
||||
for (x, c) in v.iter_mut().zip(cs) {
|
||||
*x = c.eval(*x);
|
||||
}
|
||||
n
|
||||
}
|
||||
Stage::InvCurves(cs) => {
|
||||
for (x, c) in v.iter_mut().zip(cs) {
|
||||
*x = c.eval_inverse(*x);
|
||||
}
|
||||
n
|
||||
}
|
||||
Stage::Matrix { rows, cols, m, offset } => {
|
||||
let mut o = [0.0f32; MAX];
|
||||
for r in 0..*rows {
|
||||
let mut s = offset[r];
|
||||
for c in 0..*cols {
|
||||
s += m[r * cols + c] * v[c] as f64;
|
||||
}
|
||||
o[r] = s as f32;
|
||||
}
|
||||
v[..*rows].copy_from_slice(&o[..*rows]);
|
||||
*rows
|
||||
}
|
||||
Stage::Clut(c) => {
|
||||
let mut o = [0.0f32; MAX];
|
||||
let mut inp = [0.0f32; MAX];
|
||||
inp[..n].copy_from_slice(&v[..n]);
|
||||
c.eval(&inp[..c.inputs], &mut o[..c.outputs]);
|
||||
v[..c.outputs].copy_from_slice(&o[..c.outputs]);
|
||||
c.outputs
|
||||
}
|
||||
Stage::LabToXyz => {
|
||||
let x = math::lab_to_xyz_f32([v[0], v[1], v[2]]);
|
||||
v[..3].copy_from_slice(&x);
|
||||
3
|
||||
}
|
||||
Stage::XyzToLab => {
|
||||
let l = math::xyz_to_lab_f32([v[0].max(0.0), v[1].max(0.0), v[2].max(0.0)]);
|
||||
v[..3].copy_from_slice(&l);
|
||||
3
|
||||
}
|
||||
Stage::DecodeLabV4 => {
|
||||
v[0] *= 100.0;
|
||||
v[1] = v[1] * 255.0 - 128.0;
|
||||
v[2] = v[2] * 255.0 - 128.0;
|
||||
3
|
||||
}
|
||||
Stage::EncodeLabV4 => {
|
||||
v[0] = (v[0] / 100.0).clamp(0.0, 1.0);
|
||||
v[1] = ((v[1] + 128.0) / 255.0).clamp(0.0, 1.0);
|
||||
v[2] = ((v[2] + 128.0) / 255.0).clamp(0.0, 1.0);
|
||||
3
|
||||
}
|
||||
Stage::DecodeLabV2 => {
|
||||
v[0] *= V2_L;
|
||||
v[1] = v[1] * V2_AB - 128.0;
|
||||
v[2] = v[2] * V2_AB - 128.0;
|
||||
3
|
||||
}
|
||||
Stage::EncodeLabV2 => {
|
||||
v[0] = (v[0] / V2_L).clamp(0.0, 1.0);
|
||||
v[1] = ((v[1] + 128.0) / V2_AB).clamp(0.0, 1.0);
|
||||
v[2] = ((v[2] + 128.0) / V2_AB).clamp(0.0, 1.0);
|
||||
3
|
||||
}
|
||||
Stage::DecodeXyz => {
|
||||
for x in &mut v[..3] {
|
||||
*x *= XYZ_SCALE;
|
||||
}
|
||||
3
|
||||
}
|
||||
Stage::EncodeXyz => {
|
||||
for x in &mut v[..3] {
|
||||
*x = (*x / XYZ_SCALE).clamp(0.0, 1.0);
|
||||
}
|
||||
3
|
||||
}
|
||||
Stage::Clamp01 => {
|
||||
for x in &mut v[..n] {
|
||||
*x = x.clamp(0.0, 1.0);
|
||||
}
|
||||
n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sequence of stages with fixed input and output channel counts.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pipeline {
|
||||
pub inputs: usize,
|
||||
pub outputs: usize,
|
||||
pub stages: Vec<Stage>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub fn new(inputs: usize, stages: Vec<Stage>) -> Self {
|
||||
let outputs = stages.iter().fold(inputs, |n, s| s.outputs(n));
|
||||
Pipeline { inputs, outputs, stages }
|
||||
}
|
||||
|
||||
/// Appends stages.
|
||||
pub fn extend(&mut self, stages: impl IntoIterator<Item = Stage>) {
|
||||
for s in stages {
|
||||
self.outputs = s.outputs(self.outputs);
|
||||
self.stages.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates one pixel: `input` has `inputs` values, `out` receives `outputs` values.
|
||||
#[inline]
|
||||
pub fn eval(&self, input: &[f32], out: &mut [f32]) {
|
||||
let mut v = [0.0f32; MAX];
|
||||
v[..self.inputs].copy_from_slice(&input[..self.inputs]);
|
||||
let mut n = self.inputs;
|
||||
for s in &self.stages {
|
||||
n = s.run(&mut v, n);
|
||||
}
|
||||
out[..self.outputs].copy_from_slice(&v[..self.outputs]);
|
||||
}
|
||||
|
||||
/// Removes no-op stages and cancels adjacent inverse pairs (Lab↔XYZ, encode/decode).
|
||||
pub fn optimize(&mut self) {
|
||||
let mut out: Vec<Stage> = Vec::with_capacity(self.stages.len());
|
||||
for s in self.stages.drain(..) {
|
||||
let noop = match &s {
|
||||
Stage::Curves(c) | Stage::InvCurves(c) => c.iter().all(Curve::is_identity),
|
||||
Stage::Matrix { rows: 3, cols: 3, m, offset } => {
|
||||
offset.iter().all(|o| o.abs() < 1e-12)
|
||||
&& m.iter().enumerate().all(|(i, v)| (v - if i % 4 == 0 { 1.0 } else { 0.0 }).abs() < 1e-12)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if noop {
|
||||
continue;
|
||||
}
|
||||
let cancels = matches!(
|
||||
(out.last(), &s),
|
||||
(Some(Stage::LabToXyz), Stage::XyzToLab)
|
||||
| (Some(Stage::XyzToLab), Stage::LabToXyz)
|
||||
| (Some(Stage::EncodeLabV4), Stage::DecodeLabV4)
|
||||
| (Some(Stage::EncodeLabV2), Stage::DecodeLabV2)
|
||||
| (Some(Stage::EncodeXyz), Stage::DecodeXyz)
|
||||
);
|
||||
// Decode→encode of the same encoding is only exact inside the gamut of the encoding,
|
||||
// which always holds for data coming out of a table, so those pairs cancel too.
|
||||
let cancels_dec = matches!(
|
||||
(out.last(), &s),
|
||||
(Some(Stage::DecodeLabV4), Stage::EncodeLabV4) | (Some(Stage::DecodeLabV2), Stage::EncodeLabV2) | (Some(Stage::DecodeXyz), Stage::EncodeXyz)
|
||||
);
|
||||
if cancels || cancels_dec {
|
||||
out.pop();
|
||||
continue;
|
||||
}
|
||||
// Merge consecutive 3×3 matrices.
|
||||
if let (Some(Stage::Matrix { rows: 3, cols: 3, m: a, offset: oa }), Stage::Matrix { rows: 3, cols: 3, m: b, offset: ob }) = (out.last(), &s) {
|
||||
let mut m = vec![0.0; 9];
|
||||
let mut off = vec![0.0; 3];
|
||||
for r in 0..3 {
|
||||
for c in 0..3 {
|
||||
m[r * 3 + c] = (0..3).map(|k| b[r * 3 + k] * a[k * 3 + c]).sum();
|
||||
}
|
||||
off[r] = (0..3).map(|k| b[r * 3 + k] * oa[k]).sum::<f64>() + ob[r];
|
||||
}
|
||||
out.pop();
|
||||
out.push(Stage::Matrix { rows: 3, cols: 3, m, offset: off });
|
||||
continue;
|
||||
}
|
||||
out.push(s);
|
||||
}
|
||||
self.stages = out;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,582 @@
|
|||
//! From input points to dabs: smoothing, brush pose, path walking (spacing, airbrush build-up) and
|
||||
//! the per-dab dynamics (shape, scattering, transfer, colour, texture depth).
|
||||
//!
|
||||
//! Everything is incremental (feed points in any chunking, get the same dabs) and deterministic:
|
||||
//! jitter comes from [`crate::rng`] keyed by the stroke seed and the step/dab index.
|
||||
|
||||
use photocraft_geom::Point;
|
||||
|
||||
use crate::brush::{BrushSettings, Control, Dynamic, Smoothing};
|
||||
use crate::rng::{rand_signed, rand01, stream};
|
||||
use crate::{Dab, StrokePoint};
|
||||
|
||||
/// Maximum pulled-string length at 100 % smoothing, in screen pixels.
|
||||
pub const MAX_STRING_PX: f64 = 100.0;
|
||||
|
||||
#[inline]
|
||||
fn lerp_pt(a: &StrokePoint, b: &StrokePoint, f: f64) -> StrokePoint {
|
||||
let ff = f as f32;
|
||||
StrokePoint {
|
||||
x: a.x + (b.x - a.x) * f,
|
||||
y: a.y + (b.y - a.y) * f,
|
||||
pressure: a.pressure + (b.pressure - a.pressure) * ff,
|
||||
tilt_x: a.tilt_x + (b.tilt_x - a.tilt_x) * ff,
|
||||
tilt_y: a.tilt_y + (b.tilt_y - a.tilt_y) * ff,
|
||||
rotation: a.rotation + (b.rotation - a.rotation) * ff,
|
||||
wheel: a.wheel + (b.wheel - a.wheel) * ff,
|
||||
time: a.time + (b.time - a.time) * f,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Smoothing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Incremental stroke smoothing: exponential (with optional time-based catch-up) or pulled string.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Smoother {
|
||||
cfg: Smoothing,
|
||||
zoom: f64,
|
||||
pos: Option<StrokePoint>,
|
||||
last_input: Option<StrokePoint>,
|
||||
}
|
||||
|
||||
impl Smoother {
|
||||
/// `zoom` is the view zoom (only used with `adjust_for_zoom`).
|
||||
pub fn new(cfg: &Smoothing, zoom: f32) -> Self {
|
||||
Self { cfg: cfg.clone(), zoom: f64::from(zoom.max(0.01)), pos: None, last_input: None }
|
||||
}
|
||||
|
||||
fn active(&self) -> bool {
|
||||
self.cfg.amount > 0.0
|
||||
}
|
||||
|
||||
/// Feed one input point; returns the brush positions to paint through.
|
||||
pub fn push(&mut self, p: StrokePoint, out: &mut Vec<StrokePoint>) {
|
||||
let prev_input = self.last_input.replace(p);
|
||||
if !self.active() {
|
||||
out.push(p);
|
||||
return;
|
||||
}
|
||||
let Some(pos) = self.pos else {
|
||||
self.pos = Some(p);
|
||||
out.push(p);
|
||||
return;
|
||||
};
|
||||
let amount = f64::from(self.cfg.amount.clamp(0.0, 1.0));
|
||||
if self.cfg.pulled_string {
|
||||
let mut len = amount * MAX_STRING_PX;
|
||||
if self.cfg.adjust_for_zoom {
|
||||
len /= self.zoom;
|
||||
}
|
||||
let (dx, dy) = (p.x - pos.x, p.y - pos.y);
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d > len {
|
||||
let k = (d - len) / d;
|
||||
let np = StrokePoint { x: pos.x + dx * k, y: pos.y + dy * k, ..p };
|
||||
self.pos = Some(np);
|
||||
out.push(np);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Exponential: each iteration moves `a` of the way towards the pointer.
|
||||
let a = 1.0 - amount.min(0.95);
|
||||
let iters = match prev_input {
|
||||
Some(q) if self.cfg.catch_up && p.time > q.time => ((p.time - q.time) / 16.0).round().clamp(1.0, 64.0) as usize,
|
||||
_ => 1,
|
||||
};
|
||||
let mut cur = pos;
|
||||
for _ in 0..iters {
|
||||
cur = StrokePoint { x: cur.x + (p.x - cur.x) * a, y: cur.y + (p.y - cur.y) * a, ..p };
|
||||
out.push(cur);
|
||||
}
|
||||
self.pos = Some(cur);
|
||||
}
|
||||
|
||||
/// End of stroke: with Catch-Up On Stroke End the brush finishes at the last pointer position.
|
||||
pub fn finish(&mut self, out: &mut Vec<StrokePoint>) {
|
||||
if !self.active() || !self.cfg.catch_up_on_end {
|
||||
return;
|
||||
}
|
||||
if let (Some(pos), Some(last)) = (self.pos, self.last_input)
|
||||
&& (pos.x != last.x || pos.y != last.y)
|
||||
{
|
||||
self.pos = Some(last);
|
||||
out.push(last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Smooth a whole point list with the brush's smoothing settings.
|
||||
pub fn smooth_points(cfg: &Smoothing, zoom: f32, pts: &[StrokePoint]) -> Vec<StrokePoint> {
|
||||
let mut s = Smoother::new(cfg, zoom);
|
||||
let mut out = Vec::with_capacity(pts.len());
|
||||
for p in pts {
|
||||
s.push(*p, &mut out);
|
||||
}
|
||||
s.finish(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path walking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One spacing (or airbrush time) step along the path.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct StepInput {
|
||||
pub point: StrokePoint,
|
||||
/// Visual (counter-clockwise, y-up) direction of travel in radians.
|
||||
pub direction: f32,
|
||||
pub initial_direction: f32,
|
||||
/// Sequential step number (0 = first).
|
||||
pub step: u64,
|
||||
}
|
||||
|
||||
/// Walks a polyline and emits steps every `step_len(point)` pixels, plus airbrush steps in time.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathWalker {
|
||||
last: Option<StrokePoint>,
|
||||
/// Path distance from `last` to the next step.
|
||||
next_at: f64,
|
||||
/// First point, emitted lazily once the initial direction is known.
|
||||
pending_first: Option<StrokePoint>,
|
||||
initial_dir: Option<f32>,
|
||||
dir: f32,
|
||||
step: u64,
|
||||
/// Airbrush interval in ms (None = off).
|
||||
interval: Option<f64>,
|
||||
time_acc: f64,
|
||||
}
|
||||
|
||||
impl PathWalker {
|
||||
pub fn new(build_up_interval_ms: Option<f64>) -> Self {
|
||||
Self { last: None, next_at: 0.0, pending_first: None, initial_dir: None, dir: 0.0, step: 0, interval: build_up_interval_ms, time_acc: 0.0 }
|
||||
}
|
||||
|
||||
fn emit(&mut self, point: StrokePoint, out: &mut impl FnMut(StepInput)) {
|
||||
let s = StepInput { point, direction: self.dir, initial_direction: self.initial_dir.unwrap_or(self.dir), step: self.step };
|
||||
self.step += 1;
|
||||
out(s);
|
||||
}
|
||||
|
||||
pub fn push(&mut self, p: StrokePoint, step_len: &impl Fn(&StrokePoint) -> f64, out: &mut impl FnMut(StepInput)) {
|
||||
let Some(a) = self.last else {
|
||||
self.last = Some(p);
|
||||
self.pending_first = Some(p);
|
||||
self.next_at = step_len(&p);
|
||||
return;
|
||||
};
|
||||
let (dx, dy) = (p.x - a.x, p.y - a.y);
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len > 1e-9 {
|
||||
self.dir = (-dy).atan2(dx) as f32;
|
||||
if self.initial_dir.is_none() {
|
||||
self.initial_dir = Some(self.dir);
|
||||
}
|
||||
}
|
||||
if let Some(f) = self.pending_first.take() {
|
||||
self.emit(f, out);
|
||||
}
|
||||
if len > 1e-9 {
|
||||
while self.next_at <= len + 1e-9 {
|
||||
let q = lerp_pt(&a, &p, (self.next_at / len).min(1.0));
|
||||
self.emit(q, out);
|
||||
self.next_at += step_len(&q).max(0.25);
|
||||
}
|
||||
self.next_at -= len;
|
||||
}
|
||||
if let Some(iv) = self.interval {
|
||||
let dt = p.time - a.time;
|
||||
if dt > 0.0 {
|
||||
self.time_acc += dt;
|
||||
while self.time_acc >= iv {
|
||||
self.time_acc -= iv;
|
||||
let f = ((dt - self.time_acc) / dt).clamp(0.0, 1.0);
|
||||
self.emit(lerp_pt(&a, &p, f), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.last = Some(p);
|
||||
}
|
||||
|
||||
pub fn finish(&mut self, out: &mut impl FnMut(StepInput)) {
|
||||
if let Some(f) = self.pending_first.take() {
|
||||
self.emit(f, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dynamics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Scalar control value in 0..1.
|
||||
pub fn control_value(c: Control, fade_steps: u32, s: &StepInput) -> f32 {
|
||||
let p = &s.point;
|
||||
match c {
|
||||
Control::Off | Control::InitialDirection | Control::Direction => 1.0,
|
||||
Control::Fade => 1.0 - (s.step as f32 / fade_steps.max(1) as f32).min(1.0),
|
||||
Control::PenPressure => p.pressure.clamp(0.0, 1.0),
|
||||
// Upright pen = 1, flat pen = 0 (a mouse reports no tilt, so paints at full value).
|
||||
Control::PenTilt => 1.0 - (p.tilt_x.hypot(p.tilt_y) / 90.0).clamp(0.0, 1.0),
|
||||
Control::StylusWheel => p.wheel.clamp(0.0, 1.0),
|
||||
Control::Rotation => (p.rotation.rem_euclid(360.0)) / 360.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Angle contributed by a control, in degrees (counter-clockwise).
|
||||
pub fn control_angle(c: Control, fade_steps: u32, s: &StepInput) -> f32 {
|
||||
let p = &s.point;
|
||||
match c {
|
||||
Control::Off => 0.0,
|
||||
Control::Fade => 360.0 * (s.step as f32 / fade_steps.max(1) as f32).min(1.0),
|
||||
Control::PenPressure => 360.0 * p.pressure.clamp(0.0, 1.0),
|
||||
Control::PenTilt => {
|
||||
if p.tilt_x == 0.0 && p.tilt_y == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(-p.tilt_y).atan2(p.tilt_x).to_degrees()
|
||||
}
|
||||
}
|
||||
Control::StylusWheel => 360.0 * p.wheel.clamp(0.0, 1.0),
|
||||
Control::Rotation => p.rotation,
|
||||
Control::InitialDirection => s.initial_direction.to_degrees(),
|
||||
Control::Direction => s.direction.to_degrees(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A 0..1 multiplier from a dynamic: control lerps from `minimum` to 1, jitter reduces randomly,
|
||||
/// never below `minimum`.
|
||||
fn scalar(d: &Dynamic, s: &StepInput, r: f32) -> f32 {
|
||||
if !d.is_active() {
|
||||
return 1.0;
|
||||
}
|
||||
let min = d.minimum.clamp(0.0, 1.0);
|
||||
let cv = control_value(d.control, d.fade_steps, s);
|
||||
let v = (min + (1.0 - min) * cv) * (1.0 - d.jitter.clamp(0.0, 1.0) * r);
|
||||
v.max(min)
|
||||
}
|
||||
|
||||
fn rgb_to_hsv(c: [f32; 3]) -> [f32; 3] {
|
||||
let mx = c[0].max(c[1]).max(c[2]);
|
||||
let mn = c[0].min(c[1]).min(c[2]);
|
||||
let d = mx - mn;
|
||||
let h = if d <= 0.0 {
|
||||
0.0
|
||||
} else if mx == c[0] {
|
||||
((c[1] - c[2]) / d).rem_euclid(6.0)
|
||||
} else if mx == c[1] {
|
||||
(c[2] - c[0]) / d + 2.0
|
||||
} else {
|
||||
(c[0] - c[1]) / d + 4.0
|
||||
} / 6.0;
|
||||
[h, if mx > 0.0 { d / mx } else { 0.0 }, mx]
|
||||
}
|
||||
|
||||
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> [f32; 3] {
|
||||
let h6 = h.rem_euclid(1.0) * 6.0;
|
||||
let i = h6.floor();
|
||||
let f = h6 - i;
|
||||
let (p, q, t) = (v * (1.0 - s), v * (1.0 - s * f), v * (1.0 - s * (1.0 - f)));
|
||||
match i as i32 {
|
||||
0 => [v, t, p],
|
||||
1 => [q, v, p],
|
||||
2 => [p, v, t],
|
||||
3 => [p, q, v],
|
||||
4 => [t, p, v],
|
||||
_ => [v, p, q],
|
||||
}
|
||||
}
|
||||
|
||||
/// Colour Dynamics for one dab (or once per stroke, with `index = 0`).
|
||||
pub fn dynamic_color(b: &BrushSettings, s: &StepInput, seed: u64, index: u64) -> [f32; 4] {
|
||||
let cd = &b.color_dynamics;
|
||||
let (fg, bg) = (b.color, b.background);
|
||||
let r = |st: u64| rand01(seed, index, st);
|
||||
let base = if cd.fg_bg.control != Control::Off { 1.0 - control_value(cd.fg_bg.control, cd.fg_bg.fade_steps, s) } else { 0.0 };
|
||||
let t = (base + (1.0 - base) * cd.fg_bg.jitter.clamp(0.0, 1.0) * r(stream::FG_BG)).clamp(0.0, 1.0);
|
||||
let rgb = [fg[0] + (bg[0] - fg[0]) * t, fg[1] + (bg[1] - fg[1]) * t, fg[2] + (bg[2] - fg[2]) * t];
|
||||
let [mut h, mut sat, mut v] = rgb_to_hsv(rgb);
|
||||
h += rand_signed(seed, index, stream::HUE) * cd.hue_jitter.clamp(0.0, 1.0) * 0.5;
|
||||
sat = (sat + rand_signed(seed, index, stream::SAT) * cd.saturation_jitter.clamp(0.0, 1.0)).clamp(0.0, 1.0);
|
||||
let pu = cd.purity.clamp(-1.0, 1.0);
|
||||
sat = if pu >= 0.0 { sat + (1.0 - sat) * pu } else { sat * (1.0 + pu) };
|
||||
v = (v + rand_signed(seed, index, stream::BRIGHT) * cd.brightness_jitter.clamp(0.0, 1.0)).clamp(0.0, 1.0);
|
||||
let o = hsv_to_rgb(h, sat, v);
|
||||
[o[0], o[1], o[2], fg[3] + (bg[3] - fg[3]) * t]
|
||||
}
|
||||
|
||||
/// Base (pre-jitter) diameter at a step: size × size control (pressure etc.). Drives spacing.
|
||||
pub fn base_diameter(b: &BrushSettings, s: &StepInput) -> f32 {
|
||||
let sd = &b.shape_dynamics;
|
||||
let mut f = 1.0;
|
||||
if sd.enabled && sd.size.control != Control::Off {
|
||||
let min = sd.size.minimum.clamp(0.0, 1.0);
|
||||
f = min + (1.0 - min) * control_value(sd.size.control, sd.size.fade_steps, s);
|
||||
} else if b.pressure_size {
|
||||
f = s.point.pressure.clamp(0.0, 1.0);
|
||||
}
|
||||
b.size * f
|
||||
}
|
||||
|
||||
/// Builds the primary dabs of a step (Shape Dynamics, Scattering, Transfer, Color, Texture depth).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DabBuilder {
|
||||
pub brush: BrushSettings,
|
||||
pub seed: u64,
|
||||
/// Per-stroke colour (Color Dynamics with per-tip off), computed at the first step.
|
||||
stroke_color: Option<[f32; 4]>,
|
||||
next_index: u64,
|
||||
}
|
||||
|
||||
impl DabBuilder {
|
||||
pub fn new(brush: &BrushSettings) -> Self {
|
||||
Self { brush: brush.clone(), seed: brush.seed, stroke_color: None, next_index: 0 }
|
||||
}
|
||||
|
||||
pub fn step_len(&self, p: &StrokePoint) -> f64 {
|
||||
let s = StepInput { point: *p, direction: 0.0, initial_direction: 0.0, step: 0 };
|
||||
let d = base_diameter(&self.brush, &s).max(1.0);
|
||||
f64::from(d * self.brush.spacing.max(0.01)).max(0.5)
|
||||
}
|
||||
|
||||
pub fn build(&mut self, s: &StepInput, out: &mut Vec<Dab>) {
|
||||
let b = &self.brush;
|
||||
let seed = self.seed;
|
||||
let sd = &b.shape_dynamics;
|
||||
let sc = &b.scattering;
|
||||
let count = if sc.enabled {
|
||||
let n = sc.count.clamp(1, 16) as f32;
|
||||
let k = scalar(&sc.count_jitter, s, rand01(seed, s.step, stream::COUNT));
|
||||
((n * k).round() as u32).clamp(1, 16)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let base_d = base_diameter(b, s);
|
||||
for _ in 0..count {
|
||||
let i = self.next_index;
|
||||
self.next_index += 1;
|
||||
let r = |st: u64| rand01(seed, i, st);
|
||||
// Shape.
|
||||
let mut diameter = base_d;
|
||||
let mut angle = b.angle;
|
||||
let mut roundness = b.roundness.clamp(0.01, 1.0);
|
||||
let (mut fx, mut fy) = (b.flip_x, b.flip_y);
|
||||
if sd.enabled {
|
||||
if sd.size.jitter > 0.0 {
|
||||
let min = sd.size.minimum.clamp(0.0, 1.0);
|
||||
let f = (1.0 - sd.size.jitter.clamp(0.0, 1.0) * r(stream::SIZE)).max(min);
|
||||
diameter *= f;
|
||||
}
|
||||
angle += control_angle(sd.angle.control, sd.angle.fade_steps, s) + rand_signed(seed, i, stream::ANGLE) * sd.angle.jitter.clamp(0.0, 1.0) * 180.0;
|
||||
roundness = (roundness * scalar(&sd.roundness, s, r(stream::ROUNDNESS))).clamp(0.01, 1.0);
|
||||
if sd.flip_x_jitter && r(stream::FLIP_X) < 0.5 {
|
||||
fx = !fx;
|
||||
}
|
||||
if sd.flip_y_jitter && r(stream::FLIP_Y) < 0.5 {
|
||||
fy = !fy;
|
||||
}
|
||||
}
|
||||
let radius = (diameter / 2.0).max(0.5);
|
||||
// Scatter.
|
||||
let mut c = Point::new(s.point.x, s.point.y);
|
||||
if sc.enabled && sc.scatter.jitter > 0.0 {
|
||||
let cv = control_value(sc.scatter.control, sc.scatter.fade_steps, s);
|
||||
let amt = f64::from(sc.scatter.jitter * cv * radius);
|
||||
let (ox, oy) = (rand_signed(seed, i, stream::SCATTER_X) as f64 * amt, rand_signed(seed, i, stream::SCATTER_Y) as f64 * amt);
|
||||
if sc.both_axes {
|
||||
c.x += ox;
|
||||
c.y += oy;
|
||||
} else {
|
||||
// Perpendicular to the direction of travel (y-down image space).
|
||||
let (dx, dy) = (f64::from(s.direction.cos()), -f64::from(s.direction.sin()));
|
||||
c.x += -dy * ox;
|
||||
c.y += dx * ox;
|
||||
}
|
||||
}
|
||||
// Transfer.
|
||||
let tr = &b.transfer;
|
||||
let (mut opacity, mut flow) = (1.0, b.flow);
|
||||
if tr.enabled {
|
||||
opacity = scalar(&tr.opacity, s, r(stream::OPACITY));
|
||||
flow *= scalar(&tr.flow, s, r(stream::FLOW));
|
||||
}
|
||||
if b.pressure_opacity && !(tr.enabled && tr.opacity.control != Control::Off) {
|
||||
opacity *= s.point.pressure.clamp(0.0, 1.0);
|
||||
}
|
||||
// Colour.
|
||||
let color = if b.color_dynamics.enabled {
|
||||
if b.color_dynamics.per_tip {
|
||||
dynamic_color(b, s, seed, i)
|
||||
} else {
|
||||
*self.stroke_color.get_or_insert_with(|| dynamic_color(b, s, seed, 0))
|
||||
}
|
||||
} else {
|
||||
b.color
|
||||
};
|
||||
// Texture depth (per tip).
|
||||
let tx = &b.texture;
|
||||
let depth = if tx.enabled && tx.each_tip { tx.depth.clamp(0.0, 1.0) * scalar(&tx.depth_jitter, s, r(stream::DEPTH)) } else { tx.depth.clamp(0.0, 1.0) };
|
||||
out.push(Dab {
|
||||
center: c,
|
||||
radius,
|
||||
alpha: flow.clamp(0.0, 1.0),
|
||||
angle: angle.to_radians(),
|
||||
roundness,
|
||||
flip_x: fx,
|
||||
flip_y: fy,
|
||||
opacity: opacity.clamp(0.0, 1.0),
|
||||
color,
|
||||
depth,
|
||||
index: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds Dual Brush dabs (the second tip: size, spacing, scatter, count, flip).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DualBuilder {
|
||||
pub brush: BrushSettings,
|
||||
next_index: u64,
|
||||
}
|
||||
|
||||
impl DualBuilder {
|
||||
pub fn new(brush: &BrushSettings) -> Self {
|
||||
Self { brush: brush.clone(), next_index: 0 }
|
||||
}
|
||||
pub fn step_len(&self, _p: &StrokePoint) -> f64 {
|
||||
let d = &self.brush.dual_brush;
|
||||
f64::from(d.size.max(1.0) * d.spacing.max(0.01)).max(0.5)
|
||||
}
|
||||
pub fn build(&mut self, s: &StepInput, out: &mut Vec<Dab>) {
|
||||
let d = &self.brush.dual_brush;
|
||||
let seed = self.brush.seed ^ 0xD0A1;
|
||||
for _ in 0..d.count.clamp(1, 16) {
|
||||
let i = self.next_index;
|
||||
self.next_index += 1;
|
||||
let radius = (d.size / 2.0).max(0.5);
|
||||
let amt = f64::from(d.scatter * radius);
|
||||
let (ox, oy) = (rand_signed(seed, i, stream::DUAL_X) as f64 * amt, rand_signed(seed, i, stream::DUAL_Y) as f64 * amt);
|
||||
let mut c = Point::new(s.point.x, s.point.y);
|
||||
if d.both_axes {
|
||||
c.x += ox;
|
||||
c.y += oy;
|
||||
} else {
|
||||
let (dx, dy) = (f64::from(s.direction.cos()), -f64::from(s.direction.sin()));
|
||||
c.x += -dy * ox;
|
||||
c.y += dx * ox;
|
||||
}
|
||||
let flip = d.flip && rand01(seed, i, stream::DUAL_FLIP) < 0.5;
|
||||
out.push(Dab {
|
||||
center: c,
|
||||
radius,
|
||||
alpha: 1.0,
|
||||
angle: d.angle.to_radians(),
|
||||
roundness: d.roundness.clamp(0.01, 1.0),
|
||||
flip_x: flip,
|
||||
flip_y: false,
|
||||
opacity: 1.0,
|
||||
color: [0.0, 0.0, 0.0, 1.0],
|
||||
depth: 1.0,
|
||||
index: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply Brush Pose to an input point.
|
||||
pub fn apply_pose(b: &BrushSettings, mut p: StrokePoint) -> StrokePoint {
|
||||
let pose = &b.pose;
|
||||
if pose.enabled {
|
||||
if pose.override_tilt {
|
||||
p.tilt_x = pose.tilt_x;
|
||||
p.tilt_y = pose.tilt_y;
|
||||
}
|
||||
if pose.override_rotation {
|
||||
p.rotation = pose.rotation;
|
||||
}
|
||||
if pose.override_pressure {
|
||||
p.pressure = pose.pressure;
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// Incremental dab generation for a whole brush (primary and optional dual dabs).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DabGenerator {
|
||||
smoother: Smoother,
|
||||
walker: PathWalker,
|
||||
dual_walker: Option<PathWalker>,
|
||||
builder: DabBuilder,
|
||||
dual: Option<DualBuilder>,
|
||||
scratch: Vec<StrokePoint>,
|
||||
}
|
||||
|
||||
impl DabGenerator {
|
||||
pub fn new(brush: &BrushSettings, zoom: f32) -> Self {
|
||||
let interval = brush.build_up.then(|| 1000.0 / f64::from(brush.build_up_rate.clamp(0.1, 1000.0)));
|
||||
let dual_on = brush.dual_brush.enabled;
|
||||
Self {
|
||||
smoother: Smoother::new(&brush.smoothing, zoom),
|
||||
walker: PathWalker::new(interval),
|
||||
dual_walker: dual_on.then(|| PathWalker::new(None)),
|
||||
builder: DabBuilder::new(brush),
|
||||
dual: dual_on.then(|| DualBuilder::new(brush)),
|
||||
scratch: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn walk(&mut self, pts: &[StrokePoint], dabs: &mut Vec<Dab>, dual: &mut Vec<Dab>) {
|
||||
for p in pts {
|
||||
let p = apply_pose(&self.builder.brush, *p);
|
||||
let b = &self.builder;
|
||||
let len = |q: &StrokePoint| b.step_len(q);
|
||||
let mut steps = Vec::new();
|
||||
self.walker.push(p, &len, &mut |s| steps.push(s));
|
||||
for s in &steps {
|
||||
self.builder.build(s, dabs);
|
||||
}
|
||||
if let (Some(w), Some(db)) = (self.dual_walker.as_mut(), self.dual.as_mut()) {
|
||||
let mut steps = Vec::new();
|
||||
let len = |q: &StrokePoint| db.step_len(q);
|
||||
w.push(p, &len, &mut |s| steps.push(s));
|
||||
for s in &steps {
|
||||
db.build(s, dual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed input points; appends the new primary and dual dabs.
|
||||
pub fn push(&mut self, pts: &[StrokePoint], dabs: &mut Vec<Dab>, dual: &mut Vec<Dab>) {
|
||||
let mut sm = std::mem::take(&mut self.scratch);
|
||||
sm.clear();
|
||||
for p in pts {
|
||||
self.smoother.push(*p, &mut sm);
|
||||
}
|
||||
self.walk(&sm, dabs, dual);
|
||||
self.scratch = sm;
|
||||
}
|
||||
|
||||
/// End of stroke.
|
||||
pub fn finish(&mut self, dabs: &mut Vec<Dab>, dual: &mut Vec<Dab>) {
|
||||
let mut sm = Vec::new();
|
||||
self.smoother.finish(&mut sm);
|
||||
self.walk(&sm, dabs, dual);
|
||||
let mut steps = Vec::new();
|
||||
self.walker.finish(&mut |s| steps.push(s));
|
||||
for s in &steps {
|
||||
self.builder.build(s, dabs);
|
||||
}
|
||||
if let (Some(w), Some(db)) = (self.dual_walker.as_mut(), self.dual.as_mut()) {
|
||||
let mut steps = Vec::new();
|
||||
w.finish(&mut |s| steps.push(s));
|
||||
for s in &steps {
|
||||
db.build(s, dual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue