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:
sanyamsingh001
2026-10-18 21:48:26 +00:00
co-authored by Codex
130 changed files with 6748 additions and 448 deletions
+30 -2
View File
@@ -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)
}
+18 -2
View File
@@ -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]
+21 -1
View File
@@ -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};
+1 -1
View File
@@ -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 {