feat: app state, stream embedding, G-code render wiring
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd409f56c0
commit
38d99bb0dc
5 changed files with 118 additions and 9 deletions
|
|
@ -4,7 +4,6 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>G-Code Overlay</title>
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="stage">
|
||||
|
|
|
|||
5
public/config.json
Normal file
5
public/config.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"streamUrl": "",
|
||||
"calibration": null,
|
||||
"renderDefaults": { "cutColor": "#00e5ff", "rapidColor": "#ff9800", "lineWidth": 1.5 }
|
||||
}
|
||||
36
src/app/state.test.ts
Normal file
36
src/app/state.test.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { createState } from './state';
|
||||
import type { Mat3 } from '../types';
|
||||
|
||||
const IDENT: Mat3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
|
||||
describe('app state', () => {
|
||||
it('starts with no segments and a zero alignment', () => {
|
||||
const s = createState();
|
||||
expect(s.segments).toEqual([]);
|
||||
expect(s.alignment).toEqual({ tx: 0, ty: 0, rot: 0 });
|
||||
});
|
||||
|
||||
it('loadGcode stores parsed segments and returns warning count', () => {
|
||||
const s = createState();
|
||||
const warnings = s.loadGcode('G21 G90\nG1 X10 Y0');
|
||||
expect(s.segments).toHaveLength(1);
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('projected() returns image-space segments when calibrated', () => {
|
||||
const s = createState();
|
||||
s.setHomography(IDENT);
|
||||
s.loadGcode('G21 G90\nG1 X10 Y0');
|
||||
s.alignment.tx = 2;
|
||||
const proj = s.projected();
|
||||
expect(proj[0]!.points[0]).toEqual([2, 0]);
|
||||
expect(proj[0]!.points[1]).toEqual([12, 0]);
|
||||
});
|
||||
|
||||
it('projected() returns [] when not calibrated', () => {
|
||||
const s = createState();
|
||||
s.loadGcode('G1 X10 Y0');
|
||||
expect(s.projected()).toEqual([]);
|
||||
});
|
||||
});
|
||||
33
src/app/state.ts
Normal file
33
src/app/state.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { Segment, Alignment, Mat3 } from '../types';
|
||||
import { parseGcode } from '../gcode/parser';
|
||||
import { projectSegments } from '../geometry/transform';
|
||||
|
||||
export interface AppState {
|
||||
segments: Segment[];
|
||||
alignment: Alignment;
|
||||
homography: Mat3 | null;
|
||||
loadGcode(text: string): string[];
|
||||
setHomography(H: Mat3 | null): void;
|
||||
projected(): Segment[];
|
||||
}
|
||||
|
||||
export function createState(): AppState {
|
||||
const state: AppState = {
|
||||
segments: [],
|
||||
alignment: { tx: 0, ty: 0, rot: 0 },
|
||||
homography: null,
|
||||
loadGcode(text: string): string[] {
|
||||
const { segments, warnings } = parseGcode(text);
|
||||
state.segments = segments;
|
||||
return warnings;
|
||||
},
|
||||
setHomography(H: Mat3 | null): void {
|
||||
state.homography = H;
|
||||
},
|
||||
projected(): Segment[] {
|
||||
if (!state.homography) return [];
|
||||
return projectSegments(state.segments, state.alignment, state.homography);
|
||||
},
|
||||
};
|
||||
return state;
|
||||
}
|
||||
52
src/main.ts
52
src/main.ts
|
|
@ -1,22 +1,58 @@
|
|||
import './styles.css';
|
||||
import { loadConfig } from './config';
|
||||
import { createState } from './app/state';
|
||||
import { computeOverlayRect, syncOverlay } from './app/layout';
|
||||
import { drawOverlay, clearCanvas } from './render/renderer';
|
||||
import type { RenderStyle } from './types';
|
||||
|
||||
const stage = document.getElementById('stage') as HTMLDivElement;
|
||||
const stream = document.getElementById('stream') as HTMLImageElement;
|
||||
const overlay = document.getElementById('overlay') as HTMLCanvasElement;
|
||||
const statusEl = document.getElementById('status') as HTMLParagraphElement;
|
||||
const fileInput = document.getElementById('gcode-file') as HTMLInputElement;
|
||||
const ctx = overlay.getContext('2d')!;
|
||||
|
||||
const state = createState();
|
||||
let style: RenderStyle = { cutColor: '#00e5ff', rapidColor: '#ff9800', lineWidth: 1.5 };
|
||||
|
||||
function render(): void {
|
||||
clearCanvas(ctx, overlay.width, overlay.height);
|
||||
const proj = state.projected();
|
||||
drawOverlay(ctx, proj, style);
|
||||
}
|
||||
|
||||
function resize(): void {
|
||||
const rect = computeOverlayRect(
|
||||
stream.naturalWidth,
|
||||
stream.naturalHeight,
|
||||
stage.clientWidth,
|
||||
stage.clientHeight,
|
||||
);
|
||||
const rect = computeOverlayRect(stream.naturalWidth, stream.naturalHeight, stage.clientWidth, stage.clientHeight);
|
||||
syncOverlay(overlay, rect);
|
||||
render();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
stream.addEventListener('load', resize);
|
||||
resize();
|
||||
|
||||
console.log('layout ready');
|
||||
fileInput.addEventListener('change', async () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) return;
|
||||
const text = await file.text();
|
||||
const warnings = state.loadGcode(text);
|
||||
statusEl.textContent = `${file.name}: ${state.segments.length} moves` + (warnings.length ? ` (${warnings.length} warnings)` : '');
|
||||
render();
|
||||
});
|
||||
|
||||
// expose for the UI modules added in later tasks
|
||||
export const app = { state, render, resize, overlay, stage, stream };
|
||||
|
||||
async function boot(): Promise<void> {
|
||||
try {
|
||||
const cfg = await loadConfig('config.json');
|
||||
style = cfg.renderDefaults;
|
||||
if (cfg.streamUrl) stream.src = cfg.streamUrl;
|
||||
if (cfg.calibration) state.setHomography(cfg.calibration.homography);
|
||||
statusEl.textContent = cfg.calibration ? 'Ready. Open a G-code file.' : 'Not calibrated — calibrate first.';
|
||||
} catch (e) {
|
||||
statusEl.textContent = `Config error: ${(e as Error).message}`;
|
||||
}
|
||||
resize();
|
||||
}
|
||||
|
||||
void boot();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue