mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-31 19:19:03 +01:00
Editor checkpoint: turnout domain service, web->editor rename, Id-1 templates
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.
This commit is contained in:
176
TURNOUT_SPEC.md
Normal file
176
TURNOUT_SPEC.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Rozjazd — kontrakt geometrii dla runtime i modeli
|
||||||
|
|
||||||
|
Jak edytor przekazuje rozjazd, a runtime ustawia z tego elementy. Podstawą są
|
||||||
|
**krzywe Béziera** (kubiczne). To jest warstwa graniczna: wewnątrz edytor liczy
|
||||||
|
łuki i klotoidy (kolejowo dokładne), a na wyjściu przybliża je Bézierami z
|
||||||
|
zadaną tolerancją. Runtime nie zna „typów" rozjazdów — dostaje instancję i sadzi
|
||||||
|
po niej elementy.
|
||||||
|
|
||||||
|
## Zasada
|
||||||
|
|
||||||
|
Runtime **nie bazuje na szablonie** (Rz 1:9 R190 itd.). Szablony to tylko preset
|
||||||
|
w edytorze, który wypełnia liczby. Każdy rozjazd to instancja o dowolnej
|
||||||
|
geometrii (własny skos, promień, przesunięty początek, łuk koszowy w środku).
|
||||||
|
Runtime dostaje **rozwiązaną geometrię** i z niej wszystko wyprowadza.
|
||||||
|
|
||||||
|
## Układ i jednostki
|
||||||
|
|
||||||
|
- metry; krzywe w XY (płaszczyzna planu),
|
||||||
|
- rozstaw toru 1435 mm → półrozstaw **0,7175 m**,
|
||||||
|
- pozy podawane jako pozycja + jednostkowy kierunek `(hx, hy)` (styczna, „w
|
||||||
|
kierunku jazdy"). Normalna lewa = `(-hy, hx)`,
|
||||||
|
- każdy element modelu ma origin w punkcie wstawienia, **+X = kierunek jazdy**,
|
||||||
|
**+Y = w lewo**, **+Z = w górę**. Runtime wstawia go podając tylko pozę —
|
||||||
|
żadnego per-model obrotu.
|
||||||
|
|
||||||
|
## Instancja rozjazdu
|
||||||
|
|
||||||
|
```
|
||||||
|
Turnout {
|
||||||
|
side : Left | Right // strona odgałęzienia (mirror)
|
||||||
|
crossing_mark : n // skos 1:n -> dobór odlewu krzyżownicy
|
||||||
|
rail_profile : "49E1" | "60E1" // -> dobór iglic / profilu szyny
|
||||||
|
gauge : 1.435
|
||||||
|
|
||||||
|
PR : Pose // początek rozjazdu: styczny do toru zasadniczego
|
||||||
|
KR : Pose // koniec (krzyżownica): kierunek = tor zasadniczy obrócony o alfa
|
||||||
|
crossing_angle : alfa = atan(1/n) // radiany
|
||||||
|
|
||||||
|
blade_angle : beta // kąt nagięcia iglicy (kąt zwrotnicowy)
|
||||||
|
pre_blade_length : d // odcinek przediglicowy: prosty, PR -> ostrze iglicy
|
||||||
|
|
||||||
|
through_axis : [Bezier...] // tor zasadniczy przez rozjazd (zwykle prosty)
|
||||||
|
diverging_axis: [Bezier...] // przediglicowy -> iglica(prosta @ beta) -> luk -> prosta -> KR
|
||||||
|
}
|
||||||
|
|
||||||
|
Pose { x, y, hx, hy } // pozycja + jednostkowa styczna
|
||||||
|
Bezier { p0, p1, p2, p3 } // kubiczna, punkty kontrolne w XY (metry)
|
||||||
|
```
|
||||||
|
|
||||||
|
Tor zasadniczy nie jest dzielony — `through_axis` jest podany dla kompletności
|
||||||
|
(runtime często ma już tor główny). Rozjazd wnosi przede wszystkim
|
||||||
|
`diverging_axis` + osprzęt + podrozjazdnice spinające oba tory.
|
||||||
|
|
||||||
|
## Początek odnogi: odcinek przediglicowy + iglica
|
||||||
|
|
||||||
|
Odnoga **nie zaczyna się łukiem**. To zwrotnica sieczna z iglicą prostą:
|
||||||
|
|
||||||
|
1. **odcinek przediglicowy** — prosta w kierunku toru zasadniczego (kąt 0), od
|
||||||
|
`PR` do ostrza iglicy. Tu rozjazd jest jeszcze równoległy do toru głównego;
|
||||||
|
tu siedzi napęd.
|
||||||
|
2. **iglica** — **prosta odcięta pod kątem `beta`** („prosta dogięta do
|
||||||
|
prostego"). Odnoga odchodzi od toru zasadniczego dopiero tutaj, o kąt nagięcia
|
||||||
|
iglicy `beta`.
|
||||||
|
3. **łuk** — promień `R`, skręca od `beta` do kąta krzyżownicy `alfa`, czyli o
|
||||||
|
**`alfa − beta`** (nie o całe `alfa`).
|
||||||
|
4. **prosta krzyżownicowa** — pod kątem `alfa`, do `KR`.
|
||||||
|
|
||||||
|
Stąd `diverging_axis` = `[przediglicowy] [iglica @ beta] [łuk] [prosta] `. Iglice
|
||||||
|
łukowe (styczne, `beta ≈ 0`, ostrze wtopione w łuk) to inny wariant — na razie
|
||||||
|
modelujemy iglicę prostą.
|
||||||
|
|
||||||
|
**Model:** zespół iglic kotwiony w ostrzu (`PR` + `pre_blade_length` wzdłuż toru
|
||||||
|
głównego), zorientowany wzdłuż toru zasadniczego, nadaje `beta`; długość iglicy
|
||||||
|
jest po stronie modelu.
|
||||||
|
|
||||||
|
## Bézier — definicja i próbkowanie
|
||||||
|
|
||||||
|
Kubiczna:
|
||||||
|
|
||||||
|
```
|
||||||
|
B(t) = (1-t)^3 p0 + 3(1-t)^2 t p1 + 3(1-t) t^2 p2 + t^3 p3, t in [0,1]
|
||||||
|
B'(t) = 3(1-t)^2 (p1-p0) + 6(1-t)t (p2-p1) + 3t^2 (p3-p2)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **pozycja** = `B(t)`, **kierunek** = `normalize(B'(t))`, `heading = atan2(B'y, B'x)`.
|
||||||
|
- styczna na końcach: w `t=0` wzdłuż `p1-p0`, w `t=1` wzdłuż `p3-p2` — dlatego
|
||||||
|
uchwyty `p1`, `p2` leżą **na stycznych toru** w węzłach,
|
||||||
|
- kolejne segmenty są **G1** (styczne się zgadzają) — geometria edytora jest
|
||||||
|
styczna z konstrukcji, więc łączenia są gładkie.
|
||||||
|
|
||||||
|
**Uwaga o parametryzacji:** `t` **nie jest** długością łuku. Do równego rozstawu
|
||||||
|
podrozjazdnic/podkładów trzeba reparametryzować po długości: całkować `|B'(t)|`
|
||||||
|
(albo próbkować adaptacyjnie) i szukać `t` dla zadanej odległości. Nie rozstawiaj
|
||||||
|
elementów po stałym kroku `t`.
|
||||||
|
|
||||||
|
## Skąd biorą się Béziery (aproksymacja)
|
||||||
|
|
||||||
|
**Reguła: jeden element osi = jeden Bézier.** Bez dzielenia. Węzły wyznaczają
|
||||||
|
`p0`/`p3` i styczne w węzłach, uchwyty leżą na tych stycznych:
|
||||||
|
|
||||||
|
- **prosta** → `p1`, `p2` na linii między `p0` a `p3` (uchwyty w 1/3 długości).
|
||||||
|
- **łuk** kąta `theta`, promień `R` → uchwyty wzdłuż stycznych o długości
|
||||||
|
|
||||||
|
```
|
||||||
|
k = (4/3) * R * tan(theta/4)
|
||||||
|
```
|
||||||
|
|
||||||
|
Dla kątów rozjazdowych (`theta < 8°`) błąd jest rzędu ułamka milimetra.
|
||||||
|
- **klotoida** (krzywa przejściowa, jeśli użyta) → też **jeden Bézier**: `p0`,
|
||||||
|
`p3` i styczne z końców klotoidy, uchwyty na stycznych w 1/3 cięciwy. Kubiczna
|
||||||
|
nie odda liniowo zmiennej krzywizny co do joty, ale przy długościach
|
||||||
|
przejściówek w rozjeździe odchyłka jest mała i to akceptujemy — jeden segment,
|
||||||
|
bez podziału.
|
||||||
|
|
||||||
|
Styczne w węzłach są wspólne dla sąsiednich elementów, więc Béziery łączą się G1.
|
||||||
|
|
||||||
|
## Szyny — krzywe przesunięte
|
||||||
|
|
||||||
|
Szyny to osie przesunięte o `±0,7175 m` wzdłuż normalnej lewej. **Nie** trzymaj
|
||||||
|
ich jako osobnych Bézierów — przesunięcie Béziera nie jest Bézierem. Runtime
|
||||||
|
próbkuje pozę osi i offsetuje per-próbka:
|
||||||
|
|
||||||
|
```
|
||||||
|
lewa = B(t) + (-hy, hx) * 0.7175
|
||||||
|
prawa = B(t) - (-hy, hx) * 0.7175
|
||||||
|
```
|
||||||
|
|
||||||
|
Na promieniach rozjazdowych rozjeżdżanie się offsetu jest pomijalne.
|
||||||
|
|
||||||
|
## Podział elementów
|
||||||
|
|
||||||
|
**Ciągłe — sadzone z pozy wzdłuż osi (zero zależności od typu):**
|
||||||
|
|
||||||
|
- **szyny** — po obu osiach, offset jak wyżej; potrzebny profil przekroju,
|
||||||
|
- **podrozjazdnice** — po długości (reparametryzacja!), prostopadle do
|
||||||
|
dwusiecznej obu osi, wydłużane w strefie iglic i krzyżownicy tak, by objąć oba
|
||||||
|
tory. Parametryczne (skalowana długość) albo zestaw dyskretny — do ustalenia.
|
||||||
|
|
||||||
|
**Dyskretne odlewy — dobierane parametrem, nie nazwą szablonu:**
|
||||||
|
|
||||||
|
- **krzyżownica** — fizyczny odlew o kącie `alfa`; dobierana po **`crossing_mark`
|
||||||
|
(skos 1:n)**, wstawiana w `KR`, ustawiona pod `crossing_angle`,
|
||||||
|
- **iglice (zwrotnica)** — dobierane po **`rail_profile`** (i ew. `R`), wstawiane
|
||||||
|
w `PR`,
|
||||||
|
- **kierownice** — dowiązane do `KR`,
|
||||||
|
- **napęd + latarnia** — w `PR`, odsunięte w bok o ustalony offset.
|
||||||
|
|
||||||
|
Runtime: geometrię szyn/podrozjazdnic robi z Bézierów, a krzyżownicę i iglice
|
||||||
|
**wybiera po (`crossing_mark`, `rail_profile`)** i wstawia w `PR`/`KR`.
|
||||||
|
|
||||||
|
## Iglice — stany
|
||||||
|
|
||||||
|
Rozjazd się przekłada. Iglice potrzebują dwóch stanów (na wprost / na bok) albo
|
||||||
|
animacji przełożenia — do ustalenia z modelarzem. Pozycja bazowa: `PR`.
|
||||||
|
|
||||||
|
## Do ustalenia z modelarzem
|
||||||
|
|
||||||
|
1. origin w pionie: główka szyny czy spód podkładu,
|
||||||
|
2. profil: 49E1 / 60E1 / oba,
|
||||||
|
3. podrozjazdnice: parametryczne czy zestaw dyskretny,
|
||||||
|
4. iglice: ruchome (dwa stany / animacja) czy statyczne,
|
||||||
|
5. które elementy bierze modelarz, a które generuje runtime.
|
||||||
|
|
||||||
|
## Do zrobienia po stronie edytora
|
||||||
|
|
||||||
|
`JunctionGeom` ma dziś `PR (px,py)`, `KR (fx,fy)`, kierunek wyjścia `(fhx,fhy)`,
|
||||||
|
długość i osie w postaci polilinii. Do kontraktu trzeba dołożyć:
|
||||||
|
|
||||||
|
- **`crossing_mark` i `rail_profile`** w danych eksportu,
|
||||||
|
- **`blade_angle` (beta) i `pre_blade_length`** — dziś `lay_turnout` kładzie łuk
|
||||||
|
styczny od `PR` (beta = 0, brak przediglicowego). Trzeba: odcinek przediglicowy
|
||||||
|
(prosty), iglicę prostą pod `beta`, łuk o `alfa − beta`, prostą do `KR`,
|
||||||
|
- te dwa parametry **w szablonach** (`turnout_preset`) per typ,
|
||||||
|
- osie jako **Béziery** (dziś polilinie) — konwersja łuk/klotoida → kubiczne wg
|
||||||
|
powyższego,
|
||||||
|
- jawne oznaczenie „to rozjazd" (nie zwykły łuk), z `PR`/`KR`/`alfa`.
|
||||||
@@ -218,77 +218,74 @@ void eu07_application::DiscordRPCService()
|
|||||||
discord_rpc.startTimestamp = static_cast<int64_t>(now_c);
|
discord_rpc.startTimestamp = static_cast<int64_t>(now_c);
|
||||||
discord_rpc.largeImageText = "MaSzyna";
|
discord_rpc.largeImageText = "MaSzyna";
|
||||||
|
|
||||||
// run loop
|
// run until quit — do NOT require a non-empty mode stack: pop/push during scenery reload
|
||||||
while (!glfwWindowShouldClose(m_windows.front()) && !m_modestack.empty() && !Global.applicationQuitOrder)
|
// briefly empties the stack, and exiting here would Discord_Shutdown() mid-session (or race on top())
|
||||||
|
while (!Global.applicationQuitOrder && !m_windows.empty() && !glfwWindowShouldClose(m_windows.front()))
|
||||||
{
|
{
|
||||||
auto currentMode = m_modestack.top();
|
if (m_modestack.empty())
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const currentMode = m_modestack.top();
|
||||||
if (currentMode == mode::launcher)
|
if (currentMode == mode::launcher)
|
||||||
{
|
{
|
||||||
// in launcher mode
|
|
||||||
|
|
||||||
discord_rpc.state = Translations.lookup_c("In main menu");
|
discord_rpc.state = Translations.lookup_c("In main menu");
|
||||||
discord_rpc.details = Translations.lookup_c("Browsing scenarios...");
|
discord_rpc.details = Translations.lookup_c("Browsing scenarios...");
|
||||||
discord_rpc.largeImageKey = "";
|
discord_rpc.largeImageKey = "";
|
||||||
discord_rpc.largeImageText = "MaSzyna";
|
discord_rpc.largeImageText = "MaSzyna";
|
||||||
// RPC upload
|
|
||||||
Discord_UpdatePresence(&discord_rpc);
|
Discord_UpdatePresence(&discord_rpc);
|
||||||
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(5000)); // update RPC every 5 secs
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
else if (currentMode == mode::scenarioloader)
|
else if (currentMode == mode::scenarioloader)
|
||||||
{
|
{
|
||||||
std::string rpcScnName = Global.SceneryFile;
|
std::string rpcScnName = Global.SceneryFile;
|
||||||
if (rpcScnName[0] == '$')
|
if (!rpcScnName.empty() && rpcScnName.front() == '$')
|
||||||
rpcScnName.erase(0, 1);
|
rpcScnName.erase(0, 1);
|
||||||
rpcScnName.erase(rpcScnName.size() - 4, 4);
|
if (rpcScnName.size() >= 4)
|
||||||
if (rpcScnName.find('_') != std::string::npos)
|
rpcScnName.erase(rpcScnName.size() - 4, 4);
|
||||||
{
|
std::replace(rpcScnName.begin(), rpcScnName.end(), '_', ' ');
|
||||||
std::replace(rpcScnName.begin(), rpcScnName.end(), '_', ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// realworld timestamp from datetime
|
static std::string state;
|
||||||
static std::string state = Translations.lookup_s("Scenery: ") + rpcScnName;
|
state = Translations.lookup_s("Scenery: ") + rpcScnName;
|
||||||
discord_rpc.state = state.c_str();
|
discord_rpc.state = state.c_str();
|
||||||
discord_rpc.details = Translations.lookup_c("Loading scenery...");
|
discord_rpc.details = Translations.lookup_c("Loading scenery...");
|
||||||
discord_rpc.largeImageKey = "logo";
|
discord_rpc.largeImageKey = "logo";
|
||||||
discord_rpc.largeImageText = "MaSzyna";
|
discord_rpc.largeImageText = "MaSzyna";
|
||||||
|
|
||||||
// RPC upload
|
|
||||||
Discord_UpdatePresence(&discord_rpc);
|
Discord_UpdatePresence(&discord_rpc);
|
||||||
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(5000)); // update RPC every 5 secs
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
else if (currentMode == mode::editor)
|
||||||
if (currentMode != mode::driver)
|
{
|
||||||
continue;
|
discord_rpc.state = "Dłubie w edytorze";
|
||||||
|
discord_rpc.details = Global.SceneryFile.c_str();
|
||||||
// Discord RPC updater
|
discord_rpc.largeImageKey = "logo";
|
||||||
if (simulation::is_ready)
|
discord_rpc.largeImageText = "MaSzyna";
|
||||||
|
discord_rpc.smallImageKey = "";
|
||||||
|
discord_rpc.smallImageText = "";
|
||||||
|
Discord_UpdatePresence(&discord_rpc);
|
||||||
|
}
|
||||||
|
else if (currentMode == mode::driver && simulation::is_ready)
|
||||||
{
|
{
|
||||||
std::string PlayerVehicle;
|
std::string PlayerVehicle;
|
||||||
if (simulation::Train != nullptr)
|
if (simulation::Train != nullptr && simulation::Train->Dynamic() != nullptr)
|
||||||
{
|
{
|
||||||
PlayerVehicle = simulation::Train->name();
|
PlayerVehicle = simulation::Train->name();
|
||||||
// make to upper
|
|
||||||
for (auto &c : PlayerVehicle)
|
for (auto &c : PlayerVehicle)
|
||||||
c = toupper(c);
|
c = static_cast<char>(toupper(static_cast<unsigned char>(c)));
|
||||||
|
|
||||||
PlayerVehicle = Translations.lookup_s("Driving: ") + PlayerVehicle;
|
PlayerVehicle = Translations.lookup_s("Driving: ") + PlayerVehicle;
|
||||||
discord_rpc.details = PlayerVehicle.c_str();
|
discord_rpc.details = PlayerVehicle.c_str();
|
||||||
|
|
||||||
uint16_t playerTrainVelocity = simulation::Train->Dynamic()->GetVelocity();
|
auto const playerTrainVelocity = static_cast<uint16_t>(simulation::Train->Dynamic()->GetVelocity());
|
||||||
if (playerTrainVelocity > 1)
|
if (playerTrainVelocity > 1)
|
||||||
{
|
{
|
||||||
// ikonka ze jedziemy i nie spimy
|
|
||||||
discord_rpc.smallImageKey = "driving";
|
discord_rpc.smallImageKey = "driving";
|
||||||
std::string smallText = Translations.lookup_s("Speed: ") + std::to_string(playerTrainVelocity) + " km/h";
|
static std::string smallText;
|
||||||
|
smallText = Translations.lookup_s("Speed: ") + std::to_string(playerTrainVelocity) + " km/h";
|
||||||
discord_rpc.smallImageText = smallText.c_str();
|
discord_rpc.smallImageText = smallText.c_str();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// krecimy postoj
|
|
||||||
discord_rpc.smallImageKey = "halt";
|
discord_rpc.smallImageKey = "halt";
|
||||||
discord_rpc.smallImageText = Translations.lookup_c("Stopped");
|
discord_rpc.smallImageText = Translations.lookup_c("Stopped");
|
||||||
}
|
}
|
||||||
@@ -296,7 +293,8 @@ void eu07_application::DiscordRPCService()
|
|||||||
|
|
||||||
Discord_UpdatePresence(&discord_rpc);
|
Discord_UpdatePresence(&discord_rpc);
|
||||||
}
|
}
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(5000)); // update RPC every 5 secs
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
|
||||||
}
|
}
|
||||||
|
|
||||||
Discord_Shutdown();
|
Discord_Shutdown();
|
||||||
|
|||||||
@@ -617,6 +617,7 @@ bool editor_mode::update()
|
|||||||
Global.editor_reset_scenery = false;
|
Global.editor_reset_scenery = false;
|
||||||
Global.editor_startup = true; // the editor is the base mode from here on, whatever started it
|
Global.editor_startup = true; // the editor is the base mode from here on, whatever started it
|
||||||
Global.SceneryFile = "pusta.scn";
|
Global.SceneryFile = "pusta.scn";
|
||||||
|
simulation::is_ready = false;
|
||||||
Application.pop_mode();
|
Application.pop_mode();
|
||||||
Application.push_mode(eu07_application::mode::scenarioloader);
|
Application.push_mode(eu07_application::mode::scenarioloader);
|
||||||
return true;
|
return true;
|
||||||
@@ -1326,9 +1327,29 @@ void editor_mode::on_scroll(double const Xoffset, double const Yoffset)
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// in the plan view the wheel decides how much ground fits on screen; the camera itself stays put,
|
|
||||||
// because with no perspective its height changes nothing but the clipping
|
// the wheel decides how much ground fits on screen. shift the camera so the point under the
|
||||||
Global.editor_ortho_extent = std::clamp(Global.editor_ortho_extent * std::pow(0.85f, static_cast<float>(Yoffset)), 5.0f, 20000.0f);
|
// cursor stays put - same idea as zooming a map, rather than scaling around the view centre
|
||||||
|
auto const oldextent = Global.editor_ortho_extent;
|
||||||
|
auto const newextent = std::clamp(oldextent * std::pow(0.85f, static_cast<float>(Yoffset)), 5.0f, 20000.0f);
|
||||||
|
if (newextent == oldextent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const width = std::max(1, Global.window_size.x);
|
||||||
|
auto const height = std::max(1, Global.window_size.y);
|
||||||
|
auto const ndcx = (static_cast<double>(Global.cursor_pos.x) / width) * 2.0 - 1.0;
|
||||||
|
auto const ndcy = 1.0 - (static_cast<double>(Global.cursor_pos.y) / height) * 2.0;
|
||||||
|
auto const halfheight = static_cast<double>(oldextent);
|
||||||
|
auto const halfwidth = halfheight * static_cast<double>(width) / static_cast<double>(height);
|
||||||
|
auto const scale = static_cast<double>(newextent) / static_cast<double>(oldextent);
|
||||||
|
|
||||||
|
// top-down with yaw pinned to north: view x follows world x, view y follows -world z
|
||||||
|
Camera.Pos.x += ndcx * halfwidth * (1.0 - scale);
|
||||||
|
Camera.Pos.z -= ndcy * halfheight * (1.0 - scale);
|
||||||
|
|
||||||
|
Global.editor_ortho_extent = newextent;
|
||||||
}
|
}
|
||||||
|
|
||||||
void editor_mode::update_camera(double const Deltatime)
|
void editor_mode::update_camera(double const Deltatime)
|
||||||
@@ -1346,6 +1367,9 @@ void editor_mode::update_camera(double const Deltatime)
|
|||||||
}
|
}
|
||||||
Camera.Angle.x = glm::radians(-90.0f);
|
Camera.Angle.x = glm::radians(-90.0f);
|
||||||
Camera.Angle.z = 0.0f;
|
Camera.Angle.z = 0.0f;
|
||||||
|
// a plan is read with north up. the editor's own camera starts turned around, which left the
|
||||||
|
// whole view - scenery, imagery and drawing alike - standing on its head
|
||||||
|
Camera.Angle.y = 0.0f;
|
||||||
// high enough to clear anything the scenery may have under the cursor
|
// high enough to clear anything the scenery may have under the cursor
|
||||||
Camera.Pos.y = std::max(Camera.Pos.y, 500.0);
|
Camera.Pos.y = std::max(Camera.Pos.y, 500.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
#include "rendering/renderer.h"
|
#include "rendering/renderer.h"
|
||||||
#include "utilities/Globals.h"
|
#include "utilities/Globals.h"
|
||||||
|
|
||||||
#include "maj0sted/web/ribbon.hpp"
|
#include "maj0sted/editor/ribbon.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -94,9 +94,9 @@ ImU32 element_colour(int const Kind, bool const Active)
|
|||||||
|
|
||||||
// one centreline element recovered from a left/right rail pair
|
// one centreline element recovered from a left/right rail pair
|
||||||
// dual rails from a centreline, same 1.5 m gauge as maj0sted::render::RailRenderer
|
// dual rails from a centreline, same 1.5 m gauge as maj0sted::render::RailRenderer
|
||||||
std::vector<maj0sted::web::WebPolyline> rails_from_centreline(std::vector<maj0sted::web::WebPolyline> const &Centre, double const Gauge = 1.5)
|
std::vector<maj0sted::editor::PlanPolyline> rails_from_centreline(std::vector<maj0sted::editor::PlanPolyline> const &Centre, double const Gauge = 1.5)
|
||||||
{
|
{
|
||||||
std::vector<maj0sted::web::WebPolyline> out;
|
std::vector<maj0sted::editor::PlanPolyline> out;
|
||||||
auto const half{Gauge * 0.5};
|
auto const half{Gauge * 0.5};
|
||||||
for (auto const &poly : Centre)
|
for (auto const &poly : Centre)
|
||||||
{
|
{
|
||||||
@@ -106,7 +106,7 @@ std::vector<maj0sted::web::WebPolyline> rails_from_centreline(std::vector<maj0st
|
|||||||
}
|
}
|
||||||
for (double const side : {half, -half})
|
for (double const side : {half, -half})
|
||||||
{
|
{
|
||||||
maj0sted::web::WebPolyline rail;
|
maj0sted::editor::PlanPolyline rail;
|
||||||
rail.kind = poly.kind;
|
rail.kind = poly.kind;
|
||||||
rail.length = poly.length;
|
rail.length = poly.length;
|
||||||
rail.radius_start = poly.radius_start;
|
rail.radius_start = poly.radius_start;
|
||||||
@@ -230,7 +230,7 @@ double point_segment_distance(double const Px, double const Py, double const Ax,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// distance from (X,Y) to the gap track: curve elements plus the two bounding straights
|
// distance from (X,Y) to the gap track: curve elements plus the two bounding straights
|
||||||
double distance_to_gap_track(maj0sted::web::NiweletaPolys const &Solved, int const Gap, double const X, double const Y)
|
double distance_to_gap_track(maj0sted::editor::NiweletaPolys const &Solved, int const Gap, double const X, double const Y)
|
||||||
{
|
{
|
||||||
auto best{std::numeric_limits<double>::infinity()};
|
auto best{std::numeric_limits<double>::infinity()};
|
||||||
for (auto const &poly : Solved.polylines)
|
for (auto const &poly : Solved.polylines)
|
||||||
@@ -257,9 +257,9 @@ struct GapSample
|
|||||||
};
|
};
|
||||||
|
|
||||||
// centreline of a fitted gap, sampled in element order
|
// centreline of a fitted gap, sampled in element order
|
||||||
std::vector<GapSample> sample_gap_centreline(maj0sted::web::NiweletaPolys const &Solved, int const Gap)
|
std::vector<GapSample> sample_gap_centreline(maj0sted::editor::NiweletaPolys const &Solved, int const Gap)
|
||||||
{
|
{
|
||||||
std::map<int, maj0sted::web::WebPolyline const *> by_el;
|
std::map<int, maj0sted::editor::PlanPolyline const *> by_el;
|
||||||
for (auto const &poly : Solved.polylines)
|
for (auto const &poly : Solved.polylines)
|
||||||
{
|
{
|
||||||
if (poly.gap != Gap || poly.element_index < 0 || poly.points.size() < 2)
|
if (poly.gap != Gap || poly.element_index < 0 || poly.points.size() < 2)
|
||||||
@@ -378,7 +378,7 @@ struct CmpStationHandle
|
|||||||
double y{0.0};
|
double y{0.0};
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<CmpStationHandle> compound_station_handles(maj0sted::web::GapFit const &Fit, maj0sted::web::NiweletaPolys const &Solved, int const Gap)
|
std::vector<CmpStationHandle> compound_station_handles(maj0sted::editor::GapFit const &Fit, maj0sted::editor::NiweletaPolys const &Solved, int const Gap)
|
||||||
{
|
{
|
||||||
std::vector<CmpStationHandle> handles;
|
std::vector<CmpStationHandle> handles;
|
||||||
if (Fit.mode != 3 || Fit.arcs.empty())
|
if (Fit.mode != 3 || Fit.arcs.empty())
|
||||||
@@ -420,7 +420,7 @@ std::vector<CmpStationHandle> compound_station_handles(maj0sted::web::GapFit con
|
|||||||
return handles;
|
return handles;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<GapSample> sample_draft_centreline(std::vector<maj0sted::web::WebPolyline> const &Polys)
|
std::vector<GapSample> sample_draft_centreline(std::vector<maj0sted::editor::PlanPolyline> const &Polys)
|
||||||
{
|
{
|
||||||
std::vector<GapSample> out;
|
std::vector<GapSample> out;
|
||||||
double along{0.0};
|
double along{0.0};
|
||||||
@@ -444,7 +444,7 @@ std::vector<GapSample> sample_draft_centreline(std::vector<maj0sted::web::WebPol
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draft stations: every arc has L (including the last), unlike GapFit
|
// draft stations: every arc has L (including the last), unlike GapFit
|
||||||
std::vector<CmpStationHandle> draft_station_handles(maj0sted::app::BasketDraft const &Draft, std::vector<maj0sted::web::WebPolyline> const &Polys)
|
std::vector<CmpStationHandle> draft_station_handles(maj0sted::app::BasketDraft const &Draft, std::vector<maj0sted::editor::PlanPolyline> const &Polys)
|
||||||
{
|
{
|
||||||
std::vector<CmpStationHandle> handles;
|
std::vector<CmpStationHandle> handles;
|
||||||
if (false == Draft.active || Draft.arcs.empty())
|
if (false == Draft.active || Draft.arcs.empty())
|
||||||
@@ -707,6 +707,7 @@ void plan_panel::handle_scene()
|
|||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
m_dragging_cmp = false;
|
m_dragging_cmp = false;
|
||||||
m_dragging_draft = false;
|
m_dragging_draft = false;
|
||||||
|
m_drag_junction = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +722,16 @@ void plan_panel::handle_scene()
|
|||||||
{
|
{
|
||||||
int hit_niw{-1};
|
int hit_niw{-1};
|
||||||
int hit_str{-1};
|
int hit_str{-1};
|
||||||
if (hit_straight(mouse, hit_niw, hit_str, true) && hit_niw >= 0 && static_cast<std::size_t>(hit_niw) < m_document.niwelety.size())
|
// straights catch only their trimmed remainders; fall back to the whole solved axis (curves
|
||||||
|
// included) so a double-click anywhere on the visible track still selects its niweleta
|
||||||
|
bool hit{hit_straight(mouse, hit_niw, hit_str, true)};
|
||||||
|
if (false == hit)
|
||||||
|
{
|
||||||
|
hit_niw = nearest_niweleta_axis(mouse, 18.0f);
|
||||||
|
hit_str = -1;
|
||||||
|
hit = hit_niw >= 0;
|
||||||
|
}
|
||||||
|
if (hit && hit_niw >= 0 && static_cast<std::size_t>(hit_niw) < m_document.niwelety.size())
|
||||||
{
|
{
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
m_dragging_cmp = false;
|
m_dragging_cmp = false;
|
||||||
@@ -875,6 +885,26 @@ void plan_panel::handle_scene()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// grabbing a switch's start to slide it along the through track
|
||||||
|
for (std::size_t j = 0; j < m_junctions.size(); ++j)
|
||||||
|
{
|
||||||
|
if (false == m_junctions[j].valid)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ImVec2 sw;
|
||||||
|
if (world_to_screen(plan_to_world(m_junctions[j].px, m_junctions[j].py), sw) && std::hypot(sw.x - mouse.x, sw.y - mouse.y) <= 9.0f)
|
||||||
|
{
|
||||||
|
clear_selection();
|
||||||
|
m_dragging_cmp = false;
|
||||||
|
m_dragging_draft = false;
|
||||||
|
m_drag_junction = static_cast<int>(j);
|
||||||
|
m_drawing = false;
|
||||||
|
m_status = "przesuwanie rozjazdu";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int hit_niw{-1};
|
int hit_niw{-1};
|
||||||
int hit_str{-1};
|
int hit_str{-1};
|
||||||
int hit_end{-1};
|
int hit_end{-1};
|
||||||
@@ -916,7 +946,25 @@ void plan_panel::handle_scene()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_dragging_draft)
|
if (m_drag_junction >= 0)
|
||||||
|
{
|
||||||
|
if (ImGui::IsMouseDown(0) && static_cast<std::size_t>(m_drag_junction) < m_document.junctions.size())
|
||||||
|
{
|
||||||
|
auto &junction{m_document.junctions[static_cast<std::size_t>(m_drag_junction)]};
|
||||||
|
double station{junction.station};
|
||||||
|
// slide along the through track: the cursor's nearest station on its centreline
|
||||||
|
if (junction.through >= 0 && station_on_through(static_cast<std::size_t>(junction.through), cursorx, cursory, station))
|
||||||
|
{
|
||||||
|
junction.station = station;
|
||||||
|
solve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_drag_junction = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_dragging_draft)
|
||||||
{
|
{
|
||||||
if (ImGui::IsMouseDown(0))
|
if (ImGui::IsMouseDown(0))
|
||||||
{
|
{
|
||||||
@@ -997,9 +1045,9 @@ void plan_panel::draw_on_scene()
|
|||||||
auto const display{ImGui::GetIO().DisplaySize};
|
auto const display{ImGui::GetIO().DisplaySize};
|
||||||
auto const halfheight{static_cast<double>(Global.editor_ortho_extent)};
|
auto const halfheight{static_cast<double>(Global.editor_ortho_extent)};
|
||||||
auto const halfwidth{halfheight * std::max(1.0f, display.x) / std::max(1.0f, display.y)};
|
auto const halfwidth{halfheight * std::max(1.0f, display.x) / std::max(1.0f, display.y)};
|
||||||
maj0sted::web::TileBBox const view{planx - halfwidth, plany - halfheight, planx + halfwidth, plany + halfheight};
|
maj0sted::editor::TileBBox const view{planx - halfwidth, plany - halfheight, planx + halfwidth, plany + halfheight};
|
||||||
|
|
||||||
auto const draw_box = [&](maj0sted::web::TileBBox const &Box, unsigned int const Texture) {
|
auto const draw_box = [&](maj0sted::editor::TileBBox const &Box, unsigned int const Texture) {
|
||||||
ImVec2 corners[4];
|
ImVec2 corners[4];
|
||||||
auto const ok = world_to_screen(plan_to_world(Box.min_x, Box.max_y), corners[0]) && world_to_screen(plan_to_world(Box.max_x, Box.max_y), corners[1]) &&
|
auto const ok = world_to_screen(plan_to_world(Box.min_x, Box.max_y), corners[0]) && world_to_screen(plan_to_world(Box.max_x, Box.max_y), corners[1]) &&
|
||||||
world_to_screen(plan_to_world(Box.max_x, Box.min_y), corners[2]) && world_to_screen(plan_to_world(Box.min_x, Box.min_y), corners[3]);
|
world_to_screen(plan_to_world(Box.max_x, Box.min_y), corners[2]) && world_to_screen(plan_to_world(Box.min_x, Box.min_y), corners[3]);
|
||||||
@@ -1114,7 +1162,7 @@ void plan_panel::draw_on_scene()
|
|||||||
}
|
}
|
||||||
for (auto const gap : gaps)
|
for (auto const gap : gaps)
|
||||||
{
|
{
|
||||||
std::map<int, maj0sted::web::WebPolyline const *> by_el;
|
std::map<int, maj0sted::editor::PlanPolyline const *> by_el;
|
||||||
for (auto const &poly : solved.polylines)
|
for (auto const &poly : solved.polylines)
|
||||||
{
|
{
|
||||||
if (poly.gap != gap || poly.points.size() < 2 || poly.element_index < 0)
|
if (poly.gap != gap || poly.points.size() < 2 || poly.element_index < 0)
|
||||||
@@ -1127,7 +1175,7 @@ void plan_panel::draw_on_scene()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<maj0sted::web::WebPolyline const *> arcs;
|
std::vector<maj0sted::editor::PlanPolyline const *> arcs;
|
||||||
for (auto const &[idx, poly] : by_el)
|
for (auto const &[idx, poly] : by_el)
|
||||||
{
|
{
|
||||||
(void)idx;
|
(void)idx;
|
||||||
@@ -1855,7 +1903,7 @@ void plan_panel::render_gaps()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void plan_panel::render_compound(maj0sted::web::GapFit &Fit, std::size_t const Gap, bool &Dirty)
|
void plan_panel::render_compound(maj0sted::editor::GapFit &Fit, std::size_t const Gap, bool &Dirty)
|
||||||
{
|
{
|
||||||
ImGui::Indent();
|
ImGui::Indent();
|
||||||
|
|
||||||
@@ -2189,7 +2237,7 @@ void plan_panel::fit_compound_to_guides()
|
|||||||
{
|
{
|
||||||
return std::numeric_limits<double>::infinity();
|
return std::numeric_limits<double>::infinity();
|
||||||
}
|
}
|
||||||
auto const solved{maj0sted::web::solve_project(trial.niwelety)};
|
auto const solved{maj0sted::editor::solve_project(trial.niwelety)};
|
||||||
if (m_niweleta >= solved.size())
|
if (m_niweleta >= solved.size())
|
||||||
{
|
{
|
||||||
return std::numeric_limits<double>::infinity();
|
return std::numeric_limits<double>::infinity();
|
||||||
@@ -2288,13 +2336,21 @@ void plan_panel::render_switches()
|
|||||||
{
|
{
|
||||||
m_sw_crossing = preset.crossing_n;
|
m_sw_crossing = preset.crossing_n;
|
||||||
m_sw_radius = preset.radius;
|
m_sw_radius = preset.radius;
|
||||||
|
m_sw_length = preset.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
// each single-arc template spans R*atan(1/n) between its tangent points; shown so its length is known
|
// the template's catalogue length (PR->KR); custom switches with no template just span their curve
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("dl. ~%.1f m", m_sw_radius * std::atan(1.0 / std::max(1.0, m_sw_crossing)));
|
if (m_sw_length > 0.0)
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("dl. %.3f m (Id-1)", m_sw_length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("dl. ~%.1f m (sam luk)", m_sw_radius * std::atan(1.0 / std::max(1.0, m_sw_crossing)));
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SetNextItemWidth(90.0f);
|
ImGui::SetNextItemWidth(90.0f);
|
||||||
ImGui::Combo("strona", &m_sw_side, "w lewo\0w prawo\0");
|
ImGui::Combo("strona", &m_sw_side, "w lewo\0w prawo\0");
|
||||||
@@ -2354,6 +2410,12 @@ void plan_panel::render_switches()
|
|||||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.4f, 1.0f), "nie wpasowany");
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.4f, 1.0f), "nie wpasowany");
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton(junction.side == 0 ? "L->P" : "P->L"))
|
||||||
|
{
|
||||||
|
junction.side = junction.side == 0 ? 1 : 0;
|
||||||
|
solve();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
if (ImGui::SmallButton("usuń"))
|
if (ImGui::SmallButton("usuń"))
|
||||||
{
|
{
|
||||||
m_document.junctions.erase(m_document.junctions.begin() + static_cast<long>(i));
|
m_document.junctions.erase(m_document.junctions.begin() + static_cast<long>(i));
|
||||||
@@ -2373,9 +2435,12 @@ void plan_panel::render_switches()
|
|||||||
junction.curve.mode = 3;
|
junction.curve.mode = 3;
|
||||||
if (junction.curve.arcs.empty())
|
if (junction.curve.arcs.empty())
|
||||||
{
|
{
|
||||||
// a basket needs at least two arcs; the last absorbs the rest of the turn
|
// a basket needs at least two arcs; a gentler entry radius eases into the nominal
|
||||||
junction.curve.arcs.push_back({junction.curve.radius > 0.0 ? junction.curve.radius * 2.0 : 600.0, 40.0, 0.0});
|
// one, whose closing arc absorbs the rest of the turn. a short fixed length keeps
|
||||||
junction.curve.arcs.push_back({junction.curve.radius > 0.0 ? junction.curve.radius : 300.0, 0.0, 0.0});
|
// the whole curve inside the turnout rather than far past its end
|
||||||
|
auto const nominal{junction.curve.radius > 0.0 ? junction.curve.radius : 300.0};
|
||||||
|
junction.curve.arcs.push_back({nominal * 2.0, 6.0, 0.0});
|
||||||
|
junction.curve.arcs.push_back({nominal, 0.0, 0.0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2480,52 +2545,26 @@ void plan_panel::add_switch_on(std::size_t const Through, double const Wx, doubl
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto const ¢re{m_solved[Through].centreline};
|
double best_station{0.0};
|
||||||
if (centre.size() < 2)
|
if (false == station_on_through(Through, Wx, Wy, best_station))
|
||||||
{
|
{
|
||||||
m_status = "niweleta bez osi";
|
m_status = "niweleta bez osi";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// nearest station: walk the centreline, project the click onto each segment
|
maj0sted::editor::Junction junction;
|
||||||
double best_station{0.0};
|
|
||||||
double best_dist{std::numeric_limits<double>::max()};
|
|
||||||
double travelled{0.0};
|
|
||||||
for (std::size_t i = 0; i + 1 < centre.size(); ++i)
|
|
||||||
{
|
|
||||||
double const ax{centre[i].x};
|
|
||||||
double const ay{centre[i].y};
|
|
||||||
double const dx{centre[i + 1].x - ax};
|
|
||||||
double const dy{centre[i + 1].y - ay};
|
|
||||||
double const seg{std::hypot(dx, dy)};
|
|
||||||
if (seg < 1e-9)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
double const t{std::clamp(((Wx - ax) * dx + (Wy - ay) * dy) / (seg * seg), 0.0, 1.0)};
|
|
||||||
double const px{ax + dx * t};
|
|
||||||
double const py{ay + dy * t};
|
|
||||||
double const d{std::hypot(Wx - px, Wy - py)};
|
|
||||||
if (d < best_dist)
|
|
||||||
{
|
|
||||||
best_dist = d;
|
|
||||||
best_station = travelled + seg * t;
|
|
||||||
}
|
|
||||||
travelled += seg;
|
|
||||||
}
|
|
||||||
|
|
||||||
maj0sted::web::Junction junction;
|
|
||||||
junction.through = static_cast<int>(Through);
|
junction.through = static_cast<int>(Through);
|
||||||
junction.station = best_station;
|
junction.station = best_station;
|
||||||
junction.side = m_sw_side;
|
junction.side = m_sw_side;
|
||||||
junction.facing = true;
|
junction.facing = true;
|
||||||
junction.crossing_n = m_sw_crossing;
|
junction.crossing_n = m_sw_crossing;
|
||||||
|
junction.length = m_sw_length; // catalogue length from the template, 0 for a custom switch
|
||||||
junction.curve.mode = 1; // plain arc; the internal curve can be made compound later
|
junction.curve.mode = 1; // plain arc; the internal curve can be made compound later
|
||||||
junction.curve.radius = m_sw_radius;
|
junction.curve.radius = m_sw_radius;
|
||||||
junction.branch = static_cast<int>(m_document.niwelety.size());
|
junction.branch = static_cast<int>(m_document.niwelety.size());
|
||||||
|
|
||||||
// a branch niweleta to draw the diverging track into, pinned to the frog by the solver
|
// a branch niweleta to draw the diverging track into, pinned to the frog by the solver
|
||||||
maj0sted::web::NiweletaSpec branch;
|
maj0sted::editor::NiweletaSpec branch;
|
||||||
branch.name = "odnoga " + std::to_string(m_document.niwelety.size() + 1);
|
branch.name = "odnoga " + std::to_string(m_document.niwelety.size() + 1);
|
||||||
branch.straights.push_back({Wx, Wy, Wx + 10.0, Wy, false}); // placeholder, repositioned below
|
branch.straights.push_back({Wx, Wy, Wx + 10.0, Wy, false}); // placeholder, repositioned below
|
||||||
m_document.niwelety.push_back(std::move(branch));
|
m_document.niwelety.push_back(std::move(branch));
|
||||||
@@ -2674,7 +2713,7 @@ void plan_panel::solve()
|
|||||||
}
|
}
|
||||||
maj0sted::app::apply_straight_constraints(m_document.niwelety);
|
maj0sted::app::apply_straight_constraints(m_document.niwelety);
|
||||||
// switches pin their branches, so the whole layout is solved together
|
// switches pin their branches, so the whole layout is solved together
|
||||||
auto layout{maj0sted::web::solve_layout(m_document.niwelety, m_document.junctions)};
|
auto layout{maj0sted::editor::solve_layout(m_document.niwelety, m_document.junctions)};
|
||||||
m_solved = std::move(layout.niwelety);
|
m_solved = std::move(layout.niwelety);
|
||||||
m_junctions = std::move(layout.junctions);
|
m_junctions = std::move(layout.junctions);
|
||||||
}
|
}
|
||||||
@@ -2686,12 +2725,12 @@ void plan_panel::apply_constraints()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
maj0sted::web::NiweletaSpec *plan_panel::current_niweleta()
|
maj0sted::editor::NiweletaSpec *plan_panel::current_niweleta()
|
||||||
{
|
{
|
||||||
return m_niweleta < m_document.niwelety.size() ? &m_document.niwelety[m_niweleta] : nullptr;
|
return m_niweleta < m_document.niwelety.size() ? &m_document.niwelety[m_niweleta] : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
maj0sted::web::NiweletaSpec const *plan_panel::current_niweleta() const
|
maj0sted::editor::NiweletaSpec const *plan_panel::current_niweleta() const
|
||||||
{
|
{
|
||||||
return m_niweleta < m_document.niwelety.size() ? &m_document.niwelety[m_niweleta] : nullptr;
|
return m_niweleta < m_document.niwelety.size() ? &m_document.niwelety[m_niweleta] : nullptr;
|
||||||
}
|
}
|
||||||
@@ -2721,7 +2760,7 @@ bool plan_panel::fit_applied(std::size_t const Gap) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
maj0sted::web::StraightSpec const *plan_panel::handle_straight(std::size_t const Niw, std::size_t const Index) const
|
maj0sted::editor::StraightSpec const *plan_panel::handle_straight(std::size_t const Niw, std::size_t const Index) const
|
||||||
{
|
{
|
||||||
if (Niw >= m_document.niwelety.size())
|
if (Niw >= m_document.niwelety.size())
|
||||||
{
|
{
|
||||||
@@ -2814,7 +2853,7 @@ void plan_panel::delete_selected_straight()
|
|||||||
niweleta->straights.erase(niweleta->straights.begin() + sel);
|
niweleta->straights.erase(niweleta->straights.begin() + sel);
|
||||||
|
|
||||||
// gaps touching the removed straight disappear; later gaps shift down
|
// gaps touching the removed straight disappear; later gaps shift down
|
||||||
std::vector<maj0sted::web::GapFit> kept;
|
std::vector<maj0sted::editor::GapFit> kept;
|
||||||
kept.reserve(niweleta->fits.size());
|
kept.reserve(niweleta->fits.size());
|
||||||
for (auto const &fit : niweleta->fits)
|
for (auto const &fit : niweleta->fits)
|
||||||
{
|
{
|
||||||
@@ -3394,6 +3433,82 @@ bool plan_panel::hit_endpoint(ImVec2 const &Mouse, int &OutNiw, int &OutStr, int
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool plan_panel::station_on_through(std::size_t const Through, double const Wx, double const Wy, double &OutStation) const
|
||||||
|
{
|
||||||
|
if (Through >= m_solved.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const ¢re{m_solved[Through].centreline};
|
||||||
|
if (centre.size() < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// walk the axis, project the point onto each segment, keep the nearest station (arc length)
|
||||||
|
double best_station{0.0};
|
||||||
|
double best_dist{std::numeric_limits<double>::max()};
|
||||||
|
double travelled{0.0};
|
||||||
|
for (std::size_t i = 0; i + 1 < centre.size(); ++i)
|
||||||
|
{
|
||||||
|
double const ax{centre[i].x};
|
||||||
|
double const ay{centre[i].y};
|
||||||
|
double const dx{centre[i + 1].x - ax};
|
||||||
|
double const dy{centre[i + 1].y - ay};
|
||||||
|
double const seg{std::hypot(dx, dy)};
|
||||||
|
if (seg < 1e-9)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double const t{std::clamp(((Wx - ax) * dx + (Wy - ay) * dy) / (seg * seg), 0.0, 1.0)};
|
||||||
|
double const d{std::hypot(Wx - (ax + dx * t), Wy - (ay + dy * t))};
|
||||||
|
if (d < best_dist)
|
||||||
|
{
|
||||||
|
best_dist = d;
|
||||||
|
best_station = travelled + seg * t;
|
||||||
|
}
|
||||||
|
travelled += seg;
|
||||||
|
}
|
||||||
|
OutStation = best_station;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int plan_panel::nearest_niweleta_axis(ImVec2 const &Mouse, float const Tolerance) const
|
||||||
|
{
|
||||||
|
auto best{Tolerance};
|
||||||
|
int found{-1};
|
||||||
|
for (std::size_t n = 0; n < m_solved.size(); ++n)
|
||||||
|
{
|
||||||
|
for (auto const &polyline : m_solved[n].polylines)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i + 1 < polyline.points.size(); ++i)
|
||||||
|
{
|
||||||
|
ImVec2 a;
|
||||||
|
ImVec2 b;
|
||||||
|
if (false == world_to_screen(plan_to_world(polyline.points[i].x, polyline.points[i].y), a) ||
|
||||||
|
false == world_to_screen(plan_to_world(polyline.points[i + 1].x, polyline.points[i + 1].y), b))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const dx{b.x - a.x};
|
||||||
|
auto const dy{b.y - a.y};
|
||||||
|
auto const len2{dx * dx + dy * dy};
|
||||||
|
if (len2 <= 0.0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const t{std::clamp(((Mouse.x - a.x) * dx + (Mouse.y - a.y) * dy) / len2, 0.0f, 1.0f)};
|
||||||
|
auto const d{std::hypot(Mouse.x - (a.x + t * dx), Mouse.y - (a.y + t * dy))};
|
||||||
|
if (d <= best)
|
||||||
|
{
|
||||||
|
best = d;
|
||||||
|
found = static_cast<int>(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
bool plan_panel::hit_straight(ImVec2 const &Mouse, int &OutNiw, int &OutStr, bool const AllNiwelety) const
|
bool plan_panel::hit_straight(ImVec2 const &Mouse, int &OutNiw, int &OutStr, bool const AllNiwelety) const
|
||||||
{
|
{
|
||||||
auto best{10.0f};
|
auto best{10.0f};
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
#include "maj0sted/app/editor_document.hpp"
|
#include "maj0sted/app/editor_document.hpp"
|
||||||
#include "maj0sted/app/straight_constraints.hpp"
|
#include "maj0sted/app/straight_constraints.hpp"
|
||||||
#include "maj0sted/io/scn_export.hpp"
|
#include "maj0sted/io/scn_export.hpp"
|
||||||
#include "maj0sted/web/editor.hpp"
|
#include "maj0sted/editor/editor.hpp"
|
||||||
#include "maj0sted/web/ribbon.hpp"
|
#include "maj0sted/editor/ribbon.hpp"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class plan_panel : public ui_panel
|
|||||||
void render_toolbar();
|
void render_toolbar();
|
||||||
void render_draft();
|
void render_draft();
|
||||||
void render_gaps();
|
void render_gaps();
|
||||||
void render_compound(maj0sted::web::GapFit &Fit, std::size_t const Gap, bool &Dirty);
|
void render_compound(maj0sted::editor::GapFit &Fit, std::size_t const Gap, bool &Dirty);
|
||||||
void fit_compound_to_guides();
|
void fit_compound_to_guides();
|
||||||
void clear_fit_guides();
|
void clear_fit_guides();
|
||||||
void render_storage();
|
void render_storage();
|
||||||
@@ -51,12 +51,12 @@ class plan_panel : public ui_panel
|
|||||||
void draw_on_scene();
|
void draw_on_scene();
|
||||||
void solve();
|
void solve();
|
||||||
void apply_constraints();
|
void apply_constraints();
|
||||||
maj0sted::web::NiweletaSpec *current_niweleta();
|
maj0sted::editor::NiweletaSpec *current_niweleta();
|
||||||
maj0sted::web::NiweletaSpec const *current_niweleta() const;
|
maj0sted::editor::NiweletaSpec const *current_niweleta() const;
|
||||||
void align_fits();
|
void align_fits();
|
||||||
bool fit_applied(std::size_t const Gap) const;
|
bool fit_applied(std::size_t const Gap) const;
|
||||||
// independent anchored straights: each endpoint is its own handle
|
// independent anchored straights: each endpoint is its own handle
|
||||||
maj0sted::web::StraightSpec const *handle_straight(std::size_t const Niw, std::size_t const Index) const;
|
maj0sted::editor::StraightSpec const *handle_straight(std::size_t const Niw, std::size_t const Index) const;
|
||||||
void append_vertex(double const X, double const Y);
|
void append_vertex(double const X, double const Y);
|
||||||
void drop_last_vertex();
|
void drop_last_vertex();
|
||||||
void delete_selected_straight();
|
void delete_selected_straight();
|
||||||
@@ -65,6 +65,10 @@ class plan_panel : public ui_panel
|
|||||||
// hit tests (screen pixels). niw=-1 means "current niweleta only"
|
// hit tests (screen pixels). niw=-1 means "current niweleta only"
|
||||||
bool hit_endpoint(ImVec2 const &Mouse, int &OutNiw, int &OutStr, int &OutEnd) const;
|
bool hit_endpoint(ImVec2 const &Mouse, int &OutNiw, int &OutStr, int &OutEnd) const;
|
||||||
bool hit_straight(ImVec2 const &Mouse, int &OutNiw, int &OutStr, bool const AllNiwelety) const;
|
bool hit_straight(ImVec2 const &Mouse, int &OutNiw, int &OutStr, bool const AllNiwelety) const;
|
||||||
|
// nearest niweleta by its solved axis (curves included), for a forgiving double-click select
|
||||||
|
int nearest_niweleta_axis(ImVec2 const &Mouse, float const Tolerance) const;
|
||||||
|
// nearest station (arc length) on a through niweleta's axis to a plan point; for placing/moving switches
|
||||||
|
bool station_on_through(std::size_t const Through, double const Wx, double const Wy, double &OutStation) const;
|
||||||
// compound basket: draggable ends of entry/exit KP, free arcs and between-KP
|
// compound basket: draggable ends of entry/exit KP, free arcs and between-KP
|
||||||
bool hit_compound_station(ImVec2 const &Mouse, int &OutGap, int &OutKind, int &OutArc) const;
|
bool hit_compound_station(ImVec2 const &Mouse, int &OutGap, int &OutKind, int &OutArc) const;
|
||||||
void drag_compound_station(double const X, double const Y);
|
void drag_compound_station(double const X, double const Y);
|
||||||
@@ -90,8 +94,8 @@ class plan_panel : public ui_panel
|
|||||||
|
|
||||||
// members
|
// members
|
||||||
maj0sted::app::EditorDocument m_document;
|
maj0sted::app::EditorDocument m_document;
|
||||||
std::vector<maj0sted::web::NiweletaPolys> m_solved;
|
std::vector<maj0sted::editor::NiweletaPolys> m_solved;
|
||||||
std::vector<maj0sted::web::JunctionGeom> m_junctions;
|
std::vector<maj0sted::editor::JunctionGeom> m_junctions;
|
||||||
std::size_t m_niweleta{0};
|
std::size_t m_niweleta{0};
|
||||||
|
|
||||||
// switch placement: 0 idle, 1 waiting for a click on the through track
|
// switch placement: 0 idle, 1 waiting for a click on the through track
|
||||||
@@ -99,6 +103,8 @@ class plan_panel : public ui_panel
|
|||||||
int m_sw_side{0}; // 0 left, 1 right
|
int m_sw_side{0}; // 0 left, 1 right
|
||||||
double m_sw_crossing{9.0}; // skos 1:n
|
double m_sw_crossing{9.0}; // skos 1:n
|
||||||
double m_sw_radius{190.0}; // internal arc radius (metres)
|
double m_sw_radius{190.0}; // internal arc radius (metres)
|
||||||
|
double m_sw_length{0.0}; // catalogue length from the chosen template (0 = just the curve)
|
||||||
|
int m_drag_junction{-1}; // index of the switch being slid along its through track, or -1
|
||||||
|
|
||||||
int m_sel_straight{-1};
|
int m_sel_straight{-1};
|
||||||
int m_sel_end{-1};
|
int m_sel_end{-1};
|
||||||
@@ -136,7 +142,7 @@ class plan_panel : public ui_panel
|
|||||||
std::vector<FitGuide> m_fit_guides;
|
std::vector<FitGuide> m_fit_guides;
|
||||||
|
|
||||||
maj0sted::app::BasketDraft m_draft;
|
maj0sted::app::BasketDraft m_draft;
|
||||||
std::vector<maj0sted::web::WebPolyline> m_draft_polys;
|
std::vector<maj0sted::editor::PlanPolyline> m_draft_polys;
|
||||||
// 0 idle, 1 pick straight end to continue from
|
// 0 idle, 1 pick straight end to continue from
|
||||||
int m_draft_place{0};
|
int m_draft_place{0};
|
||||||
// next click appends: 0 = luk, 1 = KP (wej/między), 2 = KP wyjście (kończy: prosta + kotwica)
|
// next click appends: 0 = luk, 1 = KP (wej/między), 2 = KP wyjście (kończy: prosta + kotwica)
|
||||||
@@ -162,9 +168,9 @@ class plan_panel : public ui_panel
|
|||||||
double m_mapviewy{0.0};
|
double m_mapviewy{0.0};
|
||||||
double m_mapscale{0.0};
|
double m_mapscale{0.0};
|
||||||
|
|
||||||
editor::wms_image m_topomap{maj0sted::web::WmsConfig::geoportal_topo()};
|
editor::wms_image m_topomap{maj0sted::editor::WmsConfig::geoportal_topo()};
|
||||||
editor::orthophoto_source m_ortho;
|
editor::orthophoto_source m_ortho;
|
||||||
// 1 km topo cells for views wider than a kilometre (orto's 100 m grid is too dense there)
|
// 1 km topo cells for views wider than a kilometre (orto's 100 m grid is too dense there)
|
||||||
editor::orthophoto_source m_topo{maj0sted::web::WmsConfig::geoportal_topo(), 1000, "topo"};
|
editor::orthophoto_source m_topo{maj0sted::editor::WmsConfig::geoportal_topo(), 1000, "topo"};
|
||||||
bool m_showortho{true};
|
bool m_showortho{true};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,12 +53,17 @@ bool scenarioloader_mode::update() {
|
|||||||
}
|
}
|
||||||
catch (invalid_scenery_exception &e) {
|
catch (invalid_scenery_exception &e) {
|
||||||
ErrorLog( "Bad init: scenario loading failed" );
|
ErrorLog( "Bad init: scenario loading failed" );
|
||||||
|
state.reset();
|
||||||
Application.pop_mode();
|
Application.pop_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteLog( "Scenario loading time: " + std::to_string( std::chrono::duration_cast<std::chrono::seconds>( std::chrono::system_clock::now() - timestart ).count() ) + " seconds" );
|
WriteLog( "Scenario loading time: " + std::to_string( std::chrono::duration_cast<std::chrono::seconds>( std::chrono::system_clock::now() - timestart ).count() ) + " seconds" );
|
||||||
// TODO: implement and use next mode cue
|
// TODO: implement and use next mode cue
|
||||||
|
|
||||||
|
// the mode object is kept for the next time a scenery is loaded; without dropping the context
|
||||||
|
// here, a later enter() would skip deserialize_begin and finish an already-spent parser again
|
||||||
|
state.reset();
|
||||||
|
|
||||||
Application.pop_mode();
|
Application.pop_mode();
|
||||||
// starting into the editor the simulation never runs at all: there's no driver mode underneath, so
|
// starting into the editor the simulation never runs at all: there's no driver mode underneath, so
|
||||||
// no physics, no AI and no timetables - just the loaded world to work on
|
// no physics, no AI and no timetables - just the loaded world to work on
|
||||||
@@ -79,6 +84,11 @@ void scenarioloader_mode::enter() {
|
|||||||
// TBD: hide cursor in fullscreen mode?
|
// TBD: hide cursor in fullscreen mode?
|
||||||
Application.set_cursor( GLFW_CURSOR_NORMAL );
|
Application.set_cursor( GLFW_CURSOR_NORMAL );
|
||||||
|
|
||||||
|
// a new map from the editor re-enters this mode while the previous load's context is still
|
||||||
|
// sitting in `state`; clear it so update() opens the file again instead of re-finishing an
|
||||||
|
// exhausted parser (Groups.close / create_map_geometry on an already-built region)
|
||||||
|
state.reset();
|
||||||
|
|
||||||
simulation::is_ready = false;
|
simulation::is_ready = false;
|
||||||
|
|
||||||
Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" );
|
Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" );
|
||||||
|
|||||||
@@ -136,11 +136,19 @@ bool http_get(std::string const &, std::vector<std::uint8_t> &, std::string &)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
orthophoto_source::orthophoto_source()
|
orthophoto_source::orthophoto_source(maj0sted::editor::WmsConfig Config, int const Gridzoom, std::string Cachesubdir)
|
||||||
|
: m_service([&] {
|
||||||
|
if (Gridzoom >= maj0sted::editor::TileGrid::kCellZoomMin)
|
||||||
|
{
|
||||||
|
Config.tile_pixels = 512; // coarse cells do not need orto's 1024 px
|
||||||
|
}
|
||||||
|
return std::move(Config);
|
||||||
|
}()),
|
||||||
|
m_gridzoom(Gridzoom), m_cachesubdir(std::move(Cachesubdir))
|
||||||
{
|
{
|
||||||
// the library asks for a URL and expects the answer whenever it arrives; the transfer is queued
|
// the library asks for a URL and expects the answer whenever it arrives; the transfer is queued
|
||||||
// here and handed back on the thread that drains it, so the service stays single threaded
|
// here and handed back on the thread that drains it, so the service stays single threaded
|
||||||
m_service.set_fetcher([this](std::string const &Url, maj0sted::web::TileService::DoneFn Done) {
|
m_service.set_fetcher([this](std::string const &Url, maj0sted::editor::TileService::DoneFn Done) {
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
m_queued.push_back({Url, std::move(Done), false, {}, {}});
|
m_queued.push_back({Url, std::move(Done), false, {}, {}});
|
||||||
@@ -210,7 +218,7 @@ void orthophoto_source::worker()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::web::TileBBox const &View, int const Maxtiles)
|
std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::editor::TileBBox const &View, int const Maxtiles)
|
||||||
{
|
{
|
||||||
// hand finished transfers to the library here, on the caller's thread
|
// hand finished transfers to the library here, on the caller's thread
|
||||||
std::deque<transfer> arrived;
|
std::deque<transfer> arrived;
|
||||||
@@ -227,8 +235,10 @@ std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::
|
|||||||
--m_inflight;
|
--m_inflight;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ready_tile> tiles;
|
// fetch only what fits the budget for the current view. the window is recentred when the
|
||||||
for (auto const &key : maj0sted::web::TileGrid::tiles_for_view(View, maj0sted::web::TileGrid::kLevel, Maxtiles))
|
// view is larger than the cap, and zoom-to-cursor moves the camera, so the set of keys asked
|
||||||
|
// for changes from frame to frame - that must not decide what is drawn
|
||||||
|
for (auto const &key : maj0sted::editor::TileGrid::tiles_for_view(View, m_gridzoom, Maxtiles))
|
||||||
{
|
{
|
||||||
// a tile kept from an earlier session seeds the cache, so no request goes out for it at all
|
// a tile kept from an earlier session seeds the cache, so no request goes out for it at all
|
||||||
if (false == m_service.has(key))
|
if (false == m_service.has(key))
|
||||||
@@ -237,33 +247,51 @@ std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto const status = m_service.request(key);
|
auto const status = m_service.request(key);
|
||||||
if (status == maj0sted::web::TileStatus::Failed)
|
if (status == maj0sted::editor::TileStatus::Failed)
|
||||||
{
|
{
|
||||||
++m_failed;
|
// counted once per tile, not once per frame it stays unavailable
|
||||||
|
m_failedkeys.insert(key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (status != maj0sted::web::TileStatus::Ready)
|
if (status != maj0sted::editor::TileStatus::Ready)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto found = m_textures.find(key);
|
if (m_textures.find(key) == m_textures.end())
|
||||||
if (found == m_textures.end())
|
|
||||||
{
|
{
|
||||||
// the first time a tile is drawn is also the moment its bytes are known to be good
|
// the first time a tile is ready is also the moment its bytes are known to be good
|
||||||
save_to_disk(key);
|
save_to_disk(key);
|
||||||
found = m_textures.emplace(key, upload(key)).first;
|
m_textures.emplace(key, upload(key));
|
||||||
}
|
}
|
||||||
if (found->second != 0)
|
}
|
||||||
|
|
||||||
|
// draw every texture we already hold that still covers the view. returning only the fetch
|
||||||
|
// window made tiles blink out during zoom as soon as they fell outside the cap, then blink
|
||||||
|
// back in once the window moved over them again
|
||||||
|
auto const overlaps = [](maj0sted::editor::TileBBox const &Left, maj0sted::editor::TileBBox const &Right) {
|
||||||
|
return Left.min_x < Right.max_x && Left.max_x > Right.min_x && Left.min_y < Right.max_y && Left.max_y > Right.min_y;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<ready_tile> tiles;
|
||||||
|
tiles.reserve(m_textures.size());
|
||||||
|
for (auto const &[key, texture] : m_textures)
|
||||||
|
{
|
||||||
|
if (texture == 0)
|
||||||
{
|
{
|
||||||
tiles.push_back({maj0sted::web::TileGrid::bbox(key), found->second});
|
continue;
|
||||||
|
}
|
||||||
|
auto const box = maj0sted::editor::TileGrid::bbox(key);
|
||||||
|
if (overlaps(box, View))
|
||||||
|
{
|
||||||
|
tiles.push_back({box, texture});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned int orthophoto_source::upload(maj0sted::web::TileKey const &Key)
|
unsigned int orthophoto_source::upload(maj0sted::editor::TileKey const &Key)
|
||||||
{
|
{
|
||||||
auto const *image = m_service.peek(Key);
|
auto const *image = m_service.peek(Key);
|
||||||
if (image == nullptr || image->bytes.empty())
|
if (image == nullptr || image->bytes.empty())
|
||||||
@@ -296,12 +324,12 @@ unsigned int orthophoto_source::upload(maj0sted::web::TileKey const &Key)
|
|||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string orthophoto_source::tile_path(maj0sted::web::TileKey const &Key)
|
std::string orthophoto_source::tile_path(maj0sted::editor::TileKey const &Key) const
|
||||||
{
|
{
|
||||||
return "editor/cache/orto/" + std::to_string(Key.zoom) + "_" + std::to_string(Key.x) + "_" + std::to_string(Key.y) + ".img";
|
return "editor/cache/" + m_cachesubdir + "/" + std::to_string(Key.zoom) + "_" + std::to_string(Key.x) + "_" + std::to_string(Key.y) + ".img";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool orthophoto_source::load_from_disk(maj0sted::web::TileKey const &Key)
|
bool orthophoto_source::load_from_disk(maj0sted::editor::TileKey const &Key)
|
||||||
{
|
{
|
||||||
std::ifstream file(tile_path(Key), std::ios::binary);
|
std::ifstream file(tile_path(Key), std::ios::binary);
|
||||||
if (false == file.is_open())
|
if (false == file.is_open())
|
||||||
@@ -320,7 +348,7 @@ bool orthophoto_source::load_from_disk(maj0sted::web::TileKey const &Key)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void orthophoto_source::save_to_disk(maj0sted::web::TileKey const &Key)
|
void orthophoto_source::save_to_disk(maj0sted::editor::TileKey const &Key)
|
||||||
{
|
{
|
||||||
auto const *image = m_service.peek(Key);
|
auto const *image = m_service.peek(Key);
|
||||||
if (image == nullptr || image->bytes.empty())
|
if (image == nullptr || image->bytes.empty())
|
||||||
@@ -350,7 +378,7 @@ std::size_t orthophoto_source::pending() const
|
|||||||
|
|
||||||
std::size_t orthophoto_source::failed() const
|
std::size_t orthophoto_source::failed() const
|
||||||
{
|
{
|
||||||
return m_failed;
|
return m_failedkeys.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -383,7 +411,7 @@ unsigned int upload_image(std::vector<std::uint8_t> const &Bytes)
|
|||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool same_box(maj0sted::web::TileBBox const &Left, maj0sted::web::TileBBox const &Right)
|
bool same_box(maj0sted::editor::TileBBox const &Left, maj0sted::editor::TileBBox const &Right)
|
||||||
{
|
{
|
||||||
auto const close = [](double const A, double const B) { return std::abs(A - B) < 1.0; };
|
auto const close = [](double const A, double const B) { return std::abs(A - B) < 1.0; };
|
||||||
return close(Left.min_x, Right.min_x) && close(Left.min_y, Right.min_y) && close(Left.max_x, Right.max_x) && close(Left.max_y, Right.max_y);
|
return close(Left.min_x, Right.min_x) && close(Left.min_y, Right.min_y) && close(Left.max_x, Right.max_x) && close(Left.max_y, Right.max_y);
|
||||||
@@ -391,7 +419,7 @@ bool same_box(maj0sted::web::TileBBox const &Left, maj0sted::web::TileBBox const
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
wms_image::wms_image(maj0sted::web::WmsConfig Config) : m_config(std::move(Config))
|
wms_image::wms_image(maj0sted::editor::WmsConfig Config) : m_config(std::move(Config))
|
||||||
{
|
{
|
||||||
m_worker = std::thread([this]() { worker(); });
|
m_worker = std::thread([this]() { worker(); });
|
||||||
}
|
}
|
||||||
@@ -446,7 +474,7 @@ void wms_image::worker()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned int wms_image::texture_for(maj0sted::web::TileBBox const &Box, int const Pixels)
|
unsigned int wms_image::texture_for(maj0sted::editor::TileBBox const &Box, int const Pixels)
|
||||||
{
|
{
|
||||||
// a finished download becomes the texture on this thread, where the graphics API is safe to touch
|
// a finished download becomes the texture on this thread, where the graphics API is safe to touch
|
||||||
{
|
{
|
||||||
@@ -482,7 +510,7 @@ unsigned int wms_image::texture_for(maj0sted::web::TileBBox const &Box, int cons
|
|||||||
config.tile_pixels = Pixels;
|
config.tile_pixels = Pixels;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
m_url = maj0sted::web::wms_getmap_url(config, Box);
|
m_url = maj0sted::editor::wms_getmap_url(config, Box);
|
||||||
m_haswork = true;
|
m_haswork = true;
|
||||||
}
|
}
|
||||||
m_loading = true;
|
m_loading = true;
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "maj0sted/web/tile_cache.hpp"
|
#include "maj0sted/editor/tile_cache.hpp"
|
||||||
|
|
||||||
namespace editor
|
namespace editor
|
||||||
{
|
{
|
||||||
@@ -37,18 +38,22 @@ class orthophoto_source
|
|||||||
public:
|
public:
|
||||||
struct ready_tile
|
struct ready_tile
|
||||||
{
|
{
|
||||||
maj0sted::web::TileBBox box;
|
maj0sted::editor::TileBBox box;
|
||||||
unsigned int texture{0};
|
unsigned int texture{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
orthophoto_source();
|
// default: Geoportal ortophoto on the fine 100 m grid. pass topo config and a coarse
|
||||||
|
// grid zoom (e.g. 1000) for the zoomed-out topographic underlay
|
||||||
|
explicit orthophoto_source(maj0sted::editor::WmsConfig Config = maj0sted::editor::WmsConfig::geoportal_ortho(), int const Gridzoom = maj0sted::editor::TileGrid::kLevel,
|
||||||
|
std::string Cachesubdir = "orto");
|
||||||
~orthophoto_source();
|
~orthophoto_source();
|
||||||
orthophoto_source(orthophoto_source const &) = delete;
|
orthophoto_source(orthophoto_source const &) = delete;
|
||||||
orthophoto_source &operator=(orthophoto_source const &) = delete;
|
orthophoto_source &operator=(orthophoto_source const &) = delete;
|
||||||
|
|
||||||
// asks for every tile covering the view and returns those that are already drawable. tiles still
|
// kicks off fetches for the view (capped) and returns every already-uploaded tile that still
|
||||||
// on their way simply aren't in the answer yet; call again next frame
|
// intersects it - including ones outside the fetch window, so a zoom cannot blink them away.
|
||||||
std::vector<ready_tile> collect(maj0sted::web::TileBBox const &View, int const Maxtiles = 48);
|
// tiles still on their way simply aren't in the answer yet; call again next frame
|
||||||
|
std::vector<ready_tile> collect(maj0sted::editor::TileBBox const &View, int const Maxtiles = 400);
|
||||||
// how many tiles are still being fetched, for the panel to report
|
// how many tiles are still being fetched, for the panel to report
|
||||||
std::size_t pending() const;
|
std::size_t pending() const;
|
||||||
// how many tiles gave up after exhausting their retries
|
// how many tiles gave up after exhausting their retries
|
||||||
@@ -59,7 +64,7 @@ class orthophoto_source
|
|||||||
struct transfer
|
struct transfer
|
||||||
{
|
{
|
||||||
std::string url;
|
std::string url;
|
||||||
maj0sted::web::TileService::DoneFn done;
|
maj0sted::editor::TileService::DoneFn done;
|
||||||
bool ok{false};
|
bool ok{false};
|
||||||
std::vector<std::uint8_t> bytes;
|
std::vector<std::uint8_t> bytes;
|
||||||
std::string content_type;
|
std::string content_type;
|
||||||
@@ -67,15 +72,17 @@ class orthophoto_source
|
|||||||
// methods
|
// methods
|
||||||
void worker();
|
void worker();
|
||||||
// decodes the cached bytes of a tile and uploads them; returns 0 when the image is unusable
|
// decodes the cached bytes of a tile and uploads them; returns 0 when the image is unusable
|
||||||
unsigned int upload(maj0sted::web::TileKey const &Key);
|
unsigned int upload(maj0sted::editor::TileKey const &Key);
|
||||||
// imagery of a given piece of ground never changes, so a fetched tile is kept on disk and the
|
// imagery of a given piece of ground never changes, so a fetched tile is kept on disk and the
|
||||||
// next session starts with it instead of going back to a service that is slow and flaky
|
// next session starts with it instead of going back to a service that is slow and flaky
|
||||||
static std::string tile_path(maj0sted::web::TileKey const &Key);
|
std::string tile_path(maj0sted::editor::TileKey const &Key) const;
|
||||||
bool load_from_disk(maj0sted::web::TileKey const &Key);
|
bool load_from_disk(maj0sted::editor::TileKey const &Key);
|
||||||
void save_to_disk(maj0sted::web::TileKey const &Key);
|
void save_to_disk(maj0sted::editor::TileKey const &Key);
|
||||||
// members
|
// members
|
||||||
maj0sted::web::TileService m_service;
|
maj0sted::editor::TileService m_service;
|
||||||
std::unordered_map<maj0sted::web::TileKey, unsigned int, maj0sted::web::TileKeyHash> m_textures;
|
int m_gridzoom{maj0sted::editor::TileGrid::kLevel};
|
||||||
|
std::string m_cachesubdir{"orto"};
|
||||||
|
std::unordered_map<maj0sted::editor::TileKey, unsigned int, maj0sted::editor::TileKeyHash> m_textures;
|
||||||
|
|
||||||
mutable std::mutex m_mutex;
|
mutable std::mutex m_mutex;
|
||||||
std::condition_variable m_wakeup;
|
std::condition_variable m_wakeup;
|
||||||
@@ -84,7 +91,7 @@ class orthophoto_source
|
|||||||
std::vector<std::thread> m_workers;
|
std::vector<std::thread> m_workers;
|
||||||
std::atomic<bool> m_quitting{false};
|
std::atomic<bool> m_quitting{false};
|
||||||
std::atomic<std::size_t> m_inflight{0};
|
std::atomic<std::size_t> m_inflight{0};
|
||||||
std::size_t m_failed{0};
|
std::unordered_set<maj0sted::editor::TileKey, maj0sted::editor::TileKeyHash> m_failedkeys;
|
||||||
};
|
};
|
||||||
|
|
||||||
// a single WMS image covering an arbitrary box, for views too coarse for the tile grid - the whole
|
// a single WMS image covering an arbitrary box, for views too coarse for the tile grid - the whole
|
||||||
@@ -94,25 +101,25 @@ class wms_image
|
|||||||
{
|
{
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit wms_image(maj0sted::web::WmsConfig Config);
|
explicit wms_image(maj0sted::editor::WmsConfig Config);
|
||||||
~wms_image();
|
~wms_image();
|
||||||
wms_image(wms_image const &) = delete;
|
wms_image(wms_image const &) = delete;
|
||||||
wms_image &operator=(wms_image const &) = delete;
|
wms_image &operator=(wms_image const &) = delete;
|
||||||
|
|
||||||
// asks for the given box at the given size and returns the texture currently available, which
|
// asks for the given box at the given size and returns the texture currently available, which
|
||||||
// may still be the previous box's until the new one arrives; 0 when there is nothing to show yet
|
// may still be the previous box's until the new one arrives; 0 when there is nothing to show yet
|
||||||
unsigned int texture_for(maj0sted::web::TileBBox const &Box, int const Pixels);
|
unsigned int texture_for(maj0sted::editor::TileBBox const &Box, int const Pixels);
|
||||||
// the box the returned texture actually covers
|
// the box the returned texture actually covers
|
||||||
maj0sted::web::TileBBox const &covered() const { return m_covered; }
|
maj0sted::editor::TileBBox const &covered() const { return m_covered; }
|
||||||
bool loading() const { return m_loading.load(); }
|
bool loading() const { return m_loading.load(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void worker();
|
void worker();
|
||||||
|
|
||||||
maj0sted::web::WmsConfig m_config;
|
maj0sted::editor::WmsConfig m_config;
|
||||||
maj0sted::web::TileBBox m_requested{};
|
maj0sted::editor::TileBBox m_requested{};
|
||||||
maj0sted::web::TileBBox m_inflightbox{};
|
maj0sted::editor::TileBBox m_inflightbox{};
|
||||||
maj0sted::web::TileBBox m_covered{};
|
maj0sted::editor::TileBBox m_covered{};
|
||||||
unsigned int m_texture{0};
|
unsigned int m_texture{0};
|
||||||
|
|
||||||
mutable std::mutex m_mutex;
|
mutable std::mutex m_mutex;
|
||||||
|
|||||||
@@ -15,15 +15,21 @@ namespace editor
|
|||||||
|
|
||||||
std::vector<turnout_preset> const &turnout_presets()
|
std::vector<turnout_preset> const &turnout_presets()
|
||||||
{
|
{
|
||||||
// the everyday PKP rozjazdy zwyczajne. the crossing mark and radius are the defining pair;
|
// the standard PKP rozjazdy zwyczajne (49E1 / 60E1). crossing mark, radius and catalogue length
|
||||||
// the length the switch spans follows from them and is shown in the editor
|
// (PR→KR along the through track). the crossing angles check out as atan(1/n); the lengths follow
|
||||||
|
// the type tables (metres). the diverging leaves not tangent but at the blade angle (beta) after a
|
||||||
|
// straight pre-blade lead, then arcs by (alfa - beta) to the frog.
|
||||||
|
//
|
||||||
|
// blade_angle and pre_blade are NOT yet from Id-1 - left at 0 (a tangent arc from PR) until the
|
||||||
|
// catalogue figures are in. Fill them per type when we have them; the geometry already supports it.
|
||||||
static std::vector<turnout_preset> const presets{
|
static std::vector<turnout_preset> const presets{
|
||||||
{"Rz 1:9 R190", 9.0, 190.0},
|
{"Rz 1:7,5 R190", 7.5, 190.0, 25.222, 0.0, 0.0},
|
||||||
{"Rz 1:9 R300", 9.0, 300.0},
|
{"Rz 1:9 R190", 9.0, 190.0, 27.138, 0.0, 0.0},
|
||||||
{"Rz 1:12 R300", 12.0, 300.0},
|
{"Rz 1:9 R300", 9.0, 300.0, 33.230, 0.0, 0.0},
|
||||||
{"Rz 1:14 R500", 14.0, 500.0},
|
{"Rz 1:12 R500", 12.0, 500.0, 41.594, 0.0, 0.0},
|
||||||
{"Rz 1:18,5 R1200", 18.5, 1200.0},
|
{"Rz 1:14 R500", 14.0, 500.0, 42.371, 0.0, 0.0},
|
||||||
{"Rz 1:26,5 R2500", 26.5, 2500.0}};
|
{"Rz 1:14 R760", 14.0, 760.0, 54.216, 0.0, 0.0},
|
||||||
|
{"Rz 1:18,5 R1200", 18.5, 1200.0, 64.818, 0.0, 0.0}};
|
||||||
return presets;
|
return presets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ namespace editor
|
|||||||
struct turnout_preset
|
struct turnout_preset
|
||||||
{
|
{
|
||||||
std::string name;
|
std::string name;
|
||||||
double crossing_n;
|
double crossing_n; // skos 1:n
|
||||||
double radius;
|
double radius; // metres
|
||||||
|
double length; // catalogue length PR->KR along the through track, metres
|
||||||
|
double blade_angle; // kąt nagięcia iglicy (beta), radians; the diverging leaves the straight here
|
||||||
|
double pre_blade; // odcinek przediglicowy: straight lead PR -> blade tip, metres
|
||||||
};
|
};
|
||||||
|
|
||||||
// typical Polish turnouts (rozjazdy zwyczajne). the (skos, R) pairs are the recognised standard;
|
// typical Polish turnouts (rozjazdy zwyczajne). the (skos, R) pairs are the recognised standard;
|
||||||
|
|||||||
@@ -31,15 +31,19 @@ add_library(maj0sted STATIC
|
|||||||
src/domain/parallelism_service.cpp
|
src/domain/parallelism_service.cpp
|
||||||
src/domain/fitting_service.cpp
|
src/domain/fitting_service.cpp
|
||||||
src/domain/gap_fitter.cpp
|
src/domain/gap_fitter.cpp
|
||||||
|
src/domain/turnout.cpp
|
||||||
src/io/project_serializer.cpp
|
src/io/project_serializer.cpp
|
||||||
src/io/svg_writer.cpp
|
src/io/svg_writer.cpp
|
||||||
|
src/io/scn_export.cpp
|
||||||
src/render/plan_sampler.cpp
|
src/render/plan_sampler.cpp
|
||||||
src/render/rail_renderer.cpp
|
src/render/rail_renderer.cpp
|
||||||
src/web/solve.cpp
|
src/editor/solve.cpp
|
||||||
src/web/editor.cpp
|
src/editor/editor.cpp
|
||||||
src/web/ribbon.cpp
|
src/editor/ribbon.cpp
|
||||||
src/web/tile_cache.cpp
|
src/editor/tile_cache.cpp
|
||||||
src/app/editor_document.cpp
|
src/app/editor_document.cpp
|
||||||
|
src/app/straight_constraints.cpp
|
||||||
|
src/app/basket_draft.cpp
|
||||||
)
|
)
|
||||||
add_library(maj0sted::maj0sted ALIAS maj0sted)
|
add_library(maj0sted::maj0sted ALIAS maj0sted)
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ Toolchain WASM: `emsdk` jest w `~/emsdk` (`source ~/emsdk/emsdk_env.sh` dodaje `
|
|||||||
```bash
|
```bash
|
||||||
cd /home/maj00r/Projects/maj0sted
|
cd /home/maj00r/Projects/maj0sted
|
||||||
FLAGS="-std=c++20 -Iinclude -Wall -Wextra -Wpedantic -Wconversion -Wshadow"
|
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"
|
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
|
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
|
g++ $FLAGS -Itests $SRC tests/test_$t.cpp -o /tmp/t_$t && /tmp/t_$t | tail -1
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
Wszystkie testy przechodzą, kompilacja bez ostrzeżeń. Oszczędnie: kompiluj tylko
|
Wszystkie testy przechodzą, kompilacja bez ostrzeżeń. Oszczędnie: kompiluj tylko
|
||||||
zmieniony test z minimalnym zestawem źródeł (np. `tile_cache` wymaga 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/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 ...`.
|
src/app/editor_document.cpp`). Szybki syntax-check: `g++ $FLAGS -fsyntax-only ...`.
|
||||||
|
|
||||||
Budowa WASM: `source ~/emsdk/emsdk_env.sh && cd web && ./build.sh`
|
Budowa WASM: `source ~/emsdk/emsdk_env.sh && cd web && ./build.sh`
|
||||||
@@ -90,7 +90,7 @@ Budowa WASM: `source ~/emsdk/emsdk_env.sh && cd web && ./build.sh`
|
|||||||
równoległości (patrz niżej — StraightId nie są globalnie unikalne).
|
równoległości (patrz niżej — StraightId nie są globalnie unikalne).
|
||||||
|
|
||||||
### Rendering: dwie szyny
|
### Rendering: dwie szyny
|
||||||
`src/web/editor.cpp` rysuje **dwie szyny w rozstawie 1500 mm** (`kHalfGauge=0.75`,
|
`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ż
|
±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)`),
|
normalnej per-wierzchołek (różnica centralna, normalna lewa `(-dy,dx)`),
|
||||||
`push_rails` emituje lewą i prawą szynę zachowując `kind` (kolor). Uchwyty edycji
|
`push_rails` emituje lewą i prawą szynę zachowując `kind` (kolor). Uchwyty edycji
|
||||||
@@ -100,7 +100,7 @@ normalnej per-wierzchołek (różnica centralna, normalna lewa `(-dy,dx)`),
|
|||||||
Zasada: **cała logika jest w libce**; `web/` to cienki renderer + przekazywanie
|
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.
|
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`) ✓
|
- **Kafle podkładu WMS** (`maj0sted::editor`, `web/tile_cache.hpp/.cpp`) ✓
|
||||||
- `TileGrid` — **jeden stały grid kafli 100 m × 100 m** w **EPSG:2180**
|
- `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
|
(`kCellMetres=100`, **1024 px/kafel** → 100/1024 m/px, origin (0,0), jeden
|
||||||
poziom `kLevel=0`, **bez piramidy per-zoom**); `tile_at`, `bbox`,
|
poziom `kLevel=0`, **bez piramidy per-zoom**); `tile_at`, `bbox`,
|
||||||
|
|||||||
58
maj0sted/include/maj0sted/app/basket_draft.hpp
Normal file
58
maj0sted/include/maj0sted/app/basket_draft.hpp
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "maj0sted/editor/editor.hpp"
|
||||||
|
#include "maj0sted/editor/ribbon.hpp"
|
||||||
|
#include "maj0sted/editor/solve.hpp"
|
||||||
|
|
||||||
|
namespace maj0sted::app {
|
||||||
|
|
||||||
|
struct BasketDraft {
|
||||||
|
bool active{false};
|
||||||
|
double x0{0.0};
|
||||||
|
double y0{0.0};
|
||||||
|
double az0{0.0};
|
||||||
|
int dir{1};
|
||||||
|
double entry_t{0.0};
|
||||||
|
double exit_t{0.0};
|
||||||
|
std::vector<maj0sted::editor::CompoundArcSpec> arcs;
|
||||||
|
double preview_next_r{300.0};
|
||||||
|
int attach_str{-1};
|
||||||
|
int attach_end{-1};
|
||||||
|
int reedit_gap{-1};
|
||||||
|
maj0sted::editor::GapFit reedit_backup{};
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] maj0sted::editor::RibbonRequest draft_to_ribbon(const BasketDraft& draft);
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<maj0sted::editor::PlanPolyline> preview_basket_draft(
|
||||||
|
BasketDraft& draft, double fallback_next_r = 300.0);
|
||||||
|
|
||||||
|
void start_basket_draft_from_end(BasketDraft& draft,
|
||||||
|
const maj0sted::editor::StraightSpec& straight,
|
||||||
|
int straight_index, int end, double default_r = 300.0);
|
||||||
|
|
||||||
|
[[nodiscard]] bool reopen_basket_draft(BasketDraft& draft,
|
||||||
|
maj0sted::editor::NiweletaSpec& niweleta,
|
||||||
|
std::size_t gap,
|
||||||
|
const maj0sted::editor::NiweletaPolys* solved);
|
||||||
|
|
||||||
|
void discard_basket_draft(BasketDraft& draft, maj0sted::editor::NiweletaSpec* niweleta);
|
||||||
|
|
||||||
|
[[nodiscard]] bool anchor_basket_draft(BasketDraft& draft,
|
||||||
|
maj0sted::editor::NiweletaSpec& niweleta,
|
||||||
|
std::size_t gap);
|
||||||
|
|
||||||
|
struct FinishBasketDraftResult {
|
||||||
|
bool ok{false};
|
||||||
|
std::string status;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] FinishBasketDraftResult finish_basket_draft(
|
||||||
|
BasketDraft& draft, maj0sted::editor::NiweletaSpec& niweleta,
|
||||||
|
std::vector<maj0sted::editor::NiweletaSpec>& all_niwelety, int niweleta_index,
|
||||||
|
double finish_straight_m);
|
||||||
|
|
||||||
|
} // namespace maj0sted::app
|
||||||
@@ -5,13 +5,13 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "maj0sted/domain/map_project.hpp"
|
#include "maj0sted/domain/map_project.hpp"
|
||||||
#include "maj0sted/web/editor.hpp"
|
#include "maj0sted/editor/editor.hpp"
|
||||||
|
|
||||||
namespace maj0sted::app {
|
namespace maj0sted::app {
|
||||||
|
|
||||||
struct EditorDocument {
|
struct EditorDocument {
|
||||||
std::vector<maj0sted::web::NiweletaSpec> niwelety;
|
std::vector<maj0sted::editor::NiweletaSpec> niwelety;
|
||||||
std::vector<maj0sted::web::Junction> junctions;
|
std::vector<maj0sted::editor::Junction> junctions;
|
||||||
double view_x{0.0};
|
double view_x{0.0};
|
||||||
double view_y{0.0};
|
double view_y{0.0};
|
||||||
double view_extent{0.0};
|
double view_extent{0.0};
|
||||||
|
|||||||
12
maj0sted/include/maj0sted/app/straight_constraints.hpp
Normal file
12
maj0sted/include/maj0sted/app/straight_constraints.hpp
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "maj0sted/editor/editor.hpp"
|
||||||
|
|
||||||
|
namespace maj0sted::app {
|
||||||
|
|
||||||
|
void apply_straight_constraints(std::vector<maj0sted::editor::NiweletaSpec>& niwelety);
|
||||||
|
void align_gap_fits(maj0sted::editor::NiweletaSpec& niweleta);
|
||||||
|
|
||||||
|
} // namespace maj0sted::app
|
||||||
85
maj0sted/include/maj0sted/domain/geometry/turnout.hpp
Normal file
85
maj0sted/include/maj0sted/domain/geometry/turnout.hpp
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "maj0sted/domain/geometry/segment_layout.hpp" // Pose
|
||||||
|
|
||||||
|
namespace maj0sted::domain {
|
||||||
|
|
||||||
|
/// The crossing mark of a turnout — the skos 1:n. It is the tangent of the angle
|
||||||
|
/// between the through and diverging tracks, so it fixes the total angle the
|
||||||
|
/// diverging curve must turn through: a turnout is 1:9, 1:12, 1:18,5 and so on.
|
||||||
|
class CrossingMark {
|
||||||
|
public:
|
||||||
|
explicit CrossingMark(double denominator) noexcept : denominator_{denominator} {}
|
||||||
|
|
||||||
|
[[nodiscard]] double denominator() const noexcept { return denominator_; }
|
||||||
|
/// The angle between the tracks, radians. Zero for a non-positive mark.
|
||||||
|
[[nodiscard]] double angle() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
double denominator_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Which way the branch leaves the through track.
|
||||||
|
enum class DivergeSide { Left, Right };
|
||||||
|
|
||||||
|
/// One arc of a turnout's diverging curve: its radius and — unless it is the
|
||||||
|
/// closing arc — its fixed length and the clothoid easing it into the next arc.
|
||||||
|
struct TurnoutArc {
|
||||||
|
double radius{0.0};
|
||||||
|
double length{0.0}; ///< fixed length; ignored for the closing arc
|
||||||
|
double transition_to_next{0.0}; ///< clothoid to the next arc (0 = none)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The shape of a turnout's diverging curve. One arc is the everyday turnout; two
|
||||||
|
/// or more make a łuk koszowy. Whatever the shape, the curve turns through exactly
|
||||||
|
/// the crossing angle — the closing (last) arc spends whatever deflection the
|
||||||
|
/// earlier arcs and clothoids leave.
|
||||||
|
struct DivergingCurve {
|
||||||
|
std::vector<TurnoutArc> arcs; ///< at least one; the last one closes the turn
|
||||||
|
double entry_transition{0.0}; ///< clothoid from the through track into the first arc
|
||||||
|
double exit_transition{0.0}; ///< clothoid from the last arc back toward tangent
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A turnout (rozjazd), described the way a catalogue does: a crossing mark, the
|
||||||
|
/// side it opens to, the free diverging curve inside it, and the length it spans
|
||||||
|
/// from its start (początek rozjazdu) to its end (koniec rozjazdu) measured along
|
||||||
|
/// the through track. The curve turns to the crossing angle and a straight frog
|
||||||
|
/// rail carries the track on to that length.
|
||||||
|
struct Turnout {
|
||||||
|
CrossingMark mark;
|
||||||
|
DivergeSide side{DivergeSide::Left};
|
||||||
|
DivergingCurve curve;
|
||||||
|
double length{0.0}; ///< catalogue length along the through track; 0 = a tangent-length rail
|
||||||
|
};
|
||||||
|
|
||||||
|
/// One laid segment of the diverging path, as the constant/linear curvature the
|
||||||
|
/// renderer and the fitter already share (see segment_layout): a straight when
|
||||||
|
/// both curvatures are zero, an arc when they are equal, a clothoid otherwise.
|
||||||
|
struct TurnoutSegment {
|
||||||
|
double k0{0.0};
|
||||||
|
double k1{0.0};
|
||||||
|
double length{0.0};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// What a turnout works out to when laid from its start pose.
|
||||||
|
struct TurnoutGeometry {
|
||||||
|
bool valid{false};
|
||||||
|
std::vector<TurnoutSegment> path; ///< diverging segments from the switch start to the frog
|
||||||
|
geometry::Pose frog{}; ///< the end (koniec rozjazdu): position + heading at the crossing angle
|
||||||
|
double tangent_front{0.0}; ///< theoretical tangent length, switch start to the crossing point
|
||||||
|
double tangent_back{0.0}; ///< theoretical tangent length, crossing point to the frog
|
||||||
|
double diverging_length{0.0}; ///< total length of the diverging path (curve + frog rail)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Lays a turnout forward from @p start — the switch start, tangent to the through
|
||||||
|
/// track — turning toward its side until the heading has swung by the crossing
|
||||||
|
/// angle, then running straight to the catalogue length. Never throws; returns
|
||||||
|
/// `valid == false` when the parameters admit no geometry: a non-positive radius,
|
||||||
|
/// or a curve whose fixed parts already overshoot the crossing angle before the
|
||||||
|
/// closing arc.
|
||||||
|
[[nodiscard]] TurnoutGeometry lay_turnout(const geometry::Pose& start,
|
||||||
|
const Turnout& turnout);
|
||||||
|
|
||||||
|
} // namespace maj0sted::domain
|
||||||
@@ -30,6 +30,10 @@ public:
|
|||||||
/// Perpendicular distance (>= 0) from @p other's start point to the
|
/// Perpendicular distance (>= 0) from @p other's start point to the
|
||||||
/// (infinite) line carrying @p origin.
|
/// (infinite) line carrying @p origin.
|
||||||
static double perpendicular_distance(const Straight& origin, const Straight& other);
|
static double perpendicular_distance(const Straight& origin, const Straight& other);
|
||||||
|
|
||||||
|
[[nodiscard]] static Straight project_onto_offset_line(const Straight& origin,
|
||||||
|
double offset_m,
|
||||||
|
const Straight& segment);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace maj0sted::domain
|
} // namespace maj0sted::domain
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "maj0sted/web/solve.hpp" // WebPoint, WebPolyline
|
#include "maj0sted/editor/solve.hpp" // PlanPoint, PlanPolyline
|
||||||
|
|
||||||
namespace maj0sted::web {
|
namespace maj0sted::editor {
|
||||||
|
|
||||||
/// An anchored straight segment (two XY endpoints).
|
/// An anchored straight segment (two XY endpoints).
|
||||||
struct StraightSpec {
|
struct StraightSpec {
|
||||||
@@ -63,7 +63,7 @@ struct NiweletaSpec {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct NiweletaPolys {
|
struct NiweletaPolys {
|
||||||
std::vector<WebPolyline> polylines;
|
std::vector<PlanPolyline> polylines;
|
||||||
/// Centreline segments after adjacent fits trim the input straights. Kept
|
/// Centreline segments after adjacent fits trim the input straights. Kept
|
||||||
/// index-aligned with NiweletaSpec::straights for editor hit handles.
|
/// index-aligned with NiweletaSpec::straights for editor hit handles.
|
||||||
std::vector<StraightSpec> rendered_straights;
|
std::vector<StraightSpec> rendered_straights;
|
||||||
@@ -74,7 +74,7 @@ struct NiweletaPolys {
|
|||||||
std::vector<GapFit> applied_fits;
|
std::vector<GapFit> applied_fits;
|
||||||
/// The solved axis as one polyline, in order, with no duplicate join points.
|
/// The solved axis as one polyline, in order, with no duplicate join points.
|
||||||
/// Lets a station along the niweleta be resolved to a pose (see pose_at).
|
/// Lets a station along the niweleta be resolved to a pose (see pose_at).
|
||||||
std::vector<WebPoint> centreline;
|
std::vector<PlanPoint> centreline;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Builds each niweleta, fits every requested gap, and returns the sampled plan
|
/// Builds each niweleta, fits every requested gap, and returns the sampled plan
|
||||||
@@ -98,6 +98,7 @@ struct Junction {
|
|||||||
int side{0}; ///< 0 = diverges left, 1 = diverges right
|
int side{0}; ///< 0 = diverges left, 1 = diverges right
|
||||||
bool facing{true}; ///< true opens toward increasing station, false back
|
bool facing{true}; ///< true opens toward increasing station, false back
|
||||||
double crossing_n{9.0}; ///< crossing mark 1:n (skos) → leaving angle atan(1/n)
|
double crossing_n{9.0}; ///< crossing mark 1:n (skos) → leaving angle atan(1/n)
|
||||||
|
double length{0.0}; ///< catalogue length (PR→KR along the through track); 0 = just the curve
|
||||||
GapFit curve; ///< the internal diverging curve: arc / compound / basket
|
GapFit curve; ///< the internal diverging curve: arc / compound / basket
|
||||||
int branch{-1}; ///< index of the branch niweleta, pinned to the frog
|
int branch{-1}; ///< index of the branch niweleta, pinned to the frog
|
||||||
};
|
};
|
||||||
@@ -114,7 +115,7 @@ struct JunctionGeom {
|
|||||||
double tangent_back{0.0}; ///< theoretical tangent length after the curve
|
double tangent_back{0.0}; ///< theoretical tangent length after the curve
|
||||||
double length{0.0}; ///< diverging curve length (switch point to frog), metres
|
double length{0.0}; ///< diverging curve length (switch point to frog), metres
|
||||||
/// The diverging curve, as rails ready to draw.
|
/// The diverging curve, as rails ready to draw.
|
||||||
std::vector<WebPolyline> polylines;
|
std::vector<PlanPolyline> polylines;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A whole layout solved at once: the niwelety plus every switch. Returned by
|
/// A whole layout solved at once: the niwelety plus every switch. Returned by
|
||||||
@@ -132,4 +133,4 @@ struct LayoutSolution {
|
|||||||
[[nodiscard]] LayoutSolution solve_layout(std::vector<NiweletaSpec> niwelety,
|
[[nodiscard]] LayoutSolution solve_layout(std::vector<NiweletaSpec> niwelety,
|
||||||
const std::vector<Junction>& junctions);
|
const std::vector<Junction>& junctions);
|
||||||
|
|
||||||
} // namespace maj0sted::web
|
} // namespace maj0sted::editor
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "maj0sted/web/solve.hpp" // WebPoint, WebPolyline
|
#include "maj0sted/editor/solve.hpp" // PlanPoint, PlanPolyline
|
||||||
|
|
||||||
namespace maj0sted::web {
|
namespace maj0sted::editor {
|
||||||
|
|
||||||
/// One element of an alignment "ribbon" (element chain), defined intrinsically.
|
/// One element of an alignment "ribbon" (element chain), defined intrinsically.
|
||||||
struct RibbonElement {
|
struct RibbonElement {
|
||||||
@@ -27,6 +27,6 @@ struct RibbonRequest {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Lays the ribbon out and returns one polyline per element, ready to draw.
|
/// Lays the ribbon out and returns one polyline per element, ready to draw.
|
||||||
[[nodiscard]] std::vector<WebPolyline> solve_ribbon(const RibbonRequest& request);
|
[[nodiscard]] std::vector<PlanPolyline> solve_ribbon(const RibbonRequest& request);
|
||||||
|
|
||||||
} // namespace maj0sted::web
|
} // namespace maj0sted::editor
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace maj0sted::web {
|
namespace maj0sted::editor {
|
||||||
|
|
||||||
/// A request from the GUI: three draggable vertices (entry straight A->B, exit
|
/// A request from the GUI: three draggable vertices (entry straight A->B, exit
|
||||||
/// straight B->C) plus the fit parameters. Plain doubles so it maps trivially
|
/// straight B->C) plus the fit parameters. Plain doubles, a flat DTO across the
|
||||||
/// onto a JS object / WASM boundary. This layer is pure C++ — the emscripten
|
/// boundary between the editor UI and this solver.
|
||||||
/// bindings are a thin wrapper around it, and it is unit-tested natively.
|
|
||||||
struct SolveRequest {
|
struct SolveRequest {
|
||||||
double ax{0.0}, ay{0.0}; ///< vertex A (entry start)
|
double ax{0.0}, ay{0.0}; ///< vertex A (entry start)
|
||||||
double bx{0.0}, by{0.0}; ///< vertex B (corner: entry end = exit start)
|
double bx{0.0}, by{0.0}; ///< vertex B (corner: entry end = exit start)
|
||||||
@@ -28,13 +27,13 @@ struct SolveRequest {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// A point in the CRS.
|
/// A point in the CRS.
|
||||||
struct WebPoint {
|
struct PlanPoint {
|
||||||
double x{0.0};
|
double x{0.0};
|
||||||
double y{0.0};
|
double y{0.0};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One polyline; @c kind is render::ElementKind (0 straight, 1 arc, 2 transition).
|
/// One polyline; @c kind is render::ElementKind (0 straight, 1 arc, 2 transition).
|
||||||
struct WebPolyline {
|
struct PlanPolyline {
|
||||||
int kind{0};
|
int kind{0};
|
||||||
/// Logical owner used by map hit-testing. Exactly one is non-negative.
|
/// Logical owner used by map hit-testing. Exactly one is non-negative.
|
||||||
int straight_index{-1};
|
int straight_index{-1};
|
||||||
@@ -45,12 +44,12 @@ struct WebPolyline {
|
|||||||
/// Zero denotes an infinite radius (straight end of a transition).
|
/// Zero denotes an infinite radius (straight end of a transition).
|
||||||
double radius_start{0.0};
|
double radius_start{0.0};
|
||||||
double radius_end{0.0};
|
double radius_end{0.0};
|
||||||
std::vector<WebPoint> points;
|
std::vector<PlanPoint> points;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Builds the niweleta, runs the requested fit (falling back to the bare
|
/// Builds the niweleta, runs the requested fit (falling back to the bare
|
||||||
/// straights if the parameters do not admit a fit) and returns the sampled plan
|
/// straights if the parameters do not admit a fit) and returns the sampled plan
|
||||||
/// as polylines ready to draw.
|
/// as polylines ready to draw.
|
||||||
[[nodiscard]] std::vector<WebPolyline> solve_scene(const SolveRequest& request);
|
[[nodiscard]] std::vector<PlanPolyline> solve_scene(const SolveRequest& request);
|
||||||
|
|
||||||
} // namespace maj0sted::web
|
} // namespace maj0sted::editor
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace maj0sted::web {
|
namespace maj0sted::editor {
|
||||||
|
|
||||||
/// A stable tile in the EPSG:2180 tiling pyramid. Tiles are addressed by their
|
/// A stable tile in the EPSG:2180 tiling pyramid. Tiles are addressed by their
|
||||||
/// zoom level and integer column/row, so the same ground square always maps to
|
/// zoom level and integer column/row, so the same ground square always maps to
|
||||||
@@ -39,42 +39,43 @@ struct TileBBox {
|
|||||||
double max_y{0.0};
|
double max_y{0.0};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The tiling scheme. A pure, static value type defining a single fixed square
|
/// The tiling scheme. A pure, static value type defining a square grid over the
|
||||||
/// grid of 100 m × 100 m ground cells over the EPSG:2180 plane — the same grid
|
/// EPSG:2180 plane. The default grid is fixed 100 m × 100 m cells (key.zoom =
|
||||||
/// at every zoom, *not* a per-zoom pyramid. Each cell has a deterministic ground
|
/// kLevel). Coarser overview grids encode the cell size in metres in
|
||||||
/// bounding box, so it is cached once and reused across all pans and zooms.
|
/// @c TileKey::zoom when that value is >= 100 (e.g. zoom 1000 → 1 km cells for
|
||||||
/// Nothing here touches the network — it is trivially unit testable natively.
|
/// topo underlays). Nothing here touches the network.
|
||||||
///
|
|
||||||
/// The zoom parameters on the methods below are ignored; they are kept only so
|
|
||||||
/// the host boundary (bindings/JS) stays unchanged.
|
|
||||||
class TileGrid {
|
class TileGrid {
|
||||||
public:
|
public:
|
||||||
static constexpr int kTilePixels = 1024; ///< pixels per cell edge (GetMap WIDTH/HEIGHT)
|
static constexpr int kTilePixels = 1024; ///< pixels per cell edge (GetMap WIDTH/HEIGHT)
|
||||||
static constexpr double kOriginX = 0.0; ///< easting of cell (0,0)'s left edge
|
static constexpr double kOriginX = 0.0; ///< easting of cell (0,0)'s left edge
|
||||||
static constexpr double kOriginY = 0.0; ///< northing of cell (0,0)'s bottom edge
|
static constexpr double kOriginY = 0.0; ///< northing of cell (0,0)'s bottom edge
|
||||||
/// Fixed cache cell size: every tile is a 100 m × 100 m ground square.
|
/// Default cache cell size: 100 m × 100 m ground square.
|
||||||
static constexpr double kCellMetres = 100.0;
|
static constexpr double kCellMetres = 100.0;
|
||||||
/// The single grid level stored in every TileKey (there is only one grid).
|
/// Default grid level stored in TileKey::zoom for the 100 m grid.
|
||||||
static constexpr int kLevel = 0;
|
static constexpr int kLevel = 0;
|
||||||
|
/// Zoom values at or above this are treated as the cell edge in metres.
|
||||||
|
static constexpr int kCellZoomMin = 100;
|
||||||
|
|
||||||
/// Ground resolution (metres per pixel) of a cell. Constant (zoom ignored).
|
/// Cell edge in metres for @p key (100 m default, or key.zoom when coarse).
|
||||||
|
[[nodiscard]] static double cell_metres(const TileKey& key) noexcept;
|
||||||
|
|
||||||
|
/// Ground resolution (metres per pixel) of a cell.
|
||||||
[[nodiscard]] static double resolution(int zoom = kLevel) noexcept;
|
[[nodiscard]] static double resolution(int zoom = kLevel) noexcept;
|
||||||
|
|
||||||
/// Edge length of a cell in metres — always kCellMetres (zoom ignored).
|
/// Edge length of a cell in metres for the given zoom/cell encoding.
|
||||||
[[nodiscard]] static double tile_span(int zoom = kLevel) noexcept;
|
[[nodiscard]] static double tile_span(int zoom = kLevel) noexcept;
|
||||||
|
|
||||||
/// The 100 m cell whose ground square contains (@p x, @p y). Zoom ignored.
|
/// The cell whose ground square contains (@p x, @p y).
|
||||||
[[nodiscard]] static TileKey tile_at(double x, double y, int zoom = kLevel) noexcept;
|
[[nodiscard]] static TileKey tile_at(double x, double y, int zoom = kLevel) noexcept;
|
||||||
|
|
||||||
/// The ground bounding box (EPSG:2180) of @p key.
|
/// The ground bounding box (EPSG:2180) of @p key.
|
||||||
[[nodiscard]] static TileBBox bbox(const TileKey& key) noexcept;
|
[[nodiscard]] static TileBBox bbox(const TileKey& key) noexcept;
|
||||||
|
|
||||||
/// The grid level for a target resolution. Always kLevel — the grid has a
|
/// The grid level for a target resolution. Always kLevel for the fine grid.
|
||||||
/// single fixed 100 m level. Kept for boundary compatibility.
|
|
||||||
[[nodiscard]] static int zoom_for_resolution(double metres_per_pixel) noexcept;
|
[[nodiscard]] static int zoom_for_resolution(double metres_per_pixel) noexcept;
|
||||||
|
|
||||||
/// Every 100 m cell whose ground square intersects @p view, capped at
|
/// Every cell whose ground square intersects @p view, capped at @p max_tiles.
|
||||||
/// @p max_tiles to guard against absurd requests. Zoom ignored.
|
/// Pass a coarse cell size (e.g. 1000) as @p zoom to get a 1 km overview grid.
|
||||||
[[nodiscard]] static std::vector<TileKey> tiles_for_view(const TileBBox& view,
|
[[nodiscard]] static std::vector<TileKey> tiles_for_view(const TileBBox& view,
|
||||||
int zoom = kLevel,
|
int zoom = kLevel,
|
||||||
int max_tiles = 64);
|
int max_tiles = 64);
|
||||||
@@ -114,10 +115,9 @@ struct TileImage {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Owns the tile grid, the tile cache and the WMS fetch. This is the single
|
/// Owns the tile grid, the tile cache and the WMS fetch. This is the single
|
||||||
/// place that decides which tile is needed, whether it is already cached, and —
|
/// place that decides which tile is needed and whether it is already cached. The
|
||||||
/// on a miss — issues the network GetMap request itself. Under Emscripten the
|
/// download itself is the host's: it injects a Fetcher, so the tiling and caching
|
||||||
/// fetch uses the Emscripten Fetch API; natively (and in tests) a fetcher can be
|
/// logic here stays transport-free and is exercised in tests without a network.
|
||||||
/// injected, so the tiling/caching logic is exercised without a real network.
|
|
||||||
class TileService {
|
class TileService {
|
||||||
public:
|
public:
|
||||||
/// Called when a fetch finishes: (success, bytes, content_type).
|
/// Called when a fetch finishes: (success, bytes, content_type).
|
||||||
@@ -131,9 +131,8 @@ public:
|
|||||||
[[nodiscard]] const WmsConfig& config() const noexcept { return cfg_; }
|
[[nodiscard]] const WmsConfig& config() const noexcept { return cfg_; }
|
||||||
void set_config(WmsConfig cfg) { cfg_ = std::move(cfg); }
|
void set_config(WmsConfig cfg) { cfg_ = std::move(cfg); }
|
||||||
|
|
||||||
/// Overrides the network backend. When set it is used on both native and
|
/// Sets the transport. When unset, a requested tile fails at once (there is
|
||||||
/// Emscripten builds; when unset, Emscripten builds fall back to
|
/// no built-in downloader — the host owns that).
|
||||||
/// emscripten_fetch and native builds mark the tile Failed.
|
|
||||||
void set_fetcher(Fetcher fetcher) { fetcher_ = std::move(fetcher); }
|
void set_fetcher(Fetcher fetcher) { fetcher_ = std::move(fetcher); }
|
||||||
|
|
||||||
/// Notified (with the tile's key) whenever a tile transitions to Ready or
|
/// Notified (with the tile's key) whenever a tile transitions to Ready or
|
||||||
@@ -161,20 +160,17 @@ public:
|
|||||||
|
|
||||||
/// Finalises an in-flight fetch: on success stores the bytes (Ready), on
|
/// Finalises an in-flight fetch: on success stores the bytes (Ready), on
|
||||||
/// failure marks the tile Failed, then fires the on_ready callback. Public so
|
/// failure marks the tile Failed, then fires the on_ready callback. Public so
|
||||||
/// the Emscripten fetch glue (which needs free C callbacks) can delegate here.
|
/// the host's fetcher callback can hand the result back here.
|
||||||
void complete_fetch(const TileKey& key, bool ok, std::vector<std::uint8_t> bytes,
|
void complete_fetch(const TileKey& key, bool ok, std::vector<std::uint8_t> bytes,
|
||||||
std::string content_type);
|
std::string content_type);
|
||||||
|
|
||||||
/// Re-issues the fetch for @p key. Public so the Emscripten async-retry glue
|
/// Re-issues the fetch for @p key.
|
||||||
/// (a free C callback) can delegate here.
|
|
||||||
void retry_now(const TileKey& key);
|
void retry_now(const TileKey& key);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void begin_fetch(const TileKey& key);
|
void begin_fetch(const TileKey& key);
|
||||||
/// Re-issues a fetch for @p key after a failure — the service retries up to
|
/// Re-issues a fetch for @p key after a failure — the service retries up to
|
||||||
/// kMaxRetries times, then marks the tile Failed. Under Emscripten the retry
|
/// kMaxRetries times, then marks the tile Failed. The fetcher owns the timing.
|
||||||
/// is deferred asynchronously (after a short delay) so it never blocks or
|
|
||||||
/// recurses; with an injected fetcher it re-issues immediately.
|
|
||||||
void schedule_retry(const TileKey& key);
|
void schedule_retry(const TileKey& key);
|
||||||
|
|
||||||
WmsConfig cfg_;
|
WmsConfig cfg_;
|
||||||
@@ -183,4 +179,4 @@ private:
|
|||||||
std::function<void(TileKey)> on_ready_;
|
std::function<void(TileKey)> on_ready_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace maj0sted::web
|
} // namespace maj0sted::editor
|
||||||
33
maj0sted/include/maj0sted/io/scn_export.hpp
Normal file
33
maj0sted/include/maj0sted/io/scn_export.hpp
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "maj0sted/editor/editor.hpp"
|
||||||
|
|
||||||
|
namespace maj0sted::io {
|
||||||
|
|
||||||
|
struct ScnExportOptions {
|
||||||
|
double origin_east{0.0};
|
||||||
|
double origin_north{0.0};
|
||||||
|
double rail_y{0.2};
|
||||||
|
double max_arc_angle{1.5707963267948966};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScnExportResult {
|
||||||
|
int tracks{0};
|
||||||
|
std::string first_track_name;
|
||||||
|
double origin_east{0.0};
|
||||||
|
double origin_north{0.0};
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ScnExportResult export_scn(
|
||||||
|
const std::vector<maj0sted::editor::NiweletaPolys>& solved,
|
||||||
|
const ScnExportOptions& options, std::ostream& out);
|
||||||
|
|
||||||
|
[[nodiscard]] ScnExportOptions resolve_scn_origin(
|
||||||
|
const std::vector<maj0sted::editor::NiweletaPolys>& solved,
|
||||||
|
ScnExportOptions options);
|
||||||
|
|
||||||
|
} // namespace maj0sted::io
|
||||||
389
maj0sted/src/app/basket_draft.cpp
Normal file
389
maj0sted/src/app/basket_draft.cpp
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
#include "maj0sted/app/basket_draft.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "maj0sted/app/straight_constraints.hpp"
|
||||||
|
|
||||||
|
namespace maj0sted::app {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using maj0sted::editor::CompoundArcSpec;
|
||||||
|
using maj0sted::editor::GapFit;
|
||||||
|
using maj0sted::editor::NiweletaPolys;
|
||||||
|
using maj0sted::editor::NiweletaSpec;
|
||||||
|
using maj0sted::editor::RibbonElement;
|
||||||
|
using maj0sted::editor::RibbonRequest;
|
||||||
|
using maj0sted::editor::StraightSpec;
|
||||||
|
using maj0sted::editor::PlanPolyline;
|
||||||
|
|
||||||
|
void sanitize_draft_arcs(BasketDraft& draft) {
|
||||||
|
for (auto& arc : draft.arcs) {
|
||||||
|
if (!(arc.radius >= 30.0)) {
|
||||||
|
arc.radius = 30.0;
|
||||||
|
}
|
||||||
|
if (!(arc.length > 0.0)) {
|
||||||
|
arc.length = 5.0;
|
||||||
|
}
|
||||||
|
if (arc.transition_to_next < 0.0) {
|
||||||
|
arc.transition_to_next = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (draft.entry_t < 0.0) {
|
||||||
|
draft.entry_t = 0.0;
|
||||||
|
}
|
||||||
|
if (draft.exit_t < 0.0) {
|
||||||
|
draft.exit_t = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool draft_end_pose(const BasketDraft& draft,
|
||||||
|
const std::vector<PlanPolyline>& polys, double& out_x,
|
||||||
|
double& out_y, double& out_az) {
|
||||||
|
if (!draft.active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out_az = draft.az0;
|
||||||
|
if (!polys.empty() && !polys.back().points.empty()) {
|
||||||
|
const auto& pts = polys.back().points;
|
||||||
|
out_x = pts.back().x;
|
||||||
|
out_y = pts.back().y;
|
||||||
|
if (pts.size() >= 2) {
|
||||||
|
out_az = std::atan2(pts.back().x - pts[pts.size() - 2].x,
|
||||||
|
pts.back().y - pts[pts.size() - 2].y);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
out_x = draft.x0;
|
||||||
|
out_y = draft.y0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
RibbonRequest draft_to_ribbon(const BasketDraft& draft) {
|
||||||
|
RibbonRequest req;
|
||||||
|
req.x0 = draft.x0;
|
||||||
|
req.y0 = draft.y0;
|
||||||
|
req.az0 = draft.az0;
|
||||||
|
if (draft.arcs.empty()) {
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
const int dir = draft.dir >= 0 ? 1 : -1;
|
||||||
|
if (draft.entry_t > 1e-6) {
|
||||||
|
RibbonElement el;
|
||||||
|
el.kind = 2;
|
||||||
|
el.length = draft.entry_t;
|
||||||
|
el.radius_start = 0.0;
|
||||||
|
el.radius = draft.arcs.front().radius;
|
||||||
|
el.dir = dir;
|
||||||
|
req.elements.push_back(el);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < draft.arcs.size(); ++i) {
|
||||||
|
const auto& arc = draft.arcs[i];
|
||||||
|
const double r = std::max(30.0, arc.radius);
|
||||||
|
const double len = std::max(1e-3, arc.length);
|
||||||
|
RibbonElement a;
|
||||||
|
a.kind = 1;
|
||||||
|
a.radius = r;
|
||||||
|
a.angle = len / r;
|
||||||
|
a.dir = dir;
|
||||||
|
req.elements.push_back(a);
|
||||||
|
if (arc.transition_to_next > 1e-6) {
|
||||||
|
RibbonElement kp;
|
||||||
|
kp.kind = 2;
|
||||||
|
kp.length = arc.transition_to_next;
|
||||||
|
kp.radius_start = r;
|
||||||
|
if (i + 1 < draft.arcs.size()) {
|
||||||
|
kp.radius = std::max(30.0, draft.arcs[i + 1].radius);
|
||||||
|
} else {
|
||||||
|
kp.radius = std::max(30.0, draft.preview_next_r);
|
||||||
|
}
|
||||||
|
kp.dir = dir;
|
||||||
|
req.elements.push_back(kp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (draft.exit_t > 1e-6) {
|
||||||
|
RibbonElement el;
|
||||||
|
el.kind = 2;
|
||||||
|
el.length = draft.exit_t;
|
||||||
|
el.radius_start = draft.arcs.back().radius;
|
||||||
|
el.radius = 0.0;
|
||||||
|
el.dir = dir;
|
||||||
|
req.elements.push_back(el);
|
||||||
|
}
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PlanPolyline> preview_basket_draft(BasketDraft& draft,
|
||||||
|
double fallback_next_r) {
|
||||||
|
if (!draft.active || draft.arcs.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
sanitize_draft_arcs(draft);
|
||||||
|
draft.preview_next_r = std::max(30.0, fallback_next_r);
|
||||||
|
return maj0sted::editor::solve_ribbon(draft_to_ribbon(draft));
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_basket_draft_from_end(BasketDraft& draft, const StraightSpec& straight,
|
||||||
|
int straight_index, int end, double default_r) {
|
||||||
|
draft = {};
|
||||||
|
draft.active = true;
|
||||||
|
draft.attach_str = straight_index;
|
||||||
|
draft.attach_end = end;
|
||||||
|
draft.dir = 1;
|
||||||
|
draft.preview_next_r = default_r;
|
||||||
|
draft.reedit_gap = -1;
|
||||||
|
draft.reedit_backup = {};
|
||||||
|
if (end == 1) {
|
||||||
|
draft.x0 = straight.x2;
|
||||||
|
draft.y0 = straight.y2;
|
||||||
|
draft.az0 = std::atan2(straight.x2 - straight.x1, straight.y2 - straight.y1);
|
||||||
|
} else {
|
||||||
|
draft.x0 = straight.x1;
|
||||||
|
draft.y0 = straight.y1;
|
||||||
|
draft.az0 = std::atan2(straight.x1 - straight.x2, straight.y1 - straight.y2);
|
||||||
|
}
|
||||||
|
draft.arcs.push_back({600.0, 60.0, 40.0});
|
||||||
|
draft.arcs.push_back({std::max(30.0, default_r), 80.0, 0.0});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reopen_basket_draft(BasketDraft& draft, NiweletaSpec& niweleta,
|
||||||
|
std::size_t gap, const NiweletaPolys* solved) {
|
||||||
|
if (gap + 1 >= niweleta.straights.size() || gap >= niweleta.fits.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& fit = niweleta.fits[gap];
|
||||||
|
if (fit.mode != 3 || fit.arcs.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> solved_arc_lengths;
|
||||||
|
if (solved != nullptr) {
|
||||||
|
for (const auto& poly : solved->polylines) {
|
||||||
|
if (poly.gap != static_cast<int>(gap) || poly.kind != 1 ||
|
||||||
|
poly.element_index < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool seen = false;
|
||||||
|
for (const auto& other : solved->polylines) {
|
||||||
|
if (&other == &poly) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (other.gap == poly.gap &&
|
||||||
|
other.element_index == poly.element_index) {
|
||||||
|
seen = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!seen) {
|
||||||
|
solved_arc_lengths.push_back(std::max(5.0, poly.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& entry = niweleta.straights[gap];
|
||||||
|
const auto& exit = niweleta.straights[gap + 1];
|
||||||
|
const double edx = entry.x2 - entry.x1;
|
||||||
|
const double edy = entry.y2 - entry.y1;
|
||||||
|
const double xdx = exit.x2 - exit.x1;
|
||||||
|
const double xdy = exit.y2 - exit.y1;
|
||||||
|
const double turn = edx * xdy - edy * xdx;
|
||||||
|
|
||||||
|
draft = {};
|
||||||
|
draft.active = true;
|
||||||
|
draft.reedit_backup = fit;
|
||||||
|
draft.reedit_gap = static_cast<int>(gap);
|
||||||
|
draft.attach_str = static_cast<int>(gap);
|
||||||
|
draft.attach_end = 1;
|
||||||
|
draft.x0 = entry.x2;
|
||||||
|
draft.y0 = entry.y2;
|
||||||
|
draft.az0 = std::atan2(edx, edy);
|
||||||
|
draft.dir = turn >= 0.0 ? 1 : -1;
|
||||||
|
draft.entry_t = std::max(0.0, fit.entry_t);
|
||||||
|
draft.exit_t = std::max(0.0, fit.exit_t);
|
||||||
|
draft.arcs = fit.arcs;
|
||||||
|
for (std::size_t i = 0; i < draft.arcs.size(); ++i) {
|
||||||
|
if (!(draft.arcs[i].radius >= 30.0)) {
|
||||||
|
draft.arcs[i].radius = 30.0;
|
||||||
|
}
|
||||||
|
if (draft.arcs[i].length <= 1e-6) {
|
||||||
|
draft.arcs[i].length =
|
||||||
|
i < solved_arc_lengths.size() ? solved_arc_lengths[i] : 60.0;
|
||||||
|
}
|
||||||
|
if (draft.arcs[i].transition_to_next < 0.0) {
|
||||||
|
draft.arcs[i].transition_to_next = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!draft.arcs.empty()) {
|
||||||
|
draft.arcs.back().transition_to_next = 0.0;
|
||||||
|
draft.preview_next_r = draft.arcs.back().radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
niweleta.fits[gap] = {};
|
||||||
|
niweleta.fits[gap].gap = static_cast<int>(gap);
|
||||||
|
niweleta.fits[gap].mode = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void discard_basket_draft(BasketDraft& draft, NiweletaSpec* niweleta) {
|
||||||
|
if (draft.reedit_gap >= 0 && niweleta != nullptr &&
|
||||||
|
static_cast<std::size_t>(draft.reedit_gap) < niweleta->fits.size()) {
|
||||||
|
niweleta->fits[static_cast<std::size_t>(draft.reedit_gap)] =
|
||||||
|
draft.reedit_backup;
|
||||||
|
}
|
||||||
|
draft = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool anchor_basket_draft(BasketDraft& draft, NiweletaSpec& niweleta,
|
||||||
|
std::size_t gap) {
|
||||||
|
if (!draft.active || draft.arcs.empty() || niweleta.straights.size() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (gap + 1 >= niweleta.straights.size() || gap >= niweleta.fits.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GapFit previous = niweleta.fits[gap];
|
||||||
|
const BasketDraft draft_backup = draft;
|
||||||
|
|
||||||
|
GapFit fit{};
|
||||||
|
fit.gap = static_cast<int>(gap);
|
||||||
|
fit.mode = 3;
|
||||||
|
fit.entry_t = std::max(0.0, draft.entry_t);
|
||||||
|
fit.exit_t = std::max(0.0, draft.exit_t);
|
||||||
|
fit.arcs = draft.arcs;
|
||||||
|
for (std::size_t i = 0; i + 1 < fit.arcs.size(); ++i) {
|
||||||
|
if (!(fit.arcs[i].radius >= 30.0)) {
|
||||||
|
fit.arcs[i].radius = 30.0;
|
||||||
|
}
|
||||||
|
if (!(fit.arcs[i].length > 0.0)) {
|
||||||
|
fit.arcs[i].length = 40.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fit.arcs.back().length = 0.0;
|
||||||
|
fit.arcs.back().transition_to_next = 0.0;
|
||||||
|
if (!(fit.arcs.back().radius >= 30.0)) {
|
||||||
|
fit.arcs.back().radius = 30.0;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 1; i < fit.arcs.size(); ++i) {
|
||||||
|
if (std::abs(fit.arcs[i].radius - fit.arcs[i - 1].radius) < 1.0) {
|
||||||
|
fit.arcs[i].radius = std::max(30.0, fit.arcs[i - 1].radius * 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
niweleta.fits[gap] = fit;
|
||||||
|
niweleta.straights[gap].rel_kind = 0;
|
||||||
|
niweleta.straights[gap + 1].rel_kind = 0;
|
||||||
|
|
||||||
|
const auto probe = maj0sted::editor::solve_project(
|
||||||
|
std::vector<NiweletaSpec>{niweleta});
|
||||||
|
bool applied = false;
|
||||||
|
if (!probe.empty()) {
|
||||||
|
for (const auto& af : probe.front().applied_fits) {
|
||||||
|
if (af.gap == static_cast<int>(gap) && af.mode == 3) {
|
||||||
|
applied = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!applied) {
|
||||||
|
niweleta.fits[gap] = previous;
|
||||||
|
draft = draft_backup;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
draft = {};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FinishBasketDraftResult finish_basket_draft(
|
||||||
|
BasketDraft& draft, NiweletaSpec& niweleta,
|
||||||
|
std::vector<NiweletaSpec>& all_niwelety, int niweleta_index,
|
||||||
|
double finish_straight_m) {
|
||||||
|
FinishBasketDraftResult result;
|
||||||
|
if (!draft.active || draft.arcs.empty() || draft.attach_str < 0 ||
|
||||||
|
static_cast<std::size_t>(draft.attach_str) >= niweleta.straights.size()) {
|
||||||
|
result.status = "brak łuku";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draft.reedit_gap >= 0) {
|
||||||
|
const auto gap = static_cast<std::size_t>(draft.reedit_gap);
|
||||||
|
const GapFit backup = draft.reedit_backup;
|
||||||
|
niweleta.fits[gap] = backup;
|
||||||
|
if (!anchor_basket_draft(draft, niweleta, gap)) {
|
||||||
|
draft.reedit_gap = static_cast<int>(gap);
|
||||||
|
draft.reedit_backup = backup;
|
||||||
|
result.status = "nie da się (kierunek / L+KP)";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.ok = true;
|
||||||
|
result.status = "luka " + std::to_string(gap);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto polys = preview_basket_draft(draft);
|
||||||
|
double tipx = 0.0, tipy = 0.0, tipaz = 0.0;
|
||||||
|
if (!draft_end_pose(draft, polys, tipx, tipy, tipaz)) {
|
||||||
|
result.status = "brak końca kosza";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NiweletaSpec backup = niweleta;
|
||||||
|
const BasketDraft draft_backup = draft;
|
||||||
|
const int attach = draft.attach_str;
|
||||||
|
|
||||||
|
auto& entry = niweleta.straights[static_cast<std::size_t>(attach)];
|
||||||
|
if (draft.attach_end == 0) {
|
||||||
|
std::swap(entry.x1, entry.x2);
|
||||||
|
std::swap(entry.y1, entry.y2);
|
||||||
|
draft.attach_end = 1;
|
||||||
|
}
|
||||||
|
entry.x2 = draft.x0;
|
||||||
|
entry.y2 = draft.y0;
|
||||||
|
entry.rel_kind = 0;
|
||||||
|
|
||||||
|
const double sl = std::max(20.0, finish_straight_m);
|
||||||
|
StraightSpec exit_st{};
|
||||||
|
exit_st.x1 = tipx;
|
||||||
|
exit_st.y1 = tipy;
|
||||||
|
exit_st.x2 = tipx + std::sin(tipaz) * sl;
|
||||||
|
exit_st.y2 = tipy + std::cos(tipaz) * sl;
|
||||||
|
|
||||||
|
const auto insert_at = static_cast<std::size_t>(attach + 1);
|
||||||
|
for (auto& fit : niweleta.fits) {
|
||||||
|
if (fit.gap >= attach) {
|
||||||
|
++fit.gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto bump_rel = [&](int delta) {
|
||||||
|
for (std::size_t n = 0; n < all_niwelety.size(); ++n) {
|
||||||
|
for (auto& st : all_niwelety[n].straights) {
|
||||||
|
if (st.rel_kind != 0 && st.rel_niw == niweleta_index &&
|
||||||
|
st.rel_str >= static_cast<int>(insert_at)) {
|
||||||
|
st.rel_str += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
bump_rel(+1);
|
||||||
|
niweleta.straights.insert(
|
||||||
|
niweleta.straights.begin() + static_cast<std::ptrdiff_t>(insert_at),
|
||||||
|
exit_st);
|
||||||
|
align_gap_fits(niweleta);
|
||||||
|
|
||||||
|
if (!anchor_basket_draft(draft, niweleta, static_cast<std::size_t>(attach))) {
|
||||||
|
bump_rel(-1);
|
||||||
|
niweleta = backup;
|
||||||
|
draft = draft_backup;
|
||||||
|
result.status = "nie da się (kierunek / L+KP)";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ok = true;
|
||||||
|
result.status = "kosz + " + std::to_string(static_cast<int>(sl)) + " m";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace maj0sted::app
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
namespace maj0sted::app {
|
namespace maj0sted::app {
|
||||||
|
|
||||||
using namespace maj0sted::domain;
|
using namespace maj0sted::domain;
|
||||||
using maj0sted::web::GapFit;
|
using maj0sted::editor::GapFit;
|
||||||
using maj0sted::web::NiweletaSpec;
|
using maj0sted::editor::NiweletaSpec;
|
||||||
using maj0sted::web::StraightSpec;
|
using maj0sted::editor::StraightSpec;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -201,6 +201,8 @@ std::string serialize_document(const EditorDocument& document) {
|
|||||||
out += " " + num(a.radius) + " " + num(a.length) + " " +
|
out += " " + num(a.radius) + " " + num(a.length) + " " +
|
||||||
num(a.transition_to_next);
|
num(a.transition_to_next);
|
||||||
}
|
}
|
||||||
|
// catalogue length trails the arcs, so files written before it still parse (length stays 0)
|
||||||
|
out += " " + num(j.length);
|
||||||
out += "\n";
|
out += "\n";
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -397,7 +399,7 @@ EditorDocument deserialize_document(const std::string& text) {
|
|||||||
if (base + 2 >= e.size()) {
|
if (base + 2 >= e.size()) {
|
||||||
throw std::runtime_error{"maj0sted: truncated 'efit2' arcs"};
|
throw std::runtime_error{"maj0sted: truncated 'efit2' arcs"};
|
||||||
}
|
}
|
||||||
f.arcs.push_back(maj0sted::web::CompoundArcSpec{
|
f.arcs.push_back(maj0sted::editor::CompoundArcSpec{
|
||||||
to_double(e[base]), to_double(e[base + 1]),
|
to_double(e[base]), to_double(e[base + 1]),
|
||||||
to_double(e[base + 2])});
|
to_double(e[base + 2])});
|
||||||
}
|
}
|
||||||
@@ -445,7 +447,7 @@ EditorDocument deserialize_document(const std::string& text) {
|
|||||||
if (e.size() < 12 || e[0] != "ejunc") {
|
if (e.size() < 12 || e[0] != "ejunc") {
|
||||||
throw std::runtime_error{"maj0sted: bad 'ejunc' line"};
|
throw std::runtime_error{"maj0sted: bad 'ejunc' line"};
|
||||||
}
|
}
|
||||||
maj0sted::web::Junction junction;
|
maj0sted::editor::Junction junction;
|
||||||
junction.through = static_cast<int>(to_long(e[1]));
|
junction.through = static_cast<int>(to_long(e[1]));
|
||||||
junction.station = to_double(e[2]);
|
junction.station = to_double(e[2]);
|
||||||
junction.side = static_cast<int>(to_long(e[3]));
|
junction.side = static_cast<int>(to_long(e[3]));
|
||||||
@@ -463,10 +465,15 @@ EditorDocument deserialize_document(const std::string& text) {
|
|||||||
if (base + 2 >= e.size()) {
|
if (base + 2 >= e.size()) {
|
||||||
throw std::runtime_error{"maj0sted: truncated 'ejunc' arcs"};
|
throw std::runtime_error{"maj0sted: truncated 'ejunc' arcs"};
|
||||||
}
|
}
|
||||||
junction.curve.arcs.push_back(maj0sted::web::CompoundArcSpec{
|
junction.curve.arcs.push_back(maj0sted::editor::CompoundArcSpec{
|
||||||
to_double(e[base]), to_double(e[base + 1]),
|
to_double(e[base]), to_double(e[base + 1]),
|
||||||
to_double(e[base + 2])});
|
to_double(e[base + 2])});
|
||||||
}
|
}
|
||||||
|
// optional catalogue length after the arcs (absent in the earliest files)
|
||||||
|
const std::size_t after_arcs = 13 + static_cast<std::size_t>(arc_count) * 3;
|
||||||
|
if (e.size() > after_arcs) {
|
||||||
|
junction.length = to_double(e[after_arcs]);
|
||||||
|
}
|
||||||
document.junctions.push_back(std::move(junction));
|
document.junctions.push_back(std::move(junction));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -476,9 +483,9 @@ EditorDocument deserialize_document(const std::string& text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string default_project_path() {
|
std::string default_project_path() {
|
||||||
// Under Emscripten the host mounts IDBFS at /data (see web/index.html), so
|
// Fallback only; the editor passes its own path (see plan_panel). Relative, so
|
||||||
// this path is persistent across reloads once FS.syncfs has flushed.
|
// it lands under the working directory rather than at a filesystem root.
|
||||||
return "/data/maj0sted/project.m0s";
|
return "editor/project.m0s";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool save_project(const EditorDocument& document, const std::string& path) {
|
bool save_project(const EditorDocument& document, const std::string& path) {
|
||||||
|
|||||||
112
maj0sted/src/app/straight_constraints.cpp
Normal file
112
maj0sted/src/app/straight_constraints.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#include "maj0sted/app/straight_constraints.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "maj0sted/domain/geometry/plan_element.hpp"
|
||||||
|
#include "maj0sted/domain/parallelism/parallelism_service.hpp"
|
||||||
|
#include "maj0sted/domain/value_objects/cartesian_position.hpp"
|
||||||
|
|
||||||
|
namespace maj0sted::app {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using maj0sted::domain::CartesianPosition;
|
||||||
|
using maj0sted::domain::ParallelismService;
|
||||||
|
using maj0sted::domain::Straight;
|
||||||
|
using maj0sted::editor::NiweletaSpec;
|
||||||
|
using maj0sted::editor::StraightSpec;
|
||||||
|
|
||||||
|
Straight to_straight(const StraightSpec& s) {
|
||||||
|
return Straight{CartesianPosition{s.x1, s.y1}, CartesianPosition{s.x2, s.y2}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void align_gap_fits(NiweletaSpec& niweleta) {
|
||||||
|
const auto gaps = niweleta.straights.empty() ? std::size_t{0}
|
||||||
|
: niweleta.straights.size() - 1;
|
||||||
|
std::vector<maj0sted::editor::GapFit> slots(gaps);
|
||||||
|
for (std::size_t gap = 0; gap < gaps; ++gap) {
|
||||||
|
slots[gap].gap = static_cast<int>(gap);
|
||||||
|
}
|
||||||
|
for (const auto& fit : niweleta.fits) {
|
||||||
|
if (fit.gap >= 0 && static_cast<std::size_t>(fit.gap) < gaps) {
|
||||||
|
slots[static_cast<std::size_t>(fit.gap)] = fit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
niweleta.fits = std::move(slots);
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_straight_constraints(std::vector<NiweletaSpec>& niwelety) {
|
||||||
|
for (int pass = 0; pass < 3; ++pass) {
|
||||||
|
bool changed = false;
|
||||||
|
for (auto& niweleta : niwelety) {
|
||||||
|
for (auto& st : niweleta.straights) {
|
||||||
|
if (st.rel_kind == 0 || st.rel_niw < 0 || st.rel_str < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (static_cast<std::size_t>(st.rel_niw) >= niwelety.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& ref_niw = niwelety[static_cast<std::size_t>(st.rel_niw)];
|
||||||
|
if (static_cast<std::size_t>(st.rel_str) >= ref_niw.straights.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& ref = ref_niw.straights[static_cast<std::size_t>(st.rel_str)];
|
||||||
|
if (&ref == &st) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double dx = ref.x2 - ref.x1;
|
||||||
|
const double dy = ref.y2 - ref.y1;
|
||||||
|
const double len = std::hypot(dx, dy);
|
||||||
|
if (len < 1e-6) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double x1 = st.x1, y1 = st.y1, x2 = st.x2, y2 = st.y2;
|
||||||
|
if (st.rel_kind == 1) {
|
||||||
|
const Straight projected = ParallelismService::project_onto_offset_line(
|
||||||
|
to_straight(ref), st.rel_offset, to_straight(st));
|
||||||
|
x1 = projected.start().x();
|
||||||
|
y1 = projected.start().y();
|
||||||
|
x2 = projected.end().x();
|
||||||
|
y2 = projected.end().y();
|
||||||
|
} else if (st.rel_kind == 2) {
|
||||||
|
const double a =
|
||||||
|
std::atan2(1.0, std::max(1e-9, st.rel_cot)) *
|
||||||
|
(st.rel_side < 0 ? -1.0 : 1.0);
|
||||||
|
const double ux = dx / len;
|
||||||
|
const double uy = dy / len;
|
||||||
|
const double ca = std::cos(a);
|
||||||
|
const double sa = std::sin(a);
|
||||||
|
const double rx = ux * ca - uy * sa;
|
||||||
|
const double ry = ux * sa + uy * ca;
|
||||||
|
double L = std::hypot(st.x2 - st.x1, st.y2 - st.y1);
|
||||||
|
if (L < 1.0) {
|
||||||
|
L = st.rel_length > 0.0 ? st.rel_length : len;
|
||||||
|
}
|
||||||
|
st.rel_length = L;
|
||||||
|
x1 = st.x1;
|
||||||
|
y1 = st.y1;
|
||||||
|
x2 = st.x1 + rx * L;
|
||||||
|
y2 = st.y1 + ry * L;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::abs(st.x1 - x1) > 1e-9 || std::abs(st.y1 - y1) > 1e-9 ||
|
||||||
|
std::abs(st.x2 - x2) > 1e-9 || std::abs(st.y2 - y2) > 1e-9) {
|
||||||
|
st.x1 = x1;
|
||||||
|
st.y1 = y1;
|
||||||
|
st.x2 = x2;
|
||||||
|
st.y2 = y2;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace maj0sted::app
|
||||||
@@ -55,4 +55,29 @@ double ParallelismService::perpendicular_distance(const Straight& origin,
|
|||||||
return std::abs(d_east * normal.east + d_north * normal.north);
|
return std::abs(d_east * normal.east + d_north * normal.north);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Straight ParallelismService::project_onto_offset_line(const Straight& origin,
|
||||||
|
double offset_m,
|
||||||
|
const Straight& segment) {
|
||||||
|
const double az = origin.azimuth().radians();
|
||||||
|
const double ux = std::sin(az);
|
||||||
|
const double uy = std::cos(az);
|
||||||
|
const Vec normal = left_normal(origin.azimuth());
|
||||||
|
const double ox = origin.start().x() + normal.east * offset_m;
|
||||||
|
const double oy = origin.start().y() + normal.north * offset_m;
|
||||||
|
|
||||||
|
auto station = [&](double x, double y) {
|
||||||
|
return (x - ox) * ux + (y - oy) * uy;
|
||||||
|
};
|
||||||
|
double t1 = station(segment.start().x(), segment.start().y());
|
||||||
|
double t2 = station(segment.end().x(), segment.end().y());
|
||||||
|
if (std::abs(t2 - t1) < 1.0) {
|
||||||
|
const double tm = 0.5 * (t1 + t2);
|
||||||
|
const double half = (t2 >= t1) ? 0.5 : -0.5;
|
||||||
|
t1 = tm - half;
|
||||||
|
t2 = tm + half;
|
||||||
|
}
|
||||||
|
return Straight{CartesianPosition{ox + ux * t1, oy + uy * t1},
|
||||||
|
CartesianPosition{ox + ux * t2, oy + uy * t2}};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace maj0sted::domain
|
} // namespace maj0sted::domain
|
||||||
|
|||||||
127
maj0sted/src/domain/turnout.cpp
Normal file
127
maj0sted/src/domain/turnout.cpp
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#include "maj0sted/domain/geometry/turnout.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace maj0sted::domain {
|
||||||
|
|
||||||
|
using geometry::layout_segment;
|
||||||
|
using geometry::Pose;
|
||||||
|
using geometry::XY;
|
||||||
|
|
||||||
|
double CrossingMark::angle() const noexcept {
|
||||||
|
return denominator_ > 0.0 ? std::atan(1.0 / denominator_) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The deflection (radians) a segment of linearly varying curvature turns through.
|
||||||
|
double deflection(const TurnoutSegment& segment) {
|
||||||
|
return 0.5 * (segment.k0 + segment.k1) * segment.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds the diverging curve as signed-curvature segments that together turn
|
||||||
|
// through exactly @p total_angle. Every part but the closing arc is fixed; the
|
||||||
|
// closing arc's length is chosen to spend whatever deflection is left. Returns
|
||||||
|
// false when a radius is non-positive or the fixed parts overshoot the angle.
|
||||||
|
bool build_curve(const DivergingCurve& curve, double total_angle, double sign,
|
||||||
|
std::vector<TurnoutSegment>& out) {
|
||||||
|
if (curve.arcs.empty()) return false;
|
||||||
|
const double first_radius = curve.arcs.front().radius;
|
||||||
|
const double last_radius = curve.arcs.back().radius;
|
||||||
|
if (first_radius <= 0.0 || last_radius <= 0.0) return false;
|
||||||
|
|
||||||
|
double consumed = 0.0; // deflection of everything before the closing arc
|
||||||
|
|
||||||
|
if (curve.entry_transition > 0.0) {
|
||||||
|
out.push_back({0.0, sign / first_radius, curve.entry_transition});
|
||||||
|
consumed += std::abs(deflection(out.back()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// every arc but the last contributes its fixed length, eased to the next
|
||||||
|
for (std::size_t i = 0; i + 1 < curve.arcs.size(); ++i) {
|
||||||
|
const double radius = curve.arcs[i].radius;
|
||||||
|
if (radius <= 0.0) return false;
|
||||||
|
out.push_back({sign / radius, sign / radius, curve.arcs[i].length});
|
||||||
|
consumed += std::abs(deflection(out.back()));
|
||||||
|
|
||||||
|
const double next_radius = curve.arcs[i + 1].radius;
|
||||||
|
if (curve.arcs[i].transition_to_next > 0.0 && next_radius > 0.0) {
|
||||||
|
out.push_back({sign / radius, sign / next_radius,
|
||||||
|
curve.arcs[i].transition_to_next});
|
||||||
|
consumed += std::abs(deflection(out.back()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const double exit_deflection = 0.5 * curve.exit_transition / last_radius;
|
||||||
|
const double closing = total_angle - consumed - exit_deflection;
|
||||||
|
if (closing <= 0.0) return false; // the fixed parts already reach the crossing angle
|
||||||
|
|
||||||
|
out.push_back({sign / last_radius, sign / last_radius, closing * last_radius});
|
||||||
|
if (curve.exit_transition > 0.0) {
|
||||||
|
out.push_back({sign / last_radius, 0.0, curve.exit_transition});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advances @p pose along @p segment (without sampling points; that is the
|
||||||
|
// renderer's job) and returns the end pose.
|
||||||
|
Pose advance(const TurnoutSegment& segment, const Pose& pose) {
|
||||||
|
return layout_segment(segment.k0, segment.k1, segment.length, pose, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where the through tangent (through @p a along @p a heading) meets the diverging
|
||||||
|
// tangent (through @p b along @p b heading). Writes the two tangent lengths; both
|
||||||
|
// stay zero when the tangents are parallel (a straight-through, angle-less turnout).
|
||||||
|
void tangent_lengths(const Pose& a, const Pose& b, double& front, double& back) {
|
||||||
|
const double denominator = a.hx * b.hy - a.hy * b.hx;
|
||||||
|
if (std::abs(denominator) < 1e-9) return;
|
||||||
|
const double u = ((b.x - a.x) * b.hy - (b.y - a.y) * b.hx) / denominator;
|
||||||
|
const double cx = a.x + a.hx * u;
|
||||||
|
const double cy = a.y + a.hy * u;
|
||||||
|
front = std::hypot(cx - a.x, cy - a.y);
|
||||||
|
back = std::hypot(cx - b.x, cy - b.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TurnoutGeometry lay_turnout(const Pose& start, const Turnout& turnout) {
|
||||||
|
TurnoutGeometry geometry;
|
||||||
|
|
||||||
|
const double angle = turnout.mark.angle();
|
||||||
|
if (angle <= 0.0) return geometry;
|
||||||
|
const double sign = turnout.side == DivergeSide::Left ? 1.0 : -1.0;
|
||||||
|
|
||||||
|
if (!build_curve(turnout.curve, angle, sign, geometry.path)) {
|
||||||
|
geometry.path.clear();
|
||||||
|
return geometry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk the curve to the frog direction (the crossing angle)
|
||||||
|
Pose pose = start;
|
||||||
|
for (const auto& segment : geometry.path) {
|
||||||
|
pose = advance(segment, pose);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a turnout always runs on past the curve as a straight frog rail. its length
|
||||||
|
// takes the switch to its catalogue length, measured along the through track;
|
||||||
|
// without a catalogue length a tangent-length rail keeps a frog rail present.
|
||||||
|
const double through_projection =
|
||||||
|
(pose.x - start.x) * start.hx + (pose.y - start.y) * start.hy;
|
||||||
|
const double cos_angle = pose.hx * start.hx + pose.hy * start.hy;
|
||||||
|
double rail = turnout.length > 0.0 && cos_angle > 1e-6
|
||||||
|
? (turnout.length - through_projection) / cos_angle
|
||||||
|
: turnout.curve.arcs.back().radius * std::tan(angle * 0.5);
|
||||||
|
if (rail > 0.0) {
|
||||||
|
const TurnoutSegment frog_rail{0.0, 0.0, rail};
|
||||||
|
geometry.path.push_back(frog_rail);
|
||||||
|
pose = advance(frog_rail, pose);
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.valid = true;
|
||||||
|
geometry.frog = pose;
|
||||||
|
for (const auto& segment : geometry.path) geometry.diverging_length += segment.length;
|
||||||
|
tangent_lengths(start, pose, geometry.tangent_front, geometry.tangent_back);
|
||||||
|
return geometry;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace maj0sted::domain
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "maj0sted/web/editor.hpp"
|
#include "maj0sted/editor/editor.hpp"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "maj0sted/domain/fitting/gap_fitter.hpp"
|
#include "maj0sted/domain/fitting/gap_fitter.hpp"
|
||||||
#include "maj0sted/domain/geometry/segment_layout.hpp"
|
#include "maj0sted/domain/geometry/segment_layout.hpp"
|
||||||
|
#include "maj0sted/domain/geometry/turnout.hpp"
|
||||||
#include "maj0sted/maj0sted.hpp"
|
#include "maj0sted/maj0sted.hpp"
|
||||||
#include "maj0sted/render/rail_renderer.hpp"
|
#include "maj0sted/render/rail_renderer.hpp"
|
||||||
#include "maj0sted/render/track_renderer.hpp"
|
#include "maj0sted/render/track_renderer.hpp"
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
// All fitting business logic lives in maj0sted::domain::GapFitter; all rail
|
// All fitting business logic lives in maj0sted::domain::GapFitter; all rail
|
||||||
// geometry lives in maj0sted::render::RailRenderer. Nothing here decides how a
|
// geometry lives in maj0sted::render::RailRenderer. Nothing here decides how a
|
||||||
// curve is fitted or how it is drawn.
|
// curve is fitted or how it is drawn.
|
||||||
namespace maj0sted::web {
|
namespace maj0sted::editor {
|
||||||
|
|
||||||
using namespace maj0sted::domain;
|
using namespace maj0sted::domain;
|
||||||
using maj0sted::domain::geometry::layout_segment;
|
using maj0sted::domain::geometry::layout_segment;
|
||||||
@@ -156,19 +157,19 @@ std::vector<render::CentrelineElement> sample_centrelines(
|
|||||||
return axis;
|
return axis;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the centreline elements to WebPolylines through the renderer
|
// Renders the centreline elements to PlanPolylines through the renderer
|
||||||
// interface. A different render::TrackRenderer could be dropped in here without
|
// interface. A different render::TrackRenderer could be dropped in here without
|
||||||
// touching anything above.
|
// touching anything above.
|
||||||
std::vector<WebPolyline> render_rails(
|
std::vector<PlanPolyline> render_rails(
|
||||||
const std::vector<render::CentrelineElement>& axis) {
|
const std::vector<render::CentrelineElement>& axis) {
|
||||||
render::RailRenderer rail_renderer;
|
render::RailRenderer rail_renderer;
|
||||||
render::TrackRenderer& renderer = rail_renderer;
|
render::TrackRenderer& renderer = rail_renderer;
|
||||||
for (const auto& element : axis) renderer.add(element);
|
for (const auto& element : axis) renderer.add(element);
|
||||||
|
|
||||||
std::vector<WebPolyline> polylines;
|
std::vector<PlanPolyline> polylines;
|
||||||
polylines.reserve(rail_renderer.rails().size());
|
polylines.reserve(rail_renderer.rails().size());
|
||||||
for (const auto& rail : rail_renderer.rails()) {
|
for (const auto& rail : rail_renderer.rails()) {
|
||||||
WebPolyline poly;
|
PlanPolyline poly;
|
||||||
poly.kind = static_cast<int>(rail.kind);
|
poly.kind = static_cast<int>(rail.kind);
|
||||||
poly.straight_index = rail.straight_index;
|
poly.straight_index = rail.straight_index;
|
||||||
poly.gap = rail.gap;
|
poly.gap = rail.gap;
|
||||||
@@ -177,7 +178,7 @@ std::vector<WebPolyline> render_rails(
|
|||||||
poly.radius_start = rail.radius_start;
|
poly.radius_start = rail.radius_start;
|
||||||
poly.radius_end = rail.radius_end;
|
poly.radius_end = rail.radius_end;
|
||||||
poly.points.reserve(rail.points.size());
|
poly.points.reserve(rail.points.size());
|
||||||
for (const auto& p : rail.points) poly.points.push_back(WebPoint{p.x, p.y});
|
for (const auto& p : rail.points) poly.points.push_back(PlanPoint{p.x, p.y});
|
||||||
polylines.push_back(std::move(poly));
|
polylines.push_back(std::move(poly));
|
||||||
}
|
}
|
||||||
return polylines;
|
return polylines;
|
||||||
@@ -226,7 +227,7 @@ NiweletaPolys build(const NiweletaSpec& spec) {
|
|||||||
const auto& last = result.centreline.back();
|
const auto& last = result.centreline.back();
|
||||||
if (std::hypot(p.x - last.x, p.y - last.y) < 1e-9) continue;
|
if (std::hypot(p.x - last.x, p.y - last.y) < 1e-9) continue;
|
||||||
}
|
}
|
||||||
result.centreline.push_back(WebPoint{p.x, p.y});
|
result.centreline.push_back(PlanPoint{p.x, p.y});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ NiweletaPolys build(const NiweletaSpec& spec) {
|
|||||||
// The pose (position + unit heading) at arc length @p station along a centreline
|
// The pose (position + unit heading) at arc length @p station along a centreline
|
||||||
// polyline. Clamped to the ends; heading comes from the segment the station falls
|
// polyline. Clamped to the ends; heading comes from the segment the station falls
|
||||||
// on. Returns false only for a centreline too short to have a direction.
|
// on. Returns false only for a centreline too short to have a direction.
|
||||||
bool pose_at(const std::vector<WebPoint>& centreline, double station, Pose& out) {
|
bool pose_at(const std::vector<PlanPoint>& centreline, double station, Pose& out) {
|
||||||
if (centreline.size() < 2) return false;
|
if (centreline.size() < 2) return false;
|
||||||
|
|
||||||
double travelled = 0.0;
|
double travelled = 0.0;
|
||||||
@@ -261,36 +262,32 @@ bool pose_at(const std::vector<WebPoint>& centreline, double station, Pose& out)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Samples a fitted curve chain into centreline elements, starting from @p start.
|
// Translates the editor's switch DTO into a domain Turnout: a plain-arc curve
|
||||||
// Same layout the gap fits go through, so the switch curve is drawn identically.
|
// (mode 1) or a basket of arcs eased by clothoids (mode 3). All the turnout
|
||||||
std::vector<render::CentrelineElement> sample_curve(
|
// business — turning to the crossing angle, closing the last arc, the frog rail —
|
||||||
const std::vector<PlanElement>& curve, Pose start) {
|
// lives in the domain; nothing here decides geometry.
|
||||||
std::vector<render::CentrelineElement> axis;
|
domain::Turnout to_turnout(const Junction& junction) {
|
||||||
Pose pose = start;
|
domain::DivergingCurve curve;
|
||||||
int element_index = 0;
|
if (junction.curve.mode == 3 && junction.curve.arcs.size() >= 2) {
|
||||||
for (const auto& element : curve) {
|
for (const auto& arc : junction.curve.arcs) {
|
||||||
double k0 = 0.0, k1 = 0.0, length = 0.0;
|
curve.arcs.push_back(
|
||||||
render::CentrelineElement out;
|
domain::TurnoutArc{arc.radius, arc.length, arc.transition_to_next});
|
||||||
out.kind = sample_params(element, k0, k1, length);
|
}
|
||||||
std::vector<XY> pts;
|
curve.entry_transition = junction.curve.entry_t;
|
||||||
pose = layout_segment(k0, k1, length, pose, &pts);
|
curve.exit_transition = junction.curve.exit_t;
|
||||||
out.points.reserve(pts.size());
|
} else {
|
||||||
for (const auto& p : pts) out.points.push_back(render::Point{p.x, p.y});
|
curve.arcs.push_back(domain::TurnoutArc{junction.curve.radius, 0.0, 0.0});
|
||||||
out.element_index = element_index++;
|
|
||||||
out.length = length;
|
|
||||||
out.radius_start = radius_of(k0);
|
|
||||||
out.radius_end = radius_of(k1);
|
|
||||||
axis.push_back(std::move(out));
|
|
||||||
}
|
}
|
||||||
return axis;
|
return domain::Turnout{
|
||||||
|
domain::CrossingMark{junction.crossing_n},
|
||||||
|
junction.side == 0 ? domain::DivergeSide::Left : domain::DivergeSide::Right,
|
||||||
|
std::move(curve), junction.length};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The diverging curve and frog of one switch, from the solved through niweleta.
|
// The drawable geometry of one switch. @p station is the switch's start on the
|
||||||
// The switch's theoretical point sits at @p station on the through track; two
|
// through track (początek rozjazdu) — the point the user clicks. The diverging
|
||||||
// tangent lines cross there (the through heading and that heading turned by the
|
// path and frog come from the domain; here we only place them on the through
|
||||||
// crossing angle), and the internal curve is fitted to round the corner between
|
// track and sample them into rails.
|
||||||
// them — a plain arc, or a compound basket, whatever the switch asks for. The
|
|
||||||
// branch begins where the curve ends.
|
|
||||||
JunctionGeom solve_junction(const Junction& junction,
|
JunctionGeom solve_junction(const Junction& junction,
|
||||||
const std::vector<NiweletaPolys>& solved) {
|
const std::vector<NiweletaPolys>& solved) {
|
||||||
JunctionGeom geom;
|
JunctionGeom geom;
|
||||||
@@ -298,61 +295,50 @@ JunctionGeom solve_junction(const Junction& junction,
|
|||||||
static_cast<std::size_t>(junction.through) >= solved.size()) {
|
static_cast<std::size_t>(junction.through) >= solved.size()) {
|
||||||
return geom;
|
return geom;
|
||||||
}
|
}
|
||||||
if (junction.crossing_n <= 0.0) return geom;
|
|
||||||
|
|
||||||
Pose theoretical{};
|
Pose start{};
|
||||||
if (!pose_at(solved[junction.through].centreline, junction.station,
|
if (!pose_at(solved[junction.through].centreline, junction.station, start)) {
|
||||||
theoretical)) {
|
|
||||||
return geom;
|
return geom;
|
||||||
}
|
}
|
||||||
|
|
||||||
// a trailing switch opens against the running direction: flip the heading
|
// a trailing switch opens against the running direction: flip the heading
|
||||||
double thx = theoretical.hx;
|
|
||||||
double thy = theoretical.hy;
|
|
||||||
if (!junction.facing) {
|
if (!junction.facing) {
|
||||||
thx = -thx;
|
start.hx = -start.hx;
|
||||||
thy = -thy;
|
start.hy = -start.hy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// the diverging tangent line: the through heading turned by the crossing
|
const auto turnout = domain::lay_turnout(start, to_turnout(junction));
|
||||||
// angle, left (positive) or right (negative)
|
if (!turnout.valid) return geom;
|
||||||
const double angle =
|
|
||||||
(junction.side == 0 ? 1.0 : -1.0) * std::atan(1.0 / junction.crossing_n);
|
|
||||||
const double dhx = thx * std::cos(angle) - thy * std::sin(angle);
|
|
||||||
const double dhy = thx * std::sin(angle) + thy * std::cos(angle);
|
|
||||||
|
|
||||||
// two long guide straights meeting at the theoretical point X; the fitter
|
// sample the domain segments into drawable rails
|
||||||
// rounds the corner and hands back the tangent points that become the switch
|
std::vector<render::CentrelineElement> axis;
|
||||||
// start and the frog. long enough that the tangent points land on them.
|
Pose pose = start;
|
||||||
const double guide = 5000.0;
|
for (const auto& segment : turnout.path) {
|
||||||
const CartesianPosition x{theoretical.x, theoretical.y};
|
std::vector<XY> pts;
|
||||||
Straight entry{CartesianPosition{x.x() - thx * guide, x.y() - thy * guide}, x};
|
render::CentrelineElement element;
|
||||||
Straight exit{x, CartesianPosition{x.x() + dhx * guide, x.y() + dhy * guide}};
|
element.kind =
|
||||||
|
std::abs(segment.k1 - segment.k0) >= 1e-12 ? render::ElementKind::Transition
|
||||||
const auto request = to_request(junction.curve);
|
: std::abs(segment.k0) >= 1e-12 ? render::ElementKind::Arc
|
||||||
if (!request) return geom; // a switch with no curve is not a switch
|
: render::ElementKind::Straight;
|
||||||
const auto connection = GapFitter::connect(entry, exit, *request);
|
pose = layout_segment(segment.k0, segment.k1, segment.length, pose, &pts);
|
||||||
if (!connection) return geom;
|
element.points.reserve(pts.size());
|
||||||
|
for (const auto& p : pts) element.points.push_back(render::Point{p.x, p.y});
|
||||||
const FitResult& fit = connection->fit;
|
element.length = segment.length;
|
||||||
const CartesianPosition s = fit.tangent_in; // switch start
|
element.radius_start = radius_of(segment.k0);
|
||||||
const CartesianPosition f = fit.tangent_out; // frog
|
element.radius_end = radius_of(segment.k1);
|
||||||
|
axis.push_back(std::move(element));
|
||||||
|
}
|
||||||
|
|
||||||
geom.valid = true;
|
geom.valid = true;
|
||||||
geom.px = s.x();
|
geom.px = start.x;
|
||||||
geom.py = s.y();
|
geom.py = start.y;
|
||||||
geom.fx = f.x();
|
geom.fx = turnout.frog.x;
|
||||||
geom.fy = f.y();
|
geom.fy = turnout.frog.y;
|
||||||
geom.fhx = dhx;
|
geom.fhx = turnout.frog.hx;
|
||||||
geom.fhy = dhy;
|
geom.fhy = turnout.frog.hy;
|
||||||
// theoretical tangent lengths: from the crossing point to each tangent point
|
geom.tangent_front = turnout.tangent_front;
|
||||||
geom.tangent_front = std::hypot(s.x() - x.x(), s.y() - x.y());
|
geom.tangent_back = turnout.tangent_back;
|
||||||
geom.tangent_back = std::hypot(f.x() - x.x(), f.y() - x.y());
|
geom.length = turnout.diverging_length;
|
||||||
|
|
||||||
const auto axis = sample_curve(fit.curve, Pose{s.x(), s.y(), thx, thy});
|
|
||||||
for (const auto& element : axis) geom.length += element.length;
|
|
||||||
geom.polylines = render_rails(axis);
|
geom.polylines = render_rails(axis);
|
||||||
|
|
||||||
return geom;
|
return geom;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,4 +391,4 @@ LayoutSolution solve_layout(std::vector<NiweletaSpec> niwelety,
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace maj0sted::web
|
} // namespace maj0sted::editor
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
#include "maj0sted/web/ribbon.hpp"
|
#include "maj0sted/editor/ribbon.hpp"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "maj0sted/domain/geometry/segment_layout.hpp"
|
#include "maj0sted/domain/geometry/segment_layout.hpp"
|
||||||
|
|
||||||
namespace maj0sted::web {
|
namespace maj0sted::editor {
|
||||||
|
|
||||||
using maj0sted::domain::geometry::layout_segment;
|
using maj0sted::domain::geometry::layout_segment;
|
||||||
using maj0sted::domain::geometry::Pose;
|
using maj0sted::domain::geometry::Pose;
|
||||||
using maj0sted::domain::geometry::XY;
|
using maj0sted::domain::geometry::XY;
|
||||||
|
|
||||||
std::vector<WebPolyline> solve_ribbon(const RibbonRequest& request) {
|
std::vector<PlanPolyline> solve_ribbon(const RibbonRequest& request) {
|
||||||
std::vector<WebPolyline> result;
|
std::vector<PlanPolyline> result;
|
||||||
result.reserve(request.elements.size());
|
result.reserve(request.elements.size());
|
||||||
|
|
||||||
Pose pose{request.x0, request.y0, std::sin(request.az0), std::cos(request.az0)};
|
Pose pose{request.x0, request.y0, std::sin(request.az0), std::cos(request.az0)};
|
||||||
@@ -41,15 +41,15 @@ std::vector<WebPolyline> solve_ribbon(const RibbonRequest& request) {
|
|||||||
std::vector<XY> points;
|
std::vector<XY> points;
|
||||||
pose = layout_segment(k0, k1, length, pose, &points);
|
pose = layout_segment(k0, k1, length, pose, &points);
|
||||||
|
|
||||||
WebPolyline polyline;
|
PlanPolyline polyline;
|
||||||
polyline.kind = kind;
|
polyline.kind = kind;
|
||||||
polyline.points.reserve(points.size());
|
polyline.points.reserve(points.size());
|
||||||
for (const auto& p : points) {
|
for (const auto& p : points) {
|
||||||
polyline.points.push_back(WebPoint{p.x, p.y});
|
polyline.points.push_back(PlanPoint{p.x, p.y});
|
||||||
}
|
}
|
||||||
result.push_back(std::move(polyline));
|
result.push_back(std::move(polyline));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace maj0sted::web
|
} // namespace maj0sted::editor
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
#include "maj0sted/web/solve.hpp"
|
#include "maj0sted/editor/solve.hpp"
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include "maj0sted/maj0sted.hpp"
|
#include "maj0sted/maj0sted.hpp"
|
||||||
#include "maj0sted/render/plan_sampler.hpp"
|
#include "maj0sted/render/plan_sampler.hpp"
|
||||||
|
|
||||||
namespace maj0sted::web {
|
namespace maj0sted::editor {
|
||||||
|
|
||||||
using namespace maj0sted::domain;
|
using namespace maj0sted::domain;
|
||||||
|
|
||||||
@@ -41,15 +41,15 @@ FitResult make_fit(const Niweleta& niweleta, StraightId entry, StraightId exit,
|
|||||||
: std::nullopt});
|
: std::nullopt});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<WebPolyline> to_polylines(const render::Scene& scene) {
|
std::vector<PlanPolyline> to_polylines(const render::Scene& scene) {
|
||||||
std::vector<WebPolyline> result;
|
std::vector<PlanPolyline> result;
|
||||||
result.reserve(scene.polylines.size());
|
result.reserve(scene.polylines.size());
|
||||||
for (const auto& polyline : scene.polylines) {
|
for (const auto& polyline : scene.polylines) {
|
||||||
WebPolyline out;
|
PlanPolyline out;
|
||||||
out.kind = static_cast<int>(polyline.kind);
|
out.kind = static_cast<int>(polyline.kind);
|
||||||
out.points.reserve(polyline.points.size());
|
out.points.reserve(polyline.points.size());
|
||||||
for (const auto& p : polyline.points) {
|
for (const auto& p : polyline.points) {
|
||||||
out.points.push_back(WebPoint{p.x, p.y});
|
out.points.push_back(PlanPoint{p.x, p.y});
|
||||||
}
|
}
|
||||||
result.push_back(std::move(out));
|
result.push_back(std::move(out));
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ std::vector<WebPolyline> to_polylines(const render::Scene& scene) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::vector<WebPolyline> solve_scene(const SolveRequest& request) {
|
std::vector<PlanPolyline> solve_scene(const SolveRequest& request) {
|
||||||
Niweleta niweleta{NiweletaId{1}};
|
Niweleta niweleta{NiweletaId{1}};
|
||||||
const CartesianPosition a{request.ax, request.ay};
|
const CartesianPosition a{request.ax, request.ay};
|
||||||
const CartesianPosition b{request.bx, request.by};
|
const CartesianPosition b{request.bx, request.by};
|
||||||
@@ -95,4 +95,4 @@ std::vector<WebPolyline> solve_scene(const SolveRequest& request) {
|
|||||||
return to_polylines(render::sample(niweleta.plan()));
|
return to_polylines(render::sample(niweleta.plan()));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace maj0sted::web
|
} // namespace maj0sted::editor
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "maj0sted/web/tile_cache.hpp"
|
#include "maj0sted/editor/tile_cache.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
@@ -7,14 +7,7 @@
|
|||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
namespace maj0sted::editor {
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include <emscripten/emscripten.h>
|
|
||||||
#include <emscripten/fetch.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace maj0sted::web {
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -33,17 +26,23 @@ constexpr int kMaxRetries = 10;
|
|||||||
|
|
||||||
// --- TileGrid ------------------------------------------------------------
|
// --- TileGrid ------------------------------------------------------------
|
||||||
|
|
||||||
double TileGrid::resolution(int /*zoom*/) noexcept {
|
double TileGrid::cell_metres(const TileKey& key) noexcept {
|
||||||
return kCellMetres / static_cast<double>(kTilePixels); // constant, single grid
|
return key.zoom >= kCellZoomMin ? static_cast<double>(key.zoom) : kCellMetres;
|
||||||
}
|
}
|
||||||
|
|
||||||
double TileGrid::tile_span(int /*zoom*/) noexcept {
|
double TileGrid::resolution(int zoom) noexcept {
|
||||||
return kCellMetres; // every cell is 100 m × 100 m
|
return tile_span(zoom) / static_cast<double>(kTilePixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
TileKey TileGrid::tile_at(double x, double y, int /*zoom*/) noexcept {
|
double TileGrid::tile_span(int zoom) noexcept {
|
||||||
const auto safe_index = [](double coordinate, double origin) {
|
return zoom >= kCellZoomMin ? static_cast<double>(zoom) : kCellMetres;
|
||||||
const double value = std::floor((coordinate - origin) / kCellMetres);
|
}
|
||||||
|
|
||||||
|
TileKey TileGrid::tile_at(double x, double y, int zoom) noexcept {
|
||||||
|
const double cell = tile_span(zoom);
|
||||||
|
const int key_zoom = zoom >= kCellZoomMin ? zoom : kLevel;
|
||||||
|
const auto safe_index = [cell](double coordinate, double origin) {
|
||||||
|
const double value = std::floor((coordinate - origin) / cell);
|
||||||
if (!std::isfinite(value)) return 0;
|
if (!std::isfinite(value)) return 0;
|
||||||
if (value <= static_cast<double>(std::numeric_limits<int>::min())) {
|
if (value <= static_cast<double>(std::numeric_limits<int>::min())) {
|
||||||
return std::numeric_limits<int>::min();
|
return std::numeric_limits<int>::min();
|
||||||
@@ -53,20 +52,21 @@ TileKey TileGrid::tile_at(double x, double y, int /*zoom*/) noexcept {
|
|||||||
}
|
}
|
||||||
return static_cast<int>(value);
|
return static_cast<int>(value);
|
||||||
};
|
};
|
||||||
return TileKey{kLevel, safe_index(x, kOriginX), safe_index(y, kOriginY)};
|
return TileKey{key_zoom, safe_index(x, kOriginX), safe_index(y, kOriginY)};
|
||||||
}
|
}
|
||||||
|
|
||||||
TileBBox TileGrid::bbox(const TileKey& key) noexcept {
|
TileBBox TileGrid::bbox(const TileKey& key) noexcept {
|
||||||
const double min_x = kOriginX + static_cast<double>(key.x) * kCellMetres;
|
const double cell = cell_metres(key);
|
||||||
const double min_y = kOriginY + static_cast<double>(key.y) * kCellMetres;
|
const double min_x = kOriginX + static_cast<double>(key.x) * cell;
|
||||||
return TileBBox{min_x, min_y, min_x + kCellMetres, min_y + kCellMetres};
|
const double min_y = kOriginY + static_cast<double>(key.y) * cell;
|
||||||
|
return TileBBox{min_x, min_y, min_x + cell, min_y + cell};
|
||||||
}
|
}
|
||||||
|
|
||||||
int TileGrid::zoom_for_resolution(double /*metres_per_pixel*/) noexcept {
|
int TileGrid::zoom_for_resolution(double /*metres_per_pixel*/) noexcept {
|
||||||
return kLevel; // one fixed 100 m grid, no pyramid
|
return kLevel; // one fixed 100 m grid, no pyramid
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<TileKey> TileGrid::tiles_for_view(const TileBBox& view, int /*zoom*/,
|
std::vector<TileKey> TileGrid::tiles_for_view(const TileBBox& view, int zoom,
|
||||||
int max_tiles) {
|
int max_tiles) {
|
||||||
std::vector<TileKey> tiles;
|
std::vector<TileKey> tiles;
|
||||||
if (max_tiles <= 0 || !std::isfinite(view.min_x) ||
|
if (max_tiles <= 0 || !std::isfinite(view.min_x) ||
|
||||||
@@ -79,8 +79,8 @@ std::vector<TileKey> TileGrid::tiles_for_view(const TileBBox& view, int /*zoom*/
|
|||||||
const double min_y = std::min(view.min_y, view.max_y);
|
const double min_y = std::min(view.min_y, view.max_y);
|
||||||
const double max_y = std::max(view.min_y, view.max_y);
|
const double max_y = std::max(view.min_y, view.max_y);
|
||||||
|
|
||||||
const TileKey lo = tile_at(min_x, min_y);
|
const TileKey lo = tile_at(min_x, min_y, zoom);
|
||||||
const TileKey hi = tile_at(max_x, max_y);
|
const TileKey hi = tile_at(max_x, max_y, zoom);
|
||||||
|
|
||||||
using Wide = std::int64_t;
|
using Wide = std::int64_t;
|
||||||
const Wide width = static_cast<Wide>(hi.x) - lo.x + 1;
|
const Wide width = static_cast<Wide>(hi.x) - lo.x + 1;
|
||||||
@@ -113,11 +113,12 @@ std::vector<TileKey> TileGrid::tiles_for_view(const TileBBox& view, int /*zoom*/
|
|||||||
centre_y - rows / 2, static_cast<Wide>(lo.y),
|
centre_y - rows / 2, static_cast<Wide>(lo.y),
|
||||||
static_cast<Wide>(hi.y) - rows + 1);
|
static_cast<Wide>(hi.y) - rows + 1);
|
||||||
|
|
||||||
|
const int key_zoom = zoom >= kCellZoomMin ? zoom : kLevel;
|
||||||
tiles.reserve(static_cast<std::size_t>(columns * rows));
|
tiles.reserve(static_cast<std::size_t>(columns * rows));
|
||||||
for (Wide row = first_row; row < first_row + rows; ++row) {
|
for (Wide row = first_row; row < first_row + rows; ++row) {
|
||||||
for (Wide col = first_col; col < first_col + columns; ++col) {
|
for (Wide col = first_col; col < first_col + columns; ++col) {
|
||||||
tiles.push_back(
|
tiles.push_back(
|
||||||
TileKey{kLevel, static_cast<int>(col), static_cast<int>(row)});
|
TileKey{key_zoom, static_cast<int>(col), static_cast<int>(row)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tiles;
|
return tiles;
|
||||||
@@ -127,8 +128,8 @@ std::vector<TileKey> TileGrid::tiles_for_view(const TileBBox& view, int /*zoom*/
|
|||||||
|
|
||||||
WmsConfig WmsConfig::geoportal_ortho() {
|
WmsConfig WmsConfig::geoportal_ortho() {
|
||||||
WmsConfig cfg;
|
WmsConfig cfg;
|
||||||
// Public Geoportal ortophoto WMS (EPSG:2180). If this endpoint blocks CORS
|
// Public Geoportal ortophoto WMS (EPSG:2180). Swap base_url/layers via
|
||||||
// for emscripten_fetch, swap base_url/layers via set_config() from the host.
|
// set_config() from the host to point at a different backend.
|
||||||
cfg.base_url =
|
cfg.base_url =
|
||||||
"https://mapy.geoportal.gov.pl/wss/service/PZGIK/ORTO/WMS/StandardResolution";
|
"https://mapy.geoportal.gov.pl/wss/service/PZGIK/ORTO/WMS/StandardResolution";
|
||||||
cfg.layers = "Raster";
|
cfg.layers = "Raster";
|
||||||
@@ -233,59 +234,11 @@ void TileService::complete_fetch(const TileKey& key, bool ok,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
|
||||||
namespace {
|
|
||||||
// Delay between retry attempts, so a persistently failing endpoint is retried
|
|
||||||
// steadily rather than in a tight loop.
|
|
||||||
constexpr int kRetryDelayMs = 1500;
|
|
||||||
|
|
||||||
struct FetchCtx {
|
|
||||||
TileService* self;
|
|
||||||
TileKey key;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RetryCtx {
|
|
||||||
TileService* self;
|
|
||||||
TileKey key;
|
|
||||||
};
|
|
||||||
|
|
||||||
void on_retry(void* user_data) {
|
|
||||||
auto* ctx = static_cast<RetryCtx*>(user_data);
|
|
||||||
TileService* self = ctx->self;
|
|
||||||
const TileKey key = ctx->key;
|
|
||||||
delete ctx;
|
|
||||||
self->retry_now(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_fetch_success(emscripten_fetch_t* fetch) {
|
|
||||||
auto* ctx = static_cast<FetchCtx*>(fetch->userData);
|
|
||||||
std::vector<std::uint8_t> bytes(
|
|
||||||
reinterpret_cast<const std::uint8_t*>(fetch->data),
|
|
||||||
reinterpret_cast<const std::uint8_t*>(fetch->data) +
|
|
||||||
static_cast<std::size_t>(fetch->numBytes));
|
|
||||||
TileService* self = ctx->self;
|
|
||||||
const TileKey key = ctx->key;
|
|
||||||
delete ctx;
|
|
||||||
emscripten_fetch_close(fetch);
|
|
||||||
self->complete_fetch(key, true, std::move(bytes), {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_fetch_error(emscripten_fetch_t* fetch) {
|
|
||||||
auto* ctx = static_cast<FetchCtx*>(fetch->userData);
|
|
||||||
TileService* self = ctx->self;
|
|
||||||
const TileKey key = ctx->key;
|
|
||||||
delete ctx;
|
|
||||||
emscripten_fetch_close(fetch);
|
|
||||||
self->complete_fetch(key, false, {}, {});
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void TileService::begin_fetch(const TileKey& key) {
|
void TileService::begin_fetch(const TileKey& key) {
|
||||||
const std::string url = url_for(key);
|
const std::string url = url_for(key);
|
||||||
|
|
||||||
// An injected fetcher wins on every platform (used by native tests and any
|
// The host injects the fetcher (the editor's threaded downloader, or a test
|
||||||
// host that wants to route the request itself).
|
// stub). Without one there is no transport, so the tile simply fails.
|
||||||
if (fetcher_) {
|
if (fetcher_) {
|
||||||
const TileKey captured = key;
|
const TileKey captured = key;
|
||||||
fetcher_(url, [this, captured](bool ok, std::vector<std::uint8_t> bytes,
|
fetcher_(url, [this, captured](bool ok, std::vector<std::uint8_t> bytes,
|
||||||
@@ -294,33 +247,15 @@ void TileService::begin_fetch(const TileKey& key) {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
|
||||||
emscripten_fetch_attr_t attr;
|
|
||||||
emscripten_fetch_attr_init(&attr);
|
|
||||||
std::strcpy(attr.requestMethod, "GET");
|
|
||||||
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
|
|
||||||
attr.userData = new FetchCtx{this, key};
|
|
||||||
attr.onsuccess = on_fetch_success;
|
|
||||||
attr.onerror = on_fetch_error;
|
|
||||||
emscripten_fetch(&attr, url.c_str());
|
|
||||||
#else
|
|
||||||
// No network backend available (native, no injected fetcher): mark failed.
|
|
||||||
complete_fetch(key, false, {}, {});
|
complete_fetch(key, false, {}, {});
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TileService::retry_now(const TileKey& key) { begin_fetch(key); }
|
void TileService::retry_now(const TileKey& key) { begin_fetch(key); }
|
||||||
|
|
||||||
void TileService::schedule_retry(const TileKey& key) {
|
void TileService::schedule_retry(const TileKey& key) {
|
||||||
#ifdef __EMSCRIPTEN__
|
// Re-issue at once; the injected fetcher owns the timing, so a fetcher that
|
||||||
// Defer asynchronously: infinite retry without blocking or recursion.
|
// eventually succeeds ends the retry loop.
|
||||||
emscripten_async_call(&on_retry, new RetryCtx{this, key}, kRetryDelayMs);
|
|
||||||
#else
|
|
||||||
// Injected-fetcher path (native/tests): re-issue immediately. The fetcher
|
|
||||||
// controls timing, so a fetcher that eventually succeeds ends the loop.
|
|
||||||
begin_fetch(key);
|
begin_fetch(key);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace maj0sted::web
|
} // namespace maj0sted::editor
|
||||||
367
maj0sted/src/io/scn_export.cpp
Normal file
367
maj0sted/src/io/scn_export.cpp
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
#include "maj0sted/io/scn_export.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <numbers>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace maj0sted::io {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using maj0sted::editor::NiweletaPolys;
|
||||||
|
using maj0sted::editor::PlanPoint;
|
||||||
|
using maj0sted::editor::PlanPolyline;
|
||||||
|
|
||||||
|
struct AxisElement {
|
||||||
|
int kind{0};
|
||||||
|
int straight_index{-1};
|
||||||
|
int gap{-1};
|
||||||
|
int element_index{-1};
|
||||||
|
double length{0.0};
|
||||||
|
double radius_start{0.0};
|
||||||
|
double radius_end{0.0};
|
||||||
|
std::vector<PlanPoint> points;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WorldXYZ {
|
||||||
|
double x{0.0};
|
||||||
|
double y{0.0};
|
||||||
|
double z{0.0};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<AxisElement> recover_axis(const NiweletaPolys& solved) {
|
||||||
|
std::vector<AxisElement> out;
|
||||||
|
const auto& polys = solved.polylines;
|
||||||
|
for (std::size_t i = 0; i < polys.size();) {
|
||||||
|
const auto& a = polys[i];
|
||||||
|
if (a.points.size() < 2) {
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (i + 1 < polys.size()) {
|
||||||
|
const auto& b = polys[i + 1];
|
||||||
|
if (b.straight_index == a.straight_index && b.gap == a.gap &&
|
||||||
|
b.element_index == a.element_index &&
|
||||||
|
b.points.size() == a.points.size()) {
|
||||||
|
AxisElement el;
|
||||||
|
el.kind = a.kind;
|
||||||
|
el.straight_index = a.straight_index;
|
||||||
|
el.gap = a.gap;
|
||||||
|
el.element_index = a.element_index;
|
||||||
|
el.length = a.length;
|
||||||
|
el.radius_start = a.radius_start;
|
||||||
|
el.radius_end = a.radius_end;
|
||||||
|
el.points.resize(a.points.size());
|
||||||
|
for (std::size_t k = 0; k < a.points.size(); ++k) {
|
||||||
|
el.points[k] = {(a.points[k].x + b.points[k].x) * 0.5,
|
||||||
|
(a.points[k].y + b.points[k].y) * 0.5};
|
||||||
|
}
|
||||||
|
out.push_back(std::move(el));
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AxisElement el;
|
||||||
|
el.kind = a.kind;
|
||||||
|
el.straight_index = a.straight_index;
|
||||||
|
el.gap = a.gap;
|
||||||
|
el.element_index = a.element_index;
|
||||||
|
el.length = a.length;
|
||||||
|
el.radius_start = a.radius_start;
|
||||||
|
el.radius_end = a.radius_end;
|
||||||
|
el.points = a.points;
|
||||||
|
out.push_back(std::move(el));
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldXYZ to_scn(double east, double north, double height, double ox, double oy) {
|
||||||
|
return {east - ox, height, -(north - oy)};
|
||||||
|
}
|
||||||
|
|
||||||
|
void plan_tangent_to_scn(double de, double dn, double& out_x, double& out_z) {
|
||||||
|
const double len = std::hypot(de, dn);
|
||||||
|
if (len < 1e-12) {
|
||||||
|
out_x = 0.0;
|
||||||
|
out_z = 1.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out_x = de / len;
|
||||||
|
out_z = -dn / len;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool axis_pose_at(const std::vector<PlanPoint>& pts, double along, PlanPoint& out_p,
|
||||||
|
double& out_tx, double& out_ty) {
|
||||||
|
if (pts.size() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
double a = 0.0;
|
||||||
|
for (std::size_t i = 1; i < pts.size(); ++i) {
|
||||||
|
const double dx = pts[i].x - pts[i - 1].x;
|
||||||
|
const double dy = pts[i].y - pts[i - 1].y;
|
||||||
|
const double seg = std::hypot(dx, dy);
|
||||||
|
if (a + seg >= along || i + 1 == pts.size()) {
|
||||||
|
const double t = seg > 1e-12 ? std::clamp((along - a) / seg, 0.0, 1.0) : 0.0;
|
||||||
|
out_p = {pts[i - 1].x + dx * t, pts[i - 1].y + dy * t};
|
||||||
|
if (seg > 1e-12) {
|
||||||
|
out_tx = dx / seg;
|
||||||
|
out_ty = dy / seg;
|
||||||
|
} else {
|
||||||
|
out_tx = 1.0;
|
||||||
|
out_ty = 0.0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
a += seg;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double axis_total_length(const std::vector<PlanPoint>& pts) {
|
||||||
|
double along = 0.0;
|
||||||
|
for (std::size_t i = 1; i < pts.size(); ++i) {
|
||||||
|
along += std::hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y);
|
||||||
|
}
|
||||||
|
return along;
|
||||||
|
}
|
||||||
|
|
||||||
|
double circular_bezier_handle(double radius, double phi) {
|
||||||
|
const double p = std::clamp(phi, 1e-6, std::numbers::pi);
|
||||||
|
return (4.0 / 3.0) * std::max(radius, 1.0) * std::tan(p * 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fit_clothoid_bezier_handles(const std::vector<PlanPoint>& pts, double s0,
|
||||||
|
double s1, const PlanPoint& p0, const PlanPoint& p3,
|
||||||
|
double t0x, double t0y, double t1x, double t1y,
|
||||||
|
double& out_l0, double& out_l1) {
|
||||||
|
const double span = s1 - s0;
|
||||||
|
if (span < 1e-6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int samples = std::max(8, static_cast<int>(std::ceil(span / 2.0)));
|
||||||
|
double m00 = 0.0, m01 = 0.0, m11 = 0.0, b0 = 0.0, b1 = 0.0;
|
||||||
|
for (int i = 0; i <= samples; ++i) {
|
||||||
|
const double t = static_cast<double>(i) / static_cast<double>(samples);
|
||||||
|
PlanPoint q{};
|
||||||
|
double tx = 1.0, ty = 0.0;
|
||||||
|
if (!axis_pose_at(pts, s0 + span * t, q, tx, ty)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const double omt = 1.0 - t;
|
||||||
|
const double c0 = 3.0 * omt * omt * t;
|
||||||
|
const double c1 = 3.0 * omt * t * t;
|
||||||
|
const double ax =
|
||||||
|
omt * omt * (1.0 + 2.0 * t) * p0.x + t * t * (3.0 - 2.0 * t) * p3.x;
|
||||||
|
const double ay =
|
||||||
|
omt * omt * (1.0 + 2.0 * t) * p0.y + t * t * (3.0 - 2.0 * t) * p3.y;
|
||||||
|
const double rx = q.x - ax;
|
||||||
|
const double ry = q.y - ay;
|
||||||
|
const double a0x = c0 * t0x, a0y = c0 * t0y;
|
||||||
|
const double a1x = -c1 * t1x, a1y = -c1 * t1y;
|
||||||
|
m00 += a0x * a0x + a0y * a0y;
|
||||||
|
m01 += a0x * a1x + a0y * a1y;
|
||||||
|
m11 += a1x * a1x + a1y * a1y;
|
||||||
|
b0 += a0x * rx + a0y * ry;
|
||||||
|
b1 += a1x * rx + a1y * ry;
|
||||||
|
}
|
||||||
|
const double det = m00 * m11 - m01 * m01;
|
||||||
|
if (std::abs(det) < 1e-12) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const double l0 = (m11 * b0 - m01 * b1) / det;
|
||||||
|
const double l1 = (-m01 * b0 + m00 * b1) / det;
|
||||||
|
if (l0 < 0.05 || l1 < 0.05) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out_l0 = std::clamp(l0, 0.05, span);
|
||||||
|
out_l1 = std::clamp(l1, 0.05, span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clothoid_handles_from_radii(double len, double phi, double r0, double r1,
|
||||||
|
double& out_l0, double& out_l1) {
|
||||||
|
auto handle = [&](double r) {
|
||||||
|
if (r < 1.0) {
|
||||||
|
return len / 3.0;
|
||||||
|
}
|
||||||
|
return circular_bezier_handle(r, phi);
|
||||||
|
};
|
||||||
|
out_l0 = handle(r0);
|
||||||
|
out_l1 = handle(r1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_scn_track(std::ostream& out, const std::string& name, const WorldXYZ& p1,
|
||||||
|
const WorldXYZ& cv1, const WorldXYZ& p2, const WorldXYZ& cv2,
|
||||||
|
double length, double radius) {
|
||||||
|
out << "node -1 0 " << name << " track normal " << length
|
||||||
|
<< " 1.435 0.15 25.0 20 0 flat vis\n";
|
||||||
|
out << " rail_screw_used1 6 1435mm/tpbps-new2 0.2 0.5 1.1\n";
|
||||||
|
auto fmt = [](double v) {
|
||||||
|
std::ostringstream s;
|
||||||
|
s.setf(std::ios::fixed);
|
||||||
|
s.precision(4);
|
||||||
|
s << v;
|
||||||
|
return s.str();
|
||||||
|
};
|
||||||
|
out << fmt(p1.x) << ' ' << fmt(p1.y) << ' ' << fmt(p1.z) << " 0\n";
|
||||||
|
out << fmt(cv1.x) << ' ' << fmt(cv1.y) << ' ' << fmt(cv1.z) << '\n';
|
||||||
|
out << fmt(cv2.x) << ' ' << fmt(cv2.y) << ' ' << fmt(cv2.z) << '\n';
|
||||||
|
out << fmt(p2.x) << ' ' << fmt(p2.y) << ' ' << fmt(p2.z) << " 0\n";
|
||||||
|
out << fmt(radius) << '\n';
|
||||||
|
out << "endtrack\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScnExportOptions resolve_scn_origin(const std::vector<NiweletaPolys>& solved,
|
||||||
|
ScnExportOptions options) {
|
||||||
|
if (std::abs(options.origin_east) >= 1.0 ||
|
||||||
|
std::abs(options.origin_north) >= 1.0) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
for (const auto& s : solved) {
|
||||||
|
const auto axis = recover_axis(s);
|
||||||
|
if (!axis.empty() && !axis.front().points.empty()) {
|
||||||
|
const auto& p = axis.front().points.front();
|
||||||
|
if (std::abs(p.x) > 10000.0 || std::abs(p.y) > 10000.0) {
|
||||||
|
options.origin_east = p.x;
|
||||||
|
options.origin_north = p.y;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScnExportResult export_scn(const std::vector<NiweletaPolys>& solved,
|
||||||
|
const ScnExportOptions& options, std::ostream& out) {
|
||||||
|
const auto opt = resolve_scn_origin(solved, options);
|
||||||
|
ScnExportResult result;
|
||||||
|
result.origin_east = opt.origin_east;
|
||||||
|
result.origin_north = opt.origin_north;
|
||||||
|
|
||||||
|
out << "//$n plan\n";
|
||||||
|
out << "//$d origin " << opt.origin_east << ' ' << opt.origin_north << "\n\n";
|
||||||
|
|
||||||
|
const double kRailY = opt.rail_y;
|
||||||
|
const double kMaxArcAngle = opt.max_arc_angle;
|
||||||
|
|
||||||
|
for (std::size_t n = 0; n < solved.size(); ++n) {
|
||||||
|
const auto axis = recover_axis(solved[n]);
|
||||||
|
for (std::size_t ei = 0; ei < axis.size(); ++ei) {
|
||||||
|
const auto& el = axis[ei];
|
||||||
|
if (el.points.size() < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const double total =
|
||||||
|
el.length > 1e-6 ? el.length : axis_total_length(el.points);
|
||||||
|
if (total < 0.05) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto curvature_at = [&](double along) {
|
||||||
|
const double k0 = el.radius_start > 1.0 ? 1.0 / el.radius_start : 0.0;
|
||||||
|
const double k1 = el.radius_end > 1.0 ? 1.0 / el.radius_end : 0.0;
|
||||||
|
const double t =
|
||||||
|
total > 1e-9 ? std::clamp(along / total, 0.0, 1.0) : 0.0;
|
||||||
|
return k0 + (k1 - k0) * t;
|
||||||
|
};
|
||||||
|
auto radius_from_k = [](double k) {
|
||||||
|
return std::abs(k) > 1e-9 ? 1.0 / std::abs(k) : 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto emit_piece = [&](double s0, double s1, double piece_len,
|
||||||
|
double radius, const char* tag) {
|
||||||
|
PlanPoint p0{}, p1{};
|
||||||
|
double t0x = 1.0, t0y = 0.0, t1x = 1.0, t1y = 0.0;
|
||||||
|
if (!axis_pose_at(el.points, s0, p0, t0x, t0y) ||
|
||||||
|
!axis_pose_at(el.points, s1, p1, t1x, t1y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto w0 = to_scn(p0.x, p0.y, kRailY, opt.origin_east,
|
||||||
|
opt.origin_north);
|
||||||
|
const auto w1 = to_scn(p1.x, p1.y, kRailY, opt.origin_east,
|
||||||
|
opt.origin_north);
|
||||||
|
double tx0 = 0.0, tz0 = 0.0, tx1 = 0.0, tz1 = 0.0;
|
||||||
|
plan_tangent_to_scn(t0x, t0y, tx0, tz0);
|
||||||
|
plan_tangent_to_scn(t1x, t1y, tx1, tz1);
|
||||||
|
|
||||||
|
WorldXYZ cv1{}, cv2{};
|
||||||
|
double radius_out = 0.0;
|
||||||
|
if (el.kind == 1 && radius >= 1.0) {
|
||||||
|
const double phi = std::clamp(piece_len / std::max(radius, 1.0),
|
||||||
|
1e-6, std::numbers::pi);
|
||||||
|
const double handle = circular_bezier_handle(radius, phi);
|
||||||
|
cv1 = {tx0 * handle, 0.0, tz0 * handle};
|
||||||
|
cv2 = {-tx1 * handle, 0.0, -tz1 * handle};
|
||||||
|
radius_out = radius;
|
||||||
|
} else if (el.kind == 2) {
|
||||||
|
const double cross = t0x * t1y - t0y * t1x;
|
||||||
|
const double dot = t0x * t1x + t0y * t1y;
|
||||||
|
const double phi = std::clamp(std::abs(std::atan2(cross, dot)),
|
||||||
|
1e-6, std::numbers::pi);
|
||||||
|
const double r0 = radius_from_k(curvature_at(s0));
|
||||||
|
const double r1 = radius_from_k(curvature_at(s1));
|
||||||
|
double l0 = piece_len / 3.0;
|
||||||
|
double l1 = piece_len / 3.0;
|
||||||
|
if (!fit_clothoid_bezier_handles(el.points, s0, s1, p0, p1, t0x,
|
||||||
|
t0y, t1x, t1y, l0, l1)) {
|
||||||
|
clothoid_handles_from_radii(piece_len, phi, r0, r1, l0, l1);
|
||||||
|
}
|
||||||
|
cv1 = {tx0 * l0, 0.0, tz0 * l0};
|
||||||
|
cv2 = {-tx1 * l1, 0.0, -tz1 * l1};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream name;
|
||||||
|
name << "plan_n" << n;
|
||||||
|
if (el.straight_index >= 0) {
|
||||||
|
name << "_s" << el.straight_index;
|
||||||
|
} else {
|
||||||
|
name << "_g" << el.gap << "_e" << el.element_index;
|
||||||
|
}
|
||||||
|
name << '_' << tag << '_' << result.tracks;
|
||||||
|
const auto track_name = name.str();
|
||||||
|
if (result.first_track_name.empty()) {
|
||||||
|
result.first_track_name = track_name;
|
||||||
|
}
|
||||||
|
write_scn_track(out, track_name, w0, cv1, w1, cv2, piece_len,
|
||||||
|
radius_out);
|
||||||
|
++result.tracks;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (el.kind == 0 || (el.kind == 1 && el.radius_start < 1.0)) {
|
||||||
|
emit_piece(0.0, total, total, 0.0, "str");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (el.kind == 2) {
|
||||||
|
emit_piece(0.0, total, total, 0.0, "kp");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double r = std::max(el.radius_start, el.radius_end);
|
||||||
|
const double phi = total / std::max(r, 1.0);
|
||||||
|
const int pieces =
|
||||||
|
std::max(1, static_cast<int>(std::ceil(phi / kMaxArcAngle)));
|
||||||
|
const double piece_len = total / static_cast<double>(pieces);
|
||||||
|
for (int p = 0; p < pieces; ++p) {
|
||||||
|
const double s0 = piece_len * static_cast<double>(p);
|
||||||
|
const double s1 =
|
||||||
|
p + 1 == pieces ? total : piece_len * static_cast<double>(p + 1);
|
||||||
|
emit_piece(s0, s1, s1 - s0, r, "arc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out << "FirstInit\n\n";
|
||||||
|
if (!result.first_track_name.empty()) {
|
||||||
|
out << "trainset none " << result.first_track_name << " 5 0\n";
|
||||||
|
out << "node -1 0 plan_eu07 dynamic PKP\\303E_V1 303E-EP-TV-424-HIST "
|
||||||
|
"303E-EP-TV 0 headdriver 35.WH25 0 enddynamic\n";
|
||||||
|
out << "endtrainset\n";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace maj0sted::io
|
||||||
@@ -33,10 +33,10 @@ target_link_libraries(maj0sted_test_render PRIVATE maj0sted::maj0sted)
|
|||||||
target_include_directories(maj0sted_test_render PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(maj0sted_test_render PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
add_test(NAME render COMMAND maj0sted_test_render)
|
add_test(NAME render COMMAND maj0sted_test_render)
|
||||||
|
|
||||||
add_executable(maj0sted_test_web test_web.cpp)
|
add_executable(maj0sted_test_editor test_editor.cpp)
|
||||||
target_link_libraries(maj0sted_test_web PRIVATE maj0sted::maj0sted)
|
target_link_libraries(maj0sted_test_editor PRIVATE maj0sted::maj0sted)
|
||||||
target_include_directories(maj0sted_test_web PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(maj0sted_test_editor PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
add_test(NAME web COMMAND maj0sted_test_web)
|
add_test(NAME editor COMMAND maj0sted_test_editor)
|
||||||
|
|
||||||
add_executable(maj0sted_test_tile_cache test_tile_cache.cpp)
|
add_executable(maj0sted_test_tile_cache test_tile_cache.cpp)
|
||||||
target_link_libraries(maj0sted_test_tile_cache PRIVATE maj0sted::maj0sted)
|
target_link_libraries(maj0sted_test_tile_cache PRIVATE maj0sted::maj0sted)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
#include "maj0sted/app/editor_document.hpp"
|
#include "maj0sted/app/editor_document.hpp"
|
||||||
|
|
||||||
using namespace maj0sted::app;
|
using namespace maj0sted::app;
|
||||||
using maj0sted::web::GapFit;
|
using maj0sted::editor::GapFit;
|
||||||
using maj0sted::web::NiweletaSpec;
|
using maj0sted::editor::NiweletaSpec;
|
||||||
using maj0sted::web::StraightSpec;
|
using maj0sted::editor::StraightSpec;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -28,14 +28,28 @@ EditorDocument sample_document() {
|
|||||||
NiweletaSpec b;
|
NiweletaSpec b;
|
||||||
b.name = "Bocznica z spacją";
|
b.name = "Bocznica z spacją";
|
||||||
b.straights = {StraightSpec{500000.25, 300000.75, 500200.0, 300000.0}};
|
b.straights = {StraightSpec{500000.25, 300000.75, 500200.0, 300000.0}};
|
||||||
|
b.straights[0].rel_kind = 1;
|
||||||
|
b.straights[0].rel_niw = 0;
|
||||||
|
b.straights[0].rel_str = 0;
|
||||||
|
b.straights[0].rel_offset = -4.5;
|
||||||
|
|
||||||
doc.niwelety = {a, b};
|
doc.niwelety = {a, b};
|
||||||
|
doc.view_x = 500100.5;
|
||||||
|
doc.view_y = 300050.25;
|
||||||
|
doc.view_extent = 420.0;
|
||||||
|
doc.origin_set = true;
|
||||||
|
doc.georeferenced = true;
|
||||||
|
doc.origin_x = 500000.0;
|
||||||
|
doc.origin_y = 300000.0;
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool same(const StraightSpec& x, const StraightSpec& y) {
|
bool same(const StraightSpec& x, const StraightSpec& y) {
|
||||||
return x.x1 == y.x1 && x.y1 == y.y1 && x.x2 == y.x2 && x.y2 == y.y2 &&
|
return x.x1 == y.x1 && x.y1 == y.y1 && x.x2 == y.x2 && x.y2 == y.y2 &&
|
||||||
x.hidden == y.hidden;
|
x.hidden == y.hidden && x.rel_kind == y.rel_kind &&
|
||||||
|
x.rel_niw == y.rel_niw && x.rel_str == y.rel_str &&
|
||||||
|
x.rel_offset == y.rel_offset && x.rel_cot == y.rel_cot &&
|
||||||
|
x.rel_side == y.rel_side && x.rel_length == y.rel_length;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool same(const GapFit& x, const GapFit& y) {
|
bool same(const GapFit& x, const GapFit& y) {
|
||||||
@@ -46,6 +60,13 @@ bool same(const GapFit& x, const GapFit& y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void check_equal(const EditorDocument& x, const EditorDocument& y) {
|
void check_equal(const EditorDocument& x, const EditorDocument& y) {
|
||||||
|
CHECK(x.view_x == y.view_x);
|
||||||
|
CHECK(x.view_y == y.view_y);
|
||||||
|
CHECK(x.view_extent == y.view_extent);
|
||||||
|
CHECK(x.origin_set == y.origin_set);
|
||||||
|
CHECK(x.georeferenced == y.georeferenced);
|
||||||
|
CHECK(x.origin_x == y.origin_x);
|
||||||
|
CHECK(x.origin_y == y.origin_y);
|
||||||
CHECK(x.niwelety.size() == y.niwelety.size());
|
CHECK(x.niwelety.size() == y.niwelety.size());
|
||||||
if (x.niwelety.size() != y.niwelety.size()) return;
|
if (x.niwelety.size() != y.niwelety.size()) return;
|
||||||
for (std::size_t n = 0; n < x.niwelety.size(); ++n) {
|
for (std::size_t n = 0; n < x.niwelety.size(); ++n) {
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
#include <numbers>
|
#include <numbers>
|
||||||
|
|
||||||
#include "check.hpp"
|
#include "check.hpp"
|
||||||
#include "maj0sted/web/editor.hpp"
|
#include "maj0sted/editor/editor.hpp"
|
||||||
#include "maj0sted/web/solve.hpp"
|
#include "maj0sted/editor/solve.hpp"
|
||||||
|
|
||||||
using namespace maj0sted::web;
|
using namespace maj0sted::editor;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
bool has_kind(const std::vector<WebPolyline>& polylines, int kind) {
|
bool has_kind(const std::vector<PlanPolyline>& polylines, int kind) {
|
||||||
for (const auto& p : polylines) {
|
for (const auto& p : polylines) {
|
||||||
if (p.kind == kind) return true;
|
if (p.kind == kind) return true;
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ void editor_emits_two_rails_at_standard_gauge() {
|
|||||||
CHECK(solved[0].polylines[0].straight_index == 0);
|
CHECK(solved[0].polylines[0].straight_index == 0);
|
||||||
CHECK(solved[0].polylines[0].gap == -1);
|
CHECK(solved[0].polylines[0].gap == -1);
|
||||||
CHECK(std::abs(solved[0].polylines[0].length - 5.0) < 1e-9);
|
CHECK(std::abs(solved[0].polylines[0].length - 5.0) < 1e-9);
|
||||||
const WebPolyline* arc = nullptr;
|
const PlanPolyline* arc = nullptr;
|
||||||
for (const auto& polyline : solved[0].polylines) {
|
for (const auto& polyline : solved[0].polylines) {
|
||||||
if (polyline.kind == 1) {
|
if (polyline.kind == 1) {
|
||||||
arc = &polyline;
|
arc = &polyline;
|
||||||
@@ -138,7 +138,7 @@ void editor_emits_two_rails_at_standard_gauge() {
|
|||||||
|
|
||||||
void editor_exposes_transition_radius_range() {
|
void editor_exposes_transition_radius_range() {
|
||||||
const auto solved = solve_project({short_corner(5.0, 2.0)});
|
const auto solved = solve_project({short_corner(5.0, 2.0)});
|
||||||
const WebPolyline* transition = nullptr;
|
const PlanPolyline* transition = nullptr;
|
||||||
for (const auto& polyline : solved[0].polylines) {
|
for (const auto& polyline : solved[0].polylines) {
|
||||||
if (polyline.kind == 2) {
|
if (polyline.kind == 2) {
|
||||||
transition = &polyline;
|
transition = &polyline;
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "check.hpp"
|
#include "check.hpp"
|
||||||
#include "maj0sted/web/tile_cache.hpp"
|
#include "maj0sted/editor/tile_cache.hpp"
|
||||||
|
|
||||||
using namespace maj0sted::web;
|
using namespace maj0sted::editor;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ node_groups::close()
|
|||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
node_groups::clear()
|
||||||
|
{
|
||||||
|
// nodes/events are owned by the region (or already gone with it); do not dereference them
|
||||||
|
m_groupmap.clear();
|
||||||
|
while( false == m_activegroup.empty() ) {
|
||||||
|
m_activegroup.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool node_groups::assign_cross_switch(map::track_switch& sw, std::string &sw_name, std::string const &id, size_t idx)
|
bool node_groups::assign_cross_switch(map::track_switch& sw, std::string &sw_name, std::string const &id, size_t idx)
|
||||||
{
|
{
|
||||||
sw.action[idx] = simulation::Events.FindEvent(sw_name + ":" + id);
|
sw.action[idx] = simulation::Events.FindEvent(sw_name + ":" + id);
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ public:
|
|||||||
// indicates creation of current group ended. returns: handle to the parent group or null_handle if group stack is empty
|
// indicates creation of current group ended. returns: handle to the parent group or null_handle if group stack is empty
|
||||||
group_handle
|
group_handle
|
||||||
close();
|
close();
|
||||||
|
// drops every group without touching node pointers — call when the region that owned
|
||||||
|
// those nodes is about to be (or has just been) destroyed, before a scenery reload
|
||||||
|
void
|
||||||
|
clear();
|
||||||
// update minimap objects
|
// update minimap objects
|
||||||
void
|
void
|
||||||
update_map();
|
update_map();
|
||||||
|
|||||||
228
scene/scenerybinary_format.md
Normal file
228
scene/scenerybinary_format.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# Binarny format scenerii („eu7") — specyfikacja zapisu na dysku
|
||||||
|
|
||||||
|
Źródło autorytatywne: [`scene/scenerybinary.h`](scenerybinary.h) i
|
||||||
|
[`scene/scenerybinary.cpp`](scenerybinary.cpp). Ten dokument opisuje format tak, jak kod go
|
||||||
|
aktualnie czyta i zapisuje (wersja na dysku: **10**). Jeśli kiedykolwiek się rozjadą,
|
||||||
|
rozstrzyga kod — wtedy zaktualizuj ten plik.
|
||||||
|
|
||||||
|
## 1. Cel
|
||||||
|
|
||||||
|
Format binarny zastępuje stare formaty tekstowe (`.scn` / `.scm` / `.inc` / `.ctr`) oraz dawny
|
||||||
|
blob terenu `.sbt`. To **przezroczysty, modularny cache**: każdy tekstowy plik scenerii
|
||||||
|
kompiluje się do dokładnie jednego binarnego „twina", konsumowanego na warstwie plikowej
|
||||||
|
`cParser`, więc deserializer scenerii nigdy nie widzi formatu. Twin przechowuje własny, w pełni
|
||||||
|
rozwinięty strumień tokenów pliku źródłowego (komentarze usunięte, liczby otypowane, stringi
|
||||||
|
zinternowane) i zachowuje dyrektywy `include` jako referencje — twin jednego pliku wskazuje na
|
||||||
|
twiny plików, które tamten dołącza.
|
||||||
|
|
||||||
|
Cele projektowe: małe pliki, **odczyt strumieniowy z bufora** (cały twin wczytany raz, potem
|
||||||
|
parsowany wskaźnikiem, a tokeny-stringi serwowane jako widoki — bez wywołań strumienia
|
||||||
|
per-bajt, bez materializacji pośredniej), równoległy/offline'owy baking oraz **progresywne
|
||||||
|
ładowanie** (najpierw infrastruktura, żeby gra mogła ruszyć, potem strumieniowanie węzłów
|
||||||
|
wizualnych w kolejności odległości od kamery).
|
||||||
|
|
||||||
|
## 2. Nazewnictwo plików (mapowanie twina)
|
||||||
|
|
||||||
|
Jeden plik źródłowy → jeden twin; rozszerzenie binarne wynika z rodzaju źródła
|
||||||
|
(`scenerybinary_extension_for()`):
|
||||||
|
|
||||||
|
| źródło | twin | `scenery_file_kind` |
|
||||||
|
|---------|---------|---------------------|
|
||||||
|
| `.scn` | `.scnb` | `scn` (0) |
|
||||||
|
| `.inc` | `.incb` | `inc` (1) |
|
||||||
|
| `.scm` | `.scmb` | `scm` (2) |
|
||||||
|
| `.ctr` | `.ctrb` | `scn` (0)¹ |
|
||||||
|
|
||||||
|
¹ `.ctr` dostaje własne rozszerzenie `.ctrb`, ale jest tokenizowany jako rodzaj `scn` (enum
|
||||||
|
`kind` nie ma osobnej wartości dla `ctr`).
|
||||||
|
|
||||||
|
Twin jest **odtwarzany tylko, jeśli jest co najmniej tak nowy jak źródło tekstowe** (porównanie
|
||||||
|
czasu modyfikacji); edycja pliku scenerii wymusza rekompilację zamiast odtworzenia
|
||||||
|
nieaktualnego twina.
|
||||||
|
|
||||||
|
## 3. Ogólny układ pliku
|
||||||
|
|
||||||
|
```
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| Nagłówek |
|
||||||
|
| magic : uint32 LE = MAKE_ID4('e','u','7',10) | 4 bajty
|
||||||
|
| kind : uint8 = scenery_file_kind | 1 bajt
|
||||||
|
| flags : uint32 LE (bit0 = infra-relevant) | 4 bajty
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| Tablica stringów |
|
||||||
|
| count : varint |
|
||||||
|
| powtórz count razy: |
|
||||||
|
| length : varint |
|
||||||
|
| bytes : <length> surowych bajtów (bez NUL) |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| Strumień wpisów |
|
||||||
|
| wpisy jeden po drugim aż do końca pliku |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
* **magic** — `"eu7"` plus jednobajtowa wersja (obecnie `10`), little-endian przez `MAKE_ID4`.
|
||||||
|
Reader odrzuca każdy bufor, którego magic/wersja nie zgadza się dokładnie (starsze twiny są
|
||||||
|
rekompilowane, nie odczytywane błędnie). Minimalny poprawny plik to 9 bajtów (nagłówek).
|
||||||
|
* **kind** — z jakiego formatu tekstowego ten twin został skompilowany (tabela wyżej).
|
||||||
|
* **flags bit0 (`infra-relevant`)** — ustawiony, jeśli plik ma co najmniej jeden węzeł
|
||||||
|
infrastruktury lub jakikolwiek `include`. Czysto wizualny liść (np. `.incb` z florą: same
|
||||||
|
kształty + dyrektywy transformacji) ma go wyzerowany, więc pass infrastrukturalny może w ogóle
|
||||||
|
nie otwierać pliku, zamiast przechodzić go po to, żeby odrzucić jego treść wizualną.
|
||||||
|
* **tablica stringów** — każdy odrębny tekst tokenu jest internowany raz i referowany dalej
|
||||||
|
przez swój zerobazowy indeks. Stringi mają prefiks długości (bez terminatora NUL), więc reader
|
||||||
|
może serwować `std::string_view` wprost w bufor.
|
||||||
|
|
||||||
|
## 4. Kodowania prymitywów
|
||||||
|
|
||||||
|
* **varint** — LEB128 bez znaku: 7 bitów danych na bajt, najwyższy bit = „kolejny bajt
|
||||||
|
następuje". Jeden bajt obejmuje wartości < 128 — to przypadek typowy dla indeksów tablicy
|
||||||
|
stringów.
|
||||||
|
* **zig-zag** — liczby ze znakiem są mapowane na bez znaku przed varintem
|
||||||
|
(`(n << 1) ^ (n >> 63)`), więc małe wartości ujemne pozostają krótkie.
|
||||||
|
* **f32 / f64** — IEEE-754, little-endian (4 / 8 bajtów), zgodnie z `sn_utils`.
|
||||||
|
|
||||||
|
Wszystkie skalary wielobajtowe są little-endian.
|
||||||
|
|
||||||
|
## 5. Strumień wpisów
|
||||||
|
|
||||||
|
Każdy wpis zaczyna się od **head varint**. Jego dolne 3 bity to tag (`TAG_MASK = 0x7`); pozostałe
|
||||||
|
górne bity (`head >> 3`) niosą ładunek wpisów-tokenów (indeks w tablicy stringów). Przypadek
|
||||||
|
typowy — token — kosztuje więc tylko ten jeden varint.
|
||||||
|
|
||||||
|
| wartość tagu | nazwa | ładunek po head |
|
||||||
|
|--------------|-----------|-------------------------------------------------------------------|
|
||||||
|
| 0 | `TOKEN` | brak — `head >> 3` to indeks w tablicy stringów. Małymi literami per-konsument przy odtwarzaniu. |
|
||||||
|
| 1 | `INCLUDE` | `varint(fileexpr_count)` + tyle `varint(string_idx)`, potem `varint(param_count)` + tyle `varint(string_idx)`. |
|
||||||
|
| 2 | `INT` | jeden zig-zag varint (wartość całkowita ze znakiem). |
|
||||||
|
| 3 | `F32` | 4 bajty, float little-endian. |
|
||||||
|
| 4 | `F64` | 8 bajtów, double little-endian. |
|
||||||
|
| 5 | `QTOKEN` | brak — `head >> 3` to indeks w tablicy stringów. **Token cytowany: case zachowany dosłownie** przy odtwarzaniu (nigdy nie zmniejszany). |
|
||||||
|
| 6 | `NODE` | marker węzła — patrz §6. Po nim następuje ciało węzła. |
|
||||||
|
|
||||||
|
Logiczne typy wpisów widziane przez konsumenta (`scenery_entry_type`): `token`, `qtoken`,
|
||||||
|
`number` (dowolny z INT/F32/F64), `include`. Podział INT/F32/F64 to szczegół kodowania ukryty
|
||||||
|
wewnątrz readera/writera.
|
||||||
|
|
||||||
|
### Kodowanie liczb (`write_number`)
|
||||||
|
|
||||||
|
Token liczbowy zapisywany jest w najbardziej zwartej formie, która wystarczająco dobrze
|
||||||
|
round-trip'uje:
|
||||||
|
|
||||||
|
1. Wartość **całkowita** w zakresie ±2⁵³ (`±9007199254740992`) → `INT` (zig-zag varint). Pokrywa
|
||||||
|
wiele małych liczb w jednym–dwóch bajtach.
|
||||||
|
2. W przeciwnym razie **`F32`**, jeśli float jest skończony i reprezentuje wartość albo dokładnie,
|
||||||
|
albo z błędem względnym ≤ `1e-6` (`|float(v) − v| ≤ 1e-6·|v|`).
|
||||||
|
3. W przeciwnym razie pełny **`F64`**.
|
||||||
|
|
||||||
|
### Wpisy include
|
||||||
|
|
||||||
|
Include zapisuje **dosłowne wyrażenie nazwy pliku** (pojedynczy token albo zbiór losowy, np.
|
||||||
|
`[ a.inc b.inc ]` — z tokenami nawiasów) plus listę parametrów, wszystko jako indeksy
|
||||||
|
zinternowanych stringów. Przy odtwarzaniu wyrażenie jest re-ewaluowane (z ponownym losowaniem
|
||||||
|
zbioru losowego), a plik-dziecko wchodzony przezroczyście; własne tokeny dziecka pochodzą z jego
|
||||||
|
twina (lub tekstu). To właśnie zachowuje `cParser::Name()`, grupowanie węzłów per-`.inc` oraz
|
||||||
|
substytucję parametrów `(pN)` dokładnie jak ścieżka tekstowa.
|
||||||
|
|
||||||
|
## 6. Markery węzłów (progresywne ładowanie)
|
||||||
|
|
||||||
|
Każdy **węzeł najwyższego poziomu** (blok `node … end<typ>`; węzły nigdy nie są zagnieżdżone)
|
||||||
|
jest opakowany w marker `NODE`, żeby reader mógł serwować lub pomijać cały węzeł per-pass.
|
||||||
|
Układ markera:
|
||||||
|
|
||||||
|
```
|
||||||
|
varint(TAG_NODE) # head, tag = 6
|
||||||
|
varint(class) # NODECLASS_* (poniżej)
|
||||||
|
varint(byte_span) # długość w bajtach buforowanego ciała następującego po markerze
|
||||||
|
[ f32 X, f32 Y, f32 Z, f32 Range ] # TYLKO gdy class == NODECLASS_VISUAL_POS (4 floaty)
|
||||||
|
<body> # `byte_span` bajtów: własne wpisy węzła (tokeny/liczby/...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Klasy węzłów (`NODECLASS_*`):
|
||||||
|
|
||||||
|
| wartość | nazwa | znaczenie |
|
||||||
|
|---------|----------------|------------------------------------------------------------------------|
|
||||||
|
| 0 | `INFRA` | węzeł infrastruktury (tor, trakcja, memcell, event, dźwięk, …). |
|
||||||
|
| 1 | `VISUAL` | węzeł wizualny bez pozycji w markerze (kształty / starsze twiny). |
|
||||||
|
| 2 | `VISUAL_POS` | wizualny węzeł modelu, którego marker niesie pozycję lokalną **oraz zasięg widoczności** — 4 f32: `X, Y, Z, Range` (`Range` = `range_max`, `-1` = nieograniczony). |
|
||||||
|
|
||||||
|
> Uwaga: komentarz w kodzie przy `NODECLASS_VISUAL_POS` wciąż mówi „3 f32"; writer realnie
|
||||||
|
> emituje **4** floaty (X, Y, Z, Range). Na dysku jest układ 4-floatowy powyżej.
|
||||||
|
|
||||||
|
`Range`/pozycja pozwalają ładowaniu wizualnemu sterowanemu odległością od kamery („ring")
|
||||||
|
**przetestować dystans i pominąć węzeł w O(1)** — bez dekodowania żadnego z jego tokenów — oraz
|
||||||
|
budować eagerly modele dalekie, ale o dużym zasięgu (range poza promieniem strumieniowania),
|
||||||
|
zamiast porzucać je na dystansie. Kształty (triangles/lines) nie mają pozycji w markerze i
|
||||||
|
spadają do testu sekcyjnego w deserializerze.
|
||||||
|
|
||||||
|
## 7. Passy ładowania (`scenery_load_pass`)
|
||||||
|
|
||||||
|
Readerowi mówi się, którą klasę węzłów serwować; `next()` pomija markery węzłów i ciała węzłów
|
||||||
|
spoza passu (przesuwając kursor o `byte_span` w O(1)), aż dojdzie do wpisu do zaserwowania.
|
||||||
|
**Wpisy nie-węzłowe (dyrektywy, include'y) są serwowane zawsze**, w każdym passie, by utrzymać
|
||||||
|
spójny stan transformacji/grup.
|
||||||
|
|
||||||
|
| pass | serwuje |
|
||||||
|
|------------------|-------------------------------------------------------------------|
|
||||||
|
| `all` (0) | wszystko (ładowanie jednoprzebiegowe, == zachowanie sprzed v6). |
|
||||||
|
| `infrastructure` | dyrektywy/include + węzły `INFRA`; węzły wizualne pominięte. |
|
||||||
|
| `visual` (2) | dyrektywy/include + węzły `VISUAL`/`VISUAL_POS`; infra pominięta. |
|
||||||
|
|
||||||
|
Ten sam twin jest otwierany raz, potem przechodzony ponownie: najpierw infrastruktura eagerly
|
||||||
|
(gra może ruszyć), potem wizuale strumieniowane po sekcjach kamery.
|
||||||
|
|
||||||
|
## 8. Powierzchnia API readera (semantyka)
|
||||||
|
|
||||||
|
`scenery_binary_reader` (bufor musi przeżyć reader):
|
||||||
|
|
||||||
|
* `open(string_view)` — waliduje nagłówek, indeksuje tablicę stringów, ustawia na pierwszym
|
||||||
|
wpisie.
|
||||||
|
* `set_pass(pass)` / `next(out)` — strumieniuje kolejny serwowany wpis (§7).
|
||||||
|
* `node_position(x, y, z, range)` — pozycja lokalna + `range_max` aktualnie serwowanego węzła,
|
||||||
|
jeśli marker je niósł (`VISUAL_POS`); w przeciwnym razie false.
|
||||||
|
* `skip_to_node_end()` — przeskakuje kursor na koniec aktualnie serwowanego węzła (O(1)), by
|
||||||
|
porzucić węzeł po podejrzeniu tylko jego pierwszych wpisów (ring skip kamery).
|
||||||
|
* `node_offset()` / `seek_node(offset)` — bajtowy offset markera bieżącego węzła oraz
|
||||||
|
repozycjonowanie tam, żeby kolejne `next()` zaserwowało ten węzeł (odbudowa jednego węzła na
|
||||||
|
żądanie bez re-skanowania twina).
|
||||||
|
* `infra_relevant()` — flaga z nagłówka (§3); pozwala passowi infrastrukturalnemu w ogóle nie
|
||||||
|
otwierać czysto wizualnych liści.
|
||||||
|
* `progress()` — ułamek skonsumowanej sekcji wpisów (0..100), dla paska ładowania.
|
||||||
|
|
||||||
|
Odczyt jest **kontrolowany pod kątem granic**: przy przekroczeniu kursor parkuje na końcu i
|
||||||
|
zwracane jest zero, więc obcięty twin dekoduje się do pustki zamiast czytać poza bufor.
|
||||||
|
|
||||||
|
## 9. Wersjonowanie
|
||||||
|
|
||||||
|
`SCENERYBINARY_MAGIC` osadza wersję w 4. bajcie magic (obecnie `10`). Podbicie unieważnia
|
||||||
|
wszystkie starsze twiny (nie przechodzą `open()` i są rekompilowane) — tak bezpiecznie wdraża
|
||||||
|
się zmiany formatu. Skrócona historia (szczegóły w nagłówku):
|
||||||
|
|
||||||
|
* **v4** — reader strumieniowy z bufora, spakowane tagi wpisów, liczby całkowite jako zig-zag
|
||||||
|
varint.
|
||||||
|
* **v5** — tokeny w oryginalnym case + flaga cytowania, więc baking jest niezależny od gramatyki
|
||||||
|
(umożliwia headless/standalone baker).
|
||||||
|
* **v6** — węzły najwyższego poziomu opakowane w marker (klasa + byte span) → progresywne
|
||||||
|
ładowanie.
|
||||||
|
* **v7** — marker modelu wizualnego niesie też pozycję lokalną modelu.
|
||||||
|
* **v8+** — poprawka terminatora eventlauncher; marker niesie też zasięg widoczności modelu
|
||||||
|
(`range_max`), dając obecny 4-floatowy ładunek `VISUAL_POS`. Aktualna wersja na dysku: **10**.
|
||||||
|
|
||||||
|
## 10. Baking
|
||||||
|
|
||||||
|
Serializacja i zapis twina to praca samodzielna, zdejmowana ze ścieżki budowy sceny:
|
||||||
|
|
||||||
|
* **Asynchronicznie** (`scenerybinary_write_async` / `scenerybinary_wait_all`) — podczas
|
||||||
|
normalnego ładowania tekstowego gotowe writery trafiają do małej puli wątków, więc I/O nakłada
|
||||||
|
się na resztę budowy sceny; wyniki logowane są na wątku głównym po zakończeniu ładowania.
|
||||||
|
* **Headless** (`scenerybinary_bake_headless`, tryb linii poleceń `-bake`) — tokenizuje
|
||||||
|
scenariusz i, rekurencyjnie, każdy dołączany plik, kompilując każdy twin **bez sceny, renderera
|
||||||
|
i okna**. Graf plików odkrywany jest przez tokenizację każdego pliku w izolacji (include'y
|
||||||
|
zapisane, ale nieotwierane), a każdy twin kompilowany na wątku roboczym; każdy plik bakowany
|
||||||
|
jest dokładnie raz (deduplikacja), więc zapisy twinów nigdy się nie ścigają. Świeże twiny są
|
||||||
|
nietknięte; rekompilowane są tylko brakujące/nieaktualne.
|
||||||
|
|
||||||
|
## 11. Kto używa maszynerii odtwarzania
|
||||||
|
|
||||||
|
Tylko deserializer scenerii (`simulation/simulationstateserializer.cpp`) steruje API
|
||||||
|
replay/pass/streaming binarnego twina. Każdy inny konsument `cParser` (modele, eventy, pojazdy,
|
||||||
|
audio, config, …) korzysta ze zwykłego tokenizera tekstu i nie wie nic o formacie binarnym.
|
||||||
@@ -40,6 +40,10 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) {
|
|||||||
// its sections, which those chunks referenced) is destroyed below
|
// its sections, which those chunks referenced) is destroyed below
|
||||||
EditorTerrain.reset();
|
EditorTerrain.reset();
|
||||||
|
|
||||||
|
// groups hold raw pointers into the region; wipe them before the region goes away so a reload
|
||||||
|
// cannot walk dangling nodes in update_map() / erase()
|
||||||
|
scene::Groups.clear();
|
||||||
|
|
||||||
// TODO: move initialization to separate routine so we can reuse it
|
// TODO: move initialization to separate routine so we can reuse it
|
||||||
SafeDelete( Region );
|
SafeDelete( Region );
|
||||||
Region = new scene::basic_region();
|
Region = new scene::basic_region();
|
||||||
|
|||||||
@@ -549,13 +549,6 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
|
|||||||
// na przechyłce doliczyć jeszcze pół przechyłki
|
// na przechyłce doliczyć jeszcze pół przechyłki
|
||||||
}
|
}
|
||||||
|
|
||||||
// length2 is better than length for comparing because it does not require sqrt function
|
|
||||||
if( glm::length2((p1 + p1 + p2) / 3.0 - p1 - cp1) < sq(0.02)
|
|
||||||
|| glm::length2((p1 + p2 + p2) / 3.0 - p2 + cp1) < sq(0.02) ) {
|
|
||||||
// "prostowanie" prostych z kontrolnymi, dokładność 2cm
|
|
||||||
cp1 = cp2 = glm::dvec3(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if( fRadius != 0 ) {
|
if( fRadius != 0 ) {
|
||||||
// gdy podany promień
|
// gdy podany promień
|
||||||
segsize =
|
segsize =
|
||||||
@@ -644,16 +637,6 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
|
|||||||
// na przechyłce doliczyć jeszcze pół przechyłki?
|
// na przechyłce doliczyć jeszcze pół przechyłki?
|
||||||
}
|
}
|
||||||
|
|
||||||
if( eType != tt_Cross ) {
|
|
||||||
// dla skrzyżowań muszą być podane kontrolne
|
|
||||||
// length2 is better than length for comparing because it does not require sqrt function
|
|
||||||
if( glm::length2(( p1 + p1 + p2 ) / 3.0 - p1 - cp1 ) < sq(0.02)
|
|
||||||
|| glm::length2(( p1 + p2 + p2 ) / 3.0 - p2 + cp1 ) < sq(0.02) ) {
|
|
||||||
// "prostowanie" prostych z kontrolnymi, dokładność 2cm
|
|
||||||
cp1 = cp2 = glm::dvec3(0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( fRadiusTable[ 0 ] != 0 ) {
|
if( fRadiusTable[ 0 ] != 0 ) {
|
||||||
// gdy podany promień
|
// gdy podany promień
|
||||||
segsize =
|
segsize =
|
||||||
@@ -709,16 +692,6 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
|
|||||||
// na przechyłce doliczyć jeszcze pół przechyłki?
|
// na przechyłce doliczyć jeszcze pół przechyłki?
|
||||||
}
|
}
|
||||||
|
|
||||||
if( eType != tt_Cross ) {
|
|
||||||
// dla skrzyżowań muszą być podane kontrolne
|
|
||||||
// length2 is better than length for comparing because it does not require sqrt function
|
|
||||||
if( glm::length2((p3 + p3 + p4) / 3.0 - p3 - cp3) < sq(0.02)
|
|
||||||
|| glm::length2((p3 + p4 + p4) / 3.0 - p4 + cp3) < sq(0.02) ) {
|
|
||||||
// "prostowanie" prostych z kontrolnymi, dokładność 2cm
|
|
||||||
cp3 = cp4 = glm::dvec3(0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( fRadiusTable[ 1 ] != 0 ) {
|
if( fRadiusTable[ 1 ] != 0 ) {
|
||||||
// gdy podany promień
|
// gdy podany promień
|
||||||
segsize =
|
segsize =
|
||||||
|
|||||||
Reference in New Issue
Block a user