|
7 | 7 | * and returns world-space bone positions at the end of every phase. This is |
8 | 8 | * the ground truth the invariant checks score against. |
9 | 9 | * |
10 | | - * Known gap vs the viewer: contact pins and reach-IK are scene-dependent |
11 | | - * (their anchors can live on props), so the probe skips them; pinned/reaching |
12 | | - * movements are covered by the parse/clamp invariants only. |
| 10 | + * Prop-backed pins and reach-IK remain scene-dependent. Floor/body-landmark |
| 11 | + * pins are deterministic, however, so the probe resolves those exactly like |
| 12 | + * the viewer; this lets contact regressions in floor work (cobra, etc.) fail |
| 13 | + * the scorecard instead of being hidden behind `usesSceneIk`. |
13 | 14 | */ |
14 | 15 |
|
15 | 16 | import * as THREE from "three"; |
@@ -109,6 +110,40 @@ export function probeMovement(source: string): ProbeResult { |
109 | 110 | anchors.set(id, v); |
110 | 111 | } |
111 | 112 | applyGroundLock(m, info.groundLock, anchors); |
| 113 | + // Resolve scene-independent pins. Unknown names here are prop anchors and |
| 114 | + // intentionally remain for browser-level coverage. |
| 115 | + if (info.pins.length > 0) { |
| 116 | + const delta = new THREE.Vector3(); |
| 117 | + let pinCount = 0; |
| 118 | + for (const pin of info.pins) { |
| 119 | + const effectorId = pin.effector === "hand_left" |
| 120 | + ? "wrist_left" |
| 121 | + : pin.effector === "hand_right" |
| 122 | + ? "wrist_right" |
| 123 | + : pin.effector === "foot_left" |
| 124 | + ? "ankle_left" |
| 125 | + : pin.effector === "foot_right" |
| 126 | + ? "ankle_right" |
| 127 | + : pin.effector; |
| 128 | + const effector = m.bones.get(effectorId); |
| 129 | + if (!effector) continue; |
| 130 | + let target: THREE.Vector3 | null = null; |
| 131 | + if (pin.anchor === "floor") { |
| 132 | + target = effector.getWorldPosition(new THREE.Vector3()); |
| 133 | + target.y = 0; |
| 134 | + } else { |
| 135 | + const landmark = m.bones.get(pin.anchor); |
| 136 | + if (landmark) target = landmark.getWorldPosition(new THREE.Vector3()); |
| 137 | + } |
| 138 | + if (!target) continue; |
| 139 | + delta.add(target.sub(effector.getWorldPosition(new THREE.Vector3()))); |
| 140 | + pinCount++; |
| 141 | + } |
| 142 | + if (pinCount > 0) { |
| 143 | + m.root.position.add(delta.multiplyScalar(1 / pinCount)); |
| 144 | + m.root.updateMatrixWorld(true); |
| 145 | + } |
| 146 | + } |
112 | 147 | // Viewer safety net: never leave the lowest mesh point below the floor. |
113 | 148 | m.root.updateMatrixWorld(true); |
114 | 149 | const box = new THREE.Box3().setFromObject(m.root); |
|
0 commit comments