mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 02:11:41 -06:00
Use symmetric HexTile cell coordinates on CPU and GPU
Keep the existing regression and tolerance while preserving upstream WireRemoval and test-service recovery changes. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -301,7 +301,8 @@ impl OffsetCurve {
|
||||
}
|
||||
let n = ext.len();
|
||||
// Tangents: the mean of the neighbouring slopes, flat at local extrema and next to flat
|
||||
// segments so the curve never overshoots its points.
|
||||
// segments, and at most three times either slope (Fritsch-Carlson) so the curve never
|
||||
// overshoots its points, even beside a short steep segment.
|
||||
let slope = |k: usize| (ext[k + 1].1 - ext[k].1) / (ext[k + 1].0 - ext[k].0).max(1e-6);
|
||||
let m: Vec<f32> = (0..n)
|
||||
.map(|k| {
|
||||
@@ -309,7 +310,12 @@ impl OffsetCurve {
|
||||
return 0.0;
|
||||
}
|
||||
let (a, b) = (slope(k - 1), slope(k));
|
||||
if a * b <= 0.0 { 0.0 } else { (a + b) * 0.5 }
|
||||
if a * b <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
let lim = 3.0 * a.abs().min(b.abs());
|
||||
((a + b) * 0.5).clamp(-lim, lim)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let eval = |x: f32| -> f32 {
|
||||
@@ -989,6 +995,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hue_saturation_curve_stays_between_its_points() {
|
||||
// #1656: a point dragged to the bottom right next to another one. The steep short
|
||||
// segment used to pull the long one beside it far outside the box (|offset| > 0.5).
|
||||
// The third pair sits across the hue wrap (0.98 and 0.005 are neighbours).
|
||||
let cases = [("0.3,0.4 0.31,0 0.7,0.6", true), ("0.1,0.3 0.3,0.5 0.31,1 0.7,0.5", false), ("0.5,0.6 0.98,0.4 0.005,0", true)];
|
||||
for (pts, periodic) in cases {
|
||||
let c = OffsetCurve::parse(pts, periodic).unwrap();
|
||||
for i in 0..=1000 {
|
||||
let x = i as f32 / 1000.0;
|
||||
let v = c.at(x);
|
||||
assert!((-0.5..=0.5).contains(&v), "{pts} at {x}: {v}");
|
||||
}
|
||||
for p in parse_points(pts) {
|
||||
assert!((c.at(p.0) - (p.1 - 0.5)).abs() < 0.02, "{pts}: passes through {p:?}");
|
||||
}
|
||||
if periodic {
|
||||
assert!((c.at(0.0) - c.at(1.0)).abs() < 1e-4, "{pts}: no seam at the wrap");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn close_px(a: [f32; 4], b: [f32; 4], eps: f32) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() <= eps)
|
||||
}
|
||||
|
||||
@@ -164,7 +164,10 @@ fn wave_warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let kind = ctx.params.e("waveType");
|
||||
let pin = ctx.params.e("pinning");
|
||||
let seed = ctx.seed ^ 0x3a7e;
|
||||
let (dx, dy) = (dir.sin(), -dir.cos());
|
||||
// Cardinal directions must not acquire a tiny perpendicular slope: at a square-wave
|
||||
// boundary cos(90°)'s rounding residue can flip a whole column on CPU but not GPU.
|
||||
let axis = |v: f64| if v.abs() < 4.0 * f64::EPSILON { 0.0 } else { v };
|
||||
let (dx, dy) = (axis(dir.sin()), -axis(dir.cos()));
|
||||
if !ctx.adjustment {
|
||||
b.pad(h.abs().ceil() as u32 + 1);
|
||||
}
|
||||
@@ -556,8 +559,21 @@ mod tests {
|
||||
let (a, b) = (o.img.get(pad, y + pad), im.get(0, y));
|
||||
assert!((0..4).all(|i| (a[i] - b[i]).abs() < 0.01), "row {y}: {a:?} vs {b:?}");
|
||||
}
|
||||
let free = run("ec.distort.wavewarp", &[("direction", num(0.0)), ("height", num(5.0))], im);
|
||||
let free = run("ec.distort.wavewarp", &[("direction", num(0.0)), ("height", num(5.0))], im.clone());
|
||||
assert!(free.img.data.iter().zip(&o.img.data).any(|(a, b)| differs(*a, *b)));
|
||||
// #1181: direction 90° → vertical displacement. Top Edge (4) holds the first row,
|
||||
// Bottom Edge (6) the last, and the rest of the layer still moves.
|
||||
for (pin, edge_row) in [(Value::Enum(4), 0i64), (Value::Enum(6), 29i64)] {
|
||||
let set = [("direction", num(90.0)), ("height", num(5.0)), ("pinning", pin)];
|
||||
let o = run("ec.distort.wavewarp", &set, im.clone());
|
||||
let pad = ((o.img.width - im.width) / 2) as i64;
|
||||
for x in 0..40 {
|
||||
let (a, b) = (o.img.get(x + pad, edge_row + pad), im.get(x, edge_row));
|
||||
assert!((0..4).all(|i| (a[i] - b[i]).abs() < 0.01), "pin {edge_row} col {x}: {a:?} vs {b:?}");
|
||||
}
|
||||
let mid = 15;
|
||||
assert!((0..40).any(|x| differs(o.img.get(x + pad, mid + pad), im.get(x, mid))), "pin {edge_row}: the middle of the layer must still distort");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -363,7 +363,9 @@ fn wire_removal(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (qx, qy) = (x as f64 + 0.5 - ax, y as f64 + 0.5 - ay);
|
||||
let d = qx * nx + qy * ny;
|
||||
let ad = d.abs();
|
||||
if ad > half + soft_w {
|
||||
// The wire is the segment A to B, not an endless line.
|
||||
let along = qx * tx + qy * ty;
|
||||
if !(0.0..=len).contains(&along) || ad > half + soft_w {
|
||||
continue;
|
||||
}
|
||||
// Positions just outside the wire on both sides.
|
||||
@@ -580,6 +582,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_removal_stays_between_a_and_b() {
|
||||
let mut img = Image::filled(20, 20, [0.5, 0.5, 0.5, 1.0]);
|
||||
for y in 0..20 {
|
||||
img.set(10, y, [0.0, 0.0, 0.0, 1.0]);
|
||||
}
|
||||
let out = run_fx(
|
||||
"ec.key.ccsimplewireremoval",
|
||||
&[("pointA", Value::Vec2([10.5, 5.0])), ("pointB", Value::Vec2([10.5, 15.0])), ("removalStyle", Value::Enum(0)), ("thickness", num(3.0))],
|
||||
img.clone(),
|
||||
0.0,
|
||||
EffectEnv::default(),
|
||||
);
|
||||
assert!((out.img.get(10, 10)[0] - 0.5).abs() < 0.05, "inside the segment");
|
||||
assert_eq!(out.img.get(10, 2), img.get(10, 2), "before A");
|
||||
assert_eq!(out.img.get(10, 18), img.get(10, 18), "after B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_removal_frame_offset_uses_neighbour_frame() {
|
||||
use crate::{EffectHost, LayerPixels};
|
||||
|
||||
@@ -351,7 +351,7 @@ fn hex_cell(x: f64, y: f64, r: f64) -> ((f64, f64), (i64, i64)) {
|
||||
let rr = (2.0 / 3.0 * y) / r;
|
||||
// Cube rounding.
|
||||
let (cx, cz) = (q, rr);
|
||||
let cy = -cx - cz;
|
||||
let cy = (-3f64.sqrt() / 3.0 * x - y / 3.0) / r;
|
||||
let (mut rx, ry, mut rz) = (cx.round(), cy.round(), cz.round());
|
||||
let (dx, dy, dz) = ((rx - cx).abs(), (ry - cy).abs(), (rz - cz).abs());
|
||||
if dx > dy && dx > dz {
|
||||
|
||||
Reference in New Issue
Block a user