@@ -28,6 +28,7 @@ import type { PosecodeEditor } from "./editor.js";
2828import { ANIMATION_PROGRESS_MESSAGE , PRESETS } from "./presets.js" ;
2929import { prioritizeFeaturedMovement } from "./library-order.js" ;
3030import { SHOWCASE_CLIPS } from "./clips.js" ;
31+ import { previewTimeForLine } from "./direct-manipulation.js" ;
3132
3233// During source-only typechecks the playground resolves posecode-render's last
3334// built declaration bundle. Keep the local extension explicit until the normal
@@ -98,6 +99,7 @@ let scrubDiagnosticsRefresh = 0;
9899let documentRevision = 1 ;
99100let pendingRenderTrigger : RenderTrigger = "initial" ;
100101let selectedBoneIds : readonly string [ ] = [ ] ;
102+ let pendingPreviewLine : number | null = null ;
101103
102104/** Keep the source selection and its live 3D joint markers in sync. */
103105function handleJointSelect (
@@ -295,13 +297,18 @@ function computePhaseRanges(
295297}
296298
297299let debounce = 0 ;
298- function scheduleRecompile ( ) : void {
300+ function scheduleRecompile ( previewLine ?: number ) : void {
299301 window . clearTimeout ( debounce ) ;
302+ pendingPreviewLine = previewLine ?? null ;
300303 debounce = window . setTimeout ( recompile , 250 ) ;
301304}
302305
303306/** Keep the address bar and library label in sync with editor changes. */
304- function handleEditorChange ( source : string , userInitiated : boolean ) : void {
307+ function handleEditorChange (
308+ source : string ,
309+ userInitiated : boolean ,
310+ context ?: { previewLine : number } ,
311+ ) : void {
305312 const editedDocumentKind = documentKind ( ) ;
306313 const preset = PRESETS . find ( ( p ) => p . source === source ) ;
307314 currentPresetId = preset ?. id ?? null ;
@@ -316,7 +323,7 @@ function handleEditorChange(source: string, userInitiated: boolean): void {
316323 source . trim ( ) ? "Custom movement" : "New movement" ,
317324 ) ;
318325 history . replaceState ( null , "" , buildNicePlayPath ( source ) ) ;
319- scheduleRecompile ( ) ;
326+ scheduleRecompile ( context ?. previewLine ) ;
320327}
321328
322329function recompile ( ) : void {
@@ -352,8 +359,6 @@ function recompile(): void {
352359 ) ;
353360 viewer . setLoop ( loop . checked ) ;
354361 viewer . setSpeed ( Number ( speed . value ) ) ;
355- viewer . play ( ) ;
356- setPlaying ( true ) ;
357362 const tl = viewer . getTimeline ( ) ;
358363 repeat = tl ?. repeat ?? 1 ;
359364 rep = 1 ;
@@ -364,6 +369,26 @@ function recompile(): void {
364369 tl ?. segments . length ?? 0 ,
365370 ) ;
366371 ed . highlightPhase ( null ) ; // next onPhase paints the active block
372+
373+ const previewTime =
374+ pendingPreviewLine !== null && tl
375+ ? previewTimeForLine ( pendingPreviewLine , phaseRanges , tl . segments )
376+ : null ;
377+ pendingPreviewLine = null ;
378+ if ( previewTime !== null && tl ) {
379+ // Direct manipulation is a pose inspection workflow: hold the affected
380+ // keyframe so even a fast phase visibly responds to a one-degree edit.
381+ viewer . seek ( previewTime ) ;
382+ viewer . pause ( ) ;
383+ setPlaying ( false ) ;
384+ scrub . value = String ( Math . round ( ( previewTime / ( tl . duration || 1 ) ) * 1000 ) ) ;
385+ paintScrub ( ) ;
386+ clock . textContent = `${ previewTime . toFixed ( 1 ) } s` ;
387+ scheduleScrubDiagnosticsRefresh ( ) ;
388+ } else {
389+ viewer . play ( ) ;
390+ setPlaying ( true ) ;
391+ }
367392 }
368393}
369394
0 commit comments