feat: config loader with calibration validation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
960d453f57
commit
799998fad2
2 changed files with 75 additions and 0 deletions
43
src/config.test.ts
Normal file
43
src/config.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { loadConfig, DEFAULT_RENDER } from './config';
|
||||
|
||||
function mockFetch(body: unknown, ok = true) {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok,
|
||||
json: () => Promise.resolve(body),
|
||||
}));
|
||||
}
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe('loadConfig', () => {
|
||||
it('loads stream URL and fills render defaults', async () => {
|
||||
mockFetch({ streamUrl: 'http://cam/stream' });
|
||||
const cfg = await loadConfig('config.json');
|
||||
expect(cfg.streamUrl).toBe('http://cam/stream');
|
||||
expect(cfg.renderDefaults).toEqual(DEFAULT_RENDER);
|
||||
expect(cfg.calibration).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps a valid calibration', async () => {
|
||||
const calibration = {
|
||||
imagePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
||||
machinePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
||||
homography: [1, 0, 0, 0, 1, 0, 0, 0, 1],
|
||||
};
|
||||
mockFetch({ streamUrl: 'x', calibration });
|
||||
const cfg = await loadConfig('config.json');
|
||||
expect(cfg.calibration).toEqual(calibration);
|
||||
});
|
||||
|
||||
it('drops a malformed calibration to null', async () => {
|
||||
mockFetch({ streamUrl: 'x', calibration: { homography: [1, 2, 3] } });
|
||||
const cfg = await loadConfig('config.json');
|
||||
expect(cfg.calibration).toBeNull();
|
||||
});
|
||||
|
||||
it('throws when the request fails', async () => {
|
||||
mockFetch({}, false);
|
||||
await expect(loadConfig('config.json')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
32
src/config.ts
Normal file
32
src/config.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { AppConfig, Calibration, Mat3, Vec2 } from './types';
|
||||
|
||||
export const DEFAULT_RENDER = { cutColor: '#00e5ff', rapidColor: '#ff9800', lineWidth: 1.5 };
|
||||
|
||||
function isVec2Array(v: unknown): v is Vec2[] {
|
||||
return Array.isArray(v) && v.every((p) => Array.isArray(p) && p.length === 2 && p.every((n) => typeof n === 'number'));
|
||||
}
|
||||
|
||||
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 (!Array.isArray(obj.homography) || obj.homography.length !== 9) return null;
|
||||
if (!obj.homography.every((n) => typeof n === 'number')) return null;
|
||||
if (obj.imagePoints.length !== obj.machinePoints.length || obj.imagePoints.length < 4) return null;
|
||||
return {
|
||||
imagePoints: obj.imagePoints,
|
||||
machinePoints: obj.machinePoints,
|
||||
homography: obj.homography as Mat3,
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
renderDefaults: { ...DEFAULT_RENDER, ...(raw.renderDefaults as object | undefined) },
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue