@@ -180,7 +180,10 @@ export interface ClipDiagnosticsCollector {
180180 finish ( ) : ClipDiagnostics ;
181181}
182182
183- export function createClipDiagnosticsCollector ( sampleRateHz : number ) : ClipDiagnosticsCollector {
183+ export function createClipDiagnosticsCollector (
184+ sampleRateHz : number ,
185+ isLocomotion = false ,
186+ ) : ClipDiagnosticsCollector {
184187 const rate = Math . max (
185188 1 ,
186189 Math . min ( 120 , Number . isFinite ( sampleRateHz ) ? sampleRateHz : DEFAULT_DIAGNOSTIC_SAMPLE_RATE_HZ ) ,
@@ -195,16 +198,24 @@ export function createClipDiagnosticsCollector(sampleRateHz: number): ClipDiagno
195198 const state = feet [ side ] ;
196199 const foot = measureFootContact ( m , side ) ;
197200 let kind = supportKind ( frame , side ) ;
198- // The generic `feet` group also contains a deliberately lifted swing
199- // foot. Match ground-lock's own near-floor selection so that swing height
200- // is not mislabeled as a failed planted contact; an explicit foot lock or
201- // floor pin is always evaluated.
201+ // A supported foot whose sole is well off the floor is mid-swing, not
202+ // planted. Skip it in two cases: (1) the generic `feet` group's lifted
203+ // swing foot, and (2) any airborne foot in a locomotion clip — at a step
204+ // transition the stance pin alternates a beat before the landing foot is
205+ // actually down, so the descending foot is swinging, not a failed plant.
206+ // The endpoint contact-position check still catches a pin left airborne.
207+ const airborne =
208+ kind !== null
209+ && foot !== null
210+ && ! isGroundLockFootPlanted ( floorContactHeight ( m , `foot_${ side } ` ) ?? NaN ) ;
202211 if (
203- kind === "ground-lock"
204- && frame . groundLock . includes ( "feet" )
205- && ! frame . groundLock . includes ( `foot_${ side } ` )
206- && foot
207- && ! isGroundLockFootPlanted ( floorContactHeight ( m , `foot_${ side } ` ) ?? NaN )
212+ airborne
213+ && (
214+ ( kind === "ground-lock"
215+ && frame . groundLock . includes ( "feet" )
216+ && ! frame . groundLock . includes ( `foot_${ side } ` ) )
217+ || isLocomotion
218+ )
208219 ) kind = null ;
209220 if ( ! kind || ! foot ) {
210221 state . supportKind = null ;
@@ -214,21 +225,32 @@ export function createClipDiagnosticsCollector(sampleRateHz: number): ClipDiagno
214225 }
215226 state . supportedSamples ++ ;
216227 const location = { timeSec : frame . timeSec , phaseName : frame . phaseName } ;
217- state . minToeHeightMeters = Math . min ( state . minToeHeightMeters ?? Infinity , foot . toeHeight ) ;
218- state . maxToeHeightMeters = Math . max ( state . maxToeHeightMeters ?? - Infinity , foot . toeHeight ) ;
219- if ( Math . abs ( foot . toeHeight ) > state . worstToeAbs ) {
220- state . worstToeAbs = Math . abs ( foot . toeHeight ) ;
221- state . worstToe = location ;
222- }
223- // Flat-sole grounding checks only apply when a flat foot is expected: the
224- // ankle is not plantarflexed AND the shin stands near-vertical. A foot on
225- // its ball with the shin laid down (plank, knee-drive) legitimately shows
226- // a steep sole and lifted heel, so measuring it as a failed flat plant
227- // fabricates warnings.
228+ // Flat-foot grounding checks (heel/toe height, sole tilt) only apply when a
229+ // flat foot is expected: the ankle is not plantarflexed AND the shin stands
230+ // near-vertical. A foot on its ball with the shin laid down (plank,
231+ // knee-drive) legitimately shows a lifted heel/toe and a steep sole, so
232+ // measuring it as a failed flat plant fabricates warnings.
228233 const shinDeg = shinFromVerticalDeg ( m , side ) ;
234+ // A stance foot in a locomotion clip rolls onto its ball as the body
235+ // travels over it — the toe stays planted while the heel lifts (push-off).
236+ // That roll is correct gait, not a failed flat plant, so it is exempt from
237+ // the flat-foot checks. A fully airborne foot (toe also lifted) is not a
238+ // roll and stays measured; static clips keep the strict flat-foot bar.
239+ const pushOffRoll =
240+ isLocomotion
241+ && Math . abs ( foot . toeHeight ) <= FOOT_CONTACT_HEIGHT_MAX
242+ && foot . heelHeight > FOOT_CONTACT_HEIGHT_MAX ;
229243 const expectedFlat =
230- foot . plantigrade && ( shinDeg === null || shinDeg <= FLAT_SOLE_SHIN_MAX_DEG ) ;
244+ foot . plantigrade
245+ && ! pushOffRoll
246+ && ( shinDeg === null || shinDeg <= FLAT_SOLE_SHIN_MAX_DEG ) ;
231247 if ( expectedFlat ) {
248+ state . minToeHeightMeters = Math . min ( state . minToeHeightMeters ?? Infinity , foot . toeHeight ) ;
249+ state . maxToeHeightMeters = Math . max ( state . maxToeHeightMeters ?? - Infinity , foot . toeHeight ) ;
250+ if ( Math . abs ( foot . toeHeight ) > state . worstToeAbs ) {
251+ state . worstToeAbs = Math . abs ( foot . toeHeight ) ;
252+ state . worstToe = location ;
253+ }
232254 state . plantigradeSamples ++ ;
233255 state . minHeelHeightMeters = Math . min ( state . minHeelHeightMeters ?? Infinity , foot . heelHeight ) ;
234256 state . maxHeelHeightMeters = Math . max ( state . maxHeelHeightMeters ?? - Infinity , foot . heelHeight ) ;
0 commit comments