Files
Phoenix/www/core/registry.js
T
2026-06-02 20:56:19 +02:00

229 lines
7.5 KiB
JavaScript

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