17
0
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:
maj00r
2026-08-01 19:55:33 +02:00
committed by maj00r
parent d069743265
commit 707b4de2f4
41 changed files with 2224 additions and 471 deletions

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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 &centre{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 &centre{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};

View File

@@ -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};
};

View File

@@ -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 + ")" );