mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-07 06:28:08 +01:00
Editor: rebuild the plan library around a solved document, and bend turnouts onto curves
The library was a heap of half-built services - fitting, parallelism, niwelety, an SVG writer - that never met each other. In their place is one authored Document and one solver that lays it whole: tracks as element chains, turnouts placed on them, and a Solution that is rebuilt from scratch every time and never fed back into the document. What cannot be laid comes back as a diagnostic with the number in it rather than as silence or a fitted-away error. Turnouts are dimensioned the way the drawings dimension them. The blade follows Koc's rys. 5.9 - the curve is theoretically tangent to the opornica at PR, the blade starts at a with a planed nose of d, and A is where the rail stands u thick - and a + d derived that way lands on his tablica 5.1. Bending follows 5.3.6: it rotates the triangle OBC about the centre of the turnout, so the tangent t and the crossing angle stand and both radii follow from them. That is Koc's (5.7) and (5.8) without coding either, and it puts a symmetrically bent Rz 49E1-190-1:9 on the catalogue's Rlus 49E1-380,292-1:9. A rozjazd standing on a curve is bent onto it; one on a track drawn straight can carry a bend of its own. The szyny laczace take up the difference, because the switch and the frog are only bent, not re-cut. The plan panel draws all of it: the catalogue drawing of a type with its construction points, the theoretical arc dashed where no rail runs, and the blade nose as the straight it is cut as. Dragging a straight now stops where it would pull the track out from under a turnout. File format goes to m0s 6. Versions 3 to 5 still open.
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/editor/editor.hpp"
|
||||
#include "maj0sted/editor/ribbon.hpp"
|
||||
#include "maj0sted/editor/solve.hpp"
|
||||
|
||||
namespace maj0sted::app {
|
||||
|
||||
struct BasketDraft {
|
||||
bool active{false};
|
||||
double x0{0.0};
|
||||
double y0{0.0};
|
||||
double az0{0.0};
|
||||
int dir{1};
|
||||
double entry_t{0.0};
|
||||
double exit_t{0.0};
|
||||
std::vector<maj0sted::editor::CompoundArcSpec> arcs;
|
||||
double preview_next_r{300.0};
|
||||
int attach_str{-1};
|
||||
int attach_end{-1};
|
||||
int reedit_gap{-1};
|
||||
maj0sted::editor::GapFit reedit_backup{};
|
||||
};
|
||||
|
||||
[[nodiscard]] maj0sted::editor::RibbonRequest draft_to_ribbon(const BasketDraft& draft);
|
||||
|
||||
[[nodiscard]] std::vector<maj0sted::editor::PlanPolyline> preview_basket_draft(
|
||||
BasketDraft& draft, double fallback_next_r = 300.0);
|
||||
|
||||
void start_basket_draft_from_end(BasketDraft& draft,
|
||||
const maj0sted::editor::StraightSpec& straight,
|
||||
int straight_index, int end, double default_r = 300.0);
|
||||
|
||||
[[nodiscard]] bool reopen_basket_draft(BasketDraft& draft,
|
||||
maj0sted::editor::NiweletaSpec& niweleta,
|
||||
std::size_t gap,
|
||||
const maj0sted::editor::NiweletaPolys* solved);
|
||||
|
||||
void discard_basket_draft(BasketDraft& draft, maj0sted::editor::NiweletaSpec* niweleta);
|
||||
|
||||
[[nodiscard]] bool anchor_basket_draft(BasketDraft& draft,
|
||||
maj0sted::editor::NiweletaSpec& niweleta,
|
||||
std::size_t gap);
|
||||
|
||||
struct FinishBasketDraftResult {
|
||||
bool ok{false};
|
||||
std::string status;
|
||||
};
|
||||
|
||||
[[nodiscard]] FinishBasketDraftResult finish_basket_draft(
|
||||
BasketDraft& draft, maj0sted::editor::NiweletaSpec& niweleta,
|
||||
std::vector<maj0sted::editor::NiweletaSpec>& all_niwelety, int niweleta_index,
|
||||
double finish_straight_m);
|
||||
|
||||
} // namespace maj0sted::app
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/map_project.hpp"
|
||||
#include "maj0sted/editor/editor.hpp"
|
||||
|
||||
namespace maj0sted::app {
|
||||
|
||||
struct EditorDocument {
|
||||
std::vector<maj0sted::editor::NiweletaSpec> niwelety;
|
||||
std::vector<maj0sted::editor::Junction> junctions;
|
||||
double view_x{0.0};
|
||||
double view_y{0.0};
|
||||
double view_extent{0.0};
|
||||
bool origin_set{false};
|
||||
bool georeferenced{false};
|
||||
double origin_x{0.0};
|
||||
double origin_y{0.0};
|
||||
};
|
||||
|
||||
[[nodiscard]] maj0sted::domain::MapProject to_map_project(const EditorDocument& document);
|
||||
[[nodiscard]] EditorDocument from_map_project(const maj0sted::domain::MapProject& project);
|
||||
[[nodiscard]] std::string serialize_document(const EditorDocument& document);
|
||||
[[nodiscard]] EditorDocument deserialize_document(const std::string& text);
|
||||
[[nodiscard]] std::string default_project_path();
|
||||
bool save_project(const EditorDocument& document,
|
||||
const std::string& path = default_project_path());
|
||||
[[nodiscard]] std::optional<EditorDocument> load_project(
|
||||
const std::string& path = default_project_path());
|
||||
|
||||
} // namespace maj0sted::app
|
||||
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/editor/editor.hpp"
|
||||
|
||||
namespace maj0sted::app {
|
||||
|
||||
void apply_straight_constraints(std::vector<maj0sted::editor::NiweletaSpec>& niwelety);
|
||||
void align_gap_fits(maj0sted::editor::NiweletaSpec& niweleta);
|
||||
|
||||
} // namespace maj0sted::app
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
#include "maj0sted/domain/value_objects/radius.hpp"
|
||||
#include "maj0sted/domain/value_objects/straight_id.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Input parameters for fitting a curve between two straights of one niweleta.
|
||||
///
|
||||
/// These map directly onto a GUI form: the user picks the entry and exit
|
||||
/// straights, types a radius, and optionally a (symmetric) transition-curve
|
||||
/// length. The turn direction is NOT an input — it follows from the geometry of
|
||||
/// the two straights (their deflection angle).
|
||||
///
|
||||
/// * transition_length absent -> a plain circular arc is fitted;
|
||||
/// * transition_length present -> a symmetric klotoida-arc-klotoida is fitted,
|
||||
/// with that length on both ends.
|
||||
struct FitParameters {
|
||||
StraightId entry{}; ///< pierwsza (wejściowa) prosta
|
||||
StraightId exit{}; ///< druga (wyjściowa) prosta
|
||||
Radius radius; ///< promień łuku kołowego
|
||||
std::optional<Length> transition_length{}; ///< symetryczna klotoida; brak = sam łuk
|
||||
};
|
||||
|
||||
/// One arc of a compound curve.
|
||||
struct CompoundArc {
|
||||
Radius radius; ///< promień łuku
|
||||
std::optional<Length> length{}; ///< długość łuku; wymagana OPRÓCZ ostatniego
|
||||
std::optional<Length> transition_to_next{}; ///< klotoida do następnego łuku (ignorowana dla ostatniego)
|
||||
};
|
||||
|
||||
/// Input parameters for fitting a compound curve — a sequence of one or more
|
||||
/// circular arcs (of different neighbouring radii, same direction) with optional
|
||||
/// transition curves (klotoidy) between the arcs and at the ends — between two
|
||||
/// straights. This single shape covers both a "łuk + klotoidy" (one arc eased by
|
||||
/// symmetric clothoids) and a "łuk koszowy" (two or more arcs).
|
||||
///
|
||||
/// Both straight lines stay put (position and azimuth). Each arc except the last
|
||||
/// carries its length, which fixes its central angle (`δ = L / R`); the last (or
|
||||
/// only) arc takes whatever deflection remains after the other arcs and every
|
||||
/// transition curve. The two straights are re-trimmed in length so the whole
|
||||
/// chain closes.
|
||||
struct CompoundFitParameters {
|
||||
StraightId entry{}; ///< pierwsza (wejściowa) prosta
|
||||
StraightId exit{}; ///< druga (wyjściowa) prosta
|
||||
std::optional<Length> entry_transition{}; ///< klotoida prosta -> pierwszy łuk
|
||||
std::optional<Length> exit_transition{}; ///< klotoida ostatni łuk -> prosta
|
||||
std::vector<CompoundArc> arcs{}; ///< >= 1 łuk; sąsiednie promienie różne
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/geometry/plan_element.hpp"
|
||||
#include "maj0sted/domain/value_objects/cartesian_position.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Result of fitting a curve between two straights.
|
||||
///
|
||||
/// The two straights keep their identity but are trimmed back to the tangent
|
||||
/// points; @c curve is the klotoida?/łuk/klotoida? to splice between them. The
|
||||
/// caller (or an aggregate command) replaces the original entry/exit straights
|
||||
/// with @c entry, then @c curve, then @c exit.
|
||||
struct FitResult {
|
||||
Straight entry; ///< trimmed entry straight (ends at tangent_in)
|
||||
std::vector<PlanElement> curve; ///< klotoida? / łuk / klotoida?
|
||||
Straight exit; ///< trimmed exit straight (starts at tangent_out)
|
||||
CartesianPosition tangent_in; ///< entry -> curve tangent point
|
||||
CartesianPosition tangent_out; ///< curve -> exit tangent point
|
||||
TurnDirection direction; ///< derived turn direction
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,69 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "maj0sted/domain/fitting/fit_parameters.hpp"
|
||||
#include "maj0sted/domain/fitting/fit_result.hpp"
|
||||
#include "maj0sted/domain/niweleta.hpp"
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
#include "maj0sted/domain/value_objects/radius.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Domain Service: fits a curve between two straights of a single niweleta.
|
||||
class FittingService {
|
||||
public:
|
||||
// --- Overloads that fit two standalone straights (no niweleta) ---
|
||||
|
||||
/// Rounds the corner between two straight lines with an arc (and optional
|
||||
/// symmetric transition curves). Works for any deflection strictly between
|
||||
/// 0° and 180°. For a 180° reversal use fit_uturn instead.
|
||||
static FitResult fit_between_straights(
|
||||
const Straight& entry, const Straight& exit, Radius radius,
|
||||
std::optional<Length> transition = std::nullopt);
|
||||
|
||||
/// Fits a compound curve between two standalone straight lines.
|
||||
static FitResult fit_compound(const Straight& entry, const Straight& exit,
|
||||
const CompoundFitParameters& params);
|
||||
|
||||
/// Connects two (anti-)parallel straights with a 180° reversal ("nawrót").
|
||||
/// The radius is derived from the perpendicular offset between the two lines
|
||||
/// (a plain arc uses R = offset / 2). Optional symmetric transition curves
|
||||
/// are solved for numerically.
|
||||
/// @throws std::invalid_argument if the straights are not anti-parallel or
|
||||
/// their offset is zero.
|
||||
static FitResult fit_uturn(const Straight& entry, const Straight& exit,
|
||||
std::optional<Length> transition = std::nullopt);
|
||||
|
||||
/// Fits an S-curve made of two tangent circular arcs with the same radius
|
||||
/// and opposite turn directions. Supports non-parallel straights and
|
||||
/// same-direction parallel straights with a non-zero lateral offset.
|
||||
/// Tangent points are required to lie on both finite straight segments.
|
||||
/// @throws std::invalid_argument if no equal-radius reverse curve fits.
|
||||
static FitResult fit_reverse(const Straight& entry, const Straight& exit,
|
||||
Radius radius);
|
||||
|
||||
/// Computes the curve (and the trimmed straights) that connects the two
|
||||
/// straights named in @p params, both belonging to @p niweleta.
|
||||
///
|
||||
/// @throws std::invalid_argument if a straight is missing, the two are the
|
||||
/// same, the straights are parallel/collinear, or the transition
|
||||
/// curves are too long for the chosen radius and deflection.
|
||||
static FitResult fit_between_straights(const Niweleta& niweleta,
|
||||
const FitParameters& params);
|
||||
|
||||
/// Computes a compound curve ("łuk koszowy") — a chain of N arcs of
|
||||
/// different neighbouring radii, same direction — between the two straights
|
||||
/// named in @p params. Both straight lines stay fixed and are re-trimmed in
|
||||
/// length; each arc's length sets its central angle, and the last arc takes
|
||||
/// the remaining deflection.
|
||||
///
|
||||
/// @throws std::invalid_argument if a straight is missing, the two are the
|
||||
/// same, there are no arcs, neighbouring radii are equal, a non-last
|
||||
/// arc has no length, the straights are parallel/collinear, or the
|
||||
/// split lengths exceed the total deflection.
|
||||
static FitResult fit_compound(const Niweleta& niweleta,
|
||||
const CompoundFitParameters& params);
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,74 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/fitting/fit_result.hpp"
|
||||
#include "maj0sted/domain/geometry/plan_element.hpp" // Straight
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
#include "maj0sted/domain/value_objects/radius.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// How the gap between two independent, finite straights should be connected.
|
||||
/// A pure domain concept: the editor's numeric fit modes map onto it, but this
|
||||
/// enum carries no UI knowledge. A plain circular arc (Arc) and a compound curve
|
||||
/// (Compound) are the only kinds — a "łuk + klotoidy" is just a Compound with a
|
||||
/// single arc eased by symmetric clothoids.
|
||||
enum class GapConnectionMode { Arc, Compound };
|
||||
|
||||
/// One arc of a compound connection.
|
||||
struct CompoundGapArc {
|
||||
double radius{0.0}; ///< arc radius (metres)
|
||||
double length{0.0}; ///< fixed arc length (metres); ignored for the last arc
|
||||
double transition_to_next{0.0}; ///< clothoid to the next arc (metres, 0 = none)
|
||||
};
|
||||
|
||||
/// Extra parameters for a compound connection: entry/exit easing clothoids plus a
|
||||
/// sequence of one or more arcs. A single arc yields "łuk + klotoidy"; two or
|
||||
/// more yield a "łuk koszowy". The last arc absorbs the remaining deflection, so
|
||||
/// its @c length / @c transition_to_next are ignored.
|
||||
struct CompoundGapParameters {
|
||||
double entry_transition{0.0};
|
||||
double exit_transition{0.0};
|
||||
std::vector<CompoundGapArc> arcs{};
|
||||
};
|
||||
|
||||
/// A request to connect two finite straights with a curve.
|
||||
struct GapConnectionRequest {
|
||||
GapConnectionMode mode{GapConnectionMode::Arc};
|
||||
double radius{0.0}; ///< plain-arc radius (metres)
|
||||
CompoundGapParameters compound{}; ///< compound only
|
||||
};
|
||||
|
||||
/// The connection actually produced.
|
||||
struct GapConnection {
|
||||
FitResult fit; ///< trimmed straights + spliced curve
|
||||
double applied_radius{0.0}; ///< radius truly used (may be reduced to fit)
|
||||
};
|
||||
|
||||
/// Domain policy connecting two independent, finite straights for the editor.
|
||||
///
|
||||
/// This is the single source of truth for the fitting business rules:
|
||||
/// * a 180° reversal is detected from the geometry and handled as a nawrót,
|
||||
/// never surfaced as a user-chosen mode;
|
||||
/// * a plain arc uses the requested radius verbatim whenever it fits; the
|
||||
/// radius is NEVER enlarged. A tighter-than-needed arc fits by extending the
|
||||
/// straights toward the corner; only a radius too large to land on the
|
||||
/// straights is reduced, to the largest one that fits. No other curve type is
|
||||
/// ever substituted;
|
||||
/// * a compound curve (one arc eased by clothoids, or a multi-arc basket) is
|
||||
/// built as specified;
|
||||
/// * a tangent point may extend a straight toward the corner, but must not cross
|
||||
/// behind its outer, anchored end.
|
||||
///
|
||||
/// Returns nullopt when nothing admissible fits (the caller then leaves the gap
|
||||
/// unconnected). Never throws.
|
||||
class GapFitter {
|
||||
public:
|
||||
[[nodiscard]] static std::optional<GapConnection> connect(
|
||||
const Straight& entry, const Straight& exit,
|
||||
const GapConnectionRequest& request);
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,60 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/geometry/plan_element.hpp"
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// How two consecutive plan elements meet.
|
||||
enum class JointContinuity {
|
||||
/// Uniform azimuth ("styczność") — the rule for every joint.
|
||||
Tangent,
|
||||
/// An explicitly marked azimuth discontinuity ("załamanie"). Allowed only
|
||||
/// between two straights that touch at different azimuths.
|
||||
AzimuthBreak,
|
||||
};
|
||||
|
||||
/// "Niweleta w planie": a directional (ordered) list of plan elements.
|
||||
///
|
||||
/// Straights are anchored in XY; the curves float and are positioned by later
|
||||
/// fitting. The list enforces the connection rules between consecutive elements:
|
||||
///
|
||||
/// * as a rule consecutive elements meet under a uniform azimuth (Tangent);
|
||||
/// * two straights may meet at different azimuths only if the joint is
|
||||
/// explicitly marked as an AzimuthBreak;
|
||||
/// * two circular arcs with the same radius and the same direction may not
|
||||
/// be adjacent (it would be a single arc).
|
||||
///
|
||||
/// Joints that involve a floating curve cannot be fully checked yet and are
|
||||
/// accepted pending the fitting step.
|
||||
class HorizontalAlignment {
|
||||
public:
|
||||
/// Appends an element. The first element ignores @p joint; every later
|
||||
/// element is validated against the current last element.
|
||||
/// @throws std::logic_error if the joint violates a connection rule.
|
||||
void append(PlanElement element, JointContinuity joint = JointContinuity::Tangent);
|
||||
|
||||
[[nodiscard]] const std::vector<PlanElement>& elements() const noexcept {
|
||||
return elements_;
|
||||
}
|
||||
/// Joint between element @p index and @p index + 1 (index < size() - 1).
|
||||
[[nodiscard]] JointContinuity joint(std::size_t index) const {
|
||||
return joints_.at(index);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool empty() const noexcept { return elements_.empty(); }
|
||||
[[nodiscard]] std::size_t size() const noexcept { return elements_.size(); }
|
||||
|
||||
/// Sum of the known element lengths; arcs without a length are skipped.
|
||||
[[nodiscard]] Length total_length() const;
|
||||
|
||||
private:
|
||||
std::vector<PlanElement> elements_;
|
||||
std::vector<JointContinuity> joints_; // size() == elements_.size() - 1
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,127 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
#include "maj0sted/domain/value_objects/azimuth.hpp"
|
||||
#include "maj0sted/domain/value_objects/cartesian_position.hpp"
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
#include "maj0sted/domain/value_objects/radius.hpp"
|
||||
#include "maj0sted/domain/value_objects/straight_id.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Direction of a horizontal turn: left ("w lewo") or right ("w prawo").
|
||||
enum class TurnDirection { Left, Right };
|
||||
|
||||
/// Krzywa w planie: odcinek prosty.
|
||||
///
|
||||
/// A straight is *anchored* by both of its XY endpoints; its azimuth and length
|
||||
/// are computed from them. During fitting a straight keeps its supporting line
|
||||
/// (position + azimuth) fixed — only its length may change as an endpoint slides
|
||||
/// along that line. The curves between straights are positioned as a result of
|
||||
/// that fitting, not the other way round.
|
||||
///
|
||||
/// A straight also carries an optional identity (StraightId). References between
|
||||
/// straight segments — e.g. parallelism — point at that id directly. Straights
|
||||
/// used purely as geometry may stay anonymous (null id).
|
||||
class Straight {
|
||||
public:
|
||||
/// Anonymous straight (null id).
|
||||
/// @throws std::invalid_argument if @p start and @p end coincide (a
|
||||
/// zero-length straight has no defined azimuth).
|
||||
Straight(CartesianPosition start, CartesianPosition end)
|
||||
: Straight{StraightId{}, start, end} {}
|
||||
|
||||
/// Identified straight.
|
||||
/// @throws std::invalid_argument if @p start and @p end coincide.
|
||||
Straight(StraightId id, CartesianPosition start, CartesianPosition end)
|
||||
: id_{id},
|
||||
start_{start},
|
||||
end_{end},
|
||||
azimuth_{Azimuth::of_direction(start, end)},
|
||||
length_{Length::from_metres(distance(start, end))} {}
|
||||
|
||||
[[nodiscard]] StraightId id() const noexcept { return id_; }
|
||||
[[nodiscard]] CartesianPosition start() const noexcept { return start_; }
|
||||
[[nodiscard]] CartesianPosition end() const noexcept { return end_; }
|
||||
[[nodiscard]] Azimuth azimuth() const noexcept { return azimuth_; }
|
||||
[[nodiscard]] Length length() const noexcept { return length_; }
|
||||
|
||||
private:
|
||||
static double distance(CartesianPosition a, CartesianPosition b) noexcept {
|
||||
const double dx = b.x() - a.x();
|
||||
const double dy = b.y() - a.y();
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
StraightId id_;
|
||||
CartesianPosition start_;
|
||||
CartesianPosition end_;
|
||||
Azimuth azimuth_;
|
||||
Length length_;
|
||||
};
|
||||
|
||||
/// Krzywa w planie: łuk kołowy o stałym promieniu.
|
||||
///
|
||||
/// A floating element: it is defined by its radius and turn direction. Its
|
||||
/// length is not essential and is therefore optional — typically it becomes
|
||||
/// known only as a resultant of fitting between the anchored straights.
|
||||
class CircularArc {
|
||||
public:
|
||||
CircularArc(Radius radius, TurnDirection direction,
|
||||
std::optional<Length> length = std::nullopt) noexcept
|
||||
: radius_{radius}, direction_{direction}, length_{length} {}
|
||||
|
||||
[[nodiscard]] Radius radius() const noexcept { return radius_; }
|
||||
[[nodiscard]] TurnDirection direction() const noexcept { return direction_; }
|
||||
[[nodiscard]] std::optional<Length> length() const noexcept { return length_; }
|
||||
|
||||
private:
|
||||
Radius radius_;
|
||||
TurnDirection direction_;
|
||||
std::optional<Length> length_;
|
||||
};
|
||||
|
||||
/// Krzywa w planie: krzywa przejściowa (klotoida).
|
||||
///
|
||||
/// A floating element defined by its length, the radii at each end and its turn
|
||||
/// direction. A missing radius (nullopt) denotes an infinite radius — a tangent
|
||||
/// to a straight — so one type covers straight->arc, arc->arc and arc->straight.
|
||||
class TransitionCurve {
|
||||
public:
|
||||
TransitionCurve(Length length, std::optional<Radius> start_radius,
|
||||
std::optional<Radius> end_radius, TurnDirection direction) noexcept
|
||||
: length_{length},
|
||||
start_radius_{start_radius},
|
||||
end_radius_{end_radius},
|
||||
direction_{direction} {}
|
||||
|
||||
[[nodiscard]] Length length() const noexcept { return length_; }
|
||||
[[nodiscard]] std::optional<Radius> start_radius() const noexcept { return start_radius_; }
|
||||
[[nodiscard]] std::optional<Radius> end_radius() const noexcept { return end_radius_; }
|
||||
[[nodiscard]] TurnDirection direction() const noexcept { return direction_; }
|
||||
|
||||
private:
|
||||
Length length_;
|
||||
std::optional<Radius> start_radius_;
|
||||
std::optional<Radius> end_radius_;
|
||||
TurnDirection direction_;
|
||||
};
|
||||
|
||||
/// Any single element of the horizontal alignment.
|
||||
using PlanElement = std::variant<Straight, CircularArc, TransitionCurve>;
|
||||
|
||||
/// Length of a plan element, if known. A circular arc may not carry one.
|
||||
[[nodiscard]] inline std::optional<Length> length_of(const PlanElement& element) {
|
||||
if (const auto* straight = std::get_if<Straight>(&element)) {
|
||||
return straight->length();
|
||||
}
|
||||
if (const auto* transition = std::get_if<TransitionCurve>(&element)) {
|
||||
return transition->length();
|
||||
}
|
||||
return std::get<CircularArc>(element).length();
|
||||
}
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,53 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
#include "maj0sted/domain/value_objects/radius.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Sense of a vertical curve: a crest ("wypukły" — hilltop) or a sag
|
||||
/// ("wklęsły" — valley).
|
||||
enum class VerticalCurvature { Crest, Sag };
|
||||
|
||||
/// Krzywa w profilu: pochylenie o stałym nachyleniu, wyrażonym w promilach (‰).
|
||||
/// Wartość dodatnia oznacza wznoszenie, ujemna — opadanie.
|
||||
class Grade {
|
||||
public:
|
||||
Grade(Length length, double slope_permille) noexcept
|
||||
: length_{length}, slope_permille_{slope_permille} {}
|
||||
|
||||
[[nodiscard]] Length length() const noexcept { return length_; }
|
||||
[[nodiscard]] double slope_permille() const noexcept { return slope_permille_; }
|
||||
|
||||
private:
|
||||
Length length_;
|
||||
double slope_permille_;
|
||||
};
|
||||
|
||||
/// Krzywa w profilu: łuk pionowy o stałym promieniu, łączący dwa pochylenia.
|
||||
class VerticalCurve {
|
||||
public:
|
||||
VerticalCurve(Length length, Radius radius, VerticalCurvature curvature) noexcept
|
||||
: length_{length}, radius_{radius}, curvature_{curvature} {}
|
||||
|
||||
[[nodiscard]] Length length() const noexcept { return length_; }
|
||||
[[nodiscard]] Radius radius() const noexcept { return radius_; }
|
||||
[[nodiscard]] VerticalCurvature curvature() const noexcept { return curvature_; }
|
||||
|
||||
private:
|
||||
Length length_;
|
||||
Radius radius_;
|
||||
VerticalCurvature curvature_;
|
||||
};
|
||||
|
||||
/// Any single element of the vertical alignment.
|
||||
using ProfileElement = std::variant<Grade, VerticalCurve>;
|
||||
|
||||
/// Length of a profile element, whatever its concrete kind.
|
||||
[[nodiscard]] inline Length length_of(const ProfileElement& element) {
|
||||
return std::visit([](const auto& e) { return e.length(); }, element);
|
||||
}
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -9,6 +9,7 @@ namespace maj0sted::domain {
|
||||
/// 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
|
||||
/// diverging curve must turn through: a turnout is 1:9, 1:12, 1:18,5 and so on.
|
||||
/// Nothing is fitted to it any more: it is what the laid path is checked against.
|
||||
class CrossingMark {
|
||||
public:
|
||||
explicit CrossingMark(double denominator) noexcept : denominator_{denominator} {}
|
||||
@@ -24,75 +25,155 @@ private:
|
||||
/// Which way the branch leaves the through track.
|
||||
enum class DivergeSide { Left, Right };
|
||||
|
||||
/// One arc of a turnout's diverging curve: its radius and — unless it is the
|
||||
/// closing arc — its fixed length and the clothoid easing it into the next arc.
|
||||
struct TurnoutArc {
|
||||
double radius{0.0};
|
||||
double length{0.0}; ///< fixed length; ignored for the closing arc
|
||||
double transition_to_next{0.0}; ///< clothoid to the next arc (0 = none)
|
||||
/// Which part of the turnout a piece of the diverging path is. The role names the
|
||||
/// piece in switch terms; it does not constrain its shape — a blade may be straight
|
||||
/// (iglica prosta sieczna) or an arc (iglica łukowa styczna).
|
||||
enum class TurnoutPart {
|
||||
PreBlade, ///< odcinek przediglicowy: PR to the blade tip, along the through track
|
||||
Blade, ///< iglica, from its (theoretical) tip to its heel
|
||||
Curve, ///< the diverging curve between the blade heel and the frog rail
|
||||
FrogRail, ///< the straight frog rail out to KR
|
||||
};
|
||||
|
||||
/// The shape of a turnout's diverging curve. One arc is the everyday turnout; two
|
||||
/// or more make a łuk koszowy. Whatever the shape, the curve turns through exactly
|
||||
/// the crossing angle — the closing (last) arc spends whatever deflection the
|
||||
/// earlier arcs and clothoids leave.
|
||||
struct DivergingCurve {
|
||||
std::vector<TurnoutArc> arcs; ///< at least one; the last one closes the turn
|
||||
double entry_transition{0.0}; ///< clothoid from the through track into the first arc
|
||||
double exit_transition{0.0}; ///< clothoid from the last arc back toward tangent
|
||||
/// One authored piece of the diverging path, in order from PR. Every length is
|
||||
/// explicit — the catalogue says it, and nothing here is solved for. A piece is a
|
||||
/// straight when both radii are zero, an arc when they are equal, a clothoid
|
||||
/// otherwise; radii are unsigned, the hand comes from DivergeSide.
|
||||
struct TurnoutPiece {
|
||||
TurnoutPart part{TurnoutPart::Curve};
|
||||
double length{0.0};
|
||||
double radius_start{0.0}; ///< 0 = straight
|
||||
double radius_end{0.0}; ///< != radius_start = clothoid
|
||||
/// Kąt nagięcia iglicy (beta): the heading break applied before this piece,
|
||||
/// radians, unsigned. Zero everywhere for a blade tangent to the stock rail.
|
||||
double turn_in{0.0};
|
||||
};
|
||||
|
||||
/// A turnout (rozjazd), described the way a catalogue does: a crossing mark, the
|
||||
/// side it opens to, the free diverging curve inside it, and the length it spans
|
||||
/// from its start (początek rozjazdu) to its end (koniec rozjazdu) measured along
|
||||
/// the through track. The curve turns to the crossing angle and a straight frog
|
||||
/// rail carries the track on to that length.
|
||||
/// Iglica — not the shape of the axis (that is a TurnoutPiece), but the two
|
||||
/// dimensions of the blade rail itself that place its real tip. A blade cannot be
|
||||
/// ground to nothing, so its tip is cut back to a thickness and the point where
|
||||
/// the axis leaves the stock rail is only a theoretical one.
|
||||
struct Blade {
|
||||
double tip_thickness{0.0}; ///< u: the blade's width where its planed nose ends
|
||||
double nose{0.0}; ///< d: the planed nose itself, from the tip to that point
|
||||
double railtop_width{0.0}; ///< w: railhead width, metres (49E1 ≈ 0,070)
|
||||
};
|
||||
|
||||
/// Half the standard gauge. The gap the blade opens is taken between two rails,
|
||||
/// and the blade's own curve is `R + s/2` where R is the diverging track's radius
|
||||
/// (Koc, 5.2), so this is what turns a gap into an angle. Stations stay on the
|
||||
/// axis: the rail radius converts, it does not measure.
|
||||
inline constexpr double kHalfGauge = 0.7175;
|
||||
|
||||
/// A turnout (rozjazd), described the way a catalogue drawing is: the crossing
|
||||
/// mark and length it is meant to work out to, and the list of pieces the
|
||||
/// diverging path is actually laid from.
|
||||
struct Turnout {
|
||||
CrossingMark mark;
|
||||
DivergeSide side{DivergeSide::Left};
|
||||
DivergingCurve curve;
|
||||
double length{0.0}; ///< catalogue length along the through track; 0 = a tangent-length rail
|
||||
/// Odcinek przediglicowy: the straight lead from the switch start to the blade
|
||||
/// tip, still along the through track. Zero for a curve tangent from the start.
|
||||
double pre_blade{0.0};
|
||||
/// Kąt nagięcia iglicy (beta), radians: the heading break at the blade tip. The
|
||||
/// diverging track leaves the through track here, not tangentially, and the
|
||||
/// curve then turns through only the remaining `alfa - beta`. Zero for a
|
||||
/// tangential (curved-blade) turnout.
|
||||
double blade_angle{0.0};
|
||||
/// The straight blade itself, from its tip to its heel where the curve begins.
|
||||
/// Zero starts the curve at the tip.
|
||||
double blade_length{0.0};
|
||||
std::vector<TurnoutPiece> path; ///< PR -> KR, in order; nothing implicit
|
||||
/// Catalogue length: a + b, the two tangent legs of the drawing — PR to the
|
||||
/// theoretical crossing point along the through track, and that point to KR
|
||||
/// along the diverging one. This is the L the type tables give (Rz 190-1:9:
|
||||
/// a 13,834 + b 13,304 = 27,138), not the projection on the through track.
|
||||
double length{0.0};
|
||||
Blade blade{};
|
||||
};
|
||||
|
||||
/// One laid segment of the diverging path, as the constant/linear curvature the
|
||||
/// renderer and the fitter already share (see segment_layout): a straight when
|
||||
/// both curvatures are zero, an arc when they are equal, a clothoid otherwise.
|
||||
/// One laid segment of the diverging path: a piece with its curvature signed for
|
||||
/// the hand it diverges to, ready for layout_segment.
|
||||
struct TurnoutSegment {
|
||||
double turn_in{0.0}; ///< signed heading break applied before laying this segment (the blade)
|
||||
double turn_in{0.0}; ///< signed heading break applied before laying this segment
|
||||
double k0{0.0};
|
||||
double k1{0.0};
|
||||
double length{0.0};
|
||||
TurnoutPart part{TurnoutPart::Curve};
|
||||
};
|
||||
|
||||
/// What a turnout works out to when laid from its start pose.
|
||||
struct TurnoutGeometry {
|
||||
bool valid{false};
|
||||
std::vector<TurnoutSegment> path; ///< diverging segments from the switch start to the frog
|
||||
geometry::Pose frog{}; ///< the end (koniec rozjazdu): position + heading at the crossing angle
|
||||
geometry::Pose frog{}; ///< the end (koniec rozjazdu): position + heading
|
||||
double tangent_front{0.0}; ///< theoretical tangent length, switch start to the crossing point
|
||||
double tangent_back{0.0}; ///< theoretical tangent length, crossing point to the frog
|
||||
double diverging_length{0.0}; ///< total length of the diverging path (curve + frog rail)
|
||||
double diverging_length{0.0}; ///< total length of the diverging path
|
||||
/// Where the turnout ends as measured on the through track: the frog projected
|
||||
/// back onto it. This is the point that closes the switch on the straight — or
|
||||
/// on the arc, when the turnout is bent.
|
||||
geometry::Pose through_end{};
|
||||
double through_length{0.0};
|
||||
|
||||
// --- łukowanie ----------------------------------------------------------
|
||||
/// Signed curvature of the through track the turnout was laid against, and
|
||||
/// what the diverging path came out at. Both zero for a rozjazd zwyczajny;
|
||||
/// `diverging_curvature` is zero too when a two-sided bend straightens the
|
||||
/// branch out. Positive turns left.
|
||||
double bend{0.0};
|
||||
double diverging_curvature{0.0};
|
||||
/// Beta: how far the through track turns over the turnout. The bend is this
|
||||
/// angle as much as it is a radius, and it is what the crossing angle is
|
||||
/// measured from once the tor zasadniczy no longer runs straight.
|
||||
double bend_angle{0.0};
|
||||
|
||||
// --- the blade, as dimensioned rather than as laid ----------------------
|
||||
// The curve is theoretically tangent to the stock rail at the switch start, but
|
||||
// the blade only begins where there is metal enough to begin it. What runs from
|
||||
// PR to that tip is the odcinek przediglicowy: the curve is already there, the
|
||||
// vehicle is still on the through track.
|
||||
bool has_blade{false};
|
||||
geometry::Pose blade_tip{}; ///< ostrze iglicy, where the blade piece starts
|
||||
geometry::Pose blade_nose_end{}; ///< A: where the planed nose ends and the blade is u thick
|
||||
geometry::Pose blade_planed{}; ///< where the blade reaches full railhead width
|
||||
double blade_station{0.0}; ///< a: PR -> the tip, the odcinek przediglicowy
|
||||
double nose_end{0.0}; ///< station of A, theoretically a + d
|
||||
double planing_length{0.0}; ///< struganie: the tip to full railhead width
|
||||
double planing_end{0.0}; ///< station where the planing ends
|
||||
double blade_angle{0.0}; ///< delta: kąt przylegania, the tangent angle at A
|
||||
/// (a + d) minus where the rail really stands `u` off the stock rail. Zero means
|
||||
/// the type's own a, d and u agree with the curve it is laid on.
|
||||
double blade_residual{0.0};
|
||||
|
||||
// --- what the catalogue numbers say about all that ----------------------
|
||||
/// Total deflection of the path minus the crossing angle. Zero = the path
|
||||
/// closes on the mark; non-zero is reported, never fitted away.
|
||||
double angle_residual{0.0};
|
||||
/// The laid tangent legs (a + b) minus the catalogue length.
|
||||
double length_residual{0.0};
|
||||
};
|
||||
|
||||
/// Lays a turnout forward from @p start — the switch start (początek rozjazdu),
|
||||
/// tangent to the through track: the pre-blade straight, the blade breaking away
|
||||
/// by `blade_angle`, the curve turning through what is left of the crossing angle,
|
||||
/// then the frog rail out to the catalogue length. Never throws; returns
|
||||
/// `valid == false` when the parameters admit no geometry: a non-positive radius,
|
||||
/// a blade angle that is negative or already reaches the crossing angle, or a
|
||||
/// curve whose fixed parts overshoot the remaining angle before the closing arc.
|
||||
[[nodiscard]] TurnoutGeometry lay_turnout(const geometry::Pose& start,
|
||||
const Turnout& turnout);
|
||||
/// How far along an axis of radius @p radius, leaving the stock rail at angle
|
||||
/// @p angle, the blade stands @p offset away from it: `R·(acos(cos β − offset/R1) − β)`
|
||||
/// for a curved blade, `offset / sin β` for a straight one. The gap opens on the
|
||||
/// blade's own curve `R1 = R + s/2`, but the answer is a station on the axis, which
|
||||
/// is what every length here is measured in.
|
||||
///
|
||||
/// This is the relation the catalogue's own numbers obey. A drawing dimensions the
|
||||
/// blade on the rail rather than on the axis, so what Koc's Tablica 5.1 gives as
|
||||
/// `a + d` is this run scaled by `R1/R` — at 1200-1:18,5 the two agree to a tenth
|
||||
/// of a millimetre. Zero when the curve neither turns nor breaks away, or when the
|
||||
/// offset is out of its reach.
|
||||
[[nodiscard]] double blade_run_to_offset(double radius, double angle, double offset);
|
||||
|
||||
/// Lays @p turnout from @p start. @p bend is the signed curvature (1/m, positive
|
||||
/// turns left) of the through track under it: zero for a rozjazd zwyczajny, and
|
||||
/// anything else for a rozjazd łukowy.
|
||||
///
|
||||
/// Bending is not adding curvature. Koc, 5.3.6: it rotates the triangle OBC about
|
||||
/// the centre of the turnout, so the crossing angle alfa and the tangent
|
||||
/// `t = R·tg(alfa/2)` come through untouched and every radius follows from them —
|
||||
/// the through track turns by `beta = 2·arctg(t·bend)`, the diverging path by
|
||||
/// `beta + alfa`, and each gets the radius `t/tg(its own angle/2)`. That is where
|
||||
/// Koc's (5.7) and (5.8) come from, and it is why a Rz 49E1-190-1:9 bent
|
||||
/// symmetrically is the catalogue's Rłs 49E1-380,292-1:9.
|
||||
///
|
||||
/// The switch and the frog are not re-cut, only bent: a straight krzyżownica stays
|
||||
/// straight and the blade keeps its own dimensions, measured — as on the ground —
|
||||
/// against the opornica that curves with it.
|
||||
///
|
||||
/// Invalid when the path has no arc to bend or its arcs disagree on a radius:
|
||||
/// there is then no single triangle to rotate, and guessing one would be a lie.
|
||||
[[nodiscard]] TurnoutGeometry lay_turnout(const geometry::Pose& start, const Turnout& turnout,
|
||||
double bend = 0.0);
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/geometry/profile_element.hpp"
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Geometria w profilu (rzut pionowy toru): uporządkowany ciąg pochyleń i łuków
|
||||
/// pionowych, gdzie każdy element zaczyna się tam, gdzie kończy się poprzedni.
|
||||
class VerticalAlignment {
|
||||
public:
|
||||
void append(ProfileElement element) { elements_.push_back(std::move(element)); }
|
||||
|
||||
[[nodiscard]] const std::vector<ProfileElement>& elements() const noexcept {
|
||||
return elements_;
|
||||
}
|
||||
[[nodiscard]] bool empty() const noexcept { return elements_.empty(); }
|
||||
[[nodiscard]] std::size_t size() const noexcept { return elements_.size(); }
|
||||
|
||||
/// Sumaryczna długość toru w profilu.
|
||||
[[nodiscard]] Length total_length() const;
|
||||
|
||||
private:
|
||||
std::vector<ProfileElement> elements_;
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/niweleta.hpp"
|
||||
#include "maj0sted/domain/value_objects/crs.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// The map project ("projekt mapy").
|
||||
///
|
||||
/// It defines the Cartesian coordinate reference system in which every position
|
||||
/// in the project is expressed (by default EPSG:2180, metres) and owns the
|
||||
/// niwelety that make up the layout.
|
||||
class MapProject {
|
||||
public:
|
||||
/// Creates a project using @p crs, or EPSG:2180 by default.
|
||||
explicit MapProject(Crs crs = Crs::default_crs());
|
||||
|
||||
[[nodiscard]] Crs crs() const noexcept { return crs_; }
|
||||
|
||||
void add_niweleta(Niweleta niweleta);
|
||||
[[nodiscard]] const std::vector<Niweleta>& niwelety() const noexcept {
|
||||
return niwelety_;
|
||||
}
|
||||
|
||||
private:
|
||||
Crs crs_;
|
||||
std::vector<Niweleta> niwelety_;
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "maj0sted/domain/fitting/fit_result.hpp"
|
||||
#include "maj0sted/domain/geometry/horizontal_alignment.hpp"
|
||||
#include "maj0sted/domain/geometry/vertical_alignment.hpp"
|
||||
#include "maj0sted/domain/value_objects/cartesian_position.hpp"
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
#include "maj0sted/domain/value_objects/niweleta_id.hpp"
|
||||
#include "maj0sted/domain/value_objects/straight_id.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Niweleta — jednolity odcinek toru złożony z wielu krzywych w planie i w
|
||||
/// profilu.
|
||||
///
|
||||
/// Aggregate Root: to granica spójności całego odcinka. Geometria w planie i w
|
||||
/// profilu opisują ten sam fizyczny tor, więc ich sumaryczne długości muszą być
|
||||
/// zgodne — pilnuje tego lengths_consistent(). Krzywe dokłada się wyłącznie
|
||||
/// przez metody agregatu, który utrzymuje ich kolejność.
|
||||
class Niweleta {
|
||||
public:
|
||||
explicit Niweleta(NiweletaId id, std::string name = {});
|
||||
|
||||
[[nodiscard]] NiweletaId id() const noexcept { return id_; }
|
||||
[[nodiscard]] const std::string& name() const noexcept { return name_; }
|
||||
|
||||
/// Dodaje prostą na końcu planu, nadając jej automatycznie StraightId,
|
||||
/// który zwraca (do późniejszych referencji: równoległość, fitowanie).
|
||||
StraightId add_straight(CartesianPosition start, CartesianPosition end,
|
||||
JointContinuity joint = JointContinuity::Tangent);
|
||||
|
||||
/// Dokłada krzywą na końcu geometrii w planie, łącząc ją z poprzednim
|
||||
/// elementem zadanym stykiem (domyślnie stycznym). Jeśli elementem jest
|
||||
/// prosta bez tożsamości, niweleta nadaje jej StraightId.
|
||||
void add_plan_element(PlanElement element,
|
||||
JointContinuity joint = JointContinuity::Tangent);
|
||||
|
||||
/// Wpina wynik fitowania w plan: podmienia dwie sąsiednie proste (entry,
|
||||
/// exit z @p result) na entry(przyciętą) → curve... → exit(przyciętą),
|
||||
/// stykami stycznymi (Tangent). Proste zachowują swoje StraightId.
|
||||
/// @throws std::invalid_argument jeśli proste nie zostaną znalezione lub nie
|
||||
/// są sąsiednie (entry bezpośrednio przed exit).
|
||||
void apply_fit(const FitResult& result);
|
||||
|
||||
/// Dokłada krzywą na końcu geometrii w profilu.
|
||||
void add_profile_element(ProfileElement element);
|
||||
|
||||
[[nodiscard]] const HorizontalAlignment& plan() const noexcept { return plan_; }
|
||||
[[nodiscard]] const VerticalAlignment& profile() const noexcept { return profile_; }
|
||||
|
||||
[[nodiscard]] Length plan_length() const { return plan_.total_length(); }
|
||||
[[nodiscard]] Length profile_length() const { return profile_.total_length(); }
|
||||
|
||||
/// Czy plan i profil opisują tor tej samej długości (z zadaną tolerancją)?
|
||||
[[nodiscard]] bool lengths_consistent(double tolerance_m = 1e-6) const;
|
||||
|
||||
private:
|
||||
StraightId next_straight_id() noexcept;
|
||||
|
||||
NiweletaId id_;
|
||||
std::string name_;
|
||||
HorizontalAlignment plan_;
|
||||
VerticalAlignment profile_;
|
||||
std::uint64_t straight_seq_{0};
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "maj0sted/domain/geometry/plan_element.hpp"
|
||||
#include "maj0sted/domain/parallelism/track_parallelism.hpp"
|
||||
#include "maj0sted/domain/value_objects/track_offset.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Domain Service: creates and reasons about parallelism between straight
|
||||
/// segments.
|
||||
class ParallelismService {
|
||||
public:
|
||||
/// Marks @p origin as the origin of a parallel relation with @p parallel,
|
||||
/// separated by @p offset. Both straights must be identified.
|
||||
/// @throws std::invalid_argument if a straight has no id or the two are the
|
||||
/// same straight.
|
||||
static TrackParallelism mark_parallel(const Straight& origin,
|
||||
const Straight& parallel,
|
||||
TrackOffset offset);
|
||||
|
||||
/// Are two straights parallel as lines (same or opposite azimuth)?
|
||||
static bool are_parallel(const Straight& a, const Straight& b,
|
||||
double tolerance_rad = 1e-9);
|
||||
|
||||
/// The line the parallel straight lies on: @p origin shifted sideways by
|
||||
/// @p offset, perpendicular to its direction of travel. Same length and
|
||||
/// azimuth as the origin; the returned straight is anonymous.
|
||||
static Straight derive_parallel_line(const Straight& origin, TrackOffset offset);
|
||||
|
||||
/// Perpendicular distance (>= 0) from @p other's start point to the
|
||||
/// (infinite) line carrying @p origin.
|
||||
static double perpendicular_distance(const Straight& origin, const Straight& other);
|
||||
|
||||
[[nodiscard]] static Straight project_onto_offset_line(const Straight& origin,
|
||||
double offset_m,
|
||||
const Straight& segment);
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include "maj0sted/domain/value_objects/straight_id.hpp"
|
||||
#include "maj0sted/domain/value_objects/track_offset.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// A reference-based relation between two straight segments, identified by their
|
||||
/// StraightId. @c origin is the authoritative reference track; @c parallel runs
|
||||
/// alongside it at @c offset (a perpendicular distance on a given side).
|
||||
///
|
||||
/// Invariants: both straights must be identified (non-null id), and a straight
|
||||
/// cannot be parallel to itself.
|
||||
class TrackParallelism {
|
||||
public:
|
||||
TrackParallelism(StraightId origin, StraightId parallel, TrackOffset offset)
|
||||
: origin_{origin}, parallel_{parallel}, offset_{offset} {
|
||||
if (origin.is_null() || parallel.is_null()) {
|
||||
throw std::invalid_argument{"Parallelism requires identified straights"};
|
||||
}
|
||||
if (origin == parallel) {
|
||||
throw std::invalid_argument{"A straight cannot be parallel to itself"};
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] StraightId origin() const noexcept { return origin_; }
|
||||
[[nodiscard]] StraightId parallel() const noexcept { return parallel_; }
|
||||
[[nodiscard]] TrackOffset offset() const noexcept { return offset_; }
|
||||
|
||||
private:
|
||||
StraightId origin_;
|
||||
StraightId parallel_;
|
||||
TrackOffset offset_;
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <compare>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "maj0sted/domain/value_objects/cartesian_position.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Value Object: a horizontal azimuth — the direction of travel in the plan,
|
||||
/// measured clockwise from north (the +y / northing axis) and normalised to
|
||||
/// [0, 2*pi) radians.
|
||||
class Azimuth {
|
||||
public:
|
||||
static Azimuth from_radians(double radians) noexcept {
|
||||
return Azimuth{normalise(radians)};
|
||||
}
|
||||
|
||||
static Azimuth from_degrees(double degrees) noexcept {
|
||||
return from_radians(degrees * std::numbers::pi / 180.0);
|
||||
}
|
||||
|
||||
/// Azimuth of the direction from @p from to @p to.
|
||||
/// @throws std::invalid_argument if the two points coincide.
|
||||
static Azimuth of_direction(CartesianPosition from, CartesianPosition to) {
|
||||
const double d_east = to.x() - from.x();
|
||||
const double d_north = to.y() - from.y();
|
||||
if (d_east == 0.0 && d_north == 0.0) {
|
||||
throw std::invalid_argument{"Azimuth is undefined for coincident points"};
|
||||
}
|
||||
return from_radians(std::atan2(d_east, d_north));
|
||||
}
|
||||
|
||||
[[nodiscard]] double radians() const noexcept { return radians_; }
|
||||
[[nodiscard]] double degrees() const noexcept {
|
||||
return radians_ * 180.0 / std::numbers::pi;
|
||||
}
|
||||
|
||||
/// Smallest absolute angular difference to @p other, in [0, pi].
|
||||
[[nodiscard]] double angular_distance(Azimuth other) const noexcept {
|
||||
double difference = std::fabs(radians_ - other.radians_);
|
||||
if (difference > std::numbers::pi) {
|
||||
difference = 2.0 * std::numbers::pi - difference;
|
||||
}
|
||||
return difference;
|
||||
}
|
||||
|
||||
friend auto operator<=>(const Azimuth&, const Azimuth&) noexcept = default;
|
||||
|
||||
private:
|
||||
explicit Azimuth(double radians) noexcept : radians_{radians} {}
|
||||
|
||||
static double normalise(double radians) noexcept {
|
||||
const double two_pi = 2.0 * std::numbers::pi;
|
||||
double result = std::fmod(radians, two_pi);
|
||||
if (result < 0.0) {
|
||||
result += two_pi;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
double radians_;
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Value Object: a position in a Cartesian coordinate reference system.
|
||||
///
|
||||
/// Coordinates are expressed in the units of the project's CRS. For the default
|
||||
/// EPSG:2180 these are metres, with @c x as easting and @c y as northing.
|
||||
class CartesianPosition {
|
||||
public:
|
||||
constexpr CartesianPosition() noexcept = default;
|
||||
constexpr CartesianPosition(double x, double y) noexcept : x_{x}, y_{y} {}
|
||||
|
||||
[[nodiscard]] constexpr double x() const noexcept { return x_; }
|
||||
[[nodiscard]] constexpr double y() const noexcept { return y_; }
|
||||
|
||||
friend constexpr bool operator==(const CartesianPosition&,
|
||||
const CartesianPosition&) noexcept = default;
|
||||
|
||||
private:
|
||||
double x_{0.0};
|
||||
double y_{0.0};
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Value Object: a Coordinate Reference System, identified by its EPSG code.
|
||||
///
|
||||
/// The default for a map project is EPSG:2180 (PUWG 1992 / Poland CS92), a
|
||||
/// Cartesian, metre-based projected system.
|
||||
class Crs {
|
||||
public:
|
||||
// EPSG code of the default map-project CRS.
|
||||
static constexpr int kDefaultEpsg = 2180;
|
||||
|
||||
explicit constexpr Crs(int epsg) noexcept : epsg_{epsg} {}
|
||||
|
||||
/// The default CRS: EPSG:2180.
|
||||
static constexpr Crs default_crs() noexcept { return Crs{kDefaultEpsg}; }
|
||||
|
||||
[[nodiscard]] constexpr int epsg() const noexcept { return epsg_; }
|
||||
|
||||
friend constexpr auto operator<=>(const Crs&, const Crs&) noexcept = default;
|
||||
|
||||
private:
|
||||
int epsg_;
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Value Object: a non-negative length, in metres (the unit of the default
|
||||
/// EPSG:2180 CRS). Lengths add and compare by value.
|
||||
class Length {
|
||||
public:
|
||||
constexpr Length() noexcept = default;
|
||||
|
||||
static constexpr Length from_metres(double metres) {
|
||||
if (metres < 0.0) {
|
||||
throw std::invalid_argument{"Length cannot be negative"};
|
||||
}
|
||||
return Length{metres};
|
||||
}
|
||||
|
||||
static constexpr Length zero() noexcept { return Length{}; }
|
||||
|
||||
[[nodiscard]] constexpr double metres() const noexcept { return metres_; }
|
||||
|
||||
[[nodiscard]] constexpr Length operator+(Length other) const noexcept {
|
||||
return Length{metres_ + other.metres_};
|
||||
}
|
||||
|
||||
friend constexpr auto operator<=>(const Length&, const Length&) noexcept = default;
|
||||
|
||||
private:
|
||||
explicit constexpr Length(double metres) noexcept : metres_{metres} {}
|
||||
|
||||
double metres_{0.0};
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Value Object: identity of a Niweleta aggregate. Cross-aggregate references
|
||||
/// (e.g. parallelism) point at niwelety by this id, never by pointer. Value 0
|
||||
/// denotes "no identity".
|
||||
class NiweletaId {
|
||||
public:
|
||||
using value_type = std::uint64_t;
|
||||
|
||||
constexpr NiweletaId() noexcept = default;
|
||||
explicit constexpr NiweletaId(value_type value) noexcept : value_{value} {}
|
||||
|
||||
[[nodiscard]] constexpr value_type value() const noexcept { return value_; }
|
||||
[[nodiscard]] constexpr bool is_null() const noexcept { return value_ == 0; }
|
||||
|
||||
friend constexpr auto operator<=>(const NiweletaId&, const NiweletaId&) noexcept = default;
|
||||
|
||||
private:
|
||||
value_type value_{0};
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
|
||||
template <>
|
||||
struct std::hash<maj0sted::domain::NiweletaId> {
|
||||
std::size_t operator()(const maj0sted::domain::NiweletaId& id) const noexcept {
|
||||
return std::hash<std::uint64_t>{}(id.value());
|
||||
}
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Value Object: a radius of curvature, in metres. Always strictly positive —
|
||||
/// the turn direction (plan) or curve sense (profile) is carried separately by
|
||||
/// the geometric element, so the radius itself is just a magnitude.
|
||||
class Radius {
|
||||
public:
|
||||
static constexpr Radius from_metres(double metres) {
|
||||
if (!(metres > 0.0)) {
|
||||
throw std::invalid_argument{"Radius must be positive"};
|
||||
}
|
||||
return Radius{metres};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr double metres() const noexcept { return metres_; }
|
||||
|
||||
friend constexpr auto operator<=>(const Radius&, const Radius&) noexcept = default;
|
||||
|
||||
private:
|
||||
explicit constexpr Radius(double metres) noexcept : metres_{metres} {}
|
||||
|
||||
double metres_;
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Value Object: identity of a straight segment. A straight that takes part in
|
||||
/// references (e.g. parallelism) carries a non-null id; anonymous straights used
|
||||
/// purely as geometry may leave it null. Value 0 denotes "no identity".
|
||||
class StraightId {
|
||||
public:
|
||||
using value_type = std::uint64_t;
|
||||
|
||||
constexpr StraightId() noexcept = default;
|
||||
explicit constexpr StraightId(value_type value) noexcept : value_{value} {}
|
||||
|
||||
[[nodiscard]] constexpr value_type value() const noexcept { return value_; }
|
||||
[[nodiscard]] constexpr bool is_null() const noexcept { return value_ == 0; }
|
||||
|
||||
friend constexpr auto operator<=>(const StraightId&, const StraightId&) noexcept = default;
|
||||
|
||||
private:
|
||||
value_type value_{0};
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
|
||||
template <>
|
||||
struct std::hash<maj0sted::domain::StraightId> {
|
||||
std::size_t operator()(const maj0sted::domain::StraightId& id) const noexcept {
|
||||
return std::hash<std::uint64_t>{}(id.value());
|
||||
}
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
|
||||
namespace maj0sted::domain {
|
||||
|
||||
/// Which side a parallel track runs on, relative to the origin straight's
|
||||
/// direction of travel: left ("po lewej") or right ("po prawej").
|
||||
enum class Side { Left, Right };
|
||||
|
||||
/// Value Object: the sideways offset of a parallel track from its origin — a
|
||||
/// strictly positive perpendicular distance plus the side it lies on.
|
||||
class TrackOffset {
|
||||
public:
|
||||
TrackOffset(Length distance, Side side) : distance_{distance}, side_{side} {
|
||||
if (!(distance.metres() > 0.0)) {
|
||||
throw std::invalid_argument{"Track offset distance must be positive"};
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] Length distance() const noexcept { return distance_; }
|
||||
[[nodiscard]] Side side() const noexcept { return side_; }
|
||||
|
||||
private:
|
||||
Length distance_;
|
||||
Side side_;
|
||||
};
|
||||
|
||||
} // namespace maj0sted::domain
|
||||
31
maj0sted/include/maj0sted/editor/chain.hpp
Normal file
31
maj0sted/include/maj0sted/editor/chain.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/geometry/segment_layout.hpp"
|
||||
#include "maj0sted/editor/model.hpp"
|
||||
#include "maj0sted/editor/solution.hpp"
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
/// What laying one track produced, and everything that went wrong doing it.
|
||||
struct TrackOutcome {
|
||||
SolvedTrack track;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
/// Lays @p track from @p start.
|
||||
///
|
||||
/// Every element states its own length, so laying is a walk: each element starts
|
||||
/// on the pose the one before it ended on, and nothing is solved for. A clothoid
|
||||
/// takes its near-end curvature from where the chain has got to, which is the
|
||||
/// only way one element depends on another.
|
||||
///
|
||||
/// Never throws. An element that cannot be laid — a non-positive length, an arc
|
||||
/// without a radius or a hand, a clothoid that changes nothing — is reported and
|
||||
/// 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);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
@@ -1,139 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/editor/solve.hpp" // PlanPoint, PlanPolyline
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
/// An anchored straight segment (two XY endpoints).
|
||||
struct StraightSpec {
|
||||
double x1{0.0}, y1{0.0}, x2{0.0}, y2{0.0};
|
||||
/// Internal tangent guide used to solve a terminal curve. It participates
|
||||
/// in fitting but is not rendered as track or exposed as an editor handle.
|
||||
bool hidden{false};
|
||||
|
||||
/// Live constraint the host reapplies before each solve (same model as the
|
||||
/// web editor's `par` / `skew`). 0 = none, 1 = parallel offset from another
|
||||
/// straight, 2 = skew 1:n from another straight.
|
||||
int rel_kind{0};
|
||||
int rel_niw{-1}; ///< index of the reference niweleta
|
||||
int rel_str{-1}; ///< index of the reference straight
|
||||
double rel_offset{0.0}; ///< parallel: signed metres (+ = left of travel)
|
||||
double rel_cot{0.0}; ///< skew: n of a 1:n turnout
|
||||
int rel_side{1}; ///< skew: +1 left / -1 right of the reference
|
||||
double rel_length{0.0}; ///< skew: length of the dependent arm
|
||||
};
|
||||
|
||||
/// One arc of a compound fit: its radius, its fixed length (ignored for the last
|
||||
/// arc, which absorbs the remaining deflection) and an optional clothoid to the
|
||||
/// next arc.
|
||||
struct CompoundArcSpec {
|
||||
double radius{0.0};
|
||||
double length{0.0};
|
||||
double transition_to_next{0.0};
|
||||
};
|
||||
|
||||
/// A fit in the gap between straight @c gap and straight @c gap + 1.
|
||||
struct GapFit {
|
||||
int gap{0};
|
||||
int mode{0}; ///< 0 none, 1 arc, 2 legacy arc+clothoids, 3 compound, 4 U-turn
|
||||
|
||||
double radius{0.0}; ///< plain arc (mode 1)
|
||||
double transition{0.0}; ///< legacy mode-2 symmetric clothoid
|
||||
|
||||
// Unified compound (mode 3): end clothoids plus one or more arcs.
|
||||
double entry_t{0.0}, exit_t{0.0};
|
||||
std::vector<CompoundArcSpec> arcs{};
|
||||
|
||||
// Legacy fixed two-arc fields, kept so old saved projects still deserialize;
|
||||
// superseded by @c arcs. Used only when @c arcs is empty.
|
||||
double r1{0.0}, arc1_len{0.0}, between{0.0}, r2{0.0};
|
||||
};
|
||||
|
||||
/// A niweleta as edited in the GUI: independent anchored straights plus fits
|
||||
/// connecting consecutive straights. Because the straights are independent, a
|
||||
/// gap can be a normal corner (straights meet at an intersection) OR a 180°
|
||||
/// reversal (parallel, offset straights) — no 180° limitation.
|
||||
struct NiweletaSpec {
|
||||
std::string name;
|
||||
std::vector<StraightSpec> straights;
|
||||
std::vector<GapFit> fits;
|
||||
};
|
||||
|
||||
struct NiweletaPolys {
|
||||
std::vector<PlanPolyline> polylines;
|
||||
/// Centreline segments after adjacent fits trim the input straights. Kept
|
||||
/// index-aligned with NiweletaSpec::straights for editor hit handles.
|
||||
std::vector<StraightSpec> rendered_straights;
|
||||
/// The authoritative fit set after solving: exactly the requested fits that
|
||||
/// actually succeeded. A requested fit that could not be produced (bad
|
||||
/// parameters or geometry) is simply absent here — the library, not the host,
|
||||
/// decides that a failed fit becomes "no fit". The host mirrors this list.
|
||||
std::vector<GapFit> applied_fits;
|
||||
/// The solved axis as one polyline, in order, with no duplicate join points.
|
||||
/// Lets a station along the niweleta be resolved to a pose (see pose_at).
|
||||
std::vector<PlanPoint> centreline;
|
||||
};
|
||||
|
||||
/// Builds each niweleta, fits every requested gap, and returns the sampled plan
|
||||
/// per niweleta. Never throws: gaps whose parameters don't admit a fit are left
|
||||
/// unconnected (the two straights simply stay as drawn).
|
||||
[[nodiscard]] std::vector<NiweletaPolys> solve_project(
|
||||
const std::vector<NiweletaSpec>& niwelety);
|
||||
|
||||
/// A switch (rozjazd): the point where a branch leaves a through niweleta. It is
|
||||
/// a relation, not a niweleta — the through track is not split. The switch is
|
||||
/// described the surveyor's way: a crossing mark 1:n (skos) that fixes the angle
|
||||
/// the branch leaves at, and an interior curve that is free to be anything — a
|
||||
/// single arc, or a compound basket (łuk koszowy). @c station marks the switch's
|
||||
/// theoretical point (środek rozjazdu, where the through and diverging tangent
|
||||
/// lines cross); the curve is fitted around it and the branch niweleta is pinned
|
||||
/// to the frog it ends on. The theoretical tangent lengths follow from the curve
|
||||
/// (see JunctionGeom) — they are read off, not set.
|
||||
struct Junction {
|
||||
int through{-1}; ///< index of the through niweleta (not modified)
|
||||
double station{0.0}; ///< theoretical point: arc length along the through centreline
|
||||
int side{0}; ///< 0 = diverges left, 1 = diverges right
|
||||
bool facing{true}; ///< true opens toward increasing station, false back
|
||||
double crossing_n{9.0}; ///< crossing mark 1:n (skos) → leaving angle atan(1/n)
|
||||
double length{0.0}; ///< catalogue length (PR→KR along the through track); 0 = just the curve
|
||||
double pre_blade{0.0}; ///< odcinek przediglicowy: straight lead PR → blade tip, metres
|
||||
double blade_angle{0.0}; ///< kąt nagięcia iglicy (beta), radians; 0 = curve tangent from PR
|
||||
double blade_length{0.0};///< the straight blade, tip → heel where the curve starts, metres
|
||||
GapFit curve; ///< the internal diverging curve: arc / compound / basket
|
||||
int branch{-1}; ///< index of the branch niweleta, pinned to the frog
|
||||
};
|
||||
|
||||
/// The solved geometry of one switch: the switch point where the branch leaves
|
||||
/// the through track, the frog it ends on, the diverging curve between them and
|
||||
/// the theoretical tangent lengths that curve works out to.
|
||||
struct JunctionGeom {
|
||||
bool valid{false};
|
||||
double px{0.0}, py{0.0}; ///< switch point (diverging leaves the through here)
|
||||
double fx{0.0}, fy{0.0}; ///< frog: where the branch niweleta is pinned
|
||||
double fhx{0.0}, fhy{0.0}; ///< unit heading the branch leaves the frog on
|
||||
double tangent_front{0.0}; ///< theoretical tangent length before the curve
|
||||
double tangent_back{0.0}; ///< theoretical tangent length after the curve
|
||||
double length{0.0}; ///< diverging curve length (switch point to frog), metres
|
||||
/// The diverging curve, as rails ready to draw.
|
||||
std::vector<PlanPolyline> polylines;
|
||||
};
|
||||
|
||||
/// A whole layout solved at once: the niwelety plus every switch. Returned by
|
||||
/// solve_layout, which additionally pins each branch's start to its frog.
|
||||
struct LayoutSolution {
|
||||
std::vector<NiweletaPolys> niwelety;
|
||||
std::vector<JunctionGeom> junctions; ///< index-aligned with the input switches
|
||||
};
|
||||
|
||||
/// Solves @p niwelety together with @p junctions: each branch niweleta's start is
|
||||
/// pinned to its switch's frog before solving, and every switch's diverging arc
|
||||
/// is computed from the through niweleta's centreline. Never throws; a switch
|
||||
/// whose through/branch is out of range or whose geometry is degenerate comes
|
||||
/// back with valid == false. Takes @p niwelety by value because it repins them.
|
||||
[[nodiscard]] LayoutSolution solve_layout(std::vector<NiweletaSpec> niwelety,
|
||||
const std::vector<Junction>& junctions);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
37
maj0sted/include/maj0sted/editor/layout.hpp
Normal file
37
maj0sted/include/maj0sted/editor/layout.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "maj0sted/domain/geometry/turnout.hpp"
|
||||
#include "maj0sted/editor/model.hpp"
|
||||
#include "maj0sted/editor/solution.hpp"
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
/// 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
|
||||
/// track's element until they really lie at the offset they were given.
|
||||
///
|
||||
/// The order is not fixed in advance — it is worked out from what refers to
|
||||
/// what. A branch anchored to a turnout's frog is laid after that turnout, which
|
||||
/// is laid after its through track, however deep that goes: a turnout on the
|
||||
/// branch of a turnout is nothing special. A reference that runs in a circle is
|
||||
/// reported and its members are left unlaid, rather than being fed through a
|
||||
/// fixed number of relaxation passes and whatever came out.
|
||||
///
|
||||
/// Nothing here writes to @p document. Never throws.
|
||||
[[nodiscard]] Solution solve(const Document& document);
|
||||
|
||||
/// The catalogue type as the geometry layer wants it: the piece list with its
|
||||
/// 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);
|
||||
|
||||
/// 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);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
217
maj0sted/include/maj0sted/editor/model.hpp
Normal file
217
maj0sted/include/maj0sted/editor/model.hpp
Normal file
@@ -0,0 +1,217 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Identity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Stable handles. Every reference in a document points at one of these, never
|
||||
/// at a position in a vector: inserting or deleting anything must not silently
|
||||
/// re-target a reference somewhere else. Ids are minted from one counter per
|
||||
/// document (Document::next_id), so they are unique across all four spaces —
|
||||
/// which makes a stray reference a lookup miss rather than a wrong hit.
|
||||
enum class ElementId : std::uint32_t { none = 0 };
|
||||
enum class TrackId : std::uint32_t { none = 0 };
|
||||
enum class TurnoutId : std::uint32_t { none = 0 };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The element
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// What an element is, geometrically. These are the three shapes
|
||||
/// `geometry::layout_segment` lays: constant zero curvature, constant non-zero
|
||||
/// curvature, linearly varying curvature.
|
||||
enum class Kind {
|
||||
Line,
|
||||
Arc,
|
||||
Clothoid,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// What holds an element
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Nothing holds it: its shape and its place are its own.
|
||||
struct Free {};
|
||||
|
||||
/// Held parallel to another track's element at a signed offset (+ = left of
|
||||
/// *that* element's travel, whichever way this one runs).
|
||||
///
|
||||
/// This is międzytorze, said once: a straight beside a straight, an arc
|
||||
/// concentric with an arc at R∓d. It fixes two things — the shape (kind, radius,
|
||||
/// hand, taken from the reference) and where the track runs across the
|
||||
/// reference. It fixes nothing along it, so the track stays where it was put and
|
||||
/// slides nowhere when the reference is edited.
|
||||
///
|
||||
/// A track that can move — one anchored at a pose of its own — is moved as one
|
||||
/// rigid piece until the held element really lies at the offset. A track pinned
|
||||
/// to a turnout's port cannot move: there the reference still fixes the shape,
|
||||
/// and the offset that actually came out is reported rather than forced.
|
||||
struct Parallel {
|
||||
ElementId ref{ElementId::none};
|
||||
double offset{0.0};
|
||||
};
|
||||
|
||||
using Hold = std::variant<Free, Parallel>;
|
||||
|
||||
/// One element of a track. It starts where the previous element ends — by pose,
|
||||
/// not by coordinates — so tangency is a property of the representation and not
|
||||
/// a rule anyone has to check or restore.
|
||||
///
|
||||
/// Everything about it is stated outright: what shape it is, how long it is, and
|
||||
/// how tightly it turns. Nothing here is left for a solver to decide — there is
|
||||
/// no "run on until you reach that line". Laying track by clicking along the
|
||||
/// ground still works, but the corner is worked out at the click and written
|
||||
/// down as numbers, so what the document says is what stands on the ground.
|
||||
struct Element {
|
||||
ElementId id{ElementId::none};
|
||||
Kind kind{Kind::Line};
|
||||
/// Arc: the radius, metres, > 0. Clothoid: the radius at its far end, with
|
||||
/// 0 meaning infinite (a clothoid running out to a straight); its near-end
|
||||
/// radius is whatever the previous element ends on. Line: ignored.
|
||||
double radius{0.0};
|
||||
/// +1 turns left, -1 turns right, 0 for a straight. Signed curvature is
|
||||
/// exactly `hand / radius`, and positive curvature turns left in
|
||||
/// `geometry::layout_segment` — so no sign is flipped anywhere downstream.
|
||||
int hand{0};
|
||||
/// Along the axis, metres, > 0.
|
||||
double length{0.0};
|
||||
/// What holds it to something else. Never changes its length.
|
||||
Hold hold{Free{}};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Anchors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Start at an explicit pose. @c az is an azimuth: radians clockwise from north,
|
||||
/// i.e. atan2(dx, dy), matching the rest of the editor.
|
||||
struct AtPose {
|
||||
double x{0.0}, y{0.0}, az{0.0};
|
||||
};
|
||||
/// Which end of a turnout a track leaves from.
|
||||
enum class Port {
|
||||
Frog, ///< KR — koniec rozjazdu, the diverging track's end
|
||||
Start, ///< PR — początek rozjazdu
|
||||
};
|
||||
/// Start at a turnout's port. This is what used to be `frog_of` plus the pinning
|
||||
/// pass: the branch no longer gets moved back into place after the fact, it was
|
||||
/// never anywhere else.
|
||||
struct AtPort {
|
||||
TurnoutId turnout{TurnoutId::none};
|
||||
Port port{Port::Frog};
|
||||
};
|
||||
|
||||
using Anchor = std::variant<AtPose, AtPort>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Track
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A niweleta: an anchor plus an ordered chain of elements. Straights, arcs and
|
||||
/// clothoids are equal citizens here — a chain may start with an arc, put two
|
||||
/// arcs back to back, or simply stop without closing onto anything.
|
||||
struct Track {
|
||||
TrackId id{TrackId::none};
|
||||
std::string name;
|
||||
Anchor anchor{AtPose{}};
|
||||
std::vector<Element> elements;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Turnouts: catalogue type + placement
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One piece of a catalogue turnout's diverging path. Mirrors
|
||||
/// domain::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
|
||||
double length{0.0}; ///< always explicit; nothing is fitted
|
||||
double radius_start{0.0}; ///< 0 = straight
|
||||
double radius_end{0.0}; ///< != radius_start = clothoid
|
||||
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.
|
||||
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
|
||||
double railtop_width{0.0}; ///< w: railhead width
|
||||
};
|
||||
|
||||
/// A catalogue turnout: the list of pieces its diverging path is laid from, plus
|
||||
/// the crossing mark and length the catalogue says it should work out to — which
|
||||
/// the solver checks the list against and never fits it to. Every number that
|
||||
/// belongs to the *type* lives here, so adding one is a catalogue change and
|
||||
/// never touches the file format for placements.
|
||||
struct TurnoutType {
|
||||
std::string name;
|
||||
double crossing_n{9.0}; ///< skos 1:n, checked against the laid deflection
|
||||
double length{0.0}; ///< catalogue length PR->KR along the through track, checked
|
||||
std::vector<TurnoutPieceSpec> pieces;
|
||||
BladeSpec blade{};
|
||||
};
|
||||
|
||||
/// An instance of a type, laid on a through track. There is nothing here to
|
||||
/// edit but where it sits — the geometry belongs to the type.
|
||||
struct TurnoutPlacement {
|
||||
TurnoutId id{TurnoutId::none};
|
||||
std::string type; ///< catalogue name
|
||||
TrackId on{TrackId::none}; ///< the through track, which is not split
|
||||
double station{0.0}; ///< PR, as arc length along the through centreline
|
||||
int hand{1}; ///< +1 diverges left, -1 diverges right
|
||||
bool facing{true}; ///< opens toward increasing station
|
||||
|
||||
/// Łukowanie. A turnout standing on a curve is bent onto it, and normally
|
||||
/// that is all there is to say: the tor zasadniczy is whatever the track does
|
||||
/// under the PR. Clearing this asks for a bend of its own instead — a rozjazd
|
||||
/// łukowy on a track drawn straight — and then `bend` is the signed curvature
|
||||
/// (1/m, positive turns left) the through track is taken to have. What the
|
||||
/// bend works out to is the solver's business and is never written back here.
|
||||
bool bend_from_track{true};
|
||||
double bend{0.0};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Document
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Everything that is authored. Nothing in here is ever written by the solver.
|
||||
struct Document {
|
||||
std::vector<TurnoutType> turnout_types;
|
||||
std::vector<TurnoutPlacement> turnouts;
|
||||
std::vector<Track> tracks;
|
||||
|
||||
double view_x{0.0}, view_y{0.0}, view_extent{0.0};
|
||||
bool origin_set{false};
|
||||
bool georeferenced{false};
|
||||
double origin_x{0.0}, origin_y{0.0};
|
||||
|
||||
/// Next handle to hand out. One counter for every id space.
|
||||
std::uint32_t next_id{1};
|
||||
};
|
||||
|
||||
/// Mints the next handle. The four overloads exist so a caller states which kind
|
||||
/// of thing it is naming.
|
||||
[[nodiscard]] ElementId mint_element(Document& document);
|
||||
[[nodiscard]] TrackId mint_track(Document& document);
|
||||
[[nodiscard]] TurnoutId mint_turnout(Document& document);
|
||||
|
||||
[[nodiscard]] const Track* find_track(const Document& document, TrackId id);
|
||||
[[nodiscard]] Track* find_track(Document& document, TrackId id);
|
||||
[[nodiscard]] const TurnoutPlacement* find_turnout(const Document& document, TurnoutId id);
|
||||
[[nodiscard]] TurnoutPlacement* find_turnout(Document& document, TurnoutId id);
|
||||
[[nodiscard]] const TurnoutType* find_type(const Document& document, const std::string& name);
|
||||
/// The element itself, wherever it is; and the track it belongs to. Null when
|
||||
/// unknown.
|
||||
[[nodiscard]] const Element* find_element(const Document& document, ElementId id);
|
||||
[[nodiscard]] const Track* find_element_track(const Document& document, ElementId id);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
35
maj0sted/include/maj0sted/editor/render.hpp
Normal file
35
maj0sted/include/maj0sted/editor/render.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/editor/solution.hpp"
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
/// 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.
|
||||
struct PlanPolyline {
|
||||
Kind kind{Kind::Line};
|
||||
ElementId element{ElementId::none};
|
||||
TrackId track{TrackId::none};
|
||||
TurnoutId turnout{TurnoutId::none};
|
||||
/// For a turnout rail: which piece of the diverging path it is, and what part
|
||||
/// that piece plays (0 przediglicowy, 1 iglica, 2 łuk, 3 krzyżownicowa), so a
|
||||
/// drawing can say every element apart. -1 on a plain track rail.
|
||||
int part{-1};
|
||||
std::size_t piece{0};
|
||||
double length{0.0};
|
||||
double radius_start{0.0}; ///< 0 == infinite
|
||||
double radius_end{0.0};
|
||||
std::vector<PlanPoint> points;
|
||||
};
|
||||
|
||||
/// Draws a solved layout as rails: two per element, a gauge apart. Turnout
|
||||
/// diverging paths are drawn too, tagged with their turnout.
|
||||
///
|
||||
/// This is presentation only. It decides nothing about geometry, and the
|
||||
/// exporter no longer reads it back — the axis it needs is in the Solution.
|
||||
[[nodiscard]] std::vector<PlanPolyline> render_rails(const Solution& solution,
|
||||
double gauge_metres = 1.5);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/editor/solve.hpp" // PlanPoint, PlanPolyline
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
/// One element of an alignment "ribbon" (element chain), defined intrinsically.
|
||||
struct RibbonElement {
|
||||
int kind{0}; ///< 0 = line, 1 = arc, 2 = spiral (clothoid)
|
||||
double length{0.0}; ///< line / spiral length (arc length is derived)
|
||||
double radius{0.0}; ///< arc radius; spiral end radius (0 = straight / infinite)
|
||||
double angle{0.0}; ///< arc central angle (radians)
|
||||
double radius_start{0.0}; ///< spiral start radius (0 = infinite)
|
||||
int dir{1}; ///< +1 left, -1 right (arc / spiral)
|
||||
};
|
||||
|
||||
/// A whole alignment defined as a start pose plus an ordered element chain,
|
||||
/// laid out head-to-tail. Handles any arc angle (including 180°+, loops) because
|
||||
/// nothing depends on tangent intersection.
|
||||
struct RibbonRequest {
|
||||
double x0{0.0};
|
||||
double y0{0.0};
|
||||
double az0{0.0}; ///< start azimuth (radians, clockwise from north)
|
||||
std::vector<RibbonElement> elements;
|
||||
};
|
||||
|
||||
/// Lays the ribbon out and returns one polyline per element, ready to draw.
|
||||
[[nodiscard]] std::vector<PlanPolyline> solve_ribbon(const RibbonRequest& request);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
118
maj0sted/include/maj0sted/editor/sketch.hpp
Normal file
118
maj0sted/include/maj0sted/editor/sketch.hpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/editor/model.hpp"
|
||||
#include "maj0sted/editor/solution.hpp"
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
/// Laying track by clicking along the ground.
|
||||
///
|
||||
/// The corner is worked out here, at the click, and written into the document as
|
||||
/// ordinary elements with ordinary lengths — a straight shortened by the
|
||||
/// tangent, an arc of the radius asked for, a straight running out to the point
|
||||
/// clicked. Nothing is left pointing at the drawing, so the numbers in the
|
||||
/// document are the numbers on the ground.
|
||||
|
||||
/// What rounding one corner costs.
|
||||
struct Corner {
|
||||
bool ok{false};
|
||||
/// How far back from the corner the arc starts, and how far past it the arc
|
||||
/// ends: R·tan(|Δ|/2). Both straights lose this much.
|
||||
double tangent{0.0};
|
||||
double arc_length{0.0}; ///< R·|Δ|
|
||||
int hand{0}; ///< +1 left, -1 right
|
||||
double deflection{0.0}; ///< Δ, radians, signed, + = left
|
||||
};
|
||||
|
||||
/// Rounds the corner between a heading in and a heading out with an arc of
|
||||
/// @p radius. Both headings are unit vectors in the plan's frame.
|
||||
///
|
||||
/// Not ok for a corner that does not bend, one that doubles back on itself, or a
|
||||
/// non-positive radius: none of those is an arc, and guessing one is how a fit
|
||||
/// used to quietly lay geometry nobody asked for.
|
||||
[[nodiscard]] Corner fit_corner(double in_hx, double in_hy, double out_hx, double out_hy,
|
||||
double radius);
|
||||
|
||||
/// The three elements a straight-arc-straight corner is made of, ready to append
|
||||
/// after @p incoming has been shortened by `corner.tangent`.
|
||||
[[nodiscard]] Element make_arc(Document& document, double radius, int hand, double length);
|
||||
[[nodiscard]] Element make_line(Document& document, double length);
|
||||
[[nodiscard]] Element make_clothoid(Document& document, double end_radius, int hand,
|
||||
double length);
|
||||
|
||||
/// Carries a change of radius or hand across the joint it belongs to.
|
||||
///
|
||||
/// A transition curve's radius is the radius it ends on, so it belongs to
|
||||
/// whatever comes after it: the arc it eases into, or nothing at all when a
|
||||
/// straight follows and it runs out flat at R=0. That is one number, not two, so
|
||||
/// editing either side of the joint carries the other with it — and the joint
|
||||
/// only ever looks forward. The transition behind an arc is the one that eases
|
||||
/// into it; the one in front of it eases into whatever is beyond, which is none
|
||||
/// of that arc's business.
|
||||
void sync_joint(Track& track, std::size_t at);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Straights as lines, and the arcs between them
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The line a straight lies on: a point on it and the direction of travel.
|
||||
///
|
||||
/// This is what a straight really is once it is drawn — where it lies and which
|
||||
/// way it points. Its length is not part of it: that falls out of where the arcs
|
||||
/// at either end come off it.
|
||||
struct Support {
|
||||
double px{0.0}, py{0.0};
|
||||
double dx{1.0}, dy{0.0};
|
||||
};
|
||||
|
||||
/// What a track hangs on: the line under every one of its straights, and where
|
||||
/// the first and last of them begin and end.
|
||||
///
|
||||
/// The corners between two straights are not in here, because they do not have
|
||||
/// to be: given the lines, each of them is worked out. A corner hanging off
|
||||
/// either end of the chain — a track that starts or finishes mid-curve — is not
|
||||
/// in here either, for the opposite reason: it has nothing on its far side to be
|
||||
/// fitted against, so it is carried along exactly as it was authored.
|
||||
struct Skeleton {
|
||||
std::vector<Support> lines;
|
||||
double start_x{0.0}, start_y{0.0}; ///< where the first straight begins
|
||||
double end_x{0.0}, end_y{0.0}; ///< where the last straight ends
|
||||
};
|
||||
|
||||
/// Pulls @p track's skeleton out of @p solved.
|
||||
///
|
||||
/// A chain must read as straights with corners between them — a corner being any
|
||||
/// run of arcs and transition curves that turns one way, one arc or a basket of
|
||||
/// them. Two straights may never touch: two straights end to end are one
|
||||
/// straight. A track with no straight at all has no skeleton, and says so.
|
||||
[[nodiscard]] bool skeleton_of(const Track& track, const SolvedTrack& solved,
|
||||
Skeleton& out, std::string& why);
|
||||
|
||||
/// Rewrites @p track to run along @p skeleton, keeping every arc's radius and
|
||||
/// hand, and every transition curve's length.
|
||||
///
|
||||
/// Each corner with a straight on both sides is fitted: the transitions turn what
|
||||
/// they turn, the arcs share out what is left of the corner in the proportions
|
||||
/// they were drawn with — so a basket curve stays that basket, tighter or wider —
|
||||
/// and the whole corner is then slid along the line it leaves until it lands on
|
||||
/// the line it joins. The turn is taken the way the arcs go, so a corner may run
|
||||
/// past a half circle, up to a full one, and nothing shortens it to the way round
|
||||
/// that happens to be shorter.
|
||||
///
|
||||
/// A corner hanging off either end of the chain keeps exactly what it was given:
|
||||
/// there is no second line to fit it against, so a track ending mid-curve simply
|
||||
/// ends where that curve takes it.
|
||||
///
|
||||
/// The skeleton's ends stay put and the anchor follows them — unless the track is
|
||||
/// pinned to a turnout's port, which is not an anchor anyone may move: a change
|
||||
/// that would drag the branch off its frog is refused instead.
|
||||
///
|
||||
/// False, with a reason, when the skeleton cannot carry the chain. Nothing is
|
||||
/// written to @p track unless all of it works out.
|
||||
[[nodiscard]] bool lay_along_skeleton(Track& track, const Skeleton& skeleton,
|
||||
std::string& why);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
169
maj0sted/include/maj0sted/editor/solution.hpp
Normal file
169
maj0sted/include/maj0sted/editor/solution.hpp
Normal file
@@ -0,0 +1,169 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/domain/geometry/segment_layout.hpp" // Pose
|
||||
#include "maj0sted/domain/geometry/turnout.hpp" // TurnoutPart
|
||||
#include "maj0sted/editor/model.hpp"
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
/// A point in the project's CRS (metres for EPSG:2180).
|
||||
struct PlanPoint {
|
||||
double x{0.0};
|
||||
double y{0.0};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Solved geometry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// What one authored element works out to on the ground. Curvatures rather than
|
||||
/// radii, because that is what `geometry::layout_segment` takes and what makes a
|
||||
/// straight (0, 0), an arc (k, k) and a clothoid (k0, k1) one shape.
|
||||
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
|
||||
double k0{0.0}; ///< signed curvature at the start (1/m)
|
||||
double k1{0.0}; ///< signed curvature at the end
|
||||
double length{0.0};
|
||||
std::vector<PlanPoint> points; ///< sampled axis, first point == start
|
||||
};
|
||||
|
||||
/// A solved track: its elements in order, plus the axis flattened into one
|
||||
/// polyline with no duplicated join points, which is what a station along the
|
||||
/// track is resolved against.
|
||||
struct SolvedTrack {
|
||||
TrackId id{TrackId::none};
|
||||
std::vector<SolvedElement> elements;
|
||||
std::vector<PlanPoint> centreline;
|
||||
domain::geometry::Pose start{};
|
||||
domain::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.
|
||||
bool complete{true};
|
||||
};
|
||||
|
||||
/// One laid segment of a turnout's diverging path.
|
||||
struct SolvedTurnoutSegment {
|
||||
domain::geometry::Pose start{};
|
||||
double k0{0.0}, k1{0.0}, length{0.0};
|
||||
domain::TurnoutPart part{domain::TurnoutPart::Curve};
|
||||
std::vector<PlanPoint> points;
|
||||
};
|
||||
|
||||
/// A construction point of a turnout: where two of its parts meet, or a point
|
||||
/// the catalogue drawing is dimensioned from. Reported rather than left to be
|
||||
/// eyeballed off the drawn curve.
|
||||
struct TurnoutMark {
|
||||
double x{0.0}, y{0.0};
|
||||
/// 0 PR, 1 ostrze iglicy (where the blade begins, the odcinek przediglicowy
|
||||
/// behind it), 2 pięta iglicy, 3 joint inside the curve, 4 end of curve /
|
||||
/// start of the frog rail, 5 KR, 6 punkt teoretyczny, 7 koniec dzioba iglicy
|
||||
/// (A, where the blade is u thick), 8 koniec strugania, 9 koniec rozjazdu on
|
||||
/// the through track (the frog projected back onto the straight).
|
||||
int kind{0};
|
||||
double station{0.0}; ///< along the diverging path from PR; 0 for the theoretical point
|
||||
};
|
||||
|
||||
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
|
||||
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
|
||||
std::vector<SolvedTurnoutSegment> path;
|
||||
std::vector<TurnoutMark> marks;
|
||||
|
||||
/// The blade as dimensioned: where its tip stands (the odcinek przediglicowy
|
||||
/// is everything before it), how long the planed nose and the planing run,
|
||||
/// the kąt przylegania, and whether the type's own a/d/u agree with the curve.
|
||||
/// Łukowanie, as laid: the signed curvature of the through track under it, the
|
||||
/// one the branch came out at, and how far the through track turns over the
|
||||
/// turnout. All zero for a rozjazd zwyczajny.
|
||||
double bend{0.0};
|
||||
double diverging_curvature{0.0};
|
||||
double bend_angle{0.0};
|
||||
|
||||
bool has_blade{false};
|
||||
double blade_station{0.0};
|
||||
double nose_end{0.0};
|
||||
double planing_length{0.0};
|
||||
double blade_angle{0.0};
|
||||
double blade_residual{0.0};
|
||||
/// What the laid path works out to, against what the catalogue promised:
|
||||
/// deflection minus the crossing angle, and through-length minus the
|
||||
/// catalogue length. Reported, never fitted away.
|
||||
double angle_residual{0.0};
|
||||
double length_residual{0.0};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diagnostics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Why something could not be laid. Reported, never silently dropped: the user
|
||||
/// gets the reason along with whatever part of the drawing did stand up.
|
||||
enum class Code {
|
||||
UnknownReference, ///< an anchor points at an id that is not there
|
||||
CyclicDependency, ///< tracks/turnouts depend on each other in a circle
|
||||
DegenerateElement, ///< non-positive radius or length, a clothoid that eases nothing
|
||||
TurnoutInvalid, ///< catalogue numbers admit no geometry
|
||||
TurnoutNotClosed, ///< the laid path misses the crossing angle or the catalogue length
|
||||
StationOffTrack, ///< a turnout sits past the end of its through track
|
||||
TouchingStraights, ///< two straights end to end, which is one straight
|
||||
HoldConflict, ///< two elements of one movable track both hold it parallel
|
||||
OffsetNotHeld, ///< a pinned track cannot move, so its offset is what it is
|
||||
};
|
||||
|
||||
[[nodiscard]] const char* code_name(Code code);
|
||||
|
||||
struct Diagnostic {
|
||||
Code code{Code::DegenerateElement};
|
||||
TrackId track{TrackId::none};
|
||||
ElementId element{ElementId::none};
|
||||
TurnoutId turnout{TurnoutId::none};
|
||||
/// Human-readable, with the number that matters in it ("R=300 m nie mieści
|
||||
/// się, maksimum 214,4 m"). The host shows this; it does not compose it.
|
||||
std::string text;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The whole solution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Everything derived. Rebuilt whole on each solve, never fed back into the
|
||||
/// document. Vectors rather than maps so iteration order is the document's and
|
||||
/// two solves of the same document compare equal.
|
||||
struct Solution {
|
||||
std::vector<SolvedTrack> tracks;
|
||||
std::vector<SolvedTurnout> turnouts;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
[[nodiscard]] const SolvedTrack* find_track(const Solution& solution, TrackId id);
|
||||
[[nodiscard]] const SolvedTurnout* find_turnout(const Solution& solution, TurnoutId id);
|
||||
[[nodiscard]] const SolvedElement* find_element(const Solution& solution, ElementId id);
|
||||
|
||||
/// 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);
|
||||
|
||||
/// 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
|
||||
/// @p curvature what it runs at (signed, positive turns left; zero on a
|
||||
/// straight). A turnout is laid as one piece of geometry, bent to the one radius
|
||||
/// under it, so it belongs on a single element and has to fit on it whole.
|
||||
/// False on a clothoid, where there is no one radius to bend to, and past the
|
||||
/// end of the track. Nothing is written then.
|
||||
[[nodiscard]] bool steady_room(const SolvedTrack& track, double station, double& curvature,
|
||||
double& behind, double& ahead);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace maj0sted::editor {
|
||||
|
||||
/// A request from the GUI: three draggable vertices (entry straight A->B, exit
|
||||
/// straight B->C) plus the fit parameters. Plain doubles, a flat DTO across the
|
||||
/// boundary between the editor UI and this solver.
|
||||
struct SolveRequest {
|
||||
double ax{0.0}, ay{0.0}; ///< vertex A (entry start)
|
||||
double bx{0.0}, by{0.0}; ///< vertex B (corner: entry end = exit start)
|
||||
double cx{0.0}, cy{0.0}; ///< vertex C (exit end)
|
||||
|
||||
int mode{0}; ///< 0 = arc, 1 = arc + symmetric transitions, 2 = compound (łuk koszowy)
|
||||
|
||||
double radius{0.0}; ///< modes 0/1: arc radius
|
||||
double transition{0.0}; ///< mode 1: symmetric transition-curve length
|
||||
|
||||
// Mode 2 (compound):
|
||||
double r1{0.0}; ///< first arc radius
|
||||
double arc1_len{0.0}; ///< first arc length (split place)
|
||||
double between{0.0}; ///< transition length between the two arcs
|
||||
double r2{0.0}; ///< second arc radius
|
||||
double entry_t{0.0}; ///< entry transition length
|
||||
double exit_t{0.0}; ///< exit transition length
|
||||
};
|
||||
|
||||
/// A point in the CRS.
|
||||
struct PlanPoint {
|
||||
double x{0.0};
|
||||
double y{0.0};
|
||||
};
|
||||
|
||||
/// One polyline; @c kind is render::ElementKind (0 straight, 1 arc, 2 transition).
|
||||
struct PlanPolyline {
|
||||
int kind{0};
|
||||
/// Logical owner used by map hit-testing. Exactly one is non-negative.
|
||||
int straight_index{-1};
|
||||
int gap{-1};
|
||||
/// Position inside a fitted curve chain (arc/clothoid/arc...).
|
||||
int element_index{-1};
|
||||
double length{0.0};
|
||||
/// Zero denotes an infinite radius (straight end of a transition).
|
||||
double radius_start{0.0};
|
||||
double radius_end{0.0};
|
||||
std::vector<PlanPoint> points;
|
||||
};
|
||||
|
||||
/// Builds the niweleta, runs the requested fit (falling back to the bare
|
||||
/// straights if the parameters do not admit a fit) and returns the sampled plan
|
||||
/// as polylines ready to draw.
|
||||
[[nodiscard]] std::vector<PlanPolyline> solve_scene(const SolveRequest& request);
|
||||
|
||||
} // namespace maj0sted::editor
|
||||
31
maj0sted/include/maj0sted/io/document_io.hpp
Normal file
31
maj0sted/include/maj0sted/io/document_io.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "maj0sted/editor/model.hpp"
|
||||
|
||||
namespace maj0sted::io {
|
||||
|
||||
/// The `.m0s` format, version 3.
|
||||
///
|
||||
/// Line-oriented text, one concept per token, every reference by id, and an
|
||||
/// explicit version on the first line. Earlier versions are not read: they
|
||||
/// described different models — anchored straights with mode-tagged fits between
|
||||
/// them (1), then elements whose lengths were solved against construction lines
|
||||
/// (2) — and translating either would have meant keeping those concepts alive in
|
||||
/// the reader after they were removed from everything else.
|
||||
///
|
||||
/// 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);
|
||||
|
||||
/// 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::string default_project_path();
|
||||
bool save(const editor::Document& document, const std::string& path = default_project_path());
|
||||
[[nodiscard]] std::optional<editor::Document> load(
|
||||
const std::string& path = default_project_path());
|
||||
|
||||
} // namespace maj0sted::io
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "maj0sted/domain/map_project.hpp"
|
||||
|
||||
namespace maj0sted::io {
|
||||
|
||||
/// Serializes a whole map project (CRS + niwelety, each with its plan and
|
||||
/// profile) to a self-contained textual format. The format is line-oriented and
|
||||
/// dependency-free; @c deserialize round-trips it exactly.
|
||||
[[nodiscard]] std::string serialize(const maj0sted::domain::MapProject& project);
|
||||
|
||||
/// Rebuilds a map project from text produced by @c serialize.
|
||||
/// @throws std::runtime_error on malformed input; std::invalid_argument if the
|
||||
/// reconstructed geometry violates a domain invariant.
|
||||
[[nodiscard]] maj0sted::domain::MapProject deserialize(const std::string& text);
|
||||
|
||||
} // namespace maj0sted::io
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/editor/editor.hpp"
|
||||
#include "maj0sted/editor/solution.hpp"
|
||||
|
||||
namespace maj0sted::io {
|
||||
|
||||
@@ -22,12 +21,18 @@ struct ScnExportResult {
|
||||
double origin_north{0.0};
|
||||
};
|
||||
|
||||
[[nodiscard]] ScnExportResult export_scn(
|
||||
const std::vector<maj0sted::editor::NiweletaPolys>& solved,
|
||||
const ScnExportOptions& options, std::ostream& out);
|
||||
/// Writes the solved layout as MaSzyna scenery. The axis comes straight out of
|
||||
/// the solution: the exporter used to average the two drawn rails back together
|
||||
/// to recover it, which put the whole geometry through the renderer and back.
|
||||
///
|
||||
/// 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::Solution& solution,
|
||||
const ScnExportOptions& options,
|
||||
std::ostream& out);
|
||||
|
||||
[[nodiscard]] ScnExportOptions resolve_scn_origin(
|
||||
const std::vector<maj0sted::editor::NiweletaPolys>& solved,
|
||||
ScnExportOptions options);
|
||||
[[nodiscard]] ScnExportOptions resolve_scn_origin(const editor::Solution& solution,
|
||||
ScnExportOptions options);
|
||||
|
||||
} // namespace maj0sted::io
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "maj0sted/render/scene.hpp"
|
||||
|
||||
namespace maj0sted::io {
|
||||
|
||||
struct SvgOptions {
|
||||
double width_px = 900.0; ///< target drawing width
|
||||
double margin_px = 24.0; ///< padding around the drawing
|
||||
double stroke_px = 2.0; ///< line width
|
||||
};
|
||||
|
||||
/// Renders a scene to a standalone SVG document (north up). Straights, arcs and
|
||||
/// transition curves are drawn in distinct colours. Dependency-free — just a
|
||||
/// simple viewer; a real GUI would consume render::Scene directly.
|
||||
[[nodiscard]] std::string to_svg(const render::Scene& scene, const SvgOptions& options = {});
|
||||
|
||||
} // namespace maj0sted::io
|
||||
@@ -1,39 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
// maj0sted — a Domain-Driven Design library for a railway track-layout editor
|
||||
// ("edytor torowiska"). Include this single header to pull in the whole public
|
||||
// domain model.
|
||||
// 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"
|
||||
|
||||
// Value Objects
|
||||
#include "maj0sted/domain/value_objects/azimuth.hpp"
|
||||
#include "maj0sted/domain/value_objects/cartesian_position.hpp"
|
||||
#include "maj0sted/domain/value_objects/crs.hpp"
|
||||
#include "maj0sted/domain/value_objects/length.hpp"
|
||||
#include "maj0sted/domain/value_objects/niweleta_id.hpp"
|
||||
#include "maj0sted/domain/value_objects/radius.hpp"
|
||||
#include "maj0sted/domain/value_objects/straight_id.hpp"
|
||||
#include "maj0sted/domain/value_objects/track_offset.hpp"
|
||||
// The authored model, and what solving it produces.
|
||||
#include "maj0sted/editor/model.hpp"
|
||||
#include "maj0sted/editor/solution.hpp"
|
||||
|
||||
// Geometry — krzywe w planie i w profilu
|
||||
#include "maj0sted/domain/geometry/horizontal_alignment.hpp"
|
||||
#include "maj0sted/domain/geometry/plan_element.hpp"
|
||||
#include "maj0sted/domain/geometry/profile_element.hpp"
|
||||
#include "maj0sted/domain/geometry/vertical_alignment.hpp"
|
||||
// Solving: one track's chain, and a whole document in dependency order.
|
||||
#include "maj0sted/editor/chain.hpp"
|
||||
#include "maj0sted/editor/layout.hpp"
|
||||
|
||||
// Aggregates
|
||||
#include "maj0sted/domain/map_project.hpp"
|
||||
#include "maj0sted/domain/niweleta.hpp"
|
||||
// Geometry primitives shared by the solver and the renderer.
|
||||
#include "maj0sted/domain/geometry/segment_layout.hpp"
|
||||
#include "maj0sted/domain/geometry/turnout.hpp"
|
||||
|
||||
// Parallelism — równoległość torów
|
||||
#include "maj0sted/domain/parallelism/parallelism_service.hpp"
|
||||
#include "maj0sted/domain/parallelism/track_parallelism.hpp"
|
||||
|
||||
// Fitting — dopasowanie krzywej między prostymi
|
||||
#include "maj0sted/domain/fitting/fit_parameters.hpp"
|
||||
#include "maj0sted/domain/fitting/fit_result.hpp"
|
||||
#include "maj0sted/domain/fitting/fitting_service.hpp"
|
||||
|
||||
// IO — serializacja projektu
|
||||
#include "maj0sted/io/project_serializer.hpp"
|
||||
// Presentation and IO.
|
||||
#include "maj0sted/editor/render.hpp"
|
||||
#include "maj0sted/io/document_io.hpp"
|
||||
#include "maj0sted/io/scn_export.hpp"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "maj0sted/domain/geometry/horizontal_alignment.hpp"
|
||||
#include "maj0sted/domain/map_project.hpp"
|
||||
#include "maj0sted/render/scene.hpp"
|
||||
|
||||
namespace maj0sted::render {
|
||||
|
||||
/// Samples a plan into absolute polylines in the CRS. Straights are
|
||||
/// authoritative for position; curves (arcs, clothoids) are laid out continuing
|
||||
/// from the previous element's end and heading. This is the reusable bridge
|
||||
/// between the domain model and any renderer.
|
||||
[[nodiscard]] Scene sample(const maj0sted::domain::HorizontalAlignment& plan);
|
||||
|
||||
/// Samples the plans of every niweleta in the project into one scene.
|
||||
[[nodiscard]] Scene sample(const maj0sted::domain::MapProject& project);
|
||||
|
||||
} // namespace maj0sted::render
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/render/track_renderer.hpp"
|
||||
@@ -12,9 +13,9 @@ namespace maj0sted::render {
|
||||
struct RailPolyline {
|
||||
ElementKind kind{ElementKind::Straight};
|
||||
std::vector<Point> points;
|
||||
int straight_index{-1};
|
||||
int gap{-1};
|
||||
int element_index{-1};
|
||||
std::uint32_t element_id{0};
|
||||
std::uint32_t owner_id{0};
|
||||
bool from_turnout{false};
|
||||
double length{0.0};
|
||||
double radius_start{0.0};
|
||||
double radius_end{0.0};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace maj0sted::render {
|
||||
|
||||
/// A point in the project's CRS (metres for EPSG:2180).
|
||||
@@ -13,21 +11,4 @@ struct Point {
|
||||
/// Which kind of plan element a polyline came from (lets a GUI colour it).
|
||||
enum class ElementKind { Straight, Arc, Transition };
|
||||
|
||||
/// A polyline approximating one plan element, in CRS coordinates.
|
||||
struct Polyline {
|
||||
ElementKind kind;
|
||||
std::vector<Point> points;
|
||||
};
|
||||
|
||||
/// A rendering-agnostic view of one or more plans: polylines plus a bounding box.
|
||||
/// Any GUI can draw this — the library computes the geometry, the GUI only paints.
|
||||
struct Scene {
|
||||
std::vector<Polyline> polylines;
|
||||
double min_x{0.0};
|
||||
double min_y{0.0};
|
||||
double max_x{0.0};
|
||||
double max_y{0.0};
|
||||
bool empty{true};
|
||||
};
|
||||
|
||||
} // namespace maj0sted::render
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/render/scene.hpp" // Point, ElementKind
|
||||
@@ -12,12 +13,12 @@ namespace maj0sted::render {
|
||||
/// hands to a renderer.
|
||||
struct CentrelineElement {
|
||||
ElementKind kind{ElementKind::Straight};
|
||||
std::vector<Point> points; ///< sampled axis in CRS metres
|
||||
int straight_index{-1}; ///< owning straight, or -1 for a curve element
|
||||
int gap{-1}; ///< owning gap, or -1 for a straight
|
||||
int element_index{-1}; ///< position within a gap's curve chain
|
||||
double length{0.0}; ///< element length (metres)
|
||||
double radius_start{0.0}; ///< 0 == infinite (straight / clothoid open end)
|
||||
std::vector<Point> points; ///< sampled axis in CRS metres
|
||||
std::uint32_t element_id{0}; ///< the authored element this came from
|
||||
std::uint32_t owner_id{0}; ///< the track or turnout it belongs to
|
||||
bool from_turnout{false}; ///< true when owner_id names a turnout
|
||||
double length{0.0}; ///< element length (metres)
|
||||
double radius_start{0.0}; ///< 0 == infinite (straight / clothoid open end)
|
||||
double radius_end{0.0};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user