Skip to content

Commit 3aaa6d1

Browse files
committed
Fix floor-pin horizontal sliding, correct lunge and superman poses, and update dips metadata
1 parent 9b784d2 commit 3aaa6d1

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
@@ -102,6 +102,67 @@ export function probeMovement(source: string): ProbeResult {
102102
}
103103
}
104104

105+
// Precompute world positions of all effectors at start of each segment
106+
const segmentStartEffectors: Map<string, THREE.Vector3>[] = [];
107+
const tempYawQ = new THREE.Quaternion();
108+
109+
const getEffectorId = (eff: string) => {
110+
if (eff === "hand_left") return "wrist_left";
111+
if (eff === "hand_right") return "wrist_right";
112+
if (eff === "foot_left") return "ankle_left";
113+
if (eff === "foot_right") return "ankle_right";
114+
return eff;
115+
};
116+
117+
let prevEffectorsMap: Map<string, THREE.Vector3> | null = null;
118+
let prevPins: typeof ir.phases[number]["pins"] = [];
119+
120+
for (let i = 0; i < tl.segments.length; i++) {
121+
const seg = tl.segments[i]!;
122+
for (const bone of m.bones.values()) bone.quaternion.identity();
123+
const info = tl.sample(seg.start, m.bones);
124+
125+
const wasPinned = (id: string) => prevPins.some(p => getEffectorId(p.effector) === id && p.anchor === "floor");
126+
const isPinned = (id: string) => info.pins.some(p => getEffectorId(p.effector) === id && p.anchor === "floor");
127+
128+
m.root.position.copy(baseRootPos);
129+
m.root.quaternion.copy(baseRootQuat);
130+
if (info.rootYaw !== 0) {
131+
tempYawQ.setFromAxisAngle(WORLD_Y, info.rootYaw);
132+
m.root.quaternion.premultiply(tempYawQ);
133+
}
134+
m.root.position.x += info.rootOffset.x;
135+
m.root.position.z += info.rootOffset.z;
136+
m.root.updateMatrixWorld(true);
137+
depenetrate(m);
138+
139+
const effectorsMap = new Map<string, THREE.Vector3>();
140+
for (const ids of Object.values(m.effectors)) {
141+
for (const id of ids) {
142+
const node = m.bones.get(id);
143+
if (node) {
144+
if (i > 0 && wasPinned(id) && isPinned(id) && prevEffectorsMap && prevEffectorsMap.has(id)) {
145+
effectorsMap.set(id, prevEffectorsMap.get(id)!);
146+
} else {
147+
effectorsMap.set(id, node.getWorldPosition(new THREE.Vector3()));
148+
}
149+
}
150+
}
151+
}
152+
segmentStartEffectors.push(effectorsMap);
153+
prevEffectorsMap = effectorsMap;
154+
prevPins = info.pins;
155+
}
156+
157+
// Restore initial state
158+
for (const bone of m.bones.values()) bone.quaternion.identity();
159+
tl.sample(0, m.bones);
160+
m.root.position.copy(baseRootPos);
161+
m.root.quaternion.copy(baseRootQuat);
162+
m.root.updateMatrixWorld(true);
163+
depenetrate(m);
164+
groundFigure(m);
165+
105166
// Sample the end of each phase, applying the viewer's per-frame root
106167
// pipeline: base root → yaw/travel → ground-lock → floor safety clamp.
107168
const yawQ = new THREE.Quaternion();
@@ -151,8 +212,14 @@ export function probeMovement(source: string): ProbeResult {
151212
if (!effector) continue;
152213
let target: THREE.Vector3 | null = null;
153214
if (pin.anchor === "floor") {
154-
target = effector.getWorldPosition(new THREE.Vector3());
155-
target.y = 0;
215+
const startPos = segmentStartEffectors[phaseIndex]?.get(effectorId);
216+
if (startPos) {
217+
target = startPos.clone();
218+
target.y = 0;
219+
} else {
220+
target = effector.getWorldPosition(new THREE.Vector3());
221+
target.y = 0;
222+
}
156223
} else if (propScene.anchors.has(pin.anchor)) {
157224
target = propScene.anchors.get(pin.anchor)!.clone();
158225
} else {

packages/posecode-render/src/index.ts

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

325329
// Camera easing targets.
326330
const desiredTarget = new THREE.Vector3(0, 0.9, 0);
@@ -504,7 +508,20 @@ export function createViewer(
504508
const effectorBone = EFFECTOR_BONE[p.effector] ?? p.effector;
505509
const effector = mannequin.bones.get(effectorBone);
506510
if (!effector) continue;
507-
const anchor = resolveReachTarget(p.anchor, effector);
511+
let anchor: THREE.Vector3 | null = null;
512+
if (p.anchor === "floor") {
513+
const startPos = segmentStartEffectors[activeSegIndex]?.get(effectorBone);
514+
if (startPos) {
515+
anchor = startPos.clone();
516+
const isFoot = effectorBone.startsWith("ankle") || effectorBone.startsWith("foot");
517+
const drop = isFoot ? (character ? character.proportions.soleDrop : 0.042) : 0;
518+
anchor.y = drop;
519+
} else {
520+
anchor = resolveReachTarget(p.anchor, effector);
521+
}
522+
} else {
523+
anchor = resolveReachTarget(p.anchor, effector);
524+
}
508525
if (!anchor) continue;
509526
delta.add(anchor.sub(effector.getWorldPosition(new THREE.Vector3())));
510527
n++;
@@ -617,6 +634,15 @@ export function createViewer(
617634
if (timeline) {
618635
const info = timeline.sample(time, mannequin.bones);
619636
solvedInfo = info;
637+
activeSegIndex = 0;
638+
const tt = timeline.duration > 0 ? ((time % timeline.duration) + timeline.duration) % timeline.duration : 0;
639+
for (let k = 0; k < timeline.segments.length; k++) {
640+
const seg = timeline.segments[k]!;
641+
if (tt >= seg.start && tt <= seg.end) {
642+
activeSegIndex = k;
643+
break;
644+
}
645+
}
620646
// Life layer rides on wall-clock time (not timeline time) so the figure
621647
// keeps breathing and blinking while paused or scrubbing.
622648
applyLife(performance.now() / 1000);
@@ -797,6 +823,61 @@ export function createViewer(
797823
captureGroundTargets();
798824
baseRootPos.copy(mannequin.root.position);
799825
baseRootQuat.copy(mannequin.root.quaternion);
826+
827+
// Precompute world positions of all effectors at start of each segment
828+
segmentStartEffectors.length = 0;
829+
if (timeline) {
830+
let prevEffectorsMap: Map<string, THREE.Vector3> | null = null;
831+
let prevPins: PinTarget[] = [];
832+
for (let i = 0; i < timeline.segments.length; i++) {
833+
const seg = timeline.segments[i]!;
834+
for (const bone of mannequin.bones.values()) bone.quaternion.identity();
835+
const info = timeline.sample(seg.start, mannequin.bones);
836+
837+
const wasPinned = (id: string) => prevPins.some(p => (EFFECTOR_BONE[p.effector] ?? p.effector) === id && p.anchor === "floor");
838+
const isPinned = (id: string) => info.pins.some(p => (EFFECTOR_BONE[p.effector] ?? p.effector) === id && p.anchor === "floor");
839+
840+
mannequin.root.position.copy(baseRootPos);
841+
mannequin.root.quaternion.copy(baseRootQuat);
842+
if (info.rootYaw !== 0) {
843+
const yawQ = new THREE.Quaternion().setFromAxisAngle(WORLD_Y, info.rootYaw);
844+
mannequin.root.quaternion.premultiply(yawQ);
845+
}
846+
mannequin.root.position.x += info.rootOffset.x;
847+
mannequin.root.position.z += info.rootOffset.z;
848+
mannequin.root.updateMatrixWorld(true);
849+
depenetrate(mannequin);
850+
851+
const effectorsMap = new Map<string, THREE.Vector3>();
852+
for (const ids of Object.values(mannequin.effectors)) {
853+
for (const id of ids) {
854+
const node = mannequin.bones.get(id);
855+
if (node) {
856+
if (i > 0 && wasPinned(id) && isPinned(id) && prevEffectorsMap && prevEffectorsMap.has(id)) {
857+
effectorsMap.set(id, prevEffectorsMap.get(id)!);
858+
} else {
859+
effectorsMap.set(id, node.getWorldPosition(new THREE.Vector3()));
860+
}
861+
}
862+
}
863+
}
864+
segmentStartEffectors.push(effectorsMap);
865+
prevEffectorsMap = effectorsMap;
866+
prevPins = info.pins;
867+
}
868+
869+
// Restore initial pose
870+
for (const bone of mannequin.bones.values()) bone.quaternion.identity();
871+
applyBaseRoot();
872+
timeline.sample(0, mannequin.bones);
873+
mannequin.root.position.copy(baseRootPos);
874+
mannequin.root.quaternion.copy(baseRootQuat);
875+
mannequin.root.updateMatrixWorld(true);
876+
depenetrate(mannequin);
877+
groundFigureOf(mannequin);
878+
levelPlantedFeet(mannequin, ir.phases[0]?.groundLock ?? []);
879+
}
880+
800881
requestClip(ir);
801882
frameCamera();
802883
},
@@ -848,6 +929,12 @@ export function createViewer(
848929
getVisibleBounds() {
849930
return character?.getBounds() ?? new THREE.Box3().setFromObject(mannequin.root);
850931
},
932+
getMannequin() {
933+
return mannequin;
934+
},
935+
getCharacter() {
936+
return character;
937+
},
851938
captureFrame() {
852939
frame();
853940
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)