17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 05:09:20 +02:00

feat(editor): user-defined model sets, random insert from set, tabbed editor panel

This commit is contained in:
2026-10-01 18:41:35 +02:00
parent 39dd1c057f
commit 1497d081ff
10 changed files with 1033 additions and 271 deletions

View File

@@ -167,6 +167,7 @@ set(SOURCES
"application/driveruipanels.cpp" "application/driveruipanels.cpp"
"input/editorkeyboardinput.cpp" "input/editorkeyboardinput.cpp"
"editor/editorSettings.cpp" "editor/editorSettings.cpp"
"editor/editorModelSets.cpp"
"editor/editorTerrain.cpp" "editor/editorTerrain.cpp"
"editor/editorTerrainStreamer.cpp" "editor/editorTerrainStreamer.cpp"
"application/editormode.cpp" "application/editormode.cpp"

View File

@@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/application.h" #include "application/application.h"
#include "editor/editorSettings.hpp" #include "editor/editorSettings.hpp"
#include "editor/editorModelSets.hpp"
#include "utilities/Globals.h" #include "utilities/Globals.h"
#include "simulation/simulation.h" #include "simulation/simulation.h"
#include "simulation/simulationtime.h" #include "simulation/simulationtime.h"
@@ -164,19 +165,6 @@ namespace
return std::abs(sum) * 0.5; return std::abs(sum) * 0.5;
} }
// short readable name of a node template (model file and texture), like the node bank shows it
std::string template_label(std::string const &Template)
{
cParser tokenizer(Template);
tokenizer.getTokens(9, false); // node, ranges, name, type, position, rotation
auto model{tokenizer.getToken<std::string>(false)};
auto texture{tokenizer.getToken<std::string>(false)};
replace_slashes(model);
erase_extension(model);
replace_slashes(texture);
return texture == "none" || texture.empty() ? model : model + " (" + texture + ")";
}
// world triangles bucketed on a uniform XZ grid, for many ground height queries over one area // world triangles bucketed on a uniform XZ grid, for many ground height queries over one area
class triangle_grid class triangle_grid
{ {
@@ -293,6 +281,9 @@ void editor_mode::editormode_input::poll()
editor_mode::editor_mode() { editor_mode::editor_mode() {
m_userinterface = std::make_shared<editor_ui>(); m_userinterface = std::make_shared<editor_ui>();
// the area fill settings live in the node bank window, in the tab of the fill mode
ui()->set_fill_options([this]() { render_area_fill(); });
ui()->set_gizmo_options([this]() { render_gizmo_options(); });
} }
editor_ui *editor_mode::ui() const editor_ui *editor_mode::ui() const
@@ -303,6 +294,7 @@ editor_ui *editor_mode::ui() const
bool editor_mode::init() bool editor_mode::init()
{ {
EditorSettings.load(); EditorSettings.load();
EditorModelSets.load();
Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr);
return m_input.init(); return m_input.init();
} }
@@ -473,17 +465,18 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button)
if (!mouseHold || !viewport_click()) if (!mouseHold || !viewport_click())
return; return;
// spacing is measured in world space: the mouse offset is camera-relative and the camera may move mid-stroke
glm::dvec3 const newPos = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position());
if (m_brush_has_last && glm::distance(newPos, oldPos) < ui()->getSpacing())
return;
// picked only when a placement is due, so a random pick from a set isn't drawn every tick
const std::string *src = ui()->get_active_node_template(); const std::string *src = ui()->get_active_node_template();
if (!src) if (!src)
return; return;
std::string name = "editor_"; std::string name = "editor_";
// spacing is measured in world space: the mouse offset is camera-relative and the camera may move mid-stroke
glm::dvec3 const newPos = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position());
if (m_brush_has_last && glm::distance(newPos, oldPos) < ui()->getSpacing())
return;
TAnimModel *cloned = simulation::State.create_model(*src, name, newPos); TAnimModel *cloned = simulation::State.create_model(*src, name, newPos);
oldPos = newPos; oldPos = newPos;
m_brush_has_last = true; m_brush_has_last = true;
@@ -819,9 +812,6 @@ bool editor_mode::update()
#endif #endif
m_userinterface->update(); m_userinterface->update();
// update brush settings visibility depending on panel mode
ui()->toggleBrushSettings(ui()->mode() == nodebank_panel::BRUSH);
if (mouseHold) if (mouseHold)
{ {
// process continuous brush placement // process continuous brush placement
@@ -892,12 +882,9 @@ bool editor_mode::update()
// --- ImGuizmo: in-viewport transform gizmo for the selected node --- // --- ImGuizmo: in-viewport transform gizmo for the selected node ---
render_gizmo(); render_gizmo();
// --- area fill: outline overlay and settings, while the mode is active --- // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) ---
if (ui()->mode() == nodebank_panel::FILL) if (ui()->mode() == nodebank_panel::FILL)
{
draw_area_fill_outline(); draw_area_fill_outline();
render_area_fill();
}
// --- ImGui: Editor Settings & History windows --- // --- ImGui: Editor Settings & History windows ---
if(m_settings_open) if(m_settings_open)
@@ -914,31 +901,39 @@ void editor_mode::render_settings()
{ {
ImGui::Begin("Editor Settings", &m_settings_open, ImGuiWindowFlags_AlwaysAutoResize); ImGui::Begin("Editor Settings", &m_settings_open, ImGuiWindowFlags_AlwaysAutoResize);
ImGui::TextUnformatted("Camera movement"); if (ImGui::BeginTabBar("##editorsettings"))
const char *schemes[] = {"WSAD (new)", "Arrows (legacy)"};
int current = EditorSettings.movement() == editorSettings::movement_scheme::legacy ? 1 : 0;
if (ImGui::Combo("##movement_scheme", &current, schemes, IM_ARRAYSIZE(schemes)))
{ {
EditorSettings.movement(current == 1 ? editorSettings::movement_scheme::legacy if (ImGui::BeginTabItem("General"))
: editorSettings::movement_scheme::wsad); {
m_input.keyboard.apply_scheme(); ImGui::TextUnformatted("Camera movement");
EditorSettings.save();
const char *schemes[] = {"WSAD (new)", "Arrows (legacy)"};
int current = EditorSettings.movement() == editorSettings::movement_scheme::legacy ? 1 : 0;
if (ImGui::Combo("##movement_scheme", &current, schemes, IM_ARRAYSIZE(schemes)))
{
EditorSettings.movement(current == 1 ? editorSettings::movement_scheme::legacy
: editorSettings::movement_scheme::wsad);
m_input.keyboard.apply_scheme();
EditorSettings.save();
}
ImGui::Separator();
ImGui::Checkbox("Transform gizmo (ImGuizmo)", &m_gizmo_enabled);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Terrain"))
{
render_terrain_ui();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
} }
ImGui::Separator();
ImGui::Checkbox("Transform gizmo (ImGuizmo)", &m_gizmo_enabled);
render_terrain_ui();
ImGui::End(); ImGui::End();
} }
void editor_mode::render_terrain_ui() void editor_mode::render_terrain_ui()
{ {
ImGui::Separator();
ImGui::TextUnformatted("Terrain");
ImGui::SetNextItemWidth(120.0f); ImGui::SetNextItemWidth(120.0f);
ImGui::InputInt("Grid cells", &m_terrain_cells); ImGui::InputInt("Grid cells", &m_terrain_cells);
m_terrain_cells = std::clamp(m_terrain_cells, 1, 512); m_terrain_cells = std::clamp(m_terrain_cells, 1, 512);
@@ -1406,10 +1401,11 @@ void editor_mode::run_area_fill()
// model package // model package
std::vector<std::string> package; std::vector<std::string> package;
if (m_fill_package == 0) if (m_fill_source.kind == model_set_ref::source::manual)
package = m_fill_custom; package = m_fill_custom;
else else
package = ui()->nodebank_group_templates(static_cast<std::size_t>(m_fill_package - 1)); for (auto const *entry : ui()->nodebank().set_entries(m_fill_source))
package.push_back(*entry);
package.erase(std::remove_if(package.begin(), package.end(), [](std::string const &Template) { return Template.empty(); }), package.end()); package.erase(std::remove_if(package.begin(), package.end(), [](std::string const &Template) { return Template.empty(); }), package.end());
if (package.empty()) if (package.empty())
{ {
@@ -1621,9 +1617,7 @@ void editor_mode::draw_area_fill_outline() const
void editor_mode::render_area_fill() void editor_mode::render_area_fill()
{ {
ImGui::Begin("Area fill", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing); ImGui::TextDisabled("LMB: add outline point Backspace: remove last point");
ImGui::TextUnformatted("LMB: add outline point, Backspace: remove last point");
double const area = polygon_area_xz(m_fill_points); double const area = polygon_area_xz(m_fill_points);
ImGui::Text("Points: %zu Area: %.0f m2 (%.2f ha)", m_fill_points.size(), area, area / 10000.0); ImGui::Text("Points: %zu Area: %.0f m2 (%.2f ha)", m_fill_points.size(), area, area / 10000.0);
if (ImGui::Button("Remove last point") && !m_fill_points.empty()) if (ImGui::Button("Remove last point") && !m_fill_points.empty())
@@ -1634,58 +1628,13 @@ void editor_mode::render_area_fill()
ImGui::Separator(); ImGui::Separator();
// model package: a node bank group, or a custom set assembled from node bank templates // model package: a hand-assembled list, a user-defined set or a node bank group
auto const groups = ui()->nodebank_groups(); auto &bank = ui()->nodebank();
m_fill_package = std::clamp(m_fill_package, 0, static_cast<int>(groups.size())); bank.set_combo("Model set", m_fill_source, "Custom list");
std::string const current = m_fill_package == 0 ? std::string("Custom set") : groups[m_fill_package - 1]; if (m_fill_source.kind == model_set_ref::source::manual)
ImGui::SetNextItemWidth(250.0f); bank.manual_list("fillset", m_fill_custom, m_fill_custom_idx);
if (ImGui::BeginCombo("Model package", current.c_str()))
{
if (ImGui::Selectable("Custom set", m_fill_package == 0))
m_fill_package = 0;
for (int i = 0; i < static_cast<int>(groups.size()); ++i)
{
auto const label = groups[i] + "##fillgroup" + std::to_string(i);
if (ImGui::Selectable(label.c_str(), m_fill_package == i + 1))
m_fill_package = i + 1;
}
ImGui::EndCombo();
}
if (m_fill_package == 0)
{
ImGui::BeginChild("fill_custom_set", ImVec2(350.0f, 110.0f), true);
for (int i = 0; i < static_cast<int>(m_fill_custom.size()); ++i)
{
auto const label = template_label(m_fill_custom[i]) + "##fillcustom" + std::to_string(i);
if (ImGui::Selectable(label.c_str(), m_fill_custom_idx == i))
m_fill_custom_idx = i;
}
ImGui::EndChild();
if (ImGui::Button("Add selected template"))
{
// the node bank selection, not a random pick from the brush set
auto const *src = ui()->get_active_node_template(true);
if (src && !src->empty())
m_fill_custom.push_back(*src);
}
ImGui::SameLine();
if (ImGui::Button("Remove") && m_fill_custom_idx >= 0 && m_fill_custom_idx < static_cast<int>(m_fill_custom.size()))
{
m_fill_custom.erase(m_fill_custom.begin() + m_fill_custom_idx);
m_fill_custom_idx = -1;
}
ImGui::SameLine();
if (ImGui::Button("Clear set"))
{
m_fill_custom.clear();
m_fill_custom_idx = -1;
}
}
else else
{ ImGui::TextDisabled("%zu templates in set", bank.set_entries(m_fill_source).size());
ImGui::Text("Templates in group: %zu", ui()->nodebank_group_templates(static_cast<std::size_t>(m_fill_package - 1)).size());
}
ImGui::Separator(); ImGui::Separator();
@@ -1697,7 +1646,9 @@ void editor_mode::render_area_fill()
m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing); m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing);
ImGui::SetNextItemWidth(200.0f); ImGui::SetNextItemWidth(200.0f);
ImGui::DragFloatRange2("Scale", &m_fill_scale_min, &m_fill_scale_max, 0.01f, 0.1f, 5.0f, "min %.2f", "max %.2f"); ImGui::DragFloatRange2("Scale", &m_fill_scale_min, &m_fill_scale_max, 0.01f, 0.1f, 5.0f, "min %.2f", "max %.2f");
ImGui::Checkbox("Random rotation (off: Functions panel setting)", &m_fill_random_rotation); ImGui::Checkbox("Random rotation", &m_fill_random_rotation);
if (!m_fill_random_rotation)
ui()->render_rotation_controls();
ImGui::Checkbox("Large models count as ground (terrain tiles)", &m_fill_models_as_ground); ImGui::Checkbox("Large models count as ground (terrain tiles)", &m_fill_models_as_ground);
ImGui::Text("Estimated objects: %.0f", std::round(area / 10000.0 * m_fill_density)); ImGui::Text("Estimated objects: %.0f", std::round(area / 10000.0 * m_fill_density));
@@ -1711,21 +1662,20 @@ void editor_mode::render_area_fill()
if (!m_fill_status.empty()) if (!m_fill_status.empty())
ImGui::TextUnformatted(m_fill_status.c_str()); ImGui::TextUnformatted(m_fill_status.c_str());
ImGui::End();
} }
void editor_mode::render_gizmo() void editor_mode::render_gizmo_options()
{ {
// the transform gizmo is suppressed while editing terrain, so the brush/chunk tool owns the mouse ImGui::Checkbox("Enabled", &m_gizmo_enabled);
if (!m_gizmo_enabled || m_terrain_sculpt || m_chunk_edit) if (!m_gizmo_enabled)
return;
if (m_terrain_sculpt || m_chunk_edit)
{ {
m_gizmo_using = false; ImGui::TextDisabled("Suspended while editing terrain");
return; return;
} }
// compact control window: lets the user pick the transform mode without keyboard shortcuts // lets the user pick the transform mode without keyboard shortcuts
ImGui::Begin("Gizmo", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing);
int op = static_cast<int>(m_gizmo_op); int op = static_cast<int>(m_gizmo_op);
ImGui::RadioButton("Translate (Q)", &op, static_cast<int>(gizmo_operation::translate)); ImGui::RadioButton("Translate (Q)", &op, static_cast<int>(gizmo_operation::translate));
ImGui::SameLine(); ImGui::SameLine();
@@ -1745,7 +1695,16 @@ void editor_mode::render_gizmo()
} }
if (!m_node) if (!m_node)
ImGui::TextDisabled("No node selected"); ImGui::TextDisabled("No node selected");
ImGui::End(); }
void editor_mode::render_gizmo()
{
// the transform gizmo is suppressed while editing terrain, so the brush/chunk tool owns the mouse
if (!m_gizmo_enabled || m_terrain_sculpt || m_chunk_edit)
{
m_gizmo_using = false;
return;
}
if (!m_node) if (!m_node)
{ {

View File

@@ -1,4 +1,4 @@
/* /*
This Source Code Form is subject to the This Source Code Form is subject to the
terms of the Mozilla Public License, v. terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not 2.0. If a copy of the MPL was not
@@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/.
#pragma once #pragma once
#include "application/applicationmode.h" #include "application/applicationmode.h"
#include "application/editoruipanels.h"
#include "input/editormouseinput.h" #include "input/editormouseinput.h"
#include "input/editorkeyboardinput.h" #include "input/editorkeyboardinput.h"
#include "vehicle/Camera.h" #include "vehicle/Camera.h"
@@ -211,7 +212,7 @@ class editor_mode : public application_mode
std::vector<TAnimModel *> m_fill_last; // instances created by the most recent fill, for "Undo last fill" std::vector<TAnimModel *> m_fill_last; // instances created by the most recent fill, for "Undo last fill"
std::vector<std::string> m_fill_custom; // user-assembled package of node templates std::vector<std::string> m_fill_custom; // user-assembled package of node templates
int m_fill_custom_idx{-1}; int m_fill_custom_idx{-1};
int m_fill_package{0}; // 0 = custom set, N = nodebank group N-1 model_set_ref m_fill_source; // manual = m_fill_custom, otherwise a user set or node bank group
float m_fill_density{200.0f}; // objects per hectare float m_fill_density{200.0f}; // objects per hectare
float m_fill_min_spacing{2.0f}; // minimal distance between placed objects (m) float m_fill_min_spacing{2.0f}; // minimal distance between placed objects (m)
bool m_fill_random_rotation{true}; // random yaw; otherwise the Functions panel rotation settings apply bool m_fill_random_rotation{true}; // random yaw; otherwise the Functions panel rotation settings apply
@@ -223,6 +224,8 @@ class editor_mode : public application_mode
// ImGuizmo-based transform gizmo for the selected node // ImGuizmo-based transform gizmo for the selected node
enum class gizmo_operation { translate, rotate, scale }; enum class gizmo_operation { translate, rotate, scale };
void render_gizmo(); void render_gizmo();
// gizmo settings, drawn in the toolset window
void render_gizmo_options();
bool m_gizmo_enabled{true}; // master switch for the in-viewport gizmo bool m_gizmo_enabled{true}; // master switch for the in-viewport gizmo
bool m_gizmo_using{false}; // tracks an ongoing drag, so a single undo snapshot is taken per drag bool m_gizmo_using{false}; // tracks an ongoing drag, so a single undo snapshot is taken per drag
bool m_gizmo_local{false}; // manipulate in the object's local space instead of world space bool m_gizmo_local{false}; // manipulate in the object's local space instead of world space

View File

@@ -21,10 +21,10 @@ editor_ui::editor_ui()
clear_panels(); clear_panels();
// bind the panels with ui object. maybe not the best place for this but, eh // bind the panels with ui object. maybe not the best place for this but, eh
add_external_panel(&m_itempropertiespanel);
add_external_panel(&m_nodebankpanel); add_external_panel(&m_nodebankpanel);
add_external_panel(&m_functionspanel);
add_external_panel(&m_brushobjects); m_nodebankpanel.mode_options = [this](nodebank_panel::edit_mode const Mode) { render_mode_options(Mode); };
m_nodebankpanel.header_sections = [this]() { render_header_sections(); };
} }
// updates state of UI elements // updates state of UI elements
@@ -43,26 +43,67 @@ void editor_ui::update()
ui_layer::update(); ui_layer::update();
m_itempropertiespanel.update(m_node); m_itempropertiespanel.update(m_node);
m_functionspanel.update(m_node); m_functionspanel.update(m_node);
auto ptr = get_active_node_template(true);
if (ptr)
m_brushobjects.update(*ptr);
} }
void editor_ui::toggleBrushSettings(bool isVisible) void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode)
{ {
if (m_brushobjects.is_open != isVisible) switch (Mode)
m_brushobjects.is_open = isVisible; {
case nodebank_panel::MODIFY:
ImGui::TextDisabled("LMB: select node F: focus End: drop to ground Del: delete");
break;
case nodebank_panel::COPY:
ImGui::TextDisabled("LMB: copy the clicked model to the node bank");
break;
case nodebank_panel::ADD:
{
ImGui::TextDisabled("LMB: insert a model at the cursor");
ImGui::Checkbox("Random model from set", &m_insertrandom);
if (m_insertrandom)
{
m_nodebankpanel.set_combo("Set##insert", m_insertset, nullptr);
auto const count{m_nodebankpanel.set_entries(m_insertset).size()};
ImGui::TextDisabled("%zu templates in set%s", count, count == 0 ? ", the node bank selection is used" : "");
}
else
{
ImGui::TextDisabled("Inserts the template selected in the node bank");
}
m_functionspanel.render_controls();
break;
}
case nodebank_panel::BRUSH:
ImGui::TextDisabled("Hold LMB: paint models along the cursor path");
m_brushobjects.render_options(m_nodebankpanel);
m_functionspanel.render_controls();
break;
case nodebank_panel::FILL:
if (m_filloptions)
m_filloptions();
break;
default:
break;
}
} }
std::vector<std::string> editor_ui::nodebank_groups() const void editor_ui::render_header_sections()
{ {
return m_nodebankpanel.group_names(); if (ImGui::CollapsingHeader("Gizmo", ImGuiTreeNodeFlags_DefaultOpen))
{
if (m_gizmooptions)
m_gizmooptions();
}
if (ImGui::CollapsingHeader("Node properties", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Indent();
m_itempropertiespanel.render_body();
ImGui::Unindent();
}
} }
std::vector<std::string> editor_ui::nodebank_group_templates(std::size_t const Group) const void editor_ui::render_rotation_controls()
{ {
return m_nodebankpanel.group_templates(Group); m_functionspanel.render_controls();
} }
void editor_ui::set_node(scene::basic_node *Node) void editor_ui::set_node(scene::basic_node *Node)
@@ -77,9 +118,23 @@ void editor_ui::add_node_template(const std::string &desc)
std::string const *editor_ui::get_active_node_template(bool bypassRandom) std::string const *editor_ui::get_active_node_template(bool bypassRandom)
{ {
if (!bypassRandom && m_brushobjects.is_open && m_brushobjects.useRandom && m_brushobjects.Objects.size() > 0) if (!bypassRandom)
{ {
return m_brushobjects.GetRandomObject(); // random pick from the active tool's set; an empty set falls back to the node bank selection
std::string const *pick{nullptr};
if (m_nodebankpanel.mode == nodebank_panel::BRUSH && m_brushobjects.useRandom)
{
if (m_brushobjects.source.kind == model_set_ref::source::manual)
pick = m_brushobjects.Objects.empty() ? nullptr : m_brushobjects.GetRandomObject();
else
pick = m_nodebankpanel.random_template(m_brushobjects.source);
}
else if (m_nodebankpanel.mode == nodebank_panel::ADD && m_insertrandom)
{
pick = m_nodebankpanel.random_template(m_insertset);
}
if (pick != nullptr)
return pick;
} }
return m_nodebankpanel.get_active_template(); return m_nodebankpanel.get_active_template();
} }

View File

@@ -36,15 +36,36 @@ class editor_ui : public ui_layer
const std::string *get_active_node_template(bool bypassRandom = false); const std::string *get_active_node_template(bool bypassRandom = false);
nodebank_panel::edit_mode mode(); nodebank_panel::edit_mode mode();
float getSpacing(); float getSpacing();
void toggleBrushSettings(bool isVisible); // node bank and model set access (set selection widgets, set contents)
std::vector<std::string> nodebank_groups() const; nodebank_panel &nodebank()
std::vector<std::string> nodebank_group_templates(std::size_t const Group) const; {
return m_nodebankpanel;
}
// rotation settings widgets, for tools drawn by the editor mode
void render_rotation_controls();
// draws the area fill settings in its node bank tab (the fill state lives in the editor mode)
void set_fill_options(std::function<void()> Renderer)
{
m_filloptions = std::move(Renderer);
}
// draws the gizmo settings in the toolset window (the gizmo state lives in the editor mode)
void set_gizmo_options(std::function<void()> Renderer)
{
m_gizmooptions = std::move(Renderer);
}
private: private:
// methods
void render_mode_options(nodebank_panel::edit_mode const Mode);
void render_header_sections();
// members // members
itemproperties_panel m_itempropertiespanel{"Node Properties", true}; itemproperties_panel m_itempropertiespanel{"Node Properties", true}; // not a window of its own, drawn in the toolset window
functions_panel m_functionspanel{"Functions", true}; functions_panel m_functionspanel{"Functions", true}; // not a window of its own, its settings are drawn in the toolset tabs
nodebank_panel m_nodebankpanel{"Node Bank", true}; nodebank_panel m_nodebankpanel{"Toolset", true}; // main editor window: gizmo, node properties, edit modes and the node bank
brush_object_list m_brushobjects{"Brush properties", false}; brush_object_list m_brushobjects;
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
model_set_ref m_insertset;
std::function<void()> m_filloptions;
std::function<void()> m_gizmooptions;
scene::basic_node *m_node{nullptr}; // currently bound scene node, if any scene::basic_node *m_node{nullptr}; // currently bound scene node, if any
}; };

View File

@@ -19,6 +19,21 @@ http://mozilla.org/MPL/2.0/.
#include "world/MemCell.h" #include "world/MemCell.h"
#include "application/editoruilayer.h" #include "application/editoruilayer.h"
#include "rendering/renderer.h" #include "rendering/renderer.h"
#include "editor/editorModelSets.hpp"
namespace
{
// list entry; the selected one is drawn with the accent colour, like selections in the starter
bool list_item(char const *Label, bool const Selected)
{
if (Selected)
ImGui::PushStyleColor(ImGuiCol_Header, ImGui::GetStyleColorVec4(ImGuiCol_HeaderActive));
auto const clicked{ImGui::Selectable(Label, Selected)};
if (Selected)
ImGui::PopStyleColor();
return clicked;
}
} // namespace
void itemproperties_panel::update(scene::basic_node *Node) void itemproperties_panel::update(scene::basic_node *Node)
{ {
@@ -285,19 +300,24 @@ void itemproperties_panel::render()
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{ {
// header section render_body();
for (auto const &line : text_lines)
{
ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str());
}
// transform editor (position/rotation/scale) — TAnimModel only
render_transform_editor();
// group section
render_group();
} }
ImGui::End(); ImGui::End();
} }
void itemproperties_panel::render_body()
{
// header section
for (auto const &line : text_lines)
{
ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str());
}
// transform editor (position/rotation/scale) — TAnimModel only
render_transform_editor();
// group section
render_group();
}
// In-place editor for position (double precision), rotation (degrees, 0-360), // In-place editor for position (double precision), rotation (degrees, 0-360),
// and uniform scale (per-axis float, 1.000) of a picked TAnimModel. // and uniform scale (per-axis float, 1.000) of a picked TAnimModel.
// Other node subclasses don't expose these knobs through the same API, so the // Other node subclasses don't expose these knobs through the same API, so the
@@ -375,27 +395,6 @@ bool itemproperties_panel::render_group()
return true; return true;
} }
brush_object_list::brush_object_list(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen)
{
size_min = {50, 100};
size_max = {1000, 500};
}
bool brush_object_list::VectorGetter(void *data, int idx, const char **out_text)
{
auto *vec = static_cast<std::vector<std::string> *>(data);
if (idx < 0 || idx >= vec->size())
return false;
*out_text = (*vec)[idx].c_str();
return true;
}
void brush_object_list::update(std::string nodeTemplate)
{
Template = nodeTemplate;
}
std::string *brush_object_list::GetRandomObject() std::string *brush_object_list::GetRandomObject()
{ {
static std::string empty; // fallback static std::string empty; // fallback
@@ -408,39 +407,25 @@ std::string *brush_object_list::GetRandomObject()
return &Objects[dist(Global.local_random_engine)]; return &Objects[dist(Global.local_random_engine)];
} }
void brush_object_list::render() void brush_object_list::render_options(nodebank_panel &Bank)
{ {
if (false == is_open) ImGui::SliderFloat("Spacing", &spacing, 0.1f, 20.0f, "%.1f m");
ImGui::Checkbox("Random model from set", &useRandom);
if (false == useRandom)
{ {
ImGui::TextDisabled("Paints the template selected in the node bank");
return; return;
} }
Bank.set_combo("Preset", source, "Manual list");
auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0); if (source.kind == model_set_ref::source::manual)
if (ImGui::Begin("Brush random set", nullptr, flags))
{ {
ImGui::SliderFloat("Spacing", &spacing, 0.1f, 20.0f, "%.1f m"); Bank.manual_list("brushset", Objects, idx);
ImGui::Checkbox("Enable brush random from set", &useRandom); }
if (useRandom) else
{ {
ImGui::Text("Set of objects to choose from:"); auto const count{Bank.set_entries(source).size()};
ImGui::ListBox("", &idx, VectorGetter, (void *)&Objects, Objects.size(), 6); ImGui::TextDisabled("%zu templates in set%s", count, count == 0 ? ", the node bank selection is used" : "");
if (ImGui::Button("Add"))
{
if (!Template.empty())
Objects.push_back(Template);
}
ImGui::SameLine();
if (ImGui::Button("Remove") && idx >= 0 && idx < Objects.size())
{
Objects.erase(Objects.begin() + idx);
}
if (ImGui::Button("Remove all"))
{
Objects.clear();
}
}
} }
ImGui::End();
} }
nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen)
@@ -518,6 +503,11 @@ void nodebank_panel::render()
{ {
ImGui::SetNextWindowSize(ImVec2S(size.x, size.y)); ImGui::SetNextWindowSize(ImVec2S(size.x, size.y));
} }
else
{
// mode settings are drawn above the list, so start with a window tall enough for both
ImGui::SetNextWindowSize(ImVec2S(440, 640), ImGuiCond_FirstUseEver);
}
if (size_min.x > 0) if (size_min.x > 0)
{ {
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y)); ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y));
@@ -526,30 +516,47 @@ void nodebank_panel::render()
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{ {
if (ImGui::Button("Reload node bank"))
ImGui::RadioButton("Modify node", (int *)&mode, MODIFY);
ImGui::SameLine();
ImGui::RadioButton("Insert from bank", (int *)&mode, ADD);
ImGui::SameLine();
ImGui::RadioButton("Brush mode", (int *)&mode, BRUSH);
ImGui::SameLine();
ImGui::RadioButton("Copy to bank", (int *)&mode, COPY);
ImGui::SameLine();
ImGui::RadioButton("Area fill", (int *)&mode, FILL);
ImGui::SameLine();
if (ImGui::Button("Reload Nodebank"))
{ {
nodebank_reload(); nodebank_reload();
} }
ImGui::SameLine();
if (mode == BRUSH) if (ImGui::Button(m_setsopen ? "Close model sets" : "Model sets..."))
{ {
// ImGui::SliderFloat("Spacing", &spacing, 0.1f, 20.0f, "%.1f m"); m_setsopen = !m_setsopen;
} }
if (header_sections)
{
header_sections();
}
// edit modes as tabs, each with its own settings
std::pair<char const *, edit_mode> const modes[] = {{"Select", MODIFY}, {"Insert", ADD}, {"Brush", BRUSH}, {"Area fill", FILL}, {"Copy to bank", COPY}};
if (ImGui::BeginTabBar("##editmodes", ImGuiTabBarFlags_FittingPolicyResizeDown))
{
for (auto const &tab : modes)
{
if (false == ImGui::BeginTabItem(tab.first))
{
continue;
}
mode = tab.second;
if (mode_options)
{
mode_options(mode);
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Separator();
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
ImGui::InputTextWithHint("Search", "Search node bank", m_nodesearch, IM_ARRAYSIZE(m_nodesearch)); ImGui::InputTextWithHint("Search", "Search node bank", m_nodesearch, IM_ARRAYSIZE(m_nodesearch));
if (ImGui::ListBoxHeader("##nodebank", ImVec2(-1, -1))) // the list takes the rest of the window, but keeps a usable height when the sections above are expanded (the window scrolls then)
auto const listheight{std::max(ImGui::GetContentRegionAvail().y, ImGui::GetTextLineHeightWithSpacing() * 10.0f)};
if (ImGui::ListBoxHeader("##nodebank", ImVec2(-1, listheight)))
{ {
auto idx{0}; auto idx{0};
auto isvisible{false}; auto isvisible{false};
@@ -572,16 +579,366 @@ void nodebank_panel::render()
continue; continue;
} }
auto const label{" " + entry.first + "##" + std::to_string(idx)}; auto const label{" " + entry.first + "##" + std::to_string(idx)};
if (ImGui::Selectable(label.c_str(), entry.second == m_selectedtemplate)) if (list_item(label.c_str(), entry.second == m_selectedtemplate))
m_selectedtemplate = entry.second; m_selectedtemplate = entry.second;
++idx; ++idx;
} }
} }
ImGui::ListBoxFooter(); ImGui::ListBoxFooter();
} }
ImGui::PopItemWidth();
} }
ImGui::End(); ImGui::End();
if (m_setsopen)
{
render_sets_window();
}
}
bool nodebank_panel::set_combo(char const *Label, model_set_ref &Ref, char const *Manuallabel)
{
auto changed{false};
auto const preview{set_name(Ref, Manuallabel)};
if (false == ImGui::BeginCombo(Label, preview.c_str()))
{
return false;
}
if (Manuallabel != nullptr)
{
if (ImGui::Selectable(Manuallabel, Ref.kind == model_set_ref::source::manual))
{
Ref = model_set_ref{};
changed = true;
}
}
ImGui::TextDisabled("User sets");
auto const &sets{EditorModelSets.sets()};
if (sets.empty())
{
ImGui::TextDisabled(" (none yet, see Model sets...)");
}
for (auto const &set : sets)
{
auto const label{" " + set.name + " (" + std::to_string(set.templates.size()) + ")##userset" + std::to_string(set.id)};
if (ImGui::Selectable(label.c_str(), Ref.kind == model_set_ref::source::user && Ref.id == set.id))
{
Ref = {model_set_ref::source::user, set.id};
changed = true;
}
}
ImGui::TextDisabled("Node bank groups");
auto const groups{group_names()};
for (int idx = 0; idx < static_cast<int>(groups.size()); ++idx)
{
auto const label{" " + groups[idx] + "##bankgroup" + std::to_string(idx)};
if (ImGui::Selectable(label.c_str(), Ref.kind == model_set_ref::source::nodebank && Ref.id == idx))
{
Ref = {model_set_ref::source::nodebank, idx};
changed = true;
}
}
ImGui::EndCombo();
return changed;
}
void nodebank_panel::manual_list(char const *Id, std::vector<std::string> &List, int &Selected)
{
ImGui::PushID(Id);
if (ImGui::ListBoxHeader("##list", ImVec2(-1, ImGui::GetTextLineHeightWithSpacing() * 6.5f)))
{
for (int idx = 0; idx < static_cast<int>(List.size()); ++idx)
{
auto const label{generate_node_label(List[idx]) + "##" + std::to_string(idx)};
if (list_item(label.c_str(), Selected == idx))
Selected = idx;
}
if (List.empty())
{
ImGui::TextDisabled("(empty)");
}
ImGui::ListBoxFooter();
}
if (ImGui::Button("Add selected"))
{
if (m_selectedtemplate && false == m_selectedtemplate->empty())
List.push_back(*m_selectedtemplate);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Adds the template selected in the node bank list");
}
ImGui::SameLine();
if (ImGui::Button("Remove") && Selected >= 0 && Selected < static_cast<int>(List.size()))
{
List.erase(List.begin() + Selected);
Selected = -1;
}
ImGui::SameLine();
if (ImGui::Button("Clear"))
{
List.clear();
Selected = -1;
}
ImGui::SameLine();
if (ImGui::Button("Save as set") && false == List.empty())
{
open_sets_window(EditorModelSets.create("New set", List));
}
ImGui::PopID();
}
std::vector<std::string const *> nodebank_panel::set_entries(model_set_ref const &Ref) const
{
std::vector<std::string const *> entries;
if (Ref.kind == model_set_ref::source::user)
{
if (auto const *set = EditorModelSets.find(Ref.id))
{
for (auto const &entry : set->templates)
entries.push_back(&entry);
}
}
else if (Ref.kind == model_set_ref::source::nodebank && Ref.id >= 0)
{
// same grouping as group_templates(), without copying the templates
int group{0};
bool started{false};
for (auto const &entry : m_nodebank)
{
if (entry.second->empty())
{
if (started)
++group;
started = true;
continue;
}
started = true;
if (group == Ref.id)
entries.push_back(entry.second.get());
else if (group > Ref.id)
break;
}
}
return entries;
}
std::string const *nodebank_panel::random_template(model_set_ref const &Ref) const
{
auto const entries{set_entries(Ref)};
if (entries.empty())
return nullptr;
std::uniform_int_distribution<std::size_t> dist(0, entries.size() - 1);
return entries[dist(Global.local_random_engine)];
}
std::string nodebank_panel::set_name(model_set_ref const &Ref, char const *Manuallabel) const
{
switch (Ref.kind)
{
case model_set_ref::source::user:
{
auto const *set = EditorModelSets.find(Ref.id);
return set != nullptr ? set->name : "(deleted set)";
}
case model_set_ref::source::nodebank:
{
auto const groups{group_names()};
return Ref.id >= 0 && Ref.id < static_cast<int>(groups.size()) ? "Node bank: " + groups[Ref.id] : "(missing node bank group)";
}
default:
return Manuallabel != nullptr ? Manuallabel : "(none)";
}
}
void nodebank_panel::open_sets_window(int const Setid)
{
m_setsopen = true;
if (Setid != 0)
{
m_setsselected = Setid;
m_setsentry = -1;
}
}
void nodebank_panel::render_sets_window()
{
ImGui::SetNextWindowSize(ImVec2S(600, 380), ImGuiCond_FirstUseEver);
if (false == ImGui::Begin("Model sets", &m_setsopen, ImGuiWindowFlags_NoCollapse))
{
ImGui::End();
return;
}
auto const &sets{EditorModelSets.sets()};
if (EditorModelSets.find(m_setsselected) == nullptr)
{
m_setsselected = sets.empty() ? 0 : sets.front().id;
m_setsentry = -1;
}
// left side: the sets
auto const buttonsheight{ImGui::GetFrameHeightWithSpacing()};
ImGui::BeginGroup();
ImGui::BeginChild("##setlist", ImVec2(190 * Global.ui_scale, -buttonsheight), true);
for (auto const &set : sets)
{
auto const label{set.name + " (" + std::to_string(set.templates.size()) + ")##set" + std::to_string(set.id)};
if (list_item(label.c_str(), set.id == m_setsselected))
{
m_setsselected = set.id;
m_setsentry = -1;
}
}
if (sets.empty())
{
ImGui::TextDisabled("No sets yet");
}
ImGui::EndChild();
if (ImGui::Button("New"))
{
open_sets_window(EditorModelSets.create("New set"));
}
auto const *edited{EditorModelSets.find(m_setsselected)};
if (edited != nullptr)
{
ImGui::SameLine();
if (ImGui::Button("Duplicate"))
{
open_sets_window(EditorModelSets.create(edited->name + " copy", edited->templates));
edited = EditorModelSets.find(m_setsselected);
}
ImGui::SameLine();
if (ImGui::Button("Delete"))
{
ImGui::OpenPopup("##deleteset");
}
if (ImGui::BeginPopup("##deleteset"))
{
ImGui::Text("Delete set \"%s\"?", edited->name.c_str());
if (ImGui::Button("Delete##confirm"))
{
EditorModelSets.remove(edited->id);
edited = nullptr;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
ImGui::EndGroup();
ImGui::SameLine();
// right side: content of the selected set
ImGui::BeginGroup();
if (edited == nullptr)
{
ImGui::TextDisabled("Create a set with \"New\", or use \"Save as set\" on a brush or area fill list.");
ImGui::TextDisabled("Sets can be chosen in Insert (random model), Brush and Area fill.");
}
else
{
if (m_setsnameid != edited->id)
{
// selection changed, load its name into the edit buffer
std::snprintf(m_setsname, sizeof(m_setsname), "%s", edited->name.c_str());
m_setsnameid = edited->id;
}
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Rename").x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x);
auto const entered{ImGui::InputText("##setname", m_setsname, IM_ARRAYSIZE(m_setsname), ImGuiInputTextFlags_EnterReturnsTrue)};
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button("Rename") || entered)
{
EditorModelSets.rename(edited->id, m_setsname);
m_setsnameid = 0; // reload, the name could have been adjusted to stay unique
}
ImGui::BeginChild("##setentries", ImVec2(0, -buttonsheight * 2), true);
for (int idx = 0; idx < static_cast<int>(edited->labels.size()); ++idx)
{
auto const label{edited->labels[idx] + "##entry" + std::to_string(idx)};
if (list_item(label.c_str(), idx == m_setsentry))
m_setsentry = idx;
}
if (edited->templates.empty())
{
ImGui::TextDisabled("(empty) select a template in the node bank and press \"Add selected\"");
}
ImGui::EndChild();
auto const setid{edited->id};
if (ImGui::Button("Add selected"))
{
if (m_selectedtemplate && false == m_selectedtemplate->empty())
EditorModelSets.add(setid, {*m_selectedtemplate});
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Adds the template selected in the node bank list");
}
ImGui::SameLine();
if (ImGui::Button("Remove") && m_setsentry >= 0)
{
EditorModelSets.erase(setid, static_cast<std::size_t>(m_setsentry));
m_setsentry = -1;
}
ImGui::SameLine();
if (ImGui::Button("Clear"))
{
ImGui::OpenPopup("##clearset");
}
if (ImGui::BeginPopup("##clearset"))
{
ImGui::TextUnformatted("Remove all templates from this set?");
if (ImGui::Button("Clear##confirm"))
{
EditorModelSets.clear(setid);
m_setsentry = -1;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
// whole node bank group at once
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Add group").x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x);
auto const groups{group_names()};
m_setsgroup.id = groups.empty() ? -1 : std::clamp(m_setsgroup.id, 0, static_cast<int>(groups.size()) - 1);
if (ImGui::BeginCombo("##addgroup", m_setsgroup.id >= 0 ? groups[m_setsgroup.id].c_str() : "(no node bank groups)"))
{
for (int idx = 0; idx < static_cast<int>(groups.size()); ++idx)
{
auto const label{groups[idx] + "##addgroup" + std::to_string(idx)};
if (ImGui::Selectable(label.c_str(), idx == m_setsgroup.id))
m_setsgroup.id = idx;
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button("Add group"))
{
std::vector<std::string> templates;
for (auto const *entry : set_entries(m_setsgroup))
templates.push_back(*entry);
EditorModelSets.add(setid, templates);
}
}
ImGui::EndGroup();
ImGui::End();
} }
void nodebank_panel::add_template(const std::string &desc) void nodebank_panel::add_template(const std::string &desc)
@@ -691,15 +1048,7 @@ void functions_panel::render()
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{ {
// header section // header section
render_controls();
ImGui::RadioButton("Random rotation", (int *)&rot_mode, RANDOM);
ImGui::RadioButton("Fixed rotation", (int *)&rot_mode, FIXED);
if (rot_mode == FIXED)
{
// ImGui::Checkbox("Get rotation from last object", &rot_from_last);
ImGui::SliderFloat("Rotation Value", &rot_value, 0.0f, 360.0f, "%.1f");
};
ImGui::RadioButton("Default rotation", (int *)&rot_mode, DEFAULT);
for (auto const &line : text_lines) for (auto const &line : text_lines)
{ {
ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str()); ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str());
@@ -707,3 +1056,19 @@ void functions_panel::render()
} }
ImGui::End(); ImGui::End();
} }
void functions_panel::render_controls()
{
// order matches rotation_mode
char const *const modes[] = {"Random", "Fixed", "Default (from template)"};
auto current{static_cast<int>(rot_mode)};
if (ImGui::Combo("Rotation", &current, modes, IM_ARRAYSIZE(modes)))
{
rot_mode = static_cast<rotation_mode>(current);
}
if (rot_mode == FIXED)
{
// ImGui::Checkbox("Get rotation from last object", &rot_from_last);
ImGui::SliderFloat("Rotation value", &rot_value, 0.0f, 360.0f, "%.1f deg");
}
}

View File

@@ -11,6 +11,8 @@ http://mozilla.org/MPL/2.0/.
#include "application/uilayer.h" #include "application/uilayer.h"
#include "utilities/Classes.h" #include "utilities/Classes.h"
#include <functional>
/* /*
// helper, associated bool is set when the primary value was changed and expects processing at the observer's leisure // helper, associated bool is set when the primary value was changed and expects processing at the observer's leisure
template<typename Type_> template<typename Type_>
@@ -37,6 +39,8 @@ class itemproperties_panel : public ui_panel
// are still treated as read-only. // are still treated as read-only.
void update(scene::basic_node *Node); void update(scene::basic_node *Node);
void render() override; void render() override;
// panel content without the window, also drawn inside the toolset window
void render_body();
private: private:
// methods // methods
@@ -53,22 +57,35 @@ class itemproperties_panel : public ui_panel
std::vector<text_line> m_grouplines; std::vector<text_line> m_grouplines;
}; };
class brush_object_list : public ui_panel // reference to a set of node templates, used by the random insert, the brush and the area fill
struct model_set_ref
{
enum class source
{
manual, // list assembled by hand in the tool itself (or no set, where the tool has no such list)
user, // user-defined set (see EditorModelSets), id == set id
nodebank // node bank group, id == group index (as returned by nodebank_panel::group_names())
};
source kind{source::manual};
int id{-1};
};
class nodebank_panel;
// brush settings, drawn inside the node bank window when the brush mode is active
class brush_object_list
{ {
private: private:
int idx; int idx{-1};
static bool VectorGetter(void *data, int idx, const char **out_text);
std::string Template;
public: public:
brush_object_list(std::string const &Name, bool const Isopen); void render_options(nodebank_panel &Bank);
void render() override;
void update(std::string nodeTemplate);
// class use // class use
std::vector<std::string> Objects; std::vector<std::string> Objects; // manual list, used when source is set to manual
std::string *GetRandomObject(); std::string *GetRandomObject();
bool useRandom = {false}; bool useRandom = {false};
model_set_ref source;
float spacing{1.0f}; float spacing{1.0f};
}; };
@@ -96,13 +113,39 @@ class nodebank_panel : public ui_panel
// node templates of the group with specified index (as returned by group_names()) // node templates of the group with specified index (as returned by group_names())
std::vector<std::string> group_templates(std::size_t const Group) const; std::vector<std::string> group_templates(std::size_t const Group) const;
// model sets (user-defined sets and node bank groups)
// combo selecting a set. Manuallabel names the tool's own hand-made list; nullptr hides that choice. returns: true on change
bool set_combo(char const *Label, model_set_ref &Ref, char const *Manuallabel);
// list of templates assembled by hand, with buttons to add the node bank selection, remove entries and save it as a user set
void manual_list(char const *Id, std::vector<std::string> &List, int &Selected);
// node templates of referenced set; empty for the manual list or a set which no longer exists
std::vector<std::string const *> set_entries(model_set_ref const &Ref) const;
// random template from referenced set, or nullptr if the set is empty
std::string const *random_template(model_set_ref const &Ref) const;
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);
// draws settings of the active edit mode inside its tab
std::function<void(edit_mode)> mode_options;
// draws sections attached above the mode tabs (gizmo, node properties)
std::function<void()> header_sections;
private: private:
// methods: // methods:
std::string generate_node_label(std::string Input) const; std::string generate_node_label(std::string Input) const;
void render_sets_window();
// members: // members:
std::vector<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank; std::vector<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank;
char m_nodesearch[128]; char m_nodesearch[128];
std::shared_ptr<std::string> m_selectedtemplate; std::shared_ptr<std::string> m_selectedtemplate;
// set manager window
bool m_setsopen{false};
int m_setsselected{0}; // id of the user set being edited
int m_setsentry{-1}; // selected template of the edited set
int m_setsnameid{0}; // id of the set whose name is held in the edit buffer
char m_setsname[128]{};
model_set_ref m_setsgroup{model_set_ref::source::nodebank, 0}; // node bank group to append to the edited set
}; };
class functions_panel : public ui_panel class functions_panel : public ui_panel
@@ -124,6 +167,8 @@ class functions_panel : public ui_panel
void update(scene::basic_node const *Node); void update(scene::basic_node const *Node);
void render() override; void render() override;
// rotation settings widgets, also drawn inside the node bank window
void render_controls();
private: private:
// methods // methods

View File

@@ -130,67 +130,89 @@ bool ui_layer::mouse_button_callback(int button, int action, int mods)
return m_imguiio->WantCaptureMouse; return m_imguiio->WantCaptureMouse;
} }
static ImVec4 imvec_lerp(const ImVec4 &a, const ImVec4 &b, float t)
{
return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t);
}
void ui_layer::imgui_style() void ui_layer::imgui_style()
{ {
// palette shared with the MaSzyna starter: flat dark panels, thin borders, green accents
auto const rgb = [](int const Color, float const Alpha = 1.0f) {
return ImVec4(((Color >> 16) & 0xff) / 255.0f, ((Color >> 8) & 0xff) / 255.0f, (Color & 0xff) / 255.0f, Alpha);
};
int constexpr panel{0x1e2327}; // window background
int constexpr base{0x15171b}; // darker background, between panels
int constexpr header{0x2a3036}; // section headers, hovered items
int constexpr border{0x3a424a};
int constexpr field{0x121517}; // input fields, combos
int constexpr accent{0x177f00}; // selection, active elements
int constexpr accentbright{0x41c400}; // marks: checkmarks, active tab underline
int constexpr accentdim{0x164b0e};
ImVec4 *colors = ImGui::GetStyle().Colors; ImVec4 *colors = ImGui::GetStyle().Colors;
colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); colors[ImGuiCol_Text] = rgb(0xffffff);
colors[ImGuiCol_TextDisabled] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f); colors[ImGuiCol_TextDisabled] = rgb(0xa0a0a2);
colors[ImGuiCol_WindowBg] = ImVec4(0.04f, 0.04f, 0.04f, Global.UIBgOpacity); // ui.bg.opacity from config file colors[ImGuiCol_WindowBg] = rgb(panel, Global.UIBgOpacity); // ui.bg.opacity from config file
colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
colors[ImGuiCol_PopupBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); colors[ImGuiCol_PopupBg] = rgb(base, 0.98f);
colors[ImGuiCol_Border] = ImVec4(0.31f, 0.34f, 0.31f, 0.50f); colors[ImGuiCol_Border] = rgb(border);
colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
colors[ImGuiCol_FrameBg] = ImVec4(0.21f, 0.28f, 0.17f, 0.54f); colors[ImGuiCol_FrameBg] = rgb(field);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.40f); colors[ImGuiCol_FrameBgHovered] = rgb(header);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.42f, 0.64f, 0.23f, 0.67f); colors[ImGuiCol_FrameBgActive] = rgb(border);
colors[ImGuiCol_TitleBg] = ImVec4(0.03f, 0.03f, 0.03f, 1.00f); colors[ImGuiCol_TitleBg] = rgb(base);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.21f, 0.28f, 0.17f, 1.00f); colors[ImGuiCol_TitleBgActive] = rgb(header);
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); colors[ImGuiCol_TitleBgCollapsed] = rgb(base, 0.75f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f); colors[ImGuiCol_MenuBarBg] = rgb(panel);
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.01f, 0.01f, 0.01f, 0.53f); colors[ImGuiCol_ScrollbarBg] = rgb(base, 0.60f);
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); colors[ImGuiCol_ScrollbarGrab] = rgb(border);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.29f, 0.29f, 0.29f, 1.00f); colors[ImGuiCol_ScrollbarGrabHovered] = rgb(0x5a636c);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.36f, 0.36f, 1.00f); colors[ImGuiCol_ScrollbarGrabActive] = rgb(0x858585);
colors[ImGuiCol_CheckMark] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f); colors[ImGuiCol_CheckMark] = rgb(accentbright);
colors[ImGuiCol_SliderGrab] = ImVec4(0.37f, 0.53f, 0.25f, 1.00f); colors[ImGuiCol_SliderGrab] = rgb(accent);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f); colors[ImGuiCol_SliderGrabActive] = rgb(accentbright);
colors[ImGuiCol_Button] = ImVec4(0.42f, 0.64f, 0.23f, 0.40f); colors[ImGuiCol_Button] = rgb(header);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f); colors[ImGuiCol_ButtonHovered] = rgb(border);
colors[ImGuiCol_ButtonActive] = ImVec4(0.37f, 0.54f, 0.19f, 1.00f); colors[ImGuiCol_ButtonActive] = rgb(accent);
colors[ImGuiCol_Header] = ImVec4(0.42f, 0.64f, 0.23f, 0.31f); // shared by section headers and selected list items
colors[ImGuiCol_HeaderHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.80f); colors[ImGuiCol_Header] = rgb(header);
colors[ImGuiCol_HeaderActive] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f); colors[ImGuiCol_HeaderHovered] = rgb(accentdim);
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.29f, 0.41f, 0.18f, 0.78f); colors[ImGuiCol_HeaderActive] = rgb(accent);
colors[ImGuiCol_SeparatorActive] = ImVec4(0.29f, 0.41f, 0.18f, 1.00f); colors[ImGuiCol_Separator] = rgb(border);
colors[ImGuiCol_ResizeGrip] = ImVec4(0.42f, 0.64f, 0.23f, 0.25f); colors[ImGuiCol_SeparatorHovered] = rgb(accent);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.67f); colors[ImGuiCol_SeparatorActive] = rgb(accentbright);
colors[ImGuiCol_ResizeGripActive] = ImVec4(0.42f, 0.64f, 0.23f, 0.95f); colors[ImGuiCol_ResizeGrip] = rgb(border, 0.50f);
colors[ImGuiCol_ResizeGripHovered] = rgb(accent);
colors[ImGuiCol_ResizeGripActive] = rgb(accentbright);
colors[ImGuiCol_Tab] = rgb(header);
colors[ImGuiCol_TabHovered] = rgb(border);
colors[ImGuiCol_TabActive] = rgb(accent); // also the line under the tab bar
colors[ImGuiCol_TabUnfocused] = rgb(header);
colors[ImGuiCol_TabUnfocusedActive] = rgb(accentdim);
colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f);
colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f);
colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f);
colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.42f, 0.64f, 0.23f, 0.35f); colors[ImGuiCol_TextSelectedBg] = rgb(accent, 0.60f);
colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); colors[ImGuiCol_DragDropTarget] = rgb(accentbright, 0.90f);
colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); colors[ImGuiCol_NavHighlight] = rgb(accentbright);
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); colors[ImGuiCol_ModalWindowDimBg] = rgb(base, 0.60f);
colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; // flat, square elements with thin borders
colors[ImGuiCol_Tab] = imvec_lerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); auto &style = ImGui::GetStyle();
colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; style.WindowRounding = 0.0f;
colors[ImGuiCol_TabActive] = imvec_lerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); style.ChildRounding = 0.0f;
colors[ImGuiCol_TabUnfocused] = imvec_lerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); style.FrameRounding = 0.0f;
colors[ImGuiCol_TabUnfocusedActive] = imvec_lerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); style.PopupRounding = 0.0f;
style.ScrollbarRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.ChildBorderSize = 1.0f;
style.PopupBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
style.TabBorderSize = 0.0f;
ImGui::GetStyle().ScaleAllSizes(Global.ui_scale); style.ScaleAllSizes(Global.ui_scale);
} }
bool ui_layer::init(GLFWwindow *Window) bool ui_layer::init(GLFWwindow *Window)

235
editor/editorModelSets.cpp Normal file
View File

@@ -0,0 +1,235 @@
/*
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 "editor/editorModelSets.hpp"
#include "utilities/Logs.h"
#include "utilities/parser.h"
#include "utilities/utilities.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
editorModelSets EditorModelSets;
// file layout:
// set <name>
// <node template, single line>
// ...
// endset
namespace
{
namespace fs = std::filesystem;
fs::path sets_path()
{
fs::path p = user_config_path("eu07_editor_sets.txt");
return p.empty() ? fs::path("eu07_editor_sets.txt") : p;
}
std::string trimmed(std::string Text)
{
auto const first = Text.find_first_not_of(" \t");
if (first == std::string::npos)
return {};
auto const last = Text.find_last_not_of(" \t");
return Text.substr(first, last - first + 1);
}
// the file stores one template per line, so line breaks inside a template (e.g. from export_as_text) become plain separators
std::string single_line(std::string Text)
{
std::replace_if(Text.begin(), Text.end(), [](char const Character) { return Character == '\r' || Character == '\n' || Character == '\t'; }, ' ');
return trimmed(Text);
}
} // namespace
bool editorModelSets::load()
{
m_sets.clear();
const fs::path path = sets_path();
std::error_code ec;
if (!fs::exists(path, ec))
return false;
std::ifstream stream(path, std::ios_base::in | std::ios_base::binary);
if (!stream.is_open())
return false;
editor_model_set *current{nullptr};
std::string line;
while (std::getline(stream, line))
{
line = single_line(line);
if (line.empty() || line.rfind("//", 0) == 0)
continue;
// a set header also closes a set with missing "endset"; templates always start with "node"
if (line.rfind("set", 0) == 0 && (line.size() == 3 || line[3] == ' '))
{
editor_model_set set;
set.id = m_nextid++;
set.name = unique_name(line.substr(3));
m_sets.push_back(std::move(set));
current = &m_sets.back();
continue;
}
if (line == "endset")
{
current = nullptr;
continue;
}
if (current == nullptr)
continue;
current->labels.push_back(label(line));
current->templates.push_back(std::move(line));
}
return true;
}
bool editorModelSets::save() const
{
const fs::path path = sets_path();
std::error_code ec;
fs::create_directories(path.parent_path(), ec);
std::ofstream stream(path, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary);
if (!stream.is_open())
{
ErrorLog("failed to save editor model sets");
return false;
}
stream << "// MaSzyna editor: user-defined model sets\n";
for (auto const &set : m_sets)
{
stream << "\nset " << set.name << "\n";
for (auto const &entry : set.templates)
stream << entry << "\n";
stream << "endset\n";
}
return true;
}
editor_model_set const *editorModelSets::find(int const Id) const
{
auto const lookup = std::find_if(m_sets.begin(), m_sets.end(), [Id](auto const &Set) { return Set.id == Id; });
return lookup != m_sets.end() ? &(*lookup) : nullptr;
}
editor_model_set *editorModelSets::find_(int const Id)
{
auto const lookup = std::find_if(m_sets.begin(), m_sets.end(), [Id](auto const &Set) { return Set.id == Id; });
return lookup != m_sets.end() ? &(*lookup) : nullptr;
}
int editorModelSets::create(std::string const &Name, std::vector<std::string> const &Templates)
{
editor_model_set set;
set.id = m_nextid++;
set.name = unique_name(Name);
for (auto const &entry : Templates)
{
auto line = single_line(entry);
if (line.empty())
continue;
set.labels.push_back(label(line));
set.templates.push_back(std::move(line));
}
m_sets.push_back(std::move(set));
save();
return m_sets.back().id;
}
void editorModelSets::remove(int const Id)
{
auto const lookup = std::find_if(m_sets.begin(), m_sets.end(), [Id](auto const &Set) { return Set.id == Id; });
if (lookup == m_sets.end())
return;
m_sets.erase(lookup);
save();
}
void editorModelSets::rename(int const Id, std::string const &Name)
{
auto *set = find_(Id);
if (set == nullptr)
return;
auto const name = unique_name(Name, Id);
if (name == set->name)
return;
set->name = name;
save();
}
void editorModelSets::add(int const Id, std::vector<std::string> const &Templates)
{
auto *set = find_(Id);
if (set == nullptr)
return;
auto const count{set->templates.size()};
for (auto const &entry : Templates)
{
auto line = single_line(entry);
if (line.empty())
continue;
set->labels.push_back(label(line));
set->templates.push_back(std::move(line));
}
if (set->templates.size() != count)
save();
}
void editorModelSets::erase(int const Id, std::size_t const Index)
{
auto *set = find_(Id);
if (set == nullptr || Index >= set->templates.size())
return;
set->templates.erase(set->templates.begin() + Index);
set->labels.erase(set->labels.begin() + Index);
save();
}
void editorModelSets::clear(int const Id)
{
auto *set = find_(Id);
if (set == nullptr || set->templates.empty())
return;
set->templates.clear();
set->labels.clear();
save();
}
std::string editorModelSets::unique_name(std::string const &Base, int const IgnoreId) const
{
auto base = single_line(Base);
if (base.empty())
base = "New set";
auto const taken = [&](std::string const &Name) {
return std::any_of(m_sets.begin(), m_sets.end(), [&](auto const &Set) { return Set.id != IgnoreId && Set.name == Name; });
};
auto name = base;
for (int suffix = 2; taken(name); ++suffix)
name = base + " " + std::to_string(suffix);
return name;
}
std::string editorModelSets::label(std::string const &Template)
{
cParser tokenizer(Template);
tokenizer.getTokens(9, false); // node, ranges, name, type, position, rotation
auto model{tokenizer.getToken<std::string>(false)};
auto texture{tokenizer.getToken<std::string>(false)};
replace_slashes(model);
erase_extension(model);
replace_slashes(texture);
return texture == "none" || texture.empty() ? model : model + " (" + texture + ")";
}

View File

@@ -0,0 +1,56 @@
/*
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/.
*/
#pragma once
#include <string>
#include <vector>
// user-defined sets of node templates (e.g. "deciduous trees", "bushes"), used by the editor brush,
// random insert and area fill. stored next to the user's ini files
struct editor_model_set
{
int id{0}; // runtime identifier, stable while the editor runs (renames don't break references)
std::string name;
std::vector<std::string> templates; // node definitions, one per entry, same format as nodebank entries
std::vector<std::string> labels; // short readable names of the templates, cached for the UI
};
class editorModelSets
{
public:
bool load();
bool save() const;
std::vector<editor_model_set> const &sets() const
{
return m_sets;
}
editor_model_set const *find(int Id) const;
// all modifying methods save the sets right away
// returns: id of the new set
int create(std::string const &Name, std::vector<std::string> const &Templates = {});
void remove(int Id);
void rename(int Id, std::string const &Name);
void add(int Id, std::vector<std::string> const &Templates);
void erase(int Id, std::size_t Index);
void clear(int Id);
// name not used by any other set, built from specified base
std::string unique_name(std::string const &Base, int IgnoreId = 0) const;
// short readable name of a node template (model file and texture)
static std::string label(std::string const &Template);
private:
editor_model_set *find_(int Id);
std::vector<editor_model_set> m_sets;
int m_nextid{1};
};
extern editorModelSets EditorModelSets;