mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 03:59:19 +02:00
Editor terrain: texture browser, undo, conversion from terrain files
- Terrain work mode shows a browser of the textures folder in place of the node bank; textures/materials can be dragged onto the palette entries (replacing the material), the material field and the water material field - Ctrl+Z/Ctrl+Y for heightmap terrain changes: a brush stroke or an operation is one step of the editor history - Ground conversion reads only the files of the terrain directives (.txtf with origin/rotate, or .btf without text), paints the chunks with the materials of the triangles, drops only the shapes of those files - Fix: ground grids converted out-of-range floats to int, so whole-scene queries returned the triangles of a single cell (shifted chunks) - Fix: terrain layers copied from textures already uploaded without their data stayed grey - Fix: ImGui ID conflict between terrain tool radios and section headers - Fix: layers of terrain .txtf files named with the scenery/ prefix and could not be saved - Editor selftest commands for the terrain
This commit is contained in:
@@ -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<std::size_t>(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<int>(std::floor((X - m_min.x) / m_cellsize)), 0, m_columns - 1);
|
||||
return static_cast<int>(std::clamp(std::floor((X - m_min.x) / m_cellsize), 0.0, static_cast<double>(m_columns - 1)));
|
||||
}
|
||||
int row(double const Z) const
|
||||
{
|
||||
return std::clamp(static_cast<int>(std::floor((Z - m_min.y) / m_cellsize)), 0, m_rows - 1);
|
||||
return static_cast<int>(std::clamp(std::floor((Z - m_min.y) / m_cellsize), 0.0, static_cast<double>(m_rows - 1)));
|
||||
}
|
||||
|
||||
std::vector<world_triangle> 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<terrain_streamer::change const>(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<terrain_streamer::change const>(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<scene::basic_node *> 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<scene::basic_node *> 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<double> &Heights, std::vector<char> &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 <typename Value_> void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
|
||||
{
|
||||
std::deque<std::size_t> 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<terrain_tool>(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<int>(i) == m_terrain_layer))
|
||||
m_terrain_layer = static_cast<int>(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<std::uint16_t>(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<std::uint64_t>(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 <typename Visitor> void visit_scenery_shapes(std::set<std::uint64_t> 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<std::uint64_t> const &Own, std::set<material_handle> 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<std::uint16_t> const &Files, std::set<material_handle> 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<material_handle, std::size_t> editor_mode::ground_materials()
|
||||
void editor_mode::load_ground_files()
|
||||
{
|
||||
std::map<material_handle, std::size_t> result;
|
||||
if (simulation::Region == nullptr)
|
||||
return result;
|
||||
std::set<std::uint64_t> own;
|
||||
for (auto *terrain : active_terrains())
|
||||
own.insert(geometry_key(terrain->geometry()));
|
||||
glm::dvec2 const everywhere{std::numeric_limits<double>::max() * 0.5};
|
||||
visit_scenery_shapes(own, [&](scene::shape_node const &Shape) {
|
||||
std::vector<world_triangle> 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<std::string, std::uint16_t> 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<std::uint16_t>(index + 1);
|
||||
source.first = conversion.triangles.size();
|
||||
source.good = scene::terrain_file::read_triangles(
|
||||
references[index],
|
||||
[&](std::string const &Material, std::array<glm::dvec3, 3> const &Triangle) {
|
||||
auto found{lookup.find(Material)};
|
||||
if (found == lookup.end())
|
||||
{
|
||||
found = lookup.emplace(Material, static_cast<std::uint16_t>(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<material_handle> 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<material_handle> 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<std::uint64_t> const own;
|
||||
// the chosen triangles of the chosen files
|
||||
std::vector<world_triangle> triangles;
|
||||
std::vector<material_handle> materials;
|
||||
glm::dvec2 const everywhere{std::numeric_limits<double>::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<std::uint16_t> materials;
|
||||
std::set<std::uint16_t> 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::pair<int, int>, std::vector<std::uint32_t>> buckets;
|
||||
std::map<heightmap::chunk_key, std::vector<std::uint32_t>> buckets;
|
||||
for (std::uint32_t i = 0; i < triangles.size(); ++i)
|
||||
{
|
||||
auto const &t = triangles[i];
|
||||
int const x0 = static_cast<int>(std::floor(std::min({t[0].x, t[1].x, t[2].x}) / size));
|
||||
int const x1 = static_cast<int>(std::floor(std::max({t[0].x, t[1].x, t[2].x}) / size));
|
||||
int const z0 = static_cast<int>(std::floor(std::min({t[0].z, t[1].z, t[2].z}) / size));
|
||||
int const z1 = static_cast<int>(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<material_handle, double> 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<std::uint16_t, std::uint16_t> 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<std::size_t>(cells) + 1;
|
||||
auto const samples{heightmap::default_paint_samples};
|
||||
std::size_t const paintside = static_cast<std::size_t>(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<material_handle> const &Materia
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
glm::dvec2 const low{key.first * size, key.second * size};
|
||||
auto const low{heightmap::chunk_corner(key)};
|
||||
std::vector<world_triangle> local;
|
||||
std::vector<std::uint16_t> 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<double>::max()};
|
||||
// heights of the grid points; where there's no ground they're taken from the nearest point which has some
|
||||
std::vector<double> heights(side * side, 0.0);
|
||||
std::vector<char> found(side * side, 0);
|
||||
bool any = false;
|
||||
@@ -2397,29 +2486,103 @@ std::string editor_mode::convert_ground(std::set<material_handle> 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<double>::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::size_t>(std::clamp<long>(std::lround((X - low.x) / cellsize), 0, cells));
|
||||
auto const iz = static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / cellsize), 0, cells));
|
||||
return static_cast<float>(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<std::uint16_t> paint(paintside * paintside, 0);
|
||||
std::vector<char> painted(paintside * paintside, 0);
|
||||
std::map<std::uint16_t, std::size_t> 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<std::pair<std::size_t, std::uint16_t>> 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<float>(heights[index]);
|
||||
std::map<std::uint16_t, std::size_t> 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<material_handle> 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<scene::layer_handle> 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<scene::layer_handle>(layer)).name};
|
||||
replace_slashes(name);
|
||||
if (name.ends_with(textfile))
|
||||
layers.insert(static_cast<scene::layer_handle>(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);
|
||||
|
||||
|
||||
@@ -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<stored_profile> profile_library;
|
||||
std::vector<array_copy> 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_streamer::change const> 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<material_handle, std::size_t> 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<material_handle> 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<material_handle, std::size_t> materials;
|
||||
std::set<material_handle> chosen;
|
||||
std::vector<source> files;
|
||||
std::vector<std::array<glm::dvec3, 3>> triangles;
|
||||
std::vector<std::uint16_t> materials; // of the triangles: index of the name
|
||||
std::vector<std::string> names; // of the materials
|
||||
std::set<std::size_t> 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<std::string> folders; // under the current one
|
||||
std::vector<item> items;
|
||||
bool listed{false};
|
||||
char filter[64]{};
|
||||
float size{96.f}; // of the images, pixels
|
||||
std::string selected;
|
||||
std::map<std::string, int> 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);
|
||||
|
||||
@@ -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<int>(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<int>(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<std::size_t> 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<int>(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()
|
||||
|
||||
@@ -72,6 +72,10 @@ http://mozilla.org/MPL/2.0/.
|
||||
// spread <track> <stop 0..2> <reach> 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 <x> <z> <radius> <metres>, terrainpaint <x> <z> <radius> <palette entry> as a stroke of the brush
|
||||
// terrainprobe <x> <z> 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 <text>
|
||||
@@ -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<std::uint16_t>(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<std::size_t>(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)
|
||||
|
||||
279
application/editormodetextures.cpp
Normal file
279
application/editormodetextures.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
|
||||
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<char>(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<char const *>(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<std::uint64_t>(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<std::string, texture_browser::item> items;
|
||||
std::error_code error;
|
||||
for (std::filesystem::directory_iterator entry{std::string{paths::textures} + browser.folder, error}, end; false == static_cast<bool>(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<int>(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<texture_browser::item const *> 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<int>((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (side + style.ItemSpacing.x)))};
|
||||
auto const rows{(static_cast<int>(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<std::size_t>(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<ImTextureID>(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<ImTextureID>(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();
|
||||
}
|
||||
@@ -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<std::size_t>(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<void()> Content, std::string Title)
|
||||
{
|
||||
m_workspacebank[static_cast<std::size_t>(Workspace)] = {std::move(Content), std::move(Title)};
|
||||
}
|
||||
void set_workspace_options(workspace const Workspace, std::function<void()> Tools, std::function<void()> Selection = {})
|
||||
{
|
||||
@@ -205,6 +212,7 @@ class editor_ui : public ui_layer
|
||||
workspace m_workspace{workspace::none};
|
||||
std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspacetools;
|
||||
std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspaceselection;
|
||||
std::array<std::pair<std::function<void()>, std::string>, static_cast<std::size_t>(workspace::count_)> m_workspacebank;
|
||||
// scene window: the models of the scenery by layer and group, rebuilt when their number changes
|
||||
struct scene_entry
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<void()> 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<void()> m_standin;
|
||||
std::string m_standintitle;
|
||||
std::vector<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank;
|
||||
char m_nodesearch[128];
|
||||
std::shared_ptr<std::string> 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<text_line> m_grouplines;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user