From 407a70fef1e41b09b7216d8be4a84faff2fdde77 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Wed, 8 Jul 2026 14:39:57 +0100 Subject: [PATCH] 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. --- prototype/src/App.tsx | 4 +-- prototype/src/sim.ts | 75 ++++++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/prototype/src/App.tsx b/prototype/src/App.tsx index 0223f28..ff0dfaa 100644 --- a/prototype/src/App.tsx +++ b/prototype/src/App.tsx @@ -141,7 +141,7 @@ export default function App(): JSX.Element { /> {ob.moving && ( - mobile + transit )} @@ -186,7 +186,7 @@ export default function App(): JSX.Element {
blue line — pose broadcast (5 Hz, ~46 B)
green line — bulk sync (sealed partitions)
flash — broadcast event delivered
-
red circle — mobile obstacle
+
red circle — transit object crossing the area
diff --git a/prototype/src/sim.ts b/prototype/src/sim.ts index 287b94a..f57f815 100644 --- a/prototype/src/sim.ts +++ b/prototype/src/sim.ts @@ -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;