17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-21 07:13:19 +02:00

feat(track): 3D sleepers and switch timbers, blade heel and throw from templates, one rail carving pass

- sleepers3d / sleepers3dplain / sleepers3dbed in eu07.ini: timbers put together from the begin, main and end parts of a model
  and stretched to their length, plain sleepers of their own model; tracks whose plain sleepers would touch are laid as one
  cluster, lines of timbers led by priority and turned to the bisectors of frogs and crossings at a limited rate, whole timbers
  under crossings, split ones between tracks with room to part them, gaps filled per rail and overlaps shortened or dropped;
  clusters worked out in a background thread while driving, on demand in the editor
- the ballast can be left out under the beams (sleepers3dbed no)
- blades get their heel and throw from the switch template or blades <heel> <throw> in the scenery; blades of double switches
  run on into the track linking them to the next switch
- rail cutting at frogs, wing rails, movable noses and switch crossings done by one carving pass (RailCarve)
- tint of the rail treads applied to the colour only, without the matte band at the frog
- a switch trailed through with a movable nose set the other way derails the vehicle
- self-tests: sleepercheck, sharedzones, beamsnear, throw, sleepersbg
This commit is contained in:
maj00r
2026-10-17 23:28:36 +02:00
parent 21eca4d9a6
commit d25f20a56c
49 changed files with 9802 additions and 848 deletions

View File

@@ -212,7 +212,7 @@ void editor_mode::draw_track_hover() const
{
using kind = track_intent::kind;
auto const *track{m_hover.track};
if (track == nullptr || (m_intent.what != kind::select && m_intent.what != kind::box && m_intent.what != kind::straight_set) || m_intent.track != track)
if (track == nullptr || (m_intent.what != kind::select && m_intent.what != kind::box && m_intent.what != kind::straight_set && m_intent.what != kind::pick_style) || m_intent.track != track)
return;
screen_projection const projection;
if (track != selected_track())
@@ -319,6 +319,13 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
}
return intent;
}
if (m_track_style.picking && (m_track_tab == track_tab::path || m_track_tab == track_tab::turnout))
{
intent.track = m_hover.track;
if (m_hover.track != nullptr)
intent.position = m_hover.point;
return make(kind::pick_style, m_hover.track != nullptr ? "Click: take the properties of this track" : "Click a track to take its properties, Esc: cancel");
}
if (m_track_tab == track_tab::path)
{
if (shift && control && m_hover.track != nullptr && rail(*m_hover.track))
@@ -333,7 +340,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
if (shift && false == gizmo_frame::over())
{
intent.track = m_hover.track;
return make(kind::box, m_hover.track == nullptr ? "Drag: the tracks in a box join the selection" : in_track_set(m_hover.track) ? "Click: take out of the selection, drag: a box" : "Click: add to the selection, drag: a box");
return make(kind::box, m_hover.track == nullptr ? "Drag: the tracks in a box join the selection" : track_selected(m_hover.track) ? "Click: take out of the selection, drag: a box" : "Click: add to the selection, drag: a box");
}
if (auto const hit{track_handle_hit()}; hit.valid() && selected != nullptr)
{
@@ -356,6 +363,11 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
return make(kind::extend_end, diverging ? "Drag: the curve of the switch goes on; Ctrl+drag: move the end" : "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end");
}
}
if (control && false == alt && m_hover.track != nullptr && false == gizmo_frame::over())
{
intent.track = m_hover.track;
return make(kind::box, track_selected(m_hover.track) ? "Click: take out of the selection" : "Click: add to the selection");
}
}
if (m_track_tab == track_tab::straights)
{
@@ -413,7 +425,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
return make(kind::switch_slide, "Click: edit this switch, drag: slide it along the straight");
return make(kind::switch_slide, m_track_tab == track_tab::path ? "Click: select this switch, drag: slide it along the straight" : "Click: edit this switch, drag: slide it along the straight");
}
if (selected != nullptr && false == selected->m_paths.empty())
{
@@ -441,6 +453,12 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
default: return make(kind::select, "Click: select");
}
}
// a drag on the empty ground selects the tracks in a box, a click there clears the selection
if (m_track_tab == track_tab::path && false == alt)
{
intent.index = 1;
intent.what = kind::box;
}
return intent;
}
@@ -462,6 +480,10 @@ void editor_mode::update_track_intent()
case kind::straight_handle:
case kind::route_vertex:
case kind::route_grip: ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); break;
case kind::box:
if (m_intent.track != nullptr)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
break;
default: ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); break;
}
}
@@ -482,7 +504,15 @@ void editor_mode::track_press(track_intent const &Intent)
});
return;
case kind::sweep: sweep_press(); return;
case kind::box: start_track_box(); return;
case kind::box: start_track_box(Intent.index == 1); return;
case kind::pick_style:
if (Intent.track != nullptr)
{
copy_track_style(*Intent.track);
m_track_style.picking = false;
m_track_style.open = true;
}
return;
case kind::spread:
if (Intent.track != nullptr)
track_spread_apply(*Intent.track);
@@ -541,31 +571,31 @@ void editor_mode::track_press(track_intent const &Intent)
}
return;
case kind::select:
case kind::none:
{
auto *hovered{Intent.track};
GfxRenderer->Pick_Node_Callback([this, hovered](scene::basic_node *node) {
if (false == viewport_click())
return;
auto *track{hovered != nullptr ? hovered : dynamic_cast<TTrack *>(node)};
if (m_track_tab == track_tab::path)
case kind::none: track_click_select(Intent.track); return;
}
}
void editor_mode::track_click_select(TTrack *Hovered)
{
GfxRenderer->Pick_Node_Callback([this, Hovered](scene::basic_node *node) {
if (false == viewport_click())
return;
auto *track{Hovered != nullptr ? Hovered : dynamic_cast<TTrack *>(node)};
if (m_track_tab == track_tab::path)
{
m_track_set.clear();
if (track == nullptr)
{
m_track_set.clear();
if (track == nullptr)
{
m_node = nullptr;
m_track_point = {};
ui()->set_node(nullptr);
return;
}
m_node = nullptr;
m_track_point = {};
ui()->set_node(nullptr);
return;
}
select_track(track != nullptr ? track : node);
if (m_track_tab == track_tab::lay && track != nullptr)
show_track_tab(track_tab::parallel);
});
return;
}
}
}
select_track(track != nullptr ? track : node);
if (m_track_tab == track_tab::lay && track != nullptr)
show_track_tab(track_tab::parallel);
});
}
void editor_mode::draw_track_intent() const
@@ -688,7 +718,9 @@ void editor_mode::render_track_context()
context.pending = false;
if (track != nullptr && false == track->m_editorremoved)
{
select_track(track);
// a track of the set keeps the set, the menu acts on all of them
if (false == (m_track_set.size() >= 2 && in_track_set(track)))
select_track(track);
ImGui::OpenPopup("##trackcontext");
ImGui::SetNextWindowPos(ImVec2(context.mouse.x, context.mouse.y));
}
@@ -703,7 +735,32 @@ void editor_mode::render_track_context()
}
auto const point{context.target.point};
bool const normal{track->eType == tt_Normal && editor_track::is_supported(*track)};
auto const tracks{selected_tracks()};
auto const many{tracks.size() >= 2};
ImGui::TextDisabled("%s (%s, %.1f m)", name_of(*track).c_str(), STR_C(type_of(*track)), track->Length());
if (many)
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_P("one of %d selected tracks", "one of %d selected tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size()));
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Copy its properties"), "Ctrl+Shift+C"))
copy_track_style(*track);
auto const paste{many ? format(STR_P("Paste the properties on %d track", "Paste the properties on %d tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size())) : std::string{STR_C("Paste the properties")}};
if (ImGui::MenuItem(paste.c_str(), "Ctrl+Shift+V", false, false == m_track_style.values.empty()))
paste_track_style(tracks);
if (false == m_track_style.values.empty() && ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Properties of %s"), m_track_style.from.c_str());
if (ImGui::BeginMenu(STR_C("Select alike")))
{
char const *const keys[] = {"rails", "trackbed", "speed", "environment", "circuits"};
char const *const labels[] = {"The same rails", "The same trackbed", "The same speed", "The same environment", "The same track circuits"};
for (std::size_t i = 0; i < std::size(keys); ++i)
if (ImGui::MenuItem(STR_C(labels[i])))
{
auto const found{similar_tracks(track_property_index(keys[i]), {track})};
select_tracks(found);
ui()->set_status(format(STR_P("%s: %d track selected in the scenery", "%s: %d tracks selected in the scenery", static_cast<long long>(found.size())), STR_C(labels[i]), static_cast<int>(found.size())), false);
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Split here"), "K", false, normal))
split_track_at(*track, point);
@@ -743,14 +800,19 @@ void editor_mode::render_track_context()
speed_start();
}
ImGui::Separator();
if (ImGui::MenuItem(in_track_set(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), "Shift+LMB"))
if (ImGui::MenuItem(track_selected(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), "Shift+LMB"))
track_set_toggle(*track);
if (ImGui::MenuItem(STR_C("Look at it")))
focus_track(*track, context.target.path, point);
if (ImGui::MenuItem(STR_C("Copy the name"), nullptr, false, false == track->name().empty()))
ImGui::SetClipboardText(track->name().c_str());
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Delete"), "Del"))
if (many)
{
if (ImGui::MenuItem(format(STR_P("Delete %d track", "Delete %d tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size())).c_str(), "Del"))
track_set_delete();
}
else if (ImGui::MenuItem(STR_C("Delete"), "Del"))
delete_selected_track();
ImGui::EndPopup();
}
@@ -907,14 +969,30 @@ bool editor_mode::in_track_set(TTrack const *Track) const
return std::find(m_track_set.begin(), m_track_set.end(), Track) != m_track_set.end();
}
bool editor_mode::track_selected(TTrack const *Track) const
{
return Track != nullptr && (m_track_set.size() >= 2 ? in_track_set(Track) : Track == m_node);
}
void editor_mode::track_set_prune()
{
m_track_set.erase(std::remove_if(m_track_set.begin(), m_track_set.end(), [](TTrack const *Track) { return Track == nullptr || Track->m_editorremoved; }), m_track_set.end());
// the set is the selection only together with the selected track, another one chosen elsewhere ends it
if (false == m_track_set.empty() && false == in_track_set(selected_track()))
m_track_set.clear();
}
void editor_mode::track_set_toggle(TTrack &Track)
{
track_set_prune();
// the set is the selection only with the selected track in it, a set left from before is forgotten
if (m_track_set.size() < 2 || false == in_track_set(selected_track()))
m_track_set.clear();
if (m_track_set.empty() && &Track == selected_track())
{
select_tracks({});
return;
}
if (m_track_set.empty())
if (auto *selected{selected_track()}; selected != nullptr && selected != &Track)
m_track_set.push_back(selected);
@@ -927,10 +1005,11 @@ void editor_mode::track_set_toggle(TTrack &Track)
ui()->set_node(m_node);
}
void editor_mode::start_track_box()
void editor_mode::start_track_box(bool const Replace)
{
auto const &io{ImGui::GetIO()};
m_track_box.active = true;
m_track_box.replace = Replace;
m_track_box.to = {io.MousePos.x, io.MousePos.y};
m_track_box.from = m_track_box.to;
m_track_box.hovered = m_hover.track;
@@ -946,7 +1025,10 @@ void editor_mode::finish_track_box()
box.to = {io.MousePos.x, io.MousePos.y};
if (glm::length(box.to - box.from) < 6.0f)
{
if (box.hovered != nullptr && false == box.hovered->m_editorremoved)
// a click on the empty ground: what the renderer finds there, a part of a track seen close up, or nothing
if (box.replace)
track_click_select(nullptr);
else if (box.hovered != nullptr && false == box.hovered->m_editorremoved)
track_set_toggle(*box.hovered);
return;
}
@@ -954,7 +1036,9 @@ void editor_mode::finish_track_box()
glm::vec2 const high{std::max(box.from.x, box.to.x), std::max(box.from.y, box.to.y)};
screen_projection const projection;
track_set_prune();
if (m_track_set.empty())
if (m_track_set.size() < 2 || false == in_track_set(selected_track()) || box.replace)
m_track_set.clear();
if (m_track_set.empty() && false == box.replace)
if (auto *selected{selected_track()}; selected != nullptr)
m_track_set.push_back(selected);
for (auto *track : simulation::Paths.sequence())
@@ -977,12 +1061,8 @@ void editor_mode::finish_track_box()
if (inside)
m_track_set.push_back(track);
}
if (false == m_track_set.empty())
{
m_node = m_track_set.back();
m_track_point = {};
ui()->set_node(m_node);
}
auto const found{m_track_set};
select_tracks(found);
}
std::vector<TTrack *> editor_mode::track_spread_from(TTrack &Start) const
@@ -1063,6 +1143,8 @@ std::vector<TTrack *> const &editor_mode::track_spread_preview(TTrack &Start) co
void editor_mode::track_spread_apply(TTrack &Start)
{
track_set_prune();
if (m_track_set.size() < 2 || false == in_track_set(selected_track()))
m_track_set.clear();
if (m_track_set.empty())
if (auto *selected{selected_track()}; selected != nullptr && selected != &Start)
m_track_set.push_back(selected);
@@ -1077,9 +1159,8 @@ void editor_mode::track_spread_apply(TTrack &Start)
if (false == in_track_set(track))
m_track_set.push_back(track);
m_spread.from = nullptr;
m_node = m_track_set.empty() ? nullptr : m_track_set.back();
m_track_point = {};
ui()->set_node(m_node);
auto const found{m_track_set};
select_tracks(found);
}
void editor_mode::track_set_grow()
@@ -1158,7 +1239,21 @@ void editor_mode::draw_track_set() const
drawlist->AddRectFilled(from, io.MousePos, IM_COL32(255, 160, 40, 40));
drawlist->AddRect(from, io.MousePos, IM_COL32(255, 160, 40, 220), 0.0f, 0, 1.5f);
}
if (m_track_set.size() < 2)
auto const *listed{m_track_list_hover_frame >= ImGui::GetFrameCount() - 1 ? m_track_list_hover : nullptr};
if (listed != nullptr && false == listed->m_editorremoved)
for (auto const &path : listed->m_paths)
{
bezier const curve{path};
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 3.0), 6, 64)};
auto previous{curve.point(0.0)};
for (int k = 1; k <= count; ++k)
{
auto const next{curve.point(static_cast<double>(k) / count)};
projection.line(drawlist, previous, next, IM_COL32(255, 255, 255, 240), 6.0f);
previous = next;
}
}
if (m_track_set.size() < 2 || false == in_track_set(selected_track()))
return;
for (auto const *track : m_track_set)
{
@@ -1179,41 +1274,6 @@ void editor_mode::draw_track_set() const
}
}
void editor_mode::track_set_apply(std::function<bool(TTrack &)> const &Change, bool const Rebuild, char const *What)
{
track_set_prune();
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> changed;
std::size_t locked{0};
for (auto *track : m_track_set)
{
if (false == scene::Layers.editable(track))
{
++locked;
continue;
}
auto const before{editor_track::capture(*track)};
if (false == Change(*track))
continue;
states.emplace_back(track, before);
changed.push_back(track);
}
if (changed.empty())
return;
push_track_snapshot(std::move(states));
for (auto *track : changed)
{
if (Rebuild)
editor_track::commit_parameters(*track);
else
track->mark_dirty();
}
auto status{format(STR_C("%s set on %zu paths, Ctrl+Z takes it back"), STR_C(What), changed.size())};
if (locked > 0)
status += format(STR_C(", %zu in the files which can't be changed are left as they are"), locked);
ui()->set_status(status, false);
}
void editor_mode::track_set_delete()
{
track_set_prune();
@@ -1246,93 +1306,13 @@ void editor_mode::track_set_delete()
void editor_mode::render_track_set_ui()
{
track_set_prune();
if (m_track_set.size() < 2)
if (m_track_set.size() < 2 || false == in_track_set(selected_track()))
return;
auto &fields{m_track_set_fields};
double length{0.0};
for (auto const *track : m_track_set)
length += track->Length();
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_C("%zu paths selected, %.1f m"), m_track_set.size(), length);
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Clear")))
{
m_track_set.clear();
return;
}
ImGui::TextDisabled("%s", STR_C("The gizmo moves them together; Shift+LMB adds or takes out a path"));
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_P("%d track selected", "%d tracks selected", static_cast<long long>(m_track_set.size())), static_cast<int>(m_track_set.size()));
ImGui::PushTextWrapPos(0.0f);
ImGui::TextDisabled("%s", STR_C("Their properties are in the inspector, the gizmo moves them together"));
ImGui::PopTextWrapPos();
render_parallel_set_ui();
if (ImGui::SmallButton(STR_C("Grow by the neighbours")))
track_set_grow();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Adds the paths joined to the set, by the rules of the spread"));
auto const button_width{90.0f};
ImGui::PushItemWidth(-button_width - ImGui::GetStyle().ItemSpacing.x);
ImGui::InputDouble("##setvelocity", &fields.velocity, 0.0, 0.0, "%.0f");
ImGui::SameLine();
if (ImGui::Button(format("%s##setv", STR_C("Velocity")).c_str(), ImVec2(button_width, 0.0f)))
{
auto const velocity{fields.velocity};
track_set_apply([velocity](TTrack &Track) { editor_track::velocity(Track, velocity); return true; }, false, "Velocity");
}
ImGui::InputTextWithHint("##setrail", STR_C("texture of the rails"), fields.rail, sizeof(fields.rail));
ImGui::SameLine();
if (ImGui::Button(format("%s##setr", STR_C("Rails")).c_str(), ImVec2(button_width, 0.0f)) && fields.rail[0] != '\0')
{
auto const material{editor_track::fetch_material(fields.rail)};
track_set_apply([material](TTrack &Track) {
Track.m_material1 = material;
if (Track.eType == tt_Switch)
Track.m_material2 = material;
return true;
}, true, "Rails");
}
ImGui::InputTextWithHint("##settrackbed", STR_C("texture of the trackbed"), fields.trackbed, sizeof(fields.trackbed));
ImGui::SameLine();
if (ImGui::Button(format("%s##setb", STR_C("Trackbed")).c_str(), ImVec2(button_width, 0.0f)) && fields.trackbed[0] != '\0')
{
auto const material{editor_track::fetch_material(fields.trackbed)};
track_set_apply([material](TTrack &Track) {
if (Track.eType == tt_Switch)
{
if (Track.SwitchExtension == nullptr)
return false;
Track.SwitchExtension->m_material3 = material;
}
else
Track.m_material2 = material;
return true;
}, true, "Trackbed");
}
ImGui::InputTextWithHint("##setcircuits", STR_C("track circuits, separated by spaces"), fields.circuits, sizeof(fields.circuits));
ImGui::SameLine();
if (ImGui::Button(format("%s##setc", STR_C("Circuits")).c_str(), ImVec2(button_width, 0.0f)))
{
std::vector<std::string> names;
std::string const text{fields.circuits};
std::size_t at{0};
while (at < text.size())
{
auto const end{text.find(' ', at)};
auto const word{text.substr(at, end == std::string::npos ? std::string::npos : end - at)};
if (false == word.empty())
names.push_back(word);
if (end == std::string::npos)
break;
at = end + 1;
}
track_set_apply([names](TTrack &Track) { editor_track::isolated(Track, names); return true; }, false, "Circuits");
}
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
ImGui::Combo("##setenvironment", &fields.environment, environments, IM_ARRAYSIZE(environments));
ImGui::SameLine();
if (ImGui::Button(format("%s##sete", STR_C("Environment")).c_str(), ImVec2(button_width, 0.0f)))
{
auto const environment{static_cast<TEnvironmentType>(fields.environment)};
track_set_apply([environment](TTrack &Track) { Track.eEnvironment = environment; return true; }, false, "Environment");
}
ImGui::PopItemWidth();
if (ImGui::Button(format(STR_C("Delete %zu paths"), m_track_set.size()).c_str()))
track_set_delete();
ImGui::Separator();
}