diff --git a/src/data_processing.rs b/src/data_processing.rs index 137bef7..a5e54c8 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -67,6 +67,11 @@ pub fn generate_world_with_options( // Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate) let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox); + // Collect coordinates covered by tunnel=building_passage highways so that + // building generation can cut ground-level openings through walls and floors. + let building_passages = + highways::collect_building_passage_coords(&elements, &xzbbox, args.scale); + // Process all elements (no longer need to partition boundaries) let elements_count: usize = elements.len(); let process_pb: ProgressBar = ProgressBar::new(elements_count as u64); @@ -138,6 +143,7 @@ pub fn generate_world_with_options( None, None, &flood_fill_cache, + &building_passages, ); } } else if way.tags.contains_key("highway") { @@ -256,6 +262,7 @@ pub fn generate_world_with_options( args, &flood_fill_cache, &xzbbox, + &building_passages, ); } else if rel.tags.contains_key("water") || rel diff --git a/src/element_processing/buildings.rs b/src/element_processing/buildings.rs index 4816269..216938b 100644 --- a/src/element_processing/buildings.rs +++ b/src/element_processing/buildings.rs @@ -7,7 +7,7 @@ use crate::coordinate_system::cartesian::XZPoint; use crate::deterministic_rng::{coord_rng, element_rng}; use crate::element_processing::historic; use crate::element_processing::subprocessor::buildings_interior::generate_building_interior; -use crate::floodfill_cache::FloodFillCache; +use crate::floodfill_cache::{CoordinateBitmap, FloodFillCache}; use crate::osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay}; use crate::world_editor::WorldEditor; use fastnbt::Value; @@ -52,6 +52,11 @@ pub(crate) struct HolePolygon { // Building Style System // ============================================================================ +/// Height (in blocks above ground floor) of a building-passage archway. +/// Walls and floors below this height are removed at tunnel=building_passage +/// highway coordinates, creating a ground-level opening through the building. +const BUILDING_PASSAGE_HEIGHT: i32 = 4; + /// Accent block options for building decoration const ACCENT_BLOCK_OPTIONS: [Block; 6] = [ POLISHED_ANDESITE, @@ -1602,11 +1607,14 @@ fn build_wall_ring( config: &BuildingConfig, args: &Args, has_sloped_roof: bool, + building_passages: &CoordinateBitmap, ) -> (Vec<(i32, i32)>, (i32, i32, i32)) { let mut previous_node: Option<(i32, i32)> = None; let mut corner_addup: (i32, i32, i32) = (0, 0, 0); let mut current_building: Vec<(i32, i32)> = Vec::new(); + let passage_height = BUILDING_PASSAGE_HEIGHT.min(config.building_height); + for node in nodes { let x = node.x; let z = node.z; @@ -1622,8 +1630,11 @@ fn build_wall_ring( ); for (bx, _, bz) in bresenham_points { + let is_passage = building_passages.contains(bx, bz); + // Create foundation pillars when using terrain - if args.terrain && config.is_ground_level { + // Skip in passage zones so the road can pass through. + if args.terrain && config.is_ground_level && !is_passage { let local_ground_level = if let Some(ground) = editor.get_ground() { ground.level(XZPoint::new( bx - editor.get_min_coords().0, @@ -1645,10 +1656,17 @@ fn build_wall_ring( } } - // Generate wall blocks with windows - for h in - (config.start_y_offset + 1)..=(config.start_y_offset + config.building_height) - { + // Generate wall blocks with windows. + // In passage zones, skip below passage ceiling so the road + // can pass through; place a floor-block lintel at the top of + // the opening and continue the wall above. + let wall_start = if is_passage { + config.start_y_offset + passage_height + 1 + } else { + config.start_y_offset + 1 + }; + + for h in wall_start..=(config.start_y_offset + config.building_height) { let block = determine_wall_block_at_position(bx, h, bz, config); editor.set_block_absolute( block, @@ -1660,6 +1678,18 @@ fn build_wall_ring( ); } + // Place passage ceiling lintel + if is_passage && passage_height < config.building_height { + editor.set_block_absolute( + config.floor_block, + bx, + config.start_y_offset + passage_height + config.abs_terrain_offset, + bz, + None, + None, + ); + } + // Add roof line only for flat roofs, sloped roofs will cover this area if !has_sloped_roof { let roof_line_block = if config.use_accent_roof_line { @@ -1697,6 +1727,7 @@ fn generate_special_doors( element: &ProcessedWay, config: &BuildingConfig, wall_outline: &[(i32, i32)], + building_passages: &CoordinateBitmap, ) { if wall_outline.is_empty() { return; @@ -1738,6 +1769,13 @@ fn generate_special_doors( (mid_x, mid_z, mid_x, mid_z + 1) }; + // Skip placing doors inside a building passage + if building_passages.contains(door1_x, door1_z) + || building_passages.contains(door2_x, door2_z) + { + continue; + } + // Place the double door (lower and upper parts) // Use empty blacklist to overwrite existing wall blocks editor.set_block_absolute( @@ -1783,9 +1821,19 @@ fn generate_special_doors( let door_idx = rng.random_range(0..wall_outline.len()); let (door_x, door_z) = wall_outline[door_idx]; - // Place single oak door (empty blacklist to overwrite wall blocks) - editor.set_block_absolute(OAK_DOOR, door_x, door_y, door_z, None, Some(&[])); - editor.set_block_absolute(OAK_DOOR_UPPER, door_x, door_y + 1, door_z, None, Some(&[])); + // Skip placing a door inside a building passage + if !building_passages.contains(door_x, door_z) { + // Place single oak door (empty blacklist to overwrite wall blocks) + editor.set_block_absolute(OAK_DOOR, door_x, door_y, door_z, None, Some(&[])); + editor.set_block_absolute( + OAK_DOOR_UPPER, + door_x, + door_y + 1, + door_z, + None, + Some(&[]), + ); + } } } } @@ -2017,6 +2065,7 @@ fn generate_residential_window_decorations( editor: &mut WorldEditor, element: &ProcessedWay, config: &BuildingConfig, + building_passages: &CoordinateBitmap, ) { // Only non-tall residential / house buildings get decorations. if config.is_tall_building { @@ -2074,6 +2123,11 @@ fn generate_residential_window_decorations( let bx = *bx; let bz = *bz; + // Skip decorations at passage openings + if building_passages.contains(bx, bz) { + continue; + } + let mod6 = ((bx + bz) % 6 + 6) % 6; // always 0..5 // --- Shutters --- @@ -2350,6 +2404,7 @@ fn generate_corner_quoins( editor: &mut WorldEditor, element: &ProcessedWay, config: &BuildingConfig, + building_passages: &CoordinateBitmap, ) { // Skip if wall and accent are the same block (nothing visible) if config.wall_block == config.accent_block { @@ -2380,9 +2435,16 @@ fn generate_corner_quoins( let quoin_block = config.accent_block; let top_h = config.start_y_offset + config.building_height; + let passage_h = config.start_y_offset + BUILDING_PASSAGE_HEIGHT.min(config.building_height); for &(cx, cz) in &corners { - for h in (config.start_y_offset + 1)..=top_h { + let is_passage = building_passages.contains(cx, cz); + let start_h = if is_passage { + passage_h + 1 + } else { + config.start_y_offset + 1 + }; + for h in start_h..=top_h { editor.set_block_absolute( quoin_block, cx, @@ -2407,6 +2469,7 @@ fn generate_wall_depth_features( element: &ProcessedWay, config: &BuildingConfig, has_sloped_roof: bool, + building_passages: &CoordinateBitmap, ) { if config.wall_depth_style == WallDepthStyle::None { return; @@ -2479,6 +2542,12 @@ fn generate_wall_depth_features( let bx = *bx; let bz = *bz; + // Skip decorative features at passage openings — the road + // passes through here so no pilasters/buttresses/etc. + if building_passages.contains(bx, bz) { + continue; + } + let mod6 = ((bx + bz) % 6 + 6) % 6; match config.wall_depth_style { @@ -3108,6 +3177,7 @@ fn generate_floors_and_ceilings( config: &BuildingConfig, args: &Args, generate_non_flat_roof: bool, + building_passages: &CoordinateBitmap, ) -> HashSet<(i32, i32)> { let mut processed_points: HashSet<(i32, i32)> = HashSet::new(); let ceiling_light_block = if config.is_abandoned_building { @@ -3116,26 +3186,38 @@ fn generate_floors_and_ceilings( GLOWSTONE }; + let passage_height = BUILDING_PASSAGE_HEIGHT.min(config.building_height); + for &(x, z) in cached_floor_area { if !processed_points.insert((x, z)) { continue; } - // Set ground floor - editor.set_block_absolute( - config.floor_block, - x, - config.start_y_offset + config.abs_terrain_offset, - z, - None, - None, - ); + let is_passage = building_passages.contains(x, z); + + // Set ground floor — skip in passage zones (the road surface is placed + // by the highway processor instead). + if !is_passage { + editor.set_block_absolute( + config.floor_block, + x, + config.start_y_offset + config.abs_terrain_offset, + z, + None, + None, + ); + } // Set intermediate ceilings with light fixtures if config.building_height > 4 { for h in (config.start_y_offset + 2 + 4..config.start_y_offset + config.building_height) .step_by(4) { + // Skip intermediate ceilings below passage opening + if is_passage && h <= config.start_y_offset + passage_height { + continue; + } + let block = if x % 5 == 0 && z % 5 == 0 { ceiling_light_block } else { @@ -3143,8 +3225,8 @@ fn generate_floors_and_ceilings( }; editor.set_block_absolute(block, x, h + config.abs_terrain_offset, z, None, None); } - } else if x % 5 == 0 && z % 5 == 0 { - // Single floor building with ceiling light + } else if x % 5 == 0 && z % 5 == 0 && !is_passage { + // Single floor building with ceiling light (skip in passage) editor.set_block_absolute( ceiling_light_block, x, @@ -3155,6 +3237,18 @@ fn generate_floors_and_ceilings( ); } + // Place passage ceiling lintel at the top of the archway + if is_passage && passage_height < config.building_height { + editor.set_block_absolute( + config.floor_block, + x, + config.start_y_offset + passage_height + config.abs_terrain_offset, + z, + None, + None, + ); + } + // Set top ceiling (only if flat roof or no roof generation) // Use the resolved style flag, not just the OSM tag, since auto-gabled roofs // may be generated for residential buildings without a roof:shape tag @@ -3233,6 +3327,7 @@ pub fn generate_buildings( relation_levels: Option, hole_polygons: Option<&[HolePolygon]>, flood_fill_cache: &FloodFillCache, + building_passages: &CoordinateBitmap, ) { // Early return for underground buildings if should_skip_underground_building(element) { @@ -3443,35 +3538,48 @@ pub fn generate_buildings( // Generate walls, pass whether this building will have a sloped roof let has_sloped_roof = args.roof && style.generate_roof && style.roof_type != RoofType::Flat; - let (wall_outline, corner_addup) = - build_wall_ring(editor, &element.nodes, &config, args, has_sloped_roof); + let (wall_outline, corner_addup) = build_wall_ring( + editor, + &element.nodes, + &config, + args, + has_sloped_roof, + building_passages, + ); if let Some(holes) = hole_polygons { for hole in holes { if hole.add_walls { - let _ = build_wall_ring(editor, &hole.way.nodes, &config, args, has_sloped_roof); + let _ = build_wall_ring( + editor, + &hole.way.nodes, + &config, + args, + has_sloped_roof, + building_passages, + ); } } } // Generate special doors (garage doors, shed doors) if config.has_garage_door || config.has_single_door { - generate_special_doors(editor, element, &config, &wall_outline); + generate_special_doors(editor, element, &config, &wall_outline, building_passages); } // Add shutters and window boxes to small residential buildings - generate_residential_window_decorations(editor, element, &config); + generate_residential_window_decorations(editor, element, &config, building_passages); // Add wall depth features (pilasters, columns, ledges, cornices, buttresses) // Only for standalone buildings, not building:part sub-sections (parts adjoin // other parts and outward protrusions would collide with neighbours). if !element.tags.contains_key("building:part") { - generate_wall_depth_features(editor, element, &config, has_sloped_roof); + generate_wall_depth_features(editor, element, &config, has_sloped_roof, building_passages); } // Add corner quoins (accent-block columns at building corners) if !element.tags.contains_key("building:part") { - generate_corner_quoins(editor, element, &config); + generate_corner_quoins(editor, element, &config, building_passages); } // Create roof area = floor area + wall outline (so roof covers the walls too) @@ -3492,8 +3600,42 @@ pub fn generate_buildings( &config, args, style.generate_roof, + building_passages, ); + // Build tunnel side walls: for each interior coordinate that borders a + // passage coordinate, place a wall column from ground to passage ceiling. + // This creates the left/right corridor walls inside the archway. + if !building_passages.is_empty() { + let passage_height = + BUILDING_PASSAGE_HEIGHT.min(config.building_height); + let abs = config.abs_terrain_offset; + for &(x, z) in &cached_floor_area { + if building_passages.contains(x, z) { + continue; // this is road, not a wall + } + // Check 4-connected neighbours for passage adjacency + let adjacent_to_passage = building_passages.contains(x - 1, z) + || building_passages.contains(x + 1, z) + || building_passages.contains(x, z - 1) + || building_passages.contains(x, z + 1); + if adjacent_to_passage { + for y in (config.start_y_offset + 1) + ..=(config.start_y_offset + passage_height) + { + editor.set_block_absolute( + config.wall_block, + x, + y + abs, + z, + None, + None, + ); + } + } + } + } + // Generate interior features if args.interior { let skip_interior = matches!( @@ -3518,6 +3660,7 @@ pub fn generate_buildings( element, abs_terrain_offset, is_abandoned_building, + building_passages, ); } } @@ -5217,6 +5360,7 @@ pub fn generate_building_from_relation( args: &Args, flood_fill_cache: &FloodFillCache, xzbbox: &crate::coordinate_system::cartesian::XZBBox, + building_passages: &CoordinateBitmap, ) { // Skip underground buildings/building parts // Check layer tag @@ -5399,6 +5543,7 @@ pub fn generate_building_from_relation( Some(relation_levels), hole_polygons.as_deref(), flood_fill_cache, + building_passages, ); } } diff --git a/src/element_processing/highways.rs b/src/element_processing/highways.rs index a79c1dd..dc01837 100644 --- a/src/element_processing/highways.rs +++ b/src/element_processing/highways.rs @@ -1,8 +1,8 @@ use crate::args::Args; use crate::block_definitions::*; use crate::bresenham::bresenham_line; -use crate::coordinate_system::cartesian::XZPoint; -use crate::floodfill_cache::FloodFillCache; +use crate::coordinate_system::cartesian::{XZBBox, XZPoint}; +use crate::floodfill_cache::{CoordinateBitmap, FloodFillCache}; use crate::osm_parser::{ProcessedElement, ProcessedWay}; use crate::world_editor::WorldEditor; use std::collections::HashMap; @@ -936,3 +936,92 @@ pub fn generate_aeroway(editor: &mut WorldEditor, way: &ProcessedWay, args: &Arg previous_node = Some((node.x, node.z)); } } + +/// Returns the half-width (block_range) for a highway type. +/// +/// This extracts the same logic used inside `generate_highways_internal` so +/// that pre-scan passes (e.g. building-passage collection) can determine road +/// width without generating any blocks. +pub(crate) fn highway_block_range( + highway_type: &str, + tags: &HashMap, + scale: f64, +) -> i32 { + let mut block_range: i32 = match highway_type { + "footway" | "pedestrian" => 1, + "path" => 1, + "motorway" | "primary" | "trunk" => 5, + "secondary" => 4, + "tertiary" => 2, + "track" => 1, + "service" => 2, + "secondary_link" | "tertiary_link" => 1, + "escape" => 1, + "steps" => 1, + _ => { + if let Some(lanes) = tags.get("lanes") { + if lanes == "2" { + 3 + } else if lanes != "1" { + 4 + } else { + 2 + } + } else { + 2 + } + } + }; + + if scale < 1.0 { + block_range = ((block_range as f64) * scale).floor() as i32; + } + + block_range +} + +/// Collect all (x, z) coordinates covered by highways tagged +/// `tunnel=building_passage`. The returned bitmap can be passed into building +/// generation to cut ground-level openings through walls and floors. +pub fn collect_building_passage_coords( + elements: &[ProcessedElement], + xzbbox: &XZBBox, + scale: f64, +) -> CoordinateBitmap { + let mut bitmap = CoordinateBitmap::new(xzbbox); + + for element in elements { + let ProcessedElement::Way(way) = element else { + continue; + }; + + // Must be tunnel=building_passage + if way.tags.get("tunnel").map(|v| v.as_str()) != Some("building_passage") { + continue; + } + + // Must have a highway tag so we know the road width + let Some(highway_type) = way.tags.get("highway") else { + continue; + }; + + let block_range = highway_block_range(highway_type, &way.tags, scale); + + for i in 1..way.nodes.len() { + let prev = way.nodes[i - 1].xz(); + let cur = way.nodes[i].xz(); + + let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z); + + for (bx, _, bz) in &points { + for dx in -block_range..=block_range { + for dz in -block_range..=block_range { + bitmap.set(bx + dx, bz + dz); + } + } + } + } + } + + bitmap +} diff --git a/src/element_processing/subprocessor/buildings_interior.rs b/src/element_processing/subprocessor/buildings_interior.rs index 00351d1..29d9d83 100644 --- a/src/element_processing/subprocessor/buildings_interior.rs +++ b/src/element_processing/subprocessor/buildings_interior.rs @@ -1,4 +1,5 @@ use crate::block_definitions::*; +use crate::floodfill_cache::CoordinateBitmap; use crate::world_editor::WorldEditor; use std::collections::HashSet; @@ -291,6 +292,7 @@ pub fn generate_building_interior( element: &crate::osm_parser::ProcessedWay, abs_terrain_offset: i32, is_abandoned_building: bool, + building_passages: &CoordinateBitmap, ) { // Skip interior generation for very small buildings let width = max_x - min_x + 1; @@ -364,6 +366,14 @@ pub fn generate_building_interior( continue; } + // Skip interior blocks in building-passage zones on floors + // that fall within the archway opening. + if building_passages.contains(x, z) + && floor_y < start_y_offset + 4.min(building_height) + { + continue; + } + // Map the world coordinates to pattern coordinates using modulo // This creates a seamless tiling effect across the entire building // Add floor_index offset to create variation between floors