17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 12:09:19 +02:00
Files
maszyna/NIWELETA_REDESIGN.md
maj00r 321c9dd7ab Editor: fold the plan library into the tree as modules, drop its own name
The track layout library came in as a subproject of its own -- its own
CMakeLists, its own install and export rules, its own include tree -- which only
made the build harder to follow now that everything else is a module. It is part
of the editor, so it lives where the editor lives.

- editor/plan_*.cppm / plan_*.cpp: the fifteen interface units and twelve
  implementation units, flat in editor/ beside editorTerrain and orthophoto, with
  the plan_ prefix keeping them clear of what was already there (editor/turnouts
  is the preset catalogue, so the geometry is plan_turnout_geometry).
- modules are named eu07.editor.plan_*, like every other module in the tree.
- namespaces move under the editor's own: editor::plan, editor::plan::geometry,
  ::io, ::render. The library's old name is gone from the sources and the docs.
- every moved file carries the MPL header the rest of the tree carries; it had
  none. version.hpp went with the subproject: macros only, and unused.
- the umbrella header is dropped -- nothing imported it.
- the unit tests survive the fold: editor/tests, behind EU07_EDITOR_PLAN_TESTS
  (off by default), compiles the interfaces a second time into a small library of
  its own, because a test can only import modules from a target it links and
  linking the game would drag the whole simulator in. The code under test needs
  nothing but a compiler. 7/7 pass.
- a switch may now be written with two dashes as well: --editor beside -editor.

Builds with clang 22 (-DWITH_BETTER_RENDERER=OFF); -editor and --editor both come
up in the editor on empty ground.
2026-09-13 13:31:01 +02:00

10 KiB
Raw Blame History

Odcinki niwelety — jak to powinno być zrobione

Dokument projektowy, nie opis stanu. Siostrzany do TURNOUT_REDESIGN.md, który w §2.2 i §3.2 nazwał ten problem i odłożył go. To jest ten odłożony krok.

Teza: model nie umie powiedzieć rzeczy najprostszej — „tu zaczyna się łuk”. Wszystko inne jest skutkiem.

1. Co jest dziś

Fakty, nie opinie:

  • Krzywa nie jest bytem. Gap i jest zawsze między prostą i a i+1. Twardo założone w align_gap_fits (przepisuje fity na gęstą tablicę straights-1), w editor.cpp (fits(n-1), prosta przycinana wyłącznie przez dwa sąsiadujące fity), w basket_draft.cpp i w planpanel.cpp.
  • Stąd wyjątki: hidden prosta-prowadnica, tryb 5, i BasketDraft — czwarta reprezentacja ciągu (obok NiweletaSpec, FitResult i RibbonRequest), która przy commicie musi dorobić sztuczną 20-metrową prostą wyjściową, żeby kosz miał na czym stanąć.
  • Referencje pozycyjne. GapFit::gap, rel_niw, rel_str, frog_of, Junction::through/branch to indeksy w wektorze; wstawienie prostej wymaga ręcznego przenumerowania w dwóch pętlach. Mocne StraightId/NiweletaId istnieją w domain/value_objects/ i nie są używane.
  • Dwa źródła prawdy o współrzędnych. Dokument trzyma to, co narysowane; solver zwraca rendered_straights i pinned, a host musi wpisać pinned z powrotem do dokumentu — inaczej „dokument i rysunek się rozjeżdżają”.
  • Solver zjada intencję. Nieudany fit wraca jako nullopt (każdy throw z FittingService jest połykany przez catch (...)) i znika z applied_fits. Bez powodu, bez liczby. basket_draft musi robić próbne solve_project i sprawdzać, czy jego fit „przeżył”.
  • Brak porządku rozwiązywania. Stała pętla 3 przebiegów relaksacji i sztywne dwa przejścia w solve_layout. Rozjazd na odnodze rozjazdu nie ma prawa się zbiec.
  • Druga, martwa warstwa. domain::Niweleta + HorizontalAlignment + MapProject + cały profil: host nie zna ani jednego z tych typów, to_map_project fabrykuje placeholderowe łuki R=300 tylko po to, żeby przejść walidację złącz, a reader i tak przewija ten blok bez czytania.
  • Junction::pre_blade/blade_angle/blade_length nie są zapisywane ani czytane — po load zerują się.

2. Jak to robią inni

  • IFC 4.3 — IfcAlignmentHorizontal/Vertical/Cant, każdy jako ciąg IfcAlignmentSegment: StartDistAlong, SegmentLength, promień na początku i na końcu, typ LINE|CIRCULARARC|CLOTHOID. Styczność jest regułą walidacji między segmentami, nie własnością reprezentacji.
  • LandXML — CoordGeom = Line|Curve|Spiral; profil osobno (ProfAlign: PVI|CircCurve|ParaCurve). Poziom i pion to dwa niezależne ciągi nad wspólną stacją.
  • Bentley OpenRoads — dwa tryby autorstwa nad jednym modelem elementów: by PI (wierzchołki i promienie — nasze „proste i luki”) oraz by Element (dokładanie odcinków — nasz BasketDraft). Element niesie rules: styczny do X, przez punkt P, offset od Y. To jest brakujące ogniwo.
  • Civil 3D Rail, „Diverted Profiles” — geometria odnogi jest wyprowadzana z toru zasadniczego w rozjeździe, nie autorowana niezależnie i doklejana.
  • railML — wszystko parametryzowane pozycją wzdłuż toru, nigdy indeksem elementu.

Wniosek wspólny: odcinek (k0, k1, L) jest jednostką, stacja jest parametrem, styczność jest własnością reprezentacji, a „jak ten odcinek jest wyznaczony” jest osobnym, jawnym polem. Kernel do tego już mamy: geometry::layout_segment.

3. Model docelowy

Płasko, w editor::plan, Id jako mocne typy.

3.1. Odcinek jest bytem

enum class Kind { Line, Arc, Clothoid };

using Determination = std::variant<
    FixedLength,   // długość podana wprost
    ToLine,        // biegnij aż dojdziesz do tej prostej kierunkowej
    ToPoint,       // przez ten punkt
    Remainder      // pochłoń resztę odchylenia
>;

struct Element { ElementId id; Kind kind; double radius; int hand; Determination det; };

Odcinek zaczyna się tam, gdzie kończy się poprzedni — pozą, nie współrzędnymi. Styczność przestaje być regułą do sprawdzania i staje się własnością zapisu. Znikają JointContinuity, validate_joint, pin_branch_to_frog, rendered_straights i cały dryf.

3.2. Prosta kierunkowa oddzielona od toru

struct ConstructionLine { LineId id; double x1,y1,x2,y2; Binding binding; };

Dziś StraightSpec jest jednocześnie prostą kierunkową i kawałkiem toru — to jest źródło dwóch prawd o współrzędnych. Prosta kierunkowa nigdy nie jest torem; torem jest Element{Line, ToLine{id}}. hidden przestaje być potrzebne: prowadnica to linia, na której nie leży żaden element.

3.3. Kotwica i porty — graf zamiast przypinania

struct Track { TrackId id; std::string name; Anchor anchor; std::vector<Element> elements; };
using Anchor = std::variant<AtLine, AtPort, AtPose>;

frog_of → AtPort. Znikają place_frog_straight, LayoutSolution::pinned i zapis solvera do dokumentu.

3.4. Rozjazd: typ + umiejscowienie

TURNOUT_REDESIGN.md §3.3, teraz odblokowane — krzyżownica jest portem:

struct TurnoutType      { name, crossing_n, radius, length, pre_blade, blade_angle, blade_length };
struct TurnoutPlacement { id, type, on, station, hand, facing };

Naprawia przy okazji niezapisywane liczby iglicy: idą do katalogu, egzemplarz trzyma nazwę typu.

3.5. Solve — czysta funkcja, po Id, z diagnostyką

struct Solution { map<TrackId,SolvedTrack>; map<TurnoutId,TurnoutGeom>; vector<Diagnostic>; };
Solution solve(const Document&);
  1. Nic nie wraca do dokumentu. Host rysuje Solution i w nią trafia.
  2. Nieudany fit to diagnostyka, nie cisza. Geometria do tego odcinka zostaje położona, łańcuch urywa się i jest rysowany jako kikut, a użytkownik dostaje powód i liczbę.
  3. Kolejność z DAG-u. Zależności (kotwice, ToLine, bindingi, rozjazdy) dają graf; sortowanie topologiczne zastępuje 3 przebiegi relaksacji i sztywne dwa przejścia. Cykl → diagnostyka, nie śmieci.

3.6. Dwa tryby autorstwa nad jednym ciągiem

To jest właściwy zysk. Dokładanie odcinek po odcinku i fitowanie przestają być dwoma światami (BasketDraft kontra GapFit) i stają się dwoma sposobami wypełnienia pola Determination.

Odcinek po odcinku (FixedLength, ToPoint) — start z kotwicy, każdy kolejny odcinek styczny do pozy końcowej poprzednika. Trzy rzeczy dziś niewyrażalne wychodzą same:

  • łuk wprost z krzyżownicy — bez prostej KR, bez prowadnicy;
  • łuk po łuku bez prostej między nimi;
  • ciąg, który się nie domyka — bez sztucznej prostej wyjściowej.

Każdy dołożony odcinek idzie od razu do dokumentu, nie do bufora szkicu. Cofanie to usunięcie ostatniego elementu. Podgląd przestaje kłamać: dziś draft_to_ribbon liczy kąt każdego łuku jako L/R, a fit_compound daje ostatniemu łukowi resztę odchylenia — to są różne krzywe.

Fitowanie (ToLine, Remainder) — między dwoma odcinkami ToLine łańcuch z dokładnie jednym Remainder:

chcesz łańcuch
sam łuk (tryb 1) Arc{R, Remainder}
łuk z klotoidami (tryb 2) Clot{R,L} → Arc{R, Remainder} → Clot{∞,L}
łuk koszowy (tryb 3) Arc{R₁,L₁} → Clot → … → Arc{Rₙ, Remainder}
nawrót 180° (tryb 4) ten sam łańcuch — nawrót wynika z geometrii
S-krzywa Arc{R,+1,L} → Arc{R,-1, Remainder}

Tryby GapFit 0–4 znikają: to były nazwy dla pięciu kształtów łańcucha.

Mieszanie w jednym ciągu — dziś nie ma tego w ogóle, bo wszystko między dwiema prostymi musi być jednym GapFit:

anchor  port turnout=3 KR
elem 11 arc  190 -1 len 40      ← odcinek po odcinku, prosto z krzyżownicy
elem 12 clot 600 -1 len 30
elem 13 arc  600 -1 rem         ← domknięcie fitem…
elem 14 arc  400  1 rem         ← …dwoma łukami, bo start jest sztywny…
elem 15 line 0    0 toline 7    ← …na prostą kierunkową 7

Stopnie swobody zamiast wyjątków. Domknięcie to dwa równania: wylądować na prostej i być do niej stycznym. Łańcuch wchodzący wzdłuż prostej może się po niej przesuwać — to jeden stopień swobody — więc potrzebuje dokładnie jednego Remainder; to jest każdy dzisiejszy fit. Łańcuch wychodzący ze sztywnej pozy (kotwica, zwykle krzyżownica) przesuwać się nie może, więc potrzebuje dwóch. Łańcuch, który nie domyka się na nic — zera.

Solver to liczy i mówi wprost: za mało → „przewiązany, brakuje N stopni swobody”; za dużo → „niedookreślony, podaj długość jednego z łuków”; Remainder ≤ 0 → ile odchylenia było do rozdania i ile zużyły odcinki o zadanej długości.

Uwaga na marginesie: odchylenie bierzemy z krótkiej gałęzi atan2, więc łuk domykający nigdy nie skręci dalej, niż narożnik naprawdę się załamuje. Jazda dookoła — nawrót powyżej 180°, pętla — jest czymś, co się kładzie zadanymi długościami, nie czymś, co fit ma po cichu za nas wybrać.

3.7. Format .m0s v2

Jawny numer wersji zamiast przemianowywania tagów, wszystko po Id, bez doklejania tokenów na koniec linii. Bez zgodności wstecz.

4. Co zostaje bez zmian

Warto powiedzieć wprost, bo to nie jest przepisywanie wszystkiego:

  • geometry::layout_segment — jedyny ewaluator odcinka, zostaje.
  • domain::lay_turnout, Turnout, TurnoutGeometry — zostają.
  • Matematyka fitowania (solve_chain, fit_compound) — zmienia się interfejs (powód zamiast throw/nullopt), nie liczby.
  • ParallelismService::project_onto_offset_line — jako Binding::Parallel.
  • RailRenderer, TrackRenderer, kafle, georeferencja, eksport .scn — bez związku z modelem.

Do wyrzucenia jest klej: przypinanie, prowadnice, tryby jako wyjątki, BasketDraft i martwa warstwa agregatowa.

5. Poza zakresem

Niweleta w profilu (pion). VerticalAlignment, ProfileElement, Grade, VerticalCurve istnieją, ale nic ich nie liczy, .m0s ich nie niesie, a scn_export i tak wpisuje y=0.2 i roll=0 dla każdego węzła — mimo że format .scn niesie z na obu końcach segmentu i roll1/roll2 interpolowane liniowo. Lecą jako martwy kod i wracają jako osobny temat: ten sam kształt ciągu, o wymiar niżej, nad tą samą stacją.