feat: add api_input LED interpolation; fix LED preview, FPS charts, dashboard layout
All checks were successful
Lint & Test / test (push) Successful in 1m26s
All checks were successful
Lint & Test / test (push) Successful in 1m26s
API Input: - Add interpolation mode (linear/nearest/none) for LED count mismatch between incoming data and device LED count - New IconSelect in editor, i18n for en/ru/zh - Mark crossfade as won't-do (client owns temporal transitions) - Mark last-write-wins as already implemented LED Preview: - Fix zone-mode preview parsing composite wire format (0xFE header bytes were rendered as color data, garbling multi-zone previews) - Fix _restoreLedPreviewState to handle zone-mode panels FPS Charts: - Seed target card charts from server metrics-history on first load - Add fetchMetricsHistory() with 5s TTL cache shared across dashboard, targets, perf-charts, and graph-editor - Fix chart padding: pass maxSamples per caller (120 for dashboard, 30 for target cards) instead of hardcoded 120 - Fix dashboard chart empty on tab switch (always fetch server history) - Left-pad with nulls for consistent chart width across targets Dashboard: - Fix metrics row alignment (grid layout with fixed column widths) - Fix FPS label overflow into uptime column
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
streamsCache, audioSourcesCache, syncClocksCache,
|
||||
colorStripSourcesCache, devicesCache, outputTargetsCache, patternTemplatesCache,
|
||||
} from '../core/state.ts';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isOpenrgbDevice } from '../core/api.ts';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isOpenrgbDevice, fetchMetricsHistory } from '../core/api.ts';
|
||||
import { t } from '../core/i18n.ts';
|
||||
import { showToast, showConfirm, formatUptime, formatCompact, setTabRefreshing, desktopFocus } from '../core/ui.ts';
|
||||
import { Modal } from '../core/modal.ts';
|
||||
@@ -132,7 +132,7 @@ function _pushTargetFps(targetId: any, actual: any, current: any) {
|
||||
}
|
||||
|
||||
function _createTargetFpsChart(canvasId: any, actualHistory: any, currentHistory: any, fpsTarget: any, maxHwFps: any) {
|
||||
return createFpsSparkline(canvasId, actualHistory, currentHistory, fpsTarget, { maxHwFps });
|
||||
return createFpsSparkline(canvasId, actualHistory, currentHistory, fpsTarget, { maxHwFps, maxSamples: _TARGET_MAX_FPS_SAMPLES });
|
||||
}
|
||||
|
||||
function _updateTargetFpsChart(targetId: any, fpsTarget: any) {
|
||||
@@ -140,7 +140,6 @@ function _updateTargetFpsChart(targetId: any, fpsTarget: any) {
|
||||
if (!chart) return;
|
||||
const actualH = _targetFpsHistory[targetId] || [];
|
||||
const currentH = _targetFpsCurrentHistory[targetId] || [];
|
||||
// Mutate in-place to avoid array copies
|
||||
const ds0 = chart.data.datasets[0].data;
|
||||
ds0.length = 0;
|
||||
ds0.push(...actualH);
|
||||
@@ -832,6 +831,25 @@ export async function loadTargetsTab() {
|
||||
// Push FPS samples and create/update charts for running targets
|
||||
const allTargets = [...ledTargets, ...kcTargets];
|
||||
const runningIds = new Set();
|
||||
const runningTargetIds = allTargets.filter(t => t.state?.processing).map(t => t.id);
|
||||
|
||||
// Seed FPS history from server if empty (first load / page reload)
|
||||
if (runningTargetIds.length > 0 && runningTargetIds.some(id => !_targetFpsHistory[id]?.length)) {
|
||||
const data = await fetchMetricsHistory();
|
||||
if (data) {
|
||||
const serverTargets = data.targets || {};
|
||||
for (const id of runningTargetIds) {
|
||||
if (!_targetFpsHistory[id]?.length) {
|
||||
const samples = serverTargets[id] || [];
|
||||
const actual = samples.map((s: any) => s.fps).filter((v: any) => v != null);
|
||||
const current = samples.map((s: any) => s.fps_current).filter((v: any) => v != null);
|
||||
_targetFpsHistory[id] = actual.slice(-_TARGET_MAX_FPS_SAMPLES);
|
||||
_targetFpsCurrentHistory[id] = current.slice(-_TARGET_MAX_FPS_SAMPLES);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allTargets.forEach(target => {
|
||||
if (target.state && target.state.processing) {
|
||||
runningIds.add(target.id);
|
||||
@@ -1322,6 +1340,8 @@ function _renderLedStripZones(panel: any, rgbBytes: any) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate mode: each zone gets the full source frame resampled to its LED count
|
||||
// (matches backend OpenRGB separate-mode behavior)
|
||||
for (const canvas of zoneCanvases) {
|
||||
const zoneName = canvas.dataset.zoneName;
|
||||
const zoneSize = cache[zoneName.toLowerCase()];
|
||||
@@ -1381,28 +1401,45 @@ function connectLedPreviewWS(targetId: any) {
|
||||
|
||||
const panel = document.getElementById(`led-preview-panel-${targetId}`);
|
||||
|
||||
// Composite wire format: [brightness] [0xFE] [layer_count] [led_count_hi] [led_count_lo] [layers...] [composite...]
|
||||
if (raw.length > 4 && raw[1] === 0xFE && panel && panel.dataset.composite === '1') {
|
||||
// Detect composite wire format: [brightness] [0xFE] [layer_count] [led_count_hi] [led_count_lo] [layers...] [composite...]
|
||||
const isCompositeWire = raw.length > 4 && raw[1] === 0xFE;
|
||||
|
||||
if (isCompositeWire) {
|
||||
const layerCount = raw[2];
|
||||
const ledCount = (raw[3] << 8) | raw[4];
|
||||
const rgbSize = ledCount * 3;
|
||||
let offset = 5;
|
||||
|
||||
// Render per-layer canvases (individual layers)
|
||||
const layerCanvases = panel.querySelectorAll('.led-preview-layer-canvas[data-layer-idx]');
|
||||
for (let i = 0; i < layerCount; i++) {
|
||||
const layerRgb = raw.subarray(offset, offset + rgbSize);
|
||||
offset += rgbSize;
|
||||
// layer canvases: idx 0 = "composite", idx 1..N = individual layers
|
||||
const canvas = layerCanvases[i + 1]; // +1 because composite canvas is first
|
||||
if (canvas) _renderLedStrip(canvas, layerRgb);
|
||||
// Render per-layer canvases if panel supports it
|
||||
if (panel && panel.dataset.composite === '1') {
|
||||
const layerCanvases = panel.querySelectorAll('.led-preview-layer-canvas[data-layer-idx]');
|
||||
for (let i = 0; i < layerCount; i++) {
|
||||
const layerRgb = raw.subarray(offset, offset + rgbSize);
|
||||
offset += rgbSize;
|
||||
// layer canvases: idx 0 = "composite", idx 1..N = individual layers
|
||||
const canvas = layerCanvases[i + 1]; // +1 because composite canvas is first
|
||||
if (canvas) _renderLedStrip(canvas, layerRgb);
|
||||
}
|
||||
} else {
|
||||
// Skip layer data (panel doesn't have layer canvases)
|
||||
offset += layerCount * rgbSize;
|
||||
}
|
||||
|
||||
// Final composite result
|
||||
const compositeRgb = raw.subarray(offset, offset + rgbSize);
|
||||
_ledPreviewLastFrame[targetId] = compositeRgb;
|
||||
const compositeCanvas = panel.querySelector('[data-layer-idx="composite"]');
|
||||
if (compositeCanvas) _renderLedStrip(compositeCanvas, compositeRgb);
|
||||
|
||||
if (panel) {
|
||||
if (panel.dataset.composite === '1') {
|
||||
const compositeCanvas = panel.querySelector('[data-layer-idx="composite"]');
|
||||
if (compositeCanvas) _renderLedStrip(compositeCanvas, compositeRgb);
|
||||
} else if (panel.dataset.zoneMode === 'separate') {
|
||||
_renderLedStripZones(panel, compositeRgb);
|
||||
} else {
|
||||
const canvas = panel.querySelector('.led-preview-canvas');
|
||||
if (canvas) _renderLedStrip(canvas, compositeRgb);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard wire format: [brightness_byte] [R G B R G B ...]
|
||||
const frame = raw.subarray(1);
|
||||
@@ -1461,9 +1498,13 @@ function _restoreLedPreviewState(targetId: any) {
|
||||
_setPreviewButtonState(targetId, true);
|
||||
// Re-render cached frame onto the new canvas
|
||||
const frame = _ledPreviewLastFrame[targetId];
|
||||
if (frame) {
|
||||
const canvas = panel?.querySelector('.led-preview-canvas');
|
||||
if (canvas) _renderLedStrip(canvas, frame);
|
||||
if (frame && panel) {
|
||||
if (panel.dataset.zoneMode === 'separate') {
|
||||
_renderLedStripZones(panel, frame);
|
||||
} else {
|
||||
const canvas = panel.querySelector('.led-preview-canvas');
|
||||
if (canvas) _renderLedStrip(canvas, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user