An indoor navigation drone's firmware and its simulator, in Rust and Dioxus
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287//! Drawing the room, the drone and the drone's own map onto a canvas.//!//! This is what the C's `SdlRenderer` did, in the same colours: a white floor,//! black walls, blue windows, green sensor beams and a black drone. Everything//! that was drawn as text there -- the readouts down the right-hand side -- is//! DOM here instead, which is why there is no font work in this file.
// Canvas coordinates are floats; the casts turn counts and indices into them.#![allow(clippy::cast_precision_loss)]
use randie_firmware::laser::LASER_MAX_DISTANCE_CM;use randie_firmware::map::{FieldState, MAP_HEIGHT, MAP_WIDTH, Map};use randie_sim::block::{Block, BlockType};use randie_sim::{Simulator, Vec2};use web_sys::CanvasRenderingContext2d;
use crate::camera::Camera;
/// The room's floor.const WHITE: &str = "#ffffff";/// Walls, the drone, and the grid.const BLACK: &str = "#000000";/// Windows.const BLUE: &str = "#0000ff";/// Sensor beams.const GREEN: &str = "#00b000";/// The origin's grid lines, and the drone on the minimap.const RED: &str = "#ff0000";/// Fields the drone has flown over.const VISITED: &str = "#00c853";
/// How faint the grid's ordinary lines are.const GRID_LINE: &str = "rgba(0, 0, 0, 0.12)";/// How faint the grid's metre lines are.const GRID_METRE: &str = "rgba(0, 0, 0, 0.35)";
/// Below this many pixels to the block, the fine grid is not drawn: it would/// be a solid wash.const MIN_GRID_PIXELS: f64 = 5.0;
/// How much of the canvas's height the minimap takes.const MINIMAP_FRACTION: f64 = 0.3;/// How far the minimap sits from the canvas's edge, in pixels.const MINIMAP_MARGIN: f64 = 12.0;
/// Draws one frame.pub fn draw( ctx: &CanvasRenderingContext2d, sim: &Simulator, camera: &Camera, width: f64, height: f64,) { ctx.set_fill_style_str(WHITE); ctx.fill_rect(0.0, 0.0, width, height);
draw_grid(ctx, camera, width, height); draw_blocks(ctx, &sim.blocks, camera); draw_drone(ctx, sim, camera); draw_scale(ctx, camera, width, height); draw_minimap(ctx, &sim.drone.firmware.map, sim, width, height);}
/// The block grid, with a heavier line every metre and a red one at the room's/// origin.////// The C stepped the grid across the window in whole blocks from the top-left/// pixel, so the lines only lined up with the blocks when the view happened to/// be panned to a multiple of the block size. These are drawn at the block/// boundaries themselves.fn draw_grid(ctx: &CanvasRenderingContext2d, camera: &Camera, width: f64, height: f64) { let spacing = camera.scale(Block::SIZE); let top_left = camera.to_world(0.0, 0.0); let bottom_right = camera.to_world(width, height);
ctx.set_line_width(1.0);
let fine = spacing >= MIN_GRID_PIXELS; let first_column = (top_left.x / Block::SIZE).floor() as i64; let last_column = (bottom_right.x / Block::SIZE).ceil() as i64;
for column in first_column..=last_column { let x = column as f64 * Block::SIZE; let metre = column % 4 == 0;
if !fine && !metre { continue; }
ctx.set_stroke_style_str(match (x == 0.0, metre) { (true, _) => RED, (false, true) => GRID_METRE, (false, false) => GRID_LINE, });
let (screen_x, _) = camera.to_screen(Vec2::new(x, 0.0)); line(ctx, screen_x, 0.0, screen_x, height); }
let first_row = (bottom_right.y / Block::SIZE).floor() as i64; let last_row = (top_left.y / Block::SIZE).ceil() as i64;
for row in first_row..=last_row { let y = row as f64 * Block::SIZE; let metre = row % 4 == 0;
if !fine && !metre { continue; }
ctx.set_stroke_style_str(match (y == 0.0, metre) { (true, _) => RED, (false, true) => GRID_METRE, (false, false) => GRID_LINE, });
let (_, screen_y) = camera.to_screen(Vec2::new(0.0, y)); line(ctx, 0.0, screen_y, width, screen_y); }}
/// The room itself.fn draw_blocks(ctx: &CanvasRenderingContext2d, blocks: &[Block], camera: &Camera) { let size = camera.scale(Block::SIZE);
for block in blocks { ctx.set_fill_style_str(match block.kind { BlockType::Air => continue, BlockType::Wall => BLACK, BlockType::Window => BLUE, });
// The canvas's origin is the top-left corner, and the room's y grows // the other way, so a block is drawn from its top-left corner. let (x, y) = camera.to_screen(Vec2::new(block.min.x, block.max.y)); ctx.fill_rect(x, y, size, size); }}
/// The drone, and where its beams reach.////// The C drew all sixty beams in the same solid green, which at the sonar's/// fifty-seven rays is a green wedge with the three laser beams lost in it./// The wedge is drawn faint here and the laser beams solid, so it is possible/// to see which is which.fn draw_drone(ctx: &CanvasRenderingContext2d, sim: &Simulator, camera: &Camera) { let drone = &sim.drone;
ctx.set_line_width(1.0); ctx.set_stroke_style_str("rgba(0, 176, 0, 0.18)"); for ray in &drone.sonar.rays { let (x1, y1) = camera.to_screen(ray.origin); let (x2, y2) = camera.to_screen(ray.end()); line(ctx, x1, y1, x2, y2); }
// Each laser beam is drawn as far as the firmware was told it reached, so // what the drone can see is what is on the screen. The C drew all sixty at // full length whatever came back, which puts a solid green line through // every wall the beam is pointed at. let readings = [ drone.firmware.rep.laser.val_left, drone.firmware.rep.laser.val_front, drone.firmware.rep.laser.val_right, ];
for (ray, reading) in drone.laser.rays.iter().zip(readings) { let hit = reading < LASER_MAX_DISTANCE_CM;
let end = if hit { ray.origin + Vec2::from_polar(f64::from(reading), ray.angle) } else { ray.end() };
ctx.set_stroke_style_str(if hit { GREEN } else { "rgba(0, 176, 0, 0.35)" }); let (x1, y1) = camera.to_screen(ray.origin); let (x2, y2) = camera.to_screen(end); line(ctx, x1, y1, x2, y2); }
let (x, y) = camera.to_screen(drone.pos); let radius = camera.scale(drone.size / 2.0);
ctx.set_fill_style_str(BLACK); ctx.begin_path(); let _ = ctx.arc(x, y, radius.max(2.0), 0.0, std::f64::consts::TAU); ctx.fill();
// Which way it is pointing. The C left this to the beams. let nose = drone.pos + Vec2::from_polar(drone.size / 2.0, drone.angle); let (nose_x, nose_y) = camera.to_screen(nose); ctx.set_stroke_style_str(WHITE); ctx.set_line_width(2.0); line(ctx, x, y, nose_x, nose_y);}
/// A bar showing how long a metre is at the current zoom. Down in the/// bottom-right, which is the one corner nothing else uses.fn draw_scale(ctx: &CanvasRenderingContext2d, camera: &Camera, width: f64, height: f64) { let length = camera.scale(100.0); let left = width - length - 24.0; let bottom = height - 24.0;
ctx.set_stroke_style_str(BLACK); ctx.set_line_width(2.0); line(ctx, left, bottom, left + length, bottom); line(ctx, left, bottom - 5.0, left, bottom + 5.0); line( ctx, left + length, bottom - 5.0, left + length, bottom + 5.0, );
ctx.set_fill_style_str(BLACK); ctx.set_font("12px ui-monospace, monospace"); let _ = ctx.fill_text("1 m", left, bottom - 10.0);}
/// The map the drone has built, in the canvas's top-right corner, with where/// it believes itself to be marked on it.fn draw_minimap( ctx: &CanvasRenderingContext2d, map: &Map, sim: &Simulator, width: f64, height: f64,) { let size = height * MINIMAP_FRACTION; let left = width - size - MINIMAP_MARGIN; let top = MINIMAP_MARGIN; let cell = size / f64::from(MAP_WIDTH);
ctx.set_fill_style_str("rgba(255, 255, 255, 0.92)"); ctx.fill_rect(left, top, size, size);
for row in 0..MAP_HEIGHT { for column in 0..MAP_WIDTH { // The map's rows run south to north and the canvas's run the other // way, so the highest row is drawn first. let field = map.read(column, MAP_HEIGHT - 1 - row);
ctx.set_fill_style_str(match field { FieldState::Unvisited => continue, FieldState::Visited => VISITED, FieldState::Wall => BLACK, FieldState::Window => BLUE, });
ctx.fill_rect( left + f64::from(column) * cell, top + f64::from(row) * cell, cell.ceil(), cell.ceil(), ); } }
// Where the drone believes it is, which is not where it is. A belief that // has drifted off the edge of the map is drawn at the edge rather than // outside the frame. let believed = sim.drone.firmware.nav.position(); let column = believed.x.min(MAP_WIDTH - 1); let row = MAP_HEIGHT - 1 - believed.y.min(MAP_HEIGHT - 1);
ctx.set_fill_style_str(RED); ctx.fill_rect( left + f64::from(column) * cell, top + f64::from(row) * cell, cell.ceil(), cell.ceil(), );
ctx.set_stroke_style_str("rgba(0, 0, 0, 0.5)"); ctx.set_line_width(1.0); ctx.stroke_rect(left, top, size, size);}
fn line(ctx: &CanvasRenderingContext2d, x1: f64, y1: f64, x2: f64, y2: f64) { ctx.begin_path(); ctx.move_to(x1, y1); ctx.line_to(x2, y2); ctx.stroke();}