@@ -27,7 +27,7 @@ import {
2727 type ClipSource ,
2828} from "./clips.js" ;
2929import { depenetrate } from "./depenetrate.js" ;
30- import { alignFloorPalms , levelPlantedFeet , wrapGrip , relaxHands , swingArms } from "./contacts.js" ;
30+ import { alignFloorPalms , levelPlantedFeet , wrapGrip , relaxHands , swingArms , aimHead } from "./contacts.js" ;
3131
3232const DEG = Math . PI / 180 ;
3333
@@ -279,6 +279,8 @@ export function createViewer(
279279 let authoredFingers = new Set < string > ( ) ;
280280 // Shoulders the document poses: L4.2 arm-swing leaves these to the author.
281281 let authoredShoulders = new Set < string > ( ) ;
282+ // True when the document poses the head/neck: L4.3 look-at then stays off.
283+ let authoredHead = false ;
282284 // The last loaded document, kept so the viewer can re-solve base pose and
283285 // ground anchors when the character (with its own proportions) arrives.
284286 let lastIR : PosecodeIR | null = null ;
@@ -538,6 +540,31 @@ export function createViewer(
538540 wrapGrip ( mannequin , grips ) ;
539541 }
540542
543+ /**
544+ * L4.3 look-at: turn the head toward the action. Collects the world points of
545+ * this phase's active grips/reaches (up at the bar, down at a floor reach) and
546+ * aims the head at their average. Skipped when the document poses the head/neck.
547+ */
548+ function applyLookAt ( info : { grips : GripTarget [ ] ; reaches : ReachTarget [ ] } ) : void {
549+ if ( authoredHead ) return ;
550+ const pts : THREE . Vector3 [ ] = [ ] ;
551+ const collect = ( effectorName : string , anchorName : string ) : void => {
552+ const bone = EFFECTOR_BONE [ effectorName ] ?? effectorName ;
553+ const eff = mannequin . bones . get ( bone ) ;
554+ if ( ! eff ) return ;
555+ const t =
556+ resolveReachTarget ( anchorName , eff ) ??
557+ resolveReachTarget ( anchorName . replace ( / _ ( l e f t | r i g h t ) $ / , "" ) , eff ) ;
558+ if ( t ) pts . push ( t ) ;
559+ } ;
560+ for ( const g of info . grips ) collect ( g . effector , g . anchor ) ;
561+ for ( const r of info . reaches ) collect ( r . effector , r . target ) ;
562+ if ( pts . length === 0 ) return ;
563+ const focus = new THREE . Vector3 ( ) ;
564+ for ( const p of pts ) focus . add ( p ) ;
565+ aimHead ( mannequin , focus . multiplyScalar ( 1 / pts . length ) ) ;
566+ }
567+
541568 function frameCamera ( ) : void {
542569 // Auto-frame the figure: fit its bounding box, keep a pleasant angle.
543570 // Include any scene prop too: a pull-up bar sits well above the figure's
@@ -609,6 +636,8 @@ export function createViewer(
609636 swingArms ( mannequin , authoredShoulders , gripSidesOf ( info . grips ) ) ;
610637 // L4.1 aliveness: relax idle hands into a natural curl (grips still wrap).
611638 relaxHands ( mannequin , gripSidesOf ( info . grips ) , authoredFingers ) ;
639+ // L4.3 aliveness: turn the head toward the active contact (bar / floor reach).
640+ applyLookAt ( info ) ;
612641 // Safety net: nothing above ever intentionally pushes part of the body
613642 // below the floor, so clamp the root up whenever the lowest point dips
614643 // below y=0, a no-op whenever the pose is legitimately grounded or
@@ -709,8 +738,10 @@ export function createViewer(
709738 levelPlantedFeet ( mannequin , ir . phases [ 0 ] ?. groundLock ?? [ ] ) ;
710739 authoredFingers = new Set ( timeline . bonesUsed . filter ( isFingerId ) ) ;
711740 authoredShoulders = new Set ( timeline . bonesUsed . filter ( ( id ) => id . startsWith ( "shoulder_" ) ) ) ;
741+ authoredHead = timeline . bonesUsed . some ( ( id ) => id === "head" || id === "neck" ) ;
712742 swingArms ( mannequin , authoredShoulders , gripSidesOf ( ir . phases [ 0 ] ?. grips ?? [ ] ) ) ;
713743 relaxHands ( mannequin , gripSidesOf ( ir . phases [ 0 ] ?. grips ?? [ ] ) , authoredFingers ) ;
744+ applyLookAt ( { grips : ir . phases [ 0 ] ?. grips ?? [ ] , reaches : ir . phases [ 0 ] ?. reaches ?? [ ] } ) ;
714745 captureGroundTargets ( ) ;
715746 baseRootPos . copy ( mannequin . root . position ) ;
716747 baseRootQuat . copy ( mannequin . root . quaternion ) ;
0 commit comments