mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 15:39:20 +02:00
What the plan tool was missing was not geometry but the ordinary business of editing: taking a mistake back, dividing what is drawn, and seeing what the next click will do before making it. Cofanie. The document is plain data, so a step is a copy of it. What counts as one step is decided by watching the document - a fingerprint over the tracks, turnouts and catalogue - rather than by dressing every edit with a call: a frame that changes anything opens a step, and the step closes when the drawing settles. That is what makes a drag, or a number typed digit by digit, one step and not thirty, and nothing can be forgotten on the way in. Ctrl+Z / Ctrl+Y; the scene editor listens for the same keys, so it hands them over while the plan has the keyboard. Złącze i wycięcie. `insert_joint` divides whichever element a station lands in and leaves one chain: the ground does not move, the curvature at the new joint is the one that was always there, and the fit reads the chain the same way afterwards. It is refused on a straight, because two straights end to end are one straight and the skeleton will not read that, and under a turnout, which is one piece of geometry on one element. `cut_track` takes a piece out and does leave two tracks - a gap is the ground between them, not a hole in a chain. Przejście rozjazdowe. A turnout may now stand against another one: where it stands is then worked out from that one's frog, not authored, so moving either carries the other with it and a trapez stays shut. The frog has to land on the line leaving the other frog - not on the track's axis, which is 1,469 m further on for a 1:9 - so the station is settled by laying the turnout and walking it along until it does. Drawing is a tool now, not what a click happens to do: until Rysuj is on, a click picks what is under it. With it on, the cursor at the growing end shows the corner and the straight the next click would lay, worked out by the same fit_corner the click itself uses. A chain only grows at a loose end, and the first element of a track is put down by two clicks instead of landing on the plan's zero. Also here: the curvature strip under the panel, pikietaż in the rows and at the cursor, how far the laid axis runs from the clicks it was drawn from, the file format saying which frame and which units it is in (v9), plans chosen from the directory instead of typed, and editor/samples/demo.m0s - two parallel tracks and a trapez - with the generator that wrote it and a test that opens it and demands it lays clean.
4492 lines
146 KiB
C++
4492 lines
146 KiB
C++
module;
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <numbers>
|
|
#include <string>
|
|
#include <variant>
|
|
#include <vector>
|
|
#include "imgui/imgui.h"
|
|
#include <GLFW/glfw3.h>
|
|
#include "utilities/Globals_macros.h"
|
|
#if !defined(_WIN32)
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
module eu07.application.planpanel;
|
|
import eu07.editor.plan_layout;
|
|
import eu07.editor.plan_model;
|
|
import eu07.editor.plan_render;
|
|
import eu07.editor.plan_solution;
|
|
import eu07.editor.plan_tilecache;
|
|
import eu07.editor.plan_scn_export;
|
|
import eu07.editor.plan_join;
|
|
import eu07.editor.plan_sketch;
|
|
import eu07.editor.plan_document_io;
|
|
import eu07.glm;
|
|
import eu07.utilities.globals;
|
|
import eu07.utilities.utilities;
|
|
import eu07.rendering.renderer;
|
|
import eu07.application.editormode;
|
|
import eu07.editor.polandmap;
|
|
import eu07.editor.turnouts;
|
|
|
|
/*
|
|
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/.
|
|
*/
|
|
|
|
namespace
|
|
{
|
|
|
|
using editor::plan::Element;
|
|
using editor::plan::ElementId;
|
|
using editor::plan::Free;
|
|
using editor::plan::Kind;
|
|
using editor::plan::Parallel;
|
|
using editor::plan::TrackId;
|
|
using editor::plan::TurnoutId;
|
|
|
|
// the plan works in EPSG:2180, the scenery in the engine's world space around its own zero. the
|
|
// georeference says which point of the projection that zero stands for; a fictional scenery leaves
|
|
// it at the origin, so the two frames differ only in the naming of the axes - easting runs along
|
|
// world x, northing against world z
|
|
glm::dvec3 plan_to_world(double const X, double const Y)
|
|
{
|
|
return {X - Global.scenery_origin.x, 0.0, -(Y - Global.scenery_origin.y)};
|
|
}
|
|
|
|
// where this very program stands on disk. the editor and the simulator are one exe, so running the
|
|
// exported scenery is running ourselves again with the scenery named on the command line
|
|
std::string executable_path()
|
|
{
|
|
#if defined(_WIN32)
|
|
char buffer[MAX_PATH]{};
|
|
auto const length{GetModuleFileNameA(nullptr, buffer, MAX_PATH)};
|
|
return length > 0 ? std::string(buffer, length) : std::string{};
|
|
#else
|
|
char buffer[4096]{};
|
|
auto const length{::readlink("/proc/self/exe", buffer, sizeof(buffer) - 1)};
|
|
return length > 0 ? std::string(buffer, static_cast<std::size_t>(length)) : std::string{};
|
|
#endif
|
|
}
|
|
|
|
// starts it and lets go: the editor keeps whatever is on its screen, and the second instance is not
|
|
// ours to wait for. on unix that is two forks, so nothing is left behind for us to reap
|
|
bool spawn_detached(std::string const &Executable, std::vector<std::string> const &Arguments, std::string const &Directory)
|
|
{
|
|
if (Executable.empty())
|
|
{
|
|
return false;
|
|
}
|
|
#if defined(_WIN32)
|
|
std::string command{'"' + Executable + '"'};
|
|
for (auto const &argument : Arguments)
|
|
{
|
|
command += " \"" + argument + '"';
|
|
}
|
|
STARTUPINFOA startup{};
|
|
startup.cb = sizeof(startup);
|
|
PROCESS_INFORMATION process{};
|
|
if (0 == CreateProcessA(nullptr, command.data(), nullptr, nullptr, FALSE, 0, nullptr, Directory.empty() ? nullptr : Directory.c_str(), &startup, &process))
|
|
{
|
|
return false;
|
|
}
|
|
CloseHandle(process.hThread);
|
|
CloseHandle(process.hProcess);
|
|
return true;
|
|
#else
|
|
// laid out before the fork: between forking and exec'ing, a process with threads in it must not
|
|
// go anywhere near the allocator
|
|
std::vector<char *> argv;
|
|
argv.push_back(const_cast<char *>(Executable.c_str()));
|
|
for (auto const &argument : Arguments)
|
|
{
|
|
argv.push_back(const_cast<char *>(argument.c_str()));
|
|
}
|
|
argv.push_back(nullptr);
|
|
|
|
auto const first{::fork()};
|
|
if (first < 0)
|
|
{
|
|
return false;
|
|
}
|
|
if (first == 0)
|
|
{
|
|
if (::fork() == 0)
|
|
{
|
|
::setsid();
|
|
if (false == Directory.empty())
|
|
{
|
|
if (::chdir(Directory.c_str()) != 0)
|
|
{
|
|
::_exit(127);
|
|
}
|
|
}
|
|
::execv(Executable.c_str(), argv.data());
|
|
}
|
|
::_exit(0);
|
|
}
|
|
int status{0};
|
|
::waitpid(first, &status, 0);
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
// the construction points of a turnout, by TurnoutMark::kind. the map and the template drawing name
|
|
// them from this one list
|
|
char const *const mark_names[]{"PR", "ostrze", "pieta iglicy", "styk", "koniec luku",
|
|
"KR", "punkt teoretyczny", "dziob u", "koniec strugania", "koniec rozjazdu"};
|
|
|
|
// an angle the way a turnout drawing gives it. the kat przylegania is a couple of dozen minutes, so
|
|
// degrees alone say nothing
|
|
std::string angle_text(double const Radians)
|
|
{
|
|
auto const total{std::abs(Radians) * 180.0 / std::numbers::pi};
|
|
auto const degrees{static_cast<int>(total)};
|
|
auto const minutes{(total - degrees) * 60.0};
|
|
char buffer[64];
|
|
std::snprintf(buffer, sizeof(buffer), "%s%d st %02d' %05.2f\"", Radians < 0.0 ? "-" : "", degrees, static_cast<int>(minutes),
|
|
(minutes - static_cast<int>(minutes)) * 60.0);
|
|
return buffer;
|
|
}
|
|
|
|
// What a bend works out to, said the way a catalogue says it: both radii and the beta the tor
|
|
// zasadniczy turns through. Jednostronny is both tracks bent the same way, dwustronny the opposite.
|
|
std::string bend_text(double const Bend_, double const Diverging_, double const Angle_)
|
|
{
|
|
if (Bend_ == 0.0)
|
|
{
|
|
return "rozjazd zwyczajny: tor zasadniczy prosty";
|
|
}
|
|
auto const way{[](double const Curvature_) { return Curvature_ > 0.0 ? "w lewo" : "w prawo"; }};
|
|
char text[256];
|
|
if (Diverging_ == 0.0)
|
|
{
|
|
std::snprintf(text, sizeof(text), "rozjazd lukowy dwustronny: zasadniczy R %.3f m %s, zwrotny prosty, beta %s", std::abs(1.0 / Bend_), way(Bend_),
|
|
angle_text(Angle_).c_str());
|
|
}
|
|
else
|
|
{
|
|
std::snprintf(text, sizeof(text), "rozjazd lukowy %s: zasadniczy R %.3f m %s, zwrotny R %.3f m %s, beta %s",
|
|
Diverging_ * Bend_ > 0.0 ? "jednostronny" : "dwustronny", std::abs(1.0 / Bend_), way(Bend_), std::abs(1.0 / Diverging_),
|
|
way(Diverging_), angle_text(Angle_).c_str());
|
|
}
|
|
return text;
|
|
}
|
|
|
|
// The two limits Koc puts on bending an ordinary turnout. Reported, never enforced: the drawing is
|
|
// the user's, and a warning that says the number is worth more than a control that refuses it
|
|
char const *bend_limit(double const Diverging_)
|
|
{
|
|
if (Diverging_ == 0.0)
|
|
{
|
|
return nullptr;
|
|
}
|
|
auto const radius{std::abs(1.0 / Diverging_)};
|
|
if (radius < 190.0)
|
|
{
|
|
return "tor zwrotny ponizej 190 m - najmniejszego promienia rozjazdu";
|
|
}
|
|
if (radius < 214.0)
|
|
{
|
|
return "tor zwrotny ponizej 214 m - potrzebne poszerzenie toru zwrotnego";
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// a dashed polyline in screen space, for the lines a drawing means as theoretical. the dash carries
|
|
// across the joints between segments so a curve built of several pieces reads as one line
|
|
void dashed_polyline(ImDrawList *Drawlist_, std::vector<ImVec2> const &Points_, ImU32 const Colour_, float const Thickness_, float const Dash_ = 7.0f,
|
|
float const Gap_ = 5.0f)
|
|
{
|
|
// the view magnifies to millimetres, so a line can run for millions of pixels: what falls outside
|
|
// the clip rect is stepped over rather than drawn, or the draw list runs out of 16-bit indices
|
|
auto const low{Drawlist_->GetClipRectMin()};
|
|
auto const high{Drawlist_->GetClipRectMax()};
|
|
auto const period{Dash_ + Gap_};
|
|
auto carried{0.0f};
|
|
auto budget{400};
|
|
for (std::size_t i = 1; i < Points_.size(); ++i)
|
|
{
|
|
auto const from{Points_[i - 1]};
|
|
auto const dx{Points_[i].x - from.x};
|
|
auto const dy{Points_[i].y - from.y};
|
|
auto const len{std::sqrt(dx * dx + dy * dy)};
|
|
if (len < 1e-4f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// the stretch of this piece that the clip rect actually shows, as a distance along it
|
|
auto first{0.0f};
|
|
auto last{len};
|
|
auto visible{true};
|
|
float const edge[]{-dx, dx, -dy, dy};
|
|
float const room[]{from.x - low.x + 8.0f, high.x - from.x + 8.0f, from.y - low.y + 8.0f, high.y - from.y + 8.0f};
|
|
for (int side = 0; side < 4 && visible; ++side)
|
|
{
|
|
if (std::abs(edge[side]) < 1e-6f)
|
|
{
|
|
visible = room[side] >= 0.0f;
|
|
}
|
|
else if (auto const cut{room[side] / edge[side] * len}; edge[side] < 0.0f)
|
|
{
|
|
first = std::max(first, cut);
|
|
}
|
|
else
|
|
{
|
|
last = std::min(last, cut);
|
|
}
|
|
}
|
|
if (false == visible || first >= last)
|
|
{
|
|
carried = std::fmod(carried + len, period);
|
|
continue;
|
|
}
|
|
|
|
auto at{std::max(0.0f, first - std::fmod(carried + first, period))};
|
|
while (at < last && budget > 0)
|
|
{
|
|
--budget;
|
|
auto const phase{std::fmod(carried + at, period)};
|
|
if (phase < Dash_)
|
|
{
|
|
auto const to{std::min(at + Dash_ - phase, len)};
|
|
Drawlist_->AddLine(ImVec2(from.x + dx * at / len, from.y + dy * at / len), ImVec2(from.x + dx * to / len, from.y + dy * to / len), Colour_,
|
|
Thickness_);
|
|
at = to;
|
|
}
|
|
else
|
|
{
|
|
at += period - phase;
|
|
}
|
|
}
|
|
carried = std::fmod(carried + len, period);
|
|
}
|
|
}
|
|
|
|
editor::turnout_preset const *find_preset(std::string const &Name_)
|
|
{
|
|
for (auto const &preset : editor::turnout_presets())
|
|
{
|
|
if (preset.name == Name_)
|
|
{
|
|
return &preset;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// the catalogue figure written onto a document's own type. a document keeps its own copy of the
|
|
// catalogue - that is what lets an instance name its type and nothing more - so this is the one
|
|
// place the preset's numbers cross over, and it has to carry all of them
|
|
void adopt_preset(editor::turnout_preset const &Preset_, editor::plan::TurnoutType &Type_)
|
|
{
|
|
Type_.name = Preset_.name;
|
|
Type_.crossing_n = Preset_.crossing_n;
|
|
Type_.length = Preset_.length;
|
|
Type_.blade.tip_thickness = Preset_.tip_thickness;
|
|
Type_.blade.nose = Preset_.nose;
|
|
Type_.blade.railtop_width = Preset_.railtop_width;
|
|
Type_.pieces.clear();
|
|
for (auto const &piece : Preset_.pieces)
|
|
{
|
|
Type_.pieces.push_back(
|
|
editor::plan::TurnoutPieceSpec{piece.part, piece.length, piece.radius_start, piece.radius_end, piece.turn_in});
|
|
}
|
|
}
|
|
|
|
void world_to_plan(glm::dvec3 const &World, double &X, double &Y)
|
|
{
|
|
X = World.x + Global.scenery_origin.x;
|
|
Y = -World.z + Global.scenery_origin.y;
|
|
}
|
|
|
|
// places a world point on screen using the camera of the most recent colour pass. the engine renders
|
|
// camera-relative, so the view matrix carries rotation only and the position is subtracted here
|
|
bool world_to_screen(glm::dvec3 const &World, ImVec2 &Screen)
|
|
{
|
|
auto const relative{glm::vec3(World - GfxRenderer->Camera_Position())};
|
|
auto const clip{GfxRenderer->Camera_Projection_Matrix() * GfxRenderer->Camera_View_Matrix() * glm::vec4(relative, 1.0f)};
|
|
|
|
if (clip.w <= 0.0f)
|
|
{
|
|
return false; // behind the camera
|
|
}
|
|
|
|
auto const ndc{glm::vec3(clip) / clip.w};
|
|
auto const display{ImGui::GetIO().DisplaySize};
|
|
Screen = {(ndc.x * 0.5f + 0.5f) * display.x, (0.5f - ndc.y * 0.5f) * display.y};
|
|
|
|
return true;
|
|
}
|
|
|
|
// ground under the cursor in the top-down plan view. depth picking is useless here: empty scenery
|
|
// has nothing to hit, so the last (or zero) offset would pin every click to the same spot
|
|
glm::dvec3 ortho_cursor_world()
|
|
{
|
|
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>(Global.editor_ortho_extent);
|
|
auto const halfwidth = halfheight * static_cast<double>(width) / static_cast<double>(height);
|
|
// yaw is pinned to north: view x = world x, view y = -world z
|
|
return {Global.pCamera.Pos.x + ndcx * halfwidth, 0.0, Global.pCamera.Pos.z - ndcy * halfheight};
|
|
}
|
|
|
|
ImU32 element_colour(Kind const Kind_, bool const Active)
|
|
{
|
|
auto const alpha{Active ? 255 : 200};
|
|
switch (Kind_)
|
|
{
|
|
case Kind::Arc:
|
|
return IM_COL32(255, 190, 90, alpha);
|
|
case Kind::Clothoid:
|
|
return IM_COL32(140, 245, 150, alpha);
|
|
default:
|
|
return IM_COL32(125, 200, 255, alpha);
|
|
}
|
|
}
|
|
|
|
/// Pikietaz, the way a railway says a distance: kilometres and metres, "1+234.56".
|
|
std::string chainage(double const Metres)
|
|
{
|
|
char buffer[64];
|
|
auto const kilometres{static_cast<long long>(std::floor(Metres / 1000.0))};
|
|
std::snprintf(buffer, sizeof(buffer), "%lld+%06.2f", kilometres, Metres - static_cast<double>(kilometres) * 1000.0);
|
|
return buffer;
|
|
}
|
|
|
|
char const *kind_name(Kind const Kind_)
|
|
{
|
|
switch (Kind_)
|
|
{
|
|
case Kind::Arc:
|
|
return "Luk";
|
|
case Kind::Clothoid:
|
|
return "Krzywa przejsciowa";
|
|
default:
|
|
return "Prosta";
|
|
}
|
|
}
|
|
|
|
double distance_point_segment(ImVec2 const &Point, ImVec2 const &A, ImVec2 const &B)
|
|
{
|
|
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)
|
|
{
|
|
return std::hypot(Point.x - A.x, Point.y - A.y);
|
|
}
|
|
auto t{((Point.x - A.x) * dx + (Point.y - A.y) * dy) / len2};
|
|
t = std::clamp(t, 0.0f, 1.0f);
|
|
return std::hypot(Point.x - (A.x + dx * t), Point.y - (A.y + dy * t));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// lifecycle
|
|
// ---------------------------------------------------------------------------
|
|
|
|
plan_panel::plan_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen)
|
|
{
|
|
size_min = {440, 260};
|
|
size_max = {900, 900};
|
|
seed_catalogue();
|
|
new_track();
|
|
solve();
|
|
}
|
|
|
|
void plan_panel::update()
|
|
{
|
|
// the plan belongs on the scenery itself, so opening the tool puts the viewport into the
|
|
// distortion-free view from above and closing it hands the camera back
|
|
Global.editor_ortho = is_open;
|
|
}
|
|
|
|
void plan_panel::render_contents()
|
|
{
|
|
m_focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
|
|
|
// Ctrl+Z / Ctrl+Y, unless a field is being typed into - there they belong to the text
|
|
auto const &io{ImGui::GetIO()};
|
|
auto const &keymap{io.KeyMap};
|
|
if (io.KeyCtrl && false == io.WantTextInput)
|
|
{
|
|
if (io.KeyShift)
|
|
{
|
|
if (keymap[ImGuiKey_Z] >= 0 && ImGui::IsKeyPressed(keymap[ImGuiKey_Z], false))
|
|
{
|
|
redo();
|
|
}
|
|
}
|
|
else if (keymap[ImGuiKey_Z] >= 0 && ImGui::IsKeyPressed(keymap[ImGuiKey_Z], false))
|
|
{
|
|
undo();
|
|
}
|
|
else if (keymap[ImGuiKey_Y] >= 0 && ImGui::IsKeyPressed(keymap[ImGuiKey_Y], false))
|
|
{
|
|
redo();
|
|
}
|
|
}
|
|
|
|
// layer keys. this ImGui indexes IsKeyPressed by the backend's own key codes, and the backend is
|
|
// GLFW, so the key is its code
|
|
if (false == io.WantTextInput && false == io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_1, false))
|
|
{
|
|
m_showortho = !m_showortho;
|
|
m_status = m_showortho ? "podklad wlaczony" : "podklad wylaczony";
|
|
}
|
|
|
|
handle_scene();
|
|
draw_on_scene();
|
|
|
|
render_toolbar();
|
|
render_elements();
|
|
render_turnouts();
|
|
render_diagnostics();
|
|
render_storage();
|
|
render_newmap_dialog();
|
|
render_location_dialog();
|
|
render_template_window();
|
|
render_curvature_strip();
|
|
|
|
watch_history();
|
|
}
|
|
|
|
bool plan_panel::takes_history_shortcuts() const
|
|
{
|
|
return is_open && m_focused;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// cofanie
|
|
// ---------------------------------------------------------------------------
|
|
|
|
namespace
|
|
{
|
|
// how many steps back the drawing remembers. a step is a copy of the document, which is a few
|
|
// hundred numbers per track - deep history costs nothing worth counting here
|
|
constexpr std::size_t kHistoryDepth{64};
|
|
|
|
void mix(std::uint64_t &Print, std::uint64_t const Value)
|
|
{
|
|
Print = (Print ^ Value) * 1099511628211ull;
|
|
}
|
|
void mix(std::uint64_t &Print, double const Value)
|
|
{
|
|
std::uint64_t bits{0};
|
|
static_assert(sizeof(bits) == sizeof(Value));
|
|
std::memcpy(&bits, &Value, sizeof(bits));
|
|
mix(Print, bits);
|
|
}
|
|
void mix(std::uint64_t &Print, std::string const &Value)
|
|
{
|
|
for (auto const character : Value)
|
|
{
|
|
mix(Print, static_cast<std::uint64_t>(static_cast<unsigned char>(character)));
|
|
}
|
|
mix(Print, Value.size());
|
|
}
|
|
} // namespace
|
|
|
|
std::uint64_t plan_panel::document_print() const
|
|
{
|
|
// everything a drawing is: the tracks, the turnouts standing on them, and the catalogue they
|
|
// are instances of. the view and the export path are not part of the drawing, so saving does
|
|
// not make a step
|
|
std::uint64_t print{14695981039346656037ull};
|
|
for (auto const &track : m_document.tracks)
|
|
{
|
|
mix(print, static_cast<std::uint64_t>(track.id));
|
|
mix(print, track.name);
|
|
if (auto const *pose{std::get_if<editor::plan::AtPose>(&track.anchor)}; pose != nullptr)
|
|
{
|
|
mix(print, pose->x);
|
|
mix(print, pose->y);
|
|
mix(print, pose->az);
|
|
}
|
|
else if (auto const *port{std::get_if<editor::plan::AtPort>(&track.anchor)}; port != nullptr)
|
|
{
|
|
mix(print, static_cast<std::uint64_t>(port->turnout));
|
|
mix(print, static_cast<std::uint64_t>(port->port));
|
|
}
|
|
for (auto const &element : track.elements)
|
|
{
|
|
mix(print, static_cast<std::uint64_t>(element.id));
|
|
mix(print, static_cast<std::uint64_t>(element.kind));
|
|
mix(print, element.radius);
|
|
mix(print, static_cast<std::uint64_t>(element.hand + 2));
|
|
mix(print, element.length);
|
|
if (auto const *parallel{std::get_if<editor::plan::Parallel>(&element.hold)}; parallel != nullptr)
|
|
{
|
|
mix(print, static_cast<std::uint64_t>(parallel->ref));
|
|
mix(print, parallel->offset);
|
|
}
|
|
}
|
|
}
|
|
for (auto const &turnout : m_document.turnouts)
|
|
{
|
|
mix(print, static_cast<std::uint64_t>(turnout.id));
|
|
mix(print, turnout.type);
|
|
mix(print, static_cast<std::uint64_t>(turnout.on));
|
|
mix(print, turnout.station);
|
|
mix(print, static_cast<std::uint64_t>(turnout.hand + 2));
|
|
mix(print, static_cast<std::uint64_t>(turnout.facing ? 1 : 0));
|
|
mix(print, static_cast<std::uint64_t>(turnout.bend_from_track ? 1 : 0));
|
|
mix(print, turnout.bend);
|
|
}
|
|
for (auto const &type : m_document.turnout_types)
|
|
{
|
|
mix(print, type.name);
|
|
mix(print, type.crossing_n);
|
|
mix(print, type.length);
|
|
mix(print, type.blade.tip_thickness);
|
|
mix(print, type.blade.nose);
|
|
mix(print, type.blade.railtop_width);
|
|
for (auto const &piece : type.pieces)
|
|
{
|
|
mix(print, static_cast<std::uint64_t>(piece.part));
|
|
mix(print, piece.length);
|
|
mix(print, piece.radius_start);
|
|
mix(print, piece.radius_end);
|
|
mix(print, piece.turn_in);
|
|
}
|
|
}
|
|
return print;
|
|
}
|
|
|
|
void plan_panel::watch_history()
|
|
{
|
|
auto const print{document_print()};
|
|
if (print != m_print)
|
|
{
|
|
if (false == m_step_open)
|
|
{
|
|
// the state to come back to is the one from before this change, which is the last
|
|
// settled one - not the document as it stands now
|
|
m_undo.push_back(m_settled);
|
|
if (m_undo.size() > kHistoryDepth)
|
|
{
|
|
m_undo.pop_front();
|
|
}
|
|
m_redo.clear();
|
|
m_step_open = true;
|
|
}
|
|
m_print = print;
|
|
return;
|
|
}
|
|
// nothing changed this frame: once nothing is being dragged and no field is being typed into,
|
|
// the step is finished and whatever comes next is a step of its own
|
|
if (m_step_open && false == ImGui::IsAnyItemActive() && false == ImGui::IsMouseDown(0))
|
|
{
|
|
m_settled = m_document;
|
|
m_step_open = false;
|
|
}
|
|
}
|
|
|
|
void plan_panel::undo()
|
|
{
|
|
if (m_undo.empty())
|
|
{
|
|
m_status = "nie ma czego cofnac";
|
|
return;
|
|
}
|
|
m_redo.push_back(m_document);
|
|
if (m_redo.size() > kHistoryDepth)
|
|
{
|
|
m_redo.pop_front();
|
|
}
|
|
m_document = std::move(m_undo.back());
|
|
m_undo.pop_back();
|
|
settle_after_history();
|
|
m_status = "cofnieto (" + std::to_string(m_undo.size()) + " krokow wstecz zostalo)";
|
|
}
|
|
|
|
void plan_panel::redo()
|
|
{
|
|
if (m_redo.empty())
|
|
{
|
|
m_status = "nie ma czego powtorzyc";
|
|
return;
|
|
}
|
|
m_undo.push_back(m_document);
|
|
m_document = std::move(m_redo.back());
|
|
m_redo.pop_back();
|
|
settle_after_history();
|
|
m_status = "powtorzono (" + std::to_string(m_redo.size()) + " krokow w przod zostalo)";
|
|
}
|
|
|
|
void plan_panel::settle_after_history()
|
|
{
|
|
// the step is closed on the state we landed on, and the watcher must not read the jump itself
|
|
// as an edit worth remembering
|
|
m_settled = m_document;
|
|
m_step_open = false;
|
|
m_print = document_print();
|
|
|
|
// the traces belonged to the state that was undone, and there is no undoing them: they are
|
|
// what the user pointed at, not something the document remembers
|
|
m_trace.clear();
|
|
|
|
// every transient errand belonged to the state that was undone
|
|
m_pending = false;
|
|
m_pick_turnout = 0;
|
|
m_pick_parallel = 0;
|
|
m_pick_join = 0;
|
|
m_pick_split = 0;
|
|
m_pick_cut = 0;
|
|
m_pick_start = 0;
|
|
m_pick_draw = 0;
|
|
m_pick_pair = 0;
|
|
m_join_first = TrackId::none;
|
|
m_dragging_turnout = false;
|
|
m_dragging_straight = false;
|
|
|
|
if (editor::plan::find_track(m_document, m_track) == nullptr)
|
|
{
|
|
m_track = m_document.tracks.empty() ? TrackId::none : m_document.tracks.front().id;
|
|
}
|
|
if (auto const *track{current_track()}; track != nullptr)
|
|
{
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", track->name.c_str());
|
|
}
|
|
if (editor::plan::find_element(m_document, m_sel_element) == nullptr)
|
|
{
|
|
m_sel_element = ElementId::none;
|
|
}
|
|
if (editor::plan::find_turnout(m_document, m_sel_turnout) == nullptr)
|
|
{
|
|
m_sel_turnout = TurnoutId::none;
|
|
}
|
|
if (m_turnout_type >= static_cast<int>(m_document.turnout_types.size()))
|
|
{
|
|
m_turnout_type = 0;
|
|
}
|
|
m_sel_piece = -1;
|
|
|
|
solve();
|
|
}
|
|
|
|
void plan_panel::seed_catalogue()
|
|
{
|
|
// the catalogue is data: an instance names its type, so a new figure goes in here and never
|
|
// touches the file format for placements.
|
|
//
|
|
// a plan saved before the iglica became a piece of its own keeps a type of that very name with
|
|
// no blade in it at all - two odcinki, luk i krzyzownicowa. that is not a figure anyone
|
|
// authored, it is what the old writer could say, so it is brought up to the current catalogue
|
|
// rather than left to lay a switch that has no ostrze. a type someone really did edit keeps its
|
|
// iglica and is left alone
|
|
auto refreshed{0};
|
|
for (auto const &preset : editor::turnout_presets())
|
|
{
|
|
auto existing{std::find_if(m_document.turnout_types.begin(), m_document.turnout_types.end(),
|
|
[&](auto const &Type_) { return Type_.name == preset.name; })};
|
|
if (existing == m_document.turnout_types.end())
|
|
{
|
|
editor::plan::TurnoutType type;
|
|
adopt_preset(preset, type);
|
|
m_document.turnout_types.push_back(std::move(type));
|
|
continue;
|
|
}
|
|
auto const has_blade{std::any_of(existing->pieces.begin(), existing->pieces.end(), [](auto const &Piece_) { return Piece_.part == 1; })};
|
|
if (false == has_blade)
|
|
{
|
|
adopt_preset(preset, *existing);
|
|
++refreshed;
|
|
}
|
|
}
|
|
if (refreshed > 0)
|
|
{
|
|
m_status = std::to_string(refreshed) + " typ(ow) rozjazdu bylo bez iglicy - wziete z katalogu na nowo";
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// document
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void plan_panel::solve()
|
|
{
|
|
m_solution = editor::plan::solve(m_document);
|
|
m_rails = editor::plan::render_rails(m_solution);
|
|
measure_trace();
|
|
}
|
|
|
|
void plan_panel::measure_trace()
|
|
{
|
|
m_deviation.clear();
|
|
m_track_deviation.clear();
|
|
m_jumps.clear();
|
|
|
|
for (auto const &track : m_solution.tracks)
|
|
{
|
|
// joints where the curvature jumps: an arc taken without a transition curve. that is
|
|
// ordinary on a line that was built that way, so it is counted, not complained about
|
|
auto jumps{0};
|
|
for (std::size_t i = 1; i < track.elements.size(); ++i)
|
|
{
|
|
if (std::abs(track.elements[i].k0 - track.elements[i - 1].k1) > 1e-9)
|
|
{
|
|
++jumps;
|
|
}
|
|
}
|
|
m_jumps[static_cast<std::uint32_t>(track.id)] = jumps;
|
|
|
|
auto const trace{m_trace.find(static_cast<std::uint32_t>(track.id))};
|
|
if (trace == m_trace.end() || trace->second.size() < 2 || track.elements.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// every clicked point belongs to whichever element runs closest to it, and that is the
|
|
// element whose fit it has something to say about
|
|
std::unordered_map<std::uint32_t, std::pair<double, int>> sums; // sum of squares, count
|
|
auto worst_of_track{0.0};
|
|
auto sum_of_track{0.0};
|
|
auto counted{0};
|
|
for (auto const &point : trace->second)
|
|
{
|
|
auto best{std::numeric_limits<double>::max()};
|
|
auto owner{editor::plan::ElementId::none};
|
|
for (auto const &element : track.elements)
|
|
{
|
|
for (std::size_t i = 1; i < element.points.size(); ++i)
|
|
{
|
|
auto const &a{element.points[i - 1]};
|
|
auto const &b{element.points[i]};
|
|
auto const dx{b.x - a.x};
|
|
auto const dy{b.y - a.y};
|
|
auto const len2{dx * dx + dy * dy};
|
|
auto t{len2 > 0.0 ? ((point.x - a.x) * dx + (point.y - a.y) * dy) / len2 : 0.0};
|
|
t = std::clamp(t, 0.0, 1.0);
|
|
auto const distance{std::hypot(point.x - (a.x + dx * t), point.y - (a.y + dy * t))};
|
|
if (distance < best)
|
|
{
|
|
best = distance;
|
|
owner = element.id;
|
|
}
|
|
}
|
|
}
|
|
if (owner == editor::plan::ElementId::none)
|
|
{
|
|
continue;
|
|
}
|
|
auto &sum{sums[static_cast<std::uint32_t>(owner)]};
|
|
sum.first += best * best;
|
|
sum.second += 1;
|
|
auto &deviation{m_deviation[static_cast<std::uint32_t>(owner)]};
|
|
deviation.first = std::max(deviation.first, best);
|
|
worst_of_track = std::max(worst_of_track, best);
|
|
sum_of_track += best * best;
|
|
++counted;
|
|
}
|
|
for (auto const &sum : sums)
|
|
{
|
|
m_deviation[sum.first].second = std::sqrt(sum.second.first / static_cast<double>(sum.second.second));
|
|
}
|
|
if (counted > 0)
|
|
{
|
|
m_track_deviation[static_cast<std::uint32_t>(track.id)] = {worst_of_track, std::sqrt(sum_of_track / static_cast<double>(counted))};
|
|
}
|
|
}
|
|
}
|
|
|
|
plan_panel::Track *plan_panel::current_track()
|
|
{
|
|
return editor::plan::find_track(m_document, m_track);
|
|
}
|
|
|
|
plan_panel::Track const *plan_panel::current_track() const
|
|
{
|
|
return editor::plan::find_track(m_document, m_track);
|
|
}
|
|
|
|
void plan_panel::new_track()
|
|
{
|
|
Track track;
|
|
track.id = editor::plan::mint_track(m_document);
|
|
track.name = "tor " + std::to_string(m_document.tracks.size() + 1);
|
|
m_document.tracks.push_back(track);
|
|
m_track = track.id;
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", track.name.c_str());
|
|
m_pending = false;
|
|
clear_selection();
|
|
}
|
|
|
|
void plan_panel::delete_current_track()
|
|
{
|
|
auto const it{std::find_if(m_document.tracks.begin(), m_document.tracks.end(), [&](Track const &Track_) { return Track_.id == m_track; })};
|
|
if (it == m_document.tracks.end())
|
|
{
|
|
return;
|
|
}
|
|
// a turnout standing on it, and anything anchored to that turnout, would be left dangling
|
|
auto const gone{it->id};
|
|
m_document.tracks.erase(it);
|
|
for (auto turnout{m_document.turnouts.begin()}; turnout != m_document.turnouts.end();)
|
|
{
|
|
turnout = (turnout->on == gone) ? m_document.turnouts.erase(turnout) : turnout + 1;
|
|
}
|
|
if (m_document.tracks.empty())
|
|
{
|
|
new_track();
|
|
}
|
|
else
|
|
{
|
|
m_track = m_document.tracks.front().id;
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", m_document.tracks.front().name.c_str());
|
|
}
|
|
m_pending = false;
|
|
clear_selection();
|
|
solve();
|
|
}
|
|
|
|
void plan_panel::clear_selection()
|
|
{
|
|
m_sel_element = ElementId::none;
|
|
m_sel_turnout = TurnoutId::none;
|
|
m_dragging_turnout = false;
|
|
}
|
|
|
|
void plan_panel::append_point_to(double const X, double const Y)
|
|
{
|
|
auto *track{current_track()};
|
|
if (track == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// an empty track has no direction yet, so the first click only says where it starts
|
|
if (track->elements.empty())
|
|
{
|
|
if (false == m_pending)
|
|
{
|
|
m_pending = true;
|
|
m_startx = X;
|
|
m_starty = Y;
|
|
m_trace[static_cast<std::uint32_t>(track->id)] = {editor::plan::PlanPoint{X, Y}};
|
|
m_status = "kliknij drugi raz, zeby polozyc pierwsza prosta";
|
|
return;
|
|
}
|
|
auto const dx{X - m_startx};
|
|
auto const dy{Y - m_starty};
|
|
auto const length{std::hypot(dx, dy)};
|
|
if (length < 1.0)
|
|
{
|
|
m_status = "za krotka prosta";
|
|
return;
|
|
}
|
|
track->anchor = editor::plan::AtPose{m_startx, m_starty, std::atan2(dx, dy)};
|
|
track->elements.push_back(editor::plan::make_line(m_document, length));
|
|
m_trace[static_cast<std::uint32_t>(track->id)].push_back(editor::plan::PlanPoint{X, Y});
|
|
m_pending = false;
|
|
solve();
|
|
m_status.clear();
|
|
return;
|
|
}
|
|
|
|
std::string why;
|
|
if (false == can_grow(why))
|
|
{
|
|
m_status = why;
|
|
return;
|
|
}
|
|
|
|
// the track ends on the last click, so that end is the corner this one turns at
|
|
auto const *solved{editor::plan::find_track(m_solution, track->id)};
|
|
if (solved == nullptr || solved->elements.empty())
|
|
{
|
|
m_status = "tor nie jest polozony - popraw najpierw to, co nie wyszlo";
|
|
return;
|
|
}
|
|
auto const corner_pose{solved->end};
|
|
|
|
auto const dx{X - corner_pose.x};
|
|
auto const dy{Y - corner_pose.y};
|
|
auto const reach{std::hypot(dx, dy)};
|
|
if (reach < 1.0)
|
|
{
|
|
m_status = "za krotki odcinek";
|
|
return;
|
|
}
|
|
|
|
auto const corner{editor::plan::fit_corner(corner_pose.hx, corner_pose.hy, dx / reach, dy / reach, m_corner_radius)};
|
|
if (false == corner.ok)
|
|
{
|
|
// straight on: nothing to round. two straights end to end are one straight, so the one
|
|
// already there simply gets longer
|
|
track->elements.back().length += reach;
|
|
m_trace[static_cast<std::uint32_t>(track->id)].push_back(editor::plan::PlanPoint{X, Y});
|
|
solve();
|
|
m_status = "prosto dalej: przedluzona ostatnia prosta do " + to_string(track->elements.back().length, 1) + " m";
|
|
return;
|
|
}
|
|
|
|
// the arc eats R*tan(kat/2) off the leg behind the corner and off the one in front. neither may
|
|
// go negative, which is the whole of "naroznik za ostry na tym promieniu"
|
|
auto &previous{track->elements.back()};
|
|
if (previous.kind != Kind::Line || previous.length <= corner.tangent + 0.5)
|
|
{
|
|
m_status = "naroznik za ostry na R=" + to_string(m_corner_radius, 0) + " m: potrzeba " + to_string(corner.tangent, 1) + " m stycznej, a przed nim jest " +
|
|
to_string(previous.kind == Kind::Line ? previous.length : 0.0, 1) + " m prostej";
|
|
return;
|
|
}
|
|
if (reach <= corner.tangent + 0.5)
|
|
{
|
|
m_status = "klikniete za blisko naroznika: potrzeba " + to_string(corner.tangent, 1) + " m stycznej, a jest " + to_string(reach, 1) + " m";
|
|
return;
|
|
}
|
|
|
|
previous.length -= corner.tangent;
|
|
track->elements.push_back(editor::plan::make_arc(m_document, m_corner_radius, corner.hand, corner.arc_length));
|
|
track->elements.push_back(editor::plan::make_line(m_document, reach - corner.tangent));
|
|
m_trace[static_cast<std::uint32_t>(track->id)].push_back(editor::plan::PlanPoint{X, Y});
|
|
solve();
|
|
m_status.clear();
|
|
}
|
|
|
|
void plan_panel::drop_last_vertex()
|
|
{
|
|
if (auto trace{m_trace.find(static_cast<std::uint32_t>(m_track))};
|
|
trace != m_trace.end() && false == trace->second.empty())
|
|
{
|
|
trace->second.pop_back();
|
|
}
|
|
auto *track{current_track()};
|
|
if (track == nullptr || track->elements.empty())
|
|
{
|
|
m_pending = false;
|
|
return;
|
|
}
|
|
if (track->elements.back().kind != Kind::Line)
|
|
{
|
|
m_status = "ostatni odcinek nie jest prosta - zdejmij go w liscie";
|
|
return;
|
|
}
|
|
|
|
track->elements.pop_back();
|
|
// and the arc that rounded the corner in front of it, whose tangent goes back onto the straight
|
|
// it was taken from. what is left ends on the click before this one
|
|
if (false == track->elements.empty() && track->elements.back().kind == Kind::Arc)
|
|
{
|
|
auto const &arc{track->elements.back()};
|
|
auto const tangent{arc.radius > 0.0 ? arc.radius * std::tan(0.5 * arc.length / arc.radius) : 0.0};
|
|
track->elements.pop_back();
|
|
if (false == track->elements.empty() && track->elements.back().kind == Kind::Line)
|
|
{
|
|
track->elements.back().length += tangent;
|
|
}
|
|
}
|
|
|
|
clear_selection();
|
|
solve();
|
|
}
|
|
|
|
bool plan_panel::can_grow(std::string &Why) const
|
|
{
|
|
auto const *track{current_track()};
|
|
if (track == nullptr)
|
|
{
|
|
Why = "nie ma edytowanego toru";
|
|
return false;
|
|
}
|
|
if (track->elements.empty())
|
|
{
|
|
// an empty track has no end to grow from yet; where it begins is said with the mouse
|
|
return true;
|
|
}
|
|
auto const *solved{editor::plan::find_track(m_solution, track->id)};
|
|
if (solved == nullptr || solved->elements.empty())
|
|
{
|
|
Why = "tor nie jest polozony - popraw najpierw to, co nie wyszlo";
|
|
return false;
|
|
}
|
|
if (false == solved->complete)
|
|
{
|
|
Why = "tor jest polozony tylko do miejsca, w ktorym sie nie udal - popraw to najpierw";
|
|
return false;
|
|
}
|
|
// the chain grows at its far end, so that end has to be free. the near one being free is no
|
|
// help: nothing here prepends, and turning the track round is a different thing to ask for
|
|
if (false == loose_end(*solved, 1))
|
|
{
|
|
Why = loose_end(*solved, 0) ? "koniec toru jest do czegos podlaczony - wolny jest tylko jego poczatek"
|
|
: "oba konce toru sa podlaczone: nie ma gdzie dolozyc odcinka";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool plan_panel::growth_pose(editor::plan::geometry::Pose &Out) const
|
|
{
|
|
auto const *track{current_track()};
|
|
if (track == nullptr || track->elements.empty())
|
|
{
|
|
return false;
|
|
}
|
|
auto const *solved{editor::plan::find_track(m_solution, track->id)};
|
|
if (solved == nullptr || solved->elements.empty())
|
|
{
|
|
return false;
|
|
}
|
|
Out = solved->end;
|
|
return true;
|
|
}
|
|
|
|
void plan_panel::append_element(Kind const Kind_)
|
|
{
|
|
auto *track{current_track()};
|
|
if (track == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::string why;
|
|
if (false == can_grow(why))
|
|
{
|
|
m_status = why;
|
|
return;
|
|
}
|
|
|
|
// an empty track begins where the mouse says it begins, not at the plan's zero: the first click
|
|
// puts it down, the second says which way it runs
|
|
if (track->elements.empty())
|
|
{
|
|
m_pick_start = 1;
|
|
m_start_kind = Kind_;
|
|
m_pending = false;
|
|
m_status = std::string{"kliknij, gdzie tor ma sie zaczynac - potem wskaz kierunek ("} + kind_name(Kind_) + ")";
|
|
return;
|
|
}
|
|
|
|
// two straights end to end are one straight - there would be no corner between them and nothing
|
|
// to grab hold of, so the one already there just gets longer
|
|
if (Kind_ == Kind::Line && false == track->elements.empty() && track->elements.back().kind == Kind::Line)
|
|
{
|
|
track->elements.back().length += m_new_length;
|
|
m_sel_element = track->elements.back().id;
|
|
solve();
|
|
m_status = "prosta doklejona do poprzedniej: " + to_string(track->elements.back().length, 1) + " m";
|
|
return;
|
|
}
|
|
|
|
Element element;
|
|
element.id = editor::plan::mint_element(m_document);
|
|
element.kind = Kind_;
|
|
element.radius = Kind_ == Kind::Line ? 0.0 : m_new_radius;
|
|
element.hand = Kind_ == Kind::Line ? 0 : m_new_hand;
|
|
element.length = m_new_length;
|
|
|
|
// what the chain is already curving at decides the joint, not what happens to be typed in the
|
|
// boxes: an arc after a transition curve is the arc that curve eases into, so it takes its
|
|
// radius and its hand. a transition after an arc turns the same way the arc does
|
|
std::string inherited;
|
|
if (false == track->elements.empty())
|
|
{
|
|
auto const &previous{track->elements.back()};
|
|
auto const curving{previous.kind != Kind::Line && previous.radius > 0.0};
|
|
if (Kind_ == Kind::Arc && previous.kind == Kind::Clothoid && curving)
|
|
{
|
|
element.radius = previous.radius;
|
|
element.hand = previous.hand;
|
|
inherited = " (R i skret z krzywej przejsciowej)";
|
|
}
|
|
else if (Kind_ == Kind::Arc && previous.kind == Kind::Clothoid)
|
|
{
|
|
// the transition was running out into a straight; now it runs into this arc, so that
|
|
// is what it eases to
|
|
track->elements.back().radius = element.radius;
|
|
track->elements.back().hand = element.hand;
|
|
inherited = " (krzywa przejsciowa dostala jego R i skret)";
|
|
}
|
|
else if (Kind_ == Kind::Clothoid && previous.kind == Kind::Arc)
|
|
{
|
|
// a transition ends on whatever follows it, and for now nothing does: it runs out
|
|
// into a straight. dokladajac za nia luk, przejmie jego promien
|
|
element.radius = 0.0;
|
|
element.hand = previous.hand;
|
|
inherited = " (wybiega w prosta)";
|
|
}
|
|
}
|
|
|
|
track->elements.push_back(element);
|
|
m_sel_element = element.id;
|
|
solve();
|
|
m_status = std::string{"dolozono: "} + kind_name(Kind_) + inherited;
|
|
}
|
|
|
|
void plan_panel::drop_last_element()
|
|
{
|
|
auto *track{current_track()};
|
|
if (track == nullptr || track->elements.empty())
|
|
{
|
|
return;
|
|
}
|
|
track->elements.pop_back();
|
|
clear_selection();
|
|
solve();
|
|
}
|
|
|
|
void plan_panel::delete_selected_element()
|
|
{
|
|
if (m_sel_element == ElementId::none)
|
|
{
|
|
return;
|
|
}
|
|
for (auto &track : m_document.tracks)
|
|
{
|
|
auto const it{std::find_if(track.elements.begin(), track.elements.end(), [&](Element const &Element_) { return Element_.id == m_sel_element; })};
|
|
if (it != track.elements.end())
|
|
{
|
|
track.elements.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
m_sel_element = ElementId::none;
|
|
solve();
|
|
}
|
|
|
|
bool plan_panel::refit_current_track(std::string &Why)
|
|
{
|
|
auto *track{current_track()};
|
|
// the solution still holds the geometry from before the change, which is exactly what the
|
|
// straights are supposed to go on standing on
|
|
auto const *solved{editor::plan::find_track(m_solution, m_track)};
|
|
if (track == nullptr || solved == nullptr)
|
|
{
|
|
Why = "tor nie jest polozony";
|
|
return false;
|
|
}
|
|
|
|
editor::plan::Skeleton skeleton;
|
|
if (false == editor::plan::skeleton_of(*track, *solved, skeleton, Why))
|
|
{
|
|
return false;
|
|
}
|
|
return editor::plan::lay_along_skeleton(*track, skeleton, Why);
|
|
}
|
|
|
|
void plan_panel::drag_straight_to(double const X, double const Y)
|
|
{
|
|
auto *track{current_track()};
|
|
auto const *solved{editor::plan::find_track(m_solution, m_track)};
|
|
if (track == nullptr || solved == nullptr || m_drag_index >= track->elements.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
editor::plan::Skeleton skeleton;
|
|
std::string why;
|
|
if (false == editor::plan::skeleton_of(*track, *solved, skeleton, why))
|
|
{
|
|
m_status = why;
|
|
return;
|
|
}
|
|
|
|
// which of the skeleton's lines the grabbed straight is: the straights in order, which is not
|
|
// every second element once corners hold baskets or transition curves
|
|
std::size_t line_index{0};
|
|
for (std::size_t i = 0; i < m_drag_index; ++i)
|
|
{
|
|
if (track->elements[i].kind == Kind::Line)
|
|
{
|
|
++line_index;
|
|
}
|
|
}
|
|
if (line_index >= skeleton.lines.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// the grabbed straight swings onto the line through its far end and the cursor. every other
|
|
// line stays exactly as it was, which is what keeps their azimuths to the millimetre
|
|
auto const dx{X - m_pivot_x};
|
|
auto const dy{Y - m_pivot_y};
|
|
auto const reach{std::hypot(dx, dy)};
|
|
if (reach < 1.0)
|
|
{
|
|
return;
|
|
}
|
|
// grabbing the end it runs out to swings it forward from the far end; grabbing the end it
|
|
// starts on swings it backward, so the direction of travel is the other way round
|
|
auto const sign{m_drag_end == 1 ? 1.0 : -1.0};
|
|
auto &line{skeleton.lines[line_index]};
|
|
line.px = m_pivot_x;
|
|
line.py = m_pivot_y;
|
|
line.dx = sign * dx / reach;
|
|
line.dy = sign * dy / reach;
|
|
|
|
// the free ends of the skeleton stay where they are, except the one being dragged
|
|
if (line_index == 0 && m_drag_end == 0)
|
|
{
|
|
skeleton.start_x = X;
|
|
skeleton.start_y = Y;
|
|
}
|
|
if (line_index + 1 == skeleton.lines.size() && m_drag_end == 1)
|
|
{
|
|
skeleton.end_x = X;
|
|
skeleton.end_y = Y;
|
|
}
|
|
|
|
// nothing is written until the whole chain works out, so a bad drag leaves the track alone
|
|
Track candidate{*track};
|
|
if (false == editor::plan::lay_along_skeleton(candidate, skeleton, why))
|
|
{
|
|
m_status = why;
|
|
return;
|
|
}
|
|
// the chain laying out is not enough: shortening a straight can pull it out from under a rozjazd
|
|
// standing on it, and a rozjazd half on an arc is not a rozjazd. the drag stops at that instead
|
|
auto const off_straight{[](editor::plan::Solution const &Solution_) {
|
|
std::size_t count{0};
|
|
for (auto const &diagnostic : Solution_.diagnostics)
|
|
{
|
|
count += diagnostic.code == editor::plan::Code::StationOffTrack ? 1 : 0;
|
|
}
|
|
return count;
|
|
}};
|
|
|
|
auto const was{off_straight(m_solution)};
|
|
Track const before{*track};
|
|
*track = std::move(candidate);
|
|
solve();
|
|
if (off_straight(m_solution) > was)
|
|
{
|
|
for (auto const &diagnostic : m_solution.diagnostics)
|
|
{
|
|
if (diagnostic.code == editor::plan::Code::StationOffTrack)
|
|
{
|
|
m_status = "dalej nie: " + diagnostic.text;
|
|
break;
|
|
}
|
|
}
|
|
*track = before;
|
|
solve();
|
|
return;
|
|
}
|
|
m_status.clear();
|
|
}
|
|
|
|
void plan_panel::place_turnout_at(TrackId const On, double const Wx, double const Wy)
|
|
{
|
|
double station{0.0};
|
|
if (false == station_on(On, Wx, Wy, station))
|
|
{
|
|
m_status = "nie ma na czym postawic rozjazdu";
|
|
return;
|
|
}
|
|
if (m_document.turnout_types.empty())
|
|
{
|
|
m_status = "katalog rozjazdow jest pusty";
|
|
return;
|
|
}
|
|
auto const type{static_cast<std::size_t>(std::clamp(m_turnout_type, 0, static_cast<int>(m_document.turnout_types.size()) - 1))};
|
|
|
|
editor::plan::TurnoutPlacement placement;
|
|
placement.id = editor::plan::mint_turnout(m_document);
|
|
placement.type = m_document.turnout_types[type].name;
|
|
placement.on = On;
|
|
placement.station = station;
|
|
placement.hand = m_new_hand;
|
|
m_document.turnouts.push_back(placement);
|
|
|
|
// a switch is worth nothing without somewhere for the branch to go, so one starts at its frog
|
|
Track branch;
|
|
branch.id = editor::plan::mint_track(m_document);
|
|
branch.name = "odnoga " + std::to_string(m_document.tracks.size() + 1);
|
|
branch.anchor = editor::plan::AtPort{placement.id, editor::plan::Port::Frog};
|
|
m_document.tracks.push_back(branch);
|
|
|
|
m_track = branch.id;
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", branch.name.c_str());
|
|
m_sel_turnout = placement.id;
|
|
m_pending = false;
|
|
solve();
|
|
m_status = "rozjazd " + placement.type + " na " + to_string(station, 1) + " m; odnoga zaczyna sie na krzyzownicy";
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// hit tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
bool plan_panel::hit_straight_end(ImVec2 const &Mouse, std::size_t &OutIndex, int &OutEnd) const
|
|
{
|
|
auto const *solved{editor::plan::find_track(m_solution, m_track)};
|
|
auto const *track{current_track()};
|
|
if (solved == nullptr || track == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
constexpr float tolerance{10.0f};
|
|
auto best{tolerance};
|
|
auto found{false};
|
|
for (std::size_t i = 0; i < solved->elements.size() && i < track->elements.size(); ++i)
|
|
{
|
|
if (track->elements[i].kind != Kind::Line)
|
|
{
|
|
continue;
|
|
}
|
|
// a branch starts on its turnout's frog and cannot be pulled off it
|
|
auto const pinned{i == 0 && false == std::holds_alternative<editor::plan::AtPose>(track->anchor)};
|
|
for (int end = 0; end < 2; ++end)
|
|
{
|
|
if (end == 0 && pinned)
|
|
{
|
|
continue;
|
|
}
|
|
auto const &pose{end == 0 ? solved->elements[i].start : solved->elements[i].end};
|
|
ImVec2 screen;
|
|
if (false == world_to_screen(plan_to_world(pose.x, pose.y), screen))
|
|
{
|
|
continue;
|
|
}
|
|
auto const distance{static_cast<float>(std::hypot(Mouse.x - screen.x, Mouse.y - screen.y))};
|
|
if (distance < best)
|
|
{
|
|
best = distance;
|
|
OutIndex = i;
|
|
OutEnd = end;
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
bool plan_panel::hit_element(ImVec2 const &Mouse, ElementId &OutElement, TrackId &OutTrack) const
|
|
{
|
|
constexpr float tolerance{8.0f};
|
|
auto best{tolerance};
|
|
auto found{false};
|
|
for (auto const &rail : m_rails)
|
|
{
|
|
if (rail.element == ElementId::none || rail.points.size() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
for (std::size_t i = 1; i < rail.points.size(); ++i)
|
|
{
|
|
ImVec2 a;
|
|
ImVec2 b;
|
|
if (false == world_to_screen(plan_to_world(rail.points[i - 1].x, rail.points[i - 1].y), a) ||
|
|
false == world_to_screen(plan_to_world(rail.points[i].x, rail.points[i].y), b))
|
|
{
|
|
continue;
|
|
}
|
|
auto const distance{static_cast<float>(distance_point_segment(Mouse, a, b))};
|
|
if (distance < best)
|
|
{
|
|
best = distance;
|
|
OutElement = rail.element;
|
|
OutTrack = rail.track;
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
bool plan_panel::hit_turnout(ImVec2 const &Mouse, TurnoutId &OutTurnout) const
|
|
{
|
|
constexpr float tolerance{10.0f};
|
|
auto best{tolerance};
|
|
auto found{false};
|
|
for (auto const &turnout : m_solution.turnouts)
|
|
{
|
|
if (false == turnout.valid)
|
|
{
|
|
continue;
|
|
}
|
|
for (auto const &pose : {turnout.start, turnout.frog})
|
|
{
|
|
ImVec2 screen;
|
|
if (false == world_to_screen(plan_to_world(pose.x, pose.y), screen))
|
|
{
|
|
continue;
|
|
}
|
|
auto const distance{std::hypot(Mouse.x - screen.x, Mouse.y - screen.y)};
|
|
if (distance < best)
|
|
{
|
|
best = distance;
|
|
OutTurnout = turnout.id;
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
bool plan_panel::station_on(TrackId const Track_, double const Wx, double const Wy, double &OutStation) const
|
|
{
|
|
auto const *track{editor::plan::find_track(m_solution, Track_)};
|
|
if (track == nullptr || track->centreline.size() < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto best{std::numeric_limits<double>::max()};
|
|
double walked{0.0};
|
|
for (std::size_t i = 1; i < track->centreline.size(); ++i)
|
|
{
|
|
auto const &a{track->centreline[i - 1]};
|
|
auto const &b{track->centreline[i]};
|
|
auto const dx{b.x - a.x};
|
|
auto const dy{b.y - a.y};
|
|
auto const len2{dx * dx + dy * dy};
|
|
auto const step{std::sqrt(len2)};
|
|
if (step <= 0.0)
|
|
{
|
|
continue;
|
|
}
|
|
auto t{((Wx - a.x) * dx + (Wy - a.y) * dy) / len2};
|
|
t = std::clamp(t, 0.0, 1.0);
|
|
auto const distance{std::hypot(Wx - (a.x + dx * t), Wy - (a.y + dy * t))};
|
|
if (distance < best)
|
|
{
|
|
best = distance;
|
|
OutStation = walked + step * t;
|
|
}
|
|
walked += step;
|
|
}
|
|
return best < std::numeric_limits<double>::max();
|
|
}
|
|
|
|
plan_panel::TrackId plan_panel::nearest_track_axis(ImVec2 const &Mouse, float const Tolerance) const
|
|
{
|
|
auto best{Tolerance};
|
|
auto found{TrackId::none};
|
|
for (auto const &track : m_solution.tracks)
|
|
{
|
|
for (std::size_t i = 1; i < track.centreline.size(); ++i)
|
|
{
|
|
ImVec2 a;
|
|
ImVec2 b;
|
|
if (false == world_to_screen(plan_to_world(track.centreline[i - 1].x, track.centreline[i - 1].y), a) ||
|
|
false == world_to_screen(plan_to_world(track.centreline[i].x, track.centreline[i].y), b))
|
|
{
|
|
continue;
|
|
}
|
|
auto const distance{static_cast<float>(distance_point_segment(Mouse, a, b))};
|
|
if (distance < best)
|
|
{
|
|
best = distance;
|
|
found = track.id;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// the scene
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void plan_panel::handle_scene()
|
|
{
|
|
auto const &io{ImGui::GetIO()};
|
|
if (io.WantCaptureMouse && false == m_dragging_turnout)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const mouse{io.MousePos};
|
|
auto const world{ortho_cursor_world()};
|
|
double wx{0.0};
|
|
double wy{0.0};
|
|
world_to_plan(world, wx, wy);
|
|
|
|
// Esc drops every transient errand back to plain selection
|
|
// this ImGui indexes IsKeyPressed by the backend's own key codes; ImGuiKey_ names are slots in
|
|
// io.KeyMap, so the mapping has to be gone through rather than passed straight in
|
|
auto const &keymap{ImGui::GetIO().KeyMap};
|
|
if (keymap[ImGuiKey_Escape] >= 0 && ImGui::IsKeyPressed(keymap[ImGuiKey_Escape]))
|
|
{
|
|
m_pick_turnout = 0;
|
|
m_pick_parallel = 0;
|
|
m_pick_join = 0;
|
|
m_pick_split = 0;
|
|
m_pick_cut = 0;
|
|
m_pick_start = 0;
|
|
m_pick_draw = 0;
|
|
m_pick_pair = 0;
|
|
m_join_first = TrackId::none;
|
|
m_pending = false;
|
|
clear_selection();
|
|
m_status.clear();
|
|
}
|
|
|
|
// --- transient errands ------------------------------------------------
|
|
if (m_pick_turnout == 1 && ImGui::IsMouseClicked(0))
|
|
{
|
|
auto const on{nearest_track_axis(mouse, 14.0f)};
|
|
if (on != TrackId::none)
|
|
{
|
|
place_turnout_at(on, wx, wy);
|
|
m_pick_turnout = 0;
|
|
}
|
|
else
|
|
{
|
|
m_status = "kliknij tor, na ktorym ma stanac rozjazd";
|
|
}
|
|
return;
|
|
}
|
|
if (m_pick_parallel != 0 && ImGui::IsMouseClicked(0))
|
|
{
|
|
ElementId reference{ElementId::none};
|
|
TrackId owner{TrackId::none};
|
|
auto *track{current_track()};
|
|
if (track != nullptr && hit_element(mouse, reference, owner) && owner != m_track)
|
|
{
|
|
auto const it{std::find_if(track->elements.begin(), track->elements.end(), [&](Element const &Element_) { return Element_.id == m_sel_element; })};
|
|
if (it != track->elements.end())
|
|
{
|
|
it->hold = Parallel{reference, m_new_offset};
|
|
m_status = "trzyma rownoleglosc, odstep " + to_string(m_new_offset, 2) + " m";
|
|
solve();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_status = "wskaz odcinek innego toru";
|
|
}
|
|
m_pick_parallel = 0;
|
|
return;
|
|
}
|
|
if (m_pick_join != 0 && ImGui::IsMouseClicked(0))
|
|
{
|
|
TrackId which{TrackId::none};
|
|
int end{0};
|
|
if (false == hit_track_end(mouse, 18.0f, which, end))
|
|
{
|
|
m_status = "kliknij w koniec toru - zapalone konce sa te, ktore da sie polaczyc";
|
|
return;
|
|
}
|
|
if (m_pick_join == 1)
|
|
{
|
|
m_join_first = which;
|
|
m_join_first_end = end;
|
|
m_pick_join = 2;
|
|
m_status = "teraz wskaz drugi koniec";
|
|
return;
|
|
}
|
|
if (which == m_join_first)
|
|
{
|
|
m_status = "to ten sam tor";
|
|
return;
|
|
}
|
|
join_ends(m_join_first, m_join_first_end, which, end);
|
|
return;
|
|
}
|
|
if (m_pick_pair != 0 && ImGui::IsMouseClicked(0))
|
|
{
|
|
TurnoutId against{TurnoutId::none};
|
|
auto *placement{editor::plan::find_turnout(m_document, m_sel_turnout)};
|
|
if (placement == nullptr)
|
|
{
|
|
m_pick_pair = 0;
|
|
return;
|
|
}
|
|
if (false == hit_turnout(mouse, against))
|
|
{
|
|
m_status = "kliknij rozjazd, naprzeciw ktorego ten ma stanac";
|
|
return;
|
|
}
|
|
if (against == placement->id)
|
|
{
|
|
m_status = "rozjazd nie stanie naprzeciw samego siebie";
|
|
return;
|
|
}
|
|
auto const *other{editor::plan::find_turnout(m_document, against)};
|
|
if (other != nullptr && other->opposite == placement->id)
|
|
{
|
|
m_status = "tamten juz stoi naprzeciw tego - jeden z pary wystarczy";
|
|
return;
|
|
}
|
|
if (other != nullptr && other->on == placement->on)
|
|
{
|
|
m_status = "oba rozjazdy stoja na tym samym torze - przejscie laczy dwa rozne";
|
|
return;
|
|
}
|
|
placement->opposite = against;
|
|
m_pick_pair = 0;
|
|
solve();
|
|
m_status = "sparowane w przejscie rozjazdowe: km wynika teraz z drugiej iglicy";
|
|
return;
|
|
}
|
|
if (m_pick_start != 0 && ImGui::IsMouseClicked(0))
|
|
{
|
|
auto *track{current_track()};
|
|
if (track == nullptr || false == track->elements.empty())
|
|
{
|
|
m_pick_start = 0;
|
|
return;
|
|
}
|
|
if (m_pick_start == 1)
|
|
{
|
|
m_startx = wx;
|
|
m_starty = wy;
|
|
m_pick_start = 2;
|
|
m_status = "teraz wskaz kierunek, w ktorym tor ma iesc";
|
|
return;
|
|
}
|
|
auto const dx{wx - m_startx};
|
|
auto const dy{wy - m_starty};
|
|
auto const reach{std::hypot(dx, dy)};
|
|
if (reach < 1.0)
|
|
{
|
|
m_status = "za blisko poczatku - kierunku nie da sie z tego odczytac";
|
|
return;
|
|
}
|
|
track->anchor = editor::plan::AtPose{m_startx, m_starty, std::atan2(dx, dy)};
|
|
// a straight is as long as the two clicks say; a curve's length is its own, because the
|
|
// distance between two clicks says nothing about how far an arc runs
|
|
auto const kind{m_start_kind};
|
|
auto const length{kind == Kind::Line ? reach : m_new_length};
|
|
Element element;
|
|
element.id = editor::plan::mint_element(m_document);
|
|
element.kind = kind;
|
|
element.radius = kind == Kind::Line ? 0.0 : m_new_radius;
|
|
element.hand = kind == Kind::Line ? 0 : m_new_hand;
|
|
element.length = length;
|
|
track->elements.push_back(element);
|
|
m_sel_element = element.id;
|
|
m_trace[static_cast<std::uint32_t>(track->id)] = {editor::plan::PlanPoint{m_startx, m_starty}, editor::plan::PlanPoint{wx, wy}};
|
|
m_pick_start = 0;
|
|
solve();
|
|
m_status = std::string{"tor zaczyna sie na klikietym punkcie: "} + kind_name(kind) + " " + to_string(length, 1) + " m";
|
|
return;
|
|
}
|
|
if ((m_pick_split != 0 || m_pick_cut != 0) && ImGui::IsMouseClicked(0))
|
|
{
|
|
auto const on{nearest_track_axis(mouse, 14.0f)};
|
|
double station{0.0};
|
|
if (on == TrackId::none || false == station_on(on, wx, wy, station))
|
|
{
|
|
m_status = "kliknij w os toru - tam, gdzie ma przejsc ciecie";
|
|
return;
|
|
}
|
|
if (m_pick_split != 0)
|
|
{
|
|
split_at(on, station);
|
|
m_pick_split = 0;
|
|
return;
|
|
}
|
|
if (m_pick_cut == 1)
|
|
{
|
|
m_cut_track = on;
|
|
m_cut_from = station;
|
|
m_pick_cut = 2;
|
|
m_status = "teraz drugi koniec wycinanego odcinka";
|
|
return;
|
|
}
|
|
if (on != m_cut_track)
|
|
{
|
|
m_status = "oba konce musza lezec na tym samym torze";
|
|
return;
|
|
}
|
|
cut_between(m_cut_track, m_cut_from, station);
|
|
m_pick_cut = 0;
|
|
return;
|
|
}
|
|
|
|
// --- ctrl: grabbing a straight by one of its ends ---------------------
|
|
// this is the one thing a modifier is needed for: the ends of a straight sit on the track, so
|
|
// a plain click there has to stay a plain click
|
|
if (io.KeyCtrl && ImGui::IsMouseClicked(0))
|
|
{
|
|
std::size_t index{0};
|
|
int end{0};
|
|
if (hit_straight_end(mouse, index, end))
|
|
{
|
|
auto const *solved{editor::plan::find_track(m_solution, m_track)};
|
|
auto const *track{current_track()};
|
|
if (solved != nullptr && index < solved->elements.size())
|
|
{
|
|
// the far end is captured now: re-fitting the arc behind it will move it
|
|
auto const &pivot{end == 1 ? solved->elements[index].start : solved->elements[index].end};
|
|
m_pivot_x = pivot.x;
|
|
m_pivot_y = pivot.y;
|
|
m_drag_index = index;
|
|
m_drag_end = end;
|
|
m_dragging_straight = true;
|
|
m_sel_element = track->elements[index].id;
|
|
if (track != nullptr && std::holds_alternative<editor::plan::Parallel>(track->elements[index].hold))
|
|
{
|
|
m_status = "ten odcinek trzyma rownoleglosc - po puszczeniu tor i tak zostanie przystawiony";
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (m_dragging_straight)
|
|
{
|
|
if (ImGui::IsMouseDown(0))
|
|
{
|
|
drag_straight_to(wx, wy);
|
|
}
|
|
else
|
|
{
|
|
m_dragging_straight = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// --- one click, whatever is under it ----------------------------------
|
|
// clicking something that is already drawn picks it up; clicking bare ground carries the
|
|
// current track on to there. there is nothing to switch between: what you clicked says what
|
|
// you meant
|
|
if (ImGui::IsMouseClicked(0))
|
|
{
|
|
TurnoutId turnout{TurnoutId::none};
|
|
ElementId element{ElementId::none};
|
|
TrackId track{TrackId::none};
|
|
|
|
if (m_pick_draw != 0)
|
|
{
|
|
// the drawing tool has the ground: a click carries the track on, wherever it lands
|
|
append_point_to(wx, wy);
|
|
}
|
|
else if (hit_turnout(mouse, turnout))
|
|
{
|
|
m_sel_turnout = turnout;
|
|
m_dragging_turnout = true;
|
|
}
|
|
else if (hit_element(mouse, element, track))
|
|
{
|
|
m_sel_element = element;
|
|
m_track = track;
|
|
auto const *chosen{editor::plan::find_track(m_document, track)};
|
|
if (chosen != nullptr)
|
|
{
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", chosen->name.c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
clear_selection();
|
|
}
|
|
}
|
|
|
|
// right-click takes back the last click - only while there is a run of clicks to take back
|
|
if (m_pick_draw != 0 && ImGui::IsMouseClicked(1))
|
|
{
|
|
drop_last_vertex();
|
|
}
|
|
|
|
if (ImGui::IsMouseDown(0) && m_dragging_turnout && m_sel_turnout != TurnoutId::none)
|
|
{
|
|
auto *placement{editor::plan::find_turnout(m_document, m_sel_turnout)};
|
|
// a turnout standing against another one has no station of its own to drag: grabbing it
|
|
// drags the one it stands against, so the whole przejscie slides as one
|
|
if (placement != nullptr && placement->opposite != TurnoutId::none)
|
|
{
|
|
placement = editor::plan::find_turnout(m_document, placement->opposite);
|
|
}
|
|
if (placement != nullptr)
|
|
{
|
|
double station{0.0};
|
|
if (station_on(placement->on, wx, wy, station))
|
|
{
|
|
placement->station = station;
|
|
solve();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ImGui::IsMouseReleased(0))
|
|
{
|
|
m_dragging_turnout = false;
|
|
}
|
|
|
|
// double-clicking anywhere on a track makes it the one being edited
|
|
if (ImGui::IsMouseDoubleClicked(0))
|
|
{
|
|
auto const track{nearest_track_axis(mouse, 16.0f)};
|
|
if (track != TrackId::none)
|
|
{
|
|
m_track = track;
|
|
auto const *chosen{editor::plan::find_track(m_document, track)};
|
|
if (chosen != nullptr)
|
|
{
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", chosen->name.c_str());
|
|
m_status = "edytowany: " + chosen->name;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void plan_panel::draw_on_scene()
|
|
{
|
|
// the background list paints over the rendered scenery but under every window, so the controls
|
|
// stay readable on top of the drawing
|
|
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
|
|
|
// orthophoto under everything else. only a georeferenced map knows where on the ground it sits,
|
|
// so a fictional one gets no imagery
|
|
if (m_showortho && Global.scenery_georeferenced)
|
|
{
|
|
double planx{0.0};
|
|
double plany{0.0};
|
|
world_to_plan(Global.pCamera.Pos, planx, plany);
|
|
|
|
// yaw is pinned north-up, so the screen is an axis-aligned box in plan space
|
|
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)};
|
|
editor::plan::TileBBox const view{planx - halfwidth, plany - halfheight, planx + halfwidth, plany + halfheight};
|
|
|
|
auto const draw_box = [&](editor::plan::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]);
|
|
if (false == ok)
|
|
{
|
|
return;
|
|
}
|
|
// north-west corner first, and the texture's first row (v=0) is its northern edge
|
|
drawlist->AddImageQuad(reinterpret_cast<ImTextureID>(static_cast<intptr_t>(Texture)), corners[0], corners[1], corners[2], corners[3], ImVec2(0.0f, 0.0f), ImVec2(1.0f, 0.0f),
|
|
ImVec2(1.0f, 1.0f), ImVec2(0.0f, 1.0f));
|
|
};
|
|
|
|
// once the view is wider than a kilometre the 100 m orto cells are the wrong scale:
|
|
// switch to a 1 km topo grid that can cover the whole screen
|
|
constexpr double kTopoPreviewMetres = 1000.0;
|
|
auto const span{std::max(halfwidth, halfheight) * 2.0};
|
|
if (span > kTopoPreviewMetres)
|
|
{
|
|
for (auto const &tile : m_topo.collect(view, 80))
|
|
{
|
|
draw_box(tile.box, tile.texture);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (auto const &tile : m_ortho.collect(view))
|
|
{
|
|
draw_box(tile.box, tile.texture);
|
|
}
|
|
}
|
|
}
|
|
|
|
// the track itself
|
|
std::vector<ImVec2> points;
|
|
for (auto const &rail : m_rails)
|
|
{
|
|
if (rail.points.size() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
auto const active{rail.track == m_track || rail.turnout != TurnoutId::none};
|
|
points.clear();
|
|
points.reserve(rail.points.size());
|
|
for (auto const &point : rail.points)
|
|
{
|
|
ImVec2 screen;
|
|
if (false == world_to_screen(plan_to_world(point.x, point.y), screen))
|
|
{
|
|
continue;
|
|
}
|
|
points.push_back(screen);
|
|
}
|
|
if (points.size() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
auto colour{element_colour(rail.kind, active)};
|
|
auto width{2.5f};
|
|
if (rail.turnout != TurnoutId::none)
|
|
{
|
|
// every element of a rozjazd is drawn as itself: the pre-blade lead runs along
|
|
// the through track, the blade is the piece that leaves the opornica, then the
|
|
// curve and the straight frog rail
|
|
switch (rail.part)
|
|
{
|
|
case 0:
|
|
// the pre-blade lead lies exactly on the through track, so it has to be
|
|
// drawn wide and in a colour of its own or it simply is not there
|
|
colour = IM_COL32(140, 240, 150, 255); // przediglicowy
|
|
width = 4.5f;
|
|
break;
|
|
case 1:
|
|
colour = IM_COL32(120, 235, 255, 245); // iglica
|
|
break;
|
|
case 3:
|
|
colour = IM_COL32(255, 200, 110, 235); // prosta krzyzownicowa
|
|
break;
|
|
default:
|
|
colour = IM_COL32(255, 140, 200, 230); // luk
|
|
break;
|
|
}
|
|
}
|
|
else if (rail.element == m_sel_element && m_sel_element != ElementId::none)
|
|
{
|
|
colour = IM_COL32(255, 255, 255, 245);
|
|
}
|
|
drawlist->AddPolyline(points.data(), static_cast<int>(points.size()), colour, false, width);
|
|
}
|
|
|
|
// turnouts: PR, the frog, and the construction points a drawing is dimensioned from
|
|
for (auto const &turnout : m_solution.turnouts)
|
|
{
|
|
if (false == turnout.valid)
|
|
{
|
|
continue;
|
|
}
|
|
auto const selected{turnout.id == m_sel_turnout};
|
|
|
|
// the points that sit on the straight - PR, the blade tip and the koniec rozjazdu -
|
|
// are drawn the way a catalogue drawing marks them: a tick across the track, not a
|
|
// dot lost among the rails the turnout runs along
|
|
auto const tick{[&](editor::plan::TurnoutMark const &Mark_, ImU32 const Colour_, float const Half_) {
|
|
ImVec2 centre;
|
|
if (false == world_to_screen(plan_to_world(Mark_.x, Mark_.y), centre))
|
|
{
|
|
return;
|
|
}
|
|
ImVec2 along;
|
|
if (false == world_to_screen(plan_to_world(Mark_.x + turnout.start.hx, Mark_.y + turnout.start.hy), along))
|
|
{
|
|
return;
|
|
}
|
|
auto dx{along.x - centre.x};
|
|
auto dy{along.y - centre.y};
|
|
auto const len{std::sqrt(dx * dx + dy * dy)};
|
|
if (len < 1e-3f)
|
|
{
|
|
return;
|
|
}
|
|
dx /= len;
|
|
dy /= len;
|
|
drawlist->AddLine(ImVec2(centre.x + dy * Half_, centre.y - dx * Half_), ImVec2(centre.x - dy * Half_, centre.y + dx * Half_), Colour_, 2.0f);
|
|
}};
|
|
|
|
for (auto const &mark : turnout.marks)
|
|
{
|
|
ImVec2 screen;
|
|
if (false == world_to_screen(plan_to_world(mark.x, mark.y), screen))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// PR and KR carry the switch - both on the diverging path and, for the
|
|
// koniec rozjazdu, on the straight it stands on. the ostrze is the other
|
|
// point worth finding on the drawing, so it gets a colour of its own
|
|
auto const primary{mark.kind == 0 || mark.kind == 5 || mark.kind == 9};
|
|
auto const blade{mark.kind == 1 || mark.kind == 7 || mark.kind == 8};
|
|
drawlist->AddCircleFilled(screen, primary ? (selected ? 6.0f : 5.0f) : (blade ? 4.0f : 3.0f),
|
|
primary ? IM_COL32(255, 210, 90, 240) : (blade ? IM_COL32(120, 235, 255, 230) : IM_COL32(255, 150, 210, 190)));
|
|
}
|
|
for (auto const &mark : turnout.marks)
|
|
{
|
|
switch (mark.kind)
|
|
{
|
|
case 0: // PR
|
|
case 9: // koniec rozjazdu na torze zasadniczym
|
|
tick(mark, IM_COL32(255, 210, 90, 245), selected ? 14.0f : 11.0f);
|
|
break;
|
|
case 1: // ostrze iglicy: koniec odcinka przediglicowego
|
|
tick(mark, IM_COL32(120, 235, 255, 245), 9.0f);
|
|
break;
|
|
case 7: // A: koniec zestruganego dzioba, iglica ma tu grubosc u
|
|
tick(mark, IM_COL32(140, 240, 150, 245), 7.0f);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// the blade's own geometry lives in the first few metres of the switch, so at any
|
|
// zoom that shows the whole rozjazd its points land on top of one another. named
|
|
// on leader lines fanned out to the side they can at least be told apart
|
|
if (selected)
|
|
{
|
|
auto ordered{turnout.marks};
|
|
std::sort(ordered.begin(), ordered.end(),
|
|
[](auto const &Left_, auto const &Right_) { return Left_.station < Right_.station; });
|
|
|
|
auto level{0};
|
|
for (auto const &mark : ordered)
|
|
{
|
|
if (mark.kind < 0 || mark.kind >= static_cast<int>(sizeof(mark_names) / sizeof(mark_names[0])))
|
|
{
|
|
continue;
|
|
}
|
|
ImVec2 centre;
|
|
ImVec2 along;
|
|
if (false == world_to_screen(plan_to_world(mark.x, mark.y), centre) ||
|
|
false == world_to_screen(plan_to_world(mark.x + turnout.start.hx, mark.y + turnout.start.hy), along))
|
|
{
|
|
continue;
|
|
}
|
|
auto dx{along.x - centre.x};
|
|
auto dy{along.y - centre.y};
|
|
auto const len{std::sqrt(dx * dx + dy * dy)};
|
|
if (len < 1e-3f)
|
|
{
|
|
continue;
|
|
}
|
|
dx /= len;
|
|
dy /= len;
|
|
|
|
// alternate sides and step further out on each pass, so the leaders fan
|
|
// instead of stacking on one line
|
|
auto const side{(level % 2) == 0 ? 1.0f : -1.0f};
|
|
auto const reach{(30.0f + 17.0f * static_cast<float>(level / 2)) * side};
|
|
ImVec2 const anchor(centre.x + dy * reach, centre.y - dx * reach);
|
|
|
|
char label[96];
|
|
if (mark.station > 0.0)
|
|
{
|
|
std::snprintf(label, sizeof(label), "%s %.3f", mark_names[mark.kind], mark.station);
|
|
}
|
|
else
|
|
{
|
|
std::snprintf(label, sizeof(label), "%s", mark_names[mark.kind]);
|
|
}
|
|
auto const size{ImGui::CalcTextSize(label)};
|
|
ImVec2 const corner(anchor.x - size.x * 0.5f, anchor.y - size.y * 0.5f);
|
|
|
|
drawlist->AddLine(centre, anchor, IM_COL32(200, 200, 200, 130), 1.0f);
|
|
drawlist->AddRectFilled(ImVec2(corner.x - 3.0f, corner.y - 2.0f), ImVec2(corner.x + size.x + 3.0f, corner.y + size.y + 2.0f),
|
|
IM_COL32(15, 20, 30, 205), 3.0f);
|
|
drawlist->AddText(corner, IM_COL32(235, 240, 250, 245), label);
|
|
++level;
|
|
}
|
|
}
|
|
}
|
|
|
|
// where a chain was cut short, say so on the spot rather than only in the list
|
|
for (auto const &track : m_solution.tracks)
|
|
{
|
|
if (track.complete || track.elements.empty())
|
|
{
|
|
continue;
|
|
}
|
|
ImVec2 screen;
|
|
if (world_to_screen(plan_to_world(track.end.x, track.end.y), screen))
|
|
{
|
|
drawlist->AddCircle(screen, 9.0f, IM_COL32(255, 110, 110, 230), 0, 2.0f);
|
|
drawlist->AddLine(ImVec2(screen.x - 6.0f, screen.y - 6.0f), ImVec2(screen.x + 6.0f, screen.y + 6.0f), IM_COL32(255, 110, 110, 230), 2.0f);
|
|
drawlist->AddLine(ImVec2(screen.x - 6.0f, screen.y + 6.0f), ImVec2(screen.x + 6.0f, screen.y - 6.0f), IM_COL32(255, 110, 110, 230), 2.0f);
|
|
}
|
|
}
|
|
|
|
// the ends of the edited track's straights, while ctrl says they are what you are after
|
|
if (ImGui::GetIO().KeyCtrl)
|
|
{
|
|
auto const *solved{editor::plan::find_track(m_solution, m_track)};
|
|
auto const *track{current_track()};
|
|
if (solved != nullptr && track != nullptr)
|
|
{
|
|
for (std::size_t i = 0; i < solved->elements.size() && i < track->elements.size(); ++i)
|
|
{
|
|
if (track->elements[i].kind != Kind::Line)
|
|
{
|
|
continue;
|
|
}
|
|
auto const pinned{i == 0 && false == std::holds_alternative<editor::plan::AtPose>(track->anchor)};
|
|
for (int end = 0; end < 2; ++end)
|
|
{
|
|
if (end == 0 && pinned)
|
|
{
|
|
continue;
|
|
}
|
|
auto const &pose{end == 0 ? solved->elements[i].start : solved->elements[i].end};
|
|
ImVec2 screen;
|
|
if (false == world_to_screen(plan_to_world(pose.x, pose.y), screen))
|
|
{
|
|
continue;
|
|
}
|
|
auto const grabbed{m_dragging_straight && i == m_drag_index && end == m_drag_end};
|
|
auto const size{grabbed ? 5.0f : 4.0f};
|
|
drawlist->AddRectFilled(ImVec2(screen.x - size, screen.y - size), ImVec2(screen.x + size, screen.y + size),
|
|
grabbed ? IM_COL32(255, 255, 255, 245) : IM_COL32(255, 240, 140, 200));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// where the run of clicks starts, before there is any track to show for it
|
|
if (m_pending || m_pick_start == 2)
|
|
{
|
|
ImVec2 screen;
|
|
if (world_to_screen(plan_to_world(m_startx, m_starty), screen))
|
|
{
|
|
drawlist->AddCircle(screen, 6.0f, IM_COL32(255, 240, 140, 230), 0, 2.0f);
|
|
}
|
|
}
|
|
|
|
draw_growth_cursor();
|
|
|
|
// scalanie: every end a join could be made from or to, and what would be laid between the one
|
|
// already picked and whichever the cursor is over
|
|
if (m_pick_join != 0)
|
|
{
|
|
auto const mouse{ImGui::GetIO().MousePos};
|
|
TrackId hovered{TrackId::none};
|
|
int hovered_end{0};
|
|
auto const over{hit_track_end(mouse, 18.0f, hovered, hovered_end)};
|
|
|
|
for (auto const &track : m_solution.tracks)
|
|
{
|
|
for (int end = 0; end < 2; ++end)
|
|
{
|
|
if (false == loose_end(track, end))
|
|
{
|
|
continue;
|
|
}
|
|
auto const &pose{end == 0 ? track.start : track.end};
|
|
ImVec2 screen;
|
|
if (false == world_to_screen(plan_to_world(pose.x, pose.y), screen))
|
|
{
|
|
continue;
|
|
}
|
|
auto const picked{m_pick_join == 2 && track.id == m_join_first && end == m_join_first_end};
|
|
auto const under{over && track.id == hovered && end == hovered_end};
|
|
auto const colour{picked ? IM_COL32(120, 255, 160, 250) : under ? IM_COL32(255, 255, 255, 250) : IM_COL32(255, 240, 140, 190)};
|
|
// a start is a square and an end is a circle, so which way a join would run is
|
|
// there to be seen and not guessed at
|
|
if (end == 0)
|
|
{
|
|
drawlist->AddRect(ImVec2(screen.x - 6.0f, screen.y - 6.0f), ImVec2(screen.x + 6.0f, screen.y + 6.0f), colour, 0.0f, 0, picked || under ? 3.0f : 1.5f);
|
|
}
|
|
else
|
|
{
|
|
drawlist->AddCircle(screen, 6.0f, colour, 0, picked || under ? 3.0f : 1.5f);
|
|
}
|
|
// the whole track lights up with the end under the cursor, so there is no doubt
|
|
// about which one is being taken hold of
|
|
if (false == (picked || under) || track.centreline.size() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
std::vector<ImVec2> axis;
|
|
axis.reserve(track.centreline.size());
|
|
for (auto const &point : track.centreline)
|
|
{
|
|
ImVec2 at;
|
|
if (world_to_screen(plan_to_world(point.x, point.y), at))
|
|
{
|
|
axis.push_back(at);
|
|
}
|
|
}
|
|
if (axis.size() > 1)
|
|
{
|
|
drawlist->AddPolyline(axis.data(), static_cast<int>(axis.size()), (colour & 0x00ffffffu) | 0x66000000u, false, 6.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// the proposal itself, before anything is done to the document. it runs from the
|
|
// end pointed at first to the one under the cursor, whichever way round the two
|
|
// tracks happen to be written down: an end is left the way it points, a start is
|
|
// left the way it came, and the arriving end reads the other way about
|
|
if (m_pick_join == 2 && over && hovered != m_join_first)
|
|
{
|
|
auto const *head{editor::plan::find_track(m_solution, m_join_first)};
|
|
auto const *tail{editor::plan::find_track(m_solution, hovered)};
|
|
if (head != nullptr && tail != nullptr)
|
|
{
|
|
auto const leaving{join_pose(*head, m_join_first_end, true)};
|
|
auto const arriving{join_pose(*tail, hovered_end, false)};
|
|
editor::plan::JoinSettings settings;
|
|
settings.radius = m_join_radius;
|
|
auto const plan{editor::plan::plan_join(leaving, arriving, settings)};
|
|
|
|
std::string label;
|
|
if (plan.ok)
|
|
{
|
|
std::vector<editor::plan::geometry::XY> points;
|
|
auto walker{leaving};
|
|
for (auto const &element : plan.elements)
|
|
{
|
|
auto const k{element.kind == Kind::Line || element.radius <= 0.0 ? 0.0 : element.hand / element.radius};
|
|
walker = editor::plan::geometry::layout_segment(k, k, element.length, walker, &points);
|
|
}
|
|
std::vector<ImVec2> line;
|
|
line.reserve(points.size());
|
|
for (auto const &point : points)
|
|
{
|
|
ImVec2 screen;
|
|
if (world_to_screen(plan_to_world(point.x, point.y), screen))
|
|
{
|
|
line.push_back(screen);
|
|
}
|
|
}
|
|
if (line.size() > 1)
|
|
{
|
|
dashed_polyline(drawlist, line, IM_COL32(120, 255, 160, 235), 2.5f);
|
|
}
|
|
label = editor::plan::join_kind_name(plan.kind);
|
|
auto laid{0.0};
|
|
for (auto const &element : plan.elements)
|
|
{
|
|
laid += element.length;
|
|
}
|
|
if (laid > 0.0)
|
|
{
|
|
label += ", " + to_string(laid, 1) + " m";
|
|
}
|
|
if (plan.tightest > 0.0)
|
|
{
|
|
label += ", R=" + to_string(plan.tightest, 0) + " m";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
label = plan.why;
|
|
}
|
|
|
|
auto const size{ImGui::CalcTextSize(label.c_str())};
|
|
ImVec2 const corner(mouse.x + 16.0f, mouse.y + 16.0f);
|
|
drawlist->AddRectFilled(ImVec2(corner.x - 4.0f, corner.y - 3.0f), ImVec2(corner.x + size.x + 4.0f, corner.y + size.y + 3.0f), IM_COL32(15, 20, 30, 215), 3.0f);
|
|
drawlist->AddText(corner, plan.ok ? IM_COL32(160, 255, 190, 250) : IM_COL32(255, 190, 110, 250), label.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void plan_panel::draw_growth_cursor()
|
|
{
|
|
// the cursor says what the drawing tool would do, so it is drawn when that tool is in hand
|
|
if (m_pick_draw == 0 && m_pick_start == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
|
auto const &io{ImGui::GetIO()};
|
|
auto const over_ground{false == io.WantCaptureMouse};
|
|
double wx{0.0};
|
|
double wy{0.0};
|
|
world_to_plan(ortho_cursor_world(), wx, wy);
|
|
|
|
auto const label_at = [&](ImVec2 const &At, char const *Text, ImU32 const Colour) {
|
|
auto const size{ImGui::CalcTextSize(Text)};
|
|
ImVec2 const corner(At.x + 14.0f, At.y + 14.0f);
|
|
drawlist->AddRectFilled(ImVec2(corner.x - 4.0f, corner.y - 3.0f), ImVec2(corner.x + size.x + 4.0f, corner.y + size.y + 3.0f), IM_COL32(15, 20, 30, 215), 3.0f);
|
|
drawlist->AddText(corner, Colour, Text);
|
|
};
|
|
|
|
// an empty track: the cursor is the direction the first element will run in
|
|
if (m_pick_start != 0)
|
|
{
|
|
ImVec2 from;
|
|
ImVec2 to;
|
|
if (m_pick_start == 2 && over_ground && world_to_screen(plan_to_world(m_startx, m_starty), from) && world_to_screen(plan_to_world(wx, wy), to))
|
|
{
|
|
dashed_polyline(drawlist, {from, to}, IM_COL32(255, 240, 140, 220), 2.0f);
|
|
auto const reach{std::hypot(wx - m_startx, wy - m_starty)};
|
|
auto const text{std::string{kind_name(m_start_kind)} + (m_start_kind == Kind::Line ? " " + to_string(reach, 1) + " m" : " R=" + to_string(m_new_radius, 0) + ", " + to_string(m_new_length, 1) + " m")};
|
|
label_at(to, text.c_str(), IM_COL32(255, 240, 140, 245));
|
|
}
|
|
else if (over_ground && world_to_screen(plan_to_world(wx, wy), to))
|
|
{
|
|
label_at(to, "tu zacznie sie tor", IM_COL32(255, 240, 140, 245));
|
|
}
|
|
return;
|
|
}
|
|
|
|
editor::plan::geometry::Pose end{};
|
|
if (false == growth_pose(end))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ImVec2 at;
|
|
if (false == world_to_screen(plan_to_world(end.x, end.y), at))
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::string why;
|
|
auto const may{can_grow(why)};
|
|
auto const colour{may ? IM_COL32(120, 255, 160, 245) : IM_COL32(255, 120, 120, 245)};
|
|
|
|
// the growing end, and which way the chain is heading there: a chevron, because a circle alone
|
|
// does not say which of the two ends this is
|
|
ImVec2 ahead;
|
|
if (world_to_screen(plan_to_world(end.x + end.hx, end.y + end.hy), ahead))
|
|
{
|
|
auto dx{ahead.x - at.x};
|
|
auto dy{ahead.y - at.y};
|
|
auto const len{std::sqrt(dx * dx + dy * dy)};
|
|
if (len > 1e-3f)
|
|
{
|
|
dx /= len;
|
|
dy /= len;
|
|
ImVec2 const tip(at.x + dx * 15.0f, at.y + dy * 15.0f);
|
|
drawlist->AddLine(at, tip, colour, 2.5f);
|
|
drawlist->AddLine(tip, ImVec2(tip.x - dx * 7.0f - dy * 5.0f, tip.y - dy * 7.0f + dx * 5.0f), colour, 2.5f);
|
|
drawlist->AddLine(tip, ImVec2(tip.x - dx * 7.0f + dy * 5.0f, tip.y - dy * 7.0f - dx * 5.0f), colour, 2.5f);
|
|
}
|
|
}
|
|
drawlist->AddCircle(at, 7.0f, colour, 0, 2.5f);
|
|
|
|
if (false == may)
|
|
{
|
|
label_at(at, why.c_str(), IM_COL32(255, 170, 120, 245));
|
|
return;
|
|
}
|
|
if (false == over_ground)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// what a click would lay from here: the corner it turns at and the straight running out to the
|
|
// cursor, exactly as append_point_to works it out - so what is drawn is what will happen
|
|
auto const dx{wx - end.x};
|
|
auto const dy{wy - end.y};
|
|
auto const reach{std::hypot(dx, dy)};
|
|
if (reach < 1.0)
|
|
{
|
|
return;
|
|
}
|
|
auto const corner{editor::plan::fit_corner(end.hx, end.hy, dx / reach, dy / reach, m_corner_radius)};
|
|
auto const *track{current_track()};
|
|
auto const *tail{track != nullptr && false == track->elements.empty() ? &track->elements.back() : nullptr};
|
|
|
|
std::vector<editor::plan::geometry::XY> points;
|
|
std::string text;
|
|
auto ok{true};
|
|
if (false == corner.ok)
|
|
{
|
|
// straight on: the straight already there simply gets longer
|
|
editor::plan::geometry::layout_segment(0.0, 0.0, reach, end, &points);
|
|
text = "prosto dalej, +" + to_string(reach, 1) + " m";
|
|
}
|
|
else if (tail == nullptr || tail->kind != Kind::Line || tail->length <= corner.tangent + 0.5)
|
|
{
|
|
ok = false;
|
|
text = "naroznik za ostry na R=" + to_string(m_corner_radius, 0) + " m: potrzeba " + to_string(corner.tangent, 1) + " m stycznej";
|
|
}
|
|
else if (reach <= corner.tangent + 0.5)
|
|
{
|
|
ok = false;
|
|
text = "za blisko naroznika: potrzeba " + to_string(corner.tangent, 1) + " m stycznej";
|
|
}
|
|
else
|
|
{
|
|
// the arc starts a tangent back from the end, so the ghost starts there too
|
|
auto const back{editor::plan::geometry::layout_segment(0.0, 0.0, -corner.tangent, end, nullptr)};
|
|
auto const k{corner.hand / m_corner_radius};
|
|
auto const after{editor::plan::geometry::layout_segment(k, k, corner.arc_length, back, &points)};
|
|
editor::plan::geometry::layout_segment(0.0, 0.0, reach - corner.tangent, after, &points);
|
|
text = "luk R=" + to_string(m_corner_radius, 0) + " (" + to_string(corner.arc_length, 1) + " m) + prosta " + to_string(reach - corner.tangent, 1) + " m";
|
|
}
|
|
|
|
if (ok && points.size() > 1)
|
|
{
|
|
std::vector<ImVec2> line;
|
|
line.reserve(points.size());
|
|
for (auto const &point : points)
|
|
{
|
|
ImVec2 screen;
|
|
if (world_to_screen(plan_to_world(point.x, point.y), screen))
|
|
{
|
|
line.push_back(screen);
|
|
}
|
|
}
|
|
if (line.size() > 1)
|
|
{
|
|
dashed_polyline(drawlist, line, IM_COL32(120, 255, 160, 215), 2.0f);
|
|
}
|
|
}
|
|
|
|
ImVec2 cursor;
|
|
if (world_to_screen(plan_to_world(wx, wy), cursor))
|
|
{
|
|
label_at(cursor, text.c_str(), ok ? IM_COL32(160, 255, 190, 245) : IM_COL32(255, 190, 110, 245));
|
|
}
|
|
}
|
|
|
|
void plan_panel::go_to_plan()
|
|
{
|
|
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 &track : m_solution.tracks)
|
|
{
|
|
for (auto const &point : track.centreline)
|
|
{
|
|
minx = std::min(minx, point.x);
|
|
maxx = std::max(maxx, point.x);
|
|
miny = std::min(miny, point.y);
|
|
maxy = std::max(maxy, point.y);
|
|
}
|
|
}
|
|
if (minx > maxx)
|
|
{
|
|
return; // nothing drawn yet, so there is nowhere to go
|
|
}
|
|
|
|
auto &camera{editor_mode::get_camera()};
|
|
auto const centre{plan_to_world((minx + maxx) * 0.5, (miny + maxy) * 0.5)};
|
|
camera.Pos.x = centre.x;
|
|
camera.Pos.z = centre.z;
|
|
|
|
// show the whole drawing with a little room around it
|
|
auto const span{std::max({maxx - minx, maxy - miny, 50.0})};
|
|
Global.editor_ortho_extent = std::clamp(static_cast<float>(span * 0.6), 5.0f, 20000.0f);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// panels
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void plan_panel::render_toolbar()
|
|
{
|
|
ImGui::Text("widok z gory, %.0f m w poprzek", Global.editor_ortho_extent * 2.0f);
|
|
|
|
// what the drawing amounts to, and what it costs: the counts, how far the laid axis runs from
|
|
// the clicks it was drawn from, and the joints taken without a transition curve
|
|
{
|
|
std::size_t elements{0};
|
|
for (auto const &track : m_document.tracks)
|
|
{
|
|
elements += track.elements.size();
|
|
}
|
|
std::string line{std::to_string(m_document.tracks.size()) + " torow, " + std::to_string(elements) + " odcinkow, " +
|
|
std::to_string(m_document.turnouts.size()) + " rozjazdow"};
|
|
if (false == m_solution.diagnostics.empty())
|
|
{
|
|
line += ", " + std::to_string(m_solution.diagnostics.size()) + " uwag";
|
|
}
|
|
if (auto const *solved{editor::plan::find_track(m_solution, m_track)}; solved != nullptr && solved->length > 0.0)
|
|
{
|
|
line += " | edytowany: km " + chainage(0.0) + " ... " + chainage(solved->length);
|
|
if (auto const jumps{m_jumps.find(static_cast<std::uint32_t>(m_track))}; jumps != m_jumps.end() && jumps->second > 0)
|
|
{
|
|
line += ", " + std::to_string(jumps->second) + " zlacz bez krzywej przejsciowej";
|
|
}
|
|
if (auto const deviation{m_track_deviation.find(static_cast<std::uint32_t>(m_track))}; deviation != m_track_deviation.end())
|
|
{
|
|
line += ", od sladu max " + to_string(deviation->second.first, 2) + " m / rms " + to_string(deviation->second.second, 2) + " m";
|
|
}
|
|
}
|
|
ImGui::TextDisabled("%s", line.c_str());
|
|
}
|
|
ImGui::TextDisabled("klik wybiera to, co pod nim lezy; zeby prowadzic tor po terenie, wlacz Rysuj");
|
|
ImGui::TextDisabled("Ctrl + LPM na krancu prostej: przeciagniecie; luki obok dopasuja sie same");
|
|
if (Global.scenery_georeferenced)
|
|
{
|
|
ImGui::Text("zero mapy: %.0f, %.0f (EPSG:2180)", Global.scenery_origin.x, Global.scenery_origin.y);
|
|
}
|
|
else
|
|
{
|
|
ImGui::TextDisabled("mapa fikcyjna, bez georeferencji");
|
|
}
|
|
|
|
// history. this ImGui has no BeginDisabled, so a step that cannot be taken is said in words
|
|
// rather than shown as a dead button
|
|
if (m_undo.empty())
|
|
{
|
|
ImGui::TextDisabled("nic do cofniecia");
|
|
}
|
|
else if (ImGui::Button("Cofnij"))
|
|
{
|
|
undo();
|
|
}
|
|
else if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("Ctrl+Z; %d krokow wstecz", static_cast<int>(m_undo.size()));
|
|
}
|
|
ImGui::SameLine();
|
|
if (false == m_redo.empty())
|
|
{
|
|
if (ImGui::Button("Powtorz"))
|
|
{
|
|
redo();
|
|
}
|
|
else if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("Ctrl+Y albo Ctrl+Shift+Z; %d krokow w przod", static_cast<int>(m_redo.size()));
|
|
}
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
if (ImGui::Button("Nowa mapa"))
|
|
{
|
|
// opened from the same stack level the modal is drawn on, which is render_contents
|
|
ImGui::OpenPopup("New map");
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("wybor miejsca: mapa fikcyjna albo punkt w Polsce, na ktorym staje zero scenerii");
|
|
}
|
|
if (Global.scenery_georeferenced)
|
|
{
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Ortofoto", &m_showortho);
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("klawisz 1 przelacza podklad: ortofoto z bliska, mapa topograficzna z daleka");
|
|
}
|
|
if (m_showortho)
|
|
{
|
|
auto const pending{static_cast<int>(m_ortho.pending() + m_topo.pending())};
|
|
if (pending > 0)
|
|
{
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(%d kafli w drodze)", pending);
|
|
}
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
|
|
// how a corner gets rounded when the track is carried on by clicking. a kink is not track, so
|
|
// the turn from where the track heads now to where the click is goes round an arc of this
|
|
// radius; it eats R*tan(kat/2) off the leg behind it and off the one in front
|
|
ImGui::SetNextItemWidth(120.0f);
|
|
ImGui::InputDouble("R w narozniku [m]", &m_corner_radius, 10.0, 100.0, "%.0f");
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("klikniecie w pusty teren prowadzi tor dalej; zwrot miedzy dotychczasowym kierunkiem\n"
|
|
"a klikietym punktem zostaje zaokraglony lukiem o tym promieniu.\n"
|
|
"im wiekszy R, tym dluzsza styczna R*tan(kat/2) sciagana z obu prostych\n"
|
|
"- dlatego ostry naroznik tuz przy koncu toru sie nie miesci.\n\n"
|
|
"prawy klik cofa ostatnie klikniecie");
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("prawy klik cofa klikniecie");
|
|
|
|
// which track is being edited
|
|
auto *track{current_track()};
|
|
if (ImGui::BeginCombo("Edytowany", track != nullptr ? track->name.c_str() : "-"))
|
|
{
|
|
for (auto const &candidate : m_document.tracks)
|
|
{
|
|
if (ImGui::Selectable(candidate.name.c_str(), candidate.id == m_track))
|
|
{
|
|
m_track = candidate.id;
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", candidate.name.c_str());
|
|
clear_selection();
|
|
m_pending = false;
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
if (track != nullptr)
|
|
{
|
|
ImGui::SetNextItemWidth(240.0f);
|
|
if (ImGui::InputText("Nazwa", m_namebuf, sizeof(m_namebuf)))
|
|
{
|
|
track->name = m_namebuf;
|
|
}
|
|
}
|
|
|
|
if (ImGui::Button("Nowy tor"))
|
|
{
|
|
new_track();
|
|
solve();
|
|
}
|
|
ImGui::SameLine();
|
|
if (m_document.tracks.size() > 1 && ImGui::Button("Usun tor"))
|
|
{
|
|
delete_current_track();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Pokaz calosc"))
|
|
{
|
|
go_to_plan();
|
|
}
|
|
|
|
if (ImGui::Button(m_pick_draw != 0 ? "Rysuje... (Esc)" : "Rysuj"))
|
|
{
|
|
m_pick_draw = m_pick_draw != 0 ? 0 : 1;
|
|
m_pick_turnout = 0;
|
|
m_pick_parallel = 0;
|
|
m_pick_join = 0;
|
|
m_pick_split = 0;
|
|
m_pick_cut = 0;
|
|
m_status = m_pick_draw != 0 ? "klikaj po terenie - kazdy klik prowadzi tor dalej; prawy klik cofa ostatni" : "";
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("prowadzenie toru klikaniem po terenie. dopoki to nie jest wlaczone,\n"
|
|
"klik w teren niczego nie rysuje - wybiera to, co pod nim lezy.\n"
|
|
"kursor na koncu toru pokazuje, co dolozy nastepny klik");
|
|
}
|
|
|
|
render_join();
|
|
render_cutting();
|
|
|
|
ImGui::Separator();
|
|
}
|
|
|
|
void plan_panel::render_join()
|
|
{
|
|
if (m_document.tracks.size() < 2)
|
|
{
|
|
return;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(m_pick_join != 0 ? "Lacze... (Esc)" : "Polacz"))
|
|
{
|
|
m_pick_join = m_pick_join != 0 ? 0 : 1;
|
|
m_pick_draw = 0;
|
|
m_join_first = TrackId::none;
|
|
m_status = m_pick_join != 0 ? "wskaz dwa wolne konce torow; podglad pokaze, co miedzy nimi stanie" : "";
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("wolne konce torow zapalaja sie na mapie: kwadrat to poczatek, kolo to koniec.\n"
|
|
"wskaz dwa dowolne - wyjdzie z tego jeden tor. gdy trzeba, jeden z nich\n"
|
|
"zostaje czytany od drugiego konca: w terenie nic sie nie rusza.\n"
|
|
"gdy oba leza na jednej prostej, wychodzi jedna prosta, bez zadnego luku");
|
|
}
|
|
if (m_pick_join != 0)
|
|
{
|
|
ImGui::SetNextItemWidth(120.0f);
|
|
ImGui::InputDouble("R polaczenia [m]", &m_join_radius, 10.0, 100.0, "%.0f");
|
|
}
|
|
}
|
|
|
|
void plan_panel::render_cutting()
|
|
{
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(m_pick_split != 0 ? "Dziele... (Esc)" : "Podziel"))
|
|
{
|
|
m_pick_split = m_pick_split != 0 ? 0 : 1;
|
|
m_pick_cut = 0;
|
|
m_pick_draw = 0;
|
|
m_status = m_pick_split != 0 ? "kliknij w os toru tam, gdzie ma go przeciac" : "";
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("wstawia zlacze w klikietym miejscu: odcinek pod kursorem dzieli sie na dwa,\n"
|
|
"a tor zostaje jednym torem - dopasowanie czyta go dalej tak samo.\n"
|
|
"w terenie nic sie nie rusza: obie polowy maja ten sam kształt.\n"
|
|
"na prostej nie ma sensu (dwie proste to jedna prosta), pod rozjazdem sie nie da");
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(m_pick_cut != 0 ? "Wycinam... (Esc)" : "Wytnij"))
|
|
{
|
|
m_pick_cut = m_pick_cut != 0 ? 0 : 1;
|
|
m_pick_split = 0;
|
|
m_pick_draw = 0;
|
|
m_cut_track = TrackId::none;
|
|
m_status = m_pick_cut != 0 ? "wskaz dwa punkty na jednym torze - to, co miedzy nimi, zniknie" : "";
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("usuwa odcinek miedzy dwoma klikietymi punktami jednego toru.\n"
|
|
"zostaja dwa tory, a miedzy nimi goly grunt - tu nie ma \"toru z dziura\".\n"
|
|
"rozjazd w wycinanym odcinku wstrzymuje ciecie: usun go najpierw");
|
|
}
|
|
}
|
|
|
|
void plan_panel::split_at(TrackId const Track_, double const Station)
|
|
{
|
|
// the track stays one track: a joint divides the element it lands in, and the chain goes on
|
|
// reading the way the fit needs it to
|
|
auto const outcome{editor::plan::insert_joint(m_document, m_solution, Track_, Station)};
|
|
if (false == outcome.ok)
|
|
{
|
|
m_status = outcome.why;
|
|
return;
|
|
}
|
|
m_track = Track_;
|
|
if (auto const *track{current_track()}; track != nullptr)
|
|
{
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", track->name.c_str());
|
|
}
|
|
m_sel_element = outcome.added;
|
|
m_sel_turnout = TurnoutId::none;
|
|
solve();
|
|
m_status = "zlacze na km " + chainage(Station) + "; dalsza polowa to osobny odcinek tego samego toru";
|
|
}
|
|
|
|
void plan_panel::cut_between(TrackId const Track_, double const From, double const To)
|
|
{
|
|
auto const outcome{editor::plan::cut_track(m_document, m_solution, Track_, From, To)};
|
|
if (false == outcome.ok)
|
|
{
|
|
m_status = outcome.why;
|
|
return;
|
|
}
|
|
m_trace.erase(static_cast<std::uint32_t>(Track_));
|
|
m_track = outcome.second;
|
|
if (auto const *track{current_track()}; track != nullptr)
|
|
{
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", track->name.c_str());
|
|
}
|
|
clear_selection();
|
|
solve();
|
|
m_status = "wyciete " + to_string(std::abs(To - From), 2) + " m miedzy km " + chainage(std::min(From, To)) + " i km " + chainage(std::max(From, To)) + "; zostaly dwa tory";
|
|
}
|
|
|
|
void plan_panel::join_ends(TrackId const A, int const Aend, TrackId const B, int const Bend)
|
|
{
|
|
editor::plan::JoinSettings settings;
|
|
settings.radius = m_join_radius;
|
|
|
|
auto const report{editor::plan::join_ends(m_document, m_solution, A, Aend, B, Bend, settings)};
|
|
if (false == report.ok)
|
|
{
|
|
m_status = report.why;
|
|
return;
|
|
}
|
|
|
|
// whichever track was left standing is the one to select afterwards, and it is not
|
|
// always the first clicked - the other one may have been the only one that could turn
|
|
auto const survivor{editor::plan::find_track(m_document, A) != nullptr ? A : B};
|
|
m_trace.erase(static_cast<std::uint32_t>(A));
|
|
m_trace.erase(static_cast<std::uint32_t>(B));
|
|
|
|
char status[256];
|
|
std::snprintf(status, sizeof(status), "polaczone: %s, %d odcinkow zlanych w jeden, %d rozjazdow przeniesionych%s", editor::plan::join_kind_name(report.join.kind), report.fused,
|
|
report.turnouts, report.reversed ? ", jeden tor czytany od drugiego konca" : "");
|
|
m_status = status;
|
|
m_track = survivor;
|
|
m_pick_join = 0;
|
|
m_join_first = TrackId::none;
|
|
auto const *joined{editor::plan::find_track(m_document, survivor)};
|
|
if (joined != nullptr)
|
|
{
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", joined->name.c_str());
|
|
}
|
|
clear_selection();
|
|
solve();
|
|
}
|
|
|
|
editor::plan::geometry::Pose plan_panel::join_pose(editor::plan::SolvedTrack const &Track_, int const End_, bool const Leaving_)
|
|
{
|
|
auto pose{End_ == 1 ? Track_.end : Track_.start};
|
|
// what is laid runs out of one end and into the other, so an end that points the
|
|
// wrong way for that is read backwards - which is exactly what turning the track
|
|
// round would do to it
|
|
if (Leaving_ ? End_ == 0 : End_ == 1)
|
|
{
|
|
pose.hx = -pose.hx;
|
|
pose.hy = -pose.hy;
|
|
}
|
|
return pose;
|
|
}
|
|
|
|
bool plan_panel::loose_end(editor::plan::SolvedTrack const &Track_, int const End_) const
|
|
{
|
|
if (Track_.elements.empty())
|
|
{
|
|
return false;
|
|
}
|
|
if (End_ == 0)
|
|
{
|
|
// a track hanging on a turnout's port starts on the frog, and that is not a loose end
|
|
auto const *authored{editor::plan::find_track(m_document, Track_.id)};
|
|
if (authored == nullptr || false == std::holds_alternative<editor::plan::AtPose>(authored->anchor))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// and one already standing on another track's end is joined to it, whether or not the two are
|
|
// one track in the document
|
|
auto const &pose{End_ == 0 ? Track_.start : Track_.end};
|
|
for (auto const &other : m_solution.tracks)
|
|
{
|
|
if (other.id == Track_.id || other.elements.empty())
|
|
{
|
|
continue;
|
|
}
|
|
for (auto const &at : {other.start, other.end})
|
|
{
|
|
if (std::hypot(at.x - pose.x, at.y - pose.y) < 0.10)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool plan_panel::hit_track_end(ImVec2 const &Mouse, float const Tolerance, TrackId &OutTrack, int &OutEnd) const
|
|
{
|
|
auto best{Tolerance};
|
|
auto found{false};
|
|
for (auto const &track : m_solution.tracks)
|
|
{
|
|
for (int end = 0; end < 2; ++end)
|
|
{
|
|
if (false == loose_end(track, end))
|
|
{
|
|
continue;
|
|
}
|
|
auto const &pose{end == 0 ? track.start : track.end};
|
|
ImVec2 screen;
|
|
if (false == world_to_screen(plan_to_world(pose.x, pose.y), screen))
|
|
{
|
|
continue;
|
|
}
|
|
auto const distance{std::hypot(screen.x - Mouse.x, screen.y - Mouse.y)};
|
|
if (distance < best)
|
|
{
|
|
best = static_cast<float>(distance);
|
|
OutTrack = track.id;
|
|
OutEnd = end;
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
void plan_panel::render_elements()
|
|
{
|
|
auto *track{current_track()};
|
|
if (track == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (false == ImGui::CollapsingHeader("Odcinki", ImGuiTreeNodeFlags_DefaultOpen))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// what a new element is made of. pick the numbers, then say which of the three it is: there is
|
|
// nothing else to an element, and nothing here is a mode of its own
|
|
ImGui::SetNextItemWidth(90.0f);
|
|
ImGui::InputDouble("dl. [m]", &m_new_length, 5.0, 50.0, "%.1f");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(90.0f);
|
|
ImGui::InputDouble("R [m]", &m_new_radius, 10.0, 100.0, "%.1f");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
{
|
|
int hand{m_new_hand >= 0 ? 0 : 1};
|
|
if (ImGui::Combo("skret", &hand, "w lewo\0w prawo\0"))
|
|
{
|
|
m_new_hand = hand == 0 ? 1 : -1;
|
|
}
|
|
}
|
|
|
|
if (ImGui::Button("+ prosta"))
|
|
{
|
|
append_element(Kind::Line);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("+ luk"))
|
|
{
|
|
append_element(Kind::Arc);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("+ krzywa przejsciowa"))
|
|
{
|
|
append_element(Kind::Clothoid);
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("R jest promieniem na jej koncu; na poczatku bierze sie z poprzedniego odcinka.\n0 = wybiega w prosta");
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cofnij") && false == track->elements.empty())
|
|
{
|
|
drop_last_element();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
if (track->elements.empty())
|
|
{
|
|
ImGui::TextDisabled("pusty tor. dolóz odcinek ponizej, albo klikaj po mapie: pierwsze dwa klikniecia kladzie prosta.");
|
|
return;
|
|
}
|
|
|
|
auto dirty{false};
|
|
// a change of shape - a radius, a hand, the length of a transition - belongs to its own corner
|
|
// and to nothing else, so the track is re-laid along the lines its straights already stood on.
|
|
// a change of length is what it says it is: the element gets longer, and what follows moves
|
|
auto refit{false};
|
|
auto const *solved{editor::plan::find_track(m_solution, track->id)};
|
|
|
|
// where each element begins along the axis, so a row can say its pikietaz
|
|
auto station{0.0};
|
|
for (std::size_t i = 0; i < track->elements.size(); ++i)
|
|
{
|
|
auto &element{track->elements[i]};
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
auto const selected{element.id == m_sel_element};
|
|
auto const *solved_element{solved != nullptr && i < solved->elements.size() ? &solved->elements[i] : nullptr};
|
|
auto const held{std::holds_alternative<Parallel>(element.hold)};
|
|
auto const starts{station};
|
|
station += solved_element != nullptr ? solved_element->length : element.length;
|
|
|
|
// the row says the whole element: what it is, how long, and how it turns. a held one says
|
|
// so too, because its shape is not its own
|
|
char label[200];
|
|
switch (element.kind)
|
|
{
|
|
case Kind::Arc:
|
|
std::snprintf(label, sizeof(label), "%zu. Luk R=%.0f %s %.1f m (%.2f st.)%s%s", i + 1, element.radius, element.hand >= 0 ? "w lewo" : "w prawo", element.length,
|
|
element.radius > 0.0 ? element.length / element.radius * 180.0 / std::numbers::pi : 0.0, held ? " [rownolegly]" : "",
|
|
solved_element != nullptr ? "" : " (nie polozony)");
|
|
break;
|
|
case Kind::Clothoid:
|
|
std::snprintf(label, sizeof(label), "%zu. Krzywa przejsciowa %.1f m (do R=%.0f %s)%s", i + 1, element.length, element.radius, element.hand >= 0 ? "w lewo" : "w prawo",
|
|
solved_element != nullptr ? "" : " (nie polozony)");
|
|
break;
|
|
default:
|
|
std::snprintf(label, sizeof(label), "%zu. Prosta %.1f m%s%s", i + 1, element.length, held ? " [rownolegla]" : "",
|
|
solved_element != nullptr ? "" : " (nie polozony)");
|
|
break;
|
|
}
|
|
if (ImGui::Selectable(label, selected))
|
|
{
|
|
m_sel_element = element.id;
|
|
}
|
|
|
|
if (selected)
|
|
{
|
|
ImGui::Indent();
|
|
|
|
int kind{element.kind == Kind::Line ? 0 : (element.kind == Kind::Arc ? 1 : 2)};
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
if (ImGui::Combo("rodzaj", &kind, "prosta\0luk\0krzywa przejsciowa\0"))
|
|
{
|
|
element.kind = kind == 0 ? Kind::Line : (kind == 1 ? Kind::Arc : Kind::Clothoid);
|
|
dirty = true;
|
|
}
|
|
|
|
ImGui::SetNextItemWidth(120.0f);
|
|
if (ImGui::InputDouble("dlugosc [m]", &element.length, 5.0, 50.0, "%.2f"))
|
|
{
|
|
dirty = true;
|
|
// a transition's length is authored, and what it takes out of the corner is the
|
|
// corner's business - so this one is absorbed rather than pushed down the track
|
|
refit = element.kind == Kind::Clothoid;
|
|
}
|
|
|
|
if (element.kind != Kind::Line)
|
|
{
|
|
ImGui::SetNextItemWidth(120.0f);
|
|
if (ImGui::InputDouble(element.kind == Kind::Clothoid ? "R na koncu [m]" : "promien [m]", &element.radius, 10.0, 100.0, "%.2f"))
|
|
{
|
|
editor::plan::sync_joint(*track, i);
|
|
dirty = true;
|
|
refit = true;
|
|
}
|
|
if (element.kind == Kind::Clothoid && ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("0 = krzywa wybiegajaca w prosta; promien na poczatku bierze sie z poprzedniego odcinka");
|
|
}
|
|
ImGui::SetNextItemWidth(120.0f);
|
|
int hand{element.hand >= 0 ? 0 : 1};
|
|
if (ImGui::Combo("skret", &hand, "w lewo\0w prawo\0"))
|
|
{
|
|
element.hand = hand == 0 ? 1 : -1;
|
|
editor::plan::sync_joint(*track, i);
|
|
dirty = true;
|
|
refit = true;
|
|
}
|
|
if (element.kind == Kind::Arc && element.radius > 0.0)
|
|
{
|
|
auto turn{element.length / element.radius * 180.0 / std::numbers::pi};
|
|
ImGui::SetNextItemWidth(120.0f);
|
|
if (ImGui::InputDouble("kat zwrotu [st.]", &turn, 1.0, 10.0, "%.3f"))
|
|
{
|
|
element.length = std::max(0.0, turn) * std::numbers::pi / 180.0 * element.radius;
|
|
dirty = true;
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("ten sam luk, powiedziany katem zamiast dlugoscia");
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: kontrolki rownoleglosci (trzymanie miedzytorza) chwilowo schowane z gui -
|
|
// samo Parallel/hold i solver zostaja, wraca tu przycisk i pole odstepu
|
|
|
|
if (solved_element != nullptr)
|
|
{
|
|
ImGui::TextDisabled("polozony: %.2f m, R %.1f -> %.1f", solved_element->length, std::abs(solved_element->k0) > 1e-9 ? 1.0 / std::abs(solved_element->k0) : 0.0,
|
|
std::abs(solved_element->k1) > 1e-9 ? 1.0 / std::abs(solved_element->k1) : 0.0);
|
|
ImGui::TextDisabled("km %s ... %s", chainage(starts).c_str(), chainage(starts + solved_element->length).c_str());
|
|
if (auto const deviation{m_deviation.find(static_cast<std::uint32_t>(element.id))}; deviation != m_deviation.end())
|
|
{
|
|
ImGui::TextDisabled("od sladu: max %.2f m, rms %.2f m", deviation->second.first, deviation->second.second);
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("odleglosc klikietych punktow od polozonej osi.\nliczona tylko dla toru rysowanego w tej sesji - slad nie jest zapisywany");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ImGui::SmallButton("usun odcinek"))
|
|
{
|
|
ImGui::Unindent();
|
|
ImGui::PopID();
|
|
delete_selected_element();
|
|
return;
|
|
}
|
|
|
|
ImGui::Unindent();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (refit)
|
|
{
|
|
// the change stands either way - it is what was asked for. what a failed fit costs is that
|
|
// the rest of the track follows along instead of staying put, and that is worth saying
|
|
std::string why;
|
|
if (refit_current_track(why))
|
|
{
|
|
m_status = "dopasowano naroznik; reszta toru bez zmian";
|
|
}
|
|
else
|
|
{
|
|
m_status = why + " - dalsza czesc toru poszla za zmiana";
|
|
}
|
|
}
|
|
if (dirty)
|
|
{
|
|
solve();
|
|
}
|
|
}
|
|
|
|
// the geometry of a turnout is the type's, and the type is a list: przediglicowy, iglica, luk,
|
|
// prosta krzyzownicowa, in the order they are laid from PR. every length is the catalogue's -
|
|
// nothing here is fitted, so the skos and the catalogue length are checked against what the list
|
|
// works out to and the difference is said out loud
|
|
void plan_panel::render_turnout_type(editor::plan::TurnoutType &Type_)
|
|
{
|
|
auto dirty{false};
|
|
|
|
ImGui::SetNextItemWidth(90.0f);
|
|
if (ImGui::InputDouble("skos 1:n", &Type_.crossing_n, 0.5, 1.0, "%.3f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(90.0f);
|
|
if (ImGui::InputDouble("dl. kat. a+b [m]", &Type_.length, 0.1, 1.0, "%.3f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("dlugosc katalogowa to suma stycznych a + b, tak jak na rysunku typu");
|
|
}
|
|
|
|
static char const *const part_names[]{"przediglicowy", "iglica", "luk", "krzyzownicowa"};
|
|
for (std::size_t i = 0; i < Type_.pieces.size(); ++i)
|
|
{
|
|
auto &piece{Type_.pieces[i]};
|
|
ImGui::PushID(static_cast<int>(2000 + i));
|
|
piece.part = std::clamp(piece.part, 0, 3);
|
|
|
|
char label[200];
|
|
if (piece.radius_start <= 0.0 && piece.radius_end <= 0.0)
|
|
{
|
|
std::snprintf(label, sizeof(label), "%zu. %s prosta %.3f m", i + 1, part_names[piece.part], piece.length);
|
|
}
|
|
else if (std::abs(piece.radius_start - piece.radius_end) < 1e-9)
|
|
{
|
|
std::snprintf(label, sizeof(label), "%zu. %s luk R %.0f %.3f m", i + 1, part_names[piece.part], piece.radius_start, piece.length);
|
|
}
|
|
else
|
|
{
|
|
std::snprintf(label, sizeof(label), "%zu. %s R %.0f -> %.0f %.3f m", i + 1, part_names[piece.part], piece.radius_start, piece.radius_end,
|
|
piece.length);
|
|
}
|
|
if (ImGui::Selectable(label, static_cast<int>(i) == m_sel_piece))
|
|
{
|
|
m_sel_piece = static_cast<int>(i);
|
|
}
|
|
if (static_cast<int>(i) == m_sel_piece)
|
|
{
|
|
ImGui::Indent();
|
|
ImGui::SetNextItemWidth(150.0f);
|
|
if (ImGui::Combo("rola", &piece.part, "przediglicowy\0iglica\0luk\0krzyzownicowa\0"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
if (ImGui::InputDouble("dlugosc [m]", &piece.length, 0.1, 1.0, "%.3f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
if (ImGui::InputDouble("R poczatek [m]", &piece.radius_start, 10.0, 100.0, "%.2f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
if (ImGui::InputDouble("R koniec [m]", &piece.radius_end, 10.0, 100.0, "%.2f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("0 = prosta; rowne promienie = luk, rozne = krzywa przejsciowa");
|
|
}
|
|
auto turn_in{piece.turn_in * 180.0 / std::numbers::pi};
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
if (ImGui::InputDouble("nagiecie [st.]", &turn_in, 0.05, 0.5, "%.4f"))
|
|
{
|
|
piece.turn_in = std::max(0.0, turn_in) * std::numbers::pi / 180.0;
|
|
dirty = true;
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("kat nagiecia iglicy (beta): 0 = iglica styczna do opornicy");
|
|
}
|
|
if (ImGui::SmallButton("usun odcinek"))
|
|
{
|
|
Type_.pieces.erase(Type_.pieces.begin() + static_cast<long>(i));
|
|
m_sel_piece = -1;
|
|
ImGui::Unindent();
|
|
ImGui::PopID();
|
|
solve();
|
|
return;
|
|
}
|
|
ImGui::Unindent();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
if (ImGui::SmallButton("dodaj odcinek"))
|
|
{
|
|
Type_.pieces.push_back(editor::plan::TurnoutPieceSpec{2, 1.0, 0.0, 0.0, 0.0});
|
|
m_sel_piece = static_cast<int>(Type_.pieces.size()) - 1;
|
|
dirty = true;
|
|
}
|
|
|
|
// the blade rail itself: a tip cannot be ground to nothing, so it is cut back to a thickness
|
|
// and the point where the blade leaves the opornica is only a theoretical one
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
if (ImGui::InputDouble("ostrze u [m]", &Type_.blade.tip_thickness, 0.001, 0.005, "%.4f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
if (ImGui::InputDouble("dziob d [m]", &Type_.blade.nose, 0.001, 0.005, "%.4f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("zestrugana dlugosc od ostrza do punktu, w ktorym iglica ma grubosc u (Koc, tablica 5.1)");
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
if (ImGui::InputDouble("glowka w [m]", &Type_.blade.railtop_width, 0.001, 0.005, "%.4f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
|
|
auto const laid{editor::plan::lay_turnout(editor::plan::geometry::Pose{0.0, 0.0, 1.0, 0.0}, editor::plan::to_domain(Type_, 1))};
|
|
if (false == laid.valid)
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.55f, 1.0f), "lista nie da sie polozyc: dlugosci > 0, promienie >= 0");
|
|
}
|
|
else
|
|
{
|
|
if (laid.has_blade)
|
|
{
|
|
ImGui::TextDisabled("iglica: ostrze a %.4f m, dziob do %.4f m (o %+.4f), struganie %.3f m, przyleganie %.4f st.", laid.blade_station, laid.nose_end,
|
|
laid.blade_residual, laid.planing_length, laid.blade_angle * 180.0 / std::numbers::pi);
|
|
}
|
|
auto const closed{std::abs(laid.angle_residual) <= 1e-6 && (Type_.length <= 0.0 || std::abs(laid.length_residual) <= 1e-3)};
|
|
ImGui::TextColored(closed ? ImVec4(0.6f, 0.9f, 0.6f, 1.0f) : ImVec4(1.0f, 0.75f, 0.45f, 1.0f), "domkniecie: kat %+.5f st., dlugosc %+.4f m",
|
|
laid.angle_residual * 180.0 / std::numbers::pi, laid.length_residual);
|
|
}
|
|
|
|
if (dirty)
|
|
{
|
|
solve();
|
|
}
|
|
ImGui::Separator();
|
|
}
|
|
|
|
// the catalogue drawing of the chosen type: the diverging path piece by piece in the colours of its
|
|
// roles, both tracks' rails, and the construction points - the same list the solver hands the map,
|
|
// so the two cannot drift apart. a rozjazd is thirty metres long and a metre and a half wide, so as
|
|
// on every real drawing the view across the track is stretched, and by how much is said on screen.
|
|
// the blade's own dimensions are millimetres; they come out under the wheel, which magnifies about
|
|
// the cursor
|
|
void plan_panel::render_template_window()
|
|
{
|
|
if (false == m_show_template)
|
|
{
|
|
return;
|
|
}
|
|
ImGui::SetNextWindowSize(ImVec2(1020.0f, 580.0f), ImGuiCond_FirstUseEver);
|
|
if (false == ImGui::Begin("Rysunek szablonu rozjazdu", &m_show_template))
|
|
{
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
if (m_document.turnout_types.empty())
|
|
{
|
|
ImGui::TextDisabled("katalog jest pusty");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
m_turnout_type = std::clamp(m_turnout_type, 0, static_cast<int>(m_document.turnout_types.size()) - 1);
|
|
auto const &type{m_document.turnout_types[static_cast<std::size_t>(m_turnout_type)]};
|
|
auto const start{editor::plan::geometry::Pose{0.0, 0.0, 1.0, 0.0}};
|
|
auto const bend{m_tpl_bend_radius > 1.0 ? (m_tpl_bend_hand == 0 ? 1.0 : -1.0) / m_tpl_bend_radius : 0.0};
|
|
auto const laid{editor::plan::lay_turnout(start, editor::plan::to_domain(type, 1), bend)};
|
|
if (false == laid.valid)
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.55f, 1.0f), bend != 0.0 ? "typu \"%s\" nie da sie wygiac na taki luk" : "typ \"%s\" nie da sie polozyc",
|
|
type.name.c_str());
|
|
ImGui::SetNextItemWidth(150.0f);
|
|
ImGui::InputDouble("R toru zasadniczego", &m_tpl_bend_radius, 10.0, 100.0, "%.3f");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
// what the drawing says in words, above what it says in lines
|
|
ImGui::Text("%s skos 1:%.4g alfa %s dlugosc katalogowa %.3f m", type.name.c_str(), type.crossing_n,
|
|
angle_text(std::atan(1.0 / std::max(type.crossing_n, 1e-9))).c_str(), type.length);
|
|
ImGui::TextDisabled("styczne a %.3f + b %.3f m na torze zasadniczym %.3f m odgalezienie %.3f m", laid.tangent_front, laid.tangent_back,
|
|
laid.through_length, laid.diverging_length);
|
|
if (laid.has_blade)
|
|
{
|
|
ImGui::TextDisabled("iglica: u %.4f d %.4f w %.3f m ostrze a %.4f m dziob u %.4f m (o %+.4f) struganie %.3f m przyleganie %s",
|
|
type.blade.tip_thickness, type.blade.nose, type.blade.railtop_width, laid.blade_station, laid.nose_end, laid.blade_residual,
|
|
laid.planing_length, angle_text(laid.blade_angle).c_str());
|
|
}
|
|
else
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.45f, 1.0f), "ten typ nie ma iglicy - zaden odcinek nie ma roli \"iglica\"");
|
|
}
|
|
auto const closed{std::abs(laid.angle_residual) <= 1e-6 && (type.length <= 0.0 || std::abs(laid.length_residual) <= 1e-3)};
|
|
ImGui::TextColored(closed ? ImVec4(0.6f, 0.9f, 0.6f, 1.0f) : ImVec4(1.0f, 0.75f, 0.45f, 1.0f), "domkniecie: kat %+.5f st., dlugosc %+.4f m",
|
|
laid.angle_residual * 180.0 / std::numbers::pi, laid.length_residual);
|
|
|
|
// lukowanie: the drawing of a bent turnout is the drawing of the same turnout, rotated about its
|
|
// centre - the tangents and the crossing angle do not move, only the radii
|
|
ImGui::SetNextItemWidth(150.0f);
|
|
if (ImGui::InputDouble("R zasadniczego (0 = prosty)", &m_tpl_bend_radius, 10.0, 100.0, "%.3f"))
|
|
{
|
|
m_tpl_bend_radius = std::max(0.0, m_tpl_bend_radius);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
ImGui::Combo("wygiety", &m_tpl_bend_hand, "w lewo\0w prawo\0");
|
|
ImGui::TextDisabled("%s", bend_text(laid.bend, laid.diverging_curvature, laid.bend_angle).c_str());
|
|
if (auto const *limit{bend_limit(laid.diverging_curvature)}; limit != nullptr)
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.45f, 1.0f), "%s", limit);
|
|
}
|
|
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
ImGui::SliderFloat("przewyzszenie", &m_tpl_exagg, 1.0f, 60.0f, "x%.0f");
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("szyny", &m_tpl_rails);
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("punkty", &m_tpl_marks);
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("dopasuj"))
|
|
{
|
|
m_tpl_zoom = 1.0f;
|
|
m_tpl_pan = 0.0f;
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("kolko powieksza, przeciaganie przesuwa");
|
|
|
|
auto const avail{ImGui::GetContentRegionAvail()};
|
|
auto const w{std::max(avail.x, 120.0f)};
|
|
auto const h{std::max(avail.y, 120.0f)};
|
|
auto const origin{ImGui::GetCursorScreenPos()};
|
|
ImGui::InvisibleButton("rysunek", ImVec2(w, h));
|
|
|
|
auto const span{std::max(laid.through_length, laid.tangent_front + laid.tangent_back) + 2.0};
|
|
auto const scale_of{[&](float const Zoom) { return static_cast<double>(w) * 0.94 / span * static_cast<double>(Zoom); }};
|
|
auto sx{scale_of(m_tpl_zoom)};
|
|
auto centre{span * 0.5 + static_cast<double>(m_tpl_pan)};
|
|
|
|
// the wheel magnifies about the cursor, so the blade can be run down to millimetres
|
|
if (ImGui::IsItemHovered() && ImGui::GetIO().MouseWheel != 0.0f)
|
|
{
|
|
auto const offset{static_cast<double>(ImGui::GetIO().MousePos.x - origin.x - w * 0.5f)};
|
|
auto const under{centre + offset / sx};
|
|
m_tpl_zoom = std::clamp(m_tpl_zoom * std::pow(1.25f, ImGui::GetIO().MouseWheel), 1.0f, 5000.0f);
|
|
sx = scale_of(m_tpl_zoom);
|
|
centre = under - offset / sx;
|
|
m_tpl_pan = static_cast<float>(centre - span * 0.5);
|
|
}
|
|
if (ImGui::IsItemActive() && ImGui::IsMouseDown(0))
|
|
{
|
|
m_tpl_pan -= static_cast<float>(static_cast<double>(ImGui::GetIO().MouseDelta.x) / sx);
|
|
centre = span * 0.5 + static_cast<double>(m_tpl_pan);
|
|
}
|
|
|
|
auto const sy{sx * static_cast<double>(m_tpl_exagg)};
|
|
auto const to_screen{[&](double const X, double const Y) {
|
|
return ImVec2(origin.x + w * 0.5f + static_cast<float>((X - centre) * sx), origin.y + h * 0.58f - static_cast<float>(Y * sy));
|
|
}};
|
|
|
|
auto *drawlist{ImGui::GetWindowDrawList()};
|
|
drawlist->PushClipRect(origin, ImVec2(origin.x + w, origin.y + h), true);
|
|
drawlist->AddRectFilled(origin, ImVec2(origin.x + w, origin.y + h), IM_COL32(16, 20, 26, 255));
|
|
|
|
auto const half{editor::plan::kHalfGauge};
|
|
|
|
// the tor zasadniczy, and the opornica the blade lies against. bent, it is an arc like any other,
|
|
// so it is walked rather than ruled
|
|
auto const through_at{[&](double const Station_) { return editor::plan::geometry::layout_segment(bend, bend, Station_, start, nullptr); }};
|
|
auto const through_line{[&](double const From_, double const To_, double const Offset_) {
|
|
auto const steps{bend == 0.0 ? 1 : 96};
|
|
std::vector<ImVec2> line;
|
|
line.reserve(static_cast<std::size_t>(steps) + 1);
|
|
for (int i = 0; i <= steps; ++i)
|
|
{
|
|
auto const pose{through_at(From_ + (To_ - From_) * static_cast<double>(i) / static_cast<double>(steps))};
|
|
line.push_back(to_screen(pose.x - pose.hy * Offset_, pose.y + pose.hx * Offset_));
|
|
}
|
|
return line;
|
|
}};
|
|
|
|
{
|
|
auto const axis{through_line(-1.0, span, 0.0)};
|
|
drawlist->AddPolyline(axis.data(), static_cast<int>(axis.size()), IM_COL32(105, 115, 130, 220), false, 1.0f);
|
|
if (m_tpl_rails)
|
|
{
|
|
for (int side = -1; side <= 1; side += 2)
|
|
{
|
|
auto const rail{through_line(-1.0, span, half * side)};
|
|
drawlist->AddPolyline(rail.data(), static_cast<int>(rail.size()), IM_COL32(150, 160, 175, 210), false, 1.5f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// the diverging path, in the colours of the roles: przediglicowy, iglica, luk, krzyzownicowa.
|
|
// everything up to the punkt A is dashed, because none of it is rail the wheel can run on: before
|
|
// the ostrze there is no blade at all, and from the ostrze to A the blade is the straight planed
|
|
// dziob, not the arc. from A on the blade lies on the arc and the line goes solid
|
|
static ImU32 const part_colour[]{IM_COL32(150, 175, 210, 255), IM_COL32(120, 235, 255, 255), IM_COL32(240, 240, 240, 255), IM_COL32(255, 200, 110, 255)};
|
|
auto const hand{laid.frog.y >= 0.0 ? 1 : -1};
|
|
auto const rail_line{[&](std::vector<editor::plan::geometry::XY> const &Points_, int const Side_) {
|
|
std::vector<ImVec2> rail;
|
|
rail.reserve(Points_.size());
|
|
for (std::size_t i = 0; i < Points_.size(); ++i)
|
|
{
|
|
auto const &back{Points_[i == 0 ? 0 : i - 1]};
|
|
auto const &ahead{Points_[i == 0 ? 1 : i]};
|
|
auto const dx{ahead.x - back.x};
|
|
auto const dy{ahead.y - back.y};
|
|
auto const len{std::hypot(dx, dy)};
|
|
if (len < 1e-12)
|
|
{
|
|
continue;
|
|
}
|
|
rail.push_back(to_screen(Points_[i].x - dy / len * half * Side_, Points_[i].y + dx / len * half * Side_));
|
|
}
|
|
return rail;
|
|
}};
|
|
|
|
// the sampling step is metres and the dziob is centimetres, so the two stretches have to part on
|
|
// the exact distance rather than on whichever sample happens to fall nearest it
|
|
auto const cut_at{[](std::vector<editor::plan::geometry::XY> const &Points_, double const Distance_,
|
|
std::vector<editor::plan::geometry::XY> &Head_, std::vector<editor::plan::geometry::XY> &Tail_) {
|
|
auto travelled{0.0};
|
|
for (std::size_t i = 0; i < Points_.size(); ++i)
|
|
{
|
|
auto const step{i == 0 ? 0.0 : std::hypot(Points_[i].x - Points_[i - 1].x, Points_[i].y - Points_[i - 1].y)};
|
|
travelled += step;
|
|
if (travelled <= Distance_)
|
|
{
|
|
Head_.push_back(Points_[i]);
|
|
continue;
|
|
}
|
|
if (Tail_.empty() && false == Head_.empty() && step > 1e-12)
|
|
{
|
|
auto const &back{Points_[i - 1]};
|
|
auto const share{(Distance_ - (travelled - step)) / step};
|
|
editor::plan::geometry::XY const meeting{back.x + (Points_[i].x - back.x) * share, back.y + (Points_[i].y - back.y) * share};
|
|
Head_.push_back(meeting);
|
|
Tail_.push_back(meeting);
|
|
}
|
|
Tail_.push_back(Points_[i]);
|
|
}
|
|
}};
|
|
|
|
auto const draw_run{[&](std::vector<editor::plan::geometry::XY> const &Points_, ImU32 const Colour_, bool const Theoretical_) {
|
|
if (Points_.size() < 2)
|
|
{
|
|
return;
|
|
}
|
|
std::vector<ImVec2> line;
|
|
line.reserve(Points_.size());
|
|
for (auto const &point : Points_)
|
|
{
|
|
line.push_back(to_screen(point.x, point.y));
|
|
}
|
|
if (Theoretical_)
|
|
{
|
|
dashed_polyline(drawlist, line, (Colour_ & 0x00ffffffu) | 0xb0000000u, 1.6f);
|
|
}
|
|
else
|
|
{
|
|
drawlist->AddPolyline(line.data(), static_cast<int>(line.size()), Colour_, false, 2.5f);
|
|
}
|
|
if (false == m_tpl_rails)
|
|
{
|
|
return;
|
|
}
|
|
for (int side = -1; side <= 1; side += 2)
|
|
{
|
|
auto const rail{rail_line(Points_, side)};
|
|
if (rail.size() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
auto const shade{(Colour_ & 0x00ffffffu) | 0x88000000u};
|
|
if (Theoretical_)
|
|
{
|
|
dashed_polyline(drawlist, rail, shade, 1.0f, 5.0f, 4.0f);
|
|
}
|
|
else
|
|
{
|
|
drawlist->AddPolyline(rail.data(), static_cast<int>(rail.size()), shade, false, 1.2f);
|
|
}
|
|
}
|
|
}};
|
|
|
|
auto walker{start};
|
|
auto station{0.0};
|
|
auto heel{0.0};
|
|
for (auto const &segment : laid.path)
|
|
{
|
|
if (segment.turn_in != 0.0)
|
|
{
|
|
auto const c{std::cos(segment.turn_in)};
|
|
auto const s{std::sin(segment.turn_in)};
|
|
walker = {walker.x, walker.y, walker.hx * c - walker.hy * s, walker.hx * s + walker.hy * c};
|
|
}
|
|
std::vector<editor::plan::geometry::XY> points;
|
|
walker = editor::plan::geometry::layout_segment(segment.k0, segment.k1, segment.length, walker, &points);
|
|
auto const from{station};
|
|
station += segment.length;
|
|
if (segment.part == editor::plan::TurnoutPart::Blade)
|
|
{
|
|
heel = station;
|
|
}
|
|
if (points.size() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
auto const colour{part_colour[std::clamp(static_cast<int>(segment.part), 0, 3)]};
|
|
|
|
// nothing is real before A; a straight blade is its own true shape, so it stays whole
|
|
auto const bent{segment.k0 != 0.0 || segment.k1 != 0.0};
|
|
auto const upto{laid.has_blade && bent ? std::clamp(laid.nose_end - from, 0.0, segment.length) : 0.0};
|
|
if (upto <= 0.0)
|
|
{
|
|
draw_run(points, colour, false);
|
|
continue;
|
|
}
|
|
std::vector<editor::plan::geometry::XY> ahead;
|
|
std::vector<editor::plan::geometry::XY> behind;
|
|
cut_at(points, upto, behind, ahead);
|
|
draw_run(behind, colour, true);
|
|
draw_run(ahead, colour, false);
|
|
}
|
|
|
|
// the dziob as it is really cut: one straight planed rail lying on the opornica at the ostrze - the
|
|
// gap there is nothing at all - and opening to u at the punkt A, where it meets the arc. the blade
|
|
// is the rail on the far side from the way the turnout leads, so it peels off the stock rail the
|
|
// diverging track leaves behind
|
|
if (laid.has_blade && laid.nose_end > laid.blade_station)
|
|
{
|
|
auto const blade_side{-hand};
|
|
auto const blade_colour{part_colour[1]};
|
|
auto const foot_pose{through_at(laid.blade_station)};
|
|
auto const tip{to_screen(foot_pose.x - foot_pose.hy * half * blade_side, foot_pose.y + foot_pose.hx * half * blade_side)};
|
|
auto const nose{to_screen(laid.blade_nose_end.x - laid.blade_nose_end.hy * half * blade_side, laid.blade_nose_end.y + laid.blade_nose_end.hx * half * blade_side)};
|
|
if (m_tpl_rails)
|
|
{
|
|
drawlist->AddLine(tip, nose, (blade_colour & 0x00ffffffu) | 0xee000000u, 2.4f);
|
|
}
|
|
|
|
// the same straight said on the axis: half a gauge in from the face, so it leaves the through
|
|
// axis at nothing and meets the arc where the face meets the opornica at u
|
|
drawlist->AddLine(to_screen(foot_pose.x, foot_pose.y), to_screen(laid.blade_nose_end.x, laid.blade_nose_end.y), blade_colour, 2.5f);
|
|
if (std::hypot(nose.x - tip.x, nose.y - tip.y) >= 26.0f)
|
|
{
|
|
char label[64];
|
|
std::snprintf(label, sizeof(label), "dziob prosty d %.4f m, od stycznosci do u", laid.nose_end - laid.blade_station);
|
|
drawlist->AddText(ImVec2((tip.x + nose.x) * 0.5f - ImGui::CalcTextSize(label).x * 0.5f, std::min(tip.y, nose.y) - 20.0f), blade_colour, label);
|
|
}
|
|
|
|
// the other blade of the pair: the straight one, which carries the through route. its running
|
|
// edge is the tor zasadniczy's own rail, so it is drawn over it; what is planed is its back,
|
|
// from the ostrze lying on the opornica lukowa down to the rail at A
|
|
auto const straight{through_line(laid.blade_station, heel, half * hand)};
|
|
drawlist->AddPolyline(straight.data(), static_cast<int>(straight.size()), (blade_colour & 0x00ffffffu) | 0xdd000000u, false, 2.4f);
|
|
auto const foot{straight.front()};
|
|
auto const shoulder{straight.back()};
|
|
auto const nose_pose{through_at(laid.nose_end)};
|
|
drawlist->AddLine(to_screen(laid.blade_tip.x - laid.blade_tip.hy * half * hand, laid.blade_tip.y + laid.blade_tip.hx * half * hand),
|
|
to_screen(nose_pose.x - nose_pose.hy * half * hand, nose_pose.y + nose_pose.hx * half * hand),
|
|
(blade_colour & 0x00ffffffu) | 0xdd000000u, 2.0f);
|
|
if (shoulder.x - foot.x >= 60.0f)
|
|
{
|
|
char label[64];
|
|
std::snprintf(label, sizeof(label), "iglica prosta - jazda na wprost");
|
|
drawlist->AddText(ImVec2((foot.x + shoulder.x) * 0.5f - ImGui::CalcTextSize(label).x * 0.5f, foot.y - (hand > 0 ? 20.0f : -8.0f)), blade_colour, label);
|
|
}
|
|
}
|
|
|
|
// the construction points, from the very list the solver reports for a placement
|
|
if (m_tpl_marks)
|
|
{
|
|
auto marks{editor::plan::turnout_marks(laid, start)};
|
|
std::sort(marks.begin(), marks.end(), [](auto const &Left_, auto const &Right_) { return Left_.station < Right_.station; });
|
|
|
|
auto level{0};
|
|
for (auto const &mark : marks)
|
|
{
|
|
if (mark.kind < 0 || mark.kind >= static_cast<int>(sizeof(mark_names) / sizeof(mark_names[0])))
|
|
{
|
|
continue;
|
|
}
|
|
auto const at{to_screen(mark.x, mark.y)};
|
|
auto const blade{mark.kind == 1 || mark.kind == 7 || mark.kind == 8};
|
|
auto const primary{mark.kind == 0 || mark.kind == 5 || mark.kind == 9};
|
|
auto const colour{primary ? IM_COL32(255, 210, 90, 245) : (blade ? IM_COL32(120, 235, 255, 245) : IM_COL32(255, 150, 210, 220))};
|
|
|
|
// a tick across the track, then the name on a leader fanned out above it
|
|
drawlist->AddLine(ImVec2(at.x, at.y - 11.0f), ImVec2(at.x, at.y + 11.0f), colour, 1.5f);
|
|
|
|
auto const reach{34.0f + 16.0f * static_cast<float>(level % 5)};
|
|
ImVec2 const anchor(at.x, origin.y + h * 0.58f - reach - 40.0f);
|
|
char label[96];
|
|
if (mark.station > 0.0)
|
|
{
|
|
std::snprintf(label, sizeof(label), "%s %.4f", mark_names[mark.kind], mark.station);
|
|
}
|
|
else
|
|
{
|
|
std::snprintf(label, sizeof(label), "%s", mark_names[mark.kind]);
|
|
}
|
|
auto const size{ImGui::CalcTextSize(label)};
|
|
ImVec2 const corner(anchor.x - size.x * 0.5f, anchor.y - size.y * 0.5f);
|
|
drawlist->AddLine(ImVec2(at.x, at.y - 11.0f), ImVec2(anchor.x, corner.y + size.y + 2.0f), IM_COL32(190, 190, 190, 110), 1.0f);
|
|
drawlist->AddRectFilled(ImVec2(corner.x - 3.0f, corner.y - 2.0f), ImVec2(corner.x + size.x + 3.0f, corner.y + size.y + 2.0f),
|
|
IM_COL32(12, 16, 24, 215), 3.0f);
|
|
drawlist->AddText(corner, colour, label);
|
|
++level;
|
|
}
|
|
}
|
|
|
|
// what the two line styles mean, since the difference between them is the whole point of the drawing
|
|
{
|
|
auto const left{origin.x + 14.0f};
|
|
auto top{origin.y + 12.0f};
|
|
auto const step{ImGui::GetTextLineHeight() + 3.0f};
|
|
std::vector<ImVec2> const sample{ImVec2(left, top + step * 0.5f), ImVec2(left + 34.0f, top + step * 0.5f)};
|
|
dashed_polyline(drawlist, sample, IM_COL32(200, 210, 230, 200), 1.6f);
|
|
drawlist->AddText(ImVec2(left + 42.0f, top), IM_COL32(200, 210, 230, 220), "teoretyczny luk od PR do punktu A: nie ma tam szyny");
|
|
top += step;
|
|
drawlist->AddLine(ImVec2(left, top + step * 0.5f), ImVec2(left + 34.0f, top + step * 0.5f), IM_COL32(120, 235, 255, 255), 2.5f);
|
|
drawlist->AddText(ImVec2(left + 42.0f, top), IM_COL32(120, 235, 255, 235), "dziob iglicy: prosty, od stycznosci z opornica do grubosci u");
|
|
}
|
|
|
|
// a metre bar, because with the view stretched across the track no shape can be trusted by eye
|
|
{
|
|
auto const decades{std::pow(10.0, std::floor(std::log10(std::max(span * 0.2, 1e-6))))};
|
|
auto bar{decades};
|
|
while (bar * sx > w * 0.3)
|
|
{
|
|
bar *= 0.5;
|
|
}
|
|
auto const left{origin.x + 14.0f};
|
|
auto const base{origin.y + h - 16.0f};
|
|
auto const right{left + static_cast<float>(bar * sx)};
|
|
drawlist->AddLine(ImVec2(left, base), ImVec2(right, base), IM_COL32(220, 220, 220, 220), 1.5f);
|
|
drawlist->AddLine(ImVec2(left, base - 5.0f), ImVec2(left, base + 5.0f), IM_COL32(220, 220, 220, 220), 1.5f);
|
|
drawlist->AddLine(ImVec2(right, base - 5.0f), ImVec2(right, base + 5.0f), IM_COL32(220, 220, 220, 220), 1.5f);
|
|
char scale_label[128];
|
|
std::snprintf(scale_label, sizeof(scale_label), "%g m wzdluz poprzecznie x%.0f", bar, m_tpl_exagg);
|
|
drawlist->AddText(ImVec2(right + 8.0f, base - ImGui::GetTextLineHeight() * 0.5f), IM_COL32(220, 220, 220, 220), scale_label);
|
|
}
|
|
|
|
drawlist->PopClipRect();
|
|
ImGui::End();
|
|
}
|
|
|
|
void plan_panel::render_turnouts()
|
|
{
|
|
if (false == ImGui::CollapsingHeader("Rozjazdy"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_document.turnout_types.empty())
|
|
{
|
|
ImGui::TextDisabled("katalog jest pusty");
|
|
return;
|
|
}
|
|
|
|
m_turnout_type = std::clamp(m_turnout_type, 0, static_cast<int>(m_document.turnout_types.size()) - 1);
|
|
auto &chosen{m_document.turnout_types[static_cast<std::size_t>(m_turnout_type)]};
|
|
ImGui::SetNextItemWidth(260.0f);
|
|
if (ImGui::BeginCombo("Typ", chosen.name.c_str()))
|
|
{
|
|
for (std::size_t i = 0; i < m_document.turnout_types.size(); ++i)
|
|
{
|
|
if (ImGui::Selectable(m_document.turnout_types[i].name.c_str(), static_cast<int>(i) == m_turnout_type))
|
|
{
|
|
m_turnout_type = static_cast<int>(i);
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
// a document carries its own copy of the catalogue, so a plan saved before the figure
|
|
// changed keeps the numbers it was saved with - and a type laid before the iglica was a
|
|
// piece of its own has no blade in it at all. this puts the current figure back
|
|
if (auto const *preset{find_preset(chosen.name)}; preset != nullptr)
|
|
{
|
|
if (ImGui::SmallButton("przywroc z katalogu"))
|
|
{
|
|
adopt_preset(*preset, chosen);
|
|
m_sel_piece = -1;
|
|
m_status = "typ \"" + chosen.name + "\" wziety z katalogu na nowo";
|
|
solve();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("wpisuje na nowo liczby z wbudowanego katalogu: odcinek przediglicowy, iglice, luk, prosta krzyzownicowa oraz u/d/w iglicy");
|
|
}
|
|
ImGui::SameLine();
|
|
}
|
|
if (ImGui::SmallButton(m_show_template ? "zamknij rysunek" : "rysunek szablonu"))
|
|
{
|
|
m_show_template = !m_show_template;
|
|
}
|
|
|
|
render_turnout_type(chosen);
|
|
|
|
if (ImGui::Button(m_pick_turnout == 1 ? "kliknij tor..." : "Postaw rozjazd"))
|
|
{
|
|
m_pick_turnout = m_pick_turnout == 1 ? 0 : 1;
|
|
m_status = m_pick_turnout == 1 ? "kliknij tor, na ktorym ma stanac rozjazd" : "";
|
|
}
|
|
|
|
ImGui::Separator();
|
|
for (std::size_t i = 0; i < m_document.turnouts.size(); ++i)
|
|
{
|
|
auto &placement{m_document.turnouts[i]};
|
|
ImGui::PushID(static_cast<int>(1000 + i));
|
|
|
|
auto const *solved{editor::plan::find_turnout(m_solution, placement.id)};
|
|
auto const *on{editor::plan::find_track(m_document, placement.on)};
|
|
char label[200];
|
|
std::snprintf(label, sizeof(label), "%s na %s, km %.1f%s", placement.type.c_str(), on != nullptr ? on->name.c_str() : "?", placement.station,
|
|
(solved != nullptr && solved->valid) ? "" : " (nie da sie zlozyc)");
|
|
if (ImGui::Selectable(label, placement.id == m_sel_turnout))
|
|
{
|
|
m_sel_turnout = placement.id;
|
|
}
|
|
|
|
if (placement.id == m_sel_turnout)
|
|
{
|
|
ImGui::Indent();
|
|
auto dirty{false};
|
|
// przejscie rozjazdowe: while this one stands against another, where it stands is
|
|
// worked out from that one's frog and there is nothing here to type
|
|
auto const paired{placement.opposite != TurnoutId::none};
|
|
if (paired)
|
|
{
|
|
auto const *against{editor::plan::find_turnout(m_document, placement.opposite)};
|
|
ImGui::TextDisabled("stoi naprzeciw: %s, km %.2f (wyliczone)", against != nullptr ? against->type.c_str() : "rozjazdu, ktorego nie ma",
|
|
(solved != nullptr && solved->valid) ? solved->station : placement.station);
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("dwa rozjazdy jednego przejscia trzymaja sie razem:\nprzesun ktorykolwiek, a drugi idzie za nim");
|
|
}
|
|
if (ImGui::SmallButton("odepnij"))
|
|
{
|
|
// left standing where it came to stand, not where it was authored years ago
|
|
if (solved != nullptr && solved->valid)
|
|
{
|
|
placement.station = solved->station;
|
|
}
|
|
placement.opposite = TurnoutId::none;
|
|
dirty = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImGui::SetNextItemWidth(130.0f);
|
|
if (ImGui::InputDouble("km", &placement.station, 1.0, 10.0, "%.2f"))
|
|
{
|
|
dirty = true;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton(m_pick_pair != 0 ? "wskaz drugi... (Esc)" : "sparuj"))
|
|
{
|
|
m_pick_pair = m_pick_pair != 0 ? 0 : 1;
|
|
m_pick_draw = 0;
|
|
m_status = m_pick_pair != 0 ? "kliknij rozjazd, naprzeciw ktorego ten ma stanac" : "";
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("przejscie rozjazdowe: ten rozjazd stanie naprzeciw wskazanego,\n"
|
|
"na torze, w ktory tamten wchodzi. od tej pory jego km wynika\n"
|
|
"z tamtej iglicy - przesuniecie jednego przesuwa oba");
|
|
}
|
|
}
|
|
int hand{placement.hand >= 0 ? 0 : 1};
|
|
ImGui::SetNextItemWidth(130.0f);
|
|
if (ImGui::Combo("odgalezia", &hand, "w lewo\0w prawo\0"))
|
|
{
|
|
placement.hand = hand == 0 ? 1 : -1;
|
|
dirty = true;
|
|
}
|
|
if (ImGui::Checkbox("zwrotny w kierunku km", &placement.facing))
|
|
{
|
|
dirty = true;
|
|
}
|
|
// lukowanie: standing on a curve is the ordinary way to get a rozjazd lukowy, and the
|
|
// solver takes the radius from under the PR. asking for one by hand is for a rozjazd
|
|
// bent on a track drawn straight
|
|
if (ImGui::Checkbox("wygiecie z toru pod rozjazdem", &placement.bend_from_track))
|
|
{
|
|
dirty = true;
|
|
}
|
|
if (false == placement.bend_from_track)
|
|
{
|
|
auto radius{placement.bend != 0.0 ? std::abs(1.0 / placement.bend) : 0.0};
|
|
int way{placement.bend >= 0.0 ? 0 : 1};
|
|
ImGui::SetNextItemWidth(130.0f);
|
|
auto touched{ImGui::InputDouble("R zasadniczego", &radius, 10.0, 100.0, "%.3f")};
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
touched = ImGui::Combo("wygiety", &way, "w lewo\0w prawo\0") || touched;
|
|
if (touched)
|
|
{
|
|
placement.bend = radius > 1.0 ? (way == 0 ? 1.0 : -1.0) / radius : 0.0;
|
|
dirty = true;
|
|
}
|
|
}
|
|
if (solved != nullptr && solved->valid)
|
|
{
|
|
ImGui::TextDisabled("styczne a %.3f / b %.3f m (razem %.3f), na torze zasadniczym %.3f m", solved->tangent_front, solved->tangent_back,
|
|
solved->tangent_front + solved->tangent_back, solved->through_length);
|
|
ImGui::TextDisabled("odgalezienie %.3f m", solved->diverging_length);
|
|
ImGui::TextDisabled("%s", bend_text(solved->bend, solved->diverging_curvature, solved->bend_angle).c_str());
|
|
if (auto const *limit{bend_limit(solved->diverging_curvature)}; limit != nullptr)
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.45f, 1.0f), "%s", limit);
|
|
}
|
|
if (solved->has_blade)
|
|
{
|
|
ImGui::TextDisabled("iglica: ostrze a %.4f m, dziob do %.4f m, struganie %.3f m", solved->blade_station, solved->nose_end, solved->planing_length);
|
|
}
|
|
}
|
|
// a rozjazd left standing across a joint, or off the end, needs a way back that does not
|
|
// mean guessing kilometres by hand: the nearest element long enough to hold it, whole
|
|
if (solved != nullptr && solved->valid)
|
|
{
|
|
auto const *through{editor::plan::find_track(m_solution, placement.on)};
|
|
double curvature{0.0};
|
|
double behind{0.0};
|
|
double ahead{0.0};
|
|
auto const room{through != nullptr && editor::plan::steady_room(*through, placement.station, curvature, behind, ahead)
|
|
? (placement.facing ? ahead : behind)
|
|
: -1.0};
|
|
if (through != nullptr && room < solved->through_length - 1e-6)
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.45f, 1.0f), "rozjazd nie miesci sie na odcinku toru, na ktorym stoi");
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("wstaw na odcinek"))
|
|
{
|
|
double station{0.0};
|
|
auto best{std::numeric_limits<double>::max()};
|
|
auto walked{0.0};
|
|
for (auto const &element : through->elements)
|
|
{
|
|
if (std::abs(element.k1 - element.k0) < 1e-12 && element.length >= solved->through_length)
|
|
{
|
|
// anywhere on this straight with the whole rozjazd still on it
|
|
auto const low{placement.facing ? walked : walked + solved->through_length};
|
|
auto const high{placement.facing ? walked + element.length - solved->through_length : walked + element.length};
|
|
auto const put{std::clamp(placement.station, low, high)};
|
|
if (std::abs(put - placement.station) < best)
|
|
{
|
|
best = std::abs(put - placement.station);
|
|
station = put;
|
|
}
|
|
}
|
|
walked += element.length;
|
|
}
|
|
if (best < std::numeric_limits<double>::max())
|
|
{
|
|
placement.station = station;
|
|
dirty = true;
|
|
}
|
|
else
|
|
{
|
|
m_status = "zaden odcinek tego toru nie jest dosc dlugi na ten rozjazd";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (ImGui::SmallButton("usun rozjazd"))
|
|
{
|
|
m_document.turnouts.erase(m_document.turnouts.begin() + static_cast<long>(i));
|
|
m_sel_turnout = TurnoutId::none;
|
|
ImGui::Unindent();
|
|
ImGui::PopID();
|
|
solve();
|
|
return;
|
|
}
|
|
ImGui::Unindent();
|
|
if (dirty)
|
|
{
|
|
solve();
|
|
}
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
void plan_panel::render_curvature_strip()
|
|
{
|
|
auto const *solved{editor::plan::find_track(m_solution, m_track)};
|
|
if (solved == nullptr || solved->elements.empty() || solved->length <= 0.0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ImGui::Separator();
|
|
ImGui::TextUnformatted("krzywizna po pikietazu");
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(prosta na osi, luk to stopien, krzywa przejsciowa to skos; sciana to zlacze bez krzywej)");
|
|
|
|
auto const width{std::max(120.0f, ImGui::GetContentRegionAvailWidth())};
|
|
auto const height{92.0f};
|
|
auto const origin{ImGui::GetCursorScreenPos()};
|
|
ImGui::InvisibleButton("##kappa", ImVec2(width, height));
|
|
auto const hovered{ImGui::IsItemHovered()};
|
|
auto *drawlist{ImGui::GetWindowDrawList()};
|
|
|
|
drawlist->AddRectFilled(origin, ImVec2(origin.x + width, origin.y + height), IM_COL32(16, 20, 26, 235), 3.0f);
|
|
|
|
// the scale is the tightest curvature on the track, so the picture always fills the strip; a
|
|
// straight track has nothing to scale to and is drawn as the axis it is
|
|
auto peak{0.0};
|
|
for (auto const &element : solved->elements)
|
|
{
|
|
peak = std::max({peak, std::abs(element.k0), std::abs(element.k1)});
|
|
}
|
|
auto const axis{origin.y + height * 0.5f};
|
|
auto const scale{peak > 1e-9 ? (height * 0.5f - 10.0f) / static_cast<float>(peak) : 0.0f};
|
|
auto const x_of{[&](double const Station) { return origin.x + static_cast<float>(Station / solved->length) * width; }};
|
|
auto const y_of{[&](double const Curvature) { return axis - static_cast<float>(Curvature) * scale; }};
|
|
|
|
drawlist->AddLine(ImVec2(origin.x, axis), ImVec2(origin.x + width, axis), IM_COL32(120, 130, 145, 180), 1.0f);
|
|
|
|
// every element as the ramp its curvature runs along, in the colour it is drawn on the map with
|
|
auto station{0.0};
|
|
for (auto const &element : solved->elements)
|
|
{
|
|
auto const from{x_of(station)};
|
|
auto const to{x_of(station + element.length)};
|
|
auto const selected{element.id == m_sel_element};
|
|
auto const colour{element_colour(element.kind, selected)};
|
|
ImVec2 const a(from, y_of(element.k0));
|
|
ImVec2 const b(to, y_of(element.k1));
|
|
// the area under it, so a left turn reads as up from the axis and a right one as down
|
|
drawlist->AddQuadFilled(ImVec2(from, axis), a, b, ImVec2(to, axis), (colour & 0x00ffffffu) | (selected ? 0x66000000u : 0x33000000u));
|
|
drawlist->AddLine(a, b, colour, selected ? 3.0f : 2.0f);
|
|
if (station > 0.0)
|
|
{
|
|
drawlist->AddLine(ImVec2(from, origin.y + 4.0f), ImVec2(from, origin.y + height - 4.0f), IM_COL32(90, 100, 115, 150), 1.0f);
|
|
}
|
|
station += element.length;
|
|
}
|
|
|
|
// a joint where the curvature jumps: the wall between the two ends
|
|
station = 0.0;
|
|
for (std::size_t i = 0; i < solved->elements.size(); ++i)
|
|
{
|
|
station += solved->elements[i].length;
|
|
if (i + 1 >= solved->elements.size())
|
|
{
|
|
break;
|
|
}
|
|
auto const before{solved->elements[i].k1};
|
|
auto const after{solved->elements[i + 1].k0};
|
|
if (std::abs(after - before) > 1e-9)
|
|
{
|
|
drawlist->AddLine(ImVec2(x_of(station), y_of(before)), ImVec2(x_of(station), y_of(after)), IM_COL32(255, 120, 120, 230), 2.0f);
|
|
}
|
|
}
|
|
|
|
// the turnouts standing on this track, where they stand
|
|
for (auto const &placement : m_document.turnouts)
|
|
{
|
|
if (placement.on != m_track || placement.station < 0.0 || placement.station > solved->length)
|
|
{
|
|
continue;
|
|
}
|
|
auto const at{x_of(placement.station)};
|
|
drawlist->AddLine(ImVec2(at, axis - 6.0f), ImVec2(at, axis + 6.0f), IM_COL32(255, 210, 90, 235), 2.0f);
|
|
}
|
|
|
|
// what the cursor is over, in the terms the strip is drawn in
|
|
if (hovered)
|
|
{
|
|
auto const mouse{ImGui::GetIO().MousePos};
|
|
auto const at{std::clamp(static_cast<double>((mouse.x - origin.x) / width), 0.0, 1.0) * solved->length};
|
|
drawlist->AddLine(ImVec2(x_of(at), origin.y + 2.0f), ImVec2(x_of(at), origin.y + height - 2.0f), IM_COL32(235, 240, 250, 120), 1.0f);
|
|
|
|
auto walked{0.0};
|
|
for (auto const &element : solved->elements)
|
|
{
|
|
if (at <= walked + element.length || &element == &solved->elements.back())
|
|
{
|
|
auto const into{std::clamp(at - walked, 0.0, element.length)};
|
|
auto const k{element.length > 0.0 ? element.k0 + (element.k1 - element.k0) * (into / element.length) : element.k0};
|
|
ImGui::BeginTooltip();
|
|
ImGui::Text("km %s", chainage(at).c_str());
|
|
ImGui::Text("%s, R %s", kind_name(element.kind), std::abs(k) > 1e-9 ? to_string(1.0 / std::abs(k), 0).c_str() : "prosta");
|
|
ImGui::EndTooltip();
|
|
// a click on the strip picks the element under it, the same as clicking the map
|
|
if (ImGui::IsMouseClicked(0))
|
|
{
|
|
m_sel_element = element.id;
|
|
}
|
|
break;
|
|
}
|
|
walked += element.length;
|
|
}
|
|
}
|
|
|
|
// the ends, and how tight it ever gets
|
|
ImGui::TextDisabled("km %s ... %s, dlugosc %.2f m%s", chainage(0.0).c_str(), chainage(solved->length).c_str(), solved->length,
|
|
peak > 1e-9 ? (" najciasniej R=" + to_string(1.0 / peak, 0) + " m").c_str() : "");
|
|
}
|
|
|
|
void plan_panel::render_diagnostics()
|
|
{
|
|
if (m_solution.diagnostics.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// a fit that does not work says why, with the figure that matters in it. it used to come back as
|
|
// nullopt and simply disappear from the drawing
|
|
char header[64];
|
|
std::snprintf(header, sizeof(header), "Nie wyszlo (%zu)###diag", m_solution.diagnostics.size());
|
|
if (false == ImGui::CollapsingHeader(header, ImGuiTreeNodeFlags_DefaultOpen))
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (std::size_t i = 0; i < m_solution.diagnostics.size(); ++i)
|
|
{
|
|
auto const &diagnostic{m_solution.diagnostics[i]};
|
|
ImGui::PushID(static_cast<int>(2000 + i));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.60f, 0.55f, 1.0f));
|
|
auto const *track{editor::plan::find_track(m_document, diagnostic.track)};
|
|
if (ImGui::Selectable(diagnostic.text.c_str()))
|
|
{
|
|
if (diagnostic.track != TrackId::none)
|
|
{
|
|
m_track = diagnostic.track;
|
|
}
|
|
if (diagnostic.element != ElementId::none)
|
|
{
|
|
m_sel_element = diagnostic.element;
|
|
}
|
|
if (diagnostic.turnout != TurnoutId::none)
|
|
{
|
|
m_sel_turnout = diagnostic.turnout;
|
|
}
|
|
}
|
|
ImGui::PopStyleColor();
|
|
if (track != nullptr)
|
|
{
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(%s)", track->name.c_str());
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
void plan_panel::render_storage()
|
|
{
|
|
ImGui::Separator();
|
|
|
|
// the plan is chosen from what is in the directory - there is nothing to type. a name is only
|
|
// needed for a plan that does not exist yet, and that is what "Zapisz jako" is for
|
|
auto const current{editor::plan::io::project_file(m_path)};
|
|
auto folder{std::filesystem::path{current}.parent_path()};
|
|
if (folder.empty())
|
|
{
|
|
folder = std::filesystem::path{"."};
|
|
}
|
|
|
|
ImGui::SetNextItemWidth(240.0f);
|
|
if (ImGui::BeginCombo("##plany", std::filesystem::path{current}.filename().generic_string().c_str()))
|
|
{
|
|
std::error_code failed;
|
|
auto found{0};
|
|
for (auto const &entry : std::filesystem::directory_iterator{folder, failed})
|
|
{
|
|
if (false == entry.is_regular_file(failed) || entry.path().extension() != ".m0s")
|
|
{
|
|
continue;
|
|
}
|
|
++found;
|
|
auto const path{(folder / entry.path().filename()).generic_string()};
|
|
if (ImGui::Selectable(entry.path().filename().generic_string().c_str(), path == current))
|
|
{
|
|
std::snprintf(m_path, sizeof(m_path), "%s", path.c_str());
|
|
m_status = "wybrane: " + path;
|
|
}
|
|
}
|
|
if (found == 0)
|
|
{
|
|
ImGui::TextDisabled("nie ma tu zadnego .m0s");
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("plany z %s. wybor tylko wskazuje plik - wczytuje go Load", folder.generic_string().c_str());
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Save"))
|
|
{
|
|
save_plan();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Zapisz jako"))
|
|
{
|
|
// the name of the plan on screen is the obvious starting point for the next one
|
|
std::snprintf(m_saveas, sizeof(m_saveas), "%s", std::filesystem::path{current}.filename().generic_string().c_str());
|
|
ImGui::OpenPopup("Save as");
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Load"))
|
|
{
|
|
auto const loaded{editor::plan::io::load(m_path)};
|
|
if (loaded)
|
|
{
|
|
m_document = *loaded;
|
|
seed_catalogue();
|
|
if (m_document.tracks.empty())
|
|
{
|
|
new_track();
|
|
}
|
|
else
|
|
{
|
|
m_track = m_document.tracks.front().id;
|
|
std::snprintf(m_namebuf, sizeof(m_namebuf), "%s", m_document.tracks.front().name.c_str());
|
|
}
|
|
if (m_document.origin_set)
|
|
{
|
|
Global.scenery_georeferenced = m_document.georeferenced;
|
|
Global.scenery_origin = {m_document.origin_x, m_document.origin_y};
|
|
}
|
|
m_document.origin_set = true;
|
|
m_document.georeferenced = Global.scenery_georeferenced;
|
|
m_document.origin_x = Global.scenery_origin.x;
|
|
m_document.origin_y = Global.scenery_origin.y;
|
|
m_pending = false;
|
|
m_pick_join = 0;
|
|
m_join_first = TrackId::none;
|
|
clear_selection();
|
|
solve();
|
|
if (m_document.view_extent > 0.0)
|
|
{
|
|
auto &camera{editor_mode::get_camera()};
|
|
auto const centre{plan_to_world(m_document.view_x, m_document.view_y)};
|
|
camera.Pos.x = centre.x;
|
|
camera.Pos.z = centre.z;
|
|
Global.editor_ortho_extent = std::clamp(static_cast<float>(m_document.view_extent), 5.0f, 20000.0f);
|
|
}
|
|
else
|
|
{
|
|
go_to_plan();
|
|
}
|
|
if (false == m_document.scn_path.empty())
|
|
{
|
|
std::snprintf(m_scn_path, sizeof(m_scn_path), "%s", m_document.scn_path.c_str());
|
|
}
|
|
m_status = "wczytane: " + editor::plan::io::project_file(m_path);
|
|
}
|
|
else
|
|
{
|
|
// version 1 files are not read: they described a different model
|
|
m_status = "nie da sie odczytac " + editor::plan::io::project_file(m_path);
|
|
}
|
|
}
|
|
ImGui::SetNextItemWidth(240.0f);
|
|
ImGui::InputText("##scnpath", m_scn_path, sizeof(m_scn_path));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Export SCN"))
|
|
{
|
|
export_scn(false);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Export i uruchom"))
|
|
{
|
|
export_scn(true);
|
|
}
|
|
ImGui::Checkbox("uruchom w edytorze", &m_scn_run_editor);
|
|
ImGui::SameLine();
|
|
// the trainset is what the simulator looks for a player train in: without one it loads the
|
|
// scenery and drops straight back out, so driving needs it and looking around does not
|
|
ImGui::Checkbox("z pociagiem", &m_scn_trainset);
|
|
if (false == m_status.empty())
|
|
{
|
|
ImGui::TextDisabled("%s", m_status.c_str());
|
|
}
|
|
for (auto const &warning : m_scn_warnings)
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "%s", warning.c_str());
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal("Save as", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
ImGui::TextDisabled("plan wyladuje w %s", folder.generic_string().c_str());
|
|
ImGui::SetNextItemWidth(280.0f);
|
|
ImGui::InputText("nazwa", m_saveas, sizeof(m_saveas));
|
|
auto const write{ImGui::Button("Zapisz")};
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Anuluj"))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
if (write)
|
|
{
|
|
std::string name{m_saveas};
|
|
while (false == name.empty() && name.back() == ' ')
|
|
{
|
|
name.pop_back();
|
|
}
|
|
if (name.empty())
|
|
{
|
|
m_status = "plan musi miec nazwe";
|
|
}
|
|
else
|
|
{
|
|
// the extension is the format's, not the user's business
|
|
if (name.size() < 4 || name.substr(name.size() - 4) != ".m0s")
|
|
{
|
|
name += ".m0s";
|
|
}
|
|
std::snprintf(m_path, sizeof(m_path), "%s", (folder / name).generic_string().c_str());
|
|
save_plan();
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
void plan_panel::export_scn(bool const Run_)
|
|
{
|
|
solve();
|
|
m_scn_warnings.clear();
|
|
// where a plan exports to belongs to the plan: say it once and it is there the next time the
|
|
// project is opened, whichever MaSzyna it was drawn for
|
|
m_document.scn_path = m_scn_path;
|
|
editor::plan::io::ScnExportOptions opt;
|
|
opt.origin_east = Global.scenery_origin.x;
|
|
opt.origin_north = Global.scenery_origin.y;
|
|
opt.trainset = m_scn_trainset;
|
|
std::ofstream out(m_scn_path, std::ios::binary | std::ios::trunc);
|
|
if (!out)
|
|
{
|
|
m_status = "could not write " + std::string(m_scn_path);
|
|
return;
|
|
}
|
|
auto const result{editor::plan::io::export_scn(m_document, m_solution, opt, out)};
|
|
out.flush();
|
|
if (false == out.good())
|
|
{
|
|
m_status = "could not write " + std::string(m_scn_path);
|
|
return;
|
|
}
|
|
out.close();
|
|
m_status = "wyeksportowano " + std::to_string(result.tracks) + " odcinków i " + std::to_string(result.switches) + " zwrotnic";
|
|
m_scn_warnings = result.warnings;
|
|
if (false == m_scn_warnings.empty())
|
|
{
|
|
m_status += ", " + std::to_string(m_scn_warnings.size()) + " uwag";
|
|
}
|
|
if (Run_)
|
|
{
|
|
run_scn();
|
|
}
|
|
}
|
|
|
|
void plan_panel::run_scn()
|
|
{
|
|
// the simulator names a scenery relative to scenery/, so that is what is handed over rather than
|
|
// the path the file was written to
|
|
std::string scenery{m_scn_path};
|
|
std::string root;
|
|
auto const slash{scenery.find_last_of("/\\")};
|
|
if (slash != std::string::npos)
|
|
{
|
|
// the game is wherever the scenery folder is: exporting into another MaSzyna's scenery and
|
|
// then running ours out of this directory would find the file and none of the models
|
|
auto const folder{scenery.substr(0, slash)};
|
|
auto const parent{folder.find_last_of("/\\")};
|
|
root = parent != std::string::npos ? folder.substr(0, parent) : std::string{};
|
|
scenery = scenery.substr(slash + 1);
|
|
}
|
|
|
|
std::vector<std::string> arguments{"-s", scenery};
|
|
if (m_scn_run_editor)
|
|
{
|
|
arguments.push_back("-editor");
|
|
}
|
|
if (spawn_detached(executable_path(), arguments, root))
|
|
{
|
|
m_status += "; uruchomiono " + scenery + (root.empty() ? "" : " w " + root);
|
|
}
|
|
else
|
|
{
|
|
m_status += "; nie udało się uruchomić symulatora";
|
|
}
|
|
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// map dialogs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void plan_panel::save_plan()
|
|
{
|
|
// where the camera stands and what the scenery's zero means go into the file with the drawing:
|
|
// opening the plan again puts the view back where the work was left
|
|
double planx{0.0};
|
|
double plany{0.0};
|
|
world_to_plan(editor_mode::get_camera().Pos, planx, plany);
|
|
m_document.view_x = planx;
|
|
m_document.view_y = plany;
|
|
m_document.view_extent = static_cast<double>(Global.editor_ortho_extent);
|
|
m_document.origin_set = true;
|
|
m_document.georeferenced = Global.scenery_georeferenced;
|
|
m_document.origin_x = Global.scenery_origin.x;
|
|
m_document.origin_y = Global.scenery_origin.y;
|
|
|
|
auto const file{editor::plan::io::project_file(m_path)};
|
|
m_status = editor::plan::io::save(m_document, m_path) ? "zapisane: " + file : "nie da sie zapisac " + file;
|
|
}
|
|
|
|
void plan_panel::render_newmap_dialog()
|
|
{
|
|
if (false == ImGui::BeginPopupModal("New map", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ImGui::TextUnformatted("What is this scenery going to be?");
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::Button("Fictional", ImVec2(160.0f, 0.0f)))
|
|
{
|
|
start_map(false, 0.0, 0.0);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("coordinates mean nothing outside the scenery");
|
|
|
|
if (ImGui::Button("Real place", ImVec2(160.0f, 0.0f)))
|
|
{
|
|
// start the picker on the middle of the country, showing the whole of it
|
|
m_pickingplace = true;
|
|
m_pickx = 500000.0;
|
|
m_picky = 350000.0;
|
|
m_mapviewx = m_pickx;
|
|
m_mapviewy = m_picky;
|
|
m_mapscale = 0.0;
|
|
ImGui::CloseCurrentPopup();
|
|
ImGui::OpenPopup("Scenery location");
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("pin the scenery's zero to a point in Poland");
|
|
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Cancel"))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
void plan_panel::render_location_dialog()
|
|
{
|
|
if (m_pickingplace)
|
|
{
|
|
// the popup has to be opened from the same stack level it is drawn on
|
|
ImGui::OpenPopup("Scenery location");
|
|
m_pickingplace = false;
|
|
}
|
|
|
|
if (false == ImGui::BeginPopupModal("Scenery location", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// the request is one square image, so the view is square too and the pixels map one to one
|
|
int const mappixels = 512;
|
|
ImVec2 const mapsize{static_cast<float>(mappixels), static_cast<float>(mappixels)};
|
|
auto const origin{ImGui::GetCursorScreenPos()};
|
|
ImGui::InvisibleButton("poland_map", mapsize);
|
|
auto const hovered{ImGui::IsItemHovered()};
|
|
|
|
// fit the whole country the first time round; afterwards the wheel decides
|
|
if (m_mapscale <= 0.0)
|
|
{
|
|
glm::dvec2 min;
|
|
glm::dvec2 max;
|
|
editor::poland_extent(min, max);
|
|
m_mapviewx = (min.x + max.x) * 0.5;
|
|
m_mapviewy = (min.y + max.y) * 0.5;
|
|
m_mapscale = mapsize.x / std::max({max.x - min.x, max.y - min.y, 1.0});
|
|
}
|
|
|
|
ImVec2 const centre{origin.x + mapsize.x * 0.5f, origin.y + mapsize.y * 0.5f};
|
|
auto const to_screen = [&](double const X, double const Y) {
|
|
return ImVec2{centre.x + static_cast<float>((X - m_mapviewx) * m_mapscale), centre.y - static_cast<float>((Y - m_mapviewy) * m_mapscale)};
|
|
};
|
|
auto const to_map = [&](ImVec2 const &Point, double &X, double &Y) {
|
|
X = m_mapviewx + (Point.x - centre.x) / m_mapscale;
|
|
Y = m_mapviewy - (Point.y - centre.y) / m_mapscale;
|
|
};
|
|
|
|
auto const mouse{ImGui::GetIO().MousePos};
|
|
if (hovered && ImGui::GetIO().MouseWheel != 0.0f)
|
|
{
|
|
double anchorx{0.0};
|
|
double anchory{0.0};
|
|
to_map(mouse, anchorx, anchory);
|
|
m_mapscale = std::clamp(m_mapscale * std::pow(1.2, ImGui::GetIO().MouseWheel), 1e-5, 1.0);
|
|
m_mapviewx = anchorx - (mouse.x - centre.x) / m_mapscale;
|
|
m_mapviewy = anchory + (mouse.y - centre.y) / m_mapscale;
|
|
}
|
|
if (hovered && ImGui::IsMouseClicked(0))
|
|
{
|
|
to_map(mouse, m_pickx, m_picky);
|
|
}
|
|
|
|
auto *drawlist{ImGui::GetWindowDrawList()};
|
|
auto const mapend{ImVec2(origin.x + mapsize.x, origin.y + mapsize.y)};
|
|
drawlist->PushClipRect(origin, mapend, true);
|
|
drawlist->AddRectFilled(origin, mapend, IM_COL32(22, 30, 38, 255));
|
|
|
|
// the topographic base map behind everything, asked for at exactly the box on screen
|
|
double viewminx{0.0};
|
|
double viewminy{0.0};
|
|
double viewmaxx{0.0};
|
|
double viewmaxy{0.0};
|
|
to_map(origin, viewminx, viewmaxy);
|
|
to_map(mapend, viewmaxx, viewminy);
|
|
auto const backdrop{m_topomap.texture_for({viewminx, viewminy, viewmaxx, viewmaxy}, mappixels)};
|
|
if (backdrop != 0)
|
|
{
|
|
// the picture on screen is the one that was fetched, which may lag the current view by a
|
|
// request; drawing it against its own box keeps it registered while the new one arrives
|
|
auto const &covered{m_topomap.covered()};
|
|
drawlist->AddImage(reinterpret_cast<ImTextureID>(static_cast<intptr_t>(backdrop)), to_screen(covered.min_x, covered.max_y), to_screen(covered.max_x, covered.min_y), ImVec2(0.0f, 0.0f),
|
|
ImVec2(1.0f, 1.0f));
|
|
}
|
|
|
|
auto const picked{to_screen(m_pickx, m_picky)};
|
|
drawlist->AddCircleFilled(picked, 5.0f, IM_COL32(255, 210, 90, 255));
|
|
drawlist->AddLine(ImVec2(picked.x - 12.0f, picked.y), ImVec2(picked.x + 12.0f, picked.y), IM_COL32(255, 210, 90, 200));
|
|
drawlist->AddLine(ImVec2(picked.x, picked.y - 12.0f), ImVec2(picked.x, picked.y + 12.0f), IM_COL32(255, 210, 90, 200));
|
|
|
|
drawlist->PopClipRect();
|
|
|
|
ImGui::TextDisabled(m_topomap.loading() ? "fetching the topographic map..." : "click to place the scenery's zero, wheel zooms");
|
|
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
ImGui::InputDouble("easting", &m_pickx, 100.0, 1000.0, "%.0f");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
ImGui::InputDouble("northing", &m_picky, 100.0, 1000.0, "%.0f");
|
|
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Use this place", ImVec2(160.0f, 0.0f)))
|
|
{
|
|
start_map(true, m_pickx, m_picky);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel"))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
void plan_panel::start_map(bool const Georeferenced, double const Originx, double const Originy)
|
|
{
|
|
Global.scenery_georeferenced = Georeferenced;
|
|
Global.scenery_origin = {Originx, Originy};
|
|
|
|
Document fresh;
|
|
fresh.origin_set = true;
|
|
fresh.georeferenced = Georeferenced;
|
|
fresh.origin_x = Originx;
|
|
fresh.origin_y = Originy;
|
|
m_document = std::move(fresh);
|
|
seed_catalogue();
|
|
new_track();
|
|
|
|
m_pick_turnout = 0;
|
|
m_pick_parallel = 0;
|
|
m_pick_join = 0;
|
|
m_join_first = TrackId::none;
|
|
solve();
|
|
|
|
// the scenery's zero is where the work starts, whichever frame it stands for
|
|
auto &camera{editor_mode::get_camera()};
|
|
camera.Pos.x = 0.0;
|
|
camera.Pos.z = 0.0;
|
|
|
|
// a new map starts on empty ground - whatever scenery was loaded has nothing to do with it
|
|
Global.editor_reset_scenery = true;
|
|
|
|
m_status = Georeferenced ? "map pinned at " + to_string(Originx, 0) + ", " + to_string(Originy, 0) + " (EPSG:2180)" : "fictional map";
|
|
}
|