Compare commits

...

21 Commits

Author SHA1 Message Date
louis-e
7c808ec352 Address code review feedback 2026-01-10 18:36:43 +01:00
louis-e
b757c5acf4 Better icons and map overlay fix 2026-01-10 17:56:37 +01:00
louis-e
ced5fc274e More landuse variations 2026-01-10 17:27:10 +01:00
Louis Erbkamm
295ca415d7 Merge pull request #660 from louis-e/copilot/fix-spawn-point-y-coordinate
Fix spawn point Y coordinate update for longitude values outside ±90°
2026-01-10 13:20:46 +01:00
Louis Erbkamm
e2b4ca8bdb Merge branch 'main' into copilot/fix-spawn-point-y-coordinate 2026-01-10 13:15:04 +01:00
Louis Erbkamm
07105f0208 Merge pull request #703 from louis-e/no-internet-warning
Send proper error for no internet
2026-01-08 23:36:36 +01:00
louis-e
ad57fdbc3a Send proper error for no internet 2026-01-08 23:32:39 +01:00
Louis Erbkamm
550870d9e0 Merge pull request #702 from louis-e/better-elevation
Implement more realistic elevation
2026-01-08 23:14:22 +01:00
louis-e
bd693ea007 Reduce log lines 2026-01-08 23:13:46 +01:00
louis-e
ce8f343414 Sample less points in bridge calc 2026-01-08 23:01:12 +01:00
louis-e
f882145780 Improve efficiency of ground generation 2026-01-08 22:42:06 +01:00
louis-e
b52d750935 Address code review feedback 2026-01-08 22:20:17 +01:00
louis-e
4d30899909 Remove snow again 2026-01-08 21:23:18 +01:00
louis-e
311610a717 Use less operations for better efficiency 2026-01-08 20:53:00 +01:00
louis-e
b4902ebc9e Add snow on top of mountains and address code review feedback 2026-01-08 20:39:56 +01:00
louis-e
e5bbb3e4a0 Address code review feedback 2026-01-08 20:03:15 +01:00
louis-e
0238cfe2d0 Implement more realistic elevation 2026-01-08 19:52:03 +01:00
copilot-swe-agent[bot]
0a51b302ee Simplify bbox format comment
Co-authored-by: louis-e <44675238+louis-e@users.noreply.github.com>
2025-12-07 14:22:50 +00:00
copilot-swe-agent[bot]
93dc9f446c Improve comment explaining bbox format conversion
Co-authored-by: louis-e <44675238+louis-e@users.noreply.github.com>
2025-12-07 14:17:56 +00:00
copilot-swe-agent[bot]
e6430f2a04 Fix spawn point Y coordinate bbox format
Co-authored-by: louis-e <44675238+louis-e@users.noreply.github.com>
2025-12-07 14:11:36 +00:00
copilot-swe-agent[bot]
5962decf44 Initial plan 2025-12-07 13:58:19 +00:00
15 changed files with 583 additions and 152 deletions

View File

@@ -262,7 +262,8 @@ pub fn generate_world_with_options(
let total_iterations_grnd: f64 = total_blocks as f64;
let progress_increment_grnd: f64 = 20.0 / total_iterations_grnd;
let groundlayer_block = GRASS_BLOCK;
// Check if terrain elevation is enabled; when disabled, we can skip ground level lookups entirely
let terrain_enabled = ground.elevation_enabled;
// Process ground generation chunk-by-chunk for better cache locality.
// This keeps the same region/chunk HashMap entries hot in CPU cache,
@@ -282,11 +283,19 @@ pub fn generate_world_with_options(
for x in chunk_min_x..=chunk_max_x {
for z in chunk_min_z..=chunk_max_z {
// Get ground level, when terrain is enabled, look it up once per block
// When disabled, use constant ground_level (no function call overhead)
let ground_y = if terrain_enabled {
editor.get_ground_level(x, z)
} else {
args.ground_level
};
// Add default dirt and grass layer if there isn't a stone layer already
if !editor.check_for_block(x, 0, z, Some(&[STONE])) {
editor.set_block(groundlayer_block, x, 0, z, None, None);
editor.set_block(DIRT, x, -1, z, None, None);
editor.set_block(DIRT, x, -2, z, None, None);
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
}
// Fill underground with stone
@@ -298,7 +307,7 @@ pub fn generate_world_with_options(
MIN_Y + 1,
z,
x,
editor.get_absolute_y(x, -3, z),
ground_y - 3,
z,
None,
None,
@@ -348,12 +357,14 @@ pub fn generate_world_with_options(
if world_format == WorldFormat::JavaAnvil {
if let Some(spawn_coords) = &args.spawn_point {
use crate::gui::update_player_spawn_y_after_generation;
// Reconstruct bbox string to match the format that GUI originally provided.
// This ensures LLBBox::from_str() can parse it correctly.
let bbox_string = format!(
"{},{},{},{}",
args.bbox.min().lng(),
args.bbox.min().lat(),
args.bbox.max().lng(),
args.bbox.max().lat()
args.bbox.min().lng(),
args.bbox.max().lat(),
args.bbox.max().lng()
);
if let Err(e) = update_player_spawn_y_after_generation(

View File

@@ -3,37 +3,97 @@ use crate::bresenham::bresenham_line;
use crate::osm_parser::ProcessedWay;
use crate::world_editor::WorldEditor;
// TODO FIX
// TODO FIX - This handles ways with bridge=yes tag (e.g., highway bridges)
#[allow(dead_code)]
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedWay) {
if let Some(_bridge_type) = element.tags.get("bridge") {
let bridge_height = 3; // Fixed height
let bridge_height = 3; // Height above the ground level
// Get start and end node elevations and use MAX for level bridge deck
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
let bridge_deck_ground_y = if element.nodes.len() >= 2 {
let start_node = &element.nodes[0];
let end_node = &element.nodes[element.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);
start_y.max(end_y)
} else {
return; // Need at least 2 nodes for a bridge
};
// Calculate total bridge length for ramp positioning
let total_length: f64 = element
.nodes
.windows(2)
.map(|pair| {
let dx = (pair[1].x - pair[0].x) as f64;
let dz = (pair[1].z - pair[0].z) as f64;
(dx * dx + dz * dz).sqrt()
})
.sum();
if total_length == 0.0 {
return;
}
let mut accumulated_length: f64 = 0.0;
for i in 1..element.nodes.len() {
let prev = &element.nodes[i - 1];
let cur = &element.nodes[i];
let segment_dx = (cur.x - prev.x) as f64;
let segment_dz = (cur.z - prev.z) as f64;
let segment_length = (segment_dx * segment_dx + segment_dz * segment_dz).sqrt();
let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z);
let total_length = points.len();
let ramp_length = 6; // Length of ramp at each end
let ramp_length = (total_length * 0.15).clamp(6.0, 20.0) as usize; // 15% of bridge, min 6, max 20 blocks
for (idx, (x, _, z)) in points.iter().enumerate() {
let height = if idx < ramp_length {
// Calculate progress along this segment
let segment_progress = if points.len() > 1 {
idx as f64 / (points.len() - 1) as f64
} else {
0.0
};
// Calculate overall progress along the entire bridge
let point_distance = accumulated_length + segment_progress * segment_length;
let overall_progress = (point_distance / total_length).clamp(0.0, 1.0);
let total_len_usize = total_length as usize;
let overall_idx = (overall_progress * total_len_usize as f64) as usize;
// Calculate ramp height offset
let ramp_offset = if overall_idx < ramp_length {
// Start ramp (rising)
(idx * bridge_height) / ramp_length
} else if idx >= total_length - ramp_length {
(overall_idx as f64 * bridge_height as f64 / ramp_length as f64) as i32
} else if overall_idx >= total_len_usize.saturating_sub(ramp_length) {
// End ramp (descending)
((total_length - idx) * bridge_height) / ramp_length
let dist_from_end = total_len_usize - overall_idx;
(dist_from_end as f64 * bridge_height as f64 / ramp_length as f64) as i32
} else {
// Middle section (constant height)
bridge_height
};
// Use fixed bridge deck height (max of endpoints) plus ramp offset
let bridge_y = bridge_deck_ground_y + ramp_offset;
// Place bridge blocks
for dx in -2..=2 {
editor.set_block(LIGHT_GRAY_CONCRETE, *x + dx, height as i32, *z, None, None);
editor.set_block_absolute(
LIGHT_GRAY_CONCRETE,
*x + dx,
bridge_y,
*z,
None,
None,
);
}
}
accumulated_length += segment_length;
}
}
}

View File

@@ -1525,6 +1525,8 @@ pub fn generate_building_from_relation(
}
/// Generates a bridge structure, paying attention to the "level" tag.
/// Bridge deck is interpolated between start and end point elevations to avoid
/// being dragged down by valleys underneath.
fn generate_bridge(
editor: &mut WorldEditor,
element: &ProcessedWay,
@@ -1534,7 +1536,7 @@ fn generate_bridge(
let floor_block: Block = STONE;
let railing_block: Block = STONE_BRICKS;
// Calculate bridge level based on the "level" tag (computed once, used throughout)
// Calculate bridge level offset based on the "level" tag
let bridge_y_offset = if let Some(level_str) = element.tags.get("level") {
if let Ok(level) = level_str.parse::<i32>() {
(level * 3) + 1
@@ -1545,21 +1547,37 @@ fn generate_bridge(
1 // Default elevation
};
// Need at least 2 nodes to form a bridge
if element.nodes.len() < 2 {
return;
}
// Get start and end node elevations and use MAX for level bridge deck
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
let start_node = &element.nodes[0];
let end_node = &element.nodes[element.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 bridge_deck_ground_y = start_y.max(end_y);
// Process the nodes to create bridge pathways and railings
let mut previous_node: Option<(i32, i32)> = None;
for node in &element.nodes {
let x: i32 = node.x;
let z: i32 = node.z;
// Create bridge path using Bresenham's line
if let Some(prev) = previous_node {
let bridge_points: Vec<(i32, i32, i32)> =
bresenham_line(prev.0, bridge_y_offset, prev.1, x, bridge_y_offset, z);
let bridge_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, 0, prev.1, x, 0, z);
for (bx, _, bz) in bridge_points.iter() {
// Use fixed bridge deck height (max of endpoints)
let bridge_y = bridge_deck_ground_y + bridge_y_offset;
for (bx, by, bz) in bridge_points {
// Place railing blocks
editor.set_block(railing_block, bx, by + 1, bz, None, None);
editor.set_block(railing_block, bx, by, bz, None, None);
editor.set_block_absolute(railing_block, *bx, bridge_y + 1, *bz, None, None);
editor.set_block_absolute(railing_block, *bx, bridge_y, *bz, None, None);
}
}
@@ -1569,8 +1587,11 @@ fn generate_bridge(
// Flood fill the area between the bridge path nodes (uses cache)
let bridge_area: Vec<(i32, i32)> = flood_fill_cache.get_or_compute(element, floodfill_timeout);
// Use the same level bridge deck height for filled areas
let floor_y = bridge_deck_ground_y + bridge_y_offset;
// Place floor blocks
for (x, z) in bridge_area {
editor.set_block(floor_block, x, bridge_y_offset, z, None, None);
editor.set_block_absolute(floor_block, x, floor_y, z, None, None);
}
}

View File

@@ -10,6 +10,9 @@ use std::collections::HashMap;
/// 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,
@@ -160,6 +163,11 @@ fn generate_highways_internal(
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.)
let is_bridge = element.tags().get("bridge").is_some_and(|v| v != "no");
// Parse the layer value for elevation calculation
let layer_value = element
.tags()
@@ -249,6 +257,7 @@ fn generate_highways_internal(
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(
@@ -257,10 +266,67 @@ fn generate_highways_internal(
highway_connectivity,
);
// Calculate total way length for slope distribution
// Calculate total way length for slope distribution (needed before valley bridge check)
let total_way_length = calculate_way_length(way);
// Check if this is a short isolated elevated segment - if so, treat as ground level
// 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;
@@ -297,17 +363,28 @@ fn generate_highways_internal(
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 based on slopes and layer
let current_y = calculate_point_elevation(
segment_index,
point_index,
segment_length,
total_segments,
effective_elevation,
effective_start_slope,
effective_end_slope,
slope_length,
);
// 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 {
@@ -323,12 +400,32 @@ fn generate_highways_internal(
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
if is_horizontal {
if set_x % 2 < 1 {
editor.set_block(
WHITE_CONCRETE,
if use_absolute_y {
editor.set_block_absolute(
WHITE_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
);
} else {
editor.set_block(
WHITE_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
);
}
} else if use_absolute_y {
editor.set_block_absolute(
BLACK_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
None,
);
} else {
@@ -342,12 +439,32 @@ fn generate_highways_internal(
);
}
} else if set_z % 2 < 1 {
editor.set_block(
WHITE_CONCRETE,
if use_absolute_y {
editor.set_block_absolute(
WHITE_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
);
} else {
editor.set_block(
WHITE_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
);
}
} else if use_absolute_y {
editor.set_block_absolute(
BLACK_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
None,
);
} else {
@@ -360,6 +477,15 @@ fn generate_highways_internal(
None,
);
}
} else if use_absolute_y {
editor.set_block_absolute(
block_type,
set_x,
current_y,
set_z,
None,
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
);
} else {
editor.set_block(
block_type,
@@ -371,30 +497,53 @@ fn generate_highways_internal(
);
}
// Add stone brick foundation underneath elevated highways for thickness
if effective_elevation > 0 && current_y > 0 {
// 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
editor.set_block(
STONE_BRICKS,
set_x,
current_y - 1,
set_z,
None,
None,
);
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
if effective_elevation != 0 && current_y > 0 {
add_highway_support_pillar(
editor,
set_x,
current_y,
set_z,
dx,
dz,
block_range,
);
// 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,
);
}
}
}
}
@@ -405,27 +554,49 @@ fn generate_highways_internal(
for dz in -block_range..=block_range {
let outline_x = x - block_range - 1;
let outline_z = z + dz;
editor.set_block(
LIGHT_GRAY_CONCRETE,
outline_x,
current_y,
outline_z,
None,
None,
);
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;
editor.set_block(
LIGHT_GRAY_CONCRETE,
outline_x,
current_y,
outline_z,
None,
None,
);
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,
);
}
}
}
@@ -434,14 +605,25 @@ fn generate_highways_internal(
if stripe_length < dash_length {
let stripe_x: i32 = *x;
let stripe_z: i32 = *z;
editor.set_block(
WHITE_CONCRETE,
stripe_x,
current_y,
stripe_z,
Some(&[BLACK_CONCRETE]),
None,
);
if use_absolute_y {
editor.set_block_absolute(
WHITE_CONCRETE,
stripe_x,
current_y,
stripe_z,
Some(&[BLACK_CONCRETE]),
None,
);
} else {
editor.set_block(
WHITE_CONCRETE,
stripe_x,
current_y,
stripe_z,
Some(&[BLACK_CONCRETE]),
None,
);
}
}
// Increment stripe_length and reset after completing a dash and gap
@@ -585,6 +767,46 @@ fn add_highway_support_pillar(
}
}
/// 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;

View File

@@ -17,6 +17,9 @@ pub fn generate_landuse(
let binding: String = "".to_string();
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(element.id);
let block_type = match landuse_tag.as_str() {
"greenfield" | "meadow" | "grass" | "orchard" | "forest" => GRASS_BLOCK,
"farmland" => FARMLAND,
@@ -28,10 +31,10 @@ pub fn generate_landuse(
if residential_tag == "rural" {
GRASS_BLOCK
} else {
STONE_BRICKS
STONE_BRICKS // Placeholder, will be randomized per-block
}
}
"commercial" => SMOOTH_STONE,
"commercial" => SMOOTH_STONE, // Placeholder, will be randomized per-block
"education" => POLISHED_ANDESITE,
"religious" => POLISHED_ANDESITE,
"industrial" => COBBLESTONE,
@@ -54,16 +57,42 @@ pub fn generate_landuse(
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(element.id);
for (x, z) in floor_area {
if landuse_tag == "traffic_island" {
editor.set_block(block_type, x, 1, z, None, None);
} else if landuse_tag == "construction" || landuse_tag == "railway" {
editor.set_block(block_type, x, 0, z, None, Some(&[SPONGE]));
// Apply per-block randomness for certain landuse types
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
// Urban residential: mix of stone bricks, cracked stone bricks, stone, cobblestone
let random_value = rng.gen_range(0..100);
if random_value < 72 {
STONE_BRICKS
} else if random_value < 87 {
CRACKED_STONE_BRICKS
} else if random_value < 92 {
STONE
} else {
COBBLESTONE
}
} else if landuse_tag == "commercial" {
// Commercial: mix of smooth stone, stone, cobblestone, stone bricks
let random_value = rng.gen_range(0..100);
if random_value < 40 {
SMOOTH_STONE
} else if random_value < 70 {
STONE_BRICKS
} else if random_value < 90 {
STONE
} else {
COBBLESTONE
}
} else {
editor.set_block(block_type, x, 0, z, None, None);
block_type
};
if landuse_tag == "traffic_island" {
editor.set_block(actual_block, x, 1, z, None, None);
} else if landuse_tag == "construction" || landuse_tag == "railway" {
editor.set_block(actual_block, x, 0, z, None, Some(&[SPONGE]));
} else {
editor.set_block(actual_block, x, 0, z, None, None);
}
// Add specific features for different landuse types

View File

@@ -7,8 +7,6 @@ use std::path::{Path, PathBuf};
/// Maximum Y coordinate in Minecraft (build height limit)
const MAX_Y: i32 = 319;
/// Scale factor for converting real elevation to Minecraft heights
const BASE_HEIGHT_SCALE: f64 = 0.7;
/// AWS S3 Terrarium tiles endpoint (no API key required)
const AWS_TERRARIUM_URL: &str =
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
@@ -318,16 +316,11 @@ pub fn fetch_elevation_data(
// This smooths terrain proportionally while preserving more detail.
let sigma: f64 = BASE_SIGMA_REF * (grid_size / BASE_GRID_REF).sqrt();
let blur_percentage: f64 = (sigma / grid_size) * 100.0;
eprintln!(
//let blur_percentage: f64 = (sigma / grid_size) * 100.0;
/*eprintln!(
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
grid_width, grid_height, sigma, blur_percentage
);
/* eprintln!(
"Grid: {}x{}, Blur sigma: {:.2}",
grid_width, grid_height, sigma
); */
);*/
// Continue with the existing blur and conversion to Minecraft heights...
let blurred_heights: Vec<Vec<f64>> = apply_gaussian_blur(&height_grid, sigma);
@@ -358,7 +351,7 @@ pub fn fetch_elevation_data(
}
}
eprintln!("Height data range: {min_height} to {max_height} m");
//eprintln!("Height data range: {min_height} to {max_height} m");
if extreme_low_count > 0 {
eprintln!(
"WARNING: Found {extreme_low_count} pixels with extremely low elevations (< -1000m)"
@@ -371,35 +364,58 @@ pub fn fetch_elevation_data(
}
let height_range: f64 = max_height - min_height;
// Apply scale factor to height scaling
let mut height_scale: f64 = BASE_HEIGHT_SCALE * scale.sqrt(); // sqrt to make height scaling less extreme
let mut scaled_range: f64 = height_range * height_scale;
// Adaptive scaling: ensure we don't exceed reasonable Y range
let available_y_range = (MAX_Y - ground_level) as f64;
let safety_margin = 0.9; // Use 90% of available range
let max_allowed_range = available_y_range * safety_margin;
// Realistic height scaling: 1 meter of real elevation = scale blocks in Minecraft
// At scale=1.0, 1 meter = 1 block (realistic 1:1 mapping)
// At scale=2.0, 1 meter = 2 blocks (exaggerated for larger worlds)
let ideal_scaled_range: f64 = height_range * scale;
if scaled_range > max_allowed_range {
let adjustment_factor = max_allowed_range / scaled_range;
height_scale *= adjustment_factor;
scaled_range = height_range * height_scale;
// Calculate available Y range in Minecraft (from ground_level to MAX_Y)
// Leave a buffer at the top for buildings, trees, and other structures
const TERRAIN_HEIGHT_BUFFER: i32 = 15;
let available_y_range: f64 = (MAX_Y - TERRAIN_HEIGHT_BUFFER - ground_level) as f64;
// Determine final height scale:
// - Use realistic 1:1 (times scale) if terrain fits within Minecraft limits
// - Only compress if the terrain would exceed the build height
let scaled_range: f64 = if ideal_scaled_range <= available_y_range {
// Terrain fits! Use realistic scaling
eprintln!(
"Height range too large, applying scaling adjustment factor: {adjustment_factor:.3}"
"Realistic elevation: {:.1}m range fits in {} available blocks",
height_range, available_y_range as i32
);
eprintln!("Adjusted scaled range: {scaled_range:.1} blocks");
}
ideal_scaled_range
} else {
// Terrain too tall, compress to fit within Minecraft limits
let compression_factor: f64 = available_y_range / height_range;
let compressed_range: f64 = height_range * compression_factor;
eprintln!(
"Elevation compressed: {:.1}m range -> {:.0} blocks ({:.2}:1 ratio, 1 block = {:.2}m)",
height_range,
compressed_range,
height_range / compressed_range,
compressed_range / height_range
);
compressed_range
};
// Convert to scaled Minecraft Y coordinates
// Lowest real elevation maps to ground_level, highest maps to ground_level + scaled_range
for row in blurred_heights {
let mc_row: Vec<i32> = row
.iter()
.map(|&h| {
// Scale the height differences
let relative_height: f64 = (h - min_height) / height_range;
// Calculate relative position within the elevation range (0.0 to 1.0)
let relative_height: f64 = if height_range > 0.0 {
(h - min_height) / height_range
} else {
0.0
};
// Scale to Minecraft blocks and add to ground level
let scaled_height: f64 = relative_height * scaled_range;
// With terrain enabled, ground_level is used as the MIN_Y for terrain
((ground_level as f64 + scaled_height).round() as i32).clamp(ground_level, MAX_Y)
// Clamp to valid Minecraft Y range (leave buffer at top for structures)
((ground_level as f64 + scaled_height).round() as i32)
.clamp(ground_level, MAX_Y - TERRAIN_HEIGHT_BUFFER)
})
.collect();
mc_heights.push(mc_row);
@@ -413,7 +429,7 @@ pub fn fetch_elevation_data(
max_block_height = max_block_height.max(height);
}
}
eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
//eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
Ok(ElevationData {
heights: mc_heights,
@@ -573,7 +589,7 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
let min_reasonable = all_heights[p1_idx];
let max_reasonable = all_heights[p99_idx];
eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
//eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
let mut outliers_filtered = 0;
@@ -588,7 +604,7 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
}
if outliers_filtered > 0 {
eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
//eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
// Re-run the NaN filling to interpolate the filtered values
fill_nan_values(height_grid);
}

View File

@@ -1071,10 +1071,9 @@ fn gui_start_generation(
Ok(())
}
Err(e) => {
let error_msg = format!("Failed to fetch data: {e}");
emit_gui_error(&error_msg);
emit_gui_error(&e.to_string());
// Session lock will be automatically released when _session_lock goes out of scope
Err(error_msg)
Err(e.to_string())
}
}
})

View File

@@ -351,7 +351,8 @@ body,
background-position: -31px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled {
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled,
.leaflet-draw-toolbar .leaflet-draw-edit-preview.editing-mode {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;

View File

@@ -287,7 +287,7 @@ button:hover {
/* Customization Settings */
.modal {
position: fixed;
z-index: 1000;
z-index: 20001;
left: 0;
top: 0;
width: 100%;
@@ -606,9 +606,12 @@ button:hover {
transition: background-color 0.3s, border-color 0.3s;
}
.settings-button .gear-icon::before {
content: "⚙️";
font-size: 18px;
.settings-button svg {
stroke: white;
width: 22px;
height: 22px;
min-width: 22px;
min-height: 22px;
}
/* Logo Animation */

4
src/gui/index.html vendored
View File

@@ -61,8 +61,8 @@
<div class="button-container">
<button type="button" id="start-button" class="start-button" onclick="startGeneration()" data-localize="start_generation">Start Generation</button>
<button type="button" class="settings-button" onclick="openSettings()">
<i class="gear-icon"></i>
<button type="button" class="settings-button" onclick="openSettings()" aria-label="Settings">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</button>
</div>
<br><br>

44
src/gui/js/bbox.js vendored
View File

@@ -564,6 +564,7 @@ $(document).ready(function () {
var worldOverlayEnabled = false;
var worldPreviewAvailable = false;
var sliderControl = null;
var worldOverlayHiddenForEdit = false; // Track if we hid the overlay for edit/delete mode
// Create the opacity slider as a proper Leaflet control
var SliderControl = L.Control.extend({
@@ -722,6 +723,32 @@ $(document).ready(function () {
}
}
// Temporarily hide the overlay (for edit/delete mode)
function hideWorldOverlayTemporarily() {
if (worldOverlay && worldOverlayEnabled) {
worldOverlayHiddenForEdit = true;
map.removeLayer(worldOverlay);
}
// Also visually disable the preview button during edit/delete mode
var btn = document.getElementById('world-preview-btn');
if (btn) {
btn.classList.add('editing-mode');
}
}
// Restore the overlay after edit/delete mode ends
function restoreWorldOverlay() {
if (worldOverlayHiddenForEdit && worldOverlay && worldOverlayEnabled) {
worldOverlay.addTo(map);
worldOverlayHiddenForEdit = false;
}
// Re-enable the preview button
var btn = document.getElementById('world-preview-btn');
if (btn) {
btn.classList.remove('editing-mode');
}
}
// Listen for messages from parent window
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'changeTileTheme') {
@@ -999,6 +1026,23 @@ $(document).ready(function () {
map.fitBounds(bounds.getBounds());
});
// Hide world preview overlay when entering edit or delete mode
map.on('draw:editstart', function() {
hideWorldOverlayTemporarily();
});
map.on('draw:deletestart', function() {
hideWorldOverlayTemporarily();
});
// Restore world preview overlay when exiting edit or delete mode
map.on('draw:editstop', function() {
restoreWorldOverlay();
});
map.on('draw:deletestop', function() {
restoreWorldOverlay();
});
function display() {
$('#boxbounds').text(formatBounds(bounds.getBounds(), '4326'));
$('#boxboundsmerc').text(formatBounds(bounds.getBounds(), currentproj));

14
src/gui/js/main.js vendored
View File

@@ -250,6 +250,20 @@ function initSettings() {
settingsModal.style.display = "none";
}
// Close settings and license modals on escape key
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
if (settingsModal.style.display === "flex") {
closeSettings();
}
const licenseModal = document.getElementById("license-modal");
if (licenseModal && licenseModal.style.display === "flex") {
closeLicense();
}
}
});
window.openSettings = openSettings;
window.closeSettings = closeSettings;

2
src/gui/maps.html vendored
View File

@@ -26,7 +26,7 @@
<div id="search-container">
<div id="search-box">
<input type="text" id="city-search" placeholder="Search for a city..." autocomplete="off" />
<button id="search-btn">🔍</button>
<button id="search-btn" aria-label="Search"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg></button>
</div>
<div id="search-results"></div>
</div>

View File

@@ -36,19 +36,17 @@ fn download_with_reqwest(url: &str, query: &str) -> Result<String, Box<dyn std::
}
Err(e) => {
if e.is_timeout() {
eprintln!(
"{}",
"Error! Request timed out. Try selecting a smaller area."
.red()
.bold()
);
emit_gui_error("Request timed out. Try selecting a smaller area.");
let msg = "Request timed out. Try selecting a smaller area.";
eprintln!("{}", format!("Error! {msg}").red().bold());
Err(msg.into())
} else if e.is_connect() {
let msg = "No internet connection.";
eprintln!("{}", format!("Error! {msg}").red().bold());
Err(msg.into())
} else {
eprintln!("{}", format!("Error! {e:.52}").red().bold());
emit_gui_error(&format!("{:.52}", e.to_string()));
Err(format!("{e:.52}").into())
}
// Always propagate errors
Err(e.into())
}
}
}

View File

@@ -151,6 +151,19 @@ impl<'a> WorldEditor<'a> {
}
}
/// Get the ground level at a specific world coordinate (without any offset)
#[inline(always)]
pub fn get_ground_level(&self, x: i32, z: i32) -> i32 {
if let Some(ground) = &self.ground {
ground.level(XZPoint::new(
x - self.xzbbox.min_x(),
z - self.xzbbox.min_z(),
))
} else {
0 // Default ground level if no terrain data
}
}
/// Returns the minimum world coordinates
pub fn get_min_coords(&self) -> (i32, i32) {
(self.xzbbox.min_x(), self.xzbbox.min_z())