17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-22 20:11:11 +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

@@ -241,6 +241,19 @@ void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex)
Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn});
}
char const *type_name(TTrack const &Track)
{
switch (Track.eType)
{
case tt_Normal: return "normal";
case tt_Switch: return "switch";
case tt_Cross: return "cross";
case tt_Table: return "turntable";
case tt_Tributary: return "tributary";
default: return "unknown";
}
}
}
TTrack *editor_mode::selected_track() const
@@ -261,6 +274,9 @@ void editor_mode::select_track(scene::basic_node *Node)
m_track_point = {};
m_track_mode_notice.clear();
}
// a track out of the set ends it, the set is the selection only together with its tracks
if (false == in_track_set(track))
m_track_set.clear();
m_node = track;
ui()->set_node(m_node);
if (ui()->mode() != nodebank_panel::TRACK)
@@ -295,7 +311,8 @@ void editor_mode::select_track(scene::basic_node *Node)
editor_track::point_ref editor_mode::track_handle_hit() const
{
auto *track = selected_track();
if (track == nullptr || false == editor_track::is_supported(*track) || m_track_tab != track_tab::path)
// the tracks of a set are moved by its gizmo, not by the points of one of them
if (track == nullptr || false == editor_track::is_supported(*track) || m_track_tab != track_tab::path || (m_track_set.size() >= 2 && in_track_set(track)))
return {};
screen_projection const projection;
@@ -326,6 +343,9 @@ void editor_mode::draw_track_overlay() const
screen_projection const projection;
ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport());
bool const trackmode = ui()->mode() == nodebank_panel::TRACK;
// a track of the set is drawn with the set, see draw_track_set()
if (trackmode && m_track_set.size() >= 2 && in_track_set(track))
return;
ImU32 const coursecolor = IM_COL32(40, 220, 255, 220);
int constexpr samples{32};
@@ -779,18 +799,45 @@ void editor_mode::render_track_tool_options()
void editor_mode::render_track_selection()
{
auto *track{selected_track()};
if (track == nullptr)
track_field_settle();
auto const tracks{selected_tracks()};
if (tracks.empty())
{
ImGui::TextDisabled("%s", STR_C("Nothing selected"));
if (m_track_tab == track_tab::path)
{
ImGui::PushTextWrapPos(0.0f);
ImGui::TextDisabled("%s", STR_C("Click a track to see its properties. Shift+click adds more tracks to the selection, a drag on the ground selects those in a box"));
ImGui::PopTextWrapPos();
}
return;
}
char const *type = track->eType == tt_Normal ? "normal" : track->eType == tt_Switch ? "switch" : track->eType == tt_Cross ? "cross" : track->eType == tt_Table ? "turntable" : track->eType == tt_Tributary ? "tributary" : "unknown";
ImGui::TextUnformatted((track->name().empty() ? std::string{"(noname)"} : track->name()).c_str());
ImGui::TextDisabled("%s, %.2f m", STR_C(type), track->Length());
// the parameters of the path belong to the select tool, the other tools show the path they work with
if (m_track_tab == track_tab::path)
render_path_ui();
// the properties belong to the select tool and to the switch tool, the other tools show the track they work with
auto const sheet{m_track_tab == track_tab::path || m_track_tab == track_tab::turnout};
if (tracks.size() >= 2)
{
render_track_set_header(tracks);
if (false == sheet)
return;
render_track_select_tools(tracks);
render_track_style(tracks);
render_track_properties(tracks);
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.18f, 0.15f, 1.0f));
if (ImGui::Button(format(STR_P("Delete %d track", "Delete %d tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size())).c_str()))
track_set_delete();
ImGui::PopStyleColor();
item_tooltip("Deletes the selected tracks (Del); Ctrl+Z brings them back");
return;
}
auto *track{tracks.front()};
if (false == sheet)
{
ImGui::TextUnformatted((track->name().empty() ? std::string{"(noname)"} : track->name()).c_str());
ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length());
return;
}
render_path_ui();
}
void editor_mode::render_track_guide()
@@ -1001,6 +1048,18 @@ void editor_mode::render_turnout_ui()
}
item_tooltip("Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\n"
"Off for a switch whose include has its own model of them");
if (frogs)
{
bool movable{track->SwitchExtension->m_movablenose};
if (ImGui::Checkbox(STR_C("Movable nose"), &movable))
{
auto const before{editor_track::capture(*track)};
track->SwitchExtension->m_movablenose = movable;
push_track_snapshot({{track, before}});
editor_track::commit_parameters(*track);
}
item_tooltip("The nose of the frog moves with the blades over to the wing rail of the track set, leaving no gap; the frog has no check rails");
}
if (false == m_switch.status.empty())
ImGui::TextWrapped("%s", m_switch.status.c_str());
if (angle > 1e-6)
@@ -1143,279 +1202,32 @@ void editor_mode::render_turnout_ui()
ImGui::TextDisabled(STR_C("Room on the straight: %.2f m back, %.2f m ahead"), -low, high - reach);
}
ImGui::Separator();
render_path_parameters(*track);
ImGui::TextDisabled("%s", STR_C("The textures, the speed and the other properties of the switch are in the inspector"));
}
void editor_mode::render_path_parameters(TTrack &Track)
void editor_mode::render_path_events(TTrack &Track)
{
auto *track{&Track};
auto const before = editor_track::capture(*track);
enum class rebuild { none, parameters, geometry };
auto const finish_edit = [&](rebuild const Rebuild) {
if (ImGui::IsItemActivated())
m_track_field_before = before;
if (ImGui::IsItemDeactivatedAfterEdit())
{
push_track_snapshot({{track, m_track_field_before}});
switch (Rebuild)
{
case rebuild::geometry: editor_track::commit({track}); break;
case rebuild::parameters: editor_track::commit_parameters(*track); break;
default: track->mark_dirty(); break;
}
}
};
auto const apply_now = [&](rebuild const Rebuild) {
push_track_snapshot({{track, before}});
if (Rebuild == rebuild::parameters)
editor_track::commit_parameters(*track);
else
track->mark_dirty();
};
ImGui::PushItemWidth(120.0f);
double velocity = editor_track::velocity(*track);
if (ImGui::InputDouble(STR_C("Velocity (km/h, -1: none)"), &velocity, 0.0, 0.0, "%.1f"))
editor_track::velocity(*track, velocity);
item_tooltip("Speed limit of the path for the AI drivers and the timetable, -1: no limit of its own");
finish_edit(rebuild::none);
char const *environments[] = {STR_C("flat"), STR_C("mountains"), STR_C("canyon"), STR_C("tunnel"), STR_C("bridge"), STR_C("bank")};
int environment = std::clamp(static_cast<int>(track->eEnvironment), 0, 5);
if (ImGui::Combo(STR_C("Environment"), &environment, environments, IM_ARRAYSIZE(environments)))
if (m_track_events_for != track && false == ImGui::IsAnyItemActive())
{
track->eEnvironment = static_cast<TEnvironmentType>(environment);
apply_now(rebuild::none);
}
item_tooltip("Surroundings of the path, they affect the sound of the passing vehicles");
ImGui::PopItemWidth();
if (ImGui::CollapsingHeader(STR_C("Ride and sound")))
{
ImGui::PushItemWidth(120.0f);
if (ImGui::InputFloat(STR_C("Friction"), &track->fFriction, 0.0f, 0.0f, "%.3f"))
track->fFriction = std::max(0.0f, track->fFriction);
item_tooltip("Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip");
finish_edit(rebuild::none);
ImGui::InputFloat(STR_C("Sound distance"), &track->fSoundDistance, 0.0f, 0.0f, "%.1f");
item_tooltip("Distance between the rail joints in metres, sets the rhythm of the wheel clatter; -1: the default");
finish_edit(rebuild::none);
ImGui::InputInt(STR_C("Quality"), &track->iQualityFlag, 0);
item_tooltip("Quality of the track, 20 is the nominal one; lower values give a rougher ride");
finish_edit(rebuild::none);
int damage = track->iDamageFlag;
if (ImGui::InputInt(STR_C("Damage flags"), &damage, 0))
editor_track::damage(*track, damage);
item_tooltip("0: track in order; any value adds jolts, 128: the vehicles derail on the path");
finish_edit(rebuild::none);
if (ImGui::InputFloat(STR_C("Width"), &track->fTrackWidth, 0.0f, 0.0f, "%.3f"))
track->fTrackWidth = std::max(0.01f, track->fTrackWidth);
item_tooltip("Gauge of the track (1.435 m for the standard gauge), width of the surface for the roads");
finish_edit(rebuild::parameters);
ImGui::PopItemWidth();
}
if (track->m_visible && ImGui::CollapsingHeader(STR_C("Textures and trackbed")))
{
material_handle *materials[] = {&track->m_material1, &track->m_material2, track->SwitchExtension ? &track->SwitchExtension->m_material3 : nullptr};
char const *pathlabels[] = {"Texture 1 (rails/surface)", "Texture 2 (trackbed/side)", "Trackbed (switch)"};
char const *switchlabels[] = {"Rails P1-P2", "Rails P3-P4", "Trackbed"};
auto const &labels{track->eType == tt_Switch ? switchlabels : pathlabels};
if (false == ImGui::IsAnyItemActive())
{
for (int i = 0; i < 3; ++i)
{
if (m_track_material_edited[i] != nullptr)
continue;
auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{};
std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1);
m_track_materials[i].back() = '\0';
}
}
for (int i = 0; i < 3; ++i)
{
if (materials[i] == nullptr || (i == 2 && track->eType != tt_Switch))
continue;
ImGui::PushItemWidth(-160.0f);
if (ImGui::InputText(labels[i], m_track_materials[i].data(), m_track_materials[i].size()))
m_track_material_edited[i] = track;
if (ImGui::IsItemDeactivated() && m_track_material_edited[i] != nullptr)
{
auto const *edited{m_track_material_edited[i]};
m_track_material_edited[i] = nullptr;
if (edited == track && m_track_materials[i].data() != editor_track::material_name(*materials[i]))
{
*materials[i] = editor_track::fetch_material(m_track_materials[i].data());
apply_now(rebuild::parameters);
}
}
ImGui::PopItemWidth();
}
ImGui::TextDisabled(STR_C("The texture changes on Enter or leaving the field, \"none\" removes it"));
ImGui::PushItemWidth(120.0f);
if (ImGui::InputFloat(STR_C("Texture length"), &track->fTexLength, 0.0f, 0.0f, "%.2f"))
track->fTexLength = std::max(0.01f, track->fTexLength);
item_tooltip("Length in metres after which the texture of the rails and the trackbed repeats");
finish_edit(rebuild::parameters);
float height = editor_track::texture_height(*track);
if (ImGui::InputFloat(STR_C("Trackbed height"), &height, 0.0f, 0.0f, "%.3f"))
editor_track::texture_height(*track, height);
item_tooltip("Height of the trackbed edge below the rail top, in metres");
finish_edit(rebuild::parameters);
ImGui::InputFloat(STR_C("Trackbed width"), &track->fTexWidth, 0.0f, 0.0f, "%.3f");
item_tooltip("Width of the trackbed outside of the rail, in metres");
finish_edit(rebuild::parameters);
ImGui::InputFloat(STR_C("Trackbed slope"), &track->fTexSlope, 0.0f, 0.0f, "%.3f");
item_tooltip("Horizontal reach of the slope at the outer edge of the trackbed, in metres");
finish_edit(rebuild::parameters);
ImGui::PopItemWidth();
}
render_path_circuits(*track);
}
void editor_mode::render_path_circuits(TTrack &Track)
{
auto *track{&Track};
auto const copy = [](std::array<char, 256> &Buffer, std::string const &Text) {
std::strncpy(Buffer.data(), Text.c_str(), Buffer.size() - 1);
Buffer.back() = '\0';
};
auto const isolated_text = [&]() {
std::string text;
for (auto const *isolated : track->Isolated)
text += (text.empty() ? "" : " ") + isolated->asName;
return text;
};
if (m_track_names_for != track && false == ImGui::IsAnyItemActive())
{
m_track_names_for = track;
copy(m_track_isolated, isolated_text());
copy(m_track_sleeper_model, track->m_sleeper_model_name);
copy(m_track_sleeper_skin, track->m_sleeper_skin_name.empty() ? std::string{"none"} : track->m_sleeper_skin_name);
m_track_events_for = track;
m_track_event[0] = '\0';
m_track_events_missing.clear();
}
// one undo step for the change made by the widget just used
auto const changed = [&](editor_track::state const &Before, bool const Geometry) {
push_track_snapshot({{track, Before}});
if (Geometry)
editor_track::commit_parameters(*track);
else
track->mark_dirty();
};
if (false == ImGui::CollapsingHeader(STR_C("Track circuit, overhead line, sleepers, events")))
auto const count{[&]() {
std::size_t count{0};
for (int list = 0; list < 6; ++list)
count += editor_track::events(*track, list).size();
return count;
}()};
if (false == ImGui::CollapsingHeader(format("%s (%zu)###trackevents", STR_C("Events"), count).c_str()))
return;
auto const before{editor_track::capture(*track)};
ImGui::PushItemWidth(-160.0f);
ImGui::InputText(STR_C("Track circuits"), m_track_isolated.data(), m_track_isolated.size());
item_tooltip("Names of the track circuits (isolated) the path belongs to, separated by spaces.\n"
"A name which doesn't exist yet makes a new circuit. Changes on leaving the field");
if (ImGui::IsItemDeactivated() && m_track_isolated.data() != isolated_text())
{
if (false == track->Dynamics.empty())
{
ui()->set_status(STR_C("The track circuits can't change while vehicles stand on the path"), true);
copy(m_track_isolated, isolated_text());
}
else
{
std::istringstream names{m_track_isolated.data()};
std::vector<std::string> list;
for (std::string name; names >> name;)
list.push_back(name);
editor_track::isolated(*track, list);
changed(before, false);
copy(m_track_isolated, isolated_text());
}
}
ImGui::PopItemWidth();
int overheadmode{track->fOverhead < 0.f ? 0 : track->fOverhead == 0.f ? 1 : 2};
ImGui::PushItemWidth(220.0f);
char const *overheadmodes[] = {STR_C("Normal"), STR_C("No current, coasting"), STR_C("Pantographs lowered, speed limit")};
if (ImGui::Combo(STR_C("Overhead line"), &overheadmode, overheadmodes, IM_ARRAYSIZE(overheadmodes)))
{
editor_track::overhead(*track, overheadmode == 0 ? -1.f : overheadmode == 1 ? 0.f : std::max(1.f, track->fOverhead > 0.f ? track->fOverhead : 40.f));
changed(before, false);
}
ImGui::PopItemWidth();
item_tooltip("State of the overhead line over the path: the trains coast through a section with no current,\nor lower the pantographs and keep to the speed limit given");
if (track->fOverhead > 0.f)
{
ImGui::PushItemWidth(120.0f);
float limit{track->fOverhead};
if (ImGui::InputFloat(STR_C("Speed with the pantographs lowered (km/h)"), &limit, 0.0f, 0.0f, "%.0f"))
editor_track::overhead(*track, std::max(1.f, limit));
if (ImGui::IsItemActivated())
m_track_field_before = before;
if (ImGui::IsItemDeactivatedAfterEdit())
changed(m_track_field_before, false);
ImGui::PopItemWidth();
}
ImGui::Separator();
bool sleepers{track->m_sleeper_enabled};
if (ImGui::Checkbox(STR_C("Sleeper models"), &sleepers))
{
if (sleepers && m_track_sleeper_model[0] == '\0')
{
ui()->set_status(STR_C("Give the sleeper model first"), true);
}
else
{
track->m_sleeper_enabled = sleepers;
track->m_sleeper_model_name = m_track_sleeper_model.data();
changed(before, true);
}
}
item_tooltip("Repeats a model along the path, the sleepers in place of the drawn trackbed");
ImGui::PushItemWidth(-160.0f);
auto const name_field = [&](char const *Label, std::array<char, 256> &Buffer, std::string &Value, char const *Tooltip) {
ImGui::InputText(Label, Buffer.data(), Buffer.size());
item_tooltip(Tooltip);
std::string typed{Buffer.data()};
if (&Value == &track->m_sleeper_skin_name && typed == "none")
typed.clear();
if (ImGui::IsItemDeactivated() && typed != Value)
{
Value = typed;
changed(before, track->m_sleeper_enabled);
}
auto const changed = [&](editor_track::state const &Before) {
push_track_snapshot({{track, Before}});
track->mark_dirty();
};
name_field(STR_C("Sleeper model"), m_track_sleeper_model, track->m_sleeper_model_name, STR_C("Path of the .e3d model, as in the scenery file"));
name_field(STR_C("Sleeper skin"), m_track_sleeper_skin, track->m_sleeper_skin_name, STR_C("Replaceable skin of the model, \"none\": the one of the model"));
ImGui::PopItemWidth();
ImGui::PushItemWidth(120.0f);
auto const number_field = [&](char const *Label, float &Value, char const *Format, char const *Tooltip) {
ImGui::InputFloat(Label, &Value, 0.0f, 0.0f, Format);
item_tooltip(Tooltip);
if (ImGui::IsItemActivated())
m_track_field_before = before;
if (ImGui::IsItemDeactivatedAfterEdit())
{
track->m_sleeper_frequency = std::max(0.1f, track->m_sleeper_frequency);
changed(m_track_field_before, track->m_sleeper_enabled);
}
};
number_field(STR_C("Sleeper spacing (m)"), track->m_sleeper_frequency, "%.3f", STR_C("Distance between the sleepers along the path"));
ImGui::PopItemWidth();
ImGui::PushItemWidth(200.0f);
ImGui::InputFloat3(STR_C("Sleeper offset (m)"), &track->m_sleeper_offset.x, "%.3f");
item_tooltip("Shift of each model: sideways, along the path, up");
if (ImGui::IsItemActivated())
m_track_field_before = before;
if (ImGui::IsItemDeactivatedAfterEdit())
changed(m_track_field_before, track->m_sleeper_enabled);
ImGui::PopItemWidth();
ImGui::PushItemWidth(120.0f);
number_field(STR_C("Trackbed shift (m)"), track->m_sleeper_ballast_z, "%.3f", STR_C("Vertical shift of the drawn trackbed under the sleepers, negative lowers it"));
ImGui::PopItemWidth();
ImGui::Separator();
ImGui::TextUnformatted(STR_C("Events"));
ImGui::TextDisabled("%s", STR_C("What the track sets off as the vehicles run on it"));
item_tooltip("event0: a vehicle with a crew stands on the path, event1: runs along it towards its start, event2: towards its end.\n"
"eventall0/1/2: the same for any vehicle");
auto const editable_events{track->Dynamics.empty()};
@@ -1436,7 +1248,7 @@ void editor_mode::render_path_circuits(TTrack &Track)
{
events.erase(events.begin() + i);
editor_track::bind_events(*track, m_track_events_missing);
changed(before, false);
changed(before);
ImGui::PopID();
break;
}
@@ -1466,7 +1278,7 @@ void editor_mode::render_path_circuits(TTrack &Track)
{
editor_track::events(*track, m_track_event_list).emplace_back(m_track_event.data(), nullptr);
editor_track::bind_events(*track, m_track_events_missing);
changed(before, false);
changed(before);
m_track_event[0] = '\0';
}
}
@@ -1490,17 +1302,26 @@ void editor_mode::render_path_ui()
return;
}
render_track_name(*track);
ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length());
std::string reason;
bool const geometry = editor_track::can_edit_geometry(*track, reason);
if (false == geometry)
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), STR_C("Geometry locked: %s"), reason.c_str());
if (track->eType == tt_Switch)
if (track->eType == tt_Switch && m_track_tab == track_tab::path)
{
ImGui::TextWrapped("%s", STR_C("A switch: the turnout tool sets its geometry, side and drive"));
if (ImGui::Button(STR_C("Edit the switch (T)")))
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Edit the switch (T)")))
show_track_tab(track_tab::turnout);
item_tooltip("Opens the turnout tool with this switch");
item_tooltip("A switch: the turnout tool sets its geometry, side and drive");
}
std::vector<TTrack *> const tracks{track};
render_track_select_tools(tracks);
render_track_style(tracks);
render_track_properties(tracks);
render_path_events(*track);
// the shape is given by dragging in the view, the numbers are for the exact values
if (m_track_tab != track_tab::path)
return;
auto const before = editor_track::capture(*track);
enum class rebuild { none, parameters, geometry };
@@ -1519,7 +1340,7 @@ void editor_mode::render_path_ui()
}
};
if (ImGui::CollapsingHeader(track->eType == tt_Switch ? STR_C("Geometry of the paths") : STR_C("Geometry"), track->eType == tt_Switch ? 0 : ImGuiTreeNodeFlags_DefaultOpen))
if (ImGui::CollapsingHeader(track->eType == tt_Switch ? STR_C("Geometry of the paths") : STR_C("Geometry")))
{
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
{
@@ -1603,7 +1424,6 @@ void editor_mode::render_path_ui()
ImGui::TreePop();
}
}
render_path_parameters(*track);
}
@@ -3954,7 +3774,12 @@ std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All)
else if (m_track_tab == track_tab::route)
hints = {{"Drag a vertex", "the curve follows"}, {"Drag the radius grip", "changes the radius"}, {"Apply", "writes the design into the tracks"}, {"Esc", "release the vertex"}};
else if (selected == nullptr)
hints = {{"LMB on a track", "selects it"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"RMB on a track", "what can be done with it"}, {"L", "lay a new track"}};
hints = {{"LMB on a track", "selects it"}, {"Drag on the ground", "selects the tracks in a box"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"RMB on a track", "what can be done with it"}, {"L", "lay a new track"}};
else if (m_track_tab == track_tab::path && m_track_set.size() >= 2 && in_track_set(selected))
{
hints = {{"Inspector", "the properties of all of them at once"}, {"Gizmo", "moves them together"}, {"Shift/Ctrl+LMB", "adds or takes out a track"}, {"Ctrl+Shift+V", "pastes the copied properties"}, {"Esc", "deselects"}, {"Del", "deletes them"}};
gizmo = true;
}
else if (m_point_drag.moved)
hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "done"}};
else if (m_track_point.valid())
@@ -5524,7 +5349,7 @@ void editor_mode::switch_placed(std::vector<TTrack *> const &Created)
if (slip)
{
auto const known{switch_store::named(**found)};
if (false == known.has_value() || known->kind != "double")
if (false == known.has_value() || (known->kind != "double" && known->kind != "slip"))
return;
std::vector<std::string> driven;
for (auto *track : Created)
@@ -5533,6 +5358,19 @@ void editor_mode::switch_placed(std::vector<TTrack *> const &Created)
join_last_steps();
driven.push_back(track->name());
}
// a slip has a drive at each of its switches
if (known->kind == "slip")
{
if (false == driven.empty())
{
std::string names;
for (auto const &name : driven)
names += (names.empty() ? "" : ", ") + name;
m_switch.status = format(STR_C("Slip with drives at its switches %s: the events of the name of a switch with + and - switch it, also in the map"), names.c_str());
ui()->set_status(m_switch.status, false);
}
return;
}
if (driven.size() == 2)
{
m_switch.status = format(STR_C("Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map"), driven[0].c_str(), driven[1].c_str(), driven[0].c_str(), driven[0].c_str(), driven[1].c_str(), driven[1].c_str());
@@ -6472,7 +6310,7 @@ std::vector<TTrack *> editor_mode::link_tracks(editor_track::snap_target const &
return pieces;
}
std::vector<TTrack *> editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style, std::string const &Type)
std::vector<TTrack *> editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type)
{
auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; };
auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) {
@@ -6485,33 +6323,91 @@ std::vector<TTrack *> editor_mode::build_double_slip(glm::dvec3 const &PointA, g
auto const halfsine{std::sqrt(std::max(1e-6, (1.0 - glm::dot(First, Second)) * 0.5))};
auto const gap{std::max(0.25, 0.06 / halfsine)};
auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0};
auto const whole{Curve.length()};
// the part of the slip curve from Start along Heading, turning by Turn to Finish, between the distances From and To along it;
// where it changes the radius made of pieces, with one curve from end to end for the versions which know no pieces
auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) {
glm::dvec2 const centre{plan_of(Start) + glm::dvec2{-Heading.y, Heading.x} * (Turn * Radius)};
auto const radial{plan_of(Start) - centre};
auto const p0{centre + turned(radial, Turn * From)};
auto const p3{centre + turned(radial, Turn * To)};
auto const d0{turned(Heading, Turn * From)};
auto const d3{turned(Heading, Turn * To)};
auto const handle{4.0 / 3.0 * std::tan((To - From) / 4.0) * Radius};
auto const y0{Start.y + (Finish.y - Start.y) * From / Angle};
auto const y3{Start.y + (Finish.y - Start.y) * To / Angle};
segment_data path;
path.points[segment_data::point::start] = {p0.x, y0, p0.y};
path.points[segment_data::point::end] = {p3.x, y3, p3.y};
path.points[segment_data::point::control1] = {d0.x * handle, 0.0, d0.y * handle};
path.points[segment_data::point::control2] = {-d3.x * handle, 0.0, -d3.y * handle};
path.radius = static_cast<float>(Radius);
auto const place = [&](double const Along) {
auto const local{Curve.point(Along)};
auto const planar{plan_of(Start) + Heading * local.x + glm::dvec2{-Heading.y, Heading.x} * (Turn * local.y)};
return glm::dvec3{planar.x, Start.y + (Finish.y - Start.y) * Along / whole, planar.y};
};
auto const bend = [&](double const Begin, double const End, double const Radius, double const Handle0, double const Handle1) {
auto const d0{turned(Heading, Turn * Curve.heading(Begin))};
auto const d3{turned(Heading, Turn * Curve.heading(End))};
segment_data path;
path.points[segment_data::point::start] = place(Begin);
path.points[segment_data::point::end] = place(End);
if (Radius > 0.0)
{
path.points[segment_data::point::control1] = {d0.x * Handle0, 0.0, d0.y * Handle0};
path.points[segment_data::point::control2] = {-d3.x * Handle1, 0.0, -d3.y * Handle1};
}
path.radius = static_cast<float>(Radius);
return path;
};
std::vector<std::pair<double, double>> zones;
for (auto const &zone : {std::pair{Curve.lead, 0.0}, {Curve.lead + Curve.ends, Curve.radius}, {Curve.lead + Curve.ends + Curve.between, Curve.middle}, {whole - Curve.lead, Curve.radius}, {whole, 0.0}})
{
if (zone.first <= (zones.empty() ? 0.0 : zones.back().first) + 1e-9)
continue;
if (false == zones.empty() && zone.second == zones.back().second)
zones.back().first = zone.first;
else
zones.push_back(zone);
}
// broken at the tips of the blades of the tables, so the model of the switch finds them there
if (Curve.tip > 0.0)
for (auto const cut : {Curve.tip, whole - Curve.tip})
for (std::size_t i = 0; i < zones.size(); ++i)
if (cut > (i > 0 ? zones[i - 1].first : 0.0) + 0.05 && cut < zones[i].first - 0.05)
{
zones.insert(zones.begin() + i, {cut, zones[i].second});
break;
}
std::vector<segment_data> pieces;
auto from{From};
for (auto const &[to, radius] : zones)
{
auto const upto{std::min(To, to)};
if (upto <= from + 1e-6)
continue;
auto const handle{radius > 0.0 ? 4.0 / 3.0 * std::tan((upto - from) / radius / 4.0) * radius : 0.0};
pieces.push_back(bend(from, upto, radius, handle, handle));
from = upto;
}
if (pieces.size() == 1)
return pieces.front();
// the handles of the curve from end to end reach 2/3 of the way to the crossing of the tangents
auto const chord{plan_of(place(To) - place(From))};
auto const d0{turned(Heading, Turn * Curve.heading(From))};
auto const d3{turned(Heading, Turn * Curve.heading(To))};
auto const sine{cross(d0, d3)};
auto path{bend(From, To, Curve.radius, 2.0 / 3.0 * cross(chord, d3) / sine, 2.0 / 3.0 * cross(d0, chord) / sine)};
path.pieces = std::move(pieces);
return path;
};
auto const middle{Angle * 0.5};
auto const split{gap / Radius};
auto const back = [](segment_data const &Path) {
auto result{reversed(Path)};
std::reverse(result.pieces.begin(), result.pieces.end());
for (auto &piece : result.pieces)
piece = reversed(piece);
return result;
};
auto const middle{whole * 0.5};
auto const split{gap};
auto const centreA{at(Crossing - First * gap, Height)};
auto const centreB{at(Crossing + First * gap, Height)};
auto const centreC{at(Crossing - Second * gap, Height)};
auto const centreD{at(Crossing + Second * gap, Height)};
std::string base;
auto const prefix{Type == "Skrz" ? "editor_skrz" : Type == "Rkp" ? "editor_dkp" : "editor_dks"};
for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr; ++i)
// a crossing keeps the names of its tracks, only its record holds the name
auto const recorded = [](std::string const &Name) {
auto const &records{switch_store::records()};
return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; });
};
for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr || recorded(base); ++i)
base = prefix + std::to_string(i);
std::vector<TTrack *> parts;
if (Type == "Skrz")
@@ -6530,19 +6426,19 @@ std::vector<TTrack *> editor_mode::build_double_slip(glm::dvec3 const &PointA, g
if (single)
parts.push_back(editor_track::create_path(Style, straight(PointB, centreB)));
else
parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), reversed(arc(PointC, Second, -side, PointB, middle + split, Angle))}, Style, base + "_b"));
parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), back(arc(PointC, Second, -side, PointB, middle + split, whole))}, Style, base + "_b"));
if (single)
parts.push_back(editor_track::create_path(Style, straight(PointC, centreC)));
else
parts.push_back(editor_track::create_switch(shape, {straight(PointC, centreC), arc(PointC, Second, -side, PointB, 0.0, middle - split)}, Style, base + "_c"));
parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), reversed(arc(PointA, First, side, PointD, middle + split, Angle))}, Style, base + "_d"));
parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), back(arc(PointA, First, side, PointD, middle + split, whole))}, Style, base + "_d"));
parts.push_back(editor_track::create_path(Style, straight(centreA, centreB)));
parts.push_back(editor_track::create_path(Style, straight(centreC, centreD)));
parts.push_back(editor_track::create_path(Style, arc(PointA, First, side, PointD, middle - split, middle + split)));
if (false == single)
parts.push_back(editor_track::create_path(Style, arc(PointC, Second, -side, PointB, middle - split, middle + split)));
parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end());
record_slip(parts, "slip", Type, Radius, Angle, base);
record_slip(parts, "slip", Type, Curve.radius, Angle, base, Curve.middle);
return parts;
}
@@ -6615,8 +6511,11 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv
auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)};
auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)};
auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5};
auto radius{Shape.radius};
auto half{plain ? Shape.a : radius * std::tan(angle * 0.5)};
auto const *gauged{Line.tracks.front()};
auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)};
auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(gauged->fTrackWidth) + gauged->rail_dimensions().head + 0.002, blade.first, blade.second)};
curve.tip = switch_design::cut_tip(Shape.rail, curve.radius);
auto half{plain ? Shape.a : curve.tangent()};
if (plain && half > available)
{
half = available;
@@ -6628,14 +6527,14 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv
}
else if (half > available)
{
curve = curve.scaled(available / half);
half = available;
radius = half / std::tan(angle * 0.5);
if (radius < 50.0)
if (curve.radius < 50.0)
{
state.status = STR_C("Not enough straight track around the crossing");
return false;
}
state.status = STR_C("R reduced to ") + std::to_string(static_cast<int>(radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast<int>(available)) + STR_C(" m from the crossing");
state.status = STR_C("R reduced to ") + std::to_string(static_cast<int>(curve.radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast<int>(available)) + STR_C(" m from the crossing");
}
auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; };
@@ -6674,8 +6573,8 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv
auto &styled{style != nullptr ? *style : *Line.tracks.front()};
// a single slip joins the pair of the tracks on the side of the click
auto const nearer{glm::distance((plan_of(pointA) + plan_of(pointD)) * 0.5, plan_of(Point)) <= glm::distance((plan_of(pointC) + plan_of(pointB)) * 0.5, plan_of(Point))};
auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, radius, styled, Shape.type)
: build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, radius, styled, Shape.type)};
auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, curve, styled, Shape.type)
: build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, curve, styled, Shape.type)};
editor_track::commit(parts);
created.insert(created.end(), parts.begin(), parts.end());
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
@@ -6752,8 +6651,12 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
glm::dvec2 const crossing{plan_of(a.point) + first * t};
auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))};
auto const oldhalf{glm::distance(plan_of(a.point), crossing)};
auto radius{Shape.radius};
auto half{radius * std::tan(angle * 0.5)};
auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)};
auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(switches[0]->fTrackWidth) + switches[0]->rail_dimensions().head + 0.002, blade.first, blade.second)};
curve.tip = switch_design::cut_tip(Shape.rail, curve.radius);
auto const full{curve.tangent()};
auto radius{curve.radius};
auto half{full};
auto const height{(a.point.y + b.point.y) * 0.5};
std::vector<std::pair<TTrack *, editor_track::state>> states;
@@ -6766,7 +6669,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
if (e->outer == nullptr || false == editor_track::is_straight(*e->outer, m_straights.tolerance))
{
half = oldhalf;
radius = half / std::tan(angle * 0.5);
radius = curve.radius * half / full;
state.status = STR_C("R limited to ") + std::to_string(static_cast<int>(radius)) + STR_C(" m, a neighbouring path isn't straight");
break;
}
@@ -6776,7 +6679,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
if (std::max(reach, reachstart) - 0.5 < half)
{
half = std::min(half, std::max(reach, reachstart) - 0.5);
radius = half / std::tan(angle * 0.5);
radius = curve.radius * half / full;
state.status = STR_C("R reduced to ") + std::to_string(static_cast<int>(radius)) + STR_C(" m to fit the straights");
}
}
@@ -6811,7 +6714,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
cut_straight(line, std::min(alongold, alongnew), std::max(alongold, alongnew), states, created, removed, nullptr);
}
}
auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, radius, *style)};
auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, curve.scaled(half / full), *style)};
std::vector<TTrack *> commitlist(parts2.begin(), parts2.end());
for (auto *track : created)
if (std::find(commitlist.begin(), commitlist.end(), track) == commitlist.end())
@@ -6820,6 +6723,16 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
created.insert(created.end(), parts2.begin(), parts2.end());
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
// the drives of the switches replaced go with them, the new ones get theirs
std::vector<scene::instance_handle> drives;
for (auto const *track : switches)
if (auto const drive{find_switch_drive(*track)})
drives.push_back(drive->standing.instance);
if (false == drives.empty() && remove_templates(drives) > 0)
join_last_steps();
for (auto *track : parts2)
if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track))
join_last_steps();
return true;
}