17
0
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:
2026-10-09 23:39:26 +02:00
parent 5716bc1669
commit 8cd0bab27b
21 changed files with 1211 additions and 145 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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()

View File

@@ -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)

View 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();
}

View File

@@ -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
{

View File

@@ -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))

View File

@@ -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;
};
};