mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-31 15:49:06 +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:
223
NIWELETA_REDESIGN.md
Normal file
223
NIWELETA_REDESIGN.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# 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ą.
|
||||
Reference in New Issue
Block a user