17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-26 07:59:30 +01:00

feat(editor): presets of the track properties, applied on the selected tracks

(cherry picked from commit 83c8c23c80)
This commit is contained in:
2026-10-21 16:09:05 +02:00
committed by maj00r
parent 7e284b293d
commit a83d5c6978
11 changed files with 689 additions and 23 deletions

View File

@@ -182,6 +182,7 @@ set(SOURCES
"input/editorkeyboardinput.cpp"
"editor/editorSettings.cpp"
"editor/editorModelSets.cpp"
"editor/editorTrackPresets.cpp"
"editor/editorIncludeInfo.cpp"
"editor/editorTerrain.cpp"
"editor/editorTrack.cpp"

View File

@@ -28,6 +28,7 @@ extern char **environ;
#include "application/application.h"
#include "editor/editorSettings.hpp"
#include "editor/editorModelSets.hpp"
#include "editor/editorTrackPresets.hpp"
#include "editor/editorIncludeInfo.hpp"
#include "editor/editorGroundMesh.hpp"
#include "editor/editorFormat.hpp"
@@ -542,6 +543,7 @@ editor_mode::editor_mode() {
ui()->set_new_scenery([this]() { m_newscenery_asked = true; });
ui()->set_open_scenery([this]() { m_openscenery_asked = true; });
ui()->set_edit_menu([this]() { render_edit_menu(); });
ui()->set_tools_menu([this]() { render_track_presets_menu(); });
ui()->set_menu_options([this]() {
render_object_menu();
render_view_menu();
@@ -648,6 +650,7 @@ bool editor_mode::init()
Global.EditorOrthoTracks = EditorSettings.top_tracks();
set_norms_free(EditorSettings.norms() == editorSettings::track_norms::free_laying);
EditorModelSets.load();
EditorTrackPresets.load();
Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr);
return m_input.init();
}

View File

@@ -1837,6 +1837,15 @@ class editor_mode : public application_mode, private editor_track::observer, pri
void render_track_style(std::vector<TTrack *> const &Tracks);
void copy_track_style(TTrack const &Track);
std::size_t paste_track_style(std::vector<TTrack *> const &Tracks);
// which groups of the properties are pasted or put on the tracks by a preset, in the order of the groups of editormodetrackprops.cpp
using track_groups = std::array<bool, 11>;
// the values of the properties of the chosen groups put on the tracks, in one step of the undo. returns: the number of the
// tracks changed, none when no value is of the chosen groups
std::optional<std::size_t> apply_track_values(std::vector<std::pair<std::string, std::string>> const &Values, track_groups const &Groups, std::vector<TTrack *> const &Tracks, char const *What);
// presets of the properties of the tracks (editorTrackPresets), made from a track and put on the selected ones
void render_track_presets(std::vector<TTrack *> const &Tracks);
void render_track_presets_menu();
std::size_t apply_track_preset(int const Id, std::vector<TTrack *> const &Tracks);
std::vector<TTrack *> similar_tracks(int const Property, std::vector<TTrack *> const &Tracks) const;
// values of the properties over the selected tracks, each with the number of the tracks having it
struct track_values
@@ -1882,12 +1891,19 @@ class editor_mode : public application_mode, private editor_track::observer, pri
{
std::string from;
std::vector<std::pair<std::string, std::string>> values;
// speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs
std::array<bool, 10> groups{false, false, false, false, true, true, true, true, false, true};
// speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs, sleeper placement
track_groups groups{false, false, false, false, true, true, true, true, false, true, true};
bool open{false};
bool picking{false}; // the next click on a track in the view copies its properties
};
track_style m_track_style;
struct track_presets_panel
{
int selected{0}; // id of the preset chosen in the inspector
int nameid{0}; // the preset whose name is in the field
char name[128]{};
};
track_presets_panel m_track_presets;
std::vector<std::vector<TTrack *>> m_track_similar;
TTrack const *m_track_list_hover{nullptr}; // in the list of the selected tracks
int m_track_list_hover_frame{-1};

View File

@@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorTrackPresets.hpp"
#include "rendering/renderer.h"
#include "world/Track.h"
@@ -219,10 +220,13 @@ enum group : int
sleepers,
ride_sound,
frogs,
sleeper_placement, // spacing and shift of the sleepers and of the trackbed under them, apart from their models
group_count
};
char const *const kGroupLabels[group_count] = {"Speed", "Environment", "Overhead line", "Track circuits", "Rails", "Trackbed", "Shape of the trackbed", "Sleepers", "Ride and sound", "Frog and check rails"};
char const *const kGroupLabels[group_count] = {"Speed", "Environment", "Overhead line", "Track circuits", "Rails", "Trackbed", "Shape of the trackbed", "Sleepers", "Ride and sound", "Frog and check rails", "Sleeper placement"};
// the groups as the presets keep them in their file, by names which don't change with the order of the groups
char const *const kGroupKeys[group_count] = {"speed", "environment", "overhead", "circuits", "rails", "trackbed", "bedshape", "sleepers", "ride", "frogs", "sleeperplacement"};
struct track_property
{
@@ -630,13 +634,13 @@ std::vector<track_property> make_properties()
};
}
}
add_float(list, "sleeperspacing", section::look_sleepers, group::sleepers, "Sleeper spacing (m)", "Distance between the sleepers along the track", &TTrack::m_sleeper_frequency, 3, 0.1f, true);
add_float(list, "sleeperspacing", section::look_sleepers, group::sleeper_placement, "Sleeper spacing (m)", "Distance between the sleepers along the track", &TTrack::m_sleeper_frequency, 3, 0.1f, true);
char const *const offsets[][3] = {{"sleeperside", "Sleeper shift sideways (m)", "Shift of each model sideways, to the right of the track"},
{"sleeperalong", "Sleeper shift along (m)", "Shift of each model along the track"},
{"sleeperup", "Sleeper shift up (m)", "Shift of each model up"}};
for (int axis = 0; axis < 3; ++axis)
{
auto &property{add(list, offsets[axis][0], field::number, section::look_sleepers, group::sleepers, offsets[axis][1], offsets[axis][2])};
auto &property{add(list, offsets[axis][0], field::number, section::look_sleepers, group::sleeper_placement, offsets[axis][1], offsets[axis][2])};
property.rebuild = true;
property.get = [axis](TTrack const &Track) { return decimal(Track.m_sleeper_offset[axis], 3); };
property.set = [axis](TTrack &Track, std::string const &Text) {
@@ -647,7 +651,7 @@ std::vector<track_property> make_properties()
return true;
};
}
add_float(list, "bedshift", section::look_sleepers, group::sleepers, "Trackbed shift (m)", "Vertical shift of the drawn trackbed under the sleepers, negative lowers it", &TTrack::m_sleeper_ballast_z, 3, lowest, true);
add_float(list, "bedshift", section::look_sleepers, group::sleeper_placement, "Trackbed shift (m)", "Vertical shift of the drawn trackbed under the sleepers, negative lowers it", &TTrack::m_sleeper_ballast_z, 3, lowest, true);
// ride and sound
add_float(list, "friction", section::ride, group::ride_sound, "Friction", "Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip", &TTrack::fFriction, 3, 0.f, false);
@@ -748,6 +752,62 @@ void row_tooltip(track_property const &Property, value_counts const &Values, std
ImGui::EndTooltip();
}
// the values of the properties of the track, as they're copied and kept by the presets
std::vector<std::pair<std::string, std::string>> values_of_track(TTrack const &Track)
{
std::vector<std::pair<std::string, std::string>> values;
for (auto const &property : properties())
if (false == property.off && property.applies(Track))
values.emplace_back(property.key, property.get(Track));
return values;
}
// the groups some of the values are of
std::array<bool, group_count> groups_in(std::vector<std::pair<std::string, std::string>> const &Values)
{
std::array<bool, group_count> groups{};
for (auto const &[key, value] : Values)
for (auto const &property : properties())
if (key == property.key)
groups[property.group] = true;
return groups;
}
// the groups the preset overwrites, from the names it keeps them by
std::array<bool, group_count> groups_of(editor_track_preset const &Preset)
{
std::array<bool, group_count> groups{};
for (auto const &key : Preset.groups)
for (int group = 0; group < group_count; ++group)
if (key == kGroupKeys[group])
groups[group] = true;
return groups;
}
std::vector<std::string> group_keys(std::array<bool, group_count> const &Groups)
{
std::vector<std::string> keys;
for (int group = 0; group < group_count; ++group)
if (Groups[group])
keys.emplace_back(kGroupKeys[group]);
return keys;
}
// what the preset puts on the tracks, for its tooltip
std::string preset_summary(editor_track_preset const &Preset)
{
auto const groups{groups_of(Preset)};
auto const available{groups_in(Preset.values)};
std::string overwrites;
for (int group = 0; group < group_count; ++group)
if (groups[group] && available[group])
overwrites += (overwrites.empty() ? "" : ", ") + std::string{STR_C(kGroupLabels[group])};
auto text{overwrites.empty() ? std::string{STR_C("Overwrites nothing: choose what it overwrites in the inspector")} : format(STR_C("Overwrites: %s"), overwrites.c_str())};
if (false == Preset.from.empty())
text += "\n" + format(STR_C("Taken from %s"), Preset.from.c_str());
return text;
}
} // namespace
std::vector<TTrack *> editor_mode::selected_tracks()
@@ -1297,6 +1357,7 @@ void editor_mode::render_track_properties(std::vector<TTrack *> const &Tracks)
if (any_in(section::look_sleepers, false) && ImGui::TreeNode(STR_C("Sleeper models")))
{
rows(section::look_sleepers, false);
render_track_presets(Tracks);
ImGui::TreePop();
}
}
@@ -1307,50 +1368,230 @@ void editor_mode::render_track_properties(std::vector<TTrack *> const &Tracks)
void editor_mode::copy_track_style(TTrack const &Track)
{
auto &style{m_track_style};
style.values.clear();
style.from = name_of(Track);
for (auto const &property : properties())
if (false == property.off && property.applies(Track))
style.values.emplace_back(property.key, property.get(Track));
style.values = values_of_track(Track);
ui()->set_status(format(STR_C("Properties of %s copied: Ctrl+Shift+V pastes them on the selected tracks"), style.from.c_str()), false);
}
std::size_t editor_mode::paste_track_style(std::vector<TTrack *> const &Tracks)
{
static_assert(group_count == std::tuple_size_v<decltype(track_style::groups)>, "a choice for each group of the pasted properties");
auto const &style{m_track_style};
if (style.values.empty() || Tracks.empty())
return 0;
auto const changed{apply_track_values(style.values, style.groups, Tracks, "Pasted properties")};
if (false == changed.has_value())
{
ui()->set_status(STR_C("Nothing chosen to paste"), true);
return 0;
}
if (Tracks.size() == 1)
ui()->set_status(*changed > 0 ? format(STR_C("Properties of %s pasted, Ctrl+Z takes them back"), style.from.c_str()) : std::string{STR_C("The track has these properties already")}, false);
return *changed;
}
std::optional<std::size_t> editor_mode::apply_track_values(std::vector<std::pair<std::string, std::string>> const &Values, track_groups const &Groups, std::vector<TTrack *> const &Tracks, char const *What)
{
static_assert(group_count == std::tuple_size_v<track_groups>, "a choice for each group of the pasted properties");
auto const &list{properties()};
std::vector<std::pair<track_property const *, std::string>> pasted;
bool rebuild{false};
for (auto const late : {false, true})
for (auto const &[key, value] : style.values)
for (auto const &[key, value] : Values)
{
auto const index{track_property_index(key)};
if (index < 0)
continue;
auto const &property{list[index]};
if (property.off || property.late != late || false == style.groups[property.group])
if (property.off || property.late != late || false == Groups[property.group])
continue;
pasted.emplace_back(&property, value);
rebuild |= property.rebuild;
}
if (pasted.empty())
{
ui()->set_status(STR_C("Nothing chosen to paste"), true);
return 0;
}
auto const changed{tracks_apply(Tracks, [&](TTrack &Track) {
return std::nullopt;
return tracks_apply(Tracks, [&](TTrack &Track) {
bool any{false};
for (auto const &[property, value] : pasted)
if (property->applies(Track))
any |= property->set(Track, value);
return any;
}, rebuild, "Pasted properties")};
if (Tracks.size() == 1)
ui()->set_status(changed > 0 ? format(STR_C("Properties of %s pasted, Ctrl+Z takes them back"), style.from.c_str()) : std::string{STR_C("The track has these properties already")}, false);
return changed;
}, rebuild, What);
}
std::size_t editor_mode::apply_track_preset(int const Id, std::vector<TTrack *> const &Tracks)
{
auto const *preset{EditorTrackPresets.find(Id)};
if (preset == nullptr || Tracks.empty())
return 0;
auto const name{preset->name};
auto const changed{apply_track_values(preset->values, groups_of(*preset), Tracks, "Applied a track preset")};
if (false == changed.has_value())
{
ui()->set_status(format(STR_C("The preset \"%s\" overwrites nothing: choose what it overwrites"), name.c_str()), true);
return 0;
}
if (*changed > 0)
ui()->set_status(format(STR_P("Preset \"%s\" applied on %d track, Ctrl+Z takes it back", "Preset \"%s\" applied on %d tracks, Ctrl+Z takes it back", static_cast<long long>(*changed)), name.c_str(), static_cast<int>(*changed)), false);
else
ui()->set_status(STR_C("The tracks have the properties of the preset already"), false);
return *changed;
}
void editor_mode::render_track_presets(std::vector<TTrack *> const &Tracks)
{
if (Tracks.empty() || false == ImGui::TreeNode(STR_C("Presets")))
return;
auto &panel{m_track_presets};
auto const &presets{EditorTrackPresets.presets()};
if (EditorTrackPresets.find(panel.selected) == nullptr)
panel.selected = presets.empty() ? 0 : presets.front().id;
auto const single{Tracks.size() == 1};
auto const *selected{EditorTrackPresets.find(panel.selected)};
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::BeginCombo("##trackpreset", selected != nullptr ? selected->name.c_str() : STR_C("No presets yet")))
{
for (auto const &preset : presets)
{
if (ImGui::Selectable(preset.name.c_str(), preset.id == panel.selected))
panel.selected = preset.id;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", preset_summary(preset).c_str());
}
ImGui::EndCombo();
}
if (selected != nullptr && ImGui::IsItemHovered())
ImGui::SetTooltip("%s", preset_summary(*selected).c_str());
auto const width{button_width(2)};
ImGui::BeginDisabled(selected == nullptr);
if (ImGui::Button(format(STR_P("Apply on %d track", "Apply on %d tracks", static_cast<long long>(Tracks.size())), static_cast<int>(Tracks.size())).c_str(), ImVec2(width, 0.0f)))
apply_track_preset(panel.selected, Tracks);
ImGui::EndDisabled();
item_tooltip("Puts the values of the preset on the selected tracks, of what it overwrites; Ctrl+Z takes them back.\nTools in the menu applies the presets too");
ImGui::SameLine();
ImGui::BeginDisabled(false == single);
if (ImGui::Button(STR_C("New from the track"), ImVec2(width, 0.0f)))
{
// rails, trackbed, its shape, sleepers, frogs and sleeper placement at first, the look of the track
std::array<bool, group_count> groups{};
for (auto const group : {group::rails, group::trackbed, group::trackbed_shape, group::sleepers, group::frogs, group::sleeper_placement})
groups[group] = true;
panel.selected = EditorTrackPresets.create(STR_C("New preset"), name_of(*Tracks.front()), values_of_track(*Tracks.front()), group_keys(groups));
panel.nameid = 0;
ui()->set_status(format(STR_C("Properties of %s saved as a preset: give it a name and choose what it overwrites"), name_of(*Tracks.front()).c_str()), false);
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", STR_C(single ? "Saves the properties of this track as a new preset" : "Select one track to save its properties as a preset"));
// pointers to the presets don't outlive a change of their list
selected = EditorTrackPresets.find(panel.selected);
if (selected == nullptr)
{
ImGui::PushTextWrapPos(0.0f);
ImGui::TextDisabled("%s", STR_C("A preset keeps the properties of a track: its rails, trackbed, sleepers... to put them on other tracks,\nwith the choice of what it overwrites"));
ImGui::PopTextWrapPos();
ImGui::TreePop();
return;
}
auto const id{selected->id};
// the name
if (panel.nameid != id)
{
std::snprintf(panel.name, sizeof(panel.name), "%s", selected->name.c_str());
panel.nameid = id;
}
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(STR_C("Rename")).x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x);
auto const entered{ImGui::InputText("##presetname", panel.name, IM_ARRAYSIZE(panel.name), ImGuiInputTextFlags_EnterReturnsTrue)};
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button(STR_C("Rename")) || entered)
{
EditorTrackPresets.rename(id, panel.name);
panel.nameid = 0; // reload, the name could have been adjusted to stay unique
selected = EditorTrackPresets.find(id);
}
// what it overwrites, of the groups it has values of
ImGui::TextDisabled("%s", STR_C("Overwrites:"));
// its own id scope: the group checkboxes share their labels with the sections of the properties
ImGui::PushID("presetgroups");
auto const available{groups_in(selected->values)};
auto groups{groups_of(*selected)};
auto changed{false};
int column{0};
auto const half{ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x * 0.5f};
for (int group = 0; group < group_count; ++group)
{
if (false == available[group])
continue;
if (column % 2 == 1)
ImGui::SameLine(half);
changed |= ImGui::Checkbox(STR_C(kGroupLabels[group]), &groups[group]);
++column;
}
ImGui::PopID();
if (changed)
{
// the groups it has no values of are kept as they were, for a track they're taken again from
auto kept{groups_of(*selected)};
for (int group = 0; group < group_count; ++group)
if (available[group])
kept[group] = groups[group];
EditorTrackPresets.set_groups(id, group_keys(kept));
selected = EditorTrackPresets.find(id);
}
ImGui::BeginDisabled(false == single);
if (ImGui::Button(STR_C("Update from the track"), ImVec2(width, 0.0f)))
{
EditorTrackPresets.update(id, name_of(*Tracks.front()), values_of_track(*Tracks.front()));
ui()->set_status(format(STR_C("Preset \"%s\" takes the properties of %s now"), selected->name.c_str(), name_of(*Tracks.front()).c_str()), false);
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", STR_C(single ? "The preset takes the properties of this track in place of the ones it had; what it overwrites stays" : "Select one track to take its properties into the preset"));
ImGui::SameLine();
if (ImGui::Button(STR_C("Delete"), ImVec2(width, 0.0f)))
ImGui::OpenPopup("##deletepreset");
if (ImGui::BeginPopup("##deletepreset"))
{
ImGui::Text(STR_C("Delete preset \"%s\"?"), selected->name.c_str());
if (ImGui::Button(STR_C("Delete##confirm")))
{
EditorTrackPresets.remove(id);
panel.selected = 0;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Cancel")))
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
ImGui::TreePop();
}
void editor_mode::render_track_presets_menu()
{
if (false == ImGui::BeginMenu(STR_C("Track presets")))
return;
auto const &presets{EditorTrackPresets.presets()};
if (presets.empty())
ImGui::TextDisabled("%s", STR_C("No presets: save one in the inspector of a track, Look, Sleeper models, Presets"));
auto const tracks{selected_tracks()};
int chosen{0};
for (auto const &preset : presets)
{
if (ImGui::MenuItem(preset.name.c_str(), nullptr, false, false == tracks.empty()))
chosen = preset.id;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", tracks.empty() ? STR_C("Select the tracks to apply the preset on first") : (format(STR_P("Applies the preset on %d selected track", "Applies the preset on %d selected tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size())) + "\n" + preset_summary(preset)).c_str());
}
ImGui::EndMenu();
if (chosen != 0)
apply_track_preset(chosen, tracks);
}
void editor_mode::render_track_style(std::vector<TTrack *> const &Tracks)
@@ -1623,7 +1864,7 @@ editor_mode::selftest_result editor_mode::selftest_props(std::string const &Comm
}
else if (action == "groups")
{
// the groups pasted, as their numbers: 0 speed ... 9 frogs
// the groups pasted, as their numbers: 0 speed ... 9 frogs, 10 sleeper placement
m_track_style.groups.fill(false);
for (int group; Words >> group;)
if (group >= 0 && group < group_count)

View File

@@ -990,6 +990,15 @@ void editor_ui::render_menu_contents()
m_menuoptions();
}
// the tools of the editor mode, in the tools menu after the common ones (logging, screenshot)
void editor_ui::render_tools_menu()
{
if (!m_toolsmenu)
return;
ImGui::Separator();
m_toolsmenu();
}
// the windows of the editor and their layout, in the windows menu after the common ones (log, hierarchy, history, settings)
void editor_ui::render_windows_menu()
{

View File

@@ -80,6 +80,11 @@ class editor_ui : public ui_layer
{
m_editmenu = std::move(Renderer);
}
// draws the items of the editor mode in the tools menu, after the common ones
void set_tools_menu(std::function<void()> Renderer)
{
m_toolsmenu = std::move(Renderer);
}
// draws the fields of work of the editor mode in a row above the toolbar
void set_workspace_bar(std::function<void()> Renderer)
{
@@ -177,6 +182,7 @@ class editor_ui : public ui_layer
void scene_row(std::size_t const Entry);
void render_menu_contents() override;
void render_windows_menu() override;
void render_tools_menu() override;
int palette() const override;
void render_dockspace() override;
void render_() override;
@@ -210,6 +216,7 @@ class editor_ui : public ui_layer
std::function<void()> m_toolbaroptions;
std::function<void()> m_workspacebar;
std::function<void()> m_editmenu;
std::function<void()> m_toolsmenu;
bool m_layoutreset{false}; // the default layout is to be built again on the next frame
std::string m_refusal;
double m_refusaltime{-100.0};

View File

@@ -696,6 +696,7 @@ void ui_layer::render_menu_contents()
if (ImGui::MenuItem(STR_C("Screenshot"), "PrtScr"))
Application.queue_screenshot();
render_tools_menu();
ImGui::EndMenu();
}
if (ImGui::BeginMenu(STR_C("Windows")))

View File

@@ -143,6 +143,8 @@ protected:
virtual void render_menu_contents();
// items of the windows menu which belong to the ui of a mode, after the common ones
virtual void render_windows_menu() {}
// items of the tools menu which belong to the ui of a mode, after the common ones
virtual void render_tools_menu() {}
// colours this ui is drawn in, see imgui_colors()
virtual int palette() const
{

View File

@@ -0,0 +1,233 @@
/*
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/editorTrackPresets.hpp"
#include "utilities/Logs.h"
#include "utilities/utilities.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <sstream>
editorTrackPresets EditorTrackPresets;
// file layout:
// preset <name>
// from <track name>
// groups <group key> <group key>...
// value <property key> <value, the rest of the line>
// ...
// endpreset
namespace
{
namespace fs = std::filesystem;
fs::path presets_path()
{
fs::path p = user_config_path("eu07_editor_track_presets.txt");
return p.empty() ? fs::path("eu07_editor_track_presets.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);
}
// one value per line in the file
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);
}
// the line past its first word, when it starts with the word
bool keyword(std::string const &Line, char const *Word, std::string &Rest)
{
auto const length{std::char_traits<char>::length(Word)};
if (Line.compare(0, length, Word) != 0 || (Line.size() > length && Line[length] != ' '))
return false;
Rest = Line.size() > length ? trimmed(Line.substr(length + 1)) : std::string{};
return true;
}
} // namespace
bool editorTrackPresets::load()
{
m_presets.clear();
const fs::path path = presets_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_track_preset *current{nullptr};
std::string line;
std::string rest;
while (std::getline(stream, line))
{
line = single_line(line);
if (line.empty() || line.rfind("//", 0) == 0)
continue;
// a preset header also closes a preset with missing "endpreset"
if (keyword(line, "preset", rest))
{
editor_track_preset preset;
preset.id = m_nextid++;
preset.name = unique_name(rest);
m_presets.push_back(std::move(preset));
current = &m_presets.back();
continue;
}
if (line == "endpreset")
{
current = nullptr;
continue;
}
if (current == nullptr)
continue;
if (keyword(line, "from", rest))
current->from = rest;
else if (keyword(line, "groups", rest))
{
std::istringstream words{rest};
current->groups.clear();
for (std::string group; words >> group;)
current->groups.push_back(group);
}
else if (keyword(line, "value", rest) && false == rest.empty())
{
auto const split{rest.find(' ')};
if (split == std::string::npos)
current->values.emplace_back(rest, std::string{});
else
current->values.emplace_back(rest.substr(0, split), trimmed(rest.substr(split + 1)));
}
}
return true;
}
bool editorTrackPresets::save() const
{
const fs::path path = presets_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 track presets");
return false;
}
stream << "// MaSzyna editor: user-defined presets of the track properties\n";
for (auto const &preset : m_presets)
{
stream << "\npreset " << preset.name << "\n";
if (false == preset.from.empty())
stream << "from " << preset.from << "\n";
stream << "groups";
for (auto const &group : preset.groups)
stream << ' ' << group;
stream << "\n";
for (auto const &[key, value] : preset.values)
stream << "value " << key << ' ' << single_line(value) << "\n";
stream << "endpreset\n";
}
return true;
}
editor_track_preset const *editorTrackPresets::find(int const Id) const
{
auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; });
return lookup != m_presets.end() ? &(*lookup) : nullptr;
}
editor_track_preset *editorTrackPresets::find_(int const Id)
{
auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; });
return lookup != m_presets.end() ? &(*lookup) : nullptr;
}
int editorTrackPresets::create(std::string const &Name, std::string const &From, std::vector<std::pair<std::string, std::string>> const &Values, std::vector<std::string> const &Groups)
{
editor_track_preset preset;
preset.id = m_nextid++;
preset.name = unique_name(Name);
preset.from = single_line(From);
preset.values = Values;
preset.groups = Groups;
m_presets.push_back(std::move(preset));
save();
return m_presets.back().id;
}
void editorTrackPresets::remove(int const Id)
{
auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; });
if (lookup == m_presets.end())
return;
m_presets.erase(lookup);
save();
}
void editorTrackPresets::rename(int const Id, std::string const &Name)
{
auto *preset = find_(Id);
if (preset == nullptr)
return;
auto const name = unique_name(Name, Id);
if (name == preset->name)
return;
preset->name = name;
save();
}
void editorTrackPresets::update(int const Id, std::string const &From, std::vector<std::pair<std::string, std::string>> const &Values)
{
auto *preset = find_(Id);
if (preset == nullptr)
return;
preset->from = single_line(From);
preset->values = Values;
save();
}
void editorTrackPresets::set_groups(int const Id, std::vector<std::string> const &Groups)
{
auto *preset = find_(Id);
if (preset == nullptr || preset->groups == Groups)
return;
preset->groups = Groups;
save();
}
std::string editorTrackPresets::unique_name(std::string const &Base, int const IgnoreId) const
{
auto base = single_line(Base);
if (base.empty())
base = "New preset";
auto const taken = [&](std::string const &Name) {
return std::any_of(m_presets.begin(), m_presets.end(), [&](auto const &Preset) { return Preset.id != IgnoreId && Preset.name == Name; });
};
auto name = base;
for (int suffix = 2; taken(name); ++suffix)
name = base + " " + std::to_string(suffix);
return name;
}

View File

@@ -0,0 +1,58 @@
/*
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 <utility>
#include <vector>
// user-defined presets of the properties of tracks (rails, trackbed, sleepers...), taken from a track and put on others
// with the choice of what they overwrite. stored next to the user's ini files, like the model sets
struct editor_track_preset
{
int id{0}; // runtime identifier, stable while the editor runs (renames don't break references)
std::string name;
std::string from; // name of the track the values were taken from
// the values of the properties as the track inspector reads and writes them, by their keys
std::vector<std::pair<std::string, std::string>> values;
// the groups of the properties the preset overwrites, by their keys; the other values of it are kept but left out
std::vector<std::string> groups;
};
class editorTrackPresets
{
public:
bool load();
bool save() const;
std::vector<editor_track_preset> const &presets() const
{
return m_presets;
}
editor_track_preset const *find(int Id) const;
// all modifying methods save the presets right away
// returns: id of the new preset
int create(std::string const &Name, std::string const &From, std::vector<std::pair<std::string, std::string>> const &Values, std::vector<std::string> const &Groups);
void remove(int Id);
void rename(int Id, std::string const &Name);
// the values taken again, from another track or the same one changed
void update(int Id, std::string const &From, std::vector<std::pair<std::string, std::string>> const &Values);
void set_groups(int Id, std::vector<std::string> const &Groups);
// name not used by any other preset, built from specified base
std::string unique_name(std::string const &Base, int IgnoreId = 0) const;
private:
editor_track_preset *find_(int Id);
std::vector<editor_track_preset> m_presets;
int m_nextid{1};
};
extern editorTrackPresets EditorTrackPresets;

View File

@@ -11532,3 +11532,98 @@ msgstr "Każdy kliknięty punkt na wysokości terenu, między punktami równe po
msgid "The track goes on at the height it's laid from, whatever the ground; the first point on the ground"
msgstr "Tor idzie dalej na wysokości, z której go kładziesz, bez względu na teren; pierwszy punkt na terenie"
msgid "new, not saved"
msgstr "nowy, niezapisany"
# scenery editor: presets of the track properties
msgid "Sleeper placement"
msgstr "Rozmieszczenie podkładów"
msgid "Overwrites nothing: choose what it overwrites in the inspector"
msgstr "Niczego nie nadpisuje: wybierz w inspektorze, co ma nadpisywać"
msgid "Overwrites: %s"
msgstr "Nadpisuje: %s"
msgid "Taken from %s"
msgstr "Wzięty z %s"
msgid "The preset \"%s\" overwrites nothing: choose what it overwrites"
msgstr "Zestaw \"%s\" niczego nie nadpisuje: wybierz, co ma nadpisywać"
msgid "Preset \"%s\" applied on %d track, Ctrl+Z takes it back"
msgid_plural "Preset \"%s\" applied on %d tracks, Ctrl+Z takes it back"
msgstr[0] "Zestaw \"%s\" zastosowany na %d torze, Ctrl+Z go cofa"
msgstr[1] "Zestaw \"%s\" zastosowany na %d torach, Ctrl+Z go cofa"
msgstr[2] "Zestaw \"%s\" zastosowany na %d torach, Ctrl+Z go cofa"
msgid "The tracks have the properties of the preset already"
msgstr "Tory mają już właściwości tego zestawu"
msgid "Presets"
msgstr "Zestawy"
msgid "No presets yet"
msgstr "Brak zestawów"
msgid "Apply on %d track"
msgid_plural "Apply on %d tracks"
msgstr[0] "Zastosuj na %d torze"
msgstr[1] "Zastosuj na %d torach"
msgstr[2] "Zastosuj na %d torach"
msgid "Puts the values of the preset on the selected tracks, of what it overwrites; Ctrl+Z takes them back.\nTools in the menu applies the presets too"
msgstr "Nadaje zaznaczonym torom wartości zestawu, z tego, co nadpisuje; Ctrl+Z je cofa.\nZestawy można też zastosować z menu Narzędzia"
msgid "New from the track"
msgstr "Nowy z toru"
msgid "New preset"
msgstr "Nowy zestaw"
msgid "Properties of %s saved as a preset: give it a name and choose what it overwrites"
msgstr "Właściwości %s zapisane jako zestaw: nadaj mu nazwę i wybierz, co ma nadpisywać"
msgid "Saves the properties of this track as a new preset"
msgstr "Zapisuje właściwości tego toru jako nowy zestaw"
msgid "Select one track to save its properties as a preset"
msgstr "Zaznacz jeden tor, żeby zapisać jego właściwości jako zestaw"
msgid "A preset keeps the properties of a track: its rails, trackbed, sleepers... to put them on other tracks,\nwith the choice of what it overwrites"
msgstr "Zestaw przechowuje właściwości toru: szyny, podsypkę, podkłady... żeby nadać je innym torom,\nz wyborem tego, co nadpisuje"
msgid "Overwrites:"
msgstr "Nadpisuje:"
msgid "Update from the track"
msgstr "Aktualizuj z toru"
msgid "Preset \"%s\" takes the properties of %s now"
msgstr "Zestaw \"%s\" ma teraz właściwości %s"
msgid "The preset takes the properties of this track in place of the ones it had; what it overwrites stays"
msgstr "Zestaw przejmuje właściwości tego toru w miejsce dotychczasowych; wybór tego, co nadpisuje, zostaje"
msgid "Select one track to take its properties into the preset"
msgstr "Zaznacz jeden tor, żeby przenieść jego właściwości do zestawu"
msgid "Delete preset \"%s\"?"
msgstr "Usunąć zestaw \"%s\"?"
msgid "Track presets"
msgstr "Zestawy torów"
msgid "No presets: save one in the inspector of a track, Look, Sleeper models, Presets"
msgstr "Brak zestawów: zapisz zestaw w inspektorze toru, Wygląd, Modele podkładów, Zestawy"
msgid "Select the tracks to apply the preset on first"
msgstr "Najpierw zaznacz tory, na których zastosować zestaw"
msgid "Applies the preset on %d selected track"
msgid_plural "Applies the preset on %d selected tracks"
msgstr[0] "Stosuje zestaw na %d zaznaczonym torze"
msgstr[1] "Stosuje zestaw na %d zaznaczonych torach"
msgstr[2] "Stosuje zestaw na %d zaznaczonych torach"