Compare commits

...

12 Commits

Author SHA1 Message Date
Louis Erbkamm
7401036675 Merge pull request #95 from louis-e/floodfill-settings
Add floodfill timeout support
2024-12-29 00:55:24 +01:00
Louis Erbkamm
46cf1bca63 FUNDING.yml 2024-12-29 00:53:10 +01:00
louis-e
ae3a91970f Add floodfill timeout support 2024-12-29 00:41:20 +01:00
Louis Erbkamm
b44b7995d1 Merge pull request #94 from louis-e/session-lock-check
Check session lock before selecting world
2024-12-28 23:55:38 +01:00
louis-e
f3ae449f02 Check session lock before selecting world 2024-12-28 23:41:45 +01:00
Louis Erbkamm
2e319d5ea0 Merge pull request #93 from louis-e/snow-mode
Added winter mode
2024-12-28 23:34:29 +01:00
louis-e
1a259d6dfc Added winter mode 2024-12-28 23:22:07 +01:00
Louis Erbkamm
036e54807e Merge pull request #92 from louis-e/force-dark-theme
Force dark theme
2024-12-28 16:24:13 +01:00
louis-e
a75cffd94b Force dark theme 2024-12-28 16:21:35 +01:00
Louis Erbkamm
135e15077a Merge pull request #91 from louis-e/settings-menu
Added settings menu including scale option and custom bbox input
2024-12-28 03:20:45 +01:00
louis-e
a284f77545 Added settings menu including scale option and custom bbox input 2024-12-28 03:07:47 +01:00
Louis Erbkamm
5cb749ba8f Added .gitattributes 2024-12-25 01:50:30 +01:00
21 changed files with 590 additions and 154 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
gui-src/** linguist-vendored

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
buy_me_a_coffee: louisdev

View File

@@ -21,6 +21,7 @@ dirs = "4.0.0"
fastanvil = "0.31.0"
fastnbt = "2.5.0"
fnv = "1.0.7"
fs2 = "0.4"
geo = "0.28.0"
indicatif = "0.17.8"
itertools = "0.13.0"

View File

@@ -60,23 +60,21 @@ The project is named after the smallest city in Germany, Arnis[^2]. The city's s
## :memo: ToDo and Known Bugs
Feel free to choose an item from the To-Do or Known Bugs list, or bring your own idea to the table. Bug reports shall be raised as a Github issue. Contributions are highly welcome and appreciated!
- [ ] Memory optimization
- [ ] Mapping real coordinates to Minecraft coordinates (https://github.com/louis-e/arnis/issues/29)
- [ ] Evaluate and implement elevation (https://github.com/louis-e/arnis/issues/66)
- [ ] Fix Github Action Workflow for releasing Linux & MacOS Binary
- [ ] Evaluate and implement multithreaded region saving
- [ ] Better code documentation
- [ ] Implement house roof types
- [ ] Refactor railway implementation
- [ ] Refactor bridges implementation
- [ ] Refactor fountain structure implementation
- [ ] Evaluate and implement faster region saving
- [ ] Automatic new world creation instead of using an existing world
- [ ] Tool for mapping real coordinates to Minecraft coordinates
- [ ] Setup fork of [https://github.com/aaronr/bboxfinder.com](https://github.com/aaronr/bboxfinder.com) for easy bbox picking
- [ ] Implement house roof types
- [ ] Refactor bridges implementation
- [ ] Refactor railway implementation
- [ ] Better code documentation
- [ ] Refactor fountain structure implementation
- [ ] Add interior to buildings
- [ ] Evaluate and implement elevation
- [ ] Generate a few big cities using high performance hardware and make them available to download
- [ ] Implement memory mapped storing of chunks to reduce memory usage
- [x] Memory optimization
- [x] Design and implement a GUI
- [x] Fix faulty empty chunks ([https://github.com/owengage/fastnbt/issues/120](https://github.com/owengage/fastnbt/issues/120)) (workaround found)
- [x] Setup fork of [https://github.com/aaronr/bboxfinder.com](https://github.com/aaronr/bboxfinder.com) for easy bbox picking
## :trophy: Open Source
#### Key objectives of this project

129
gui-src/css/styles.css vendored
View File

@@ -55,7 +55,7 @@ a:hover {
display: flex;
gap: 20px;
justify-content: center;
align-items: stretch; /* Ensures both sections take full height */
align-items: stretch;
margin-top: 5px;
}
@@ -73,7 +73,6 @@ a:hover {
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
/* No display: flex here, so buttons and content aren't stretched */
}
.controls-content {
@@ -83,7 +82,7 @@ a:hover {
}
.controls-box .progress-section {
margin-top: auto; /* Keeps the progress section at the bottom */
margin-top: auto;
}
.map-container {
@@ -112,11 +111,11 @@ button {
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
margin-top: 10px;
width: auto; /* Ensures buttons dont stretch */
width: auto;
}
button:hover {
border-color: #396cd8;
border-color: #656565;
}
#selected-directory {
@@ -238,4 +237,122 @@ button:hover {
.controls-box button {
width: 100%;
}
}
/* Customization Settings */
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #797979;
padding: 20px;
border: 1px solid #797979;
border-radius: 10px;
width: 400px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
}
.close-button {
color: #e9e9e9;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close-button:hover {
color: #ffffff;
}
#winter-toggle {
accent-color: #fecc44;
}
.winter-toggle-container, .scale-slider-container {
margin: 15px 0;
}
.scale-slider-container label {
display: block;
margin-bottom: 5px;
}
#scale-value-slider {
accent-color: #fecc44;
}
#slider-value {
margin-left: 10px;
font-weight: bold;
}
.bbox-input-container {
margin-bottom: 20px;
}
.bbox-input-container label {
display: block;
margin-bottom: 5px;
}
#bbox-coords {
width: 100%;
padding: 8px;
border: 1px solid #fecc44;
border-radius: 4px;
font-size: 14px;
}
#bbox-coords:focus {
outline: none;
border-color: #fecc44;
box-shadow: 0 0 5px #fecc44;
}
.button-container {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 5px;
}
.start-button {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.start-button:hover {
background-color: #4caf50;
}
.settings-button {
width: 40px !important;
height: 38px;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.3s, border-color 0.3s;
}
.settings-button .gear-icon::before {
content: "⚙️";
font-size: 18px;
}

50
gui-src/index.html vendored
View File

@@ -34,7 +34,7 @@
<section class="section controls-box">
<div class="controls-content">
<h2>Select World</h2>
<!-- Updated Tooltip Structure -->
<div class="tooltip" style="width: 100%;">
<button type="button" onclick="pickDirectory()" style="padding: 10px; line-height: 1.2; width: 100%;">
@@ -48,12 +48,15 @@
Please select a Minecraft world that can be overwritten, as the generation process will replace existing structures in the chosen world!
</span>
</div>
<br>
<button type="button" onclick="startGeneration()">Start Generation</button>
<div class="button-container">
<button type="button" id="start-button" class="start-button" onclick="startGeneration()">Start Generation</button>
<button type="button" class="settings-button" onclick="openSettings()">
<i class="gear-icon"></i>
</button>
</div>
<br><br>
<div class="progress-section">
<h2>Progress</h2>
<div class="progress-bar-container">
@@ -68,6 +71,39 @@
</section>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close-button" onclick="closeSettings()">&times;</span>
<h2>Customization Settings</h2>
<!-- Winter Mode Toggle Button -->
<div class="winter-toggle-container">
<label for="winter-toggle">Winter:</label>
<input type="checkbox" id="winter-toggle" name="winter-toggle">
</div>
<!-- World Scale Slider -->
<div class="scale-slider-container">
<label for="scale-value-slider">World Scale:</label>
<input type="range" id="scale-value-slider" name="scale-value-slider" min="0.50" max="2.5" step="0.25" value="1">
<span id="slider-value">1.00</span>
</div>
<!-- Bounding Box Input -->
<div class="bbox-input-container">
<label for="bbox-coords">Custom Bounding Box:</label>
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" style="width: 280px;" autocomplete="one-time-code" placeholder="Format: lat,lng,lat,lng">
</div>
<!-- Floodfill Timeout Input -->
<div class="timeout-input-container">
<label for="floodfill-timeout">Floodfill Timeout (sec):</label>
<input type="number" id="floodfill-timeout" name="floodfill-timeout" min="0" step="1" value="20" style="width: 100px;" placeholder="Seconds">
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<a href="https://github.com/louis-e/arnis" target="_blank" class="footer-link">
@@ -77,4 +113,4 @@
</main>
</body>
</html>
</html>

148
gui-src/js/main.js vendored
View File

@@ -2,10 +2,21 @@ const { invoke } = window.__TAURI__.core;
// Initialize elements and start the demo progress
window.addEventListener("DOMContentLoaded", async () => {
initFooter();
await checkForUpdates();
registerMessageEvent();
window.pickDirectory = pickDirectory;
window.startGeneration = startGeneration;
setupProgressListener();
initSettings();
handleBboxInput();
});
// Function to initialize the footer with the current year and version
async function initFooter() {
const currentYear = new Date().getFullYear();
document.getElementById("current-year").textContent = currentYear;
// Update displayed version
try {
const version = await invoke('gui_get_version');
const footerLink = document.querySelector(".footer-link");
@@ -13,8 +24,10 @@ window.addEventListener("DOMContentLoaded", async () => {
} catch (error) {
console.error("Failed to fetch version:", error);
}
}
// Check for updates
// Function to check for updates and display a notification if available
async function checkForUpdates() {
try {
const isUpdateAvailable = await invoke('gui_check_for_updates');
if (isUpdateAvailable) {
@@ -35,8 +48,10 @@ window.addEventListener("DOMContentLoaded", async () => {
} catch (error) {
console.error("Failed to check for updates: ", error);
}
}
// Register bbox update event for iframe map
// Function to register the event listener for bbox updates from iframe
function registerMessageEvent() {
window.addEventListener('message', function (event) {
const bboxText = event.data.bboxText;
@@ -45,15 +60,14 @@ window.addEventListener("DOMContentLoaded", async () => {
displayBboxInfoText(bboxText);
}
});
}
window.pickDirectory = pickDirectory;
window.startGeneration = startGeneration;
// Function to set up the progress bar listener
function setupProgressListener() {
const progressBar = document.getElementById("progress-bar");
const progressMessage = document.getElementById("progress-message");
const progressDetail = document.getElementById("progress-detail");
// Listen for progress-update events
window.__TAURI__.event.listen("progress-update", (event) => {
const { progress, message } = event.payload;
@@ -64,15 +78,102 @@ window.addEventListener("DOMContentLoaded", async () => {
if (message != "") {
progressMessage.textContent = message;
if (message.startsWith("Error!")) {
progressMessage.style.color = "#fa7878";
generationButtonEnabled = true;
} else if (message.startsWith("Done!")) {
progressMessage.style.color = "#7bd864";
generationButtonEnabled = true;
} else {
progressMessage.style.color = "";
}
}
});
});
}
function initSettings() {
// Settings
const settingsModal = document.getElementById("settings-modal");
const slider = document.getElementById("scale-value-slider");
const sliderValue = document.getElementById("slider-value");
// Open settings modal
function openSettings() {
settingsModal.style.display = "flex";
settingsModal.style.justifyContent = "center";
settingsModal.style.alignItems = "center";
}
// Close settings modal
function closeSettings() {
settingsModal.style.display = "none";
}
window.openSettings = openSettings;
window.closeSettings = closeSettings;
// Update slider value display
slider.addEventListener("input", () => {
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
});
}
// Function to validate and handle bbox input
function handleBboxInput() {
const inputBox = document.getElementById("bbox-coords");
const bboxInfo = document.getElementById("bbox-info");
inputBox.addEventListener("input", function () {
const input = inputBox.value.trim();
if (input === "") {
bboxInfo.textContent = "";
bboxInfo.style.color = "";
selectedBBox = "";
return;
}
// Regular expression to validate bbox input (supports both comma and space-separated formats)
const bboxPattern = /^(-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)[,\s](-?\d+(\.\d+)?)$/;
if (bboxPattern.test(input)) {
const matches = input.match(bboxPattern);
// Extract coordinates (Lat / Lng order expected)
const lat1 = parseFloat(matches[1]);
const lng1 = parseFloat(matches[3]);
const lat2 = parseFloat(matches[5]);
const lng2 = parseFloat(matches[7]);
// Validate latitude and longitude ranges in the expected Lat / Lng order
if (
lat1 >= -90 && lat1 <= 90 &&
lng1 >= -180 && lng1 <= 180 &&
lat2 >= -90 && lat2 <= 90 &&
lng2 >= -180 && lng2 <= 180
) {
// Input is valid; trigger the event
const bboxText = `${lat1} ${lng1} ${lat2} ${lng2}`;
window.dispatchEvent(new MessageEvent('message', { data: { bboxText } }));
// Update the info text
bboxInfo.textContent = "Custom selection confirmed!";
bboxInfo.style.color = "#7bd864";
} else {
// Valid numbers but invalid order or range
bboxInfo.textContent = "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).";
bboxInfo.style.color = "#fecc44";
selectedBBox = "";
}
} else {
// Input doesn't match the required format
bboxInfo.textContent = "Invalid format. Please use 'lat,lng,lat,lng' or 'lat lng lat lng'.";
bboxInfo.style.color = "#fecc44";
selectedBBox = "";
}
});
}
// Function to calculate the bounding box "size" in square meters based on latitude and longitude
function calculateBBoxSize(lng1, lat1, lng2, lat2) {
@@ -145,26 +246,39 @@ async function pickDirectory() {
}
}
let generationButtonEnabled = true;
async function startGeneration() {
try {
if (worldPath === "No world selected" || worldPath == "Invalid Minecraft world" || worldPath === "") {
if (generationButtonEnabled === false) {
return;
}
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
document.getElementById('bbox-info').textContent = "Select a location first!";
document.getElementById('bbox-info').style.color = "#fa7878";
return;
}
if (worldPath === "No world selected" || worldPath == "Invalid Minecraft world" || worldPath == "The selected world is currently in use" || worldPath === "") {
document.getElementById('selected-world').textContent = "Select a Minecraft world first!";
document.getElementById('selected-world').style.color = "#fa7878";
return;
}
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
document.getElementById('bbox-info').textContent = "Select a location firsta using the rectangle tool!";
document.getElementById('bbox-info').style.color = "#fa7878";
return;
}
var winter_mode = document.getElementById("winter-toggle").checked;
var scale = parseFloat(document.getElementById("scale-value-slider").value);
var floodfill_timeout = parseInt(document.getElementById("floodfill-timeout").value, 10);
// Validate the floodfill timeout
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
// Pass the bounding box and selected world to the Rust backend
await invoke("gui_start_generation", { bboxText: selectedBBox, selectedWorld: worldPath });
await invoke("gui_start_generation", { bboxText: selectedBBox, selectedWorld: worldPath, worldScale: scale, winterMode: winter_mode, floodfillTimeout: floodfill_timeout });
// Update the UI or show a loading/progress message if needed
console.log("Generation process started.");
generationButtonEnabled = false;
} catch (error) {
console.error("Error starting generation:", error);
generationButtonEnabled = true;
}
}

View File

@@ -33,11 +33,15 @@ pub struct Args {
#[arg(long, default_value = "1.0")]
pub scale: f64,
/// Enable winter mode (default: false)
#[arg(long, default_value_t = false)]
pub winter: bool,
/// Enable debug mode (optional)
#[arg(long, default_value_t = false, action = clap::ArgAction::SetTrue)]
pub debug: bool,
/// Set floodfill timeout (seconds) (optional) // TODO
/// Set floodfill timeout (seconds) (optional)
#[arg(long, value_parser = parse_duration)]
pub timeout: Option<Duration>,
}

View File

@@ -138,6 +138,8 @@ impl Block {
108 => "potatoes",
109 => "wheat",
110 => "bedrock",
111 => "snow_block",
112 => "snow",
_ => panic!("Invalid id"),
}
}
@@ -285,6 +287,8 @@ pub const MAGENTA_CONCRETE: Block = Block::new(101);
pub const BROWN_WOOL: Block = Block::new(102);
pub const OXIDIZED_COPPER: Block = Block::new(103);
pub const YELLOW_TERRACOTTA: Block = Block::new(104);
pub const SNOW_BLOCK: Block = Block::new(111);
pub const SNOW_LAYER: Block = Block::new(112);
pub const CARROTS: Block = Block::new(105);
pub const DARK_OAK_DOOR_LOWER: Block = Block::new(106);

View File

@@ -1,5 +1,5 @@
use crate::args::Args;
use crate::block_definitions::{DIRT, GRASS_BLOCK};
use crate::block_definitions::{DIRT, GRASS_BLOCK, SNOW_BLOCK};
use crate::element_processing::*;
use crate::osm_parser::ProcessedElement;
use crate::progress::emit_gui_progress_update;
@@ -54,47 +54,17 @@ pub fn generate_world(
match element {
ProcessedElement::Way(way) => {
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
buildings::generate_buildings(
&mut editor,
way,
GROUND_LEVEL,
args.timeout.as_ref(),
);
buildings::generate_buildings(&mut editor, way, GROUND_LEVEL, args);
} else if way.tags.contains_key("highway") {
highways::generate_highways(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
highways::generate_highways(&mut editor, element, GROUND_LEVEL, args);
} else if way.tags.contains_key("landuse") {
landuse::generate_landuse(
&mut editor,
way,
GROUND_LEVEL,
args.timeout.as_ref(),
);
landuse::generate_landuse(&mut editor, way, GROUND_LEVEL, args);
} else if way.tags.contains_key("natural") {
natural::generate_natural(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
natural::generate_natural(&mut editor, element, GROUND_LEVEL, args);
} else if way.tags.contains_key("amenity") {
amenities::generate_amenities(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
amenities::generate_amenities(&mut editor, element, GROUND_LEVEL, args);
} else if way.tags.contains_key("leisure") {
leisure::generate_leisure(
&mut editor,
way,
GROUND_LEVEL,
args.timeout.as_ref(),
);
leisure::generate_leisure(&mut editor, way, GROUND_LEVEL, args);
} else if way.tags.contains_key("barrier") {
barriers::generate_barriers(&mut editor, element, GROUND_LEVEL);
} else if way.tags.contains_key("waterway") {
@@ -113,28 +83,13 @@ pub fn generate_world(
} else if node.tags.contains_key("natural")
&& node.tags.get("natural") == Some(&"tree".to_string())
{
natural::generate_natural(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
natural::generate_natural(&mut editor, element, GROUND_LEVEL, args);
} else if node.tags.contains_key("amenity") {
amenities::generate_amenities(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
amenities::generate_amenities(&mut editor, element, GROUND_LEVEL, args);
} else if node.tags.contains_key("barrier") {
barriers::generate_barriers(&mut editor, element, GROUND_LEVEL);
} else if node.tags.contains_key("highway") {
highways::generate_highways(
&mut editor,
element,
GROUND_LEVEL,
args.timeout.as_ref(),
);
highways::generate_highways(&mut editor, element, GROUND_LEVEL, args);
} else if node.tags.contains_key("tourism") {
tourisms::generate_tourisms(&mut editor, node, GROUND_LEVEL);
}
@@ -172,9 +127,11 @@ pub fn generate_world(
let total_iterations_grnd: f64 = (scale_factor_x + 1.0) * (scale_factor_z + 1.0);
let progress_increment_grnd: f64 = 30.0 / total_iterations_grnd;
let groundlayer_block = if args.winter { SNOW_BLOCK } else { GRASS_BLOCK };
for x in 0..=(scale_factor_x as i32) {
for z in 0..=(scale_factor_z as i32) {
editor.set_block(GRASS_BLOCK, x, GROUND_LEVEL, z, None, None);
editor.set_block(groundlayer_block, x, GROUND_LEVEL, z, None, None);
editor.set_block(DIRT, x, GROUND_LEVEL - 1, z, None, None);
block_counter += 1;

View File

@@ -1,5 +1,4 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
@@ -10,7 +9,7 @@ pub fn generate_amenities(
editor: &mut WorldEditor,
element: &ProcessedElement,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags().get("layer") {
@@ -52,7 +51,7 @@ pub fn generate_amenities(
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let floor_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, floodfill_timeout);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Fill the floor area
for (x, z) in floor_area.iter() {
@@ -152,7 +151,7 @@ pub fn generate_amenities(
if corner_addup.2 > 0 {
let polygon_coords: Vec<(i32, i32)> = current_amenity.to_vec();
let flood_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, floodfill_timeout);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in flood_area {
editor.set_block(

View File

@@ -1,3 +1,4 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::colors::{color_text_to_rgb_tuple, rgb_distance, RGBTuple};
@@ -12,7 +13,7 @@ pub fn generate_buildings(
editor: &mut WorldEditor,
element: &ProcessedWay,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
@@ -95,7 +96,7 @@ pub fn generate_buildings(
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let floor_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, floodfill_timeout);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Fill the floor area
for (x, z) in floor_area.iter() {
@@ -152,7 +153,8 @@ pub fn generate_buildings(
.iter()
.map(|node: &crate::osm_parser::ProcessedNode| (node.x, node.z))
.collect();
let roof_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout); // Use flood-fill to determine the area
let roof_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref()); // Use flood-fill to determine the area
// Fill the interior of the roof with STONE_BRICK_SLAB
for (x, z) in roof_area.iter() {
@@ -172,7 +174,7 @@ pub fn generate_buildings(
building_height = 23
}
} else if building_type == "bridge" {
generate_bridge(editor, element, ground_level, floodfill_timeout);
generate_bridge(editor, element, ground_level, args.timeout.as_ref());
return;
}
}
@@ -201,6 +203,7 @@ pub fn generate_buildings(
}
}
}
// Ceiling cobblestone
editor.set_block(
COBBLESTONE,
bx,
@@ -208,7 +211,19 @@ pub fn generate_buildings(
bz,
None,
None,
); // Ceiling cobblestone
);
if args.winter {
editor.set_block(
SNOW_LAYER,
x,
ground_level + building_height + 2,
z,
None,
None,
);
}
current_building.push((bx, bz));
corner_addup = (corner_addup.0 + bx, corner_addup.1 + bz, corner_addup.2 + 1);
}
@@ -224,7 +239,7 @@ pub fn generate_buildings(
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in floor_area {
if processed_points.insert((x, z)) {
@@ -253,6 +268,17 @@ pub fn generate_buildings(
None,
None,
);
if args.winter {
editor.set_block(
SNOW_LAYER,
x,
ground_level + building_height + 2,
z,
None,
None,
);
}
}
}
}

View File

@@ -1,16 +1,15 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedElement, ProcessedWay};
use crate::world_editor::WorldEditor; // Assuming you have a flood fill function for area filling
use crate::world_editor::WorldEditor;
pub fn generate_highways(
editor: &mut WorldEditor,
element: &ProcessedElement,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
if let Some(highway_type) = element.tags().get("highway") {
if highway_type == "street_lamp" {
@@ -37,6 +36,10 @@ pub fn generate_highways(
editor.set_block(GREEN_WOOL, x, ground_level + 4, z, None, None);
editor.set_block(YELLOW_WOOL, x, ground_level + 5, z, None, None);
editor.set_block(RED_WOOL, x, ground_level + 6, z, None, None);
if args.winter {
editor.set_block(SNOW_LAYER, x, ground_level + 7, z, None, None);
}
}
}
}
@@ -72,7 +75,13 @@ pub fn generate_highways(
"wood" => OAK_PLANKS,
"asphalt" => BLACK_CONCRETE,
"gravel" | "fine_gravel" => GRAVEL,
"grass" => GRASS_BLOCK,
"grass" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"dirt" => DIRT,
"sand" => SAND,
"concrete" => LIGHT_GRAY_CONCRETE,
@@ -86,7 +95,8 @@ pub fn generate_highways(
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in filled_area {
editor.set_block(surface_block, x, ground_level, z, None, None);
@@ -95,7 +105,7 @@ pub fn generate_highways(
let mut previous_node: Option<(i32, i32)> = None;
let mut block_type = BLACK_CONCRETE;
let mut block_range: i32 = 2;
let mut add_stripe = false; // Flag for adding stripes
let mut add_stripe = false;
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = element.tags().get("layer") {

View File

@@ -1,5 +1,4 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::element_processing::tree::create_tree;
@@ -12,7 +11,7 @@ pub fn generate_landuse(
editor: &mut WorldEditor,
element: &ProcessedWay,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
@@ -23,9 +22,21 @@ pub fn generate_landuse(
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
let block_type = match landuse_tag.as_str() {
"greenfield" | "meadow" | "grass" => GRASS_BLOCK,
"greenfield" | "meadow" | "grass" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"farmland" => FARMLAND,
"forest" => GRASS_BLOCK,
"forest" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"cemetery" => PODZOL,
"beach" => SAND,
"construction" => DIRT,
@@ -36,9 +47,17 @@ pub fn generate_landuse(
"industrial" => COBBLESTONE,
"military" => GRAY_CONCRETE,
"railway" => GRAVEL,
_ => GRASS_BLOCK,
_ => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
};
let bresenham_block: Block = if args.winter { SNOW_BLOCK } else { GRASS_BLOCK };
// Process landuse nodes to fill the area
for node in &element.nodes {
let x: i32 = node.x;
@@ -49,7 +68,7 @@ pub fn generate_landuse(
let bresenham_points: Vec<(i32, i32, i32)> =
bresenham_line(prev.0, ground_level, prev.1, x, ground_level, z);
for (bx, _, bz) in bresenham_points {
editor.set_block(GRASS_BLOCK, bx, ground_level, bz, None, None);
editor.set_block(bresenham_block, bx, ground_level, bz, None, None);
}
current_landuse.push((x, z));
@@ -62,7 +81,7 @@ pub fn generate_landuse(
// If there are landuse nodes, flood-fill the area
if !current_landuse.is_empty() {
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
@@ -156,7 +175,14 @@ pub fn generate_landuse(
editor.set_block(RED_FLOWER, x, ground_level + 1, z, None, None);
}
} else if random_choice < 33 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
}
}
}
@@ -164,7 +190,14 @@ pub fn generate_landuse(
if !editor.check_for_block(x, ground_level, z, None, Some(&[WATER])) {
let random_choice: i32 = rng.gen_range(0..21);
if random_choice == 20 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
} else if random_choice == 2 {
let flower_block: Block = match rng.gen_range(1..=4) {
1 => RED_FLOWER,
@@ -206,6 +239,7 @@ pub fn generate_landuse(
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
} else if special_choice <= 6 {
editor.set_block(HAY_BALE, x, ground_level + 1, z, None, None);
@@ -319,7 +353,14 @@ pub fn generate_landuse(
if !editor.check_for_block(x, ground_level, z, None, Some(&[WATER])) {
let random_choice: i32 = rng.gen_range(0..1001);
if random_choice < 5 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
} else if random_choice < 800 {
editor.set_block(GRASS, x, ground_level + 1, z, None, None);
}

View File

@@ -1,5 +1,4 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::element_processing::tree::create_tree;
@@ -12,7 +11,7 @@ pub fn generate_leisure(
editor: &mut WorldEditor,
element: &ProcessedWay,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
if let Some(leisure_type) = element.tags.get("leisure") {
let mut previous_node: Option<(i32, i32)> = None;
@@ -21,7 +20,13 @@ pub fn generate_leisure(
// Determine block type based on leisure type
let block_type: Block = match leisure_type.as_str() {
"park" => GRASS_BLOCK,
"park" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"playground" | "recreation_ground" | "pitch" => {
if let Some(surface) = element.tags.get("surface") {
match surface.as_str() {
@@ -34,9 +39,21 @@ pub fn generate_leisure(
GREEN_STAINED_HARDENED_CLAY
}
}
"garden" => GRASS_BLOCK,
"garden" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"swimming_pool" => WATER,
_ => GRASS_BLOCK,
_ => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
};
// Process leisure area nodes
@@ -78,7 +95,8 @@ pub fn generate_leisure(
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
for (x, z) in filled_area {
editor.set_block(block_type, x, ground_level, z, Some(&[GRASS_BLOCK]), None);
@@ -113,7 +131,14 @@ pub fn generate_leisure(
}
71..=80 => {
// Tree
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
}
_ => {}
}

View File

@@ -1,5 +1,4 @@
use std::time::Duration;
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::element_processing::tree::create_tree;
@@ -12,7 +11,7 @@ pub fn generate_natural(
editor: &mut WorldEditor,
element: &ProcessedElement,
ground_level: i32,
floodfill_timeout: Option<&Duration>,
args: &Args,
) {
if let Some(natural_type) = element.tags().get("natural") {
if natural_type == "tree" {
@@ -21,7 +20,14 @@ pub fn generate_natural(
let z: i32 = node.z;
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
}
} else {
let mut previous_node: Option<(i32, i32)> = None;
@@ -30,11 +36,29 @@ pub fn generate_natural(
// Determine block type based on natural tag
let block_type: Block = match natural_type.as_str() {
"scrub" | "grassland" | "wood" => GRASS_BLOCK,
"scrub" | "grassland" | "wood" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"beach" | "sand" => SAND,
"tree_row" => GRASS_BLOCK,
"tree_row" => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
"wetland" | "water" => WATER,
_ => GRASS_BLOCK,
_ => {
if args.winter {
SNOW_BLOCK
} else {
GRASS_BLOCK
}
}
};
let ProcessedElement::Way(way) = element else {
@@ -69,7 +93,7 @@ pub fn generate_natural(
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, floodfill_timeout);
flood_fill_area(&polygon_coords, args.timeout.as_ref());
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
@@ -84,7 +108,14 @@ pub fn generate_natural(
let random_choice: i32 = rng.gen_range(0..26);
if random_choice == 25 {
create_tree(editor, x, ground_level + 1, z, rng.gen_range(1..=3));
create_tree(
editor,
x,
ground_level + 1,
z,
rng.gen_range(1..=3),
args.winter,
);
} else if random_choice == 2 {
let flower_block = match rng.gen_range(1..=4) {
1 => RED_FLOWER,

View File

@@ -46,7 +46,7 @@ fn round3(editor: &mut WorldEditor, material: Block, x: i32, y: i32, z: i32) {
}
/// Function to create different types of trees.
pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u8) {
pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u8, snow: bool) {
let mut blacklist: Vec<Block> = Vec::new();
blacklist.extend(building_corner_variations());
blacklist.extend(building_wall_variations());
@@ -60,7 +60,6 @@ pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u
match typetree {
1 => {
// Oak tree
editor.fill_blocks(OAK_LOG, x, y, z, x, y + 8, z, None, None);
editor.fill_blocks(OAK_LEAVES, x - 1, y + 3, z, x - 1, y + 9, z, None, None);
editor.fill_blocks(OAK_LEAVES, x + 1, y + 3, z, x + 1, y + 9, z, None, None);
@@ -79,6 +78,24 @@ pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u
round2(editor, OAK_LEAVES, x, y + 4, z);
round3(editor, OAK_LEAVES, x, y + 6, z);
round3(editor, OAK_LEAVES, x, y + 5, z);
if snow {
editor.set_block(SNOW_LAYER, x, y + 11, z, None, None);
editor.set_block(SNOW_LAYER, x + 1, y + 10, z, None, None);
editor.set_block(SNOW_LAYER, x - 1, y + 10, z, None, None);
editor.set_block(SNOW_LAYER, x, y + 10, z - 1, None, None);
editor.set_block(SNOW_LAYER, x, y + 10, z + 1, None, None);
round1(editor, SNOW_LAYER, x, y + 9, z);
round1(editor, SNOW_LAYER, x, y + 8, z);
round1(editor, SNOW_LAYER, x, y + 7, z);
round1(editor, SNOW_LAYER, x, y + 6, z);
round2(editor, SNOW_LAYER, x, y + 8, z);
round2(editor, SNOW_LAYER, x, y + 7, z);
round2(editor, SNOW_LAYER, x, y + 6, z);
round2(editor, SNOW_LAYER, x, y + 5, z);
round3(editor, SNOW_LAYER, x, y + 7, z);
round3(editor, SNOW_LAYER, x, y + 6, z);
}
}
2 => {
// Spruce tree
@@ -95,6 +112,21 @@ pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u
round1(editor, BIRCH_LEAVES, x, y + 3, z);
round2(editor, BIRCH_LEAVES, x, y + 6, z);
round2(editor, BIRCH_LEAVES, x, y + 3, z);
if snow {
editor.set_block(SNOW_LAYER, x, y + 11, z, None, None);
editor.set_block(SNOW_LAYER, x + 1, y + 11, z, None, None);
editor.set_block(SNOW_LAYER, x - 1, y + 11, z, None, None);
editor.set_block(SNOW_LAYER, x, y + 11, z - 1, None, None);
editor.set_block(SNOW_LAYER, x, y + 11, z + 1, None, None);
round1(editor, SNOW_LAYER, x, y + 10, z);
round1(editor, SNOW_LAYER, x, y + 8, z);
round1(editor, SNOW_LAYER, x, y + 7, z);
round1(editor, SNOW_LAYER, x, y + 5, z);
round1(editor, SNOW_LAYER, x, y + 4, z);
round2(editor, SNOW_LAYER, x, y + 7, z);
round2(editor, SNOW_LAYER, x, y + 4, z);
}
}
3 => {
// Birch tree
@@ -112,6 +144,22 @@ pub fn create_tree(editor: &mut WorldEditor, x: i32, y: i32, z: i32, typetree: u
round2(editor, BIRCH_LEAVES, x, y + 2, z);
round2(editor, BIRCH_LEAVES, x, y + 3, z);
round2(editor, BIRCH_LEAVES, x, y + 4, z);
if snow {
editor.set_block(SNOW_LAYER, x, y + 9, z, None, None);
editor.set_block(SNOW_LAYER, x + 1, y + 8, z, None, None);
editor.set_block(SNOW_LAYER, x - 1, y + 8, z, None, None);
editor.set_block(SNOW_LAYER, x, y + 8, z - 1, None, None);
editor.set_block(SNOW_LAYER, x, y + 8, z + 1, None, None);
round1(editor, SNOW_LAYER, x, y + 7, z);
round1(editor, SNOW_LAYER, x, y + 6, z);
round1(editor, SNOW_LAYER, x, y + 5, z);
round1(editor, SNOW_LAYER, x, y + 4, z);
round1(editor, SNOW_LAYER, x, y + 3, z);
round2(editor, SNOW_LAYER, x, y + 3, z);
round2(editor, SNOW_LAYER, x, y + 4, z);
round2(editor, SNOW_LAYER, x, y + 5, z);
}
}
_ => {} // Do nothing if typetree is not recognized
}

View File

@@ -56,7 +56,7 @@ pub fn flood_fill_area(
while let Some((start_x, start_z)) = candidate_points.pop_front() {
if let Some(timeout) = timeout {
if &start_time.elapsed() > timeout {
eprintln!("Floodfill timeout"); // TODO only print when debug arg is set?
eprintln!("Floodfill timeout");
break;
}
}
@@ -70,7 +70,7 @@ pub fn flood_fill_area(
while let Some((x, z)) = queue.pop_front() {
if let Some(timeout) = timeout {
if &start_time.elapsed() > timeout {
eprintln!("Floodfill timeout"); // TODO only print when debug arg is set?
eprintln!("Floodfill timeout");
break;
}
}

View File

@@ -16,6 +16,7 @@ mod world_editor;
use args::Args;
use clap::Parser;
use colored::*;
use fs2::FileExt;
use rfd::FileDialog;
use std::fs::File;
use std::io::Write;
@@ -168,6 +169,20 @@ fn gui_pick_directory() -> Result<String, String> {
// Check if the "region" folder exists within the selected directory
if path.join("region").exists() {
// Check the 'session.lock' file
let session_lock_path = path.join("session.lock");
if session_lock_path.exists() {
// Try to acquire a lock on the session.lock file
if let Ok(file) = File::open(&session_lock_path) {
if file.try_lock_shared().is_err() {
return Err("The selected world is currently in use".to_string());
} else {
// Release the lock immediately
let _ = file.unlock();
}
}
}
return Ok(path.display().to_string());
} else {
// Notify the frontend that no valid Minecraft world was found
@@ -193,7 +208,13 @@ fn gui_check_for_updates() -> Result<bool, String> {
}
#[tauri::command]
fn gui_start_generation(bbox_text: String, selected_world: String) -> Result<(), String> {
fn gui_start_generation(
bbox_text: String,
selected_world: String,
world_scale: f64,
winter_mode: bool,
floodfill_timeout: u64,
) -> Result<(), String> {
tauri::async_runtime::spawn(async move {
if let Err(e) = tokio::task::spawn_blocking(move || {
// Utility function to reorder bounding box coordinates
@@ -217,9 +238,10 @@ fn gui_start_generation(bbox_text: String, selected_world: String) -> Result<(),
file: None,
path: selected_world,
downloader: "requests".to_string(),
scale: 1.0,
scale: world_scale,
winter: winter_mode,
debug: false,
timeout: None,
timeout: Some(std::time::Duration::from_secs(floodfill_timeout)),
};
// Reorder bounding box coordinates for further processing

View File

@@ -389,7 +389,7 @@ impl<'a> WorldEditor<'a> {
.progress_chars("█▓░"),
);
let total_steps: f64 = 10.0;
let total_steps: f64 = 9.0;
let progress_increment_save: f64 = total_steps / total_regions as f64;
let mut current_progress_save: f64 = 90.0;
let mut last_emitted_progress: f64 = current_progress_save;

View File

@@ -15,7 +15,8 @@
"height": 650,
"resizable": false,
"transparent": true,
"center": true
"center": true,
"theme": "Dark"
}
],
"security": {