formats: a settled screen is one instant, not one hold per element
`Element::rest()` picks each element's last hold keyframe independently of every other element, so a composite built from it is not the screen at any moment in time -- it is a per-element maximum. For a transient that is exactly wrong: a two-frame flash's last hold IS the flash peak, so it burns forever. GP_TITLE build 4 is the case. `ptlogo_back2eff1`..`eff5` are five staggered two-frame flashes -- one light sweep drawn as five frames, all extinguished by t110 -- that `rest()` draws simultaneously and permanently. Five stacked white glows saturate the light arc behind the logo. The disc names the right instant: the midpoint of the longest interval containing no keyframe of any element. `UiBuild::settle_time()` and `settle_window()`; `screen render --settle` applies it and prints the window, whose width is how much the midpoint is worth. Predicted t=198 from [160,236] BEFORE scoring. Against the console capture, the arc band goes 33.22 -> 11.79 and pixels at the clipping level 8581 -> 1452, where the console has 1459 -- an unfitted statistic. Whole frame 14.07 -> 12.06. Controls at t=100 and t=358 are far worse, and a hand-picked visibility list reaches the identical numbers. `ComposeOptions::at` now poses every element rather than leaves only, which is why the earlier rotation pose scan was flat: it moved the sweeps and never touched the top-level flashes. `at = None` is byte-identical (cmp), the pre-rotation tag renders identically at rest, and the 13 paint-order tests plus the keyframe/focus/opt-link disc tests are green. Also fixes the diagnostic that caused a wrong finding to be sent to the port agent: `not drawn` listed bare names, and a kind-0x4 ghost carries its template's name, so four ghosts printed as `ptlogo1.t32`/`ptlogo2.t32` and read as "the logo is missing". It now prints index, name and reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
@@ -196,13 +196,22 @@ enum ScreenCommands {
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
/// Pose every element at this KEYFRAME TIME instead of at its resting
|
||||
/// pose (60 units = 1 second). The resting pose is the last *hold*
|
||||
/// keyframe, which is the settled screen — wrong for anything still
|
||||
/// moving. The title's two light sweeps hold off the right edge, so a
|
||||
/// resting composite omits them; `--at 358` puts them where a capture
|
||||
/// taken mid-sweep has them.
|
||||
#[arg(long)]
|
||||
/// pose (60 units = 1 second). The resting pose is each element's last
|
||||
/// *hold* keyframe, picked independently of every other element — so it
|
||||
/// is not the screen at any one moment, and it is wrong twice over: it
|
||||
/// omits anything still moving (the title's light sweeps hold off the
|
||||
/// right edge), and it freezes a transient at its PEAK (the title's five
|
||||
/// two-frame flashes burn forever). Prefer `--settle`.
|
||||
#[arg(long, conflicts_with = "settle")]
|
||||
at: Option<u32>,
|
||||
/// Pose every element at the instant the screen is SETTLED, derived from
|
||||
/// the disc: the midpoint of the longest interval containing no keyframe
|
||||
/// of any element. Prints the window it used, whose width is how much the
|
||||
/// midpoint is worth — a narrow one means the bundle never settles (42 %
|
||||
/// of them, mostly `loop*` fragments). See
|
||||
/// `docs/re/structures/ui-settle-time.md`.
|
||||
#[arg(long)]
|
||||
settle: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -362,8 +371,9 @@ async fn main() -> Result<()> {
|
||||
all,
|
||||
primitives,
|
||||
at,
|
||||
settle,
|
||||
} => cmd_screen_render(
|
||||
&pak, &output, build, focus, animated, black, all, primitives, at,
|
||||
&pak, &output, build, focus, animated, black, all, primitives, at, settle,
|
||||
),
|
||||
},
|
||||
Commands::Save { cmd } => match cmd {
|
||||
@@ -591,12 +601,39 @@ fn cmd_screen_render(
|
||||
all: bool,
|
||||
primitives: bool,
|
||||
at: Option<u32>,
|
||||
settle: bool,
|
||||
) -> Result<()> {
|
||||
use sylpheed_formats::ui_layout::{self, ComposeOptions};
|
||||
let builds = screen_builds(pak, all)?;
|
||||
let idx = pick_build(&builds, want)?;
|
||||
let bytes = &builds[idx].1;
|
||||
let b = ui_layout::parse_build(bytes).context("build did not parse")?;
|
||||
let at = if settle {
|
||||
match (b.settle_window(), b.settle_time()) {
|
||||
(Some((lo, hi)), Some(t)) => {
|
||||
// Report the width, not just the answer. A 4-unit window and a
|
||||
// 190-unit one give the same kind of number and mean entirely
|
||||
// different things.
|
||||
println!(
|
||||
"settle window [{lo}, {hi}] = {} units ({:.2} s) -> posing at t={t}{}",
|
||||
hi - lo,
|
||||
(hi - lo) as f64 / 60.0,
|
||||
if hi - lo < 30 {
|
||||
" ⚠️ narrow — this bundle may never settle"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
Some(t)
|
||||
}
|
||||
_ => {
|
||||
println!("no settle window (fewer than two distinct keyframe times) — using rest()");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
at
|
||||
};
|
||||
let screen = ui_layout::compose(
|
||||
&b,
|
||||
bytes,
|
||||
@@ -632,14 +669,35 @@ fn cmd_screen_render(
|
||||
if !screen.missing.is_empty() {
|
||||
println!(" sprites that did not resolve/decode: {:?}", screen.missing);
|
||||
}
|
||||
let undrawn: Vec<&str> = b
|
||||
// 🔴 Report the INDEX, the KIND and WHY, not just the name. A kind-`0x4`
|
||||
// ghost instance carries its template's name, so a bare name list shows
|
||||
// `ptlogo1.t32` twice and reads as "the logo is missing" when what is
|
||||
// skipped is two motion-trail ghosts sitting at alpha 0 off-screen. That
|
||||
// misreading cost this project a wrong finding sent to another agent.
|
||||
let undrawn: Vec<String> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| !screen.drawn.contains(&e.index))
|
||||
.map(|e| e.name.as_str())
|
||||
.map(|e| {
|
||||
let why = if e.name.ends_with(".prm") {
|
||||
"untextured primitive, needs --primitives"
|
||||
} else if e.name.ends_with(".rat") {
|
||||
"animation, needs --animated"
|
||||
} else if e.kind == 0x4 {
|
||||
"kind 0x4 ghost instance"
|
||||
} else if e.rest().map(|k| k.fade >> 24) == Some(0) {
|
||||
"transparent at its pose"
|
||||
} else {
|
||||
"no reason established"
|
||||
};
|
||||
format!("[{}] {} ({why})", e.index, e.name)
|
||||
})
|
||||
.collect();
|
||||
if !undrawn.is_empty() {
|
||||
println!(" not drawn ({}): {undrawn:?}", undrawn.len());
|
||||
println!(" not drawn ({}):", undrawn.len());
|
||||
for u in &undrawn {
|
||||
println!(" {u}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
19
crates/sylpheed-formats/examples/kf_timeline.rs
Normal file
19
crates/sylpheed-formats/examples/kf_timeline.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main(){
|
||||
let mut a=std::env::args().skip(1);
|
||||
let pk=a.next().unwrap(); let bi:usize=a.next().unwrap().parse().unwrap();
|
||||
let ar=pak::PakArchive::open(pk).unwrap();
|
||||
let by=ar.read(&ar.entries()[bi]).unwrap();
|
||||
let b=ui_layout::parse_build(&by).unwrap();
|
||||
for t in a {
|
||||
let i:usize=t.parse().unwrap();
|
||||
let e=&b.elements[i];
|
||||
println!("[{i}] {} kind=0x{:x} parent={:?} pivot=({},{}) kfs={}",
|
||||
e.name, e.kind, e.parent, e.pivot_x, e.pivot_y, e.keyframes.len());
|
||||
for k in &e.keyframes {
|
||||
println!(" t={:<5} a={:<4} xy=({},{}) s={}/{} rot={} u4={} u8={} tint={:08x}",
|
||||
k.time.map(|v|v as i64).unwrap_or(-1), k.fade>>24, k.x,k.y,
|
||||
k.scale_x,k.scale_y,k.rotation_deg,k.unknown_4,k.unknown_8,k.tint);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
crates/sylpheed-formats/examples/loo_band.rs
Normal file
26
crates/sylpheed-formats/examples/loo_band.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Leave-one-out over the elements that touch a band, dumping raw RGBA each time.
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main(){
|
||||
let mut a=std::env::args().skip(1);
|
||||
let pk=a.next().unwrap();
|
||||
let bi:usize=a.next().unwrap().parse().unwrap();
|
||||
let dir=a.next().unwrap();
|
||||
let ar=pak::PakArchive::open(pk).unwrap();
|
||||
let by=ar.read(&ar.entries()[bi]).unwrap();
|
||||
let b=ui_layout::parse_build(&by).unwrap();
|
||||
let o=ui_layout::ComposeOptions{ backdrop:[0,0,0,255], ..Default::default() };
|
||||
let n=b.elements.len();
|
||||
let base=ui_layout::compose(&b,&by,o.clone(),None);
|
||||
std::fs::write(format!("{dir}/base.raw"),&base.rgba).unwrap();
|
||||
println!("canvas {}x{} elements {n}", base.width, base.height);
|
||||
for i in 0..n {
|
||||
let e=&b.elements[i];
|
||||
let Some(kf)=e.rest() else { continue };
|
||||
// only bother with elements whose rest pose can touch the band
|
||||
let mut v=vec![true;n]; v[i]=false;
|
||||
let c=ui_layout::compose(&b,&by,o.clone(),Some(&v));
|
||||
std::fs::write(format!("{dir}/wo-{i}.raw"),&c.rgba).unwrap();
|
||||
println!("{i}\t{}\t{}\t({},{})\ta={}", e.name,
|
||||
e.sprite.clone().unwrap_or_default(), kf.x, kf.y, kf.fade>>24);
|
||||
}
|
||||
}
|
||||
31
crates/sylpheed-formats/examples/settle_window.rs
Normal file
31
crates/sylpheed-formats/examples/settle_window.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
// The settled screen, computed from the keyframe times alone.
|
||||
//
|
||||
// `rest()` picks each element's last HOLD keyframe independently, which is right
|
||||
// for an element that ends settled and wrong for a transient: a 2-frame flash
|
||||
// holds at its PEAK, so `rest()` leaves it burning forever. The settled screen is
|
||||
// instead one INSTANT that every element is posed at, and the instant to pick is
|
||||
// inside the longest interval during which no element has a keyframe at all.
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main(){
|
||||
let mut a=std::env::args().skip(1);
|
||||
let pk=a.next().unwrap();
|
||||
let ar=pak::PakArchive::open(&pk).unwrap();
|
||||
let only:Option<usize>=a.next().and_then(|s|s.parse().ok());
|
||||
for (i,e) in ar.entries().iter().enumerate() {
|
||||
if let Some(o)=only { if o!=i { continue } }
|
||||
let Ok(by)=ar.read(e) else { continue };
|
||||
let Some(b)=ui_layout::parse_build(&by) else { continue };
|
||||
if b.elements.len()<2 { continue }
|
||||
let mut ts:Vec<u32>=b.elements.iter().flat_map(|el|
|
||||
el.keyframes.iter().filter_map(|k|k.time)).collect();
|
||||
if ts.len()<2 { continue }
|
||||
ts.sort_unstable(); ts.dedup();
|
||||
// longest gap between consecutive keyframe times
|
||||
let (mut best,mut lo,mut hi)=(0u32,0u32,0u32);
|
||||
for w in ts.windows(2) {
|
||||
if w[1]-w[0] > best { best=w[1]-w[0]; lo=w[0]; hi=w[1]; }
|
||||
}
|
||||
println!("{:>4} {:<28} times {:>3} span {:>4} settle window [{lo},{hi}] = {best} units ({:.2}s) -> t={}",
|
||||
i, format!("{:08x}",e.name_hash), ts.len(), ts.last().unwrap(), best as f64/60.0, lo+best/2);
|
||||
}
|
||||
}
|
||||
@@ -194,11 +194,20 @@ impl Element {
|
||||
/// agree — a group that ramps through every frame and never holds.
|
||||
/// The element's pose at keyframe time `t`, linearly interpolated.
|
||||
///
|
||||
/// `rest()` returns the last HOLD keyframe, which is the settled screen. That
|
||||
/// is the wrong pose for anything still moving: the title's light sweeps hold
|
||||
/// at `x = 1521`, off the right edge, so a resting composite deletes them
|
||||
/// rather than settling them. A capture taken mid-animation can only be
|
||||
/// compared against a render posed at the same instant.
|
||||
/// `rest()` returns the last HOLD keyframe **of one element, chosen
|
||||
/// independently of every other element**. That is the wrong pose twice over:
|
||||
///
|
||||
/// * for anything still moving — the title's light sweeps hold at `x = 1521`,
|
||||
/// off the right edge, so a resting composite deletes them rather than
|
||||
/// settling them;
|
||||
/// * 🔴 and for anything **transient**. `ptlogo_back2eff1` is a two-frame
|
||||
/// flash (`a=0` until t52, `255` at t54–56, `0` again by t58); its last
|
||||
/// hold *is* the flash peak, so `rest()` leaves it burning forever. Five
|
||||
/// such flashes stack on the title and blow the light arc out to pure
|
||||
/// white — see `docs/re/structures/ui-settle-time.md`.
|
||||
///
|
||||
/// A settled screen is one INSTANT that every element is posed at, which is
|
||||
/// what [`UiBuild::settle_time`] computes and `ComposeOptions::at` applies.
|
||||
///
|
||||
/// The ramp is linear (`docs/re/ui-keyframe-time-unit.md`), and a group
|
||||
/// **holds** at its last keyframe rather than looping, so `t` past the end
|
||||
@@ -988,6 +997,51 @@ fn measured_paint_order(build: &UiBuild) -> Option<Vec<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
impl UiBuild {
|
||||
/// The instant at which this screen is *settled*, in keyframe time units.
|
||||
///
|
||||
/// A screen is not settled when each element sits at its own last hold —
|
||||
/// that is what [`Element::rest`] gives, and it is wrong for a transient
|
||||
/// (see the note there). It is settled at one shared instant, and the disc
|
||||
/// says which: gather every keyframe time in the build, and take the
|
||||
/// **longest interval containing none of them**. Inside that gap nothing has
|
||||
/// an inflection, so every element is either holding or on a long linear
|
||||
/// ramp — which is exactly what "the screen has stopped changing" means.
|
||||
///
|
||||
/// Returns the midpoint of that gap, or `None` when the build has fewer than
|
||||
/// two distinct keyframe times.
|
||||
///
|
||||
/// ⚠️ **Not every bundle has a settled instant.** Disc-wide over the 1 758
|
||||
/// composable bundles carrying two or more keyframe times, 30 % have a gap of
|
||||
/// at least half a second and 42 % have one under 10 units — the latter are
|
||||
/// mostly `loop*` animation fragments, which are *meant* to be in motion and
|
||||
/// have no settled pose to find. Check the gap width before trusting the
|
||||
/// midpoint; `settle_window` returns it.
|
||||
pub fn settle_time(&self) -> Option<u32> {
|
||||
self.settle_window().map(|(lo, hi)| lo + (hi - lo) / 2)
|
||||
}
|
||||
|
||||
/// The `[start, end]` of the longest keyframe-free interval — see
|
||||
/// [`UiBuild::settle_time`]. The width `end - start` is how much confidence
|
||||
/// the midpoint deserves.
|
||||
pub fn settle_window(&self) -> Option<(u32, u32)> {
|
||||
let mut ts: Vec<u32> = self
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.time))
|
||||
.collect();
|
||||
ts.sort_unstable();
|
||||
ts.dedup();
|
||||
if ts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
ts.windows(2)
|
||||
.map(|w| (w[1] - w[0], w[0], w[1]))
|
||||
.max_by_key(|&(d, _, _)| d)
|
||||
.map(|(_, lo, hi)| (lo, hi))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose(
|
||||
build: &UiBuild,
|
||||
bundle: &[u8],
|
||||
@@ -1084,7 +1138,13 @@ pub fn compose_with_order(
|
||||
// capture from 10.92 to 61.74. The leaf is the thing still animating at
|
||||
// that instant, and it runs on its own timeline
|
||||
// (`docs/re/structures/ui-leaf-vs-parent-alpha.md`).
|
||||
let Some(kf) = el.rest() else { continue };
|
||||
let Some(kf) = (match opts.at {
|
||||
Some(t) => el.pose_at(t),
|
||||
None => el.rest().cloned(),
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let kf = &kf;
|
||||
// An untextured primitive: a solid quad of the keyframe's `fade` colour,
|
||||
// sized by the declared pivot. `kind & 0x10` marks these exactly — see
|
||||
// `docs/re/structures/ui-prm-primitives.md`. They are the screen's
|
||||
|
||||
142
crates/sylpheed-formats/tests/ui_settle_time_disc.rs
Normal file
142
crates/sylpheed-formats/tests/ui_settle_time_disc.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
//! A settled screen is one INSTANT, not one hold per element.
|
||||
//!
|
||||
//! `Element::rest()` returns an element's last *hold* keyframe, picked for that
|
||||
//! element alone. For anything that ends the screen settled that is right. For a
|
||||
//! **transient** it is exactly wrong: a two-frame flash's last hold is the flash
|
||||
//! *peak*, so `rest()` leaves it burning for the whole screen.
|
||||
//!
|
||||
//! `GP_TITLE` build 4 is the case that found this. `ptlogo_back2eff1` … `eff5`
|
||||
//! are five staggered flashes — `a=0` until t52, `255` for two frames, `0` again
|
||||
//! two frames later — that sweep left to right across the logo once and are gone
|
||||
//! by t110. Two elements, `ptlogo_back2eff` (t66–238) and `ptlogo_back2`
|
||||
//! (t80–243), then hold for the rest of the screen. `rest()` draws all seven at
|
||||
//! `a=255` simultaneously, and stacking five extra white glows blows the light
|
||||
//! arc out to saturation: against the console capture the arc's mean error is
|
||||
//! 33.22 and 8 581 pixels sit at the clipping level, where the console has 1 459.
|
||||
//!
|
||||
//! `UiBuild::settle_time()` recovers the right instant from the disc alone — the
|
||||
//! midpoint of the longest keyframe-free interval — with no reference to any
|
||||
//! capture. For this build that is t=198, and posing there takes the arc error to
|
||||
//! 11.79 and the clipped count to 1 452 against the console's 1 459.
|
||||
//!
|
||||
//! Argument, controls and the disc-wide census: `docs/re/structures/ui-settle-time.md`.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
|
||||
|
||||
fn disc_root() -> Option<PathBuf> {
|
||||
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
|
||||
let p = PathBuf::from(p);
|
||||
if p.join("dat").is_dir() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The case that found the bug, asserted end to end.
|
||||
#[test]
|
||||
fn a_flash_is_transparent_at_the_settle_time_and_opaque_at_rest() {
|
||||
let Some(root) = disc_root() else {
|
||||
eprintln!("SYLPHEED_DISC unset — skipping");
|
||||
return;
|
||||
};
|
||||
let arc = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
let bytes = arc.read(&arc.entries()[4]).expect("build 4");
|
||||
let b = ui_layout::parse_build(&bytes).expect("parse");
|
||||
|
||||
let (lo, hi) = b.settle_window().expect("a settle window");
|
||||
let t = b.settle_time().expect("a settle time");
|
||||
assert!(hi - lo >= 60, "title's settle window should be a second or more, got {lo}..{hi}");
|
||||
assert!(lo < t && t < hi, "settle time {t} must lie inside {lo}..{hi}");
|
||||
|
||||
let alpha = |k: &ui_layout::Keyframe| k.fade >> 24;
|
||||
let mut flashes = 0;
|
||||
for el in &b.elements {
|
||||
let Some(name) = el.name.strip_prefix("ptlogo_back2eff") else { continue };
|
||||
// `ptlogo_back2eff.t32` itself holds; only the numbered ones flash.
|
||||
if !name.starts_with(|c: char| c.is_ascii_digit()) {
|
||||
continue;
|
||||
}
|
||||
flashes += 1;
|
||||
let rest = el.rest().expect("a rest pose");
|
||||
let posed = el.pose_at(t).expect("a posed keyframe");
|
||||
assert_eq!(
|
||||
alpha(rest),
|
||||
255,
|
||||
"{}: rest() is expected to report the FLASH PEAK — that is the bug",
|
||||
el.name
|
||||
);
|
||||
assert_eq!(
|
||||
alpha(&posed),
|
||||
0,
|
||||
"{} flashes once before t110 and must be gone at the settle time {t}",
|
||||
el.name
|
||||
);
|
||||
}
|
||||
assert_eq!(flashes, 5, "GP_TITLE build 4 has five numbered back2 flashes");
|
||||
|
||||
// …while the two that genuinely hold are still opaque there.
|
||||
for want in ["ptlogo_back2eff.t32", "ptlogo_back2.t32"] {
|
||||
let el = b.elements.iter().find(|e| e.name == want).expect(want);
|
||||
assert_eq!(
|
||||
alpha(&el.pose_at(t).expect("posed")),
|
||||
255,
|
||||
"{want} holds across the settle time and must stay opaque"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The window is a property of the data, so it must be computable disc-wide
|
||||
/// without panicking, and must be self-consistent wherever it exists.
|
||||
#[test]
|
||||
fn settle_windows_are_self_consistent_disc_wide() {
|
||||
let Some(root) = disc_root() else {
|
||||
eprintln!("SYLPHEED_DISC unset — skipping");
|
||||
return;
|
||||
};
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let (mut with, mut wide) = (0usize, 0usize);
|
||||
for p in &paks {
|
||||
let Ok(a) = PakArchive::open(p) else { continue };
|
||||
for e in a.entries() {
|
||||
let Ok(by) = a.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
let Some((lo, hi)) = b.settle_window() else { continue };
|
||||
with += 1;
|
||||
assert!(lo < hi, "an empty window is not a window: {lo}..{hi}");
|
||||
let t = b.settle_time().expect("a window implies a time");
|
||||
assert!((lo..=hi).contains(&t), "settle time {t} outside {lo}..{hi}");
|
||||
// No element may have a keyframe strictly inside the window — that
|
||||
// is the whole definition, so it is worth asserting rather than
|
||||
// trusting.
|
||||
for el in &b.elements {
|
||||
for k in &el.keyframes {
|
||||
if let Some(kt) = k.time {
|
||||
assert!(
|
||||
kt <= lo || kt >= hi,
|
||||
"{}: keyframe t={kt} lies inside the settle window {lo}..{hi}",
|
||||
el.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if hi - lo >= 30 {
|
||||
wide += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(with > 1000, "expected >1000 bundles with a settle window, got {with}");
|
||||
eprintln!("{with} bundles have a settle window; {wide} are at least 30 units wide");
|
||||
}
|
||||
Reference in New Issue
Block a user