From 3aaa6d1471ea196bf19ba978ba54bb6cff8bd0ae Mon Sep 17 00:00:00 2001 From: a-baran-orhan Date: Mon, 13 Jul 2026 02:03:50 +0300 Subject: [PATCH 1/2] Fix floor-pin horizontal sliding, correct lunge and superman poses, and update dips metadata --- packages/posecode-eval/src/probe.ts | 71 ++++++++++++++++++++- packages/posecode-render/src/index.ts | 89 ++++++++++++++++++++++++++- playground/src/presets.ts | 2 +- spec/examples/forward-lunge.posecode | 4 +- spec/examples/superman.posecode | 4 +- 5 files changed, 162 insertions(+), 8 deletions(-) diff --git a/packages/posecode-eval/src/probe.ts b/packages/posecode-eval/src/probe.ts index d7e2ba0..b69c5f3 100644 --- a/packages/posecode-eval/src/probe.ts +++ b/packages/posecode-eval/src/probe.ts @@ -102,6 +102,67 @@ export function probeMovement(source: string): ProbeResult { } } + // Precompute world positions of all effectors at start of each segment + const segmentStartEffectors: Map[] = []; + const tempYawQ = new THREE.Quaternion(); + + const getEffectorId = (eff: string) => { + if (eff === "hand_left") return "wrist_left"; + if (eff === "hand_right") return "wrist_right"; + if (eff === "foot_left") return "ankle_left"; + if (eff === "foot_right") return "ankle_right"; + return eff; + }; + + let prevEffectorsMap: Map | null = null; + let prevPins: typeof ir.phases[number]["pins"] = []; + + for (let i = 0; i < tl.segments.length; i++) { + const seg = tl.segments[i]!; + for (const bone of m.bones.values()) bone.quaternion.identity(); + const info = tl.sample(seg.start, m.bones); + + const wasPinned = (id: string) => prevPins.some(p => getEffectorId(p.effector) === id && p.anchor === "floor"); + const isPinned = (id: string) => info.pins.some(p => getEffectorId(p.effector) === id && p.anchor === "floor"); + + m.root.position.copy(baseRootPos); + m.root.quaternion.copy(baseRootQuat); + if (info.rootYaw !== 0) { + tempYawQ.setFromAxisAngle(WORLD_Y, info.rootYaw); + m.root.quaternion.premultiply(tempYawQ); + } + m.root.position.x += info.rootOffset.x; + m.root.position.z += info.rootOffset.z; + m.root.updateMatrixWorld(true); + depenetrate(m); + + const effectorsMap = new Map(); + for (const ids of Object.values(m.effectors)) { + for (const id of ids) { + const node = m.bones.get(id); + if (node) { + if (i > 0 && wasPinned(id) && isPinned(id) && prevEffectorsMap && prevEffectorsMap.has(id)) { + effectorsMap.set(id, prevEffectorsMap.get(id)!); + } else { + effectorsMap.set(id, node.getWorldPosition(new THREE.Vector3())); + } + } + } + } + segmentStartEffectors.push(effectorsMap); + prevEffectorsMap = effectorsMap; + prevPins = info.pins; + } + + // Restore initial state + for (const bone of m.bones.values()) bone.quaternion.identity(); + tl.sample(0, m.bones); + m.root.position.copy(baseRootPos); + m.root.quaternion.copy(baseRootQuat); + m.root.updateMatrixWorld(true); + depenetrate(m); + groundFigure(m); + // Sample the end of each phase, applying the viewer's per-frame root // pipeline: base root → yaw/travel → ground-lock → floor safety clamp. const yawQ = new THREE.Quaternion(); @@ -151,8 +212,14 @@ export function probeMovement(source: string): ProbeResult { if (!effector) continue; let target: THREE.Vector3 | null = null; if (pin.anchor === "floor") { - target = effector.getWorldPosition(new THREE.Vector3()); - target.y = 0; + const startPos = segmentStartEffectors[phaseIndex]?.get(effectorId); + if (startPos) { + target = startPos.clone(); + target.y = 0; + } else { + target = effector.getWorldPosition(new THREE.Vector3()); + target.y = 0; + } } else if (propScene.anchors.has(pin.anchor)) { target = propScene.anchors.get(pin.anchor)!.clone(); } else { diff --git a/packages/posecode-render/src/index.ts b/packages/posecode-render/src/index.ts index 1c462d3..b7559ee 100644 --- a/packages/posecode-render/src/index.ts +++ b/packages/posecode-render/src/index.ts @@ -60,6 +60,8 @@ export interface Viewer { getTimeline(): TimelineInfo | null; /** Precise visible world bounds; intended for audits and deterministic export. */ getVisibleBounds(): THREE.Box3; + getMannequin(): any; + getCharacter(): any; /** * Render the current time synchronously and return the frame as a PNG data * URL. Works without preserveDrawingBuffer because the read happens in the @@ -304,6 +306,7 @@ export function createViewer( // ground anchors when the character (with its own proportions) arrives. let lastIR: PosecodeIR | null = null; let groundTargets = new Map(); + const segmentStartEffectors: Map[] = []; // World-space anchor points contributed by scene props (chair seat, bar grip, // wall surface). Populated when a doc declares props; empty otherwise. let propAnchors = new Map(); @@ -321,6 +324,7 @@ export function createViewer( let tickCb: (time: number, duration: number) => void = () => {}; let loopCb: () => void = () => {}; let lastPhaseName = ""; + let activeSegIndex = 0; // Camera easing targets. const desiredTarget = new THREE.Vector3(0, 0.9, 0); @@ -504,7 +508,20 @@ export function createViewer( const effectorBone = EFFECTOR_BONE[p.effector] ?? p.effector; const effector = mannequin.bones.get(effectorBone); if (!effector) continue; - const anchor = resolveReachTarget(p.anchor, effector); + let anchor: THREE.Vector3 | null = null; + if (p.anchor === "floor") { + const startPos = segmentStartEffectors[activeSegIndex]?.get(effectorBone); + if (startPos) { + anchor = startPos.clone(); + const isFoot = effectorBone.startsWith("ankle") || effectorBone.startsWith("foot"); + const drop = isFoot ? (character ? character.proportions.soleDrop : 0.042) : 0; + anchor.y = drop; + } else { + anchor = resolveReachTarget(p.anchor, effector); + } + } else { + anchor = resolveReachTarget(p.anchor, effector); + } if (!anchor) continue; delta.add(anchor.sub(effector.getWorldPosition(new THREE.Vector3()))); n++; @@ -617,6 +634,15 @@ export function createViewer( if (timeline) { const info = timeline.sample(time, mannequin.bones); solvedInfo = info; + activeSegIndex = 0; + const tt = timeline.duration > 0 ? ((time % timeline.duration) + timeline.duration) % timeline.duration : 0; + for (let k = 0; k < timeline.segments.length; k++) { + const seg = timeline.segments[k]!; + if (tt >= seg.start && tt <= seg.end) { + activeSegIndex = k; + break; + } + } // Life layer rides on wall-clock time (not timeline time) so the figure // keeps breathing and blinking while paused or scrubbing. applyLife(performance.now() / 1000); @@ -797,6 +823,61 @@ export function createViewer( captureGroundTargets(); baseRootPos.copy(mannequin.root.position); baseRootQuat.copy(mannequin.root.quaternion); + + // Precompute world positions of all effectors at start of each segment + segmentStartEffectors.length = 0; + if (timeline) { + let prevEffectorsMap: Map | null = null; + let prevPins: PinTarget[] = []; + for (let i = 0; i < timeline.segments.length; i++) { + const seg = timeline.segments[i]!; + for (const bone of mannequin.bones.values()) bone.quaternion.identity(); + const info = timeline.sample(seg.start, mannequin.bones); + + const wasPinned = (id: string) => prevPins.some(p => (EFFECTOR_BONE[p.effector] ?? p.effector) === id && p.anchor === "floor"); + const isPinned = (id: string) => info.pins.some(p => (EFFECTOR_BONE[p.effector] ?? p.effector) === id && p.anchor === "floor"); + + mannequin.root.position.copy(baseRootPos); + mannequin.root.quaternion.copy(baseRootQuat); + if (info.rootYaw !== 0) { + const yawQ = new THREE.Quaternion().setFromAxisAngle(WORLD_Y, info.rootYaw); + mannequin.root.quaternion.premultiply(yawQ); + } + mannequin.root.position.x += info.rootOffset.x; + mannequin.root.position.z += info.rootOffset.z; + mannequin.root.updateMatrixWorld(true); + depenetrate(mannequin); + + const effectorsMap = new Map(); + for (const ids of Object.values(mannequin.effectors)) { + for (const id of ids) { + const node = mannequin.bones.get(id); + if (node) { + if (i > 0 && wasPinned(id) && isPinned(id) && prevEffectorsMap && prevEffectorsMap.has(id)) { + effectorsMap.set(id, prevEffectorsMap.get(id)!); + } else { + effectorsMap.set(id, node.getWorldPosition(new THREE.Vector3())); + } + } + } + } + segmentStartEffectors.push(effectorsMap); + prevEffectorsMap = effectorsMap; + prevPins = info.pins; + } + + // Restore initial pose + for (const bone of mannequin.bones.values()) bone.quaternion.identity(); + applyBaseRoot(); + timeline.sample(0, mannequin.bones); + mannequin.root.position.copy(baseRootPos); + mannequin.root.quaternion.copy(baseRootQuat); + mannequin.root.updateMatrixWorld(true); + depenetrate(mannequin); + groundFigureOf(mannequin); + levelPlantedFeet(mannequin, ir.phases[0]?.groundLock ?? []); + } + requestClip(ir); frameCamera(); }, @@ -848,6 +929,12 @@ export function createViewer( getVisibleBounds() { return character?.getBounds() ?? new THREE.Box3().setFromObject(mannequin.root); }, + getMannequin() { + return mannequin; + }, + getCharacter() { + return character; + }, captureFrame() { frame(); return renderer.domElement.toDataURL("image/png"); diff --git a/playground/src/presets.ts b/playground/src/presets.ts index 865d454..5a9fb5d 100644 --- a/playground/src/presets.ts +++ b/playground/src/presets.ts @@ -144,7 +144,7 @@ export const PRESETS: Preset[] = [ { id: "box-step-taps", label: "Box step taps", domain: "Warm-up", bodyPart: "Upper legs", target: "Hip flexors", equipment: "Box", difficulty: "Beginner", source: boxStepTaps }, { 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 }, { id: "step-up", label: "Step-up (box)", domain: "Functional", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Box", difficulty: "Intermediate", source: stepUp }, - { 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 }, + { 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 }, { id: "quad-stretch", label: "Standing quad stretch", domain: "Mobility", bodyPart: "Upper legs", target: "Quadriceps", equipment: "Body weight", difficulty: "Beginner", source: quadStretch }, // --- Education / anatomy: single-joint ROM demos --- diff --git a/spec/examples/forward-lunge.posecode b/spec/examples/forward-lunge.posecode index 1e4d561..c0419f7 100644 --- a/spec/examples/forward-lunge.posecode +++ b/spec/examples/forward-lunge.posecode @@ -7,7 +7,7 @@ posecode exercise "Forward lunge" knee_right: flex 95 hip_left: extend 15 knee_left: flex 80 - ankle_right: plantarflex 50 + ankle_left: plantarflex 50 spine: extend 4 travel: 0 0.3 pin: foot_left floor @@ -19,7 +19,7 @@ posecode exercise "Forward lunge" knee_right: flex 0 hip_left: extend 0 knee_left: flex 0 - ankle_right: plantarflex 0 + ankle_left: plantarflex 0 spine: flex 0 travel: 0 0 ground-lock: feet diff --git a/spec/examples/superman.posecode b/spec/examples/superman.posecode index ab4c532..b950a9e 100644 --- a/spec/examples/superman.posecode +++ b/spec/examples/superman.posecode @@ -3,7 +3,7 @@ posecode exercise "Superman" pose start = prone step "Lift" 1.5s settle: - shoulders: abduct 130 + shoulders: flex 140 spine: extend 20 chest: extend 15 neck: extend 25 @@ -12,7 +12,7 @@ posecode exercise "Superman" cue "Lift the arms, chest, and legs off the floor" step "Lower" 1.5s drive: - shoulders: abduct 0 + shoulders: flex 0 spine: flex 0 chest: flex 0 neck: flex 0 From 4cc207f3a85c934a00c9af2956eb6a65944c4fcf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:08:25 +0000 Subject: [PATCH 2/2] Fix typecheck error for optional soleDrop in floor pinning --- packages/posecode-render/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/posecode-render/src/index.ts b/packages/posecode-render/src/index.ts index b7559ee..93a97ef 100644 --- a/packages/posecode-render/src/index.ts +++ b/packages/posecode-render/src/index.ts @@ -514,7 +514,7 @@ export function createViewer( if (startPos) { anchor = startPos.clone(); const isFoot = effectorBone.startsWith("ankle") || effectorBone.startsWith("foot"); - const drop = isFoot ? (character ? character.proportions.soleDrop : 0.042) : 0; + const drop = isFoot ? (character?.proportions.soleDrop ?? 0.042) : 0; anchor.y = drop; } else { anchor = resolveReachTarget(p.anchor, effector);