2026-06-11 09:25:47 +02:00
|
|
|
import type { AppConfig, Calibration, PolyMap, PolyWarp, Vec2, RenderStyle } from './types';
|
2026-06-08 22:28:07 +02:00
|
|
|
|
2026-06-08 22:30:39 +02:00
|
|
|
export const DEFAULT_RENDER: RenderStyle = { cutColor: '#00e5ff', rapidColor: '#ff9800', lineWidth: 1.5 };
|
|
|
|
|
|
|
|
|
|
function isFiniteNumber(n: unknown): n is number {
|
|
|
|
|
return typeof n === 'number' && Number.isFinite(n);
|
|
|
|
|
}
|
2026-06-08 22:28:07 +02:00
|
|
|
|
|
|
|
|
function isVec2Array(v: unknown): v is Vec2[] {
|
2026-06-08 22:30:39 +02:00
|
|
|
return Array.isArray(v) && v.every((p) => Array.isArray(p) && p.length === 2 && p.every(isFiniteNumber));
|
2026-06-08 22:28:07 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 09:25:47 +02:00
|
|
|
function isNumArray(v: unknown, len: number): v is number[] {
|
|
|
|
|
return Array.isArray(v) && v.length === len && v.every(isFiniteNumber);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isVec2(v: unknown): v is Vec2 {
|
|
|
|
|
return Array.isArray(v) && v.length === 2 && v.every(isFiniteNumber);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function termCount(degree: number): number {
|
|
|
|
|
return ((degree + 1) * (degree + 2)) / 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parsePolyMap(m: unknown, degree: number): PolyMap | null {
|
|
|
|
|
if (!m || typeof m !== 'object') return null;
|
|
|
|
|
const o = m as Record<string, unknown>;
|
|
|
|
|
const norm = o.norm as Record<string, unknown> | undefined;
|
|
|
|
|
if (!norm || !isVec2(norm.off) || !isVec2(norm.scl)) return null;
|
|
|
|
|
const n = termCount(degree);
|
|
|
|
|
if (!isNumArray(o.cu, n) || !isNumArray(o.cv, n)) return null;
|
|
|
|
|
return { degree, norm: { off: norm.off as Vec2, scl: norm.scl as Vec2 }, cu: o.cu as number[], cv: o.cv as number[] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseWarp(w: unknown): PolyWarp | null {
|
|
|
|
|
if (!w || typeof w !== 'object') return null;
|
|
|
|
|
const o = w as Record<string, unknown>;
|
2026-06-11 09:28:04 +02:00
|
|
|
// Require a positive integer degree: a negative/zero/fractional degree makes termCount
|
|
|
|
|
// collapse to 0, which would vacuously accept empty coefficient arrays (a garbage warp).
|
|
|
|
|
if (!isFiniteNumber(o.degree) || !Number.isInteger(o.degree) || o.degree < 1) return null;
|
2026-06-11 09:25:47 +02:00
|
|
|
const fwd = parsePolyMap(o.fwd, o.degree);
|
|
|
|
|
const inv = parsePolyMap(o.inv, o.degree);
|
|
|
|
|
if (!fwd || !inv) return null;
|
|
|
|
|
return { degree: o.degree, fwd, inv };
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 22:28:07 +02:00
|
|
|
function parseCalibration(c: unknown): Calibration | null {
|
|
|
|
|
if (!c || typeof c !== 'object') return null;
|
|
|
|
|
const obj = c as Record<string, unknown>;
|
|
|
|
|
if (!isVec2Array(obj.imagePoints) || !isVec2Array(obj.machinePoints)) return null;
|
|
|
|
|
if (obj.imagePoints.length !== obj.machinePoints.length || obj.imagePoints.length < 4) return null;
|
2026-06-11 09:25:47 +02:00
|
|
|
const warp = parseWarp(obj.warp);
|
|
|
|
|
if (!warp) return null;
|
|
|
|
|
return { imagePoints: obj.imagePoints, machinePoints: obj.machinePoints, warp };
|
2026-06-08 22:28:07 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-08 22:30:39 +02:00
|
|
|
function parseRenderStyle(r: unknown): RenderStyle {
|
|
|
|
|
const obj = (r && typeof r === 'object' ? r : {}) as Record<string, unknown>;
|
|
|
|
|
return {
|
|
|
|
|
cutColor: typeof obj.cutColor === 'string' ? obj.cutColor : DEFAULT_RENDER.cutColor,
|
|
|
|
|
rapidColor: typeof obj.rapidColor === 'string' ? obj.rapidColor : DEFAULT_RENDER.rapidColor,
|
|
|
|
|
lineWidth: isFiniteNumber(obj.lineWidth) ? obj.lineWidth : DEFAULT_RENDER.lineWidth,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 22:28:07 +02:00
|
|
|
export async function loadConfig(url: string): Promise<AppConfig> {
|
|
|
|
|
const res = await fetch(url);
|
|
|
|
|
if (!res.ok) throw new Error(`Failed to load ${url}: ${res.status}`);
|
|
|
|
|
const raw = (await res.json()) as Record<string, unknown>;
|
|
|
|
|
return {
|
|
|
|
|
streamUrl: typeof raw.streamUrl === 'string' ? raw.streamUrl : '',
|
|
|
|
|
calibration: parseCalibration(raw.calibration),
|
2026-06-08 22:30:39 +02:00
|
|
|
renderDefaults: parseRenderStyle(raw.renderDefaults),
|
2026-06-08 22:28:07 +02:00
|
|
|
};
|
|
|
|
|
}
|