|
3 | 3 | * render loop and the headless eval harness (posecode-eval) use the identical |
4 | 4 | * solver. |
5 | 5 | * |
6 | | - * "Ground-lock" = keep the declared grouped or per-side effectors planted while the |
7 | | - * body moves, tuned per support type: |
| 6 | + * "Ground-lock" = keep the declared grouped, per-side, or axial contacts |
| 7 | + * planted while the body moves, tuned per support type: |
8 | 8 | * |
9 | 9 | * - **Hands + feet (push-up / plank):** pivot the whole rigid body about the |
10 | 10 | * foot line (the toes stay planted) until the hands reach the floor. As the |
|
23 | 23 | * the heels). Only feet near the floor anchor (a swing leg in a curl or |
24 | 24 | * march must stay free), and only the average delta is corrected so |
25 | 25 | * symmetric spreads (jumping jacks) don't fight the lock. |
| 26 | + * - **Back (dead bug / supine floor work):** translate the body vertically so |
| 27 | + * the pelvis-to-ribcage surface stays on the floor while the limbs move. |
26 | 28 | * |
27 | 29 | * Both paths ground the visible MESH (bounding boxes), not just bone origins: |
28 | 30 | * an ankle bone sits ~0.04m above the sole, so anchoring bones alone left the |
@@ -97,8 +99,14 @@ export function applyGroundLock( |
97 | 99 | const hands = ids.filter((id) => id.startsWith("wrist")); |
98 | 100 | const forearms = ids.filter((id) => id.startsWith("elbow")); |
99 | 101 | const feet = ids.filter((id) => id.startsWith("ankle")); |
| 102 | + const back = ids.filter((id) => id === "pelvis" || id === "spine" || id === "chest"); |
100 | 103 | const upperSupports = forearms.length > 0 ? forearms : hands; |
101 | 104 |
|
| 105 | + if (back.length > 0) { |
| 106 | + dropOwnMeshesToFloor(m, back); |
| 107 | + return; |
| 108 | + } |
| 109 | + |
102 | 110 | if (upperSupports.length > 0 && feet.length > 0) { |
103 | 111 | // Plant the feet FIRST: drop the body so the foot mesh rests on the floor, |
104 | 112 | // so the pivot the body then rotates about is itself at floor level. The |
@@ -146,6 +154,37 @@ export function applyGroundLock( |
146 | 154 | } |
147 | 155 | } |
148 | 156 |
|
| 157 | +/** |
| 158 | + * Drop only the meshes owned by the selected bones onto the floor. `Box3` on |
| 159 | + * a torso bone would include its child limbs, so an overhead arm could |
| 160 | + * otherwise lift a supine person's back. Bone child subtrees are deliberately |
| 161 | + * excluded; non-bone groups (capsules/ellipsoids) remain part of the surface. |
| 162 | + */ |
| 163 | +function dropOwnMeshesToFloor(m: Mannequin, boneIds: string[]): void { |
| 164 | + const boneNodes = new Set(m.bones.values()); |
| 165 | + const box = new THREE.Box3(); |
| 166 | + const childBox = new THREE.Box3(); |
| 167 | + let found = false; |
| 168 | + |
| 169 | + for (const id of boneIds) { |
| 170 | + const bone = m.bones.get(id); |
| 171 | + if (!bone) continue; |
| 172 | + for (const child of bone.children) { |
| 173 | + if (boneNodes.has(child)) continue; |
| 174 | + childBox.setFromObject(child); |
| 175 | + if (!childBox.isEmpty()) { |
| 176 | + box.union(childBox); |
| 177 | + found = true; |
| 178 | + } |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + if (found && Number.isFinite(box.min.y)) { |
| 183 | + m.root.position.y -= box.min.y; |
| 184 | + m.root.updateMatrixWorld(true); |
| 185 | + } |
| 186 | +} |
| 187 | + |
149 | 188 | /** |
150 | 189 | * Drop the whole body vertically so the lowest FOOT-mesh point rests on the |
151 | 190 | * floor. Grounds the visible sole (bounding box), not the ankle bone, which |
|
0 commit comments