// core/registry.js — Lazy Loader + Safe Runner für Meter // Lädt Meter-Module on-demand (oder via registerMeter) und kapselt Fehler. const cache = { meters: new Map(), // id -> module }; const meterRenderCache = new Map(); // key -> surface const METER_CACHE_MARGIN = 32; let activeFrameStamp = 0; const CAN_USE_OFFSCREEN = typeof OffscreenCanvas === 'function'; function createCacheSurface(width, height) { const w = Math.max(1, Math.ceil(width)); const h = Math.max(1, Math.ceil(height)); if (CAN_USE_OFFSCREEN) { const canvas = new OffscreenCanvas(w, h); const ctx = canvas.getContext('2d'); if (ctx) return { canvas, ctx, width: w, height: h, stamp: -1 }; } if (typeof document !== 'undefined') { const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); if (ctx) return { canvas, ctx, width: w, height: h, stamp: -1 }; } return null; } function cacheKeyForMeter(id, rect) { const x = Math.round(Number(rect?.x) || 0); const y = Math.round(Number(rect?.y) || 0); const w = Math.max(1, Math.round(Number(rect?.w) || 0)); const h = Math.max(1, Math.round(Number(rect?.h) || 0)); return `${id}::${x},${y},${w}x${h}`; } function ensureCacheSurface(id, rect) { const key = cacheKeyForMeter(id, rect); let surface = meterRenderCache.get(key); if (!surface) { surface = createCacheSurface(rect.w + METER_CACHE_MARGIN * 2, rect.h + METER_CACHE_MARGIN * 2); if (!surface) return null; meterRenderCache.set(key, surface); } if (!surface.canvas || !surface.ctx) return null; const w = Math.max(1, Math.ceil(rect.w + METER_CACHE_MARGIN * 2)); const h = Math.max(1, Math.ceil(rect.h + METER_CACHE_MARGIN * 2)); if (surface.canvas.width !== w || surface.canvas.height !== h) { surface.canvas.width = w; surface.canvas.height = h; surface.stamp = -1; } surface.width = w; surface.height = h; surface.margin = METER_CACHE_MARGIN; return surface; } export async function loadMeter(id) { if (!id) throw new Error('loadMeter: leere Meter-ID'); if (cache.meters.has(id)) return cache.meters.get(id); const mod = await import(`../meters/${id}.js`); cache.meters.set(id, mod); return mod; } // --- Meter-Facade ----------------------------------------------------------- // Views rufen nur diese Fassade, kennen also die Module/States nicht direkt. const meterStates = new Map(); // instanceKey -> { id, shared } const DEFAULT_INSTANCE_SUFFIX = '::default'; function makeMeterInstanceKey(id, rect) { if (!rect) return `${id}::default`; const x = Math.round(Number(rect.x) || 0); const y = Math.round(Number(rect.y) || 0); const w = Math.round(Number(rect.w) || 0); const h = Math.round(Number(rect.h) || 0); return `${id}::${x},${y},${w},${h}`; } function makeDefaultMeterInstanceKey(id) { return `${id}${DEFAULT_INSTANCE_SUFFIX}`; } function getMeterSharedById(id) { const matches = []; for (const entry of meterStates.values()) { if (entry?.id === id && entry.shared) matches.push(entry.shared); } return matches; } function getPreferredMeterShared(id) { const defaultEntry = meterStates.get(makeDefaultMeterInstanceKey(id)); if (defaultEntry?.shared) return defaultEntry.shared; for (const entry of meterStates.values()) { if (entry?.id === id && entry.shared) return entry.shared; } return null; } async function ensureMeter(id, config, rect) { const mod = await loadMeter(id); const instanceKey = makeMeterInstanceKey(id, rect); if (!meterStates.has(instanceKey)) { const shared = mod.initShared?.(config) || {}; meterStates.set(instanceKey, { id, shared }); } return { mod, instanceKey, shared: meterStates.get(instanceKey)?.shared || null }; } async function ensureDefaultMeterShared(id, config) { const mod = await loadMeter(id); const instanceKey = makeDefaultMeterInstanceKey(id); if (!meterStates.has(instanceKey)) { const shared = mod.initShared?.(config) || {}; meterStates.set(instanceKey, { id, shared }); } return { mod, instanceKey, shared: meterStates.get(instanceKey)?.shared || null }; } export const meterFacade = { async update(packet, config, activeIds) { const seen = new Set(); for (const rawId of activeIds || []) { if (!rawId || seen.has(rawId)) continue; seen.add(rawId); const id = rawId; try { const { mod } = await ensureDefaultMeterShared(id, config); const sharedList = getMeterSharedById(id); for (const shared of sharedList) { mod.update?.(packet, shared); } } catch (e) { // ein defektes Meter wird übersprungen, andere laufen weiter console.warn(`Meter ${id} update error:`, e); } } }, async pointer(evt, rect, id, config) { if (!id) return false; try { const { mod, shared } = await ensureMeter(id, config, rect); if (typeof mod.pointer === 'function') { const handled = await mod.pointer(evt, rect, config, shared); return !!handled; } } catch (e) { console.warn(`Meter ${id} pointer error:`, e); } return false; }, async draw(ctx, rect, id, config) { try { const { mod, shared } = await ensureMeter(id, config, rect); const allowCache = !(mod && mod.disableCache); if (!allowCache) { await mod.draw?.(ctx, rect, config, shared); return; } const cacheSurface = ensureCacheSurface(id, rect); const stamp = activeFrameStamp; const margin = cacheSurface ? cacheSurface.margin || METER_CACHE_MARGIN : 0; const drawX = rect.x - margin; const drawY = rect.y - margin; if (cacheSurface && cacheSurface.stamp === stamp) { ctx.drawImage(cacheSurface.canvas, drawX, drawY); return; } if (cacheSurface && cacheSurface.ctx) { cacheSurface.ctx.save(); try { cacheSurface.ctx.setTransform(1, 0, 0, 1, 0, 0); cacheSurface.ctx.clearRect(0, 0, cacheSurface.canvas.width, cacheSurface.canvas.height); cacheSurface.ctx.font = ctx.font; cacheSurface.ctx.textAlign = ctx.textAlign; cacheSurface.ctx.textBaseline = ctx.textBaseline; cacheSurface.ctx.translate(-drawX, -drawY); await mod.draw?.(cacheSurface.ctx, rect, config, shared); } finally { cacheSurface.ctx.restore(); } cacheSurface.stamp = stamp; ctx.drawImage(cacheSurface.canvas, drawX, drawY); return; } await mod.draw?.(ctx, rect, config, shared); } catch (e) { // Slot neutral darstellen ctx.save(); ctx.strokeStyle = 'rgba(200,80,80,.8)'; ctx.setLineDash([6,4]); ctx.strokeRect(rect.x+0.5, rect.y+0.5, rect.w-1, rect.h-1); ctx.setLineDash([]); ctx.fillStyle = '#ffdddd'; ctx.textAlign = 'center'; ctx.fillText(`Meter "${id}" defekt`, rect.x + rect.w/2, rect.y + rect.h/2); ctx.textAlign = 'start'; ctx.restore(); } }, setFrameStamp(stamp) { activeFrameStamp = stamp || 0; }, getState(id) { return getPreferredMeterShared(id); }, invalidateAll() { meterStates.clear(); meterRenderCache.clear(); }, }; if (typeof window !== 'undefined') { window.__AN_REGISTRY__ = window.__AN_REGISTRY__ || {}; window.__AN_REGISTRY__.invalidateAll = () => meterFacade.invalidateAll(); } // --- Meter Registration System --- export function registerMeter(meterModule) { if (meterModule && meterModule.id) { cache.meters.set(meterModule.id, Promise.resolve(meterModule)); } }