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.
12 KiB
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)
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/editor/solve.cpp src/editor/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/editor/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: trzymaCrs(domyślnie EPSG:2180) + kolekcję niwelet (add_niweleta,niwelety()) ✓Niweleta(domain/niweleta.hpp) — id, nazwa, plan + profil. Sama nadajeStraightId:add_straight(start,end,joint) -> StraightId;add_plan_elementmintuje 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) naentry → curve... → exitstykami Tangent; proste zachowują id ✓
Geometria planu (domain/geometry/)
PlanElement = variant<Straight, CircularArc, TransitionCurve>Straight— zakotwiczony w XY (start/end), azymut i długość liczone; opcjonalnyStraightId. 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 łukCompoundFitParameters { 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/editor/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::editor,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 poziomkLevel=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=10razy na kafel (licznikTileImage.attempts); po przekroczeniu limitu kafel dostaje statusFailed. 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 przezto_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 wstrzykiwanyFetcher(bez sieci). Callbackon_readybudzi 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średnifetch/emscripten_fetchz przeglądarki dostaje 404. Proxy używa UAcurl/8.5.0. Frontend ustawiaModule.wmsSetConfig({base_url: origin+'/wms'})wfinishBoot. Libka nadal robi fetch (emscripten_fetch) — tylko nalocalhost. Uruchamianie:python3 web/serve.py 8080(zamiasthttp.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ą zno-store(nie cache'ują się). Uwaga: cacheTileServicew 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 pobranieWMS_PROXY_RETRIES=6razy 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żywaio::serialize(osadza prawdziwy projekt maj0sted) + sekcjaeditorz prostymi i parametrami fitów.to_map_project/from_map_project— konwersja do/zMapProject(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 niweletapolylinesorazapplied_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 wapplied_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)iloadDefaultProject(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, azymut1e-9 rad.
Znane ograniczenia
StraightIdsą lokalne per-niweleta (licznik startuje od 1 w każdej niwelecie), więc NIE są globalnie unikalne.TrackParallelismreferuje proste po samymStraightId— 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)
- Klotoidy w łuku koszowym (między łukami i/lub na końcach). (NIE zrobione.)
- Asymetryczne klotoidy w
fit_between_straights(teraz tylko symetryczne). (NIE zrobione.) - Rejestr relacji równoległości + poprawka referencji na
(NiweletaId, StraightId)i ich serializacja. (NIE.) - Solver ustawiający prostą równoległą na
derive_parallel_linei przeliczający resztę. (NIE.) - Instalacja
cmake/ninjai budowa przez CMake (na razie tylkog++).
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-onlydo 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ę).