diff --git a/CMakeLists.txt b/CMakeLists.txt index 366b2506..62b41aa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,7 @@ set(SOURCES "application/editormodetrackux.cpp" "application/editormoderoad.cpp" "application/editormodeprofile.cpp" +"application/editormodetextures.cpp" "application/editormodegauge.cpp" "application/editormodeinfra.cpp" "application/editormodespeed.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 4bf80d58..73916325 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -45,6 +45,7 @@ extern char **environ; #include "utilities/Float3d.h" #include "scene/scene.h" #include "scene/scenelayers.h" +#include "scene/sceneterrain.h" #include "utilities/parser.h" #include "utilities/utilities.h" @@ -213,8 +214,8 @@ namespace } } - // highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one - bool height_at(double const X, double const Z, double const Ceiling, double &OutY) const + // highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one, and which triangle it is in Index if asked + bool height_at(double const X, double const Z, double const Ceiling, double &OutY, std::uint32_t *Index = nullptr) const { bool found = false; for (auto const idx : m_cells[static_cast(row(Z)) * m_columns + column(X)]) @@ -225,6 +226,8 @@ namespace { OutY = y; found = true; + if (Index != nullptr) + *Index = idx; } } return found; @@ -233,11 +236,11 @@ namespace private: int column(double const X) const { - return std::clamp(static_cast(std::floor((X - m_min.x) / m_cellsize)), 0, m_columns - 1); + return static_cast(std::clamp(std::floor((X - m_min.x) / m_cellsize), 0.0, static_cast(m_columns - 1))); } int row(double const Z) const { - return std::clamp(static_cast(std::floor((Z - m_min.y) / m_cellsize)), 0, m_rows - 1); + return static_cast(std::clamp(std::floor((Z - m_min.y) / m_cellsize), 0.0, static_cast(m_rows - 1))); } std::vector m_triangles; @@ -498,6 +501,7 @@ editor_mode::editor_mode() { ui()->set_track_options([this]() { render_track_tool_options(); }, [this]() { render_track_selection(); }); ui()->set_workspace_options(editor_ui::workspace::roads, [this]() { render_road_tool_options(); }, [this]() { render_road_selection(); }); ui()->set_workspace_options(editor_ui::workspace::terrain, [this]() { render_terrain_tool_options(); }); + ui()->set_workspace_bank(editor_ui::workspace::terrain, [this]() { render_texture_browser(); }, STR("Textures")); // a node picked in the scene window is selected as if clicked in the view; a double click flies the camera to it ui()->set_scene_select([this](scene::basic_node *Node, bool const Focus) { if (Focus) @@ -876,8 +880,50 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) m_array.base = nullptr; } +void editor_mode::terrain_history_step() +{ + if (false == m_streamer.active()) + return; + if (m_streamer.recording()) + { + if (terrain_brush() && mouseHold && false == m_history_busy) + return; // the stroke goes on + auto change{m_streamer.end_change()}; + if (false == change.empty()) + { + trim_history(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Terrain; + snap.node_name = STR("terrain"); + if (false == change.chunks.empty()) + { + auto const centre{heightmap::chunk_corner(change.chunks.begin()->first) + glm::dvec2{heightmap::chunk_size * 0.5}}; + snap.position = {centre.x, 0.0, centre.y}; + } + snap.terrain = std::make_shared(std::move(change)); + m_history.push_back(std::move(snap)); + g_redo.clear(); + } + } + m_streamer.begin_change(); +} + +void editor_mode::restore_terrain(EditorSnapshot &Snapshot) +{ + if (Snapshot.terrain == nullptr) + return; + auto opposite{m_streamer.restore(*Snapshot.terrain)}; + Snapshot.terrain = std::make_shared(std::move(opposite)); + forget_ground(); + m_water_selected = -1; +} + void editor_mode::undo_last() { + // a change of the terrain made a moment ago is a step of its own before it's taken back + m_history_busy = true; + terrain_history_step(); + m_history_busy = false; // the changes made together are taken back together, and their models are the selection again std::vector touched; while (false == m_history.empty()) @@ -906,6 +952,13 @@ void editor_mode::undo_one() EditorSnapshot snap = m_history.back(); m_history.pop_back(); + if (snap.action == EditorSnapshot::Action::Terrain) + { + restore_terrain(snap); + g_redo.push_back(std::move(snap)); + return; + } + if (snap.action == EditorSnapshot::Action::Bend) { restore_bend(snap, true); @@ -1036,6 +1089,10 @@ void editor_mode::undo_one() void editor_mode::redo_last() { + // a change of the terrain made since the undo goes to the history, and the redo steps are dropped with it + m_history_busy = true; + terrain_history_step(); + m_history_busy = false; // the first of the changes made together is on the top, the others follow it std::vector touched; do @@ -1060,6 +1117,13 @@ void editor_mode::redo_one() EditorSnapshot snap = g_redo.back(); g_redo.pop_back(); + if (snap.action == EditorSnapshot::Action::Terrain) + { + restore_terrain(snap); + m_history.push_back(std::move(snap)); + return; + } + if (snap.action == EditorSnapshot::Action::Bend) { restore_bend(snap, false); @@ -1238,6 +1302,7 @@ bool editor_mode::update() m_dragging = false; m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE); } + terrain_history_step(); // fixed step render time routines (50 Hz) fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable @@ -1479,8 +1544,8 @@ namespace return changed; } - // grid points without a height take the height of the nearest point which has one (by steps along the grid) - void fill_from_nearest(std::vector &Heights, std::vector &Found, std::size_t const Side) + // grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid) + template void fill_from_nearest(std::vector &Heights, std::vector &Found, std::size_t const Side) { std::deque queue; for (std::size_t index = 0; index < Found.size(); ++index) @@ -1530,6 +1595,8 @@ void editor_mode::render_terrain_ui() if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltip); }; + // the tools are named like the headers below, which would give them the same IDs + ImGui::PushID("terrain_tools"); radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings")); ImGui::SameLine(); radio(STR_C("Chunks"), terrain_tool::chunks, STR_C("LMB adds a chunk shaped as the ground under it, next to the clicked one; Shift+LMB removes the clicked chunk")); @@ -1542,6 +1609,7 @@ void editor_mode::render_terrain_ui() radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette")); ImGui::SameLine(); radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back")); + ImGui::PopID(); m_terrain_tool = static_cast(tool); if (ImGui::CollapsingHeader(STR_C("Chunks"), ImGuiTreeNodeFlags_DefaultOpen)) @@ -1594,6 +1662,10 @@ void editor_mode::render_terrain_ui() auto const label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())}; if (ImGui::Selectable(label.c_str(), static_cast(i) == m_terrain_layer)) m_terrain_layer = static_cast(i); + // a material dropped on an entry takes its place, on every chunk painted with it + std::string dropped; + if (material_drop(dropped)) + m_streamer.layer_material(static_cast(i), dropped); } if (false == layers.empty()) { @@ -1607,10 +1679,12 @@ void editor_mode::render_terrain_ui() } ImGui::SetNextItemWidth(160.0f); ImGui::InputText(STR_C("Material"), m_terrain_material, IM_ARRAYSIZE(m_terrain_material)); + material_drop(m_terrain_material, IM_ARRAYSIZE(m_terrain_material)); ImGui::SameLine(); if (ImGui::Button(STR_C("Add to the palette")) && m_terrain_material[0] != '\0') m_terrain_layer = m_streamer.layer(m_terrain_material, m_terrain_material_size); - ImGui::TextDisabled("%s", STR_C("New chunks are covered with the first material. A chunk holds up to 8 of them.")); + ImGui::TextDisabled("%s", STR_C("New chunks are covered with the first material. A chunk holds up to 8 of them. " + "A texture dragged from the browser onto an entry of the palette takes its place.")); } if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0)) @@ -1623,6 +1697,7 @@ void editor_mode::render_terrain_ui() m_water_level_edited = true; ImGui::SetNextItemWidth(160.0f); ImGui::InputText(STR_C("Material##water"), m_water_material, IM_ARRAYSIZE(m_water_material)); + material_drop(m_water_material, IM_ARRAYSIZE(m_water_material)); if (ImGui::IsItemHovered()) ImGui::SetTooltip(STR_C("Empty: %s, the water material of the terrain"), m_streamer.manifest().water_material.c_str()); ImGui::BeginDisabled(m_water_points.size() < 3); @@ -2254,41 +2329,13 @@ void editor_mode::handle_terrain_click(bool const Shift) namespace { - std::uint64_t geometry_key(gfx::geometry_handle const &Geometry) - { - return (static_cast(Geometry.bank) << 32) | Geometry.chunk; - } - - // the shapes of the scenery which can be ground: the ones the editor makes itself (its terrain, roads, imagery) left out - template void visit_scenery_shapes(std::set const &Own, Visitor const &Visit) - { - auto const scenery = [&](scene::shape_node const &Shape) { - auto const &data = Shape.data(); - return Own.count(geometry_key(data.geometry)) == 0 && false == editor_orthophoto::owns(data.geometry) && false == owned_shapes::owns(data.geometry); - }; - for (std::size_t i = 0; i < sq(scene::EU07_REGIONSIDESECTIONCOUNT); ++i) - { - auto *section = simulation::Region->get_section(i); - if (section == nullptr) - continue; - for (auto &shape : section->m_shapes) - if (scenery(shape)) - Visit(shape); - for (auto &cell : section->m_cells) - for (auto &shape : cell.m_shapesopaque) - if (scenery(shape)) - Visit(shape); - } - } - - // takes the shapes of specified materials out of the scene - std::size_t drop_scenery_shapes(std::set const &Own, std::set const &Materials) + // takes the shapes of specified terrain files and materials out of the scene. returns: the count of the shapes + std::size_t drop_terrain_shapes(std::set const &Files, std::set const &Materials) { std::size_t dropped = 0; auto const dropping = [&](scene::shape_node const &Shape) { auto const &data = Shape.data(); - bool const drop = Materials.count(data.material) != 0 && Own.count(geometry_key(data.geometry)) == 0 && false == editor_orthophoto::owns(data.geometry) && - false == owned_shapes::owns(data.geometry); + bool const drop = Files.count(data.terrainfile) != 0 && Materials.count(data.material) != 0; dropped += drop ? 1 : 0; return drop; }; @@ -2299,33 +2346,63 @@ namespace continue; section->m_shapes.erase(std::remove_if(section->m_shapes.begin(), section->m_shapes.end(), dropping), section->m_shapes.end()); for (auto &cell : section->m_cells) + { cell.m_shapesopaque.erase(std::remove_if(cell.m_shapesopaque.begin(), cell.m_shapesopaque.end(), dropping), cell.m_shapesopaque.end()); + cell.m_shapestranslucent.erase(std::remove_if(cell.m_shapestranslucent.begin(), cell.m_shapestranslucent.end(), dropping), cell.m_shapestranslucent.end()); + } } return dropped; } } // namespace -std::map editor_mode::ground_materials() +void editor_mode::load_ground_files() { - std::map result; - if (simulation::Region == nullptr) - return result; - std::set own; - for (auto *terrain : active_terrains()) - own.insert(geometry_key(terrain->geometry())); - glm::dvec2 const everywhere{std::numeric_limits::max() * 0.5}; - visit_scenery_shapes(own, [&](scene::shape_node const &Shape) { - std::vector triangles; - gather_shape_triangles(Shape, -everywhere, everywhere, triangles, true); - if (false == triangles.empty()) - result[Shape.data().material] += triangles.size(); - }); - return result; + auto &conversion{m_ground_conversion}; + conversion.files.clear(); + conversion.triangles.clear(); + conversion.materials.clear(); + conversion.names.clear(); + conversion.chosen.clear(); + std::map lookup; + auto const &references{scene::terrain_file::references()}; + for (std::size_t index = 0; index < references.size(); ++index) + { + ground_conversion::source source; + source.file = references[index].file; + source.reference = static_cast(index + 1); + source.first = conversion.triangles.size(); + source.good = scene::terrain_file::read_triangles( + references[index], + [&](std::string const &Material, std::array const &Triangle) { + auto found{lookup.find(Material)}; + if (found == lookup.end()) + { + found = lookup.emplace(Material, static_cast(conversion.names.size())).first; + conversion.names.push_back(Material); + } + conversion.triangles.push_back(Triangle); + conversion.materials.push_back(found->second); + }, + &source.message, &source.text); + if (false == source.good) + { + // what was read before the failure doesn't count + conversion.triangles.resize(source.first); + conversion.materials.resize(source.first); + source.chosen = false; + } + source.count = conversion.triangles.size() - source.first; + conversion.files.push_back(std::move(source)); + } + // the terrain files hold the ground, all of it is taken unless told otherwise + for (std::size_t index = 0; index < conversion.names.size(); ++index) + conversion.chosen.insert(index); } -std::string editor_mode::convert_ground(std::set const &Materials) +std::string editor_mode::convert_ground() { - if (simulation::Region == nullptr || Materials.empty()) + auto &conversion{m_ground_conversion}; + if (simulation::Region == nullptr || conversion.chosen.empty()) return {}; if (false == ensure_terrain()) return STR_C("The heightmap terrain can't be opened"); @@ -2333,50 +2410,56 @@ std::string editor_mode::convert_ground(std::set const &Materia int const cells = heightmap::cells_for(m_terrain_spacing); double const cellsize = size / cells; - // the shapes of the heightmap terrain hold no triangles of their own, nothing to leave out - std::set const own; + // the chosen triangles of the chosen files std::vector triangles; - std::vector materials; - glm::dvec2 const everywhere{std::numeric_limits::max() * 0.5}; - visit_scenery_shapes(own, [&](scene::shape_node const &Shape) { - if (Materials.count(Shape.data().material) == 0) - return; - gather_shape_triangles(Shape, -everywhere, everywhere, triangles, true); - materials.resize(triangles.size(), Shape.data().material); - }); + std::vector materials; + std::set files; + for (auto const &source : conversion.files) + { + if (false == source.good || false == source.chosen) + continue; + files.insert(source.reference); + for (auto index = source.first; index < source.first + source.count; ++index) + { + if (conversion.chosen.count(conversion.materials[index]) == 0) + continue; + triangles.push_back(conversion.triangles[index]); + materials.push_back(conversion.materials[index]); + } + } if (triangles.empty()) - return STR_C("The chosen materials have no triangles"); + return STR_C("The chosen files and materials have no triangles"); // the triangles reaching each chunk - std::map, std::vector> buckets; + std::map> buckets; for (std::uint32_t i = 0; i < triangles.size(); ++i) { auto const &t = triangles[i]; - int const x0 = static_cast(std::floor(std::min({t[0].x, t[1].x, t[2].x}) / size)); - int const x1 = static_cast(std::floor(std::max({t[0].x, t[1].x, t[2].x}) / size)); - int const z0 = static_cast(std::floor(std::min({t[0].z, t[1].z, t[2].z}) / size)); - int const z1 = static_cast(std::floor(std::max({t[0].z, t[1].z, t[2].z}) / size)); - for (int x = x0; x <= x1; ++x) - for (int z = z0; z <= z1; ++z) + // a triangle which only touches the edge of a chunk gives it nothing but the edge, which the chunk on the other side has + auto const margin{1e-3}; + auto const low{heightmap::chunk_at(std::min({t[0].x, t[1].x, t[2].x}) + margin, std::min({t[0].z, t[1].z, t[2].z}) + margin)}; + auto const high{heightmap::chunk_at(std::max({t[0].x, t[1].x, t[2].x}) - margin, std::max({t[0].z, t[1].z, t[2].z}) - margin)}; + for (int x = low.first; x <= high.first; ++x) + for (int z = low.second; z <= high.second; ++z) buckets[{x, z}].push_back(i); } - // the chunks are covered with the material which covers the most; it goes to the palette of the terrain - std::map coverage; - for (std::size_t i = 0; i < triangles.size(); ++i) - { - auto const &t = triangles[i]; - coverage[materials[i]] += 0.5 * std::abs((t[1].x - t[0].x) * (t[2].z - t[0].z) - (t[2].x - t[0].x) * (t[1].z - t[0].z)); - } - auto const dominant = std::max_element(coverage.begin(), coverage.end(), [](auto const &A, auto const &B) { return A.second < B.second; })->first; - std::uint16_t layer = 0; - if (auto const *material = GfxRenderer->Material(dominant)) - layer = m_streamer.layer(material->GetName()); + // palette entries of the materials, added as they're needed + std::map palette; + auto const layer_of = [&](std::uint16_t const Material) { + auto const found{palette.find(Material)}; + if (found != palette.end()) + return found->second; + auto const layer{m_streamer.layer(conversion.names[Material])}; + palette.emplace(Material, layer); + return layer; + }; - // the old ground goes first, so what's made in its place isn't taken for it - auto const dropped = drop_scenery_shapes(own, Materials); std::size_t made = 0, skipped = 0; std::size_t const side = static_cast(cells) + 1; + auto const samples{heightmap::default_paint_samples}; + std::size_t const paintside = static_cast(samples) + 1; + double const paintstep = size / samples; for (auto const &bucket : buckets) { auto const &key = bucket.first; @@ -2385,11 +2468,17 @@ std::string editor_mode::convert_ground(std::set const &Materia ++skipped; continue; } - glm::dvec2 const low{key.first * size, key.second * size}; + auto const low{heightmap::chunk_corner(key)}; std::vector local; + std::vector localmaterials; for (auto const i : bucket.second) + { local.push_back(triangles[i]); + localmaterials.push_back(materials[i]); + } triangle_grid const ground(std::move(local), low - glm::dvec2{1.0}, low + glm::dvec2{size + 1.0}); + auto const ceiling{std::numeric_limits::max()}; + // heights of the grid points; where there's no ground they're taken from the nearest point which has some std::vector heights(side * side, 0.0); std::vector found(side * side, 0); bool any = false; @@ -2397,29 +2486,103 @@ std::string editor_mode::convert_ground(std::set const &Materia for (std::size_t ix = 0; ix < side; ++ix) { auto const index = iz * side + ix; - if (ground.height_at(low.x + ix * cellsize, low.y + iz * cellsize, std::numeric_limits::max(), heights[index])) + if (ground.height_at(low.x + ix * cellsize, low.y + iz * cellsize, ceiling, heights[index])) found[index] = 1, any = true; } if (false == any) continue; fill_from_nearest(heights, found, side); - auto const sampler = [&](double X, double Z) { - auto const ix = static_cast(std::clamp(std::lround((X - low.x) / cellsize), 0, cells)); - auto const iz = static_cast(std::clamp(std::lround((Z - low.y) / cellsize), 0, cells)); - return static_cast(heights[iz * side + ix]); - }; - if (m_streamer.add_chunk(key.first, key.second, m_terrain_spacing, sampler, layer)) + // the material on top at each paint sample + std::vector paint(paintside * paintside, 0); + std::vector painted(paintside * paintside, 0); + std::map counts; + for (std::size_t iz = 0; iz < paintside; ++iz) + for (std::size_t ix = 0; ix < paintside; ++ix) + { + double height; + std::uint32_t triangle; + if (false == ground.height_at(low.x + ix * paintstep, low.y + iz * paintstep, ceiling, height, &triangle)) + continue; + auto const index = iz * paintside + ix; + paint[index] = localmaterials[triangle]; + painted[index] = 1; + ++counts[paint[index]]; + } + if (counts.empty()) + { + // the points of the grid hit the ground, the samples in between didn't; the nearest triangle covers the chunk + counts[localmaterials.front()] = 1; + std::fill(paint.begin(), paint.end(), localmaterials.front()); + } + else + fill_from_nearest(paint, painted, paintside); + // a chunk holds up to 8 materials, the ones covering the most; the rest is given to the one covering the most + std::vector> ranking; + for (auto const &count : counts) + ranking.emplace_back(count.second, count.first); + std::sort(ranking.begin(), ranking.end(), [](auto const &A, auto const &B) { return A.first > B.first; }); + if (ranking.size() > heightmap::max_chunk_layers) + ranking.resize(heightmap::max_chunk_layers); + + auto data{heightmap::chunk_data::make_flat(key, m_terrain_spacing, 0.f)}; + for (std::size_t index = 0; index < data->heights.size(); ++index) + data->heights[index] = static_cast(heights[index]); + std::map slots; + for (std::size_t slot = 0; slot < ranking.size(); ++slot) + { + data->layers[slot] = layer_of(ranking[slot].second); + slots.emplace(ranking[slot].second, slot); + } + if (ranking.size() > 1) + { + auto const plane{paintside * paintside}; + data->paint = samples; + data->weights.assign(plane * ranking.size(), 0); + for (std::size_t index = 0; index < plane; ++index) + { + auto const slot{slots.find(paint[index])}; + data->weights[(slot != slots.end() ? slot->second : 0) * plane + index] = 255; + } + } + if (m_streamer.add_chunk(std::move(data))) ++made; } - ground_tiles.clear(); - forget_ground_shapes(); + forget_ground(); - auto const erased = scene::Layers.erase_shapes(Materials); - auto summary = format(STR_C("Ground converted: %zu chunks of heightmap terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the scenery files on save"), made, dropped, erased.first); + // the converted ground goes from the scene, and from the text files on save + std::set handles; + for (auto const material : conversion.chosen) + if (material < conversion.names.size()) + handles.insert(GfxRenderer->Fetch_Material(conversion.names[material])); + auto const dropped = drop_terrain_shapes(files, handles); + std::set layers; + std::size_t binary = 0; + for (auto const &source : conversion.files) + { + if (false == source.good || false == source.chosen) + continue; + if (false == source.text) + { + ++binary; + continue; + } + auto const textfile{source.file + ".txtf"}; + for (std::size_t layer = 1; layer <= scene::Layers.size(); ++layer) + { + auto name{scene::Layers.layer(static_cast(layer)).name}; + replace_slashes(name); + if (name.ends_with(textfile)) + layers.insert(static_cast(layer)); + } + } + auto const erased = scene::Layers.erase_shapes(handles, &layers); + auto summary = format(STR_C("Ground converted: %zu chunks of heightmap terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the terrain files on save"), made, dropped, erased.first); if (skipped > 0) summary += format(STR_C(", %zu chunks kept as there was terrain already"), skipped); if (erased.second > 0) summary += format(STR_C(", %zu nodes stay as their files can't be rewritten"), erased.second); + if (binary > 0) + summary += format(STR_C(". %zu of the files are binary (.btf) without their text: their triangles come back with the next load, until the files are taken out of the terrain directive"), binary); WriteLog("Editor: " + summary, logtype::generic); return summary; } @@ -5261,7 +5424,8 @@ void editor_mode::render_change_history(){ s.action == EditorSnapshot::Action::Scale ? "SCA" : s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : - s.action == EditorSnapshot::Action::Array ? "ARR" : "OTH", + s.action == EditorSnapshot::Action::Array ? "ARR" : + s.action == EditorSnapshot::Action::Terrain ? "TER" : "OTH", s.node_name.empty() ? "(noname)" : s.node_name.c_str(), s.position.x, s.position.y, s.position.z); diff --git a/application/editormode.h b/application/editormode.h index ebe40cd7..35a5645d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -132,7 +132,7 @@ class editor_mode : public application_mode, private editor_track::observer struct stored_profile; struct EditorSnapshot { - enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend }; + enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain }; // copy of a model an array made struct array_copy { @@ -170,6 +170,7 @@ class editor_mode : public application_mode, private editor_track::observer std::vector profile_library; std::vector copies; // models an array added to the one it was made of bool joined{false}; // undone and redone together with the change before it + std::shared_ptr terrain; // the heightmap terrain from before the change (Action::Terrain) }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -183,6 +184,12 @@ class editor_mode : public application_mode, private editor_track::observer // a single step of the history, see undo_last() void undo_one(); void redo_one(); + // the changes of the heightmap terrain since the last step become a step of the history: a stroke of the brush, + // or what a frame did; the next change is recorded from now on + void terrain_history_step(); + // puts the terrain back as the step recorded it, the step left with the state it replaced + void restore_terrain(EditorSnapshot &Snapshot); + bool m_history_busy{false}; // undo or redo under way void handle_brush_mouse_hold(int Action, int Button); void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value); // members @@ -313,18 +320,59 @@ class editor_mode : public application_mode, private editor_track::observer void draw_terrain_overlay(); // forgets what's known about the ground, after the terrain changed void forget_ground(); - // the scenery ground: triangles of each material, the ones the editor makes itself left out - std::map ground_materials(); - // replaces the triangles of specified materials with chunks of heightmap terrain of their shape; the scenery files lose them on save. - // returns: summary for the user - std::string convert_ground(std::set const &Materials); + // conversion of the ground of the terrain files of the scenery (terrain directive, .txtf/.btf) to heightmap terrain struct ground_conversion { + struct source + { + std::string file; // as the terrain directive names it + std::uint16_t reference{0}; // see scene::terrain_file::reference_of() + bool text{false}; // read from the text file, rather than the binary one + bool good{false}; + bool chosen{true}; + std::string message; + std::size_t first{0}, count{0}; // its triangles + }; bool open{false}; - std::map materials; - std::set chosen; + std::vector files; + std::vector> triangles; + std::vector materials; // of the triangles: index of the name + std::vector names; // of the materials + std::set chosen; // materials to convert std::string status; } m_ground_conversion; + // reads the triangles of the terrain files of the scenery for the conversion + void load_ground_files(); + // the folder of the textures as a browser, in the window of the node bank while the terrain is edited: the textures and the + // materials can be dragged onto the fields which take a material + struct texture_browser + { + struct item + { + std::string name; // as the materials are named: relative to the folder of the textures, without the extension + std::string label; // the file, without the folder + bool material{false}; // there's a .mat file of the name + }; + std::string folder; // relative to the folder of the textures, empty or with a trailing slash + std::vector folders; // under the current one + std::vector items; + bool listed{false}; + char filter[64]{}; + float size{96.f}; // of the images, pixels + std::string selected; + std::map thumbnails; // texture shown for the item of the name, 0: none + int loads{0}; // textures read in the frame, a few at most so the browser doesn't stall the editor + } m_textures; + void render_texture_browser(); + // texture of the image of the item of the browser, 0 while there's none + std::uint64_t texture_thumbnail(texture_browser::item const &Item); + // takes a material dragged from the texture browser onto the item drawn last. returns: true if one was dropped, its name in Name + static bool material_drop(std::string &Name); + // the same, for a text field + static bool material_drop(char *Buffer, std::size_t Size); + // replaces the chosen triangles of the chosen terrain files with chunks of heightmap terrain of their shape, painted with + // their materials; the files lose them on save. returns: summary for the user + std::string convert_ground(); void render_ground_conversion(); // handles a click of the chunk, spacing and water tools (Shift: the other way round) void handle_terrain_click(bool const Shift); diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index f23eff53..4fafe1d3 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -674,54 +674,84 @@ void editor_mode::profile_restore_ground() void editor_mode::render_ground_conversion() { auto &conversion{m_ground_conversion}; + ImGui::PushID("ground_conversion"); if (false == conversion.open) { - if (ImGui::Button(STR_C("Convert the scenery ground to heightmap terrain..."))) + if (ImGui::Button(STR_C("Convert the terrain files to heightmap terrain..."))) { conversion.open = true; - conversion.materials = ground_materials(); - conversion.chosen.clear(); - // the material with the most triangles is the ground, most likely - auto const most{std::max_element(conversion.materials.begin(), conversion.materials.end(), [](auto const &A, auto const &B) { return A.second < B.second; })}; - if (most != conversion.materials.end()) - conversion.chosen.insert(most->first); + load_ground_files(); } if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the chosen materials with heightmap terrain of the same shape,\nwhich can be shaped, sculpted and painted; the scenery files lose these triangles on save")); + ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the terrain files of the scenery (terrain directive, .txtf/.btf) with heightmap terrain\n" + "of the same shape, painted with their materials; the files lose these triangles on save")); if (false == conversion.status.empty()) ImGui::TextWrapped("%s", conversion.status.c_str()); + ImGui::PopID(); return; } - ImGui::TextWrapped("%s", STR_C("Materials of the scenery triangles: tick the ones which are the ground")); - if (conversion.materials.empty()) - ImGui::TextDisabled("%s", STR_C("The scenery has no triangles")); - for (auto const &entry : conversion.materials) + if (conversion.files.empty()) + ImGui::TextWrapped("%s", STR_C("The scenery has no terrain files: no terrain directive names a .txtf or .btf file")); + else + ImGui::TextUnformatted(STR_C("Terrain files of the scenery:")); + for (std::size_t index = 0; index < conversion.files.size(); ++index) { - auto const *material{GfxRenderer->Material(entry.first)}; - auto const name{material != nullptr ? material->GetName() : std::string{"?"}}; - bool chosen{conversion.chosen.count(entry.first) != 0}; - ImGui::PushID(static_cast(entry.first)); - if (ImGui::Checkbox(format(STR_C("%s (%zu triangles)"), name.c_str(), entry.second).c_str(), &chosen)) + auto &source{conversion.files[index]}; + ImGui::PushID(static_cast(index)); + ImGui::BeginDisabled(false == source.good); + ImGui::Checkbox(source.file.c_str(), &source.chosen); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (source.good) + ImGui::TextDisabled(source.text ? STR_C("(text, %zu triangles)") : STR_C("(binary, %zu triangles)"), source.count); + else + ImGui::TextDisabled("(%s)", source.message.c_str()); + ImGui::PopID(); + } + // the materials of the chosen files + std::vector counts(conversion.names.size(), 0); + for (auto const &source : conversion.files) + if (source.good && source.chosen) + for (auto index = source.first; index < source.first + source.count; ++index) + ++counts[conversion.materials[index]]; + if (false == conversion.names.empty()) + ImGui::TextUnformatted(STR_C("Materials: tick the ones which are the ground")); + for (std::size_t index = 0; index < conversion.names.size(); ++index) + { + if (counts[index] == 0) + continue; + bool chosen{conversion.chosen.count(index) != 0}; + ImGui::PushID(static_cast(index) + 0x10000); + if (ImGui::Checkbox(format(STR_C("%s (%zu triangles)"), conversion.names[index].c_str(), counts[index]).c_str(), &chosen)) { if (chosen) - conversion.chosen.insert(entry.first); + conversion.chosen.insert(index); else - conversion.chosen.erase(entry.first); + conversion.chosen.erase(index); } ImGui::PopID(); } - ImGui::TextWrapped("%s", STR_C("The chunks are covered with the material which covers the most, added to the palette of the terrain. Until saved, loading the scenery again takes it all back.")); - if (conversion.chosen.empty()) - ImGui::TextDisabled("%s", STR_C("Tick at least one material")); - else if (ImGui::Button(STR_C("Convert"))) + ImGui::TextWrapped("%s", STR_C("The chunks are painted with the materials of the triangles, up to 8 of them per chunk, added to the palette of the terrain. " + "Until saved, loading the scenery again takes it all back.")); + auto const ready{std::any_of(conversion.files.begin(), conversion.files.end(), [](auto const &Source) { return Source.good && Source.chosen; }) && false == conversion.chosen.empty()}; + ImGui::BeginDisabled(false == ready); + if (ImGui::Button(STR_C("Convert"))) { - conversion.status = convert_ground(conversion.chosen); + conversion.status = convert_ground(); conversion.open = false; profile_resample(); } + ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button(STR_C("Cancel"))) conversion.open = false; + if (false == conversion.open) + { + // the triangles take memory, they're read again the next time + conversion.triangles = {}; + conversion.materials = {}; + } + ImGui::PopID(); } void editor_mode::render_profile_earthworks() diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index c44fd133..3052df40 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -72,6 +72,10 @@ http://mozilla.org/MPL/2.0/. // spread logs the paths the spread from the track takes // newscenery the editor starts again with the wizard of a new scenery // vehicles logs each vehicle: its head, facing, and what its couplers hold +// terrainmode the terrain field of work, with its texture browser +// terrainsculpt , terrainpaint as a stroke of the brush +// terrainprobe logs the height of the heightmap terrain and the materials of the chunk +// terrainconvert converts every material of the terrain files of the scenery // screenshot // save // log @@ -1134,6 +1138,53 @@ void editor_mode::selftest_step() } } } + else if (command == "terrainmode") + { + m_terrain_open = true; + WriteLog("SELFTEST terrainmode"); + } + else if (command == "terrainsculpt" || command == "terrainpaint") + { + double x{0.0}, z{0.0}, radius{10.0}, value{1.0}; + words >> x >> z >> radius >> value; + auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value) : m_streamer.paint(x, z, radius, 1.0, static_cast(value))}; + forget_ground(); + WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed")); + } + else if (command == "terrainprobe") + { + double x{0.0}, z{0.0}, height{0.0}; + words >> x >> z; + auto const *chunk{m_streamer.terrain_at(x, z)}; + std::string materials; + if (chunk != nullptr) + for (std::size_t slot = 0; slot < std::max(1, chunk->data().layer_count()); ++slot) + { + auto const layer{chunk->data().layers[slot]}; + auto const &palette{m_streamer.manifest().layers}; + materials += (layer < palette.size() ? palette[layer].material : std::string{"?"}) + " "; + } + auto const found{m_streamer.height_at(x, z, height)}; + WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str())); + } + else if (command == "terrainconvert") + { + load_ground_files(); + for (auto const &source : m_ground_conversion.files) + { + glm::dvec3 low{1e30}, high{-1e30}; + for (auto index = source.first; index < source.first + source.count; ++index) + for (auto const &vertex : m_ground_conversion.triangles[index]) + { + low = glm::min(low, vertex); + high = glm::max(high, vertex); + } + WriteLog("SELFTEST terrain file " + source.file + (source.text ? " (text): " : " (binary): ") + source.message + + format(", from %.1f %.1f %.1f to %.1f %.1f %.1f", low.x, low.y, low.z, high.x, high.y, high.z)); + } + m_ground_conversion.status = convert_ground(); + WriteLog("SELFTEST terrainconvert: " + m_ground_conversion.status); + } else if (command == "quit") { if (false == test.quitting) diff --git a/application/editormodetextures.cpp b/application/editormodetextures.cpp new file mode 100644 index 00000000..f18dff0a --- /dev/null +++ b/application/editormodetextures.cpp @@ -0,0 +1,279 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" +#include "utilities/translation.h" +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + +// payload of a material dragged from the texture browser: its name, with the terminating zero +char const *const material_payload{"EU07_MATERIAL"}; + +// files the browser shows: textures, and the materials, which are kept with them +bool texture_file(std::string const &Extension) +{ + return Extension == ".dds" || Extension == ".tga" || Extension == ".png" || Extension == ".bmp" || Extension == ".jpg" || Extension == ".ktx" || Extension == ".tex"; +} + +std::string lowercase(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +// the text cut to fit the width, with an ellipsis +std::string fitted(std::string const &Text, float const Width) +{ + if (ImGui::CalcTextSize(Text.c_str()).x <= Width) + return Text; + auto result{Text}; + while (false == result.empty() && ImGui::CalcTextSize((result + "...").c_str()).x > Width) + result.pop_back(); + return result + "..."; +} + +} // namespace + +bool editor_mode::material_drop(std::string &Name) +{ + if (false == ImGui::BeginDragDropTarget()) + return false; + auto dropped{false}; + if (auto const *payload{ImGui::AcceptDragDropPayload(material_payload)}) + { + auto const *data{static_cast(payload->Data)}; + Name.assign(data, std::find(data, data + payload->DataSize, '\0')); + dropped = false == Name.empty(); + } + ImGui::EndDragDropTarget(); + return dropped; +} + +bool editor_mode::material_drop(char *Buffer, std::size_t const Size) +{ + std::string name; + if (Size == 0 || false == material_drop(name)) + return false; + auto const length{std::min(name.size(), Size - 1)}; + std::memcpy(Buffer, name.data(), length); + Buffer[length] = '\0'; + return true; +} + +std::uint64_t editor_mode::texture_thumbnail(texture_browser::item const &Item) +{ + auto &browser{m_textures}; + auto lookup{browser.thumbnails.find(Item.name)}; + if (lookup == browser.thumbnails.end()) + { + // reading a texture takes a moment, a few of them go in a frame + if (browser.loads >= 2) + return 0; + ++browser.loads; + texture_handle texture{null_handle}; + if (Item.material) + { + auto const material{GfxRenderer->Fetch_Material(Item.name)}; + if (auto const *data{GfxRenderer->Material(material)}; material != null_handle && data != nullptr) + texture = data->GetTexture(0); + } + else + texture = GfxRenderer->Fetch_Texture(Item.name, true); + lookup = browser.thumbnails.emplace(Item.name, texture).first; + } + if (lookup->second == null_handle) + return 0; + auto &texture{GfxRenderer->Texture(lookup->second)}; + if (false == texture.get_is_ready()) + texture.create(); + return texture.get_is_ready() ? static_cast(texture.get_id()) : 0; +} + +void editor_mode::render_texture_browser() +{ + auto &browser{m_textures}; + browser.loads = 0; + if (false == browser.listed) + { + browser.listed = true; + browser.folders.clear(); + browser.items.clear(); + std::map items; + std::error_code error; + for (std::filesystem::directory_iterator entry{std::string{paths::textures} + browser.folder, error}, end; false == static_cast(error) && entry != end; entry.increment(error)) + { + auto const path{entry->path()}; + if (entry->is_directory(error)) + { + browser.folders.push_back(path.filename().string()); + continue; + } + auto const extension{lowercase(path.extension().string())}; + auto const material{extension == ".mat"}; + if (false == material && false == texture_file(extension)) + continue; + auto const stem{path.stem().string()}; + auto &item{items[stem]}; + item.name = browser.folder + stem; + item.label = stem; + item.material |= material; + } + for (auto &item : items) + browser.items.push_back(std::move(item.second)); + std::sort(browser.folders.begin(), browser.folders.end()); + } + + // where the browser is, and the folders under it + ImGui::BeginDisabled(browser.folder.empty()); + if (ImGui::Button(STR_C("Up"))) + { + auto folder{browser.folder}; + if (false == folder.empty()) + folder.pop_back(); + auto const slash{folder.rfind('/')}; + browser.folder = (slash == std::string::npos ? std::string{} : folder.substr(0, slash + 1)); + browser.listed = false; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Refresh"))) + { + browser.listed = false; + browser.thumbnails.clear(); + } + ImGui::SameLine(); + ImGui::TextUnformatted((std::string{paths::textures} + browser.folder).c_str()); + ImGui::SetNextItemWidth(std::max(80.0f, ImGui::GetContentRegionAvail().x * 0.5f)); + ImGui::InputTextWithHint("##texturefilter", STR_C("Filter"), browser.filter, IM_ARRAYSIZE(browser.filter)); + ImGui::SameLine(); + ImGui::SetNextItemWidth(std::max(60.0f, ImGui::GetContentRegionAvail().x)); + ImGui::SliderFloat("##texturesize", &browser.size, 48.0f, 256.0f, "%.0f px"); + if (false == browser.folders.empty()) + { + // the folders flow along the width of the window + auto const right{ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x}; + for (std::size_t index = 0; index < browser.folders.size(); ++index) + { + auto const label{"[" + browser.folders[index] + "]"}; + auto const width{ImGui::CalcTextSize(label.c_str()).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + if (index > 0) + { + ImGui::SameLine(); + if (ImGui::GetCursorScreenPos().x + width > right) + ImGui::NewLine(); + } + ImGui::PushID(static_cast(index)); + if (ImGui::SmallButton(label.c_str())) + { + browser.folder += browser.folders[index] + "/"; + browser.listed = false; + } + ImGui::PopID(); + } + if (false == browser.listed) + { + // the list is made anew in the next frame + return; + } + } + ImGui::Separator(); + + std::vector shown; + auto const filter{lowercase(browser.filter)}; + for (auto const &item : browser.items) + if (filter.empty() || lowercase(item.label).find(filter) != std::string::npos) + shown.push_back(&item); + if (shown.empty()) + { + ImGui::TextDisabled("%s", STR_C("No textures here")); + return; + } + ImGui::TextDisabled("%s", STR_C("Drag a texture onto a field of a material; double click: add it to the palette of the terrain")); + + ImGui::BeginChild("##texturecards"); + auto const &style{ImGui::GetStyle()}; + auto const side{std::max(16.0f, browser.size * std::max(1.0f, Global.ui_scale))}; + auto const cellheight{side + ImGui::GetTextLineHeight() + style.ItemInnerSpacing.y}; + auto const columns{std::max(1, static_cast((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (side + style.ItemSpacing.x)))}; + auto const rows{(static_cast(shown.size()) + columns - 1) / columns}; + auto *drawlist{ImGui::GetWindowDrawList()}; + ImGuiListClipper clipper; + clipper.Begin(rows, cellheight + style.ItemSpacing.y); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + for (int column = 0; column < columns; ++column) + { + auto const index{static_cast(row) * columns + column}; + if (index >= shown.size()) + break; + auto const &item{*shown[index]}; + if (column > 0) + ImGui::SameLine(); + ImGui::PushID(item.name.c_str()); + ImGui::BeginGroup(); + auto const corner{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##card", ImVec2(side, cellheight)); + auto const hovered{ImGui::IsItemHovered()}; + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + { + browser.selected = item.name; + std::snprintf(m_terrain_material, IM_ARRAYSIZE(m_terrain_material), "%s", item.name.c_str()); + } + if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && m_streamer.active()) + m_terrain_layer = m_streamer.layer(item.name, m_terrain_material_size); + auto const texture{texture_thumbnail(item)}; + if (ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload(material_payload, item.name.c_str(), item.name.size() + 1); + if (texture != 0) + ImGui::Image(static_cast(texture), ImVec2(64.0f, 64.0f), ImVec2(0, 1), ImVec2(1, 0)); + ImGui::TextUnformatted(item.name.c_str()); + ImGui::EndDragDropSource(); + } + ImVec2 const low{corner.x, corner.y}, high{corner.x + side, corner.y + side}; + if (texture != 0) + drawlist->AddImage(static_cast(texture), low, high, ImVec2(0, 1), ImVec2(1, 0)); + else + { + drawlist->AddRectFilled(low, high, ImGui::GetColorU32(ImGuiCol_FrameBg)); + drawlist->AddText(ImVec2(low.x + style.FramePadding.x, low.y + style.FramePadding.y), ImGui::GetColorU32(ImGuiCol_TextDisabled), "..."); + } + if (item.material) + drawlist->AddText(ImVec2(low.x + style.FramePadding.x, high.y - ImGui::GetTextLineHeight() - style.FramePadding.y), IM_COL32(255, 220, 120, 255), "mat"); + if (browser.selected == item.name || hovered) + drawlist->AddRect(low, high, ImGui::GetColorU32(browser.selected == item.name ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), 0.0f, 0, 2.0f); + drawlist->AddText(ImVec2(low.x, high.y + style.ItemInnerSpacing.y), ImGui::GetColorU32(ImGuiCol_Text), fitted(item.label, side).c_str()); + if (hovered) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(item.name.c_str()); + ImGui::TextDisabled("%s", item.material ? STR_C("material") : STR_C("texture")); + ImGui::EndTooltip(); + } + ImGui::EndGroup(); + ImGui::PopID(); + } + } + } + ImGui::EndChild(); +} diff --git a/application/editoruilayer.h b/application/editoruilayer.h index deb58bfb..020fd4be 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -128,6 +128,13 @@ class editor_ui : public ui_layer void set_workspace(workspace const Workspace) { m_workspace = Workspace; + auto const &bank{m_workspacebank[static_cast(Workspace)]}; + m_nodebankpanel.stand_in(bank.first, bank.second); + } + // content of the window of the node bank while the field of work is chosen, in place of the node bank + void set_workspace_bank(workspace const Workspace, std::function Content, std::string Title) + { + m_workspacebank[static_cast(Workspace)] = {std::move(Content), std::move(Title)}; } void set_workspace_options(workspace const Workspace, std::function Tools, std::function Selection = {}) { @@ -205,6 +212,7 @@ class editor_ui : public ui_layer workspace m_workspace{workspace::none}; std::array, static_cast(workspace::count_)> m_workspacetools; std::array, static_cast(workspace::count_)> m_workspaceselection; + std::array, std::string>, static_cast(workspace::count_)> m_workspacebank; // scene window: the models of the scenery by layer and group, rebuilt when their number changes struct scene_entry { diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 2e177f42..79808c2d 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -934,6 +934,14 @@ void nodebank_panel::render() { ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y)); } + if (m_standin) + { + // the same window, so it keeps its place in the dock + if (ImGui::Begin((m_standintitle + "###" + name()).c_str(), nullptr, flags)) + m_standin(); + ImGui::End(); + return; + } auto const panelname{(title.empty() ? name() : title) + "###" + name()}; if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) diff --git a/application/editoruipanels.h b/application/editoruipanels.h index dbb3f8bc..f79946b4 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -156,6 +156,13 @@ class nodebank_panel : public ui_panel std::string set_name(model_set_ref const &Ref, char const *Manuallabel) const; // shows the set manager window, optionally with specified user set selected void open_sets_window(int const Setid = 0); + // content drawn in the window of the node bank in its place, with its own title (a field of work of the editor uses the window + // for its own browser); none: the node bank + void stand_in(std::function Content, std::string Title) + { + m_standin = std::move(Content); + m_standintitle = std::move(Title); + } private: @@ -183,6 +190,8 @@ class nodebank_panel : public ui_panel // true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take bool node_selected() const; // members: + std::function m_standin; + std::string m_standintitle; std::vector>> m_nodebank; char m_nodesearch[128]; std::shared_ptr m_selectedtemplate; @@ -295,4 +304,4 @@ class functions_panel : public ui_panel scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel std::string m_groupprefix; std::vector m_grouplines; -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/editor/editorGroundMesh.hpp b/editor/editorGroundMesh.hpp index 068b3fbb..4fc3f10b 100644 --- a/editor/editorGroundMesh.hpp +++ b/editor/editorGroundMesh.hpp @@ -120,13 +120,15 @@ class ground_mesh } private: + // NOTE: clamped before the conversion: a rectangle asked about can reach far beyond the mesh (to infinity even), + // and a float out of the range of int doesn't turn into anything sensible int column(float const X) const { - return std::clamp(static_cast(std::floor((X - m_low.x) / m_cellsize)), 0, m_columns - 1); + return static_cast(std::clamp(std::floor((X - m_low.x) / m_cellsize), 0.f, static_cast(m_columns - 1))); } int row(float const Z) const { - return std::clamp(static_cast(std::floor((Z - m_low.y) / m_cellsize)), 0, m_rows - 1); + return static_cast(std::clamp(std::floor((Z - m_low.y) / m_cellsize), 0.f, static_cast(m_rows - 1))); } // cells the bounds of a triangle reach: first column, first row, last column, last row std::array cells(triangle const &Triangle) const diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 482f436a..a6b8606d 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -136,6 +136,8 @@ void terrain_streamer::close() m_loading.clear(); m_removed.clear(); m_palette.clear(); + m_recording = 0; + m_change = change(); m_active = false; m_name.clear(); m_directory.clear(); @@ -159,6 +161,8 @@ void terrain_streamer::reset() m_removed.clear(); m_palette.clear(); clear_water(false); + m_recording = 0; + m_change = change(); m_active = false; m_name.clear(); m_directory.clear(); @@ -898,6 +902,24 @@ bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, std::functioncells; ++iz) for (int ix = 0; ix <= data->cells; ++ix) data->heights[static_cast(iz) * data->side() + ix] = Height(low.x + ix * step, low.y + iz * step); + return add_chunk(std::move(data)); +} + +bool terrain_streamer::add_chunk(std::shared_ptr Data) +{ + if (Data == nullptr || false == Data->valid()) + return false; + auto const key{Data->key}; + if (false == m_active || load_now(key) != nullptr || m_chunks.count(key) > 0) + return false; + if (false == in_region(glm::dvec2{chunk_centre(key).x, chunk_centre(key).z})) + return false; + for (auto &layer : Data->layers) + if (layer != heightmap::no_layer && layer >= m_manifest.layers.size()) + layer = 0; + auto data{std::move(Data)}; + auto const low{data->corner()}; + remember(key, nullptr); auto chunk{std::make_unique(std::move(data))}; chunk->m_modified = true; auto const [lowest, highest]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())}; @@ -915,6 +937,12 @@ bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, std::functioncells()) return; + remember(*chunk); chunk->replace(chunk->data().resampled(Spacing), true, false); auto const low{chunk->data().corner()}; apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size}); @@ -957,6 +986,7 @@ bool terrain_streamer::reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast(iz) * current.side() + ix]); if (false == shaped) continue; + remember(*chunk); auto &data{chunk->edit(true, false)}; (data.base.empty() ? data.heights : data.base) = std::move(heights); if (false == data.base.empty()) @@ -1081,6 +1111,7 @@ bool terrain_streamer::paint(double X, double Z, double Radius, double Strength, if (false == painted) continue; data->compact_layers(); + remember(*chunk); chunk->replace(std::move(data), false, true); changed = true; } @@ -1092,6 +1123,7 @@ std::uint16_t terrain_streamer::layer(std::string const &Material, float const S for (std::size_t i = 0; i < m_manifest.layers.size(); ++i) if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size)) return static_cast(i); + remember_manifest(); m_manifest.layers.push_back({Material, std::max(0.f, Size)}); m_manifestchanged = true; return static_cast(m_manifest.layers.size() - 1); @@ -1101,6 +1133,7 @@ void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size) { if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].size == Size) return; + remember_manifest(); m_manifest.layers[Layer].size = std::max(0.f, Size); m_manifestchanged = true; // the materials of the chunks using it are made again @@ -1109,6 +1142,19 @@ void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size) entry.second->m_paintedversion = 0; } +void terrain_streamer::layer_material(std::uint16_t const Layer, std::string const &Material) +{ + if (Layer >= m_manifest.layers.size() || Material.empty() || m_manifest.layers[Layer].material == Material) + return; + remember_manifest(); + m_manifest.layers[Layer].material = Material; + m_manifestchanged = true; + m_palette.erase(Layer); + for (auto &entry : m_chunks) + if (entry.second->data().slot_of(Layer) >= 0 || (Layer == 0 && entry.second->data().layer_count() == 0)) + entry.second->m_paintedversion = 0; +} + void terrain_streamer::compress(bool const State) { if (m_manifest.compress == State) @@ -1128,6 +1174,7 @@ void terrain_streamer::default_spacing(float const Spacing) void terrain_streamer::water(std::vector Water) { + remember_manifest(); m_manifest.water = std::move(Water); m_manifestchanged = true; m_waterchanged = true; @@ -1155,12 +1202,14 @@ void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const & // no modifier reaches the chunk (any more): its own heights are what's left if (false == current.base.empty()) { + remember(Chunk); auto &data{Chunk.edit(true, false)}; data.heights = std::move(data.base); data.base.clear(); } return; } + remember(Chunk); auto &data{Chunk.edit(true, false)}; if (data.base.empty()) data.base = data.heights; @@ -1187,6 +1236,7 @@ void terrain_streamer::apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const & void terrain_streamer::modifier(heightmap::modifier Modifier) { + remember_manifest(); auto bounds{Modifier.bounds()}; auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Modifier.name; })}; if (existing != m_manifest.modifiers.end()) @@ -1208,6 +1258,7 @@ bool terrain_streamer::remove_modifier(std::string const &Name) auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })}; if (existing == m_manifest.modifiers.end()) return false; + remember_manifest(); auto const bounds{existing->bounds()}; m_manifest.modifiers.erase(existing); m_manifestchanged = true; @@ -1222,6 +1273,103 @@ bool terrain_streamer::modified() const return std::any_of(m_chunks.begin(), m_chunks.end(), [](auto const &Entry) { return Entry.second->modified(); }); } +// undo + +void terrain_streamer::begin_change() +{ + if (m_recording++ == 0) + m_change = change(); +} + +terrain_streamer::change terrain_streamer::end_change() +{ + if (m_recording == 0) + return change(); + if (--m_recording > 0) + return change(); // the outer change goes on + change result{std::move(m_change)}; + m_change = change(); + return result; +} + +void terrain_streamer::remember(chunk_key const &Key, heightmap::chunk_ptr const &Data) +{ + if (m_recording > 0) + m_change.chunks.emplace(Key, Data); // only the first state counts +} + +void terrain_streamer::remember_manifest() +{ + if (m_recording == 0 || m_change.manifest) + return; + m_change.manifest = true; + m_change.layers = m_manifest.layers; + m_change.water = m_manifest.water; + m_change.modifiers = m_manifest.modifiers; +} + +terrain_streamer::change terrain_streamer::restore(change const &Change) +{ + change opposite; + if (false == m_active) + return opposite; + for (auto const &[key, data] : Change.chunks) + { + auto *current{load_now(key)}; + opposite.chunks.emplace(key, current != nullptr ? current->shared() : nullptr); + if (data == nullptr) + { + if (current != nullptr) + { + undraw(*current); + m_chunks.erase(key); + m_removed.insert(key); + touch_neighbours(key); + } + continue; + } + if (current != nullptr) + { + current->replace(data, true, true); + } + else + { + auto chunk{std::make_unique(data)}; + chunk->m_modified = true; + auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())}; + auto const corner{data->corner()}; + chunk->m_low = {corner.x, *low, corner.y}; + chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size}; + m_chunks.emplace(key, std::move(chunk)); + m_removed.erase(key); + m_loading.erase(key); + } + touch_neighbours(key); + } + if (Change.manifest) + { + opposite.manifest = true; + opposite.layers = m_manifest.layers; + opposite.water = m_manifest.water; + opposite.modifiers = m_manifest.modifiers; + auto const samelayers{std::equal(Change.layers.begin(), Change.layers.end(), m_manifest.layers.begin(), m_manifest.layers.end(), + [](heightmap::layer_def const &A, heightmap::layer_def const &B) { return A.material == B.material && A.size == B.size; })}; + m_manifest.layers = Change.layers; + m_manifest.modifiers = Change.modifiers; + m_manifest.water = Change.water; + m_manifestchanged = true; + m_waterchanged = true; + if (false == samelayers) + { + // the materials of the chunks are made anew from the palette as it was + m_palette.clear(); + for (auto &entry : m_chunks) + entry.second->m_paintedversion = 0; + } + } + return opposite; +} + bool terrain_streamer::save(std::string *Error) { if (false == m_active) diff --git a/editor/editorTerrainStreamer.hpp b/editor/editorTerrainStreamer.hpp index 90eac7ff..2e99e449 100644 --- a/editor/editorTerrainStreamer.hpp +++ b/editor/editorTerrainStreamer.hpp @@ -84,6 +84,8 @@ class terrain_streamer bool add_chunk(int Cx, int Cz, float Spacing, float Height, std::uint16_t Layer = 0); // a chunk at (Cx, Cz), heights given for each grid point by Height (from its world position) bool add_chunk(int Cx, int Cz, float Spacing, std::function const &Height, std::uint16_t Layer = 0); + // a chunk made elsewhere, at the place its key says; its layers are indices of the palette + bool add_chunk(std::shared_ptr Data); void remove_chunk(int Cx, int Cz); // changes the point spacing of the chunk at (Cx, Cz), heights resampled void spacing(int Cx, int Cz, float Spacing); @@ -99,6 +101,8 @@ class terrain_streamer // palette entry of the material, added to the palette if needed std::uint16_t layer(std::string const &Material, float Size = 0.f); void layer_size(std::uint16_t Layer, float Size); + // gives the palette entry Layer another material; the chunks painted with it change with it + void layer_material(std::uint16_t Layer, std::string const &Material); void compress(bool State); void default_spacing(float Spacing); // bodies of water @@ -110,6 +114,26 @@ class terrain_streamer std::vector const &modifiers() const { return m_manifest.modifiers; } // changed since the last save bool modified() const; + + // undo: the state of what a change touched, from before it + struct change + { + std::map chunks; // the data the chunks had; null: there was no chunk + bool manifest{false}; // the palette, the water and the modifiers below were changed + std::vector layers; + std::vector water; + std::vector modifiers; + + bool empty() const { return chunks.empty() && false == manifest; } + }; + // from now on the state of what's changed is kept, until end_change(); nested calls join the change in progress + void begin_change(); + // returns: the state from before the changes made since begin_change() (empty if nothing changed) + change end_change(); + bool recording() const { return m_recording > 0; } + // puts the terrain back in the state recorded. returns: the state it replaced, to go forward again + change restore(change const &Change); + // writes the description and the changed pack files. returns: true on success, Error describing the failure bool save(std::string *Error = nullptr); @@ -200,6 +224,10 @@ class terrain_streamer void build_water(); void clear_water(bool const Touch); std::string pack_path(heightmap::pack_key const &Key) const; + // the state of a chunk (null: no chunk) or of the description is kept for the change in progress, if it's the first time + void remember(chunk_key const &Key, heightmap::chunk_ptr const &Data); + void remember(editor_terrain const &Chunk) { remember(Chunk.key(), Chunk.shared()); } + void remember_manifest(); bool m_active{false}; std::string m_name; @@ -221,6 +249,9 @@ class terrain_streamer gfx::geometrybank_handle m_waterbank{0, 0}; bool m_waterchanged{false}; + int m_recording{0}; + change m_change; + glm::dvec3 m_camera{0.0}; glm::dvec3 m_velocity{0.0}; double m_cameratime{0.0}; diff --git a/rendering/opengl33terrain.cpp b/rendering/opengl33terrain.cpp index 48faccfe..35c3fd61 100644 --- a/rendering/opengl33terrain.cpp +++ b/rendering/opengl33terrain.cpp @@ -329,11 +329,24 @@ bool opengl33_terrain_materials::copy_layer(layer &Layer) if (slots[i] < 0 || material.textures[slots[i]] == null_handle) continue; auto &texture{m_textures.mark_as_used(material.textures[slots[i]])}; - if (false == texture.create()) + if (false == texture.is_ready) { - if (++Layer.attempts < copy_attempts) - return false; - continue; // waited long enough, the layer goes without it + // a texture which is on the card already may have dropped its data after the upload (create() would refuse it then), + // one released by the sweep or never read has to be read first + if (texture.data_state == resource_state::none) + { + texture.reload_on_use = false; + texture.load(); + } + if (texture.data_state == resource_state::failed) + continue; + if (false == texture.create()) + { + if (++Layer.attempts < copy_attempts) + return false; + WriteLog("heightmap terrain: texture \"" + texture.name + "\" isn't ready, the terrain goes without it"); + continue; // waited long enough, the layer goes without it + } } sources[i] = &texture; } diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 78a01895..b735e344 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -499,12 +499,12 @@ void node_layers::shape(material_handle const Material, source_span const &Span) m_shapes.push_back({current, Span, Material, false}); } -std::pair node_layers::erase_shapes(std::set const &Materials) +std::pair node_layers::erase_shapes(std::set const &Materials, std::set const *Layers) { std::pair result{0, 0}; for (auto &shape : m_shapes) { - if (shape.erased || Materials.count(shape.material) == 0) + if (shape.erased || Materials.count(shape.material) == 0 || (Layers != nullptr && Layers->count(shape.layer) == 0)) { continue; } diff --git a/scene/scenelayers.h b/scene/scenelayers.h index fc5ec082..daf3c587 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -267,9 +267,9 @@ class node_layers { return m_shapes; } - // drops the definitions of the shapes of specified materials from the writable layer files on save. + // drops the definitions of the shapes of specified materials from the writable layer files on save; only from specified layers, if given. // returns: amount of the shapes to drop, and of the ones which stay as their files can't be rewritten - std::pair erase_shapes(std::set const &Materials); + std::pair erase_shapes(std::set const &Materials, std::set const *Layers = nullptr); // indicates the scenario initialization (FirstInit) is being performed. Span: location of the directive. // Infile: the directive comes straight from the layer file being loaded void initialization(source_span const &Span, bool Infile); diff --git a/scene/scenenode.cpp b/scene/scenenode.cpp index 18f9e047..301b6ea1 100644 --- a/scene/scenenode.cpp +++ b/scene/scenenode.cpp @@ -429,7 +429,9 @@ bool shape_node::merge( shape_node &Shape ) { if( m_data.material != Shape.m_data.material - || m_data.lighting != Shape.m_data.lighting ) { + || m_data.lighting != Shape.m_data.lighting + || m_data.terrainfile != Shape.m_data.terrainfile ) { + // (shapes of different terrain files are kept apart, so the editor can tell which file the geometry comes from) // can't merge nodes with different appearance return false; } diff --git a/scene/scenenode.h b/scene/scenenode.h index b7fa9fda..48dad5b6 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -99,6 +99,8 @@ public: gfx::geometry_handle geometry { 0, 0 }; // relative origin-centered chunk of geometry held by gfx renderer std::vector vertices; // world space source data of the geometry gfx::userdata_array userdata; + // terrain file the shape comes from, see scene::terrain_file::reference_of(); 0: none, or not known (only the editor asks) + std::uint16_t terrainfile { 0 }; // methods: // sends content of the struct to provided stream void @@ -152,6 +154,9 @@ public: // sets whether the shape is drawn with the translucent geometry (used by generated geometry) void translucent( bool const Translucent ); + // notes the terrain file the shape comes from + void + source_file( std::uint16_t const Reference ) { m_data.terrainfile = Reference; } // data access shapenode_data const & data() const; diff --git a/scene/sceneterrain.cpp b/scene/sceneterrain.cpp index db0135d3..d0453f2b 100644 --- a/scene/sceneterrain.cpp +++ b/scene/sceneterrain.cpp @@ -23,6 +23,8 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include +#include #include /* @@ -75,6 +77,7 @@ struct terrain_source }; std::string file; std::vector materials; + std::uint16_t reference{0}; // see terrain_file::reference_of() }; namespace @@ -739,6 +742,233 @@ std::string terrain_file::extra(std::string const &Binaryfile) return text; } +std::vector &terrain_file::references() +{ + static std::vector references; + return references; +} + +std::uint16_t terrain_file::reference_of(std::string const &File) +{ + auto file{File}; + replace_slashes(file); + auto const &list{references()}; + for (std::size_t index{0}; index < list.size() && index < std::numeric_limits::max(); ++index) + { + auto const base{Global.asCurrentSceneryPath + list[index].file}; + if (file.ends_with(base + ".txtf") || file.ends_with(base + ".btf")) + { + return static_cast(index + 1); + } + } + return 0; +} + +namespace +{ + +// world position of a vertex of a shape placed with specified rotation and offset, the way basic_region::insert() does it +glm::dvec3 place_vertex(glm::dvec3 Position, glm::vec3 const &Rotation, glm::dvec3 const &Offset) +{ + if (Rotation != glm::vec3(0.f)) + { + auto const rotation{glm::radians(glm::dvec3{Rotation})}; + Position = glm::rotateZ(Position, rotation.z); + Position = glm::rotateX(Position, rotation.x); + Position = glm::rotateY(Position, rotation.y); + } + return Position + Offset; +} + +bool read_text_triangles(std::string const &Text, terrain_reference const &Reference, terrain_file::triangle_visitor const &Visit, std::string &Message) +{ + conversion_scope scope; + std::stack offsets; + offsets.push(Reference.offset); + auto rotation{Reference.rotation}; + std::size_t count{0}; + cParser input{Text, cParser::buffer_TEXT}; + input.expandIncludes = false; + auto token{input.getToken()}; + while (false == token.empty()) + { + if (token == "node") + { + node_data nodedata; + input.getTokens(4); + input >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type; + if (nodedata.type == "triangles" || nodedata.type == "triangle_strip" || nodedata.type == "triangle_fan") + { + auto const shape{shape_node().import(input, nodedata)}; + auto const &data{shape.data()}; + auto const *material{GfxRenderer->Material(data.material)}; + auto const name{material != nullptr ? material->GetName() : std::string{}}; + for (std::size_t index{0}; index + 2 < data.vertices.size(); index += 3) + { + Visit(name, {place_vertex(data.vertices[index].position, rotation, offsets.top()), place_vertex(data.vertices[index + 1].position, rotation, offsets.top()), + place_vertex(data.vertices[index + 2].position, rotation, offsets.top())}); + ++count; + } + } + else + { + // anything else the file holds (the editor keeps sweeps there) is of no interest here + auto const end{"end" + nodedata.type}; + auto rest{input.getToken()}; + while (false == rest.empty() && rest != end) + rest = input.getToken(); + } + } + else if (token == "origin") + { + glm::dvec3 offset; + input.getTokens(3); + input >> offset.x >> offset.y >> offset.z; + offsets.push(offset + offsets.top()); + } + else if (token == "endorigin") + { + if (offsets.size() > 1) + { + offsets.pop(); + } + } + else if (token == "rotate") + { + input.getTokens(3); + input >> rotation.x >> rotation.y >> rotation.z; + } + token = input.getToken(); + } + Message = std::to_string(count) + " triangle(s)"; + return true; +} + +bool read_binary_triangles(std::string const &Binaryfile, terrain_file::triangle_visitor const &Visit, std::string &Message) +{ + std::string content; + if (false == read_file(Binaryfile, content)) + { + Message = "can't read the file"; + return false; + } + std::istringstream stream{content}; + file_header header; + if (false == read_header(stream, header)) + { + Message = "the file is of either unrecognized type or version"; + return false; + } + std::vector materials; + for (std::uint32_t index{0}; index < header.materialcount; ++index) + { + std::string name; + if (false == static_cast(std::getline(stream, name, '\0'))) + { + Message = "the file is damaged"; + return false; + } + materials.emplace_back(std::move(name)); + } + auto const directorystart{static_cast(stream.tellg())}; + std::size_t const entrysize{sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)}; + if (directorystart > content.size() || content.size() - directorystart < header.sectioncount * entrysize) + { + Message = "the file is damaged"; + return false; + } + data_reader directory{content.data() + directorystart, header.sectioncount * entrysize}; + std::size_t count{0}; + for (std::uint32_t entry{0}; entry < header.sectioncount; ++entry) + { + auto const sectionindex{directory.get()}; + auto const offset{directory.get()}; + auto const size{directory.get()}; + directory.get(); + if (false == directory.good() || offset > content.size() || content.size() - offset < size) + { + Message = "the file is damaged"; + return false; + } + // the vertices are relative to the centre of their section, see attach() + auto const column{static_cast(sectionindex % EU07_REGIONSIDESECTIONCOUNT)}; + auto const row{static_cast(sectionindex / EU07_REGIONSIDESECTIONCOUNT)}; + auto const centeroffset{-(EU07_REGIONSIDESECTIONCOUNT / 2 * EU07_SECTIONSIZE) + EU07_SECTIONSIZE / 2}; + glm::dvec3 const centre{static_cast(centeroffset + column * EU07_SECTIONSIZE), 0.0, static_cast(centeroffset + row * EU07_SECTIONSIZE)}; + data_reader reader{content.data() + offset, size}; + if (reader.get() != EU07_TERRAINBLOCK) + { + Message = "the file is damaged"; + return false; + } + reader.get(); // section index + auto shapecount{reader.get()}; + while (shapecount-- > 0 && reader.good()) + { + auto const materialindex{reader.get()}; + // cell, ranges, colours, centre and radius + reader.skip(sizeof(std::uint32_t) + 2 * sizeof(double) + 12 * sizeof(float) + 3 * sizeof(double) + sizeof(float)); + auto const vertexcount{reader.get()}; + if (false == reader.good() || false == reader.has(static_cast(vertexcount) * EU07_TERRAINVERTEXSIZE) || materialindex >= materials.size()) + { + Message = "the file is damaged"; + return false; + } + std::array triangle; + for (std::uint32_t vertex{0}; vertex < vertexcount; ++vertex) + { + float values[8]; + reader.get(values, 8); + triangle[vertex % 3] = centre + glm::dvec3{values[0], values[1], values[2]}; + if (vertex % 3 == 2) + { + Visit(materials[materialindex], triangle); + ++count; + } + } + } + if (false == reader.good()) + { + Message = "the file is damaged"; + return false; + } + } + Message = std::to_string(count) + " triangle(s)"; + return true; +} + +} // namespace + +bool terrain_file::read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message, bool *Text) +{ + auto const textfile{Global.asCurrentSceneryPath + Reference.file + ".txtf"}; + auto const binaryfile{Global.asCurrentSceneryPath + Reference.file + ".btf"}; + std::string message; + auto result{false}; + auto const fromtext{FileExists(textfile)}; + if (fromtext) + { + std::string text; + result = read_file(textfile, text) && read_text_triangles(text, Reference, Visit, message); + if (message.empty()) + message = "can't read the file"; + } + else if (FileExists(binaryfile)) + { + // the binary file holds the geometry where the text alone puts it; it stands in for files placed without offset or rotation only + result = read_binary_triangles(binaryfile, Visit, message); + } + else + { + message = "the file is missing"; + } + if (Message != nullptr) + *Message = message; + if (Text != nullptr) + *Text = fromtext; + return result; +} + // makes content of specified binary terrain file a part of provided region bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, bool Deferred) { @@ -751,6 +981,7 @@ bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, b } auto source{std::make_shared()}; source->file = Binaryfile; + source->reference = reference_of(Binaryfile); source->materials.reserve(header.materialcount); for (std::uint32_t index{0}; index < header.materialcount; ++index) { @@ -878,6 +1109,7 @@ void terrain_file::load(basic_section &Section, terrain_block const &Block) } shapedata.material = material.handle; shapedata.translucent = material.translucent; + shapedata.terrainfile = source.reference; shapedata.vertices.resize(vertexcount); for (auto &vertex : shapedata.vertices) { diff --git a/scene/sceneterrain.h b/scene/sceneterrain.h index 07359fbd..7291ed92 100644 --- a/scene/sceneterrain.h +++ b/scene/sceneterrain.h @@ -9,8 +9,12 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include +#include #include +#include +#include namespace scene { @@ -22,10 +26,20 @@ struct terrain_block; // binary terrain file (.btf): static geometry of a text terrain file (.txtf), arranged by the sections // of the scene so each can be loaded on its own, when it comes into use. // the binary file records length and checksum of the text it was made from, and is made anew when these don't match +// terrain file named by a terrain directive of the scenery, with the placement it was loaded with +struct terrain_reference +{ + std::string file; // name relative to the scenery folder, without extension + glm::dvec3 offset{0.0}; + glm::vec3 rotation{0.f}; +}; + class terrain_file { public: // types + // receives a triangle of a terrain file: name of its material, the vertices in the world + using triangle_visitor = std::function const &Triangle)>; // what a scenery is left with for a terrain file it refers to enum class state { @@ -47,6 +61,13 @@ class terrain_file // loads specified piece of a binary terrain file into provided section static void load(basic_section &Section, terrain_block const &Block); static std::string extra(std::string const &Binaryfile); + // terrain files used by the scenery being loaded or shown, in the order the directives named them + static std::vector &references(); + // number (counted from 1) of the reference whose text or binary file is the specified file, 0 if there's none + static std::uint16_t reference_of(std::string const &File); + // goes through the triangles of the terrain file of specified reference, read from the text file if it's there, + // otherwise from the binary one. returns: true on success, Message describing what was read or the failure + static bool read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message = nullptr, bool *Text = nullptr); private: // converts provided content of a text terrain file to binary format and stores it in specified file. returns: true on success, false otherwise diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 1f96d151..1ff771d1 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -57,6 +57,7 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { // drop any streamed editor terrain from a previously loaded scenery before the old region (and // its sections, which those chunks referenced) is destroyed below EditorTerrain.reset(); + scene::terrain_file::references().clear(); // TODO: move initialization to separate routine so we can reuse it SafeDelete( Region ); @@ -722,6 +723,9 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch ++Scratchpad.binary.geometry_imported; auto shape { scene::shape_node().import( Input, nodedata ) }; material = shape.data().material; + if( true == Global.editor_session ) { + shape.source_file( scene::terrain_file::reference_of( Input.Name() ) ); + } simulation::Region->insert( std::move( shape ), Scratchpad, @@ -1188,6 +1192,11 @@ include_terrain_file( std::string File, std::string &Includes, scene::scratch_da return; } binary.terrain_textfiles.emplace_back( File ); + // the editor converts the triangles of the terrain files to heightmap terrain, it has to know them and where they're placed + scene::terrain_file::references().push_back( { + File, + ( Scratchpad.location.offset.empty() ? glm::dvec3( 0.0 ) : Scratchpad.location.offset.top() ), + Scratchpad.location.rotation } ); auto const textfile { Global.asCurrentSceneryPath + File + ".txtf" }; auto const binaryfile { Global.asCurrentSceneryPath + File + ".btf" }; diff --git a/utilities/parser.cpp b/utilities/parser.cpp index 5c4ed31b..b64aee39 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -485,6 +485,11 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri return; } + // layers are named by their files relative to the scenery folder; the terrain files (.txtf) come from includes made up by + // the loader, relative to the folder of the simulator + auto layername{mPath + includefile}; + layername = (layername.starts_with(Global.asCurrentSceneryPath) ? layername.substr(Global.asCurrentSceneryPath.size()) : includefile); + const bool isTerrain = contains(includefile, "_ter.scm"); if (isTerrain && true == Global.file_binary_terrain_state) { WriteLog("SBT found, ignoring: " + includefile); @@ -493,7 +498,7 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri if (sceneryLayers) { // the file is still a part of the scenery, even though its content comes from the binary terrain file - scene::Layers.layer(scene::Layers.open(includefile, include_site())).binary = true; + scene::Layers.layer(scene::Layers.open(layername, include_site())).binary = true; scene::Layers.close(); } return; @@ -537,7 +542,7 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri { auto site{include_site()}; site.parameters = false == mIncludeParser->parameters.empty(); - scene::Layers.open(includefile, site); + scene::Layers.open(layername, site); mIncludeParser->mLayerFile = true; } }