diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 9e98c813..ee69513c 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -708,12 +708,12 @@ std::vector make_properties() list.back().applies = any; { // the unevenness of the rails (world/TrackUneven.h), mm up to track_uneven::most; with Unset a negative value is -1, the - // component taken from the unevenness - auto const uneven = [&](char const *Key, char const *Label, char const *Tooltip, float TTrack::*Member, bool const Unset, bool const Switches) { + // component taken from the unevenness. Plain, Switches: the kinds of the tracks having it + auto const uneven = [&](char const *Key, char const *Label, char const *Tooltip, float TTrack::*Member, bool const Unset, bool const Plain, bool const Switches) { auto &property{add(list, Key, field::number, section::ride, group::ride_sound, Label, Tooltip)}; property.rebuild = true; - property.applies = [Switches](TTrack const &Track) { - return Track.m_visible && (Track.iCategoryFlag & 15) == kRailCategory && (Track.eType == tt_Normal || (Switches && Track.eType == tt_Switch)); + property.applies = [Plain, Switches](TTrack const &Track) { + return Track.m_visible && (Track.iCategoryFlag & 15) == kRailCategory && ((Plain && Track.eType == tt_Normal) || (Switches && Track.eType == tt_Switch)); }; property.get = [Member](TTrack const &Track) { return decimal(Track.*Member, 1); }; property.set = [Member, Unset](TTrack &Track, std::string const &Text) { @@ -727,11 +727,14 @@ std::vector make_properties() return true; }; }; - uneven("uneven", "Unevenness (mm)", "Peak deviation of a rail on a 10 m chord. Id-14 limits: 6 mm at 160 km/h, 10 at 120, 18 at 80, 35 at 40.\nWith a sound distance set, the rails also dip at the joints. Joined switches use the lowest of their values", &TTrack::m_uneven, false, true); - uneven("unevenup", "Up and down (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenup, true, true); - uneven("unevenside", "Sideways (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenside, true, true); - uneven("unevencross", "Cross level (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevencross, true, false); - uneven("jointdip", "Dip at rail joints (mm)", "Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness", &TTrack::m_jointdip, true, false); + uneven("uneven", "Unevenness (mm)", "Peak deviation of a rail on a 10 m chord. Id-14 limits: 6 mm at 160 km/h, 10 at 120, 18 at 80, 35 at 40.\nWith a sound distance set, the rails also dip at the joints. Joined switches use the lowest of their values", &TTrack::m_uneven, false, true, true); + uneven("unevenup", "Up and down (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenup, true, true, true); + uneven("unevenside", "Sideways (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenside, true, true, true); + uneven("unevencross", "Cross level (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevencross, true, true, false); + uneven("unevenbranch", "Diverging path (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenbranch, true, false, true); + uneven("unevenbranchup", "Diverging, up and down (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenbranchup, true, false, true); + uneven("unevenbranchside", "Diverging, sideways (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenbranchside, true, false, true); + uneven("jointdip", "Dip at rail joints (mm)", "Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness", &TTrack::m_jointdip, true, true, false); } { auto &property{add(list, "quality", field::number, section::ride, group::ride_sound, "Quality", "Quality of the track, 20 is the nominal one; lower values give a rougher ride")}; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index a4e17a9b..7f4b807e 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -252,6 +252,9 @@ editor_track::state editor_track::capture(TTrack const &Track) result.unevenup = Track.m_unevenup; result.unevenside = Track.m_unevenside; result.unevencross = Track.m_unevencross; + result.unevenbranch = Track.m_unevenbranch; + result.unevenbranchup = Track.m_unevenbranchup; + result.unevenbranchside = Track.m_unevenbranchside; result.jointdip = Track.m_jointdip; result.sleepers = Track.m_sleeper_enabled; result.sleeper_frequency = Track.m_sleeper_frequency; @@ -364,6 +367,9 @@ void editor_track::apply(TTrack &Track, state const &State) Track.m_unevenup = State.unevenup; Track.m_unevenside = State.unevenside; Track.m_unevencross = State.unevencross; + Track.m_unevenbranch = State.unevenbranch; + Track.m_unevenbranchup = State.unevenbranchup; + Track.m_unevenbranchside = State.unevenbranchside; Track.m_jointdip = State.jointdip; Track.m_sleeper_enabled = State.sleepers; Track.m_sleeper_frequency = State.sleeper_frequency; @@ -3235,16 +3241,7 @@ void write_ending(std::ostream &Text, TTrack const &Style) { if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) Text << "railprofile " << Style.m_profile1.first << ' '; - if (Style.m_uneven > 0.f) - Text << "uneven " << Style.m_uneven << ' '; - if (Style.m_unevenup >= 0.f) - Text << "unevenup " << Style.m_unevenup << ' '; - if (Style.m_unevenside >= 0.f) - Text << "unevenside " << Style.m_unevenside << ' '; - if (Style.m_unevencross >= 0.f) - Text << "unevencross " << Style.m_unevencross << ' '; - if (Style.m_jointdip >= 0.f) - Text << "jointdip " << Style.m_jointdip << ' '; + Text << Style.uneven_text(); // a piece of the track made from another one stays in its track circuit and under the same overhead line for (auto const *isolated : Style.Isolated) Text << "isolated " << isolated->asName << ' '; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 9b93dc61..1cdcf96e 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -67,6 +67,9 @@ class editor_track float unevenup{-1.f}; float unevenside{-1.f}; float unevencross{-1.f}; + float unevenbranch{-1.f}; + float unevenbranchup{-1.f}; + float unevenbranchside{-1.f}; float jointdip{-1.f}; bool sleepers{false}; float sleeper_frequency{0.6f}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 8ca13b46..2aeaefec 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -562,6 +562,15 @@ msgstr "W poziomie (mm)" msgid "Cross level (mm)" msgstr "Przechyłka (mm)" +msgid "Diverging path (mm)" +msgstr "Tor na bok (mm)" + +msgid "Diverging, up and down (mm)" +msgstr "Na bok, w pionie (mm)" + +msgid "Diverging, sideways (mm)" +msgstr "Na bok, w poziomie (mm)" + msgid "Set apart from the unevenness, mm. Negative: the unevenness" msgstr "Osobno od nierówności, mm. Ujemna: wartość nierówności" diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index afea0b67..22970fa6 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -140,6 +140,43 @@ TEST_CASE("uneven track: the field stays within its bound") CHECK(highest > 0.5); } +TEST_CASE("uneven track: all the values in one attribute") +{ + auto const one{track_uneven::read_fields<6>("18")}; + CHECK(one[0] == doctest::Approx(18.f)); + CHECK(one[1] < 0.f); + auto const both{track_uneven::read_fields<6>("14;35")}; + CHECK(both[0] == doctest::Approx(14.f)); + CHECK(both[1] == doctest::Approx(35.f)); + CHECK(both[2] < 0.f); + auto const gaps{track_uneven::read_fields<6>("18.5;;;25;6")}; + CHECK(gaps[0] == doctest::Approx(18.5f)); + CHECK(gaps[1] < 0.f); + CHECK(gaps[2] < 0.f); + CHECK(gaps[3] == doctest::Approx(25.f)); + CHECK(gaps[4] == doctest::Approx(6.f)); + auto const bounds{track_uneven::read_fields<6>("500;-3;x")}; + CHECK(bounds[0] == doctest::Approx(track_uneven::most)); + CHECK(bounds[1] == doctest::Approx(0.f)); + CHECK(bounds[2] < 0.f); + CHECK(track_uneven::write_fields<6>({14.f, 35.f, -1.f, -1.f, -1.f, -1.f}) == "14;35"); + CHECK(track_uneven::write_fields<6>({18.5f, -1.f, -1.f, 25.f, 6.f, -1.f}) == "18.5;;;25;6"); + CHECK(track_uneven::write_fields<6>({0.f, -1.f, 30.f, -1.f, -1.f, -1.f}) == "0;;30"); + CHECK(track_uneven::write_fields<6>({18.f, -1.f, -1.f, -1.f, -1.f, -1.f}) == "18"); +} + +TEST_CASE("uneven track: amounts take the lowest of each component") +{ + auto const least{track_uneven::least({14.0, 9.0, 0.0}, {7.0, 35.0, 3.0})}; + CHECK(least.up == doctest::Approx(7.0)); + CHECK(least.side == doctest::Approx(9.0)); + CHECK(least.cross == doctest::Approx(0.0)); + // the diverging path of a switch: from the common end at the lowest to its own value at the far end + double const length{30.0}; + CHECK(track_uneven::along(35.0, 7.0, 35.0, 0.0, length) == doctest::Approx(7.0)); + CHECK(track_uneven::along(35.0, 7.0, 35.0, length, length) == doctest::Approx(35.0)); +} + TEST_CASE("uneven track: the value along a track meets its neighbours exactly") { double const length{500.0}; @@ -165,6 +202,18 @@ TEST_CASE("uneven track: the value along a track meets its neighbours exactly") } } +TEST_CASE("uneven track: the foot of the ballast stays where it was") +{ + double const rail{0.18}; + double const height{0.38}; + CHECK(track_uneven::bed_share(0.0, rail, height) == doctest::Approx(1.0)); // rail top + CHECK(track_uneven::bed_share(rail, rail, height) == doctest::Approx(1.0)); // top of the ballast + CHECK(track_uneven::bed_share(rail + 0.5 * height, rail, height) == doctest::Approx(0.5)); + CHECK(track_uneven::bed_share(rail + height, rail, height) == doctest::Approx(0.0)); // its foot + CHECK(track_uneven::bed_share(rail + 2.0 * height, rail, height) == doctest::Approx(0.0)); + CHECK(track_uneven::bed_share(rail + 1.0, rail, 0.0) == doctest::Approx(1.0)); // no ballast +} + TEST_CASE("uneven track: the rails dip around a joint") { CHECK(track_uneven::dip(0.0) == doctest::Approx(1.0)); diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py new file mode 100644 index 00000000..eb39d7c6 --- /dev/null +++ b/tools/track_uneven_grade.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +"""Grades the tracks of any scenery for the uneven attribute, from what the scenery shows: textures, grass on the track, +curves, speed and road crossings. The originals stay as they are, graded copies of the files with tracks go to --out. + + python track_uneven_grade.py scenery/td.scn + python track_uneven_grade.py scenery/baltyk_skm1.scn --out graded + python track_uneven_grade.py scenery/baltyk_skm1.scn --out graded --route -107,2300 -4120,2491 --straight tor0290,tor0906 + python track_uneven_grade.py scenery/mytracks.scm --root scenery --out graded --gain 1.2 + +With no --out it only reports. See track_uneven_doc.md for the attribute. Python 3.8 or later, no other packages. +""" +import argparse +import collections +import heapq +import math +import pathlib +import re +import sys +import zlib + +# PLK Id-14 appendix 1, class C: vertical limit in mm by speed in km/h +LIMITS = [(20, 50), (30, 40), (40, 35), (50, 30), (60, 25), (70, 21), (80, 18), (90, 16), (100, 14), (110, 12), (120, 10), (130, 9), (140, 8), (150, 7), (160, 6)] +# how worn a texture looks, 0-1: the first pattern found in its name counts, anything else is 0.5 +BALLAST_WEAR = [("grass|traw", 0.95), ("old|star", 0.9), ("oil|smar", 0.6), ("new|nowa", 0.0), ("bps|beton|conc|tpb", 0.2), ("drew|wood|tpd", 0.45)] +RAIL_WEAR = [("unused2", 1.0), ("unused", 0.85), ("rused2", 0.8), ("rused", 0.65), ("used2", 0.45), ("used", 0.2), ("new", 0.0), ("old|rust", 0.8)] +WOOD = re.compile("drew|wood|tpd") +GRASS = re.compile("grass|traw|chwast|weed") +NO_SPEED = 40.0 # km/h, a stretch with no velocity anywhere: station and side tracks +SWITCH_MOST = 35.0 # the limit of 40 km/h +DIVERGING = 40.0 # km/h through the diverging path +MOST, MOST_UP = 50.0, 60.0 # of uneven and of its vertical part +REACH = 0.3 # how near the ends of tracks count as joined, m +SHORT = 2.0 # the pieces inside a slip don't tell the quality of the line +PIECE = 100.0 # a track without a speed shorter than this can take it through a switch, m +GRASS_NEAR, GRASS_FULL = 2.5, 14.0 # grass this near the middle grows on the track; plants per 100 m of an overgrown one +NEAR_LEAST, NEAR_MOST = 0.5, 0.95 # how far a chosen track goes towards the limit +CURVE_STEP = 1.0 / 2500.0 # a difference of curvature that counts as a change, 1/m +RISE = 0.0273 * 4.5 # how fast pressure lifts a pantograph, m/s, by TDynamicObject::Update +GAP_MOST = 0.006 # the gap to the wire allowed; at 0.01 m the voltage goes and the main breaker trips +OVERSPEED = 1.1 # a driver can go this much over the speed of the track +# as up_waves and the dip of the joints in world/TrackUneven.h +STRAY_PER_MM, DIP_SHARE, DIP_REACH = 0.00145, 0.4, 3.0 +GOLDEN = math.pi * (3.0 - math.sqrt(5.0)) +UP_WAVES = [(2 * math.pi / length * math.cos(GOLDEN * i), 2 * math.pi / length * math.sin(GOLDEN * i), GOLDEN * (i + 1), share) for i, (length, share) in enumerate(zip((8.3, 11.9, 16.7, 23.9, 37.1, 47.3, 61.9, 83.3), (.14, .19, .22, .22, .23, .23, .23, .23)))] +TOKEN = re.compile(r"//[^\n]*|\S+") +NUMBER = re.compile(r"^[-+]?\d*\.?\d+([eE][-+]?\d+)?$") + + +def limit(speed): + if speed <= LIMITS[0][0]: + return float(LIMITS[0][1]) + for (v0, n0), (v1, n1) in zip(LIMITS, LIMITS[1:]): + if speed <= v1: + return n0 + (n1 - n0) * (speed - v0) / (v1 - v0) + return float(LIMITS[-1][1]) + + +def wear(name, table): + name = (name or "").lower() + for pattern, value in table: + if re.search(pattern, name): + return value + return 0.5 + + +def hashed(salt, name): + return zlib.crc32(salt + name.encode("latin-1", "replace")) / 0xffffffff + + +class Scenery: + """All that's found by following the includes from the given files.""" + + def __init__(self, root): + self.root = root + self.tracks, self.roads, self.plants = [], [], collections.defaultdict(list) + self.seen = set() + self.notes = collections.Counter() + self.texts = {} + + def read(self, path, offset=(0.0, 0.0, 0.0)): + path = path.resolve() + if path in self.seen: + return + self.seen.add(path) + text = path.read_bytes().decode("latin-1") + self.texts[path] = text + matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")] + words = [m.group(0) for m in matches] + lower = [w.lower() for w in words] + origins = [offset] + i = 0 + while i < len(words): + word = lower[i] + if word == "include" and i + 1 < len(words): + end = i + 1 + while end < len(words) and lower[end] != "end": + end += 1 + self.include(words[i + 1], words[i + 2:end], origins[-1]) + i = end + elif word == "origin" and i + 3 < len(words) and all(NUMBER.match(w) for w in words[i + 1:i + 4]): + origins.append(tuple(o + float(w) for o, w in zip(origins[-1], words[i + 1:i + 4]))) + i += 3 + elif word == "endorigin" and len(origins) > 1: + origins.pop() + elif word == "rotate" and i + 3 < len(words) and any(w.strip("0.-") for w in words[i + 1:i + 4]): + self.notes["rotate"] += 1 + elif word == "node" and i + 4 < len(words): + kind = lower[i + 4] + if kind == "track": + i = self.track(path, matches, words, lower, i, origins[-1]) + elif kind == "model" and i + 9 < len(words) and GRASS.search(lower[i + 9]) and all(NUMBER.match(w) for w in words[i + 5:i + 8]): + self.plant(float(words[i + 5]) + origins[-1][0], float(words[i + 7]) + origins[-1][2]) + i += 1 + + def include(self, name, params, origin): + path = self.root / name.replace("\\", "/") + if GRASS.search(name.lower()): + numbers = [float(w) for w in params if NUMBER.match(w)] + if len(numbers) >= 3: + self.plant(numbers[0] + origin[0], numbers[2] + origin[2]) + return + if not path.is_file(): + self.notes["brak pliku " + name] += 1 + return + if params: + # the same file can come many times with other parameters: its tracks can't be told apart in one copy + if path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I): + self.notes["pliki z parametrami, tory pominiete"] += 1 + return + self.read(path, origin) + + def plant(self, x, z): + self.plants[(round(x / 10), round(z / 10))].append((x, z)) + + def track(self, path, matches, words, lower, i, origin): + at = i + 4 + end = at + while end < len(words) and lower[end] != "endtrack": + end += 1 + if end >= len(words) or at + 10 >= end: + return i + kind = lower[at + 1] + if kind not in ("normal", "switch", "road"): + return end + cursor = at + 10 + rail = ballast = None + if lower[at + 9] == "vis": + rail, ballast = lower[cursor], lower[cursor + 2] + cursor += 6 + paths = [] + for _ in range(2 if kind == "switch" else 1): + values = words[cursor:cursor + 15] + if len(values) < 15 or not all(NUMBER.match(v) for v in values): + self.notes["tory z parametrami, pominiete"] += 1 + return end + n = [float(v) for v in values] + part = dict(p1=[n[j] + origin[j] for j in range(3)], c1=n[4:7], c2=n[7:10], p2=[n[10 + j] + origin[j] for j in range(3)], radius=abs(n[14])) + part["line"] = line_of(part) + part["length"] = sum(math.dist(a, b) for a, b in zip(part["line"], part["line"][1:])) + paths.append(part) + cursor += 15 + if kind == "road": + self.roads.append(paths[0]) + return end + block = lower[at:end] + taken = any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip")) + velocity = float(words[at + block.index("velocity") + 1]) if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]) else 0.0 + sound = float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0 + self.tracks.append(dict(name=words[i + 3], kind=kind, rail=rail, ballast=ballast, sound=sound, paths=paths, speed=velocity if velocity > 0 else None, source="wpis" if velocity > 0 else None, file=path, offset=matches[end].start(), taken=taken)) + return end + + +def bezier(part, t): + a = [part["p1"][i] + part["c1"][i] for i in range(3)] + b = [part["p2"][i] + part["c2"][i] for i in range(3)] + u = 1 - t + return [u * u * u * part["p1"][i] + 3 * u * u * t * a[i] + 3 * u * t * t * b[i] + t * t * t * part["p2"][i] for i in range(3)] + + +def line_of(part, step=5.0): + if not any(part["c1"]) and not any(part["c2"]): + return [list(part["p1"]), list(part["p2"])] + count = max(4, int(math.dist(part["p1"], part["p2"]) / step)) + return [bezier(part, i / count) for i in range(count + 1)] + + +def plan_radius(line): + least = math.inf + for a, b, c in zip(line, line[1:], line[2:]): + area = abs((b[0] - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (b[2] - a[2])) / 2 + if area > 1e-6: + least = min(least, math.dist((a[0], a[2]), (b[0], b[2])) * math.dist((b[0], b[2]), (c[0], c[2])) * math.dist((c[0], c[2]), (a[0], a[2])) / (4 * area)) + return least + + +def end_curve(part, key): + line = part["line"] + if len(line) < 3: + return 1.0 / part["radius"] if part["radius"] > 0 else 0.0 + a, b, c = line[:3] if key == "p1" else line[-3:] + sides = math.dist(a, b) * math.dist(b, c) * math.dist(c, a) + return 2.0 * ((b[0] - a[0]) * (c[2] - b[2]) - (b[2] - a[2]) * (c[0] - b[0])) / sides if sides > 0 else 0.0 + + +class Joints: + def __init__(self, tracks): + self.ends = collections.defaultdict(list) + for t in tracks: + for part in t["paths"]: + for key in ("p1", "p2"): + self.ends[(round(part[key][0]), round(part[key][2]))].append((t, part, key)) + + def at(self, point): + found = [] + for cx in (round(point[0]) - 1, round(point[0]), round(point[0]) + 1): + for cz in (round(point[2]) - 1, round(point[2]), round(point[2]) + 1): + for item in self.ends.get((cx, cz), ()): + if abs(item[1][item[2]][0] - point[0]) <= REACH and abs(item[1][item[2]][2] - point[2]) <= REACH: + found.append(item) + return found + + +def settle_speeds(tracks, joints): + # from the nearest track on the stretch between switches, a short piece also along the straight path of a switch + heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) + while heads: + t = heads.popleft() + for key in ("p1", "p2"): + for other, _, _ in joints.at(t["paths"][0][key]): + if other is not t and other["kind"] == "normal" and other["speed"] is None: + other["speed"], other["source"] = t["speed"], "sasiad" + heads.append(other) + heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) + while heads: + t = heads.popleft() + for key in ("p1", "p2"): + points, seen = [t["paths"][0][key]], set() + for _ in range(6): + onward = [] + for point in points: + for other, part, side in joints.at(point): + if other is t or id(part) in seen: + continue + seen.add(id(part)) + if other["kind"] == "switch": + if other["paths"].index(part) != other["bent"]: + onward.append(part["p2" if side == "p1" else "p1"]) + elif other["speed"] is None and other["paths"][0]["length"] < PIECE: + other["speed"], other["source"] = t["speed"], "sasiad" + heads.append(other) + points = onward + for t in tracks: + if t["speed"] is None: + t["speed"], t["source"] = NO_SPEED, "brak" + + +def distances(tracks, joints, station): + best, heap = {}, [] + for t in tracks: + for part in t["paths"]: + for key in ("p1", "p2"): + gap = math.dist((part[key][0], part[key][2]), station) + node = (round(part[key][0], 1), round(part[key][2], 1)) + if gap < 300 and t["kind"] == "normal" and gap < best.get(node, math.inf): + best[node] = gap + heapq.heappush(heap, (gap, node, part[key])) + while heap: + distance, node, point = heapq.heappop(heap) + if distance > best.get(node, math.inf): + continue + for _, part, key in joints.at(point): + other = part["p2" if key == "p1" else "p1"] + onward = (round(other[0], 1), round(other[2], 1)) + if distance + part["length"] < best.get(onward, math.inf): + best[onward] = distance + part["length"] + heapq.heappush(heap, (best[onward], onward, other)) + return best + + +def grass_on(part, plants): + near, count = set(), max(1, int(part["length"] / 3)) + line = part["line"] + for i in range(count + 1): + position = i / count * (len(line) - 1) + low = min(int(position), len(line) - 2) + share = position - low + x, z = line[low][0] + (line[low + 1][0] - line[low][0]) * share, line[low][2] + (line[low + 1][2] - line[low][2]) * share + for cx in (round(x / 10) - 1, round(x / 10), round(x / 10) + 1): + for cz in (round(z / 10) - 1, round(z / 10), round(z / 10) + 1): + for plant in plants.get((cx, cz), ()): + if (plant[0] - x) ** 2 + (plant[1] - z) ** 2 < GRASS_NEAR * GRASS_NEAR: + near.add(plant) + return len(near) * 100.0 / max(part["length"], 5.0) + + +def road_cells(roads): + cells = collections.defaultdict(list) + for road in roads: + for a, b in zip(road["line"], road["line"][1:]): + for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): + for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): + cells[(x, z)].append((a, b)) + return cells + + +def hits_road(part, cells): + for a, b in zip(part["line"], part["line"][1:]): + for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): + for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): + for c, d in cells.get((x, z), ()): + rx, rz, sx, sz = b[0] - a[0], b[2] - a[2], d[0] - c[0], d[2] - c[2] + k = rx * sz - rz * sx + if abs(k) < 1e-9: + continue + t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k + u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k + if 0 <= t <= 1 and 0 <= u <= 1: + return True + return False + + +def heights_per_mm(t): + # the height of the rail under a pantograph frame by frame, per mm of the vertical part + step = t["speed"] * OVERSPEED / 3.6 / 60.0 + heights, done = [], 0.0 + line = t["paths"][0]["line"] + for a, b in zip(line, line[1:]): + length = math.dist(a, b) + count = max(1, int(length / step)) + for i in range(count): + share = i / count + x, z = a[0] + (b[0] - a[0]) * share, a[2] + (b[2] - a[2]) * share + height = STRAY_PER_MM * sum(s * math.sin(wx * x + wz * z + phase) for wx, wz, phase, s in UP_WAVES) + if t["sound"] > 2.0 * DIP_REACH: + phase = math.fmod(done + length * share, t["sound"]) + joint = min(phase, t["sound"] - phase) + if joint < DIP_REACH: + height -= 0.001 * DIP_SHARE * math.cos(math.pi / 2 * joint / DIP_REACH) ** 2 + heights.append(height) + done += length + return heights + + +def largest_gap(heights, up): + gap = worst = 0.0 + climb = RISE / 60.0 + for before, after in zip(heights, heights[1:]): + gap = max(0.0, gap + (before - after) * up - climb) + worst = max(worst, gap) + return worst + + +def ceiling(t, product): + # the worst track of a speed comes up to it and the better ones stay under it, so the state of a track still shows + return product / t["speed"] * (0.7 + 0.3 * min(1.0, max(0.0, (t["state"] - 0.5) / 0.7))) + + +def grade(scenery, options): + tracks = scenery.tracks + joints = Joints(tracks) + for t in tracks: + if t["kind"] == "switch": + radii = [min(plan_radius(p["line"]), p["radius"] if p["radius"] > 0 else math.inf) for p in t["paths"]] + t["bent"] = 1 if len(radii) < 2 or radii[1] <= radii[0] else 0 + settle_speeds(tracks, joints) + route = None + if options.route: + first, last = options.route + from_first, from_last = distances(tracks, joints, first), distances(tracks, joints, last) + ends = [from_last[n] for n in from_last if math.dist(n, first) < 300] + route = (from_first, from_last, min(ends) if ends else math.dist(first, last)) + if not ends: + print("uwaga: po torach nie ma polaczenia miedzy punktami --route, licze w linii prostej") + cells = road_cells(scenery.roads) + normal = [t for t in tracks if t["kind"] == "normal"] + for t in tracks: + part = t["paths"][0] + radius = min(plan_radius(part["line"]), part["radius"] if part["radius"] > 0 else math.inf) + curve = 0.0 if radius >= 6000 else min(1.0, max(0.0, (1200 - radius) / 950)) + density = grass_on(part, scenery.plants) if t["kind"] == "normal" else 0.0 + crossing = t["kind"] == "normal" and hits_road(part, cells) + progress = 0.5 + if route: + shares = [] + for key in ("p1", "p2"): + node = (round(part[key][0], 1), round(part[key][2], 1)) + a, b = route[0].get(node), route[1].get(node) + if a is None or b is None: + a, b = math.dist((part[key][0], part[key][2]), options.route[0]), math.dist((part[key][0], part[key][2]), options.route[1]) + shares.append(min(1.0, max(0.0, (a - b + route[2]) / (2 * route[2])))) + progress = 0.5 * sum(shares) + state = 0.50 + 0.20 * wear(t["ballast"], BALLAST_WEAR) + 0.10 * wear(t["rail"], RAIL_WEAR) + 0.12 * min(1.0, density / GRASS_FULL) + 0.06 * curve + 0.22 * progress + 0.18 * crossing + 0.06 * (hashed(b"", t["name"]) - 0.5) + t.update(state=min(state, 1.2) * options.gain, curve=curve, crossing=crossing, grass=density) + if t["kind"] == "normal": + value = min(limit(t["speed"]) * t["state"], MOST) + t.update(value=value, up=min(value * 1.25, MOST_UP), side=min(value * (0.9 + 0.4 * curve), MOST)) + for t in normal: + t["heights"] = heights_per_mm(t) + # the largest product of the vertical part and the speed with the gap small on every track + low, high = 100, 4000 + while high - low > 25: + middle = (low + high) // 2 + if all(largest_gap(t["heights"], min(t["up"], ceiling(t, middle))) <= GAP_MOST for t in normal): + low = middle + else: + high = middle + product = low + for t in normal: + up = ceiling(t, product) + if up < t["up"]: + ratio = up / t["up"] + t["up"], t["value"], t["side"] = up, t["value"] * ratio, t["side"] * ratio + # on wooden sleepers and where the curve changes, a share of the tracks goes nearer the limit, chosen for good by the place + chosen = 0 + for t in normal: + part = t["paths"][0] + change = abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP + for key in ("p1", "p2"): + for other, theirs, side in joints.at(part[key]): + if other is not t and other["kind"] == "normal": + their = end_curve(theirs, side) + if abs(end_curve(part, key) - (-their if side == key else their)) > CURVE_STEP: + change = True + if not (change or WOOD.search(t["ballast"] or "")): + continue + name = f"{t['name']}:{part['p1'][0]:.0f}:{part['p1'][2]:.0f}" + if hashed(b"near", name) >= options.near: + continue + value = t["value"] + (limit(t["speed"]) - t["value"]) * (NEAR_LEAST + (NEAR_MOST - NEAR_LEAST) * hashed(b"how", name)) + if value <= t["value"]: + continue + up = value + while up > t["up"] and largest_gap(t["heights"], up) > GAP_MOST: + up -= 0.5 + t["value"], t["up"], t["side"] = value, max(up, t["up"]), min(value * (0.9 + 0.4 * t["curve"]), MOST) + chosen += 1 + worst = max((largest_gap(t["heights"], t["up"]) for t in normal), default=0.0) + for t in normal: + del t["heights"] + if t["name"].lower() in options.straight: + t["value"] = t["up"] = t["side"] = 0.0 + # each path of a switch by the plain tracks it leads to, the worst of them; those only among switches from their neighbours + switches = [t for t in tracks if t["kind"] == "switch"] + for t in switches: + t["values"] = [None] * len(t["paths"]) + for index, part in enumerate(t["paths"]): + found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT] + if found: + speed = max(o["speed"] for o in found) + if index == t["bent"]: + speed = min(speed, DIVERGING) + t["values"][index] = min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed) + for _ in range(5): + for t in switches: + for index, part in enumerate(t["paths"]): + if t["values"][index] is None: + found = [o["values"][i] for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "switch" and o is not t for i, q in enumerate(o["paths"]) if q is theirs and o["values"][i] is not None] + if found: + t["values"][index] = max(found) + for t in switches: + t["values"] = [min(SWITCH_MOST, product / NO_SPEED) if v is None else v for v in t["values"]] + return product, chosen, worst + + +def report(scenery, product, chosen, worst): + normal = [t for t in scenery.tracks if t["kind"] == "normal"] + switches = [t for t in scenery.tracks if t["kind"] == "switch"] + print(f"pliki: {len(scenery.seen)}, tory: {len(normal)}, rozjazdy: {len(switches)}, drogi: {len(scenery.roads)}, trawa: {sum(len(v) for v in scenery.plants.values())}") + print(f"pion razy km/h najwyzej {product}, blizej granicy {chosen} torow, najwieksza przerwa do drutu {worst * 100:.2f} cm (bez wznosu samego drutu)") + groups = collections.defaultdict(list) + for t in normal: + groups[(t["source"], t["speed"])].append(t) + print("predkosc skad torow km limit uneven min/mediana/max pion max") + for key in sorted(groups, key=lambda k: (-k[1], k[0])): + rows = groups[key] + values = sorted(t["value"] for t in rows) + print(f"{key[1]:8.0f} {key[0]:8} {len(rows):6} {sum(t['paths'][0]['length'] for t in rows) / 1000:6.1f} {limit(key[1]):6.1f} {values[0]:6.1f} {values[len(values) // 2]:6.1f} {values[-1]:6.1f} {max(t['up'] for t in rows):8.1f}") + if switches: + values = sorted(v for t in switches for v in t["values"]) + print(f"rozjazdy: {values[0]:.1f}-{values[-1]:.1f} mm, mediana {values[len(values) // 2]:.1f}") + for note, count in sorted(scenery.notes.items()): + print(f"uwaga: {note}: {count}" if not note.startswith("brak") else f"uwaga: {note}") + if scenery.notes["rotate"]: + print("uwaga: bloki rotate nie sa uwzgledniane, polaczenia torow z nich moga byc nierozpoznane") + taken = sum(1 for t in scenery.tracks if t["taken"]) + if taken: + print(f"uwaga: {taken} torow ma juz nierownosci, zostaja bez zmian") + + +def attribute(t): + if t["kind"] == "normal": + return f"uneven {t['value']:.1f};{t['up']:.1f};{t['side']:.1f} " if t["value"] > 0 else "" + return "uneven " + ";".join(f"{v:.1f}" for v in t["values"]) + " " + + +def write(scenery, out): + by_file = collections.defaultdict(list) + for t in scenery.tracks: + if not t["taken"]: + by_file[t["file"]].append(t) + written = 0 + for path, tracks in by_file.items(): + text = scenery.texts[path] + pieces, last = [], 0 + for t in sorted(tracks, key=lambda t: t["offset"]): + pieces += [text[last:t["offset"]], attribute(t)] + last = t["offset"] + pieces.append(text[last:]) + target = out / path.relative_to(scenery.root) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes("".join(pieces).encode("latin-1")) + written += 1 + print(f"zapisane pliki: {written}, w {out}") + + +def point(text): + x, z = text.split(",") + return float(x), float(z) + + +def main(): + parser = argparse.ArgumentParser(description="Ocena torow dowolnej scenerii dla atrybutu uneven.") + parser.add_argument("files", nargs="+", type=pathlib.Path, help=".scn, .scm albo .inc; includy sa czytane dalej") + parser.add_argument("--root", type=pathlib.Path, help="folder scenery, wzgledem ktorego ida includy; domyslnie folder pierwszego pliku") + parser.add_argument("--out", type=pathlib.Path, help="folder na kopie plikow z torami; bez tego tylko raport") + parser.add_argument("--route", nargs=2, type=point, metavar="X,Z", help="tory pogarszaja sie od pierwszego punktu do drugiego, liczac po torach") + parser.add_argument("--gain", type=float, default=1.0, help="mnoznik stanu torow, domyslnie 1") + parser.add_argument("--near", type=float, default=0.6, help="jaka czesc torow na drewnie i przy zmianach krzywizny idzie blizej granicy Id-14, domyslnie 0.6") + parser.add_argument("--straight", default="", help="nazwy torow bez nierownosci, po przecinku") + options = parser.parse_args() + options.straight = {name.strip().lower() for name in options.straight.split(",") if name.strip()} + root =(options.root or options.files[0].parent).resolve() + if options.out: + options.out = options.out.resolve() + if options.out == root or root in options.out.parents: + sys.exit("--out nie moze lezec w folderze scenerii: oryginaly maja zostac nietkniete") + scenery = Scenery(root) + for path in options.files: + scenery.read(path) + if not scenery.tracks: + sys.exit("nie znaleziono torow") + product, chosen, worst = grade(scenery, options) + report(scenery, product, chosen, worst) + if options.out: + write(scenery, options.out) + + +if __name__ == "__main__": + main() diff --git a/track_uneven_doc.md b/track_uneven_doc.md index aca9ca32..325e8944 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -1,6 +1,6 @@ # Nierówności toru -Opcjonalne atrybuty wpisu toru, przed `endtrack`: +Opcjonalny atrybut wpisu toru, przed `endtrack`: ``` node -1 0 tor1 track normal 100.0 1.435 0.15 25.0 20 0 flat vis @@ -10,21 +10,22 @@ rail_screw_used1 6 1435mm/tpbps-new2 0.2 0.5 1.1 0.0 0.0 0.0 0.0 0.2 100.0 0.0 0.0 -uneven 18 -unevencross 25 -jointdip 6 +uneven 18;;;25;6 endtrack ``` -| atrybut | dotyczy | opis | -|-------------------|-----------------------|---------------------------------------------------------------------------------------| -| `uneven ` | `normal`, `switch` | największa odchyłka toku szynowego na cięciwie 10 m; 0-100, domyślnie 0 | -| `unevenup ` | `normal`, `switch` | składowa w pionie, osobno od `uneven` | -| `unevenside ` | `normal`, `switch` | składowa w poziomie, osobno od `uneven` | -| `unevencross `| `normal` | składowa przechyłki, osobno od `uneven`; rozjazd jej nie ma | -| `jointdip ` | `normal` | głębokość zapadnięcia na styku szyn; 0-100, domyślnie 0,4 składowej w pionie | +Liczby w mm, 0-100, rozdzielone średnikiem, bez spacji: -Składowa niepodana przyjmuje wartość `uneven`. Na przykład sam bok to `unevenside 30`, a same zapadnięcia na stykach to `jointdip 6`. +| pozycja | tor `normal` | rozjazd `switch` | +|---------|--------------------------------------------------|-----------------------------| +| 1 | największa odchyłka toku na cięciwie 10 m | to samo, droga p1-p2 | +| 2 | w pionie | droga p3-p4 | +| 3 | w poziomie | droga p1-p2, w pionie | +| 4 | przechyłka | droga p1-p2, w poziomie | +| 5 | zapadnięcie na styku, domyślnie 0,4 pionu | droga p3-p4, w pionie | +| 6 | | droga p3-p4, w poziomie | + +Pozycję pominiętą zastępuje pierwsza liczba, a zapadnięcie liczy się z pionu. Końcowe można opuścić. Sam bok to `uneven 0;;30`, a same zapadnięcia na stykach to `uneven 0;;;;6`. Rozjazd bez składowych na drodze p3-p4 przyjmuje wartości z drogi p1-p2. Rozjazd na 100 km/h na wprost i 40 km/h na bok to na przykład `uneven 14;35`. Wszystkie są też w edytorze, w panelu toru, w sekcji „Ride and sound”. Na drogach, rzekach i obrotnicach są ignorowane. @@ -56,18 +57,39 @@ Nierówność to pole przesunięć zależne od położenia na mapie, złożone z Każda składowa łączy się osobno: - **Tor z torem:** na styku obowiązuje mniejsza z dwóch wartości, a każdy tor dochodzi do niej płynnie na 20 m. Wolny koniec toru leży w projekcie. -- **Rozjazd:** wszystkie jego drogi mają jedną wartość. Rozjazdy złączone bezpośrednio przyjmują najmniejszą ze swoich. Dołączony tor dochodzi do wartości rozjazdu, także większej od własnej. Tor bez danej składowej trzyma w niej cały rozjazd w projekcie. +- **Rozjazd:** każda droga ma swoją wartość, a na wspólnym końcu obu obowiązuje mniejsza z nich. Rozjazdy złączone bezpośrednio przyjmują najmniejszą z tej samej drogi, a na styku dwóch dróg mniejszą z obu. Dołączony tor dochodzi do wartości rozjazdu na końcu, do którego dochodzi, także większej od własnej. Tor bez danej składowej trzyma w niej cały rozjazd w projekcie. - **Styki:** zapadnięcia są co `fSoundDistance`, na długości 6 m, i biegną dalej przez kolejne tory o tym samym odstępie. Stukot osi wypada w miejscu zapadnięcia. Przy odstępie 6 m lub mniejszym oraz na rozjazdach zapadnięć nie ma. - **Przechyłka:** dolicza się do projektowej. Jest pochyleniem w planie mapy, więc tory wpisane ku sobie spotykają się na tej samej przechyłce. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. -Za nierównością idą szyny, podsypka, `sleepermodel`, osie, przechył pudła i bujanie kabiny. +Końce złączonych dróg spotykają się w środku między sobą, z jednym kierunkiem poprzecznym, jeśli są bliżej niż 20 cm, co zamyka pęknięcia z niezgodnych współrzędnych w danych, także na torach bez nierówności. Przechyłkę na takim styku rozjazd bierze od zwykłego toru, a dwa tory albo dwa rozjazdy spotykają się w połowie. To dotyczy też fizyki. + +Za nierównością idą szyny, wierzch podsypki, `sleepermodel`, osie, przechył pudła i bujanie kabiny. Skarpa podsypki przejmuje różnicę, a jej spód zostaje w projekcie, na terenie. Nie idą za nią modele z includów (np. napędy i podkłady rozjazdów), teren ani sieć trakcyjna, więc przy dużych wartościach szyna rozjeżdża się z takim modelem o kilka centymetrów. +Na torze z nierównością kabinę buja sam tor. Odpada kołysanie wyliczane z przyspieszenia poprzecznego, czyli z promienia łuku, oraz losowe szarpnięcia przy większej prędkości. Na torach bez nierówności działają jak dotąd. + Pantograf podnosi się z szybkością ciśnienia, ok. 0,14 m/s, a szczelina do drutu większa niż 1 cm odcina napięcie i wybija wyłącznik szybki. Nierówność dokłada się do tego przy dużej prędkości. Na torach Bałtyku symulacja daje szczelinę 0,6 cm, gdy składowa w pionie razy prędkość w km/h wynosi 925, czyli np. 8 mm przy 120 km/h, 13 mm przy 70 km/h i 23 mm przy 40 km/h. Wartości z tabeli Id-14 są od tego większe. Sam drut też to potrafi: na Bałtyku przy `tor0290` wznosi się o ok. 1%, więc pantograf nadąża tam do ok. 46 km/h, a powyżej wyłącznik wybija także na prostym torze. Fizyka bierze promień łuku i pochylenie z projektu, więc nierówności nie zmieniają tych wartości, a do wykolejeń dokładają tylko przechyłkę, ok. 0,06 m/s² przy `uneven 18` wobec progu ok. 3,2 m/s². +## Ocena torów dowolnej scenerii + +`tools/track_uneven_grade.py` wylicza wartości z tego, co widać w scenerii: z tekstur podsypki i szyn, trawy na torze, łuków, prędkości i przejazdów drogowych. Czyta plik `.scn`, `.scm` albo `.inc` razem z jego includami, oryginałów nie zmienia, a kopie plików z torami zapisuje do `--out`. Bez `--out` tylko wypisuje raport. + +``` +python tools/track_uneven_grade.py scenery/lk357.scn --out graded +python tools/track_uneven_grade.py scenery/baltyk_skm1.scn --out graded --route -107,2300 -4120,2491 +``` + +- `--route X,Z X,Z`: tory pogarszają się od pierwszego punktu do drugiego, licząc po torach. +- `--gain`: mnożnik stanu torów, domyślnie 1. +- `--near`: jaka część torów na drewnie i przy zmianach krzywizny idzie bliżej granicy Id-14, domyślnie 0,6. Wybór jest stały. +- `--straight`: nazwy torów bez nierówności, po przecinku. +- `--root`: folder `scenery`, gdy podany plik leży głębiej. + +Wartości trzymają się pod sufitem pantografu, liczonym symulacją na tych torach, ale bez wznosu samego drutu. Zużycie tekstur odczytywane jest z ich nazw, według tabel na początku skryptu, a nieznana nazwa dostaje 0,5. Tory z plików `.inc` z parametrami są pomijane, a bloki `rotate` nieuwzględniane. W obu przypadkach raport wypisuje uwagę. Tor, który ma już nierówność, zostaje bez zmian. + ## Wydajność Tor z nierównością ma geometrię co 2 m zamiast co 10 m na prostych. Rozjazdy zachowują swój podział. diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 370b486b..27db3975 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -8058,10 +8058,12 @@ TDynamicObject::update_shake( double const Timedelta ) { shakevector *= 0; auto const iVel { std::min( GetVelocity(), 150.0 ) }; + // an uneven track shakes the cab by itself, in place of the sway drawn from the curve and the jolts drawn at random + auto const uneven { MyTrack != nullptr && MyTrack->is_uneven() }; if( iVel > 0.5 ) { // acceleration-driven base shake shakevector += glm::dvec3( - -MoverParameters->AccN * Timedelta * 5.0 * Global.ShakingMultiplierRL, // highlight side sway + uneven ? 0.0 : -MoverParameters->AccN * Timedelta * 5.0 * Global.ShakingMultiplierRL, // highlight side sway -MoverParameters->AccVert * Timedelta * Global.ShakingMultiplierUD, -MoverParameters->AccSVBased * Timedelta * 1.5 * Global.ShakingMultiplierBF); // accent acceleration/deceleration // unevenness of the track under the outer axles @@ -8074,7 +8076,7 @@ TDynamicObject::update_shake( double const Timedelta ) { auto shake { 1.25 * ShakeSpring.ComputateForces( shakevector, ShakeState.offset ) }; - if( LocalRandom( iVel ) > 25.0 ) { + if( false == uneven && LocalRandom( iVel ) > 25.0 ) { // extra shake at increased velocity shake += ShakeSpring.ComputateForces( glm::dvec3( diff --git a/world/Segment.cpp b/world/Segment.cpp index 567bb23d..379a944c 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -81,6 +81,10 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N // poprawienie przechyłki fRoll1 = static_cast(glm::radians(fNewRoll1)); // Ra: przeliczone jest bardziej przydatne do obliczeń fRoll2 = static_cast(glm::radians(fNewRoll2)); + m_designrolls[ 0 ] = fRoll1; + m_designrolls[ 1 ] = fRoll2; + m_joints[ 0 ].reset(); + m_joints[ 1 ].reset(); bCurve = bIsCurve; if (Global.bRollFix) { // Ra: poprawianie przechyłki @@ -175,6 +179,8 @@ bool TSegment::InitChain(std::vector> Pieces) Point2 = last.Point2; fRoll1 = first.fRoll1; fRoll2 = last.fRoll2; + m_designrolls[ 0 ] = first.m_designrolls[ 0 ]; + m_designrolls[ 1 ] = last.m_designrolls[ 1 ]; bCurve = true; fDirection = -std::atan2(Point2.x - Point1.x, Point2.z - Point1.z); fStoop = std::atan2(Point2.y - Point1.y, fLength); @@ -437,7 +443,7 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: // direction and slope stay as designed: the physics takes the curve radius and the grade from them auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; glm::dvec3 const across { -std::cos( rotation.z ), 0.0, -std::sin( rotation.z ) }; - auto const amount { pOwner->uneven_at( fDistance ) }; + auto const amount { pOwner->uneven_at( fDistance, m_path ) }; rotation.x += static_cast( track_uneven::stray_per_mm * ( amount.up * track_uneven::roll( position, across, gauge ) + amount.cross * track_uneven::level( position, across ) / gauge ) ); @@ -445,7 +451,7 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: } glm::dvec3 TSegment::stray( glm::dvec3 const &Point, double const Distance ) const { - auto const amount { pOwner->uneven_at( Distance ) }; + auto const amount { pOwner->uneven_at( Distance, m_path ) }; auto const field { track_uneven::field( Point ) }; glm::dvec3 result { field.x * amount.side, field.y * amount.up, field.z * amount.side }; result *= track_uneven::stray_per_mm; @@ -468,11 +474,12 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi if( false == m_uneven ) { return; } - // the rails settling apart turn about the middle of the track, what's outside them follows the rail + // the rails settling apart turn about the middle of the track, what's outside them follows the rail; the foot of the ballast stays auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; auto const across { std::clamp( glm::dot( Vertex.position - Position, Parallel ), -0.5f * gauge, 0.5f * gauge ) }; - Vertex.position.y -= static_cast( track_uneven::stray_per_mm * pOwner->uneven_at( Distance ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); - Vertex.position += glm::vec3 { stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; + auto const share { pOwner->uneven_bed_share( Position.y - Vertex.position.y ) }; + Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * pOwner->uneven_at( Distance, m_path ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); + Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; } void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { diff --git a/world/Segment.h b/world/Segment.h index ec87f98a..e316ec1b 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -9,12 +9,13 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include + #include "utilities/Classes.h" #include "rendering/geometrybank.h" #include #include #include -#include #include "world/Knots.h" struct map_colored_paths { @@ -101,7 +102,11 @@ class TSegment glm::dvec3 Point2; float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach + float m_designrolls[ 2 ] { 0.f, 0.f }; // as entered; the two above can differ at a joint bool m_uneven { false }; // see TrackUneven.h + int m_path { 0 }; // which path of a switch + // where the path meets the next one: the middle of their ends and their common vector across + std::optional> m_joints[ 2 ]; double fLength { -1.0 }; // długość policzona std::vector fTsBuffer; // wartości parametru krzywej t dla równych odcinków s std::vector fTsSlope; // nachylenie dt/du (= dt/ds * fStep) w tych samych punktach @@ -115,7 +120,6 @@ class TSegment TTrack *pOwner = nullptr; // wskaźnik na właściciela std::vector> m_pieces; // parts of a path made of several curves, parametrized by distance std::vector m_starts; // distance from Point1 to the start of each part - std::optional> m_joints[ 2 ]; double m_dense { 0.0 }; // a switch makes the profiles of its rails and its bed at the mean of the rolls of its paths: lofted along a path, they're turned // from it to the roll of the path there, so a switch on a cant ramp follows it; none for the other paths @@ -188,8 +192,16 @@ public: void RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const; // call before Init, it sets the density of the geometry void - set_uneven( bool const Uneven ) { - m_uneven = Uneven; } + set_uneven( bool const Uneven, int const Path = 0 ) { + m_uneven = Uneven; + m_path = Path; } + float + design_roll( int const End ) const { + return m_designrolls[ End ]; } + void + set_joint( int const End, std::optional> const &Joint, float const Roll ) { + m_joints[ End ] = Joint; + ( End == 0 ? fRoll1 : fRoll2 ) = Roll; } // second derivative of the displacement along the path; times speed squared: acceleration of an axle glm::dvec3 uneven_bend( double const Distance ) const; @@ -227,9 +239,6 @@ public: // point of the path and the unit vector across it towards the right rail profile, at a distance from Point1 void plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const; - void - set_joint( int const End, std::optional> const &Joint ) { - m_joints[ End ] = Joint; } void set_dense( double const Distance ) { m_dense = Distance; } diff --git a/world/Track.cpp b/world/Track.cpp index 180063c5..751f2bc8 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -87,6 +87,9 @@ TTrack::profiles_array TTrack::m_profiles; TTrack::profiles_map TTrack::m_profilesmap; int TTrack::s_layoutstamp { 0 }; +// how near an end of a path counts as being at a point, m +static double const switch_end_reach { 0.3 }; + TSwitchExtension::TSwitchExtension(TTrack *owner, int const what) { // na początku wszystko puste pNexts[0] = nullptr; // wskaźniki do kolejnych odcinków ruchu @@ -737,24 +740,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } } else if( str == "uneven" ) { - // uneven : see TrackUneven.h - auto const uneven { parser->getToken() }; + // uneven [;...]: see read_uneven and TrackUneven.h. The semicolons stay in the token + auto const values { parser->getToken( false, "\n\r\t " ) }; if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { - m_uneven = std::clamp( uneven, 0.f, track_uneven::most ); - } - } - else if( str == "unevenup" || str == "unevenside" || str == "unevencross" ) { - // the components of uneven set apart, mm - auto const value { parser->getToken() }; - if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { - ( str == "unevenup" ? m_unevenup : str == "unevenside" ? m_unevenside : m_unevencross ) = std::clamp( value, 0.f, track_uneven::most ); - } - } - else if( str == "jointdip" ) { - // jointdip : depth of the dip at rail joints, with or without uneven - auto const jointdip { parser->getToken() }; - if( eType == tt_Normal && iCategoryFlag == 1 ) { - m_jointdip = std::clamp( jointdip, 0.f, track_uneven::most ); + read_uneven( values ); } } else if( str == "friction" ) { @@ -833,75 +822,166 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) // to a stale origin and the renderer would draw sleepers in the wrong place). } -track_uneven::amounts TTrack::uneven_declared() const { +void TTrack::read_uneven( std::string const &Text ) { - return { + auto const fields { track_uneven::read_fields<6>( Text ) }; + m_uneven = std::max( fields[ 0 ], 0.f ); + if( eType == tt_Switch ) { + m_unevenbranch = fields[ 1 ]; + m_unevenup = fields[ 2 ]; + m_unevenside = fields[ 3 ]; + m_unevenbranchup = fields[ 4 ]; + m_unevenbranchside = fields[ 5 ]; + } + else { + m_unevenup = fields[ 1 ]; + m_unevenside = fields[ 2 ]; + m_unevencross = fields[ 3 ]; + m_jointdip = fields[ 4 ]; + } +} + +std::string TTrack::uneven_text() const { + + auto const fields { eType == tt_Switch ? + std::array { m_uneven, m_unevenbranch, m_unevenup, m_unevenside, m_unevenbranchup, m_unevenbranchside } : + std::array { m_uneven, m_unevenup, m_unevenside, m_unevencross, m_jointdip, -1.f } }; + if( m_uneven <= 0.f && std::all_of( fields.begin() + 1, fields.end(), []( float const Field ) { return Field < 0.f; } ) ) { + return {}; + } + return "uneven " + track_uneven::write_fields( fields ) + ' '; +} + +track_uneven::amounts TTrack::uneven_declared( int const Path ) const { + + track_uneven::amounts result { m_unevenup >= 0.f ? m_unevenup : m_uneven, m_unevenside >= 0.f ? m_unevenside : m_uneven, eType != tt_Normal ? 0.f : m_unevencross >= 0.f ? m_unevencross : m_uneven }; + if( Path == 1 ) { + auto const branch = [ this ]( float const Part, double const Straight ) -> double { + return Part >= 0.f ? Part : m_unevenbranch >= 0.f ? m_unevenbranch : Straight; }; + result.up = branch( m_unevenbranchup, result.up ); + result.side = branch( m_unevenbranchside, result.side ); + } + return result; } bool TTrack::is_uneven() const { - auto const declared { uneven_declared() }; - return declared.up > 0.0 || declared.side > 0.0 || declared.cross > 0.0 || joint_depth() > 0.0; + auto const any = []( track_uneven::amounts const &Amount ) { + return Amount.up > 0.0 || Amount.side > 0.0 || Amount.cross > 0.0; }; + return any( uneven_declared( 0 ) ) || any( uneven_declared( 1 ) ) || joint_depth() > 0.0; } -track_uneven::amounts TTrack::uneven_at( double const Distance ) const { +double TTrack::uneven_bed_share( double const Depth ) const { + return track_uneven::bed_share( Depth, std::abs( track_rail_profile( m_profile1.second ).front().position.y ), fTexHeight1 ); +} + +track_uneven::amounts TTrack::uneven_at( double const Distance, int const Path ) const { + + auto const along = [ Distance ]( track_uneven::amounts const &Own, track_uneven::amounts const &Start, track_uneven::amounts const &End, double const Length ) -> track_uneven::amounts { + return { + track_uneven::along( Own.up, Start.up, End.up, Distance, Length ), + track_uneven::along( Own.side, Start.side, End.side, Distance, Length ), + track_uneven::along( Own.cross, Start.cross, End.cross, Distance, Length ) }; }; if( eType == tt_Switch ) { - return switch_uneven(); + auto const &stray { switch_uneven() }; + return along( stray.paths[ Path ], stray.ends[ Path ][ 0 ], stray.ends[ Path ][ 1 ], SwitchExtension->Segments[ Path ]->GetLength() ); } auto const own { uneven_declared() }; - auto const atend = [&]( TTrack const *Other ) -> track_uneven::amounts { + auto const atend = [&]( TTrack const *Other, glm::dvec3 const &Point ) -> track_uneven::amounts { if( Other == nullptr ) { return {}; } if( Other->eType == tt_Switch ) { - return Other->switch_uneven(); + return Other->switch_uneven_at( Point ); } - auto const theirs { Other->uneven_declared() }; - return { std::min( own.up, theirs.up ), std::min( own.side, theirs.side ), std::min( own.cross, theirs.cross ) }; }; - auto const start { atend( trPrev ) }; - auto const end { atend( trNext ) }; - return { - track_uneven::along( own.up, start.up, end.up, Distance, Length() ), - track_uneven::along( own.side, start.side, end.side, Distance, Length() ), - track_uneven::along( own.cross, start.cross, end.cross, Distance, Length() ) }; + return track_uneven::least( own, Other->uneven_declared() ); }; + return along( own, atend( trPrev, Segment->FastGetPoint_0() ), atend( trNext, Segment->FastGetPoint_1() ), Length() ); } -track_uneven::amounts TTrack::switch_uneven() const { +TTrack::switch_stray const &TTrack::switch_uneven() const { if( m_switchstray.stamp == s_layoutstamp ) { - return m_switchstray.value; + return m_switchstray; } std::vector group { this }; - auto least { uneven_declared() }; + std::array least { uneven_declared( 0 ), uneven_declared( 1 ) }; for( std::size_t index = 0; index < group.size(); ++index ) { auto const &extension { *group[ index ]->SwitchExtension }; for( auto const *other : { extension.pPrevs[ 0 ], extension.pPrevs[ 1 ], extension.pNexts[ 0 ], extension.pNexts[ 1 ] } ) { if( other == nullptr ) { continue; } - auto const theirs { other->uneven_declared() }; if( other->eType == tt_Switch && other->SwitchExtension ) { - least.up = std::min( least.up, theirs.up ); - least.side = std::min( least.side, theirs.side ); + least[ 0 ] = track_uneven::least( least[ 0 ], other->uneven_declared( 0 ) ); + least[ 1 ] = track_uneven::least( least[ 1 ], other->uneven_declared( 1 ) ); if( std::find( group.begin(), group.end(), other ) == group.end() ) { group.push_back( other ); } } else { // a track with none of a component keeps to its design in it, any other comes to the value of the switch - if( theirs.up <= 0.0 ) { least.up = 0.0; } - if( theirs.side <= 0.0 ) { least.side = 0.0; } + auto const theirs { other->uneven_declared() }; + for( auto &path : least ) { + if( theirs.up <= 0.0 ) { path.up = 0.0; } + if( theirs.side <= 0.0 ) { path.side = 0.0; } + } } } } for( auto const *track : group ) { - track->m_switchstray = { least, s_layoutstamp }; + track->m_switchstray.paths = least; + track->m_switchstray.stamp = s_layoutstamp; } - return least; + // the lowest of the paths of a switch ending at a point + auto const paths_at = []( TTrack const &Track, glm::dvec3 const &Point ) { + auto result { track_uneven::least( Track.m_switchstray.paths[ 0 ], Track.m_switchstray.paths[ 1 ] ) }; + auto found { false }; + track_uneven::amounts ending { track_uneven::most, track_uneven::most, track_uneven::most }; + for( int path = 0; path < 2; ++path ) { + auto const &segment { *Track.SwitchExtension->Segments[ path ] }; + if( glm::length( segment.FastGetPoint_0() - Point ) < switch_end_reach || glm::length( segment.FastGetPoint_1() - Point ) < switch_end_reach ) { + ending = track_uneven::least( ending, Track.m_switchstray.paths[ path ] ); + found = true; + } + } + return found ? ending : result; }; + for( auto const *track : group ) { + auto const &extension { *track->SwitchExtension }; + for( int path = 0; path < 2; ++path ) { + auto const &segment { *extension.Segments[ path ] }; + for( int end = 0; end < 2; ++end ) { + auto const point { end == 0 ? segment.FastGetPoint_0() : segment.FastGetPoint_1() }; + auto value { paths_at( *track, point ) }; + auto const *other { end == 0 ? extension.pPrevs[ path ] : extension.pNexts[ path ] }; + if( other != nullptr && other->eType == tt_Switch && other->SwitchExtension ) { + value = track_uneven::least( value, paths_at( *other, point ) ); + } + track->m_switchstray.ends[ path ][ end ] = value; + } + } + } + return m_switchstray; +} + +track_uneven::amounts TTrack::switch_uneven_at( glm::dvec3 const &Point ) const { + + auto const &stray { switch_uneven() }; + auto found { false }; + track_uneven::amounts result { track_uneven::most, track_uneven::most, track_uneven::most }; + for( int path = 0; path < 2; ++path ) { + auto const &segment { *SwitchExtension->Segments[ path ] }; + for( int end = 0; end < 2; ++end ) { + if( glm::length( ( end == 0 ? segment.FastGetPoint_0() : segment.FastGetPoint_1() ) - Point ) < switch_end_reach ) { + result = track_uneven::least( result, stray.ends[ path ][ end ] ); + found = true; + } + } + } + return found ? result : track_uneven::least( stray.paths[ 0 ], stray.paths[ 1 ] ); } double TTrack::joint_depth() const { @@ -1007,7 +1087,7 @@ void TTrack::init_segments( bool const Initial ) { ++s_layoutstamp; if( eType == tt_Switch && SwitchExtension ) { SwitchExtension->Segments[ 0 ]->set_uneven( is_uneven() ); - SwitchExtension->Segments[ 1 ]->set_uneven( is_uneven() ); + SwitchExtension->Segments[ 1 ]->set_uneven( is_uneven(), 1 ); } else if( Segment ) { Segment->set_uneven( is_uneven() ); @@ -1742,6 +1822,7 @@ void TTrack::share_joints() { } for( int end = 0; end < 2; ++end ) { std::optional> joint; + auto roll { segment->design_roll( end ) }; auto *neighbour { track_rails::path_neighbour( *this, path, end ) }; if( neighbour != nullptr && neighbour != this && ( neighbour->iCategoryFlag & 15 ) == ( iCategoryFlag & 15 ) ) { glm::dvec3 point; @@ -1766,10 +1847,35 @@ void TTrack::share_joints() { } closest = gap; joint.emplace( 0.5 * ( point + theirpoint ), glm::normalize( across + theiracross ) ); + if( iCategoryFlag & 1 ) { + // the cant of a plain track is what was designed, a switch takes it; alike ones meet halfway + auto const own { segment->design_roll( end ) }; + auto const their { theirend == end ? -theirs->design_roll( theirend ) : theirs->design_roll( theirend ) }; + roll = ( + ( eType == tt_Switch ) == ( neighbour->eType == tt_Switch ) ? 0.5f * ( own + their ) : + eType == tt_Switch ? their : + own ); + } } } } - segment->set_joint( end, joint ); + segment->set_joint( end, joint, roll ); + } + } + if( eType != tt_Switch && Segment && ( iCategoryFlag & 1 ) ) { + float roll1, roll2; + Segment->GetRolls( roll1, roll2 ); + if( roll1 != 0.f || roll2 != 0.f ) { + iTrapezoid |= 1; + } + } + if( eType == tt_Switch && SwitchExtension ) { + // the profiles of the switch are made at the mean of the rolls of its paths as they meet the tracks joined, and turned from it + // along each path (init_segments() bakes the mean of the rolls as designed) + float mean1, mean2; + profile_rolls( mean1, mean2 ); + for( int path = 0; path < 2; ++path ) { + SwitchExtension->Segments[ path ]->set_baked_roll( mean1 ); } } } @@ -3030,21 +3136,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; } - if( m_uneven > 0.f ) { - Output << "uneven " << m_uneven << ' '; - } - if( m_unevenup >= 0.f ) { - Output << "unevenup " << m_unevenup << ' '; - } - if( m_unevenside >= 0.f ) { - Output << "unevenside " << m_unevenside << ' '; - } - if( m_unevencross >= 0.f ) { - Output << "unevencross " << m_unevencross << ' '; - } - if( m_jointdip >= 0.f ) { - Output << "jointdip " << m_jointdip << ' '; - } + Output << uneven_text(); if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } @@ -4314,6 +4406,11 @@ path_table::InitTracks() { isolated = isolated->Next(); } + // the physics goes by the cant at the joints too, and comes before any geometry is made + for( auto *track : m_items ) { + track->share_joints(); + } + TTrack::fetch_default_profiles(); } diff --git a/world/Track.h b/world/Track.h index e14970d3..458abec5 100644 --- a/world/Track.h +++ b/world/Track.h @@ -292,6 +292,10 @@ public: float m_unevenup { -1.f }; float m_unevenside { -1.f }; float m_unevencross { -1.f }; + // the diverging path of a switch, mm; negative: as the straight one + float m_unevenbranch { -1.f }; + float m_unevenbranchup { -1.f }; + float m_unevenbranchside { -1.f }; float m_jointdip { -1.f }; // depth of the dip at rail joints, mm; negative: from m_uneven; plain tracks only private: double fVelocity = -1.0; // ograniczenie prędkości // prędkość dla AI (powyżej rośnie prawdopowobieństwo wykolejenia) @@ -413,11 +417,19 @@ public: void VelocitySet(float v); double VelocityGet(); float Friction() const; - // the unevenness the track declares - track_uneven::amounts uneven_declared() const; + // the attribute of the entry. A plain track: the value;up and down;sideways;cross level;dip at the joints. + // A switch: path p1-p2;path p3-p4;up and down of the first;sideways of the first;up and down of the second;sideways of the second + void read_uneven( std::string const &Text ); + std::string uneven_text() const; + // the unevenness the track declares, on a path of a switch + track_uneven::amounts uneven_declared( int const Path = 0 ) const; bool is_uneven() const; - // the unevenness at a distance along the track, meeting the tracks joined at its ends - track_uneven::amounts uneven_at( double const Distance ) const; + // the unevenness at a distance along a path, meeting the tracks joined at its ends + track_uneven::amounts uneven_at( double const Distance, int const Path = 0 ) const; + // how much of the displacement a point of the profile at the depth takes + double uneven_bed_share( double const Depth ) const; + // closes the gaps and steps of cant between the ends of joined paths + void share_joints(); double joint_depth() const; // mm double joint_dip( double const Distance ) const; // m // distance from the rail joint behind; negative: the track has no joints that dip @@ -453,7 +465,6 @@ private: void create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const *Shape = nullptr ); void create_track_blade_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); void profile_rolls( float &Roll1, float &Roll2 ) const; - void share_joints(); void create_track_bed_profile( gfx::vertex_array &Output, TTrack const *Previous, TTrack const *Next ); // rail joints run on across joined tracks, however the line is cut into them, so the clatter keeps its rhythm struct rail_run { @@ -463,13 +474,17 @@ private: }; void settle_rail_run() const; mutable rail_run m_railrun; - // one value along all paths of a switch: the lowest of the switches joined, 0 next to a track with none - track_uneven::amounts switch_uneven() const; + // the value along each path of a switch: the lowest of that path in the switches joined, 0 next to a track with none. + // And at the ends of the paths, by what meets there: the other path at the common end, the path of a switch joined struct switch_stray { - track_uneven::amounts value; + std::array paths; + std::array, 2> ends; int stamp { -1 }; }; mutable switch_stray m_switchstray; + switch_stray const &switch_uneven() const; + // the value of a switch where a track joined to it meets it + track_uneven::amounts switch_uneven_at( glm::dvec3 const &Point ) const; static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above void create_road_profile( gfx::vertex_array &Output, bool const Forcetransition = false ); void create_road_side_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const &Road, bool const Forcetransition = false ); diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 9031c6ca..f4a50eb7 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -11,7 +11,10 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include +#include +#include #include #include @@ -34,6 +37,11 @@ struct amounts { double cross { 0.0 }; }; +inline amounts least( amounts const &Left, amounts const &Right ) { + + return { std::min( Left.up, Right.up ), std::min( Left.side, Right.side ), std::min( Left.cross, Right.cross ) }; +} + struct wave { double x; double z; @@ -41,6 +49,44 @@ struct wave { double share; }; +// the numbers of an entry like "uneven 14;35", negative where left out +template +std::array read_fields( std::string const &Text ) { + + std::array result; + result.fill( -1.f ); + std::size_t start { 0 }; + for( std::size_t index = 0; index < Count && start < Text.size(); ++index ) { + auto const end { std::min( Text.find( ';', start ), Text.size() ) }; + auto value { 0.f }; + if( end > start && std::from_chars( Text.data() + start, Text.data() + end, value ).ec == std::errc {} ) { + result[ index ] = std::clamp( value, 0.f, most ); + } + start = end + 1; + } + return result; +} + +// and back, with the ones left out at the end dropped +template +std::string write_fields( std::array const &Fields ) { + + auto count { Count }; + while( count > 1 && Fields[ count - 1 ] < 0.f ) { + --count; + } + std::ostringstream text; + for( std::size_t index = 0; index < count; ++index ) { + if( index > 0 ) { + text << ';'; + } + if( Fields[ index ] >= 0.f ) { + text << Fields[ index ]; + } + } + return text.str(); +} + template using wave_set = std::array; @@ -109,6 +155,13 @@ inline double along( double const Full, double const Atstart, double const Atend return ends + ( Full - ends ) * std::min( smooth( Distance / fade ), smooth( ( Length - Distance ) / fade ) ); } +// how much of the displacement a point of the profile takes, by its depth under the rail top: all of it down to the foot of the rail, +// none at the foot of the ballast, which stays on the ground as designed +inline double bed_share( double const Depth, double const Rail, double const Height ) { + + return Height > 0.0 ? std::clamp( 1.0 - ( Depth - Rail ) / Height, 0.0, 1.0 ) : 1.0; +} + // distance from the joint behind the point inline double rail_phase( double const Along, double const Rail ) {