Files
Phoenix/www/views/quad_view.js
T

318 lines
11 KiB
JavaScript

// views/quad_view.js — Quad-View: vier Plots (2x2) + optionales Meter-Panel (wie Split-View)
import { DEFAULT_TOP_INSET } from '../core/theme.js';
import {
CHILD_VIEWS,
computeMultiViewBaseLayout,
drawEmptyPlot,
drawMetersPanel,
readMultiViewMeters,
sanitizeChildId,
sanitizePlotId,
withClippedSubRect,
} from './multi_view_shared.js';
export const id = 'quad-view';
const CONTENT_TOP = DEFAULT_TOP_INSET;
const CONTENT_BOTTOM = 0;
const ROW_GAP = 10;
function readQuadMeters(CONFIG) {
return readMultiViewMeters(CONFIG, 'QUAD_VIEW');
}
function computeBaseLayout(rect, slots) {
return computeMultiViewBaseLayout(rect, slots, CONTENT_TOP, CONTENT_BOTTOM);
}
function splitRectToRows(rect) {
const w = Math.max(0, rect?.w || 0);
const h = Math.max(0, rect?.h || 0);
const gap = Math.max(0, ROW_GAP);
const topH = Math.max(0, Math.floor((h - gap) / 2));
const bottomH = Math.max(0, h - gap - topH);
return {
top: { x: rect.x, y: rect.y, w, h: topH },
bottom: { x: rect.x, y: rect.y + topH + gap, w, h: bottomH },
};
}
function unionRectHoriz(a, b) {
if (!a && !b) return null;
if (!a) return { ...b };
if (!b) return { ...a };
const left = Math.min(a.x, b.x);
const right = Math.max(a.x + a.w, b.x + b.w);
const top = Math.min(a.y, b.y);
const bottom = Math.max(a.y + a.h, b.y + b.h);
return { x: left, y: top, w: Math.max(0, right - left), h: Math.max(0, bottom - top) };
}
function ensureChildRectSize(env, rect, child) {
if (!child || child.id === 'none' || !rect) return;
const sig = `${Math.max(0, rect.w | 0)}x${Math.max(0, rect.h | 0)}`;
if (child._lastSizeSig === sig) return;
child._lastSizeSig = sig;
resizeChild(env, { x: 0, y: 0, w: rect.w, h: rect.h }, child);
}
function destroyChild(child) {
if (!child || child.id === 'none') return;
const mod = CHILD_VIEWS[child.id];
if (mod && typeof mod.destroy === 'function') {
try { mod.destroy(child.state); } catch (e) { console.warn('Quad child destroy error:', e); }
}
}
function initChild(env, childId) {
const id = sanitizeChildId(childId, 'none');
if (id === 'none') return { id: 'none', state: {} };
const mod = CHILD_VIEWS[id];
const state = (mod && typeof mod.init === 'function') ? (mod.init(env) || {}) : {};
return { id, state };
}
function resizeChild(env, rect, child) {
if (!child || child.id === 'none') return;
const mod = CHILD_VIEWS[child.id];
if (!mod || typeof mod.resize !== 'function') return;
try { mod.resize({ rect }, child.state); } catch (e) { console.warn('Quad child resize error:', e); }
}
async function renderChild(env, rect, child, opts = {}) {
const { ctx: g } = env;
if (!child || child.id === 'none') return;
const mod = CHILD_VIEWS[child.id];
if (!mod || typeof mod.render !== 'function') return;
const topInset = Number.isFinite(Number(opts.topInset)) ? Number(opts.topInset) : null;
await withClippedSubRect(g, rect, async () => {
const plotOnlySlots = (viewId) => {
if (viewId === 'peak-history' || viewId === 'classic-needles' || viewId === 'panel') {
const configured = env?.slots?.(viewId);
if (Array.isArray(configured) && configured.length) return configured;
}
if (viewId === 'peak-history') return ['ppm-din'];
if (viewId === 'classic-needles') return ['vu'];
if (viewId === 'panel') return ['vu', 'ppm-ebu', 'ppm-din', 'tp', 'rms'];
return ['none'];
};
const subEnv = Object.assign({}, env, {
rect: { x: 0, y: 0, w: rect.w, h: rect.h },
embedded: true,
containerView: 'quad-view',
slots: plotOnlySlots,
embeddedOffsetX: rect.x,
embeddedOffsetY: rect.y,
});
if (topInset !== null) subEnv.topInset = topInset;
await mod.render(subEnv, child.state);
});
}
export function init(env) {
const tlId = sanitizePlotId(env?.config?.QUAD_VIEW_TL, 'phase-wheel');
const trId = sanitizePlotId(env?.config?.QUAD_VIEW_TR, 'realtime');
const blId = sanitizePlotId(env?.config?.QUAD_VIEW_BL, 'goniometer-rtw');
const brId = sanitizePlotId(env?.config?.QUAD_VIEW_BR, 'none');
const state = {
staticLayers: new Map(),
tl: initChild(env, tlId),
tr: initChild(env, trId),
bl: initChild(env, blId),
br: initChild(env, brId),
popup: initChild(env, 'none'),
lastTlId: tlId,
lastTrId: trId,
lastBlId: blId,
lastBrId: brId,
lastPopupId: 'none',
lastRectSig: '',
};
return state;
}
export function destroy(state) {
if (state) state.staticLayers = null;
destroyChild(state?.tl);
destroyChild(state?.tr);
destroyChild(state?.bl);
destroyChild(state?.br);
destroyChild(state?.popup);
}
export function resize({ rect }, state) {
if (!state || !rect) return;
const sig = `${rect.w}x${rect.h}`;
if (state.lastRectSig === sig) return;
state.lastRectSig = sig;
const w = Math.max(0, rect.w);
const h = Math.max(0, rect.h);
const contentY = CONTENT_TOP;
const contentH = Math.max(0, h - CONTENT_TOP - CONTENT_BOTTOM);
const gap = OUTER_GAP;
const half = Math.floor((w - gap) / 2);
const leftCol = { x: 0, y: contentY, w: Math.max(0, half), h: contentH };
const rightCol = { x: Math.max(0, half + gap), y: contentY, w: Math.max(0, w - (half + gap)), h: contentH };
const leftRows = splitRectToRows(leftCol);
const rightRows = splitRectToRows(rightCol);
resizeChild(null, { x: 0, y: 0, w: leftRows.top.w, h: leftRows.top.h }, state.tl);
resizeChild(null, { x: 0, y: 0, w: rightRows.top.w, h: rightRows.top.h }, state.tr);
resizeChild(null, { x: 0, y: 0, w: leftRows.bottom.w, h: leftRows.bottom.h }, state.bl);
resizeChild(null, { x: 0, y: 0, w: rightRows.bottom.w, h: rightRows.bottom.h }, state.br);
resizeChild(null, { x: 0, y: 0, w, h }, state.popup);
}
export async function render(env, state) {
const { ctx: g, rect, config: CONFIG } = env;
const contentY = CONTENT_TOP;
const contentH = Math.max(0, rect.h - CONTENT_TOP - CONTENT_BOTTOM);
const popupCfg = env?.quadPopup;
const popupWanted = popupCfg && popupCfg.open ? sanitizeChildId(popupCfg.viewId, 'none') : 'none';
// Popup-Modus: rendere die View in Originalgröße (vollflächig),
// ohne Quad-Hintergrund/Layout. So sieht es exakt wie die Einzel-View aus.
if (popupWanted !== 'none' && state) {
if (state.lastPopupId !== popupWanted) {
destroyChild(state.popup);
state.popup = initChild(env, popupWanted);
state.lastPopupId = popupWanted;
resizeChild(null, { x: 0, y: 0, w: rect.w, h: rect.h }, state.popup);
}
state._quadHit = {
contentY: 0,
quadrants: [],
meters: [],
popupBox: { x: 0, y: 0, w: rect.w, h: rect.h },
};
const mod = CHILD_VIEWS[state.popup.id];
if (mod && typeof mod.render === 'function') {
const subEnv = Object.assign({}, env, {
rect: { x: 0, y: 0, w: rect.w, h: rect.h },
embedded: false,
});
await mod.render(subEnv, state.popup.state);
}
return;
}
if (state && state.lastPopupId !== 'none') {
destroyChild(state.popup);
state.popup = initChild(env, 'none');
state.lastPopupId = 'none';
}
const desiredTlId = sanitizePlotId(CONFIG?.QUAD_VIEW_TL, 'phase-wheel');
const desiredTrId = sanitizePlotId(CONFIG?.QUAD_VIEW_TR, 'realtime');
const desiredBlId = sanitizePlotId(CONFIG?.QUAD_VIEW_BL, 'goniometer-rtw');
const desiredBrId = sanitizePlotId(CONFIG?.QUAD_VIEW_BR, 'none');
if (state.lastTlId !== desiredTlId) {
destroyChild(state.tl);
state.tl = initChild(env, desiredTlId);
state.lastTlId = desiredTlId;
}
if (state.lastTrId !== desiredTrId) {
destroyChild(state.tr);
state.tr = initChild(env, desiredTrId);
state.lastTrId = desiredTrId;
}
if (state.lastBlId !== desiredBlId) {
destroyChild(state.bl);
state.bl = initChild(env, desiredBlId);
state.lastBlId = desiredBlId;
}
if (state.lastBrId !== desiredBrId) {
destroyChild(state.br);
state.br = initChild(env, desiredBrId);
state.lastBrId = desiredBrId;
}
const slots = readQuadMeters(CONFIG);
const layout = computeBaseLayout(rect, slots);
const leftColRaw = layout?.plots?.left || { x: 0, y: 0, w: Math.floor(rect.w / 2), h: rect.h };
const rightColRaw = layout?.plots?.right || { x: Math.floor(rect.w / 2), y: 0, w: rect.w - Math.floor(rect.w / 2), h: rect.h };
const leftCol = { x: leftColRaw.x, y: contentY, w: leftColRaw.w, h: contentH };
const rightCol = { x: rightColRaw.x, y: contentY, w: rightColRaw.w, h: contentH };
const leftRows = splitRectToRows(leftCol);
const rightRows = splitRectToRows(rightCol);
const canRowMerge = !layout?.meters?.center;
const topLeftActive = desiredTlId !== 'none';
const topRightActive = desiredTrId !== 'none';
const bottomLeftActive = desiredBlId !== 'none';
const bottomRightActive = desiredBrId !== 'none';
let tlRect = { ...leftRows.top };
let trRect = { ...rightRows.top };
let blRect = { ...leftRows.bottom };
let brRect = { ...rightRows.bottom };
if (canRowMerge) {
if (topLeftActive && !topRightActive) {
tlRect = unionRectHoriz(tlRect, trRect);
trRect = null;
} else if (!topLeftActive && topRightActive) {
trRect = unionRectHoriz(tlRect, trRect);
tlRect = null;
}
if (bottomLeftActive && !bottomRightActive) {
blRect = unionRectHoriz(blRect, brRect);
brRect = null;
} else if (!bottomLeftActive && bottomRightActive) {
brRect = unionRectHoriz(blRect, brRect);
blRect = null;
}
}
if (state) {
state._quadHit = {
contentY,
quadrants: [
{ key: 'tl', rect: tlRect, viewId: desiredTlId },
{ key: 'tr', rect: trRect, viewId: desiredTrId },
{ key: 'bl', rect: blRect, viewId: desiredBlId },
{ key: 'br', rect: brRect, viewId: desiredBrId },
],
meters: [layout?.meters?.left, layout?.meters?.center, layout?.meters?.right].filter(Boolean).map((r) => ({ ...r })),
popupBox: null,
};
}
if (tlRect) {
if (state.tl?.id === 'none') drawEmptyPlot(state, g, tlRect, 'OL: (leer)');
else {
ensureChildRectSize(null, tlRect, state.tl);
await renderChild(env, tlRect, state.tl, { topInset: 0 });
}
}
if (trRect) {
if (state.tr?.id === 'none') drawEmptyPlot(state, g, trRect, 'OR: (leer)');
else {
ensureChildRectSize(null, trRect, state.tr);
await renderChild(env, trRect, state.tr, { topInset: 0 });
}
}
if (blRect) {
if (state.bl?.id === 'none') drawEmptyPlot(state, g, blRect, 'UL: (leer)');
else {
ensureChildRectSize(null, blRect, state.bl);
await renderChild(env, blRect, state.bl, { topInset: 0 });
}
}
if (brRect) {
if (state.br?.id === 'none') drawEmptyPlot(state, g, brRect, 'UR: (leer)');
else {
ensureChildRectSize(null, brRect, state.br);
await renderChild(env, brRect, state.br, { topInset: 0 });
}
}
if (layout?.meters?.left) await drawMetersPanel(env, state, layout.meters.left, layout.meterIds.left, layout.slotW);
if (layout?.meters?.center) await drawMetersPanel(env, state, layout.meters.center, layout.meterIds.center, layout.slotW);
if (layout?.meters?.right) await drawMetersPanel(env, state, layout.meters.right, layout.meterIds.right, layout.slotW);
}