mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-18 14:59:19 +02:00
Editor checkpoint: turnout domain service, web->editor rename, Id-1 templates
Checkpoint of the editor branch so work isn't carried loose across a branch switch. Builds. - maj0sted turnout geometry moved into the domain: domain::lay_turnout lays the diverging path (pre-blade lead, blade at beta, arc of alfa-beta, frog rail) and solve_junction is a thin adapter onto it. - the maj0sted web/ tree renamed to editor/ (namespace and paths) - there is no WASM here, it's plain native C++ the editor calls. - click places the switch at its start (początek rozjazdu); the branch's leaving straight is locked tangent to the frog; switches can be slid along the through track and flipped L/P. - Polish turnout templates carry catalogue length (Id-1-consistent lengths), plus blade-angle and pre-blade fields (left 0 until the catalogue figures are in). - TURNOUT_SPEC.md: the Bezier-based runtime/model contract. - also folds in other pending branch changes (scenery, tests) to leave a clean tree.
This commit is contained in:
@@ -218,77 +218,74 @@ void eu07_application::DiscordRPCService()
|
||||
discord_rpc.startTimestamp = static_cast<int64_t>(now_c);
|
||||
discord_rpc.largeImageText = "MaSzyna";
|
||||
|
||||
// run loop
|
||||
while (!glfwWindowShouldClose(m_windows.front()) && !m_modestack.empty() && !Global.applicationQuitOrder)
|
||||
// run until quit — do NOT require a non-empty mode stack: pop/push during scenery reload
|
||||
// briefly empties the stack, and exiting here would Discord_Shutdown() mid-session (or race on top())
|
||||
while (!Global.applicationQuitOrder && !m_windows.empty() && !glfwWindowShouldClose(m_windows.front()))
|
||||
{
|
||||
auto currentMode = m_modestack.top();
|
||||
if (m_modestack.empty())
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const currentMode = m_modestack.top();
|
||||
if (currentMode == mode::launcher)
|
||||
{
|
||||
// in launcher mode
|
||||
|
||||
discord_rpc.state = Translations.lookup_c("In main menu");
|
||||
discord_rpc.details = Translations.lookup_c("Browsing scenarios...");
|
||||
discord_rpc.largeImageKey = "";
|
||||
discord_rpc.largeImageText = "MaSzyna";
|
||||
// RPC upload
|
||||
Discord_UpdatePresence(&discord_rpc);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5000)); // update RPC every 5 secs
|
||||
continue;
|
||||
}
|
||||
else if (currentMode == mode::scenarioloader)
|
||||
{
|
||||
std::string rpcScnName = Global.SceneryFile;
|
||||
if (rpcScnName[0] == '$')
|
||||
if (!rpcScnName.empty() && rpcScnName.front() == '$')
|
||||
rpcScnName.erase(0, 1);
|
||||
rpcScnName.erase(rpcScnName.size() - 4, 4);
|
||||
if (rpcScnName.find('_') != std::string::npos)
|
||||
{
|
||||
std::replace(rpcScnName.begin(), rpcScnName.end(), '_', ' ');
|
||||
}
|
||||
if (rpcScnName.size() >= 4)
|
||||
rpcScnName.erase(rpcScnName.size() - 4, 4);
|
||||
std::replace(rpcScnName.begin(), rpcScnName.end(), '_', ' ');
|
||||
|
||||
// realworld timestamp from datetime
|
||||
static std::string state = Translations.lookup_s("Scenery: ") + rpcScnName;
|
||||
static std::string state;
|
||||
state = Translations.lookup_s("Scenery: ") + rpcScnName;
|
||||
discord_rpc.state = state.c_str();
|
||||
discord_rpc.details = Translations.lookup_c("Loading scenery...");
|
||||
discord_rpc.largeImageKey = "logo";
|
||||
discord_rpc.largeImageText = "MaSzyna";
|
||||
|
||||
// RPC upload
|
||||
Discord_UpdatePresence(&discord_rpc);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5000)); // update RPC every 5 secs
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentMode != mode::driver)
|
||||
continue;
|
||||
|
||||
// Discord RPC updater
|
||||
if (simulation::is_ready)
|
||||
else if (currentMode == mode::editor)
|
||||
{
|
||||
discord_rpc.state = "Dłubie w edytorze";
|
||||
discord_rpc.details = Global.SceneryFile.c_str();
|
||||
discord_rpc.largeImageKey = "logo";
|
||||
discord_rpc.largeImageText = "MaSzyna";
|
||||
discord_rpc.smallImageKey = "";
|
||||
discord_rpc.smallImageText = "";
|
||||
Discord_UpdatePresence(&discord_rpc);
|
||||
}
|
||||
else if (currentMode == mode::driver && simulation::is_ready)
|
||||
{
|
||||
std::string PlayerVehicle;
|
||||
if (simulation::Train != nullptr)
|
||||
if (simulation::Train != nullptr && simulation::Train->Dynamic() != nullptr)
|
||||
{
|
||||
PlayerVehicle = simulation::Train->name();
|
||||
// make to upper
|
||||
for (auto &c : PlayerVehicle)
|
||||
c = toupper(c);
|
||||
c = static_cast<char>(toupper(static_cast<unsigned char>(c)));
|
||||
|
||||
PlayerVehicle = Translations.lookup_s("Driving: ") + PlayerVehicle;
|
||||
discord_rpc.details = PlayerVehicle.c_str();
|
||||
|
||||
uint16_t playerTrainVelocity = simulation::Train->Dynamic()->GetVelocity();
|
||||
auto const playerTrainVelocity = static_cast<uint16_t>(simulation::Train->Dynamic()->GetVelocity());
|
||||
if (playerTrainVelocity > 1)
|
||||
{
|
||||
// ikonka ze jedziemy i nie spimy
|
||||
discord_rpc.smallImageKey = "driving";
|
||||
std::string smallText = Translations.lookup_s("Speed: ") + std::to_string(playerTrainVelocity) + " km/h";
|
||||
static std::string smallText;
|
||||
smallText = Translations.lookup_s("Speed: ") + std::to_string(playerTrainVelocity) + " km/h";
|
||||
discord_rpc.smallImageText = smallText.c_str();
|
||||
}
|
||||
else
|
||||
{
|
||||
// krecimy postoj
|
||||
discord_rpc.smallImageKey = "halt";
|
||||
discord_rpc.smallImageText = Translations.lookup_c("Stopped");
|
||||
}
|
||||
@@ -296,7 +293,8 @@ void eu07_application::DiscordRPCService()
|
||||
|
||||
Discord_UpdatePresence(&discord_rpc);
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5000)); // update RPC every 5 secs
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
|
||||
}
|
||||
|
||||
Discord_Shutdown();
|
||||
|
||||
@@ -617,6 +617,7 @@ bool editor_mode::update()
|
||||
Global.editor_reset_scenery = false;
|
||||
Global.editor_startup = true; // the editor is the base mode from here on, whatever started it
|
||||
Global.SceneryFile = "pusta.scn";
|
||||
simulation::is_ready = false;
|
||||
Application.pop_mode();
|
||||
Application.push_mode(eu07_application::mode::scenarioloader);
|
||||
return true;
|
||||
@@ -1326,9 +1327,29 @@ void editor_mode::on_scroll(double const Xoffset, double const Yoffset)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// in the plan view the wheel decides how much ground fits on screen; the camera itself stays put,
|
||||
// because with no perspective its height changes nothing but the clipping
|
||||
Global.editor_ortho_extent = std::clamp(Global.editor_ortho_extent * std::pow(0.85f, static_cast<float>(Yoffset)), 5.0f, 20000.0f);
|
||||
|
||||
// the wheel decides how much ground fits on screen. shift the camera so the point under the
|
||||
// cursor stays put - same idea as zooming a map, rather than scaling around the view centre
|
||||
auto const oldextent = Global.editor_ortho_extent;
|
||||
auto const newextent = std::clamp(oldextent * std::pow(0.85f, static_cast<float>(Yoffset)), 5.0f, 20000.0f);
|
||||
if (newextent == oldextent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto const width = std::max(1, Global.window_size.x);
|
||||
auto const height = std::max(1, Global.window_size.y);
|
||||
auto const ndcx = (static_cast<double>(Global.cursor_pos.x) / width) * 2.0 - 1.0;
|
||||
auto const ndcy = 1.0 - (static_cast<double>(Global.cursor_pos.y) / height) * 2.0;
|
||||
auto const halfheight = static_cast<double>(oldextent);
|
||||
auto const halfwidth = halfheight * static_cast<double>(width) / static_cast<double>(height);
|
||||
auto const scale = static_cast<double>(newextent) / static_cast<double>(oldextent);
|
||||
|
||||
// top-down with yaw pinned to north: view x follows world x, view y follows -world z
|
||||
Camera.Pos.x += ndcx * halfwidth * (1.0 - scale);
|
||||
Camera.Pos.z -= ndcy * halfheight * (1.0 - scale);
|
||||
|
||||
Global.editor_ortho_extent = newextent;
|
||||
}
|
||||
|
||||
void editor_mode::update_camera(double const Deltatime)
|
||||
@@ -1346,6 +1367,9 @@ void editor_mode::update_camera(double const Deltatime)
|
||||
}
|
||||
Camera.Angle.x = glm::radians(-90.0f);
|
||||
Camera.Angle.z = 0.0f;
|
||||
// a plan is read with north up. the editor's own camera starts turned around, which left the
|
||||
// whole view - scenery, imagery and drawing alike - standing on its head
|
||||
Camera.Angle.y = 0.0f;
|
||||
// high enough to clear anything the scenery may have under the cursor
|
||||
Camera.Pos.y = std::max(Camera.Pos.y, 500.0);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "rendering/renderer.h"
|
||||
#include "utilities/Globals.h"
|
||||
|
||||
#include "maj0sted/web/ribbon.hpp"
|
||||
#include "maj0sted/editor/ribbon.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -94,9 +94,9 @@ ImU32 element_colour(int const Kind, bool const Active)
|
||||
|
||||
// one centreline element recovered from a left/right rail pair
|
||||
// dual rails from a centreline, same 1.5 m gauge as maj0sted::render::RailRenderer
|
||||
std::vector<maj0sted::web::WebPolyline> rails_from_centreline(std::vector<maj0sted::web::WebPolyline> const &Centre, double const Gauge = 1.5)
|
||||
std::vector<maj0sted::editor::PlanPolyline> rails_from_centreline(std::vector<maj0sted::editor::PlanPolyline> const &Centre, double const Gauge = 1.5)
|
||||
{
|
||||
std::vector<maj0sted::web::WebPolyline> out;
|
||||
std::vector<maj0sted::editor::PlanPolyline> out;
|
||||
auto const half{Gauge * 0.5};
|
||||
for (auto const &poly : Centre)
|
||||
{
|
||||
@@ -106,7 +106,7 @@ std::vector<maj0sted::web::WebPolyline> rails_from_centreline(std::vector<maj0st
|
||||
}
|
||||
for (double const side : {half, -half})
|
||||
{
|
||||
maj0sted::web::WebPolyline rail;
|
||||
maj0sted::editor::PlanPolyline rail;
|
||||
rail.kind = poly.kind;
|
||||
rail.length = poly.length;
|
||||
rail.radius_start = poly.radius_start;
|
||||
@@ -230,7 +230,7 @@ double point_segment_distance(double const Px, double const Py, double const Ax,
|
||||
}
|
||||
|
||||
// distance from (X,Y) to the gap track: curve elements plus the two bounding straights
|
||||
double distance_to_gap_track(maj0sted::web::NiweletaPolys const &Solved, int const Gap, double const X, double const Y)
|
||||
double distance_to_gap_track(maj0sted::editor::NiweletaPolys const &Solved, int const Gap, double const X, double const Y)
|
||||
{
|
||||
auto best{std::numeric_limits<double>::infinity()};
|
||||
for (auto const &poly : Solved.polylines)
|
||||
@@ -257,9 +257,9 @@ struct GapSample
|
||||
};
|
||||
|
||||
// centreline of a fitted gap, sampled in element order
|
||||
std::vector<GapSample> sample_gap_centreline(maj0sted::web::NiweletaPolys const &Solved, int const Gap)
|
||||
std::vector<GapSample> sample_gap_centreline(maj0sted::editor::NiweletaPolys const &Solved, int const Gap)
|
||||
{
|
||||
std::map<int, maj0sted::web::WebPolyline const *> by_el;
|
||||
std::map<int, maj0sted::editor::PlanPolyline const *> by_el;
|
||||
for (auto const &poly : Solved.polylines)
|
||||
{
|
||||
if (poly.gap != Gap || poly.element_index < 0 || poly.points.size() < 2)
|
||||
@@ -378,7 +378,7 @@ struct CmpStationHandle
|
||||
double y{0.0};
|
||||
};
|
||||
|
||||
std::vector<CmpStationHandle> compound_station_handles(maj0sted::web::GapFit const &Fit, maj0sted::web::NiweletaPolys const &Solved, int const Gap)
|
||||
std::vector<CmpStationHandle> compound_station_handles(maj0sted::editor::GapFit const &Fit, maj0sted::editor::NiweletaPolys const &Solved, int const Gap)
|
||||
{
|
||||
std::vector<CmpStationHandle> handles;
|
||||
if (Fit.mode != 3 || Fit.arcs.empty())
|
||||
@@ -420,7 +420,7 @@ std::vector<CmpStationHandle> compound_station_handles(maj0sted::web::GapFit con
|
||||
return handles;
|
||||
}
|
||||
|
||||
std::vector<GapSample> sample_draft_centreline(std::vector<maj0sted::web::WebPolyline> const &Polys)
|
||||
std::vector<GapSample> sample_draft_centreline(std::vector<maj0sted::editor::PlanPolyline> const &Polys)
|
||||
{
|
||||
std::vector<GapSample> out;
|
||||
double along{0.0};
|
||||
@@ -444,7 +444,7 @@ std::vector<GapSample> sample_draft_centreline(std::vector<maj0sted::web::WebPol
|
||||
}
|
||||
|
||||
// draft stations: every arc has L (including the last), unlike GapFit
|
||||
std::vector<CmpStationHandle> draft_station_handles(maj0sted::app::BasketDraft const &Draft, std::vector<maj0sted::web::WebPolyline> const &Polys)
|
||||
std::vector<CmpStationHandle> draft_station_handles(maj0sted::app::BasketDraft const &Draft, std::vector<maj0sted::editor::PlanPolyline> const &Polys)
|
||||
{
|
||||
std::vector<CmpStationHandle> handles;
|
||||
if (false == Draft.active || Draft.arcs.empty())
|
||||
@@ -707,6 +707,7 @@ void plan_panel::handle_scene()
|
||||
m_dragging = false;
|
||||
m_dragging_cmp = false;
|
||||
m_dragging_draft = false;
|
||||
m_drag_junction = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -721,7 +722,16 @@ void plan_panel::handle_scene()
|
||||
{
|
||||
int hit_niw{-1};
|
||||
int hit_str{-1};
|
||||
if (hit_straight(mouse, hit_niw, hit_str, true) && hit_niw >= 0 && static_cast<std::size_t>(hit_niw) < m_document.niwelety.size())
|
||||
// straights catch only their trimmed remainders; fall back to the whole solved axis (curves
|
||||
// included) so a double-click anywhere on the visible track still selects its niweleta
|
||||
bool hit{hit_straight(mouse, hit_niw, hit_str, true)};
|
||||
if (false == hit)
|
||||
{
|
||||
hit_niw = nearest_niweleta_axis(mouse, 18.0f);
|
||||
hit_str = -1;
|
||||
hit = hit_niw >= 0;
|
||||
}
|
||||
if (hit && hit_niw >= 0 && static_cast<std::size_t>(hit_niw) < m_document.niwelety.size())
|
||||
{
|
||||
m_dragging = false;
|
||||
m_dragging_cmp = false;
|
||||
@@ -875,6 +885,26 @@ void plan_panel::handle_scene()
|
||||
return;
|
||||
}
|
||||
|
||||
// grabbing a switch's start to slide it along the through track
|
||||
for (std::size_t j = 0; j < m_junctions.size(); ++j)
|
||||
{
|
||||
if (false == m_junctions[j].valid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ImVec2 sw;
|
||||
if (world_to_screen(plan_to_world(m_junctions[j].px, m_junctions[j].py), sw) && std::hypot(sw.x - mouse.x, sw.y - mouse.y) <= 9.0f)
|
||||
{
|
||||
clear_selection();
|
||||
m_dragging_cmp = false;
|
||||
m_dragging_draft = false;
|
||||
m_drag_junction = static_cast<int>(j);
|
||||
m_drawing = false;
|
||||
m_status = "przesuwanie rozjazdu";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int hit_niw{-1};
|
||||
int hit_str{-1};
|
||||
int hit_end{-1};
|
||||
@@ -916,7 +946,25 @@ void plan_panel::handle_scene()
|
||||
}
|
||||
}
|
||||
|
||||
if (m_dragging_draft)
|
||||
if (m_drag_junction >= 0)
|
||||
{
|
||||
if (ImGui::IsMouseDown(0) && static_cast<std::size_t>(m_drag_junction) < m_document.junctions.size())
|
||||
{
|
||||
auto &junction{m_document.junctions[static_cast<std::size_t>(m_drag_junction)]};
|
||||
double station{junction.station};
|
||||
// slide along the through track: the cursor's nearest station on its centreline
|
||||
if (junction.through >= 0 && station_on_through(static_cast<std::size_t>(junction.through), cursorx, cursory, station))
|
||||
{
|
||||
junction.station = station;
|
||||
solve();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_drag_junction = -1;
|
||||
}
|
||||
}
|
||||
else if (m_dragging_draft)
|
||||
{
|
||||
if (ImGui::IsMouseDown(0))
|
||||
{
|
||||
@@ -997,9 +1045,9 @@ void plan_panel::draw_on_scene()
|
||||
auto const display{ImGui::GetIO().DisplaySize};
|
||||
auto const halfheight{static_cast<double>(Global.editor_ortho_extent)};
|
||||
auto const halfwidth{halfheight * std::max(1.0f, display.x) / std::max(1.0f, display.y)};
|
||||
maj0sted::web::TileBBox const view{planx - halfwidth, plany - halfheight, planx + halfwidth, plany + halfheight};
|
||||
maj0sted::editor::TileBBox const view{planx - halfwidth, plany - halfheight, planx + halfwidth, plany + halfheight};
|
||||
|
||||
auto const draw_box = [&](maj0sted::web::TileBBox const &Box, unsigned int const Texture) {
|
||||
auto const draw_box = [&](maj0sted::editor::TileBBox const &Box, unsigned int const Texture) {
|
||||
ImVec2 corners[4];
|
||||
auto const ok = world_to_screen(plan_to_world(Box.min_x, Box.max_y), corners[0]) && world_to_screen(plan_to_world(Box.max_x, Box.max_y), corners[1]) &&
|
||||
world_to_screen(plan_to_world(Box.max_x, Box.min_y), corners[2]) && world_to_screen(plan_to_world(Box.min_x, Box.min_y), corners[3]);
|
||||
@@ -1114,7 +1162,7 @@ void plan_panel::draw_on_scene()
|
||||
}
|
||||
for (auto const gap : gaps)
|
||||
{
|
||||
std::map<int, maj0sted::web::WebPolyline const *> by_el;
|
||||
std::map<int, maj0sted::editor::PlanPolyline const *> by_el;
|
||||
for (auto const &poly : solved.polylines)
|
||||
{
|
||||
if (poly.gap != gap || poly.points.size() < 2 || poly.element_index < 0)
|
||||
@@ -1127,7 +1175,7 @@ void plan_panel::draw_on_scene()
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<maj0sted::web::WebPolyline const *> arcs;
|
||||
std::vector<maj0sted::editor::PlanPolyline const *> arcs;
|
||||
for (auto const &[idx, poly] : by_el)
|
||||
{
|
||||
(void)idx;
|
||||
@@ -1855,7 +1903,7 @@ void plan_panel::render_gaps()
|
||||
}
|
||||
}
|
||||
|
||||
void plan_panel::render_compound(maj0sted::web::GapFit &Fit, std::size_t const Gap, bool &Dirty)
|
||||
void plan_panel::render_compound(maj0sted::editor::GapFit &Fit, std::size_t const Gap, bool &Dirty)
|
||||
{
|
||||
ImGui::Indent();
|
||||
|
||||
@@ -2189,7 +2237,7 @@ void plan_panel::fit_compound_to_guides()
|
||||
{
|
||||
return std::numeric_limits<double>::infinity();
|
||||
}
|
||||
auto const solved{maj0sted::web::solve_project(trial.niwelety)};
|
||||
auto const solved{maj0sted::editor::solve_project(trial.niwelety)};
|
||||
if (m_niweleta >= solved.size())
|
||||
{
|
||||
return std::numeric_limits<double>::infinity();
|
||||
@@ -2288,13 +2336,21 @@ void plan_panel::render_switches()
|
||||
{
|
||||
m_sw_crossing = preset.crossing_n;
|
||||
m_sw_radius = preset.radius;
|
||||
m_sw_length = preset.length;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
// each single-arc template spans R*atan(1/n) between its tangent points; shown so its length is known
|
||||
// the template's catalogue length (PR->KR); custom switches with no template just span their curve
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("dl. ~%.1f m", m_sw_radius * std::atan(1.0 / std::max(1.0, m_sw_crossing)));
|
||||
if (m_sw_length > 0.0)
|
||||
{
|
||||
ImGui::TextDisabled("dl. %.3f m (Id-1)", m_sw_length);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("dl. ~%.1f m (sam luk)", m_sw_radius * std::atan(1.0 / std::max(1.0, m_sw_crossing)));
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(90.0f);
|
||||
ImGui::Combo("strona", &m_sw_side, "w lewo\0w prawo\0");
|
||||
@@ -2354,6 +2410,12 @@ void plan_panel::render_switches()
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.4f, 1.0f), "nie wpasowany");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(junction.side == 0 ? "L->P" : "P->L"))
|
||||
{
|
||||
junction.side = junction.side == 0 ? 1 : 0;
|
||||
solve();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("usuń"))
|
||||
{
|
||||
m_document.junctions.erase(m_document.junctions.begin() + static_cast<long>(i));
|
||||
@@ -2373,9 +2435,12 @@ void plan_panel::render_switches()
|
||||
junction.curve.mode = 3;
|
||||
if (junction.curve.arcs.empty())
|
||||
{
|
||||
// a basket needs at least two arcs; the last absorbs the rest of the turn
|
||||
junction.curve.arcs.push_back({junction.curve.radius > 0.0 ? junction.curve.radius * 2.0 : 600.0, 40.0, 0.0});
|
||||
junction.curve.arcs.push_back({junction.curve.radius > 0.0 ? junction.curve.radius : 300.0, 0.0, 0.0});
|
||||
// a basket needs at least two arcs; a gentler entry radius eases into the nominal
|
||||
// one, whose closing arc absorbs the rest of the turn. a short fixed length keeps
|
||||
// the whole curve inside the turnout rather than far past its end
|
||||
auto const nominal{junction.curve.radius > 0.0 ? junction.curve.radius : 300.0};
|
||||
junction.curve.arcs.push_back({nominal * 2.0, 6.0, 0.0});
|
||||
junction.curve.arcs.push_back({nominal, 0.0, 0.0});
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2480,52 +2545,26 @@ void plan_panel::add_switch_on(std::size_t const Through, double const Wx, doubl
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto const ¢re{m_solved[Through].centreline};
|
||||
if (centre.size() < 2)
|
||||
double best_station{0.0};
|
||||
if (false == station_on_through(Through, Wx, Wy, best_station))
|
||||
{
|
||||
m_status = "niweleta bez osi";
|
||||
return;
|
||||
}
|
||||
|
||||
// nearest station: walk the centreline, project the click onto each segment
|
||||
double best_station{0.0};
|
||||
double best_dist{std::numeric_limits<double>::max()};
|
||||
double travelled{0.0};
|
||||
for (std::size_t i = 0; i + 1 < centre.size(); ++i)
|
||||
{
|
||||
double const ax{centre[i].x};
|
||||
double const ay{centre[i].y};
|
||||
double const dx{centre[i + 1].x - ax};
|
||||
double const dy{centre[i + 1].y - ay};
|
||||
double const seg{std::hypot(dx, dy)};
|
||||
if (seg < 1e-9)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double const t{std::clamp(((Wx - ax) * dx + (Wy - ay) * dy) / (seg * seg), 0.0, 1.0)};
|
||||
double const px{ax + dx * t};
|
||||
double const py{ay + dy * t};
|
||||
double const d{std::hypot(Wx - px, Wy - py)};
|
||||
if (d < best_dist)
|
||||
{
|
||||
best_dist = d;
|
||||
best_station = travelled + seg * t;
|
||||
}
|
||||
travelled += seg;
|
||||
}
|
||||
|
||||
maj0sted::web::Junction junction;
|
||||
maj0sted::editor::Junction junction;
|
||||
junction.through = static_cast<int>(Through);
|
||||
junction.station = best_station;
|
||||
junction.side = m_sw_side;
|
||||
junction.facing = true;
|
||||
junction.crossing_n = m_sw_crossing;
|
||||
junction.length = m_sw_length; // catalogue length from the template, 0 for a custom switch
|
||||
junction.curve.mode = 1; // plain arc; the internal curve can be made compound later
|
||||
junction.curve.radius = m_sw_radius;
|
||||
junction.branch = static_cast<int>(m_document.niwelety.size());
|
||||
|
||||
// a branch niweleta to draw the diverging track into, pinned to the frog by the solver
|
||||
maj0sted::web::NiweletaSpec branch;
|
||||
maj0sted::editor::NiweletaSpec branch;
|
||||
branch.name = "odnoga " + std::to_string(m_document.niwelety.size() + 1);
|
||||
branch.straights.push_back({Wx, Wy, Wx + 10.0, Wy, false}); // placeholder, repositioned below
|
||||
m_document.niwelety.push_back(std::move(branch));
|
||||
@@ -2674,7 +2713,7 @@ void plan_panel::solve()
|
||||
}
|
||||
maj0sted::app::apply_straight_constraints(m_document.niwelety);
|
||||
// switches pin their branches, so the whole layout is solved together
|
||||
auto layout{maj0sted::web::solve_layout(m_document.niwelety, m_document.junctions)};
|
||||
auto layout{maj0sted::editor::solve_layout(m_document.niwelety, m_document.junctions)};
|
||||
m_solved = std::move(layout.niwelety);
|
||||
m_junctions = std::move(layout.junctions);
|
||||
}
|
||||
@@ -2686,12 +2725,12 @@ void plan_panel::apply_constraints()
|
||||
}
|
||||
|
||||
|
||||
maj0sted::web::NiweletaSpec *plan_panel::current_niweleta()
|
||||
maj0sted::editor::NiweletaSpec *plan_panel::current_niweleta()
|
||||
{
|
||||
return m_niweleta < m_document.niwelety.size() ? &m_document.niwelety[m_niweleta] : nullptr;
|
||||
}
|
||||
|
||||
maj0sted::web::NiweletaSpec const *plan_panel::current_niweleta() const
|
||||
maj0sted::editor::NiweletaSpec const *plan_panel::current_niweleta() const
|
||||
{
|
||||
return m_niweleta < m_document.niwelety.size() ? &m_document.niwelety[m_niweleta] : nullptr;
|
||||
}
|
||||
@@ -2721,7 +2760,7 @@ bool plan_panel::fit_applied(std::size_t const Gap) const
|
||||
return false;
|
||||
}
|
||||
|
||||
maj0sted::web::StraightSpec const *plan_panel::handle_straight(std::size_t const Niw, std::size_t const Index) const
|
||||
maj0sted::editor::StraightSpec const *plan_panel::handle_straight(std::size_t const Niw, std::size_t const Index) const
|
||||
{
|
||||
if (Niw >= m_document.niwelety.size())
|
||||
{
|
||||
@@ -2814,7 +2853,7 @@ void plan_panel::delete_selected_straight()
|
||||
niweleta->straights.erase(niweleta->straights.begin() + sel);
|
||||
|
||||
// gaps touching the removed straight disappear; later gaps shift down
|
||||
std::vector<maj0sted::web::GapFit> kept;
|
||||
std::vector<maj0sted::editor::GapFit> kept;
|
||||
kept.reserve(niweleta->fits.size());
|
||||
for (auto const &fit : niweleta->fits)
|
||||
{
|
||||
@@ -3394,6 +3433,82 @@ bool plan_panel::hit_endpoint(ImVec2 const &Mouse, int &OutNiw, int &OutStr, int
|
||||
return found;
|
||||
}
|
||||
|
||||
bool plan_panel::station_on_through(std::size_t const Through, double const Wx, double const Wy, double &OutStation) const
|
||||
{
|
||||
if (Through >= m_solved.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto const ¢re{m_solved[Through].centreline};
|
||||
if (centre.size() < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// walk the axis, project the point onto each segment, keep the nearest station (arc length)
|
||||
double best_station{0.0};
|
||||
double best_dist{std::numeric_limits<double>::max()};
|
||||
double travelled{0.0};
|
||||
for (std::size_t i = 0; i + 1 < centre.size(); ++i)
|
||||
{
|
||||
double const ax{centre[i].x};
|
||||
double const ay{centre[i].y};
|
||||
double const dx{centre[i + 1].x - ax};
|
||||
double const dy{centre[i + 1].y - ay};
|
||||
double const seg{std::hypot(dx, dy)};
|
||||
if (seg < 1e-9)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double const t{std::clamp(((Wx - ax) * dx + (Wy - ay) * dy) / (seg * seg), 0.0, 1.0)};
|
||||
double const d{std::hypot(Wx - (ax + dx * t), Wy - (ay + dy * t))};
|
||||
if (d < best_dist)
|
||||
{
|
||||
best_dist = d;
|
||||
best_station = travelled + seg * t;
|
||||
}
|
||||
travelled += seg;
|
||||
}
|
||||
OutStation = best_station;
|
||||
return true;
|
||||
}
|
||||
|
||||
int plan_panel::nearest_niweleta_axis(ImVec2 const &Mouse, float const Tolerance) const
|
||||
{
|
||||
auto best{Tolerance};
|
||||
int found{-1};
|
||||
for (std::size_t n = 0; n < m_solved.size(); ++n)
|
||||
{
|
||||
for (auto const &polyline : m_solved[n].polylines)
|
||||
{
|
||||
for (std::size_t i = 0; i + 1 < polyline.points.size(); ++i)
|
||||
{
|
||||
ImVec2 a;
|
||||
ImVec2 b;
|
||||
if (false == world_to_screen(plan_to_world(polyline.points[i].x, polyline.points[i].y), a) ||
|
||||
false == world_to_screen(plan_to_world(polyline.points[i + 1].x, polyline.points[i + 1].y), b))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto const dx{b.x - a.x};
|
||||
auto const dy{b.y - a.y};
|
||||
auto const len2{dx * dx + dy * dy};
|
||||
if (len2 <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto const t{std::clamp(((Mouse.x - a.x) * dx + (Mouse.y - a.y) * dy) / len2, 0.0f, 1.0f)};
|
||||
auto const d{std::hypot(Mouse.x - (a.x + t * dx), Mouse.y - (a.y + t * dy))};
|
||||
if (d <= best)
|
||||
{
|
||||
best = d;
|
||||
found = static_cast<int>(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
bool plan_panel::hit_straight(ImVec2 const &Mouse, int &OutNiw, int &OutStr, bool const AllNiwelety) const
|
||||
{
|
||||
auto best{10.0f};
|
||||
|
||||
@@ -16,8 +16,8 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "maj0sted/app/editor_document.hpp"
|
||||
#include "maj0sted/app/straight_constraints.hpp"
|
||||
#include "maj0sted/io/scn_export.hpp"
|
||||
#include "maj0sted/web/editor.hpp"
|
||||
#include "maj0sted/web/ribbon.hpp"
|
||||
#include "maj0sted/editor/editor.hpp"
|
||||
#include "maj0sted/editor/ribbon.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -39,7 +39,7 @@ class plan_panel : public ui_panel
|
||||
void render_toolbar();
|
||||
void render_draft();
|
||||
void render_gaps();
|
||||
void render_compound(maj0sted::web::GapFit &Fit, std::size_t const Gap, bool &Dirty);
|
||||
void render_compound(maj0sted::editor::GapFit &Fit, std::size_t const Gap, bool &Dirty);
|
||||
void fit_compound_to_guides();
|
||||
void clear_fit_guides();
|
||||
void render_storage();
|
||||
@@ -51,12 +51,12 @@ class plan_panel : public ui_panel
|
||||
void draw_on_scene();
|
||||
void solve();
|
||||
void apply_constraints();
|
||||
maj0sted::web::NiweletaSpec *current_niweleta();
|
||||
maj0sted::web::NiweletaSpec const *current_niweleta() const;
|
||||
maj0sted::editor::NiweletaSpec *current_niweleta();
|
||||
maj0sted::editor::NiweletaSpec const *current_niweleta() const;
|
||||
void align_fits();
|
||||
bool fit_applied(std::size_t const Gap) const;
|
||||
// independent anchored straights: each endpoint is its own handle
|
||||
maj0sted::web::StraightSpec const *handle_straight(std::size_t const Niw, std::size_t const Index) const;
|
||||
maj0sted::editor::StraightSpec const *handle_straight(std::size_t const Niw, std::size_t const Index) const;
|
||||
void append_vertex(double const X, double const Y);
|
||||
void drop_last_vertex();
|
||||
void delete_selected_straight();
|
||||
@@ -65,6 +65,10 @@ class plan_panel : public ui_panel
|
||||
// hit tests (screen pixels). niw=-1 means "current niweleta only"
|
||||
bool hit_endpoint(ImVec2 const &Mouse, int &OutNiw, int &OutStr, int &OutEnd) const;
|
||||
bool hit_straight(ImVec2 const &Mouse, int &OutNiw, int &OutStr, bool const AllNiwelety) const;
|
||||
// nearest niweleta by its solved axis (curves included), for a forgiving double-click select
|
||||
int nearest_niweleta_axis(ImVec2 const &Mouse, float const Tolerance) const;
|
||||
// nearest station (arc length) on a through niweleta's axis to a plan point; for placing/moving switches
|
||||
bool station_on_through(std::size_t const Through, double const Wx, double const Wy, double &OutStation) const;
|
||||
// compound basket: draggable ends of entry/exit KP, free arcs and between-KP
|
||||
bool hit_compound_station(ImVec2 const &Mouse, int &OutGap, int &OutKind, int &OutArc) const;
|
||||
void drag_compound_station(double const X, double const Y);
|
||||
@@ -90,8 +94,8 @@ class plan_panel : public ui_panel
|
||||
|
||||
// members
|
||||
maj0sted::app::EditorDocument m_document;
|
||||
std::vector<maj0sted::web::NiweletaPolys> m_solved;
|
||||
std::vector<maj0sted::web::JunctionGeom> m_junctions;
|
||||
std::vector<maj0sted::editor::NiweletaPolys> m_solved;
|
||||
std::vector<maj0sted::editor::JunctionGeom> m_junctions;
|
||||
std::size_t m_niweleta{0};
|
||||
|
||||
// switch placement: 0 idle, 1 waiting for a click on the through track
|
||||
@@ -99,6 +103,8 @@ class plan_panel : public ui_panel
|
||||
int m_sw_side{0}; // 0 left, 1 right
|
||||
double m_sw_crossing{9.0}; // skos 1:n
|
||||
double m_sw_radius{190.0}; // internal arc radius (metres)
|
||||
double m_sw_length{0.0}; // catalogue length from the chosen template (0 = just the curve)
|
||||
int m_drag_junction{-1}; // index of the switch being slid along its through track, or -1
|
||||
|
||||
int m_sel_straight{-1};
|
||||
int m_sel_end{-1};
|
||||
@@ -136,7 +142,7 @@ class plan_panel : public ui_panel
|
||||
std::vector<FitGuide> m_fit_guides;
|
||||
|
||||
maj0sted::app::BasketDraft m_draft;
|
||||
std::vector<maj0sted::web::WebPolyline> m_draft_polys;
|
||||
std::vector<maj0sted::editor::PlanPolyline> m_draft_polys;
|
||||
// 0 idle, 1 pick straight end to continue from
|
||||
int m_draft_place{0};
|
||||
// next click appends: 0 = luk, 1 = KP (wej/między), 2 = KP wyjście (kończy: prosta + kotwica)
|
||||
@@ -162,9 +168,9 @@ class plan_panel : public ui_panel
|
||||
double m_mapviewy{0.0};
|
||||
double m_mapscale{0.0};
|
||||
|
||||
editor::wms_image m_topomap{maj0sted::web::WmsConfig::geoportal_topo()};
|
||||
editor::wms_image m_topomap{maj0sted::editor::WmsConfig::geoportal_topo()};
|
||||
editor::orthophoto_source m_ortho;
|
||||
// 1 km topo cells for views wider than a kilometre (orto's 100 m grid is too dense there)
|
||||
editor::orthophoto_source m_topo{maj0sted::web::WmsConfig::geoportal_topo(), 1000, "topo"};
|
||||
editor::orthophoto_source m_topo{maj0sted::editor::WmsConfig::geoportal_topo(), 1000, "topo"};
|
||||
bool m_showortho{true};
|
||||
};
|
||||
|
||||
@@ -53,12 +53,17 @@ bool scenarioloader_mode::update() {
|
||||
}
|
||||
catch (invalid_scenery_exception &e) {
|
||||
ErrorLog( "Bad init: scenario loading failed" );
|
||||
state.reset();
|
||||
Application.pop_mode();
|
||||
}
|
||||
|
||||
WriteLog( "Scenario loading time: " + std::to_string( std::chrono::duration_cast<std::chrono::seconds>( std::chrono::system_clock::now() - timestart ).count() ) + " seconds" );
|
||||
// TODO: implement and use next mode cue
|
||||
|
||||
// the mode object is kept for the next time a scenery is loaded; without dropping the context
|
||||
// here, a later enter() would skip deserialize_begin and finish an already-spent parser again
|
||||
state.reset();
|
||||
|
||||
Application.pop_mode();
|
||||
// starting into the editor the simulation never runs at all: there's no driver mode underneath, so
|
||||
// no physics, no AI and no timetables - just the loaded world to work on
|
||||
@@ -79,6 +84,11 @@ void scenarioloader_mode::enter() {
|
||||
// TBD: hide cursor in fullscreen mode?
|
||||
Application.set_cursor( GLFW_CURSOR_NORMAL );
|
||||
|
||||
// a new map from the editor re-enters this mode while the previous load's context is still
|
||||
// sitting in `state`; clear it so update() opens the file again instead of re-finishing an
|
||||
// exhausted parser (Groups.close / create_map_geometry on an already-built region)
|
||||
state.reset();
|
||||
|
||||
simulation::is_ready = false;
|
||||
|
||||
Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" );
|
||||
|
||||
Reference in New Issue
Block a user