589 lines
20 KiB
JavaScript
589 lines
20 KiB
JavaScript
// views/quad_view.js — Quad-View: vier Plots (2x2) + optionales Meter-Panel (wie Split-View)
|
|
|
|
import * as viewGoni from './goniometer_rtw.js';
|
|
import * as viewPhaseWheel from './phase_wheel.js';
|
|
import * as viewPanel from './panel.js';
|
|
import * as viewRealtime from './realtime.js';
|
|
import * as viewClassicNeedles from './classic_needles.js';
|
|
import * as viewPeakHistory from './peak_history.js';
|
|
import * as viewClock from './clock.js';
|
|
import * as viewWaveform from './waveform.js';
|
|
import * as viewSpectrogram from './spectrogram.js';
|
|
import { DEFAULT_TOP_INSET, FRAME_COLOR, PANEL_BG } from '../core/theme.js';
|
|
|
|
export const id = 'quad-view';
|
|
|
|
const CONTENT_TOP = DEFAULT_TOP_INSET;
|
|
const CONTENT_BOTTOM = 0;
|
|
|
|
const CHILD_VIEWS = {
|
|
'realtime': viewRealtime,
|
|
'classic-needles': viewClassicNeedles,
|
|
'peak-history': viewPeakHistory,
|
|
'goniometer-rtw': viewGoni,
|
|
'phase-wheel': viewPhaseWheel,
|
|
'panel': viewPanel,
|
|
'clock': viewClock,
|
|
'waveform': viewWaveform,
|
|
'spectrogram': viewSpectrogram,
|
|
};
|
|
const ALLOWED_CHILD_VIEW_IDS = new Set(['none', ...Object.keys(CHILD_VIEWS)]);
|
|
const ALLOWED_PLOT_IDS = new Set(['none', 'phase-wheel', 'realtime', 'goniometer-rtw', 'peak-history', 'classic-needles', 'panel', 'clock', 'waveform', 'spectrogram']);
|
|
const ALLOWED_METER_IDS = new Set(['none', 'vu', 'ppm-ebu', 'ppm-din', 'tp', 'hifi-peak', 'rms', 'lufs', 'stopwatch']);
|
|
const ALLOWED_METER_POSITIONS = new Set(['left', 'center', 'right']);
|
|
|
|
const OUTER_GAP = 10;
|
|
const SIDE_INNER_GAP = 8;
|
|
const ROW_GAP = 10;
|
|
const METER_GAP = 12;
|
|
const METER_W_DEFAULT = 140;
|
|
const METER_W_MIN = 90;
|
|
const MIN_PLOT_W = 240;
|
|
const METER_PAD_TOP = 15;
|
|
const METER_PAD_BOTTOM = 5;
|
|
const METER_SLOT_SHRINK = 24;
|
|
const METER_EXTRA_BOTTOM_PAD = 6;
|
|
|
|
function sanitizeChildId(val, fallback) {
|
|
return ALLOWED_CHILD_VIEW_IDS.has(val) ? val : fallback;
|
|
}
|
|
|
|
function sanitizePlotId(val, fallback) {
|
|
return ALLOWED_PLOT_IDS.has(val) ? val : fallback;
|
|
}
|
|
|
|
function sanitizeMeterId(val, fallback) {
|
|
const id = String(val || '');
|
|
return ALLOWED_METER_IDS.has(id) ? id : fallback;
|
|
}
|
|
|
|
function sanitizeMeterPos(val, fallback) {
|
|
const id = String(val || '');
|
|
return ALLOWED_METER_POSITIONS.has(id) ? id : fallback;
|
|
}
|
|
|
|
function clampMeterCount(val) {
|
|
const n = Number(val);
|
|
if (!Number.isFinite(n)) return 0;
|
|
return Math.max(0, Math.min(3, n | 0));
|
|
}
|
|
|
|
async function withClippedSubRect(g, rect, fn) {
|
|
g.save();
|
|
g.translate(rect.x, rect.y);
|
|
g.beginPath();
|
|
g.rect(0, 0, rect.w, rect.h);
|
|
g.clip();
|
|
try { return await fn(); } finally { g.restore(); }
|
|
}
|
|
|
|
function drawSubframe(g, rect) {
|
|
g.save();
|
|
g.strokeStyle = FRAME_COLOR;
|
|
g.lineWidth = 2;
|
|
g.strokeRect(rect.x + 0.5, rect.y + 0.5, Math.max(0, rect.w - 1), Math.max(0, rect.h - 1));
|
|
g.restore();
|
|
}
|
|
|
|
function createStaticLayerCanvas(width, height) {
|
|
const w = Math.max(1, width | 0);
|
|
const h = Math.max(1, height | 0);
|
|
if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(w, h);
|
|
const c = document.createElement('canvas');
|
|
c.width = w;
|
|
c.height = h;
|
|
return c;
|
|
}
|
|
|
|
function drawCachedStaticLayer(state, g, layerId, key, rect, build) {
|
|
if (!state || !rect || rect.w <= 0 || rect.h <= 0) return;
|
|
if (!state.staticLayers) state.staticLayers = new Map();
|
|
const fullKey = `${layerId}:${key}:${Math.max(0, rect.w | 0)}x${Math.max(0, rect.h | 0)}`;
|
|
let layer = state.staticLayers.get(fullKey);
|
|
if (!layer) {
|
|
const canvas = createStaticLayerCanvas(rect.w, rect.h);
|
|
const ctx = canvas.getContext('2d');
|
|
build(ctx, rect);
|
|
layer = { canvas };
|
|
state.staticLayers.set(fullKey, layer);
|
|
}
|
|
g.drawImage(layer.canvas, rect.x, rect.y);
|
|
}
|
|
|
|
function drawEmptyPlot(state, g, rect, label) {
|
|
drawCachedStaticLayer(state, g, 'empty-plot', String(label || '(leer)'), rect, (lg) => {
|
|
lg.fillStyle = PANEL_BG;
|
|
lg.fillRect(0, 0, rect.w, rect.h);
|
|
lg.fillStyle = '#9aa';
|
|
lg.textAlign = 'left';
|
|
lg.font = 'bold 14px ui-monospace, monospace';
|
|
lg.fillText(label || '(leer)', 12, 32);
|
|
lg.textAlign = 'start';
|
|
});
|
|
}
|
|
|
|
function readQuadMeters(CONFIG) {
|
|
const count = clampMeterCount(CONFIG?.QUAD_VIEW_METER_COUNT);
|
|
const slots = [
|
|
{
|
|
id: sanitizeMeterId(CONFIG?.QUAD_VIEW_METER_1, 'vu'),
|
|
pos: sanitizeMeterPos(CONFIG?.QUAD_VIEW_METER_1_POS, 'right'),
|
|
},
|
|
{
|
|
id: sanitizeMeterId(CONFIG?.QUAD_VIEW_METER_2, 'ppm-din'),
|
|
pos: sanitizeMeterPos(CONFIG?.QUAD_VIEW_METER_2_POS, 'right'),
|
|
},
|
|
{
|
|
id: sanitizeMeterId(CONFIG?.QUAD_VIEW_METER_3, 'lufs'),
|
|
pos: sanitizeMeterPos(CONFIG?.QUAD_VIEW_METER_3_POS, 'right'),
|
|
},
|
|
].slice(0, count);
|
|
return slots;
|
|
}
|
|
|
|
function groupMetersByPosition(slots) {
|
|
const out = { left: [], center: [], right: [] };
|
|
for (const s of slots || []) {
|
|
if (!s) continue;
|
|
if (s.pos === 'left') out.left.push(s.id);
|
|
else if (s.pos === 'center') out.center.push(s.id);
|
|
else out.right.push(s.id);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function computeBaseLayout(rect, slots) {
|
|
const contentH = Math.max(0, rect.h - CONTENT_TOP - CONTENT_BOTTOM);
|
|
const hasContent = contentH >= 140;
|
|
const BLOCK_GAP = SIDE_INNER_GAP;
|
|
|
|
let effectiveSlots = hasContent ? (Array.isArray(slots) ? slots.slice(0, 3) : []) : [];
|
|
while (true) {
|
|
const grouped = groupMetersByPosition(effectiveSlots);
|
|
const leftCount = grouped.left.length;
|
|
const centerCount = grouped.center.length;
|
|
const rightCount = grouped.right.length;
|
|
const totalSlots = leftCount + centerCount + rightCount;
|
|
const hasLeftMeters = leftCount > 0;
|
|
const hasCenterMeters = centerCount > 0;
|
|
const hasRightMeters = rightCount > 0;
|
|
|
|
const interBlockGaps =
|
|
(hasLeftMeters ? BLOCK_GAP : 0) +
|
|
(hasRightMeters ? BLOCK_GAP : 0) +
|
|
(hasCenterMeters ? (2 * BLOCK_GAP) : OUTER_GAP);
|
|
|
|
const internalGaps =
|
|
Math.max(0, leftCount - 1) * METER_GAP +
|
|
Math.max(0, centerCount - 1) * METER_GAP +
|
|
Math.max(0, rightCount - 1) * METER_GAP;
|
|
|
|
const minRequired = 2 * MIN_PLOT_W + interBlockGaps + internalGaps;
|
|
const remainingForMeters = rect.w - minRequired;
|
|
|
|
let slotW = 0;
|
|
if (totalSlots > 0) {
|
|
slotW = Math.floor(remainingForMeters / totalSlots);
|
|
slotW = Math.min(METER_W_DEFAULT, slotW);
|
|
}
|
|
|
|
if (totalSlots > 0 && slotW < METER_W_MIN && effectiveSlots.length) {
|
|
effectiveSlots = effectiveSlots.slice(0, -1);
|
|
continue;
|
|
}
|
|
if (totalSlots > 0) slotW = Math.max(METER_W_MIN, Math.min(METER_W_DEFAULT, slotW));
|
|
|
|
const leftW = hasLeftMeters ? (leftCount * slotW + Math.max(0, leftCount - 1) * METER_GAP) : 0;
|
|
const centerW = hasCenterMeters ? (centerCount * slotW + Math.max(0, centerCount - 1) * METER_GAP) : 0;
|
|
const rightW = hasRightMeters ? (rightCount * slotW + Math.max(0, rightCount - 1) * METER_GAP) : 0;
|
|
|
|
const metersTotalW = leftW + centerW + rightW;
|
|
const plotAvail = Math.max(0, rect.w - interBlockGaps - metersTotalW);
|
|
const leftPlotW = Math.floor(plotAvail / 2);
|
|
const rightPlotW = plotAvail - leftPlotW;
|
|
|
|
const plotFits = leftPlotW >= MIN_PLOT_W && rightPlotW >= MIN_PLOT_W;
|
|
if (!plotFits && effectiveSlots.length) {
|
|
effectiveSlots = effectiveSlots.slice(0, -1);
|
|
continue;
|
|
}
|
|
|
|
let x = 0;
|
|
const leftMetersRect = hasLeftMeters ? { x, y: CONTENT_TOP, w: leftW, h: contentH } : null;
|
|
if (leftMetersRect) x += leftW + BLOCK_GAP;
|
|
|
|
const leftPlotRect = { x, y: 0, w: leftPlotW, h: rect.h };
|
|
x += leftPlotW;
|
|
|
|
let centerMetersRect = null;
|
|
if (hasCenterMeters) {
|
|
x += BLOCK_GAP;
|
|
centerMetersRect = { x, y: CONTENT_TOP, w: centerW, h: contentH };
|
|
x += centerW + BLOCK_GAP;
|
|
} else {
|
|
x += OUTER_GAP;
|
|
}
|
|
|
|
const rightPlotRect = { x, y: 0, w: rightPlotW, h: rect.h };
|
|
x += rightPlotW;
|
|
|
|
let rightMetersRect = null;
|
|
if (hasRightMeters) {
|
|
x += BLOCK_GAP;
|
|
rightMetersRect = { x, y: CONTENT_TOP, w: rightW, h: contentH };
|
|
}
|
|
|
|
return {
|
|
plots: { left: leftPlotRect, right: rightPlotRect },
|
|
meters: { left: leftMetersRect, center: centerMetersRect, right: rightMetersRect },
|
|
meterIds: grouped,
|
|
slotW,
|
|
effectiveSlots,
|
|
};
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
async function drawMetersPanel(env, state, rect, meterIds, slotW) {
|
|
const { ctx: g, meters, config: CONFIG } = env;
|
|
if (!rect || rect.w <= 0 || rect.h <= 0) return;
|
|
const ids = Array.isArray(meterIds) ? meterIds.slice(0, 3) : [];
|
|
const count = ids.length;
|
|
if (!count) return;
|
|
|
|
drawCachedStaticLayer(state, g, 'meter-panel-shell', 'bg-frame', rect, (lg) => {
|
|
lg.fillStyle = PANEL_BG;
|
|
lg.fillRect(0, 0, rect.w, rect.h);
|
|
drawSubframe(lg, { x: 0, y: 0, w: rect.w, h: rect.h });
|
|
});
|
|
|
|
const gap = METER_GAP;
|
|
const n = Math.max(1, Math.min(3, count));
|
|
const usedW = n * slotW + (n - 1) * gap;
|
|
const startX = rect.x + Math.max(0, Math.floor((rect.w - usedW) / 2));
|
|
const shrink = Math.max(0, METER_SLOT_SHRINK);
|
|
const innerHeight = Math.max(40, rect.h - METER_PAD_TOP - METER_PAD_BOTTOM - shrink - METER_EXTRA_BOTTOM_PAD);
|
|
const innerOffset = shrink / 2;
|
|
const slotY = rect.y + METER_PAD_TOP + innerOffset;
|
|
const slotH = innerHeight;
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
const id = sanitizeMeterId(ids[i], 'none');
|
|
const r = { x: startX + i * (slotW + gap), y: slotY, w: slotW, h: slotH };
|
|
if (id === 'none') {
|
|
drawCachedStaticLayer(state, g, 'meter-slot-empty', `${slotW}x${slotH}`, r, (lg) => {
|
|
lg.strokeStyle = 'rgba(0,231,255,0.25)';
|
|
lg.setLineDash([6, 5]);
|
|
lg.strokeRect(0.5, 0.5, Math.max(0, r.w - 1), Math.max(0, r.h - 1));
|
|
lg.setLineDash([]);
|
|
lg.fillStyle = '#9aa';
|
|
lg.textAlign = 'center';
|
|
lg.font = '12px ui-monospace, monospace';
|
|
lg.fillText('(leer)', r.w / 2, 22);
|
|
lg.textAlign = 'start';
|
|
});
|
|
} else {
|
|
try {
|
|
g.save();
|
|
g.beginPath();
|
|
g.rect(rect.x + 1, rect.y + 1, Math.max(0, rect.w - 2), Math.max(0, rect.h - 2));
|
|
g.clip();
|
|
await meters.draw(g, r, id, CONFIG);
|
|
g.restore();
|
|
} catch (e) {
|
|
g.restore();
|
|
console.warn('Quad meter draw error:', e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|