mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-03 17:50:21 +01:00
Vendor the maj0sted track layout library and link it into the editor
Drop-in copy of C:\Users\maj00r\Projects\maj0sted-master minus its web/ directory - the WASM frontend is not being ported, the editor drives the same C++ API natively. What is left is plain C++20 with no dependencies: domain model (niweleta, plan and profile geometry, fitting, parallelism), project serialization, rail sampling and the editor document. src/web/ribbon.cpp was missing from the library source list; added, since without it the ribbon symbols never make it into the archive.
This commit is contained in:
323
maj0sted/src/app/editor_document.cpp
Normal file
323
maj0sted/src/app/editor_document.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "maj0sted/app/editor_document.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/io/project_serializer.hpp"
|
||||
#include "maj0sted/maj0sted.hpp"
|
||||
|
||||
namespace maj0sted::app {
|
||||
|
||||
using namespace maj0sted::domain;
|
||||
using maj0sted::web::GapFit;
|
||||
using maj0sted::web::NiweletaSpec;
|
||||
using maj0sted::web::StraightSpec;
|
||||
|
||||
namespace {
|
||||
|
||||
// Shortest round-trippable, locale-independent double — matches maj0sted::io.
|
||||
std::string num(double value) {
|
||||
char buffer[64];
|
||||
const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
|
||||
return std::string(buffer, result.ptr);
|
||||
}
|
||||
|
||||
std::vector<std::string> tokenize(const std::string& line) {
|
||||
std::vector<std::string> tokens;
|
||||
std::size_t i = 0;
|
||||
const std::size_t n = line.size();
|
||||
while (i < n) {
|
||||
while (i < n && std::isspace(static_cast<unsigned char>(line[i]))) ++i;
|
||||
std::size_t j = i;
|
||||
while (j < n && !std::isspace(static_cast<unsigned char>(line[j]))) ++j;
|
||||
if (j > i) tokens.push_back(line.substr(i, j - i));
|
||||
i = j;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
double to_double(const std::string& text) {
|
||||
double value{};
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (result.ec != std::errc{}) {
|
||||
throw std::runtime_error{"maj0sted: bad number '" + text + "'"};
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
long to_long(const std::string& text) {
|
||||
long value{};
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (result.ec != std::errc{}) {
|
||||
throw std::runtime_error{"maj0sted: bad integer '" + text + "'"};
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Placeholder turn direction for the resolved-domain view: derived from the two
|
||||
// straights' headings (sign of the cross product).
|
||||
TurnDirection gap_direction(const StraightSpec& a, const StraightSpec& b) noexcept {
|
||||
const double ax = a.x2 - a.x1, ay = a.y2 - a.y1;
|
||||
const double bx = b.x2 - b.x1, by = b.y2 - b.y1;
|
||||
return (ax * by - ay * bx) >= 0.0 ? TurnDirection::Left : TurnDirection::Right;
|
||||
}
|
||||
|
||||
double gap_radius(const GapFit* fit) noexcept {
|
||||
if (fit != nullptr) {
|
||||
if (fit->radius > 0.0) return fit->radius;
|
||||
if (!fit->arcs.empty() && fit->arcs.front().radius > 0.0) {
|
||||
return fit->arcs.front().radius;
|
||||
}
|
||||
if (fit->r1 > 0.0) return fit->r1;
|
||||
}
|
||||
return 300.0; // benign placeholder; arcs carry no length in this view
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MapProject to_map_project(const EditorDocument& document) {
|
||||
MapProject project{Crs::default_crs()};
|
||||
|
||||
for (std::size_t index = 0; index < document.niwelety.size(); ++index) {
|
||||
const NiweletaSpec& spec = document.niwelety[index];
|
||||
try {
|
||||
Niweleta niweleta{NiweletaId{static_cast<std::uint64_t>(index + 1)},
|
||||
spec.name};
|
||||
for (std::size_t i = 0; i < spec.straights.size(); ++i) {
|
||||
const StraightSpec& s = spec.straights[i];
|
||||
if (i > 0) {
|
||||
// Keep two anchored straights from being adjacent (which the
|
||||
// alignment forbids) by threading a floating placeholder arc.
|
||||
const GapFit* fit = nullptr;
|
||||
for (const auto& f : spec.fits) {
|
||||
if (static_cast<std::size_t>(f.gap) == i - 1 && f.mode > 0) {
|
||||
fit = &f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
niweleta.add_plan_element(
|
||||
CircularArc{Radius::from_metres(gap_radius(fit)),
|
||||
gap_direction(spec.straights[i - 1], s)});
|
||||
}
|
||||
niweleta.add_plan_element(
|
||||
Straight{CartesianPosition{s.x1, s.y1}, CartesianPosition{s.x2, s.y2}});
|
||||
}
|
||||
project.add_niweleta(std::move(niweleta));
|
||||
} catch (...) {
|
||||
// Degenerate/inconsistent niweleta: keep the (named) shell so the
|
||||
// project still lists it; the editor section restores the details.
|
||||
project.add_niweleta(Niweleta{
|
||||
NiweletaId{static_cast<std::uint64_t>(index + 1)}, spec.name});
|
||||
}
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
EditorDocument from_map_project(const MapProject& project) {
|
||||
EditorDocument document;
|
||||
for (const auto& niweleta : project.niwelety()) {
|
||||
NiweletaSpec spec;
|
||||
spec.name = niweleta.name();
|
||||
for (const auto& element : niweleta.plan().elements()) {
|
||||
if (const auto* straight = std::get_if<Straight>(&element)) {
|
||||
spec.straights.push_back(StraightSpec{
|
||||
straight->start().x(), straight->start().y(),
|
||||
straight->end().x(), straight->end().y()});
|
||||
}
|
||||
}
|
||||
document.niwelety.push_back(std::move(spec));
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
std::string serialize_document(const EditorDocument& document) {
|
||||
// Reuse the domain serializer for a real, inspectable maj0sted project...
|
||||
std::string out = maj0sted::io::serialize(to_map_project(document));
|
||||
|
||||
// ...then the lossless editor section (straights + fit parameters).
|
||||
out += "editor " + std::to_string(document.niwelety.size()) + "\n";
|
||||
for (const auto& spec : document.niwelety) {
|
||||
out += "eniw " + std::to_string(spec.straights.size()) + " " +
|
||||
std::to_string(spec.fits.size()) + "\n";
|
||||
out += "ename " + spec.name + "\n";
|
||||
for (const auto& s : spec.straights) {
|
||||
out += "estr " + num(s.x1) + " " + num(s.y1) + " " + num(s.x2) + " " +
|
||||
num(s.y2) + " " + (s.hidden ? "1" : "0") + "\n";
|
||||
}
|
||||
for (const auto& f : spec.fits) {
|
||||
// efit2: gap mode radius transition entry_t exit_t <n> [r len trans]*
|
||||
// The variable arc list supersedes the legacy fixed two-arc 'efit'.
|
||||
out += "efit2 " + std::to_string(f.gap) + " " +
|
||||
std::to_string(f.mode) + " " + num(f.radius) + " " +
|
||||
num(f.transition) + " " + num(f.entry_t) + " " + num(f.exit_t) +
|
||||
" " + std::to_string(f.arcs.size());
|
||||
for (const auto& a : f.arcs) {
|
||||
out += " " + num(a.radius) + " " + num(a.length) + " " +
|
||||
num(a.transition_to_next);
|
||||
}
|
||||
out += "\n";
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
EditorDocument deserialize_document(const std::string& text) {
|
||||
std::vector<std::string> lines;
|
||||
{
|
||||
std::istringstream in(text);
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
lines.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Locate the editor section (the domain block before it is interop-only).
|
||||
std::size_t cursor = lines.size();
|
||||
for (std::size_t i = 0; i < lines.size(); ++i) {
|
||||
const auto tokens = tokenize(lines[i]);
|
||||
if (!tokens.empty() && tokens[0] == "editor") {
|
||||
cursor = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cursor >= lines.size()) {
|
||||
throw std::runtime_error{"maj0sted: missing 'editor' section"};
|
||||
}
|
||||
|
||||
const auto next = [&]() -> const std::string& {
|
||||
while (cursor < lines.size()) {
|
||||
const std::string& l = lines[cursor++];
|
||||
bool only_ws = true;
|
||||
for (const char c : l) {
|
||||
if (!std::isspace(static_cast<unsigned char>(c))) {
|
||||
only_ws = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!only_ws) return l;
|
||||
}
|
||||
throw std::runtime_error{"maj0sted: unexpected end of editor section"};
|
||||
};
|
||||
|
||||
EditorDocument document;
|
||||
const auto header = tokenize(next());
|
||||
if (header.size() < 2 || header[0] != "editor") {
|
||||
throw std::runtime_error{"maj0sted: expected 'editor <count>'"};
|
||||
}
|
||||
const long niweleta_count = to_long(header[1]);
|
||||
for (long n = 0; n < niweleta_count; ++n) {
|
||||
const auto eniw = tokenize(next());
|
||||
if (eniw.size() < 3 || eniw[0] != "eniw") {
|
||||
throw std::runtime_error{"maj0sted: expected 'eniw <straights> <fits>'"};
|
||||
}
|
||||
const long straight_count = to_long(eniw[1]);
|
||||
const long fit_count = to_long(eniw[2]);
|
||||
|
||||
NiweletaSpec spec;
|
||||
const std::string& name_line = next();
|
||||
if (name_line.rfind("ename", 0) != 0) {
|
||||
throw std::runtime_error{"maj0sted: expected 'ename'"};
|
||||
}
|
||||
spec.name = name_line.size() > 6 ? name_line.substr(6) : std::string{};
|
||||
|
||||
for (long i = 0; i < straight_count; ++i) {
|
||||
const auto e = tokenize(next());
|
||||
if (e.size() < 5 || e[0] != "estr") {
|
||||
throw std::runtime_error{"maj0sted: bad 'estr' line"};
|
||||
}
|
||||
spec.straights.push_back(
|
||||
StraightSpec{to_double(e[1]), to_double(e[2]), to_double(e[3]),
|
||||
to_double(e[4]), e.size() >= 6 && to_long(e[5]) != 0});
|
||||
}
|
||||
for (long i = 0; i < fit_count; ++i) {
|
||||
const auto e = tokenize(next());
|
||||
GapFit f;
|
||||
if (!e.empty() && e[0] == "efit2") {
|
||||
// New variable-arc format.
|
||||
if (e.size() < 8) {
|
||||
throw std::runtime_error{"maj0sted: bad 'efit2' line"};
|
||||
}
|
||||
f.gap = static_cast<int>(to_long(e[1]));
|
||||
f.mode = static_cast<int>(to_long(e[2]));
|
||||
f.radius = to_double(e[3]);
|
||||
f.transition = to_double(e[4]);
|
||||
f.entry_t = to_double(e[5]);
|
||||
f.exit_t = to_double(e[6]);
|
||||
const long arc_count = to_long(e[7]);
|
||||
for (long a = 0; a < arc_count; ++a) {
|
||||
const std::size_t base = 8 + static_cast<std::size_t>(a) * 3;
|
||||
if (base + 2 >= e.size()) {
|
||||
throw std::runtime_error{"maj0sted: truncated 'efit2' arcs"};
|
||||
}
|
||||
f.arcs.push_back(maj0sted::web::CompoundArcSpec{
|
||||
to_double(e[base]), to_double(e[base + 1]),
|
||||
to_double(e[base + 2])});
|
||||
}
|
||||
} else if (!e.empty() && e[0] == "efit" && e.size() >= 11) {
|
||||
// Legacy fixed two-arc format; arcs stay empty and to_request
|
||||
// falls back to the r1/r2 scalar fields.
|
||||
f.gap = static_cast<int>(to_long(e[1]));
|
||||
f.mode = static_cast<int>(to_long(e[2]));
|
||||
f.radius = to_double(e[3]);
|
||||
f.transition = to_double(e[4]);
|
||||
f.r1 = to_double(e[5]);
|
||||
f.arc1_len = to_double(e[6]);
|
||||
f.between = to_double(e[7]);
|
||||
f.r2 = to_double(e[8]);
|
||||
f.entry_t = to_double(e[9]);
|
||||
f.exit_t = to_double(e[10]);
|
||||
} else {
|
||||
throw std::runtime_error{"maj0sted: bad 'efit'/'efit2' line"};
|
||||
}
|
||||
spec.fits.push_back(f);
|
||||
}
|
||||
document.niwelety.push_back(std::move(spec));
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
std::string default_project_path() {
|
||||
// Under Emscripten the host mounts IDBFS at /data (see web/index.html), so
|
||||
// this path is persistent across reloads once FS.syncfs has flushed.
|
||||
return "/data/maj0sted/project.m0s";
|
||||
}
|
||||
|
||||
bool save_project(const EditorDocument& document, const std::string& path) {
|
||||
try {
|
||||
const std::filesystem::path file{path};
|
||||
if (file.has_parent_path()) {
|
||||
std::filesystem::create_directories(file.parent_path());
|
||||
}
|
||||
std::ofstream out(file, std::ios::binary | std::ios::trunc);
|
||||
if (!out) return false;
|
||||
out << serialize_document(document);
|
||||
return static_cast<bool>(out);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EditorDocument> load_project(const std::string& path) {
|
||||
try {
|
||||
const std::filesystem::path file{path};
|
||||
if (!std::filesystem::exists(file)) return std::nullopt;
|
||||
std::ifstream in(file, std::ios::binary);
|
||||
if (!in) return std::nullopt;
|
||||
std::ostringstream buffer;
|
||||
buffer << in.rdbuf();
|
||||
return deserialize_document(buffer.str());
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace maj0sted::app
|
||||
Reference in New Issue
Block a user