mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-21 22:54:11 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44dc6f002d |
No files matched your search
@@ -59,6 +59,22 @@ fn main() {
|
||||
prev = p;
|
||||
}
|
||||
println!("forward-warp segment (600 px brush, 25 px moves) + proxy update: mean {:.2} ms, worst {:.2} ms", total / n as f64, worst);
|
||||
// One 3000 px drag with a 200 px brush (a straight leg, then a diagonal one), as a slow
|
||||
// (every 2 px) and a fast (every 60 px) pointer reports it: the field cost of the whole
|
||||
// stroke, and how far the two results differ.
|
||||
let mut drag = LiquifyStroke::new(LiquifyTool::ForwardWarp, 200.0);
|
||||
let mut fields = Vec::new();
|
||||
let along = |d: f64| if d <= 1500.0 { vec![1500.0 + d, 2000.0] } else { vec![3000.0 + (d - 1500.0) * 0.6, 2000.0 + (d - 1500.0) * 0.8] };
|
||||
for step in [2.0, 60.0] {
|
||||
drag.points = (0..=(3000.0 / step) as usize).map(|k| along(k as f64 * step)).collect();
|
||||
let t = Instant::now();
|
||||
let mut f = LiquifyField::new(b, cell);
|
||||
f.apply_stroke(&drag);
|
||||
println!("forward-warp 3000 px drag (200 px brush), an event every {step} px: {:.1} ms", t.elapsed().as_secs_f64() * 1e3);
|
||||
fields.push(f);
|
||||
}
|
||||
let diff = fields[0].d.iter().zip(&fields[1].d).map(|(p, q)| (p[0] - q[0]).abs().max((p[1] - q[1]).abs())).fold(0.0f32, f32::max);
|
||||
println!(" slow vs fast pointer: fields differ by at most {diff:.3} px (max displacement {:.1} px)", fields[0].max_displacement());
|
||||
st.tool = LiquifyTool::Bloat;
|
||||
let t = Instant::now();
|
||||
let mut d = Rect::EMPTY;
|
||||
|
||||
+196
-56
@@ -154,7 +154,7 @@ impl LiquifyStroke {
|
||||
}
|
||||
|
||||
/// The displacement field plus the freeze mask, over `bounds` (document pixels).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LiquifyField {
|
||||
pub bounds: Rect,
|
||||
/// Document pixels per field node.
|
||||
@@ -165,6 +165,38 @@ pub struct LiquifyField {
|
||||
pub d: Vec<[f32; 2]>,
|
||||
/// Freeze mask per node (0 = free, 1 = frozen).
|
||||
pub freeze: Vec<f32>,
|
||||
/// Where the brush stroke in progress stands, so dab spacing runs on across its segments.
|
||||
cursor: Option<StrokeCursor>,
|
||||
}
|
||||
|
||||
/// Two fields are equal when their displacement and freeze mask are: the stroke cursor is
|
||||
/// transient brush state, not part of the result.
|
||||
impl PartialEq for LiquifyField {
|
||||
fn eq(&self, o: &Self) -> bool {
|
||||
self.bounds == o.bounds && self.cell == o.cell && self.w == o.w && self.h == o.h && self.d == o.d && self.freeze == o.freeze
|
||||
}
|
||||
}
|
||||
|
||||
/// The brush stroke in progress: the point its last segment ended at, its last dab and how far
|
||||
/// the brush has travelled along the path since that dab.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct StrokeCursor {
|
||||
end: [f64; 2],
|
||||
last_dab: [f64; 2],
|
||||
carry: f64,
|
||||
}
|
||||
|
||||
/// Distance between dabs along a stroke for brush radius `r`: a twentieth of the radius, so a
|
||||
/// drag is many small pushes and its edge stays smooth.
|
||||
fn dab_spacing(r: f64) -> f64 {
|
||||
(r * 0.05).max(0.5)
|
||||
}
|
||||
|
||||
/// Per-dab strength of a stationary tool (Twirl, Pucker, Bloat, Reconstruct, Smooth, Freeze,
|
||||
/// Thaw) while it moves: scaled to the spacing so its effect per distance travelled stays what it
|
||||
/// was with dabs a fifth of the radius apart.
|
||||
fn moving_weight(r: f64) -> f64 {
|
||||
dab_spacing(r) / (r * 0.2).max(0.5)
|
||||
}
|
||||
|
||||
/// The default field resolution for `bounds`: 2 px per node up to 4 MP, 4 px beyond.
|
||||
@@ -177,7 +209,7 @@ impl LiquifyField {
|
||||
let cell = cell.clamp(1.0, 64.0);
|
||||
let w = (f64::from(bounds.width()) / cell).ceil() as usize + 2;
|
||||
let h = (f64::from(bounds.height()) / cell).ceil() as usize + 2;
|
||||
LiquifyField { bounds, cell, w, h, d: vec![[0.0; 2]; w * h], freeze: vec![0.0; w * h] }
|
||||
LiquifyField { bounds, cell, w, h, d: vec![[0.0; 2]; w * h], freeze: vec![0.0; w * h], cursor: None }
|
||||
}
|
||||
|
||||
/// Document position of node (i, j) (node 0 sits on the first pixel centre).
|
||||
@@ -220,6 +252,7 @@ impl LiquifyField {
|
||||
|
||||
/// Applies one stroke. Returns the document rectangle whose output changed.
|
||||
pub fn apply_stroke(&mut self, s: &LiquifyStroke) -> Rect {
|
||||
self.cursor = None;
|
||||
match s.tool {
|
||||
LiquifyTool::LassoMask => return self.apply_lasso(s),
|
||||
LiquifyTool::ReconstructAll => {
|
||||
@@ -306,77 +339,120 @@ impl LiquifyField {
|
||||
/// callers use this plus [`Self::stroke_segment`] per new point, which is exactly what
|
||||
/// [`Self::apply_stroke`] does with the whole point list.
|
||||
pub fn stroke_begin(&mut self, s: &LiquifyStroke, p: [f64; 3]) -> Rect {
|
||||
if s.tool.is_stationary() && !s.tool.is_global() { self.dab(s, [p[0], p[1]], [0.0, 0.0], p[2].clamp(0.0, 1.0)) } else { Rect::EMPTY }
|
||||
self.cursor = Some(StrokeCursor { end: [p[0], p[1]], last_dab: [p[0], p[1]], carry: 0.0 });
|
||||
if s.tool.is_stationary() && !s.tool.is_global() { self.dab(s, [p[0], p[1]], [0.0, 0.0], p[2].clamp(0.0, 1.0), 1.0) } else { Rect::EMPTY }
|
||||
}
|
||||
|
||||
/// Continues a stroke from `a` to `b`: dabs every fifth of the brush radius (a stationary
|
||||
/// tool held still dabs once at `b`).
|
||||
/// Continues a stroke from `a` to `b`. Dabs fall every [`dab_spacing`] of distance travelled
|
||||
/// along the whole stroke, carried from one segment to the next, so the result is the same
|
||||
/// however finely the pointer path was sampled (a fast and a slow mouse give one warp). A
|
||||
/// stationary tool held still dabs once at `b`. When `a` is not where the previous segment
|
||||
/// ended, the stroke starts afresh at `a`.
|
||||
pub fn stroke_segment(&mut self, s: &LiquifyStroke, a: [f64; 3], b: [f64; 3]) -> Rect {
|
||||
self.walk_segment(s, a, b, true)
|
||||
}
|
||||
|
||||
/// [`Self::stroke_segment`]; `skip` leaves out the dabs that cannot reach a field node (they
|
||||
/// would change nothing), which tests compare with the full walk.
|
||||
fn walk_segment(&mut self, s: &LiquifyStroke, a: [f64; 3], b: [f64; 3], skip: bool) -> Rect {
|
||||
let mut dirty = Rect::EMPTY;
|
||||
let (pa, pb) = (a[2].clamp(0.0, 1.0), b[2].clamp(0.0, 1.0));
|
||||
let len = (b[0] - a[0]).hypot(b[1] - a[1]);
|
||||
let stationary = s.tool.is_stationary();
|
||||
let cur = match self.cursor {
|
||||
Some(c) if c.end == [a[0], a[1]] => c,
|
||||
_ => StrokeCursor { end: [a[0], a[1]], last_dab: [a[0], a[1]], carry: 0.0 },
|
||||
};
|
||||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||||
let len = dx.hypot(dy);
|
||||
if len == 0.0 {
|
||||
return if s.tool.is_stationary() { self.dab(s, [b[0], b[1]], [0.0, 0.0], pb) } else { dirty };
|
||||
self.cursor = Some(cur);
|
||||
return if stationary { self.dab(s, [b[0], b[1]], [0.0, 0.0], pb, 1.0) } else { dirty };
|
||||
}
|
||||
// NaN or a length that overflowed: nothing sensible to walk; the next segment restarts.
|
||||
if !len.is_finite() {
|
||||
self.cursor = None;
|
||||
return dirty;
|
||||
}
|
||||
let r = (s.size / 2.0).max(0.5);
|
||||
let spacing = (r * 0.2).max(0.5);
|
||||
let steps = (len / spacing).ceil().max(1.0) as usize;
|
||||
// Walk only the steps whose dab can reach a field node: the rest return from `dab` before
|
||||
// touching anything, and a segment to a far-off point has trillions of them (#937).
|
||||
let Some((k0, k1)) = self.reachable_steps(a, b, steps, r, s.tool.is_stationary()) else { return dirty };
|
||||
for k in k0..=k1 {
|
||||
let t0 = (k - 1) as f64 / steps as f64;
|
||||
let t1 = k as f64 / steps as f64;
|
||||
let c = [a[0] + (b[0] - a[0]) * t1, a[1] + (b[1] - a[1]) * t1];
|
||||
let delta = [(b[0] - a[0]) * (t1 - t0), (b[1] - a[1]) * (t1 - t0)];
|
||||
let pr = pa + (pb - pa) * t1;
|
||||
// Forward Warp centres the dab where the brush was (it drags what is under it).
|
||||
let centre = if s.tool.is_stationary() { c } else { [c[0] - delta[0], c[1] - delta[1]] };
|
||||
dirty = dirty.union(&self.dab(s, centre, delta, pr));
|
||||
let spacing = dab_spacing(r);
|
||||
let weight = if stationary { moving_weight(r) } else { 1.0 };
|
||||
// Distance along this segment of its first dab, then one every `spacing`.
|
||||
let first = (spacing - cur.carry).clamp(0.0, spacing);
|
||||
// Distances are compared with a little slack: a dab that rounding puts a hair past the
|
||||
// segment's end still falls on it, so cutting one segment in two never adds or drops a dab.
|
||||
let slack = spacing * 1e-6;
|
||||
if first > len + slack {
|
||||
self.cursor = Some(StrokeCursor { end: [b[0], b[1]], last_dab: cur.last_dab, carry: cur.carry + len });
|
||||
return dirty;
|
||||
}
|
||||
let last_k = ((len - first + slack) / spacing).floor().max(0.0);
|
||||
let at = |dist: f64| [a[0] + dx * dist / len, a[1] + dy * dist / len];
|
||||
let (mut k0, mut k1) = (0.0f64, last_k);
|
||||
if skip {
|
||||
match self.reachable_span(a, [dx, dy], len, r) {
|
||||
// A dragging dab is centred one spacing behind its position: pad by that and a step.
|
||||
Some((d0, d1)) => {
|
||||
k0 = k0.max(((d0 - spacing - first) / spacing).floor() - 1.0);
|
||||
k1 = k1.min(((d1 + spacing - first) / spacing).ceil() + 1.0);
|
||||
}
|
||||
None => k1 = -1.0,
|
||||
}
|
||||
}
|
||||
if k0 <= k1 {
|
||||
let mut prev = if k0 > 0.0 { at(first + (k0 - 1.0) * spacing) } else { cur.last_dab };
|
||||
for k in (k0 as u64)..=(k1 as u64) {
|
||||
let dist = (first + k as f64 * spacing).min(len);
|
||||
let c = at(dist);
|
||||
let delta = [c[0] - prev[0], c[1] - prev[1]];
|
||||
// Forward Warp centres the dab where the brush was (it drags what is under it).
|
||||
let centre = if stationary { c } else { prev };
|
||||
dirty = dirty.union(&self.dab(s, centre, delta, pa + (pb - pa) * dist / len, weight));
|
||||
prev = c;
|
||||
}
|
||||
}
|
||||
let last = (first + last_k * spacing).min(len);
|
||||
self.cursor = Some(StrokeCursor { end: [b[0], b[1]], last_dab: at(last), carry: (len - last).clamp(0.0, spacing) });
|
||||
dirty
|
||||
}
|
||||
|
||||
/// The steps `k0..=k1` of a segment walked in `steps` whose dab can touch a field node, or
|
||||
/// `None` when none can. Step `k` centres its dab at `a + (b - a)·(k - o)/steps`, with `o` = 1
|
||||
/// for the dragging tools and 0 for the stationary ones (see `stroke_segment`). A dab reaches a
|
||||
/// node only when its centre is within `r` plus a node of the node grid, so the window below
|
||||
/// pads that by another node and a step: the dabs it leaves out would each change nothing.
|
||||
fn reachable_steps(&self, a: [f64; 3], b: [f64; 3], steps: usize, r: f64, stationary: bool) -> Option<(usize, usize)> {
|
||||
/// The distances along a segment from `a` (direction `dir`, length `len`) whose points lie
|
||||
/// within a dab radius `r` plus two nodes of the node grid, or `None` when none do. Only dabs
|
||||
/// near that span can touch a node; a segment to a far-off point has trillions of others
|
||||
/// (#937).
|
||||
fn reachable_span(&self, a: [f64; 3], dir: [f64; 2], len: f64, r: f64) -> Option<(f64, f64)> {
|
||||
if self.w == 0 || self.h == 0 {
|
||||
return None;
|
||||
}
|
||||
let pad = r + 2.0 * self.cell;
|
||||
let lo = [f64::from(self.bounds.x0) + 0.5 - pad, f64::from(self.bounds.y0) + 0.5 - pad];
|
||||
let hi = [lo[0] + (self.w - 1) as f64 * self.cell + 2.0 * pad, lo[1] + (self.h - 1) as f64 * self.cell + 2.0 * pad];
|
||||
let (n, o) = (steps as f64, if stationary { 0.0 } else { 1.0 });
|
||||
let (mut k0, mut k1) = (1.0f64, n);
|
||||
let (mut u0, mut u1) = (0.0f64, 1.0f64);
|
||||
for ax in 0..2 {
|
||||
let d = b[ax] - a[ax];
|
||||
let d = dir[ax];
|
||||
if d == 0.0 {
|
||||
if a[ax].is_nan() || a[ax] < lo[ax] || a[ax] > hi[ax] {
|
||||
return None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (u0, u1) = ((lo[ax] - a[ax]) / d, (hi[ax] - a[ax]) / d);
|
||||
k0 = k0.max((u0.min(u1) * n + o).floor() - 1.0);
|
||||
k1 = k1.min((u0.max(u1) * n + o).ceil() + 1.0);
|
||||
let (v0, v1) = ((lo[ax] - a[ax]) / d, (hi[ax] - a[ax]) / d);
|
||||
u0 = u0.max(v0.min(v1));
|
||||
u1 = u1.min(v0.max(v1));
|
||||
}
|
||||
// NaN: a segment whose length overflowed.
|
||||
if k0.is_nan() || k1.is_nan() || k0 > k1 {
|
||||
if u0.is_nan() || u1.is_nan() || u0 > u1 {
|
||||
return None;
|
||||
}
|
||||
Some((k0 as usize, k1 as usize))
|
||||
Some((u0 * len, u1 * len))
|
||||
}
|
||||
|
||||
/// One dab at `c` with brush motion `delta`. Returns the document rect it affected.
|
||||
fn dab(&mut self, s: &LiquifyStroke, c: [f64; 2], delta: [f64; 2], point_pressure: f64) -> Rect {
|
||||
/// One dab at `c` with brush motion `delta`, its strength scaled by `weight`. Returns the
|
||||
/// document rect it affected.
|
||||
fn dab(&mut self, s: &LiquifyStroke, c: [f64; 2], delta: [f64; 2], point_pressure: f64, weight: f64) -> Rect {
|
||||
if s.tool == LiquifyTool::LassoMask {
|
||||
return Rect::EMPTY; // the lasso is polygon-only; never a brush dab
|
||||
}
|
||||
let r = (s.size / 2.0).max(0.5);
|
||||
let strength = (s.pressure / 100.0).clamp(0.0, 1.0) * point_pressure;
|
||||
let strength = (s.pressure / 100.0).clamp(0.0, 1.0) * point_pressure * weight;
|
||||
let rate = (s.rate / 100.0).clamp(0.0, 1.0);
|
||||
let density = (s.density / 100.0).clamp(0.0, 1.0);
|
||||
if strength <= 0.0 {
|
||||
@@ -921,21 +997,9 @@ mod tests {
|
||||
use super::*;
|
||||
use photocraft_color::{ColorMode, PixelFormat, SampleType};
|
||||
|
||||
/// The segment walk before #937: every step, whether or not its dab can reach the field.
|
||||
/// The segment walk before #937: every dab, whether or not it can reach the field.
|
||||
fn full_walk(f: &mut LiquifyField, s: &LiquifyStroke, a: [f64; 3], b: [f64; 3]) -> Rect {
|
||||
let mut dirty = Rect::EMPTY;
|
||||
let (pa, pb) = (a[2].clamp(0.0, 1.0), b[2].clamp(0.0, 1.0));
|
||||
let len = (b[0] - a[0]).hypot(b[1] - a[1]);
|
||||
let r = (s.size / 2.0).max(0.5);
|
||||
let steps = (len / (r * 0.2).max(0.5)).ceil().max(1.0) as usize;
|
||||
for k in 1..=steps {
|
||||
let (t0, t1) = ((k - 1) as f64 / steps as f64, k as f64 / steps as f64);
|
||||
let c = [a[0] + (b[0] - a[0]) * t1, a[1] + (b[1] - a[1]) * t1];
|
||||
let delta = [(b[0] - a[0]) * (t1 - t0), (b[1] - a[1]) * (t1 - t0)];
|
||||
let centre = if s.tool.is_stationary() { c } else { [c[0] - delta[0], c[1] - delta[1]] };
|
||||
dirty = dirty.union(&f.dab(s, centre, delta, pa + (pb - pa) * t1));
|
||||
}
|
||||
dirty
|
||||
f.walk_segment(s, a, b, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -945,7 +1009,7 @@ mod tests {
|
||||
// every result bit-identical to the full walk.
|
||||
use LiquifyTool::*;
|
||||
let bounds = Rect::new(-7, 3, 89, 67);
|
||||
let segments: [([f64; 3], [f64; 3]); 9] = [
|
||||
let segments: [([f64; 3], [f64; 3]); 10] = [
|
||||
([10.0, 20.0, 1.0], [70.0, 50.0, 0.5]), // inside
|
||||
([-900.0, 30.0, 1.0], [40.0, 31.0, 1.0]), // in from far left
|
||||
([40.0, 30.0, 1.0], [40.0, 4000.0, 0.2]), // out downwards
|
||||
@@ -955,6 +1019,7 @@ mod tests {
|
||||
([88.5, -50.0, 1.0], [88.5, 120.0, 1.0]), // vertical, along the right edge
|
||||
([200.0, 200.0, 1.0], [201.0, 900.0, 1.0]), // entirely outside
|
||||
([0.0, 0.0, 1.0], [0.25, 0.1, 1.0]), // shorter than one step
|
||||
([60.0, 40.0, 1.0], [-2000.0, 45.0, 1.0]), // continues the stroke before it, out left
|
||||
];
|
||||
for tool in [ForwardWarp, PushLeft, Reconstruct, Smooth, TwirlCw, TwirlCcw, Pucker, Bloat, Freeze, Thaw] {
|
||||
for size in [6.0, 40.0] {
|
||||
@@ -973,6 +1038,81 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The same path sampled by `parts` pointer events per corner-to-corner leg, unevenly.
|
||||
fn sampled(path: &[[f64; 3]], parts: usize) -> Vec<Vec<f64>> {
|
||||
let mut out = vec![path[0].to_vec()];
|
||||
for w in path.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
// Uneven steps, as a real pointer gives: t grows quadratically.
|
||||
for k in 1..=parts {
|
||||
let t = (k * k) as f64 / (parts * parts) as f64;
|
||||
out.push((0..3).map(|i| a[i] + (b[i] - a[i]) * t).collect());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// #2606: dabs restarted at every pointer event, so a fast mouse (few far-apart events) and a
|
||||
/// slow one (many close ones) warped the same path differently. Spacing now runs on along the
|
||||
/// whole stroke: any sampling of a path gives the same field.
|
||||
#[test]
|
||||
fn the_result_does_not_depend_on_how_finely_the_path_was_sampled() {
|
||||
use LiquifyTool::*;
|
||||
let path = [[20.0, 20.0, 1.0], [62.0, 26.0, 0.6], [70.0, 50.0, 0.9], [28.0, 55.0, 1.0], [30.0, 30.0, 1.0]];
|
||||
for tool in [ForwardWarp, PushLeft, TwirlCw, Bloat, Reconstruct, Smooth, Freeze] {
|
||||
for size in [12.0, 40.0] {
|
||||
let field = |parts: usize| {
|
||||
let mut warp = LiquifyStroke::new(ForwardWarp, 30.0);
|
||||
warp.points = vec![vec![30.0, 30.0], vec![50.0, 40.0]];
|
||||
let mut s = LiquifyStroke::new(tool, size);
|
||||
s.points = sampled(&path, parts);
|
||||
// Some displacement first, so Reconstruct and Smooth have something to change.
|
||||
LiquifyField::from_strokes(bounds(), 2.0, &[warp, s])
|
||||
};
|
||||
let coarse = field(1);
|
||||
for parts in [3, 17, 60] {
|
||||
let fine = field(parts);
|
||||
let dd = coarse.d.iter().zip(&fine.d).map(|(p, q)| (p[0] - q[0]).abs().max((p[1] - q[1]).abs())).fold(0.0f32, f32::max);
|
||||
let df = coarse.freeze.iter().zip(&fine.freeze).map(|(p, q)| (p - q).abs()).fold(0.0f32, f32::max);
|
||||
assert!(dd < 1e-3 && df < 1e-4, "{tool:?} {size}px, {parts} events per leg: field differs by {dd} px, mask by {df}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A long drag whose events land exactly on dab positions, along a diagonal: rounding must not
|
||||
/// add or drop the dab at a segment's end, or the deep displacement a long Forward Warp builds
|
||||
/// up differs by a whole dab between a slow and a fast pointer.
|
||||
#[test]
|
||||
fn a_long_diagonal_drag_gives_one_field_at_any_event_spacing() {
|
||||
let field = |step: f64| {
|
||||
let mut s = LiquifyStroke::new(LiquifyTool::ForwardWarp, 40.0);
|
||||
s.points = (0..=(300.0 / step) as usize).map(|k| vec![40.0 + k as f64 * step * 0.6, 20.0 + k as f64 * step * 0.8]).collect();
|
||||
LiquifyField::from_strokes(Rect::new(0, 0, 320, 300), 2.0, &[s])
|
||||
};
|
||||
let (slow, fast) = (field(2.0), field(60.0));
|
||||
let dd = slow.d.iter().zip(&fast.d).map(|(p, q)| (p[0] - q[0]).abs().max((p[1] - q[1]).abs())).fold(0.0f32, f32::max);
|
||||
assert!(slow.max_displacement() > 50.0, "the drag builds up a deep displacement");
|
||||
assert!(dd < 1e-3, "slow and fast pointer fields differ by {dd} px");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stroke_interrupted_by_a_jump_restarts_its_spacing() {
|
||||
// A segment that does not start where the last one ended begins a new run of dabs there.
|
||||
let s = LiquifyStroke::new(LiquifyTool::ForwardWarp, 20.0);
|
||||
let mut f = LiquifyField::new(bounds(), 2.0);
|
||||
f.stroke_begin(&s, [10.0, 10.0, 1.0]);
|
||||
f.stroke_segment(&s, [10.0, 10.0, 1.0], [10.3, 10.0, 1.0]);
|
||||
let mut g = LiquifyField::new(bounds(), 2.0);
|
||||
f.stroke_segment(&s, [40.0, 30.0, 1.0], [50.0, 30.0, 1.0]);
|
||||
g.stroke_begin(&s, [40.0, 30.0, 1.0]);
|
||||
g.stroke_segment(&s, [40.0, 30.0, 1.0], [50.0, 30.0, 1.0]);
|
||||
assert_eq!(f, g);
|
||||
// Hostile points: nothing panics and the next segment starts afresh.
|
||||
f.stroke_segment(&s, [f64::NAN, 3.0, 1.0], [4.0, f64::INFINITY, 1.0]);
|
||||
f.stroke_segment(&s, [4.0, 3.0, f64::NAN], [9.0, 3.0, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_segment_to_a_far_off_point_returns() {
|
||||
// The issue's request: about 3.3e14 steps before the fix. Also a segment whose length
|
||||
@@ -1110,8 +1250,8 @@ mod tests {
|
||||
/// dab's circle (a hard-edged block at Density 0, half strength at the default 50).
|
||||
#[test]
|
||||
fn forward_warp_leaves_the_brush_rim_in_place() {
|
||||
// A drag of one dab spacing (a fifth of the radius): one dab, centred where it started.
|
||||
let (r, drag) = (20.0, 4.0);
|
||||
// A drag of one dab spacing (a twentieth of the radius): one dab, centred where it started.
|
||||
let (r, drag) = (20.0, 1.0);
|
||||
for density in [0.0, 50.0, 100.0] {
|
||||
let mut s = LiquifyStroke::new(LiquifyTool::ForwardWarp, 2.0 * r);
|
||||
s.density = density;
|
||||
|
||||
Reference in new issue
Block a user