17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 10:59:20 +02:00
Files
maszyna/editor/plan_turnout_geometry.cpp
maj00r 321c9dd7ab 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.
2026-09-13 13:31:01 +02:00

324 lines
14 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 <algorithm>
#include <cmath>
#include <numbers>
#include <vector>
module eu07.editor.plan_turnout_geometry;
namespace editor::plan {
using geometry::layout_segment;
using geometry::Pose;
using geometry::XY;
double CrossingMark::angle() const noexcept {
return denominator_ > 0.0 ? std::atan(1.0 / denominator_) : 0.0;
}
namespace {
// The deflection (radians) a segment of linearly varying curvature turns through.
double deflection(const TurnoutSegment& segment) {
return 0.5 * (segment.k0 + segment.k1) * segment.length;
}
// Turns @p pose in place by @p angle (a heading break, no travel).
Pose turn(const Pose& pose, double angle) {
if (angle == 0.0) return pose;
const double c = std::cos(angle);
const double s = std::sin(angle);
return Pose{pose.x, pose.y, pose.hx * c - pose.hy * s, pose.hx * s + pose.hy * c};
}
// Advances @p pose over @p segment (without sampling points; that is the
// renderer's job): the heading break first, then the travel. Returns the end pose.
Pose advance(const TurnoutSegment& segment, const Pose& pose) {
return layout_segment(segment.k0, segment.k1, segment.length,
turn(pose, segment.turn_in), nullptr);
}
// Where the through tangent (through @p a along @p a heading) meets the diverging
// tangent (through @p b along @p b heading). Writes the two tangent lengths; both
// stay zero when the tangents are parallel (a straight-through, angle-less turnout).
void tangent_lengths(const Pose& a, const Pose& b, double& front, double& back) {
const double denominator = a.hx * b.hy - a.hy * b.hx;
if (std::abs(denominator) < 1e-9) return;
const double u = ((b.x - a.x) * b.hy - (b.y - a.y) * b.hx) / denominator;
const double cx = a.x + a.hx * u;
const double cy = a.y + a.hy * u;
front = std::hypot(cx - a.x, cy - a.y);
back = std::hypot(cx - b.x, cy - b.y);
}
// The heading of @p pose off the through track that runs through @p start, signed
// so that it grows as the path leaves it.
double heading_off(const Pose& start, const Pose& pose, double sign) {
return sign * std::atan2(pose.hy * start.hx - pose.hx * start.hy,
pose.hx * start.hx + pose.hy * start.hy);
}
// The pose @p station metres along @p path. Stations are the same whichever frame
// the path is measured in, because bending moves curvature and never length.
Pose pose_at_station(const std::vector<TurnoutSegment>& path, const Pose& start,
double station) {
Pose pose = start;
double walked = 0.0;
for (const auto& segment : path) {
const Pose from = turn(pose, segment.turn_in);
if (station <= walked + segment.length) {
return layout_segment(segment.k0, segment.k1, station - walked, from, nullptr);
}
walked += segment.length;
pose = layout_segment(segment.k0, segment.k1, segment.length, from, nullptr);
}
return pose;
}
// Bends the laid path onto a through track of curvature @p bend, the way a rozjazd
// łukowy is made: the triangle the turnout is dimensioned by is rotated about its
// centre, so `t` and the crossing angle stand and every arc takes the radius that
// follows from them.
//
// What is re-cut and what is not comes straight from Koc: the zwrotnica and the
// krzyżownica keep their dimensions and are only bent, so the blade, the odcinek
// przediglicowy and the frog rail keep their lengths to the millimetre. The szyny
// łączące — the plain curve between them — take up the whole difference, which is
// small: the arc between two tangent points barely changes length when it is bent.
//
// False when there is no single arc radius to rotate about, nothing flexible to
// take up the change, or the bend is sharp enough to eat the connecting rails whole.
bool bend_path(std::vector<TurnoutSegment>& path, double alpha, double sign, double bend,
double& tangent_out, double& curvature_out, double& beta_out) {
double radius = 0.0;
double cut = 0.0; // what bending does not re-cut: switch and frog
double flexible = 0.0; // the connecting rails, which it does
for (const auto& segment : path) {
if (segment.k0 == 0.0 && segment.k1 == 0.0) continue;
if (segment.k0 != segment.k1) return false; // a clothoid has no one radius
const double own = std::abs(1.0 / segment.k0);
if (radius != 0.0 && std::abs(own - radius) > 1e-6 * radius) return false;
radius = own;
(segment.part == TurnoutPart::Curve ? flexible : cut) += segment.length;
}
if (radius <= 0.0 || flexible <= 0.0 || alpha <= 0.0) return false;
const double tangent = radius * std::tan(0.5 * alpha);
const double beta = 2.0 * std::atan(tangent * bend);
const double turn_to = beta + sign * alpha;
if (std::abs(turn_to) >= std::numbers::pi - 1e-9) return false;
// t/tg(theta/2) is the radius of every leg of the triangle, the through track's
// as much as the branch's. At theta = 0 the branch comes out straight, and its
// two tangents are then all the length there is between PR and the crossing
const double curvature =
std::abs(turn_to) > 1e-12 ? std::tan(0.5 * turn_to) / tangent : 0.0;
const double run = curvature != 0.0 ? turn_to / curvature : 2.0 * tangent;
const double spare = run - cut;
if (spare <= 0.0) return false;
for (auto& segment : path) {
if (segment.k0 == 0.0 && segment.k1 == 0.0) continue;
segment.k0 = curvature;
segment.k1 = curvature;
if (segment.part == TurnoutPart::Curve) {
segment.length *= spare / flexible;
}
}
tangent_out = tangent;
curvature_out = curvature;
beta_out = beta;
return true;
}
// Where along the laid path the blade first stands @p target away from the stock
// rail — the dimension a catalogue drawing gives its blade in (the tip is where the
// rail is u thick, the planing ends where it is a railhead wide). The gap is not the
// offset of the axis: it opens on the blade's own curve, half a gauge wider than the
// axis it is offset from (Koc, 5.2), so it is carried segment by segment and the one
// that crosses it is solved in place. The station is on the axis. Returns false when
// the path never gets that far.
bool station_at_offset(const std::vector<TurnoutSegment>& path, const Pose& start,
double target, double sign, double& station_out, Pose& pose_out) {
if (target <= 0.0) return false;
Pose pose = start;
double station = 0.0;
double gap = 0.0;
for (const auto& segment : path) {
const Pose from = turn(pose, segment.turn_in);
const Pose to = advance(segment, pose);
const double heading = heading_off(start, from, sign);
const double radius = segment.k0 != 0.0 ? std::abs(1.0 / segment.k0) : 0.0;
const double opened =
radius > 0.0
? (radius + kHalfGauge) *
(std::cos(heading) - std::cos(heading_off(start, to, sign)))
: segment.length * std::sin(heading);
if (gap <= target && gap + opened >= target) {
const double run = std::clamp(
blade_run_to_offset(radius, heading, target - gap), 0.0, segment.length);
station_out = station + run;
pose_out = layout_segment(segment.k0, segment.k1, run, from, nullptr);
return true;
}
gap += opened;
station += segment.length;
pose = to;
}
return false;
}
// Fills the blade dimensions: where its tip stands, how much of the curve runs ahead
// of it as the odcinek przediglicowy, where its planed nose ends and where it reaches
// full railhead width.
// @p relative is the path with the through track's own curvature taken out of it:
// the blade opens against the opornica, so what dimensions it is how the two part
// company, not how either of them runs across the ground. For a rozjazd zwyczajny
// the two paths are the same thing.
void dimension_blade(const Turnout& turnout, const Pose& start,
const std::vector<TurnoutSegment>& relative, TurnoutGeometry& geometry,
double sign) {
double station = 0.0;
Pose pose = start;
for (const auto& segment : geometry.path) {
if (segment.part != TurnoutPart::Blade) {
station += segment.length;
pose = advance(segment, pose);
continue;
}
geometry.has_blade = true;
// the blade begins where its piece does; everything ahead of it is the lead
geometry.blade_station = station;
geometry.blade_tip = turn(pose, segment.turn_in);
double at{0.0};
Pose at_pose{};
if (station_at_offset(relative, start, turnout.blade.tip_thickness, sign, at, at_pose)) {
geometry.nose_end = at;
geometry.blade_nose_end = pose_at_station(geometry.path, start, at);
geometry.blade_angle = heading_off(start, at_pose, sign);
geometry.blade_residual = station + turnout.blade.nose - at;
}
if (station_at_offset(relative, start, turnout.blade.railtop_width, sign, at, at_pose)) {
geometry.planing_end = at;
geometry.blade_planed = pose_at_station(geometry.path, start, at);
geometry.planing_length = at - geometry.blade_station;
}
return;
}
}
} // namespace
double blade_run_to_offset(double radius, double angle, double offset) {
if (offset <= 0.0) return 0.0;
if (radius > 0.0) {
// the blade is an arc: the gap opens as `R1(cos beta - cos(beta + theta))`
// on the rail's own curve, and the axis spends `R theta` getting there
const double rail = radius + kHalfGauge;
const double target = std::cos(angle) - offset / rail;
if (target < -1.0) return 0.0; // the offset is more than the arc ever reaches
return radius * (std::acos(std::clamp(target, -1.0, 1.0)) - angle);
}
// a straight blade: the offset grows with the sine of the break
const double s = std::sin(angle);
return s > 1e-12 ? offset / s : 0.0;
}
TurnoutGeometry lay_turnout(const Pose& start, const Turnout& turnout, double bend) {
TurnoutGeometry geometry;
if (turnout.path.empty()) return geometry;
const double sign = turnout.side == DivergeSide::Left ? 1.0 : -1.0;
for (const auto& piece : turnout.path) {
if (piece.length <= 0.0) return geometry;
if (piece.radius_start < 0.0 || piece.radius_end < 0.0) return geometry;
if (piece.turn_in < 0.0) return geometry;
geometry.path.push_back(TurnoutSegment{
sign * piece.turn_in, piece.radius_start > 0.0 ? sign / piece.radius_start : 0.0,
piece.radius_end > 0.0 ? sign / piece.radius_end : 0.0, piece.length, piece.part});
}
double tangent = 0.0;
double beta = 0.0;
if (bend != 0.0) {
double curvature = 0.0;
if (!bend_path(geometry.path, turnout.mark.angle(), sign, bend, tangent, curvature,
beta)) {
return geometry;
}
geometry.bend = bend;
geometry.bend_angle = beta;
geometry.diverging_curvature = curvature;
}
// the path is laid exactly as authored: what it works out to is then checked
// against the catalogue numbers rather than fitted to them
Pose pose = start;
double deflected = 0.0;
for (const auto& segment : geometry.path) {
deflected += segment.turn_in + deflection(segment);
pose = advance(segment, pose);
geometry.diverging_length += segment.length;
}
geometry.valid = true;
geometry.frog = pose;
tangent_lengths(start, pose, geometry.tangent_front, geometry.tangent_back);
// the blade is dimensioned against the opornica, which bends with the turnout
std::vector<TurnoutSegment> relative = geometry.path;
for (auto& segment : relative) {
segment.k0 -= bend;
segment.k1 -= bend;
}
dimension_blade(turnout, start, relative, geometry, sign);
// deflected is signed by the hand, and it is the through track the crossing
// angle is measured from — which turns by beta when the turnout is bent
geometry.angle_residual = sign * (deflected - beta) - turnout.mark.angle();
// where the switch ends on the tor zasadniczy: the frog, projected back onto it.
// this is the second point a drawing marks on the through track, and on a bent
// turnout the projection runs along the radius rather than square to a straight
if (bend == 0.0) {
geometry.through_length =
(pose.x - start.x) * start.hx + (pose.y - start.y) * start.hy;
geometry.through_end = Pose{start.x + start.hx * geometry.through_length,
start.y + start.hy * geometry.through_length, start.hx,
start.hy};
} else {
const double centre_x = start.x - start.hy / bend;
const double centre_y = start.y + start.hx / bend;
const double from = std::atan2(start.y - centre_y, start.x - centre_x);
const double to = std::atan2(pose.y - centre_y, pose.x - centre_x);
double swept = to - from;
while (swept > std::numbers::pi) swept -= 2.0 * std::numbers::pi;
while (swept < -std::numbers::pi) swept += 2.0 * std::numbers::pi;
geometry.through_length = swept / bend;
geometry.through_end =
layout_segment(bend, bend, geometry.through_length, start, nullptr);
}
// the catalogue length is a + b, the two tangent legs of the drawing
if (turnout.length > 0.0) {
geometry.length_residual =
geometry.tangent_front + geometry.tangent_back - turnout.length;
}
return geometry;
}
} // namespace editor::plan