diff --git a/CMakeLists.txt b/CMakeLists.txt index 703138bb..4104efa7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,6 +117,7 @@ set(SOURCES "world/RailPlan.cpp" "world/RailCarve.cpp" "world/RailLayer.cpp" +"world/TrackUneven.cpp" "world/Road.cpp" "world/Sweep.cpp" "world/RoadPoint.cpp" diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index ee69513c..892c2485 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -709,32 +709,32 @@ std::vector make_properties() { // 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. 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 const uneven = [&](char const *Key, char const *Label, char const *Tooltip, float track_uneven::attribute::*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 = [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.get = [Member](TTrack const &Track) { return decimal(Track.m_uneven.*Member, 1); }; property.set = [Member, Unset](TTrack &Track, std::string const &Text) { double value; if (false == parse_number(Text, value)) return false; auto const clamped{(Unset && value < 0.0) ? -1.f : std::clamp(static_cast(value), 0.f, track_uneven::most)}; - if (decimal(clamped, 1) == decimal(Track.*Member, 1)) + if (decimal(clamped, 1) == decimal(Track.m_uneven.*Member, 1)) return false; - Track.*Member = clamped; + Track.m_uneven.*Member = clamped; 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, 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); + 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", &track_uneven::attribute::value, false, true, true); + uneven("unevenup", "Up and down (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &track_uneven::attribute::up, true, true, true); + uneven("unevenside", "Sideways (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &track_uneven::attribute::side, true, true, true); + uneven("unevencross", "Cross level (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &track_uneven::attribute::cross, true, true, false); + uneven("unevenbranch", "Diverging path (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &track_uneven::attribute::branch, true, false, true); + uneven("unevenbranchup", "Diverging, up and down (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &track_uneven::attribute::branchup, true, false, true); + uneven("unevenbranchside", "Diverging, sideways (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &track_uneven::attribute::branchside, 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", &track_uneven::attribute::dip, 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 7f4b807e..73f5a975 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -249,13 +249,6 @@ editor_track::state editor_track::capture(TTrack const &Track) result.isolated.push_back(isolated->asName); result.overhead = Track.fOverhead; result.uneven = Track.m_uneven; - 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; result.sleeper_model = Track.m_sleeper_model_name; @@ -364,13 +357,6 @@ void editor_track::apply(TTrack &Track, state const &State) isolated(Track, State.isolated); overhead(Track, State.overhead); Track.m_uneven = State.uneven; - 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; Track.m_sleeper_model_name = State.sleeper_model; @@ -3237,11 +3223,11 @@ void write_points(std::ostream &Text, segment_data const &Path) Text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; } -void write_ending(std::ostream &Text, TTrack const &Style) +void write_ending(std::ostream &Text, TTrack const &Style, bool const Switch) { if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) Text << "railprofile " << Style.m_profile1.first << ' '; - Text << Style.uneven_text(); + Text << track_uneven::write_attribute(Style.m_uneven, Switch); // 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 << ' '; @@ -3303,7 +3289,7 @@ TTrack *editor_track::create_switch(switch_template const &Template, std::vector write_points(text, Paths[1]); if (auto const trackbed{ballast_of(Style)}; trackbed != null_handle) text << "trackbed " << material_name(trackbed) << ' '; - write_ending(text, Style); + write_ending(text, Style, true); track = load_path(text.str(), Style, Name); } track->SwitchExtension->m_frogs = true; @@ -3615,7 +3601,7 @@ TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path, write_points(text, Path); if (auto const speed{velocity(Style)}; speed > 0.0) text << "velocity " << speed << ' '; - write_ending(text, Style); + write_ending(text, Style, false); auto *track{load_path(text.str(), Style, Name)}; track->m_paths = {Path}; return track; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 1cdcf96e..d3b230fe 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -20,6 +20,7 @@ http: #include "world/Segment.h" #include "editor/editorAlignment.hpp" #include "editor/editorSwitchDesign.hpp" +#include "world/TrackUneven.h" #include "utilities/Classes.h" #include "world/TrackBlend.h" @@ -63,14 +64,7 @@ class editor_track float texslope{0.9f}; std::vector isolated; float overhead{-1.f}; - float uneven{0.f}; - 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}; + track_uneven::attribute uneven; bool sleepers{false}; float sleeper_frequency{0.6f}; std::string sleeper_model; diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 22970fa6..65b7da6e 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -165,6 +165,81 @@ TEST_CASE("uneven track: all the values in one attribute") CHECK(track_uneven::write_fields<6>({18.f, -1.f, -1.f, -1.f, -1.f, -1.f}) == "18"); } +TEST_CASE("uneven track: the attribute goes by the entry it's written into") +{ + auto const plain{track_uneven::read_attribute("18;22;19;25;6", false)}; + CHECK(track_uneven::write_attribute(plain, false) == "uneven 18;22;19;25;6 "); + // a switch made in the style of a plain track: no path p3-p4 of its own, the components in their places + CHECK(track_uneven::write_attribute(plain, true) == "uneven 18;;22;19 "); + auto const made{track_uneven::read_attribute("18;;22;19", true)}; + CHECK(track_uneven::declared(made, false, 0).up == doctest::Approx(22.0)); + CHECK(track_uneven::declared(made, false, 0).side == doctest::Approx(19.0)); + CHECK(track_uneven::declared(made, false, 1).up == doctest::Approx(22.0)); + CHECK(track_uneven::declared(made, false, 0).cross == doctest::Approx(0.0)); + // a switch with its own values, and a plain track in its style + auto const points{track_uneven::read_attribute("14;35;16;17", true)}; + CHECK(track_uneven::write_attribute(points, true) == "uneven 14;35;16;17 "); + CHECK(track_uneven::declared(points, false, 1).up == doctest::Approx(35.0)); + CHECK(track_uneven::declared(points, false, 1).side == doctest::Approx(35.0)); + CHECK(track_uneven::write_attribute(points, false) == "uneven 14;16;17 "); + // a plain track: the value stands in for what's left out, only there is the cross level + auto const track{track_uneven::read_attribute("18;;30", false)}; + CHECK(track_uneven::declared(track, true, 0).up == doctest::Approx(18.0)); + CHECK(track_uneven::declared(track, true, 0).side == doctest::Approx(30.0)); + CHECK(track_uneven::declared(track, true, 0).cross == doctest::Approx(18.0)); + // nothing given, nothing written + CHECK(track_uneven::write_attribute({}, false).empty()); + CHECK(track_uneven::write_attribute({}, true).empty()); + // a value of 0 stays when there's more after it + CHECK(track_uneven::write_attribute(track_uneven::read_attribute("0;;;;6", false), false) == "uneven 0;;;;6 "); +} + +TEST_CASE("uneven track: each component comes to its neighbours along a track") +{ + double const length{500.0}; + auto const atstart{track_uneven::along(track_uneven::amounts{18.0, 16.0, 14.0}, {5.0, 4.0, 3.0}, {7.0, 8.0, 9.0}, 0.0, length)}; + CHECK(atstart.up == doctest::Approx(5.0)); + CHECK(atstart.side == doctest::Approx(4.0)); + CHECK(atstart.cross == doctest::Approx(3.0)); + auto const inside{track_uneven::along(track_uneven::amounts{18.0, 16.0, 14.0}, {5.0, 4.0, 3.0}, {7.0, 8.0, 9.0}, 0.5 * length, length)}; + CHECK(inside.up == doctest::Approx(18.0)); + CHECK(inside.side == doctest::Approx(16.0)); + CHECK(inside.cross == doctest::Approx(14.0)); +} + +TEST_CASE("uneven track: axles don't stray along the track") +{ + glm::dvec3 const across{0.6, 0.0, -0.8}; + glm::dvec3 const along{0.8, 0.0, 0.6}; + auto const moved{track_uneven::across_only(across * 0.03 + along * 0.05 + glm::dvec3{0.0, 0.02, 0.0}, across)}; + CHECK(glm::dot(moved, along) == doctest::Approx(0.0)); + CHECK(glm::dot(moved, across) == doctest::Approx(0.03)); + CHECK(moved.y == doctest::Approx(0.02)); +} + +TEST_CASE("uneven track: a path of no length gives no vector across") +{ + glm::dvec3 const none{0.0}; + CHECK(track_uneven::across_of(none, none) == none); + CHECK(glm::length(track_uneven::across_of({3.0, 0.0, 4.0}, none)) == doctest::Approx(1.0)); + // it can't pass for a joint: the check of the two vectors being alike fails + CHECK(glm::dot(track_uneven::across_of(none, none), glm::dvec3{1.0, 0.0, 0.0}) < 0.9); + // falls back on the chord + auto const chord{track_uneven::across_of(none, {0.0, 0.0, 10.0})}; + CHECK(chord.x == doctest::Approx(-1.0)); + CHECK(chord.z == doctest::Approx(0.0)); + // straight up has no direction in the plan + CHECK(glm::length(track_uneven::across_of({0.0, 5.0, 0.0}, {0.0, 0.0, 10.0})) == doctest::Approx(1.0)); +} + +TEST_CASE("uneven track: the cant at a joint") +{ + CHECK(track_uneven::joint_roll(0.1f, -0.1f, true, true) == doctest::Approx(0.f)); + CHECK(track_uneven::joint_roll(0.2f, 0.4f, false, false) == doctest::Approx(0.3f)); + CHECK(track_uneven::joint_roll(0.1f, 0.0f, true, false) == doctest::Approx(0.f)); + CHECK(track_uneven::joint_roll(0.0f, 0.1f, false, true) == doctest::Approx(0.f)); +} + 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})}; diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py index eb39d7c6..d643f94f 100644 --- a/tools/track_uneven_grade.py +++ b/tools/track_uneven_grade.py @@ -114,10 +114,10 @@ class Scenery: 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]) + # a plant takes its place in the parameters; a file with grass in its name and none of them just lists plants + numbers = [float(w) for w in params if NUMBER.match(w)] + if GRASS.search(name.lower()) and 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 @@ -355,47 +355,58 @@ def ceiling(t, product): 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)) +def path_radius(part): + return min(plan_radius(part["line"]), part["radius"] if part["radius"] > 0 else math.inf) + + +def mark_diverging_paths(switches): + # the path that bends more + for t in switches: + radii = [path_radius(p) for p in t["paths"]] + t["bent"] = 1 if len(radii) < 2 or radii[1] <= radii[0] else 0 + + +class Route: + """How far along from the first point to the second a place lies, measured along the tracks.""" + + def __init__(self, tracks, joints, first, last): + self.first, self.last = first, last + self.from_first, self.from_last = distances(tracks, joints, first), distances(tracks, joints, last) + ends = [self.from_last[n] for n in self.from_last if math.dist(n, first) < 300] + self.length = 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") + + def share(self, part): + shares = [] + for key in ("p1", "p2"): + place = (part[key][0], part[key][2]) + node = (round(place[0], 1), round(place[1], 1)) + a, b = self.from_first.get(node), self.from_last.get(node) + if a is None or b is None: + a, b = math.dist(place, self.first), math.dist(place, self.last) + shares.append(min(1.0, max(0.0, (a - b + self.length) / (2 * self.length)))) + return 0.5 * sum(shares) + + +def grade_states(scenery, route, gain): cells = road_cells(scenery.roads) - normal = [t for t in tracks if t["kind"] == "normal"] - for t in tracks: + for t in scenery.tracks: part = t["paths"][0] - radius = min(plan_radius(part["line"]), part["radius"] if part["radius"] > 0 else math.inf) + radius = path_radius(part) 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) + plain = t["kind"] == "normal" + density = grass_on(part, scenery.plants) if plain else 0.0 + crossing = plain and hits_road(part, cells) + progress = route.share(part) if route else 0.5 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": + t.update(state=min(state, 1.2) * gain, curve=curve, crossing=crossing, grass=density) + if plain: 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) + + +def fit_under_pantograph(normal): # 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: @@ -404,43 +415,51 @@ def grade(scenery, options): low = middle else: high = middle - product = low for t in normal: - up = ceiling(t, product) + up = ceiling(t, low) if up < t["up"]: ratio = up / t["up"] t["up"], t["value"], t["side"] = up, t["value"] * ratio, t["side"] * ratio + return low + + +def curve_changes(t, joints): + part = t["paths"][0] + if abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP: + return True + 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: + return True + return False + + +def move_near_limit(normal, joints, share): # 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 "")): + if not (WOOD.search(t["ballast"] or "") or curve_changes(t, joints)): continue + part = t["paths"][0] name = f"{t['name']}:{part['p1'][0]:.0f}:{part['p1'][2]:.0f}" - if hashed(b"near", name) >= options.near: + if hashed(b"near", name) >= share: continue value = t["value"] + (limit(t["speed"]) - t["value"]) * (NEAR_LEAST + (NEAR_MOST - NEAR_LEAST) * hashed(b"how", name)) if value <= t["value"]: continue + # sideways and the cross level don't move a pantograph, up and down only as far as it keeps to the wire on this track 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"] + return chosen + + +def grade_switches(switches, joints, product): + # each path by the plain tracks it leads to, the worst of them; those only among switches from their neighbours for t in switches: t["values"] = [None] * len(t["paths"]) for index, part in enumerate(t["paths"]): @@ -459,6 +478,26 @@ def grade(scenery, options): 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"]] + + +def grade(scenery, options): + tracks = scenery.tracks + normal = [t for t in tracks if t["kind"] == "normal"] + switches = [t for t in tracks if t["kind"] == "switch"] + joints = Joints(tracks) + mark_diverging_paths(switches) + settle_speeds(tracks, joints) + grade_states(scenery, Route(tracks, joints, *options.route) if options.route else None, options.gain) + for t in normal: + t["heights"] = heights_per_mm(t) + product = fit_under_pantograph(normal) + chosen = move_near_limit(normal, joints, options.near) + 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 + grade_switches(switches, joints, product) return product, chosen, worst diff --git a/track_uneven_doc.md b/track_uneven_doc.md index 325e8944..c7446e34 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -1,6 +1,6 @@ -# Nierówności toru +# Track unevenness -Opcjonalny atrybut wpisu toru, przed `endtrack`: +An optional attribute of a track entry, before `endtrack`: ``` node -1 0 tor1 track normal 100.0 1.435 0.15 25.0 20 0 flat vis @@ -14,92 +14,94 @@ uneven 18;;;25;6 endtrack ``` -Liczby w mm, 0-100, rozdzielone średnikiem, bez spacji: +The numbers are in mm, 0-100, separated by semicolons with no spaces: -| 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 | +| position | `normal` track | `switch` | +|----------|-----------------------------------------------|--------------------------| +| 1 | peak deviation of a rail on a 10 m chord | the same, path p1-p2 | +| 2 | vertical | path p3-p4 | +| 3 | sideways | path p1-p2, vertical | +| 4 | cross level | path p1-p2, sideways | +| 5 | dip at rail joints, by default 0.4 of vertical | path p3-p4, vertical | +| 6 | | path p3-p4, sideways | -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`. +A position left out takes the first number, and the dip follows the vertical value. Positions at the end can be dropped. Sideways alone is `uneven 0;;30`, and dips at the joints alone are `uneven 0;;;;6`. A switch with no components of its own on path p3-p4 takes the values of path p1-p2. A switch for 100 km/h on the straight path and 40 km/h on the diverging one is, for example, `uneven 14;35`. -Wszystkie są też w edytorze, w panelu toru, w sekcji „Ride and sound”. Na drogach, rzekach i obrotnicach są ignorowane. +All of them are also in the editor, in the track panel, section "Ride and sound". On roads, rivers and turntables they are ignored. -## Jaką wartość wybrać +## Choosing a value -`uneven` odpowiada kolumnie „nierówności pionowe” z instrukcji PLK Id-14, załącznik 1 (klasa C): +`uneven` corresponds to the "vertical irregularities" column of the PLK Id-14 instruction, appendix 1 (class C): -| prędkość [km/h] | 20 | 40 | 60 | 80 | 100 | 120 | 160 | -|-----------------|----|----|----|----|-----|-----|-----| -| `uneven` [mm] | 50 | 35 | 25 | 18 | 14 | 10 | 6 | +| speed [km/h] | 20 | 40 | 60 | 80 | 100 | 120 | 160 | +|---------------|----|----|----|----|-----|-----|-----| +| `uneven` [mm] | 50 | 35 | 25 | 18 | 14 | 10 | 6 | -Tor w dobrym stanie to według instrukcji połowa tej wartości (klasa A), w dostatecznym 3/4 (klasa B). +According to the instruction, a track in good condition is half of this value (class A), and one in fair condition 3/4 (class B). -Tor z `uneven 18` mierzy się tak (`tests/unit/test_track_uneven.cpp`), w nawiasie limit Id-14 dla 80 km/h: +At these limits a pantograph may not keep up at speed, which trips the main breaker: see the end of "How it works". `tools/track_uneven_grade.py` keeps the values below that. -| parametr | wynik | Id-14 | -|---------------------------|---------|---------| -| nierówności pionowe | 18 mm | 18 mm | -| nierówności poziome | 19 mm | 17 mm | -| wichrowatość na bazie 5 m | 15 mm | 16 mm | -| szerokość toru | ±10 mm | +10/-8 mm | -| przechyłka | 20 mm | 20 mm | -| wskaźnik J | 6,1 | 5,3 | +A track with `uneven 18` measures as follows (`tests/unit/test_track_uneven.cpp`), against the Id-14 limit for 80 km/h: -## Jak to działa +| parameter | result | Id-14 | +|--------------------------|--------|-----------| +| vertical irregularities | 18 mm | 18 mm | +| horizontal irregularities | 19 mm | 17 mm | +| twist on a 5 m base | 15 mm | 16 mm | +| gauge | ±10 mm | +10/-8 mm | +| cross level | 20 mm | 20 mm | +| J index | 6.1 | 5.3 | -Nierówność to pole przesunięć zależne od położenia na mapie, złożone z fal o długości 5-42 m. W pionie krótkich fal jest mniej, żeby częściej dochodziły blisko wartości szczytowej, a do tego są fale 47-83 m: na cięciwie 10 m prawie się nie liczą, pudło unosi się na nich i opada, a pantograf za nimi nadąża. Nie ma w nim losowania: to samo miejsce wygina się tak samo przy każdym uruchomieniu. Przesunięty tor dostaje fale z nowego miejsca. +## How it works -Każda składowa łączy się osobno: +Unevenness is a displacement field that depends on the position on the map, made of waves 5-42 m long. Vertically there are fewer short waves, so the rails come near the peak value more often, and there are also waves of 47-83 m: they hardly count on the 10 m chord, the body rises and falls on them, and a pantograph keeps up with them. Nothing in it is random: one place bends the same way on every run. A track that is moved gets the waves of its new place. -- **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:** 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. +Each component joins separately: -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. +- **Track to track:** the lower of the two values applies at the joint, and each track comes to it smoothly over 20 m. A free end of a track stays as designed. +- **Switch:** each path has its own value, and the lower of the two applies at their common end. Directly joined switches take the lowest value of the same path, and where two paths meet, the lower of the two. A joined track comes to the value of the switch at the end it meets, even if that is higher than its own. A track with none of a component keeps the whole switch as designed in it. +- **Joints:** the dips come every `fSoundDistance`, over 6 m, and run on through following tracks with the same spacing. The wheel clatter falls where the dip is. With a spacing of 6 m or less, and on switches, there are no dips. +- **Cross level:** adds to the designed cant. It is a tilt of the map plan, so tracks laid towards each other meet at the same cant. On switches there is only the part coming from the waves, with no separate settling of the rails. -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. +The ends of joined paths meet in the middle between them, with one vector across, if they are less than 20 cm apart. This closes the cracks from coordinates that don't match in the data, on tracks with no unevenness as well. At such a joint a switch takes the cant of the plain track, and two tracks or two switches meet halfway. This applies to the physics too. -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. +The rails, the top of the ballast, `sleepermodel`, the axles, the roll of the body and the cab shake follow the unevenness. The slope of the ballast takes up the difference, and its foot stays as designed, on the ground. -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. +Models from includes (e.g. switch drives and timbers), the terrain and the catenary don't follow it, so at large values a rail shifts away from such a model by a few centimetres. -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. +On a track with unevenness the track itself shakes the cab. The sway calculated from the lateral acceleration, that is from the curve radius, and the random jolts at higher speed are off. On tracks with no unevenness they work as before. -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². +A pantograph rises at the rate set by the pressure, about 0.14 m/s, and a gap to the wire over 1 cm cuts the voltage and trips the main breaker. Unevenness adds to this at high speed. On the tracks of Bałtyk a simulation gives a gap of 0.6 cm when the vertical value times the speed in km/h is 925, for example 8 mm at 120 km/h, 13 mm at 70 km/h and 23 mm at 40 km/h. The values in the Id-14 table are higher than that. The wire alone can do it too: at `tor0290` on Bałtyk it rises by about 1%, so a pantograph keeps up to about 46 km/h there, and above that the breaker trips on a straight track as well. -## Ocena torów dowolnej scenerii +The physics takes the curve radius and the grade from the design, so unevenness doesn't change them. It adds only the cross level towards derailment, about 0.06 m/s² at `uneven 18` against a threshold of about 3.2 m/s². -`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. +## Grading the tracks of any scenery + +`tools/track_uneven_grade.py` works out the values from what the scenery shows: ballast and rail textures, grass on the track, curves, speed and road crossings. It reads a `.scn`, `.scm` or `.inc` file with its includes and leaves the originals as they are, writing copies of the files with tracks to `--out`. With no `--out` it only prints a report. ``` 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. +- `--route X,Z X,Z`: tracks get worse from the first point to the second, measured along the tracks. +- `--gain`: multiplier of the state of the tracks, 1 by default. +- `--near`: the share of tracks on wooden sleepers and at changes of curve that go nearer the Id-14 limit, 0.6 by default. The choice is fixed. +- `--straight`: names of tracks with no unevenness, separated by commas. +- `--root`: the `scenery` folder, when the given file lies deeper. -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. +The values stay under what a pantograph can follow, by a simulation over those tracks, but the rise of the wire itself isn't counted. Texture wear is read from the names, by the tables at the top of the script, and an unknown name gets 0.5. Tracks in `.inc` files taking parameters are skipped and `rotate` blocks aren't applied; the report says so in both cases. A track that already has unevenness stays as it is. -## Wydajność +## Performance -Tor z nierównością ma geometrię co 2 m zamiast co 10 m na prostych. Rozjazdy zachowują swój podział. +A track with unevenness has geometry every 2 m instead of every 10 m on straights. Switches keep their division. -Zmierzone (kamera w ruchu lub kabina, 1920×1080): +Measured (camera moving or in the cab, 1920×1080): -| scena | bez flag | z flagami 18/35 | -|---------------------------------------|--------------------|--------------------| -| Bałtyk, 56 km torów: wierzchołki | 0,56 mln (ok. 27 MB) | 2,17 mln (ok. 104 MB) | -| Bałtyk: najdłuższa klatka w przelocie | 31,5 ms | 64 ms | -| `td.scn`, jazda 120 km/h: klatka | 2,73 ms | 2,93 ms | +| scene | no flags | flags 18/35 | +|-------------------------------------|-----------------------|------------------------| +| Bałtyk, 56 km of tracks: vertices | 0.56 M (about 27 MB) | 2.17 M (about 104 MB) | +| Bałtyk: longest frame in a fly-through | 31.5 ms | 64 ms | +| `td.scn`, driving at 120 km/h: frame | 2.73 ms | 2.93 ms | -Koszt rośnie z liczbą oflagowanych torów, a najdłuższe klatki pojawiają się przy budowaniu geometrii kolejnych sekcji. Sama jazda po takim torze praktycznie nie kosztuje: ok. 0,3 µs na oś w kroku fizyki. +The cost grows with the number of flagged tracks, and the longest frames occur when the geometry of further sections is built. Driving on such a track costs practically nothing: about 0.3 µs per axle in a physics step. diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 27db3975..277fb421 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3676,23 +3676,7 @@ bool TDynamicObject::Update(double dt, double dt1) 1 : ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { - // on a track with dipping joints the clatter comes from them, elsewhere the axle counts a rail off itself - auto const railphase { Axle0.RailPhase( axle.offset - 0.5 * fAxleDist ) }; - auto crossed { false }; - if( railphase >= 0.0 ) { - // the rails under the axle, which aren't always those under the track the vehicle is on - crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * Axle0.GetTrack()->fSoundDistance; - axle.distance = railphase; - } - else { - axle.distance += dDOMoveLen * directioninconsist; - crossed = axle.distance < 0 || axle.distance > dRailLength; - if( crossed ) { - axle.distance = clamp_circular( axle.distance, dRailLength ); - } - } - axle.atjoints = ( railphase >= 0.0 ); - if( crossed ) { + if( axle_passed_joint( axle, dDOMoveLen, directioninconsist ) ) { if( MoverParameters->Vel > 0.1 ) { // NOTE: for combined clatter sound we supply 1/100th of actual value, as the sound module converts does the opposite, converting received (typically) 0-1 values to 0-100 range auto const frequency = true == axle.clatter.is_combined() ? MoverParameters->Vel * 0.01 : 1.0; @@ -8102,6 +8086,28 @@ TDynamicObject::update_shake( double const Timedelta ) { } } +bool +TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) { + + // by the outer axle on its side of the vehicle: the two can be on different tracks + auto const &outer { Axle.offset >= 0.0 ? Axle0 : Axle1 }; + auto const railphase { outer.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; + if( railphase >= 0.0 ) { + // the rails under the axle, which aren't always those under the track the vehicle is on + auto const passed { Axle.atjoints && std::abs( railphase - Axle.distance ) > 0.5 * outer.GetTrack()->fSoundDistance }; + Axle.distance = railphase; + Axle.atjoints = true; + return passed; + } + Axle.atjoints = false; + Axle.distance += Move * Direction; + if( Axle.distance >= 0 && Axle.distance <= dRailLength ) { + return false; + } + Axle.distance = clamp_circular( Axle.distance, dRailLength ); + return true; +} + std::pair TDynamicObject::shake_angles() const { diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index e0238c77..b62c9fd2 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -403,6 +403,8 @@ private: sound_source clatter; // clatter emitter bool atjoints { false }; // distance comes from TTrack::rail_phase }; + // whether the axle has just passed a rail joint. On a track with dipping joints it goes by them, elsewhere the axle counts a rail off itself + bool axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ); struct powertrain_sounds { sound_source inverter { sound_placement::engine }; diff --git a/world/Segment.cpp b/world/Segment.cpp index 379a944c..806caf6b 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -447,13 +447,12 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: 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 ) ); - position += stray( position, fDistance ); + position += track_uneven::across_only( stray( position, amount, fDistance ), across ); } -glm::dvec3 TSegment::stray( glm::dvec3 const &Point, double const Distance ) const { - auto const amount { pOwner->uneven_at( Distance, m_path ) }; +glm::dvec3 TSegment::stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const { auto const field { track_uneven::field( Point ) }; - glm::dvec3 result { field.x * amount.side, field.y * amount.up, field.z * amount.side }; + glm::dvec3 result { field.x * Amount.side, field.y * Amount.up, field.z * Amount.side }; result *= track_uneven::stray_per_mm; result.y -= pOwner->joint_dip( Distance ); return result; @@ -463,13 +462,25 @@ glm::dvec3 TSegment::uneven_bend( double const Distance ) const { if( false == m_uneven ) { return glm::dvec3 { 0.0 }; } - // over about a bogie; FastGetTFromS extrapolates past the ends of the path + // over about a bogie auto const reach { 1.5 }; auto const strayed = [&]( double const At ) { - return stray( FastGetPoint( FastGetTFromS( At ) ), At ); }; + return stray( point_extended( At ), pOwner->uneven_at( At, m_path ), At ); }; return ( strayed( Distance + reach ) - 2.0 * strayed( Distance ) + strayed( Distance - reach ) ) / ( reach * reach ); } +glm::dvec3 TSegment::point_extended( double const Distance ) const { + auto const on { std::clamp( Distance, 0.0, fLength ) }; + auto const t { FastGetTFromS( on ) }; + auto const point { FastGetPoint( t ) }; + if( on == Distance ) { + return point; + } + auto const direction { FastGetDirection( t, 0.1 / fLength ) }; + auto const size { glm::length( direction ) }; + return size > 0.0 ? point + direction * ( ( Distance - on ) / size ) : point; +} + void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origin, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const { if( false == m_uneven ) { return; @@ -478,8 +489,12 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi 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 ) }; 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 ) }; + if( share <= 0.0 ) { + return; + } + auto const amount { pOwner->uneven_at( Distance, m_path ) }; + Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * amount.cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); + Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, amount, Distance ) }; } void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { @@ -785,15 +800,15 @@ bool TSegment::RenderLoftSteps( gfx::vertex_array &Output, glm::dvec3 const &Ori } void TSegment::plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const { + if( fTsBuffer.empty() ) { + // never made into a path: tt_Cross and the like + Point = Point1; + Across = glm::dvec3 { 0.0 }; + return; + } auto const t { FastGetTFromS( std::clamp( Distance, 0.0, fLength ) ) }; Point = FastGetPoint( t ); - auto const dir { FastGetDirection( t, 0.1 / fLength ) }; - Across = glm::dvec3{ -dir.z, 0.0, dir.x }; - if( glm::length2( Across ) == 0.0 ) { - auto const chord { FastGetPoint_1() - FastGetPoint_0() }; - Across = glm::dvec3{ -chord.z, 0.0, chord.x }; - } - Across = glm::normalize( Across ); + Across = track_uneven::across_of( FastGetDirection( t, 0.1 / fLength ), FastGetPoint_1() - FastGetPoint_0() ); } bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, loft_span const &Span ) const { diff --git a/world/Segment.h b/world/Segment.h index e316ec1b..fb41173f 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" #include "rendering/geometrybank.h" +#include "world/TrackUneven.h" #include #include #include @@ -139,7 +140,9 @@ class TSegment // moves a lofted vertex with the unevenness of the track void settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origin, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const; // displacement of a point by the unevenness, at a distance along the path - glm::dvec3 stray( glm::dvec3 const &Point, double const Distance ) const; + glm::dvec3 stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const; + // a point of the path at a distance, past its ends straight on: the polynomial would run off on a short curved path + glm::dvec3 point_extended( double const Distance ) const; void design_position( double const fDistance, glm::dvec3 &position, glm::vec3 &rotation ) const; public: @@ -195,6 +198,9 @@ public: set_uneven( bool const Uneven, int const Path = 0 ) { m_uneven = Uneven; m_path = Path; } + glm::dvec3 + end_point( int const End ) const { + return End == 0 ? Point1 : Point2; } float design_roll( int const End ) const { return m_designrolls[ End ]; } diff --git a/world/Track.cpp b/world/Track.cpp index 751f2bc8..b6f1b2b9 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -87,9 +87,6 @@ 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 @@ -740,10 +737,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } } else if( str == "uneven" ) { - // uneven [;...]: see read_uneven and TrackUneven.h. The semicolons stay in the token + // uneven [;...]: see 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 ) { - read_uneven( values ); + m_uneven = track_uneven::read_attribute( values, eType == tt_Switch ); } } else if( str == "friction" ) { @@ -822,250 +819,6 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) // to a stale origin and the renderer would draw sleepers in the wrong place). } -void TTrack::read_uneven( std::string const &Text ) { - - 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 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; -} - -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 ) { - 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, glm::dvec3 const &Point ) -> track_uneven::amounts { - if( Other == nullptr ) { - return {}; - } - if( Other->eType == tt_Switch ) { - return Other->switch_uneven_at( Point ); - } - return track_uneven::least( own, Other->uneven_declared() ); }; - return along( own, atend( trPrev, Segment->FastGetPoint_0() ), atend( trNext, Segment->FastGetPoint_1() ), Length() ); -} - -TTrack::switch_stray const &TTrack::switch_uneven() const { - - if( m_switchstray.stamp == s_layoutstamp ) { - return m_switchstray; - } - std::vector group { this }; - 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; - } - if( other->eType == tt_Switch && other->SwitchExtension ) { - 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 - 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.paths = least; - track->m_switchstray.stamp = s_layoutstamp; - } - // 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 { - - if( eType != tt_Normal ) { - return 0.0; - } - return ( m_jointdip >= 0.f ? m_jointdip : track_uneven::dip_share * uneven_declared().up ); -} - -double TTrack::joint_dip( double const Distance ) const { - - auto const phase { rail_phase( Distance ) }; - if( phase < 0.0 ) { - return 0.0; - } - auto const fromjoint { std::min( phase, fSoundDistance - phase ) }; - if( fromjoint >= track_uneven::dip_reach ) { - return 0.0; - } - auto const depth { joint_depth() }; - auto const atend = [&]( TTrack const *Other ) { - return ( Other == nullptr || Other->fSoundDistance != fSoundDistance ? 0.0 : std::min( depth, Other->joint_depth() ) ); }; - return 0.001 * track_uneven::dip( fromjoint ) * track_uneven::along( depth, atend( trPrev ), atend( trNext ), Distance, Length() ); -} - -double TTrack::rail_phase( double const Distance ) const { - - if( joint_depth() <= 0.0 || fSoundDistance <= 2.0 * track_uneven::dip_reach ) { - return -1.0; - } - if( m_railrun.stamp != s_layoutstamp ) { - settle_rail_run(); - } - return track_uneven::rail_phase( m_railrun.origin + m_railrun.way * Distance, fSoundDistance ); -} - -void TTrack::settle_rail_run() const { - - // the next track of the run past an end of a track, and which of its ends is joined there - auto const onward = []( TTrack const &Track, int const End, int &Joined ) -> TTrack const * { - auto const *other { End == 0 ? Track.trPrev : Track.trNext }; - if( other == nullptr || other == &Track || other->joint_depth() <= 0.0 || other->fSoundDistance != Track.fSoundDistance ) { - return nullptr; - } - Joined = ( End == 0 ? Track.iPrevDirection : Track.iNextDirection ) & 1; - return other; }; - // walks from this track to an end of the run - auto const walk = [&]( int &End ) { - auto const *track { this }; - int joined { 0 }; - for( int step = 0; step < 1000000; ++step ) { - auto const *next { onward( *track, End, joined ) }; - if( next == nullptr || next == this ) { - break; - } - track = next; - End = 1 - joined; - } - return track; }; - auto const place = []( TTrack const &Track, int const End ) { - auto const point { End == 0 ? Track.Segment->FastGetPoint_0() : Track.Segment->FastGetPoint_1() }; - return std::make_pair( point.x, point.z ); }; - - // start at the end with the lesser coordinates, to get the same joints from any track of the run - int end { 0 }; - int otherend { 1 }; - auto const *track { walk( end ) }; - auto const *other { walk( otherend ) }; - if( place( *other, otherend ) < place( *track, end ) ) { - track = other; - end = otherend; - } - auto along { 0.0 }; - while( track != nullptr && track->m_railrun.stamp != s_layoutstamp ) { - auto const length { track->Length() }; - track->m_railrun = { end == 0 ? along : along + length, end == 0 ? 1.0 : -1.0, s_layoutstamp }; - along += length; - int joined { 0 }; - track = onward( *track, 1 - end, joined ); - end = joined; - } -} - void TTrack::update_location() { location( ( @@ -1813,73 +1566,6 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { create_geometry( Bank ); } -void TTrack::share_joints() { - - for( int path = 0; path < track_rails::path_count( *this ); ++path ) { - auto *segment { eType == tt_Switch ? SwitchExtension->Segments[ path ].get() : Segment.get() }; - if( segment == nullptr ) { - continue; - } - 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; - glm::dvec3 across; - segment->plan_frame( end ? segment->GetLength() : 0.0, point, across ); - auto closest { 0.2 }; - for( int theirpath = 0; theirpath < track_rails::path_count( *neighbour ); ++theirpath ) { - auto const *theirs { neighbour->eType == tt_Switch ? neighbour->SwitchExtension->Segments[ theirpath ].get() : neighbour->Segment.get() }; - if( theirs == nullptr ) { - continue; - } - for( int theirend = 0; theirend < 2; ++theirend ) { - glm::dvec3 theirpoint; - glm::dvec3 theiracross; - theirs->plan_frame( theirend ? theirs->GetLength() : 0.0, theirpoint, theiracross ); - if( theirend == end ) { - theiracross = -theiracross; - } - auto const gap { glm::length( theirpoint - point ) }; - if( gap >= closest || glm::dot( across, theiracross ) < 0.9 ) { - continue; - } - 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, 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 ); - } - } -} - void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { gfx::userdata_array empty_userdata; // the rails of a frog with a movable nose are laid again as it moves over, in the same chunks @@ -3136,7 +2822,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; } - Output << uneven_text(); + Output << track_uneven::write_attribute( m_uneven, eType == tt_Switch ); if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } diff --git a/world/Track.h b/world/Track.h index 458abec5..8f6a8e32 100644 --- a/world/Track.h +++ b/world/Track.h @@ -287,16 +287,7 @@ public: TEnvironmentType eEnvironment = e_flat; // dźwięk i oświetlenie int iAction = 0; // czy modyfikowany eventami (specjalna obsługa przy skanowaniu) float fOverhead = -1.0; // można normalnie pobierać prąd (0 dla jazdy bezprądowej po danym odcinku, >0-z opuszczonym i ograniczeniem prędkości) - float m_uneven { 0.f }; // mm, see TrackUneven.h; plain tracks and switches - // the components set apart from m_uneven, mm; negative: m_uneven. Cross level on plain tracks only - 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 + track_uneven::attribute m_uneven; // plain tracks and switches private: double fVelocity = -1.0; // ograniczenie prędkości // prędkość dla AI (powyżej rośnie prawdopowobieństwo wykolejenia) public: @@ -417,10 +408,10 @@ public: void VelocitySet(float v); double VelocityGet(); float Friction() 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; + // a switch has two paths, any other track the one + int path_count() const; + TSegment *path_segment( int const Path ) const; + TTrack *path_neighbour( int const Path, int const End ) 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; @@ -483,6 +474,11 @@ private: }; mutable switch_stray m_switchstray; switch_stray const &switch_uneven() const; + void settle_switch_stray() const; + // the lowest value of the paths ending at the point + track_uneven::amounts switch_paths_at( glm::dvec3 const &Point ) const; + // what a plain track comes to at its end, where it meets the other one + track_uneven::amounts uneven_met( TTrack const *Other, glm::dvec3 const &Point ) 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 diff --git a/world/TrackUneven.cpp b/world/TrackUneven.cpp new file mode 100644 index 00000000..c5942964 --- /dev/null +++ b/world/TrackUneven.cpp @@ -0,0 +1,345 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "world/Track.h" + +// The part of TTrack that goes with the unevenness of a track and with what it meets at its ends: joined tracks and switches. +// The rules themselves are in TrackUneven.h, here they get applied to the tracks. + +namespace { + +double const switch_end_reach { 0.3 }; // how near an end of a path counts as being at a point, m +double const joint_reach { 0.2 }; // ends of joined paths further apart than this stay as they are, m +double const joint_alike { 0.9 }; // ends of joined paths less alike in direction than this stay as they are, cosine + +bool is_switch( TTrack const *Track ) { + + return Track != nullptr && Track->eType == tt_Switch && Track->SwitchExtension; +} + +std::array joined_to_switch( TTrack const &Switch ) { + + auto const &extension { *Switch.SwitchExtension }; + return { extension.pPrevs[ 0 ], extension.pPrevs[ 1 ], extension.pNexts[ 0 ], extension.pNexts[ 1 ] }; +} + +// the switch and the switches joined directly to it, and to those +std::vector switch_group( TTrack const &Switch ) { + + std::vector group { &Switch }; + for( std::size_t index = 0; index < group.size(); ++index ) { + for( auto const *other : joined_to_switch( *group[ index ] ) ) { + if( is_switch( other ) && std::find( group.begin(), group.end(), other ) == group.end() ) { + group.push_back( other ); + } + } + } + return group; +} + +// the value along each path: the lowest of that path in the group, 0 in a component that a plain track joined has none of +std::array group_path_values( std::vector const &Group ) { + + std::array values { Group.front()->uneven_declared( 0 ), Group.front()->uneven_declared( 1 ) }; + for( auto const *track : Group ) { + values[ 0 ] = track_uneven::least( values[ 0 ], track->uneven_declared( 0 ) ); + values[ 1 ] = track_uneven::least( values[ 1 ], track->uneven_declared( 1 ) ); + for( auto const *other : joined_to_switch( *track ) ) { + if( other == nullptr || is_switch( other ) ) { + continue; + } + // any other comes to the value of the switch + auto const theirs { other->uneven_declared() }; + for( auto &path : values ) { + if( theirs.up <= 0.0 ) { path.up = 0.0; } + if( theirs.side <= 0.0 ) { path.side = 0.0; } + } + } + } + return values; +} + +// the lowest Value( path, end ) of the ends of the paths at the point; Otherwise when none of them is there +template +track_uneven::amounts least_at_ends( TTrack const &Switch, glm::dvec3 const &Point, track_uneven::amounts const &Otherwise, Value_ const &Value ) { + + auto found { false }; + track_uneven::amounts result { track_uneven::most, track_uneven::most, track_uneven::most }; + for( int path = 0; path < 2; ++path ) { + for( int end = 0; end < 2; ++end ) { + if( glm::length( Switch.path_segment( path )->end_point( end ) - Point ) < switch_end_reach ) { + result = track_uneven::least( result, Value( path, end ) ); + found = true; + } + } + } + return found ? result : Otherwise; +} + +// how the end of a path meets the track joined to it +struct path_joint { + std::optional> place; // none: too far apart, or going different ways + float roll; +}; + +path_joint meet( TTrack const &Track, TSegment const &Segment, int const End, TTrack const &Neighbour ) { + + path_joint result { {}, Segment.design_roll( End ) }; + glm::dvec3 point; + glm::dvec3 across; + Segment.plan_frame( End ? Segment.GetLength() : 0.0, point, across ); + auto closest { joint_reach }; + for( int path = 0; path < Neighbour.path_count(); ++path ) { + auto const *theirs { Neighbour.path_segment( path ) }; + if( theirs == nullptr ) { + continue; + } + for( int end = 0; end < 2; ++end ) { + // laid towards each other, they look across and lean to opposite sides + auto const opposite { end == End }; + glm::dvec3 theirpoint; + glm::dvec3 theiracross; + theirs->plan_frame( end ? theirs->GetLength() : 0.0, theirpoint, theiracross ); + if( opposite ) { + theiracross = -theiracross; + } + auto const gap { glm::length( theirpoint - point ) }; + if( gap >= closest || glm::dot( across, theiracross ) < joint_alike ) { + continue; + } + closest = gap; + result.place.emplace( 0.5 * ( point + theirpoint ), glm::normalize( across + theiracross ) ); + if( Track.iCategoryFlag & 1 ) { + result.roll = track_uneven::joint_roll( + Segment.design_roll( End ), + opposite ? -theirs->design_roll( end ) : theirs->design_roll( end ), + Track.eType == tt_Switch, + Neighbour.eType == tt_Switch ); + } + } + } + return result; +} + +} // namespace + +int TTrack::path_count() const { + + return eType == tt_Switch ? 2 : 1; +} + +TSegment *TTrack::path_segment( int const Path ) const { + + return eType == tt_Switch ? SwitchExtension->Segments[ Path ].get() : Segment.get(); +} + +TTrack *TTrack::path_neighbour( int const Path, int const End ) const { + + if( eType == tt_Switch ) { + return End == 0 ? SwitchExtension->pPrevs[ Path ] : SwitchExtension->pNexts[ Path ]; + } + return End == 0 ? trPrev : trNext; +} + +track_uneven::amounts TTrack::uneven_declared( int const Path ) const { + + return track_uneven::declared( m_uneven, eType == tt_Normal, Path ); +} + +bool TTrack::is_uneven() const { + + 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; +} + +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 { + + if( eType == tt_Switch ) { + auto const &stray { switch_uneven() }; + return track_uneven::along( stray.paths[ Path ], stray.ends[ Path ][ 0 ], stray.ends[ Path ][ 1 ], Distance, path_segment( Path )->GetLength() ); + } + return track_uneven::along( uneven_declared(), uneven_met( trPrev, Segment->end_point( 0 ) ), uneven_met( trNext, Segment->end_point( 1 ) ), Distance, Length() ); +} + +track_uneven::amounts TTrack::uneven_met( TTrack const *Other, glm::dvec3 const &Point ) const { + + if( Other == nullptr ) { + return {}; + } + return Other->eType == tt_Switch ? Other->switch_uneven_at( Point ) : track_uneven::least( uneven_declared(), Other->uneven_declared() ); +} + +TTrack::switch_stray const &TTrack::switch_uneven() const { + + if( m_switchstray.stamp != s_layoutstamp ) { + settle_switch_stray(); + } + return m_switchstray; +} + +void TTrack::settle_switch_stray() const { + + auto const group { switch_group( *this ) }; + auto const values { group_path_values( group ) }; + for( auto const *track : group ) { + track->m_switchstray.paths = values; + track->m_switchstray.stamp = s_layoutstamp; + } + // at the ends of the paths, with the paths known in all of the group + for( auto const *track : group ) { + for( int path = 0; path < 2; ++path ) { + for( int end = 0; end < 2; ++end ) { + auto const point { track->path_segment( path )->end_point( end ) }; + auto value { track->switch_paths_at( point ) }; + auto const *other { track->path_neighbour( path, end ) }; + if( is_switch( other ) ) { + value = track_uneven::least( value, other->switch_paths_at( point ) ); + } + track->m_switchstray.ends[ path ][ end ] = value; + } + } + } +} + +track_uneven::amounts TTrack::switch_paths_at( glm::dvec3 const &Point ) const { + + auto const &paths { m_switchstray.paths }; + return least_at_ends( *this, Point, track_uneven::least( paths[ 0 ], paths[ 1 ] ), [ & ]( int const Path, int ) { return paths[ Path ]; } ); +} + +track_uneven::amounts TTrack::switch_uneven_at( glm::dvec3 const &Point ) const { + + auto const &stray { switch_uneven() }; + return least_at_ends( *this, Point, track_uneven::least( stray.paths[ 0 ], stray.paths[ 1 ] ), [ & ]( int const Path, int const End ) { return stray.ends[ Path ][ End ]; } ); +} + +double TTrack::joint_depth() const { + + if( eType != tt_Normal ) { + return 0.0; + } + return ( m_uneven.dip >= 0.f ? m_uneven.dip : track_uneven::dip_share * uneven_declared().up ); +} + +double TTrack::joint_dip( double const Distance ) const { + + auto const phase { rail_phase( Distance ) }; + if( phase < 0.0 ) { + return 0.0; + } + auto const fromjoint { std::min( phase, fSoundDistance - phase ) }; + if( fromjoint >= track_uneven::dip_reach ) { + return 0.0; + } + auto const depth { joint_depth() }; + auto const atend = [&]( TTrack const *Other ) { + return ( Other == nullptr || Other->fSoundDistance != fSoundDistance ? 0.0 : std::min( depth, Other->joint_depth() ) ); }; + return 0.001 * track_uneven::dip( fromjoint ) * track_uneven::along( depth, atend( trPrev ), atend( trNext ), Distance, Length() ); +} + +double TTrack::rail_phase( double const Distance ) const { + + if( joint_depth() <= 0.0 || fSoundDistance <= 2.0 * track_uneven::dip_reach ) { + return -1.0; + } + if( m_railrun.stamp != s_layoutstamp ) { + settle_rail_run(); + } + return track_uneven::rail_phase( m_railrun.origin + m_railrun.way * Distance, fSoundDistance ); +} + +void TTrack::settle_rail_run() const { + + // the next track of the run past an end of a track, and which of its ends is joined there + auto const onward = []( TTrack const &Track, int const End, int &Joined ) -> TTrack const * { + auto const *other { End == 0 ? Track.trPrev : Track.trNext }; + if( other == nullptr || other == &Track || other->joint_depth() <= 0.0 || other->fSoundDistance != Track.fSoundDistance ) { + return nullptr; + } + Joined = ( End == 0 ? Track.iPrevDirection : Track.iNextDirection ) & 1; + return other; }; + // walks from this track to an end of the run + auto const walk = [&]( int &End ) { + auto const *track { this }; + int joined { 0 }; + for( int step = 0; step < 1000000; ++step ) { + auto const *next { onward( *track, End, joined ) }; + if( next == nullptr || next == this ) { + break; + } + track = next; + End = 1 - joined; + } + return track; }; + auto const place = []( TTrack const &Track, int const End ) { + auto const point { Track.Segment->end_point( End ) }; + return std::make_pair( point.x, point.z ); }; + + // start at the end with the lesser coordinates, to get the same joints from any track of the run + int end { 0 }; + int otherend { 1 }; + auto const *track { walk( end ) }; + auto const *other { walk( otherend ) }; + if( place( *other, otherend ) < place( *track, end ) ) { + track = other; + end = otherend; + } + auto along { 0.0 }; + while( track != nullptr && track->m_railrun.stamp != s_layoutstamp ) { + auto const length { track->Length() }; + track->m_railrun = { end == 0 ? along : along + length, end == 0 ? 1.0 : -1.0, s_layoutstamp }; + along += length; + int joined { 0 }; + track = onward( *track, 1 - end, joined ); + end = joined; + } + // tracks joined one way only can leave this one out of the run: it stands on its own then, not to walk all of it again on every call + if( m_railrun.stamp != s_layoutstamp ) { + m_railrun = { 0.0, 1.0, s_layoutstamp }; + } +} + +void TTrack::share_joints() { + + for( int path = 0; path < path_count(); ++path ) { + auto *segment { path_segment( path ) }; + if( segment == nullptr ) { + continue; + } + for( int end = 0; end < 2; ++end ) { + auto const *neighbour { path_neighbour( path, end ) }; + auto const joined { neighbour != nullptr && neighbour != this && ( neighbour->iCategoryFlag & 15 ) == ( iCategoryFlag & 15 ) }; + auto const joint { joined ? meet( *this, *segment, end, *neighbour ) : path_joint { {}, segment->design_roll( end ) } }; + segment->set_joint( end, joint.place, 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 ); + } + } +} diff --git a/world/TrackUneven.h b/world/TrackUneven.h index f4a50eb7..624e6db1 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -87,6 +87,68 @@ std::string write_fields( std::array const &Fields ) { return text.str(); } +// the values of the attribute, mm, negative where not given. Cross level and the dip at joints for plain tracks, the path p3-p4 for switches +struct attribute { + float value { 0.f }; + float up { -1.f }; + float side { -1.f }; + float cross { -1.f }; + float dip { -1.f }; + float branch { -1.f }; + float branchup { -1.f }; + float branchside { -1.f }; +}; + +// a plain track: value;up;side;cross;dip. A switch: p1-p2;p3-p4;up of p1-p2;side of p1-p2;up of p3-p4;side of p3-p4 +inline attribute read_attribute( std::string const &Text, bool const Switch ) { + + auto const fields { read_fields<6>( Text ) }; + attribute result; + result.value = std::max( fields[ 0 ], 0.f ); + if( Switch ) { + result.branch = fields[ 1 ]; + result.up = fields[ 2 ]; + result.side = fields[ 3 ]; + result.branchup = fields[ 4 ]; + result.branchside = fields[ 5 ]; + } + else { + result.up = fields[ 1 ]; + result.side = fields[ 2 ]; + result.cross = fields[ 3 ]; + result.dip = fields[ 4 ]; + } + return result; +} + +// in the layout of the entry it goes into, which can be of another type than the track the values come from +inline std::string write_attribute( attribute const &Values, bool const Switch ) { + + auto const fields { Switch ? + std::array { Values.value, Values.branch, Values.up, Values.side, Values.branchup, Values.branchside } : + std::array { Values.value, Values.up, Values.side, Values.cross, Values.dip, -1.f } }; + if( Values.value <= 0.f && std::all_of( fields.begin() + 1, fields.end(), []( float const Field ) { return Field < 0.f; } ) ) { + return {}; + } + return "uneven " + write_fields( fields ) + ' '; +} + +// what a path comes to from the values: what's not given takes the first value, p3-p4 those of p1-p2. Cross level on plain tracks only +inline amounts declared( attribute const &Values, bool const Plain, int const Path ) { + + amounts result { + Values.up >= 0.f ? Values.up : Values.value, + Values.side >= 0.f ? Values.side : Values.value, + false == Plain ? 0.f : Values.cross >= 0.f ? Values.cross : Values.value }; + if( Path == 1 ) { + auto const branch = [ &Values ]( float const Part, double const Straight ) -> double { + return Part >= 0.f ? Part : Values.branch >= 0.f ? Values.branch : Straight; }; + result.up = branch( Values.branchup, result.up ); + result.side = branch( Values.branchside, result.side ); + } + return result; +} + template using wave_set = std::array; @@ -162,6 +224,40 @@ inline double bed_share( double const Depth, double const Rail, double const Hei return Height > 0.0 ? std::clamp( 1.0 - ( Depth - Rail ) / Height, 0.0, 1.0 ) : 1.0; } +// a displacement with the part along the track taken out, Across being a unit vector in the plan. Along the track +// it would only change the distance between coupled vehicles, and with it the forces between them +inline glm::dvec3 across_only( glm::dvec3 const &Offset, glm::dvec3 const &Across ) { + + return Across * glm::dot( Offset, Across ) + glm::dvec3 { 0.0, Offset.y, 0.0 }; +} + +// the unit vector across a direction in the plan, or across the chord if it has none; zero for a path of no length, which has neither +inline glm::dvec3 across_of( glm::dvec3 const &Direction, glm::dvec3 const &Chord ) { + + for( auto const &along : { Direction, Chord } ) { + glm::dvec3 const across { -along.z, 0.0, along.x }; + if( glm::dot( across, across ) > 0.0 ) { + return glm::normalize( across ); + } + } + return glm::dvec3 { 0.0 }; +} + +// the cant where two paths meet, theirs turned to the way of this one: a switch takes that of a plain track, alike ones meet halfway +inline float joint_roll( float const Own, float const Their, bool const Switch, bool const Theirswitch ) { + + return Switch == Theirswitch ? 0.5f * ( Own + Their ) : Switch ? Their : Own; +} + +// each component the same way +inline amounts along( amounts const &Full, amounts const &Atstart, amounts const &Atend, double const Distance, double const Length ) { + + return { + along( Full.up, Atstart.up, Atend.up, Distance, Length ), + along( Full.side, Atstart.side, Atend.side, Distance, Length ), + along( Full.cross, Atstart.cross, Atend.cross, Distance, Length ) }; +} + // distance from the joint behind the point inline double rail_phase( double const Along, double const Rail ) {