Skip to content

Commit 67491e8

Browse files
Merge pull request #49 from posecode-dev/codex/xbot-grounding
Fix floor-pin horizontal sliding, forward lunge and superman poses, and dips metadata
2 parents f1e8795 + 1a64768 commit 67491e8

5 files changed

Lines changed: 162 additions & 8 deletions

File tree

packages/posecode-eval/src/probe.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,67 @@ export function probeMovement(source: string): ProbeResult {
117117
}
118118
}
119119

120+
// Precompute world positions of all effectors at start of each segment
121+
const segmentStartEffectors: Map<string, THREE.Vector3>[] = [];
122+
const tempYawQ = new THREE.Quaternion();
123+
124+
const getEffectorId = (eff: string) => {
125+
if (eff === "hand_left") return "wrist_left";
126+
if (eff === "hand_right") return "wrist_right";
127+
if (eff === "foot_left") return "ankle_left";
128+
if (eff === "foot_right") return "ankle_right";
129+
return eff;
130+
};
131+
132+
let prevEffectorsMap: Map<string, THREE.Vector3> | null = null;
133+
let prevPins: typeof ir.phases[number]["pins"] = [];
134+
135+
for (let i = 0; i < tl.segments.length; i++) {
136+
const seg = tl.segments[i]!;
137+
for (const bone of m.bones.values()) bone.quaternion.identity();
138+
const info = tl.sample(seg.start, m.bones);
139+
140+
const wasPinned = (id: string) => prevPins.some(p => getEffectorId(p.effector) === id && p.anchor === "floor");
141+
const isPinned = (id: string) => info.pins.some(p => getEffectorId(p.effector) === id && p.anchor === "floor");
142+
143+
m.root.position.copy(baseRootPos);
144+
m.root.quaternion.copy(baseRootQuat);
145+
if (info.rootYaw !== 0) {
146+
tempYawQ.setFromAxisAngle(WORLD_Y, info.rootYaw);
147+
m.root.quaternion.premultiply(tempYawQ);
148+
}
149+
m.root.position.x += info.rootOffset.x;
150+
m.root.position.z += info.rootOffset.z;
151+
m.root.updateMatrixWorld(true);
152+
depenetrate(m);
153+
154+
const effectorsMap = new Map<string, THREE.Vector3>();
155+
for (const ids of Object.values(m.effectors)) {
156+
for (const id of ids) {
157+
const node = m.bones.get(id);
158+
if (node) {
159+
if (i > 0 && wasPinned(id) && isPinned(id) && prevEffectorsMap && prevEffectorsMap.has(id)) {
160+
effectorsMap.set(id, prevEffectorsMap.get(id)!);
161+
} else {
162+
effectorsMap.set(id, node.getWorldPosition(new THREE.Vector3()));
163+
}
164+
}
165+
}
166+
}
167+
segmentStartEffectors.push(effectorsMap);
168+
prevEffectorsMap = effectorsMap;
169+
prevPins = info.pins;
170+
}
171+
172+
// Restore initial state
173+
for (const bone of m.bones.values()) bone.quaternion.identity();
174+
tl.sample(0, m.bones);
175+
m.root.position.copy(baseRootPos);
176+
m.root.quaternion.copy(baseRootQuat);
177+
m.root.updateMatrixWorld(true);
178+
depenetrate(m);
179+
groundFigure(m);
180+
120181
// Sample the end of each phase, applying the viewer's per-frame root
121182
// pipeline: base root → yaw/travel → ground-lock → floor safety clamp.
122183
const yawQ = new THREE.Quaternion();
@@ -166,8 +227,14 @@ export function probeMovement(source: string): ProbeResult {
166227
if (!effector) continue;
167228
let target: THREE.Vector3 | null = null;
168229
if (pin.anchor === "floor") {
169-
target = effector.getWorldPosition(new THREE.Vector3());
170-
target.y = 0;
230+
const startPos = segmentStartEffectors[phaseIndex]?.get(effectorId);
231+
if (startPos) {
232+
target = startPos.clone();
233+
target.y = 0;
234+
} else {
235+
target = effector.getWorldPosition(new THREE.Vector3());
236+
target.y = 0;
237+
}
171238
} else if (propScene.anchors.has(pin.anchor)) {
172239
target = propScene.anchors.get(pin.anchor)!.clone();
173240
} else {

packages/posecode-render/src/index.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export interface Viewer {
6161
getTimeline(): TimelineInfo | null;
6262
/** Precise visible world bounds; intended for audits and deterministic export. */
6363
getVisibleBounds(): THREE.Box3;
64+
getMannequin(): any;
65+
getCharacter(): any;
6466
/**
6567
* Render the current time synchronously and return the frame as a PNG data
6668
* URL. Works without preserveDrawingBuffer because the read happens in the
@@ -305,6 +307,7 @@ export function createViewer(
305307
// ground anchors when the character (with its own proportions) arrives.
306308
let lastIR: PosecodeIR | null = null;
307309
let groundTargets = new Map<string, THREE.Vector3>();
310+
const segmentStartEffectors: Map<string, THREE.Vector3>[] = [];
308311
// World-space anchor points contributed by scene props (chair seat, bar grip,
309312
// wall surface). Populated when a doc declares props; empty otherwise.
310313
let propAnchors = new Map<string, THREE.Vector3>();
@@ -322,6 +325,7 @@ export function createViewer(
322325
let tickCb: (time: number, duration: number) => void = () => {};
323326
let loopCb: () => void = () => {};
324327
let lastPhaseName = "";
328+
let activeSegIndex = 0;
325329

326330
// Camera easing targets.
327331
const desiredTarget = new THREE.Vector3(0, 0.9, 0);
@@ -505,7 +509,20 @@ export function createViewer(
505509
const effectorBone = EFFECTOR_BONE[p.effector] ?? p.effector;
506510
const effector = mannequin.bones.get(effectorBone);
507511
if (!effector) continue;
508-
const anchor = resolveReachTarget(p.anchor, effector);
512+
let anchor: THREE.Vector3 | null = null;
513+
if (p.anchor === "floor") {
514+
const startPos = segmentStartEffectors[activeSegIndex]?.get(effectorBone);
515+
if (startPos) {
516+
anchor = startPos.clone();
517+
const isFoot = effectorBone.startsWith("ankle") || effectorBone.startsWith("foot");
518+
const drop = isFoot ? (character?.proportions.soleDrop ?? 0.042) : 0;
519+
anchor.y = drop;
520+
} else {
521+
anchor = resolveReachTarget(p.anchor, effector);
522+
}
523+
} else {
524+
anchor = resolveReachTarget(p.anchor, effector);
525+
}
509526
if (!anchor) continue;
510527
delta.add(anchor.sub(effector.getWorldPosition(new THREE.Vector3())));
511528
n++;
@@ -631,6 +648,15 @@ export function createViewer(
631648
if (timeline) {
632649
const info = timeline.sample(time, mannequin.bones);
633650
solvedInfo = info;
651+
activeSegIndex = 0;
652+
const tt = timeline.duration > 0 ? ((time % timeline.duration) + timeline.duration) % timeline.duration : 0;
653+
for (let k = 0; k < timeline.segments.length; k++) {
654+
const seg = timeline.segments[k]!;
655+
if (tt >= seg.start && tt <= seg.end) {
656+
activeSegIndex = k;
657+
break;
658+
}
659+
}
634660
// Life layer rides on wall-clock time (not timeline time) so the figure
635661
// keeps breathing and blinking while paused or scrubbing.
636662
applyLife(performance.now() / 1000);
@@ -823,6 +849,61 @@ export function createViewer(
823849
captureGroundTargets();
824850
baseRootPos.copy(mannequin.root.position);
825851
baseRootQuat.copy(mannequin.root.quaternion);
852+
853+
// Precompute world positions of all effectors at start of each segment
854+
segmentStartEffectors.length = 0;
855+
if (timeline) {
856+
let prevEffectorsMap: Map<string, THREE.Vector3> | null = null;
857+
let prevPins: PinTarget[] = [];
858+
for (let i = 0; i < timeline.segments.length; i++) {
859+
const seg = timeline.segments[i]!;
860+
for (const bone of mannequin.bones.values()) bone.quaternion.identity();
861+
const info = timeline.sample(seg.start, mannequin.bones);
862+
863+
const wasPinned = (id: string) => prevPins.some(p => (EFFECTOR_BONE[p.effector] ?? p.effector) === id && p.anchor === "floor");
864+
const isPinned = (id: string) => info.pins.some(p => (EFFECTOR_BONE[p.effector] ?? p.effector) === id && p.anchor === "floor");
865+
866+
mannequin.root.position.copy(baseRootPos);
867+
mannequin.root.quaternion.copy(baseRootQuat);
868+
if (info.rootYaw !== 0) {
869+
const yawQ = new THREE.Quaternion().setFromAxisAngle(WORLD_Y, info.rootYaw);
870+
mannequin.root.quaternion.premultiply(yawQ);
871+
}
872+
mannequin.root.position.x += info.rootOffset.x;
873+
mannequin.root.position.z += info.rootOffset.z;
874+
mannequin.root.updateMatrixWorld(true);
875+
depenetrate(mannequin);
876+
877+
const effectorsMap = new Map<string, THREE.Vector3>();
878+
for (const ids of Object.values(mannequin.effectors)) {
879+
for (const id of ids) {
880+
const node = mannequin.bones.get(id);
881+
if (node) {
882+
if (i > 0 && wasPinned(id) && isPinned(id) && prevEffectorsMap && prevEffectorsMap.has(id)) {
883+
effectorsMap.set(id, prevEffectorsMap.get(id)!);
884+
} else {
885+
effectorsMap.set(id, node.getWorldPosition(new THREE.Vector3()));
886+
}
887+
}
888+
}
889+
}
890+
segmentStartEffectors.push(effectorsMap);
891+
prevEffectorsMap = effectorsMap;
892+
prevPins = info.pins;
893+
}
894+
895+
// Restore initial pose
896+
for (const bone of mannequin.bones.values()) bone.quaternion.identity();
897+
applyBaseRoot();
898+
timeline.sample(0, mannequin.bones);
899+
mannequin.root.position.copy(baseRootPos);
900+
mannequin.root.quaternion.copy(baseRootQuat);
901+
mannequin.root.updateMatrixWorld(true);
902+
depenetrate(mannequin);
903+
groundFigureOf(mannequin);
904+
levelPlantedFeet(mannequin, ir.phases[0]?.groundLock ?? []);
905+
}
906+
826907
requestClip(ir);
827908
frameCamera();
828909
},
@@ -874,6 +955,12 @@ export function createViewer(
874955
getVisibleBounds() {
875956
return character?.getBounds() ?? new THREE.Box3().setFromObject(mannequin.root);
876957
},
958+
getMannequin() {
959+
return mannequin;
960+
},
961+
getCharacter() {
962+
return character;
963+
},
877964
captureFrame() {
878965
frame();
879966
return renderer.domElement.toDataURL("image/png");

playground/src/presets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export const PRESETS: Preset[] = [
144144
{ id: "box-step-taps", label: "Box step taps", domain: "Warm-up", bodyPart: "Upper legs", target: "Hip flexors", equipment: "Box", difficulty: "Beginner", source: boxStepTaps },
145145
{ id: "pull-up", label: "Pull-up", domain: "Fitness", bodyPart: "Back", target: "Lats", equipment: "Bar", difficulty: "Advanced", status: "development", developmentNote: "Hand grip and wrist contact are still being refined", source: pullUp },
146146
{ id: "step-up", label: "Step-up (box)", domain: "Functional", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Box", difficulty: "Intermediate", source: stepUp },
147-
{ id: "triceps-dips", label: "Triceps dips (chair)", domain: "Fitness", bodyPart: "Upper arms", target: "Triceps", equipment: "Chair", difficulty: "Intermediate", status: "development", developmentNote: "Hand support and wrist contact are still being refined", source: tricepsDips },
147+
{ id: "triceps-dips", label: "Triceps dips (bars)", domain: "Fitness", bodyPart: "Upper arms", target: "Triceps", equipment: "Bars", difficulty: "Intermediate", status: "development", developmentNote: "Hand support and wrist contact are still being refined", source: tricepsDips },
148148
{ id: "quad-stretch", label: "Standing quad stretch", domain: "Mobility", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Body weight", difficulty: "Beginner", source: quadStretch },
149149

150150
// --- Education / anatomy: single-joint ROM demos ---

spec/examples/forward-lunge.posecode

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ posecode exercise "Forward lunge"
77
knee_right: flex 95
88
hip_left: extend 15
99
knee_left: flex 80
10-
ankle_right: plantarflex 50
10+
ankle_left: plantarflex 50
1111
spine: extend 4
1212
travel: 0 0.3
1313
pin: foot_left floor
@@ -19,7 +19,7 @@ posecode exercise "Forward lunge"
1919
knee_right: flex 0
2020
hip_left: extend 0
2121
knee_left: flex 0
22-
ankle_right: plantarflex 0
22+
ankle_left: plantarflex 0
2323
spine: flex 0
2424
travel: 0 0
2525
ground-lock: feet

spec/examples/superman.posecode

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ posecode exercise "Superman"
33
pose start = prone
44

55
step "Lift" 1.5s settle:
6-
shoulders: abduct 130
6+
shoulders: flex 140
77
spine: extend 20
88
chest: extend 15
99
neck: extend 25
@@ -12,7 +12,7 @@ posecode exercise "Superman"
1212
cue "Lift the arms, chest, and legs off the floor"
1313

1414
step "Lower" 1.5s drive:
15-
shoulders: abduct 0
15+
shoulders: flex 0
1616
spine: flex 0
1717
chest: flex 0
1818
neck: flex 0

0 commit comments

Comments
 (0)