Skip to content

Commit cb090af

Browse files
committed
Add parameter displaying in recording
1 parent c11d1e6 commit cb090af

4 files changed

Lines changed: 359 additions & 18 deletions

File tree

js/app/core/renderer.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,14 +1461,22 @@ if (typeof window !== 'undefined') {
14611461
resizeForOfflineRecording: function(w, h) {
14621462
if (!renderer) return false;
14631463
renderer.setPixelRatio(1);
1464-
// Pass false so the canvas CSS is NOT updated — the layout stays
1465-
// at the window dimensions, keeping the in-browser viewport intact
1466-
// while the WebGL backing store renders at the recording resolution.
1464+
// Pass false so Three.js does NOT set the canvas CSS width/height.
1465+
// We set object-fit:contain ourselves so the browser letterboxes the
1466+
// content without distorting it when the window aspect ≠ recording aspect.
14671467
renderer.setSize(w, h, false);
1468+
renderer.domElement.style.objectFit = 'contain';
14681469
var rw = renderer.domElement.width;
14691470
var rh = renderer.domElement.height;
14701471
if (bloomPass) bloomPass.resize(rw, rh);
14711472
if (taaPass) taaPass.resize(rw, rh);
1473+
// Match the Three.js camera's projection aspect to the recording resolution
1474+
// so that world→screen projection (used by annotation anchor points) is
1475+
// consistent with what the raytracer shader actually renders.
1476+
if (camera) {
1477+
camera.aspect = w / h;
1478+
camera.updateProjectionMatrix();
1479+
}
14721480
// Warm up TAA history at the new resolution so the first recorded
14731481
// frame already has a converged accumulation buffer. We render
14741482
// without advancing simulation time: same scene, different jitter
@@ -1481,8 +1489,14 @@ if (typeof window !== 'undefined') {
14811489
return true;
14821490
},
14831491
restoreWindowSizeAfterRecording: function() {
1492+
renderer.domElement.style.objectFit = '';
14841493
resizeRendererAndPasses();
14851494
resetTemporalAAHistory();
1495+
// Restore camera aspect ratio to match the window again.
1496+
if (camera) {
1497+
camera.aspect = window.innerWidth / window.innerHeight;
1498+
camera.updateProjectionMatrix();
1499+
}
14861500
}
14871501
};
14881502
}

js/app/presentation/presentation-controller.js

Lines changed: 264 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
174184
var presentationUiRefreshAccumulator = 0.0;
175185

176186
function 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+
317505
function 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

Comments
 (0)