From 798e644b33d4c8b7abf2c686bde2557097f288b2 Mon Sep 17 00:00:00 2001 From: Paris DOUADY Date: Sun, 14 Jul 2024 16:02:05 +0200 Subject: [PATCH] Merge roads Fixes #111 --- Cargo.lock | 7 + Cargo.toml | 1 + native_app/src/gui/hud.rs | 4 +- native_app/src/gui/tools/roadbuild.rs | 21 ++- .../src/rendering/map_rendering/map_mesh.rs | 2 +- simulation/Cargo.toml | 2 + simulation/src/init.rs | 2 + simulation/src/lib.rs | 3 +- simulation/src/map/map.rs | 172 +++++++++++++++--- simulation/src/map/objects/intersection.rs | 66 ++++--- simulation/src/map/objects/lane.rs | 2 +- simulation/src/map/turn_policy.rs | 2 +- simulation/src/map_dynamic/dispatch.rs | 2 +- simulation/src/rerun.rs | 48 +++++ simulation/src/tests/test_iso.rs | 32 +++- 15 files changed, 303 insertions(+), 63 deletions(-) create mode 100644 simulation/src/rerun.rs diff --git a/Cargo.lock b/Cargo.lock index 52475f33..8ad7b023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,6 +754,12 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dispatch" version = "0.2.0" @@ -2664,6 +2670,7 @@ dependencies = [ "bitflags 2.6.0", "common", "derive_more", + "diff", "easybench", "egui-inspect", "flat_spatial", diff --git a/Cargo.toml b/Cargo.toml index ee08b283..8b0da565 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ yakui-core = { git = "https://github.com/Uriopass/yakui", branch = "dev" } yakui-widgets = { git = "https://github.com/Uriopass/yakui", branch = "dev" } itertools = { version = "0.13.0", default-features = false } mlua = { version = "0.9.4", features = ["luau"] } +# rerun = { version = "0.17.0", default-features = false, features = ["sdk"] } # Set the settings for build scripts and proc-macros. [profile.dev.build-override] diff --git a/native_app/src/gui/hud.rs b/native_app/src/gui/hud.rs index 5763459b..141d51f5 100644 --- a/native_app/src/gui/hud.rs +++ b/native_app/src/gui/hud.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::time::{Duration, Instant}; use goryak::{image_button, minrow, on_secondary_container, textc}; use ordered_float::OrderedFloat; @@ -47,7 +47,7 @@ pub fn render_newgui(uiworld: &UiWorld, sim: &Simulation) { } fn auto_save(uiworld: &UiWorld) { - let every = uiworld.read::().auto_save_every.into(); + let every: Option = uiworld.read::().auto_save_every.into(); let mut gui = uiworld.write::(); if let Some(every) = every { if gui.last_save.elapsed() > every { diff --git a/native_app/src/gui/tools/roadbuild.rs b/native_app/src/gui/tools/roadbuild.rs index ef1eec54..d8f1a4be 100644 --- a/native_app/src/gui/tools/roadbuild.rs +++ b/native_app/src/gui/tools/roadbuild.rs @@ -368,9 +368,9 @@ pub struct RoadBuildResource { #[derive(Default, Clone, Copy)] pub enum Snapping { - #[default] None, SnapToGrid, + #[default] SnapToAngle, } @@ -387,7 +387,7 @@ fn check_angle(map: &Map, from: MapProject, to: Vec2, is_rail: bool) -> bool { let max_turn_angle = if is_rail { 0.0 } else { - 30.0 * std::f32::consts::PI / 180.0 + 25.0 * std::f32::consts::PI / 180.0 }; match from.kind { @@ -401,7 +401,7 @@ fn check_angle(map: &Map, from: MapProject, to: Vec2, is_rail: bool) -> bool { .roads .iter() .map(|road_id| map.roads()[*road_id].dir_from(i)) - .any(|v| v.angle(dir).abs() >= max_turn_angle) + .all(|v| v.angle(dir).abs() >= max_turn_angle) } Road(r) => { let Some(r) = map.roads().get(r) else { @@ -536,9 +536,18 @@ impl RoadBuildResource { .color(col); } - immdraw.circle(p.first(), patwidth * 0.5).color(col); - immdraw.circle(p.last(), patwidth * 0.5).color(col); - immdraw.polyline(p.into_vec(), patwidth, false).color(col); + immdraw.circle(p.first().up(0.1), patwidth * 0.5).color(col); + immdraw.circle(p.last().up(0.1), patwidth * 0.5).color(col); + immdraw + .polyline( + p.into_vec() + .into_iter() + .map(|v| v.up(0.1)) + .collect::>(), + patwidth, + false, + ) + .color(col); } pub fn possible_interpolations(&self, map: &Map, mousepos: Vec3) -> Option> { diff --git a/native_app/src/rendering/map_rendering/map_mesh.rs b/native_app/src/rendering/map_rendering/map_mesh.rs index b35da567..412ec371 100644 --- a/native_app/src/rendering/map_rendering/map_mesh.rs +++ b/native_app/src/rendering/map_rendering/map_mesh.rs @@ -766,7 +766,7 @@ impl MapBuilders { LotKind::Residential => simulation::colors().lot_residential_col, }; tess_lots.set_color(col); - tess_lots.draw_filled_polygon(&lot.shape.corners, lot.height + 0.3); + tess_lots.draw_filled_polygon(&lot.shape.corners, lot.height + 0.28); } } } diff --git a/simulation/Cargo.toml b/simulation/Cargo.toml index a7485e5e..8a34241f 100644 --- a/simulation/Cargo.toml +++ b/simulation/Cargo.toml @@ -26,6 +26,8 @@ arc-swap = "1.3.0" derive_more = { workspace = true } bitflags = "2.4.1" itertools = { workspace = true } +diff = "0.1.13" +# rerun = { workspace = true } [dev-dependencies] diff --git a/simulation/src/init.rs b/simulation/src/init.rs index 3c452a22..1e7116a0 100644 --- a/simulation/src/init.rs +++ b/simulation/src/init.rs @@ -28,6 +28,8 @@ use serde::de::DeserializeOwned; use serde::Serialize; pub fn init() { + //crate::rerun::init_rerun(); + // # Safety // This function is called only once, before any other function in this crate. unsafe { diff --git a/simulation/src/lib.rs b/simulation/src/lib.rs index ade65a5d..30d03925 100644 --- a/simulation/src/lib.rs +++ b/simulation/src/lib.rs @@ -2,7 +2,7 @@ #![allow(clippy::type_complexity)] use crate::init::{GSYSTEMS, INIT_FUNCS, SAVELOAD_FUNCS}; -use crate::map::{BuildingKind, Heightmap, Map}; +use crate::map::{BuildingKind, Map}; use crate::map_dynamic::{Itinerary, ItineraryLeader}; use crate::souls::add_souls_to_empty_buildings; use crate::utils::resources::{Ref, RefMut, Resources}; @@ -42,6 +42,7 @@ pub mod init; pub mod map; pub mod map_dynamic; pub mod multiplayer; +mod rerun; pub mod souls; #[cfg(test)] mod tests; diff --git a/simulation/src/map/map.rs b/simulation/src/map/map.rs index 504faacd..32bbf729 100644 --- a/simulation/src/map/map.rs +++ b/simulation/src/map/map.rs @@ -12,13 +12,13 @@ use geom::{Vec2, Vec3}; use ordered_float::OrderedFloat; use prototypes::{BuildingGen, Tick}; use serde::{Deserialize, Serialize}; -use slotmapd::HopSlotMap; +use slotmapd::SlotMap; -pub type Roads = HopSlotMap; -pub type Lanes = HopSlotMap; -pub type Intersections = HopSlotMap; -pub type Buildings = HopSlotMap; -pub type Lots = HopSlotMap; +pub type Roads = SlotMap; +pub type Lanes = SlotMap; +pub type Intersections = SlotMap; +pub type Buildings = SlotMap; +pub type Lots = SlotMap; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct MapProject { @@ -160,6 +160,7 @@ impl Map { { return None; } + info!("make_connection {:?} {:?} {:?}", from, to, interpoint); let connection_segment = match interpoint { Some(x) => RoadSegmentKind::from_elbow(from.pos.xy(), to.pos.xy(), x), @@ -189,10 +190,24 @@ impl Map { return None; }; - info!( - "connect {:?}({:?}) {:?}({:?}) {:?} {:?}: {:?}", - from, from_id, to, to_id, pattern, &interpoint, r - ); + self.invalidate(from_id); + self.invalidate(to_id); + + let mut maybe_merge = |inter_id| { + let inter = &self.intersections[inter_id]; + if let [r1_id, r2_id] = *inter.roads { + let r1 = &self.roads[r1_id]; + let r2 = &self.roads[r2_id]; + // only merge if angle is shallow + if r1.dir_from(inter_id).dot(r2.dir_from(inter_id)) < -0.9 { + self.merge_road(r1_id, r2_id); + } + } + }; + maybe_merge(from_id); + maybe_merge(to_id); + + info!("connected {:?} {:?}: {:?}", from_id, to_id, r); self.check_invariants(); @@ -476,12 +491,13 @@ impl Map { ) -> Option { info!("split_road {:?} {:?}", split_road_id, pos); - let pat = self.roads.get(split_road_id)?.pattern(&self.lanes); - - let r = unwrap_or!(self.remove_raw_road(split_road_id), { - log::error!("Trying to split unexisting road"); + if !self.roads.contains_key(split_road_id) { + log::error!("splitting non-existing road {:?}", split_road_id); return None; - }); + } + + let pat = self.roads.get(split_road_id)?.pattern(&self.lanes); + let r = self.remove_raw_road(split_road_id)?; self.subscribers.dispatch(UpdateType::Road, &r); for (id, _) in r.lanes_iter() { @@ -496,6 +512,10 @@ impl Map { let r1 = self.connect(r.src, id, &pat, RoadSegmentKind::Arbitrary(before))?; let r2 = self.connect(id, r.dst, &pat, RoadSegmentKind::Arbitrary(after))?; + self.invalidate(r.src); + self.invalidate(r.dst); + self.invalidate(id); + log::info!( "{} parking spots reused when splitting", self.parking.clean_reuse() @@ -556,6 +576,109 @@ impl Map { Some(id) } + pub(crate) fn merge_road(&mut self, road1: RoadID, road2: RoadID) -> Option { + info!("merge_road {:?} {:?}", road1, road2); + + let r1 = self.roads.get(road1)?; + let r2 = self.roads.get(road2)?; + + let same_inter = if r1.src == r2.src || r1.src == r2.dst { + r1.src + } else if r1.dst == r2.src || r1.dst == r2.dst { + r1.dst + } else { + warn!("trying to merge roads that are not connected to each other"); + return None; + }; + + let mut pat1 = r1.pattern(&self.lanes); + let mut pat2 = r2.pattern(&self.lanes); + + if r1.src == same_inter { + std::mem::swap(&mut pat1.lanes_backward, &mut pat1.lanes_forward); + } + + if r2.src != same_inter { + std::mem::swap(&mut pat2.lanes_backward, &mut pat2.lanes_forward); + } + + if pat1 != pat2 { + log::info!("merge refused because patterns don't match"); + return None; + } + + let r1_extremity = if r1.src == same_inter { r1.dst } else { r1.src }; + let r2_extremity = if r2.src == same_inter { r2.dst } else { r2.src }; + + if r1_extremity == r2_extremity { + log::info!("merge refused because connecting to itself"); + return None; + } + + let r1 = self.remove_raw_road(road1)?; + let r2 = self.remove_raw_road(road2)?; + + self.remove_intersection_inner(same_inter); + + self.subscribers.dispatch(UpdateType::Road, &r1); + self.subscribers.dispatch(UpdateType::Road, &r2); + + for (id, _) in r2.lanes_iter().chain(r1.lanes_iter()) { + self.parking.remove_to_reuse(id); + } + + let mut new_polyline = r1.points; + if r1.src == same_inter { + new_polyline.reverse(); + } + + if r2.src == same_inter { + new_polyline.extend(r2.points.iter().skip(1)) + } else { + new_polyline.extend(r2.points.iter().rev().skip(1)) + } + + let new_r = self.connect( + r1_extremity, + r2_extremity, + &pat1, + RoadSegmentKind::Arbitrary(new_polyline), + )?; + + let new_r = &mut self.roads[new_r]; + log::info!("merge_road new road is {:?}", new_r.id); + + log::info!( + "{} parking spots reused when merging", + self.parking.clean_reuse() + ); + + for b in r1 + .connected_buildings + .iter() + .chain(r2.connected_buildings.iter()) + { + let b = self.buildings.get_mut(*b)?; + b.connected_road = Some(new_r.id); + new_r.connected_buildings.push(b.id); + + self.electricity.add_edge(b.id, new_r.id); + } + + for lot in &mut self.lots.values_mut() { + if lot.parent == road1 || lot.parent == road2 { + lot.parent = new_r.id; + } + } + + let new_id = new_r.id; + + self.invalidate(r1_extremity); + self.invalidate(r2_extremity); + + Some(new_id) + } + /// Returns None if one of the intersections don't exist pub(crate) fn connect( &mut self, @@ -567,6 +690,8 @@ impl Map { let src = self.intersections.get(src_id)?; let dst = self.intersections.get(dst_id)?; + let gen_lots = !matches!(segment, RoadSegmentKind::Arbitrary(_)); + let rid = Road::make( src, dst, @@ -589,11 +714,10 @@ impl Map { self.intersections.get_mut(src_id)?.add_road(&self.roads, r); self.intersections.get_mut(dst_id)?.add_road(&self.roads, r); - self.invalidate(src_id); - self.invalidate(dst_id); - Lot::remove_intersecting_lots(self, rid); - Lot::generate_along_road(self, rid); + if gen_lots { + Lot::generate_along_road(self, rid); + } #[allow(clippy::indexing_slicing)] let r = &self.roads[rid]; @@ -787,9 +911,9 @@ impl Map { assert_eq!(turn.id.parent, inter.id); assert!( self.lanes.contains_key(turn.id.src), - "{:?} {:?}", + "Turn src doesn't exist for inter:{:?} src:{:?}", inter.id, - turn.id.src + turn.id.src, ); assert!( self.lanes.contains_key(turn.id.dst), @@ -911,7 +1035,11 @@ impl Map { for lot in self.lots.values() { log::debug!("{:?}", lot.id); assert!(lot.shape.axis().iter().all(|x| x.mag() > 0.0)); - assert!(self.roads.contains_key(lot.parent), "{:?}", lot.parent); + assert!( + self.roads.contains_key(lot.parent), + "Lot parent {:?} doesn't exist", + lot.parent + ); assert!(self.spatial_map.contains(lot.id)); } diff --git a/simulation/src/map/objects/intersection.rs b/simulation/src/map/objects/intersection.rs index 54a2a923..9385118c 100644 --- a/simulation/src/map/objects/intersection.rs +++ b/simulation/src/map/objects/intersection.rs @@ -2,7 +2,7 @@ use crate::map::{ Intersections, LaneID, LaneKind, Lanes, LightPolicy, Road, RoadID, Roads, SpatialMap, TraverseDirection, Turn, TurnID, TurnPolicy, }; -use geom::{pseudo_angle, Circle}; +use geom::{pseudo_angle, Circle, Ray}; use geom::{Vec2, Vec3}; use ordered_float::OrderedFloat; use serde::{Deserialize, Serialize}; @@ -109,27 +109,55 @@ impl Intersection { self.light_policy.apply(self, lanes, roads); } - fn check_dead_roads(&mut self, roads: &Roads) { - let id = self.id; - self.roads.retain(|x| { - let v = roads.contains_key(*x); - if !v { - log::error!( - "{:?} contained unexisting {:?} when updating interface radius", - id, - x - ); - } - v - }); - } - const MIN_INTERFACE: f32 = 9.0; // allow slicing since we remove all roads not in self.roads #[allow(clippy::indexing_slicing)] pub fn update_interface_radius(&mut self, roads: &mut Roads) { let id = self.id; - self.check_dead_roads(roads); + + if let [] = *self.roads { + return; + } + + if let [r1_id] = *self.roads { + let r = &mut roads[r1_id]; + r.set_interface(id, Self::empty_interface(r.width)); + return; + } + + if let [r1_id, r2_id] = *self.roads { + let (r1, r2) = (&roads[r1_id], &roads[r2_id]); + let (dir1, dir2) = (r1.dir_from(id), r2.dir_from(id)); + let (r1w, r2w) = (r1.width, r2.width); + let elbow = (dir1 + dir2) * 0.5; + + if elbow.mag() < 0.001 { + roads[r1_id].set_interface(id, 1.0); + roads[r2_id].set_interface(id, 1.0); + return; + } + + let ray1 = Ray::new( + self.pos.xy() + + dir1.perpendicular() * dir1.perpendicular().dot(elbow).signum() * r1w * 0.5, + dir1, + ); + let ray2 = Ray::new( + self.pos.xy() + + dir2.perpendicular() * dir2.perpendicular().dot(elbow).signum() * r2w * 0.5, + dir2, + ); + + let Some((dist_a, dist_b)) = ray1.both_dist_to_inter(&ray2) else { + roads[r1_id].set_interface(id, Self::empty_interface(r1w)); + roads[r2_id].set_interface(id, Self::empty_interface(r2w)); + return; + }; + + roads[r1_id].set_interface(id, dist_a); + roads[r2_id].set_interface(id, dist_b); + return; + } for &r in &self.roads { let r = &mut roads[r]; @@ -145,10 +173,6 @@ impl Intersection { } } - if self.roads.len() <= 1 { - return; - } - for i in 0..self.roads.len() { let r1_id = self.roads[i]; let r2_id = self.roads[(i + 1) % self.roads.len()]; diff --git a/simulation/src/map/objects/lane.rs b/simulation/src/map/objects/lane.rs index ee78385d..17804433 100644 --- a/simulation/src/map/objects/lane.rs +++ b/simulation/src/map/objects/lane.rs @@ -79,7 +79,7 @@ pub struct Lane { pub dist_from_bottom: f32, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct LanePattern { pub lanes_forward: Vec<(LaneKind, f32)>, pub lanes_backward: Vec<(LaneKind, f32)>, diff --git a/simulation/src/map/turn_policy.rs b/simulation/src/map/turn_policy.rs index ae5b83ec..06dacc7a 100644 --- a/simulation/src/map/turn_policy.rs +++ b/simulation/src/map/turn_policy.rs @@ -221,7 +221,7 @@ impl TurnPolicy { )); } - if self.crosswalks && n_roads >= 2 { + if self.crosswalks && n_roads > 2 { if let (Some(incoming), Some(outgoing_in)) = (a.incoming, a.outgoing) { turns.push(( TurnID::new(inter.id, incoming, outgoing_in, true), diff --git a/simulation/src/map_dynamic/dispatch.rs b/simulation/src/map_dynamic/dispatch.rs index 8efe02d4..eaad55af 100644 --- a/simulation/src/map_dynamic/dispatch.rs +++ b/simulation/src/map_dynamic/dispatch.rs @@ -468,7 +468,7 @@ mod tests { kind: ProjectKind::Intersection(i), pos: Vec3::x(100.0), }, - MapProject::ground(Vec3::x(200.0)), + MapProject::ground(Vec3::new(200.0, 50.0, 0.0)), None, &LanePatternBuilder::new().one_way(true).rail(true).build(), ) diff --git a/simulation/src/rerun.rs b/simulation/src/rerun.rs new file mode 100644 index 00000000..4f1f545b --- /dev/null +++ b/simulation/src/rerun.rs @@ -0,0 +1,48 @@ +#![allow(unused)] +/* +use geom::{Vec2, Vec3}; +use lazy_static::lazy_static; +use rerun::{Points2D, Points3D}; +use std::sync::Mutex; + +lazy_static! { +static ref RERUN: Mutex> = Mutex::new(None); +} + +pub fn init_rerun() { + let rec = rerun::RecordingStreamBuilder::new("rerun_example_dna_abacus") + .connect() + .unwrap(); + *RERUN.lock().unwrap() = Some(rec); +} + +pub fn rr_poly(iter: impl Iterator) { + RERUN + .lock() + .unwrap() + .as_ref() + .unwrap() + .log("/goria", &Points2D::new(iter.map(|v| [v.x, v.y]))) + .unwrap(); +} + +pub fn rr_v2(v: Vec2) { + RERUN + .lock() + .unwrap() + .as_ref() + .unwrap() + .log("/goria", &Points2D::new([[v.x, v.y]])) + .unwrap(); +} + +pub fn rr_v3(v: Vec3) { + RERUN + .lock() + .unwrap() + .as_ref() + .unwrap() + .log("/goria", &Points3D::new([[v.x, v.y, v.z]])) + .unwrap(); +} +*/ diff --git a/simulation/src/tests/test_iso.rs b/simulation/src/tests/test_iso.rs index 269ee393..78e27772 100644 --- a/simulation/src/tests/test_iso.rs +++ b/simulation/src/tests/test_iso.rs @@ -4,9 +4,9 @@ use crate::utils::scheduler::SeqSchedule; use crate::World; use crate::{Replay, Simulation}; use common::logger::MyLog; -use common::saveload::{Bincode, Encoder}; +use common::saveload::{Bincode, Encoder, JSONPretty}; use geom::vec3; -use quickcheck::{Arbitrary, Gen}; +use quickcheck::{Arbitrary, Gen, TestResult}; static REPLAY: &[u8] = include_bytes!("world_replay.json"); @@ -65,9 +65,9 @@ impl Arbitrary for MapAction { #[test] fn quickcheck_map_ser() { - let mut q = quickcheck::QuickCheck::new(); + let mut q = quickcheck::QuickCheck::new().tests(100); q.quickcheck( - (|vals: Vec<(MapAction, u32, F3201, F3201)>| -> bool { + (|vals: Vec<(MapAction, u32, F3201, F3201)>| -> TestResult { let mut m = Map::empty(); let mut m2 = Map::empty(); @@ -120,7 +120,7 @@ fn quickcheck_map_ser() { ); m2.make_connection( MapProject { - pos: m.intersections[i].pos, + pos: m2.intersections[i].pos, kind: ProjectKind::Intersection(i), }, MapProject { @@ -216,8 +216,26 @@ fn quickcheck_map_ser() { } } - Bincode::encode(&m).unwrap() == Bincode::encode(&m2).unwrap() - }) as fn(_) -> bool, + let v = Bincode::encode(&m).unwrap() == Bincode::encode(&m2).unwrap(); + if !v { + let m_enc = unsafe { String::from_utf8_unchecked(JSONPretty::encode(&m).unwrap()) }; + let m2_enc = + unsafe { String::from_utf8_unchecked(JSONPretty::encode(&m2).unwrap()) }; + let diff = diff::lines(&m_enc, &m2_enc); + let mut diff_str = String::new(); + for line in diff { + match line { + diff::Result::Left(l) => diff_str.push_str(&format!("- {}\n", l)), + diff::Result::Both(l, _) => diff_str.push_str(&format!(" {}\n", l)), + diff::Result::Right(r) => diff_str.push_str(&format!("+ {}\n", r)), + } + } + + TestResult::error(diff_str) + } else { + TestResult::passed() + } + }) as fn(_) -> TestResult, ); }