feat(prototype): adapt the patrol field to the display aspect ratio
CI & Release / Verify simulator (push) Successful in 7s
CI & Release / Trivy scan (push) Successful in 10s
CI & Release / Semantic Release (push) Successful in 1s

The field was a fixed square, leaving wide monitors with dead side
margins. Field width now follows the window aspect (capped at 2.2:1,
quantized to avoid rebuilds on minor resizes); routes, obstacles and
transit spawns stretch with it, and the grid keeps 100 m cells.
This commit is contained in:
2026-07-08 14:51:33 +01:00
parent 407a70fef1
commit f008da1427
2 changed files with 86 additions and 37 deletions
+42 -9
View File
@@ -1,6 +1,8 @@
import { useEffect, useMemo, useRef, useState } from "react";
import {
AREA,
MAX_ASPECT,
areaWidth,
fmtBytes,
makeWorld,
seed,
@@ -9,22 +11,48 @@ import {
} from "./sim";
const VIEW = 1000;
const PANEL_W = 320;
const HEADER_H = 70;
// Patrol field aspect ratio for the current window: wide monitors get a
// wider field instead of empty side margins. Quantized to 0.25 steps so
// minor resizes don't rebuild the world.
function displayAspect(): number {
const w = Math.max(320, window.innerWidth - PANEL_W);
const h = Math.max(320, window.innerHeight - HEADER_H);
const q = Math.round((w / h) * 4) / 4;
return Math.max(1, Math.min(MAX_ASPECT, q));
}
export default function App(): JSX.Element {
const [droneCount, setDroneCount] = useState(8);
const [speedup, setSpeedup] = useState(1);
const [paused, setPaused] = useState(false);
const [aspect, setAspect] = useState(displayAspect);
const [world, setWorld] = useState<World>(() => {
seed(42);
return makeWorld(8);
return makeWorld(8, displayAspect());
});
const raf = useRef(0);
const last = useRef(performance.now());
useEffect(() => {
seed(42);
setWorld(makeWorld(droneCount));
}, [droneCount]);
setWorld(makeWorld(droneCount, aspect));
}, [droneCount, aspect]);
useEffect(() => {
let timer = 0;
const onResize = (): void => {
window.clearTimeout(timer);
timer = window.setTimeout(() => setAspect(displayAspect()), 250);
};
window.addEventListener("resize", onResize);
return () => {
window.clearTimeout(timer);
window.removeEventListener("resize", onResize);
};
}, []);
useEffect(() => {
const loop = (now: number): void => {
@@ -86,7 +114,7 @@ export default function App(): JSX.Element {
<div style={styles.main}>
<svg
viewBox={`0 0 ${VIEW} ${VIEW}`}
viewBox={`0 0 ${sx(areaWidth())} ${VIEW}`}
style={styles.canvas}
preserveAspectRatio="xMidYMid meet"
>
@@ -96,7 +124,7 @@ export default function App(): JSX.Element {
<stop offset="100%" stopColor="#0a0f18" />
</radialGradient>
</defs>
<rect width={VIEW} height={VIEW} fill="url(#ground)" />
<rect width={sx(areaWidth())} height={VIEW} fill="url(#ground)" />
{gridLines()}
{/* Mesh links */}
@@ -202,12 +230,17 @@ function sy(y: number): number {
}
function gridLines(): JSX.Element {
const width = sx(areaWidth());
const step = VIEW / 10; // one cell per 100 m in both directions
const lines = [];
for (let i = 1; i < 10; i++) {
const p = (i / 10) * VIEW;
for (let p = step; p < width; p += step) {
lines.push(
<line key={`v${i}`} x1={p} y1={0} x2={p} y2={VIEW} stroke="#1a2436" strokeWidth={1} />,
<line key={`h${i}`} x1={0} y1={p} x2={VIEW} y2={p} stroke="#1a2436" strokeWidth={1} />,
<line key={`v${p}`} x1={p} y1={0} x2={p} y2={VIEW} stroke="#1a2436" strokeWidth={1} />,
);
}
for (let p = step; p < VIEW; p += step) {
lines.push(
<line key={`h${p}`} x1={0} y1={p} x2={width} y2={p} stroke="#1a2436" strokeWidth={1} />,
);
}
return <g>{lines}</g>;