mirror of
https://github.com/louis-e/arnis.git
synced 2026-04-22 15:10:10 -04:00
- Make BUILDING_PASSAGE_HEIGHT pub(crate) and use it in buildings_interior.rs instead of hardcoded literal 4 (Copilot comment 4) - Add CoordinateBitmap::new_empty() zero-allocation constructor - Skip bitmap allocation in collect_building_passage_coords when no tunnel=building_passage ways exist (Copilot comment 5)
1041 lines
43 KiB
Rust
1041 lines
43 KiB
Rust
use crate::args::Args;
|
|
use crate::block_definitions::*;
|
|
use crate::bresenham::bresenham_line;
|
|
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;
|
|
|
|
/// Pick a road surface block deterministically based on coordinates.
|
|
/// Returns a random-looking mix of gray_concrete_powder and cyan_terracotta.
|
|
#[inline]
|
|
fn road_block(x: i32, z: i32) -> Block {
|
|
// Combine coordinates into a single value and apply bit mixing for a scattered look
|
|
let mut h = (x as u32).wrapping_mul(0x9E3779B9) ^ (z as u32).wrapping_mul(0x517CC1B7);
|
|
h ^= h >> 16;
|
|
h = h.wrapping_mul(0x45D9F3B);
|
|
h ^= h >> 16;
|
|
if h & 1 == 0 {
|
|
GRAY_CONCRETE_POWDER
|
|
} else {
|
|
CYAN_TERRACOTTA
|
|
}
|
|
}
|
|
|
|
/// Type alias for highway connectivity map
|
|
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
|
|
|
/// Minimum terrain dip (in blocks) below max endpoint elevation to classify a bridge as valley-spanning
|
|
const VALLEY_BRIDGE_THRESHOLD: i32 = 7;
|
|
|
|
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
|
pub fn generate_highways(
|
|
editor: &mut WorldEditor,
|
|
element: &ProcessedElement,
|
|
args: &Args,
|
|
highway_connectivity: &HighwayConnectivityMap,
|
|
flood_fill_cache: &FloodFillCache,
|
|
) {
|
|
generate_highways_internal(
|
|
editor,
|
|
element,
|
|
args,
|
|
highway_connectivity,
|
|
flood_fill_cache,
|
|
);
|
|
}
|
|
|
|
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
|
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
|
|
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
|
|
|
|
for element in elements {
|
|
if let ProcessedElement::Way(way) = element {
|
|
if way.tags.contains_key("highway") {
|
|
let layer_value = way
|
|
.tags
|
|
.get("layer")
|
|
.and_then(|layer| layer.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
// Treat negative layers as ground level (0) for connectivity
|
|
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
|
|
|
// Add connectivity for start and end nodes
|
|
if !way.nodes.is_empty() {
|
|
let start_node = &way.nodes[0];
|
|
let end_node = &way.nodes[way.nodes.len() - 1];
|
|
|
|
let start_coord = (start_node.x, start_node.z);
|
|
let end_coord = (end_node.x, end_node.z);
|
|
|
|
connectivity_map
|
|
.entry(start_coord)
|
|
.or_default()
|
|
.push(layer_value);
|
|
connectivity_map
|
|
.entry(end_coord)
|
|
.or_default()
|
|
.push(layer_value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
connectivity_map
|
|
}
|
|
|
|
/// Internal function that generates highways with connectivity context for elevation handling
|
|
fn generate_highways_internal(
|
|
editor: &mut WorldEditor,
|
|
element: &ProcessedElement,
|
|
args: &Args,
|
|
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>, // Maps node coordinates to list of layers that connect to this node
|
|
flood_fill_cache: &FloodFillCache,
|
|
) {
|
|
if let Some(highway_type) = element.tags().get("highway") {
|
|
if highway_type == "street_lamp" {
|
|
// Handle street lamps
|
|
if let ProcessedElement::Node(first_node) = element {
|
|
let x: i32 = first_node.x;
|
|
let z: i32 = first_node.z;
|
|
editor.set_block(COBBLESTONE_WALL, x, 1, z, None, None);
|
|
for dy in 2..=4 {
|
|
editor.set_block(OAK_FENCE, x, dy, z, None, None);
|
|
}
|
|
editor.set_block(GLOWSTONE, x, 5, z, None, None);
|
|
}
|
|
} else if highway_type == "crossing" {
|
|
// Handle traffic signals for crossings
|
|
if let Some(crossing_type) = element.tags().get("crossing") {
|
|
if crossing_type == "traffic_signals" {
|
|
if let ProcessedElement::Node(node) = element {
|
|
let x: i32 = node.x;
|
|
let z: i32 = node.z;
|
|
|
|
for dy in 1..=3 {
|
|
editor.set_block(COBBLESTONE_WALL, x, dy, z, None, None);
|
|
}
|
|
|
|
editor.set_block(GREEN_WOOL, x, 4, z, None, None);
|
|
editor.set_block(YELLOW_WOOL, x, 5, z, None, None);
|
|
editor.set_block(RED_WOOL, x, 6, z, None, None);
|
|
}
|
|
}
|
|
}
|
|
} else if highway_type == "bus_stop" {
|
|
// Handle bus stops
|
|
if let ProcessedElement::Node(node) = element {
|
|
let x = node.x;
|
|
let z = node.z;
|
|
for dy in 1..=3 {
|
|
editor.set_block(COBBLESTONE_WALL, x, dy, z, None, None);
|
|
}
|
|
|
|
editor.set_block(WHITE_WOOL, x, 4, z, None, None);
|
|
editor.set_block(WHITE_WOOL, x + 1, 4, z, None, None);
|
|
}
|
|
} else if element
|
|
.tags()
|
|
.get("area")
|
|
.is_some_and(|v: &String| v == "yes")
|
|
{
|
|
let ProcessedElement::Way(way) = element else {
|
|
return;
|
|
};
|
|
|
|
// Handle areas like pedestrian plazas
|
|
let mut surface_block: Block = STONE; // Default block
|
|
|
|
// Determine the block type based on the 'surface' tag
|
|
if let Some(surface) = element.tags().get("surface") {
|
|
surface_block = match surface.as_str() {
|
|
"paving_stones" | "sett" => STONE_BRICKS,
|
|
"bricks" => BRICK,
|
|
"wood" => OAK_PLANKS,
|
|
"asphalt" => BLACK_CONCRETE,
|
|
"gravel" | "fine_gravel" => GRAVEL,
|
|
"grass" => GRASS_BLOCK,
|
|
"dirt" | "ground" | "earth" => DIRT,
|
|
"sand" => SAND,
|
|
"concrete" => LIGHT_GRAY_CONCRETE,
|
|
_ => STONE, // Default to stone for unknown surfaces
|
|
};
|
|
}
|
|
|
|
// Fill the area using flood fill cache
|
|
let filled_area: Vec<(i32, i32)> =
|
|
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
|
|
|
for (x, z) in filled_area {
|
|
editor.set_block(surface_block, x, 0, z, None, None);
|
|
}
|
|
} else {
|
|
let mut previous_node: Option<(i32, i32)> = None;
|
|
let mut block_type = GRAY_CONCRETE_POWDER; // placeholder, overridden per-block by road_block() when use_road_mix is true
|
|
let mut use_road_mix = true;
|
|
let mut block_range: i32 = 2;
|
|
let mut add_stripe = false;
|
|
let mut add_outline = false;
|
|
let scale_factor = args.scale;
|
|
|
|
// Check if this is a bridge - bridges need special elevation handling
|
|
// to span across valleys instead of following terrain
|
|
// Accept any bridge tag value except "no" (e.g., "yes", "viaduct", "aqueduct", etc.)
|
|
// Indoor highways are never treated as bridges (indoor corridors should not
|
|
// generate elevated decks or support pillars).
|
|
let is_indoor = element.tags().get("indoor").is_some_and(|v| v == "yes");
|
|
let is_bridge = !is_indoor && element.tags().get("bridge").is_some_and(|v| v != "no");
|
|
|
|
// Parse the layer value for elevation calculation
|
|
let mut layer_value = element
|
|
.tags()
|
|
.get("layer")
|
|
.and_then(|layer| layer.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
// Treat negative layers as ground level (0)
|
|
if layer_value < 0 {
|
|
layer_value = 0;
|
|
}
|
|
|
|
// If the way is indoor, treat it as ground level to avoid creating
|
|
// bridges/supports inside buildings (indoor=yes should not produce bridges)
|
|
if is_indoor {
|
|
layer_value = 0;
|
|
}
|
|
|
|
// Skip if 'level' is negative in the tags (indoor mapping)
|
|
if let Some(level) = element.tags().get("level") {
|
|
if level.parse::<i32>().unwrap_or(0) < 0 {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Determine block type and range based on highway type
|
|
match highway_type.as_str() {
|
|
"footway" | "pedestrian" => {
|
|
block_type = GRAY_CONCRETE;
|
|
use_road_mix = false;
|
|
block_range = 1;
|
|
}
|
|
"path" => {
|
|
block_type = DIRT_PATH;
|
|
use_road_mix = false;
|
|
block_range = 1;
|
|
}
|
|
"motorway" | "primary" | "trunk" => {
|
|
block_range = 5;
|
|
add_stripe = true;
|
|
}
|
|
"secondary" => {
|
|
block_range = 4;
|
|
add_stripe = true;
|
|
}
|
|
"tertiary" => {
|
|
add_stripe = true;
|
|
}
|
|
"track" => {
|
|
block_range = 1;
|
|
}
|
|
"service" => {
|
|
block_type = GRAY_CONCRETE;
|
|
use_road_mix = false;
|
|
block_range = 2;
|
|
}
|
|
"secondary_link" | "tertiary_link" => {
|
|
//Exit ramps, sliproads
|
|
block_range = 1;
|
|
}
|
|
"escape" => {
|
|
// Sand trap for vehicles on mountainous roads
|
|
block_type = SAND;
|
|
use_road_mix = false;
|
|
block_range = 1;
|
|
}
|
|
"steps" => {
|
|
//TODO: Add correct stairs respecting height, step_count, etc.
|
|
block_type = GRAY_CONCRETE;
|
|
use_road_mix = false;
|
|
block_range = 1;
|
|
}
|
|
|
|
_ => {
|
|
if let Some(lanes) = element.tags().get("lanes") {
|
|
if lanes == "2" {
|
|
block_range = 3;
|
|
add_stripe = true;
|
|
add_outline = true;
|
|
} else if lanes != "1" {
|
|
block_range = 4;
|
|
add_stripe = true;
|
|
add_outline = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let ProcessedElement::Way(way) = element else {
|
|
return;
|
|
};
|
|
|
|
if scale_factor < 1.0 {
|
|
block_range = ((block_range as f64) * scale_factor).floor() as i32;
|
|
}
|
|
|
|
// Calculate elevation based on layer
|
|
const LAYER_HEIGHT_STEP: i32 = 6; // Each layer is 6 blocks higher/lower
|
|
let base_elevation = layer_value * LAYER_HEIGHT_STEP;
|
|
|
|
// Check if we need slopes at start and end
|
|
// This is used for overpasses that need ramps to ground-level roads
|
|
let needs_start_slope =
|
|
should_add_slope_at_node(&way.nodes[0], layer_value, highway_connectivity);
|
|
let needs_end_slope = should_add_slope_at_node(
|
|
&way.nodes[way.nodes.len() - 1],
|
|
layer_value,
|
|
highway_connectivity,
|
|
);
|
|
|
|
// Calculate total way length for slope distribution (needed before valley bridge check)
|
|
let total_way_length = calculate_way_length(way);
|
|
|
|
// For bridges: detect if this spans a valley by checking terrain profile
|
|
// A valley bridge has terrain that dips significantly below the endpoints
|
|
// Skip valley detection entirely if terrain is disabled (no valleys in flat terrain)
|
|
// Skip very short bridges (< 25 blocks) as they're unlikely to span significant valleys
|
|
let terrain_enabled = editor
|
|
.get_ground()
|
|
.map(|g| g.elevation_enabled)
|
|
.unwrap_or(false);
|
|
|
|
let (is_valley_bridge, bridge_deck_y) =
|
|
if is_bridge && terrain_enabled && way.nodes.len() >= 2 && total_way_length >= 25 {
|
|
let start_node = &way.nodes[0];
|
|
let end_node = &way.nodes[way.nodes.len() - 1];
|
|
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
|
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
|
let max_endpoint_y = start_y.max(end_y);
|
|
|
|
// Sample terrain at middle nodes only (excluding endpoints we already have)
|
|
// This avoids redundant get_ground_level() calls
|
|
let middle_nodes = &way.nodes[1..way.nodes.len().saturating_sub(1)];
|
|
let sampled_min = if middle_nodes.is_empty() {
|
|
// No middle nodes, just use endpoints
|
|
start_y.min(end_y)
|
|
} else {
|
|
// Sample up to 3 middle points (5 total with endpoints) for performance
|
|
// Valleys are wide terrain features, so sparse sampling is sufficient
|
|
let sample_count = middle_nodes.len().min(3);
|
|
let step = if sample_count > 1 {
|
|
(middle_nodes.len() - 1) / (sample_count - 1)
|
|
} else {
|
|
1
|
|
};
|
|
|
|
middle_nodes
|
|
.iter()
|
|
.step_by(step.max(1))
|
|
.map(|node| editor.get_ground_level(node.x, node.z))
|
|
.min()
|
|
.unwrap_or(max_endpoint_y)
|
|
};
|
|
|
|
// Include endpoint elevations in the minimum calculation
|
|
let min_terrain_y = sampled_min.min(start_y).min(end_y);
|
|
|
|
// If ANY sampled point along the bridge is significantly lower than the max endpoint,
|
|
// treat as valley bridge
|
|
let is_valley = min_terrain_y < max_endpoint_y - VALLEY_BRIDGE_THRESHOLD;
|
|
|
|
if is_valley {
|
|
(true, max_endpoint_y)
|
|
} else {
|
|
(false, 0)
|
|
}
|
|
} else {
|
|
(false, 0)
|
|
};
|
|
|
|
// Check if this is a short isolated elevated segment (layer > 0), if so, treat as ground level
|
|
let is_short_isolated_elevated =
|
|
needs_start_slope && needs_end_slope && layer_value > 0 && total_way_length <= 35;
|
|
|
|
// Override elevation and slopes for short isolated segments
|
|
let (effective_elevation, effective_start_slope, effective_end_slope) =
|
|
if is_short_isolated_elevated {
|
|
(0, false, false) // Treat as ground level
|
|
} else {
|
|
(base_elevation, needs_start_slope, needs_end_slope)
|
|
};
|
|
|
|
let slope_length = (total_way_length as f32 * 0.35).clamp(15.0, 50.0) as usize; // 35% of way length, max 50 blocks, min 15 blocks
|
|
|
|
// Check if this is a marked zebra crossing (only depends on tags, compute once)
|
|
let is_zebra_crossing = highway_type == "footway"
|
|
&& element.tags().get("footway").map(|s| s.as_str()) == Some("crossing")
|
|
&& !matches!(
|
|
element.tags().get("crossing").map(|s| s.as_str()),
|
|
Some("no" | "unmarked")
|
|
)
|
|
&& element.tags().get("crossing:markings").map(|s| s.as_str()) != Some("no");
|
|
|
|
// Iterate over nodes to create the highway
|
|
let mut segment_index = 0;
|
|
let total_segments = way.nodes.len() - 1;
|
|
|
|
for node in &way.nodes {
|
|
if let Some(prev) = previous_node {
|
|
let (x1, z1) = prev;
|
|
let x2: i32 = node.x;
|
|
let z2: i32 = node.z;
|
|
|
|
// Generate the line of coordinates between the two nodes
|
|
let bresenham_points: Vec<(i32, i32, i32)> =
|
|
bresenham_line(x1, 0, z1, x2, 0, z2);
|
|
|
|
// Calculate elevation for this segment
|
|
let segment_length = bresenham_points.len();
|
|
|
|
// Variables to manage dashed line pattern
|
|
let mut stripe_length: i32 = 0;
|
|
let dash_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
|
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
|
|
|
for (point_index, (x, _, z)) in bresenham_points.iter().enumerate() {
|
|
// Calculate Y elevation for this point
|
|
// For valley bridges: use fixed deck height (max of endpoints) to stay level
|
|
// For overpasses and regular roads: use terrain-relative elevation with slopes
|
|
let (current_y, use_absolute_y) = if is_valley_bridge {
|
|
// Valley bridge deck is level at the maximum endpoint elevation
|
|
// Don't add base_elevation - the layer tag indicates it's above water/road,
|
|
// not that it should be higher than the terrain endpoints
|
|
(bridge_deck_y, true)
|
|
} else {
|
|
// Regular road or overpass: use terrain-relative calculation with ramps
|
|
let y = calculate_point_elevation(
|
|
segment_index,
|
|
point_index,
|
|
segment_length,
|
|
total_segments,
|
|
effective_elevation,
|
|
effective_start_slope,
|
|
effective_end_slope,
|
|
slope_length,
|
|
);
|
|
(y, false)
|
|
};
|
|
|
|
// Draw the road surface for the entire width
|
|
for dx in -block_range..=block_range {
|
|
for dz in -block_range..=block_range {
|
|
let set_x: i32 = x + dx;
|
|
let set_z: i32 = z + dz;
|
|
|
|
// Zebra crossing logic
|
|
if is_zebra_crossing {
|
|
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
|
|
if is_horizontal {
|
|
if set_x % 2 < 1 {
|
|
if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
WHITE_CONCRETE,
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
Some(&[
|
|
BLACK_CONCRETE,
|
|
GRAY_CONCRETE_POWDER,
|
|
CYAN_TERRACOTTA,
|
|
]),
|
|
None,
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
WHITE_CONCRETE,
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
Some(&[
|
|
BLACK_CONCRETE,
|
|
GRAY_CONCRETE_POWDER,
|
|
CYAN_TERRACOTTA,
|
|
]),
|
|
None,
|
|
);
|
|
}
|
|
} else if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
road_block(set_x, set_z),
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
None,
|
|
None,
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
road_block(set_x, set_z),
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
None,
|
|
None,
|
|
);
|
|
}
|
|
} else if set_z % 2 < 1 {
|
|
if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
WHITE_CONCRETE,
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
Some(&[GRAY_CONCRETE_POWDER, CYAN_TERRACOTTA]),
|
|
None,
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
WHITE_CONCRETE,
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
Some(&[GRAY_CONCRETE_POWDER, CYAN_TERRACOTTA]),
|
|
None,
|
|
);
|
|
}
|
|
} else if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
road_block(set_x, set_z),
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
None,
|
|
None,
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
road_block(set_x, set_z),
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
None,
|
|
None,
|
|
);
|
|
}
|
|
} else {
|
|
let effective_block = if use_road_mix {
|
|
road_block(set_x, set_z)
|
|
} else {
|
|
block_type
|
|
};
|
|
if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
effective_block,
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
None,
|
|
Some(&[
|
|
BLACK_CONCRETE,
|
|
GRAY_CONCRETE_POWDER,
|
|
CYAN_TERRACOTTA,
|
|
WHITE_CONCRETE,
|
|
]),
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
effective_block,
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
None,
|
|
Some(&[
|
|
BLACK_CONCRETE,
|
|
GRAY_CONCRETE_POWDER,
|
|
CYAN_TERRACOTTA,
|
|
WHITE_CONCRETE,
|
|
]),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add stone brick foundation underneath elevated highways/bridges for thickness
|
|
if (effective_elevation > 0 || use_absolute_y) && current_y > 0 {
|
|
// Add 1 layer of stone bricks underneath the highway surface
|
|
if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
STONE_BRICKS,
|
|
set_x,
|
|
current_y - 1,
|
|
set_z,
|
|
None,
|
|
None,
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
STONE_BRICKS,
|
|
set_x,
|
|
current_y - 1,
|
|
set_z,
|
|
None,
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add support pillars for elevated highways/bridges
|
|
if (effective_elevation != 0 || use_absolute_y) && current_y > 0 {
|
|
if use_absolute_y {
|
|
add_highway_support_pillar_absolute(
|
|
editor,
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
dx,
|
|
dz,
|
|
block_range,
|
|
);
|
|
} else {
|
|
add_highway_support_pillar(
|
|
editor,
|
|
set_x,
|
|
current_y,
|
|
set_z,
|
|
dx,
|
|
dz,
|
|
block_range,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add light gray concrete outline for multi-lane roads
|
|
if add_outline {
|
|
// Left outline
|
|
for dz in -block_range..=block_range {
|
|
let outline_x = x - block_range - 1;
|
|
let outline_z = z + dz;
|
|
if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
LIGHT_GRAY_CONCRETE,
|
|
outline_x,
|
|
current_y,
|
|
outline_z,
|
|
None,
|
|
None,
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
LIGHT_GRAY_CONCRETE,
|
|
outline_x,
|
|
current_y,
|
|
outline_z,
|
|
None,
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
// Right outline
|
|
for dz in -block_range..=block_range {
|
|
let outline_x = x + block_range + 1;
|
|
let outline_z = z + dz;
|
|
if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
LIGHT_GRAY_CONCRETE,
|
|
outline_x,
|
|
current_y,
|
|
outline_z,
|
|
None,
|
|
None,
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
LIGHT_GRAY_CONCRETE,
|
|
outline_x,
|
|
current_y,
|
|
outline_z,
|
|
None,
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add a dashed white line in the middle for larger roads
|
|
if add_stripe {
|
|
if stripe_length < dash_length {
|
|
let stripe_x: i32 = *x;
|
|
let stripe_z: i32 = *z;
|
|
if use_absolute_y {
|
|
editor.set_block_absolute(
|
|
WHITE_CONCRETE,
|
|
stripe_x,
|
|
current_y,
|
|
stripe_z,
|
|
Some(&[GRAY_CONCRETE_POWDER, CYAN_TERRACOTTA]),
|
|
None,
|
|
);
|
|
} else {
|
|
editor.set_block(
|
|
WHITE_CONCRETE,
|
|
stripe_x,
|
|
current_y,
|
|
stripe_z,
|
|
Some(&[GRAY_CONCRETE_POWDER, CYAN_TERRACOTTA]),
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Increment stripe_length and reset after completing a dash and gap
|
|
stripe_length += 1;
|
|
if stripe_length >= dash_length + gap_length {
|
|
stripe_length = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
segment_index += 1;
|
|
}
|
|
previous_node = Some((node.x, node.z));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper function to determine if a slope should be added at a specific node
|
|
fn should_add_slope_at_node(
|
|
node: &crate::osm_parser::ProcessedNode,
|
|
current_layer: i32,
|
|
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>,
|
|
) -> bool {
|
|
let node_coord = (node.x, node.z);
|
|
|
|
// If we don't have connectivity information, always add slopes for non-zero layers
|
|
if highway_connectivity.is_empty() {
|
|
return current_layer != 0;
|
|
}
|
|
|
|
// Check if there are other highways at different layers connected to this node
|
|
if let Some(connected_layers) = highway_connectivity.get(&node_coord) {
|
|
// Count how many ways are at the same layer as current way
|
|
let same_layer_count = connected_layers
|
|
.iter()
|
|
.filter(|&&layer| layer == current_layer)
|
|
.count();
|
|
|
|
// If this is the only way at this layer connecting to this node, we need a slope
|
|
// (unless we're at ground level and connecting to ground level ways)
|
|
if same_layer_count <= 1 {
|
|
return current_layer != 0;
|
|
}
|
|
|
|
// If there are multiple ways at the same layer, don't add slope
|
|
false
|
|
} else {
|
|
// No other highways connected, add slope if not at ground level
|
|
current_layer != 0
|
|
}
|
|
}
|
|
|
|
/// Helper function to calculate the total length of a way in blocks
|
|
fn calculate_way_length(way: &ProcessedWay) -> usize {
|
|
let mut total_length = 0;
|
|
let mut previous_node: Option<&crate::osm_parser::ProcessedNode> = None;
|
|
|
|
for node in &way.nodes {
|
|
if let Some(prev) = previous_node {
|
|
let dx = (node.x - prev.x).abs();
|
|
let dz = (node.z - prev.z).abs();
|
|
total_length += ((dx * dx + dz * dz) as f32).sqrt() as usize;
|
|
}
|
|
previous_node = Some(node);
|
|
}
|
|
|
|
total_length
|
|
}
|
|
|
|
/// Calculate the Y elevation for a specific point along the highway
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn calculate_point_elevation(
|
|
segment_index: usize,
|
|
point_index: usize,
|
|
segment_length: usize,
|
|
total_segments: usize,
|
|
base_elevation: i32,
|
|
needs_start_slope: bool,
|
|
needs_end_slope: bool,
|
|
slope_length: usize,
|
|
) -> i32 {
|
|
// If no slopes needed, return base elevation
|
|
if !needs_start_slope && !needs_end_slope {
|
|
return base_elevation;
|
|
}
|
|
|
|
// Calculate total distance from start
|
|
let total_distance_from_start = segment_index * segment_length + point_index;
|
|
let total_way_length = total_segments * segment_length;
|
|
|
|
// Ensure we have reasonable values
|
|
if total_way_length == 0 || slope_length == 0 {
|
|
return base_elevation;
|
|
}
|
|
|
|
// Start slope calculation - gradual rise from ground level
|
|
if needs_start_slope && total_distance_from_start <= slope_length {
|
|
let slope_progress = total_distance_from_start as f32 / slope_length as f32;
|
|
let elevation_offset = (base_elevation as f32 * slope_progress) as i32;
|
|
return elevation_offset;
|
|
}
|
|
|
|
// End slope calculation - gradual descent to ground level
|
|
if needs_end_slope
|
|
&& total_distance_from_start >= (total_way_length.saturating_sub(slope_length))
|
|
{
|
|
let distance_from_end = total_way_length - total_distance_from_start;
|
|
let slope_progress = distance_from_end as f32 / slope_length as f32;
|
|
let elevation_offset = (base_elevation as f32 * slope_progress) as i32;
|
|
return elevation_offset;
|
|
}
|
|
|
|
// Middle section at full elevation
|
|
base_elevation
|
|
}
|
|
|
|
/// Add support pillars for elevated highways
|
|
fn add_highway_support_pillar(
|
|
editor: &mut WorldEditor,
|
|
x: i32,
|
|
highway_y: i32,
|
|
z: i32,
|
|
dx: i32,
|
|
dz: i32,
|
|
_block_range: i32, // Keep for future use
|
|
) {
|
|
// Only add pillars at specific intervals and positions
|
|
if dx == 0 && dz == 0 && (x + z) % 8 == 0 {
|
|
// Add pillar from ground to highway level
|
|
for y in 1..highway_y {
|
|
editor.set_block(STONE_BRICKS, x, y, z, None, None);
|
|
}
|
|
|
|
// Add pillar base
|
|
for base_dx in -1..=1 {
|
|
for base_dz in -1..=1 {
|
|
editor.set_block(STONE_BRICKS, x + base_dx, 0, z + base_dz, None, None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Add support pillars for bridges using absolute Y coordinates
|
|
/// Pillars extend from ground level up to the bridge deck
|
|
fn add_highway_support_pillar_absolute(
|
|
editor: &mut WorldEditor,
|
|
x: i32,
|
|
bridge_deck_y: i32,
|
|
z: i32,
|
|
dx: i32,
|
|
dz: i32,
|
|
_block_range: i32, // Keep for future use
|
|
) {
|
|
// Only add pillars at specific intervals and positions
|
|
if dx == 0 && dz == 0 && (x + z) % 8 == 0 {
|
|
// Get the actual ground level at this position
|
|
let ground_y = editor.get_ground_level(x, z);
|
|
|
|
// Add pillar from ground up to bridge deck
|
|
// Only if the bridge is actually above the ground
|
|
if bridge_deck_y > ground_y {
|
|
for y in (ground_y + 1)..bridge_deck_y {
|
|
editor.set_block_absolute(STONE_BRICKS, x, y, z, None, None);
|
|
}
|
|
|
|
// Add pillar base at ground level
|
|
for base_dx in -1..=1 {
|
|
for base_dz in -1..=1 {
|
|
editor.set_block_absolute(
|
|
STONE_BRICKS,
|
|
x + base_dx,
|
|
ground_y,
|
|
z + base_dz,
|
|
None,
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generates a siding using stone brick slabs
|
|
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay) {
|
|
let mut previous_node: Option<XZPoint> = None;
|
|
let siding_block: Block = STONE_BRICK_SLAB;
|
|
|
|
for node in &element.nodes {
|
|
let current_node = node.xz();
|
|
|
|
// Draw the siding using Bresenham's line algorithm between nodes
|
|
if let Some(prev_node) = previous_node {
|
|
let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(
|
|
prev_node.x,
|
|
0,
|
|
prev_node.z,
|
|
current_node.x,
|
|
0,
|
|
current_node.z,
|
|
);
|
|
|
|
for (bx, _, bz) in bresenham_points {
|
|
if !editor.check_for_block(
|
|
bx,
|
|
0,
|
|
bz,
|
|
Some(&[
|
|
BLACK_CONCRETE,
|
|
GRAY_CONCRETE_POWDER,
|
|
CYAN_TERRACOTTA,
|
|
WHITE_CONCRETE,
|
|
]),
|
|
) {
|
|
editor.set_block(siding_block, bx, 1, bz, None, None);
|
|
}
|
|
}
|
|
}
|
|
|
|
previous_node = Some(current_node);
|
|
}
|
|
}
|
|
|
|
/// Generates an aeroway
|
|
pub fn generate_aeroway(editor: &mut WorldEditor, way: &ProcessedWay, args: &Args) {
|
|
let mut previous_node: Option<(i32, i32)> = None;
|
|
let surface_block = LIGHT_GRAY_CONCRETE;
|
|
|
|
for node in &way.nodes {
|
|
if let Some(prev) = previous_node {
|
|
let (x1, z1) = prev;
|
|
let x2 = node.x;
|
|
let z2 = node.z;
|
|
let points = bresenham_line(x1, 0, z1, x2, 0, z2);
|
|
let way_width: i32 = (12.0 * args.scale).ceil() as i32;
|
|
|
|
for (x, _, z) in points {
|
|
for dx in -way_width..=way_width {
|
|
for dz in -way_width..=way_width {
|
|
let set_x = x + dx;
|
|
let set_z = z + dz;
|
|
editor.set_block(surface_block, set_x, 0, set_z, None, None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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<String, String>,
|
|
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 {
|
|
// Quick scan: skip bitmap allocation entirely when there are no passage ways.
|
|
let has_any = elements.iter().any(|e| {
|
|
if let ProcessedElement::Way(w) = e {
|
|
w.tags.get("tunnel").map(|v| v.as_str()) == Some("building_passage")
|
|
&& w.tags.contains_key("highway")
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
if !has_any {
|
|
return CoordinateBitmap::new_empty();
|
|
}
|
|
|
|
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
|
|
}
|