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 charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>G-Code Overlay</title>
|
<title>G-Code Overlay</title>
|
||||||
<link rel="stylesheet" href="/src/styles.css" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="stage">
|
<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 './styles.css';
|
||||||
|
import { loadConfig } from './config';
|
||||||
|
import { createState } from './app/state';
|
||||||
import { computeOverlayRect, syncOverlay } from './app/layout';
|
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 stage = document.getElementById('stage') as HTMLDivElement;
|
||||||
const stream = document.getElementById('stream') as HTMLImageElement;
|
const stream = document.getElementById('stream') as HTMLImageElement;
|
||||||
const overlay = document.getElementById('overlay') as HTMLCanvasElement;
|
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 {
|
function resize(): void {
|
||||||
const rect = computeOverlayRect(
|
const rect = computeOverlayRect(stream.naturalWidth, stream.naturalHeight, stage.clientWidth, stage.clientHeight);
|
||||||
stream.naturalWidth,
|
|
||||||
stream.naturalHeight,
|
|
||||||
stage.clientWidth,
|
|
||||||
stage.clientHeight,
|
|
||||||
);
|
|
||||||
syncOverlay(overlay, rect);
|
syncOverlay(overlay, rect);
|
||||||
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
stream.addEventListener('load', 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