// 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); }