@@ -171,6 +171,16 @@ var presentationAnnotationState = {
171171 resizeBound : false
172172} ;
173173
174+ // ── Parameter HUD — live numeric / boolean readouts drawn on the overlay canvas ──
175+ var presentationParamHudState = {
176+ enabled : true ,
177+ includeInRecording : false ,
178+ items : [ ] , // array of { path, label }
179+ anchorX : 0.0 , // 0–1 fractional position (left edge of box)
180+ anchorY : 1.0 , // 0–1 fractional position (bottom edge of box, so 1=bottom)
181+ fontSize : 11 // px
182+ } ;
183+
174184var presentationUiRefreshAccumulator = 0.0 ;
175185
176186function clonePresentationData ( value ) {
@@ -314,6 +324,184 @@ function getPresentationAnnotationsState() {
314324 } ;
315325}
316326
327+ // ── Parameter HUD API ────────────────────────────────────────────────────────
328+
329+ function setPresentationParamHudEnabled ( enabled ) {
330+ presentationParamHudState . enabled = ! ! enabled ;
331+ updatePresentationOverlay ( ) ;
332+ return presentationParamHudState . enabled ;
333+ }
334+
335+ function setPresentationParamHudIncludedInRecording ( enabled ) {
336+ presentationParamHudState . includeInRecording = ! ! enabled ;
337+ return presentationParamHudState . includeInRecording ;
338+ }
339+
340+ function getPresentationParamHudState ( ) {
341+ return {
342+ enabled : ! ! presentationParamHudState . enabled ,
343+ includeInRecording : ! ! presentationParamHudState . includeInRecording ,
344+ anchorX : presentationParamHudState . anchorX ,
345+ anchorY : presentationParamHudState . anchorY ,
346+ fontSize : presentationParamHudState . fontSize ,
347+ items : clonePresentationData ( presentationParamHudState . items )
348+ } ;
349+ }
350+
351+ function setParamHudLayout ( opts ) {
352+ if ( ! opts || typeof opts !== 'object' ) return ;
353+ if ( typeof opts . anchorX === 'number' && isFinite ( opts . anchorX ) ) {
354+ presentationParamHudState . anchorX = Math . max ( 0 , Math . min ( 1 , opts . anchorX ) ) ;
355+ }
356+ if ( typeof opts . anchorY === 'number' && isFinite ( opts . anchorY ) ) {
357+ presentationParamHudState . anchorY = Math . max ( 0 , Math . min ( 1 , opts . anchorY ) ) ;
358+ }
359+ if ( typeof opts . fontSize === 'number' && isFinite ( opts . fontSize ) ) {
360+ presentationParamHudState . fontSize = Math . max ( 8 , Math . min ( 48 , Math . round ( opts . fontSize ) ) ) ;
361+ }
362+ updatePresentationOverlay ( ) ;
363+ }
364+
365+ function isParamInHud ( path ) {
366+ for ( var i = 0 ; i < presentationParamHudState . items . length ; i ++ ) {
367+ if ( presentationParamHudState . items [ i ] . path === path ) return true ;
368+ }
369+ return false ;
370+ }
371+
372+ function addParamToHud ( path , label ) {
373+ if ( ! path || typeof path !== 'string' ) return false ;
374+ if ( isParamInHud ( path ) ) return false ;
375+ presentationParamHudState . items . push ( { path : path , label : label || path } ) ;
376+ updatePresentationOverlay ( ) ;
377+ return true ;
378+ }
379+
380+ function removeParamFromHud ( path ) {
381+ var before = presentationParamHudState . items . length ;
382+ presentationParamHudState . items = presentationParamHudState . items . filter ( function ( item ) {
383+ return item . path !== path ;
384+ } ) ;
385+ if ( presentationParamHudState . items . length !== before ) {
386+ updatePresentationOverlay ( ) ;
387+ return true ;
388+ }
389+ return false ;
390+ }
391+
392+ function toggleParamInHud ( path , label ) {
393+ if ( isParamInHud ( path ) ) {
394+ removeParamFromHud ( path ) ;
395+ return false ;
396+ }
397+ addParamToHud ( path , label ) ;
398+ return true ;
399+ }
400+
401+ function clearParamHud ( ) {
402+ presentationParamHudState . items = [ ] ;
403+ updatePresentationOverlay ( ) ;
404+ }
405+
406+ function formatParamHudValue ( val ) {
407+ if ( val === undefined || val === null ) return '\u2014' ;
408+ if ( typeof val === 'boolean' ) return val ? 'true' : 'false' ;
409+ if ( typeof val === 'number' ) {
410+ if ( ! isFinite ( val ) ) return String ( val ) ;
411+ // Snap floating-point noise near zero to zero
412+ if ( Math . abs ( val ) < 1e-9 ) return '0' ;
413+ var abs = Math . abs ( val ) ;
414+ // Choose decimal places so we get ~4 significant figures, no sci notation
415+ var decimals ;
416+ if ( abs >= 1000 ) decimals = 0 ;
417+ else if ( abs >= 100 ) decimals = 1 ;
418+ else if ( abs >= 10 ) decimals = 2 ;
419+ else if ( abs >= 1 ) decimals = 3 ;
420+ else if ( abs >= 0.1 ) decimals = 4 ;
421+ else if ( abs >= 0.01 ) decimals = 5 ;
422+ else decimals = 6 ;
423+ var s = val . toFixed ( decimals ) ;
424+ // Strip trailing decimal zeros
425+ if ( s . indexOf ( '.' ) !== - 1 ) s = s . replace ( / \. ? 0 + $ / , '' ) ;
426+ return s ;
427+ }
428+ return String ( val ) ;
429+ }
430+
431+ function drawParamHudOnCanvas ( ctx , viewWidth , viewHeight ) {
432+ var items = presentationParamHudState . items ;
433+ if ( ! items . length ) return ;
434+
435+ // Collect rows with current live values
436+ var rows = [ ] ;
437+ for ( var i = 0 ; i < items . length ; i ++ ) {
438+ var item = items [ i ] ;
439+ var val = getPresentationPathValue ( item . path ) ;
440+ rows . push ( { label : item . label || item . path , value : formatParamHudValue ( val ) } ) ;
441+ }
442+ if ( ! rows . length ) return ;
443+
444+ // Layout constants (scale with user-chosen font size)
445+ var fs = Math . max ( 8 , Math . min ( 48 , presentationParamHudState . fontSize || 11 ) ) ;
446+ var fontStr = fs + 'px Consolas, "Courier New", monospace' ;
447+ var paddingX = Math . round ( fs * 0.9 ) ;
448+ var paddingY = Math . round ( fs * 0.7 ) ;
449+ var rowH = Math . round ( fs * 1.55 ) ;
450+ var gap = Math . round ( fs * 0.7 ) ;
451+
452+ ctx . save ( ) ;
453+ ctx . font = fontStr ;
454+ var maxLabelW = 0 , maxValueW = 0 ;
455+ for ( var r = 0 ; r < rows . length ; r ++ ) {
456+ maxLabelW = Math . max ( maxLabelW , ctx . measureText ( rows [ r ] . label + ':' ) . width ) ;
457+ maxValueW = Math . max ( maxValueW , ctx . measureText ( rows [ r ] . value ) . width ) ;
458+ }
459+
460+ var boxW = paddingX * 2 + maxLabelW + gap + maxValueW ;
461+ var boxH = paddingY * 2 + rows . length * rowH ;
462+
463+ // Anchor: anchorX is box left as fraction of view width,
464+ // anchorY is box top as fraction of view height (0=top, 1=bottom-aligned).
465+ // When anchorY===1 the box stays just above the bottom (72px margin).
466+ var ax = presentationParamHudState . anchorX ;
467+ var ay = presentationParamHudState . anchorY ;
468+ var minMarginX = 8 ;
469+ var minMarginY = 8 ;
470+ var x , y ;
471+ if ( ay >= 1.0 ) {
472+ // Legacy bottom-docked behaviour
473+ x = ax * viewWidth ;
474+ y = viewHeight - boxH - 72 ;
475+ } else {
476+ x = ax * viewWidth ;
477+ y = ay * viewHeight ;
478+ }
479+ // Clamp so box stays within viewport
480+ x = Math . max ( minMarginX , Math . min ( viewWidth - boxW - minMarginX , x ) ) ;
481+ y = Math . max ( minMarginY , Math . min ( viewHeight - boxH - minMarginY , y ) ) ;
482+
483+ // Background
484+ ctx . shadowBlur = 0 ;
485+ drawRoundedRectPath ( ctx , x , y , boxW , boxH , Math . round ( fs * 0.5 ) ) ;
486+ ctx . fillStyle = 'rgba(6, 14, 28, 0.82)' ;
487+ ctx . fill ( ) ;
488+ ctx . lineWidth = 1 ;
489+ ctx . strokeStyle = 'rgba(80, 140, 200, 0.45)' ;
490+ ctx . stroke ( ) ;
491+
492+ // Rows
493+ ctx . font = fontStr ;
494+ for ( var r2 = 0 ; r2 < rows . length ; r2 ++ ) {
495+ var ry = y + paddingY + r2 * rowH + rowH - Math . round ( fs * 0.25 ) ;
496+ ctx . fillStyle = '#7bbce8' ;
497+ ctx . fillText ( rows [ r2 ] . label + ':' , x + paddingX , ry ) ;
498+ ctx . fillStyle = '#f0f5ff' ;
499+ ctx . fillText ( rows [ r2 ] . value , x + paddingX + maxLabelW + gap , ry ) ;
500+ }
501+
502+ ctx . restore ( ) ;
503+ }
504+
317505function wrapCanvasTextLines ( ctx , text , maxWidth ) {
318506 var clean = ( text || '' ) . toString ( ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
319507 if ( ! clean ) return [ ] ;
@@ -750,16 +938,21 @@ function updatePresentationOverlay() {
750938 var viewHeight = parseFloat ( canvas . style . height ) || window . innerHeight || 1 ;
751939 ctx . clearRect ( 0 , 0 , viewWidth , viewHeight ) ;
752940
753- if ( ! presentationAnnotationState . enabled ) return ;
754- var notes = presentationAnnotationState . notes ;
755- var channels = Object . keys ( notes ) ;
756- for ( var i = 0 ; i < channels . length ; i ++ ) {
757- var ch = channels [ i ] ;
758- var note = notes [ ch ] ;
759- if ( ! note ) continue ;
760- var alpha = getChannelFadeAlpha ( ch ) ;
761- var layout = buildPresentationNoteLayout ( ctx , note , viewWidth , viewHeight ) ;
762- drawPresentationNote ( ctx , layout , alpha ) ;
941+ if ( presentationAnnotationState . enabled ) {
942+ var notes = presentationAnnotationState . notes ;
943+ var channels = Object . keys ( notes ) ;
944+ for ( var i = 0 ; i < channels . length ; i ++ ) {
945+ var ch = channels [ i ] ;
946+ var note = notes [ ch ] ;
947+ if ( ! note ) continue ;
948+ var alpha = getChannelFadeAlpha ( ch ) ;
949+ var layout = buildPresentationNoteLayout ( ctx , note , viewWidth , viewHeight ) ;
950+ drawPresentationNote ( ctx , layout , alpha ) ;
951+ }
952+ }
953+
954+ if ( presentationParamHudState . enabled && presentationParamHudState . items . length > 0 ) {
955+ drawParamHudOnCanvas ( ctx , viewWidth , viewHeight ) ;
763956 }
764957}
765958
@@ -1417,7 +1610,10 @@ function getPresentationState() {
14171610 recording_output_width : presentationCaptureState . outputWidth || 0 ,
14181611 recording_output_height : presentationCaptureState . outputHeight || 0 ,
14191612 annotations_enabled : ! ! presentationAnnotationState . enabled ,
1420- annotations_in_recording : ! ! presentationAnnotationState . includeInRecording
1613+ annotations_in_recording : ! ! presentationAnnotationState . includeInRecording ,
1614+ param_hud_enabled : ! ! presentationParamHudState . enabled ,
1615+ param_hud_in_recording : ! ! presentationParamHudState . includeInRecording ,
1616+ param_hud_count : presentationParamHudState . items . length
14211617 } ;
14221618}
14231619
@@ -1432,6 +1628,9 @@ function updatePresentation(dt) {
14321628 processPresentationEvents ( previousTime , nextTime ) ;
14331629 presentationState . time = nextTime ;
14341630 applyPresentationTracks ( nextTime ) ;
1631+ if ( presentationParamHudState . enabled && presentationParamHudState . items . length > 0 ) {
1632+ updatePresentationOverlay ( ) ;
1633+ }
14351634 presentationUiRefreshAccumulator += dt ;
14361635 if ( presentationUiRefreshAccumulator >= 0.2 ) {
14371636 presentationUiRefreshAccumulator = 0.0 ;
@@ -1443,6 +1642,9 @@ function updatePresentation(dt) {
14431642 // Final frame of the segment
14441643 processPresentationEvents ( previousTime , duration ) ;
14451644 applyPresentationTracks ( duration ) ;
1645+ if ( presentationParamHudState . enabled && presentationParamHudState . items . length > 0 ) {
1646+ updatePresentationOverlay ( ) ;
1647+ }
14461648
14471649 if ( presentationState . loop ) {
14481650 nextTime = nextTime % duration ;
@@ -1741,7 +1943,9 @@ function drawPresentationCaptureFrame() {
17411943 syncPresentationCaptureCanvasForCurrentResolution ( ) ;
17421944
17431945 var source = renderer . domElement ;
1744- if ( presentationCaptureState . includeAnnotationsInRecording ) {
1946+ var includeOverlayInRecording = presentationCaptureState . includeAnnotationsInRecording ||
1947+ ( presentationParamHudState . includeInRecording && presentationParamHudState . items . length > 0 ) ;
1948+ if ( includeOverlayInRecording ) {
17451949 if ( ! drawPresentationCompositeFrame (
17461950 presentationCaptureState . compositeCanvas ,
17471951 presentationCaptureState . compositeCtx
@@ -1811,8 +2015,42 @@ function drawPresentationCompositeFrame(canvas, ctx) {
18112015 var h = canvas . height ;
18122016 ctx . clearRect ( 0 , 0 , w , h ) ;
18132017 ctx . drawImage ( renderer . domElement , 0 , 0 , w , h ) ;
1814- if ( presentationAnnotationState . enabled && presentationAnnotationState . canvas ) {
1815- ctx . drawImage ( presentationAnnotationState . canvas , 0 , 0 , w , h ) ;
2018+ var showOverlayCanvas = ( presentationAnnotationState . enabled ||
2019+ ( presentationParamHudState . enabled && presentationParamHudState . items . length > 0 ) ) ;
2020+ if ( showOverlayCanvas && presentationAnnotationState . canvas ) {
2021+ var annCanvas = presentationAnnotationState . canvas ;
2022+ var annCtx = presentationAnnotationState . ctx ;
2023+ // When the annotation canvas is a different resolution from the composite
2024+ // (e.g. recording at 2560×1440 while the window is a different aspect ratio)
2025+ // temporarily resize it to the composite dimensions so that text is laid out
2026+ // at the correct positions and scale, then restore it to the window size.
2027+ if ( annCtx && ( annCanvas . width !== w || annCanvas . height !== h ) ) {
2028+ var savedStyleW = annCanvas . style . width ;
2029+ var savedStyleH = annCanvas . style . height ;
2030+ var savedW = annCanvas . width ;
2031+ var savedH = annCanvas . height ;
2032+ annCanvas . style . width = w + 'px' ;
2033+ annCanvas . style . height = h + 'px' ;
2034+ annCanvas . width = w ;
2035+ annCanvas . height = h ;
2036+ annCtx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ;
2037+ if ( typeof updatePresentationOverlay === 'function' ) {
2038+ updatePresentationOverlay ( ) ;
2039+ }
2040+ ctx . drawImage ( annCanvas , 0 , 0 , w , h ) ;
2041+ // Restore annotation canvas to window dimensions for the DOM overlay.
2042+ annCanvas . style . width = savedStyleW ;
2043+ annCanvas . style . height = savedStyleH ;
2044+ annCanvas . width = savedW ;
2045+ annCanvas . height = savedH ;
2046+ var dpr = Math . max ( ( typeof window !== 'undefined' && window . devicePixelRatio ) || 1 , 1 ) ;
2047+ annCtx . setTransform ( dpr , 0 , 0 , dpr , 0 , 0 ) ;
2048+ if ( typeof updatePresentationOverlay === 'function' ) {
2049+ updatePresentationOverlay ( ) ;
2050+ }
2051+ } else {
2052+ ctx . drawImage ( annCanvas , 0 , 0 , w , h ) ;
2053+ }
18162054 }
18172055 return true ;
18182056}
@@ -2336,6 +2574,8 @@ function startPresentationRecording(options) {
23362574 var includeAnnotationsInRecording = ( options . includeAnnotationsInRecording === undefined )
23372575 ? presentationAnnotationState . includeInRecording
23382576 : ! ! options . includeAnnotationsInRecording ;
2577+ var includeOverlayInRecording = includeAnnotationsInRecording ||
2578+ ( presentationParamHudState . includeInRecording && presentationParamHudState . items . length > 0 ) ;
23392579
23402580 var requestedMode = normalizePresentationRecordingMode (
23412581 ( options . recordingMode === undefined )
@@ -2408,7 +2648,7 @@ function startPresentationRecording(options) {
24082648 var recorder = null ;
24092649 var mimeType = 'video/webm' ;
24102650 var offlineJob = null ;
2411- if ( includeAnnotationsInRecording ) {
2651+ if ( includeOverlayInRecording ) {
24122652 ensurePresentationAnnotationCanvas ( ) ;
24132653 updatePresentationOverlay ( ) ;
24142654
@@ -2731,6 +2971,15 @@ if (typeof window !== 'undefined') {
27312971 showAnnotation : setPresentationAnnotation ,
27322972 clearAnnotation : clearPresentationAnnotation ,
27332973 getPathValue : getPresentationPathValue ,
2974+ setParamHudEnabled : setPresentationParamHudEnabled ,
2975+ setParamHudIncludedInRecording : setPresentationParamHudIncludedInRecording ,
2976+ paramHudState : getPresentationParamHudState ,
2977+ setParamHudLayout : setParamHudLayout ,
2978+ addParamToHud : addParamToHud ,
2979+ removeParamFromHud : removeParamFromHud ,
2980+ toggleParamInHud : toggleParamInHud ,
2981+ isParamInHud : isParamInHud ,
2982+ clearParamHud : clearParamHud ,
27342983 startRecording : startPresentationRecording ,
27352984 stopRecording : stopPresentationRecording ,
27362985 captureScreenshot : capturePresentationScreenshot
0 commit comments