17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-30 10:50:28 +01:00
Files
maszyna/maj0sted/HANDOFF.md
maj00r a083269867 Vendor the maj0sted track layout library and link it into the editor
Drop-in copy of C:\Users\maj00r\Projects\maj0sted-master minus its web/
directory - the WASM frontend is not being ported, the editor drives the
same C++ API natively. What is left is plain C++20 with no dependencies:
domain model (niweleta, plan and profile geometry, fitting, parallelism),
project serialization, rail sampling and the editor document.

src/web/ribbon.cpp was missing from the library source list; added, since
without it the ribbon symbols never make it into the archive.
2026-08-27 19:36:55 +02:00

185 lines
12 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.
# HANDOFF — maj0sted
Stan prac i punkt zaczepienia dla kolejnej sesji (np. po przejściu na `claude` CLI).
Kod jest samoopisowy; ten plik to skrót "gdzie jesteśmy i co dalej".
## Czym jest projekt
Statyczna biblioteka **C++20** (DDD) do edytora torowiska. Czysty model domenowy,
bez zależności zewnętrznych. Namespace: `maj0sted::domain`.
**Uwaga (sandbox):** na tej maszynie wyłączone są nieuprzywilejowane user
namespaces, więc domyślny sandbox komend pada w preflight (komenda „nie zwraca
statusu”). Uruchamiaj polecenia powłoki **z wyłączonym sandboxem** (w Cursorze:
`required_permissions: ["all"]`). Wtedy `git`, `g++`, `node` działają normalnie.
**Uwaga:** `cmake` NIE jest zainstalowany. Weryfikacja odbywa się przez `g++ 14.2`.
Toolchain WASM: `emsdk` jest w `~/emsdk` (`source ~/emsdk/emsdk_env.sh` dodaje `em++`).
## Jak weryfikować (bez cmake)
```bash
cd /home/maj00r/Projects/maj0sted
FLAGS="-std=c++20 -Iinclude -Wall -Wextra -Wpedantic -Wconversion -Wshadow"
SRC="src/domain/*.cpp src/io/*.cpp src/render/*.cpp src/web/solve.cpp src/web/tile_cache.cpp src/app/editor_document.cpp"
for t in map_project niweleta plan_alignment parallelism fitting serialization render web tile_cache document; do
g++ $FLAGS -Itests $SRC tests/test_$t.cpp -o /tmp/t_$t && /tmp/t_$t | tail -1
done
```
Wszystkie testy przechodzą, kompilacja bez ostrzeżeń. Oszczędnie: kompiluj tylko
zmieniony test z minimalnym zestawem źródeł (np. `tile_cache` wymaga tylko
`src/web/tile_cache.cpp`; `document` — `src/domain/*.cpp src/io/project_serializer.cpp
src/app/editor_document.cpp`). Szybki syntax-check: `g++ $FLAGS -fsyntax-only ...`.
Budowa WASM: `source ~/emsdk/emsdk_env.sh && cd web && ./build.sh`
(produkuje `web/maj0sted.js` + `.wasm`, oba w `.gitignore`).
## Model — co już jest (✓ = zaimplementowane + testy)
### Value Objects (`include/maj0sted/domain/value_objects/`)
- `Crs` — układ EPSG, domyślnie **EPSG:2180** (metry) ✓
- `CartesianPosition` — (x=easting, y=northing), metry, double ✓
- `Length` (≥0), `Radius` (>0) ✓
- `Azimuth` — radiany, **zgodnie z ruchem wskazówek zegara od północy (+y)**, znormalizowany [0,2π) ✓
- `NiweletaId`, `StraightId` — silne tożsamości ✓
- `TrackOffset` — dystans (>0) + `Side::Left/Right` ✓
### Agregaty
- `MapProject` — **projekt**: trzyma `Crs` (domyślnie EPSG:2180) + kolekcję niwelet
(`add_niweleta`, `niwelety()`) ✓
- `Niweleta` (`domain/niweleta.hpp`) — id, nazwa, plan + profil. **Sama nadaje `StraightId`**:
`add_straight(start,end,joint) -> StraightId`; `add_plan_element` mintuje id anonimowej prostej
(i podbija licznik przy id wczytanym z pliku). `lengths_consistent()`.
**`apply_fit(FitResult)`** — wpina wynik fitowania w plan: podmienia sąsiednie proste
(entry, exit) na `entry → curve... → exit` stykami Tangent; proste zachowują id ✓
### Geometria planu (`domain/geometry/`)
- `PlanElement = variant<Straight, CircularArc, TransitionCurve>`
- `Straight` — **zakotwiczony w XY (start/end)**, azymut i długość liczone; opcjonalny `StraightId`.
Zasada: prosta trzyma linię nośną (pozycja+azymut), zmienia się tylko długość; krzywe to wypadkowa.
- `CircularArc` — promień + kierunek + **opcjonalna** długość
- `TransitionCurve` (klotoida) — długość + promienie końcowe (`optional`=∞) + kierunek
- `HorizontalAlignment` — lista kierunkowa + `JointContinuity{Tangent, AzimuthBreak}`. Reguły styku ✓:
- sąsiednie proste Tangent: muszą się stykać i mieć równy azymut
- różny azymut prostych → wymaga `AzimuthBreak` (tylko prosta-prosta, muszą się stykać, różne azymuty)
- sąsiednie łuki o tym samym promieniu i kierunku → zabronione
- styki z krzywą akceptowane "pending fit"
### Geometria profilu
- `ProfileElement = variant<Grade, VerticalCurve>`; `VerticalAlignment`. (Profil na razie nietknięty.)
### Równoległość (`domain/parallelism/`)
- `TrackParallelism { StraightId origin, StraightId parallel, TrackOffset }` — **referencje wprost po StraightId**;
niezmienniki: oba id niepuste, różne ✓
- `ParallelismService`: `mark_parallel(originStraight, parallelStraight, offset)`, `are_parallel`,
`derive_parallel_line`, `perpendicular_distance` ✓
### Fitowanie (`domain/fitting/`)
- `FitParameters { entry, exit, radius, optional transition_length }` — klotoida symetryczna lub sam łuk
- `CompoundFitParameters { entry, exit, radius1, radius2, split_station }` — **łuk koszowy**
(miejsce podziału = kilometraż pierwszego łuku; `δ1 = split_station/R1`)
- `FitResult { entry(przycięta), curve[], exit(przycięta), tangent_in, tangent_out, direction }`
- `FittingService`:
- `fit_between_straights` — klotoida–łuk–klotoida (lub sam łuk), przycina obie proste ✓
- `fit_compound` — dwa łuki różnych promieni; **obie linie prostych bez zmian**, obie proste
doprzycięte na długości; podział ustala `δ1`. Metoda stycznej pośredniej ✓
### Serializacja (`maj0sted::io`, `include/maj0sted/io/project_serializer.hpp`)
- `serialize(MapProject) -> std::string` / `deserialize(std::string) -> MapProject` ✓
- Format tekstowy, liniowy, bez zależności; liczby przez `to_chars`/`from_chars` (dokładny round-trip,
niezależny od locale). Round-trip jest bajtowo stabilny (test to sprawdza).
- Zapisuje: CRS + niwelety (id, nazwa, plan z elementami i stykami, profil). **NIE** zapisuje relacji
równoległości (patrz niżej — StraightId nie są globalnie unikalne).
### Rendering: dwie szyny
`src/web/editor.cpp` rysuje **dwie szyny w rozstawie 1500 mm** (`kHalfGauge=0.75`,
±750 mm od osi) zamiast samej osi. `offset_points` przesuwa linię osi wzdłuż
normalnej per-wierzchołek (różnica centralna, normalna lewa `(-dy,dx)`),
`push_rails` emituje lewą i prawą szynę zachowując `kind` (kolor). Uchwyty edycji
(węzły/końce prostych) w JS dalej rysują się z osi (model).
### Warstwa web (WASM) — biblioteka robi wszystko, JS tylko rysuje
Zasada: **cała logika jest w libce**; `web/` to cienki renderer + przekazywanie
wejścia. `web/maj0sted_bindings.cpp` (embind) to jedyny plik zależny od emscripten.
- **Kafle podkładu WMS** (`maj0sted::web`, `web/tile_cache.hpp/.cpp`) ✓
- `TileGrid` — **jeden stały grid kafli 100 m × 100 m** w **EPSG:2180**
(`kCellMetres=100`, **1024 px/kafel** → 100/1024 m/px, origin (0,0), jeden
poziom `kLevel=0`, **bez piramidy per-zoom**); `tile_at`, `bbox`,
`tiles_for_view`, `zoom_for_resolution` (argument zoom ignorowany, sygnatury
zachowane dla boundary). Czysta, testowana natywnie.
- **Retry:** nieudany fetch jest ponawiany **maks. `kMaxRetries=10` razy na
kafel** (licznik `TileImage.attempts`); po przekroczeniu limitu kafel dostaje
status `Failed`. Pod Emscripten retry jest odroczony async
(`emscripten_async_call`, `kRetryDelayMs=1500`), bez blokowania/rekurencji;
z wstrzykniętym fetcherem (testy) powtarza od razu.
- `WmsConfig` + `wms_getmap_url` — WMS **1.1.1** GetMap (BBOX = minx,miny,maxx,maxy),
presety Geoportalu (ortofoto/topo). Liczby przez `to_chars`.
- `TileService` — cache + **fetch**: na trafienie zwraca z cache, na pudło sam
wykonuje GetMap. Pod `__EMSCRIPTEN__` używa **Emscripten Fetch** (`-sFETCH`);
natywnie/w testach przez wstrzykiwany `Fetcher` (bez sieci). Callback
`on_ready` budzi przerysowanie w JS.
- **Proxy WMS (`web/serve.py`):** przeglądarka pobiera kafle **same-origin**
(`/wms?...`), a lokalny serwer pobiera je serwer-serwer z Geoportalu. Powód:
WAF Geoportalu **zwraca 404 dla User-Agentów zawierających „Mozilla"** (czyli
dla prawdziwych przeglądarek), a przepuszcza proste UA (`curl`), więc bezpośredni
`fetch`/`emscripten_fetch` z przeglądarki dostaje 404. Proxy używa UA `curl/8.5.0`.
Frontend ustawia `Module.wmsSetConfig({base_url: origin+'/wms'})` w `finishBoot`.
Libka nadal robi fetch (`emscripten_fetch`) — tylko na `localhost`.
Uruchamianie: `python3 web/serve.py 8080` (zamiast `http.server`).
- **Cache na dysku:** proxy odsyła udane kafle z `Cache-Control: public,
max-age=31536000, immutable`, więc przeglądarka trzyma je w HTTP cache na dysku —
po odświeżeniu strony kafle są brane z cache, bez ponownego pobierania. Błędy
(`502`) idą z `no-store` (nie cache'ują się). Uwaga: cache `TileService` w C++ jest
tylko w pamięci WASM (ginie przy refreshu) — trwałość zapewnia właśnie HTTP cache.
- **Retry w proxy:** WAF Geoportalu bywa niestabilny (sporadyczne 404/reset nawet dla
UA `curl`), więc proxy ponawia pobranie `WMS_PROXY_RETRIES=6` razy zanim zwróci 502;
do tego dochodzi client-side retry libki (10×).
- **Trwałość projektu** (`maj0sted::app`, `app/editor_document.hpp/.cpp`) ✓
- `EditorDocument` = model GUI (niezależne proste + parametry fitów) — bezstratny.
- `serialize_document`/`deserialize_document` — **reużywa `io::serialize`** (osadza
prawdziwy projekt maj0sted) + sekcja `editor` z prostymi i parametrami fitów.
- `to_map_project`/`from_map_project` — konwersja do/z `MapProject` (best-effort,
nie rzuca).
- `save_project`/`load_project` — **I/O plikowe w libce** (`std::fstream` +
`std::filesystem`), domyślna ścieżka `/data/maj0sted/project.m0s`.
- **Fit nieudany → „brak" (decyzja w libce):** `solve_project` (editor) zwraca per
niweleta `polylines` **oraz `applied_fits`** — autorytatywną listę fitów, które
faktycznie zaszły. Fit, którego nie da się policzyć (zły parametr/geometria, punkty
styczne poza odcinkami prostych), jest **odrzucany w libce** i nie ma go w `applied_fits`.
Frontend tylko lustrzanie przyjmuje tę listę (bez własnej reguły o porażce).
- **Boundary (embind, cienki):** `solveProject`, `wmsTilesForView`,
`wmsZoomForResolution`, `wmsTileBytes`, `wmsSetOnReady`, `wmsSetConfig`,
`saveProject`, `loadDefaultProject`.
- **Frontend (`web/index.html`):** rysuje podkład WMS pod geometrią (georeferencja
EPSG:2180, trzyma się przy pan/zoom); `Ctrl+S` → `saveProject` + `FS.syncfs`;
na starcie montuje **IDBFS** w `/data`, `syncfs(true)` i `loadDefaultProject`
(fallback: demo). Flow budowy: prosta → krzywa (czeka jako „pending”, rysowana
przerywaną) → kolejna prosta kotwiczy ją z obu stron i **dopiero wtedy** liczony
jest fit.
## Konwencje geometryczne
- EPSG:2180, jednostka = metr; kierunek jazdy `d = (sin az, cos az)`; normalna lewa `(-cos az, sin az)`.
- Tolerancje: pozycja `1e-6 m`, azymut `1e-9 rad`.
## Znane ograniczenia
- **`StraightId` są lokalne per-niweleta** (licznik startuje od 1 w każdej niwelecie), więc NIE są
globalnie unikalne. `TrackParallelism` referuje proste po samym `StraightId` — działa dla luźnych
obiektów, ale w projekcie z wieloma niweletami trzeba by referować `(NiweletaId, StraightId)`.
Do przemyślenia, jeśli równoległość ma być trwała/serializowana.
## Następne kroki (do decyzji / do zrobienia)
1. **Klotoidy w łuku koszowym** (między łukami i/lub na końcach). (NIE zrobione.)
2. Asymetryczne klotoidy w `fit_between_straights` (teraz tylko symetryczne). (NIE zrobione.)
3. **Rejestr relacji równoległości** + poprawka referencji na `(NiweletaId, StraightId)` i ich serializacja. (NIE.)
4. **Solver** ustawiający prostą równoległą na `derive_parallel_line` i przeliczający resztę. (NIE.)
5. Instalacja `cmake`/`ninja` i budowa przez CMake (na razie tylko `g++`).
## Preferencje pracy (ustalone z użytkownikiem)
- **Nie edytować `README.md`** (utrzymuje go użytkownik).
- Robić **tylko to, o co proszono** — bez dokładania funkcji z własnej inicjatywy.
- **Oszczędzać CPU**: kompilować tylko zmieniony test, `-fsyntax-only` do szybkiej walidacji,
pełne testy na koniec/na żądanie; bez procesów w tle, przeglądarki i podagentów.
- Język domeny: **niweleta** = jednolity odcinek toru złożony z wielu krzywych w planie i profilu.
## Git
Repozytorium zainicjalizowane; pliki dodane do stage'a, **brak commitów** (commit dopiero na wyraźną prośbę).