mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-18 00:59:18 +02:00
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.
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
585
application/planpanel.cpp
Normal file
585
application/planpanel.cpp
Normal file
@@ -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<float>((X - viewx) * scale), middle.y - static_cast<float>((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<int>(niweleta->straights.size()) : 0, static_cast<int>(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<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (m_draggedvertex == -1 && m_drawing)
|
||||
{
|
||||
append_vertex(mousex, mousey);
|
||||
}
|
||||
}
|
||||
if (m_draggedvertex != -1)
|
||||
{
|
||||
if (ImGui::IsMouseDown(0))
|
||||
{
|
||||
move_vertex(static_cast<std::size_t>(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<ImVec2> 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<int>(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<int>(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<int>(gap));
|
||||
|
||||
ImGui::Text("gap %d", static_cast<int>(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<maj0sted::web::GapFit> slots(gaps);
|
||||
for (std::size_t gap = 0; gap < gaps; ++gap)
|
||||
{
|
||||
slots[gap].gap = static_cast<int>(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<std::size_t>(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<std::size_t>(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<double>::max()};
|
||||
auto miny{std::numeric_limits<double>::max()};
|
||||
auto maxx{std::numeric_limits<double>::lowest()};
|
||||
auto maxy{std::numeric_limits<double>::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);
|
||||
}
|
||||
69
application/planpanel.h
Normal file
69
application/planpanel.h
Normal file
@@ -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<maj0sted::web::NiweletaPolys> 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"};
|
||||
};
|
||||
@@ -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 :
|
||||
|
||||
Reference in New Issue
Block a user