From 9bd7f245d7313e1ad502bb192150bd45baaa6d0a Mon Sep 17 00:00:00 2001 From: hadelan Date: Wed, 2 Sep 2026 14:48:18 +0800 Subject: [PATCH] kinematics: an empty band is no hand even when min_zones is 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit min_zones = 0 is a legal config value, and it makes the length check pass on an empty band — straight into the low-percentile index below, which panics on an empty Vec. Floor the check at one zone: no zones is no hand, whatever the configured floor is. --- kinematics/src/hand.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/kinematics/src/hand.rs b/kinematics/src/hand.rs index 8ca49019..1b08ec4c 100644 --- a/kinematics/src/hand.rs +++ b/kinematics/src/hand.rs @@ -143,7 +143,9 @@ impl Tracker { } } - if in_band.len() < self.config.min_zones { + // The band must hold at least one zone whatever `min_zones` says: at 0 the length + // check passes on an empty band, and the percentile index below would panic on it. + if in_band.len() < self.config.min_zones.max(1) { // Hold the last hand briefly. This is the whole anti-chop mechanism: the note // rides over a dropout, and stops when one lasts. return match self.last { @@ -318,6 +320,21 @@ mod tests { assert!(tracker.track(&distance, &status, Instant::now()).is_some()); } + /// `min_zones = 0` is a legal config, and an empty band then passes the length check — + /// straight into a percentile index that has nothing to index. No zones is no hand, + /// whatever the floor is. + #[test] + fn an_empty_band_is_not_a_hand_even_with_min_zones_zero() { + let mut tracker = Tracker::new(Config { + min_zones: 0, + ..Config::default() + }); + assert_eq!(tracker.track(&[], &[], Instant::now()), None); + // But a single usable zone still is one: the floor is on emptiness, not on count. + let (distance, status) = frame(0.25, 5, 1); + assert!(tracker.track(&distance, &status, Instant::now()).is_some()); + } + /// A negative distance under a believed status is a failed convergence, and a frame /// shorter than the grid must not panic or read past its end — the wire carries vectors, /// and a peer from another release can send fewer.