From 3faca1ba9de8d34d399ac901887deee82b6c8961 Mon Sep 17 00:00:00 2001 From: Ben Chan Date: Wed, 11 Nov 2026 15:07:49 +0800 Subject: [PATCH] Crossing editor: show neighbour lanes and joints across large junctions - Center neighbour lane queries and 3D snapping around the camera target instead of the object origin so external roads at the perimeter of large junctions (1-2 km across) are drawn and selectable - Expand reach limits for neighbour lanes, nearby points, and auto-linking up to 3500-4000 m - Account for crossing footprint and existing path extents when gathering nearby ends - Keep neighbour lanes visible when editing an empty or new crossing draft - Lift neighbour lane ribbons to 0.25 m and adjust line width/contrast to prevent z-fighting with road meshes --- src/app.rs | 12 +++++------- src/editor.rs | 24 ++++++++++++------------ src/lanes.rs | 7 ++++--- src/ui_crossing.rs | 15 ++++++++++++--- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4f0af5b..32d0274 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2443,8 +2443,8 @@ impl App { // stays in the object's frame, the frame the crossing's paths are written in - nothing // else is to convert it) let mapped = nearby_on_map(self.crossing.neighbours(), world, &m); - let reach = (self.cam.distance * 1.6).clamp(40.0, 180.0); - self.ed.crossing_nearby = cull_nearby(mapped, self.crossing.nearby_own(), self.cam.target.truncate(), reach, 240); + let reach = (self.cam.distance * 3.0).clamp(500.0, 3500.0); + self.ed.crossing_nearby = cull_nearby(mapped, self.crossing.nearby_own(), self.cam.target.truncate(), reach, 500); self.ed.crossing_complete = self.crossing.completing(); } @@ -2652,11 +2652,9 @@ impl App { fn crossing_neighbour_joints(&mut self) -> Vec { let Some(Sel::Object(k, id)) = self.crossing.object() else { return Vec::new() }; let Some(doc) = self.ed.doc.as_ref() else { return Vec::new() }; - let Some(o) = doc.object(k, id) else { return Vec::new() }; - let file = o.file.clone(); - let abs = self.assets.as_mut().and_then(|a| a.object(&file)).map(|m| m.abs_height).unwrap_or(true); - let centre = doc.object_world(k, o, abs).truncate(); - let radius = (self.cam.distance * 2.0).clamp(80.0, 260.0); + let Some(_o) = doc.object(k, id) else { return Vec::new() }; + let centre = self.cam.target.truncate(); + let radius = (self.cam.distance * 3.0).clamp(300.0, 3500.0); let Some(assets) = self.assets.as_mut() else { return Vec::new() }; let (lanes, _) = crate::lanes::near_skipping(doc, assets, centre, radius, Some((k, id))); // (roads' lanes as well as other objects': a crossing's end is put onto either. Their diff --git a/src/editor.rs b/src/editor.rs index b6dc110..6c5fb04 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -5455,11 +5455,11 @@ impl Editor { x.show_technical = self.show_technical || self.tool == Tool::Logic; x.hide = self.hide; // (the crossing being edited draws its own paths below, not the file's) - let draft = self.crossing_draft.as_ref().filter(|d| !d.2.is_empty()).map(|d| (d.0, d.1)); + let draft = self.crossing_draft.as_ref().map(|d| (d.0, d.1)); // the AI's lanes: arrows the way traffic goes, ends green where joined, red where not // (thin and small: a lane is a line on the road, not a road of its own) if self.tool == Tool::Logic || self.show_lanes { - let r = (cam.distance * 1.3).clamp(40.0, 500.0); + let r = (cam.distance * 2.5).clamp(100.0, 3500.0); let (lanes, ends) = crate::lanes::near(doc, assets, cam.target.truncate(), r); let lw = (cam.distance * 0.00048).clamp(0.012, 0.42) as f32; for l in lanes.iter().filter(|l| !(draft.is_some() && l.object == draft)) { @@ -5482,7 +5482,7 @@ impl Editor { } } // the crossing being edited: its paths as the window has them, on the map itself - if let (Some((k, id)), Some((_, _, paths))) = (draft, self.crossing_draft.as_ref()) { + if let Some((k, id, paths)) = self.crossing_draft.as_ref().map(|(k, id, p)| (*k, *id, p)) { if let Some(o) = doc.object(k, id) { let abs = assets.object(&o.file).map(|m| m.abs_height).unwrap_or(true); let world = doc.object_world(k, o, abs); @@ -5519,12 +5519,12 @@ impl Editor { // own paths are what stand out) if !(self.tool == Tool::Logic || self.show_lanes) { if let Some(o) = doc.object(k, id) { - let abs = assets.object(&o.file).map(|m| m.abs_height).unwrap_or(true); - let centre = doc.object_world(k, o, abs).truncate(); - let radius = crate::lanes::neighbour_reach(cam.distance); + let _abs = assets.object(&o.file).map(|m| m.abs_height).unwrap_or(true); + let centre = cam.target.truncate(); + let radius = crate::lanes::neighbour_reach(cam.distance).max(500.0); // (thin and pale: they are the crossing's surroundings, not what is edited) - let lw = (cam.distance * 0.00020).clamp(0.005, 0.13) as f32; - let lift = DVec3::Z * 0.1; + let lw = (cam.distance * 0.00045).clamp(0.02, 0.35) as f32; + let lift = DVec3::Z * 0.25; let (lanes, _) = crate::lanes::near(doc, assets, centre, radius); // (nearest first: a long spline used to fill the buffer before the roads // that actually meet the crossing were drawn) @@ -5544,11 +5544,11 @@ impl Editor { let Some((lo, hi, _)) = crate::lanes::visible_span(&l.pts, centre, radius) else { continue }; shown += 1; let color = match (l.kind, l.spline.is_some()) { - (1, _) => [0.35, 0.85, 1.0, 0.8], - (2, _) => [0.85, 0.55, 1.0, 0.8], + (1, _) => [0.35, 0.85, 1.0, 0.9], + (2, _) => [0.85, 0.55, 1.0, 0.9], // (a road's lane is a little paler than another object's) - (_, true) => [0.45, 0.85, 0.5, 0.55], - _ => [0.45, 0.95, 0.55, 0.8], + (_, true) => [0.38, 0.90, 0.48, 0.85], + _ => [0.38, 0.98, 0.48, 0.95], }; // (only the part inside the reach, and not every sample: a kilometre // of road sampled every 3 m used up the buffer on its own) diff --git a/src/lanes.rs b/src/lanes.rs index 310315e..37c65df 100644 --- a/src/lanes.rs +++ b/src/lanes.rs @@ -112,7 +112,7 @@ impl Lane { /// Zoomed in, still wide enough for the roads that meet a large junction; zoomed out, not /// the whole map (a map of lanes overflowed the graphics buffer). pub fn neighbour_reach(distance: f64) -> f64 { - (distance * 3.0).clamp(250.0, 700.0) + (distance * 3.0).clamp(300.0, 3500.0) } /// The samples of a lane that fall inside `radius` of `centre`, widened by one sample so the @@ -426,9 +426,10 @@ mod tests { #[test] fn neighbour_reach_stays_between_a_junction_and_the_map() { - assert_eq!(neighbour_reach(10.0), 250.0); + assert_eq!(neighbour_reach(10.0), 300.0); assert_eq!(neighbour_reach(100.0), 300.0); - assert_eq!(neighbour_reach(1_000.0), 700.0); + assert_eq!(neighbour_reach(1_000.0), 3000.0); + assert_eq!(neighbour_reach(2_000.0), 3500.0); } #[test] diff --git a/src/ui_crossing.rs b/src/ui_crossing.rs index 1daa09b..7655572 100644 --- a/src/ui_crossing.rs +++ b/src/ui_crossing.rs @@ -2366,7 +2366,7 @@ impl CrossingPaths { let m = crate::render::object_matrix(DVec3::ZERO, rot); // (the roads come up to the crossing's edge: a lane further out than this is passing // it, not ending at it) - let reach = (self.span() * 0.6 + 25.0).clamp(40.0, 220.0); + let reach = (self.span() * 0.6 + 50.0).clamp(40.0, 3500.0); let Some(assets) = assets else { return false }; let (lanes, ends) = crate::lanes::near_skipping(doc, assets, world.truncate(), reach, Some((k, id))); let roads = road_lane_ends(&lanes, &ends, world, &m, rot[0], doc); @@ -2448,7 +2448,16 @@ impl CrossingPaths { let inv = m.inverse(); // (the same reach the 3D view draws: a shorter one left green circles on the map // that no click could take, because they were never in this list) - let reach = ed.neighbour_reach(); + let mut max_ext: f64 = 0.0; + if let Some(sheet) = self.sheet.as_ref() { + for p in &sheet.crossing.paths { + max_ext = max_ext.max(p.start().truncate().length()).max(p.end().truncate().length()); + } + } + if let Some((_, _, b)) = &self.footprint { + max_ext = max_ext.max(b[0].abs().max(b[2].abs()).max(b[1].abs().max(b[3].abs()))); + } + let reach = ed.neighbour_reach().max(max_ext + 300.0).clamp(300.0, 3500.0); // (ends only: the lines between them are already drawn, and laying every one out // again just to find its two ends was the stall) let tips = crate::lanes::tips_skipping(doc, assets, world.truncate(), reach, Some((k, id))); @@ -2570,7 +2579,7 @@ impl CrossingPaths { if lo.x > hi.x { return 30.0; } - (hi - lo).length().clamp(6.0, 300.0) + (hi - lo).length().clamp(6.0, 4000.0) } /// The ends of the paths, three numbers each in the object's own frame (x, y, z and