17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-03 19:00:22 +01:00

Editor checkpoint: turnout domain service, web->editor rename, Id-1 templates

Checkpoint of the editor branch so work isn't carried loose across a branch
switch. Builds.

- maj0sted turnout geometry moved into the domain: domain::lay_turnout lays the
  diverging path (pre-blade lead, blade at beta, arc of alfa-beta, frog rail) and
  solve_junction is a thin adapter onto it.
- the maj0sted web/ tree renamed to editor/ (namespace and paths) - there is no
  WASM here, it's plain native C++ the editor calls.
- click places the switch at its start (początek rozjazdu); the branch's leaving
  straight is locked tangent to the frog; switches can be slid along the through
  track and flipped L/P.
- Polish turnout templates carry catalogue length (Id-1-consistent lengths), plus
  blade-angle and pre-blade fields (left 0 until the catalogue figures are in).
- TURNOUT_SPEC.md: the Bezier-based runtime/model contract.
- also folds in other pending branch changes (scenery, tests) to leave a clean tree.
This commit is contained in:
maj00r
2026-08-01 19:55:33 +02:00
committed by maj00r
parent d069743265
commit 707b4de2f4
41 changed files with 2224 additions and 471 deletions

View File

@@ -0,0 +1,58 @@
#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

View File

@@ -5,13 +5,13 @@
#include <vector>
#include "maj0sted/domain/map_project.hpp"
#include "maj0sted/web/editor.hpp"
#include "maj0sted/editor/editor.hpp"
namespace maj0sted::app {
struct EditorDocument {
std::vector<maj0sted::web::NiweletaSpec> niwelety;
std::vector<maj0sted::web::Junction> junctions;
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};

View File

@@ -0,0 +1,12 @@
#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

View File

@@ -0,0 +1,85 @@
#pragma once
#include <vector>
#include "maj0sted/domain/geometry/segment_layout.hpp" // Pose
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.
class CrossingMark {
public:
explicit CrossingMark(double denominator) noexcept : denominator_{denominator} {}
[[nodiscard]] double denominator() const noexcept { return denominator_; }
/// The angle between the tracks, radians. Zero for a non-positive mark.
[[nodiscard]] double angle() const noexcept;
private:
double denominator_;
};
/// 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)
};
/// 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
};
/// 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.
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
};
/// 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.
struct TurnoutSegment {
double k0{0.0};
double k1{0.0};
double length{0.0};
};
/// 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
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)
};
/// Lays a turnout forward from @p start — the switch start, tangent to the through
/// track — turning toward its side until the heading has swung by the crossing
/// angle, then running straight to the catalogue length. Never throws; returns
/// `valid == false` when the parameters admit no geometry: a non-positive radius,
/// or a curve whose fixed parts already overshoot the crossing angle before the
/// closing arc.
[[nodiscard]] TurnoutGeometry lay_turnout(const geometry::Pose& start,
const Turnout& turnout);
} // namespace maj0sted::domain

View File

@@ -30,6 +30,10 @@ public:
/// 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

View File

@@ -3,9 +3,9 @@
#include <string>
#include <vector>
#include "maj0sted/web/solve.hpp" // WebPoint, WebPolyline
#include "maj0sted/editor/solve.hpp" // PlanPoint, PlanPolyline
namespace maj0sted::web {
namespace maj0sted::editor {
/// An anchored straight segment (two XY endpoints).
struct StraightSpec {
@@ -63,7 +63,7 @@ struct NiweletaSpec {
};
struct NiweletaPolys {
std::vector<WebPolyline> polylines;
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;
@@ -74,7 +74,7 @@ struct NiweletaPolys {
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<WebPoint> centreline;
std::vector<PlanPoint> centreline;
};
/// Builds each niweleta, fits every requested gap, and returns the sampled plan
@@ -98,6 +98,7 @@ struct Junction {
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
GapFit curve; ///< the internal diverging curve: arc / compound / basket
int branch{-1}; ///< index of the branch niweleta, pinned to the frog
};
@@ -114,7 +115,7 @@ struct JunctionGeom {
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<WebPolyline> polylines;
std::vector<PlanPolyline> polylines;
};
/// A whole layout solved at once: the niwelety plus every switch. Returned by
@@ -132,4 +133,4 @@ struct LayoutSolution {
[[nodiscard]] LayoutSolution solve_layout(std::vector<NiweletaSpec> niwelety,
const std::vector<Junction>& junctions);
} // namespace maj0sted::web
} // namespace maj0sted::editor

View File

@@ -2,9 +2,9 @@
#include <vector>
#include "maj0sted/web/solve.hpp" // WebPoint, WebPolyline
#include "maj0sted/editor/solve.hpp" // PlanPoint, PlanPolyline
namespace maj0sted::web {
namespace maj0sted::editor {
/// One element of an alignment "ribbon" (element chain), defined intrinsically.
struct RibbonElement {
@@ -27,6 +27,6 @@ struct RibbonRequest {
};
/// Lays the ribbon out and returns one polyline per element, ready to draw.
[[nodiscard]] std::vector<WebPolyline> solve_ribbon(const RibbonRequest& request);
[[nodiscard]] std::vector<PlanPolyline> solve_ribbon(const RibbonRequest& request);
} // namespace maj0sted::web
} // namespace maj0sted::editor

View File

@@ -2,12 +2,11 @@
#include <vector>
namespace maj0sted::web {
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 so it maps trivially
/// onto a JS object / WASM boundary. This layer is pure C++ — the emscripten
/// bindings are a thin wrapper around it, and it is unit-tested natively.
/// 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)
@@ -28,13 +27,13 @@ struct SolveRequest {
};
/// A point in the CRS.
struct WebPoint {
struct PlanPoint {
double x{0.0};
double y{0.0};
};
/// One polyline; @c kind is render::ElementKind (0 straight, 1 arc, 2 transition).
struct WebPolyline {
struct PlanPolyline {
int kind{0};
/// Logical owner used by map hit-testing. Exactly one is non-negative.
int straight_index{-1};
@@ -45,12 +44,12 @@ struct WebPolyline {
/// Zero denotes an infinite radius (straight end of a transition).
double radius_start{0.0};
double radius_end{0.0};
std::vector<WebPoint> points;
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<WebPolyline> solve_scene(const SolveRequest& request);
[[nodiscard]] std::vector<PlanPolyline> solve_scene(const SolveRequest& request);
} // namespace maj0sted::web
} // namespace maj0sted::editor

View File

@@ -6,7 +6,7 @@
#include <unordered_map>
#include <vector>
namespace maj0sted::web {
namespace maj0sted::editor {
/// A stable tile in the EPSG:2180 tiling pyramid. Tiles are addressed by their
/// zoom level and integer column/row, so the same ground square always maps to
@@ -39,42 +39,43 @@ struct TileBBox {
double max_y{0.0};
};
/// The tiling scheme. A pure, static value type defining a single fixed square
/// grid of 100 m × 100 m ground cells over the EPSG:2180 plane — the same grid
/// at every zoom, *not* a per-zoom pyramid. Each cell has a deterministic ground
/// bounding box, so it is cached once and reused across all pans and zooms.
/// Nothing here touches the network — it is trivially unit testable natively.
///
/// The zoom parameters on the methods below are ignored; they are kept only so
/// the host boundary (bindings/JS) stays unchanged.
/// The tiling scheme. A pure, static value type defining a square grid over the
/// EPSG:2180 plane. The default grid is fixed 100 m × 100 m cells (key.zoom =
/// kLevel). Coarser overview grids encode the cell size in metres in
/// @c TileKey::zoom when that value is >= 100 (e.g. zoom 1000 → 1 km cells for
/// topo underlays). Nothing here touches the network.
class TileGrid {
public:
static constexpr int kTilePixels = 1024; ///< pixels per cell edge (GetMap WIDTH/HEIGHT)
static constexpr double kOriginX = 0.0; ///< easting of cell (0,0)'s left edge
static constexpr double kOriginY = 0.0; ///< northing of cell (0,0)'s bottom edge
/// Fixed cache cell size: every tile is a 100 m × 100 m ground square.
/// Default cache cell size: 100 m × 100 m ground square.
static constexpr double kCellMetres = 100.0;
/// The single grid level stored in every TileKey (there is only one grid).
/// Default grid level stored in TileKey::zoom for the 100 m grid.
static constexpr int kLevel = 0;
/// Zoom values at or above this are treated as the cell edge in metres.
static constexpr int kCellZoomMin = 100;
/// Ground resolution (metres per pixel) of a cell. Constant (zoom ignored).
/// Cell edge in metres for @p key (100 m default, or key.zoom when coarse).
[[nodiscard]] static double cell_metres(const TileKey& key) noexcept;
/// Ground resolution (metres per pixel) of a cell.
[[nodiscard]] static double resolution(int zoom = kLevel) noexcept;
/// Edge length of a cell in metres — always kCellMetres (zoom ignored).
/// Edge length of a cell in metres for the given zoom/cell encoding.
[[nodiscard]] static double tile_span(int zoom = kLevel) noexcept;
/// The 100 m cell whose ground square contains (@p x, @p y). Zoom ignored.
/// The cell whose ground square contains (@p x, @p y).
[[nodiscard]] static TileKey tile_at(double x, double y, int zoom = kLevel) noexcept;
/// The ground bounding box (EPSG:2180) of @p key.
[[nodiscard]] static TileBBox bbox(const TileKey& key) noexcept;
/// The grid level for a target resolution. Always kLevel — the grid has a
/// single fixed 100 m level. Kept for boundary compatibility.
/// The grid level for a target resolution. Always kLevel for the fine grid.
[[nodiscard]] static int zoom_for_resolution(double metres_per_pixel) noexcept;
/// Every 100 m cell whose ground square intersects @p view, capped at
/// @p max_tiles to guard against absurd requests. Zoom ignored.
/// Every cell whose ground square intersects @p view, capped at @p max_tiles.
/// Pass a coarse cell size (e.g. 1000) as @p zoom to get a 1 km overview grid.
[[nodiscard]] static std::vector<TileKey> tiles_for_view(const TileBBox& view,
int zoom = kLevel,
int max_tiles = 64);
@@ -114,10 +115,9 @@ struct TileImage {
};
/// Owns the tile grid, the tile cache and the WMS fetch. This is the single
/// place that decides which tile is needed, whether it is already cached, and —
/// on a miss — issues the network GetMap request itself. Under Emscripten the
/// fetch uses the Emscripten Fetch API; natively (and in tests) a fetcher can be
/// injected, so the tiling/caching logic is exercised without a real network.
/// place that decides which tile is needed and whether it is already cached. The
/// download itself is the host's: it injects a Fetcher, so the tiling and caching
/// logic here stays transport-free and is exercised in tests without a network.
class TileService {
public:
/// Called when a fetch finishes: (success, bytes, content_type).
@@ -131,9 +131,8 @@ public:
[[nodiscard]] const WmsConfig& config() const noexcept { return cfg_; }
void set_config(WmsConfig cfg) { cfg_ = std::move(cfg); }
/// Overrides the network backend. When set it is used on both native and
/// Emscripten builds; when unset, Emscripten builds fall back to
/// emscripten_fetch and native builds mark the tile Failed.
/// Sets the transport. When unset, a requested tile fails at once (there is
/// no built-in downloader — the host owns that).
void set_fetcher(Fetcher fetcher) { fetcher_ = std::move(fetcher); }
/// Notified (with the tile's key) whenever a tile transitions to Ready or
@@ -161,20 +160,17 @@ public:
/// Finalises an in-flight fetch: on success stores the bytes (Ready), on
/// failure marks the tile Failed, then fires the on_ready callback. Public so
/// the Emscripten fetch glue (which needs free C callbacks) can delegate here.
/// the host's fetcher callback can hand the result back here.
void complete_fetch(const TileKey& key, bool ok, std::vector<std::uint8_t> bytes,
std::string content_type);
/// Re-issues the fetch for @p key. Public so the Emscripten async-retry glue
/// (a free C callback) can delegate here.
/// Re-issues the fetch for @p key.
void retry_now(const TileKey& key);
private:
void begin_fetch(const TileKey& key);
/// Re-issues a fetch for @p key after a failure — the service retries up to
/// kMaxRetries times, then marks the tile Failed. Under Emscripten the retry
/// is deferred asynchronously (after a short delay) so it never blocks or
/// recurses; with an injected fetcher it re-issues immediately.
/// kMaxRetries times, then marks the tile Failed. The fetcher owns the timing.
void schedule_retry(const TileKey& key);
WmsConfig cfg_;
@@ -183,4 +179,4 @@ private:
std::function<void(TileKey)> on_ready_;
};
} // namespace maj0sted::web
} // namespace maj0sted::editor

View File

@@ -0,0 +1,33 @@
#pragma once
#include <ostream>
#include <string>
#include <vector>
#include "maj0sted/editor/editor.hpp"
namespace maj0sted::io {
struct ScnExportOptions {
double origin_east{0.0};
double origin_north{0.0};
double rail_y{0.2};
double max_arc_angle{1.5707963267948966};
};
struct ScnExportResult {
int tracks{0};
std::string first_track_name;
double origin_east{0.0};
double origin_north{0.0};
};
[[nodiscard]] ScnExportResult export_scn(
const std::vector<maj0sted::editor::NiweletaPolys>& solved,
const ScnExportOptions& options, std::ostream& out);
[[nodiscard]] ScnExportOptions resolve_scn_origin(
const std::vector<maj0sted::editor::NiweletaPolys>& solved,
ScnExportOptions options);
} // namespace maj0sted::io