From 4bef28cf945b9e5faa599c02f9b49a2188b1494d Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 31 Jul 2026 19:00:35 +0200 Subject: [PATCH] Add a track layout drawing board to the editor A "Plan" panel, opened from Track layout in the editor's menu bar, draws a niweleta the way the web frontend did: click to lay chained straights, drag a corner to move it, and pick a fit for each gap between them. The geometry is entirely maj0sted's - the panel gathers the input, hands the document to solve_project and paints the rails that come back, coloured by whether they came from a straight, an arc or a clothoid. Scenery nodes are untouched. A fit the library cannot produce is simply absent from its answer, so the gap is labelled as not fitting rather than left looking as though its parameters took effect. Save/load go through the library's own document format, so a project written here opens in the same state. Also translates the comments added with -editor into English, matching the files they sit in. --- CMakeLists.txt | 1 + application/application.cpp | 4 +- application/editormode.cpp | 17 +- application/editoruilayer.cpp | 12 + application/editoruilayer.h | 5 + application/planpanel.cpp | 585 +++++++++++++++++++++++++++++ application/planpanel.h | 69 ++++ application/scenarioloadermode.cpp | 4 +- 8 files changed, 684 insertions(+), 13 deletions(-) create mode 100644 application/planpanel.cpp create mode 100644 application/planpanel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 06f5f6e4..e420d41a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,6 +173,7 @@ set(SOURCES "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" +"application/planpanel.cpp" "application/scenarioloadermode.cpp" "application/scenarioloaderuilayer.cpp" "scene/scenenodegroups.cpp" diff --git a/application/application.cpp b/application/application.cpp index 72fdda0a..156bb04a 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -1155,8 +1155,8 @@ int eu07_application::init_settings(int Argc, char *Argv[]) if (Global.editor_startup && Global.SceneryFile.empty()) { - // edytor potrzebuje wczytanego świata, ale niekoniecznie czegokolwiek w nim - pusta sceneria daje - // sam region, oświetlenie i zegar, bez taboru i rozkładów + // the editor needs a loaded world, but not necessarily anything in it - an empty scenery gives + // just the region, the lighting and the clock, with no rolling stock and no timetables Global.SceneryFile = "pusta.scn"; } diff --git a/application/editormode.cpp b/application/editormode.cpp index 01a17aca..8358ce11 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1349,16 +1349,16 @@ void editor_mode::enter() { if (Global.editor_startup) { - // kamerę ze scenerii ustawia tryb prowadzenia, a przy starcie prosto do edytora on w ogóle nie - // wstaje - bez tego kopiujemy niżej pustą kamerę globalną i widok jest czarny + // the scenery camera is set up by the driver mode, which never runs when we start straight in + // the editor; without this we'd copy an untouched global camera below and render a black view auto const scenerycamera = Global.FreeCameraInit[0] != glm::dvec3(0.0); - // sceneria nie musi podawać kamery; wtedy zostaje domyślny widok edytora, znad punktu zerowego + // a scenery doesn't have to name a camera; fall back on the editor's own view above the origin Camera.Init(scenerycamera ? Global.FreeCameraInit[0] : glm::dvec3{0.0, 15.0, 0.0}, scenerycamera ? Global.FreeCameraInitAngle[0] : glm::vec3{glm::radians(-30.0f), glm::radians(180.0f), 0.0f}, nullptr); Global.pCamera = Camera; Global.pDebugCamera = Camera; FreeFlyModeFlag = true; - Timer::ResetTimers(); // czas wczytywania scenerii nie ma trafić w deltę pierwszej klatki + Timer::ResetTimers(); // keep the scenery loading time out of the first frame's delta } m_statebackup = {Global.pCamera, FreeFlyModeFlag, Global.ControlPicking}; @@ -1377,9 +1377,8 @@ void editor_mode::enter() Camera.LookAt = vehicle->GetPosition(); Camera.RaLook(); // single camera reposition } - // edytor pracuje wyłącznie kamerą swobodną. Przy starcie prosto do edytora nie ma pojazdu, z - // którego można by wysiąść, a flagę i tak trzeba podnieść - inaczej kamera zostaje w trybie - // kabinowym, nie mając kabiny + // the editor always works with a free camera. starting straight in it there's no vehicle to + // step out of, but the flag still has to go up or the camera stays in cab mode without a cab FreeFlyModeFlag = true; } @@ -1472,8 +1471,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in if (!Global.ctrlState && !Global.shiftState) { - // przy starcie do edytora nie ma trybu, do którego można wrócić - zdjęcie ostatniego trybu - // zostawiłoby pusty stos, z którego reszta klatki nadal by czytała + // when started straight in the editor there's no mode to fall back to, and popping the last + // one would leave the rest of the frame reading an empty stack if (Global.editor_startup) Application.queue_quit(true); else diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index d87dd8fa..469a844c 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -25,6 +25,7 @@ editor_ui::editor_ui() add_external_panel(&m_nodebankpanel); add_external_panel(&m_functionspanel); add_external_panel(&m_brushobjects); + add_external_panel(&m_planpanel); } // updates state of UI elements @@ -49,6 +50,17 @@ void editor_ui::update() m_brushobjects.update(*ptr); } +void editor_ui::render_menu_contents() +{ + ui_layer::render_menu_contents(); + + if (ImGui::BeginMenu("Track layout")) + { + ImGui::MenuItem(m_planpanel.name().c_str(), nullptr, &m_planpanel.is_open); + ImGui::EndMenu(); + } +} + void editor_ui::toggleBrushSettings(bool isVisible) { if (m_brushobjects.is_open != isVisible) diff --git a/application/editoruilayer.h b/application/editoruilayer.h index ad3d5447..8033deff 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/uilayer.h" #include "application/editoruipanels.h" +#include "application/planpanel.h" namespace scene { @@ -38,11 +39,15 @@ class editor_ui : public ui_layer float getSpacing(); void toggleBrushSettings(bool isVisible); + protected: + void render_menu_contents() override; + private: // members itemproperties_panel m_itempropertiespanel{"Node Properties", true}; functions_panel m_functionspanel{"Functions", true}; nodebank_panel m_nodebankpanel{"Node Bank", true}; brush_object_list m_brushobjects{"Brush properties", false}; + plan_panel m_planpanel{"Plan", false}; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any }; diff --git a/application/planpanel.cpp b/application/planpanel.cpp new file mode 100644 index 00000000..bb104c1f --- /dev/null +++ b/application/planpanel.cpp @@ -0,0 +1,585 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/planpanel.h" + +#include "utilities/Globals.h" + +namespace +{ + +// screen placement of the canvas together with the plan point it is centred on; rebuilt every frame +// from the panel's view state, so the transform never goes stale against a resized window +struct canvas_view +{ + ImVec2 origin{0.f, 0.f}; + ImVec2 size{0.f, 0.f}; + double viewx{0.0}; + double viewy{0.0}; + double scale{1.0}; + + ImVec2 centre() const { return {origin.x + size.x * 0.5f, origin.y + size.y * 0.5f}; } + + // northing grows towards the top of the screen, so the vertical axis is flipped + ImVec2 to_screen(double const X, double const Y) const + { + auto const middle{centre()}; + return {middle.x + static_cast((X - viewx) * scale), middle.y - static_cast((Y - viewy) * scale)}; + } + + void to_plan(ImVec2 const &Point, double &X, double &Y) const + { + auto const middle{centre()}; + X = viewx + (Point.x - middle.x) / scale; + Y = viewy - (Point.y - middle.y) / scale; + } +}; + +// grid spacing is picked so the cells stay roughly the same size on screen whatever the zoom, and +// always lands on a round number of metres a surveyor would recognise +double grid_step(double const Scale) +{ + auto const target{80.0 / std::max(Scale, 1e-6)}; // metres per cell at the wanted screen size + auto const magnitude{std::pow(10.0, std::floor(std::log10(std::max(target, 1e-6))))}; + auto const steps = {1.0, 2.0, 5.0, 10.0}; + for (auto const step : steps) + { + if (magnitude * step >= target) + { + return magnitude * step; + } + } + return magnitude * 10.0; +} + +ImU32 element_colour(int const Kind, bool const Active) +{ + auto const alpha{Active ? 255 : 90}; + switch (Kind) + { + case 1: // arc + return IM_COL32(255, 190, 90, alpha); + case 2: // transition curve + return IM_COL32(140, 245, 150, alpha); + default: // straight + return IM_COL32(125, 200, 255, alpha); + } +} + +} // namespace + +plan_panel::plan_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) +{ + size_min = {520, 460}; + size_max = {2000, 1600}; + m_document.niwelety.push_back({"niweleta 1", {}, {}}); + solve(); +} + +void plan_panel::render_contents() +{ + render_toolbar(); + render_canvas(); + render_gaps(); + render_storage(); +} + +void plan_panel::render_toolbar() +{ + auto *niweleta{current_niweleta()}; + + if (ImGui::Button("New niweleta")) + { + m_document.niwelety.push_back({"niweleta " + std::to_string(m_document.niwelety.size() + 1), {}, {}}); + m_niweleta = m_document.niwelety.size() - 1; + m_pending = false; + solve(); + } + ImGui::SameLine(); + if (ImGui::Button("Undo point")) + { + drop_last_vertex(); + } + ImGui::SameLine(); + if (ImGui::Button("Frame all")) + { + frame_all(); + } + ImGui::SameLine(); + ImGui::Checkbox("Draw", &m_drawing); + + if (m_document.niwelety.size() > 1) + { + auto const preview{m_document.niwelety[m_niweleta].name}; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::BeginCombo("Edited", preview.c_str())) + { + for (std::size_t i = 0; i < m_document.niwelety.size(); ++i) + { + if (ImGui::Selectable(m_document.niwelety[i].name.c_str(), i == m_niweleta)) + { + m_niweleta = i; + m_pending = false; + } + } + ImGui::EndCombo(); + } + } + + ImGui::Text("straights: %d, points: %d", niweleta != nullptr ? static_cast(niweleta->straights.size()) : 0, static_cast(vertex_count())); + ImGui::Separator(); +} + +void plan_panel::render_canvas() +{ + auto const available{ImGui::GetContentRegionAvail()}; + // the tables below the canvas need room of their own, but the canvas takes whatever is left + ImVec2 const canvassize{std::max(available.x, 240.0f), std::max(available.y - 210.0f, 200.0f)}; + + canvas_view view; + view.origin = ImGui::GetCursorScreenPos(); + view.size = canvassize; + view.viewx = m_viewx; + view.viewy = m_viewy; + view.scale = m_scale; + + ImGui::InvisibleButton("plan_canvas", canvassize); + auto const hovered{ImGui::IsItemHovered()}; + auto const mouse{ImGui::GetIO().MousePos}; + + double mousex{0.0}; + double mousey{0.0}; + view.to_plan(mouse, mousex, mousey); + + // zoom around the cursor, so the ground under it stays put + if (hovered && ImGui::GetIO().MouseWheel != 0.0f) + { + m_scale = std::clamp(m_scale * std::pow(1.15, ImGui::GetIO().MouseWheel), 0.002, 40.0); + view.scale = m_scale; + auto const middle{view.centre()}; + m_viewx = mousex - (mouse.x - middle.x) / m_scale; + m_viewy = mousey + (mouse.y - middle.y) / m_scale; + view.viewx = m_viewx; + view.viewy = m_viewy; + } + + // panning latches on, so the view keeps following the cursor once it leaves the canvas + if (hovered && ImGui::IsMouseDragging(1)) + { + m_panning = true; + } + if (m_panning) + { + if (false == ImGui::IsMouseDragging(1)) + { + m_panning = false; + } + else + { + auto const delta{ImGui::GetIO().MouseDelta}; + m_viewx -= delta.x / m_scale; + m_viewy += delta.y / m_scale; + view.viewx = m_viewx; + view.viewy = m_viewy; + } + } + + // grab an existing point before laying a new one, so corners can be nudged into place + if (hovered && ImGui::IsMouseClicked(0)) + { + m_draggedvertex = -1; + for (std::size_t i = 0; i < vertex_count(); ++i) + { + double x{0.0}; + double y{0.0}; + vertex_position(i, x, y); + auto const point{view.to_screen(x, y)}; + if (std::abs(point.x - mouse.x) <= 6.0f && std::abs(point.y - mouse.y) <= 6.0f) + { + m_draggedvertex = static_cast(i); + break; + } + } + if (m_draggedvertex == -1 && m_drawing) + { + append_vertex(mousex, mousey); + } + } + if (m_draggedvertex != -1) + { + if (ImGui::IsMouseDown(0)) + { + move_vertex(static_cast(m_draggedvertex), mousex, mousey); + } + else + { + m_draggedvertex = -1; + } + } + + auto *drawlist{ImGui::GetWindowDrawList()}; + auto const canvasend{ImVec2(view.origin.x + canvassize.x, view.origin.y + canvassize.y)}; + drawlist->PushClipRect(view.origin, canvasend, true); + drawlist->AddRectFilled(view.origin, canvasend, IM_COL32(24, 26, 30, 255)); + + // grid, with the project origin picked out + auto const step{grid_step(m_scale)}; + double left{0.0}; + double top{0.0}; + double right{0.0}; + double bottom{0.0}; + view.to_plan(view.origin, left, top); + view.to_plan(canvasend, right, bottom); + for (auto x = std::floor(left / step) * step; x <= right; x += step) + { + auto const colour{std::abs(x) < step * 0.5 ? IM_COL32(90, 90, 110, 255) : IM_COL32(48, 50, 58, 255)}; + drawlist->AddLine(view.to_screen(x, top), view.to_screen(x, bottom), colour); + } + for (auto y = std::floor(bottom / step) * step; y <= top; y += step) + { + auto const colour{std::abs(y) < step * 0.5 ? IM_COL32(90, 90, 110, 255) : IM_COL32(48, 50, 58, 255)}; + drawlist->AddLine(view.to_screen(left, y), view.to_screen(right, y), colour); + } + + // solved geometry: two rails per centreline, coloured by the kind of element they came from + std::vector points; + for (std::size_t n = 0; n < m_solved.size(); ++n) + { + auto const active{n == m_niweleta}; + for (auto const &polyline : m_solved[n].polylines) + { + if (polyline.points.size() < 2) + { + continue; + } + points.clear(); + points.reserve(polyline.points.size()); + for (auto const &point : polyline.points) + { + points.push_back(view.to_screen(point.x, point.y)); + } + drawlist->AddPolyline(points.data(), static_cast(points.size()), element_colour(polyline.kind, active), false, active ? 2.0f : 1.0f); + } + } + + // editable points of the niweleta being drawn, plus the corner still waiting for its partner + for (std::size_t i = 0; i < vertex_count(); ++i) + { + double x{0.0}; + double y{0.0}; + vertex_position(i, x, y); + auto const point{view.to_screen(x, y)}; + drawlist->AddCircleFilled(point, 4.0f, static_cast(i) == m_draggedvertex ? IM_COL32(255, 240, 120, 255) : IM_COL32(235, 235, 235, 255)); + } + if (m_pending) + { + auto const point{view.to_screen(m_pendingx, m_pendingy)}; + drawlist->AddCircleFilled(point, 4.0f, IM_COL32(255, 240, 120, 255)); + if (hovered) + { + drawlist->AddLine(point, mouse, IM_COL32(255, 240, 120, 160)); + } + } + else if (hovered && m_drawing && m_draggedvertex == -1 && vertex_count() > 0) + { + double x{0.0}; + double y{0.0}; + vertex_position(vertex_count() - 1, x, y); + drawlist->AddLine(view.to_screen(x, y), mouse, IM_COL32(255, 240, 120, 160)); + } + + drawlist->PopClipRect(); + + ImGui::Text("cursor: %.1f, %.1f m grid: %g m scale: %.3g px/m", mousex, mousey, step, m_scale); + ImGui::TextDisabled("left click lays a point, drag a point to move it, right drag pans, wheel zooms"); +} + +void plan_panel::render_gaps() +{ + auto *niweleta{current_niweleta()}; + if (niweleta == nullptr || niweleta->straights.size() < 2) + { + return; + } + + ImGui::Separator(); + if (false == ImGui::CollapsingHeader("Fits", ImGuiTreeNodeFlags_DefaultOpen)) + { + return; + } + + ImGui::BeginChild("plan_gaps", ImVec2(0.0f, 120.0f), false); + for (std::size_t gap = 0; gap < niweleta->fits.size(); ++gap) + { + auto &fit{niweleta->fits[gap]}; + ImGui::PushID(static_cast(gap)); + + ImGui::Text("gap %d", static_cast(gap)); + ImGui::SameLine(70.0f); + ImGui::SetNextItemWidth(150.0f); + if (ImGui::Combo("##mode", &fit.mode, "none\0arc\0arc + clothoids\0")) + { + if (fit.mode != 0 && fit.radius <= 0.0) + { + fit.radius = 300.0; // something drawable to start from, rather than a rejected fit + } + solve(); + } + if (fit.mode != 0) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputDouble("R", &fit.radius, 10.0, 100.0, "%.1f")) + { + solve(); + } + } + if (fit.mode == 2) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputDouble("L", &fit.transition, 5.0, 20.0, "%.1f")) + { + solve(); + } + } + // a fit the library could not produce is simply absent from its answer; say so rather than + // leaving the parameters looking as though they took effect + if (fit.mode != 0 && false == fit_applied(gap)) + { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.4f, 1.0f), "does not fit"); + } + + ImGui::PopID(); + } + ImGui::EndChild(); +} + +void plan_panel::render_storage() +{ + ImGui::Separator(); + ImGui::SetNextItemWidth(280.0f); + ImGui::InputText("##path", m_path, sizeof(m_path)); + ImGui::SameLine(); + if (ImGui::Button("Save")) + { + m_status = maj0sted::app::save_project(m_document, m_path) ? "saved to " + std::string(m_path) : "could not write " + std::string(m_path); + } + ImGui::SameLine(); + if (ImGui::Button("Load")) + { + auto const loaded{maj0sted::app::load_project(m_path)}; + if (loaded) + { + m_document = *loaded; + if (m_document.niwelety.empty()) + { + m_document.niwelety.push_back({"niweleta 1", {}, {}}); + } + m_niweleta = 0; + m_pending = false; + m_draggedvertex = -1; + solve(); + frame_all(); + m_status = "loaded " + std::string(m_path); + } + else + { + m_status = "could not read " + std::string(m_path); + } + } + if (false == m_status.empty()) + { + ImGui::TextDisabled("%s", m_status.c_str()); + } +} + +void plan_panel::solve() +{ + align_fits(); + m_solved = maj0sted::web::solve_project(m_document.niwelety); +} + +maj0sted::web::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 +{ + return m_niweleta < m_document.niwelety.size() ? &m_document.niwelety[m_niweleta] : nullptr; +} + +void plan_panel::align_fits() +{ + for (auto &niweleta : m_document.niwelety) + { + auto const gaps{niweleta.straights.empty() ? std::size_t{0} : niweleta.straights.size() - 1}; + std::vector slots(gaps); + for (std::size_t gap = 0; gap < gaps; ++gap) + { + slots[gap].gap = static_cast(gap); + } + // a document read from disk carries only the gaps that were actually fitted + for (auto const &fit : niweleta.fits) + { + if (fit.gap >= 0 && static_cast(fit.gap) < gaps) + { + slots[fit.gap] = fit; + } + } + niweleta.fits = std::move(slots); + } +} + +bool plan_panel::fit_applied(std::size_t const Gap) const +{ + if (m_niweleta >= m_solved.size()) + { + return false; + } + for (auto const &fit : m_solved[m_niweleta].applied_fits) + { + if (fit.gap >= 0 && static_cast(fit.gap) == Gap) + { + return true; + } + } + return false; +} + +std::size_t plan_panel::vertex_count() const +{ + auto const *niweleta{current_niweleta()}; + return (niweleta == nullptr || niweleta->straights.empty()) ? 0 : niweleta->straights.size() + 1; +} + +void plan_panel::vertex_position(std::size_t const Index, double &X, double &Y) const +{ + auto const *niweleta{current_niweleta()}; + if (niweleta == nullptr || niweleta->straights.empty() || Index >= niweleta->straights.size() + 1) + { + return; + } + if (Index == 0) + { + X = niweleta->straights.front().x1; + Y = niweleta->straights.front().y1; + return; + } + X = niweleta->straights[Index - 1].x2; + Y = niweleta->straights[Index - 1].y2; +} + +void plan_panel::append_vertex(double const X, double const Y) +{ + auto *niweleta{current_niweleta()}; + if (niweleta == nullptr) + { + return; + } + + if (niweleta->straights.empty()) + { + if (false == m_pending) + { + // nothing to attach to yet, so the first corner waits for the one that closes the straight + m_pending = true; + m_pendingx = X; + m_pendingy = Y; + return; + } + m_pending = false; + niweleta->straights.push_back({m_pendingx, m_pendingy, X, Y, false}); + } + else + { + auto const &last{niweleta->straights.back()}; + niweleta->straights.push_back({last.x2, last.y2, X, Y, false}); + } + + solve(); +} + +void plan_panel::move_vertex(std::size_t const Index, double const X, double const Y) +{ + auto *niweleta{current_niweleta()}; + if (niweleta == nullptr || Index >= niweleta->straights.size() + 1) + { + return; + } + + // the point belongs to both straights meeting at it, so both ends follow it + if (Index > 0) + { + niweleta->straights[Index - 1].x2 = X; + niweleta->straights[Index - 1].y2 = Y; + } + if (Index < niweleta->straights.size()) + { + niweleta->straights[Index].x1 = X; + niweleta->straights[Index].y1 = Y; + } + + solve(); +} + +void plan_panel::drop_last_vertex() +{ + if (m_pending) + { + m_pending = false; + return; + } + + auto *niweleta{current_niweleta()}; + if (niweleta == nullptr || niweleta->straights.empty()) + { + return; + } + + niweleta->straights.pop_back(); + solve(); +} + +void plan_panel::frame_all() +{ + auto minx{std::numeric_limits::max()}; + auto miny{std::numeric_limits::max()}; + auto maxx{std::numeric_limits::lowest()}; + auto maxy{std::numeric_limits::lowest()}; + + for (auto const &niweleta : m_document.niwelety) + { + for (auto const &straight : niweleta.straights) + { + minx = std::min({minx, straight.x1, straight.x2}); + maxx = std::max({maxx, straight.x1, straight.x2}); + miny = std::min({miny, straight.y1, straight.y2}); + maxy = std::max({maxy, straight.y1, straight.y2}); + } + } + + if (minx > maxx) + { + // nothing drawn yet: sit on the project origin at a scale that shows a few hundred metres + m_viewx = 0.0; + m_viewy = 0.0; + m_scale = 2.0; + return; + } + + m_viewx = (minx + maxx) * 0.5; + m_viewy = (miny + maxy) * 0.5; + + auto const span{std::max({maxx - minx, maxy - miny, 50.0})}; + m_scale = std::clamp(400.0 / span, 0.002, 40.0); +} diff --git a/application/planpanel.h b/application/planpanel.h new file mode 100644 index 00000000..4a40f63c --- /dev/null +++ b/application/planpanel.h @@ -0,0 +1,69 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/uilayer.h" + +#include "maj0sted/app/editor_document.hpp" +#include "maj0sted/web/editor.hpp" + +// drawing board for track layout: straights laid down by hand and the curves fitted into the gaps +// between them, in the project's own cartesian frame (EPSG:2180, metres). none of the geometry is +// computed here - the panel collects the input, hands the whole document to maj0sted and paints +// what comes back. scenery nodes are not touched. +class plan_panel : public ui_panel +{ + + public: + plan_panel(std::string const &Name, bool const Isopen); + + void render_contents() override; + + private: + // methods + void render_toolbar(); + void render_canvas(); + void render_gaps(); + void render_storage(); + // re-runs the solver over the whole document; cheap enough to do on every edit + void solve(); + maj0sted::web::NiweletaSpec *current_niweleta(); + maj0sted::web::NiweletaSpec const *current_niweleta() const; + // keeps exactly one fit slot per gap, so a gap's parameters survive edits to its neighbours + void align_fits(); + // true when the solver actually produced the fit requested for the given gap + bool fit_applied(std::size_t const Gap) const; + // the panel edits a chained polyline, so a vertex is shared by the two straights meeting at it. + // the library allows fully independent straights; that freedom just isn't exposed here yet + std::size_t vertex_count() const; + void vertex_position(std::size_t const Index, double &X, double &Y) const; + void append_vertex(double const X, double const Y); + void move_vertex(std::size_t const Index, double const X, double const Y); + void drop_last_vertex(); + // centres the view on the drawing, or on a default patch of ground when nothing is drawn yet + void frame_all(); + // members + maj0sted::app::EditorDocument m_document; + std::vector m_solved; + std::size_t m_niweleta{0}; // niweleta being drawn into + int m_draggedvertex{-1}; + bool m_drawing{true}; // whether a click on the canvas lays track or only picks vertices + bool m_panning{false}; + // first corner of a niweleta has nothing to attach to, so it waits for the second click + bool m_pending{false}; + double m_pendingx{0.0}; + double m_pendingy{0.0}; + // view: the plan point sitting at the centre of the canvas, and the zoom in pixels per metre + double m_viewx{0.0}; + double m_viewy{0.0}; + double m_scale{2.0}; + std::string m_status; + char m_path[256]{"editor/plan.m0s"}; +}; diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index 34c91652..4d554cf9 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -60,8 +60,8 @@ bool scenarioloader_mode::update() { // TODO: implement and use next mode cue Application.pop_mode(); - // przy starcie do edytora symulacja w ogóle nie rusza: nie ma trybu prowadzenia pod spodem, więc nie - // liczy się fizyki, AI ani rozkładów - zostaje sam wczytany świat do obrabiania + // 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 Application.push_mode( Global.editor_startup ? eu07_application::mode::editor :