feat: page layout and overlay canvas sizing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sjat 2026-06-08 22:34:28 +02:00
parent 96221a217d
commit dd409f56c0
5 changed files with 108 additions and 1 deletions

View file

@ -4,8 +4,22 @@
<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">
<img id="stream" alt="CNC camera stream" />
<canvas id="overlay"></canvas>
</div>
<aside id="panel">
<h1>G-Code Overlay</h1>
<section>
<label class="filebtn">Open G-code<input id="gcode-file" type="file" accept=".nc,.gcode,.tap,.txt,.ngc" hidden /></label>
<p id="status">No file loaded.</p>
</section>
<section id="align-panel"></section>
<section id="calib-panel"></section>
</aside>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

18
src/app/layout.test.ts Normal file
View file

@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest';
import { computeOverlayRect } from './layout';
describe('computeOverlayRect', () => {
it('matches the displayed video box (letterboxed: wide video in tall box)', () => {
// video is 16:9 (1600×900 native) shown in a 800×800 box → fits to width, 450 tall, centred
const r = computeOverlayRect(1600, 900, 800, 800);
expect(r.width).toBeCloseTo(800, 3);
expect(r.height).toBeCloseTo(450, 3);
expect(r.left).toBeCloseTo(0, 3);
expect(r.top).toBeCloseTo(175, 3);
});
it('falls back to the container when the video has no intrinsic size yet', () => {
const r = computeOverlayRect(0, 0, 640, 480);
expect(r).toEqual({ left: 0, top: 0, width: 640, height: 480 });
});
});

40
src/app/layout.ts Normal file
View file

@ -0,0 +1,40 @@
export interface Rect {
left: number;
top: number;
width: number;
height: number;
}
/**
* Compute the on-screen rectangle of a media element shown with object-fit: contain
* inside a container. Returns container bounds if the media size is unknown.
*/
export function computeOverlayRect(
mediaW: number,
mediaH: number,
containerW: number,
containerH: number,
): Rect {
if (mediaW <= 0 || mediaH <= 0) {
return { left: 0, top: 0, width: containerW, height: containerH };
}
const scale = Math.min(containerW / mediaW, containerH / mediaH);
const width = mediaW * scale;
const height = mediaH * scale;
return {
left: (containerW - width) / 2,
top: (containerH - height) / 2,
width,
height,
};
}
/** Position and size a canvas to overlay a media element inside a container. */
export function syncOverlay(canvas: HTMLCanvasElement, rect: Rect): void {
canvas.style.left = `${rect.left}px`;
canvas.style.top = `${rect.top}px`;
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
canvas.width = Math.round(rect.width);
canvas.height = Math.round(rect.height);
}

View file

@ -1 +1,22 @@
console.log('gcode-overlay booting');
import './styles.css';
import { computeOverlayRect, syncOverlay } from './app/layout';
const stage = document.getElementById('stage') as HTMLDivElement;
const stream = document.getElementById('stream') as HTMLImageElement;
const overlay = document.getElementById('overlay') as HTMLCanvasElement;
function resize(): void {
const rect = computeOverlayRect(
stream.naturalWidth,
stream.naturalHeight,
stage.clientWidth,
stage.clientHeight,
);
syncOverlay(overlay, rect);
}
window.addEventListener('resize', resize);
stream.addEventListener('load', resize);
resize();
console.log('layout ready');

14
src/styles.css Normal file
View file

@ -0,0 +1,14 @@
* { box-sizing: border-box; }
body { margin: 0; display: flex; height: 100vh; font-family: system-ui, sans-serif; background: #111; color: #eee; }
#stage { position: relative; flex: 1; overflow: hidden; background: #000; }
#stream { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
#overlay { position: absolute; pointer-events: none; }
#overlay.interactive { pointer-events: auto; cursor: crosshair; }
#panel { width: 320px; padding: 16px; overflow-y: auto; background: #1b1b1b; }
#panel h1 { font-size: 16px; }
.filebtn { display: inline-block; padding: 8px 12px; background: #2962ff; border-radius: 4px; cursor: pointer; }
button { padding: 6px 10px; margin: 2px 0; background: #333; color: #eee; border: 1px solid #555; border-radius: 4px; cursor: pointer; }
input[type="number"] { width: 80px; background: #222; color: #eee; border: 1px solid #555; border-radius: 4px; padding: 4px; }
section { margin-top: 16px; border-top: 1px solid #333; padding-top: 12px; }
.row { display: flex; align-items: center; gap: 6px; margin: 4px 0; }
small { color: #999; }