17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-24 22:37:36 +02:00

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

This commit is contained in:
2026-10-21 16:09:05 +02:00
parent 16c77ab6fb
commit 83c8c23c80
11 changed files with 686 additions and 23 deletions

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"
@@ -224,10 +225,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
{
@@ -632,13 +636,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) {
@@ -649,7 +653,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);
@@ -750,6 +754,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 (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()
@@ -1288,6 +1348,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();
}
}
@@ -1298,50 +1359,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 (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.late != late || false == style.groups[property.group])
if (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)
@@ -1614,7 +1855,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)