@@ -61,6 +61,8 @@ export interface Viewer {
6161 getTimeline ( ) : TimelineInfo | null ;
6262 /** Precise visible world bounds; intended for audits and deterministic export. */
6363 getVisibleBounds ( ) : THREE . Box3 ;
64+ getMannequin ( ) : any ;
65+ getCharacter ( ) : any ;
6466 /**
6567 * Render the current time synchronously and return the frame as a PNG data
6668 * URL. Works without preserveDrawingBuffer because the read happens in the
@@ -305,6 +307,7 @@ export function createViewer(
305307 // ground anchors when the character (with its own proportions) arrives.
306308 let lastIR : PosecodeIR | null = null ;
307309 let groundTargets = new Map < string , THREE . Vector3 > ( ) ;
310+ const segmentStartEffectors : Map < string , THREE . Vector3 > [ ] = [ ] ;
308311 // World-space anchor points contributed by scene props (chair seat, bar grip,
309312 // wall surface). Populated when a doc declares props; empty otherwise.
310313 let propAnchors = new Map < string , THREE . Vector3 > ( ) ;
@@ -322,6 +325,7 @@ export function createViewer(
322325 let tickCb : ( time : number , duration : number ) => void = ( ) => { } ;
323326 let loopCb : ( ) => void = ( ) => { } ;
324327 let lastPhaseName = "" ;
328+ let activeSegIndex = 0 ;
325329
326330 // Camera easing targets.
327331 const desiredTarget = new THREE . Vector3 ( 0 , 0.9 , 0 ) ;
@@ -505,7 +509,20 @@ export function createViewer(
505509 const effectorBone = EFFECTOR_BONE [ p . effector ] ?? p . effector ;
506510 const effector = mannequin . bones . get ( effectorBone ) ;
507511 if ( ! effector ) continue ;
508- const anchor = resolveReachTarget ( p . anchor , effector ) ;
512+ let anchor : THREE . Vector3 | null = null ;
513+ if ( p . anchor === "floor" ) {
514+ const startPos = segmentStartEffectors [ activeSegIndex ] ?. get ( effectorBone ) ;
515+ if ( startPos ) {
516+ anchor = startPos . clone ( ) ;
517+ const isFoot = effectorBone . startsWith ( "ankle" ) || effectorBone . startsWith ( "foot" ) ;
518+ const drop = isFoot ? ( character ?. proportions . soleDrop ?? 0.042 ) : 0 ;
519+ anchor . y = drop ;
520+ } else {
521+ anchor = resolveReachTarget ( p . anchor , effector ) ;
522+ }
523+ } else {
524+ anchor = resolveReachTarget ( p . anchor , effector ) ;
525+ }
509526 if ( ! anchor ) continue ;
510527 delta . add ( anchor . sub ( effector . getWorldPosition ( new THREE . Vector3 ( ) ) ) ) ;
511528 n ++ ;
@@ -631,6 +648,15 @@ export function createViewer(
631648 if ( timeline ) {
632649 const info = timeline . sample ( time , mannequin . bones ) ;
633650 solvedInfo = info ;
651+ activeSegIndex = 0 ;
652+ const tt = timeline . duration > 0 ? ( ( time % timeline . duration ) + timeline . duration ) % timeline . duration : 0 ;
653+ for ( let k = 0 ; k < timeline . segments . length ; k ++ ) {
654+ const seg = timeline . segments [ k ] ! ;
655+ if ( tt >= seg . start && tt <= seg . end ) {
656+ activeSegIndex = k ;
657+ break ;
658+ }
659+ }
634660 // Life layer rides on wall-clock time (not timeline time) so the figure
635661 // keeps breathing and blinking while paused or scrubbing.
636662 applyLife ( performance . now ( ) / 1000 ) ;
@@ -823,6 +849,61 @@ export function createViewer(
823849 captureGroundTargets ( ) ;
824850 baseRootPos . copy ( mannequin . root . position ) ;
825851 baseRootQuat . copy ( mannequin . root . quaternion ) ;
852+
853+ // Precompute world positions of all effectors at start of each segment
854+ segmentStartEffectors . length = 0 ;
855+ if ( timeline ) {
856+ let prevEffectorsMap : Map < string , THREE . Vector3 > | null = null ;
857+ let prevPins : PinTarget [ ] = [ ] ;
858+ for ( let i = 0 ; i < timeline . segments . length ; i ++ ) {
859+ const seg = timeline . segments [ i ] ! ;
860+ for ( const bone of mannequin . bones . values ( ) ) bone . quaternion . identity ( ) ;
861+ const info = timeline . sample ( seg . start , mannequin . bones ) ;
862+
863+ const wasPinned = ( id : string ) => prevPins . some ( p => ( EFFECTOR_BONE [ p . effector ] ?? p . effector ) === id && p . anchor === "floor" ) ;
864+ const isPinned = ( id : string ) => info . pins . some ( p => ( EFFECTOR_BONE [ p . effector ] ?? p . effector ) === id && p . anchor === "floor" ) ;
865+
866+ mannequin . root . position . copy ( baseRootPos ) ;
867+ mannequin . root . quaternion . copy ( baseRootQuat ) ;
868+ if ( info . rootYaw !== 0 ) {
869+ const yawQ = new THREE . Quaternion ( ) . setFromAxisAngle ( WORLD_Y , info . rootYaw ) ;
870+ mannequin . root . quaternion . premultiply ( yawQ ) ;
871+ }
872+ mannequin . root . position . x += info . rootOffset . x ;
873+ mannequin . root . position . z += info . rootOffset . z ;
874+ mannequin . root . updateMatrixWorld ( true ) ;
875+ depenetrate ( mannequin ) ;
876+
877+ const effectorsMap = new Map < string , THREE . Vector3 > ( ) ;
878+ for ( const ids of Object . values ( mannequin . effectors ) ) {
879+ for ( const id of ids ) {
880+ const node = mannequin . bones . get ( id ) ;
881+ if ( node ) {
882+ if ( i > 0 && wasPinned ( id ) && isPinned ( id ) && prevEffectorsMap && prevEffectorsMap . has ( id ) ) {
883+ effectorsMap . set ( id , prevEffectorsMap . get ( id ) ! ) ;
884+ } else {
885+ effectorsMap . set ( id , node . getWorldPosition ( new THREE . Vector3 ( ) ) ) ;
886+ }
887+ }
888+ }
889+ }
890+ segmentStartEffectors . push ( effectorsMap ) ;
891+ prevEffectorsMap = effectorsMap ;
892+ prevPins = info . pins ;
893+ }
894+
895+ // Restore initial pose
896+ for ( const bone of mannequin . bones . values ( ) ) bone . quaternion . identity ( ) ;
897+ applyBaseRoot ( ) ;
898+ timeline . sample ( 0 , mannequin . bones ) ;
899+ mannequin . root . position . copy ( baseRootPos ) ;
900+ mannequin . root . quaternion . copy ( baseRootQuat ) ;
901+ mannequin . root . updateMatrixWorld ( true ) ;
902+ depenetrate ( mannequin ) ;
903+ groundFigureOf ( mannequin ) ;
904+ levelPlantedFeet ( mannequin , ir . phases [ 0 ] ?. groundLock ?? [ ] ) ;
905+ }
906+
826907 requestClip ( ir ) ;
827908 frameCamera ( ) ;
828909 } ,
@@ -874,6 +955,12 @@ export function createViewer(
874955 getVisibleBounds ( ) {
875956 return character ?. getBounds ( ) ?? new THREE . Box3 ( ) . setFromObject ( mannequin . root ) ;
876957 } ,
958+ getMannequin ( ) {
959+ return mannequin ;
960+ } ,
961+ getCharacter ( ) {
962+ return character ;
963+ } ,
877964 captureFrame ( ) {
878965 frame ( ) ;
879966 return renderer . domElement . toDataURL ( "image/png" ) ;
0 commit comments