17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +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"
"input/editorkeyboardinput.cpp"
"editor/editorSettings.cpp"
"editor/editorModelSets.cpp"
"editor/editorTerrain.cpp"
"editor/editorTerrainStreamer.cpp"
"application/editormode.cpp"

View File

@@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/application.h"
#include "editor/editorSettings.hpp"
#include "editor/editorModelSets.hpp"
#include "utilities/Globals.h"
#include "simulation/simulation.h"
#include "simulation/simulationtime.h"
@@ -164,19 +165,6 @@ namespace
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
class triangle_grid
{
@@ -293,6 +281,9 @@ void editor_mode::editormode_input::poll()
editor_mode::editor_mode() {
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
@@ -303,6 +294,7 @@ editor_ui *editor_mode::ui() const
bool editor_mode::init()
{
EditorSettings.load();
EditorModelSets.load();
Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr);
return m_input.init();
}
@@ -473,17 +465,18 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button)
if (!mouseHold || !viewport_click())
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();
if (!src)
return;
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);
oldPos = newPos;
m_brush_has_last = true;
@@ -819,9 +812,6 @@ bool editor_mode::update()
#endif
m_userinterface->update();
// update brush settings visibility depending on panel mode
ui()->toggleBrushSettings(ui()->mode() == nodebank_panel::BRUSH);
if (mouseHold)
{
// process continuous brush placement
@@ -892,12 +882,9 @@ bool editor_mode::update()
// --- ImGuizmo: in-viewport transform gizmo for the selected node ---
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)
{
draw_area_fill_outline();
render_area_fill();
}
// --- ImGui: Editor Settings & History windows ---
if(m_settings_open)
@@ -914,31 +901,39 @@ void editor_mode::render_settings()
{
ImGui::Begin("Editor Settings", &m_settings_open, ImGuiWindowFlags_AlwaysAutoResize);
ImGui::TextUnformatted("Camera movement");
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)))
if (ImGui::BeginTabBar("##editorsettings"))
{
EditorSettings.movement(current == 1 ? editorSettings::movement_scheme::legacy
: editorSettings::movement_scheme::wsad);
m_input.keyboard.apply_scheme();
EditorSettings.save();
if (ImGui::BeginTabItem("General"))
{
ImGui::TextUnformatted("Camera movement");
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();
}
void editor_mode::render_terrain_ui()
{
ImGui::Separator();
ImGui::TextUnformatted("Terrain");
ImGui::SetNextItemWidth(120.0f);
ImGui::InputInt("Grid cells", &m_terrain_cells);
m_terrain_cells = std::clamp(m_terrain_cells, 1, 512);
@@ -1406,10 +1401,11 @@ void editor_mode::run_area_fill()
// model 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;
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());
if (package.empty())
{
@@ -1621,9 +1617,7 @@ void editor_mode::draw_area_fill_outline() const
void editor_mode::render_area_fill()
{
ImGui::Begin("Area fill", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing);
ImGui::TextUnformatted("LMB: add outline point, Backspace: remove last point");
ImGui::TextDisabled("LMB: add outline point Backspace: remove last point");
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);
if (ImGui::Button("Remove last point") && !m_fill_points.empty())
@@ -1634,58 +1628,13 @@ void editor_mode::render_area_fill()
ImGui::Separator();
// model package: a node bank group, or a custom set assembled from node bank templates
auto const groups = ui()->nodebank_groups();
m_fill_package = std::clamp(m_fill_package, 0, static_cast<int>(groups.size()));
std::string const current = m_fill_package == 0 ? std::string("Custom set") : groups[m_fill_package - 1];
ImGui::SetNextItemWidth(250.0f);
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;
}
}
// model package: a hand-assembled list, a user-defined set or a node bank group
auto &bank = ui()->nodebank();
bank.set_combo("Model set", m_fill_source, "Custom list");
if (m_fill_source.kind == model_set_ref::source::manual)
bank.manual_list("fillset", m_fill_custom, m_fill_custom_idx);
else
{
ImGui::Text("Templates in group: %zu", ui()->nodebank_group_templates(static_cast<std::size_t>(m_fill_package - 1)).size());
}
ImGui::TextDisabled("%zu templates in set", bank.set_entries(m_fill_source).size());
ImGui::Separator();
@@ -1697,7 +1646,9 @@ void editor_mode::render_area_fill()
m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing);
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::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::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())
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
if (!m_gizmo_enabled || m_terrain_sculpt || m_chunk_edit)
ImGui::Checkbox("Enabled", &m_gizmo_enabled);
if (!m_gizmo_enabled)
return;
if (m_terrain_sculpt || m_chunk_edit)
{
m_gizmo_using = false;
ImGui::TextDisabled("Suspended while editing terrain");
return;
}
// compact control window: lets the user pick the transform mode without keyboard shortcuts
ImGui::Begin("Gizmo", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing);
// lets the user pick the transform mode without keyboard shortcuts
int op = static_cast<int>(m_gizmo_op);
ImGui::RadioButton("Translate (Q)", &op, static_cast<int>(gizmo_operation::translate));
ImGui::SameLine();
@@ -1745,7 +1695,16 @@ void editor_mode::render_gizmo()
}
if (!m_node)
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)
{

View File

@@ -1,4 +1,4 @@
/*
/*
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
@@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include "application/applicationmode.h"
#include "application/editoruipanels.h"
#include "input/editormouseinput.h"
#include "input/editorkeyboardinput.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<std::string> m_fill_custom; // user-assembled package of node templates
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_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
@@ -223,6 +224,8 @@ class editor_mode : public application_mode
// ImGuizmo-based transform gizmo for the selected node
enum class gizmo_operation { translate, rotate, scale };
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_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

View File

@@ -21,10 +21,10 @@ editor_ui::editor_ui()
clear_panels();
// 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_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
@@ -43,26 +43,67 @@ void editor_ui::update()
ui_layer::update();
m_itempropertiespanel.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)
m_brushobjects.is_open = isVisible;
switch (Mode)
{
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)
@@ -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)
{
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();
}

View File

@@ -36,15 +36,36 @@ class editor_ui : public ui_layer
const std::string *get_active_node_template(bool bypassRandom = false);
nodebank_panel::edit_mode mode();
float getSpacing();
void toggleBrushSettings(bool isVisible);
std::vector<std::string> nodebank_groups() const;
std::vector<std::string> nodebank_group_templates(std::size_t const Group) const;
// node bank and model set access (set selection widgets, set contents)
nodebank_panel &nodebank()
{
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:
// methods
void render_mode_options(nodebank_panel::edit_mode const Mode);
void render_header_sections();
// members
itemproperties_panel m_itempropertiespanel{"Node Properties", true};
functions_panel m_functionspanel{"Functions", true};
nodebank_panel m_nodebankpanel{"Node Bank", true};
brush_object_list m_brushobjects{"Brush properties", false};
itemproperties_panel m_itempropertiespanel{"Node Properties", true}; // not a window of its own, drawn in the toolset window
functions_panel m_functionspanel{"Functions", true}; // not a window of its own, its settings are drawn in the toolset tabs
nodebank_panel m_nodebankpanel{"Toolset", true}; // main editor window: gizmo, node properties, edit modes and the node bank
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
};

View File

@@ -19,6 +19,21 @@ http://mozilla.org/MPL/2.0/.
#include "world/MemCell.h"
#include "application/editoruilayer.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)
{
@@ -285,19 +300,24 @@ void itemproperties_panel::render()
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{
// 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();
render_body();
}
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),
// 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
@@ -375,27 +395,6 @@ bool itemproperties_panel::render_group()
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()
{
static std::string empty; // fallback
@@ -408,39 +407,25 @@ std::string *brush_object_list::GetRandomObject()
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;
}
auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0);
if (ImGui::Begin("Brush random set", nullptr, flags))
Bank.set_combo("Preset", source, "Manual list");
if (source.kind == model_set_ref::source::manual)
{
ImGui::SliderFloat("Spacing", &spacing, 0.1f, 20.0f, "%.1f m");
ImGui::Checkbox("Enable brush random from set", &useRandom);
if (useRandom)
{
ImGui::Text("Set of objects to choose from:");
ImGui::ListBox("", &idx, VectorGetter, (void *)&Objects, Objects.size(), 6);
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();
}
}
Bank.manual_list("brushset", Objects, idx);
}
else
{
auto const count{Bank.set_entries(source).size()};
ImGui::TextDisabled("%zu templates in set%s", count, count == 0 ? ", the node bank selection is used" : "");
}
ImGui::End();
}
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));
}
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)
{
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))
{
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"))
if (ImGui::Button("Reload node bank"))
{
nodebank_reload();
}
if (mode == BRUSH)
ImGui::SameLine();
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::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 isvisible{false};
@@ -572,16 +579,366 @@ void nodebank_panel::render()
continue;
}
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;
++idx;
}
}
ImGui::ListBoxFooter();
}
ImGui::PopItemWidth();
}
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)
@@ -691,15 +1048,7 @@ void functions_panel::render()
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{
// header section
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);
render_controls();
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());
@@ -707,3 +1056,19 @@ void functions_panel::render()
}
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 "utilities/Classes.h"
#include <functional>
/*
// helper, associated bool is set when the primary value was changed and expects processing at the observer's leisure
template<typename Type_>
@@ -37,6 +39,8 @@ class itemproperties_panel : public ui_panel
// are still treated as read-only.
void update(scene::basic_node *Node);
void render() override;
// panel content without the window, also drawn inside the toolset window
void render_body();
private:
// methods
@@ -53,22 +57,35 @@ class itemproperties_panel : public ui_panel
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:
int idx;
static bool VectorGetter(void *data, int idx, const char **out_text);
std::string Template;
int idx{-1};
public:
brush_object_list(std::string const &Name, bool const Isopen);
void render() override;
void update(std::string nodeTemplate);
void render_options(nodebank_panel &Bank);
// class use
std::vector<std::string> Objects;
std::vector<std::string> Objects; // manual list, used when source is set to manual
std::string *GetRandomObject();
bool useRandom = {false};
model_set_ref source;
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())
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:
// methods:
std::string generate_node_label(std::string Input) const;
void render_sets_window();
// members:
std::vector<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank;
char m_nodesearch[128];
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
@@ -124,6 +167,8 @@ class functions_panel : public ui_panel
void update(scene::basic_node const *Node);
void render() override;
// rotation settings widgets, also drawn inside the node bank window
void render_controls();
private:
// methods

View File

@@ -130,67 +130,89 @@ bool ui_layer::mouse_button_callback(int button, int action, int mods)
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()
{
// 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;
colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
colors[ImGuiCol_TextDisabled] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f);
colors[ImGuiCol_WindowBg] = ImVec4(0.04f, 0.04f, 0.04f, Global.UIBgOpacity); // ui.bg.opacity from config file
colors[ImGuiCol_Text] = rgb(0xffffff);
colors[ImGuiCol_TextDisabled] = rgb(0xa0a0a2);
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_PopupBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f);
colors[ImGuiCol_Border] = ImVec4(0.31f, 0.34f, 0.31f, 0.50f);
colors[ImGuiCol_PopupBg] = rgb(base, 0.98f);
colors[ImGuiCol_Border] = rgb(border);
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_FrameBgHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.40f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.42f, 0.64f, 0.23f, 0.67f);
colors[ImGuiCol_TitleBg] = ImVec4(0.03f, 0.03f, 0.03f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.21f, 0.28f, 0.17f, 1.00f);
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f);
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.01f, 0.01f, 0.01f, 0.53f);
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.29f, 0.29f, 0.29f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.36f, 0.36f, 1.00f);
colors[ImGuiCol_CheckMark] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f);
colors[ImGuiCol_SliderGrab] = ImVec4(0.37f, 0.53f, 0.25f, 1.00f);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.42f, 0.64f, 0.23f, 0.40f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.37f, 0.54f, 0.19f, 1.00f);
colors[ImGuiCol_Header] = ImVec4(0.42f, 0.64f, 0.23f, 0.31f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.80f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f);
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.29f, 0.41f, 0.18f, 0.78f);
colors[ImGuiCol_SeparatorActive] = ImVec4(0.29f, 0.41f, 0.18f, 1.00f);
colors[ImGuiCol_ResizeGrip] = ImVec4(0.42f, 0.64f, 0.23f, 0.25f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.67f);
colors[ImGuiCol_ResizeGripActive] = ImVec4(0.42f, 0.64f, 0.23f, 0.95f);
colors[ImGuiCol_FrameBg] = rgb(field);
colors[ImGuiCol_FrameBgHovered] = rgb(header);
colors[ImGuiCol_FrameBgActive] = rgb(border);
colors[ImGuiCol_TitleBg] = rgb(base);
colors[ImGuiCol_TitleBgActive] = rgb(header);
colors[ImGuiCol_TitleBgCollapsed] = rgb(base, 0.75f);
colors[ImGuiCol_MenuBarBg] = rgb(panel);
colors[ImGuiCol_ScrollbarBg] = rgb(base, 0.60f);
colors[ImGuiCol_ScrollbarGrab] = rgb(border);
colors[ImGuiCol_ScrollbarGrabHovered] = rgb(0x5a636c);
colors[ImGuiCol_ScrollbarGrabActive] = rgb(0x858585);
colors[ImGuiCol_CheckMark] = rgb(accentbright);
colors[ImGuiCol_SliderGrab] = rgb(accent);
colors[ImGuiCol_SliderGrabActive] = rgb(accentbright);
colors[ImGuiCol_Button] = rgb(header);
colors[ImGuiCol_ButtonHovered] = rgb(border);
colors[ImGuiCol_ButtonActive] = rgb(accent);
// shared by section headers and selected list items
colors[ImGuiCol_Header] = rgb(header);
colors[ImGuiCol_HeaderHovered] = rgb(accentdim);
colors[ImGuiCol_HeaderActive] = rgb(accent);
colors[ImGuiCol_Separator] = rgb(border);
colors[ImGuiCol_SeparatorHovered] = rgb(accent);
colors[ImGuiCol_SeparatorActive] = rgb(accentbright);
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_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 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_TextSelectedBg] = ImVec4(0.42f, 0.64f, 0.23f, 0.35f);
colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
colors[ImGuiCol_TextSelectedBg] = rgb(accent, 0.60f);
colors[ImGuiCol_DragDropTarget] = rgb(accentbright, 0.90f);
colors[ImGuiCol_NavHighlight] = rgb(accentbright);
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_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f);
colors[ImGuiCol_ModalWindowDimBg] = rgb(base, 0.60f);
colors[ImGuiCol_Separator] = colors[ImGuiCol_Border];
colors[ImGuiCol_Tab] = imvec_lerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f);
colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered];
colors[ImGuiCol_TabActive] = imvec_lerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f);
colors[ImGuiCol_TabUnfocused] = imvec_lerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f);
colors[ImGuiCol_TabUnfocusedActive] = imvec_lerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f);
// flat, square elements with thin borders
auto &style = ImGui::GetStyle();
style.WindowRounding = 0.0f;
style.ChildRounding = 0.0f;
style.FrameRounding = 0.0f;
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)

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;