mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 00:29:19 +02:00
- 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
280 lines
9.7 KiB
C++
280 lines
9.7 KiB
C++
/*
|
|
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();
|
|
}
|