From e68d48ea33e430afd61883280c38b8845d1c4d23 Mon Sep 17 00:00:00 2001 From: wabalabudabdab Date: Sat, 21 Mar 2026 07:44:33 +0000 Subject: [PATCH] fix(railways): respect layer tag and use ascending rails on terrain slopes Railways were always placed at y=0/y=1 regardless of the OSM `layer` tag, causing elevated railways (bridges, overpasses) to clip through terrain instead of floating above it. Additionally, when terrain is enabled, flat rail blocks were placed at every point without accounting for ground-level changes between consecutive nodes, leaving visible gaps where the terrain rises or falls. Changes: - Read `layer` tag and offset gravel/rail placement by `layer * LAYER_HEIGHT_STEP` (6 blocks per layer, matching highways) - Detect ground-level slope using `editor.get_ground_level()` at adjacent points; place `RAIL_ASCENDING_*` variants when climbing or descending - Fill the vertical gap in the gravel substrate when terrain rises steeply between consecutive track points Fixes #331 --- src/element_processing/railways.rs | 124 ++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 12 deletions(-) diff --git a/src/element_processing/railways.rs b/src/element_processing/railways.rs index eb2736249..141ab797b 100644 --- a/src/element_processing/railways.rs +++ b/src/element_processing/railways.rs @@ -3,6 +3,9 @@ use crate::bresenham::bresenham_line; use crate::osm_parser::ProcessedWay; use crate::world_editor::WorldEditor; +/// Number of blocks per OSM layer level (matches highway elevation step). +const LAYER_HEIGHT_STEP: i32 = 6; + pub fn generate_railways(editor: &mut WorldEditor, element: &ProcessedWay) { if let Some(railway_type) = element.tags.get("railway") { if [ @@ -30,6 +33,17 @@ pub fn generate_railways(editor: &mut WorldEditor, element: &ProcessedWay) { } } + // Respect the OSM `layer` tag so elevated railways (bridges, overpasses) + // are placed above ground level instead of clipping through it. + let layer_value: i32 = element + .tags + .get("layer") + .and_then(|l| l.parse::().ok()) + .unwrap_or(0) + .max(0); // underground (<0) is handled by the tunnel check above + + let layer_offset = layer_value * LAYER_HEIGHT_STEP; + for i in 1..element.nodes.len() { let prev_node = element.nodes[i - 1].xz(); let cur_node = element.nodes[i].xz(); @@ -40,35 +54,121 @@ pub fn generate_railways(editor: &mut WorldEditor, element: &ProcessedWay) { for j in 0..smoothed_points.len() { let (bx, _, bz) = smoothed_points[j]; - editor.set_block(GRAVEL, bx, 0, bz, None, None); + // Base offsets from ground level. + // When layer_offset > 0 the rail floats above terrain (bridge/overpass). + let gravel_y = layer_offset; + let rail_y = layer_offset + 1; + + // --- Terrain-slope detection (only for at-grade railways) --- + // When terrain is enabled and layer == 0 we try to place ascending + // rail variants so consecutive blocks stay visually connected even + // when the ground rises or falls by one block per step. + let prev_ground = if j > 0 { + let (px, _, pz) = smoothed_points[j - 1]; + editor.get_ground_level(px, pz) + } else { + editor.get_ground_level(bx, bz) + }; - let prev = if j > 0 { - Some(smoothed_points[j - 1]) + let next_ground = if j + 1 < smoothed_points.len() { + let (nx, _, nz) = smoothed_points[j + 1]; + editor.get_ground_level(nx, nz) + } else { + editor.get_ground_level(bx, bz) + }; + + let current_ground = editor.get_ground_level(bx, bz); + + // Fill the vertical gap under the rail when terrain rises steeply + // so there is always a solid gravel block supporting the track. + if layer_offset == 0 && prev_ground < current_ground { + for fill_y in prev_ground..current_ground { + editor.set_block_absolute(GRAVEL, bx, fill_y, bz, None, None); + } + } + + editor.set_block(GRAVEL, bx, gravel_y, bz, None, None); + + let prev_xz = if j > 0 { + let (px, _, pz) = smoothed_points[j - 1]; + Some((px, pz)) } else { None }; - let next = if j < smoothed_points.len() - 1 { - Some(smoothed_points[j + 1]) + let next_xz = if j + 1 < smoothed_points.len() { + let (nx, _, nz) = smoothed_points[j + 1]; + Some((nx, nz)) } else { None }; - let rail_block = determine_rail_direction( - (bx, bz), - prev.map(|(x, _, z)| (x, z)), - next.map(|(x, _, z)| (x, z)), - ); + let rail_block = if layer_offset == 0 { + determine_rail_with_slope( + (bx, bz), + prev_xz, + next_xz, + prev_ground, + current_ground, + next_ground, + ) + } else { + determine_rail_direction((bx, bz), prev_xz, next_xz) + }; - editor.set_block(rail_block, bx, 1, bz, None, None); + editor.set_block(rail_block, bx, rail_y, bz, None, None); if bx % 4 == 0 { - editor.set_block(OAK_LOG, bx, 0, bz, None, None); + editor.set_block(OAK_LOG, bx, gravel_y, bz, None, None); } } } } } +/// Choose between a flat or ascending rail block based on the ground-level +/// difference between the previous, current, and next track points. +fn determine_rail_with_slope( + current: (i32, i32), + prev: Option<(i32, i32)>, + next: Option<(i32, i32)>, + prev_ground: i32, + current_ground: i32, + next_ground: i32, +) -> Block { + // Ascending toward the *higher* neighbour. + if next_ground > current_ground { + if let Some((nx, nz)) = next { + return ascending_toward(current, (nx, nz)); + } + } + if prev_ground > current_ground { + if let Some((px, pz)) = prev { + return ascending_toward(current, (px, pz)); + } + } + // Flat section – fall back to standard direction logic. + determine_rail_direction(current, prev, next) +} + +/// Return the ascending rail variant that climbs from `from` toward `to`. +fn ascending_toward(from: (i32, i32), to: (i32, i32)) -> Block { + let (fx, fz) = from; + let (tx, tz) = to; + let dx = tx - fx; + let dz = tz - fz; + if dx.abs() >= dz.abs() { + if dx > 0 { + RAIL_ASCENDING_EAST + } else { + RAIL_ASCENDING_WEST + } + } else if dz < 0 { + RAIL_ASCENDING_NORTH + } else { + RAIL_ASCENDING_SOUTH + } +} + fn smooth_diagonal_rails(points: &[(i32, i32, i32)]) -> Vec<(i32, i32, i32)> { let mut smoothed = Vec::new();