@@ -60,6 +60,8 @@ export interface Viewer {
6060 getTimeline ( ) : TimelineInfo | null ;
6161 /** Precise visible world bounds; intended for audits and deterministic export. */
6262 getVisibleBounds ( ) : THREE . Box3 ;
63+ getMannequin ( ) : any ;
64+ getCharacter ( ) : any ;
6365 /**
6466 * Render the current time synchronously and return the frame as a PNG data
6567 * URL. Works without preserveDrawingBuffer because the read happens in the
@@ -304,6 +306,7 @@ export function createViewer(
304306 // ground anchors when the character (with its own proportions) arrives.
305307 let lastIR : PosecodeIR | null = null ;
306308 let groundTargets = new Map < string , THREE . Vector3 > ( ) ;
309+ const segmentStartEffectors : Map < string , THREE . Vector3 > [ ] = [ ] ;
307310 // World-space anchor points contributed by scene props (chair seat, bar grip,
308311 // wall surface). Populated when a doc declares props; empty otherwise.
309312 let propAnchors = new Map < string , THREE . Vector3 > ( ) ;
@@ -321,6 +324,7 @@ export function createViewer(
321324 let tickCb : ( time : number , duration : number ) => void = ( ) => { } ;
322325 let loopCb : ( ) => void = ( ) => { } ;
323326 let lastPhaseName = "" ;
327+ let activeSegIndex = 0 ;
324328
325329 // Camera easing targets.
326330 const desiredTarget = new THREE . Vector3 ( 0 , 0.9 , 0 ) ;
@@ -504,7 +508,20 @@ export function createViewer(
504508 const effectorBone = EFFECTOR_BONE [ p . effector ] ?? p . effector ;
505509 const effector = mannequin . bones . get ( effectorBone ) ;
506510 if ( ! effector ) continue ;
507- const anchor = resolveReachTarget ( p . anchor , effector ) ;
511+ let anchor : THREE . Vector3 | null = null ;
512+ if ( p . anchor === "floor" ) {
513+ const startPos = segmentStartEffectors [ activeSegIndex ] ?. get ( effectorBone ) ;
514+ if ( startPos ) {
515+ anchor = startPos . clone ( ) ;
516+ const isFoot = effectorBone . startsWith ( "ankle" ) || effectorBone . startsWith ( "foot" ) ;
517+ const drop = isFoot ? ( character ? character . proportions . soleDrop : 0.042 ) : 0 ;
518+ anchor . y = drop ;
519+ } else {
520+ anchor = resolveReachTarget ( p . anchor , effector ) ;
521+ }
522+ } else {
523+ anchor = resolveReachTarget ( p . anchor , effector ) ;
524+ }
508525 if ( ! anchor ) continue ;
509526 delta . add ( anchor . sub ( effector . getWorldPosition ( new THREE . Vector3 ( ) ) ) ) ;
510527 n ++ ;
@@ -617,6 +634,15 @@ export function createViewer(
617634 if ( timeline ) {
618635 const info = timeline . sample ( time , mannequin . bones ) ;
619636 solvedInfo = info ;
637+ activeSegIndex = 0 ;
638+ const tt = timeline . duration > 0 ? ( ( time % timeline . duration ) + timeline . duration ) % timeline . duration : 0 ;
639+ for ( let k = 0 ; k < timeline . segments . length ; k ++ ) {
640+ const seg = timeline . segments [ k ] ! ;
641+ if ( tt >= seg . start && tt <= seg . end ) {
642+ activeSegIndex = k ;
643+ break ;
644+ }
645+ }
620646 // Life layer rides on wall-clock time (not timeline time) so the figure
621647 // keeps breathing and blinking while paused or scrubbing.
622648 applyLife ( performance . now ( ) / 1000 ) ;
@@ -797,6 +823,61 @@ export function createViewer(
797823 captureGroundTargets ( ) ;
798824 baseRootPos . copy ( mannequin . root . position ) ;
799825 baseRootQuat . copy ( mannequin . root . quaternion ) ;
826+
827+ // Precompute world positions of all effectors at start of each segment
828+ segmentStartEffectors . length = 0 ;
829+ if ( timeline ) {
830+ let prevEffectorsMap : Map < string , THREE . Vector3 > | null = null ;
831+ let prevPins : PinTarget [ ] = [ ] ;
832+ for ( let i = 0 ; i < timeline . segments . length ; i ++ ) {
833+ const seg = timeline . segments [ i ] ! ;
834+ for ( const bone of mannequin . bones . values ( ) ) bone . quaternion . identity ( ) ;
835+ const info = timeline . sample ( seg . start , mannequin . bones ) ;
836+
837+ const wasPinned = ( id : string ) => prevPins . some ( p => ( EFFECTOR_BONE [ p . effector ] ?? p . effector ) === id && p . anchor === "floor" ) ;
838+ const isPinned = ( id : string ) => info . pins . some ( p => ( EFFECTOR_BONE [ p . effector ] ?? p . effector ) === id && p . anchor === "floor" ) ;
839+
840+ mannequin . root . position . copy ( baseRootPos ) ;
841+ mannequin . root . quaternion . copy ( baseRootQuat ) ;
842+ if ( info . rootYaw !== 0 ) {
843+ const yawQ = new THREE . Quaternion ( ) . setFromAxisAngle ( WORLD_Y , info . rootYaw ) ;
844+ mannequin . root . quaternion . premultiply ( yawQ ) ;
845+ }
846+ mannequin . root . position . x += info . rootOffset . x ;
847+ mannequin . root . position . z += info . rootOffset . z ;
848+ mannequin . root . updateMatrixWorld ( true ) ;
849+ depenetrate ( mannequin ) ;
850+
851+ const effectorsMap = new Map < string , THREE . Vector3 > ( ) ;
852+ for ( const ids of Object . values ( mannequin . effectors ) ) {
853+ for ( const id of ids ) {
854+ const node = mannequin . bones . get ( id ) ;
855+ if ( node ) {
856+ if ( i > 0 && wasPinned ( id ) && isPinned ( id ) && prevEffectorsMap && prevEffectorsMap . has ( id ) ) {
857+ effectorsMap . set ( id , prevEffectorsMap . get ( id ) ! ) ;
858+ } else {
859+ effectorsMap . set ( id , node . getWorldPosition ( new THREE . Vector3 ( ) ) ) ;
860+ }
861+ }
862+ }
863+ }
864+ segmentStartEffectors . push ( effectorsMap ) ;
865+ prevEffectorsMap = effectorsMap ;
866+ prevPins = info . pins ;
867+ }
868+
869+ // Restore initial pose
870+ for ( const bone of mannequin . bones . values ( ) ) bone . quaternion . identity ( ) ;
871+ applyBaseRoot ( ) ;
872+ timeline . sample ( 0 , mannequin . bones ) ;
873+ mannequin . root . position . copy ( baseRootPos ) ;
874+ mannequin . root . quaternion . copy ( baseRootQuat ) ;
875+ mannequin . root . updateMatrixWorld ( true ) ;
876+ depenetrate ( mannequin ) ;
877+ groundFigureOf ( mannequin ) ;
878+ levelPlantedFeet ( mannequin , ir . phases [ 0 ] ?. groundLock ?? [ ] ) ;
879+ }
880+
800881 requestClip ( ir ) ;
801882 frameCamera ( ) ;
802883 } ,
@@ -848,6 +929,12 @@ export function createViewer(
848929 getVisibleBounds ( ) {
849930 return character ?. getBounds ( ) ?? new THREE . Box3 ( ) . setFromObject ( mannequin . root ) ;
850931 } ,
932+ getMannequin ( ) {
933+ return mannequin ;
934+ } ,
935+ getCharacter ( ) {
936+ return character ;
937+ } ,
851938 captureFrame ( ) {
852939 frame ( ) ;
853940 return renderer . domElement . toDataURL ( "image/png" ) ;
0 commit comments