229 lines
7.5 KiB
JavaScript
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));
|
|
}
|
|
}
|