diff --git a/.gitignore b/.gitignore index b19a093..4f94bb3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,8 @@ input/ *.png -.vscode/ \ No newline at end of file +.vscode/ + +*.json + +tile_images/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 7ef79cf..416e43f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,6 @@ edition = "2021" [dependencies] macroquad = "0.3" glob = "0.3.0" -futures = "0.3.21" \ No newline at end of file +futures = "0.3.21" +serde = { version = "1.0.104", features = ["derive"] } +serde_json = "1.0.48" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ab8dcc5..f0005e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use glob::{glob, GlobError}; use std::{ collections::HashMap, path::PathBuf, - sync::{Arc, Mutex}, + sync::{Arc, Mutex}, fs::File, io::Read, }; use macroquad::prelude::*; @@ -51,54 +51,32 @@ fn get_files_in_dir(path: &str, filetype: &str) -> Result, GlobErro } -const TILE_DIR: &str = "./tile_images/moon/"; +const TILE_DIR: &str = "./tile_images/world1/terrain/"; -async fn get_textures_for_zoom_level( - level: u32, - directory: &str, - tile_dimensions: (f32, f32), -) -> HashMap<(i32, i32), Texture2D> { - let files = get_files_in_dir(&(directory.to_owned() + &level.to_string()), "").unwrap(); - - let mut sector_to_texture = HashMap::new(); - - for file in files { - // get sector from filename - let file_name = file.file_stem().unwrap().to_str().unwrap(); - let split: Vec<&str> = file_name.split(',').collect(); - let x: i32 = split[0].parse().unwrap(); - let z: i32 = split[1].parse().unwrap(); - - // map sector to texture - let texture: Texture2D = load_texture(file.to_str().unwrap()).await.unwrap(); - texture.set_filter(FilterMode::Nearest); - if texture.width() != tile_dimensions.0 || texture.height() != tile_dimensions.1 { - panic!("File: \"{}\" has differing dimensions", file_name) - } - sector_to_texture.insert((x, z), texture); - } - - sector_to_texture -} - -fn coord_to_screen_pos(x: f32, y: f32, camera: &CameraSettings) -> (f32, f32) { +fn coord_to_screen_pos(x: f32, y: f32, camera: &CameraSettings) -> Point { let out_x = screen_width() / 2. + ((camera.x_offset + x) * camera.zoom_multiplier); let out_y = screen_height() / 2. + ((camera.y_offset + y) * camera.zoom_multiplier); - (out_x, out_y) + Point{ + x: out_x, + y: out_y + } } -fn screen_pos_to_coord(x: f32, y: f32, camera: &CameraSettings) -> (f32, f32) { +fn screen_pos_to_coord(x: f32, y: f32, camera: &CameraSettings) -> Point { let x_out = -camera.x_offset + (x as f32 - screen_width() / 2.) / camera.zoom_multiplier; let y_out = -camera.y_offset + (y as f32 - screen_height() / 2.) / camera.zoom_multiplier; - (x_out, y_out) + Point{ + x: x_out, + y: y_out, + } } -// struct Rectangle { -// x: f32, -// y: f32, -// width: f32, -// height: f32, -// } +struct Rectangle { + x: f32, + y: f32, + width: f32, + height: f32, +} // fn value_in_range(value: f32, min: f32, max: f32) -> bool { // (value >= min) && (value <= max) @@ -133,18 +111,18 @@ fn sector_at_screen_pos( // get sector x let tile_world_x_size = tile_dimensions.0 as f32 * two.powf(lod as f32); - let screen_point_sector_x = if screen_point_coords.0 < 0.0 { - (screen_point_coords.0 / tile_world_x_size) as i32 - 1 + let screen_point_sector_x = if screen_point_coords.x < 0. { + (screen_point_coords.x / tile_world_x_size) as i32 - 1 } else { - (screen_point_coords.0 / tile_world_x_size) as i32 + (screen_point_coords.x / tile_world_x_size) as i32 }; // get sector y let tile_world_y_size = tile_dimensions.1 as f32 * two.powf(lod as f32); - let screen_point_sector_y = if screen_point_coords.1 < 0.0 { - (screen_point_coords.1 / tile_world_y_size) as i32 - 1 + let screen_point_sector_y = if screen_point_coords.y < 0.0 { + (screen_point_coords.y / tile_world_y_size) as i32 - 1 } else { - (screen_point_coords.1 / tile_world_y_size) as i32 + (screen_point_coords.y / tile_world_y_size) as i32 }; (screen_point_sector_x, screen_point_sector_y) @@ -185,6 +163,294 @@ async fn cache_texture( drop(retrieving_tile_map); } +fn pathtype_to_color(pathtype: &str) -> Color{ + match pathtype { + "iceroad" => BLUE, + "roofless iceroad" => SKYBLUE, + "rail" => GRAY, + "normal" => GREEN, + _ => GREEN, + } +} + +/// Determins if screen point is currently on the screen +fn point_on_screen(point: &Point) -> bool { + !(point.x < 0. || point.x > screen_width() - 0. || point.y < 0. || point.y > screen_height() - 0.) +} + +/// Extracts lines from json path data for easier rendering +fn get_path_lines(path_data: &serde_json::Value) -> (Vec, Vec){ + + let mut path_lines = Vec::new(); + let mut intersections = Vec::new(); + + let nodes = path_data.as_object().unwrap(); + let mut already_rendered: HashMap = HashMap::new(); + + // for every existing + for (node, node_value) in nodes{ + // println!("node: {}",node); + if let Some(neighbors) = node_value["connections"].as_object(){ + for (neighbor, _) in neighbors { + + // if line not already rendered + if already_rendered.get(&(neighbor.to_owned() + node)) == None{ + + // mark line as rendered + already_rendered.insert(node.to_owned() + neighbor, true); + + let node_pathtype = nodes[node]["pathType"].as_str().unwrap(); + let neighbor_pathtype = nodes[neighbor]["pathType"].as_str().unwrap(); + + let path_type = if node_pathtype == neighbor_pathtype{ + node_pathtype + } else{ + "normal" + }; + + + + path_lines.push(PathLine{ + line: Line{ + point1: Point { x: nodes[node]["x"].as_f64().unwrap() as f32, y: nodes[node]["z"].as_f64().unwrap() as f32 }, + point2: Point { x: nodes[neighbor]["x"].as_f64().unwrap() as f32, y: nodes[neighbor]["z"].as_f64().unwrap() as f32 }, + }, + color: pathtype_to_color(path_type), + }); + } + } + if neighbors.len() > 2{ + + + intersections.push(Intersection{ + point: Point { x: nodes[node]["x"].as_f64().unwrap() as f32, y: nodes[node]["z"].as_f64().unwrap() as f32 }, + color: pathtype_to_color(nodes[node]["pathType"].as_str().unwrap()), + }); + } + } + } + + (path_lines, intersections) +} + +fn render_lines(path_lines: &[PathLine], camera: &CameraSettings){ + let mut min_dist = f32::MAX; + let mut closest_point = Point{x: 0., y: 0.}; + for path_line in path_lines{ + let coords1 = coord_to_screen_pos(path_line.line.point1.x + 0.5, path_line.line.point1.y + 0.5, &camera); + let coords2 = coord_to_screen_pos(path_line.line.point2.x + 0.5, path_line.line.point2.y + 0.5, &camera); + + let line_line = Line{ + point1: Point { x: coords1.x, y: coords1.y }, + point2: Point { x: coords2.x, y: coords2.y }, + }; + + let screen_rectangle = Rectangle{ + x: 0., + y: 0., + width: screen_width() - 0., + height: screen_height() - 0., + }; + + let mouse = mouse_position(); + let mouse_coord = screen_pos_to_coord(mouse.0, mouse.1, &camera); + let color = if point_on_line(&Point{x: mouse_coord.x, y: mouse_coord.y}, &path_line.line, 10.){ + + + + RED + } else { + path_line.color + }; + + + // if let Some(closest_point) = closest_point_on_line_strict(&Point{x: mouse_coord.x, y: mouse_coord.y}, &path_line.line){ + // let intersection_coords = coord_to_screen_pos(closest_point.x + 0.5, closest_point.y + 0.5, &camera); + // draw_circle(intersection_coords.x, intersection_coords.y, 10.0, RED); + // } + + // let closest_point = closest_point_on_line(&Point{x: mouse_coord.x, y: mouse_coord.y}, &path_line.line); + // let intersection_coords = coord_to_screen_pos(closest_point.x + 0.5, closest_point.y + 0.5, &camera); + // if point_on_screen(&intersection_coords){ + // draw_circle(intersection_coords.x, intersection_coords.y, 10.0, RED); + // } + + + if point_on_screen(&coords1) || point_on_screen(&coords2) || line_intersects_rectangle(&line_line, &screen_rectangle) { + draw_line(coords1.x, coords1.y, coords2.x, coords2.y, 5.0, color); + + let closest_point_on_line= closest_point_on_line(&Point{x: mouse_coord.x, y: mouse_coord.y}, &path_line.line); + let cur_dist = distance_between_points(&Point{x: mouse_coord.x, y: mouse_coord.y}, &closest_point_on_line); + if cur_dist < min_dist{ + min_dist = cur_dist; + closest_point = closest_point_on_line; + } + } + + } + + let intersection_coords = coord_to_screen_pos(closest_point.x + 0.5, closest_point.y + 0.5, &camera); + draw_circle(intersection_coords.x, intersection_coords.y, 10.0, RED); +} + +fn render_intersections(intersections: &[Intersection], camera: &CameraSettings){ + for intersection in intersections{ + let intersection_coords = coord_to_screen_pos(intersection.point.x + 0.5, intersection.point.y + 0.5, &camera); + if point_on_screen(&intersection_coords){ + draw_circle(intersection_coords.x, intersection_coords.y, 8.0, intersection.color); + } + } +} + +/// Returns true when two line segments intersect eachother +fn lines_intersect(line1: &Line, line2: &Line) -> bool { + + let x1 = line1.point1.x; + let x2 = line1.point2.x; + let x3 = line2.point1.x; + let x4 = line2.point2.x; + + let y1 = line1.point1.y; + let y2 = line1.point2.y; + let y3 = line2.point1.y; + let y4 = line2.point2.y; + + let u_a: f32 = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)); + let u_b: f32 = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)); + + u_a >= 0.0 && u_a <= 1.0 && u_b >= 0.0 && u_b <= 1.0 +} + +/// Tells if a line segment intersects a rectangle. Does not return true if line is inside rectangle but not intersecting it's sides. +fn line_intersects_rectangle(line: &Line, rectangle: &Rectangle) -> bool { + let top_line = Line{ + point1: Point{ + x: rectangle.x, + y: rectangle.y, + }, + point2: Point{ + x: rectangle.x + rectangle.width, + y: rectangle.y, + }, + }; + + let left_line = Line{ + point1: Point{ + x: rectangle.x, + y: rectangle.y, + }, + point2: Point{ + x: rectangle.x, + y: rectangle.y + rectangle.height, + }, + }; + + let bottom_line = Line{ + point1: Point{ + x: rectangle.x, + y: rectangle.y + rectangle.height, + }, + point2: Point{ + x: rectangle.x + rectangle.width, + y: rectangle.y + rectangle.height, + }, + }; + + let right_line = Line{ + point1: Point{ + x: rectangle.x + rectangle.width, + y: rectangle.y, + }, + point2: Point{ + x: rectangle.x + rectangle.width, + y: rectangle.y + rectangle.height, + }, + }; + + let left = lines_intersect(&left_line, line); + let right = lines_intersect(&right_line, line); + let bottom = lines_intersect(&bottom_line, line); + let top = lines_intersect(&top_line, line); + + left || right || bottom || top +} + +/// Determins distance between two 2d points +fn distance_between_points(point1: &Point, point2: &Point) -> f32 { + ((point1.x - point2.x).abs().powf(2.) + (point1.y - point2.y).abs().powf(2.)).sqrt() +} + +/// Determins if point is on line. Increase `buffer` will increase the distance considered a detection. +fn point_on_line(point: &Point, line: &Line, buffer: f32) -> bool { + let line_len = distance_between_points(&line.point1, &line.point2); + + let d1: f32 = distance_between_points(&point, &line.point1); + let d2: f32 = distance_between_points(&point, &line.point2); + + d1+d2 >= line_len-buffer && d1+d2 <= line_len+buffer +} + + +/// returns closest point on line semgent, if the closest point, were the line infinite and not a segment, is on the line segment. +fn closest_point_on_line_strict(point: &Point, line: &Line) -> Option { + let line_len = distance_between_points(&line.point1, &line.point2); + + let dot: f32 = ( ((point.x-line.point1.x)*(line.point2.x-line.point1.x)) + ((point.y-line.point1.y)*(line.point2.y-line.point1.y)) ) / line_len.powf(2.); + + let closest_x: f32 = line.point1.x + (dot * (line.point2.x-line.point1.x)); + let closest_y: f32 = line.point1.y + (dot * (line.point2.y-line.point1.y)); + + if point_on_line(&Point{x: closest_x, y: closest_y}, line, 0.1){ + Some(Point{x: closest_x, y: closest_y}) + } else { + None + } +} + +/// returns closest point on line semgent. +fn closest_point_on_line(point: &Point, line: &Line) -> Point { + let line_len = distance_between_points(&line.point1, &line.point2); + + let dot: f32 = ( ((point.x-line.point1.x)*(line.point2.x-line.point1.x)) + ((point.y-line.point1.y)*(line.point2.y-line.point1.y)) ) / line_len.powf(2.); + + let closest_x: f32 = line.point1.x + (dot * (line.point2.x-line.point1.x)); + let closest_y: f32 = line.point1.y + (dot * (line.point2.y-line.point1.y)); + + if point_on_line(&Point{x: closest_x, y: closest_y}, line, 0.1){ + Point{x: closest_x, y: closest_y} + } else { + if distance_between_points(point, &line.point1) < distance_between_points(point, &line.point2){ + line.point1.clone() + } else { + line.point2.clone() + } + } +} + + +#[derive(Clone)] +// A 2d point +struct Point{ + x: f32, + y: f32, +} + +// A 2d line segment +struct Line { + point1: Point, + point2: Point, +} + +struct PathLine { + line: Line, + color: Color, +} + +struct Intersection{ + point: Point, + color: Color, +} + struct CameraSettings { x_offset: f32, y_offset: f32, @@ -237,12 +503,19 @@ async fn main() { Arc::new(Mutex::new(HashMap::new())); use futures::executor::LocalPool; - use futures::future::{pending, ready}; use futures::task::LocalSpawnExt; let mut pool = LocalPool::new(); let spawner = pool.spawner(); + // retrieve paths from json + let mut file = File::open("./paths/nodes.json").expect("Failed to open file"); + let mut contents = String::new(); + file.read_to_string(&mut contents).expect("Failed to read to string"); + let path_data: serde_json::Value = serde_json::from_str(&contents).expect("JSON was not well-formatted"); + + let (path_lines, intersections) = get_path_lines(&path_data); + loop { clear_background(GRAY); @@ -306,8 +579,8 @@ async fn main() { camera.zoom_multiplier = camera.zoom_multiplier.clamp(min_zoom, 20.); // center camera on where mouse was in world - camera.x_offset = -mouse_world_pos.0; - camera.y_offset = -mouse_world_pos.1; + camera.x_offset = -mouse_world_pos.x; + camera.y_offset = -mouse_world_pos.y; let screen_x_to_change = mouse_screen_pos.0 - screen_width() / 2.; let screen_y_to_change = mouse_screen_pos.1 - screen_height() / 2.; @@ -328,8 +601,8 @@ async fn main() { camera.zoom_multiplier = camera.zoom_multiplier.clamp(min_zoom, 20.); // center camera on where mouse was in world - camera.x_offset = -mouse_world_pos.0; - camera.y_offset = -mouse_world_pos.1; + camera.x_offset = -mouse_world_pos.x; + camera.y_offset = -mouse_world_pos.y; let screen_x_to_change = mouse_screen_pos.0 - screen_width() / 2.; let screen_y_to_change = mouse_screen_pos.1 - screen_height() / 2.; @@ -451,7 +724,7 @@ async fn main() { let tile_world_x = tile_world_width * sector_x as f32; let tile_world_y = tile_world_height * sector_y as f32; - let (tile_screen_x, tile_screen_y) = + let tile_screen_point = coord_to_screen_pos(tile_world_x, tile_world_y, &camera); let params = DrawTextureParams { @@ -463,7 +736,7 @@ async fn main() { pivot: None, }; - draw_texture_ex(texture, tile_screen_x, tile_screen_y, WHITE, params); + draw_texture_ex(texture, tile_screen_point.x, tile_screen_point.y, WHITE, params); rendered_tiles += 1; } } @@ -522,6 +795,9 @@ async fn main() { // } //< + render_lines(&path_lines, &camera); + render_intersections(&intersections, &camera); + draw_text( &("fps: ".to_owned() + &get_fps().to_string()), 20.0, @@ -557,7 +833,7 @@ async fn main() { let mouse = mouse_position(); let mouse_coord = screen_pos_to_coord(mouse.0, mouse.1, &camera); draw_text( - &("mouse.x: ".to_owned() + &mouse_coord.0.to_string()), + &("mouse.x: ".to_owned() + &mouse_coord.x.to_string()), 20.0, 100.0, 30.0, @@ -565,7 +841,7 @@ async fn main() { ); draw_text( - &("mouse.y: ".to_owned() + &mouse_coord.1.to_string()), + &("mouse.y: ".to_owned() + &mouse_coord.y.to_string()), 20.0, 120.0, 30.0,