mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 21:29:19 +02:00
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.
716 lines
32 KiB
C++
716 lines
32 KiB
C++
/*
|
|
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>
|
|
#include <cstdio>
|
|
#include <numbers>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
module eu07.editor.plan_layout;
|
|
import eu07.editor.plan_turnout_geometry;
|
|
import eu07.editor.plan_chain;
|
|
import eu07.editor.plan_sketch;
|
|
|
|
namespace editor::plan {
|
|
namespace {
|
|
|
|
constexpr double kEps = 1e-9;
|
|
|
|
using geometry::Pose;
|
|
|
|
std::string num(double value, int decimals = 1) {
|
|
char buffer[64];
|
|
std::snprintf(buffer, sizeof(buffer), "%.*f", decimals, value);
|
|
return buffer;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The dependency graph
|
|
// ---------------------------------------------------------------------------
|
|
|
|
enum class NodeKind { Turnout, Track };
|
|
|
|
struct Node {
|
|
NodeKind kind{NodeKind::Turnout};
|
|
std::size_t index{0};
|
|
std::vector<std::size_t> depends_on;
|
|
};
|
|
|
|
/// Everything a track needs laid before it can be laid itself: the turnout a
|
|
/// branch hangs off, and every track it holds an element parallel to — however
|
|
/// deep either goes. A hold that runs in a circle is caught as a cycle, the same
|
|
/// as anything else.
|
|
void track_dependencies(const Document& document, const Track& track,
|
|
const std::vector<std::size_t>& turnout_node,
|
|
const std::vector<std::size_t>& track_node, Node& node) {
|
|
if (const auto* at_port = std::get_if<AtPort>(&track.anchor)) {
|
|
for (std::size_t i = 0; i < document.turnouts.size(); ++i) {
|
|
if (document.turnouts[i].id == at_port->turnout) {
|
|
node.depends_on.push_back(turnout_node[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
for (const auto& element : track.elements) {
|
|
const auto* parallel = std::get_if<Parallel>(&element.hold);
|
|
if (parallel == nullptr) {
|
|
continue;
|
|
}
|
|
for (std::size_t i = 0; i < document.tracks.size(); ++i) {
|
|
if (document.tracks[i].id == track.id) {
|
|
continue;
|
|
}
|
|
for (const auto& candidate : document.tracks[i].elements) {
|
|
if (candidate.id == parallel->ref) {
|
|
node.depends_on.push_back(track_node[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Holds: parallelism between elements
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Where a held element has to be, and what shape it has to be, to lie at the
|
|
/// offset asked for. Everything is worked out from the reference as solved, so
|
|
/// this is geometry and not a guess.
|
|
struct Held {
|
|
bool ok{false};
|
|
Kind kind{Kind::Line};
|
|
double radius{0.0};
|
|
int hand{0};
|
|
Pose target{}; ///< the pose the held element must start on
|
|
double actual{0.0}; ///< the offset that came out, for a track that cannot move
|
|
std::string why; ///< why not, when !ok
|
|
};
|
|
|
|
/// The offset curve of @p reference at @p offset, taken where @p current is now.
|
|
///
|
|
/// Across the reference the answer is forced; along it, nothing is: the point
|
|
/// nearest to where the element already sits is the one taken, so holding a
|
|
/// track parallel never slides it sideways along the reference.
|
|
Held hold_against(const SolvedElement& reference, double offset, const Pose& current) {
|
|
Held held;
|
|
|
|
if (std::abs(reference.k1 - reference.k0) > kEps) {
|
|
held.why =
|
|
"równoległości nie da się trzymać do krzywej przejściowej — odsunięcie "
|
|
"krzywej przejściowej nie jest krzywą przejściową";
|
|
return held;
|
|
}
|
|
|
|
const Pose& base = reference.start;
|
|
// Which way this element runs along the reference. The offset is always
|
|
// measured to the left of the *reference's* travel, whichever way this one
|
|
// goes, so a track laid against the run of another still sits where it looks.
|
|
const double sense = (current.hx * base.hx + current.hy * base.hy) >= 0.0 ? 1.0 : -1.0;
|
|
const double nx = -base.hy;
|
|
const double ny = base.hx;
|
|
const double k = reference.k0;
|
|
|
|
if (std::abs(k) < kEps) {
|
|
const double px = base.x + nx * offset;
|
|
const double py = base.y + ny * offset;
|
|
const double hx = sense * base.hx;
|
|
const double hy = sense * base.hy;
|
|
const double along = (current.x - px) * hx + (current.y - py) * hy;
|
|
held.ok = true;
|
|
held.kind = Kind::Line;
|
|
held.target = Pose{px + hx * along, py + hy * along, hx, hy};
|
|
held.actual = (current.x - base.x) * nx + (current.y - base.y) * ny;
|
|
return held;
|
|
}
|
|
|
|
// Concentric with the reference: same centre, radius R∓d.
|
|
const double radius = 1.0 / std::abs(k);
|
|
const double cx = base.x + nx / k;
|
|
const double cy = base.y + ny / k;
|
|
const double curvature = k / (1.0 - offset * k);
|
|
if (!std::isfinite(curvature) || std::abs(1.0 - offset * k) < kEps) {
|
|
held.why = "odstęp " + num(offset, 2) + " m trafia w środek łuku R=" +
|
|
num(radius) + " m";
|
|
return held;
|
|
}
|
|
|
|
double ux = current.x - cx;
|
|
double uy = current.y - cy;
|
|
const double reach = std::hypot(ux, uy);
|
|
if (reach < kEps) {
|
|
held.why = "odcinek zaczyna się w środku łuku, do którego ma trzymać "
|
|
"równoległość";
|
|
return held;
|
|
}
|
|
ux /= reach;
|
|
uy /= reach;
|
|
|
|
const double held_radius = 1.0 / std::abs(curvature);
|
|
const double turn = curvature > 0.0 ? 1.0 : -1.0;
|
|
// The centre lies to the left of travel for a left-hand curve, so the
|
|
// heading follows from where the point sits on the circle.
|
|
const double hx = sense * turn * uy;
|
|
const double hy = sense * turn * -ux;
|
|
|
|
held.ok = true;
|
|
held.kind = Kind::Arc;
|
|
held.radius = held_radius;
|
|
held.hand = (sense * turn) > 0.0 ? 1 : -1;
|
|
held.target = Pose{cx + ux * held_radius, cy + uy * held_radius, hx, hy};
|
|
// What offset this element actually keeps, as the drawing stands.
|
|
held.actual = (k > 0.0 ? 1.0 : -1.0) * (radius - reach);
|
|
return held;
|
|
}
|
|
|
|
/// Moves a whole chain, as one rigid piece, so that the pose @p from becomes the
|
|
/// pose @p to. Returns the anchor pose to lay it from again.
|
|
Pose rigid_move(const Pose& anchor, const Pose& from, const Pose& to) {
|
|
const double cos_t = from.hx * to.hx + from.hy * to.hy;
|
|
const double sin_t = from.hx * to.hy - from.hy * to.hx;
|
|
const double dx = anchor.x - from.x;
|
|
const double dy = anchor.y - from.y;
|
|
return Pose{to.x + dx * cos_t - dy * sin_t, to.y + dx * sin_t + dy * cos_t,
|
|
anchor.hx * cos_t - anchor.hy * sin_t,
|
|
anchor.hx * sin_t + anchor.hy * cos_t};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Turnouts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
plan::TurnoutPart part_of(int part) {
|
|
switch (part) {
|
|
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(plan::TurnoutPart part) {
|
|
switch (part) {
|
|
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;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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,
|
|
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(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 plan::TurnoutGeometry& geometry,
|
|
const geometry::Pose& start) {
|
|
std::vector<TurnoutMark> marks;
|
|
if (!geometry.valid) {
|
|
return marks;
|
|
}
|
|
|
|
// PR stands whatever the path starts with, so it is said once here
|
|
marks.push_back(TurnoutMark{start.x, start.y, 0, 0.0});
|
|
|
|
geometry::Pose walker = start;
|
|
double station = 0.0;
|
|
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 = 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 == plan::TurnoutPart::PreBlade)) {
|
|
marks.push_back(
|
|
TurnoutMark{walker.x, walker.y, mark_kind_for(segment.part), station});
|
|
}
|
|
previous = segment.part;
|
|
first = false;
|
|
|
|
walker = geometry::layout_segment(segment.k0, segment.k1, segment.length,
|
|
walker, nullptr);
|
|
station += segment.length;
|
|
}
|
|
|
|
marks.push_back(TurnoutMark{geometry.frog.x, geometry.frog.y, 5, station});
|
|
marks.push_back(TurnoutMark{start.x + start.hx * geometry.tangent_front,
|
|
start.y + start.hy * geometry.tangent_front, 6, 0.0});
|
|
// PR is on the through track already; this is where the switch ends on it, so the
|
|
// straight carries both ends of the rozjazd
|
|
marks.push_back(TurnoutMark{geometry.through_end.x, geometry.through_end.y, 9,
|
|
geometry.through_length});
|
|
// the ostrze itself is marked with the blade piece; what the drawing dimensions on
|
|
// top of it is where the planed nose ends (A, the rail u thick) and where the
|
|
// planing does
|
|
if (geometry.has_blade && geometry.nose_end > geometry.blade_station) {
|
|
marks.push_back(TurnoutMark{geometry.blade_nose_end.x, geometry.blade_nose_end.y,
|
|
7, geometry.nose_end});
|
|
}
|
|
if (geometry.has_blade && geometry.planing_length > 0.0) {
|
|
marks.push_back(TurnoutMark{geometry.blade_planed.x, geometry.blade_planed.y, 8,
|
|
geometry.planing_end});
|
|
}
|
|
return marks;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Solution solve(const Document& document) {
|
|
Solution solution;
|
|
|
|
const std::size_t turnout_count = document.turnouts.size();
|
|
const std::size_t track_count = document.tracks.size();
|
|
|
|
std::vector<Node> nodes;
|
|
nodes.reserve(turnout_count + track_count);
|
|
std::vector<std::size_t> turnout_node(turnout_count, 0);
|
|
std::vector<std::size_t> track_node(track_count, 0);
|
|
|
|
for (std::size_t i = 0; i < turnout_count; ++i) {
|
|
turnout_node[i] = nodes.size();
|
|
nodes.push_back(Node{NodeKind::Turnout, i, {}});
|
|
}
|
|
for (std::size_t i = 0; i < track_count; ++i) {
|
|
track_node[i] = nodes.size();
|
|
nodes.push_back(Node{NodeKind::Track, i, {}});
|
|
}
|
|
|
|
// Edges.
|
|
for (std::size_t i = 0; i < turnout_count; ++i) {
|
|
for (std::size_t k = 0; k < track_count; ++k) {
|
|
if (document.tracks[k].id == document.turnouts[i].on) {
|
|
nodes[turnout_node[i]].depends_on.push_back(track_node[k]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
for (std::size_t i = 0; i < track_count; ++i) {
|
|
track_dependencies(document, document.tracks[i], turnout_node, track_node,
|
|
nodes[track_node[i]]);
|
|
}
|
|
|
|
// Topological order, Kahn's algorithm.
|
|
std::vector<std::size_t> remaining(nodes.size(), 0);
|
|
std::vector<std::vector<std::size_t>> dependents(nodes.size());
|
|
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
|
remaining[i] = nodes[i].depends_on.size();
|
|
for (const std::size_t dependency : nodes[i].depends_on) {
|
|
dependents[dependency].push_back(i);
|
|
}
|
|
}
|
|
std::vector<std::size_t> order;
|
|
order.reserve(nodes.size());
|
|
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
|
if (remaining[i] == 0) {
|
|
order.push_back(i);
|
|
}
|
|
}
|
|
for (std::size_t head = 0; head < order.size(); ++head) {
|
|
for (const std::size_t next : dependents[order[head]]) {
|
|
if (--remaining[next] == 0) {
|
|
order.push_back(next);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Whatever never came free is in a cycle.
|
|
if (order.size() != nodes.size()) {
|
|
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
|
if (remaining[i] == 0) {
|
|
continue;
|
|
}
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::CyclicDependency;
|
|
switch (nodes[i].kind) {
|
|
case NodeKind::Track:
|
|
diagnostic.track = document.tracks[nodes[i].index].id;
|
|
diagnostic.text = "tor „" + document.tracks[nodes[i].index].name +
|
|
"” zależy sam od siebie w kółko";
|
|
break;
|
|
case NodeKind::Turnout:
|
|
diagnostic.turnout = document.turnouts[nodes[i].index].id;
|
|
diagnostic.text = "rozjazd zależy sam od siebie w kółko";
|
|
break;
|
|
}
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
}
|
|
}
|
|
|
|
// ---- walk the order ---------------------------------------------------
|
|
for (const std::size_t index : order) {
|
|
const Node& node = nodes[index];
|
|
switch (node.kind) {
|
|
case NodeKind::Turnout: {
|
|
const TurnoutPlacement& placement = document.turnouts[node.index];
|
|
SolvedTurnout solved;
|
|
solved.id = placement.id;
|
|
|
|
const TurnoutType* type = find_type(document, placement.type);
|
|
if (type == nullptr) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::UnknownReference;
|
|
diagnostic.turnout = placement.id;
|
|
diagnostic.text = "rozjazd odwołuje się do typu „" + placement.type +
|
|
"”, którego nie ma w katalogu";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
solution.turnouts.push_back(std::move(solved));
|
|
break;
|
|
}
|
|
|
|
const SolvedTrack* through = find_track(solution, placement.on);
|
|
Pose at{};
|
|
if (through == nullptr || !pose_at(*through, placement.station, at)) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::StationOffTrack;
|
|
diagnostic.turnout = placement.id;
|
|
diagnostic.track = placement.on;
|
|
diagnostic.text = "rozjazd stoi na torze, którego nie da się położyć";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
solution.turnouts.push_back(std::move(solved));
|
|
break;
|
|
}
|
|
// łukowanie: the turnout is bent onto whatever the track does under
|
|
// it, unless it was asked to carry a bend of its own. Read the room
|
|
// here too — both answers come off the same element
|
|
double curvature = 0.0;
|
|
double behind = 0.0;
|
|
double ahead = 0.0;
|
|
const bool steady = steady_room(*through, placement.station, curvature, behind,
|
|
ahead);
|
|
double bend = placement.bend_from_track ? (steady ? curvature : 0.0)
|
|
: placement.bend;
|
|
|
|
if (!placement.facing) {
|
|
at = Pose{at.x, at.y, -at.hx, -at.hy};
|
|
// laid against the way the track runs, a left curve is a right one
|
|
bend = -bend;
|
|
}
|
|
|
|
const plan::TurnoutGeometry geometry =
|
|
plan::lay_turnout(at, to_domain(*type, placement.hand), bend);
|
|
if (!geometry.valid) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::TurnoutInvalid;
|
|
diagnostic.turnout = placement.id;
|
|
diagnostic.text =
|
|
bend != 0.0
|
|
? "typu „" + type->name + "” nie da się wygiąć na łuk R " +
|
|
num(std::abs(1.0 / bend)) +
|
|
" m: łukowanie wymaga jednego promienia i szyn łączących, "
|
|
"które je przyjmą"
|
|
: "typ „" + type->name +
|
|
"” nie daje się złożyć: sprawdź skos, promienie i iglicę";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
solution.turnouts.push_back(std::move(solved));
|
|
break;
|
|
}
|
|
|
|
// a turnout is one piece of geometry bent to the one radius under it, so it has to
|
|
// stand on a single element and fit on it whole, or the drawing says something the
|
|
// ground would not. facing turnouts eat the room ahead, trailing ones behind
|
|
if (!steady) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::StationOffTrack;
|
|
diagnostic.turnout = placement.id;
|
|
diagnostic.track = placement.on;
|
|
diagnostic.text =
|
|
through->length > 0.0 && placement.station > through->length + 1e-6
|
|
? "rozjazd stoi " + num(placement.station - through->length) +
|
|
" m za końcem toru zasadniczego"
|
|
: "rozjazd stoi na krzywej przejściowej: nie ma jednego promienia, "
|
|
"na który dałoby się go wygiąć";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
} else if (const double room = placement.facing ? ahead : behind;
|
|
geometry.through_length > room + 1e-6) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::StationOffTrack;
|
|
diagnostic.turnout = placement.id;
|
|
diagnostic.track = placement.on;
|
|
diagnostic.text = "rozjazd wychodzi " + num(geometry.through_length - room) +
|
|
" m poza odcinek toru, na którym stoi: potrzebuje " +
|
|
num(geometry.through_length) + " m, a zostało " + num(room);
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
}
|
|
|
|
solved.valid = true;
|
|
solved.bend = geometry.bend;
|
|
solved.diverging_curvature = geometry.diverging_curvature;
|
|
solved.bend_angle = geometry.bend_angle;
|
|
solved.start = at;
|
|
solved.frog = geometry.frog;
|
|
solved.tangent_front = geometry.tangent_front;
|
|
solved.tangent_back = geometry.tangent_back;
|
|
solved.diverging_length = geometry.diverging_length;
|
|
solved.through_length = geometry.through_length;
|
|
solved.has_blade = geometry.has_blade;
|
|
solved.blade_station = geometry.blade_station;
|
|
solved.nose_end = geometry.nose_end;
|
|
solved.planing_length = geometry.planing_length;
|
|
solved.blade_angle = geometry.blade_angle;
|
|
solved.blade_residual = geometry.blade_residual;
|
|
solved.angle_residual = geometry.angle_residual;
|
|
solved.length_residual = geometry.length_residual;
|
|
|
|
solved.marks = turnout_marks(geometry, at);
|
|
|
|
Pose walker = at;
|
|
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 = Pose{walker.x, walker.y,
|
|
walker.hx * cos_t - walker.hy * sin_t,
|
|
walker.hx * sin_t + walker.hy * cos_t};
|
|
}
|
|
|
|
SolvedTurnoutSegment out_segment;
|
|
out_segment.start = walker;
|
|
out_segment.k0 = segment.k0;
|
|
out_segment.k1 = segment.k1;
|
|
out_segment.length = segment.length;
|
|
std::vector<geometry::XY> points;
|
|
walker = geometry::layout_segment(segment.k0, segment.k1,
|
|
segment.length, walker,
|
|
&points);
|
|
out_segment.part = segment.part;
|
|
out_segment.points.reserve(points.size());
|
|
for (const auto& point : points) {
|
|
out_segment.points.push_back(PlanPoint{point.x, point.y});
|
|
}
|
|
solved.path.push_back(std::move(out_segment));
|
|
}
|
|
|
|
// the list is laid as authored; whether it keeps the catalogue's
|
|
// promises is said out loud rather than fitted away
|
|
if (std::abs(geometry.angle_residual) > 1e-6 ||
|
|
(type->length > 0.0 && std::abs(geometry.length_residual) > 1e-3)) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::TurnoutNotClosed;
|
|
diagnostic.turnout = placement.id;
|
|
diagnostic.text =
|
|
"typ „" + type->name + "” nie domyka się: kąt o " +
|
|
num(geometry.angle_residual * 180.0 / std::numbers::pi) +
|
|
" st., długość o " + num(geometry.length_residual) + " m";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
}
|
|
|
|
solution.turnouts.push_back(std::move(solved));
|
|
break;
|
|
}
|
|
|
|
case NodeKind::Track: {
|
|
const Track& track = document.tracks[node.index];
|
|
Pose start{0.0, 0.0, 0.0, 1.0};
|
|
bool anchored = true;
|
|
|
|
if (const auto* pose = std::get_if<AtPose>(&track.anchor)) {
|
|
start = Pose{pose->x, pose->y, std::sin(pose->az), std::cos(pose->az)};
|
|
} else if (const auto* at_port = std::get_if<AtPort>(&track.anchor)) {
|
|
const SolvedTurnout* turnout = find_turnout(solution, at_port->turnout);
|
|
if (turnout == nullptr || !turnout->valid) {
|
|
anchored = false;
|
|
} else {
|
|
start = at_port->port == Port::Frog ? turnout->frog : turnout->start;
|
|
}
|
|
}
|
|
|
|
if (!anchored) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::UnknownReference;
|
|
diagnostic.track = track.id;
|
|
diagnostic.text = "tor „" + track.name +
|
|
"” jest zakotwiczony w czymś, czego nie ma "
|
|
"albo czego nie dało się położyć";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
SolvedTrack empty;
|
|
empty.id = track.id;
|
|
empty.complete = false;
|
|
solution.tracks.push_back(std::move(empty));
|
|
break;
|
|
}
|
|
|
|
// Laying is a walk, so a hold is resolved around it rather than
|
|
// inside it: lay once to see which way the held element runs,
|
|
// take its shape from the reference, lay again, and finally move
|
|
// the whole chain onto the offset it is held at.
|
|
const bool movable = std::holds_alternative<AtPose>(track.anchor);
|
|
Track resolved = track;
|
|
TrackOutcome outcome = lay_track(document, resolved, start);
|
|
|
|
std::size_t held_index = resolved.elements.size();
|
|
for (std::size_t e = 0; e < resolved.elements.size(); ++e) {
|
|
const auto* parallel = std::get_if<Parallel>(&resolved.elements[e].hold);
|
|
if (parallel == nullptr) {
|
|
continue;
|
|
}
|
|
const SolvedElement* reference =
|
|
find_element(solution, parallel->ref);
|
|
if (reference == nullptr || e >= outcome.track.elements.size()) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::UnknownReference;
|
|
diagnostic.track = track.id;
|
|
diagnostic.element = resolved.elements[e].id;
|
|
diagnostic.text =
|
|
"odcinek ma trzymać równoległość do czegoś, czego nie ma "
|
|
"albo czego nie dało się położyć";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
continue;
|
|
}
|
|
const Held held = hold_against(*reference, parallel->offset,
|
|
outcome.track.elements[e].start);
|
|
if (!held.ok) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::DegenerateElement;
|
|
diagnostic.track = track.id;
|
|
diagnostic.element = resolved.elements[e].id;
|
|
diagnostic.text = held.why;
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
continue;
|
|
}
|
|
resolved.elements[e].kind = held.kind;
|
|
resolved.elements[e].radius = held.radius;
|
|
resolved.elements[e].hand = held.hand;
|
|
if (held_index == resolved.elements.size()) {
|
|
held_index = e;
|
|
} else if (movable) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::HoldConflict;
|
|
diagnostic.track = track.id;
|
|
diagnostic.element = resolved.elements[e].id;
|
|
diagnostic.text =
|
|
"tor „" + track.name +
|
|
"” trzyma równoległość dwoma odcinkami naraz; przystawiony "
|
|
"jest do pierwszego z nich";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
}
|
|
}
|
|
|
|
if (held_index < resolved.elements.size()) {
|
|
outcome = lay_track(document, resolved, start);
|
|
if (held_index < outcome.track.elements.size()) {
|
|
const auto* parallel =
|
|
std::get_if<Parallel>(&resolved.elements[held_index].hold);
|
|
const SolvedElement* reference =
|
|
find_element(solution, parallel->ref);
|
|
const Held held =
|
|
hold_against(*reference, parallel->offset,
|
|
outcome.track.elements[held_index].start);
|
|
if (held.ok && movable) {
|
|
const Pose moved =
|
|
rigid_move(start, outcome.track.elements[held_index].start,
|
|
held.target);
|
|
outcome = lay_track(document, resolved, moved);
|
|
} else if (held.ok) {
|
|
// A track pinned to a turnout's port cannot be moved
|
|
// onto the offset, so the corner between the port and
|
|
// the held element has to take up the difference —
|
|
// the same fit as pulling a straight about by hand.
|
|
std::string why;
|
|
Skeleton skeleton;
|
|
Track fitted = resolved;
|
|
bool done = false;
|
|
if (resolved.elements[held_index].kind == Kind::Line &&
|
|
skeleton_of(resolved, outcome.track, skeleton, why)) {
|
|
std::size_t line_index = 0;
|
|
for (std::size_t e = 0; e < held_index; ++e) {
|
|
if (resolved.elements[e].kind == Kind::Line) {
|
|
++line_index;
|
|
}
|
|
}
|
|
if (line_index == 0) {
|
|
why = "przed odcinkiem trzymającym równoległość nie ma "
|
|
"łuku, który mógłby na nią wyprowadzić";
|
|
} else {
|
|
skeleton.lines[line_index] =
|
|
Support{held.target.x, held.target.y, held.target.hx,
|
|
held.target.hy};
|
|
if (line_index + 1 == skeleton.lines.size()) {
|
|
skeleton.end_x = held.target.x;
|
|
skeleton.end_y = held.target.y;
|
|
}
|
|
done = lay_along_skeleton(fitted, skeleton, why);
|
|
}
|
|
}
|
|
if (done) {
|
|
resolved = std::move(fitted);
|
|
outcome = lay_track(document, resolved, start);
|
|
} else if (std::abs(held.actual - parallel->offset) > 1e-3) {
|
|
Diagnostic diagnostic;
|
|
diagnostic.code = Code::OffsetNotHeld;
|
|
diagnostic.track = track.id;
|
|
diagnostic.element = resolved.elements[held_index].id;
|
|
diagnostic.text =
|
|
"tor „" + track.name +
|
|
"” jest przypięty do rozjazdu i nie da się go dopasować (" +
|
|
why + "): odstęp wychodzi " + num(held.actual, 2) +
|
|
" m zamiast " + num(parallel->offset, 2) + " m";
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& diagnostic : outcome.diagnostics) {
|
|
solution.diagnostics.push_back(std::move(diagnostic));
|
|
}
|
|
solution.tracks.push_back(std::move(outcome.track));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Anything left out by a cycle still gets an entry, so the host can index by
|
|
// id without checking for holes.
|
|
for (const auto& track : document.tracks) {
|
|
if (find_track(solution, track.id) == nullptr) {
|
|
SolvedTrack empty;
|
|
empty.id = track.id;
|
|
empty.complete = false;
|
|
solution.tracks.push_back(std::move(empty));
|
|
}
|
|
}
|
|
for (const auto& turnout : document.turnouts) {
|
|
if (find_turnout(solution, turnout.id) == nullptr) {
|
|
SolvedTurnout empty;
|
|
empty.id = turnout.id;
|
|
solution.turnouts.push_back(std::move(empty));
|
|
}
|
|
}
|
|
|
|
return solution;
|
|
}
|
|
|
|
} // namespace editor::plan
|