mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
This commit is contained in:
@@ -297,6 +297,43 @@ impl Buf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Motion Tile's output frame for a buffer with layer origin `offset` (pixels) at `scale`:
|
||||
/// `(width, height, offset)`. Output Width / Height grow (or crop) the layer rectangle about its
|
||||
/// centre, on adjustment layers too, where the rectangle is the comp's and starts at the comp
|
||||
/// origin in the buffer below (After Effects; a Transform after it brings the tiles into view,
|
||||
/// #1727). An adjustment at 100 % keeps the buffer it was given; one whose frame would pass
|
||||
/// 16 384 pixels a side does too.
|
||||
pub fn motion_tile_frame(ctx: &EffectCtx, offset: [f64; 2], size: (u32, u32), scale: f64) -> (u32, u32, [f64; 2]) {
|
||||
let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]);
|
||||
let (pw, ph) = (ctx.params.f("outputWidth"), ctx.params.f("outputHeight"));
|
||||
let ow = (lw * pw / 100.0).max(1.0 / scale);
|
||||
let oh = (lh * ph / 100.0).max(1.0 / scale);
|
||||
let (nw, nh) = ((ow * scale).round().max(1.0), (oh * scale).round().max(1.0));
|
||||
let grow = [(ow - lw) * 0.5 * scale, (oh - lh) * 0.5 * scale];
|
||||
if !ctx.adjustment {
|
||||
return (nw as u32, nh as u32, grow);
|
||||
}
|
||||
// The grown rectangle's top-left in the buffer below, on whole pixels so the result lines
|
||||
// up with the comp again when the stack ends.
|
||||
let (x0, y0) = ((offset[0] - grow[0]).round(), (offset[1] - grow[1]).round());
|
||||
let fits = [x0, y0, nw, nh].iter().all(|v| v.is_finite()) && nw <= 16384.0 && nh <= 16384.0;
|
||||
if (pw == 100.0 && ph == 100.0) || !fits {
|
||||
return (size.0, size.1, offset);
|
||||
}
|
||||
(nw as u32, nh as u32, [offset[0] - x0, offset[1] - y0])
|
||||
}
|
||||
|
||||
/// Where an adjustment stack's result (comp origin at `offset`, `size` pixels) lies on the canvas
|
||||
/// it came from (comp origin at `canvas_offset`): the result pixel at canvas pixel (0, 0).
|
||||
/// `None` when it already is the canvas, frame for frame.
|
||||
pub fn adjustment_crop(offset: [f64; 2], canvas_offset: [f64; 2], size: (u32, u32), canvas: (u32, u32)) -> Option<(i64, i64)> {
|
||||
let (dx, dy) = ((offset[0] - canvas_offset[0]).round(), (offset[1] - canvas_offset[1]).round());
|
||||
if !dx.is_finite() || !dy.is_finite() || (dx == 0.0 && dy == 0.0 && size == canvas) {
|
||||
return None;
|
||||
}
|
||||
Some((dx as i64, dy as i64))
|
||||
}
|
||||
|
||||
/// Where [`Buf::cover`] would take a buffer: shift its pixels by `(dx, dy)` and resize it to
|
||||
/// `width` × `height` so it spans the `size`-layer-pixel region that starts at its offset — the
|
||||
/// whole layer rectangle, which a sized source's buffer already is. `None` when the buffer
|
||||
|
||||
@@ -433,13 +433,7 @@ fn motion_tile(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let mirror = ctx.params.b("mirrorEdges");
|
||||
let phase = ctx.params.f("phase") / 360.0;
|
||||
let hshift = ctx.params.b("horizontalPhaseShift");
|
||||
let (nw, nh, noff) = if ctx.adjustment {
|
||||
(b.img.width, b.img.height, b.offset)
|
||||
} else {
|
||||
let ow = (lw * ctx.params.f("outputWidth") / 100.0).max(1.0 / s);
|
||||
let oh = (lh * ctx.params.f("outputHeight") / 100.0).max(1.0 / s);
|
||||
((ow * s).round().max(1.0) as u32, (oh * s).round().max(1.0) as u32, [(ow - lw) * 0.5 * s, (oh - lh) * 0.5 * s])
|
||||
};
|
||||
let (nw, nh, noff) = crate::motion_tile_frame(ctx, b.offset, (b.img.width, b.img.height), s);
|
||||
let mut out = Image::new(nw, nh);
|
||||
let src = &b.img;
|
||||
out.rows_mut().for_each(|(y, row)| {
|
||||
|
||||
@@ -1246,6 +1246,24 @@ impl<'g> Enc<'g> {
|
||||
);
|
||||
}
|
||||
|
||||
/// A `w` × `h` image showing `src` with its pixel (`x`, `y`) at the top-left, transparent
|
||||
/// where `src` doesn't reach (Image::crop).
|
||||
pub fn crop(&mut self, src: &GpuImage, x: i64, y: i64, w: u32, h: u32) -> GpuImage {
|
||||
let dst = self.image(w, h);
|
||||
let span = |o: i64, n: u32, len: u32| {
|
||||
let (a, b) = (o.max(0), (o + n as i64).min(len as i64));
|
||||
(b > a).then(|| (a as u32, (a - o) as u32, (b - a) as u32))
|
||||
};
|
||||
let (Some((sx, dx, cw)), Some((sy, dy, ch))) = (span(x, w, src.width), span(y, h, src.height)) else { return dst };
|
||||
let enc = self.encoder();
|
||||
enc.copy_texture_to_texture(
|
||||
wgpu::TexelCopyTextureInfo { texture: &src.texture, mip_level: 0, origin: wgpu::Origin3d { x: sx, y: sy, z: 0 }, aspect: wgpu::TextureAspect::All },
|
||||
wgpu::TexelCopyTextureInfo { texture: &dst.texture, mip_level: 0, origin: wgpu::Origin3d { x: dx, y: dy, z: 0 }, aspect: wgpu::TextureAspect::All },
|
||||
wgpu::Extent3d { width: cw, height: ch, depth_or_array_layers: 1 },
|
||||
);
|
||||
dst
|
||||
}
|
||||
|
||||
/// Submit recorded work.
|
||||
pub fn submit(&mut self) {
|
||||
if let Some((enc, id)) = self.enc.take() {
|
||||
|
||||
@@ -637,13 +637,7 @@ fn motion_tile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let mirror = ctx.params.b("mirrorEdges");
|
||||
let phase = ctx.params.f("phase") / 360.0;
|
||||
let hshift = ctx.params.b("horizontalPhaseShift");
|
||||
let (nw, nh, noff) = if ctx.adjustment {
|
||||
(b.img.width, b.img.height, b.offset)
|
||||
} else {
|
||||
let ow = (lw * ctx.params.f("outputWidth") / 100.0).max(1.0 / s);
|
||||
let oh = (lh * ctx.params.f("outputHeight") / 100.0).max(1.0 / s);
|
||||
((ow * s).round().max(1.0) as u32, (oh * s).round().max(1.0) as u32, [(ow - lw) * 0.5 * s, (oh - lh) * 0.5 * s])
|
||||
};
|
||||
let (nw, nh, noff) = effectcraft_effects::motion_tile_frame(ctx, b.offset, (b.img.width, b.img.height), s);
|
||||
if !e.g.fits(nw, nh) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ fn tiles_kaleida_repetile() {
|
||||
"ec.stylize.motiontile",
|
||||
&[("horizontalPhaseShift", on()), ("phase", n(33.0)), ("tileCenter", pt(30.0, 17.0)), ("tileHeight", n(55.0)), ("outputWidth", n(80.0))],
|
||||
);
|
||||
// Output under 100 % on an adjustment crops the comp below (#1727).
|
||||
effect_case("ec.stylize.motiontile", &[("outputWidth", n(60.0)), ("outputHeight", n(300.0)), ("mirrorEdges", on())]);
|
||||
effect_case("ec.stylize.cckaleida", &[]);
|
||||
effect_case("ec.stylize.cckaleida", &[("mirroring", e(2)), ("rotation", n(20.0)), ("size", n(150.0))]);
|
||||
effect_case("ec.stylize.ccrepetile", &[("expandRight", n(10.0)), ("expandUp", n(4.0))]);
|
||||
@@ -225,3 +227,42 @@ fn transform_motion_blur() {
|
||||
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::from_seconds_f64(0.3)), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Motion Tile's Output size on an adjustment layer with a Transform after it that scales the
|
||||
/// tiles into view, and cropping (Output under 100 %): the stack's grown or cropped frame goes
|
||||
/// back onto the canvas as on the CPU (#1727).
|
||||
#[test]
|
||||
fn adjustment_motion_tile_output_then_transform() {
|
||||
for (out_w, out_h, scale) in [(300.0, 300.0, 40.0), (250.0, 120.0, 70.0), (60.0, 80.0, 100.0)] {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.footage(97, 61);
|
||||
s.push(bg);
|
||||
let mut adj = s.solid([1.0, 1.0, 1.0], 97, 61);
|
||||
adj.switches.adjustment = true;
|
||||
s.effect(&mut adj, "ec.stylize.motiontile", &[("outputWidth", n(out_w)), ("outputHeight", n(out_h)), ("mirrorEdges", on())]);
|
||||
s.effect(&mut adj, "ec.distort.transform", &[("scaleHeight", n(scale)), ("scaleWidth", n(scale))]);
|
||||
s.push(adj);
|
||||
let label = format!("adjustment Motion Tile {out_w}×{out_h} % then Scale {scale} % {depth:?}");
|
||||
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
|
||||
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same on a full-frame footage layer (#1991).
|
||||
#[test]
|
||||
fn layer_motion_tile_output_then_transform() {
|
||||
for (out_w, out_h, scale) in [(300.0, 300.0, 40.0), (250.0, 120.0, 70.0)] {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let mut l = s.footage(97, 61);
|
||||
s.effect(&mut l, "ec.stylize.motiontile", &[("outputWidth", n(out_w)), ("outputHeight", n(out_h)), ("mirrorEdges", on())]);
|
||||
s.effect(&mut l, "ec.distort.transform", &[("scaleHeight", n(scale)), ("scaleWidth", n(scale))]);
|
||||
s.push(l);
|
||||
let label = format!("layer Motion Tile {out_w}×{out_h} % then Scale {scale} % {depth:?}");
|
||||
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
|
||||
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +357,11 @@ fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canv
|
||||
};
|
||||
let mut fx = crate::effects::GpuFx::new(e, below, [0.0; 2], r.opts.scale, pipe.levels);
|
||||
r.run_effects_on(ctx, layer, true, &mut fx);
|
||||
let (mut adjusted, _, _) = fx.finish()?;
|
||||
let (mut adjusted, offset, _) = fx.finish()?;
|
||||
// An effect that changed the frame (Motion Tile's Output size): back onto the canvas.
|
||||
if let Some((dx, dy)) = effectcraft_effects::adjustment_crop(offset, [0.0; 2], (adjusted.width, adjusted.height), (w, h)) {
|
||||
adjusted = e.crop(&adjusted, dx, dy, w, h);
|
||||
}
|
||||
if adjusted.width != w || adjusted.height != h {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
@@ -2052,6 +2052,12 @@ impl<'a> Renderer<'a> {
|
||||
color::convert(&mut below.img, &c);
|
||||
}
|
||||
let mut adjusted = self.apply_effects(ctx, layer, below, true);
|
||||
// An effect that changed the frame (Motion Tile's Output size): back onto the canvas.
|
||||
if let Some((dx, dy)) =
|
||||
effectcraft_effects::adjustment_crop(adjusted.offset, o, (adjusted.img.width, adjusted.img.height), (canvas.width, canvas.height))
|
||||
{
|
||||
adjusted.img = adjusted.img.crop(dx, dy, canvas.width, canvas.height);
|
||||
}
|
||||
if let Some(c) = self.pipe.to_blend() {
|
||||
color::convert(&mut adjusted.img, &c);
|
||||
}
|
||||
@@ -2431,6 +2437,8 @@ mod tests_mask_blur;
|
||||
#[cfg(test)]
|
||||
mod tests_mask_expansion;
|
||||
#[cfg(test)]
|
||||
mod tests_motion_tile;
|
||||
#[cfg(test)]
|
||||
mod tests_nested;
|
||||
#[cfg(test)]
|
||||
mod tests_paint;
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
//! Motion Tile's Output Width / Height on adjustment layers and on full-frame layers, with a
|
||||
//! Transform after it that scales the tiled result into view (#1727, #1991).
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{Comp, ItemId, ItemKind, Layer, LayerSource, Project, Solid};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::{Image, render_frame};
|
||||
|
||||
const W: u32 = 200;
|
||||
const H: u32 = 100;
|
||||
|
||||
fn solid(p: &mut Project, comp: &Comp, color: [f32; 3], w: u32, h: u32, pos: [f64; 2]) -> Layer {
|
||||
let sid = p.add_item("Solid", Label::Red, None, ItemKind::Solid(Solid { color, width: w, height: h, pixel_aspect: 1.0 }));
|
||||
let mut l = build::layer(p, comp, "Solid", LayerSource::Solid { item: sid }, (w, h), None);
|
||||
l.props.prop_mut("transform/position").unwrap().value = Value::Vec3([pos[0], pos[1], 0.0]);
|
||||
l
|
||||
}
|
||||
|
||||
/// A white frame with a red 20×20 square in its top-left corner.
|
||||
fn picture(p: &mut Project, comp: &Comp) -> Vec<Layer> {
|
||||
vec![solid(p, comp, [1.0, 0.0, 0.0], 20, 20, [10.0, 10.0]), solid(p, comp, [1.0, 1.0, 1.0], W, H, [100.0, 50.0])]
|
||||
}
|
||||
|
||||
/// Motion Tile (Output `out` %) then Transform (Scale `scale` %), on `l`.
|
||||
fn tile_then_scale(p: &mut Project, l: &mut Layer, out: f64, scale: f64) {
|
||||
let size = [W as f64, H as f64];
|
||||
for (id, vals) in [
|
||||
("ec.stylize.motiontile", vec![("outputWidth", Value::Scalar(out)), ("outputHeight", Value::Scalar(out))]),
|
||||
("ec.distort.transform", vec![("scaleHeight", Value::Scalar(scale)), ("scaleWidth", Value::Scalar(scale))]),
|
||||
] {
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let mut next = p.next_id;
|
||||
let mut g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), spec.name, size);
|
||||
p.next_id = next;
|
||||
for (k, v) in vals {
|
||||
g.prop_mut(k).unwrap().value = v;
|
||||
}
|
||||
l.props.sub_mut("effects").unwrap().children.push(g.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn comp() -> (Project, ItemId, Comp) {
|
||||
let mut p = Project::default();
|
||||
p.settings.bit_depth = effectcraft_project::BitDepth::Bpc32;
|
||||
let comp = Comp::new(W, H, FrameRate::FPS_30, Tick::from_seconds_f64(1.0));
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
||||
(p, cid, comp)
|
||||
}
|
||||
|
||||
/// The picture under an adjustment layer carrying the two effects.
|
||||
fn adjustment_scene(out: f64, scale: f64) -> (Project, ItemId) {
|
||||
let (mut p, cid, comp) = comp();
|
||||
let mut adj = solid(&mut p, &comp, [1.0, 1.0, 1.0], W, H, [100.0, 50.0]);
|
||||
adj.switches.adjustment = true;
|
||||
tile_then_scale(&mut p, &mut adj, out, scale);
|
||||
let mut layers = vec![adj];
|
||||
layers.extend(picture(&mut p, &comp));
|
||||
p.comp_mut(cid).unwrap().layers = layers;
|
||||
(p, cid)
|
||||
}
|
||||
|
||||
/// The picture as a full-frame precomp layer carrying the two effects.
|
||||
fn layer_scene(out: f64, scale: f64) -> (Project, ItemId) {
|
||||
let (mut p, cid, comp) = comp();
|
||||
let inner = Comp::new(W, H, FrameRate::FPS_30, Tick::from_seconds_f64(1.0));
|
||||
let iid = p.add_item("Picture", Label::Sandstone, None, ItemKind::Comp(inner.clone().into()));
|
||||
let layers = picture(&mut p, &inner);
|
||||
p.comp_mut(iid).unwrap().layers = layers;
|
||||
let mut pre = build::layer(&mut p, &comp, "Picture", LayerSource::Comp { item: iid }, (W, H), None);
|
||||
tile_then_scale(&mut p, &mut pre, out, scale);
|
||||
p.comp_mut(cid).unwrap().layers.push(pre);
|
||||
(p, cid)
|
||||
}
|
||||
|
||||
fn is(img: &Image, x: i64, y: i64, want: [f32; 4], what: &str) {
|
||||
let got = img.get(x, y);
|
||||
assert!(got.iter().zip(want).all(|(a, b)| (a - b).abs() < 0.02), "{what}: ({x}, {y}) is {got:?}, want {want:?}");
|
||||
}
|
||||
|
||||
const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
|
||||
const WHITE: [f32; 4] = [1.0; 4];
|
||||
const CLEAR: [f32; 4] = [0.0; 4];
|
||||
|
||||
/// Output 300 % then Scale 50 % about the frame centre: comp pixel (x, y) shows the tiled frame
|
||||
/// at (100 + 2(x − 100), 50 + 2(y − 50)), so the red square repeats every 100 × 50 pixels and
|
||||
/// the frame is covered edge to edge, the tiles from beyond the frame included.
|
||||
fn assert_tiled(img: &Image, what: &str) {
|
||||
for (x, y) in [(55, 30), (155, 30), (55, 80), (155, 80)] {
|
||||
is(img, x, y, RED, what);
|
||||
}
|
||||
for (x, y) in [(5, 5), (195, 5), (5, 95), (195, 95), (100, 50), (100, 5)] {
|
||||
is(img, x, y, WHITE, what);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjustment_motion_tile_output_spreads_tiles_past_the_frame() {
|
||||
let (p, cid) = adjustment_scene(300.0, 50.0);
|
||||
assert_tiled(&render_frame(&p, cid, Tick::ZERO, 1.0), "adjustment, Output 300 %");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_motion_tile_output_spreads_tiles_past_the_frame() {
|
||||
let (p, cid) = layer_scene(300.0, 50.0);
|
||||
assert_tiled(&render_frame(&p, cid, Tick::ZERO, 1.0), "full-frame layer, Output 300 %");
|
||||
}
|
||||
|
||||
/// Output under 100 % crops: only the centre 50 % of the frame is left, the rest is transparent
|
||||
/// (After Effects shows the comp background there).
|
||||
#[test]
|
||||
fn adjustment_motion_tile_output_under_100_crops_the_frame() {
|
||||
let (p, cid) = adjustment_scene(50.0, 100.0);
|
||||
let img = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
is(&img, 100, 50, WHITE, "inside the kept centre");
|
||||
is(&img, 60, 30, WHITE, "inside the kept centre");
|
||||
for (x, y) in [(5, 5), (10, 10), (195, 95), (45, 50), (100, 20)] {
|
||||
is(&img, x, y, CLEAR, "outside the kept centre");
|
||||
}
|
||||
}
|
||||
|
||||
/// The After Effects 2026 frames in #1727: a 1920×1080 red solid under an adjustment layer with
|
||||
/// Motion Tile (Output 910 × 340 %, then 90 × 340 %) and Transform (Scale 12 %, Position 1361, 540).
|
||||
/// The tiled frame, 9.1 × 3.4 comps scaled to 12 %, is a 2097 × 441 band centred on (1361, 540):
|
||||
/// x 313–1920 (cut by the frame), y 320–760; at 90 % a 207-pixel column, x 1257–1465.
|
||||
#[test]
|
||||
fn adjustment_motion_tile_matches_the_after_effects_frames() {
|
||||
for (out_w, x0, x1) in [(910.0, 313.0, 1920.0), (90.0, 1257.0, 1465.0)] {
|
||||
let mut p = Project::default();
|
||||
p.settings.bit_depth = effectcraft_project::BitDepth::Bpc32;
|
||||
let comp = Comp::new(1920, 1080, FrameRate::FPS_30, Tick::from_seconds_f64(1.0));
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
||||
let red = solid(&mut p, &comp, [1.0, 0.0, 0.0], 1920, 1080, [960.0, 540.0]);
|
||||
let mut adj = solid(&mut p, &comp, [1.0, 1.0, 1.0], 1920, 1080, [960.0, 540.0]);
|
||||
adj.switches.adjustment = true;
|
||||
for (id, vals) in [
|
||||
("ec.stylize.motiontile", vec![("outputWidth", Value::Scalar(out_w)), ("outputHeight", Value::Scalar(340.0))]),
|
||||
(
|
||||
"ec.distort.transform",
|
||||
vec![("scaleHeight", Value::Scalar(12.0)), ("scaleWidth", Value::Scalar(12.0)), ("position", Value::Vec2([1361.0, 540.0]))],
|
||||
),
|
||||
] {
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let mut next = p.next_id;
|
||||
let mut g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), spec.name, [1920.0, 1080.0]);
|
||||
p.next_id = next;
|
||||
for (k, v) in vals {
|
||||
g.prop_mut(k).unwrap().value = v;
|
||||
}
|
||||
adj.props.sub_mut("effects").unwrap().children.push(g.into());
|
||||
}
|
||||
p.comp_mut(cid).unwrap().layers = vec![adj, red];
|
||||
// Quarter resolution: comp pixel (x, y) is output pixel (x / 4, y / 4).
|
||||
let img = render_frame(&p, cid, Tick::ZERO, 0.25);
|
||||
let at = |x: f64, y: f64| img.get((x / 4.0) as i64, (y / 4.0) as i64);
|
||||
let what = format!("Output {out_w} %");
|
||||
for (x, y) in [(x0 + 8.0, 540.0), (x1 - 8.0, 540.0), (1361.0, 328.0), (1361.0, 752.0)] {
|
||||
assert_eq!(at(x, y), RED, "{what}: ({x}, {y}) is red");
|
||||
}
|
||||
for (x, y) in [(x0 - 8.0, 540.0), (1361.0, 312.0), (1361.0, 768.0), (100.0, 100.0)] {
|
||||
assert_eq!(at(x, y), CLEAR, "{what}: ({x}, {y}) is empty");
|
||||
}
|
||||
if x1 < 1920.0 {
|
||||
assert_eq!(at(x1 + 8.0, 540.0), CLEAR, "{what}: right of the column");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user