Skip to content

Commit fd26d8f

Browse files
Reduce contact warnings: honest reach/ground-lock + locomotion-aware grounding (#99)
1 parent 094d453 commit fd26d8f

15 files changed

Lines changed: 129 additions & 42 deletions

packages/posecode-eval/src/diagnostics.ts

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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);

packages/posecode-eval/src/probe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -846,7 +846,7 @@ export function probeMovement(
846846
// Endpoint probes above power semantic movement checks. Separately sample
847847
// the solved clip between endpoints so a heel lift or collision that appears
848848
// only mid-transition cannot hide behind two valid terminal poses.
849-
const diagnosticsCollector = createClipDiagnosticsCollector(diagnosticSampleRateHz);
849+
const diagnosticsCollector = createClipDiagnosticsCollector(diagnosticSampleRateHz, clipHasTravel);
850850
for (let phaseIndex = 0; phaseIndex < tl.segments.length; phaseIndex++) {
851851
const seg = tl.segments[phaseIndex]!;
852852
const steps = Math.max(1, Math.ceil((seg.end - seg.start) * diagnosticSampleRateHz));
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, it, expect } from "vitest";
2+
import { readFileSync } from "node:fs";
3+
import { fileURLToPath } from "node:url";
4+
import { dirname, resolve } from "node:path";
5+
import { probeMovement } from "../src/index.js";
6+
7+
const examplesDir = resolve(
8+
dirname(fileURLToPath(import.meta.url)),
9+
"../../../spec/examples",
10+
);
11+
const load = (name: string): string =>
12+
readFileSync(resolve(examplesDir, `${name}.posecode`), "utf8");
13+
14+
const footWarnings = (name: string) =>
15+
probeMovement(load(name)).diagnostics.warnings.filter((w) =>
16+
["heel-height", "toe-height", "sole-angle", "grounding-rom-conflict"].includes(w.kind),
17+
);
18+
19+
/**
20+
* A stance foot in a traveling clip rolls onto its ball as the body passes
21+
* (push-off) and is briefly airborne at each step transition — correct gait,
22+
* not a grounding artifact, so it is exempt from the static flat-foot checks.
23+
* A static clip keeps the strict bar, so genuine heel-lift must still surface.
24+
*/
25+
describe("locomotion grounding exemption", () => {
26+
it("exempts a locomotion stance foot's airborne swing (no large float at a step transition)", () => {
27+
// Before the fix, box-step's stance pin was measured while it was still
28+
// descending from the previous phase's swing — a ~0.12m heel float and a
29+
// ~33° sole tilt mid-transition. The airborne-swing exemption removes those
30+
// large phase-transition floats (a smaller settling roll may remain).
31+
const large = footWarnings("box-step").filter(
32+
(w) =>
33+
(w.kind === "heel-height" && w.value > 0.1) ||
34+
(w.kind === "sole-angle" && w.value > 30),
35+
);
36+
expect(large, large.map((w) => w.detail).join("\n")).toHaveLength(0);
37+
});
38+
39+
it("still flags genuine heel-lift in a static deep pose (no over-suppression)", () => {
40+
// superhero-landing holds a deep static landing whose shin exceeds the ankle
41+
// dorsiflexion ROM; that heel-lift is a real artifact and must stay flagged.
42+
const warns = footWarnings("superhero-landing");
43+
expect(warns.some((w) => w.kind === "grounding-rom-conflict" || w.kind === "heel-height")).toBe(
44+
true,
45+
);
46+
});
47+
});

spec/examples/box-step-taps.posecode

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ posecode exercise "Box step taps"
1414
hip_right: flex 0
1515
knee_right: flex 0
1616
ankle_right: plantarflex 0
17-
ground-lock: feet
17+
ground-lock: foot_left
18+
reach: foot_right floor
1819
cue "Return the right foot to the floor"
1920

2021
step "Left tap" 0.6s settle:
@@ -28,7 +29,8 @@ posecode exercise "Box step taps"
2829
hip_left: flex 0
2930
knee_left: flex 0
3031
ankle_left: plantarflex 0
31-
ground-lock: feet
32+
ground-lock: foot_right
33+
reach: foot_left floor
3234
cue "Back to the floor: keep a light, quick rhythm"
3335

3436
repeat 6

spec/examples/box-step.posecode

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ posecode exercise "Box step"
4444
ankle_right: plantarflex 0
4545
shoulders: flex 0
4646
travel: 0 0
47-
ground-lock: feet
47+
reach: foot_left floor
48+
reach: foot_right floor
4849
cue "Close the left foot and settle over both feet, completing the square"
4950

5051
repeat 4

spec/examples/chasse.posecode

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ posecode exercise "Chassé"
6767
shoulders: abduct 0
6868
elbows: flex 0
6969
travel: 0 0
70-
ground-lock: feet
70+
reach: foot_left floor
71+
reach: foot_right floor
7172
cue "Close home over both feet and let the momentum resolve"
7273

7374
repeat 4

spec/examples/front-kick.posecode

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ posecode exercise "Front kick"
2323
step "Return" 0.6s settle:
2424
hip_right: flex 0
2525
knee_right: flex 0
26-
ground-lock: feet
26+
ground-lock: foot_left
27+
reach: foot_right floor
2728
cue "Set the foot back down to a fighting stance"
2829

2930
repeat 5

spec/examples/grapevine.posecode

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ posecode exercise "Grapevine"
7272
hip_right: abduct 0
7373
shoulders: abduct 0
7474
travel: 0 0
75-
ground-lock: feet
75+
reach: foot_left floor
76+
reach: foot_right floor
7677
cue "Close the left foot under the body and settle the phrase"
7778

7879
repeat 3

spec/examples/high-knee-march.posecode

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ posecode exercise "High-knee march"
1616
hip_left: flex 90
1717
knee_left: flex 90
1818
shoulder_right: flex 40
19-
pin: foot_right floor
19+
reach: foot_right floor
2020
cue "Plant and drive the left knee up"
2121

2222
step "Down" 0.6s settle:
2323
hip_left: flex 0
2424
knee_left: flex 0
2525
shoulder_right: flex 0
26-
ground-lock: feet
26+
reach: foot_left floor
27+
reach: foot_right floor
2728
cue "Return to a tall, ready stance"
2829

2930
repeat 6

spec/examples/hip-abduction.posecode

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ posecode exercise "Standing hip abduction"
99

1010
step "Lower" 1.6s settle:
1111
hip_right: abduct 0
12-
ground-lock: feet
12+
ground-lock: foot_left
13+
reach: foot_right floor
1314
cue "Lower the leg back to the midline"
1415

1516
repeat 10

0 commit comments

Comments
 (0)