feat(prototype): crossing patrol routes and through-transit objects

Mobile obstacles used to bounce until they wedged into far corners, and
a single perimeter route let the mesh stretch into segments that stopped
exchanging data. Even drones now fly the perimeter while odd drones fly
an X through the center, keeping bridge links alive, and transit objects
enter at one edge, cross the area, and respawn at a fresh edge.
This commit is contained in:
2026-07-08 14:39:57 +01:00
parent 2787a361bd
commit e47c6aaa48
2 changed files with 59 additions and 20 deletions
+2 -2
View File
@@ -141,7 +141,7 @@ export default function App(): JSX.Element {
/>
{ob.moving && (
<text x={sx(ob.pos.x)} y={sy(ob.pos.y) + 4} style={styles.obLabel as never}>
mobile
transit
</text>
)}
</g>
@@ -186,7 +186,7 @@ export default function App(): JSX.Element {
<div style={styles.dim}>blue line pose broadcast (5 Hz, ~46 B)</div>
<div style={styles.dim}>green line bulk sync (sealed partitions)</div>
<div style={styles.dim}>flash broadcast event delivered</div>
<div style={styles.dim}>red circle mobile obstacle</div>
<div style={styles.dim}>red circle transit object crossing the area</div>
</Section>
</aside>
</div>
+57 -18
View File
@@ -65,12 +65,52 @@ function rnd(): number {
return ((seedState >>> 0) % 100000) / 100000;
}
const WAYPOINTS: Vec[] = [
// Two interleaved patrol routes: even drones circle the perimeter, odd
// drones fly an X through the center. Paths cross mid-field, so the mesh
// keeps bridging between the front and the back of the formation instead
// of stretching into disconnected segments along one loop.
const PERIMETER: Vec[] = [
{ x: 120, y: 120 },
{ x: AREA - 120, y: 150 },
{ x: AREA - 150, y: AREA - 120 },
{ x: 150, y: AREA - 150 },
];
const CROSS: Vec[] = [
{ x: 150, y: 150 },
{ x: AREA - 150, y: AREA - 150 },
{ x: AREA - 150, y: 150 },
{ x: 150, y: AREA - 150 },
];
function routeOf(d: Drone): Vec[] {
return d.id % 2 === 0 ? PERIMETER : CROSS;
}
// Transit objects enter at one edge, cross the whole area, and respawn at
// a fresh edge once they leave — a stream of through-traffic instead of
// obstacles bouncing until they wedge into a corner.
const TRANSIT_MARGIN = 120;
function spawnTransit(id: number): Obstacle {
const speed = 26 + rnd() * 30;
const along = 120 + rnd() * (AREA - 240);
const skew = (rnd() - 0.5) * speed * 0.9; // diagonal-ish crossings
const edge = Math.floor(rnd() * 4);
const table: { pos: Vec; vel: Vec }[] = [
{ pos: { x: along, y: -TRANSIT_MARGIN + 20 }, vel: { x: skew, y: speed } },
{ pos: { x: along, y: AREA + TRANSIT_MARGIN - 20 }, vel: { x: skew, y: -speed } },
{ pos: { x: -TRANSIT_MARGIN + 20, y: along }, vel: { x: speed, y: skew } },
{ pos: { x: AREA + TRANSIT_MARGIN - 20, y: along }, vel: { x: -speed, y: skew } },
];
const { pos, vel } = table[edge];
return { id, pos, radius: 22 + rnd() * 16, moving: true, vel };
}
function outOfTransit(ob: Obstacle): boolean {
return (
ob.pos.x < -TRANSIT_MARGIN ||
ob.pos.x > AREA + TRANSIT_MARGIN ||
ob.pos.y < -TRANSIT_MARGIN ||
ob.pos.y > AREA + TRANSIT_MARGIN
);
}
export function makeWorld(droneCount: number): World {
const drones: Drone[] = Array.from({ length: droneCount }, (_, i) => {
@@ -83,7 +123,7 @@ export function makeWorld(droneCount: number): World {
},
vel: { x: 0, y: 0 },
heading: angle,
waypoint: i % WAYPOINTS.length,
waypoint: i % PERIMETER.length,
battery: 90 + rnd() * 10,
channel: CHANNELS[i % CHANNELS.length],
};
@@ -92,8 +132,9 @@ export function makeWorld(droneCount: number): World {
{ id: 0, pos: { x: 340, y: 420 }, radius: 60, moving: false, vel: { x: 0, y: 0 } },
{ id: 1, pos: { x: 700, y: 260 }, radius: 45, moving: false, vel: { x: 0, y: 0 } },
{ id: 2, pos: { x: 560, y: 720 }, radius: 70, moving: false, vel: { x: 0, y: 0 } },
{ id: 3, pos: { x: 200, y: 800 }, radius: 30, moving: true, vel: { x: 14, y: -6 } },
{ id: 4, pos: { x: 820, y: 620 }, radius: 26, moving: true, vel: { x: -10, y: 9 } },
spawnTransit(3),
spawnTransit(4),
spawnTransit(5),
];
return { t: 0, drones, obstacles, links: new Map(), totalBytes: 0, broadcasts: 0 };
}
@@ -103,12 +144,13 @@ function linkKey(a: number, b: number): string {
}
function steer(d: Drone, world: World, dt: number): Drone {
const wp = WAYPOINTS[(d.waypoint + d.id) % WAYPOINTS.length];
const route = routeOf(d);
const wp = route[(d.waypoint + d.id) % route.length];
let ax = wp.x - d.pos.x;
let ay = wp.y - d.pos.y;
const wpDist = Math.hypot(ax, ay);
let waypoint = d.waypoint;
if (wpDist < 90) waypoint = (d.waypoint + 1) % WAYPOINTS.length;
if (wpDist < 90) waypoint = (d.waypoint + 1) % route.length;
ax /= wpDist || 1;
ay /= wpDist || 1;
@@ -157,18 +199,15 @@ function steer(d: Drone, world: World, dt: number): Drone {
export function tick(world: World, dt: number): World {
const drones = world.drones.map((d) => steer(d, world, dt));
const obstacles = world.obstacles.map((ob) =>
ob.moving
? {
...ob,
pos: { x: ob.pos.x + ob.vel.x * dt, y: ob.pos.y + ob.vel.y * dt },
vel: {
x: ob.pos.x < 60 || ob.pos.x > AREA - 60 ? -ob.vel.x : ob.vel.x,
y: ob.pos.y < 60 || ob.pos.y > AREA - 60 ? -ob.vel.y : ob.vel.y,
},
}
: ob,
);
const obstacles = world.obstacles.map((ob) => {
if (!ob.moving) return ob;
const moved = {
...ob,
pos: { x: ob.pos.x + ob.vel.x * dt, y: ob.pos.y + ob.vel.y * dt },
};
// Once a transit object leaves the area, respawn it at a random edge
return outOfTransit(moved) ? spawnTransit(ob.id) : moved;
});
const links = new Map(world.links);
let totalBytes = world.totalBytes;