17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 02:49:19 +02:00

Editor: fold the plan library into the tree as modules, drop its own name

The track layout library came in as a subproject of its own -- its own
CMakeLists, its own install and export rules, its own include tree -- which only
made the build harder to follow now that everything else is a module. It is part
of the editor, so it lives where the editor lives.

- editor/plan_*.cppm / plan_*.cpp: the fifteen interface units and twelve
  implementation units, flat in editor/ beside editorTerrain and orthophoto, with
  the plan_ prefix keeping them clear of what was already there (editor/turnouts
  is the preset catalogue, so the geometry is plan_turnout_geometry).
- modules are named eu07.editor.plan_*, like every other module in the tree.
- namespaces move under the editor's own: editor::plan, editor::plan::geometry,
  ::io, ::render. The library's old name is gone from the sources and the docs.
- every moved file carries the MPL header the rest of the tree carries; it had
  none. version.hpp went with the subproject: macros only, and unused.
- the umbrella header is dropped -- nothing imported it.
- the unit tests survive the fold: editor/tests, behind EU07_EDITOR_PLAN_TESTS
  (off by default), compiles the interfaces a second time into a small library of
  its own, because a test can only import modules from a target it links and
  linking the game would drag the whole simulator in. The code under test needs
  nothing but a compiler. 7/7 pass.
- a switch may now be written with two dashes as well: --editor beside -editor.

Builds with clang 22 (-DWITH_BETTER_RENDERER=OFF); -editor and --editor both come
up in the editor on empty ground.
This commit is contained in:
maj00r
2026-09-13 13:31:01 +02:00
parent c15e200e37
commit 321c9dd7ab
56 changed files with 931 additions and 952 deletions

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ ci_shadervalidator/validateshaders
# CMake build output
build/
build-*/
bin/
#Visual Studio [Code] folders

View File

@@ -199,6 +199,18 @@ set(SOURCES
"editor/polandmap.cpp"
"editor/turnouts.cpp"
"editor/orthophoto.cpp"
"editor/plan_chain.cpp"
"editor/plan_document_io.cpp"
"editor/plan_join.cpp"
"editor/plan_layout.cpp"
"editor/plan_model.cpp"
"editor/plan_rail_renderer.cpp"
"editor/plan_render.cpp"
"editor/plan_scn_export.cpp"
"editor/plan_sketch.cpp"
"editor/plan_solution.cpp"
"editor/plan_tilecache.cpp"
"editor/plan_turnout_geometry.cpp"
"editor/editorTerrain.cpp"
"editor/editorTerrainStreamer.cpp"
"application/editormode.cpp"
@@ -633,9 +645,6 @@ target_link_libraries(${PROJECT_NAME} nlohmann_json::nlohmann_json)
target_include_directories(${PROJECT_NAME} PRIVATE ${NLOHMANN_JSON_INCLUDE_INSTALL_DIR})
# track layout domain model used by the editor; plain C++20, no dependencies of its own
add_subdirectory(maj0sted)
set_target_properties(maj0sted PROPERTIES FOLDER "libraries")
target_link_libraries(${PROJECT_NAME} maj0sted::maj0sted)
@@ -716,6 +725,8 @@ endif()
# copied off it rather than restated; this has to happen after all the
# third-party wiring above and before betterRenderer is added, or the mirrored
# link list would point back at betterRenderer and make the dependency circular.
add_subdirectory(editor/tests)
add_library(eu07_modules STATIC)
get_target_property(EU07_INCLUDES ${PROJECT_NAME} INCLUDE_DIRECTORIES)

View File

@@ -63,7 +63,7 @@ osobnym, jawnym polem.** Kernel do tego już mamy: `geometry::layout_segment`.
## 3. Model docelowy
Płasko, w `maj0sted::editor`, Id jako mocne typy.
Płasko, w `editor::plan`, Id jako mocne typy.
### 3.1. Odcinek jest bytem

View File

@@ -1176,6 +1176,13 @@ int eu07_application::init_settings(int Argc, char *Argv[])
std::string token{Argv[i]};
// a switch may be written with one dash or two: both spellings are common on the command
// line and neither says anything different, so the second dash is simply dropped here
if (token.size() > 2 && token.compare(0, 2, "--") == 0)
{
token.erase(0, 1);
}
if (token == "-s")
{
if (i + 1 < Argc)
@@ -1199,7 +1206,8 @@ int eu07_application::init_settings(int Argc, char *Argv[])
{
std::cout << "usage: " << std::string(Argv[0]) << " [-s sceneryfilepath]"
<< " [-v vehiclename]"
<< " [-editor]" << std::endl;
<< " [-editor]"
<< " (every switch also takes a double dash: --editor)" << std::endl;
return -1;
}
}

View File

@@ -6,24 +6,25 @@ module;
#include <limits>
#include <numbers>
#include <string>
#include <variant>
#include <vector>
#include "imgui/imgui.h"
#include "utilities/Globals_macros.h"
#include "maj0sted/editor/layout.hpp"
#include "maj0sted/editor/model.hpp"
#include "maj0sted/editor/render.hpp"
#include "maj0sted/editor/solution.hpp"
#include "maj0sted/editor/tile_cache.hpp"
#include "maj0sted/io/scn_export.hpp"
#include "maj0sted/editor/join.hpp"
#include "maj0sted/editor/sketch.hpp"
#include "maj0sted/io/document_io.hpp"
#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;
@@ -44,13 +45,13 @@ http://mozilla.org/MPL/2.0/.
namespace
{
using maj0sted::editor::Element;
using maj0sted::editor::ElementId;
using maj0sted::editor::Free;
using maj0sted::editor::Kind;
using maj0sted::editor::Parallel;
using maj0sted::editor::TrackId;
using maj0sted::editor::TurnoutId;
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
@@ -286,7 +287,7 @@ editor::turnout_preset const *find_preset(std::string const &Name_)
// 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_, maj0sted::editor::TurnoutType &Type_)
void adopt_preset(editor::turnout_preset const &Preset_, editor::plan::TurnoutType &Type_)
{
Type_.name = Preset_.name;
Type_.crossing_n = Preset_.crossing_n;
@@ -298,7 +299,7 @@ void adopt_preset(editor::turnout_preset const &Preset_, maj0sted::editor::Turno
for (auto const &piece : Preset_.pieces)
{
Type_.pieces.push_back(
maj0sted::editor::TurnoutPieceSpec{piece.part, piece.length, piece.radius_start, piece.radius_end, piece.turn_in});
editor::plan::TurnoutPieceSpec{piece.part, piece.length, piece.radius_start, piece.radius_end, piece.turn_in});
}
}
@@ -436,7 +437,7 @@ void plan_panel::seed_catalogue()
[&](auto const &Type_) { return Type_.name == preset.name; })};
if (existing == m_document.turnout_types.end())
{
maj0sted::editor::TurnoutType type;
editor::plan::TurnoutType type;
adopt_preset(preset, type);
m_document.turnout_types.push_back(std::move(type));
continue;
@@ -460,24 +461,24 @@ void plan_panel::seed_catalogue()
void plan_panel::solve()
{
m_solution = maj0sted::editor::solve(m_document);
m_rails = maj0sted::editor::render_rails(m_solution);
m_solution = editor::plan::solve(m_document);
m_rails = editor::plan::render_rails(m_solution);
}
plan_panel::Track *plan_panel::current_track()
{
return maj0sted::editor::find_track(m_document, m_track);
return editor::plan::find_track(m_document, m_track);
}
plan_panel::Track const *plan_panel::current_track() const
{
return maj0sted::editor::find_track(m_document, m_track);
return editor::plan::find_track(m_document, m_track);
}
void plan_panel::new_track()
{
Track track;
track.id = maj0sted::editor::mint_track(m_document);
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;
@@ -548,8 +549,8 @@ void plan_panel::append_point_to(double const X, double const Y)
m_status = "za krotka prosta";
return;
}
track->anchor = maj0sted::editor::AtPose{m_startx, m_starty, std::atan2(dx, dy)};
track->elements.push_back(maj0sted::editor::make_line(m_document, length));
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_pending = false;
solve();
m_status.clear();
@@ -557,7 +558,7 @@ void plan_panel::append_point_to(double const X, double const Y)
}
// the track ends on the last click, so that end is the corner this one turns at
auto const *solved{maj0sted::editor::find_track(m_solution, track->id)};
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";
@@ -574,7 +575,7 @@ void plan_panel::append_point_to(double const X, double const Y)
return;
}
auto const corner{maj0sted::editor::fit_corner(corner_pose.hx, corner_pose.hy, dx / reach, dy / reach, m_corner_radius)};
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
@@ -601,8 +602,8 @@ void plan_panel::append_point_to(double const X, double const Y)
}
previous.length -= corner.tangent;
track->elements.push_back(maj0sted::editor::make_arc(m_document, m_corner_radius, corner.hand, corner.arc_length));
track->elements.push_back(maj0sted::editor::make_line(m_document, reach - 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));
solve();
m_status.clear();
}
@@ -659,7 +660,7 @@ void plan_panel::append_element(Kind const Kind_)
}
Element element;
element.id = maj0sted::editor::mint_element(m_document);
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;
@@ -739,33 +740,33 @@ 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{maj0sted::editor::find_track(m_solution, m_track)};
auto const *solved{editor::plan::find_track(m_solution, m_track)};
if (track == nullptr || solved == nullptr)
{
Why = "tor nie jest polozony";
return false;
}
maj0sted::editor::Skeleton skeleton;
if (false == maj0sted::editor::skeleton_of(*track, *solved, skeleton, Why))
editor::plan::Skeleton skeleton;
if (false == editor::plan::skeleton_of(*track, *solved, skeleton, Why))
{
return false;
}
return maj0sted::editor::lay_along_skeleton(*track, skeleton, Why);
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{maj0sted::editor::find_track(m_solution, m_track)};
auto const *solved{editor::plan::find_track(m_solution, m_track)};
if (track == nullptr || solved == nullptr || m_drag_index >= track->elements.size())
{
return;
}
maj0sted::editor::Skeleton skeleton;
editor::plan::Skeleton skeleton;
std::string why;
if (false == maj0sted::editor::skeleton_of(*track, *solved, skeleton, why))
if (false == editor::plan::skeleton_of(*track, *solved, skeleton, why))
{
m_status = why;
return;
@@ -818,18 +819,18 @@ void plan_panel::drag_straight_to(double const X, double const Y)
// nothing is written until the whole chain works out, so a bad drag leaves the track alone
Track candidate{*track};
if (false == maj0sted::editor::lay_along_skeleton(candidate, skeleton, why))
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{[](maj0sted::editor::Solution const &Solution_) {
auto const off_straight{[](editor::plan::Solution const &Solution_) {
std::size_t count{0};
for (auto const &diagnostic : Solution_.diagnostics)
{
count += diagnostic.code == maj0sted::editor::Code::StationOffTrack ? 1 : 0;
count += diagnostic.code == editor::plan::Code::StationOffTrack ? 1 : 0;
}
return count;
}};
@@ -842,7 +843,7 @@ void plan_panel::drag_straight_to(double const X, double const Y)
{
for (auto const &diagnostic : m_solution.diagnostics)
{
if (diagnostic.code == maj0sted::editor::Code::StationOffTrack)
if (diagnostic.code == editor::plan::Code::StationOffTrack)
{
m_status = "dalej nie: " + diagnostic.text;
break;
@@ -870,8 +871,8 @@ void plan_panel::place_turnout_at(TrackId const On, double const Wx, double cons
}
auto const type{static_cast<std::size_t>(std::clamp(m_turnout_type, 0, static_cast<int>(m_document.turnout_types.size()) - 1))};
maj0sted::editor::TurnoutPlacement placement;
placement.id = maj0sted::editor::mint_turnout(m_document);
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;
@@ -880,9 +881,9 @@ void plan_panel::place_turnout_at(TrackId const On, double const Wx, double cons
// a switch is worth nothing without somewhere for the branch to go, so one starts at its frog
Track branch;
branch.id = maj0sted::editor::mint_track(m_document);
branch.id = editor::plan::mint_track(m_document);
branch.name = "odnoga " + std::to_string(m_document.tracks.size() + 1);
branch.anchor = maj0sted::editor::AtPort{placement.id, maj0sted::editor::Port::Frog};
branch.anchor = editor::plan::AtPort{placement.id, editor::plan::Port::Frog};
m_document.tracks.push_back(branch);
m_track = branch.id;
@@ -899,7 +900,7 @@ void plan_panel::place_turnout_at(TrackId const On, double const Wx, double cons
bool plan_panel::hit_straight_end(ImVec2 const &Mouse, std::size_t &OutIndex, int &OutEnd) const
{
auto const *solved{maj0sted::editor::find_track(m_solution, m_track)};
auto const *solved{editor::plan::find_track(m_solution, m_track)};
auto const *track{current_track()};
if (solved == nullptr || track == nullptr)
{
@@ -916,7 +917,7 @@ bool plan_panel::hit_straight_end(ImVec2 const &Mouse, std::size_t &OutIndex, in
continue;
}
// a branch starts on its turnout's frog and cannot be pulled off it
auto const pinned{i == 0 && false == std::holds_alternative<maj0sted::editor::AtPose>(track->anchor)};
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)
@@ -1007,7 +1008,7 @@ bool plan_panel::hit_turnout(ImVec2 const &Mouse, TurnoutId &OutTurnout) const
bool plan_panel::station_on(TrackId const Track_, double const Wx, double const Wy, double &OutStation) const
{
auto const *track{maj0sted::editor::find_track(m_solution, Track_)};
auto const *track{editor::plan::find_track(m_solution, Track_)};
if (track == nullptr || track->centreline.size() < 2)
{
return false;
@@ -1171,7 +1172,7 @@ void plan_panel::handle_scene()
int end{0};
if (hit_straight_end(mouse, index, end))
{
auto const *solved{maj0sted::editor::find_track(m_solution, m_track)};
auto const *solved{editor::plan::find_track(m_solution, m_track)};
auto const *track{current_track()};
if (solved != nullptr && index < solved->elements.size())
{
@@ -1183,7 +1184,7 @@ void plan_panel::handle_scene()
m_drag_end = end;
m_dragging_straight = true;
m_sel_element = track->elements[index].id;
if (track != nullptr && std::holds_alternative<maj0sted::editor::Parallel>(track->elements[index].hold))
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";
}
@@ -1223,7 +1224,7 @@ void plan_panel::handle_scene()
{
m_sel_element = element;
m_track = track;
auto const *chosen{maj0sted::editor::find_track(m_document, 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());
@@ -1243,7 +1244,7 @@ void plan_panel::handle_scene()
if (ImGui::IsMouseDown(0) && m_dragging_turnout && m_sel_turnout != TurnoutId::none)
{
auto *placement{maj0sted::editor::find_turnout(m_document, m_sel_turnout)};
auto *placement{editor::plan::find_turnout(m_document, m_sel_turnout)};
if (placement != nullptr)
{
double station{0.0};
@@ -1267,7 +1268,7 @@ void plan_panel::handle_scene()
if (track != TrackId::none)
{
m_track = track;
auto const *chosen{maj0sted::editor::find_track(m_document, 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());
@@ -1295,9 +1296,9 @@ void plan_panel::draw_on_scene()
auto const display{ImGui::GetIO().DisplaySize};
auto const halfheight{static_cast<double>(Global.editor_ortho_extent)};
auto const halfwidth{halfheight * std::max(1.0f, display.x) / std::max(1.0f, display.y)};
maj0sted::editor::TileBBox const view{planx - halfwidth, plany - halfheight, planx + halfwidth, plany + halfheight};
editor::plan::TileBBox const view{planx - halfwidth, plany - halfheight, planx + halfwidth, plany + halfheight};
auto const draw_box = [&](maj0sted::editor::TileBBox const &Box, unsigned int const Texture) {
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]);
@@ -1399,7 +1400,7 @@ void plan_panel::draw_on_scene()
// 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{[&](maj0sted::editor::TurnoutMark const &Mark_, ImU32 const Colour_, float const Half_) {
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))
{
@@ -1536,7 +1537,7 @@ void plan_panel::draw_on_scene()
// the ends of the edited track's straights, while ctrl says they are what you are after
if (ImGui::GetIO().KeyCtrl)
{
auto const *solved{maj0sted::editor::find_track(m_solution, m_track)};
auto const *solved{editor::plan::find_track(m_solution, m_track)};
auto const *track{current_track()};
if (solved != nullptr && track != nullptr)
{
@@ -1546,7 +1547,7 @@ void plan_panel::draw_on_scene()
{
continue;
}
auto const pinned{i == 0 && false == std::holds_alternative<maj0sted::editor::AtPose>(track->anchor)};
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)
@@ -1643,25 +1644,25 @@ void plan_panel::draw_on_scene()
// 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{maj0sted::editor::find_track(m_solution, m_join_first)};
auto const *tail{maj0sted::editor::find_track(m_solution, hovered)};
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)};
maj0sted::editor::JoinSettings settings;
editor::plan::JoinSettings settings;
settings.radius = m_join_radius;
auto const plan{maj0sted::editor::plan_join(leaving, arriving, settings)};
auto const plan{editor::plan::plan_join(leaving, arriving, settings)};
std::string label;
if (plan.ok)
{
std::vector<maj0sted::domain::geometry::XY> points;
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 = maj0sted::domain::geometry::layout_segment(k, k, element.length, walker, &points);
walker = editor::plan::geometry::layout_segment(k, k, element.length, walker, &points);
}
std::vector<ImVec2> line;
line.reserve(points.size());
@@ -1677,7 +1678,7 @@ void plan_panel::draw_on_scene()
{
dashed_polyline(drawlist, line, IM_COL32(120, 255, 160, 235), 2.5f);
}
label = maj0sted::editor::join_kind_name(plan.kind);
label = editor::plan::join_kind_name(plan.kind);
auto laid{0.0};
for (auto const &element : plan.elements)
{
@@ -1872,10 +1873,10 @@ void plan_panel::render_join()
void plan_panel::join_ends(TrackId const A, int const Aend, TrackId const B, int const Bend)
{
maj0sted::editor::JoinSettings settings;
editor::plan::JoinSettings settings;
settings.radius = m_join_radius;
auto const report{maj0sted::editor::join_ends(m_document, m_solution, A, Aend, B, Bend, settings)};
auto const report{editor::plan::join_ends(m_document, m_solution, A, Aend, B, Bend, settings)};
if (false == report.ok)
{
m_status = report.why;
@@ -1884,16 +1885,16 @@ void plan_panel::join_ends(TrackId const A, int const Aend, TrackId const B, int
// 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{maj0sted::editor::find_track(m_document, A) != nullptr ? A : B};
auto const survivor{editor::plan::find_track(m_document, A) != nullptr ? A : B};
char status[256];
std::snprintf(status, sizeof(status), "polaczone: %s, %d odcinkow zlanych w jeden, %d rozjazdow przeniesionych%s", maj0sted::editor::join_kind_name(report.join.kind), report.fused,
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{maj0sted::editor::find_track(m_document, survivor)};
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());
@@ -1902,7 +1903,7 @@ void plan_panel::join_ends(TrackId const A, int const Aend, TrackId const B, int
solve();
}
maj0sted::domain::geometry::Pose plan_panel::join_pose(maj0sted::editor::SolvedTrack const &Track_, int const End_, bool const Leaving_)
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
@@ -1916,7 +1917,7 @@ maj0sted::domain::geometry::Pose plan_panel::join_pose(maj0sted::editor::SolvedT
return pose;
}
bool plan_panel::loose_end(maj0sted::editor::SolvedTrack const &Track_, int const End_) const
bool plan_panel::loose_end(editor::plan::SolvedTrack const &Track_, int const End_) const
{
if (Track_.elements.empty())
{
@@ -1925,8 +1926,8 @@ bool plan_panel::loose_end(maj0sted::editor::SolvedTrack const &Track_, int cons
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{maj0sted::editor::find_track(m_document, Track_.id)};
if (authored == nullptr || false == std::holds_alternative<maj0sted::editor::AtPose>(authored->anchor))
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;
}
@@ -2050,7 +2051,7 @@ void plan_panel::render_elements()
// 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{maj0sted::editor::find_track(m_solution, track->id)};
auto const *solved{editor::plan::find_track(m_solution, track->id)};
for (std::size_t i = 0; i < track->elements.size(); ++i)
{
@@ -2111,7 +2112,7 @@ void plan_panel::render_elements()
ImGui::SetNextItemWidth(120.0f);
if (ImGui::InputDouble(element.kind == Kind::Clothoid ? "R na koncu [m]" : "promien [m]", &element.radius, 10.0, 100.0, "%.2f"))
{
maj0sted::editor::sync_joint(*track, i);
editor::plan::sync_joint(*track, i);
dirty = true;
refit = true;
}
@@ -2124,7 +2125,7 @@ void plan_panel::render_elements()
if (ImGui::Combo("skret", &hand, "w lewo\0w prawo\0"))
{
element.hand = hand == 0 ? 1 : -1;
maj0sted::editor::sync_joint(*track, i);
editor::plan::sync_joint(*track, i);
dirty = true;
refit = true;
}
@@ -2191,7 +2192,7 @@ void plan_panel::render_elements()
// 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(maj0sted::editor::TurnoutType &Type_)
void plan_panel::render_turnout_type(editor::plan::TurnoutType &Type_)
{
auto dirty{false};
@@ -2290,7 +2291,7 @@ void plan_panel::render_turnout_type(maj0sted::editor::TurnoutType &Type_)
}
if (ImGui::SmallButton("dodaj odcinek"))
{
Type_.pieces.push_back(maj0sted::editor::TurnoutPieceSpec{2, 1.0, 0.0, 0.0, 0.0});
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;
}
@@ -2319,7 +2320,7 @@ void plan_panel::render_turnout_type(maj0sted::editor::TurnoutType &Type_)
dirty = true;
}
auto const laid{maj0sted::domain::lay_turnout(maj0sted::domain::geometry::Pose{0.0, 0.0, 1.0, 0.0}, maj0sted::editor::to_domain(Type_, 1))};
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");
@@ -2370,9 +2371,9 @@ void plan_panel::render_template_window()
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{maj0sted::domain::geometry::Pose{0.0, 0.0, 1.0, 0.0}};
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{maj0sted::domain::lay_turnout(start, maj0sted::editor::to_domain(type, 1), bend)};
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",
@@ -2469,11 +2470,11 @@ void plan_panel::render_template_window()
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{maj0sted::domain::kHalfGauge};
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 maj0sted::domain::geometry::layout_segment(bend, bend, Station_, start, nullptr); }};
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;
@@ -2505,7 +2506,7 @@ void plan_panel::render_template_window()
// 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<maj0sted::domain::geometry::XY> const &Points_, int const Side_) {
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)
@@ -2526,8 +2527,8 @@ void plan_panel::render_template_window()
// 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<maj0sted::domain::geometry::XY> const &Points_, double const Distance_,
std::vector<maj0sted::domain::geometry::XY> &Head_, std::vector<maj0sted::domain::geometry::XY> &Tail_) {
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)
{
@@ -2542,7 +2543,7 @@ void plan_panel::render_template_window()
{
auto const &back{Points_[i - 1]};
auto const share{(Distance_ - (travelled - step)) / step};
maj0sted::domain::geometry::XY const meeting{back.x + (Points_[i].x - back.x) * share, back.y + (Points_[i].y - back.y) * share};
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);
}
@@ -2550,7 +2551,7 @@ void plan_panel::render_template_window()
}
}};
auto const draw_run{[&](std::vector<maj0sted::domain::geometry::XY> const &Points_, ImU32 const Colour_, bool const Theoretical_) {
auto const draw_run{[&](std::vector<editor::plan::geometry::XY> const &Points_, ImU32 const Colour_, bool const Theoretical_) {
if (Points_.size() < 2)
{
return;
@@ -2603,11 +2604,11 @@ void plan_panel::render_template_window()
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<maj0sted::domain::geometry::XY> points;
walker = maj0sted::domain::geometry::layout_segment(segment.k0, segment.k1, segment.length, walker, &points);
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 == maj0sted::domain::TurnoutPart::Blade)
if (segment.part == editor::plan::TurnoutPart::Blade)
{
heel = station;
}
@@ -2625,8 +2626,8 @@ void plan_panel::render_template_window()
draw_run(points, colour, false);
continue;
}
std::vector<maj0sted::domain::geometry::XY> ahead;
std::vector<maj0sted::domain::geometry::XY> behind;
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);
@@ -2680,7 +2681,7 @@ void plan_panel::render_template_window()
// the construction points, from the very list the solver reports for a placement
if (m_tpl_marks)
{
auto marks{maj0sted::editor::turnout_marks(laid, start)};
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};
@@ -2820,8 +2821,8 @@ void plan_panel::render_turnouts()
auto &placement{m_document.turnouts[i]};
ImGui::PushID(static_cast<int>(1000 + i));
auto const *solved{maj0sted::editor::find_turnout(m_solution, placement.id)};
auto const *on{maj0sted::editor::find_track(m_document, placement.on)};
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)");
@@ -2891,11 +2892,11 @@ void plan_panel::render_turnouts()
// mean guessing kilometres by hand: the nearest element long enough to hold it, whole
if (solved != nullptr && solved->valid)
{
auto const *through{maj0sted::editor::find_track(m_solution, placement.on)};
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 && maj0sted::editor::steady_room(*through, placement.station, curvature, behind, ahead)
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)
@@ -2975,7 +2976,7 @@ void plan_panel::render_diagnostics()
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{maj0sted::editor::find_track(m_document, diagnostic.track)};
auto const *track{editor::plan::find_track(m_document, diagnostic.track)};
if (ImGui::Selectable(diagnostic.text.c_str()))
{
if (diagnostic.track != TrackId::none)
@@ -3019,12 +3020,12 @@ void plan_panel::render_storage()
m_document.georeferenced = Global.scenery_georeferenced;
m_document.origin_x = Global.scenery_origin.x;
m_document.origin_y = Global.scenery_origin.y;
m_status = maj0sted::io::save(m_document, m_path) ? "saved to " + std::string(m_path) : "could not write " + std::string(m_path);
m_status = editor::plan::io::save(m_document, m_path) ? "saved to " + std::string(m_path) : "could not write " + std::string(m_path);
}
ImGui::SameLine();
if (ImGui::Button("Load"))
{
auto const loaded{maj0sted::io::load(m_path)};
auto const loaded{editor::plan::io::load(m_path)};
if (loaded)
{
m_document = *loaded;
@@ -3110,7 +3111,7 @@ void plan_panel::export_scn(bool const Run_)
// 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;
maj0sted::io::ScnExportOptions opt;
editor::plan::io::ScnExportOptions opt;
opt.origin_east = Global.scenery_origin.x;
opt.origin_north = Global.scenery_origin.y;
opt.trainset = m_scn_trainset;
@@ -3120,7 +3121,7 @@ void plan_panel::export_scn(bool const Run_)
m_status = "could not write " + std::string(m_scn_path);
return;
}
auto const result{maj0sted::io::export_scn(m_document, m_solution, opt, out)};
auto const result{editor::plan::io::export_scn(m_document, m_solution, opt, out)};
out.flush();
if (false == out.good())
{

View File

@@ -8,16 +8,17 @@ http://mozilla.org/MPL/2.0/.
*/
module;
#include <string>
#include <variant>
#include <vector>
#include "imgui/imgui.h"
#include "maj0sted/editor/tile_cache.hpp"
#include "maj0sted/editor/layout.hpp"
#include "maj0sted/editor/model.hpp"
#include "maj0sted/editor/render.hpp"
#include "maj0sted/editor/solution.hpp"
#include "maj0sted/io/scn_export.hpp"
export module eu07.application.planpanel;
import eu07.editor.plan_tilecache;
import eu07.editor.plan_layout;
import eu07.editor.plan_model;
import eu07.editor.plan_render;
import eu07.editor.plan_solution;
import eu07.editor.plan_scn_export;
import eu07.application.uilayer;
import eu07.editor.orthophoto;
@@ -25,7 +26,7 @@ export {
// track layout tool. the drawing happens in the viewport, which the tool puts into a top-down
// orthographic plan view; the panel holds only the controls. none of the geometry is computed here:
// maj0sted solves the whole document and the panel paints what comes back.
// the plan library solves the whole document and the panel paints what comes back.
//
// a track is a chain of elements, and every one of them says outright what it is: prosta of so many
// metres, luk of radius R turning so far, krzywa przejsciowa easing to R over so many metres. there
@@ -45,13 +46,13 @@ class plan_panel : public ui_panel
void render_contents() override;
private:
using Document = maj0sted::editor::Document;
using Element = maj0sted::editor::Element;
using ElementId = maj0sted::editor::ElementId;
using Solution = maj0sted::editor::Solution;
using Track = maj0sted::editor::Track;
using TrackId = maj0sted::editor::TrackId;
using TurnoutId = maj0sted::editor::TurnoutId;
using Document = editor::plan::Document;
using Element = editor::plan::Element;
using ElementId = editor::plan::ElementId;
using Solution = editor::plan::Solution;
using Track = editor::plan::Track;
using TrackId = editor::plan::TrackId;
using TurnoutId = editor::plan::TurnoutId;
// panels
void render_toolbar();
@@ -60,7 +61,7 @@ class plan_panel : public ui_panel
// joining two tracks into one: the radius a join may bend at, and the button that starts
// pointing at the ends to join. everything else about it happens on the map
void render_join();
void render_turnout_type(maj0sted::editor::TurnoutType &Type_);
void render_turnout_type(editor::plan::TurnoutType &Type_);
// the catalogue drawing of the chosen type, in its own window: the diverging path piece by
// piece, both tracks' rails, and the points a drawing is dimensioned from
void render_template_window();
@@ -94,7 +95,7 @@ class plan_panel : public ui_panel
// taken off the straight before it - which leaves the track ending on the click before
void drop_last_vertex();
// appends one element to the end of the current track, exactly as asked for
void append_element(maj0sted::editor::Kind const Kind);
void append_element(editor::plan::Kind const Kind);
void drop_last_element();
void delete_selected_element();
void place_turnout_at(TrackId const On, double const Wx, double const Wy);
@@ -117,10 +118,10 @@ class plan_panel : public ui_panel
bool hit_track_end(ImVec2 const &Mouse, float const Tolerance, TrackId &OutTrack, int &OutEnd) const;
// whether that end is loose at all: one that already stands on another track's end is joined
// to it, and there is nothing to join twice
bool loose_end(maj0sted::editor::SolvedTrack const &Track_, int const End_) const;
bool loose_end(editor::plan::SolvedTrack const &Track_, int const End_) const;
// how an end reads for a join laid out of it or into it: one facing the wrong way for that
// is taken backwards, which is what turning the track round would do to it anyway
static maj0sted::domain::geometry::Pose join_pose(maj0sted::editor::SolvedTrack const &Track_, int const End_, bool const Leaving_);
static editor::plan::geometry::Pose join_pose(editor::plan::SolvedTrack const &Track_, int const End_, bool const Leaving_);
// lays the join and leaves one track, saying what it cost
void join_ends(TrackId const A, int const Aend, TrackId const B, int const Bend);
// nearest station along a solved track's axis, for placing and sliding turnouts
@@ -130,7 +131,7 @@ class plan_panel : public ui_panel
// members
Document m_document;
Solution m_solution;
std::vector<maj0sted::editor::PlanPolyline> m_rails;
std::vector<editor::plan::PlanPolyline> m_rails;
TrackId m_track{};
ElementId m_sel_element{};
@@ -210,10 +211,10 @@ class plan_panel : public ui_panel
double m_mapviewy{0.0};
double m_mapscale{0.0};
editor::wms_image m_topomap{maj0sted::editor::WmsConfig::geoportal_topo()};
editor::wms_image m_topomap{editor::plan::WmsConfig::geoportal_topo()};
editor::orthophoto_source m_ortho;
// 1 km topo cells for views wider than a kilometre (orto's 100 m grid is too dense there)
editor::orthophoto_source m_topo{maj0sted::editor::WmsConfig::geoportal_topo(), 1000, "topo"};
editor::orthophoto_source m_topo{editor::plan::WmsConfig::geoportal_topo(), 1000, "topo"};
bool m_showortho{true};
};

View File

@@ -14,7 +14,6 @@ module;
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "maj0sted/editor/tile_cache.hpp"
#include <glad/glad.h>
#include "stb/stb_image.h"
#ifdef _WIN32
@@ -26,6 +25,7 @@ module;
#endif
module eu07.editor.orthophoto;
import eu07.editor.plan_tilecache;
import eu07.utilities.logs;
/*
@@ -209,9 +209,9 @@ bool http_get(std::string const &Url, std::vector<std::uint8_t> &Bytes, std::str
} // namespace
orthophoto_source::orthophoto_source(maj0sted::editor::WmsConfig Config, int const Gridzoom, std::string Cachesubdir)
orthophoto_source::orthophoto_source(editor::plan::WmsConfig Config, int const Gridzoom, std::string Cachesubdir)
: m_service([&] {
if (Gridzoom >= maj0sted::editor::TileGrid::kCellZoomMin)
if (Gridzoom >= editor::plan::TileGrid::kCellZoomMin)
{
Config.tile_pixels = 512; // coarse cells do not need orto's 1024 px
}
@@ -221,7 +221,7 @@ orthophoto_source::orthophoto_source(maj0sted::editor::WmsConfig Config, int con
{
// the library asks for a URL and expects the answer whenever it arrives; the transfer is queued
// here and handed back on the thread that drains it, so the service stays single threaded
m_service.set_fetcher([this](std::string const &Url, maj0sted::editor::TileService::DoneFn Done) {
m_service.set_fetcher([this](std::string const &Url, editor::plan::TileService::DoneFn Done) {
{
std::lock_guard<std::mutex> lock(m_mutex);
m_queued.push_back({Url, std::move(Done), false, {}, {}});
@@ -291,7 +291,7 @@ void orthophoto_source::worker()
}
}
std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::editor::TileBBox const &View, int const Maxtiles)
std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(editor::plan::TileBBox const &View, int const Maxtiles)
{
// hand finished transfers to the library here, on the caller's thread
std::deque<transfer> arrived;
@@ -311,7 +311,7 @@ std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::
// fetch only what fits the budget for the current view. the window is recentred when the
// view is larger than the cap, and zoom-to-cursor moves the camera, so the set of keys asked
// for changes from frame to frame - that must not decide what is drawn
for (auto const &key : maj0sted::editor::TileGrid::tiles_for_view(View, m_gridzoom, Maxtiles))
for (auto const &key : editor::plan::TileGrid::tiles_for_view(View, m_gridzoom, Maxtiles))
{
// a tile kept from an earlier session seeds the cache, so no request goes out for it at all
if (false == m_service.has(key))
@@ -320,13 +320,13 @@ std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::
}
auto const status = m_service.request(key);
if (status == maj0sted::editor::TileStatus::Failed)
if (status == editor::plan::TileStatus::Failed)
{
// counted once per tile, not once per frame it stays unavailable
m_failedkeys.insert(key);
continue;
}
if (status != maj0sted::editor::TileStatus::Ready)
if (status != editor::plan::TileStatus::Ready)
{
continue;
}
@@ -342,7 +342,7 @@ std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::
// draw every texture we already hold that still covers the view. returning only the fetch
// window made tiles blink out during zoom as soon as they fell outside the cap, then blink
// back in once the window moved over them again
auto const overlaps = [](maj0sted::editor::TileBBox const &Left, maj0sted::editor::TileBBox const &Right) {
auto const overlaps = [](editor::plan::TileBBox const &Left, editor::plan::TileBBox const &Right) {
return Left.min_x < Right.max_x && Left.max_x > Right.min_x && Left.min_y < Right.max_y && Left.max_y > Right.min_y;
};
@@ -354,7 +354,7 @@ std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::
{
continue;
}
auto const box = maj0sted::editor::TileGrid::bbox(key);
auto const box = editor::plan::TileGrid::bbox(key);
if (overlaps(box, View))
{
tiles.push_back({box, texture});
@@ -364,7 +364,7 @@ std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::
return tiles;
}
unsigned int orthophoto_source::upload(maj0sted::editor::TileKey const &Key)
unsigned int orthophoto_source::upload(editor::plan::TileKey const &Key)
{
auto const *image = m_service.peek(Key);
if (image == nullptr || image->bytes.empty())
@@ -397,12 +397,12 @@ unsigned int orthophoto_source::upload(maj0sted::editor::TileKey const &Key)
return texture;
}
std::string orthophoto_source::tile_path(maj0sted::editor::TileKey const &Key) const
std::string orthophoto_source::tile_path(editor::plan::TileKey const &Key) const
{
return "editor/cache/" + m_cachesubdir + "/" + std::to_string(Key.zoom) + "_" + std::to_string(Key.x) + "_" + std::to_string(Key.y) + ".img";
}
bool orthophoto_source::load_from_disk(maj0sted::editor::TileKey const &Key)
bool orthophoto_source::load_from_disk(editor::plan::TileKey const &Key)
{
std::ifstream file(tile_path(Key), std::ios::binary);
if (false == file.is_open())
@@ -421,7 +421,7 @@ bool orthophoto_source::load_from_disk(maj0sted::editor::TileKey const &Key)
return true;
}
void orthophoto_source::save_to_disk(maj0sted::editor::TileKey const &Key)
void orthophoto_source::save_to_disk(editor::plan::TileKey const &Key)
{
auto const *image = m_service.peek(Key);
if (image == nullptr || image->bytes.empty())
@@ -484,7 +484,7 @@ unsigned int upload_image(std::vector<std::uint8_t> const &Bytes)
return texture;
}
bool same_box(maj0sted::editor::TileBBox const &Left, maj0sted::editor::TileBBox const &Right)
bool same_box(editor::plan::TileBBox const &Left, editor::plan::TileBBox const &Right)
{
auto const close = [](double const A, double const B) { return std::abs(A - B) < 1.0; };
return close(Left.min_x, Right.min_x) && close(Left.min_y, Right.min_y) && close(Left.max_x, Right.max_x) && close(Left.max_y, Right.max_y);
@@ -492,7 +492,7 @@ bool same_box(maj0sted::editor::TileBBox const &Left, maj0sted::editor::TileBBox
} // namespace
wms_image::wms_image(maj0sted::editor::WmsConfig Config) : m_config(std::move(Config))
wms_image::wms_image(editor::plan::WmsConfig Config) : m_config(std::move(Config))
{
m_worker = std::thread([this]() { worker(); });
}
@@ -547,7 +547,7 @@ void wms_image::worker()
}
}
unsigned int wms_image::texture_for(maj0sted::editor::TileBBox const &Box, int const Pixels)
unsigned int wms_image::texture_for(editor::plan::TileBBox const &Box, int const Pixels)
{
// a finished download becomes the texture on this thread, where the graphics API is safe to touch
{
@@ -583,7 +583,7 @@ unsigned int wms_image::texture_for(maj0sted::editor::TileBBox const &Box, int c
config.tile_pixels = Pixels;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_url = maj0sted::editor::wms_getmap_url(config, Box);
m_url = editor::plan::wms_getmap_url(config, Box);
m_haswork = true;
}
m_loading = true;

View File

@@ -17,9 +17,9 @@ module;
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "maj0sted/editor/tile_cache.hpp"
export module eu07.editor.orthophoto;
import eu07.editor.plan_tilecache;
export {
@@ -39,13 +39,13 @@ class orthophoto_source
public:
struct ready_tile
{
maj0sted::editor::TileBBox box;
editor::plan::TileBBox box;
unsigned int texture{0};
};
// default: Geoportal ortophoto on the fine 100 m grid. pass topo config and a coarse
// grid zoom (e.g. 1000) for the zoomed-out topographic underlay
explicit orthophoto_source(maj0sted::editor::WmsConfig Config = maj0sted::editor::WmsConfig::geoportal_ortho(), int const Gridzoom = maj0sted::editor::TileGrid::kLevel,
explicit orthophoto_source(editor::plan::WmsConfig Config = editor::plan::WmsConfig::geoportal_ortho(), int const Gridzoom = editor::plan::TileGrid::kLevel,
std::string Cachesubdir = "orto");
~orthophoto_source();
orthophoto_source(orthophoto_source const &) = delete;
@@ -54,7 +54,7 @@ class orthophoto_source
// kicks off fetches for the view (capped) and returns every already-uploaded tile that still
// intersects it - including ones outside the fetch window, so a zoom cannot blink them away.
// tiles still on their way simply aren't in the answer yet; call again next frame
std::vector<ready_tile> collect(maj0sted::editor::TileBBox const &View, int const Maxtiles = 400);
std::vector<ready_tile> collect(editor::plan::TileBBox const &View, int const Maxtiles = 400);
// how many tiles are still being fetched, for the panel to report
std::size_t pending() const;
// how many tiles gave up after exhausting their retries
@@ -65,7 +65,7 @@ class orthophoto_source
struct transfer
{
std::string url;
maj0sted::editor::TileService::DoneFn done;
editor::plan::TileService::DoneFn done;
bool ok{false};
std::vector<std::uint8_t> bytes;
std::string content_type;
@@ -73,17 +73,17 @@ class orthophoto_source
// methods
void worker();
// decodes the cached bytes of a tile and uploads them; returns 0 when the image is unusable
unsigned int upload(maj0sted::editor::TileKey const &Key);
unsigned int upload(editor::plan::TileKey const &Key);
// imagery of a given piece of ground never changes, so a fetched tile is kept on disk and the
// next session starts with it instead of going back to a service that is slow and flaky
std::string tile_path(maj0sted::editor::TileKey const &Key) const;
bool load_from_disk(maj0sted::editor::TileKey const &Key);
void save_to_disk(maj0sted::editor::TileKey const &Key);
std::string tile_path(editor::plan::TileKey const &Key) const;
bool load_from_disk(editor::plan::TileKey const &Key);
void save_to_disk(editor::plan::TileKey const &Key);
// members
maj0sted::editor::TileService m_service;
int m_gridzoom{maj0sted::editor::TileGrid::kLevel};
editor::plan::TileService m_service;
int m_gridzoom{editor::plan::TileGrid::kLevel};
std::string m_cachesubdir{"orto"};
std::unordered_map<maj0sted::editor::TileKey, unsigned int, maj0sted::editor::TileKeyHash> m_textures;
std::unordered_map<editor::plan::TileKey, unsigned int, editor::plan::TileKeyHash> m_textures;
mutable std::mutex m_mutex;
std::condition_variable m_wakeup;
@@ -92,7 +92,7 @@ class orthophoto_source
std::vector<std::thread> m_workers;
std::atomic<bool> m_quitting{false};
std::atomic<std::size_t> m_inflight{0};
std::unordered_set<maj0sted::editor::TileKey, maj0sted::editor::TileKeyHash> m_failedkeys;
std::unordered_set<editor::plan::TileKey, editor::plan::TileKeyHash> m_failedkeys;
};
// a single WMS image covering an arbitrary box, for views too coarse for the tile grid - the whole
@@ -102,25 +102,25 @@ class wms_image
{
public:
explicit wms_image(maj0sted::editor::WmsConfig Config);
explicit wms_image(editor::plan::WmsConfig Config);
~wms_image();
wms_image(wms_image const &) = delete;
wms_image &operator=(wms_image const &) = delete;
// asks for the given box at the given size and returns the texture currently available, which
// may still be the previous box's until the new one arrives; 0 when there is nothing to show yet
unsigned int texture_for(maj0sted::editor::TileBBox const &Box, int const Pixels);
unsigned int texture_for(editor::plan::TileBBox const &Box, int const Pixels);
// the box the returned texture actually covers
maj0sted::editor::TileBBox const &covered() const { return m_covered; }
editor::plan::TileBBox const &covered() const { return m_covered; }
bool loading() const { return m_loading.load(); }
private:
void worker();
maj0sted::editor::WmsConfig m_config;
maj0sted::editor::TileBBox m_requested{};
maj0sted::editor::TileBBox m_inflightbox{};
maj0sted::editor::TileBBox m_covered{};
editor::plan::WmsConfig m_config;
editor::plan::TileBBox m_requested{};
editor::plan::TileBBox m_inflightbox{};
editor::plan::TileBBox m_covered{};
unsigned int m_texture{0};
mutable std::mutex m_mutex;

View File

@@ -1,16 +1,25 @@
#include "maj0sted/editor/chain.hpp"
/*
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/.
*/
module;
#include <cmath>
#include <cstddef>
#include <string>
#include <vector>
namespace maj0sted::editor {
module eu07.editor.plan_chain;
namespace editor::plan {
namespace {
constexpr double kEps = 1e-9;
using domain::geometry::Pose;
using geometry::Pose;
Diagnostic make(Code code, TrackId track, ElementId element, std::string text) {
Diagnostic diagnostic;
@@ -113,8 +122,8 @@ TrackOutcome lay_track(const Document& /*document*/, const Track& track, const P
solved.k1 = k1;
solved.length = element.length;
std::vector<domain::geometry::XY> points;
walker = domain::geometry::layout_segment(k0, k1, element.length, walker, &points);
std::vector<geometry::XY> points;
walker = geometry::layout_segment(k0, k1, element.length, walker, &points);
solved.end = walker;
solved.points.reserve(points.size());
for (const auto& point : points) {
@@ -138,4 +147,4 @@ TrackOutcome lay_track(const Document& /*document*/, const Track& track, const P
return out;
}
} // namespace maj0sted::editor
} // namespace editor::plan

View File

@@ -1,12 +1,22 @@
#pragma once
/*
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/.
*/
module;
#include <vector>
#include "maj0sted/domain/geometry/segment_layout.hpp"
#include "maj0sted/editor/model.hpp"
#include "maj0sted/editor/solution.hpp"
export module eu07.editor.plan_chain;
export import eu07.editor.plan_segment;
export import eu07.editor.plan_model;
export import eu07.editor.plan_solution;
namespace maj0sted::editor {
export {
namespace editor::plan {
/// What laying one track produced, and everything that went wrong doing it.
struct TrackOutcome {
@@ -26,6 +36,8 @@ struct TrackOutcome {
/// ends the chain there; everything before it is still returned and still
/// drawable.
[[nodiscard]] TrackOutcome lay_track(const Document& document, const Track& track,
const domain::geometry::Pose& start);
const geometry::Pose& start);
} // namespace maj0sted::editor
} // namespace editor::plan
} // export

View File

@@ -1,5 +1,15 @@
#include "maj0sted/io/document_io.hpp"
/*
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/.
*/
module;
#include <optional>
#include <variant>
#include <string>
#include <charconv>
#include <cmath>
#include <cstdint>
@@ -9,10 +19,12 @@
#include <system_error>
#include <vector>
namespace maj0sted::io {
module eu07.editor.plan_document_io;
namespace editor::plan::io {
namespace {
using namespace maj0sted::editor;
using namespace editor::plan;
constexpr int kVersion = 7;
constexpr int kNoExportPath = 6; // before a plan remembered where it exports to
@@ -553,4 +565,4 @@ std::optional<Document> load(const std::string& path) {
return deserialize(buffer.str());
}
} // namespace maj0sted::io
} // namespace editor::plan::io

View File

@@ -1,11 +1,21 @@
#pragma once
/*
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/.
*/
module;
#include <optional>
#include <string>
#include "maj0sted/editor/model.hpp"
export module eu07.editor.plan_document_io;
export import eu07.editor.plan_model;
namespace maj0sted::io {
export {
namespace editor::plan::io {
/// The `.m0s` format, version 3.
///
@@ -18,14 +28,16 @@ namespace maj0sted::io {
///
/// Doubles go through std::to_chars/from_chars, so a round trip is exact and
/// does not depend on the locale.
[[nodiscard]] std::string serialize(const editor::Document& document);
[[nodiscard]] std::string serialize(const plan::Document& document);
/// Returns nothing when @p text is not a version 3 document or is malformed.
[[nodiscard]] std::optional<editor::Document> deserialize(const std::string& text);
[[nodiscard]] std::optional<plan::Document> deserialize(const std::string& text);
[[nodiscard]] std::string default_project_path();
bool save(const editor::Document& document, const std::string& path = default_project_path());
[[nodiscard]] std::optional<editor::Document> load(
bool save(const plan::Document& document, const std::string& path = default_project_path());
[[nodiscard]] std::optional<plan::Document> load(
const std::string& path = default_project_path());
} // namespace maj0sted::io
} // namespace editor::plan::io
} // export

View File

@@ -1,8 +1,21 @@
/*
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/.
*/
module;
#include <string>
#include <vector>
module eu07.editor.plan_join;
// TYMCZASOWA ZASLEPKA - patrz komentarz w join.hpp.
#include "maj0sted/editor/join.hpp"
namespace maj0sted::editor {
namespace editor::plan {
namespace {
constexpr const char* kMissing = "scalanie koncow niedostepne: brak modulu join";
@@ -24,7 +37,7 @@ const char* join_kind_name(JoinKind kind) {
return "nic";
}
JoinPlan plan_join(const domain::geometry::Pose& leaving, const domain::geometry::Pose& arriving,
JoinPlan plan_join(const geometry::Pose& leaving, const geometry::Pose& arriving,
const JoinSettings& settings) {
(void)leaving;
(void)arriving;
@@ -48,4 +61,4 @@ JoinReport join_ends(Document& document, const Solution& solution, TrackId a, in
return report;
}
} // namespace maj0sted::editor
} // namespace editor::plan

View File

@@ -1,4 +1,21 @@
#pragma once
/*
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/.
*/
module;
#include <string>
#include <vector>
export module eu07.editor.plan_join;
export import eu07.editor.plan_segment;
export import eu07.editor.plan_model;
export import eu07.editor.plan_solution;
export {
// TYMCZASOWA ZASLEPKA. Modul scalania koncow (plan_join / join_ends) nie zostal nigdy
// zacommitowany: commit 4ddfeb02 dopisal `src/editor/join.cpp` do CMakeLists i `#include` w
@@ -7,14 +24,9 @@
// API, ktorego uzywa panel planu, i mowi "niedostepne" - zeby reszta edytora dala sie uruchomic.
// Do zastapienia prawdziwym rozwiazaniem geometrii (luk-prosta-luk) i chirurgia dokumentu.
#include <string>
#include <vector>
#include "maj0sted/domain/geometry/segment_layout.hpp" // Pose
#include "maj0sted/editor/model.hpp"
#include "maj0sted/editor/solution.hpp"
namespace maj0sted::editor {
namespace editor::plan {
/// Co stanelo miedzy dwoma koncami.
enum class JoinKind {
@@ -41,8 +53,8 @@ struct JoinPlan {
double tightest{0.0};
};
[[nodiscard]] JoinPlan plan_join(const domain::geometry::Pose& leaving,
const domain::geometry::Pose& arriving,
[[nodiscard]] JoinPlan plan_join(const geometry::Pose& leaving,
const geometry::Pose& arriving,
const JoinSettings& settings);
/// Co scalenie kosztowalo.
@@ -58,4 +70,6 @@ struct JoinReport {
[[nodiscard]] JoinReport join_ends(Document& document, const Solution& solution, TrackId a,
int a_end, TrackId b, int b_end, const JoinSettings& settings);
} // namespace maj0sted::editor
} // namespace editor::plan
} // export

View File

@@ -1,5 +1,13 @@
#include "maj0sted/editor/layout.hpp"
/*
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/.
*/
module;
#include <variant>
#include <algorithm>
#include <cmath>
#include <cstddef>
@@ -8,16 +16,17 @@
#include <string>
#include <vector>
#include "maj0sted/domain/geometry/turnout.hpp"
#include "maj0sted/editor/chain.hpp"
#include "maj0sted/editor/sketch.hpp"
module eu07.editor.plan_layout;
import eu07.editor.plan_turnout_geometry;
import eu07.editor.plan_chain;
import eu07.editor.plan_sketch;
namespace maj0sted::editor {
namespace editor::plan {
namespace {
constexpr double kEps = 1e-9;
using domain::geometry::Pose;
using geometry::Pose;
std::string num(double value, int decimals = 1) {
char buffer[64];
@@ -180,21 +189,21 @@ Pose rigid_move(const Pose& anchor, const Pose& from, const Pose& to) {
// Turnouts
// ---------------------------------------------------------------------------
domain::TurnoutPart part_of(int part) {
plan::TurnoutPart part_of(int part) {
switch (part) {
case 0: return domain::TurnoutPart::PreBlade;
case 1: return domain::TurnoutPart::Blade;
case 3: return domain::TurnoutPart::FrogRail;
default: return domain::TurnoutPart::Curve;
case 0: return plan::TurnoutPart::PreBlade;
case 1: return plan::TurnoutPart::Blade;
case 3: return plan::TurnoutPart::FrogRail;
default: return plan::TurnoutPart::Curve;
}
}
int mark_kind_for(domain::TurnoutPart part) {
int mark_kind_for(plan::TurnoutPart part) {
switch (part) {
case domain::TurnoutPart::PreBlade: return 0; // PR
case domain::TurnoutPart::Blade: return 1; // ostrze iglicy
case domain::TurnoutPart::Curve: return 2; // pięta iglicy
case domain::TurnoutPart::FrogRail: return 4; // koniec krzywej
case plan::TurnoutPart::PreBlade: return 0; // PR
case plan::TurnoutPart::Blade: return 1; // ostrze iglicy
case plan::TurnoutPart::Curve: return 2; // pięta iglicy
case plan::TurnoutPart::FrogRail: return 4; // koniec krzywej
}
return 3;
}
@@ -203,25 +212,25 @@ int mark_kind_for(domain::TurnoutPart part) {
// ---------------------------------------------------------------------------
domain::Turnout to_domain(const TurnoutType& type, int hand) {
domain::Turnout turnout{
domain::CrossingMark{type.crossing_n},
hand >= 0 ? domain::DivergeSide::Left : domain::DivergeSide::Right,
plan::Turnout to_domain(const TurnoutType& type, int hand) {
plan::Turnout turnout{
plan::CrossingMark{type.crossing_n},
hand >= 0 ? plan::DivergeSide::Left : plan::DivergeSide::Right,
{},
type.length,
domain::Blade{type.blade.tip_thickness, type.blade.nose, type.blade.railtop_width},
plan::Blade{type.blade.tip_thickness, type.blade.nose, type.blade.railtop_width},
};
turnout.path.reserve(type.pieces.size());
for (const auto& piece : type.pieces) {
turnout.path.push_back(domain::TurnoutPiece{part_of(piece.part), piece.length,
turnout.path.push_back(plan::TurnoutPiece{part_of(piece.part), piece.length,
piece.radius_start, piece.radius_end,
piece.turn_in});
}
return turnout;
}
std::vector<TurnoutMark> turnout_marks(const domain::TurnoutGeometry& geometry,
const domain::geometry::Pose& start) {
std::vector<TurnoutMark> turnout_marks(const plan::TurnoutGeometry& geometry,
const geometry::Pose& start) {
std::vector<TurnoutMark> marks;
if (!geometry.valid) {
return marks;
@@ -230,27 +239,27 @@ std::vector<TurnoutMark> turnout_marks(const domain::TurnoutGeometry& geometry,
// PR stands whatever the path starts with, so it is said once here
marks.push_back(TurnoutMark{start.x, start.y, 0, 0.0});
domain::geometry::Pose walker = start;
geometry::Pose walker = start;
double station = 0.0;
domain::TurnoutPart previous = domain::TurnoutPart::PreBlade;
plan::TurnoutPart previous = plan::TurnoutPart::PreBlade;
bool first = true;
for (const auto& segment : geometry.path) {
if (std::abs(segment.turn_in) > kEps) {
const double cos_t = std::cos(segment.turn_in);
const double sin_t = std::sin(segment.turn_in);
walker = domain::geometry::Pose{walker.x, walker.y,
walker = geometry::Pose{walker.x, walker.y,
walker.hx * cos_t - walker.hy * sin_t,
walker.hx * sin_t + walker.hy * cos_t};
}
if ((first || segment.part != previous) &&
!(first && segment.part == domain::TurnoutPart::PreBlade)) {
!(first && segment.part == plan::TurnoutPart::PreBlade)) {
marks.push_back(
TurnoutMark{walker.x, walker.y, mark_kind_for(segment.part), station});
}
previous = segment.part;
first = false;
walker = domain::geometry::layout_segment(segment.k0, segment.k1, segment.length,
walker = geometry::layout_segment(segment.k0, segment.k1, segment.length,
walker, nullptr);
station += segment.length;
}
@@ -409,8 +418,8 @@ Solution solve(const Document& document) {
bend = -bend;
}
const domain::TurnoutGeometry geometry =
domain::lay_turnout(at, to_domain(*type, placement.hand), bend);
const plan::TurnoutGeometry geometry =
plan::lay_turnout(at, to_domain(*type, placement.hand), bend);
if (!geometry.valid) {
Diagnostic diagnostic;
diagnostic.code = Code::TurnoutInvalid;
@@ -491,8 +500,8 @@ Solution solve(const Document& document) {
out_segment.k0 = segment.k0;
out_segment.k1 = segment.k1;
out_segment.length = segment.length;
std::vector<domain::geometry::XY> points;
walker = domain::geometry::layout_segment(segment.k0, segment.k1,
std::vector<geometry::XY> points;
walker = geometry::layout_segment(segment.k0, segment.k1,
segment.length, walker,
&points);
out_segment.part = segment.part;
@@ -703,4 +712,4 @@ Solution solve(const Document& document) {
return solution;
}
} // namespace maj0sted::editor
} // namespace editor::plan

View File

@@ -1,10 +1,23 @@
#pragma once
/*
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/.
*/
module;
#include <string>
#include <vector>
#include "maj0sted/domain/geometry/turnout.hpp"
#include "maj0sted/editor/model.hpp"
#include "maj0sted/editor/solution.hpp"
export module eu07.editor.plan_layout;
export import eu07.editor.plan_turnout_geometry;
export import eu07.editor.plan_model;
export import eu07.editor.plan_solution;
namespace maj0sted::editor {
export {
namespace editor::plan {
/// Solves a whole document: lays every turnout on its through track, lays every
/// track from its anchor, and moves the tracks that are held parallel to another
@@ -24,14 +37,16 @@ namespace maj0sted::editor {
/// curvatures signed for @p hand (+1 diverges left). Public because the editor
/// dimensions a *type* with it — laying one from an identity pose is how the
/// catalogue numbers are checked before any instance stands on a track.
[[nodiscard]] domain::Turnout to_domain(const TurnoutType& type, int hand);
[[nodiscard]] plan::Turnout to_domain(const TurnoutType& type, int hand);
/// The construction points of a turnout laid from @p start: where its parts meet,
/// where its blade is dimensioned, and the points the drawing takes its lengths
/// between. Public for the same reason as to_domain — so a bare type is drawn from
/// the very same list the solver reports for a placement, rather than from a second
/// idea of what a construction point is.
[[nodiscard]] std::vector<TurnoutMark> turnout_marks(const domain::TurnoutGeometry& geometry,
const domain::geometry::Pose& start);
[[nodiscard]] std::vector<TurnoutMark> turnout_marks(const plan::TurnoutGeometry& geometry,
const geometry::Pose& start);
} // namespace maj0sted::editor
} // namespace editor::plan
} // export

View File

@@ -1,6 +1,20 @@
#include "maj0sted/editor/model.hpp"
/*
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/.
*/
module;
#include <cstdint>
#include <string>
#include <variant>
#include <vector>
namespace maj0sted::editor {
module eu07.editor.plan_model;
namespace editor::plan {
namespace {
template <typename Container, typename Id>
@@ -71,4 +85,4 @@ const Track* find_element_track(const Document& document, ElementId id) {
return nullptr;
}
} // namespace maj0sted::editor
} // namespace editor::plan

View File

@@ -1,11 +1,22 @@
#pragma once
/*
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/.
*/
module;
#include <cstdint>
#include <string>
#include <variant>
#include <vector>
namespace maj0sted::editor {
export module eu07.editor.plan_model;
export {
namespace editor::plan {
// ---------------------------------------------------------------------------
// Identity
@@ -129,7 +140,7 @@ struct Track {
// ---------------------------------------------------------------------------
/// One piece of a catalogue turnout's diverging path. Mirrors
/// domain::TurnoutPiece; kept here so the document does not depend on the
/// plan::TurnoutPiece; kept here so the document does not depend on the
/// geometry header.
struct TurnoutPieceSpec {
int part{2}; ///< 0 przediglicowy, 1 iglica, 2 łuk, 3 krzyżownicowa
@@ -139,7 +150,7 @@ struct TurnoutPieceSpec {
double turn_in{0.0}; ///< kąt nagięcia iglicy, radians, unsigned
};
/// The blade's own dimensions, which place its cut tip. Mirrors domain::Blade.
/// The blade's own dimensions, which place its cut tip. Mirrors plan::Blade.
struct BladeSpec {
double tip_thickness{0.0}; ///< u: width where the planed nose ends
double nose{0.0}; ///< d: the planed nose, from the tip to that point
@@ -219,4 +230,6 @@ struct Document {
[[nodiscard]] const Element* find_element(const Document& document, ElementId id);
[[nodiscard]] const Track* find_element_track(const Document& document, ElementId id);
} // namespace maj0sted::editor
} // namespace editor::plan
} // export

View File

@@ -1,10 +1,21 @@
#include "maj0sted/render/rail_renderer.hpp"
/*
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/.
*/
module;
#include <cstdint>
#include <vector>
#include <cmath>
#include <cstddef>
#include <utility>
namespace maj0sted::render {
module eu07.editor.plan_rail_renderer;
namespace editor::plan::render {
namespace {
@@ -57,4 +68,4 @@ void RailRenderer::add(const CentrelineElement& element) {
}
}
} // namespace maj0sted::render
} // namespace editor::plan::render

View File

@@ -1,11 +1,21 @@
#pragma once
/*
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/.
*/
module;
#include <cstdint>
#include <vector>
#include "maj0sted/render/track_renderer.hpp"
export module eu07.editor.plan_rail_renderer;
export import eu07.editor.plan_track_renderer;
namespace maj0sted::render {
export {
namespace editor::plan::render {
/// A drawable polyline produced by RailRenderer: one rail, carrying the logical
/// metadata of the centreline element it came from so the host can colour and
@@ -40,4 +50,6 @@ private:
std::vector<RailPolyline> rails_;
};
} // namespace maj0sted::render
} // namespace editor::plan::render
} // export

View File

@@ -1,11 +1,21 @@
#include "maj0sted/editor/render.hpp"
/*
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/.
*/
module;
#include <vector>
#include <cstdint>
#include <cmath>
#include <utility>
#include "maj0sted/render/rail_renderer.hpp"
module eu07.editor.plan_render;
import eu07.editor.plan_rail_renderer;
namespace maj0sted::editor {
namespace editor::plan {
namespace {
render::ElementKind kind_of(Kind kind) {
@@ -119,4 +129,4 @@ std::vector<PlanPolyline> render_rails(const Solution& solution, double gauge_me
return out;
}
} // namespace maj0sted::editor
} // namespace editor::plan

View File

@@ -1,10 +1,20 @@
#pragma once
/*
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/.
*/
module;
#include <vector>
#include "maj0sted/editor/solution.hpp"
export module eu07.editor.plan_render;
export import eu07.editor.plan_solution;
namespace maj0sted::editor {
export {
namespace editor::plan {
/// One drawable rail, tagged with what it came from so the host can colour it
/// and hit-test it. Exactly one of @c element / @c turnout is set.
@@ -32,4 +42,6 @@ struct PlanPolyline {
[[nodiscard]] std::vector<PlanPolyline> render_rails(const Solution& solution,
double gauge_metres = 1.5);
} // namespace maj0sted::editor
} // namespace editor::plan
} // export

26
editor/plan_scene.cppm Normal file
View File

@@ -0,0 +1,26 @@
/*
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/.
*/
export module eu07.editor.plan_scene;
export {
namespace editor::plan::render {
/// A point in the project's CRS (metres for EPSG:2180).
struct Point {
double x;
double y;
};
/// Which kind of plan element a polyline came from (lets a GUI colour it).
enum class ElementKind { Straight, Arc, Transition };
} // namespace editor::plan::render
} // export

View File

@@ -1,5 +1,13 @@
#include "maj0sted/io/scn_export.hpp"
/*
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/.
*/
module;
#include <ostream>
#include <algorithm>
#include <cmath>
#include <cstddef>
@@ -10,16 +18,18 @@
#include <string>
#include <vector>
namespace maj0sted::io {
module eu07.editor.plan_scn_export;
namespace editor::plan::io {
namespace {
using maj0sted::editor::Document;
using maj0sted::editor::Kind;
using maj0sted::editor::PlanPoint;
using maj0sted::editor::SolvedElement;
using maj0sted::editor::SolvedTrack;
using maj0sted::editor::SolvedTurnout;
using maj0sted::editor::TurnoutPlacement;
using editor::plan::Document;
using editor::plan::Kind;
using editor::plan::PlanPoint;
using editor::plan::SolvedElement;
using editor::plan::SolvedTrack;
using editor::plan::SolvedTurnout;
using editor::plan::TurnoutPlacement;
/// What `TTrack::Equal` calls one point, and so what the simulator will wire
/// together. Everything the exporter writes has to land well inside it.
@@ -413,7 +423,7 @@ struct Span {
/// What the catalogue calls the turnout's radius: the one the longest curved
/// piece runs at, which is the łuk itself and not an easement into it.
double catalogue_radius(const editor::TurnoutType& type) {
double catalogue_radius(const plan::TurnoutType& type) {
double longest = 0.0;
double radius = 0.0;
for (const auto& piece : type.pieces) {
@@ -448,7 +458,7 @@ std::string switch_include(double radius, int hand) {
/// Which way the switch faces, in the degrees an include's `rotate` takes: zero
/// along the simulator's +z, turning towards +x.
double heading_degrees(const domain::geometry::Pose& pose) {
double heading_degrees(const geometry::Pose& pose) {
return std::atan2(pose.hx, -pose.hy) * 180.0 / std::numbers::pi;
}
@@ -470,7 +480,7 @@ double diverging_radius(const SolvedTurnout& turnout) {
// ---------------------------------------------------------------------------
ScnExportOptions resolve_scn_origin(const maj0sted::editor::Solution& solution,
ScnExportOptions resolve_scn_origin(const editor::plan::Solution& solution,
ScnExportOptions options) {
if (std::abs(options.origin_east) >= 1.0 || std::abs(options.origin_north) >= 1.0) {
return options;
@@ -490,7 +500,7 @@ ScnExportOptions resolve_scn_origin(const maj0sted::editor::Solution& solution,
}
ScnExportResult export_scn(const Document& document,
const maj0sted::editor::Solution& solution,
const editor::plan::Solution& solution,
const ScnExportOptions& options, std::ostream& out) {
const auto opt = resolve_scn_origin(solution, options);
ScnExportResult result;
@@ -637,7 +647,7 @@ ScnExportResult export_scn(const Document& document,
turnout.start.hy};
const double exit_station = span.facing ? span.to : span.from;
domain::geometry::Pose exit_pose{};
geometry::Pose exit_pose{};
if (!pose_at(track, exit_station, exit_pose)) {
result.warnings.push_back("rozjazd „" + span.placement->type + "” na torze „" +
base + "” nie ma końca na torze zasadniczym");
@@ -753,4 +763,4 @@ ScnExportResult export_scn(const Document& document,
return result;
}
} // namespace maj0sted::io
} // namespace editor::plan::io

View File

@@ -1,13 +1,23 @@
#pragma once
/*
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/.
*/
module;
#include <ostream>
#include <string>
#include <vector>
#include "maj0sted/editor/model.hpp"
#include "maj0sted/editor/solution.hpp"
export module eu07.editor.plan_scn_export;
export import eu07.editor.plan_model;
export import eu07.editor.plan_solution;
namespace maj0sted::io {
export {
namespace editor::plan::io {
struct ScnExportOptions {
double origin_east{0.0};
@@ -64,12 +74,14 @@ struct ScnExportResult {
/// Height is still a constant and both roll slots are still zero — the vertical
/// alignment is a separate piece of work, and nothing in the editor produces one
/// yet.
[[nodiscard]] ScnExportResult export_scn(const editor::Document& document,
const editor::Solution& solution,
[[nodiscard]] ScnExportResult export_scn(const plan::Document& document,
const plan::Solution& solution,
const ScnExportOptions& options,
std::ostream& out);
[[nodiscard]] ScnExportOptions resolve_scn_origin(const editor::Solution& solution,
[[nodiscard]] ScnExportOptions resolve_scn_origin(const plan::Solution& solution,
ScnExportOptions options);
} // namespace maj0sted::io
} // namespace editor::plan::io
} // export

View File

@@ -1,12 +1,23 @@
#pragma once
/*
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/.
*/
module;
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <numbers>
#include <vector>
namespace maj0sted::domain::geometry {
export module eu07.editor.plan_segment;
export {
namespace editor::plan::geometry {
struct XY {
double x;
@@ -87,4 +98,6 @@ inline Pose layout_segment(double k0, double k1, double length, Pose start,
start.hx * sin_t + start.hy * cos_t};
}
} // namespace maj0sted::domain::geometry
} // namespace editor::plan::geometry
} // export

View File

@@ -1,17 +1,27 @@
#include "maj0sted/editor/sketch.hpp"
/*
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/.
*/
module;
#include <cmath>
#include <variant>
#include <cstdio>
#include <numbers>
#include <string>
#include <vector>
namespace maj0sted::editor {
module eu07.editor.plan_sketch;
namespace editor::plan {
namespace {
constexpr double kEps = 1e-9;
} // namespace
using domain::geometry::Pose;
using geometry::Pose;
Corner fit_corner(double in_hx, double in_hy, double out_hx, double out_hy, double radius) {
@@ -217,7 +227,7 @@ Pose lay_group(const Track& track, const Group& group, const std::vector<double>
const double k0 = element.kind == Kind::Clothoid ? k_prev : k1;
const double length =
element.kind == Kind::Arc ? arc_length[arc_at++] : element.length;
pose = domain::geometry::layout_segment(k0, k1, length, pose, nullptr);
pose = geometry::layout_segment(k0, k1, length, pose, nullptr);
k_prev = k1;
}
return pose;
@@ -492,4 +502,4 @@ bool lay_along_skeleton(Track& track, const Skeleton& skeleton, std::string& why
return true;
}
} // namespace maj0sted::editor
} // namespace editor::plan

View File

@@ -1,12 +1,22 @@
#pragma once
/*
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/.
*/
module;
#include <string>
#include <vector>
#include "maj0sted/editor/model.hpp"
#include "maj0sted/editor/solution.hpp"
export module eu07.editor.plan_sketch;
export import eu07.editor.plan_model;
export import eu07.editor.plan_solution;
namespace maj0sted::editor {
export {
namespace editor::plan {
/// Laying track by clicking along the ground.
///
@@ -86,7 +96,7 @@ struct Skeleton {
/// curve comes out, rather than the curve being slid along a line drawn
/// before the edit.
bool pinned{false};
domain::geometry::Pose begins{};
geometry::Pose begins{};
};
/// Pulls @p track's skeleton out of @p solved.
@@ -122,4 +132,6 @@ struct Skeleton {
[[nodiscard]] bool lay_along_skeleton(Track& track, const Skeleton& skeleton,
std::string& why);
} // namespace maj0sted::editor
} // namespace editor::plan
} // export

View File

@@ -1,9 +1,20 @@
#include "maj0sted/editor/solution.hpp"
/*
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/.
*/
module;
#include <string>
#include <vector>
#include <algorithm>
#include <cmath>
namespace maj0sted::editor {
module eu07.editor.plan_solution;
namespace editor::plan {
const char* code_name(Code code) {
switch (code) {
@@ -58,13 +69,13 @@ const SolvedElement* find_element(const Solution& solution, ElementId id) {
return nullptr;
}
bool pose_at(const SolvedTrack& track, double station, domain::geometry::Pose& out) {
bool pose_at(const SolvedTrack& track, double station, geometry::Pose& out) {
const auto& line = track.centreline;
if (line.empty()) {
return false;
}
if (line.size() == 1) {
out = domain::geometry::Pose{line[0].x, line[0].y, track.start.hx, track.start.hy};
out = geometry::Pose{line[0].x, line[0].y, track.start.hx, track.start.hy};
return true;
}
@@ -78,7 +89,7 @@ bool pose_at(const SolvedTrack& track, double station, domain::geometry::Pose& o
}
if (station <= walked + step) {
const double t = std::clamp((station - walked) / step, 0.0, 1.0);
out = domain::geometry::Pose{line[i - 1].x + dx * t, line[i - 1].y + dy * t,
out = geometry::Pose{line[i - 1].x + dx * t, line[i - 1].y + dy * t,
dx / step, dy / step};
return true;
}
@@ -115,4 +126,4 @@ bool steady_room(const SolvedTrack& track, double station, double& curvature, do
return false;
}
} // namespace maj0sted::editor
} // namespace editor::plan

View File

@@ -1,13 +1,23 @@
#pragma once
/*
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/.
*/
module;
#include <string>
#include <vector>
#include "maj0sted/domain/geometry/segment_layout.hpp" // Pose
#include "maj0sted/domain/geometry/turnout.hpp" // TurnoutPart
#include "maj0sted/editor/model.hpp"
export module eu07.editor.plan_solution;
export import eu07.editor.plan_segment;
export import eu07.editor.plan_turnout_geometry;
export import eu07.editor.plan_model;
namespace maj0sted::editor {
export {
namespace editor::plan {
/// A point in the project's CRS (metres for EPSG:2180).
struct PlanPoint {
@@ -25,8 +35,8 @@ struct PlanPoint {
struct SolvedElement {
ElementId id{ElementId::none};
Kind kind{Kind::Line};
domain::geometry::Pose start{}; ///< pose the element is laid from
domain::geometry::Pose end{}; ///< pose it ends on = the next element's start
geometry::Pose start{}; ///< pose the element is laid from
geometry::Pose end{}; ///< pose it ends on = the next element's start
double k0{0.0}; ///< signed curvature at the start (1/m)
double k1{0.0}; ///< signed curvature at the end
double length{0.0};
@@ -40,8 +50,8 @@ struct SolvedTrack {
TrackId id{TrackId::none};
std::vector<SolvedElement> elements;
std::vector<PlanPoint> centreline;
domain::geometry::Pose start{};
domain::geometry::Pose end{};
geometry::Pose start{};
geometry::Pose end{};
double length{0.0};
/// True when the chain was laid whole. False means it was cut short by a
/// diagnostic — the elements up to the cut are still here and still drawable.
@@ -50,9 +60,9 @@ struct SolvedTrack {
/// One laid segment of a turnout's diverging path.
struct SolvedTurnoutSegment {
domain::geometry::Pose start{};
geometry::Pose start{};
double k0{0.0}, k1{0.0}, length{0.0};
domain::TurnoutPart part{domain::TurnoutPart::Curve};
plan::TurnoutPart part{plan::TurnoutPart::Curve};
std::vector<PlanPoint> points;
};
@@ -73,8 +83,8 @@ struct TurnoutMark {
struct SolvedTurnout {
TurnoutId id{TurnoutId::none};
bool valid{false};
domain::geometry::Pose start{}; ///< PR, on the through track
domain::geometry::Pose frog{}; ///< KR, where a branch anchors
geometry::Pose start{}; ///< PR, on the through track
geometry::Pose frog{}; ///< KR, where a branch anchors
double tangent_front{0.0}, tangent_back{0.0};
double diverging_length{0.0};
double through_length{0.0}; ///< PR to the frog's projection, along the through track
@@ -154,7 +164,7 @@ struct Solution {
/// Pose at @p station metres along a solved track's centreline. Clamped to the
/// ends. Returns false for an empty track.
[[nodiscard]] bool pose_at(const SolvedTrack& track, double station,
domain::geometry::Pose& out);
geometry::Pose& out);
/// How much track of one steady curvature there is around @p station: @p behind
/// back to where the element begins, @p ahead on to where it ends, and
@@ -166,4 +176,6 @@ struct Solution {
[[nodiscard]] bool steady_room(const SolvedTrack& track, double station, double& curvature,
double& behind, double& ahead);
} // namespace maj0sted::editor
} // namespace editor::plan
} // export

View File

@@ -1,5 +1,17 @@
#include "maj0sted/editor/tile_cache.hpp"
/*
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/.
*/
module;
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
#include <algorithm>
#include <charconv>
#include <cmath>
@@ -7,12 +19,14 @@
#include <numeric>
#include <utility>
namespace maj0sted::editor {
module eu07.editor.plan_tilecache;
namespace editor::plan {
namespace {
// Shortest round-trippable, locale-independent representation of a double —
// same convention as maj0sted::io so URLs are stable and precise.
// same convention as editor::plan::io so URLs are stable and precise.
std::string num(double value) {
char buffer[64];
const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
@@ -258,4 +272,4 @@ void TileService::schedule_retry(const TileKey& key) {
begin_fetch(key);
}
} // namespace maj0sted::editor
} // namespace editor::plan

View File

@@ -1,12 +1,23 @@
#pragma once
/*
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/.
*/
module;
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
namespace maj0sted::editor {
export module eu07.editor.plan_tilecache;
export {
namespace editor::plan {
/// A stable tile in the EPSG:2180 tiling pyramid. Tiles are addressed by their
/// zoom level and integer column/row, so the same ground square always maps to
@@ -179,4 +190,6 @@ private:
std::function<void(TileKey)> on_ready_;
};
} // namespace maj0sted::editor
} // namespace editor::plan
} // export

View File

@@ -1,11 +1,21 @@
#pragma once
/*
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/.
*/
module;
#include <cstdint>
#include <vector>
#include "maj0sted/render/scene.hpp" // Point, ElementKind
export module eu07.editor.plan_track_renderer;
export import eu07.editor.plan_scene;
namespace maj0sted::render {
export {
namespace editor::plan::render {
/// One sampled centreline element of a solved niweleta, tagged with its logical
/// origin so a UI can colour, label and hit-test it. Rendering-neutral: it holds
@@ -36,4 +46,6 @@ public:
virtual void add(const CentrelineElement& element) = 0;
};
} // namespace maj0sted::render
} // namespace editor::plan::render
} // export

View File

@@ -1,11 +1,20 @@
#include "maj0sted/domain/geometry/turnout.hpp"
/*
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/.
*/
module;
#include <algorithm>
#include <cmath>
#include <numbers>
#include <vector>
namespace maj0sted::domain {
module eu07.editor.plan_turnout_geometry;
namespace editor::plan {
using geometry::layout_segment;
using geometry::Pose;
@@ -311,4 +320,4 @@ TurnoutGeometry lay_turnout(const Pose& start, const Turnout& turnout, double be
return geometry;
}
} // namespace maj0sted::domain
} // namespace editor::plan

View File

@@ -1,10 +1,20 @@
#pragma once
/*
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/.
*/
module;
#include <vector>
#include "maj0sted/domain/geometry/segment_layout.hpp" // Pose
export module eu07.editor.plan_turnout_geometry;
export import eu07.editor.plan_segment;
namespace maj0sted::domain {
export {
namespace editor::plan {
/// The crossing mark of a turnout — the skos 1:n. It is the tangent of the angle
/// between the through and diverging tracks, so it fixes the total angle the
@@ -176,4 +186,6 @@ struct TurnoutGeometry {
[[nodiscard]] TurnoutGeometry lay_turnout(const geometry::Pose& start, const Turnout& turnout,
double bend = 0.0);
} // namespace maj0sted::domain
} // namespace editor::plan
} // export

View File

@@ -0,0 +1,52 @@
# Unit tests for the plan library (the geometry and the solver behind the track
# layout tool). They are off by default: switched on, the interface units are
# compiled a second time into a small library of their own, because a test
# executable can only import modules from a target it links, and linking the game
# would drag the whole simulator in. The code under test needs nothing but a
# C++20 compiler - no glm, no engine, no imgui.
option(EU07_EDITOR_PLAN_TESTS "Build the editor plan library unit tests" OFF)
if (NOT EU07_EDITOR_PLAN_TESTS)
return()
endif()
enable_testing()
set(PLAN_INTERFACES
../plan_segment.cppm
../plan_turnout_geometry.cppm
../plan_model.cppm
../plan_solution.cppm
../plan_chain.cppm
../plan_join.cppm
../plan_layout.cppm
../plan_render.cppm
../plan_sketch.cppm
../plan_tilecache.cppm
../plan_document_io.cppm
../plan_scn_export.cppm
../plan_scene.cppm
../plan_track_renderer.cppm
../plan_rail_renderer.cppm)
file(GLOB PLAN_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/../plan_*.cpp")
add_library(editor_plan STATIC ${PLAN_SOURCES})
target_sources(editor_plan
PUBLIC FILE_SET CXX_MODULES
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/..
FILES ${PLAN_INTERFACES})
set_target_properties(editor_plan PROPERTIES
CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF
POSITION_INDEPENDENT_CODE ON FOLDER "editor")
foreach(name chain hold lines graph format turnout tile_cache)
add_executable(editor_plan_test_${name} test_${name}.cpp)
target_link_libraries(editor_plan_test_${name} PRIVATE editor_plan)
target_include_directories(editor_plan_test_${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(editor_plan_test_${name} PROPERTIES
CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF FOLDER "editor")
add_test(NAME plan_${name} COMMAND editor_plan_test_${name})
endforeach()
# test_scn.cpp and test_join.cpp are missing: they were never committed, along
# with the join module those two exercised (see the stub in plan_join.cpp).

View File

@@ -1,3 +1,11 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
// A tiny zero-dependency check framework so the test suite needs nothing but a
@@ -6,18 +14,18 @@
#include <cstdio>
namespace maj0test {
namespace plantest {
inline int checks = 0;
inline int failures = 0;
} // namespace maj0test
} // namespace plantest
// Variadic so that brace-init lists with commas, e.g. CHECK(Point{1, 2} == ...),
// are passed through as a single condition instead of several macro arguments.
#define CHECK(...) \
do { \
++::maj0test::checks; \
++::plantest::checks; \
if (!(__VA_ARGS__)) { \
++::maj0test::failures; \
++::plantest::failures; \
std::printf(" [FAIL] %s:%d CHECK(%s)\n", __FILE__, __LINE__, \
#__VA_ARGS__); \
} \
@@ -25,7 +33,7 @@ inline int failures = 0;
#define CHECK_THROWS(...) \
do { \
++::maj0test::checks; \
++::plantest::checks; \
bool threw = false; \
try { \
(void)(__VA_ARGS__); \
@@ -33,7 +41,7 @@ inline int failures = 0;
threw = true; \
} \
if (!threw) { \
++::maj0test::failures; \
++::plantest::failures; \
std::printf(" [FAIL] %s:%d CHECK_THROWS(%s)\n", __FILE__, \
__LINE__, #__VA_ARGS__); \
} \
@@ -46,6 +54,6 @@ inline int failures = 0;
} while (0)
#define REPORT() \
(std::printf("\n%d checks, %d failures\n", ::maj0test::checks, \
::maj0test::failures), \
::maj0test::failures == 0 ? 0 : 1)
(std::printf("\n%d checks, %d failures\n", ::plantest::checks, \
::plantest::failures), \
::plantest::failures == 0 ? 0 : 1)

View File

@@ -1,14 +1,23 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include <cmath>
#include <numbers>
#include "check.hpp"
import eu07.editor.plan_chain;
// The element chain: a walk, one element at a time, with every length said
// outright. Nothing here is solved for — what the document says is what is laid.
#include <cmath>
#include <numbers>
#include "check.hpp"
#include "maj0sted/editor/chain.hpp"
using namespace maj0sted::editor;
using maj0sted::domain::geometry::Pose;
using namespace editor::plan;
using editor::plan::geometry::Pose;
namespace {

View File

@@ -1,16 +1,27 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include <variant>
#include <cmath>
#include <string>
#include "check.hpp"
import eu07.editor.plan_layout;
import eu07.editor.plan_document_io;
// The .m0s format, version 5: everything by id, an explicit version, and a
// round trip that is byte-identical. Older files still open: a version-3 one has
// its arcs and blade fields converted to the piece list on the way in, and a
// version-4 one simply had no nose on its blade yet.
#include <cmath>
#include <string>
#include "check.hpp"
#include "maj0sted/editor/layout.hpp"
#include "maj0sted/io/document_io.hpp"
using namespace maj0sted::editor;
using namespace editor::plan;
namespace {
@@ -108,20 +119,20 @@ bool same(const Document& a, const Document& b) {
void round_trip_is_byte_identical() {
const Document document = sample();
const std::string text = maj0sted::io::serialize(document);
const std::string text = editor::plan::io::serialize(document);
const auto reloaded = maj0sted::io::deserialize(text);
const auto reloaded = editor::plan::io::deserialize(text);
CHECK(reloaded.has_value());
if (!reloaded) {
return;
}
CHECK(same(document, *reloaded));
CHECK(maj0sted::io::serialize(*reloaded) == text);
CHECK(editor::plan::io::serialize(*reloaded) == text);
}
void every_field_survives() {
const Document document = sample();
const auto reloaded = maj0sted::io::deserialize(maj0sted::io::serialize(document));
const auto reloaded = editor::plan::io::deserialize(editor::plan::io::serialize(document));
CHECK(reloaded.has_value());
if (!reloaded) {
return;
@@ -180,14 +191,14 @@ void an_older_file_is_refused() {
// outright rather than half-understood. Version 3 is not refused — it is
// converted, which an_old_file_upgrades_to_the_piece_list checks.
const std::string version_1 =
"maj0sted 1\ncrs 2180\nniwelety 0\neditor 1\neniw 2 1\nename niweleta 1\n";
CHECK(!maj0sted::io::deserialize(version_1).has_value());
"the plan library 1\ncrs 2180\nniwelety 0\neditor 1\neniw 2 1\nename niweleta 1\n";
CHECK(!editor::plan::io::deserialize(version_1).has_value());
const std::string version_2 =
"m0s 2\nnext 9\nview 0 0 0\norigin 0 0 0 0\nlines 0\ntypes 0\n"
"tracks 0\n";
CHECK(!maj0sted::io::deserialize(version_2).has_value());
CHECK(!maj0sted::io::deserialize("").has_value());
CHECK(!maj0sted::io::deserialize("m0s 8\n").has_value());
CHECK(!editor::plan::io::deserialize(version_2).has_value());
CHECK(!editor::plan::io::deserialize("").has_value());
CHECK(!editor::plan::io::deserialize("m0s 8\n").has_value());
}
void a_file_from_before_lukowanie_still_opens() {
@@ -197,7 +208,7 @@ void a_file_from_before_lukowanie_still_opens() {
const std::string version_5 =
"m0s 5\nnext 5\nview 0 0 0\norigin 0 0 0 0\ntypes 0\n"
"turnouts 1\nturnout 2 1 100 1 1\ntutype Rz 1:9 R190\ntracks 0\n";
const auto document = maj0sted::io::deserialize(version_5);
const auto document = editor::plan::io::deserialize(version_5);
CHECK(document.has_value());
if (!document) {
return;
@@ -206,7 +217,7 @@ void a_file_from_before_lukowanie_still_opens() {
CHECK(document->turnouts[0].station == 100.0);
CHECK(document->turnouts[0].bend_from_track);
CHECK(document->turnouts[0].bend == 0.0);
CHECK(maj0sted::io::serialize(*document).find("m0s 7") != std::string::npos);
CHECK(editor::plan::io::serialize(*document).find("m0s 7") != std::string::npos);
}
void a_file_from_before_the_export_path_still_opens() {
@@ -216,14 +227,14 @@ void a_file_from_before_the_export_path_still_opens() {
"m0s 6\nnext 3\nview 0 0 0\norigin 0 0 0 0\ntypes 0\nturnouts 0\n"
"tracks 1\ntrack 1\ntkname tor\nanchor pose 0 0 0\nelements 1\n"
"element 2 line 0 0 250\n";
const auto document = maj0sted::io::deserialize(version_6);
const auto document = editor::plan::io::deserialize(version_6);
CHECK(document.has_value());
if (!document) {
return;
}
CHECK(document->scn_path.empty());
CHECK(document->tracks.size() == 1);
CHECK(maj0sted::io::serialize(*document).find("m0s 7") != std::string::npos);
CHECK(editor::plan::io::serialize(*document).find("m0s 7") != std::string::npos);
}
void a_file_from_before_the_nose_still_opens() {
@@ -234,7 +245,7 @@ void a_file_from_before_the_nose_still_opens() {
"m0s 4\nnext 1\nview 0 0 0\norigin 0 0 0 0\n"
"types 1\ntype 9 27.138 0.0049 0.07 1\ntseg 2 21.02 190 190 0\n"
"tname Rz 1:9 R190\nturnouts 0\ntracks 0\n";
const auto document = maj0sted::io::deserialize(version_4);
const auto document = editor::plan::io::deserialize(version_4);
CHECK(document.has_value());
if (!document) {
return;
@@ -246,7 +257,7 @@ void a_file_from_before_the_nose_still_opens() {
CHECK(document->turnout_types[0].pieces.size() == 1);
// and it is written back in the new form, with a place for the nose
CHECK(maj0sted::io::serialize(*document).find("m0s 7") != std::string::npos);
CHECK(editor::plan::io::serialize(*document).find("m0s 7") != std::string::npos);
}
void an_old_file_upgrades_to_the_piece_list() {
@@ -257,7 +268,7 @@ void an_old_file_upgrades_to_the_piece_list() {
"m0s 3\nnext 1\nview 0 0 0\norigin 0 0 0 0\n"
"types 1\ntype 9 0 0 27.138 0 0 0 1\ntarc 190 0 0\ntname Rz 1:9 R190\n"
"turnouts 0\ntracks 0\n";
const auto document = maj0sted::io::deserialize(version_3);
const auto document = editor::plan::io::deserialize(version_3);
CHECK(document.has_value());
if (!document) {
return;
@@ -270,20 +281,20 @@ void an_old_file_upgrades_to_the_piece_list() {
CHECK(std::abs(type.pieces[0].length - 190.0 * std::atan(1.0 / 9.0)) < 1e-9);
CHECK(type.pieces[1].part == 3);
const auto laid = maj0sted::domain::lay_turnout(
maj0sted::domain::geometry::Pose{0.0, 0.0, 1.0, 0.0}, to_domain(type, 1));
const auto laid = editor::plan::lay_turnout(
editor::plan::geometry::Pose{0.0, 0.0, 1.0, 0.0}, to_domain(type, 1));
CHECK(laid.valid);
CHECK(std::abs(laid.angle_residual) < 1e-9);
CHECK(std::abs(laid.length_residual) < 1e-9);
// and it is written back in the new form
CHECK(maj0sted::io::serialize(*document).find("tseg ") != std::string::npos);
CHECK(editor::plan::io::serialize(*document).find("tseg ") != std::string::npos);
}
void a_truncated_file_is_refused() {
const std::string text = maj0sted::io::serialize(sample());
const std::string text = editor::plan::io::serialize(sample());
// Cut off half way: the counts no longer match what follows.
CHECK(!maj0sted::io::deserialize(text.substr(0, text.size() / 2)).has_value());
CHECK(!editor::plan::io::deserialize(text.substr(0, text.size() / 2)).has_value());
}
} // namespace

View File

@@ -1,13 +1,22 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include <cmath>
#include "check.hpp"
import eu07.editor.plan_layout;
// What refers to what decides the order things are laid in. A turnout on the
// branch of a turnout is not a special case, and a reference that runs in a
// circle is reported rather than relaxed at for a fixed number of passes.
#include <cmath>
#include "check.hpp"
#include "maj0sted/editor/layout.hpp"
using namespace maj0sted::editor;
using namespace editor::plan;
namespace {

View File

@@ -1,16 +1,25 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include <cmath>
#include <cstdio>
#include <numbers>
#include "check.hpp"
import eu07.editor.plan_layout;
import eu07.editor.plan_sketch;
// What one track keeps against another, and what clicking along the ground
// works out at the click. Both are the same idea from two ends: geometry is
// decided once, in numbers, and nothing is left pointing at a drawing.
#include <cmath>
#include <cstdio>
#include <numbers>
#include "check.hpp"
#include "maj0sted/editor/layout.hpp"
#include "maj0sted/editor/sketch.hpp"
using namespace maj0sted::editor;
using namespace editor::plan;
namespace {

View File

@@ -1,19 +1,30 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include <variant>
#include <cmath>
#include <numbers>
#include <string>
#include <vector>
#include "check.hpp"
import eu07.editor.plan_layout;
import eu07.editor.plan_sketch;
// Pulling a straight about by one of its ends. The arcs beside it re-fit, every
// other straight keeps its bearing, and an arc that turns past a half circle
// stays as far round as it was drawn — that last one is the whole reason the
// construction goes through the arc's centre and not through where the tangents
// would have crossed.
#include <cmath>
#include <numbers>
#include <string>
#include <vector>
#include "check.hpp"
#include "maj0sted/editor/layout.hpp"
#include "maj0sted/editor/sketch.hpp"
using namespace maj0sted::editor;
using namespace editor::plan;
namespace {

View File

@@ -1,12 +1,20 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include <cmath>
#include <limits>
#include <string>
#include <vector>
#include "check.hpp"
#include "maj0sted/editor/tile_cache.hpp"
#include <cstdint>
import eu07.editor.plan_tilecache;
using namespace maj0sted::editor;
using namespace editor::plan;
namespace {

View File

@@ -1,3 +1,18 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include <cmath>
#include <numbers>
#include "check.hpp"
#include <initializer_list>
#include <vector>
import eu07.editor.plan_turnout_geometry;
// A turnout is laid from the list its catalogue entry gives, piece by piece, and
// nothing in it is fitted: whether the list keeps the promises of the crossing
// mark and the catalogue length is reported, not corrected. The blade is
@@ -6,14 +21,10 @@
// rail at PR, but the blade only starts at a, and its planed nose runs on to A
// where the rail is u thick.
#include <cmath>
#include <numbers>
#include "check.hpp"
#include "maj0sted/domain/geometry/turnout.hpp"
using namespace maj0sted::domain;
using maj0sted::domain::geometry::Pose;
using namespace editor::plan;
using editor::plan::geometry::Pose;
namespace {
@@ -76,7 +87,7 @@ Pose pose_after(const TurnoutGeometry& geometry, std::size_t index) {
pose = Pose{pose.x, pose.y, pose.hx * c - pose.hy * s,
pose.hx * s + pose.hy * c};
}
pose = geometry::layout_segment(segment.k0, segment.k1, segment.length, pose,
pose = editor::plan::geometry::layout_segment(segment.k0, segment.k1, segment.length, pose,
nullptr);
}
return pose;

View File

@@ -2,9 +2,9 @@ module;
#include <cmath>
#include <string>
#include <vector>
#include "maj0sted/domain/geometry/turnout.hpp"
module eu07.editor.turnouts;
import eu07.editor.plan_turnout_geometry;
/*
This Source Code Form is subject to the
@@ -47,8 +47,8 @@ turnout_preset curved_tangent(std::string name, double crossing_n, double radius
auto const alfa{std::atan(1.0 / crossing_n)};
auto const tangent{radius * std::tan(0.5 * alfa)};
auto const b{back_tangent > 0.0 ? back_tangent : tangent};
auto const lead{maj0sted::domain::blade_run_to_offset(radius, 0.0, tip_thickness) - nose};
auto const blade{maj0sted::domain::blade_run_to_offset(radius, 0.0, heel_offset) - lead};
auto const lead{editor::plan::blade_run_to_offset(radius, 0.0, tip_thickness) - nose};
auto const blade{editor::plan::blade_run_to_offset(radius, 0.0, heel_offset) - lead};
turnout_preset preset{std::move(name), crossing_n, tangent + b, tip_thickness, nose, railtop_width, {}};
preset.pieces.push_back({0, lead, radius, radius, 0.0});

View File

@@ -19,7 +19,7 @@ namespace editor
{
// one piece of a preset's diverging path, in catalogue order from PR. mirrors
// maj0sted::editor::TurnoutPieceSpec so this header stays free of the library
// editor::plan::TurnoutPieceSpec so this header stays free of the library
struct turnout_preset_piece
{
int part; // 0 przediglicowy, 1 iglica, 2 luk, 3 krzyzownicowa

View File

@@ -1,12 +0,0 @@
---
Language: Cpp
BasedOnStyle: Google
Standard: c++20
ColumnLimit: 90
IndentWidth: 4
AccessModifierOffset: -4
PointerAlignment: Left
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: false
SortIncludes: true
IncludeBlocks: Preserve

24
maj0sted/.gitignore vendored
View File

@@ -1,24 +0,0 @@
# Build output
/build/
/out/
/cmake-build-*/
# Compiled objects / archives
*.o
*.obj
*.a
*.lib
*.so
*.dylib
# Editors / IDEs
.vscode/
.idea/
*.swp
.cache/
compile_commands.json
# WASM / web build artifacts (produced by web/build.sh)
web/maj0sted.js
web/maj0sted.wasm
web/serve

View File

@@ -1,101 +0,0 @@
cmake_minimum_required(VERSION 3.20)
project(maj0sted
VERSION 0.1.0
DESCRIPTION "Model and solver for a railway track-layout editor"
LANGUAGES CXX)
# ----------------------------------------------------------------------------
# Options
# ----------------------------------------------------------------------------
option(MAJ0STED_BUILD_TESTS "Build unit tests" ON)
option(MAJ0STED_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF)
# Only drive tests/examples/install defaults when built on its own, not when
# pulled in via add_subdirectory() from a larger project.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(MAJ0STED_STANDALONE ON)
else()
set(MAJ0STED_STANDALONE OFF)
endif()
# ----------------------------------------------------------------------------
# The static library
# ----------------------------------------------------------------------------
add_library(maj0sted STATIC
src/domain/turnout.cpp
src/editor/model.cpp
src/editor/solution.cpp
src/editor/chain.cpp
src/editor/sketch.cpp
src/editor/join.cpp
src/editor/layout.cpp
src/editor/render.cpp
src/editor/tile_cache.cpp
src/render/rail_renderer.cpp
src/io/document_io.cpp
src/io/scn_export.cpp
)
add_library(maj0sted::maj0sted ALIAS maj0sted)
target_compile_features(maj0sted PUBLIC cxx_std_20)
target_include_directories(maj0sted PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
if(MSVC)
target_compile_options(maj0sted PRIVATE /W4)
else()
target_compile_options(maj0sted PRIVATE
-Wall -Wextra -Wpedantic -Wconversion -Wshadow)
endif()
if(MAJ0STED_WARNINGS_AS_ERRORS)
if(MSVC)
target_compile_options(maj0sted PRIVATE /WX)
else()
target_compile_options(maj0sted PRIVATE -Werror)
endif()
endif()
# ----------------------------------------------------------------------------
# Tests
# ----------------------------------------------------------------------------
if(MAJ0STED_BUILD_TESTS AND MAJ0STED_STANDALONE)
enable_testing()
add_subdirectory(tests)
endif()
# ----------------------------------------------------------------------------
# Install / package
# ----------------------------------------------------------------------------
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
install(TARGETS maj0sted
EXPORT maj0stedTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT maj0stedTargets
FILE maj0stedTargets.cmake
NAMESPACE maj0sted::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/maj0sted)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/maj0stedConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/maj0stedConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/maj0sted)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/maj0stedConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/maj0stedConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/maj0stedConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/maj0sted)

View File

@@ -1,184 +0,0 @@
# HANDOFF — maj0sted
Stan prac i punkt zaczepienia dla kolejnej sesji (np. po przejściu na `claude` CLI).
Kod jest samoopisowy; ten plik to skrót "gdzie jesteśmy i co dalej".
## Czym jest projekt
Statyczna biblioteka **C++20** (DDD) do edytora torowiska. Czysty model domenowy,
bez zależności zewnętrznych. Namespace: `maj0sted::domain`.
**Uwaga (sandbox):** na tej maszynie wyłączone są nieuprzywilejowane user
namespaces, więc domyślny sandbox komend pada w preflight (komenda „nie zwraca
statusu”). Uruchamiaj polecenia powłoki **z wyłączonym sandboxem** (w Cursorze:
`required_permissions: ["all"]`). Wtedy `git`, `g++`, `node` działają normalnie.
**Uwaga:** `cmake` NIE jest zainstalowany. Weryfikacja odbywa się przez `g++ 14.2`.
Toolchain WASM: `emsdk` jest w `~/emsdk` (`source ~/emsdk/emsdk_env.sh` dodaje `em++`).
## Jak weryfikować (bez cmake)
```bash
cd /home/maj00r/Projects/maj0sted
FLAGS="-std=c++20 -Iinclude -Wall -Wextra -Wpedantic -Wconversion -Wshadow"
SRC="src/domain/*.cpp src/io/*.cpp src/render/*.cpp src/editor/solve.cpp src/editor/tile_cache.cpp src/app/editor_document.cpp"
for t in map_project niweleta plan_alignment parallelism fitting serialization render web tile_cache document; do
g++ $FLAGS -Itests $SRC tests/test_$t.cpp -o /tmp/t_$t && /tmp/t_$t | tail -1
done
```
Wszystkie testy przechodzą, kompilacja bez ostrzeżeń. Oszczędnie: kompiluj tylko
zmieniony test z minimalnym zestawem źródeł (np. `tile_cache` wymaga tylko
`src/editor/tile_cache.cpp`; `document` — `src/domain/*.cpp src/io/project_serializer.cpp
src/app/editor_document.cpp`). Szybki syntax-check: `g++ $FLAGS -fsyntax-only ...`.
Budowa WASM: `source ~/emsdk/emsdk_env.sh && cd web && ./build.sh`
(produkuje `web/maj0sted.js` + `.wasm`, oba w `.gitignore`).
## Model — co już jest (✓ = zaimplementowane + testy)
### Value Objects (`include/maj0sted/domain/value_objects/`)
- `Crs` — układ EPSG, domyślnie **EPSG:2180** (metry) ✓
- `CartesianPosition` — (x=easting, y=northing), metry, double ✓
- `Length` (≥0), `Radius` (>0) ✓
- `Azimuth` — radiany, **zgodnie z ruchem wskazówek zegara od północy (+y)**, znormalizowany [0,2π) ✓
- `NiweletaId`, `StraightId` — silne tożsamości ✓
- `TrackOffset` — dystans (>0) + `Side::Left/Right` ✓
### Agregaty
- `MapProject` — **projekt**: trzyma `Crs` (domyślnie EPSG:2180) + kolekcję niwelet
(`add_niweleta`, `niwelety()`) ✓
- `Niweleta` (`domain/niweleta.hpp`) — id, nazwa, plan + profil. **Sama nadaje `StraightId`**:
`add_straight(start,end,joint) -> StraightId`; `add_plan_element` mintuje id anonimowej prostej
(i podbija licznik przy id wczytanym z pliku). `lengths_consistent()`.
**`apply_fit(FitResult)`** — wpina wynik fitowania w plan: podmienia sąsiednie proste
(entry, exit) na `entry → curve... → exit` stykami Tangent; proste zachowują id ✓
### Geometria planu (`domain/geometry/`)
- `PlanElement = variant<Straight, CircularArc, TransitionCurve>`
- `Straight` — **zakotwiczony w XY (start/end)**, azymut i długość liczone; opcjonalny `StraightId`.
Zasada: prosta trzyma linię nośną (pozycja+azymut), zmienia się tylko długość; krzywe to wypadkowa.
- `CircularArc` — promień + kierunek + **opcjonalna** długość
- `TransitionCurve` (klotoida) — długość + promienie końcowe (`optional`=∞) + kierunek
- `HorizontalAlignment` — lista kierunkowa + `JointContinuity{Tangent, AzimuthBreak}`. Reguły styku ✓:
- sąsiednie proste Tangent: muszą się stykać i mieć równy azymut
- różny azymut prostych → wymaga `AzimuthBreak` (tylko prosta-prosta, muszą się stykać, różne azymuty)
- sąsiednie łuki o tym samym promieniu i kierunku → zabronione
- styki z krzywą akceptowane "pending fit"
### Geometria profilu
- `ProfileElement = variant<Grade, VerticalCurve>`; `VerticalAlignment`. (Profil na razie nietknięty.)
### Równoległość (`domain/parallelism/`)
- `TrackParallelism { StraightId origin, StraightId parallel, TrackOffset }` — **referencje wprost po StraightId**;
niezmienniki: oba id niepuste, różne ✓
- `ParallelismService`: `mark_parallel(originStraight, parallelStraight, offset)`, `are_parallel`,
`derive_parallel_line`, `perpendicular_distance` ✓
### Fitowanie (`domain/fitting/`)
- `FitParameters { entry, exit, radius, optional transition_length }` — klotoida symetryczna lub sam łuk
- `CompoundFitParameters { entry, exit, radius1, radius2, split_station }` — **łuk koszowy**
(miejsce podziału = kilometraż pierwszego łuku; `δ1 = split_station/R1`)
- `FitResult { entry(przycięta), curve[], exit(przycięta), tangent_in, tangent_out, direction }`
- `FittingService`:
- `fit_between_straights` — klotoida–łuk–klotoida (lub sam łuk), przycina obie proste ✓
- `fit_compound` — dwa łuki różnych promieni; **obie linie prostych bez zmian**, obie proste
doprzycięte na długości; podział ustala `δ1`. Metoda stycznej pośredniej ✓
### Serializacja (`maj0sted::io`, `include/maj0sted/io/project_serializer.hpp`)
- `serialize(MapProject) -> std::string` / `deserialize(std::string) -> MapProject` ✓
- Format tekstowy, liniowy, bez zależności; liczby przez `to_chars`/`from_chars` (dokładny round-trip,
niezależny od locale). Round-trip jest bajtowo stabilny (test to sprawdza).
- Zapisuje: CRS + niwelety (id, nazwa, plan z elementami i stykami, profil). **NIE** zapisuje relacji
równoległości (patrz niżej — StraightId nie są globalnie unikalne).
### Rendering: dwie szyny
`src/editor/editor.cpp` rysuje **dwie szyny w rozstawie 1500 mm** (`kHalfGauge=0.75`,
±750 mm od osi) zamiast samej osi. `offset_points` przesuwa linię osi wzdłuż
normalnej per-wierzchołek (różnica centralna, normalna lewa `(-dy,dx)`),
`push_rails` emituje lewą i prawą szynę zachowując `kind` (kolor). Uchwyty edycji
(węzły/końce prostych) w JS dalej rysują się z osi (model).
### Warstwa web (WASM) — biblioteka robi wszystko, JS tylko rysuje
Zasada: **cała logika jest w libce**; `web/` to cienki renderer + przekazywanie
wejścia. `web/maj0sted_bindings.cpp` (embind) to jedyny plik zależny od emscripten.
- **Kafle podkładu WMS** (`maj0sted::editor`, `web/tile_cache.hpp/.cpp`) ✓
- `TileGrid` — **jeden stały grid kafli 100 m × 100 m** w **EPSG:2180**
(`kCellMetres=100`, **1024 px/kafel** → 100/1024 m/px, origin (0,0), jeden
poziom `kLevel=0`, **bez piramidy per-zoom**); `tile_at`, `bbox`,
`tiles_for_view`, `zoom_for_resolution` (argument zoom ignorowany, sygnatury
zachowane dla boundary). Czysta, testowana natywnie.
- **Retry:** nieudany fetch jest ponawiany **maks. `kMaxRetries=10` razy na
kafel** (licznik `TileImage.attempts`); po przekroczeniu limitu kafel dostaje
status `Failed`. Pod Emscripten retry jest odroczony async
(`emscripten_async_call`, `kRetryDelayMs=1500`), bez blokowania/rekurencji;
z wstrzykniętym fetcherem (testy) powtarza od razu.
- `WmsConfig` + `wms_getmap_url` — WMS **1.1.1** GetMap (BBOX = minx,miny,maxx,maxy),
presety Geoportalu (ortofoto/topo). Liczby przez `to_chars`.
- `TileService` — cache + **fetch**: na trafienie zwraca z cache, na pudło sam
wykonuje GetMap. Pod `__EMSCRIPTEN__` używa **Emscripten Fetch** (`-sFETCH`);
natywnie/w testach przez wstrzykiwany `Fetcher` (bez sieci). Callback
`on_ready` budzi przerysowanie w JS.
- **Proxy WMS (`web/serve.py`):** przeglądarka pobiera kafle **same-origin**
(`/wms?...`), a lokalny serwer pobiera je serwer-serwer z Geoportalu. Powód:
WAF Geoportalu **zwraca 404 dla User-Agentów zawierających „Mozilla"** (czyli
dla prawdziwych przeglądarek), a przepuszcza proste UA (`curl`), więc bezpośredni
`fetch`/`emscripten_fetch` z przeglądarki dostaje 404. Proxy używa UA `curl/8.5.0`.
Frontend ustawia `Module.wmsSetConfig({base_url: origin+'/wms'})` w `finishBoot`.
Libka nadal robi fetch (`emscripten_fetch`) — tylko na `localhost`.
Uruchamianie: `python3 web/serve.py 8080` (zamiast `http.server`).
- **Cache na dysku:** proxy odsyła udane kafle z `Cache-Control: public,
max-age=31536000, immutable`, więc przeglądarka trzyma je w HTTP cache na dysku —
po odświeżeniu strony kafle są brane z cache, bez ponownego pobierania. Błędy
(`502`) idą z `no-store` (nie cache'ują się). Uwaga: cache `TileService` w C++ jest
tylko w pamięci WASM (ginie przy refreshu) — trwałość zapewnia właśnie HTTP cache.
- **Retry w proxy:** WAF Geoportalu bywa niestabilny (sporadyczne 404/reset nawet dla
UA `curl`), więc proxy ponawia pobranie `WMS_PROXY_RETRIES=6` razy zanim zwróci 502;
do tego dochodzi client-side retry libki (10×).
- **Trwałość projektu** (`maj0sted::app`, `app/editor_document.hpp/.cpp`) ✓
- `EditorDocument` = model GUI (niezależne proste + parametry fitów) — bezstratny.
- `serialize_document`/`deserialize_document` — **reużywa `io::serialize`** (osadza
prawdziwy projekt maj0sted) + sekcja `editor` z prostymi i parametrami fitów.
- `to_map_project`/`from_map_project` — konwersja do/z `MapProject` (best-effort,
nie rzuca).
- `save_project`/`load_project` — **I/O plikowe w libce** (`std::fstream` +
`std::filesystem`), domyślna ścieżka `/data/maj0sted/project.m0s`.
- **Fit nieudany → „brak" (decyzja w libce):** `solve_project` (editor) zwraca per
niweleta `polylines` **oraz `applied_fits`** — autorytatywną listę fitów, które
faktycznie zaszły. Fit, którego nie da się policzyć (zły parametr/geometria, punkty
styczne poza odcinkami prostych), jest **odrzucany w libce** i nie ma go w `applied_fits`.
Frontend tylko lustrzanie przyjmuje tę listę (bez własnej reguły o porażce).
- **Boundary (embind, cienki):** `solveProject`, `wmsTilesForView`,
`wmsZoomForResolution`, `wmsTileBytes`, `wmsSetOnReady`, `wmsSetConfig`,
`saveProject`, `loadDefaultProject`.
- **Frontend (`web/index.html`):** rysuje podkład WMS pod geometrią (georeferencja
EPSG:2180, trzyma się przy pan/zoom); `Ctrl+S` → `saveProject` + `FS.syncfs`;
na starcie montuje **IDBFS** w `/data`, `syncfs(true)` i `loadDefaultProject`
(fallback: demo). Flow budowy: prosta → krzywa (czeka jako „pending”, rysowana
przerywaną) → kolejna prosta kotwiczy ją z obu stron i **dopiero wtedy** liczony
jest fit.
## Konwencje geometryczne
- EPSG:2180, jednostka = metr; kierunek jazdy `d = (sin az, cos az)`; normalna lewa `(-cos az, sin az)`.
- Tolerancje: pozycja `1e-6 m`, azymut `1e-9 rad`.
## Znane ograniczenia
- **`StraightId` są lokalne per-niweleta** (licznik startuje od 1 w każdej niwelecie), więc NIE są
globalnie unikalne. `TrackParallelism` referuje proste po samym `StraightId` — działa dla luźnych
obiektów, ale w projekcie z wieloma niweletami trzeba by referować `(NiweletaId, StraightId)`.
Do przemyślenia, jeśli równoległość ma być trwała/serializowana.
## Następne kroki (do decyzji / do zrobienia)
1. **Klotoidy w łuku koszowym** (między łukami i/lub na końcach). (NIE zrobione.)
2. Asymetryczne klotoidy w `fit_between_straights` (teraz tylko symetryczne). (NIE zrobione.)
3. **Rejestr relacji równoległości** + poprawka referencji na `(NiweletaId, StraightId)` i ich serializacja. (NIE.)
4. **Solver** ustawiający prostą równoległą na `derive_parallel_line` i przeliczający resztę. (NIE.)
5. Instalacja `cmake`/`ninja` i budowa przez CMake (na razie tylko `g++`).
## Preferencje pracy (ustalone z użytkownikiem)
- **Nie edytować `README.md`** (utrzymuje go użytkownik).
- Robić **tylko to, o co proszono** — bez dokładania funkcji z własnej inicjatywy.
- **Oszczędzać CPU**: kompilować tylko zmieniony test, `-fsyntax-only` do szybkiej walidacji,
pełne testy na koniec/na żądanie; bez procesów w tle, przeglądarki i podagentów.
- Język domeny: **niweleta** = jednolity odcinek toru złożony z wielu krzywych w planie i profilu.
## Git
Repozytorium zainicjalizowane; pliki dodane do stage'a, **brak commitów** (commit dopiero na wyraźną prośbę).

View File

@@ -1,86 +0,0 @@
# maj0sted
Statyczna biblioteka **C++20** do obsługi **edytora torowiska**, zbudowana
zgodnie z **Domain-Driven Design (DDD)**.
Biblioteka dostarcza czysty model domenowy — bez zależności od UI, bazy danych
czy frameworka. Warstwy aplikacji i infrastruktury podpina się do zdefiniowanych
tu typów.
## Model domenowy
```
maj0sted::domain
├── value_objects/ Value Objects (niezmienne, porównywane przez wartość)
│ ├── Crs układ odniesienia (kod EPSG), domyślnie EPSG:2180
│ └── CartesianPosition pozycja kartezjańska (x, y w jednostkach CRS)
└── MapProject projekt mapy — definiuje CRS dla wszystkich pozycji
```
Projekt mapy (`MapProject`) definiuje kartezjański układ odniesienia, w którym
wyrażane są wszystkie pozycje. Domyślnie jest to **EPSG:2180** (PUWG 1992 /
Poland CS92), którego jednostką jest metr.
## Szybki start
```cpp
#include "maj0sted/maj0sted.hpp"
using namespace maj0sted::domain;
MapProject project; // domyślnie EPSG:2180
int epsg = project.crs().epsg(); // 2180
CartesianPosition p{500000.0, 300000.0}; // x, y w metrach
```
Pełniejszy przykład: [`examples/basic_editor.cpp`](examples/basic_editor.cpp).
## Budowanie
Wymagania: kompilator C++20 (GCC 11+/Clang 14+/MSVC 19.3+) oraz CMake ≥ 3.20.
```bash
cmake -S . -B build # dodaj -G Ninja, jeśli masz ninja
cmake --build build
ctest --test-dir build --output-on-failure
./build/examples/maj0sted_example
```
### Opcje CMake
| Opcja | Domyślnie | Znaczenie |
|------------------------------|-----------|-------------------------------|
| `MAJ0STED_BUILD_TESTS` | `ON` | buduj testy jednostkowe |
| `MAJ0STED_BUILD_EXAMPLES` | `ON` | buduj przykłady |
| `MAJ0STED_WARNINGS_AS_ERRORS`| `OFF` | traktuj ostrzeżenia jak błędy |
### Użycie w innym projekcie
Po `cmake --install build` (albo przez `add_subdirectory`):
```cmake
find_package(maj0sted REQUIRED)
target_link_libraries(twoj_target PRIVATE maj0sted::maj0sted)
```
## Struktura projektu
```
maj0sted/
├── CMakeLists.txt
├── include/maj0sted/ nagłówki publiczne (API)
├── src/domain/ implementacje (.cpp)
├── tests/ testy jednostkowe (CTest, bez zależności)
├── examples/ przykłady użycia
└── cmake/ pliki pakietu find_package
```
## Testy
Testy używają własnego, malutkiego frameworka (`tests/check.hpp`) — nie wymagają
żadnych zewnętrznych bibliotek. Każdy plik testowy to osobny plik wykonywalny
zwracający kod wyjścia dla CTest.
## Licencja
Do uzupełnienia (np. MIT) — dodaj plik `LICENSE`.

View File

@@ -1,5 +0,0 @@
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/maj0stedTargets.cmake")
check_required_components(maj0sted)

View File

@@ -1,27 +0,0 @@
#pragma once
// maj0sted — the model behind a railway track-layout editor ("edytor
// torowiska"). One header pulls in the whole public surface.
//
// The shape of it: a Document is authored (construction lines, catalogue
// turnout types and their placements, tracks made of elements), `solve` turns it
// into a Solution, and nothing ever travels back the other way.
#include "maj0sted/version.hpp"
// The authored model, and what solving it produces.
#include "maj0sted/editor/model.hpp"
#include "maj0sted/editor/solution.hpp"
// Solving: one track's chain, and a whole document in dependency order.
#include "maj0sted/editor/chain.hpp"
#include "maj0sted/editor/layout.hpp"
// Geometry primitives shared by the solver and the renderer.
#include "maj0sted/domain/geometry/segment_layout.hpp"
#include "maj0sted/domain/geometry/turnout.hpp"
// Presentation and IO.
#include "maj0sted/editor/render.hpp"
#include "maj0sted/io/document_io.hpp"
#include "maj0sted/io/scn_export.hpp"

View File

@@ -1,14 +0,0 @@
#pragma once
namespace maj0sted::render {
/// A point in the project's CRS (metres for EPSG:2180).
struct Point {
double x;
double y;
};
/// Which kind of plan element a polyline came from (lets a GUI colour it).
enum class ElementKind { Straight, Arc, Transition };
} // namespace maj0sted::render

View File

@@ -1,7 +0,0 @@
#pragma once
// Version of the maj0sted library. Keep in sync with project() in CMakeLists.txt.
#define MAJ0STED_VERSION_MAJOR 0
#define MAJ0STED_VERSION_MINOR 1
#define MAJ0STED_VERSION_PATCH 0
#define MAJ0STED_VERSION_STRING "0.1.0"

View File

@@ -1,45 +0,0 @@
add_executable(maj0sted_test_chain test_chain.cpp)
target_link_libraries(maj0sted_test_chain PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_chain PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME chain COMMAND maj0sted_test_chain)
add_executable(maj0sted_test_hold test_hold.cpp)
target_link_libraries(maj0sted_test_hold PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_hold PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME hold COMMAND maj0sted_test_hold)
add_executable(maj0sted_test_lines test_lines.cpp)
target_link_libraries(maj0sted_test_lines PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_lines PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME lines COMMAND maj0sted_test_lines)
add_executable(maj0sted_test_graph test_graph.cpp)
target_link_libraries(maj0sted_test_graph PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_graph PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME graph COMMAND maj0sted_test_graph)
add_executable(maj0sted_test_format test_format.cpp)
target_link_libraries(maj0sted_test_format PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_format PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME format COMMAND maj0sted_test_format)
add_executable(maj0sted_test_turnout test_turnout.cpp)
target_link_libraries(maj0sted_test_turnout PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_turnout PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME turnout COMMAND maj0sted_test_turnout)
add_executable(maj0sted_test_scn test_scn.cpp)
target_link_libraries(maj0sted_test_scn PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_scn PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME scn COMMAND maj0sted_test_scn)
add_executable(maj0sted_test_join test_join.cpp)
target_link_libraries(maj0sted_test_join PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_join PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME join COMMAND maj0sted_test_join)
add_executable(maj0sted_test_tile_cache test_tile_cache.cpp)
target_link_libraries(maj0sted_test_tile_cache PRIVATE maj0sted::maj0sted)
target_include_directories(maj0sted_test_tile_cache PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME tile_cache COMMAND maj0sted_test_tile_cache)