@@ -27,7 +27,7 @@ import {
2727 type ClipSource ,
2828} from "./clips.js" ;
2929import { depenetrate } from "./depenetrate.js" ;
30- import { alignFloorPalms , levelPlantedFeet , wrapGrip } from "./contacts.js" ;
30+ import { alignFloorPalms , levelPlantedFeet , wrapGrip , relaxHands } from "./contacts.js" ;
3131
3232const DEG = Math . PI / 180 ;
3333
@@ -267,6 +267,9 @@ export function createViewer(
267267 }
268268
269269 let timeline : BuiltTimeline | null = null ;
270+ // Finger bones the loaded document explicitly poses (make-a-fist, finger-spell,
271+ // hand-wave): the L4.1 resting-hand curl leaves these alone.
272+ let authoredFingers = new Set < string > ( ) ;
270273 // The last loaded document, kept so the viewer can re-solve base pose and
271274 // ground anchors when the character (with its own proportions) arrives.
272275 let lastIR : PosecodeIR | null = null ;
@@ -593,6 +596,8 @@ export function createViewer(
593596 // lower-body poses (squat, lunge, deadlift) don't balance on the toes.
594597 // Runs before the floor clamp so the leveled sole is what rests on y=0.
595598 levelPlantedFeet ( mannequin , info . groundLock ) ;
599+ // L4.1 aliveness: relax idle hands into a natural curl (grips still wrap).
600+ relaxHands ( mannequin , gripSidesOf ( info . grips ) , authoredFingers ) ;
596601 // Safety net: nothing above ever intentionally pushes part of the body
597602 // below the floor, so clamp the root up whenever the lowest point dips
598603 // below y=0, a no-op whenever the pose is legitimately grounded or
@@ -691,6 +696,8 @@ export function createViewer(
691696 depenetrate ( mannequin ) ;
692697 groundFigureOf ( mannequin ) ;
693698 levelPlantedFeet ( mannequin , ir . phases [ 0 ] ?. groundLock ?? [ ] ) ;
699+ authoredFingers = new Set ( timeline . bonesUsed . filter ( isFingerId ) ) ;
700+ relaxHands ( mannequin , gripSidesOf ( ir . phases [ 0 ] ?. grips ?? [ ] ) , authoredFingers ) ;
694701 captureGroundTargets ( ) ;
695702 baseRootPos . copy ( mannequin . root . position ) ;
696703 baseRootQuat . copy ( mannequin . root . quaternion ) ;
@@ -797,6 +804,21 @@ export function createViewer(
797804 return api ;
798805}
799806
807+ /** True for a finger bone id (thumb/index/middle/ring/pinky_left|right). */
808+ function isFingerId ( id : string ) : boolean {
809+ return / ^ ( t h u m b | i n d e x | m i d d l e | r i n g | p i n k y ) _ / . test ( id ) ;
810+ }
811+
812+ /** The hand sides ("left"/"right") gripping this phase, from its grip targets. */
813+ function gripSidesOf ( grips : readonly { effector : string } [ ] ) : Set < "left" | "right" > {
814+ const sides = new Set < "left" | "right" > ( ) ;
815+ for ( const g of grips ) {
816+ if ( g . effector . endsWith ( "_left" ) || g . effector === "hands" ) sides . add ( "left" ) ;
817+ if ( g . effector . endsWith ( "_right" ) || g . effector === "hands" ) sides . add ( "right" ) ;
818+ }
819+ return sides ;
820+ }
821+
800822function enableShadows ( root : THREE . Object3D ) : void {
801823 root . traverse ( ( obj ) => {
802824 if ( ( obj as THREE . Mesh ) . isMesh ) {
0 commit comments