diff --git a/CMakeLists.txt b/CMakeLists.txt index 56cf1bce..5f4ae645 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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" diff --git a/application/editormode.cpp b/application/editormode.cpp index 94b1424d..1bdd4ed7 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -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(false)}; - auto texture{tokenizer.getToken(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(); + // 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", ¤t, 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", ¤t, 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 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(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(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(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(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(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(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(m_gizmo_op); ImGui::RadioButton("Translate (Q)", &op, static_cast(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) { diff --git a/application/editormode.h b/application/editormode.h index 413b4225..d8dfa979 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -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 m_fill_last; // instances created by the most recent fill, for "Undo last fill" std::vector 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 diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index b00b1e51..17514efb 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -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 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 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(); } diff --git a/application/editoruilayer.h b/application/editoruilayer.h index 57d75466..8b15d35d 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -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 nodebank_groups() const; - std::vector 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 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 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 m_filloptions; + std::function m_gizmooptions; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any }; diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 80eec0ba..6c9a2df9 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -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 *>(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 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(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 &List, int &Selected) +{ + ImGui::PushID(Id); + if (ImGui::ListBoxHeader("##list", ImVec2(-1, ImGui::GetTextLineHeightWithSpacing() * 6.5f))) + { + for (int idx = 0; idx < static_cast(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(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 nodebank_panel::set_entries(model_set_ref const &Ref) const +{ + std::vector 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 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(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(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(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(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(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 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,19 +1048,27 @@ 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()); } } ImGui::End(); +} + +void functions_panel::render_controls() +{ + // order matches rotation_mode + char const *const modes[] = {"Random", "Fixed", "Default (from template)"}; + auto current{static_cast(rot_mode)}; + if (ImGui::Combo("Rotation", ¤t, modes, IM_ARRAYSIZE(modes))) + { + rot_mode = static_cast(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"); + } } \ No newline at end of file diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 50e8bf99..55a0ca1a 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -11,6 +11,8 @@ http://mozilla.org/MPL/2.0/. #include "application/uilayer.h" #include "utilities/Classes.h" + +#include /* // helper, associated bool is set when the primary value was changed and expects processing at the observer's leisure template @@ -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 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 Objects; + std::vector 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 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 &List, int &Selected); + // node templates of referenced set; empty for the manual list or a set which no longer exists + std::vector 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 mode_options; + // draws sections attached above the mode tabs (gizmo, node properties) + std::function header_sections; + private: // methods: std::string generate_node_label(std::string Input) const; + void render_sets_window(); // members: std::vector>> m_nodebank; char m_nodesearch[128]; std::shared_ptr 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 diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 60c8a778..19f32302 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -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) diff --git a/editor/editorModelSets.cpp b/editor/editorModelSets.cpp new file mode 100644 index 00000000..4a9c10ec --- /dev/null +++ b/editor/editorModelSets.cpp @@ -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 +#include +#include + +editorModelSets EditorModelSets; + +// file layout: +// set +// +// ... +// 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 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 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(false)}; + auto texture{tokenizer.getToken(false)}; + replace_slashes(model); + erase_extension(model); + replace_slashes(texture); + return texture == "none" || texture.empty() ? model : model + " (" + texture + ")"; +} diff --git a/editor/editorModelSets.hpp b/editor/editorModelSets.hpp new file mode 100644 index 00000000..9e9f6362 --- /dev/null +++ b/editor/editorModelSets.hpp @@ -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 +#include + +// 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 templates; // node definitions, one per entry, same format as nodebank entries + std::vector labels; // short readable names of the templates, cached for the UI +}; + +class editorModelSets +{ + public: + bool load(); + bool save() const; + + std::vector 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 const &Templates = {}); + void remove(int Id); + void rename(int Id, std::string const &Name); + void add(int Id, std::vector 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 m_sets; + int m_nextid{1}; +}; + +extern editorModelSets EditorModelSets;