17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-28 13:27:30 +01:00
Files
maszyna/NIWELETA_REDESIGN.md
maj00r 2376e4867b 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.
2026-08-29 19:32:46 +02:00

224 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `maj0sted::editor`, Id jako mocne typy.
### 3.1. Odcinek jest bytem
```cpp
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
```cpp
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
```cpp
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:
```cpp
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ą
```cpp
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ą.