Skip to content

Commit 8f855e4

Browse files
committed
feat(render): apply foot-flat in the viewer frame loop and on load
1 parent be3354f commit 8f855e4

2 files changed

Lines changed: 42 additions & 1 deletion

File tree

packages/posecode-render/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
type ClipSource,
2828
} from "./clips.js";
2929
import { depenetrate } from "./depenetrate.js";
30-
import { alignFloorPalms } from "./contacts.js";
30+
import { alignFloorPalms, levelPlantedFeet } from "./contacts.js";
3131

3232
const DEG = Math.PI / 180;
3333

@@ -544,6 +544,10 @@ export function createViewer(
544544
// floor/landmark targets resolve against.
545545
applyReaches(info.reaches);
546546
alignFloorPalms(mannequin, info.reaches, info.pins);
547+
// Plantigrade correction: keep planted soles flat to the floor so grounded
548+
// lower-body poses (squat, lunge, deadlift) don't balance on the toes.
549+
// Runs before the floor clamp so the leveled sole is what rests on y=0.
550+
levelPlantedFeet(mannequin, info.groundLock);
547551
// Safety net: nothing above ever intentionally pushes part of the body
548552
// below the floor, so clamp the root up whenever the lowest point dips
549553
// below y=0, a no-op whenever the pose is legitimately grounded or
@@ -641,6 +645,7 @@ export function createViewer(
641645
mannequin.root.updateMatrixWorld(true);
642646
depenetrate(mannequin);
643647
groundFigureOf(mannequin);
648+
levelPlantedFeet(mannequin, ir.phases[0]?.groundLock ?? []);
644649
captureGroundTargets();
645650
baseRootPos.copy(mannequin.root.position);
646651
baseRootQuat.copy(mannequin.root.quaternion);

packages/posecode-render/test/render.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { solveCCD } from "../src/ik.js";
66
import { poseFor } from "../src/poses.js";
77
import { buildProps } from "../src/props.js";
88
import { applyGroundLock, groundFigure } from "../src/groundlock.js";
9+
import { levelPlantedFeet } from "../src/contacts.js";
910
import { parse, eulerRomFor } from "posecode-parser";
1011

1112
const DEG = Math.PI / 180;
@@ -751,3 +752,38 @@ describe("spline interpolation (L2)", () => {
751752
expect(v).toBeLessThan(0.2);
752753
});
753754
});
755+
756+
describe("foot-flat correction (L3.1)", () => {
757+
it("rests a squatting foot flatter on the floor (not on the toes)", () => {
758+
const src = [
759+
'posecode exercise "sq"',
760+
" rig humanoid",
761+
" pose start = standing",
762+
' step "Descend" 1s settle:',
763+
" hips: flex 80",
764+
" knees: flex 95",
765+
" pelvis: hinge 25",
766+
" ground-lock: feet",
767+
].join("\n");
768+
const { ir } = parse(src);
769+
const tl = buildTimeline(ir!);
770+
const m = buildMannequin();
771+
tl.sample(1, m.bones);
772+
m.root.updateMatrixWorld(true);
773+
groundFigure(m);
774+
applyGroundLock(m, ["feet"]);
775+
const soleDot = () => {
776+
const ankle = m.bones.get("ankle_left")!;
777+
return new THREE.Vector3(0, -1, 0)
778+
.applyQuaternion(ankle.getWorldQuaternion(new THREE.Quaternion()))
779+
.normalize()
780+
.dot(new THREE.Vector3(0, -1, 0));
781+
};
782+
const before = soleDot();
783+
levelPlantedFeet(m, ["feet"]);
784+
m.root.updateMatrixWorld(true);
785+
// Leveling makes the sole markedly flatter than raw ground-lock leaves it.
786+
expect(soleDot()).toBeGreaterThan(before);
787+
expect(soleDot()).toBeGreaterThan(0.9);
788+
});
789+
});

0 commit comments

Comments
 (0)