From 619f048f3118672f6a839593f6a93f17d66a8495 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 01:12:06 +0100 Subject: [PATCH] =?UTF-8?q?Sie=C4=87=20trakcyjna:=20kotwienia,=20kotwienie?= =?UTF-8?q?=20=C5=9Brodkowe,=20parasole=20i=20wysi=C4=99gniki=20rozstawion?= =?UTF-8?q?e,=20konsole=204890/4690?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sam kod exe z gałęzi siec-trakcyjna od poprzedniego commita (bez testów, notatek i danych z tests/catenary). Dane (data/traction_*.txt) leżą poza repozytorium. - kotwienia ciężarowe i stałe (0404/0405, przewody zakotwione osobno, sieci bez własnych danych: network as ) - kotwienie środkowe 0406/0408/0418/0416: lina, V i zaciski na linie nośnej liczonej jak przęsło, obejma 4160 na teownikach - parasole i wysięgniki rozstawione na konsolach 4890, wysięgi pomocnicze 0167 na belkach 4690, trzeci wysięgnik w osi słupa, lina nośna z innego wysięgnika (messenger_from) - przęsła w nietypowych sytuacjach nie przechodzą na stary wygląd (bez Y, bez wieszaków, lina nad przewodem) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GfTogWDsfCE8RGFGvm2eyp --- application/editormodecatenary.cpp | 1627 ++++++++++++++++++++++++++-- application/editormodecatenary.h | 56 +- lang/editor_pl.po | 357 ++++++ rendering/opengl33renderer.cpp | 7 + rendering/openglrenderer.cpp | 7 + world/Traction.cpp | 18 +- world/catenary_cantilever.cpp | 71 +- world/catenary_cantilever.h | 18 + world/catenary_network.cpp | 65 +- world/catenary_network.h | 5 + world/catenary_scene.cpp | 999 ++++++++++++++++- world/catenary_scene.h | 21 + world/catenary_span.cpp | 51 +- world/catenary_span.h | 14 +- world/catenary_suspension.cpp | 53 +- world/catenary_suspension.h | 33 +- world/catenary_tensioner.cpp | 970 +++++++++++++++-- world/catenary_tensioner.h | 150 ++- 18 files changed, 4237 insertions(+), 285 deletions(-) diff --git a/application/editormodecatenary.cpp b/application/editormodecatenary.cpp index 4286ec23..261acfb8 100644 --- a/application/editormodecatenary.cpp +++ b/application/editormodecatenary.cpp @@ -45,6 +45,8 @@ double const kSpanSearch{150.0}; // m, the farthest a neighbouring pole of the s double const kSameLine{1.5}; // m across the span, farther the two poles belong to different tracks double const kCurveRadius{4000.0}; // m, a track bent less is a straight for the cards (table 0007) float const kPolePick{12.0f}; +double const kInsertWidth{1.5}; // m in the plan from the line of a span: a pole placed there goes into the span +double const kInsertEnds{2.0}; // m from the ends of the span where it doesn't double const kSwitchRange{8.0}; // m from a pole to a switch path: the pole stands by the switch (also on the neighbouring track) double nearest_parameter(TTrack const &Track, int const Path, glm::dvec3 const &Point) @@ -61,7 +63,18 @@ double nearest_parameter(TTrack const &Track, int const Path, glm::dvec3 const & parameter = k / 256.0; } } - return parameter; + // on a long path a step of the search is metres: refined around the best one, so that a point square to the track (a pole + // given by another cantilever on it) gets its own foot on the track and not one a metre off + auto low{std::max(0.0, parameter - 1.0 / 256.0)}, high{std::min(1.0, parameter + 1.0 / 256.0)}; + for (int k = 0; k < 40; ++k) + { + auto const a{low + (high - low) / 3.0}, b{high - (high - low) / 3.0}; + if (geometry::plan_distance(curve.point(a), Point) < geometry::plan_distance(curve.point(b), Point)) + high = b; + else + low = a; + } + return 0.5 * (low + high); } glm::dvec2 forward_at(TTrack const &Track, int const Path, double const Parameter) @@ -87,6 +100,23 @@ glm::dvec3 wire_of(catenary::suspension const &Data) return {Data.at[0], Data.at[1], Data.at[2]}; } +// the suspensions on the pole of this one (the same type at the same point, as the scenery pairs an umbrella): this one too +std::vector pole_sharers(std::size_t const Index) +{ + std::vector list; + auto const *own{catenary_scene_get(Index)}; + if (own == nullptr || false == own->has_pole) + return list; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + if (data != nullptr && data->has_pole && data->pole_type == own->pole_type + && glm::length(glm::dvec3{data->pole[0] - own->pole[0], data->pole[1] - own->pole[1], data->pole[2] - own->pole[2]}) < 0.05) + list.push_back(index); + } + return list; +} + // from the pole towards the wires, in the plan; across the track glm::dvec2 across_of(catenary::suspension const &Data) { @@ -404,6 +434,11 @@ bool catenary_editor::catenary_spot_at(TTrack &Track, int const Path, glm::dvec3 auto const after{curve.point(std::min(1.0, parameter + step))}; double a[2]{before.x, before.z}, b[2]{Spot.axis.x, Spot.axis.z}, c[2]{after.x, after.z}; Spot.radius = catenary::plan_radius(a, b, c); + { + // the centre of the bend is on the side of the chord's middle + auto const bend{glm::dot(0.5 * (plan_of(before) + plan_of(after)) - plan_of(Spot.axis), right)}; + Spot.inward = std::abs(bend) < 1e-6 ? 0 : bend > 0.0 ? 1 : -1; + } switch (m_catenary.bend) { case 1: Spot.curve = false; break; @@ -812,6 +847,32 @@ bool catenary_editor::catenary_switch_error(std::size_t const Index) const return check.error; } +double catenary_editor::catenary_cant(catenary_spot const &Spot) +{ + if (Spot.track == nullptr || Spot.path < 0 || Spot.path >= static_cast(Spot.track->m_paths.size())) + return 0.0; + auto const &rolls{Spot.track->m_paths[Spot.path].rolls}; + return std::abs(glm::radians(glm::mix(static_cast(rolls[0]), static_cast(rolls[1]), std::clamp(Spot.parameter, 0.0, 1.0)))); +} + +glm::dvec3 catenary_editor::catenary_cant_shift(catenary_spot const &Spot, double const Height) +{ + auto const cant{catenary_cant(Spot)}; + if (cant < 1e-4) + return glm::dvec3{0.0}; + // towards the centre of the bend; on a cant without a bend (shouldn't be) by the sign of the roll: a positive one lifts the left rail + auto side{Spot.inward}; + if (side == 0) + { + auto const &rolls{Spot.track->m_paths[Spot.path].rolls}; + side = glm::mix(rolls[0], rolls[1], static_cast(Spot.parameter)) > 0.0f ? 1 : -1; + } + auto const right{right_of(Spot.forward) * static_cast(side)}; + auto const shift{Height * std::sin(cant)}; + // the rail heads: the lower rail stays at the points of the track, the middle goes up by half the cant (TSegment::Init) + return glm::dvec3{right.x * shift, 0.75 * std::sin(cant), right.y * shift}; +} + void catenary_editor::catenary_compose(catenary_spot &Spot, double const Clearance, double const Stagger, bool const Toward, std::string const &Pole) const { auto const &tool{m_catenary}; @@ -832,8 +893,9 @@ void catenary_editor::catenary_compose(catenary_spot &Spot, double const Clearan auto const axisdistance{Clearance + 0.5 * pole->depth(0.0)}; auto const offset{Toward ? Stagger : -Stagger}; auto const polepoint{glm::dvec3{Spot.axis.x, railhead, Spot.axis.z} + out * axisdistance}; - auto const wire{glm::dvec3{Spot.axis.x, railhead + height, Spot.axis.z} + out * offset}; - Spot.reach = axisdistance - offset - 0.5 * pole->depth(height); + // on a superelevated track the stagger is measured from the leaning axis of the track (table 23-0007) + auto const wire{glm::dvec3{Spot.axis.x, railhead + height, Spot.axis.z} + catenary_cant_shift(Spot, height) + out * offset}; + Spot.reach = glm::dot(plan_of(polepoint) - plan_of(wire), plan_of(out)) - 0.5 * pole->depth(height); // the card: from the group of the described catalogue, or (without it) from the tubular families 11xx/12xx catenary::cantilever_scheme const *scheme{nullptr}; auto const &structures{catenary_scene_structures()}; @@ -935,7 +997,8 @@ catenary_editor::catenary_spot const *catenary_editor::catenary_spot_of(std::siz } // what the suspension has, not what the panel would make auto const right{right_of(spot.forward) * static_cast(spot.side)}; - spot.toward = glm::dot(plan_of(wire) - plan_of(spot.axis), right) >= 0.0; + auto const lean{catenary_cant_shift(spot, wire.y - (spot.axis.y + kRailTop))}; + spot.toward = glm::dot(plan_of(wire) - plan_of(spot.axis + lean), right) >= 0.0; spot.data = *data; spot.card = data->card; auto const decade{(data->card / 10) % 10}; @@ -946,7 +1009,9 @@ catenary_editor::catenary_spot const *catenary_editor::catenary_spot_of(std::siz std::string catenary_editor::catenary_intent_label(catenary_spot const &Spot) const { - auto const where{Spot.curve ? format(STR_C("curve R %.0f m"), Spot.radius) : std::string{STR("straight")}}; + auto where{Spot.curve ? format(STR_C("curve R %.0f m"), Spot.radius) : std::string{STR("straight")}}; + if (auto const cant{catenary_cant(Spot)}; cant >= 1e-4) + where += format(STR_C(", superelevation %.0f mm"), 1500.0 * std::sin(cant)); auto const offset{Spot.toward ? STR("towards the pole") : STR("away from the pole")}; if (Spot.card == 0) return format(STR_C("Pole here, %s, wires %s: %s"), where.c_str(), offset.c_str(), Spot.problem.c_str()); @@ -976,7 +1041,13 @@ int catenary_editor::catenary_hit() const auto const foot{pole_of(*data)}; if (glm::distance(foot, camera) > kCatenaryRange) continue; - auto const distance{mouse_to_segment(projection, foot, foot + glm::dvec3{0.0, pole_top(*data), 0.0})}; + // the pole, or the cantilever from it to the wires (two cantilevers of an umbrella or spread ones share one pole: the + // cantilever tells them apart, and wins a tie with the pole) + auto const wire{wire_of(*data)}; + auto const plane{data->has_spread ? glm::dvec3{data->spread[0], foot.y, data->spread[2]} : foot}; + glm::dvec3 const root{plane.x, wire.y, plane.z}; + auto const arm{mouse_to_segment(projection, root, root + (wire - root) * 0.85)}; // not up to the wires: the span is picked there + auto const distance{std::min(mouse_to_segment(projection, foot, foot + glm::dvec3{0.0, pole_top(*data), 0.0}), arm * 0.999f)}; if (distance < best) { best = distance; @@ -1015,9 +1086,36 @@ int catenary_editor::catenary_span_hit() const return result; } +// the anchor whose device is under the cursor: the line from the face of the pole to the moving pulley, or the column of weights +int catenary_editor::catenary_anchor_hit() const +{ + screen_projection const projection; + auto best{kPolePick * kPolePick * 0.5f}; + int result{-1}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (std::size_t index = 0; index < catenary_anchor_size(); ++index) + { + double shape[4][3]; + if (false == catenary_anchor_shape(index, shape)) + continue; + glm::dvec3 const face{shape[0][0], shape[0][1], shape[0][2]}, lever{shape[1][0], shape[1][1], shape[1][2]}; + glm::dvec3 const top{shape[2][0], shape[2][1], shape[2][2]}, bottom{shape[3][0], shape[3][1], shape[3][2]}; + if (glm::distance(face, camera) > kCatenaryRange) + continue; + auto const distance{std::min(mouse_to_segment(projection, face, lever), mouse_to_segment(projection, top, bottom))}; + if (distance < best) + { + best = distance; + result = static_cast(index); + } + } + return result; +} + void catenary_editor::catenary_select(int const Index) { auto &tool{m_catenary}; + tool.anchor_selected = -1; tool.selected = Index; tool.probed = false; auto const *data{Index >= 0 ? catenary_scene_get(static_cast(Index)) : nullptr}; @@ -1089,6 +1187,9 @@ void catenary_editor::catenary_apply(catenary_spot const &Spot) data.skin = current->skin; data.overrides = current->overrides; data.umbrella = current->umbrella; + data.has_spread = current->has_spread; + data.shared_pole = current->shared_pole; + std::copy(std::begin(current->spread), std::end(current->spread), data.spread); // dragging a slider changes it many times: all of that is taken back at once auto const &last{tool.undo.empty() ? catenary_change{} : tool.undo.back()}; if (tool.undo.empty() || last.chain || last.index != index || last.removed || false == last.before.has_value()) @@ -1128,6 +1229,8 @@ void catenary_editor::catenary_copy(std::vector const &Targets) catenary_compose(spot, glm::dot(plan_of(pole_of(*current)) - plan_of(spot.axis), right) - 0.5 * own->depth(0.0), std::abs(offset), offset >= 0.0, current->pole_type); std::copy(std::begin(current->pole), std::end(current->pole), spot.data.pole); } + else if (false == drop && pole_sharers(index).size() > 1) + catenary_keep_pole(spot, *current, std::abs(offset), offset >= 0.0); else catenary_compose(spot, tool.clearance, std::abs(offset), offset >= 0.0, drop ? current->pole_type : std::string{}); if (drop) @@ -1144,6 +1247,9 @@ void catenary_editor::catenary_copy(std::vector const &Targets) data.skin = current->skin; data.overrides = current->overrides; data.umbrella = current->umbrella; + data.has_spread = current->has_spread; + data.shared_pole = current->shared_pole; + std::copy(std::begin(current->spread), std::end(current->spread), data.spread); tool.undo.push_back({tool.group, false, index, *current, std::nullopt, false}); catenary_scene_update(index, data); tool.spots[index] = spot; @@ -1194,12 +1300,108 @@ bool catenary_editor::catenary_propose_toward(bool &Toward) return true; } +bool catenary_editor::catenary_insert_at(catenary_spot const &Spot, chain_insert &Insert) const +{ + auto const &tool{m_catenary}; + if (false == tool.insert_spans || false == Spot.data.has_at || (tool.page == 1 && tool.gantry_selected >= 0)) + return false; + auto const wire{plan_of(wire_of(Spot.data))}; + glm::dvec3 const camera{Global.pCamera.Pos}; + auto best{kInsertWidth}; + auto found{false}; + for (std::size_t chain = 0; chain < catenary_chain_size(); ++chain) + { + if (catenary_chain_get(chain) == nullptr) + continue; + auto const members{catenary_chain_members(chain)}; + for (std::size_t k = 0; k + 1 < members.size(); ++k) + { + auto const *a{catenary_scene_get(members[k])}; + auto const *b{catenary_scene_get(members[k + 1])}; + if (a == nullptr || b == nullptr || false == a->has_at || false == b->has_at || glm::distance(wire_of(*a), camera) > kCatenaryRange) + continue; + auto const pa{plan_of(wire_of(*a))}, pb{plan_of(wire_of(*b))}; + auto const span{pb - pa}; + auto const length{glm::length(span)}; + if (length < 2.0 * kInsertEnds) + continue; + auto const along{glm::dot(wire - pa, span / length)}; + // not at the ends: a pole there is the pole of the end, or the next one past it + if (along < kInsertEnds || along > length - kInsertEnds) + continue; + auto const aside{std::abs(glm::dot(wire - pa, glm::dvec2{-span.y, span.x} / length))}; + if (aside < best) + { + best = aside; + Insert.chain = static_cast(chain); + Insert.after = k; + Insert.from = members[k]; + Insert.to = members[k + 1]; + found = true; + } + } + } + return found; +} + +void catenary_editor::catenary_chain_end() +{ + auto &tool{m_catenary}; + tool.chain = -1; + tool.last = -1; + tool.connect_from = -1; + tool.anchor_pick = -1; + tool.status = STR("The chain is ended: the next pole starts a new one"); +} + +bool catenary_editor::catenary_chain_continue(std::size_t const Pole) +{ + auto &tool{m_catenary}; + auto const *data{catenary_scene_get(Pole)}; + if (data == nullptr || false == data->has_at) + return false; + std::size_t position{0}; + auto const chain{catenary_chain_of(Pole, position)}; + if (chain >= 0) + { + auto const members{catenary_chain_members(static_cast(chain))}; + auto const atend{position + 1 == members.size()}; + if (position != 0 && false == atend) + { + tool.status = STR("The pole is in the middle of a chain: the chain goes on from its ends only"); + return false; + } + tool.chain = chain; + tool.append = atend; + tool.chain_selected = chain; + } + else + tool.chain = -1; // a new chain from this pole + // the next poles go to the file of this one: a chain is in one file + std::snprintf(tool.file, sizeof(tool.file), "%s", catenary_scene_file_of(Pole).c_str()); + tool.last = static_cast(Pole); + tool.chaining = true; + tool.connect_from = -1; + tool.anchor_pick = -1; + tool.anchor_selected = -1; + tool.selected = -1; + tool.probed = false; + tool.page = 0; + if (bool toward{tool.toward}; catenary_propose_toward(toward)) + tool.toward = toward; + tool.status = format(STR_C("The next poles go on from %s: LMB by the track; Esc ends the chain"), data->name.empty() ? "-" : data->name.c_str()); + return true; +} + void catenary_editor::catenary_place(catenary_spot const &Spot) { auto &tool{m_catenary}; ++tool.group; auto data{Spot.data}; - std::string const file{tool.file}; + // in a span of a chain: the pole goes into the chain between the two, in the file of the chain + chain_insert insert; + auto const inserting{catenary_insert_at(Spot, insert)}; + std::string const file{inserting ? catenary_chain_file(static_cast(insert.chain)) : std::string{tool.file}}; data.name = catenary_scene_new_name(file); // a drop on a gantry takes the texture of the gantry if (auto const *gantry{tool.gantry_selected >= 0 ? catenary_gantry_get(static_cast(tool.gantry_selected)) : nullptr}; gantry != nullptr && data.pole_type != tool.poles[std::clamp(tool.pole, 0, static_cast(tool.poles.size()) - 1)]) @@ -1212,6 +1414,23 @@ void catenary_editor::catenary_place(catenary_spot const &Spot) tool.status = format(STR_C("Pole %s with card %d execution %d placed, A = %.2f m"), data.pole_type.c_str(), Spot.card, data.execution, Spot.reach); if (catenary_scene_parts(index) == 0) tool.status += STR(" - the cantilever didn't assemble, see the log"); + if (inserting) + { + // the chain going on (tool.last) stays as it was + if (auto const *chain{catenary_chain_get(static_cast(insert.chain))}; chain != nullptr && insert.after + 1 < chain->suspensions.size()) + { + auto changed{*chain}; + tool.undo.push_back({tool.group, true, static_cast(insert.chain), std::nullopt, *chain, false}); + changed.suspensions.insert(changed.suspensions.begin() + static_cast(insert.after + 1), data.name); + auto const name{changed.name}; + catenary_chain_update(static_cast(insert.chain), changed); + tool.chain_selected = insert.chain; + auto const *a{catenary_scene_get(insert.from)}; + auto const *b{catenary_scene_get(insert.to)}; + tool.status += format(STR_C("; put into chain %s between %s and %s"), name.c_str(), a != nullptr ? a->name.c_str() : "-", b != nullptr ? b->name.c_str() : "-"); + } + return; + } // the span from the last pole: a new chain of the two, or one more pole of the chain auto const *previous{tool.chaining && tool.last >= 0 ? catenary_scene_get(static_cast(tool.last)) : nullptr}; if (previous != nullptr && catenary_scene_file_of(static_cast(tool.last)) == file) @@ -1259,6 +1478,352 @@ void catenary_editor::catenary_place(catenary_spot const &Spot) tool.toward = toward; } +bool catenary_editor::catenary_umbrella_spot(TTrack &Track, int const Path, catenary_spot &Spot) +{ + auto &tool{m_catenary}; + auto const *source{tool.umbrella_from >= 0 ? catenary_scene_get(static_cast(tool.umbrella_from)) : nullptr}; + auto const *catalog{catenary_scene_catalog()}; + auto const *pole{source != nullptr && catalog != nullptr ? catalog->pole(source->pole_type) : nullptr}; + if (pole == nullptr || false == source->has_pole || false == source->has_at) + return false; + auto const foot{pole_of(*source)}; + { + // square to the pole on this track, on the side of the pole; not onto a drop of the selected gantry + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(Track, Path, foot, foot, 0, Spot)) + return false; + } + // the cantilever on the same pole (or drop): its distance is the one of the pole, the panel gives the rest + auto const right{right_of(Spot.forward) * static_cast(Spot.side)}; + auto const clearance{glm::dot(plan_of(foot) - plan_of(Spot.axis), right) - 0.5 * pole->depth(0.0)}; + auto const stagger{Spot.curve ? std::max(static_cast(tool.stagger), catenary_scene_layout().curve_stagger()) : static_cast(tool.stagger)}; + catenary_compose(Spot, clearance, stagger, Spot.toward, source->pole_type); + std::copy(std::begin(source->pole), std::end(source->pole), Spot.data.pole); + if (Spot.card == 0) + return true; + // the second cantilever goes the other way from the pole than the first one + auto const mine{plan_of(wire_of(Spot.data)) - plan_of(foot)}; + auto const other{plan_of(wire_of(*source)) - plan_of(foot)}; + if (glm::dot(mine, other) > 0.0) + { + Spot.card = 0; + Spot.problem = STR("This track is on the same side of the pole as its cantilever: the second cantilever of an umbrella goes to a track on the other side"); + } + return true; +} + +void catenary_editor::catenary_umbrella_place(catenary_spot const &Spot) +{ + auto &tool{m_catenary}; + auto const from{static_cast(tool.umbrella_from)}; + auto const *source{tool.umbrella_from >= 0 ? catenary_scene_get(from) : nullptr}; + tool.umbrella_from = -1; + if (source == nullptr) + return; + ++tool.group; + auto const file{catenary_scene_file_of(from)}; + auto sourcedata{*source}; + tool.undo.push_back({tool.group, false, from, sourcedata, std::nullopt, false}); + if (sourcedata.name.empty()) + sourcedata.name = catenary_scene_new_name(file); // a pole from before the editor had no name + // a name of the umbrella no other suspension has + std::string name; + for (int k = 1; name.empty(); ++k) + { + auto const candidate{"parasol_" + sourcedata.name + (k > 1 ? "_" + std::to_string(k) : std::string{})}; + auto used{false}; + for (std::size_t index = 0; index < catenary_scene_size() && false == used; ++index) + if (auto const *data{catenary_scene_get(index)}; data != nullptr && data->umbrella == candidate) + used = true; + if (false == used) + name = candidate; + } + // the first one named first, so that the second gets a name of its own + sourcedata.umbrella = name; + catenary_scene_update(from, sourcedata); + if (auto found{tool.spots.find(from)}; found != tool.spots.end()) + found->second.data = sourcedata; + auto data{Spot.data}; + data.name = catenary_scene_new_name(file); + data.skin = sourcedata.skin; + data.umbrella = name; + auto const index{catenary_scene_add(file, data)}; + tool.spots[index] = Spot; + tool.spots[index].data = data; + tool.undo.push_back({tool.group, false, index, std::nullopt, std::nullopt, false}); + catenary_select(static_cast(index)); + tool.status = format(STR_C("Umbrella %s: the second cantilever (card %d execution %d, A = %.2f m) on pole %s of %s; hang its wires with the button below or C"), name.c_str(), Spot.card, data.execution, Spot.reach, data.pole_type.c_str(), sourcedata.name.c_str()); + if (catenary_scene_parts(index) == 0) + tool.status += STR(" - the cantilever didn't assemble, see the log"); +} + +void catenary_editor::catenary_keep_pole(catenary_spot &Spot, catenary::suspension const &Current, double const Stagger, bool const Toward) const +{ + auto const *catalog{catenary_scene_catalog()}; + auto const *own{catalog != nullptr ? catalog->pole(Current.pole_type) : nullptr}; + if (own == nullptr) + { + Spot.card = 0; + Spot.problem = STR("No pole: the catalogue (data/traction_cantilevers.txt, data/traction_supports.txt) isn't loaded or has no poles"); + return; + } + // the plane of the cantilever: through the pole, or through its mount on the console of spread cantilevers + auto const plane{Current.has_spread ? glm::dvec3{Current.spread[0], Current.pole[1], Current.spread[2]} : pole_of(Current)}; + auto const right{right_of(Spot.forward) * static_cast(Spot.side)}; + catenary_compose(Spot, glm::dot(plan_of(plane) - plan_of(Spot.axis), right) - 0.5 * own->depth(0.0), Stagger, Toward, Current.pole_type); + std::copy(std::begin(Current.pole), std::end(Current.pole), Spot.data.pole); + Spot.data.has_spread = Current.has_spread; + std::copy(std::begin(Current.spread), std::end(Current.spread), Spot.data.spread); +} + +void catenary_editor::catenary_spread_create(std::size_t const Index) +{ + auto &tool{m_catenary}; + auto const *current{catenary_scene_get(Index)}; + auto const *found{catenary_spot_of(Index)}; + auto const *catalog{catenary_scene_catalog()}; + auto const *pole{current != nullptr && catalog != nullptr ? catalog->pole(current->pole_type) : nullptr}; + // a cantilever without strut and stay (spread steady arms 0167) goes on the beam 4690, the others on the console 4890 + auto const *scheme{current != nullptr && catalog != nullptr ? catalog->scheme(current->card, current->execution) : nullptr}; + auto const steadyonly{scheme != nullptr && scheme->roles.count("strut") == 0 && scheme->roles.count("stay") == 0}; + auto const consolename{pole == nullptr ? std::string{} : steadyonly && false == pole->steady_console.empty() ? pole->steady_console : pole->console}; + auto const *console{false == consolename.empty() ? catalog->mount(consolename) : nullptr}; + if (current == nullptr || found == nullptr || false == current->has_pole || false == current->umbrella.empty()) + return; + if (console == nullptr || false == console->console) + { + tool.status = format(STR_C("Pole %s carries no console for spread cantilevers (\"console\" in its entry in traction_supports.txt)"), current->pole_type.c_str()); + return; + } + auto const sourcedata{*current}; + auto const spot{*found}; + auto const foot{pole_of(sourcedata)}; + // the axis of the console mounts (the X axis of the cantilevers): along the track, square to the way from the track to the pole + auto const out{right_of(spot.forward) * static_cast(spot.side)}; + glm::dvec3 const xaxis{-out.y, 0.0, out.x}; + auto const right{right_of(spot.forward) * static_cast(spot.side)}; + auto const offset{glm::dot(plan_of(wire_of(sourcedata)) - plan_of(spot.axis), right)}; + catenary_spot spots[2]; + for (int k = 0; k < 2; ++k) + { + auto const point{foot + xaxis * console->console_x[k]}; + { + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(*spot.track, spot.path, point, foot, spot.side, spots[k])) + return; + } + spots[k].curve = spot.curve; + catenary::suspension planned{sourcedata}; + planned.has_spread = true; + planned.spread[0] = point.x; + planned.spread[1] = foot.y; + planned.spread[2] = point.z; + catenary_keep_pole(spots[k], planned, std::abs(offset), offset >= 0.0); + if (spots[k].card == 0) + { + tool.status = spots[k].problem; + return; + } + } + ++tool.group; + auto const file{catenary_scene_file_of(Index)}; + tool.undo.push_back({tool.group, false, Index, sourcedata, std::nullopt, false}); + auto first{spots[0].data}; + first.name = sourcedata.name.empty() ? catenary_scene_new_name(file) : sourcedata.name; + first.range_max = sourcedata.range_max; + first.range_min = sourcedata.range_min; + first.skin = sourcedata.skin; + first.overrides = sourcedata.overrides; + std::string name; + for (int k = 1; name.empty(); ++k) + { + auto const candidate{"rozstaw_" + first.name + (k > 1 ? "_" + std::to_string(k) : std::string{})}; + auto used{false}; + for (std::size_t index = 0; index < catenary_scene_size() && false == used; ++index) + if (auto const *data{catenary_scene_get(index)}; data != nullptr && data->umbrella == candidate) + used = true; + if (false == used) + name = candidate; + } + first.umbrella = name; + catenary_scene_update(Index, first); + tool.spots[Index] = spots[0]; + tool.spots[Index].data = first; + auto second{spots[1].data}; + second.name = catenary_scene_new_name(file); + second.skin = sourcedata.skin; + second.umbrella = name; + auto const added{catenary_scene_add(file, second)}; + tool.spots[added] = spots[1]; + tool.spots[added].data = second; + tool.undo.push_back({tool.group, false, added, std::nullopt, std::nullopt, false}); + catenary_select(static_cast(added)); + tool.status = format(STR_C("Spread cantilevers %s on console %s: %s and %s, %.2f m apart; give each its chain, \"Messenger held by\" splits the messenger and the wires between them"), + name.c_str(), console->name.c_str(), first.name.c_str(), second.name.c_str(), console->console_x[0] - console->console_x[1]); + if (catenary_scene_parts(added) == 0 || catenary_scene_parts(Index) == 0) + tool.status += STR(" - the cantilever didn't assemble, see the log"); +} + +void catenary_editor::catenary_third_create(std::size_t const Index) +{ + auto &tool{m_catenary}; + auto const *current{catenary_scene_get(Index)}; + auto const *found{catenary_spot_of(Index)}; + if (current == nullptr || found == nullptr || found->track == nullptr || false == current->has_pole) + return; + auto const sourcedata{*current}; + auto const source{*found}; + auto const foot{pole_of(sourcedata)}; + // in the axis of the pole, on its own mounts, to the same track; the pole is stood by the spread pair + catenary_spot spot; + { + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(*source.track, source.path, foot, foot, source.side, spot)) + return; + } + spot.curve = source.curve; + catenary::suspension planned{sourcedata}; + planned.has_spread = false; + planned.umbrella.clear(); + planned.shared_pole = true; + catenary_keep_pole(spot, planned, tool.stagger, tool.toward); + if (spot.card == 0) + { + tool.status = spot.problem; + return; + } + auto data{spot.data}; + auto const file{catenary_scene_file_of(Index)}; + data.name = catenary_scene_new_name(file); + data.skin = sourcedata.skin; + data.shared_pole = true; + ++tool.group; + auto const added{catenary_scene_add(file, data)}; + tool.spots[added] = spot; + tool.spots[added].data = data; + tool.undo.push_back({tool.group, false, added, std::nullopt, std::nullopt, false}); + catenary_select(static_cast(added)); + tool.status = format(STR_C("Cantilever %s in the axis of pole %s (card %d execution %d): set its card (a messenger only one: 1002-1015, 1290-1297), hang it in a chain, an umbrella to the other track can go on it too"), + data.name.c_str(), data.pole_type.c_str(), data.card, data.execution); + if (catenary_scene_parts(added) == 0) + tool.status += STR(" - the cantilever didn't assemble, see the log"); +} + +void catenary_editor::catenary_pair_move(std::size_t const Selected, glm::dvec3 const &Foot, bool const Panel) +{ + auto &tool{m_catenary}; + auto const *current{catenary_scene_get(Selected)}; + if (current == nullptr || false == current->has_pole) + return; + auto const oldfoot{pole_of(*current)}; + std::vector> moved; + for (auto const index : pole_sharers(Selected)) + { + auto const *data{catenary_scene_get(index)}; + if (data == nullptr) + continue; + auto const *found{catenary_spot_of(index)}; + if (found == nullptr || found->track == nullptr) + return; + auto const found_copy{*found}; + // each keeps its place against the pole (its console mount) and its stagger, on its own track + auto const plane{data->has_spread ? glm::dvec3{data->spread[0], data->pole[1], data->spread[2]} : pole_of(*data)}; + auto planenew{Foot + (plane - oldfoot)}; + planenew.y = Foot.y; + auto const right{right_of(found_copy.forward) * static_cast(found_copy.side)}; + auto offset{glm::dot(plan_of(wire_of(*data)) - plan_of(found_copy.axis), right)}; + // from the panel: the selected one takes its stagger from there + if (Panel && index == Selected) + offset = tool.toward ? static_cast(tool.stagger) : -static_cast(tool.stagger); + catenary_spot spot; + { + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(*found_copy.track, found_copy.path, planenew, Foot, found_copy.side, spot)) + return; + } + spot.curve = found_copy.curve; + catenary::suspension planned{*data}; + planned.pole[0] = Foot.x; + planned.pole[1] = Foot.y; + planned.pole[2] = Foot.z; + planned.spread[0] = planenew.x; + planned.spread[1] = Foot.y; + planned.spread[2] = planenew.z; + catenary_keep_pole(spot, planned, std::abs(offset), offset >= 0.0); + if (spot.card == 0) + { + tool.status = spot.problem; + return; + } + auto &result{spot.data}; + result.name = data->name; + result.range_max = data->range_max; + result.range_min = data->range_min; + result.skin = data->skin; + result.overrides = data->overrides; + result.umbrella = data->umbrella; + result.shared_pole = data->shared_pole; + moved.emplace_back(index, spot); + } + // one undo group for the whole drag (of the pole, or of the slider of its distance) + auto record{false}; + int group{0}; + if (Panel) + { + record = tool.shift_group == 0 || tool.undo.empty() || tool.undo.back().group != tool.shift_group; + if (record) + tool.shift_group = ++tool.group; + group = tool.shift_group; + } + else + { + record = tool.drag_undo == 0; + if (record) + tool.drag_undo = ++tool.group; + group = tool.drag_undo; + } + for (auto const &[index, spot] : moved) + { + if (record) + tool.undo.push_back({group, false, index, *catenary_scene_get(index), std::nullopt, false}); + catenary_scene_update(index, spot.data); + tool.spots[index] = spot; + } + tool.probed = false; +} + +void catenary_editor::catenary_umbrella_break(std::size_t const Index) +{ + auto &tool{m_catenary}; + auto const *current{catenary_scene_get(Index)}; + if (current == nullptr || current->umbrella.empty()) + return; + auto const name{current->umbrella}; + ++tool.group; + // another one on the pole stands it already (the third one by spread cantilevers stands none) + auto placed{false}; + for (auto const other : pole_sharers(Index)) + if (auto const *data{catenary_scene_get(other)}; data != nullptr && data->umbrella != name && false == data->shared_pole) + placed = true; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + if (data == nullptr || data->umbrella != name) + continue; + auto changed{*data}; + tool.undo.push_back({tool.group, false, index, changed, std::nullopt, false}); + changed.umbrella.clear(); + changed.has_spread = false; // a cantilever off its console pair stands by the pole as one of its own + changed.shared_pole = changed.shared_pole || placed; // the pole stands once + placed = placed || false == changed.shared_pole; + catenary_scene_update(index, changed); + if (auto found{tool.spots.find(index)}; found != tool.spots.end()) + found->second.data = changed; + } + tool.probed = false; + tool.status = format(STR_C("Umbrella %s separated: two cantilevers on their own mounts"), name.c_str()); +} + // the span from the suspension the wires are being hung from (tool.connect_from) to To: To goes on the chain at its end, // From goes on the chain of To at its end, the two chains become one, or the two make a new chain void catenary_editor::catenary_connect(std::size_t const To) @@ -1409,6 +1974,14 @@ void catenary_editor::catenary_press(track_intent const &Intent) tool.pressed = {mouse.x, mouse.y}; if (is_intent(Intent, intent_detail::select)) { + if (tool.anchor_pick >= 0) + { + // the tensioning device on this pole + if (catenary_anchor_create(static_cast(tool.anchor_pick), tool.anchor_pick_start, Intent.index, tool.anchor_pick_contact)) + tool.anchor_pick = -1; + return; + } + tool.umbrella_from = -1; if (tool.connect_from >= 0) { // hanging the wires on: the span from the last pole to this one @@ -1428,6 +2001,7 @@ void catenary_editor::catenary_press(track_intent const &Intent) { tool.gantry_selected = Intent.index; tool.selected = -1; + tool.anchor_selected = -1; tool.page = 1; tool.status = STR("The cantilevers now go on drops on this gantry: LMB over a track; drag the beam to move the gantry"); // held and dragged: the whole gantry goes with the cursor @@ -1523,24 +2097,37 @@ void catenary_editor::catenary_press(track_intent const &Intent) } if (is_intent(Intent, intent_detail::chain)) { - tool.selected = -1; - tool.page = 2; - tool.chain_selected = Intent.index; - if (auto const *chain{catenary_chain_get(static_cast(Intent.index))}; chain != nullptr) - { - // the panel shows what the chain has - std::snprintf(tool.power, sizeof(tool.power), "%s", chain->power.c_str()); - tool.voltage = static_cast(chain->voltage); - tool.material = chain->material == "al" ? 1 : 0; - tool.state = chain->state == 2 ? 1 : chain->state == 1 ? 2 : 0; - if (auto const found{std::find(tool.networks.begin(), tool.networks.end(), chain->type)}; found != tool.networks.end()) - tool.network = static_cast(found - tool.networks.begin()); - } + tool.anchor_selected = -1; + catenary_chain_show(Intent.index); + return; + } + if (is_intent(Intent, intent_detail::anchor)) + { + // the anchor: its chain on the wires page, the anchor marked in the panel + bool start{false}; + auto const chain{catenary_anchor_chain(static_cast(Intent.index), start)}; + catenary_chain_show(chain); + tool.anchor_selected = Intent.index; + tool.anchor_scroll = true; + if (auto const *anchor{catenary_anchor_get(static_cast(Intent.index))}; anchor != nullptr) + tool.status = chain >= 0 ? format(STR_C("Anchor %s selected: its settings are below, Del removes it"), anchor->name.c_str()) + : format(STR_C("Anchor %s isn't at the end of any chain"), anchor->name.c_str()); return; } if (Intent.track == nullptr) return; catenary_spot spot; + if (tool.umbrella_from >= 0) + { + // the second cantilever of an umbrella on the pole of the first one + if (false == catenary_umbrella_spot(*Intent.track, Intent.path, spot)) + return; + if (spot.card == 0) + tool.status = spot.problem; + else + catenary_umbrella_place(spot); + return; + } if (false == catenary_spot_at(*Intent.track, Intent.path, e.m_hover.point, e.cursor_level(e.m_hover.point.y), 0, spot)) return; if (spot.card == 0) @@ -1551,6 +2138,24 @@ void catenary_editor::catenary_press(track_intent const &Intent) catenary_place(spot); } +void catenary_editor::catenary_chain_show(int const Chain) +{ + auto &tool{m_catenary}; + tool.selected = -1; + tool.page = 2; + tool.chain_selected = Chain; + if (auto const *chain{Chain >= 0 ? catenary_chain_get(static_cast(Chain)) : nullptr}; chain != nullptr) + { + // the panel shows what the chain has + std::snprintf(tool.power, sizeof(tool.power), "%s", chain->power.c_str()); + tool.voltage = static_cast(chain->voltage); + tool.material = chain->material == "al" ? 1 : 0; + tool.state = chain->state == 2 ? 1 : chain->state == 1 ? 2 : 0; + if (auto const found{std::find(tool.networks.begin(), tool.networks.end(), chain->type)}; found != tool.networks.end()) + tool.network = static_cast(found - tool.networks.begin()); + } +} + // the panel settings of the chain go to the selected chain void catenary_editor::catenary_chain_apply() { @@ -1578,6 +2183,35 @@ bool catenary_editor::catenary_delete_selected() { auto &tool{m_catenary}; tool.connect_from = -1; + tool.anchor_pick = -1; + if (tool.selected < 0 && tool.anchor_selected >= 0) + { + // the anchor picked in the view + auto const picked{static_cast(tool.anchor_selected)}; + tool.anchor_selected = -1; + auto const *anchor{catenary_anchor_get(picked)}; + if (anchor == nullptr) + return false; + bool start{false}; + if (auto const chain{catenary_anchor_chain(picked, start)}; chain >= 0) + { + auto const *owner{catenary_chain_get(static_cast(chain))}; + auto const contact{owner != nullptr && (start ? owner->anchor_start_contact : owner->anchor_end_contact) == anchor->name}; + catenary_anchor_delete(static_cast(chain), start, contact); + } + else + { + catenary_change removed; + removed.group = ++tool.group; + removed.anchor = true; + removed.index = picked; + removed.anchor_before = *anchor; + tool.undo.push_back(removed); + catenary_anchor_remove(picked); + } + tool.status = STR("Anchor removed"); + return true; + } if (tool.selected < 0) return false; auto const index{static_cast(tool.selected)}; @@ -1590,9 +2224,44 @@ bool catenary_editor::catenary_delete_selected() { auto const *data{catenary_chain_get(chain)}; if (data != nullptr && false == current->name.empty() && catenary_chain_file(chain) == catenary_scene_file_of(index) - && std::find(data->suspensions.begin(), data->suspensions.end(), current->name) != data->suspensions.end()) + && (std::find(data->suspensions.begin(), data->suspensions.end(), current->name) != data->suspensions.end() + || std::any_of(data->messenger_from.begin(), data->messenger_from.end(), [&](auto const &Pair) { return Pair.first == current->name || Pair.second == current->name; }))) tool.undo.push_back({tool.group, true, chain, std::nullopt, *data, false}); } + // the other cantilever of its umbrella stays alone + if (false == current->umbrella.empty()) + for (std::size_t other = 0; other < catenary_scene_size(); ++other) + { + auto const *data{catenary_scene_get(other)}; + if (other == index || data == nullptr || data->umbrella != current->umbrella) + continue; + auto changed{*data}; + tool.undo.push_back({tool.group, false, other, changed, std::nullopt, false}); + changed.umbrella.clear(); // a spread one stays alone on its console + catenary_scene_update(other, changed); + if (auto found{tool.spots.find(other)}; found != tool.spots.end()) + found->second.data = changed; + } + current = catenary_scene_get(index); + // it stood the pole and others stay on it: one of them stands it now (not one of a pair, which the pair stands itself) + if (false == current->shared_pole) + { + std::vector rest; + for (auto const other : pole_sharers(index)) + if (other != index) + rest.push_back(other); + auto const standing{std::any_of(rest.begin(), rest.end(), [](std::size_t const Other) { auto const *data{catenary_scene_get(Other)}; return data != nullptr && false == data->shared_pole; })}; + if (false == rest.empty() && false == standing) + { + auto changed{*catenary_scene_get(rest.front())}; + tool.undo.push_back({tool.group, false, rest.front(), changed, std::nullopt, false}); + changed.shared_pole = false; + catenary_scene_update(rest.front(), changed); + if (auto found{tool.spots.find(rest.front())}; found != tool.spots.end()) + found->second.data = changed; + } + current = catenary_scene_get(index); + } tool.undo.push_back({tool.group, false, index, *current, std::nullopt, true}); catenary_scene_remove(index); tool.selected = -1; @@ -1606,6 +2275,7 @@ bool catenary_editor::catenary_undo() { auto &tool{m_catenary}; tool.connect_from = -1; + tool.anchor_pick = -1; // in the middle of a drag, or with nothing of its own: the editor takes back its own changes if (tool.dragging != 0 || tool.undo.empty()) return false; @@ -1738,9 +2408,9 @@ void catenary_editor::render_catenary_chain_ui() } } -std::size_t catenary_editor::catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool const Start) const +std::size_t catenary_editor::catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool const Start, bool const Contact) const { - auto const &name{Start ? Chain.anchor_start : Chain.anchor_end}; + auto const &name{Contact ? (Start ? Chain.anchor_start_contact : Chain.anchor_end_contact) : (Start ? Chain.anchor_start : Chain.anchor_end)}; if (name.empty()) return catenary_anchor_size(); for (std::size_t index = 0; index < catenary_anchor_size(); ++index) @@ -1766,6 +2436,114 @@ int weight_color_index(std::string const &Name) } // namespace +bool catenary_editor::catenary_anchor_create(std::size_t const Chain, bool const Start, int const On, bool const Contact) +{ + auto &tool{m_catenary}; + auto const *chain{catenary_chain_get(Chain)}; + if (chain == nullptr) + return false; + auto const file{catenary_chain_file(Chain)}; + auto const members{catenary_chain_members(Chain)}; + if (members.size() < 2) + return false; + auto const *last{catenary_scene_get(Start ? members.front() : members.back())}; + auto const *previous{catenary_scene_get(Start ? members[1] : members[members.size() - 2])}; + if (last == nullptr || previous == nullptr || false == last->has_at || false == previous->has_at) + return false; + catenary::anchor data; + data.name = catenary_scene_new_anchor_name(file); + data.network = chain->type; + if (On >= 0) + { + // on an existing pole: it has to stand (a pole entry) and be in the same file as the chain + auto const *carrier{catenary_scene_get(static_cast(On))}; + if (carrier == nullptr || false == carrier->has_pole || carrier->name.empty() || catenary_scene_file_of(static_cast(On)) != file) + { + tool.status = STR("This pole can't carry the anchor: it has to be a pole (not a drop or a point) from the file of the chain"); + return false; + } + if (static_cast(On) == (Start ? members.front() : members.back())) + { + tool.status = STR("The anchor goes on another pole than the last one of the chain"); + return false; + } + data.on = carrier->name; + data.pole_type = carrier->pole_type; + std::copy(std::begin(carrier->pole), std::end(carrier->pole), data.pole); + } + else + { + // the anchor pole 24 m past the last pole, in line with it (catalogue 22 m; Glowacki 24 m for big tensions) + glm::dvec2 along{last->at[0] - previous->at[0], last->at[2] - previous->at[2]}; + if (glm::length(along) < 0.1) + return false; + along = glm::normalize(along); + auto const base{last->has_pole ? glm::dvec2{last->pole[0], last->pole[2]} : glm::dvec2{last->at[0], last->at[2]}}; + auto const at{base + along * 24.0}; + data.pole_type = tool.poles.empty() ? std::string{} : tool.poles[std::clamp(tool.pole, 0, static_cast(tool.poles.size()) - 1)]; + data.pole[0] = at.x; + data.pole[1] = last->has_pole ? last->pole[1] : last->at[1] - static_cast(tool.height); + data.pole[2] = at.y; + } + std::copy(std::begin(last->at), std::end(last->at), data.toward); + // from the middle anchor: half of the chain, by default + double length{0.0}; + for (std::size_t k = 1; k < members.size(); ++k) + { + auto const *a{catenary_scene_get(members[k - 1])}; + auto const *b{catenary_scene_get(members[k])}; + if (a != nullptr && b != nullptr) + length += std::hypot(b->at[0] - a->at[0], b->at[2] - a->at[2]); + } + data.distance = std::clamp(std::round(0.5 * length / 10.0) * 10.0, 100.0, 750.0); + data.color = "szary"; + data.top_color = "zolty"; + auto changed{*chain}; + ++tool.group; + if (Contact) + { + // the contact wires by themselves (0404, or the wires of a split one); the anchor of the messenger keeps the messenger only + auto const main{catenary_anchor_of(*chain, file, Start)}; + auto const *messenger{main < catenary_anchor_size() ? catenary_anchor_get(main) : nullptr}; + if (messenger == nullptr) + return false; + auto const *devices{catenary_scene_tensioners()}; + auto const *device{devices != nullptr ? devices->device(messenger->device) : nullptr}; + auto const fixed{device == nullptr || device->fixed}; + data.device = fixed ? "0404" : "0482-c"; + if (device == nullptr || device->only != "messenger") + { + auto changedmessenger{*messenger}; + changedmessenger.device = fixed ? "0405" : "0482-m"; + catenary_change before; + before.group = tool.group; + before.anchor = true; + before.index = main; + before.anchor_before = *messenger; + tool.undo.push_back(before); + catenary_anchor_update(main, changedmessenger); + } + (Start ? changed.anchor_start_contact : changed.anchor_end_contact) = data.name; + } + else + (Start ? changed.anchor_start : changed.anchor_end) = data.name; + catenary_change added; + added.group = tool.group; + added.anchor = true; + added.index = catenary_anchor_add(file, data); + tool.undo.push_back(added); + catenary_change linked; + linked.group = tool.group; + linked.chain = true; + linked.index = Chain; + linked.chain_before = *chain; + tool.undo.push_back(linked); + catenary_chain_update(Chain, changed); + tool.anchor_pick_contact = false; + tool.status = Contact ? STR("The contact wires are anchored on their own; the other anchor holds the messenger only") : STR("Anchored: the last span goes without droppers to the tensioning device"); + return true; +} + void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) { auto &tool{m_catenary}; @@ -1777,16 +2555,122 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) if (members.size() < 2) return; ImGui::Separator(); - ImGui::TextUnformatted(STR_C("Anchors (weight tensioning, card 23-0461)")); - for (int end = 0; end < 2; ++end) + // middle anchor (cards 63-0406 / 0408): at one of the inner poles of the chain, the anchor rope goes to the poles on both sides { + auto changed{*chain}; + std::vector labels{STR("none")}; + int chosen{0}; + for (std::size_t k = 1; k + 1 < chain->suspensions.size(); ++k) + { + labels.push_back(format(STR_C("pole %d (%s)"), static_cast(k + 1), chain->suspensions[k].c_str())); + if (chain->suspensions[k] == chain->middle) + chosen = static_cast(labels.size()) - 1; + } + ImGui::PushItemWidth(180.0f); + auto edited{false}; + if (ImGui::BeginCombo(STR_C("Middle anchor"), labels[chosen].c_str())) + { + for (std::size_t k = 0; k < labels.size(); ++k) + if (ImGui::Selectable(labels[k].c_str(), static_cast(k) == chosen)) + { + changed.middle = k == 0 ? std::string{} : chain->suspensions[k]; + changed.middle_heights[0] = changed.middle_heights[1] = 0.0; + edited = true; + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The anchor rope is clamped on the messenger 1 m on each side of this pole and goes to the poles next to it; in the middle of both spans a V ties it to the contact wire. The weight anchors measure their distance from it")); + if (false == chain->middle.empty()) + { + char const *const cards[] = {"0406", "0408", "0418", "0416"}; + int insulated{0}; + for (int k = 0; k < 4; ++k) + if (chain->middle_device == cards[k]) + insulated = k; + std::string const kinds[] = {STR("with insulation (card 0406)"), STR("without insulation (card 0408)"), STR("insulated on one side (card 0418)"), STR("to the beam of a gantry (card 0416)")}; + char const *const items[] = {kinds[0].c_str(), kinds[1].c_str(), kinds[2].c_str(), kinds[3].c_str()}; + if (ImGui::Combo(STR_C("Middle anchor kind"), &insulated, items, 4)) + { + changed.middle_device = cards[std::clamp(insulated, 0, 3)]; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("0416: the middle pole is a drop of a gantry; the anchor ropes go from the beam above the messenger to clamps on it about 3 m on both sides, the poles next to it carry nothing")); + // heights of the anchor rope on the poles before and after (when the cantilever is in the way) + auto const where{std::find(chain->suspensions.begin(), chain->suspensions.end(), chain->middle) - chain->suspensions.begin()}; + auto const members{catenary_chain_members(Chain)}; + for (int side = 0; side < 2 && chain->middle_device != "0416"; ++side) + { + auto const k{static_cast(where) + (side == 0 ? -1 : 1)}; + auto const *neighbour{k < members.size() ? catenary_scene_get(members[k]) : nullptr}; + if (neighbour == nullptr || false == neighbour->has_pole) + continue; + auto const automatic{(neighbour->has_rope ? neighbour->rope[1] : neighbour->at[1] + 1.70) - neighbour->pole[1]}; + auto height{static_cast(chain->middle_heights[side] > 0.0 ? chain->middle_heights[side] : automatic)}; + ImGui::PushID(side); + if (ImGui::SliderFloat(side == 0 ? STR_C("Anchor rope on the pole before (m)") : STR_C("Anchor rope on the pole after (m)"), &height, 4.0f, 10.0f, "%.2f")) + { + changed.middle_heights[side] = height; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Height of the anchor of the middle anchor rope on that pole above the rail heads; by default at the messenger. Move it when it hits the cantilever")); + ImGui::PopID(); + } + if ((chain->middle_heights[0] > 0.0 || chain->middle_heights[1] > 0.0) && ImGui::Button(STR_C("Anchor rope at the messenger"))) + { + changed.middle_heights[0] = changed.middle_heights[1] = 0.0; + edited = true; + } + } + ImGui::PopItemWidth(); + if (edited) + { + // one undo step for one drag of a slider + auto const &last{tool.undo.empty() ? catenary_change{} : tool.undo.back()}; + if (tool.undo.empty() || false == last.chain || last.index != Chain || false == ImGui::IsMouseDragging(0)) + tool.undo.push_back({++tool.group, true, Chain, std::nullopt, *chain, false}); + catenary_chain_update(Chain, changed); + chain = catenary_chain_get(Chain); + if (chain == nullptr) + return; + } + } + ImGui::TextUnformatted(STR_C("Anchors")); + // each end: the anchor of the network (or of the messenger only), then the one of the contact wires when they're anchored apart + for (int slot = 0; slot < 4; ++slot) + { + auto const end{slot / 2}; + auto const contact{slot % 2 == 1}; auto const start{end == 0}; - ImGui::PushID(end); + auto const separate{false == (start ? chain->anchor_start_contact : chain->anchor_end_contact).empty()}; + ImGui::PushID(slot); // the last two poles at this end: the wires come along them to the anchor auto const *last{catenary_scene_get(start ? members.front() : members.back())}; auto const *previous{catenary_scene_get(start ? members[1] : members[members.size() - 2])}; - auto const index{catenary_anchor_of(*chain, file, start)}; + auto const index{catenary_anchor_of(*chain, file, start, contact)}; auto const *anchor{index < catenary_anchor_size() ? catenary_anchor_get(index) : nullptr}; + if (contact && anchor == nullptr) + { + // the contact wires apart from the messenger (0405 + 0404, or the two halves of a split one): on another pole + if (catenary_anchor_of(*chain, file, start) < catenary_anchor_size()) + { + auto const picking{tool.anchor_pick == static_cast(Chain) && tool.anchor_pick_start == start && tool.anchor_pick_contact}; + if (ImGui::SmallButton(picking ? STR_C("Stop picking the pole") : STR_C("Contact wires anchored apart: on another pole"))) + { + tool.anchor_pick = picking ? -1 : static_cast(Chain); + tool.anchor_pick_start = start; + tool.anchor_pick_contact = false == picking; + tool.connect_from = -1; + tool.status = picking ? std::string{} : STR("LMB on the pole the anchor of the contact wires goes on"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The messenger stays on the anchor above (it becomes one of the messenger only: 0405 or the messenger of a split one), the contact wires go to an anchor of their own on the pole picked (0404 or the wires of a split one)")); + } + ImGui::PopID(); + continue; + } if (last == nullptr || previous == nullptr || false == last->has_at || false == previous->has_at) { ImGui::PopID(); @@ -1805,55 +2689,78 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) if (anchor == nullptr) { if (ImGui::Button(start ? STR_C("Anchor the start of the chain") : STR_C("Anchor the end of the chain"))) - { - // the anchor pole 24 m past the last pole, in line with it (catalogue 22 m; Glowacki 24 m for big tensions) - catenary::anchor data; - data.name = catenary_scene_new_anchor_name(file); - data.network = chain->type; - data.pole_type = tool.poles.empty() ? std::string{} : tool.poles[std::clamp(tool.pole, 0, static_cast(tool.poles.size()) - 1)]; - auto const at{base + along * 24.0}; - data.pole[0] = at.x; - data.pole[1] = baseheight; - data.pole[2] = at.y; - std::copy(std::begin(last->at), std::end(last->at), data.toward); - // from the middle anchor: half of the chain, by default - double length{0.0}; - for (std::size_t k = 1; k < members.size(); ++k) - { - auto const *a{catenary_scene_get(members[k - 1])}; - auto const *b{catenary_scene_get(members[k])}; - if (a != nullptr && b != nullptr) - length += std::hypot(b->at[0] - a->at[0], b->at[2] - a->at[2]); - } - data.distance = std::clamp(std::round(0.5 * length / 10.0) * 10.0, 100.0, 750.0); - data.color = "szary"; - data.top_color = "zolty"; - auto changed{*chain}; - (start ? changed.anchor_start : changed.anchor_end) = data.name; - ++tool.group; - catenary_change added; - added.group = tool.group; - added.anchor = true; - added.index = catenary_anchor_add(file, data); - tool.undo.push_back(added); - catenary_change linked; - linked.group = tool.group; - linked.chain = true; - linked.index = Chain; - linked.chain_before = *chain; - tool.undo.push_back(linked); - catenary_chain_update(Chain, changed); - tool.status = STR("Anchored: the last span goes without droppers to the tensioning device"); - } + catenary_anchor_create(Chain, start, -1); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("An anchor pole 24 m past the last pole, in line with it, with the weight tensioning device; the last span hangs without droppers")); + ImGui::SameLine(); + auto const picking{tool.anchor_pick == static_cast(Chain) && tool.anchor_pick_start == start}; + if (ImGui::Button(picking ? STR_C("Stop picking the pole") : STR_C("to an existing pole"))) + { + tool.anchor_pick = picking ? -1 : static_cast(Chain); + tool.anchor_pick_start = start; + tool.anchor_pick_contact = false; + tool.connect_from = -1; + tool.status = picking ? std::string{} : STR("LMB on the pole the tensioning device goes on"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The tensioning device goes on a pole already standing (with its cantilever); the pole stays as it is")); ImGui::PopID(); continue; } - ImGui::TextColored(ImVec4(0.3f, 0.85f, 1.0f, 1.0f), "%s", format(start ? STR_C("Start: anchor %s") : STR_C("End: anchor %s"), anchor->name.c_str()).c_str()); + auto const picked{tool.anchor_selected == static_cast(index)}; + ImGui::TextColored(picked ? ImVec4(1.0f, 0.85f, 0.2f, 1.0f) : ImVec4(0.3f, 0.85f, 1.0f, 1.0f), "%s", + (format(start ? STR_C("Start: anchor %s") : STR_C("End: anchor %s"), anchor->name.c_str()) + (contact ? " - " + STR("contact wires") : separate ? " - " + STR("messenger") : std::string{}) + + (picked ? " " + STR("(selected in the view)") : std::string{})).c_str()); + if (picked && tool.anchor_scroll) + { + // picked in the view just now: the panel goes to its settings + ImGui::SetScrollHereY(0.2f); + tool.anchor_scroll = false; + } auto data{*anchor}; auto edited{false}; ImGui::PushItemWidth(180.0f); + // kind of the anchor: the card of the device (weight tensioning 0461, fixed 0403); variants for network types go by themselves + auto fixed{false}; + { + auto const devices{catenary_anchor_devices(contact ? "contact" : separate ? "messenger" : "")}; + auto card{data.device.substr(0, data.device.find('-'))}; + for (auto const &item : devices) + if (item.card == data.device) + card = data.device; // a variant listed by itself (0461-2x2) + std::vector labels; + int chosen{-1}; + for (std::size_t k = 0; k < devices.size(); ++k) + { + if (false == devices[k].title.empty()) + labels.push_back(devices[k].title + " (" + devices[k].card + ")"); + else + labels.push_back(format(devices[k].fixed ? STR_C("fixed (card %s)") : devices[k].split ? STR_C("weights, messenger and contact wires separately (card %s)") : devices[k].series ? STR_C("weights, pulleys in series (card %s)") : STR_C("weight tensioning (card %s)"), devices[k].card.c_str())); + if (devices[k].card == card) + { + chosen = static_cast(k); + fixed = devices[k].fixed; + } + } + if (ImGui::BeginCombo(STR_C("Kind"), chosen >= 0 ? labels[chosen].c_str() : data.device.c_str())) + { + for (std::size_t k = 0; k < devices.size(); ++k) + if (ImGui::Selectable(labels[k].c_str(), static_cast(k) == chosen)) + { + data.device = devices[k].card; + data.height = 0.0; + data.rope_height = 0.0; + fixed = devices[k].fixed; + edited = true; + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Weight tensioning keeps the tension with the air temperature; a fixed anchor holds the wires straight on the pole (the other end of the half-section has the weights)")); + } + if (false == data.on.empty()) + ImGui::TextDisabled("%s", format(STR_C("On the pole of %s"), data.on.c_str()).c_str()); + else { // the anchor pole along the line of the last two poles glm::dvec2 const pole{data.pole[0], data.pole[2]}; @@ -1866,7 +2773,7 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) edited = true; } } - if (ImGui::BeginCombo(STR_C("Anchor pole"), data.pole_type.c_str())) + if (data.on.empty() && ImGui::BeginCombo(STR_C("Anchor pole"), data.pole_type.c_str())) { auto chosen{static_cast(std::find(tool.poles.begin(), tool.poles.end(), data.pole_type) - tool.poles.begin())}; if (grouped_items(tool.poles, chosen) && chosen >= 0 && chosen < static_cast(tool.poles.size())) @@ -1876,6 +2783,9 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) } ImGui::EndCombo(); } + if (false == fixed && false == chain->middle.empty()) + ImGui::TextDisabled("%s", STR_C("Distance from the middle anchor: along the chain")); + else if (false == fixed) { auto distance{static_cast(data.distance)}; if (ImGui::SliderFloat(STR_C("From the middle anchor (m)"), &distance, 50.0f, 900.0f, "%.0f")) @@ -1888,6 +2798,7 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) } std::string const colornames[] = {STR(kWeightColorNames[0]), STR(kWeightColorNames[1]), STR(kWeightColorNames[2]), STR(kWeightColorNames[3])}; char const *const colors[] = {colornames[0].c_str(), colornames[1].c_str(), colornames[2].c_str(), colornames[3].c_str()}; + if (false == fixed) { auto color{weight_color_index(data.color)}; if (ImGui::Combo(STR_C("Weights"), &color, colors, 4)) @@ -1902,23 +2813,71 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) edited = true; } } + if (false == fixed) { - auto missing{data.missing}; - if (ImGui::SliderInt(STR_C("Weights missing"), &missing, 0, 10)) + // the insulators of the chains to the wires: resin 7150 or the old roller ESN-2025 + static char const *const values[] = {"", "new", "old", "messenger", "contact"}; + std::string const names[] = {STR("by the wires (roller ones when tarnished)"), STR("resin 7150"), STR("roller ESN-2025 on both"), STR("roller on the messenger only"), STR("roller on the contact wires only")}; + char const *const items[] = {names[0].c_str(), names[1].c_str(), names[2].c_str(), names[3].c_str(), names[4].c_str()}; + int chosen{0}; + for (int i = 0; i < 5; ++i) + if (data.insulators == values[i]) + chosen = i; + if (ImGui::Combo(STR_C("Insulators"), &chosen, items, 5)) { - data.missing = missing; + data.insulators = values[chosen]; edited = true; } if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Stolen from the top of the stack: about 100 daN less tension each")); - auto elongation{static_cast(data.elongation * 100.0)}; - if (ImGui::SliderFloat(STR_C("Wires stretched since regulated (cm)"), &elongation, 0.0f, 60.0f, "%.0f")) + ImGui::SetTooltip("%s", STR_C("Old assemblies have the roller insulator ESN-2025 straight in the lever hole, then the turnbuckle and the clamp; by default the roller ones come with tarnished wires, as the 5110 clamps do")); + } + { + if (false == fixed) { - data.elongation = elongation / 100.0; + auto missing{data.missing}; + if (ImGui::SliderInt(STR_C("Weights missing"), &missing, 0, 10)) + { + data.missing = missing; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Stolen from the top of the stack: about 100 daN less tension each")); + auto elongation{static_cast(data.elongation * 100.0)}; + if (ImGui::SliderFloat(STR_C("Wires stretched since regulated (cm)"), &elongation, 0.0f, 60.0f, "%.0f")) + { + data.elongation = elongation / 100.0; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Creep and wear of an old network: the weights hang 4 times this much lower; on the ground the network loses its tension")); + } + auto const *solved{catenary_anchor_result(index)}; + auto height{static_cast(data.height > 0.0 ? data.height : (solved != nullptr && solved->height > 0.0 ? solved->height : 6.70))}; + if (ImGui::SliderFloat(fixed ? STR_C("Contact wire anchor above the rail heads (m)") : STR_C("Pulleys above the rail heads (m)"), &height, 4.5f, 9.0f, "%.2f")) + { + data.height = height; edited = true; } if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Creep and wear of an old network: the weights hang 4 times this much lower; on the ground the network loses its tension")); + ImGui::SetTooltip("%s", fixed ? STR_C("Height of the anchor of the contact wires on the mast (the messenger one goes as much higher as at the last pole); by default at the height of the wires at the last pole. Drag or Ctrl+click to type") + : STR_C("Height of the pulley axles on the mast; the weights move up or down with them. The mounting table gives 6.70 m. Drag or Ctrl+click to type")); + if (fixed) + { + auto rope{static_cast(data.rope_height > 0.0 ? data.rope_height : (solved != nullptr && solved->rope_height > 0.0 ? solved->rope_height : 7.40))}; + if (ImGui::SliderFloat(STR_C("Messenger anchor above the rail heads (m)"), &rope, 5.0f, 10.0f, "%.2f")) + { + data.rope_height = rope; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Height of the anchor of the messenger on the mast; by default as at the last pole. Drag or Ctrl+click to type")); + if ((data.height > 0.0 || data.rope_height > 0.0) && ImGui::Button(STR_C("Heights as at the last pole"))) + { + data.height = 0.0; + data.rope_height = 0.0; + edited = true; + } + } } ImGui::PopItemWidth(); if (edited) @@ -1938,35 +2897,94 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) } if (auto const *result{catenary_anchor_result(index)}; result != nullptr) { - ImGui::TextDisabled("%s", format(STR_C("%.1f deg C: pulley %.2f m from the pole, weights %.2f m over the rails; %d weights, messenger %.0f daN, contact wires %.0f daN"), - Global.AirTemperature, result->b, result->s, result->weights, result->messenger_tension, result->contact_tension).c_str()); + if (result->fixed) + ImGui::TextDisabled("%s", format(STR_C("Fixed anchor: messenger %.2f m, contact wires %.2f m over the rails"), result->rope_height, result->height).c_str()); + else + ImGui::TextDisabled("%s", format(STR_C("%.1f deg C: pulley %.2f m from the pole, weights %.2f m over the rails; %d weights, messenger %.0f daN, contact wires %.0f daN"), + Global.AirTemperature, result->b, result->s, result->weights, result->messenger_tension, result->contact_tension).c_str()); for (auto const &warning : result->warnings) ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "%s", warning.c_str()); } if (ImGui::Button(STR_C("Remove the anchor"))) { - auto changed{*chain}; - (start ? changed.anchor_start : changed.anchor_end).clear(); - ++tool.group; - catenary_change removed; - removed.group = tool.group; - removed.anchor = true; - removed.index = index; - removed.anchor_before = *anchor; - tool.undo.push_back(removed); - catenary_change unlinked; - unlinked.group = tool.group; - unlinked.chain = true; - unlinked.index = Chain; - unlinked.chain_before = *chain; - tool.undo.push_back(unlinked); - catenary_anchor_remove(index); - catenary_chain_update(Chain, changed); + catenary_anchor_delete(Chain, start, contact); + if (tool.anchor_selected == static_cast(index)) + tool.anchor_selected = -1; } ImGui::PopID(); } } +void catenary_editor::catenary_anchor_delete(std::size_t const Chain, bool const Start, bool const Contact) +{ + auto &tool{m_catenary}; + auto const *chain{catenary_chain_get(Chain)}; + if (chain == nullptr) + return; + auto const file{catenary_chain_file(Chain)}; + auto const index{catenary_anchor_of(*chain, file, Start, Contact)}; + auto const *anchor{index < catenary_anchor_size() ? catenary_anchor_get(index) : nullptr}; + if (anchor == nullptr) + return; + auto changed{*chain}; + ++tool.group; + if (Contact) + { + changed.anchor_start_contact = Start ? std::string{} : changed.anchor_start_contact; + changed.anchor_end_contact = Start ? changed.anchor_end_contact : std::string{}; + // the anchor of the messenger takes the contact wires again: the whole network on it + auto const main{catenary_anchor_of(*chain, file, Start)}; + if (auto const *messenger{main < catenary_anchor_size() ? catenary_anchor_get(main) : nullptr}; messenger != nullptr) + { + auto const *devices{catenary_scene_tensioners()}; + auto const *device{devices != nullptr ? devices->device(messenger->device) : nullptr}; + if (device != nullptr && device->only == "messenger") + { + auto whole{*messenger}; + whole.device = device->fixed ? "0403" : "0461"; + catenary_change before; + before.group = tool.group; + before.anchor = true; + before.index = main; + before.anchor_before = *messenger; + tool.undo.push_back(before); + catenary_anchor_update(main, whole); + } + } + } + else + { + (Start ? changed.anchor_start : changed.anchor_end).clear(); + // the contact wires anchored apart go with it + auto const contact{catenary_anchor_of(*chain, file, Start, true)}; + if (auto const *wires{contact < catenary_anchor_size() ? catenary_anchor_get(contact) : nullptr}; wires != nullptr) + { + catenary_change gone; + gone.group = tool.group; + gone.anchor = true; + gone.index = contact; + gone.anchor_before = *wires; + tool.undo.push_back(gone); + catenary_anchor_remove(contact); + (Start ? changed.anchor_start_contact : changed.anchor_end_contact).clear(); + } + } + catenary_change removed; + removed.group = tool.group; + removed.anchor = true; + removed.index = index; + removed.anchor_before = *anchor; + tool.undo.push_back(removed); + catenary_change unlinked; + unlinked.group = tool.group; + unlinked.chain = true; + unlinked.index = Chain; + unlinked.chain_before = *chain; + tool.undo.push_back(unlinked); + catenary_anchor_remove(index); + catenary_chain_update(Chain, changed); +} + // the cantilever from the described catalogue: category, group, family (automatic by the track), card (automatic by A), // execution. Returns true when anything changed bool catenary_editor::render_catenary_catalog_ui() @@ -2107,6 +3125,81 @@ bool catenary_editor::render_catenary_catalog_ui() return changed; } +void catenary_editor::render_catenary_chain_state() +{ + auto &tool{m_catenary}; + auto const colour{ImVec4(0.3f, 0.85f, 1.0f, 1.0f)}; + ImGui::Separator(); + // the selected pole: the chain can go on from it + if (tool.selected >= 0) + { + auto const pole{static_cast(tool.selected)}; + std::size_t position{0}; + auto const chain{catenary_chain_of(pole, position)}; + auto const *data{catenary_chain_get(chain >= 0 ? static_cast(chain) : catenary_chain_size())}; + auto const middle{data != nullptr && position != 0 && position + 1 != data->suspensions.size()}; + if (middle) + { + ImGui::TextDisabled("%s", format(STR_C("In the middle of chain %s (pole %zu of %zu)"), data->name.c_str(), position + 1, data->suspensions.size()).c_str()); + return; + } + if (ImGui::Button(data != nullptr ? STR_C("Go on with the chain from this pole (C)") : STR_C("Start a chain from this pole (C)"))) + tool.continue_pending = tool.selected; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The poles placed next by the track are joined to this one with spans, one after another; Esc ends the chain")); + if (data != nullptr) + { + ImGui::SameLine(); + ImGui::TextDisabled("%s", format(position == 0 ? STR_C("start of chain %s") : STR_C("end of chain %s"), data->name.c_str()).c_str()); + } + return; + } + ImGui::Checkbox(STR_C("Chain: a span from the last pole"), &tool.chaining); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Each pole placed is joined with a span to the one placed before it, one chain from anchor to anchor")); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("Into spans"), &tool.insert_spans); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A pole placed under the span of a chain (its wire point within 1.5 m of the span, not at its ends) goes into that chain between the two poles")); + if (false == tool.chaining) + return; + auto const *last{tool.last >= 0 ? catenary_scene_get(static_cast(tool.last)) : nullptr}; + if (last == nullptr) + { + ImGui::TextDisabled("%s", STR_C("The next pole starts a new chain; LMB on a pole and C goes on from it")); + return; + } + auto const *chain{tool.chain >= 0 ? catenary_chain_get(static_cast(tool.chain)) : nullptr}; + auto const lastname{last->name.empty() ? std::string{"-"} : last->name}; + if (chain != nullptr) + ImGui::TextColored(colour, "%s", format(tool.append ? STR_C("Next pole: a span from %s, at the end of chain %s (%zu poles)") : STR_C("Next pole: a span from %s, at the start of chain %s (%zu poles)"), + lastname.c_str(), chain->name.c_str(), chain->suspensions.size()).c_str()); + else + ImGui::TextColored(colour, "%s", format(STR_C("Next pole: a new chain from %s"), lastname.c_str()).c_str()); + if (ImGui::Button(STR_C("End the chain (Esc)"))) + catenary_chain_end(); + if (chain != nullptr && chain->suspensions.size() >= 2) + { + auto const index{static_cast(tool.chain)}; + auto const start{false == tool.append}; + if (catenary_anchor_of(*chain, catenary_chain_file(index), start) >= catenary_anchor_size()) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Anchor here and end"))) + { + if (catenary_anchor_create(index, start, -1)) + { + catenary_chain_end(); + tool.chain_selected = static_cast(index); + tool.status = STR("Anchored and ended: the anchor pole is 24 m past the last pole; its settings are on the Wires page"); + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("An anchor pole 24 m past the last pole, in line with it, with the weight tensioning device; the chain is ended")); + } + } +} + // the wires page: the selected chain and its settings, the spans hung from the selected pole to the next ones void catenary_editor::render_catenary_wires_ui() { @@ -2124,6 +3217,7 @@ void catenary_editor::render_catenary_wires_ui() } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Joins this pole with the poles clicked after it: it goes on the chain at its end, a new chain of the two, or two chains become one")); + render_catenary_chain_state(); } render_catenary_chain_ui(); } @@ -2139,6 +3233,8 @@ void catenary_editor::catenary_choose_page(int const Page) tool.gantry_single = false; tool.gantry_first.reset(); tool.connect_from = -1; + tool.anchor_pick = -1; + tool.anchor_selected = -1; if (Page != 1) tool.gantry_selected = -1; // the poles go by the track again, not on the drops of a gantry tool.status.clear(); @@ -2156,18 +3252,7 @@ void catenary_editor::render_catenary_cantilever_ui() ImGui::InputText(STR_C("Catenary file"), tool.file, sizeof(tool.file)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("File in the scenery directory the new poles go to; a new one is added to the scenery with the directive catenary endcatenary when it's saved")); - ImGui::Checkbox(STR_C("Chain: a span from the last pole"), &tool.chaining); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Each pole placed is joined with a span to the one placed before it, one chain from anchor to anchor")); - if (tool.last >= 0 && catenary_scene_get(static_cast(tool.last)) != nullptr) - { - ImGui::SameLine(); - if (ImGui::Button(STR_C("End the chain"))) - { - tool.chain = -1; - tool.last = -1; - } - } + render_catenary_chain_state(); } else ImGui::TextColored(ImVec4(0.3f, 0.85f, 1.0f, 1.0f), "%s", format(STR_C("Selected pole (%s): changes apply to it at once"), catenary_scene_file_of(static_cast(tool.selected)).c_str()).c_str()); @@ -2262,13 +3347,34 @@ void catenary_editor::render_catenary_cantilever_ui() catenary_compose(spot, glm::dot(plan_of(pole_of(*current)) - plan_of(spot.axis), right) - 0.5 * own->depth(0.0), tool.stagger, tool.toward, current->pole_type); std::copy(std::begin(current->pole), std::end(current->pole), spot.data.pole); } + else if (false == drop && own != nullptr && pole_sharers(static_cast(tool.selected)).size() > 1) + { + // an umbrella or spread cantilevers: the pole stays, unless its face from the track was changed - then it goes + // across the track and the other cantilever goes with it + auto const plane{current->has_spread ? glm::dvec3{current->spread[0], current->pole[1], current->spread[2]} : pole_of(*current)}; + auto const right{right_of(spot.forward) * static_cast(spot.side)}; + auto const clearance{glm::dot(plan_of(plane) - plan_of(spot.axis), right) - 0.5 * own->depth(0.0)}; + if (std::abs(clearance - static_cast(tool.clearance)) > 1e-4) + { + catenary_compose(spot, tool.clearance, tool.stagger, tool.toward); + if (spot.card != 0) + { + auto foot{glm::dvec3{spot.data.pole[0], spot.data.pole[1], spot.data.pole[2]} - (plane - pole_of(*current))}; + foot.y = current->pole[1]; + catenary_pair_move(static_cast(tool.selected), foot, true); + spot.card = -1; // done + } + } + else + catenary_keep_pole(spot, *current, tool.stagger, tool.toward); + } else catenary_compose(spot, tool.clearance, tool.stagger, tool.toward, drop ? current->pole_type : std::string{}); if (drop) spot.data.pole[1] = current->pole[1]; - if (spot.card != 0) + if (spot.card > 0) catenary_apply(spot); - else + else if (spot.card == 0) tool.status = spot.problem; } } @@ -2322,6 +3428,67 @@ void catenary_editor::render_catenary_cantilever_ui() } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Joins this pole with the poles clicked after it: it goes on the chain at its end, a new chain of the two, or two chains become one")); + // an umbrella: the second cantilever on this pole (or drop), to the track on its other side + if (data->umbrella.empty()) + { + if (ImGui::Button(tool.umbrella_from >= 0 ? STR_C("Stop the umbrella") : STR_C("Umbrella: a cantilever to the other track"))) + { + tool.umbrella_from = tool.umbrella_from >= 0 ? -1 : tool.selected; + tool.connect_from = -1; + tool.anchor_pick = -1; + tool.status = tool.umbrella_from >= 0 ? STR("LMB by the track on the other side of the pole: the second cantilever goes on this pole, with the settings of the panel") : std::string{}; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Two cantilevers on one pole or drop, to the tracks on both sides of it; on adjustable mounts (band round the pole) they share the double mounts 4640-4670 at one height")); + auto const *ownscheme{catalog->scheme(data->card, data->execution)}; + auto const steadyonly{ownscheme != nullptr && ownscheme->roles.count("strut") == 0 && ownscheme->roles.count("stay") == 0}; + if (auto const *own{catalog->pole(data->pole_type)}; own != nullptr && (steadyonly ? false == own->steady_console.empty() : false == own->console.empty())) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Spread cantilevers"))) + { + catenary_spread_create(static_cast(tool.selected)); + data = tool.selected >= 0 ? catenary_scene_get(static_cast(tool.selected)) : nullptr; + if (data == nullptr) + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Two cantilevers to this track on consoles 4890 (cards 0517/0518), 1.19 m apart along it: this one goes onto the first mount, the second onto the other; each can then hold any chain or only its messenger or its wires")); + } + } + else + { + std::string partner; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + if (auto const *other{catenary_scene_get(index)}; other != nullptr && static_cast(index) != tool.selected && other->umbrella == data->umbrella) + partner = other->name; + ImGui::TextUnformatted(format(data->has_spread ? STR_C("Spread cantilevers %s with %s") : STR_C("Umbrella %s with %s"), data->umbrella.c_str(), partner.empty() ? "-" : partner.c_str()).c_str()); + if (data->has_spread) + { + // a third one on the same pole, in its axis on its own mounts (messenger only by the spread ones holding the wires) + if (ImGui::SmallButton(STR_C("A cantilever in the axis of the pole"))) + { + catenary_third_create(static_cast(tool.selected)); + data = tool.selected >= 0 ? catenary_scene_get(static_cast(tool.selected)) : nullptr; + if (data == nullptr) + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A third cantilever to this track on the same pole, in its axis on the usual mounts (move them apart from the consoles by hand if they meet); the pole stays one")); + ImGui::SameLine(); + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Separate"))) + { + catenary_umbrella_break(static_cast(tool.selected)); + data = catenary_scene_get(static_cast(tool.selected)); + if (data == nullptr) + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The two cantilevers stay where they are, each on its own single mounts")); + } + render_catenary_chain_state(); // copying to the neighbours in the chain std::size_t position{0}; auto const chain{catenary_chain_of(static_cast(tool.selected), position)}; @@ -2350,6 +3517,7 @@ void catenary_editor::render_catenary_cantilever_ui() catenary_copy(members); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The pole, its face from the track, execution, height of the wires and insulators; each pole keeps its stagger")); + render_catenary_messenger_from(static_cast(chain)); } if (ImGui::Button(STR_C("Remove the pole"))) catenary_delete_selected(); @@ -2360,10 +3528,103 @@ void catenary_editor::render_catenary_cantilever_ui() } } +// the messenger of the chain at the selected suspension held by another cantilever on the same pole or next to it (spread +// cantilevers on the console 4890, a cantilever of the messenger only by a pull-off of the wires): chain entry messenger_from +void catenary_editor::render_catenary_messenger_from(std::size_t const Chain) +{ + auto &tool{m_catenary}; + auto const *chain{catenary_chain_get(Chain)}; + auto const *data{tool.selected >= 0 ? catenary_scene_get(static_cast(tool.selected)) : nullptr}; + if (chain == nullptr || data == nullptr || data->name.empty()) + return; + auto const file{catenary_chain_file(Chain)}; + auto const holder{catenary::messenger_holder(*chain, data->name)}; + // a cantilever of the messenger only in the chain between two others of its pole (spread steady arms 0167 with a messenger + // cantilever in the axis of the pole): it leaves the list, the wires go straight between the two, the messenger through it + { + auto const &list{chain->suspensions}; + auto const found{std::find(list.begin(), list.end(), data->name)}; + auto const position{static_cast(found - list.begin())}; + if (found != list.end() && position > 0 && position + 1 < list.size() && chain->middle != data->name) + { + auto const sharers{pole_sharers(static_cast(tool.selected))}; + auto const onpole = [&](std::string const &Name) { + return std::any_of(sharers.begin(), sharers.end(), [&](std::size_t const Index) { + auto const *other{catenary_scene_get(Index)}; + return other != nullptr && other->name == Name; + }); + }; + if (onpole(list[position - 1]) && onpole(list[position + 1])) + { + if (ImGui::Button(STR_C("Messenger only: the wires straight past it"))) + { + auto changed{*chain}; + tool.undo.push_back({++tool.group, true, Chain, std::nullopt, *chain, false}); + auto const before{list[position - 1]}, after{list[position + 1]}; + auto &pairs{changed.messenger_from}; + pairs.erase(std::remove_if(pairs.begin(), pairs.end(), [&](auto const &Pair) { return Pair.first == before || Pair.first == after || Pair.first == data->name; }), pairs.end()); + pairs.emplace_back(before, data->name); + pairs.emplace_back(after, data->name); + changed.suspensions.erase(changed.suspensions.begin() + static_cast(position)); + tool.status = format(STR_C("The wires of the chain go straight from %s to %s, the messenger through %s"), before.c_str(), after.c_str(), data->name.c_str()); + catenary_chain_update(Chain, changed); + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("This cantilever holds only the messenger: it leaves the chain, the contact wires go straight between the two cantilevers of the pole next to it in the chain, the messenger goes through this one and their cords hang from it")); + } + } + } + // the cantilevers up to 3 m from this pole (in the plan), in the file of the chain + constexpr double kHolderRange{3.0}; + std::vector names{data->name}; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *other{catenary_scene_get(index)}; + if (other == nullptr || static_cast(index) == tool.selected || other->name.empty() || false == other->has_rope || catenary_scene_file_of(index) != file) + continue; + if (glm::length(plan_of(pole_of(*other)) - plan_of(pole_of(*data))) <= kHolderRange) + names.push_back(other->name); + } + if (names.size() < 2 && holder == data->name) + return; + ImGui::PushItemWidth(160.0f); + auto const label{holder == data->name ? format(STR_C("%s (this one)"), holder.c_str()) : holder}; + if (ImGui::BeginCombo(STR_C("Messenger held by"), label.c_str())) + { + for (auto const &name : names) + { + auto const text{name == data->name ? format(STR_C("%s (this one)"), name.c_str()) : name}; + if (ImGui::Selectable(text.c_str(), name == holder) && name != holder) + { + auto changed{*chain}; + tool.undo.push_back({++tool.group, true, Chain, std::nullopt, *chain, false}); + auto &pairs{changed.messenger_from}; + pairs.erase(std::remove_if(pairs.begin(), pairs.end(), [&](auto const &Pair) { return Pair.first == data->name; }), pairs.end()); + if (name != data->name) + pairs.emplace_back(data->name, name); + tool.status = name == data->name ? STR("The messenger goes through this cantilever again") + : format(STR_C("The wires of the chain go through %s, its messenger through %s"), data->name.c_str(), name.c_str()); + catenary_chain_update(Chain, changed); + break; + } + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Another cantilever up to 3 m from this pole holds the messenger of this chain here, this one only its wires: spread cantilevers on a console, a cantilever of the messenger only, chains crossing over in a tension span")); + ImGui::PopItemWidth(); +} + void catenary_editor::render_catenary_ui() { auto &tool{m_catenary}; catenary_scan(); + if (tool.continue_pending >= 0) + { + catenary_chain_continue(static_cast(tool.continue_pending)); + tool.continue_pending = -1; + } auto const *catalog{catenary_scene_catalog()}; if (catalog == nullptr || tool.poles.empty()) { @@ -2883,6 +4144,51 @@ void catenary_editor::draw_catenary_overlay() const drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), colour, label.c_str()); } } + // anchors: the device from the face of the pole to the moving pulley and the column of weights; not assembled - red at the pole + for (std::size_t index = 0; index < catenary_anchor_size(); ++index) + { + auto const *anchor{catenary_anchor_get(index)}; + if (anchor == nullptr) + continue; + auto const selected{static_cast(index) == tool.anchor_selected}; + auto const hovered{is_intent(e.m_intent, intent_detail::anchor) && e.m_intent.index == static_cast(index)}; + double shape[4][3]; + ImVec2 head; + if (false == catenary_anchor_shape(index, shape)) + { + if (false == anchor->on.empty()) + continue; // on the pole of a suspension: that one shows what's wrong with it + glm::dvec3 const foot{anchor->pole[0], anchor->pole[1], anchor->pole[2]}; + if (glm::distance(foot, camera) > kCatenaryRange) + continue; + projection.line(drawlist, foot, foot + glm::dvec3{0.0, 8.0, 0.0}, overlay_color::invalid, 2.0f); + if (projection.project(foot + glm::dvec3{0.0, 8.3, 0.0}, head)) + drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), overlay_color::invalid, (anchor->name + " " + STR("doesn't assemble")).c_str()); + continue; + } + glm::dvec3 const face{shape[0][0], shape[0][1], shape[0][2]}, lever{shape[1][0], shape[1][1], shape[1][2]}; + glm::dvec3 const top{shape[2][0], shape[2][1], shape[2][2]}, bottom{shape[3][0], shape[3][1], shape[3][2]}; + if (glm::distance(face, camera) > kCatenaryRange) + continue; + auto const *result{catenary_anchor_result(index)}; + auto const warned{result != nullptr && false == result->warnings.empty()}; + auto const colour{selected ? overlay_color::selected : hovered ? overlay_color::highlight : warned ? overlay_color::invalid : IM_COL32(120, 200, 255, 170)}; + auto const width{selected || hovered ? 3.0f : 1.5f}; + projection.line(drawlist, face, lever, colour, width); + projection.line(drawlist, top, bottom, colour, width); + ImVec2 point; + if (projection.project(lever, point)) + drawlist->AddCircleFilled(point, width + 1.5f, colour); + if ((selected || hovered || warned) && projection.project(top + glm::dvec3{0.0, 0.4, 0.0}, head)) + { + auto label{anchor->name}; + if (result != nullptr) + label += format(" %d x, %.0f daN", result->weights, result->tension); + if (warned) + label += " (" + result->warnings.front() + ")"; + drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), colour, label.c_str()); + } + } // spans against the catalogue: green within the limit, red over it; the selected chain brighter auto const &network{tool.networks.empty() ? std::string{} : tool.networks[std::clamp(tool.network, 0, static_cast(tool.networks.size()) - 1)]}; for (auto const &span : spans_around(camera, network, tool.zone)) @@ -2910,7 +4216,22 @@ void catenary_editor::draw_catenary_overlay() const auto const colour{spot.card != 0 && false == spot.switch_error ? overlay_color::marked : overlay_color::invalid}; draw_pole(spot.data, colour, 3.0f); projection.line(drawlist, spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, pole_of(spot.data), colour, 1.5f); - if (tool.chaining && tool.last >= 0) + chain_insert insert; + if (catenary_insert_at(spot, insert)) + { + // into the span: the two new spans to its ends instead of the old one + auto const *a{catenary_scene_get(insert.from)}; + auto const *b{catenary_scene_get(insert.to)}; + if (a != nullptr && b != nullptr) + { + projection.line(drawlist, wire_of(*a), wire_of(spot.data), overlay_color::grip, 2.5f); + projection.line(drawlist, wire_of(spot.data), wire_of(*b), overlay_color::grip, 2.5f); + ImVec2 point; + if (projection.project(wire_of(spot.data) + glm::dvec3{0.0, 0.6, 0.0}, point)) + drawlist->AddText(ImVec2(point.x + 6.0f, point.y - 14.0f), overlay_color::grip, format(STR_C("into the span %s - %s"), a->name.c_str(), b->name.c_str()).c_str()); + } + } + else if (tool.chaining && tool.last >= 0) if (auto const *previous{catenary_scene_get(static_cast(tool.last))}; previous != nullptr) projection.line(drawlist, wire_of(*previous), wire_of(spot.data), colour, 1.5f); } @@ -2955,6 +4276,17 @@ void catenary_editor::catenary_drag_update() return; // a drop, or a cantilever on a mast, goes with its gantry spot.curve = tool.bend == 2 || (tool.bend == 0 && spot.radius > 0.0 && spot.radius < kCurveRadius); catenary_compose(spot, tool.clearance, tool.stagger, tool.toward); + if (pole_sharers(static_cast(tool.selected)).size() > 1) + { + // an umbrella or spread cantilevers: the pole goes where this one puts it, the other cantilever goes with it + if (spot.card == 0) + return; + auto const plane{current->has_spread ? glm::dvec3{current->spread[0], current->pole[1], current->spread[2]} : pole_of(*current)}; + auto foot{glm::dvec3{spot.data.pole[0], spot.data.pole[1], spot.data.pole[2]} - (plane - pole_of(*current))}; + foot.y = spot.data.pole[1]; + catenary_pair_move(static_cast(tool.selected), foot, false); + return; + } if (spot.card != 0) catenary_apply(spot); return; @@ -3185,10 +4517,20 @@ bool catenary_editor::catenary_intent(track_intent &Intent) Intent.detail = static_cast(intent_detail::mast); return true; } + // the tensioning device of an anchor (not while a pole is being picked for an anchor or a span) + if (m_catenary.anchor_pick < 0 && m_catenary.connect_from < 0) + if (auto const hit{catenary_anchor_hit()}; hit >= 0) + { + Intent.index = hit; + Intent.action = STR("Click: select the anchor"); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::anchor); + return true; + } if (auto const hit{catenary_hit()}; hit >= 0) { Intent.index = hit; - Intent.action = m_catenary.connect_from >= 0 ? STR("Click: hang the span to this pole") : STR("Click: select the pole"); + Intent.action = m_catenary.anchor_pick >= 0 ? STR("Click: the tensioning device on this pole") : m_catenary.connect_from >= 0 ? STR("Click: hang the span to this pole") : STR("Click: select the pole"); Intent.what = kind::catenary; Intent.detail = static_cast(intent_detail::select); return true; @@ -3209,6 +4551,22 @@ bool catenary_editor::catenary_intent(track_intent &Intent) Intent.detail = static_cast(intent_detail::chain); return true; } + // the second cantilever of an umbrella: by any track, onto the pole of the first one + if (m_catenary.umbrella_from >= 0 && e.m_hover.track != nullptr && m_catenary.page != 2) + { + catenary_spot spot; + if (catenary_umbrella_spot(*e.m_hover.track, e.m_hover.path, spot)) + { + Intent.track = e.m_hover.track; + Intent.path = e.m_hover.path; + Intent.position = spot.axis; + Intent.action = spot.card == 0 ? format(STR_C("Umbrella: %s"), spot.problem.c_str()) + : format(STR_C("Click: the second cantilever of the umbrella, card %04d execution %d, A = %.2f m"), spot.card, spot.data.execution, spot.reach); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::place); + return true; + } + } // the wires page places nothing, the gantries page places only on the selected gantry if (e.m_hover.track != nullptr && m_catenary.page != 2 && (m_catenary.page != 1 || m_catenary.gantry_selected >= 0)) { @@ -3230,27 +4588,68 @@ bool catenary_editor::catenary_intent(track_intent &Intent) std::string catenary_editor::catenary_mode_name() const { if (m_catenary.page == 2) - return m_catenary.connect_from >= 0 ? STR_C("Wires: LMB on the next pole hangs the span to it") : STR_C("Wires: LMB on a span selects its chain, LMB on a pole selects it"); + return m_catenary.anchor_pick >= 0 ? STR_C("Wires: LMB on the pole the tensioning device goes on") : m_catenary.connect_from >= 0 ? STR_C("Wires: LMB on the next pole hangs the span to it") : STR_C("Wires: LMB on a span selects its chain, LMB on a pole selects it"); if (m_catenary.page == 1) { if (m_catenary.gantry_placing) return STR_C("Gantries: LMB the masts"); return m_catenary.gantry_selected >= 0 ? STR_C("Gantries: LMB by a track puts a cantilever on the selected gantry, drag the beam to move it") : STR_C("Gantries: LMB on a gantry selects it, or place a new one below"); } + if (m_catenary.umbrella_from >= 0) + return STR_C("Umbrella: LMB by the track on the other side of the pole puts the second cantilever on it; Esc stops"); if (m_catenary.selected >= 0) - return STR_C("Catenary: the selected pole follows the settings below"); + return STR_C("Catenary: the selected pole follows the settings below; C goes on with the chain from it"); + if (m_catenary.chaining && m_catenary.last >= 0) + return STR_C("Catenary: LMB by the track places the next pole of the chain; Esc ends the chain"); return STR_C("Catenary: LMB by the track places a pole on that side, LMB on a pole selects it"); } void catenary_editor::catenary_key_hints(std::vector &Hints) const { Hints = {{"LMB by a track", "places a pole on that side, with the card for its distance"}, {"LMB on a pole", "selects it: the settings then change it at once"}}; + Hints.push_back({"LMB on a tensioning device", "selects the anchor and its chain"}); + Hints.push_back({"LMB under a span", "puts the pole into the chain between the two poles"}); + if (m_catenary.chaining && m_catenary.last >= 0 && m_catenary.selected < 0) + Hints.push_back({"Esc", "ends the chain: the next pole starts a new one"}); if (m_catenary.selected >= 0) + { + if (m_catenary.umbrella_from >= 0) + Hints.push_back({"LMB by the other track", "the second cantilever of the umbrella on this pole"}); + Hints.push_back({"C", "the chain goes on from the selected pole"}); Hints.push_back({"Del", "removes the selected pole"}); + } + else if (m_catenary.anchor_selected >= 0) + Hints.push_back({"Del", "removes the selected anchor"}); } bool catenary_editor::catenary_on_key(int const Key, int const Action, int const Mods) { + auto &tool{m_catenary}; + if (Action != GLFW_PRESS || (Mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SHIFT)) != 0 || ImGui::GetIO().WantTextInput) + return false; + if (Key == GLFW_KEY_ESCAPE) + { + // first what's being picked, then the chain going on; with nothing of that the editor has the key + if (tool.anchor_pick >= 0 || tool.connect_from >= 0 || tool.umbrella_from >= 0) + { + tool.anchor_pick = -1; + tool.connect_from = -1; + tool.umbrella_from = -1; + tool.status.clear(); + return true; + } + if (tool.chaining && tool.last >= 0 && tool.selected < 0) + { + catenary_chain_end(); + return true; + } + return false; + } + if (Key == GLFW_KEY_C && tool.selected >= 0) + { + catenary_chain_continue(static_cast(tool.selected)); + return true; + } return false; } diff --git a/application/editormodecatenary.h b/application/editormodecatenary.h index c17abab0..1964c45d 100644 --- a/application/editormodecatenary.h +++ b/application/editormodecatenary.h @@ -29,7 +29,7 @@ struct catenary_editor using track_intent = editor_mode::track_intent; using key_hint = editor_mode::key_hint; // what a catenary intent (track_intent::kind::catenary) is about, in its detail - enum class intent_detail { place, select, chain, gantry, mast }; + enum class intent_detail { place, select, chain, gantry, mast, anchor }; static bool is_intent(track_intent const &Intent, intent_detail const Detail) { return Intent.what == track_intent::kind::catenary && Intent.detail == static_cast(Detail); @@ -46,6 +46,7 @@ struct catenary_editor glm::dvec2 forward{0.0, 1.0}; int side{1}; // the pole on the right (+1) or on the left (-1) of the direction of the path double radius{0.0}; // of the track there, 0 on a straight + int inward{0}; // the centre of the bend: +1 on the right of the direction of the path, -1 on the left, 0 straight bool curve{false}; bool toward{true}; // the wires are offset towards the pole double reach{0.0}; // A: from the wires to the face of the pole @@ -90,11 +91,19 @@ struct catenary_editor int selected{-1}; // chains: the poles placed one after another are joined by spans bool chaining{true}; + int continue_pending{-1}; // the chain goes on from this pole, from the next frame of the panel (the button is in the middle of it) + bool insert_spans{true}; // a pole placed in a span of a chain goes into the chain between the two poles of the span int chain{-1}; // the chain the next pole is added to int last{-1}; // the pole the next span starts at bool append{true}; // the next pole goes after the last one of the chain, otherwise before the first int chain_selected{-1}; // the chain shown in the panel int connect_from{-1}; // hanging the wires on from this suspension: the next poles clicked get the spans + int umbrella_from{-1}; // an umbrella from this suspension: the next LMB by a track puts the second cantilever on its pole (or drop) + int anchor_pick{-1}; // anchoring this chain to an existing pole: the next pole clicked carries the tensioning device + bool anchor_pick_start{false}; // ... at the start of the chain, otherwise at its end + bool anchor_pick_contact{false}; // ... the contact wires only (anchored apart from the messenger) + int anchor_selected{-1}; // the anchor picked in the view: its chain is shown, the panel marks it, Del removes it + bool anchor_scroll{false}; // the panel scrolls to the anchor picked in the view, once char power[64]{"none"}; float voltage{3000.0f}; int material{0}; // 0 cu, 1 al @@ -157,6 +166,10 @@ struct catenary_editor mutable std::vector> m_switch_paths; // the paths of switches, gathered again when the number of tracks changes mutable std::size_t m_switch_tracks{static_cast(-1)}; mutable std::map m_switch_checks; + // superelevation of the track at the spot (radians, unsigned) and the horizontal shift of the axis of a leaning + // pantograph at Height above the rail heads, towards the inside of the bend (table 23-0007, curve with superelevation) + static double catenary_cant(catenary_spot const &Spot); + static glm::dvec3 catenary_cant_shift(catenary_spot const &Spot, double Height); void catenary_compose(catenary_spot &Spot, double const Clearance, double const Stagger, bool const Toward, std::string const &Pole = {}) const; bool catenary_on_mast(catenary::gantry const &Gantry, double const Stagger, catenary_spot &Spot) const; catenary_spot const *catenary_spot_of(std::size_t const Index); @@ -168,7 +181,38 @@ struct catenary_editor void catenary_copy(std::vector const &Targets); bool catenary_propose_toward(bool &Toward); void catenary_place(catenary_spot const &Spot); + // the second cantilever of an umbrella on the pole (or drop) of tool.umbrella_from, towards this track; false when it can't + // be put there (Spot.problem says why when Spot.card is 0) + bool catenary_umbrella_spot(TTrack &Track, int Path, catenary_spot &Spot); + void catenary_umbrella_place(catenary_spot const &Spot); + // the two cantilevers of the umbrella of this suspension become separate ones + void catenary_umbrella_break(std::size_t Index); + // a cantilever of an umbrella or of spread cantilevers: composed with its pole (and its console mount) where they are + void catenary_keep_pole(catenary_spot &Spot, catenary::suspension const &Current, double Stagger, bool Toward) const; + // spread cantilevers on the console of the pole: this one onto the first mount, a second one onto the other, same track + void catenary_spread_create(std::size_t Index); + // the pole of an umbrella or of spread cantilevers moved to Foot (rail heads): both cantilevers go with it + // (Panel: from the distance slider - the selected one takes the stagger of the panel, the slider is one undo group) + void catenary_pair_move(std::size_t Selected, glm::dvec3 const &Foot, bool Panel); + // a third cantilever on the pole of spread cantilevers, in the axis of the pole, to the same track (shared_pole) + void catenary_third_create(std::size_t Index); + // the span of a chain the spot falls in (in the plan: the wire point near the line of the span, between its ends) + struct chain_insert + { + int chain{-1}; + std::size_t after{0}; // the new pole goes after this position of the chain + std::size_t from{0}, to{0}; // the suspensions at both ends of the span + }; + bool catenary_insert_at(catenary_spot const &Spot, chain_insert &Insert) const; + // the next pole placed starts a new chain + void catenary_chain_end(); + // the next poles placed go on from this pole: at the end (or the start) of its chain, or a new chain from it; + // false when the pole is in the middle of a chain + bool catenary_chain_continue(std::size_t Pole); + // the chain block of the poles page: what the next click does, end, anchor, go on from the selected pole + void render_catenary_chain_state(); void catenary_chain_apply(); + void render_catenary_messenger_from(std::size_t Chain); void catenary_connect(std::size_t const To); int catenary_gantry_hit() const; bool catenary_gantry_beam(std::size_t const Index, glm::dvec3 &From, glm::dvec3 &To, double &Bottom) const; @@ -194,7 +238,15 @@ struct catenary_editor // the anchors at both ends of the selected chain: add, settings, remove void render_catenary_anchor_ui(std::size_t Chain); // the anchor of the chain at its start (Start) or end; catenary_anchor_size() when none - std::size_t catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool Start) const; + std::size_t catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool Start, bool Contact = false) const; + // anchors the start or the end of the chain: on a new pole past the last one, or (On >= 0) on that existing pole + bool catenary_anchor_create(std::size_t Chain, bool Start, int On, bool Contact = false); + // removes the anchor at the start or the end of the chain (one undo step with the chain) + void catenary_anchor_delete(std::size_t Chain, bool Start, bool Contact = false); + // the anchor whose tensioning device is under the cursor; -1 if none + int catenary_anchor_hit() const; + // shows the chain in the panel as a click on its span does + void catenary_chain_show(int Chain); void render_catenary_cantilever_ui(); void render_catenary_wires_ui(); void catenary_choose_page(int const Page); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 17e0fdbe..ddad587c 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5894,3 +5894,360 @@ msgstr "żółte, pędzlowane" msgid "grey, shabby" msgstr "szare, brzydkie" + +msgid "Stop picking the pole" +msgstr "Przerwij wybieranie słupa" + +msgid "to an existing pole" +msgstr "do istniejącego słupa" + +msgid "LMB on the pole the tensioning device goes on" +msgstr "LPM na słup, na którym ma stanąć urządzenie naprężające" + +msgid "The tensioning device goes on a pole already standing (with its cantilever); the pole stays as it is" +msgstr "Urządzenie naprężające na słupie, który już stoi (z wysięgnikiem); słup zostaje jak jest" + +msgid "On the pole of %s" +msgstr "Na słupie %s" + +msgid "This pole can't carry the anchor: it has to be a pole (not a drop or a point) from the file of the chain" +msgstr "Na tym słupie nie da się zakotwić: musi to być słup (nie zwis ani punkt) z pliku łańcucha" + +msgid "The anchor goes on another pole than the last one of the chain" +msgstr "Kotwienie musi być na innym słupie niż ostatni słup łańcucha" + +msgid "Click: the tensioning device on this pole" +msgstr "Klik: urządzenie naprężające na tym słupie" + +msgid "Wires: LMB on the pole the tensioning device goes on" +msgstr "Druty: LPM na słup, na którym ma stanąć urządzenie naprężające" + +msgid "Pulleys above the rail heads (m)" +msgstr "Rolki nad główką szyny (m)" + +msgid "Height of the pulley axles on the mast; the weights move up or down with them. The mounting table gives 6.70 m. Drag or Ctrl+click to type" +msgstr "Wysokość osi rolek na słupie; ciężary przesuwają się razem z nimi. Tablica montażowa podaje 6,70 m. Przeciągnij albo Ctrl+klik, żeby wpisać" + +msgid ", superelevation %.0f mm" +msgstr ", przechyłka %.0f mm" + +msgid "Click: select the anchor" +msgstr "Klik: zaznacz kotwienie" + +msgid "Anchor %s selected: its settings are below, Del removes it" +msgstr "Zaznaczone kotwienie %s: ustawienia poniżej, Del je usuwa" + +msgid "Anchor %s isn't at the end of any chain" +msgstr "Kotwienie %s nie kończy żadnego łańcucha" + +msgid "Anchor removed" +msgstr "Kotwienie usunięte" + +msgid "(selected in the view)" +msgstr "(zaznaczone w oknie)" + +msgid "LMB on a tensioning device" +msgstr "LPM na urządzeniu naprężającym" + +msgid "selects the anchor and its chain" +msgstr "zaznacza kotwienie i jego łańcuch" + +msgid "removes the selected anchor" +msgstr "usuwa zaznaczone kotwienie" + +msgid "The chain is ended: the next pole starts a new one" +msgstr "Łańcuch zakończony: następny słup zaczyna nowy" + +msgid "The pole is in the middle of a chain: the chain goes on from its ends only" +msgstr "Słup jest w środku łańcucha: łańcuch można ciągnąć tylko od jego końców" + +msgid "The next poles go on from %s: LMB by the track; Esc ends the chain" +msgstr "Następne słupy idą od %s: LPM przy torze; Esc kończy łańcuch" + +msgid "; put into chain %s between %s and %s" +msgstr "; wstawiony do łańcucha %s między %s a %s" + +msgid "into the span %s - %s" +msgstr "w przęsło %s - %s" + +msgid "In the middle of chain %s (pole %zu of %zu)" +msgstr "W środku łańcucha %s (słup %zu z %zu)" + +msgid "Go on with the chain from this pole (C)" +msgstr "Ciągnij łańcuch od tego słupa (C)" + +msgid "Start a chain from this pole (C)" +msgstr "Zacznij łańcuch od tego słupa (C)" + +msgid "The poles placed next by the track are joined to this one with spans, one after another; Esc ends the chain" +msgstr "Kolejne słupy stawiane przy torze łączą się z tym przęsłami, jeden za drugim; Esc kończy łańcuch" + +msgid "start of chain %s" +msgstr "początek łańcucha %s" + +msgid "end of chain %s" +msgstr "koniec łańcucha %s" + +msgid "Into spans" +msgstr "W przęsła" + +msgid "A pole placed under the span of a chain (its wire point within 1.5 m of the span, not at its ends) goes into that chain between the two poles" +msgstr "Słup postawiony pod przęsłem łańcucha (punkt drutu do 1,5 m od przęsła, nie przy jego końcach) wchodzi do łańcucha między te dwa słupy" + +msgid "The next pole starts a new chain; LMB on a pole and C goes on from it" +msgstr "Następny słup zaczyna nowy łańcuch; LPM na słupie i C – ciągnięcie od niego" + +msgid "Next pole: a span from %s, at the end of chain %s (%zu poles)" +msgstr "Następny słup: przęsło od %s, na końcu łańcucha %s (%zu słupów)" + +msgid "Next pole: a span from %s, at the start of chain %s (%zu poles)" +msgstr "Następny słup: przęsło od %s, na początku łańcucha %s (%zu słupów)" + +msgid "Next pole: a new chain from %s" +msgstr "Następny słup: nowy łańcuch od %s" + +msgid "End the chain (Esc)" +msgstr "Zakończ łańcuch (Esc)" + +msgid "Anchor here and end" +msgstr "Zakotwij tu i zakończ" + +msgid "Anchored and ended: the anchor pole is 24 m past the last pole; its settings are on the Wires page" +msgstr "Zakotwiono i zakończono: słup kotwowy 24 m za ostatnim słupem; ustawienia na stronie Druty" + +msgid "An anchor pole 24 m past the last pole, in line with it, with the weight tensioning device; the chain is ended" +msgstr "Słup kotwowy 24 m za ostatnim słupem, w jego linii, z urządzeniem naprężającym; łańcuch zostaje zakończony" + +msgid "Catenary: the selected pole follows the settings below; C goes on with the chain from it" +msgstr "Sieć trakcyjna: zaznaczony słup idzie za ustawieniami poniżej; C ciągnie od niego łańcuch" + +msgid "Catenary: LMB by the track places the next pole of the chain; Esc ends the chain" +msgstr "Sieć trakcyjna: LPM przy torze stawia następny słup łańcucha; Esc kończy łańcuch" + +msgid "LMB under a span" +msgstr "LPM pod przęsłem" + +msgid "puts the pole into the chain between the two poles" +msgstr "wstawia słup do łańcucha między te dwa słupy" + +msgid "ends the chain: the next pole starts a new one" +msgstr "kończy łańcuch: następny słup zaczyna nowy" + +msgid "the chain goes on from the selected pole" +msgstr "łańcuch idzie dalej od zaznaczonego słupa" + +msgid "by the wires (roller ones when tarnished)" +msgstr "wg drutów (rolkowe przy zaśniedziałych)" + +msgid "resin 7150" +msgstr "żywiczne 7150" + +msgid "roller ESN-2025 on both" +msgstr "rolkowe ESN-2025 na obu" + +msgid "roller on the messenger only" +msgstr "rolkowy tylko na linie nośnej" + +msgid "roller on the contact wires only" +msgstr "rolkowe tylko na przewodach" + +msgid "Insulators" +msgstr "Izolatory" + +msgid "Old assemblies have the roller insulator ESN-2025 straight in the lever hole, then the turnbuckle and the clamp; by default the roller ones come with tarnished wires, as the 5110 clamps do" +msgstr "Stare złożenia mają izolator rolkowy ESN-2025 wprost w otworze dźwigni, za nim śrubę rzymską i uchwyt; domyślnie rolkowe są przy drutach zaśniedziałych, tak jak uchwyty 5110" + +msgid "Anchors" +msgstr "Kotwienia" + +msgid "fixed (card %s)" +msgstr "stałe (karta %s)" + +msgid "weight tensioning (card %s)" +msgstr "ciężarowe (karta %s)" + +msgid "Weight tensioning keeps the tension with the air temperature; a fixed anchor holds the wires straight on the pole (the other end of the half-section has the weights)" +msgstr "Ciężarowe trzyma naciąg przy każdej temperaturze; stałe trzyma druty wprost na słupie (ciężary są na drugim końcu połówki odcinka)" + +msgid "Contact wire anchor above the rail heads (m)" +msgstr "Kotwienie przewodów nad główką szyny (m)" + +msgid "Height of the anchor of the contact wires on the mast (the messenger one goes as much higher as at the last pole); by default at the height of the wires at the last pole. Drag or Ctrl+click to type" +msgstr "Wysokość zakotwienia przewodów jezdnych na słupie (lina nośna o tyle wyżej, co przy ostatnim słupie); domyślnie na wysokości drutów przy ostatnim słupie. Przeciągnij albo Ctrl+klik, żeby wpisać" + +msgid "Fixed anchor: messenger %.2f m, contact wires %.2f m over the rails" +msgstr "Kotwienie stałe: lina %.2f m, przewody %.2f m nad główką szyny" + +msgid "weights, pulleys in series (card %s)" +msgstr "ciężarowe, rolki szeregowe (karta %s)" + +msgid "weights, messenger and contact wires separately (card %s)" +msgstr "ciężarowe rozdzielone, lina i przewody osobno (karta %s)" + +msgid "Messenger anchor above the rail heads (m)" +msgstr "Kotwienie liny nośnej nad główką szyny (m)" + +msgid "Height of the anchor of the messenger on the mast; by default as at the last pole. Drag or Ctrl+click to type" +msgstr "Wysokość zakotwienia liny nośnej na słupie; domyślnie jak przy ostatnim słupie. Przeciągnij albo Ctrl+klik, żeby wpisać" + +msgid "Heights as at the last pole" +msgstr "Wysokości jak przy ostatnim słupie" + +msgid "pole %d (%s)" +msgstr "słup %d (%s)" + +msgid "Middle anchor" +msgstr "Kotwienie środkowe" + +msgid "The anchor rope is clamped on the messenger 1 m on each side of this pole and goes to the poles next to it; in the middle of both spans a V ties it to the contact wire. The weight anchors measure their distance from it" +msgstr "Lina kotwienia jest zaciśnięta na linie nośnej 1 m po obu stronach tego słupa i idzie do słupów obok; w połowie obu przęseł V wiąże ją z przewodem jezdnym. Kotwienia ciężarowe liczą odległość od niego" + +msgid "Middle anchor kind" +msgstr "Rodzaj kotwienia środkowego" + +msgid "with insulation (card 0406)" +msgstr "z izolacją (karta 0406)" + +msgid "without insulation (card 0408)" +msgstr "bez izolacji (karta 0408)" + +msgid "none" +msgstr "brak" + +msgid "Distance from the middle anchor: along the chain" +msgstr "Odległość od kotwienia środkowego: wzdłuż łańcucha" + +msgid "Anchor rope on the pole before (m)" +msgstr "Lina kotwienia na słupie przed (m)" + +msgid "Anchor rope on the pole after (m)" +msgstr "Lina kotwienia na słupie za (m)" + +msgid "Height of the anchor of the middle anchor rope on that pole above the rail heads; by default at the messenger. Move it when it hits the cantilever" +msgstr "Wysokość zakotwienia liny kotwienia środkowego na tym słupie nad główką szyny; domyślnie przy linie nośnej. Przesuń, gdy wchodzi w wysięgnik" + +msgid "Anchor rope at the messenger" +msgstr "Lina kotwienia przy linie nośnej" + +msgid "This track is on the same side of the pole as its cantilever: the second cantilever of an umbrella goes to a track on the other side" +msgstr "Ten tor jest po tej samej stronie słupa co jego wysięgnik: drugi wysięgnik parasola idzie do toru po drugiej stronie" + +msgid "Umbrella %s: the second cantilever (card %d execution %d, A = %.2f m) on pole %s of %s; hang its wires with the button below or C" +msgstr "Parasol %s: drugi wysięgnik (karta %d wykonanie %d, A = %.2f m) na słupie %s podwieszenia %s; druty rozwieś przyciskiem niżej albo C" + +msgid "Umbrella %s separated: two cantilevers on their own mounts" +msgstr "Parasol %s rozdzielony: dwa wysięgniki na własnych mocowaniach" + +msgid "Umbrella: %s" +msgstr "Parasol: %s" + +msgid "Click: the second cantilever of the umbrella, card %04d execution %d, A = %.2f m" +msgstr "Kliknij: drugi wysięgnik parasola, karta %04d wykonanie %d, A = %.2f m" + +msgid "Umbrella: LMB by the track on the other side of the pole puts the second cantilever on it; Esc stops" +msgstr "Parasol: LPM przy torze po drugiej stronie słupa stawia na nim drugi wysięgnik; Esc przerywa" + +msgid "Stop the umbrella" +msgstr "Przerwij parasol" + +msgid "Umbrella: a cantilever to the other track" +msgstr "Parasol: wysięgnik do drugiego toru" + +msgid "LMB by the track on the other side of the pole: the second cantilever goes on this pole, with the settings of the panel" +msgstr "LPM przy torze po drugiej stronie słupa: drugi wysięgnik staje na tym słupie, z ustawieniami panelu" + +msgid "Two cantilevers on one pole or drop, to the tracks on both sides of it; on adjustable mounts (band round the pole) they share the double mounts 4640-4670 at one height" +msgstr "Dwa wysięgniki na jednym słupie albo zwisie, do torów po obu jego stronach; przy mocowaniach regulowanych (opaska wokół słupa) mają wspólne mocowania dwustronne 4640–4670 na jednej wysokości" + +msgid "Umbrella %s with %s" +msgstr "Parasol %s z %s" + +msgid "Separate" +msgstr "Rozdziel" + +msgid "The two cantilevers stay where they are, each on its own single mounts" +msgstr "Oba wysięgniki zostają na miejscu, każdy na własnych mocowaniach jednostronnych" + +msgid "LMB by the other track" +msgstr "LPM przy drugim torze" + +msgid "the second cantilever of the umbrella on this pole" +msgstr "drugi wysięgnik parasola na tym słupie" + +msgid "%s (this one)" +msgstr "%s (ten)" + +msgid "Messenger held by" +msgstr "Linę nośną trzyma" + +msgid "The messenger goes through this cantilever again" +msgstr "Lina nośna znowu idzie przez ten wysięgnik" + +msgid "The wires of the chain go through %s, its messenger through %s" +msgstr "Przewody jezdne łańcucha idą przez %s, lina nośna przez %s" + +msgid "Another cantilever up to 3 m from this pole holds the messenger of this chain here, this one only its wires: spread cantilevers on a console, a cantilever of the messenger only, chains crossing over in a tension span" +msgstr "Linę nośną łańcucha trzyma tu inny wysięgnik do 3 m od tego słupa, ten tylko przewody jezdne: wysięgniki rozstawione na konsoli, wysięgnik samej liny, łańcuchy krzyżujące się w przęśle naprężenia" + +msgid "Two cantilevers share this pole (umbrella or spread cantilevers): separate them to move it" +msgstr "Na tym słupie są dwa wysięgniki (parasol albo rozstawione): rozdziel je, żeby go przesunąć" + +msgid "Pole %s carries no console for spread cantilevers (\"console\" in its entry in traction_supports.txt)" +msgstr "Słup %s nie ma konsoli wysięgników rozstawionych (\"console\" w jego wpisie w traction_supports.txt)" + +msgid "Spread cantilevers %s on console %s: %s and %s, %.2f m apart; give each its chain, \"Messenger held by\" splits the messenger and the wires between them" +msgstr "Wysięgniki rozstawione %s na konsoli %s: %s i %s, %.2f m od siebie; każdemu daj jego łańcuch, „Linę nośną trzyma” dzieli linę i przewody między nie" + +msgid "Spread cantilevers" +msgstr "Wysięgniki rozstawione" + +msgid "Two cantilevers to this track on consoles 4890 (cards 0517/0518), 1.19 m apart along it: this one goes onto the first mount, the second onto the other; each can then hold any chain or only its messenger or its wires" +msgstr "Dwa wysięgniki do tego toru na konsolach 4890 (karty 0517/0518), 1,19 m od siebie wzdłuż toru: ten idzie na pierwsze mocowanie, drugi na drugie; potem każdy może trzymać dowolny łańcuch albo samą jego linę nośną lub same przewody" + +msgid "Spread cantilevers %s with %s" +msgstr "Wysięgniki rozstawione %s z %s" + +msgid "Cantilever %s in the axis of pole %s (card %d execution %d): set its card (a messenger only one: 1002-1015, 1290-1297), hang it in a chain, an umbrella to the other track can go on it too" +msgstr "Wysięgnik %s w osi słupa %s (karta %d wykonanie %d): ustaw mu kartę (samej liny: 1002–1015, 1290–1297), rozwieś w łańcuchu; może też dostać parasol do drugiego toru" + +msgid "A cantilever in the axis of the pole" +msgstr "Wysięgnik w osi słupa" + +msgid "A third cantilever to this track on the same pole, in its axis on the usual mounts (move them apart from the consoles by hand if they meet); the pole stays one" +msgstr "Trzeci wysięgnik do tego toru na tym samym słupie, w jego osi na zwykłych mocowaniach (jeśli wejdą w konsole, rozsuń ręcznie); słup zostaje jeden" + +msgid "insulated on one side (card 0418)" +msgstr "z jednostronną izolacją (karta 0418)" + +msgid "to the beam of a gantry (card 0416)" +msgstr "do dźwigara bramki (karta 0416)" + +msgid "0416: the middle pole is a drop of a gantry; the anchor ropes go from the beam above the messenger to clamps on it about 3 m on both sides, the poles next to it carry nothing" +msgstr "0416: podwieszenie środkowe jest na zwisie bramki; liny kotwienia idą od dźwigara nad liną nośną do zacisków na niej ok. 3 m z obu stron, słupy sąsiednie nic nie niosą" + +msgid "Contact wires anchored apart: on another pole" +msgstr "Przewody jezdne zakotwione osobno: na innym słupie" + +msgid "LMB on the pole the anchor of the contact wires goes on" +msgstr "LPM na słupie, na którym ma być kotwienie przewodów jezdnych" + +msgid "The messenger stays on the anchor above (it becomes one of the messenger only: 0405 or the messenger of a split one), the contact wires go to an anchor of their own on the pole picked (0404 or the wires of a split one)" +msgstr "Lina nośna zostaje na kotwieniu wyżej (staje się kotwieniem samej liny: 0405 albo liną rozdzielonego), przewody jezdne idą na własne kotwienie na wskazanym słupie (0404 albo przewody rozdzielonego)" + +msgid "The contact wires are anchored on their own; the other anchor holds the messenger only" +msgstr "Przewody jezdne zakotwione osobno; drugie kotwienie trzyma samą linę nośną" + +msgid "contact wires" +msgstr "przewody jezdne" + +msgid "messenger" +msgstr "lina nośna" + +msgid "Messenger only: the wires straight past it" +msgstr "Sama lina: przewody prosto obok" + +msgid "The wires of the chain go straight from %s to %s, the messenger through %s" +msgstr "Przewody łańcucha idą prosto od %s do %s, lina nośna przez %s" + +msgid "This cantilever holds only the messenger: it leaves the chain, the contact wires go straight between the two cantilevers of the pole next to it in the chain, the messenger goes through this one and their cords hang from it" +msgstr "Ten wysięgnik trzyma tylko linę nośną: wypada z listy łańcucha, przewody jezdne idą prosto między dwoma wysięgnikami tego słupa obok niego w łańcuchu, lina nośna przez niego, a ich linki wiszą na niej" diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index f4dd0212..03afe621 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -4887,6 +4887,13 @@ void opengl33_renderer::Render_Alpha(TTraction *Traction) draw(solid.fittings); ++m_renderpass.draw_stats.drawcalls; } + // lina urządzenia naprężającego przy końcu przęsła (stal) + if (solid.rope != null_handle) + { + set_metal(catenary_rope_metal()); + draw(solid.rope); + ++m_renderpass.draw_stats.drawcalls; + } ::glDepthMask(depthwrite); } diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index a4bd8f61..c9f2650e 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -3673,6 +3673,13 @@ opengl_renderer::Render_Alpha( TTraction *Traction ) { m_geometry.draw( solid.fittings ); ++m_renderpass.draw_stats.drawcalls; } + // lina urządzenia naprężającego przy końcu przęsła (stal) + if( solid.rope != null_handle ) { + auto const &steel { catenary_rope_metal() }; + ::glColor4fv( glm::value_ptr( glm::vec4{ steel.color[ 0 ], steel.color[ 1 ], steel.color[ 2 ], 1.f } ) ); + m_geometry.draw( solid.rope ); + ++m_renderpass.draw_stats.drawcalls; + } ::glDepthMask( depthwrite ); } // debug data diff --git a/world/Traction.cpp b/world/Traction.cpp index 0d46f518..387b855c 100644 --- a/world/Traction.cpp +++ b/world/Traction.cpp @@ -140,6 +140,9 @@ TTraction::Load( cParser *parser, glm::dvec3 const &pOrigin ) { std::string token { parser->getToken() }; std::string catenarytype; + double catenarytension[ 2 ] { 0.0, 0.0 }; + glm::dvec3 catenaryrope2[ 2 ]; + bool catenaryrope2set[ 2 ] { false, false }; while( false == token.empty() && token != "endtraction" ) { @@ -152,11 +155,24 @@ TTraction::Load( cParser *parser, glm::dvec3 const &pOrigin ) { // symbol typu sieci, np. YwsC120-2C; starsze wersje symulatora pomijają ten dopisek catenarytype = parser->getToken( false ); } + else if( token == "rope2" ) { + // koniec drugiej liny nośnej przy kotwieniu: "rope2 start|end x y z" + auto const which { parser->getToken() }; + auto const point { LoadPoint( *parser ) + pOrigin }; + auto const end { which == "end" ? 1 : 0 }; + catenaryrope2[ end ] = point; + catenaryrope2set[ end ] = true; + } + else if( token == "tension" ) { + // naciąg liny nośnej i przewodów jezdnych w daN inny niż w typie sieci (brakujące ciężary w kotwieniu) + parser->getTokens( 2 ); + *parser >> catenarytension[ 0 ] >> catenarytension[ 1 ]; + } token = parser->getToken(); } Init(); // przeliczenie parametrów - catenary_span_assign( *this, catenarytype ); + catenary_span_assign( *this, catenarytype, catenarytension[ 0 ], catenarytension[ 1 ], catenaryrope2, catenaryrope2set ); // calculate traction location location( glm::mix( pPoint2, pPoint1, 0.5 ) ); diff --git a/world/catenary_cantilever.cpp b/world/catenary_cantilever.cpp index b08f135a..2cd0177c 100644 --- a/world/catenary_cantilever.cpp +++ b/world/catenary_cantilever.cpp @@ -280,6 +280,7 @@ void cantilever_catalog::parse( std::vector const &Tokens, std::str else if( key == "top" ) { number( pole.top, good ); } else if( key == "length" ) { number( pole.length, good ); } else if( key == "depth" ) { number( pole.depth_top, good ); number( pole.depth_base, good ); } + else if( key == "width" ) { number( pole.width, good ); } else if( key == "mount_max" ) { number( pole.mount_max, good ); } else if( key == "mount_min" ) { number( pole.mount_min, good ); pole.has_mount_min = true; } else if( key == "turn" ) { number( pole.turn, good ); } @@ -287,10 +288,13 @@ void cantilever_catalog::parse( std::vector const &Tokens, std::str else if( key == "strut" ) { text( pole.strut ); } else if( key == "stay" ) { text( pole.stay ); } else if( key == "steady" ) { text( pole.steady ); } + else if( key == "console" ) { text( pole.console ); } + else if( key == "steady_console" ) { text( pole.steady_console ); } else { report( entry, "unknown keyword \"" + key + "\"" ); good = false; --index; } } else { if( key == "back" ) { text( mount.back ); } + else if( key == "console" ) { mount.console = true; number( mount.console_x[ 0 ], good ); number( mount.console_x[ 1 ], good ); number( mount.console_face, good ); } else if( key == "hinge" ) { text( mount.hinge ); number( mount.hinge_eye[ 0 ], good ); number( mount.hinge_eye[ 1 ], good ); } else if( key == "rod" ) { text( mount.rod ); } else if( key == "twin" ) { text( mount.twin ); } @@ -2071,6 +2075,20 @@ bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_sch return solve_cantilever_on_pole( Catalog, Scheme, Pole, Distance, RailHead, Messenger, Overrides, cantilever_umbrella(), Result ); } +void spread_mounts( cantilever_pole const &Pole, cantilever_result const &First, cantilever_result const &Second, + cantilever_umbrella &FirstSide, cantilever_umbrella &SecondSide ) { + + FirstSide.shared.clear(); + SecondSide.shared.clear(); + for( auto const *role : { "stay", "strut", "steady" } ) { + auto const a { First.values.find( std::string( "mount." ) + role ) }, b { Second.values.find( std::string( "mount." ) + role ) }; + if( a == First.values.end() || b == Second.values.end() ) { continue; } + auto shared { 0.5 * ( a->second + b->second ) }; + if( Pole.mount_max != 0.0 ) { shared = std::min( shared, std::max( Pole.mount_max, std::min( a->second, b->second ) ) ); } + FirstSide.shared[ role ] = SecondSide.shared[ role ] = shared; + } +} + void umbrella_mounts( cantilever_catalog const &Catalog, cantilever_pole const &Pole, cantilever_result const &First, cantilever_result const &Second, cantilever_umbrella &FirstSide, cantilever_umbrella &SecondSide ) { @@ -2142,17 +2160,40 @@ bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme co if( mount == nullptr ) { fail( std::string( Use ) + " mount \"" + Name + "\" isn't in the catalogue" ); } return mount; }; - auto const *strut { mountpart( Pole.strut, "strut" ) }; - auto const *stay { mountpart( Pole.stay, "stay" ) }; - auto const *steady { Scheme.steady_mounted ? mountpart( Pole.steady, "steady arm" ) : nullptr }; + // wysięgniki rozstawione: mocowania -06 tej samej karty (4610-06, 4620-06, 4630-06) na konsoli słupa (4890); rozstawione + // wysięgi pomocnicze (0167, bez ukośnika i odciągu) na belce 4690 (steady_console, karta 33-0519), gdy słup ją ma + auto const onconsole = [ & ]( std::string const &Name ) { + if( false == Umbrella.spread || Name.empty() ) { return Name; } + return Name.substr( 0, Name.find( '-' ) ) + "-06"; + }; + auto const steadyonly { Scheme.roles.count( "strut" ) == 0 && Scheme.roles.count( "stay" ) == 0 }; + std::map consoles; + if( Umbrella.spread ) { + for( auto const *role : { "stay", "strut", "steady" } ) { + if( ( std::string( role ) != "steady" && steadyonly ) || ( std::string( role ) == "steady" && false == Scheme.steady_mounted ) ) { continue; } + // sam wysięg pomocniczy (0167) na belce 4690; wysięg wysięgnika trzyzaczepowego na tej samej konsoli 4890, co reszta + auto const &name { steadyonly && false == Pole.steady_console.empty() ? Pole.steady_console : Pole.console }; + auto const *found { name.empty() ? nullptr : Catalog.mount( name ) }; + if( found == nullptr || false == found->console ) { + return fail( name.empty() ? "the pole has no console for spread cantilevers (\"console\" / \"steady_console\" in its entry)" : "console \"" + name + "\" isn't in the catalogue as a console" ); + } + consoles[ role ] = found; + } + } + auto const *strut { mountpart( onconsole( Pole.strut ), "strut" ) }; + auto const *stay { mountpart( onconsole( Pole.stay ), "stay" ) }; + auto const *steady { Scheme.steady_mounted ? mountpart( onconsole( Pole.steady ), "steady arm" ) : nullptr }; if( strut == nullptr || stay == nullptr || ( Scheme.steady_mounted && steady == nullptr ) ) { return false; } + // oś słupa wzdłuż toru: przy wysięgnikach rozstawionych przesunięta od płaszczyzny wysięgnika + auto const polex { Umbrella.spread ? Umbrella.console_x : 0.0 }; std::vector poleparts; - auto const add = [ & ]( std::string const &Role, std::string const &Part, std::string const &Model, double const Y, double const Z ) { + auto const add = [ & ]( std::string const &Role, std::string const &Part, std::string const &Model, double const Y, double const Z, double const X = 0.0 ) { cantilever_placement placement; placement.role = Role; placement.part = Part; placement.model = Model; + placement.position[ 0 ] = X; placement.position[ 1 ] = Y; placement.position[ 2 ] = Z; poleparts.push_back( placement ); @@ -2163,15 +2204,25 @@ bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme co auto const fix = [ & ]( std::string const &Role, cantilever_mount const &Mount, double Bolts ) { // parasol: mocowanie dwustronne na wspólnej wysokości, bez tyłu; po stronie z nakrętkami jego połówka "twin" auto const shared { Umbrella.shared.find( Role ) }; - auto const twin { shared != Umbrella.shared.end() && false == Mount.twin.empty() }; - if( twin ) { Bolts = RailHead + shared->second; } + auto const twin { false == Umbrella.spread && shared != Umbrella.shared.end() && false == Mount.twin.empty() }; + if( twin || ( Umbrella.spread && shared != Umbrella.shared.end() ) ) { Bolts = RailHead + shared->second; } auto const height { Bolts - RailHead }; heights[ Role ] = height; if( height > Pole.top ) { notes.push_back( Role + " mount " + to_text( height - Pole.top ) + " m above the top of the pole" ); } auto const depth { Pole.depth( height ) }; - auto const face { Distance - 0.5 * depth }; + auto face { Distance - 0.5 * depth }; + auto const found { consoles.find( Role ) }; + auto const *console { found != consoles.end() ? found->second : nullptr }; + if( console != nullptr ) { + // konsola na słupie (jedna na rolę, stawia ją jedna strona), mocowanie na jej punkcie, bliżej toru o jej grubość + if( Umbrella.console_parts ) { + add( Role + "_console", console->name, "", face, Bolts, polex ); + if( false == console->back.empty() ) { add( Role + "_console_back", console->back, "", face + depth, Bolts, polex ); } + } + face += console->console_face; + } add( Role + "_mount", ( twin && Umbrella.nuts.count( Role ) ) ? Mount.twin : Mount.name, "", face, Bolts ); - if( false == twin && false == Mount.back.empty() ) { add( Role + "_back", Mount.back, "", face + depth, Bolts ); } + if( false == twin && console == nullptr && false == Mount.back.empty() ) { add( Role + "_back", Mount.back, "", face + depth, Bolts ); } if( false == Mount.hinge.empty() ) { add( Role + "_hinge", Mount.hinge, "", face + Mount.point[ 0 ], Bolts + Mount.point[ 1 ] ); } if( false == Mount.rod.empty() ) { add( Role + "_rod", Mount.rod, "", face + Mount.point[ 0 ], Bolts + Mount.point[ 1 ] ); } return cantilever_point { face + Mount.attach_y(), Bolts + Mount.attach_z() }; @@ -2179,7 +2230,7 @@ bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme co // słup bramki stawia bramka if( Umbrella.pole && false == Pole.gantry ) { - add( "pole", Pole.name, Pole.model, Distance, RailHead ); + add( "pole", Pole.name, Pole.model, Distance, RailHead, polex ); if( Pole.turn != 0.0 ) { // model obrócony wokół pionu (zwis na bramce: profil szerszym bokiem wzdłuż toru) auto const angle { Pole.turn * 3.14159265358979323846 / 180.0 }; @@ -2188,7 +2239,7 @@ bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme co std::copy( std::begin( turned ), std::end( turned ), poleparts.back().rotation ); } // fundament ma to samo zero co słup (oś słupa na wysokości główki szyny) - if( false == Pole.foundation.empty() ) { add( "foundation", Pole.name + "_foundation", Pole.foundation, Distance, RailHead ); } + if( false == Pole.foundation.empty() ) { add( "foundation", Pole.name + "_foundation", Pole.foundation, Distance, RailHead, polex ); } } auto const raise { Messenger.z - Scheme.height }; auto staybolts { Scheme.mount_stay + raise - stay->attach_z() }; diff --git a/world/catenary_cantilever.h b/world/catenary_cantilever.h index a9b540ae..113e91e9 100644 --- a/world/catenary_cantilever.h +++ b/world/catenary_cantilever.h @@ -114,6 +114,7 @@ struct cantilever_pole { double length { 0.0 }; // długość, na której biegnie zbieżność, liczona od góry double depth_top { 0.0 }; // wymiar od toru w głąb (prostopadle do toru) u góry double depth_base { 0.0 }; // to samo na końcu długości zbieżności + double width { 0.0 }; // wzdłuż toru (słup kratowy 161x: wysokość ceownika, karta 33-1611-E wymiar a); 0 = nieznana double mount_max { 0.0 }; // najwyżej położona oś śrub mocowania; 0 = bez ograniczenia (zwis na bramce: ujemna, pod dźwigarem) double mount_min { 0.0 }; // najniżej położona oś śrub mocowania (zwis: nad jego końcem); tylko gdy has_mount_min bool has_mount_min { false }; @@ -122,6 +123,8 @@ struct cantilever_pole { std::string strut; // mocowanie ukośnika (część z punktem axis i zawiasem) std::string stay; // mocowanie izolatora odciągu std::string steady; // mocowanie izolatora wysięgu pomocniczego (schematy z trzema zaczepami) + std::string console; // konsola wysięgników rozstawionych (wpis "mount ... console"); pusta = słup ich nie nosi + std::string steady_console; // belka 4690 rozstawionych wysięgów pomocniczych (karta 33-0519, wysięgi 0167); mocowanie wysięgu na niej // słup bramki z wysięgnikiem (zrobiony z wpisu "mast" z mocowaniami): model stawia bramka, nie wysięgnik; mount_max pod // dźwigarem bramki liczy sceneria wg jej poziomu (mast_low/mast_high minus below_beam), tu - pod dolnym mocowaniem bool gantry { false }; @@ -140,6 +143,11 @@ struct cantilever_mount { std::string hinge; // zawias obrotowy stawiany w punkcie axis (wspornik ukośnika) std::string rod; // pręt z uchem stawiany w punkcie eye (uchwyt odciągu) std::string twin; // połówka z nakrętkami mocowania dwustronnego (parasol); pusta = mocowanie bez regulacji, parasol z dwóch niezależnych + // konsola wysięgników rozstawionych (4890, karty 0517/0518): dwa punkty mocowań w X (wzdłuż toru) i ich odsunięcie od + // płaszczyzny słupa (Y, ujemne - w stronę toru); na konsoli mocowania -06 (4610-06, 4620-06, 4630-06) + bool console { false }; + double console_x[ 2 ] { 0.0, 0.0 }; + double console_face { 0.0 }; double point[ 2 ] { 0.0, 0.0 }; // Y, Z punktu axis (gdy jest zawias) albo eye double hinge_eye[ 2 ] { 0.0, 0.0 }; // Y, Z ucha zawiasu względem jego zera // punkt zaczepu izolatora względem zera mocowania @@ -293,6 +301,11 @@ struct cantilever_umbrella { std::map shared; // rola mocowania -> wspólna wysokość osi śrub nad główką szyny std::set nuts; // role mocowań, które po tej stronie są połówką z nakrętkami bool pole { true }; // czy stawiać słup (stawia go tylko jedna strona) + // wysięgniki rozstawione: oba po tej samej stronie słupa, na konsolach (konsola słupa), mocowania -06 na wspólnych + // wysokościach z shared; płaszczyzna wysięgnika przechodzi przez punkt mocowania na konsoli, oś słupa leży w X = ConsoleX + bool spread { false }; + double console_x { 0.0 }; + bool console_parts { true }; // czy stawiać konsole (stawia je jedna strona) }; bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_pole const &Pole, double Distance, double RailHead, cantilever_point const &Messenger, std::map const &Overrides, @@ -306,6 +319,11 @@ bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_sch void umbrella_mounts( cantilever_catalog const &Catalog, cantilever_pole const &Pole, cantilever_result const &First, cantilever_result const &Second, cantilever_umbrella &FirstSide, cantilever_umbrella &SecondSide ); +// wspólne wysokości mocowań wysięgników rozstawionych: każda rola obu wysięgników na jednej konsoli, na średniej wysokości +// (nie ponad mount_max słupa). Wypełnia shared obu stron +void spread_mounts( cantilever_pole const &Pole, cantilever_result const &First, cantilever_result const &Second, + cantilever_umbrella &FirstSide, cantilever_umbrella &SecondSide ); + // część ustawiona w układzie scenerii symulatora (X, Y w górę, Z), względem punktu osi przewodów jezdnych struct cantilever_scene_part { double offset[ 3 ] { 0.0, 0.0, 0.0 }; // przesunięcie od punktu osi przewodów jezdnych diff --git a/world/catenary_network.cpp b/world/catenary_network.cpp index 2be44403..7628bf4d 100644 --- a/world/catenary_network.cpp +++ b/world/catenary_network.cpp @@ -99,7 +99,10 @@ void type_registry::load_tables() { double mass { 0.0 }, diameter { 0.0 }; stream >> mass >> diameter; if( stream.fail() || mass <= 0.0 ) { report( "Catenary: bad wire entry in \"" + filename + "\" (line " + std::to_string( linenumber ) + ")" ); continue; } - m_wires[ key ] = { mass, diameter * 0.001 }; + // dalej opcjonalnie: przekrój mm² i moduł sprężystości GPa + double area { 0.0 }, modulus { 0.0 }; + if( stream >> area ) { stream >> modulus; } + m_wires[ key ] = { mass, diameter * 0.001, std::max( 0.0, area ) * 1e-6, std::max( 0.0, modulus ) * 1e9 }; } else if( kind == "spacing" ) { int span { 0 }; @@ -267,25 +270,34 @@ bool solve_span_points( network_definition const &Definition, std::vector 1 ? type.messenger_stack : 0.0 ) < -0.01 ) { - Error = "messenger wire would hang below the contact wire (span too long or suspension too low for this network type)"; - return false; + auto solved { false }; + // nietypowe przęsła (niska wysokość konstrukcyjna jednego końca, naciąg liny obniżony brakującymi ciężarami poniżej naciągu + // linki Y): zamiast rysowania po staremu najpierw bez linek Y, potem jako odcinek bez wieszaków + for( int attempt = 0; attempt < 3 && false == solved; ++attempt ) { + try { + if( attempt == 2 || Hangergaps.empty() ) { result = solve_free_span( type, length, input.height_start, input.height_end ); } + else { + if( attempt == 1 ) { input.y_at_start = input.y_at_end = false; } + result = solve_span( type, input, 4.0 ); } + solved = true; + } + catch( std::exception const &exception ) { + Error = exception.what(); } } + if( false == solved ) { return false; } + // lina poniżej przewodu jezdnego (przęsło za długie albo za mała wysokość konstrukcyjna dla tego typu sieci): podciągnięta + // nad przewód, wieszaki skrócone - przęsło wygląda dziwnie, ale się rysuje + // (końce zostają tam, gdzie są punkty przęsła: najniżej na prostej między nimi) + auto const lowest { ( type.messenger_count > 1 ? type.messenger_stack : 0.0 ) + 0.02 }; + for( auto *line : { &result.messenger, &result.y_start, &result.y_end } ) { + for( auto &point : *line ) { + auto const chord { input.height_start + ( input.height_end - input.height_start ) * std::clamp( point.x / length, 0.0, 1.0 ) }; + point.y = std::max( point.y, std::min( lowest, chord ) ); + } + } + for( auto &hanger : result.hangers ) { hanger.length = std::max( hanger.length, 0.0 ); } Solution.length = length; Solution.result = std::move( result ); return true; @@ -435,10 +447,27 @@ void build_geometry( network_definition const &Definition, span_solution const & if( type.messenger_count > 1 ) { std::vector middle; if( false == start.empty() ) { middle.push_back( start.back() ); } + // końce dolnej liny podane osobno (kotwienie): przesunięcie rozłożone liniowo wzdłuż przęsła + vec3 shiftstart { 0.0, 0.0, 0.0 }, shiftend { 0.0, 0.0, 0.0 }; + if( Points.messenger2_start_set && false == result.messenger.empty() ) { + auto const from { rope( result.messenger.front().x, result.messenger.front().y - type.messenger_stack ) }; + shiftstart = vec3 { Points.messenger2_start.x - from.x, Points.messenger2_start.y - from.y, Points.messenger2_start.z - from.z }; + } + if( Points.messenger2_end_set && false == result.messenger.empty() ) { + auto const to { rope( result.messenger.back().x, result.messenger.back().y - type.messenger_stack ) }; + shiftend = vec3 { Points.messenger2_end.x - to.x, Points.messenger2_end.y - to.y, Points.messenger2_end.z - to.z }; + } for( auto const &point : result.messenger ) { auto const instart { reachstart > 0.0 && point.x < reachstart + 1e-6 }; auto const inend { reachend > 0.0 && point.x > length - reachend - 1e-6 }; - if( false == instart && false == inend ) { middle.push_back( rope( point.x, point.y - type.messenger_stack ) ); } + if( false == instart && false == inend ) { + auto at { rope( point.x, point.y - type.messenger_stack ) }; + auto const t { length > 1e-9 ? point.x / length : 0.0 }; + at.x += shiftstart.x * ( 1.0 - t ) + shiftend.x * t; + at.y += shiftstart.y * ( 1.0 - t ) + shiftend.y * t; + at.z += shiftstart.z * ( 1.0 - t ) + shiftend.z * t; + middle.push_back( at ); + } } if( false == end.empty() ) { middle.push_back( end.front() ); } polyline( middle, element_kind::messenger ); diff --git a/world/catenary_network.h b/world/catenary_network.h index e0806838..c50ed5ee 100644 --- a/world/catenary_network.h +++ b/world/catenary_network.h @@ -16,6 +16,8 @@ namespace catenary { struct wire_data { double mass { 0.0 }; // kg/m double diameter { 0.0 }; // m + double area { 0.0 }; // m², przekrój czynny (do wydłużeń sprężystych); 0 = nieznany + double modulus { 0.0 }; // Pa, moduł sprężystości przewodu (liny skręcane: niższy niż drutu); 0 = nieznany }; // pełny opis typu sieci: dane dla solvera i dane potrzebne tylko do rysowania @@ -67,6 +69,9 @@ struct vec3 { double x, y, z; }; struct span_points { vec3 contact_start, contact_end; vec3 messenger_start, messenger_end; + // druga (dolna) lina nośna kończy się gdzie indziej niż o odstęp lin pod pierwszą (uchwyt krańcowy w kotwieniu z dwiema liniami) + bool messenger2_start_set { false }, messenger2_end_set { false }; + vec3 messenger2_start, messenger2_end; }; // policzone przęsło diff --git a/world/catenary_scene.cpp b/world/catenary_scene.cpp index 5bf8bcad..a09cf2fa 100644 --- a/world/catenary_scene.cpp +++ b/world/catenary_scene.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "world/catenary_tensioner.h" #include "world/Traction.h" #include "model/AnimModel.h" +#include #include "simulation/simulation.h" #include "utilities/Globals.h" @@ -55,6 +56,12 @@ struct scene_suspension { std::string file; // plik sieci, z którego pochodzi wpis (albo do którego trafi) std::vector parts; // części wysięgnika wstawione do scenerii bool removed { false }; // usunięty w edytorze + // siodło liny nośnej (4160) ostatniego złożenia: obejma liny kotwienia środkowego staje obok niego na teowniku + bool has_saddle { false }; + std::string saddle_model; + catenary::cantilever_placement saddle; + double saddle_heading { 0.0 }; + glm::dvec3 saddle_origin { 0.0 }; }; std::vector Suspensions; @@ -75,6 +82,10 @@ struct scene_chain { std::string file; std::vector spans; // przęsło k: od podwieszenia k do k+1; nullptr gdy nie dało się go zrobić bool removed { false }; + // kotwienie środkowe: części na słupach sąsiednich podwieszeń, bryły (lina, V, zaciski) dla przęsła przed i za środkowym + std::vector middle_parts; + std::vector middle_solid[ 2 ]; + std::size_t middle_span { static_cast( -1 ) }; // przęsło przed podwieszeniem środkowym (za nim: +1) }; std::vector Chains; @@ -96,8 +107,13 @@ struct scene_anchor { bool solved { false }; double messenger[ 3 ] { 0.0, 0.0, 0.0 }; // koniec liny nośnej (uchwyt krańcowy), współrzędne scenerii double contact[ 3 ] { 0.0, 0.0, 0.0 }; // koniec przewodów jezdnych (środek między przewodami) + bool has_messenger2 { false }; // druga lina nośna kończy się na swoim uchwycie (przęsło dociąga do niego dolną linę) + double messenger2[ 3 ] { 0.0, 0.0, 0.0 }; double temperature { 0.0 }; // temperatura, przy której złożono catenary::tensioner_result result; + std::vector rope; // lina jako bryła proceduralna (trójkąty, współrzędne scenerii); rysuje ją przęsło do kotwienia + // obrys urządzenia dla edytora (współrzędne scenerii): czoło słupa i rolka ruchoma na wysokości rolek, góra i spód kolumny ciężarów + double shape[ 4 ][ 3 ] { { 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 } }; }; std::vector Anchors; @@ -391,7 +407,15 @@ assemble( scene_suspension &Suspension, catenary::cantilever_catalog const &Cata // każda część to zwykły egzemplarz modelu, taki sam jak z wpisu node model std::size_t count { 0 }; auto const skin { Suspension.skin.empty() ? std::string( "none" ) : Suspension.skin }; + Suspension.has_saddle = false; for( auto const &placement : result.parts ) { + if( false == Suspension.has_saddle && placement.role == "messenger_clamp" ) { + Suspension.has_saddle = true; + Suspension.saddle = placement; + Suspension.saddle_model = ( placement.model.empty() ? Catalog.models() + placement.part : placement.model ); + Suspension.saddle_heading = heading; + Suspension.saddle_origin = glm::dvec3 { Wire.x, Wire.y, Wire.z }; + } auto const part { catenary::cantilever_to_scene( placement, heading ) }; auto entry { "node " + text_number( data.range_max, 1 ) + " " + text_number( data.range_min, 1 ) + " none model 0 0 0 0 " @@ -421,18 +445,35 @@ entry_points( catenary::suspension const &Data, glm::dvec3 &Wire, glm::dvec3 &Ro Attach = ( Data.has_pole ? glm::dvec3 { Data.pole[ 0 ], Data.pole[ 1 ], Data.pole[ 2 ] } : glm::dvec3 { Data.attach[ 0 ], Data.attach[ 1 ], Data.attach[ 2 ] } ); } +bool held_rope_at( std::size_t Index, glm::dvec3 &Rope ); + // składa wysięgnik wpisu z punktami, bez parasola; zwraca liczbę wstawionych części std::size_t assemble_alone( std::size_t const Index, catenary::cantilever_catalog const &Catalog ) { auto &suspension { Suspensions[ Index ] }; glm::dvec3 wire, rope, attach; entry_points( suspension.data, wire, rope, attach ); - return assemble( suspension, Catalog, wire, ( suspension.data.has_rope ? &rope : nullptr ), attach ); + // linę nośną łańcucha w tym miejscu trzyma inny wysięgnik: linki i zaczepy celują w nią + auto const held { held_rope_at( Index, rope ) }; + auto const *ropepoint { held || suspension.data.has_rope ? &rope : nullptr }; + // słup stawia inny wpis (trzeci wysięgnik przy rozstawionych) + catenary::cantilever_umbrella own; + own.pole = false == suspension.data.shared_pole; + if( suspension.data.has_spread ) { + // sam jeden na konsoli: płaszczyzna wysięgnika przez punkt mocowania na konsoli, konsole stawia on + auto const &point { suspension.data.spread }; + glm::dvec3 const plane { point[ 0 ], attach.y, point[ 2 ] }; + auto const heading { std::atan2( plane.x - wire.x, plane.z - wire.z ) }; + own.spread = true; + own.console_x = glm::dot( attach - plane, glm::dvec3 { -std::cos( heading ), 0.0, std::sin( heading ) } ); + return assemble( suspension, Catalog, wire, ropepoint, plane, &own ); + } + return assemble( suspension, Catalog, wire, ropepoint, attach, ( suspension.data.shared_pole ? &own : nullptr ) ); } // parasol: oba wysięgniki składane najpierw osobno, żeby poznać wysokości mocowań, potem razem na wspólnym słupie void -assemble_umbrella( std::string const &Name, std::vector const &List, catenary::cantilever_catalog const &Catalog, std::size_t &Assembled, std::size_t &Parts ) { +assemble_umbrella( std::string const &Name, std::vector const &List, catenary::cantilever_catalog const &Catalog, std::size_t &Assembled, std::size_t &Parts, bool const Report = true ) { auto const alone = [ & ]( std::size_t const Index ) { auto const count { assemble_alone( Index, Catalog ) }; if( count > 0 ) { ++Assembled; Parts += count; } @@ -441,32 +482,60 @@ assemble_umbrella( std::string const &Name, std::vector const &List auto const *pole { Catalog.pole( first.pole_type ) }; auto paired { List.size() == 2 && pole != nullptr }; if( List.size() != 2 ) { - ErrorLog( "Bad scenario: umbrella \"" + Name + "\" has " + std::to_string( List.size() ) + " suspension(s), it needs exactly two; they're assembled separately" ); + // w edytorze (kolejne zmiany obu wpisów pary) to stan chwilowy: komunikat tylko przy wczytaniu + if( Report ) { ErrorLog( "Bad scenario: umbrella \"" + Name + "\" has " + std::to_string( List.size() ) + " suspension(s), it needs exactly two; they're assembled separately" ); } } else { auto const &second { Suspensions[ List.back() ].data }; auto const dx { second.pole[ 0 ] - first.pole[ 0 ] }, dy { second.pole[ 1 ] - first.pole[ 1 ] }, dz { second.pole[ 2 ] - first.pole[ 2 ] }; if( second.pole_type != first.pole_type || std::sqrt( dx * dx + dy * dy + dz * dz ) > 0.05 ) { - ErrorLog( "Bad scenario: umbrella \"" + Name + "\" has its suspensions on different poles; they're assembled separately" ); + // w edytorze (kolejne zmiany obu wpisów pary) to stan chwilowy: komunikat tylko przy wczytaniu + if( Report ) { ErrorLog( "Bad scenario: umbrella \"" + Name + "\" has its suspensions on different poles; they're assembled separately" ); } paired = false; } } if( false == paired ) { for( auto const index : List ) { alone( index ); } return; } - glm::dvec3 wires[ 2 ], ropes[ 2 ], poles[ 2 ]; + // wysięgniki rozstawione: oba wpisy z punktem na konsoli; jeden bez niego - błąd, składane osobno + auto const spread { first.has_spread && Suspensions[ List.back() ].data.has_spread }; + if( spread != ( first.has_spread || Suspensions[ List.back() ].data.has_spread ) ) { + // w edytorze (kolejne zmiany obu wpisów pary) to stan chwilowy: komunikat tylko przy wczytaniu + if( Report ) { ErrorLog( "Bad scenario: umbrella \"" + Name + "\" has \"spread\" in one suspension only; they're assembled separately" ); } + for( auto const index : List ) { alone( index ); } + return; + } + glm::dvec3 wires[ 2 ], ropes[ 2 ], poles[ 2 ], attach[ 2 ]; + bool held[ 2 ] { false, false }; // punkt liny podany (wprost albo z wysięgnika, który ją tu trzyma) catenary::cantilever_result probes[ 2 ]; + catenary::cantilever_umbrella sides[ 2 ]; auto probed { true }; for( int side = 0; side < 2; ++side ) { auto &suspension { Suspensions[ List[ side ] ] }; entry_points( suspension.data, wires[ side ], ropes[ side ], poles[ side ] ); - probed = probed && assemble( suspension, Catalog, wires[ side ], ( suspension.data.has_rope ? &ropes[ side ] : nullptr ), poles[ 0 ], nullptr, &probes[ side ] ) > 0; + held[ side ] = held_rope_at( List[ side ], ropes[ side ] ) || suspension.data.has_rope; + attach[ side ] = poles[ 0 ]; + if( spread ) { + // płaszczyzna wysięgnika przez punkt mocowania na konsoli; oś słupa przesunięta od niej wzdłuż osi X wysięgnika + auto const &point { suspension.data.spread }; + attach[ side ] = { point[ 0 ], poles[ 0 ].y, point[ 2 ] }; + auto const heading { std::atan2( attach[ side ].x - wires[ side ].x, attach[ side ].z - wires[ side ].z ) }; + glm::dvec3 const xaxis { -std::cos( heading ), 0.0, std::sin( heading ) }; + sides[ side ].spread = true; + sides[ side ].console_x = glm::dot( poles[ 0 ] - attach[ side ], xaxis ); + } + auto probe { sides[ side ] }; + probe.console_parts = false; + probe.pole = false; + probed = probed && assemble( suspension, Catalog, wires[ side ], ( held[ side ] ? &ropes[ side ] : nullptr ), attach[ side ], ( spread ? &probe : nullptr ), &probes[ side ] ) > 0; } if( false == probed ) { for( auto const index : List ) { alone( index ); } return; } - catenary::cantilever_umbrella sides[ 2 ]; - catenary::umbrella_mounts( Catalog, *pole, probes[ 0 ], probes[ 1 ], sides[ 0 ], sides[ 1 ] ); + if( spread ) { catenary::spread_mounts( *pole, probes[ 0 ], probes[ 1 ], sides[ 0 ], sides[ 1 ] ); } + else { catenary::umbrella_mounts( Catalog, *pole, probes[ 0 ], probes[ 1 ], sides[ 0 ], sides[ 1 ] ); } + sides[ 0 ].pole = false == first.shared_pole && false == Suspensions[ List.back() ].data.shared_pole; sides[ 1 ].pole = false; + sides[ 1 ].console_parts = false; for( int side = 0; side < 2; ++side ) { auto &suspension { Suspensions[ List[ side ] ] }; - auto const count { assemble( suspension, Catalog, wires[ side ], ( suspension.data.has_rope ? &ropes[ side ] : nullptr ), poles[ 0 ], &sides[ side ] ) }; + auto const count { assemble( suspension, Catalog, wires[ side ], ( held[ side ] ? &ropes[ side ] : nullptr ), attach[ side ], &sides[ side ] ) }; if( count > 0 ) { ++Assembled; Parts += count; } } } @@ -596,6 +665,19 @@ assemble_gantry( scene_gantry &Gantry ) { return count; } +std::size_t suspension_named( std::string const &File, std::string const &Name ); +// czy łańcuch kończy się na tym kotwieniu (lina z całością albo same przewody: anchor_*_contact); Start - na początku łańcucha +inline bool +anchors_chain( catenary::chain const &Chain, std::string const &Name, bool *Start = nullptr ) { + if( Name.empty() ) { return false; } + auto const start { Chain.anchor_start == Name || Chain.anchor_start_contact == Name }; + if( Start != nullptr ) { *Start = start; } + return start || Chain.anchor_end == Name || Chain.anchor_end_contact == Name; +} +void follow_anchor( scene_anchor const &Anchor ); +void add_tube( std::vector &Triangles, std::vector const &Points, double Radius ); +void rope_of( catenary::suspension const &Data, double *Rope ); + // urządzenia naprężające (traction_tensioners.txt); czytane przy pierwszym użyciu catenary::tensioner_catalog const * tensioners() { @@ -630,23 +712,201 @@ assemble_anchor( scene_anchor &Anchor ) { if( catalog == nullptr || devices == nullptr ) { return 0; } auto const *device { devices->device( data.device ) }; if( device == nullptr ) { ErrorLog( label + "uses device \"" + data.device + "\" which isn't in traction_tensioners.txt" ); return 0; } - auto const *pole { catalog->pole( data.pole_type ) }; - if( pole == nullptr ) { ErrorLog( label + "uses pole \"" + data.pole_type + "\" which isn't in the supports catalogue" ); return 0; } - catenary::tensioner_input input; - if( false == data.network.empty() ) { - if( auto const *type { catenary::types().find( data.network ) }; type != nullptr ) { - input.type = type->type.code; - input.contacts = type->type.contact_count; + // na istniejącym słupie: typ i punkt z podwieszenia, które go stawia + auto poletype { data.pole_type }; + double polepoint[ 3 ] { data.pole[ 0 ], data.pole[ 1 ], data.pole[ 2 ] }; + double cantilever[ 2 ] { 0.0, 0.0 }; // na istniejącym słupie: kierunek jego wysięgnika w planie + if( false == data.on.empty() ) { + auto const index { suspension_named( Anchor.file, data.on ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_pole ) { + ErrorLog( label + "is on the pole of suspension \"" + data.on + "\", which isn't in the file or has no pole" ); + return 0; } - else { ErrorLog( label + "uses network type \"" + data.network + "\" which has no file" ); } + poletype = Suspensions[ index ].data.pole_type; + std::copy( std::begin( Suspensions[ index ].data.pole ), std::end( Suspensions[ index ].data.pole ), polepoint ); + // płaszczyzna wysięgnika tego słupa (od słupa do drutu): oś słupa wzdłuż toru jest do niej prostopadła + if( Suspensions[ index ].data.has_at ) { + cantilever[ 0 ] = Suspensions[ index ].data.at[ 0 ] - polepoint[ 0 ]; + cantilever[ 1 ] = Suspensions[ index ].data.at[ 2 ] - polepoint[ 2 ]; + } + } + auto const *pole { catalog->pole( poletype ) }; + if( pole == nullptr ) { ErrorLog( label + "uses pole \"" + poletype + "\" which isn't in the supports catalogue" ); return 0; } + catenary::tensioner_input input; + // typ sieci: łańcucha, który się na tym kotwieniu kończy (zmiana typu w łańcuchu zmienia kotwienie), inaczej z wpisu + auto network { data.network }; + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != Anchor.file || data.name.empty() || chain.data.type.empty() ) { continue; } + if( anchors_chain( chain.data, data.name ) ) { network = chain.data.type; break; } + } + if( false == network.empty() ) { + if( auto const *type { catenary::types().find( network ) }; type != nullptr ) { + input.type = devices->code_for( type->type.symbol, type->type.code ); + input.contacts = type->type.contact_count; + input.messengers = type->type.messenger_count; + input.messenger_stack = type->type.messenger_stack; + // sztywności i ciężar sieci do skracania przy brakujących ciężarach (przekrój i moduł z traction_tables.txt) + auto const &net { type->type }; + input.messenger_stiffness = net.messenger_count * type->messenger_wire.area * type->messenger_wire.modulus * 0.1; + input.contact_stiffness = net.contact_count * type->contact_wire.area * type->contact_wire.modulus * 0.1; + input.load = ( net.messenger_count * net.messenger_mass + net.contact_count * net.contact_mass ) * input.gravity; + // urządzenie bez danych dla tego typu (np. 0461 dla dwóch lin nośnych, 0403 dla sieci 32): najpierw z tej samej karty + // (nazwa do "-": 0461-2x2, 0403-32), potem dowolne tego samego rodzaju (ciężarowe / stałe) + if( device->type( input.type ) == nullptr ) { + auto const card { data.device.substr( 0, data.device.find( '-' ) ) }; + catenary::tensioner_device const *same { nullptr }, *kind { nullptr }; + for( auto const &other : devices->devices() ) { + if( other.fixed != device->fixed || other.only != device->only || other.type( input.type ) == nullptr ) { continue; } + if( same == nullptr && other.name.substr( 0, other.name.find( '-' ) ) == card ) { same = &other; } + if( kind == nullptr ) { kind = &other; } + } + if( same != nullptr ) { device = same; } + else if( kind != nullptr ) { device = kind; } + // nikt nie ma danych dla tej sieci (brak wpisu "network ... as"): pierwsze dane urządzenia, żeby kotwienie stało + if( device->type( input.type ) == nullptr && false == device->types.empty() ) { + WriteLog( ( label + "network type \"" + network + "\" has no data in the anchor devices; device " + device->name + " uses the data of network type " + std::to_string( device->types.front().code ) ).c_str() ); + input.type = device->types.front().code; + } + } + } + else { ErrorLog( label + "uses network type \"" + network + "\" which has no file" ); } } input.temperature = Global.AirTemperature; input.distance = data.distance; input.elongation = data.elongation; input.missing = data.missing; input.ground = data.ground; + input.height = data.height; + input.rope_height = data.rope_height; + // uchwyty do słupa: tył po drugiej stronie słupa; zero urządzenia na czole przy wysokości rolek, słup zbieżny + input.pole = poletype; + { + auto const *devicetable { devices->table( device->table ) }; + auto const rollers { data.height > 0.0 ? data.height : devicetable != nullptr ? devicetable->height : 6.7 }; + // słup z szerokością wzdłuż toru (161x): urządzenie i uchwyty na ścianach wzdłuż drutów, śruby przez szerokość + input.depth = [ pole, rollers ]( double const Height ) { return pole->width > 0.0 ? pole->width : 0.5 * pole->depth( rollers ) + 0.5 * pole->depth( Height ); }; + } input.color = data.color; input.top_color = data.top_color; + // stare złożenie: łańcuch z tym kotwieniem ma druty zaśniedziałe (stan 1); średnia rozpiętość jego przęseł między podwieszeniami + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != Anchor.file || data.name.empty() ) { continue; } + if( anchors_chain( chain.data, data.name ) ) { + input.old_fittings = ( chain.data.state == 1 ); + // łańcuch z kotwieniem środkowym: odległość od niego liczona wzdłuż łańcucha (podwieszenia aż do słupa kotwienia) + if( false == chain.data.middle.empty() ) { + auto const &names { chain.data.suspensions }; + auto const middle { std::find( names.begin(), names.end(), chain.data.middle ) }; + if( middle != names.end() ) { + bool start { false }; + anchors_chain( chain.data, data.name, &start ); + auto const from { static_cast( middle - names.begin() ) }; + auto const to { start ? std::ptrdiff_t { 0 } : static_cast( names.size() ) - 1 }; + double along { 0.0 }; + double previous[ 2 ]; + bool has { false }, good { true }; + for( auto k { from }; good; k += ( start ? -1 : 1 ) ) { + auto const index { suspension_named( Anchor.file, names[ k ] ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_at ) { good = false; break; } + auto const *at { Suspensions[ index ].data.at }; + if( has ) { along += std::hypot( at[ 0 ] - previous[ 0 ], at[ 2 ] - previous[ 1 ] ); } + previous[ 0 ] = at[ 0 ]; previous[ 1 ] = at[ 2 ]; + has = true; + if( k == to ) { break; } + } + if( good && has ) { + along += std::hypot( polepoint[ 0 ] - previous[ 0 ], polepoint[ 2 ] - previous[ 1 ] ); + input.distance = along; + } + } + } + double total { 0.0 }; + int spans { 0 }; + double previous[ 3 ]; + bool has { false }; + for( auto const &name : chain.data.suspensions ) { + auto const index { suspension_named( Anchor.file, name ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_at ) { has = false; continue; } + auto const *at { Suspensions[ index ].data.at }; + if( has ) { total += std::hypot( at[ 0 ] - previous[ 0 ], at[ 2 ] - previous[ 2 ] ); ++spans; } + std::copy( at, at + 3, previous ); + has = true; + } + if( spans > 0 ) { input.span = total / spans; } + } + } + // izolatory: rolkowe ESN-2025 z wyboru, a bez wyboru przy drutach zaśniedziałych (jak uchwyty 5110) + if( data.insulators.empty() ) { input.old_messenger = input.old_contact = input.old_fittings; } + else { + input.old_messenger = ( data.insulators == "old" || data.insulators == "messenger" ); + input.old_contact = ( data.insulators == "old" || data.insulators == "contact" ); + } + // kierunek drutów w planie; zero urządzenia na czole słupa + glm::dvec3 const axis { polepoint[ 0 ], polepoint[ 1 ], polepoint[ 2 ] }; + auto dx { data.toward[ 0 ] - axis.x }, dz { data.toward[ 2 ] - axis.z }; + auto const along { std::sqrt( dx * dx + dz * dz ) }; + if( along < 1.0 ) { ErrorLog( label + "has its \"toward\" point less than 1 m from the pole" ); return 0; } + dx /= along; + dz /= along; + auto const table { devices->table( device->table ) }; + // czoło: kotwienie ciągnie wzdłuż toru, więc słup kratowy z szerokością wzdłuż toru ma czoło w połowie szerokości + auto const face { pole->width > 0.0 ? 0.5 * pole->width : 0.5 * pole->depth( data.height > 0.0 ? data.height : table != nullptr ? table->height : 6.7 ) }; + // oś słupa wzdłuż toru (w stronę kotwionego przęsła): własny słup kotwowy stoi w linii drutów; istniejący - prostopadle + // do swojego wysięgnika. Uchwyty, prowadnica i stos są przy słupie, urządzenie od przegubu ciągnie do drutów + auto ax { dx }, az { dz }; + if( auto const reach { std::hypot( cantilever[ 0 ], cantilever[ 1 ] ) }; reach > 0.1 ) { + ax = -cantilever[ 1 ] / reach; + az = cantilever[ 0 ] / reach; + if( ax * dx + az * dz < 0.0 ) { ax = -ax; az = -az; } + } + glm::dvec3 const origin { axis.x + ax * face, axis.y, axis.z + az * face }; + { + // urządzenie od czoła w stronę ostatniego podwieszenia + auto const ex { data.toward[ 0 ] - origin.x }, ez { data.toward[ 2 ] - origin.z }; + auto const reach { std::hypot( ex, ez ) }; + if( reach > 0.5 ) { dx = ex / reach; dz = ez / reach; } + } + auto const heading { std::atan2( dz, -dx ) }; + auto const toscene = [ & ]( double const *Point ) { + catenary::cantilever_placement placement; + std::copy( Point, Point + 3, placement.position ); + auto const part { catenary::cantilever_to_scene( placement, heading ) }; + return glm::dvec3 { origin.x + part.offset[ 0 ], origin.y + part.offset[ 1 ], origin.z + part.offset[ 2 ] }; + }; + // łańcuchy do drutów wycelowane w ostatnie podwieszenie przed kotwieniem (przewody podchodzą pod kątem) + { + // punkt sceny -> układ urządzenia: osie urządzenia w scenie z toscene, układ prostokątny + double const zero[ 3 ] { 0.0, 0.0, 0.0 }; + auto const base { toscene( zero ) }; + glm::dvec3 axes[ 3 ]; + for( int a = 0; a < 3; ++a ) { + double unit[ 3 ] { 0.0, 0.0, 0.0 }; + unit[ a ] = 1.0; + axes[ a ] = toscene( unit ) - base; + } + auto const todevice = [ & ]( double const *Point, double *Out ) { + glm::dvec3 const d { Point[ 0 ] - base.x, Point[ 1 ] - base.y, Point[ 2 ] - base.z }; + for( int a = 0; a < 3; ++a ) { Out[ a ] = glm::dot( d, axes[ a ] ); } + }; + // oś słupa w układzie urządzenia + glm::dvec3 const structure { ax, 0.0, az }; + input.structure_angle = std::atan2( glm::dot( structure, axes[ 1 ] ), glm::dot( structure, axes[ 0 ] ) ); + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != Anchor.file || data.name.empty() || chain.data.suspensions.empty() ) { continue; } + std::string neighbour; + bool start { false }; + if( false == anchors_chain( chain.data, data.name, &start ) ) { continue; } + neighbour = ( start ? chain.data.suspensions.front() : chain.data.suspensions.back() ); + auto const index { suspension_named( Anchor.file, neighbour ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_at ) { break; } + double rope[ 3 ]; + rope_of( Suspensions[ index ].data, rope ); + todevice( Suspensions[ index ].data.at, input.contact_target ); + todevice( rope, input.messenger_target ); + input.aimed = true; + break; + } + } auto &result { Anchor.result }; if( false == catenary::solve_tensioner( *devices, *device, input, result ) ) { for( auto const &warning : result.warnings ) { ErrorLog( label + "can't be assembled: " + warning ); } @@ -655,29 +915,45 @@ assemble_anchor( scene_anchor &Anchor ) { for( auto const &warning : result.warnings ) { if( Notes.insert( warning ).second ) { WriteLog( ( "Catenary: " + warning + " (first seen in anchor \"" + data.name + "\")" ).c_str() ); } } - if( auto const *cord { catalog->part( device->rope ) }; cord != nullptr ) { - catenary::tensioner_rope_parts( result, device->rope, cord->length, cord->girth ); + // lina: ucha jako części, reszta jedną bryłą proceduralną (bez łączeń na łukach rolek), przeliczoną na scenerię; + // rysuje ją z bliska przęsło dochodzące do kotwienia (rebuild_span) + { + std::vector tube; + catenary::tensioner_rope_solid( result, 0.5 * device->rope_diameter, 8, device->rope_eye, device->rope_eye_length, tube, device->rope_spacer, device->rope_spacer_length ); + // kotwienie rozdzielone: druga lina (urządzenie przewodów), z uchami + if( false == result.rope2.empty() ) { + catenary::tensioner_result second; + second.rope = result.rope2; + catenary::tensioner_rope_solid( second, 0.5 * device->rope_diameter, 8, device->rope_eye, device->rope_eye_length, tube, device->rope_spacer, device->rope_spacer_length ); + for( auto part : second.parts ) { part.role = "contact_device." + part.role; result.parts.push_back( part ); } + } + double const zero[ 3 ] { 0.0, 0.0, 0.0 }; + auto const base { toscene( zero ) }; + glm::dvec3 axes[ 3 ]; + for( int a = 0; a < 3; ++a ) { + double unit[ 3 ] { 0.0, 0.0, 0.0 }; + unit[ a ] = 1.0; + axes[ a ] = toscene( unit ) - base; + } + // przeliczenie na scenerię może odbijać (zamiana osi): wtedy odwrócić kolejność wierzchołków trójkątów + auto const &ax { axes[ 0 ] }, &ay { axes[ 1 ] }, &az { axes[ 2 ] }; + auto const mirrored { ax.x * ( ay.y * az.z - ay.z * az.y ) + ax.y * ( ay.z * az.x - ay.x * az.z ) + ax.z * ( ay.x * az.y - ay.y * az.x ) < 0.0 }; + Anchor.rope.clear(); + Anchor.rope.reserve( tube.size() ); + for( std::size_t k = 0; k < tube.size(); ++k ) { + auto const &item { tube[ mirrored ? ( k - k % 3 ) + ( 2 - k % 3 ) : k ] }; + auto const at { toscene( item.position ) }; + catenary::solid_vertex vertex; + vertex.position = catenary::vec3 { at.x, at.y, at.z }; + vertex.normal = catenary::vec3 { + ax.x * item.normal[ 0 ] + ay.x * item.normal[ 1 ] + az.x * item.normal[ 2 ], + ax.y * item.normal[ 0 ] + ay.y * item.normal[ 1 ] + az.y * item.normal[ 2 ], + ax.z * item.normal[ 0 ] + ay.z * item.normal[ 1 ] + az.z * item.normal[ 2 ] }; + Anchor.rope.push_back( vertex ); + } } - else { ErrorLog( label + "rope part \"" + device->rope + "\" isn't in the cantilever catalogue, the rope isn't drawn" ); } Anchor.temperature = Global.AirTemperature; - // kierunek drutów w planie; zero urządzenia na czole słupa - glm::dvec3 const axis { data.pole[ 0 ], data.pole[ 1 ], data.pole[ 2 ] }; - auto dx { data.toward[ 0 ] - axis.x }, dz { data.toward[ 2 ] - axis.z }; - auto const along { std::sqrt( dx * dx + dz * dz ) }; - if( along < 1.0 ) { ErrorLog( label + "has its \"toward\" point less than 1 m from the pole" ); return 0; } - dx /= along; - dz /= along; - auto const table { devices->table( device->table ) }; - auto const face { 0.5 * pole->depth( table != nullptr ? table->height : 6.7 ) }; - glm::dvec3 const origin { axis.x + dx * face, axis.y, axis.z + dz * face }; - auto const heading { std::atan2( dz, -dx ) }; - auto const toscene = [ & ]( double const *Point ) { - catenary::cantilever_placement placement; - std::copy( Point, Point + 3, placement.position ); - auto const part { catenary::cantilever_to_scene( placement, heading ) }; - return glm::dvec3 { origin.x + part.offset[ 0 ], origin.y + part.offset[ 1 ], origin.z + part.offset[ 2 ] }; - }; { auto const messenger { toscene( result.messenger_end ) }; double contact[ 3 ] { 0.0, 0.0, 0.0 }; @@ -692,8 +968,32 @@ assemble_anchor( scene_anchor &Anchor ) { Anchor.messenger[ 0 ] = messenger.x; Anchor.messenger[ 1 ] = messenger.y; Anchor.messenger[ 2 ] = messenger.z; + // druga lina nośna: koniec na jej uchwycie krańcowym, przęsło do kotwienia dociąga tam dolną linę (dopisek "rope2") + auto const second { result.messenger2_end[ 0 ] != 0.0 || result.messenger2_end[ 1 ] != 0.0 || result.messenger2_end[ 2 ] != 0.0 }; + Anchor.has_messenger2 = second && input.messengers >= 2; + if( Anchor.has_messenger2 ) { + auto const at { toscene( result.messenger2_end ) }; + Anchor.messenger2[ 0 ] = at.x; + Anchor.messenger2[ 1 ] = at.y; + Anchor.messenger2[ 2 ] = at.z; + } for( int k = 0; k < 3; ++k ) { Anchor.contact[ k ] = contact[ k ] / ends; } } + { + // obrys dla edytora: czoło słupa i rolka ruchoma na wysokości rolek, kolumna ciężarów od wolnego końca liny do spodu + auto const column { result.rope.empty() ? std::vector { 0.0, 0.0, result.height } : result.rope.back() }; + double const points[ 4 ][ 3 ] { + { 0.0, 0.0, result.height }, + { result.b, 0.0, result.height }, + { column[ 0 ], column[ 1 ], column[ 2 ] }, + { column[ 0 ], column[ 1 ], result.s } }; + for( int k = 0; k < 4; ++k ) { + auto const at { toscene( points[ k ] ) }; + Anchor.shape[ k ][ 0 ] = at.x; + Anchor.shape[ k ][ 1 ] = at.y; + Anchor.shape[ k ][ 2 ] = at.z; + } + } Anchor.solved = true; auto const skin { data.skin.empty() ? std::string( "none" ) : data.skin }; @@ -714,8 +1014,9 @@ assemble_anchor( scene_anchor &Anchor ) { ++count; } }; - // słup: Y wysięgnika od toru do słupa (słup po tej stronie drutów, po której stoi względem punktu "toward") - { + // słup: Y wysięgnika od toru do słupa (słup po tej stronie drutów, po której stoi względem punktu "toward"); + // istniejący słup stawia jego podwieszenie + if( data.on.empty() ) { double sidex { dz }, sidez { -dx }; if( ( axis.x - data.toward[ 0 ] ) * sidex + ( axis.z - data.toward[ 2 ] ) * sidez < 0.0 ) { sidex = -sidex; sidez = -sidez; } auto const poleheading { std::atan2( sidex, sidez ) }; @@ -784,6 +1085,8 @@ chain_points( scene_chain const &Chain ) { return Chain.data.suspensions.size() + ( Chain.data.anchor_start.empty() ? 0 : 1 ) + ( Chain.data.anchor_end.empty() ? 0 : 1 ); } +bool held_rope( scene_chain const &Chain, std::size_t Position, double *Rope ); + bool chain_point( scene_chain const &Chain, std::size_t Point, double *Wire, double *Rope, bool &Anchored ) { auto const &data { Chain.data }; @@ -799,14 +1102,107 @@ chain_point( scene_chain const &Chain, std::size_t Point, double *Wire, double * } if( Anchored ) { if( anchor == nullptr || false == anchor->solved ) { return false; } - std::copy( std::begin( anchor->contact ), std::end( anchor->contact ), Wire ); + // przewody zakotwione osobno: ich koniec z drugiego kotwienia + auto const &contactname { Point == 0 && false == data.anchor_start.empty() ? data.anchor_start_contact : data.anchor_end_contact }; + auto const *contact { contactname.empty() ? anchor : anchor_named( Chain.file, contactname ) }; + if( contact == nullptr || false == contact->solved ) { return false; } + std::copy( std::begin( contact->contact ), std::end( contact->contact ), Wire ); std::copy( std::begin( anchor->messenger ), std::end( anchor->messenger ), Rope ); return true; } auto const index { suspension_named( Chain.file, data.suspensions[ Point ] ) }; if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_at ) { return false; } std::copy( std::begin( Suspensions[ index ].data.at ), std::end( Suspensions[ index ].data.at ), Wire ); - rope_of( Suspensions[ index ].data, Rope ); + // lina nośna z innego wysięgnika (rozstawione, sama lina): gdy go nie ma, z tego samego + if( false == held_rope( Chain, Point, Rope ) ) { rope_of( Suspensions[ index ].data, Rope ); } + return true; +} + +// lina nośna łańcucha w miejscu Position listy, gdy trzyma ją inny wysięgnik (messenger_from): punkt liny trzymającego, +// przesunięty wzdłuż łańcucha do miejsca tego podwieszenia, na wysokości liny trzymającego (rozstawione wysięgi pomocnicze +// 0167 i wysięgnik samej liny w osi słupa między nimi: lina idzie prosto przez siodło, linki obu wysięgów trafiają w nią). +// false, gdy linę trzyma to samo podwieszenie +bool +held_rope( scene_chain const &Chain, std::size_t const Position, double *Rope ) { + auto const &names { Chain.data.suspensions }; + if( Position >= names.size() ) { return false; } + auto const &holder { catenary::messenger_holder( Chain.data, names[ Position ] ) }; + if( holder == names[ Position ] ) { return false; } + auto const own { suspension_named( Chain.file, names[ Position ] ) }, other { suspension_named( Chain.file, holder ) }; + if( own >= Suspensions.size() || other >= Suspensions.size() || false == Suspensions[ other ].data.has_at ) { return false; } + auto const &listed { Suspensions[ own ].data }, &holding { Suspensions[ other ].data }; + rope_of( holding, Rope ); + // przesunięcie tylko, gdy ten sam wysięgnik trzyma linę także w sąsiednim punkcie listy (para wysięgów pomocniczych z + // wysięgnikiem samej liny między nimi); sam jeden (rozstawione na konsoli) - lina łamie się w punkcie trzymającego + auto const sameholder = [ & ]( std::size_t const K ) { return K < names.size() && K != Position && catenary::messenger_holder( Chain.data, names[ K ] ) == holder; }; + if( false == sameholder( Position + 1 ) && ( Position == 0 || false == sameholder( Position - 1 ) ) ) { return true; } + // kierunek łańcucha w tym miejscu: od poprzedniego do następnego podwieszenia listy + auto const at = [ & ]( std::size_t const K ) { + auto const index { suspension_named( Chain.file, names[ K ] ) }; + auto const &data { index < Suspensions.size() && Suspensions[ index ].data.has_at ? Suspensions[ index ].data : listed }; + return glm::dvec2 { data.at[ 0 ], data.at[ 2 ] }; + }; + auto const way { at( Position + 1 < names.size() ? Position + 1 : Position ) - at( Position > 0 ? Position - 1 : Position ) }; + if( glm::length( way ) < 0.1 ) { return true; } + auto const along { glm::normalize( way ) }; + auto const shift { along * glm::dot( glm::dvec2 { listed.at[ 0 ] - holding.at[ 0 ], listed.at[ 2 ] - holding.at[ 2 ] }, along ) }; + Rope[ 0 ] += shift.x; + Rope[ 2 ] += shift.y; + return true; +} + +// to samo dla podwieszenia Index (pierwszy łańcuch jego pliku, w którym linę w jego miejscu trzyma inny wysięgnik) +bool +held_rope_at( std::size_t const Index, glm::dvec3 &Rope ) { + if( Index >= Suspensions.size() || Suspensions[ Index ].data.name.empty() ) { return false; } + auto const &name { Suspensions[ Index ].data.name }; + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != Suspensions[ Index ].file ) { continue; } + auto const &names { chain.data.suspensions }; + auto const found { std::find( names.begin(), names.end(), name ) }; + if( found == names.end() ) { continue; } + double point[ 3 ]; + if( held_rope( chain, static_cast( found - names.begin() ), point ) ) { + Rope = glm::dvec3 { point[ 0 ], point[ 1 ], point[ 2 ] }; + return true; + } + } + return false; +} + +// naciąg przęsła k łańcucha od kotwienia z niepełnym stosem ciężarów (daN): przęsła od tego kotwienia do kotwienia +// środkowego albo stałego (distance wzdłuż łańcucha); false, gdy przęsło ma naciąg typu sieci. Bliższe kotwienie wygrywa, +// ciężary na gruncie zostawiają naciąg typu (sieć bez naciągu urządzenia nie da się policzyć) +bool +span_tension( scene_chain const &Chain, std::size_t const Span, double &Messenger, double &Contact ) { + auto const *type { Chain.data.type.empty() ? nullptr : catenary::types().find( Chain.data.type ) }; + if( type == nullptr ) { return false; } + auto const points { chain_points( Chain ) }; + if( Span + 1 >= points ) { return false; } + // odległości wzdłuż łańcucha od początku do środka przęsła i od środka przęsła do końca + std::vector along( points, 0.0 ); + double previous[ 3 ], wire[ 3 ], rope[ 3 ]; + bool anchored; + for( std::size_t point = 0; point < points; ++point ) { + if( false == chain_point( Chain, point, wire, rope, anchored ) ) { return false; } + if( point > 0 ) { along[ point ] = along[ point - 1 ] + std::hypot( wire[ 0 ] - previous[ 0 ], wire[ 2 ] - previous[ 2 ] ); } + std::copy( wire, wire + 3, previous ); + } + auto const middle { 0.5 * ( along[ Span ] + along[ Span + 1 ] ) }; + double best { 1e30 }, ratio { 1.0 }; + for( auto const end : { 0, 1 } ) { + auto const &name { end == 0 ? Chain.data.anchor_start : Chain.data.anchor_end }; + if( name.empty() ) { continue; } + auto const *anchor { anchor_named( Chain.file, name ) }; + if( anchor == nullptr || false == anchor->solved || anchor->result.grounded || anchor->result.nominal_tension <= 0.0 ) { continue; } + auto const reach { end == 0 ? middle : along.back() - middle }; + if( reach > anchor->data.distance || reach >= best ) { continue; } + best = reach; + ratio = anchor->result.tension / anchor->result.nominal_tension; + } + if( std::abs( ratio - 1.0 ) < 1e-3 || ratio <= 0.0 ) { return false; } + Messenger = 0.1 * type->type.messenger_tension * ratio; + Contact = 0.1 * type->type.contact_tension * ratio; return true; } @@ -823,8 +1219,20 @@ span_body( scene_chain const &Chain, std::size_t const Span ) { body += point_text( wirea ) + "\n" + point_text( wireb ) + "\n" + point_text( ropea ) + "\n" + point_text( ropeb ) + "\n"; auto const spacing { anchoreda || anchoredb ? 0.0 : data.hangers[ 1 ] }; body += text_number( data.hangers[ 0 ], 3 ) + " " + text_number( spacing, 3 ) + " " + std::to_string( static_cast( data.hangers[ 2 ] ) ) + " " + text_number( data.hangers[ 3 ], 3 ) + " vis"; - if( false == data.type.empty() ) { body += " type " + data.type; } + if( false == data.type.empty() ) { + body += " type " + data.type; + // brakujące ciężary w kotwieniu: mniejszy naciąg, większy zwis (starsze wersje symulatora pomijają ten dopisek) + double messenger, contact; + if( span_tension( Chain, Span, messenger, contact ) ) { body += " tension " + text_number( messenger, 0 ) + " " + text_number( contact, 0 ); } + } if( false == data.parallel.empty() ) { body += " parallel " + data.parallel; } + // kotwienie z dwiema liniami: dolna lina przęsła kończy się na swoim uchwycie krańcowym + for( auto const end : { 0, 1 } ) { + if( ( end == 0 ? anchoreda : anchoredb ) == false ) { continue; } + auto const *anchor { anchor_named( Chain.file, end == 0 ? ( Span == 0 ? data.anchor_start : std::string() ) : data.anchor_end ) }; + if( anchor == nullptr || false == anchor->has_messenger2 ) { continue; } + body += std::string( " rope2 " ) + ( end == 0 ? "start " : "end " ) + point_text( anchor->messenger2 ); + } return body + " endtraction"; } @@ -833,6 +1241,355 @@ span_name( scene_chain const &Chain, std::size_t const Span ) { return ( Chain.data.name.empty() ? std::string( "none" ) : Chain.data.name + "_" + std::to_string( Span + 1 ) ); } +// prostopadłościan (bryła zastępcza osprzętu bez modelu): środek, oś wzdłuż Along, długość i przekrój +void +add_box( std::vector &Triangles, glm::dvec3 const &Center, glm::dvec3 Along, double const Length, double const Size ) { + Along = glm::length( Along ) > 1e-9 ? glm::normalize( Along ) : glm::dvec3 { 1.0, 0.0, 0.0 }; + auto side { glm::cross( Along, glm::dvec3 { 0.0, 1.0, 0.0 } ) }; + if( glm::length( side ) < 1e-6 ) { side = glm::dvec3 { 1.0, 0.0, 0.0 }; } + side = glm::normalize( side ); + auto const up { glm::normalize( glm::cross( side, Along ) ) }; + glm::dvec3 const axes[ 3 ] { Along * ( 0.5 * Length ), side * ( 0.5 * Size ), up * ( 0.5 * Size ) }; + // sześć ścian: dla każdej osi strona + i -; wierzchołki przeciwnie do ruchu wskazówek patrząc z zewnątrz + for( int a = 0; a < 3; ++a ) { + for( auto const sign : { 1.0, -1.0 } ) { + auto const normal { axes[ a ] * sign }; + auto const &u { axes[ ( a + 1 ) % 3 ] }, &v { axes[ ( a + 2 ) % 3 ] }; + auto const c { Center + normal }; + glm::dvec3 const corner[ 4 ] { c - u - v, c + u - v, c + u + v, c - u + v }; + int const order[ 2 ][ 6 ] { { 0, 1, 2, 0, 2, 3 }, { 0, 2, 1, 0, 3, 2 } }; + auto const n { glm::normalize( normal ) }; + for( auto const k : order[ sign > 0.0 ? 0 : 1 ] ) { + catenary::solid_vertex vertex; + vertex.position = catenary::vec3 { corner[ k ].x, corner[ k ].y, corner[ k ].z }; + vertex.normal = catenary::vec3 { n.x, n.y, n.z }; + Triangles.push_back( vertex ); + } + } + } +} + +// rura po łamanej (lina kotwienia środkowego, ramiona V), współrzędne scenerii +void +add_tube( std::vector &Triangles, std::vector const &Points, double const Radius ) { + catenary::tensioner_result path; + for( auto const &point : Points ) { path.rope.push_back( { point.x, point.y, point.z } ); } + std::vector tube; + catenary::tensioner_rope_solid( path, Radius, 6, std::string(), 0.0, tube ); + for( auto const &item : tube ) { + catenary::solid_vertex vertex; + vertex.position = catenary::vec3 { item.position[ 0 ], item.position[ 1 ], item.position[ 2 ] }; + vertex.normal = catenary::vec3 { item.normal[ 0 ], item.normal[ 1 ], item.normal[ 2 ] }; + Triangles.push_back( vertex ); + } +} + +// czy przęsło k łańcucha ma linkę Y przy końcu End (0: punkt k, 1: punkt k+1) - tak samo jak y_at w catenary_span.cpp: +// oba przęsła przy podwieszeniu mają wieszaki i tę samą długość Y z tablicy typu +bool +span_y_at( scene_chain const &Chain, catenary::network_definition const &Type, std::size_t const Span, int const End ) { + if( Type.type.y_tension <= 0.0 ) { return false; } + auto const ylength = [ & ]( std::size_t const K, double &Out ) { + if( K + 1 >= chain_points( Chain ) ) { return false; } + double wirea[ 3 ], ropea[ 3 ], wireb[ 3 ], ropeb[ 3 ]; + bool anchoreda, anchoredb; + if( false == chain_point( Chain, K, wirea, ropea, anchoreda ) || false == chain_point( Chain, K + 1, wireb, ropeb, anchoredb ) ) { return false; } + if( anchoreda || anchoredb || Chain.data.hangers[ 1 ] <= 0.0 ) { return false; } // przęsło bez wieszaków + auto const length { std::hypot( wireb[ 0 ] - wirea[ 0 ], wireb[ 2 ] - wirea[ 2 ] ) }; + auto const y { Type.type.y_length( length ) }; + Out = ( y < length ? y : 0.0 ); + return Out > 0.0; + }; + double mine, other; + if( false == ylength( Span, mine ) ) { return false; } + if( End == 0 && Span == 0 ) { return false; } + if( false == ylength( End == 0 ? Span - 1 : Span + 1, other ) ) { return false; } + return std::abs( mine - other ) <= 0.01; +} + +// kotwienie środkowe łańcucha (karty 63-0406 / 0408): przy podwieszeniu "middle" lina kotwienia zaciśnięta na linie nośnej 1,00 m +// z każdej strony, dalej do słupów sąsiednich podwieszeń (tam 4430 w uchwytach i łańcuch z izolatorem - urządzenie +// i -2 z regulacją), w połowie obu przęseł V 1,50 + 1,50 m do przewodu jezdnego. Lina, V i zaciski jako bryły (bez modeli) +void +assemble_middle( scene_chain &Chain ) { + for( auto *part : Chain.middle_parts ) { catenary_part_delete( part ); } + Chain.middle_parts.clear(); + Chain.middle_solid[ 0 ].clear(); + Chain.middle_solid[ 1 ].clear(); + Chain.middle_span = static_cast( -1 ); + auto const &data { Chain.data }; + if( Chain.removed || data.middle.empty() ) { return; } + auto const label { "Bad scenario: chain \"" + ( data.name.empty() ? std::string( "none" ) : data.name ) + "\" " }; + auto const found { std::find( data.suspensions.begin(), data.suspensions.end(), data.middle ) }; + if( found == data.suspensions.end() ) { ErrorLog( label + "has its middle anchor at \"" + data.middle + "\", which isn't in the chain" ); return; } + auto const position { static_cast( found - data.suspensions.begin() ) }; + if( position == 0 || position + 1 >= data.suspensions.size() ) { ErrorLog( label + "has its middle anchor at an end of the chain; it needs a pole on each side" ); return; } + auto const offset { data.anchor_start.empty() ? std::size_t { 0 } : std::size_t { 1 } }; + double wires[ 3 ][ 3 ], ropes[ 3 ][ 3 ]; + bool anchored; + for( int k = 0; k < 3; ++k ) { + if( false == chain_point( Chain, position + offset + k - 1, wires[ k ], ropes[ k ], anchored ) ) { return; } + } + auto const *catalog { cantilevers() }; + auto const *devices { tensioners() }; + auto const *type { data.type.empty() ? nullptr : catenary::types().find( data.type ) }; + if( catalog == nullptr || devices == nullptr ) { return; } + Chain.middle_span = position + offset - 1; + auto const vec = []( double const *P ) { return glm::dvec3 { P[ 0 ], P[ 1 ], P[ 2 ] }; }; + auto const middlerope { vec( ropes[ 1 ] ) }; + double const ropeheight { 0.0045 }; // promień liny 9831-5 - ZAŁOŻENIE (przekroju brak) + // 63-0416 do dźwigara bramki: podwieszenie środkowe na zwisie, liny od dźwigara (4590 na jego spodzie, nad liną nośną) do + // zacisków na linie nośnej ok. 3,00 m z obu stron; bez urządzeń na słupach sąsiednich + auto tobeam { data.middle_device == "0416" }; + glm::dvec3 beam { middlerope }; + if( tobeam ) { + auto const index { suspension_named( Chain.file, data.middle ) }; + auto const *own { index < Suspensions.size() && Suspensions[ index ].data.has_pole ? catalog->pole( Suspensions[ index ].data.pole_type ) : nullptr }; + if( own == nullptr || own->drop.empty() ) { + ErrorLog( label + "has a middle anchor to a gantry beam (0416) at \"" + data.middle + "\", which doesn't hang on a drop of a gantry; anchored to the poles next to it instead (0406)" ); + tobeam = false; + } + else { beam.y = Suspensions[ index ].data.pole[ 1 ]; } // spód dźwigara (zero zwisu) + } + // wysięgnik teowy na słupie środkowym: lina kotwienia idzie przez obejmę na wysięgniku (4160, stare karty 33-0418, kolprojekt1 + // s. 156 poz. 1), bez zacisków 1,00 m od słupa (Jan 2026-10-23: zaciski rozstawione o 1 m tylko przy rurowych). ZAŁOŻENIE: + // obejmą jest 4160-1 z karty, lina przechodzi przez nią na wysokości liny nośnej + auto tee { false }; + glm::dvec3 collar { middlerope }; // przez nią przechodzi lina kotwienia przy teowniku + if( false == tobeam ) { + auto const index { suspension_named( Chain.file, data.middle ) }; + if( index < Suspensions.size() ) { + auto const &own { Suspensions[ index ] }; + auto const *scheme { catalog->scheme( own.data.card, own.data.execution ) }; + tee = scheme != nullptr && scheme->assembly.rfind( "tee_", 0 ) == 0; + // obejma 4160 na teowniku obok siodła liny nośnej, w stronę słupa (Jan 2026-10-26: lina kotwienia ma swoje mocowanie + // obok, nie w siodle liny); odstęp 0,15 m - ZAŁOŻENIE na oko + if( tee && own.has_saddle ) { + double const beside { 0.15 }; + auto placement { own.saddle }; + placement.position[ 1 ] += beside; + auto const scened { catenary::cantilever_to_scene( placement, own.saddle_heading ) }; + auto const entry { + "node " + text_number( data.range_max > 0.0 ? data.range_max : 1000.0, 1 ) + " " + text_number( data.range_min, 1 ) + " none model 0 0 0 0 " + own.saddle_model + ".t3d none" + + " angles " + text_number( scened.angles[ 0 ] ) + " " + text_number( scened.angles[ 1 ] ) + " " + text_number( scened.angles[ 2 ] ) + " endmodel" }; + glm::dvec3 const at { own.saddle_origin.x + scened.offset[ 0 ], own.saddle_origin.y + scened.offset[ 1 ], own.saddle_origin.z + scened.offset[ 2 ] }; + if( auto *model3d { simulation::State.create_model( entry, "none", at ) }; model3d != nullptr ) { + catenary_part_detach( model3d ); + Chain.middle_parts.push_back( model3d ); + } + // przesunięcie o tyle samo w stronę konstrukcji (oś Y wysięgnika w scenerii) + double const unit[ 3 ] { 0.0, 1.0, 0.0 }; + catenary::cantilever_placement axis; + std::copy( unit, unit + 3, axis.position ); + auto const toward { catenary::cantilever_to_scene( axis, own.saddle_heading ) }; + collar = middlerope + glm::dvec3 { toward.offset[ 0 ], toward.offset[ 1 ], toward.offset[ 2 ] } * beside; + } + } + } + for( int side = 0; side < 2; ++side ) { + auto const neighbour { side == 0 ? 0 : 2 }; + auto &solid { Chain.middle_solid[ side ] }; + // zacisk na linie nośnej 1,00 m od środkowego podwieszenia w stronę sąsiedniego (do dźwigara: ok. 3,00 m; teownik: w obejmie) + auto const towards { vec( ropes[ neighbour ] ) - middlerope }; + auto const clampdistance { tobeam ? 3.0 : tee ? 0.0 : 1.0 }; + auto const clampchord { middlerope + towards * ( clampdistance / std::max( std::max( clampdistance, 1e-3 ), glm::length( glm::dvec2 { towards.x, towards.z } ) ) ) }; + // połowa przęsła: przewód jezdny (prosto między końcami), lina nośna nad nim na wysokości najkrótszego wieszaka + auto const wiremiddle { ( vec( wires[ 1 ] ) + vec( wires[ neighbour ] ) ) * 0.5 }; + auto const alongspan { glm::normalize( glm::dvec3 { wires[ neighbour ][ 0 ] - wires[ 1 ][ 0 ], 0.0, wires[ neighbour ][ 2 ] - wires[ 1 ][ 2 ] } ) }; + // lina nośna w przęśle tak, jak ją rysuje przęsło: solver sieci z odstępami wieszaków z tablicy typu (t - ułamek + // przęsła od podwieszenia środkowego); bez typu - prosto między podwieszeniami + auto const spanlength { glm::length( glm::dvec2 { wires[ neighbour ][ 0 ] - wires[ 1 ][ 0 ], wires[ neighbour ][ 2 ] - wires[ 1 ][ 2 ] } ) }; + catenary::span_solution solution; + auto solved { false }; + if( type != nullptr ) { + // przęsło liczone dokładnie jak rysowane: w jego kierunku (od punktu k do k+1), z tym samym naciągiem (dopisek + // tension) i z linkami Y tylko tam, gdzie przęsło je dostaje (sąsiednie przęsło z wieszakami i tą samą długością Y) + auto const span { position + offset - 1 + side }; + auto definition { *type }; + double messengertension, contacttension; + if( span_tension( Chain, span, messengertension, contacttension ) ) { + if( messengertension > 0.0 ) { definition.type.messenger_tension = 10.0 * messengertension; } + if( contacttension > 0.0 && definition.type.contact_tension > 0.0 ) { definition.type.contact_tension = 10.0 * contacttension; } + } + auto const first { side == 0 ? neighbour : 1 }, second { side == 0 ? 1 : neighbour }; + catenary::span_points points; + points.contact_start = catenary::vec3 { wires[ first ][ 0 ], wires[ first ][ 1 ], wires[ first ][ 2 ] }; + points.contact_end = catenary::vec3 { wires[ second ][ 0 ], wires[ second ][ 1 ], wires[ second ][ 2 ] }; + points.messenger_start = catenary::vec3 { ropes[ first ][ 0 ], ropes[ first ][ 1 ], ropes[ first ][ 2 ] }; + points.messenger_end = catenary::vec3 { ropes[ second ][ 0 ], ropes[ second ][ 1 ], ropes[ second ][ 2 ] }; + std::string error; + auto const gaps { catenary::types().hanger_gaps( type->hanger_table, spanlength ) }; + solved = catenary::solve_span_points( definition, gaps, points, span_y_at( Chain, *type, span, 0 ), span_y_at( Chain, *type, span, 1 ), solution, error ) + && solution.result.messenger.size() >= 2; + } + // punkt liny nośnej jak w przęśle: w planie na cięciwie liny (nie nad osią przewodu jezdnego, który idzie zygzakiem), + // w pionie nad prostą przewodu jezdnego o wysokość z solvera + auto const messengerat = [ & ]( double const T ) { + auto const chord { vec( ropes[ 1 ] ) + ( vec( ropes[ neighbour ] ) - vec( ropes[ 1 ] ) ) * T }; + if( false == solved ) { return chord; } + auto const wirey { wires[ 1 ][ 1 ] + ( wires[ neighbour ][ 1 ] - wires[ 1 ][ 1 ] ) * T }; + auto const &line { solution.result.messenger }; + auto const x { ( side == 0 ? 1.0 - T : T ) * solution.length }; // przęsło przed środkowym liczone od sąsiada + auto above { line.back().y }; + for( std::size_t k = 1; k < line.size(); ++k ) { + if( line[ k ].x >= x ) { + auto const f { line[ k ].x > line[ k - 1 ].x ? ( x - line[ k - 1 ].x ) / ( line[ k ].x - line[ k - 1 ].x ) : 0.0 }; + above = line[ k - 1 ].y + ( line[ k ].y - line[ k - 1 ].y ) * f; + break; + } + } + return glm::dvec3 { chord.x, wirey + above, chord.z }; + }; + // zacisk na linie nośnej tam, gdzie ją rysuje przęsło (lina przy podwieszeniu opada, prosta między podwieszeniami + // mijała ją o kilka cm) + auto const clamp { tee ? collar : solved && spanlength > clampdistance ? messengerat( clampdistance / spanlength ) : clampchord }; + // koniec liny na słupie sąsiedniego podwieszenia: urządzenie kotwienia stałego tylko z liną + auto const index { suspension_named( Chain.file, data.suspensions[ position + ( side == 0 ? -1 : 1 ) ] ) }; + glm::dvec3 ropeend { tobeam ? beam : vec( ropes[ neighbour ] ) }; + if( false == tobeam && index < Suspensions.size() && Suspensions[ index ].data.has_pole ) { + auto const &suspension { Suspensions[ index ].data }; + auto const *pole { catalog->pole( suspension.pole_type ) }; + auto const name { data.middle_device + ( side == 0 ? "" : "-2" ) }; + auto const *device { devices->device( name ) }; + if( device == nullptr ) { device = devices->device( data.middle_device ); } + if( pole != nullptr && device != nullptr ) { + glm::dvec3 const axis { suspension.pole[ 0 ], suspension.pole[ 1 ], suspension.pole[ 2 ] }; + // oś słupa wzdłuż toru, prostopadle do jego wysięgnika, w stronę środkowego słupa + glm::dvec2 cantilever { suspension.has_at ? glm::dvec2 { suspension.at[ 0 ] - axis.x, suspension.at[ 2 ] - axis.z } : glm::dvec2 { 0.0, 0.0 } }; + glm::dvec2 const tomiddle { clamp.x - axis.x, clamp.z - axis.z }; + glm::dvec2 structure { glm::length( cantilever ) > 0.1 ? glm::dvec2 { -cantilever.y, cantilever.x } / glm::length( cantilever ) : glm::normalize( tomiddle ) }; + if( glm::dot( structure, tomiddle ) < 0.0 ) { structure = -structure; } + auto const face { pole->width > 0.0 ? 0.5 * pole->width : 0.5 * pole->depth( ropes[ neighbour ][ 1 ] - axis.y ) }; + glm::dvec3 const origin { axis.x + structure.x * face, axis.y, axis.z + structure.y * face }; + auto const way { glm::normalize( glm::dvec2 { clamp.x - origin.x, clamp.z - origin.z } ) }; + auto const heading { std::atan2( way.y, -way.x ) }; + auto const toscene = [ & ]( double const *Point ) { + catenary::cantilever_placement placement; + std::copy( Point, Point + 3, placement.position ); + auto const part { catenary::cantilever_to_scene( placement, heading ) }; + return glm::dvec3 { origin.x + part.offset[ 0 ], origin.y + part.offset[ 1 ], origin.z + part.offset[ 2 ] }; + }; + double const zero[ 3 ] { 0.0, 0.0, 0.0 }; + auto const base { toscene( zero ) }; + glm::dvec3 axes[ 3 ]; + for( int a = 0; a < 3; ++a ) { + double unit[ 3 ] { 0.0, 0.0, 0.0 }; + unit[ a ] = 1.0; + axes[ a ] = toscene( unit ) - base; + } + auto const todevice = [ & ]( glm::dvec3 const &Point, double *Out ) { + auto const d { Point - base }; + for( int a = 0; a < 3; ++a ) { Out[ a ] = glm::dot( d, axes[ a ] ); } + }; + catenary::tensioner_input input; + if( type != nullptr ) { + input.type = devices->code_for( type->type.symbol, type->type.code ); + input.contacts = type->type.contact_count; + input.messengers = type->type.messenger_count; + } + if( device->type( input.type ) == nullptr && false == device->types.empty() ) { input.type = device->types.front().code; } + input.pole = suspension.pole_type; + input.depth = [ pole ]( double const Height ) { return pole->width > 0.0 ? pole->width : pole->depth( Height ); }; + input.structure_angle = std::atan2( glm::dot( glm::dvec3 { structure.x, 0.0, structure.y }, axes[ 1 ] ), glm::dot( glm::dvec3 { structure.x, 0.0, structure.y }, axes[ 0 ] ) ); + input.aimed = true; + todevice( clamp, input.messenger_target ); + todevice( vec( wires[ neighbour ] ), input.contact_target ); + input.rope_height = data.middle_heights[ side ] > 0.0 ? data.middle_heights[ side ] : ropes[ neighbour ][ 1 ] - axis.y; + input.height = wires[ neighbour ][ 1 ] - axis.y; + catenary::tensioner_result result; + if( catenary::solve_tensioner( *devices, *device, input, result ) ) { + ropeend = toscene( result.messenger_end ); + auto const skin { std::string( "none" ) }; + for( auto const &item : result.parts ) { + catenary::cantilever_placement placement; + std::copy( std::begin( item.position ), std::end( item.position ), placement.position ); + std::copy( std::begin( item.rotation ), std::end( item.rotation ), placement.rotation ); + std::copy( std::begin( item.scale ), std::end( item.scale ), placement.scale ); + auto const *part { catalog->part( item.part ) }; + auto const model { catalog->models() + ( part != nullptr && false == part->model.empty() ? part->model : item.part ) }; + auto const scened { catenary::cantilever_to_scene( placement, heading ) }; + auto entry { + "node " + text_number( data.range_max > 0.0 ? data.range_max : 1000.0, 1 ) + " " + text_number( data.range_min, 1 ) + " none model 0 0 0 0 " + model + ".t3d " + skin + + " angles " + text_number( scened.angles[ 0 ] ) + " " + text_number( scened.angles[ 1 ] ) + " " + text_number( scened.angles[ 2 ] ) + " endmodel" }; + glm::dvec3 const at { origin.x + scened.offset[ 0 ], origin.y + scened.offset[ 1 ], origin.z + scened.offset[ 2 ] }; + if( auto *model3d { simulation::State.create_model( entry, "none", at ) }; model3d != nullptr ) { + catenary_part_detach( model3d ); + Chain.middle_parts.push_back( model3d ); + } + } + } + else { + for( auto const &warning : result.warnings ) { ErrorLog( label + "middle anchor can't be assembled: " + warning ); } + } + } + } + // lina kotwienia: od końca na sąsiednim słupie do zacisku przy słupie środkowym, zwisa jak lina (parabola, 0,5 m + // na 60 m - ZAŁOŻENIE), nad liną nośną + auto const hanging = [ & ]( glm::dvec3 const &From, glm::dvec3 const &To, double const Sag, int const Steps ) { + std::vector points; + for( int k = 0; k <= Steps; ++k ) { + auto const t { static_cast( k ) / Steps }; + auto point { From + ( To - From ) * t }; + point.y -= 4.0 * Sag * t * ( 1.0 - t ); + points.push_back( point ); + } + return points; + }; + auto const flat = []( glm::dvec3 const &A, glm::dvec3 const &B ) { return glm::length( glm::dvec2 { A.x - B.x, A.z - B.z } ); }; + auto rope { hanging( ropeend, clamp, 0.5 * flat( ropeend, clamp ) / 60.0, 24 ) }; + if( tobeam ) { + // od dźwigara: uchwyt 4590 i izolator 7150 (bryły zastępcze - wymiary izolatora jak model 7150-01, uchwyt ZAŁOŻENIE) + auto const down { clamp - beam }; + auto const along { glm::length( down ) > 1e-6 ? glm::normalize( down ) : glm::dvec3 { 0.0, -1.0, 0.0 } }; + add_box( solid, beam, towards, 0.25, 0.08 ); + add_box( solid, beam + along * ( 0.15 + 0.4162 ), along, 0.8324, 0.06 ); + } + // między zaciskami przy słupie środkowym lina zwisa swobodnie pod uchwytem liny nośnej (Jan 2026-10-25): do połowy + // odległości (słup środkowy) 0,25 m poniżej liny nośnej + if( tee ) { + // obejma na wysięgniku teowym (model 4160 obok siodła; bez siodła w złożeniu - bryła zastępcza nad liną, raz) + if( side == 0 && glm::length( collar - middlerope ) < 1e-6 ) { add_box( solid, middlerope + glm::dvec3 { 0.0, 0.03, 0.0 }, towards, 0.12, 0.06 ); } + } + else if( false == tobeam ) { + auto const below { hanging( clamp, middlerope, 0.0, 6 ) }; + for( std::size_t k = 1; k < below.size(); ++k ) { + auto point { below[ k ] }; + auto const t { static_cast( k ) / ( below.size() - 1 ) }; + point.y -= 0.25 * std::sin( 0.5 * 3.14159265358979323846 * t ); + rope.push_back( point ); + } + } + add_tube( solid, rope, ropeheight ); + // V (złączka 6410-5 na przewodzie w połowie przęsła, ramiona do liny nośnej 1,50 m w obie strony wzdłuż przęsła). + // Linka jest sztywna: z zacisku wychodzi chwilę wzdłuż liny nośnej, potem skośnie w dół i dochodzi do złączki wzdłuż + // przewodu (Jan 2026-10-25, zdjęcie) - krzywa trzeciego stopnia ze styczną do liny i do przewodu, wektory kontrolne + // po 0,6 odległości w poziomie (ZAŁOŻENIE dobrane na oko do zdjęcia) + auto const reach { spanlength > 4.0 ? 1.5 / spanlength : 0.0 }; + auto const a { messengerat( 0.5 - reach ) }, b { messengerat( 0.5 + reach ) }; + auto const unit = []( glm::dvec3 const &V ) { return glm::length( V ) > 1e-9 ? glm::normalize( V ) : glm::dvec3 { 1.0, 0.0, 0.0 }; }; + auto const wiredirection { unit( vec( wires[ neighbour ] ) - vec( wires[ 1 ] ) ) }; + auto const step { spanlength > 4.0 ? 0.1 / spanlength : 0.0 }; + auto const stiff = [ & ]( glm::dvec3 const &From, glm::dvec3 const &Fromdirection, glm::dvec3 const &To, glm::dvec3 const &Todirection ) { + auto const handle { 0.6 * flat( From, To ) }; + auto const p1 { From + Fromdirection * handle }, p2 { To - Todirection * handle }; + std::vector points; + int const steps { 16 }; + for( int k = 0; k <= steps; ++k ) { + auto const t { static_cast( k ) / steps }, u { 1.0 - t }; + points.push_back( From * ( u * u * u ) + p1 * ( 3.0 * u * u * t ) + p2 * ( 3.0 * u * t * t ) + To * ( t * t * t ) ); + } + return points; + }; + add_tube( solid, stiff( a, unit( messengerat( 0.5 - reach + step ) - a ), wiremiddle, wiredirection ), ropeheight ); + add_tube( solid, stiff( wiremiddle, wiredirection, b, unit( b - messengerat( 0.5 + reach - step ) ) ), ropeheight ); + // bryły zastępcze: zaciski 5560/5570 (na linie nośnej przy słupie środkowym i na końcach V), złączka 6410 na przewodzie + if( false == tee ) { add_box( solid, clamp, towards, 0.10, 0.045 ); } + add_box( solid, a, alongspan, 0.08, 0.04 ); + add_box( solid, b, alongspan, 0.08, 0.04 ); + add_box( solid, wiremiddle, alongspan, 0.10, 0.05 ); + } +} + // robi od nowa przęsło k łańcucha (przy podwieszeniu przesuniętym w edytorze tylko dwa przęsła przy nim) void rebuild_span( scene_chain &Chain, std::size_t const Span ) { @@ -844,11 +1601,34 @@ rebuild_span( scene_chain &Chain, std::size_t const Span ) { auto const body { span_body( Chain, Span ) }; if( body.empty() ) { return; } Chain.spans[ Span ] = catenary_span_create( span_name( Chain, Span ), Chain.data.range_max, Chain.data.range_min, body, Initialized ); + // przęsło do kotwienia rysuje z bliska jego linę (bryła proceduralna urządzenia naprężającego) + if( auto *span { Chain.spans[ Span ] }; span != nullptr ) { + std::vector rope; + auto const ends { chain_points( Chain ) }; + for( auto const *name : { Span == 0 ? &Chain.data.anchor_start : nullptr, Span + 2 == ends ? &Chain.data.anchor_end : nullptr, + Span == 0 ? &Chain.data.anchor_start_contact : nullptr, Span + 2 == ends ? &Chain.data.anchor_end_contact : nullptr } ) { + if( name == nullptr || name->empty() ) { continue; } + if( auto const *anchor { anchor_named( Chain.file, *name ) }; anchor != nullptr && anchor->solved ) { + rope.insert( rope.end(), anchor->rope.begin(), anchor->rope.end() ); + } + } + // kotwienie środkowe: lina, V i zaciski na przęsłach przed i za podwieszeniem środkowym + if( Span == Chain.middle_span || Span == Chain.middle_span + 1 ) { + auto const &solid { Chain.middle_solid[ Span == Chain.middle_span ? 0 : 1 ] }; + rope.insert( rope.end(), solid.begin(), solid.end() ); + } + if( false == rope.empty() ) { catenary_span_attach( *span, rope ); } + } } // wszystkie przęsła łańcucha od nowa (zmiana typu, materiału, listy podwieszeń) void rebuild_chain( scene_chain &Chain ) { + // kotwienia łańcucha od nowa (stan drutów, kierunek do pierwszego/ostatniego podwieszenia), przed przęsłami do nich + for( auto const *name : { &Chain.data.anchor_start, &Chain.data.anchor_end, &Chain.data.anchor_start_contact, &Chain.data.anchor_end_contact } ) { + if( auto *anchor { anchor_named( Chain.file, *name ) }; anchor != nullptr ) { assemble_anchor( *anchor ); } + } + assemble_middle( Chain ); for( auto *span : Chain.spans ) { if( span != nullptr ) { catenary_span_delete( span ); } } Chain.spans.assign( Chain.removed || Chain.data.suspensions.size() < 2 ? 0 : chain_points( Chain ) - 1, nullptr ); for( std::size_t span = 0; span < Chain.spans.size(); ++span ) { rebuild_span( Chain, span ); } @@ -1013,6 +1793,21 @@ catenary_scene_resolve() { if( catalog != nullptr ) { for( auto const &umbrella : umbrellas ) { assemble_umbrella( umbrella.first, umbrella.second, *catalog, assembled, parts ); } } + // kotwienia środkowe przy teownikach od nowa: obejma liny kotwienia staje obok siodła liny nośnej, a wysięgniki złożyły + // się dopiero teraz (łańcuchy powstały przy wczytaniu) + auto const anymiddle { std::any_of( Chains.begin(), Chains.end(), []( scene_chain const &Chain ) { return false == Chain.removed && false == Chain.data.middle.empty(); } ) }; + if( auto const *cantilevercatalog { anymiddle ? cantilevers() : nullptr }; cantilevercatalog != nullptr ) { + for( auto &chain : Chains ) { + if( chain.removed || chain.data.middle.empty() ) { continue; } + auto const index { suspension_named( chain.file, chain.data.middle ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].has_saddle ) { continue; } + auto const *scheme { cantilevercatalog->scheme( Suspensions[ index ].data.card, Suspensions[ index ].data.execution ) }; + if( scheme == nullptr || scheme->assembly.rfind( "tee_", 0 ) != 0 ) { continue; } + assemble_middle( chain ); + if( chain.middle_span == static_cast( -1 ) ) { continue; } + for( auto const span : { chain.middle_span, chain.middle_span + 1 } ) { rebuild_span( chain, span ); } + } + } WriteLog( ( "Catenary: " + std::to_string( complete ) + " of " + std::to_string( Suspensions.size() - first ) + " suspension(s) linked to supports and traction pieces" ).c_str() ); if( assembled > 0 ) { WriteLog( ( "Catenary: " + std::to_string( assembled ) + " cantilever(s) assembled from " + std::to_string( parts ) + " part(s)" ).c_str() ); @@ -1108,25 +1903,65 @@ reassemble( std::size_t const Index ) { std::size_t assembled { 0 }, parts { 0 }; if( false == suspension.data.umbrella.empty() ) { auto const members { umbrella_members( suspension.data.umbrella ) }; + // w edytorze parasol na chwilę ma jeden wysięgnik (drugi dopiero dochodzi albo właśnie odszedł): bez komunikatu + if( members.size() == 1 ) { + drop_parts( suspension ); + assemble_alone( Index, *catalog ); + return; + } for( auto const index : members ) { drop_parts( Suspensions[ index ] ); } - assemble_umbrella( suspension.data.umbrella, members, *catalog, assembled, parts ); + assemble_umbrella( suspension.data.umbrella, members, *catalog, assembled, parts, false ); return; } drop_parts( suspension ); assemble_alone( Index, *catalog ); } -// przęsła łańcuchów przy podwieszeniu idą za nim +// przęsła łańcuchów przy podwieszeniu idą za nim, kotwienia na jego słupie też void follow_chains( std::size_t const Index ) { auto const &suspension { Suspensions[ Index ] }; if( suspension.data.name.empty() ) { return; } + for( auto &anchor : Anchors ) { + if( anchor.removed || anchor.file != suspension.file || anchor.data.on != suspension.data.name ) { continue; } + assemble_anchor( anchor ); + follow_anchor( anchor ); + } + // trzyma linę w miejscu innych podwieszeń: ich linki celują w nią + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != suspension.file ) { continue; } + for( auto const &pair : chain.data.messenger_from ) { + if( pair.second != suspension.data.name || pair.first == pair.second ) { continue; } + if( auto const index { suspension_named( chain.file, pair.first ) }; index < Suspensions.size() && index != Index ) { reassemble( index ); } + } + } for( auto &chain : Chains ) { if( chain.removed || chain.file != suspension.file ) { continue; } auto const &names { chain.data.suspensions }; for( std::size_t position = 0; position < names.size(); ++position ) { - if( names[ position ] != suspension.data.name ) { continue; } + if( names[ position ] != suspension.data.name && catenary::messenger_holder( chain.data, names[ position ] ) != suspension.data.name ) { continue; } + // skrajne podwieszenie: kotwienie celuje w nie łańcuchami do drutów + std::string const *end { position == 0 ? &chain.data.anchor_start : position + 1 == names.size() ? &chain.data.anchor_end : nullptr }; + if( end != nullptr && false == end->empty() ) { + if( auto *anchor { anchor_named( chain.file, *end ) }; anchor != nullptr ) { assemble_anchor( *anchor ); } + } + // przewody zakotwione osobno + std::string const *endcontact { position == 0 ? &chain.data.anchor_start_contact : position + 1 == names.size() ? &chain.data.anchor_end_contact : nullptr }; + if( endcontact != nullptr && false == endcontact->empty() ) { + if( auto *anchor { anchor_named( chain.file, *endcontact ) }; anchor != nullptr ) { assemble_anchor( *anchor ); } + } auto const point { position + ( chain.data.anchor_start.empty() ? 0 : 1 ) }; + // kotwienie środkowe przy tym podwieszeniu albo przy sąsiednim: od nowa, z oboma przęsłami przy nim + if( false == chain.data.middle.empty() ) { + auto const middle { std::find( names.begin(), names.end(), chain.data.middle ) }; + auto const at { static_cast( middle - names.begin() ) }; + if( middle != names.end() && position + 1 >= at && position <= at + 1 ) { + assemble_middle( chain ); + for( auto const span : { chain.middle_span, chain.middle_span + 1 } ) { + if( span != point && span + 1 != point ) { rebuild_span( chain, span ); } + } + } + } if( point > 0 ) { rebuild_span( chain, point - 1 ); } rebuild_span( chain, point ); } @@ -1203,7 +2038,11 @@ catenary_scene_remove( std::size_t const Index ) { auto &names { chain.data.suspensions }; auto const before { names.size() }; names.erase( std::remove( names.begin(), names.end(), suspension.data.name ), names.end() ); - if( names.size() == before ) { continue; } + // lina nośna trzymana przez nie albo w jego punkcie: wraca do wysięgnika z listy + auto &pairs { chain.data.messenger_from }; + auto const pairsbefore { pairs.size() }; + pairs.erase( std::remove_if( pairs.begin(), pairs.end(), [ & ]( auto const &Pair ) { return Pair.first == suspension.data.name || Pair.second == suspension.data.name; } ), pairs.end() ); + if( names.size() == before && pairs.size() == pairsbefore ) { continue; } if( names.size() < 2 ) { chain.removed = true; } rebuild_chain( chain ); } @@ -1367,8 +2206,16 @@ bool catenary_chain_update( std::size_t const Index, catenary::chain const &Data ) { if( Index >= Chains.size() ) { return false; } auto &chain { Chains[ Index ] }; + // podwieszenia, których linę trzyma (trzymał) inny wysięgnik: ich linki celują w linę, więc składane od nowa + std::set held; + for( auto const *pairs : { &static_cast( chain.data ).messenger_from, &Data.messenger_from } ) { + for( auto const &pair : *pairs ) { if( pair.first != pair.second ) { held.insert( pair.first ); } } + } chain.data = Data; chain.removed = Data.suspensions.size() < 2; + for( auto const &name : held ) { + if( auto const index { suspension_named( chain.file, name ) }; index < Suspensions.size() ) { reassemble( index ); } + } rebuild_chain( chain ); mark_changed( chain.file ); return true; @@ -1382,6 +2229,16 @@ catenary_chain_remove( std::size_t const Index ) { mark_changed( Chains[ Index ].file ); } +bool +catenary_chain_middle( std::size_t const Index, std::size_t &Parts, std::size_t &Triangles ) { + Parts = Triangles = 0; + if( Index >= Chains.size() || Chains[ Index ].removed || Chains[ Index ].data.middle.empty() ) { return false; } + auto const &chain { Chains[ Index ] }; + Parts = chain.middle_parts.size(); + Triangles = ( chain.middle_solid[ 0 ].size() + chain.middle_solid[ 1 ].size() ) / 3; + return chain.middle_span != static_cast( -1 ); +} + int catenary_chain_of( std::size_t const Suspension, std::size_t &Position ) { if( Suspension >= Suspensions.size() || Suspensions[ Suspension ].data.name.empty() ) { return -1; } @@ -1502,13 +2359,14 @@ catenary_scene_new_gantry_name( std::string const &File ) { namespace { -// przęsła do kotwienia (pierwsze albo ostatnie łańcucha) idą za nim +// przęsła łańcucha z kotwieniem idą za nim: przęsło do kotwienia (położenie końców), a pozostałe przez naciąg +// (brakujące ciężary zmieniają zwis wszystkich przęseł do kotwienia środkowego) void follow_anchor( scene_anchor const &Anchor ) { for( auto &chain : Chains ) { if( chain.removed || chain.file != Anchor.file || Anchor.data.name.empty() ) { continue; } - if( chain.data.anchor_start == Anchor.data.name && false == chain.spans.empty() ) { rebuild_span( chain, 0 ); } - if( chain.data.anchor_end == Anchor.data.name && false == chain.spans.empty() ) { rebuild_span( chain, chain.spans.size() - 1 ); } + if( false == anchors_chain( chain.data, Anchor.data.name ) ) { continue; } + for( std::size_t span = 0; span < chain.spans.size(); ++span ) { rebuild_span( chain, span ); } } } @@ -1541,6 +2399,49 @@ catenary_anchor_result( std::size_t const Index ) { return &Anchors[ Index ].result; } +std::vector +catenary_anchor_devices( std::string const &Only ) { + std::vector result; + // urządzenia kotwienia środkowego (lina kotwienia środkowego) nie są kotwieniem końca łańcucha + static std::set const middle { "0406", "0406-2", "0408", "0408-2", "0418", "0418-2" }; + if( auto const *devices { tensioners() }; devices != nullptr ) { + for( auto const &device : devices->devices() ) { + if( device.only != Only || middle.count( device.name ) ) { continue; } + // odmiany dla typów sieci (-10, -32, ...) dobiera się samo; połówki rozdzielonego (-m, -c) i nazwane - na liście + auto const dash { device.name.find( '-' ) }; + auto const half { dash != std::string::npos && ( device.name.substr( dash ) == "-m" || device.name.substr( dash ) == "-c" ) }; + if( dash == std::string::npos || false == device.title.empty() || ( false == Only.empty() && half ) ) { + result.push_back( { device.name, device.fixed, false == device.small_pulley.empty() || device.split, device.split, device.title } ); + } + } + } + return result; +} + +catenary::tensioner_catalog const * +catenary_scene_tensioners() { + return tensioners(); +} + +bool +catenary_anchor_shape( std::size_t const Index, double ( &Shape )[ 4 ][ 3 ] ) { + if( Index >= Anchors.size() || Anchors[ Index ].removed || false == Anchors[ Index ].solved ) { return false; } + for( int k = 0; k < 4; ++k ) { std::copy( std::begin( Anchors[ Index ].shape[ k ] ), std::end( Anchors[ Index ].shape[ k ] ), Shape[ k ] ); } + return true; +} + +int +catenary_anchor_chain( std::size_t const Index, bool &Start ) { + if( Index >= Anchors.size() || Anchors[ Index ].removed ) { return -1; } + auto const &anchor { Anchors[ Index ] }; + for( std::size_t k = 0; k < Chains.size(); ++k ) { + auto const &chain { Chains[ k ] }; + if( chain.removed || chain.file != anchor.file || anchor.data.name.empty() ) { continue; } + if( anchors_chain( chain.data, anchor.data.name, &Start ) ) { return static_cast( k ); } + } + return -1; +} + std::size_t catenary_anchor_add( std::string const &File, catenary::anchor const &Data ) { if( file_named( File ) == nullptr ) { diff --git a/world/catenary_scene.h b/world/catenary_scene.h index cde4e2e5..35b7397f 100644 --- a/world/catenary_scene.h +++ b/world/catenary_scene.h @@ -30,6 +30,7 @@ void catenary_scene_data( std::string const &Directory ); // Zmiany trafiają do plików sieci dopiero przy catenary_scene_save(). #include +#include #include "world/catenary_suspension.h" #include "world/catenary_cantilever.h" #include "world/catenary_layout.h" @@ -87,6 +88,9 @@ std::size_t catenary_chain_add( std::string const &File, catenary::chain const & // zmienia łańcuch i robi jego przęsła od nowa (mniej niż dwa podwieszenia = łańcuch usunięty) bool catenary_chain_update( std::size_t Index, catenary::chain const &Data ); void catenary_chain_remove( std::size_t Index ); +// kotwienie środkowe łańcucha: liczba wstawionych części (na słupach sąsiednich podwieszeń) i trójkątów brył (lina, V, zaciski); +// false, gdy łańcuch go nie ma albo się nie złożyło +bool catenary_chain_middle( std::size_t Index, std::size_t &Parts, std::size_t &Triangles ); // łańcuch, do którego należy podwieszenie, i jego miejsce w łańcuchu; -1 gdy żaden int catenary_chain_of( std::size_t Suspension, std::size_t &Position ); @@ -97,6 +101,23 @@ std::string catenary_anchor_file( std::size_t Index ); std::size_t catenary_anchor_parts( std::size_t Index ); // wynik solvera ostatniego złożenia (b, s, ciężary, naciągi, uwagi); nullptr gdy usunięte catenary::tensioner_result const *catenary_anchor_result( std::size_t Index ); +// obrys złożonego urządzenia dla edytora (współrzędne scenerii): czoło słupa i oś rolki ruchomej na wysokości rolek, góra i spód +// kolumny ciężarów; false gdy usunięte albo niezłożone +bool catenary_anchor_shape( std::size_t Index, double ( &Shape )[ 4 ][ 3 ] ); +// karty urządzeń kotwienia do wyboru (bez odmian dla typów sieci, np. 0461-2x2): nazwa, kotwienie stałe, rolki szeregowe +struct catenary_anchor_card { + std::string card; + bool fixed { false }; + bool series { false }; + bool split { false }; // dwa stosy (lina i przewody osobno) + std::string title; // opis z danych (odmiany pokazane osobno, np. 0461-2x2) +}; +// Only: puste - kotwienia całości; "messenger" / "contact" - samej liny / samych przewodów (przewody zakotwione osobno) +std::vector catenary_anchor_devices( std::string const &Only = {} ); +// katalog urządzeń naprężających (traction_tensioners.txt); nullptr, gdy go nie ma +catenary::tensioner_catalog const *catenary_scene_tensioners(); +// łańcuch, który kończy się tym kotwieniem (Start: na początku), -1 gdy żaden +int catenary_anchor_chain( std::size_t Index, bool &Start ); // dodaje kotwienie, składa je i przebudowuje łańcuchy, które go używają. zwraca numer std::size_t catenary_anchor_add( std::string const &File, catenary::anchor const &Data ); // zmienia kotwienie (także przywraca usunięte) i przebudowuje łańcuchy, które go używają diff --git a/world/catenary_span.cpp b/world/catenary_span.cpp index 01de759d..b012c257 100644 --- a/world/catenary_span.cpp +++ b/world/catenary_span.cpp @@ -10,6 +10,8 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/catenary_span.h" +#include + #include "world/Traction.h" #include "world/catenary_network.h" #include "utilities/Globals.h" @@ -31,6 +33,10 @@ struct catenary_span { gfx::geometrybank_handle solidbank; // osobny bank na bryły tego przęsła catenary_solid solid; // bryły do oglądania z bliska bool solidloaded { false }; + std::vector attached; // bryła dołączona (lina kotwienia), układ scenerii + std::shared_ptr own; // kopia typu z naciągiem z dopisku "tension" + bool rope2set[ 2 ] { false, false }; // końce drugiej liny nośnej z dopisku "rope2" + catenary::vec3 rope2[ 2 ]; }; namespace { @@ -43,11 +49,19 @@ definition( TTraction const &Traction ) { catenary::span_points points( TTraction const &Traction ) { - return { + catenary::span_points result { { Traction.pPoint1.x, Traction.pPoint1.y, Traction.pPoint1.z }, { Traction.pPoint2.x, Traction.pPoint2.y, Traction.pPoint2.z }, { Traction.pPoint3.x, Traction.pPoint3.y, Traction.pPoint3.z }, { Traction.pPoint4.x, Traction.pPoint4.y, Traction.pPoint4.z } }; + if( Traction.m_catenaryspan ) { + auto const &span { *Traction.m_catenaryspan }; + result.messenger2_start_set = span.rope2set[ 0 ]; + result.messenger2_end_set = span.rope2set[ 1 ]; + result.messenger2_start = span.rope2[ 0 ]; + result.messenger2_end = span.rope2[ 1 ]; + } + return result; } // długość podwieszenia Y, jaką typ sieci przewiduje dla tego przęsła; 0 gdy brak @@ -249,13 +263,17 @@ catenary_span_metal( TTraction const &Traction, bool const Fitting ) { // przypisuje przęsłu typ sieci z dopisku "type" i dobiera rozstaw wieszaków void -catenary_span_assign( TTraction &Traction, std::string const &Symbol ) { +catenary_span_assign( TTraction &Traction, std::string const &Symbol, double const Messenger, double const Contact, glm::dvec3 const *Rope2, bool const *Rope2set ) { Traction.m_catenaryspan.reset(); if( Symbol.empty() ) { return; } auto span { std::make_shared() }; span->symbol = Symbol; + for( int end = 0; end < 2 && Rope2 != nullptr && Rope2set != nullptr; ++end ) { + span->rope2set[ end ] = Rope2set[ end ]; + span->rope2[ end ] = catenary::vec3 { Rope2[ end ].x, Rope2[ end ].y, Rope2[ end ].z }; + } Traction.m_catenaryspan = span; if( false == Global.CatenaryProcedural ) { return; } @@ -265,6 +283,13 @@ catenary_span_assign( TTraction &Traction, std::string const &Symbol ) { } span->definition = registry.find( Symbol ); if( span->definition == nullptr ) { return; } // brak typu został już zgłoszony, przęsło rysuje się po staremu + // naciąg z dopisku "tension" (daN): przęsło liczone z kopią typu + if( Messenger > 0.0 || Contact > 0.0 ) { + span->own = std::make_shared( *span->definition ); + if( Messenger > 0.0 ) { span->own->type.messenger_tension = 10.0 * Messenger; } + if( Contact > 0.0 && span->own->type.contact_tension > 0.0 ) { span->own->type.contact_tension = 10.0 * Contact; } + span->definition = span->own.get(); + } // odstęp wieszaków 0 w scenerii oznacza odcinek bez wieszaków (do kotwienia albo urządzenia naprężającego): // rysowane są same liny i przewody w liczbie wg typu, a rozstaw wieszaków zostaje pusty if( Traction.iNumSections == 0 ) { return; } @@ -274,6 +299,13 @@ catenary_span_assign( TTraction &Traction, std::string const &Symbol ) { std::hypot( Traction.pPoint2.x - Traction.pPoint1.x, Traction.pPoint2.z - Traction.pPoint1.z ) ); } +// wygląd liny stalowej (ocynk, lekko zmatowiały) +catenary_metal const & +catenary_rope_metal() { + static catenary_metal const steel { { 0.56f, 0.57f, 0.58f }, 0.8f, 0.30f, 0.20f }; + return steel; +} + // symbol typu sieci przypisany przęsłu; pusty gdy brak std::string catenary_span_symbol( TTraction const &Traction ) { @@ -318,6 +350,7 @@ catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camera ) { // kamera daleko: zwolnienie pamięci, linie wystarczą release( span.solid.wires ); release( span.solid.fittings ); + release( span.solid.rope ); span.solid = {}; span.solidloaded = false; return {}; @@ -344,6 +377,10 @@ catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camera ) { auto data { vertices( Traction, fittings ) }; span.solid.fittings = GfxRenderer->Insert( data, userdata, span.solidbank, GL_TRIANGLES ); } + if( false == span.attached.empty() ) { + auto data { vertices( Traction, span.attached ) }; + span.solid.rope = GfxRenderer->Insert( data, userdata, span.solidbank, GL_TRIANGLES ); + } span.solidloaded = ( span.solid.wires != null_handle ); return ( span.solidloaded ? span.solid : catenary_solid() ); } @@ -358,6 +395,7 @@ drop_solid( TTraction &Traction ) { if( false == span.solidloaded ) { return; } release( span.solid.wires ); release( span.solid.fittings ); + release( span.solid.rope ); span.solid = {}; span.solidloaded = false; } @@ -383,3 +421,12 @@ catenary_span_release( TTraction &Traction ) { drop_solid( Traction ); release( Traction.m_geometry ); } + +// bryła spoza przęsła rysowana razem z nim (lina kotwienia); zrobi się przy następnym podejściu kamery +bool +catenary_span_attach( TTraction &Traction, std::vector const &Triangles ) { + if( false == static_cast( Traction.m_catenaryspan ) ) { return false; } + drop_solid( Traction ); + Traction.m_catenaryspan->attached = Triangles; + return ( Traction.m_catenaryspan->definition != nullptr ); +} diff --git a/world/catenary_span.h b/world/catenary_span.h index 1a18931e..0e477243 100644 --- a/world/catenary_span.h +++ b/world/catenary_span.h @@ -14,10 +14,12 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include #include "rendering/geometrybank.h" class TTraction; +namespace catenary { struct solid_vertex; } // ustawienia rysowania sieci, zmieniane z eu07.ini i z panelu ustawień struct catenary_settings { @@ -27,7 +29,11 @@ extern catenary_settings CatenarySettings; // przypisuje przęsłu typ sieci z dopisku "type" i dobiera rozstaw wieszaków; wołane po wczytaniu punktów przęsła. // pusty symbol albo wyłączone rysowanie wg typu zostawia przęsło rysowane po staremu -void catenary_span_assign( TTraction &Traction, std::string const &Symbol ); +// Messenger, Contact: naciąg liny nośnej i przewodów jezdnych w daN zamiast naciągu typu (dopisek "tension"); 0 = jak w typie +// Rope2: końce drugiej liny nośnej podane w wpisie ("rope2 start|end x y z", kotwienie z dwiema liniami): [0] start, [1] koniec, +// Rope2set - które podane +void catenary_span_assign( TTraction &Traction, std::string const &Symbol, double Messenger = 0.0, double Contact = 0.0, + glm::dvec3 const *Rope2 = nullptr, bool const *Rope2set = nullptr ); // symbol typu sieci przypisany przęsłu; pusty gdy brak std::string catenary_span_symbol( TTraction const &Traction ); // odcinki przęsła policzone wg typu sieci, dopisywane do Vertices; false gdy przęsło ma być narysowane po staremu @@ -37,6 +43,7 @@ bool catenary_span_lines( TTraction &Traction, gfx::vertex_array &Vertices ); struct catenary_solid { gfx::geometry_handle wires; gfx::geometry_handle fittings; + gfx::geometry_handle rope; // bryła dołączona z zewnątrz (lina urządzenia naprężającego na końcu przęsła), stal }; // bryły tworzone są, gdy kamera jest blisko przęsła, i zwalniane, gdy się oddali catenary_solid catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camera ); @@ -44,6 +51,9 @@ catenary_solid catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camer void catenary_span_refresh( TTraction &Traction ); // zwalnia linie i bryły przęsła przed jego usunięciem void catenary_span_release( TTraction &Traction ); +// dołącza do brył przęsła bryłę spoza przęsła (lina kotwienia przy jego końcu; trójkąty w układzie scenerii), rysowaną +// razem z nim z bliska, ze stali. Pusta lista odłącza. false, gdy przęsło nie jest rysowane wg typu sieci +bool catenary_span_attach( TTraction &Traction, std::vector const &Triangles ); // wygląd metalu brył: kolor własny (sRGB), metaliczność, połysk i siła odbicia otoczenia, każde w zakresie 0–1 struct catenary_metal { @@ -55,3 +65,5 @@ struct catenary_metal { // wygląd przewodów (Fitting = false) albo osprzętu przęsła o podanym materiale i stanie z wpisu scenerii. // wartości domyślne można zmienić bez kompilacji plikiem data/traction_materials.txt, czytanym przy pierwszym użyciu catenary_metal const &catenary_span_metal( TTraction const &Traction, bool Fitting ); +// wygląd liny stalowej (cięgło 4490 urządzenia naprężającego) +catenary_metal const &catenary_rope_metal(); diff --git a/world/catenary_suspension.cpp b/world/catenary_suspension.cpp index b778bdf8..b227b679 100644 --- a/world/catenary_suspension.cpp +++ b/world/catenary_suspension.cpp @@ -33,7 +33,7 @@ std::string normalize_path( std::string Path ) { // słowa kluczowe wewnątrz wpisu podwieszenia bool is_keyword( std::string const &Token ) { - static std::set const keywords { "support", "scheme", "messenger", "contact", "insulators", "override", "skin", "at", "attach", "rope", "pole", "umbrella", "endsuspension" }; + static std::set const keywords { "support", "scheme", "messenger", "contact", "insulators", "override", "skin", "at", "attach", "rope", "pole", "umbrella", "spread", "shared_pole", "endsuspension" }; return keywords.find( Token ) != keywords.end(); } @@ -101,6 +101,15 @@ void parse_suspensions( std::vector const &Tokens, std::string cons else if( key == "parallel" ) { one( line.parallel ); } else if( key == "anchor_start" ) { one( line.anchor_start ); } else if( key == "anchor_end" ) { one( line.anchor_end ); } + else if( key == "anchor_start_contact" ) { one( line.anchor_start_contact ); } + else if( key == "anchor_end_contact" ) { one( line.anchor_end_contact ); } + else if( key == "middle" ) { one( line.middle ); } + else if( key == "middle_device" ) { one( line.middle_device ); } + else if( key == "middle_height" ) { number( line.middle_heights[ 0 ] ); number( line.middle_heights[ 1 ] ); } + else if( key == "messenger_from" ) { + std::string listed, holder; + if( one( listed ) && one( holder ) ) { line.messenger_from.emplace_back( listed, holder ); } + } else if( key == "hangers" ) { for( auto &value : line.hangers ) { number( value ); } } else if( key == "suspensions" ) { while( index < count && Tokens[ index ] != "endchain" && Tokens[ index ] != "node" ) { line.suspensions.push_back( Tokens[ index++ ] ); } @@ -177,12 +186,21 @@ void parse_suspensions( std::vector const &Tokens, std::string cons else if( key == "network" ) { line.network = one(); } else if( key == "pole" ) { line.pole_type = one(); point( line.pole ); haspole = true; } else if( key == "toward" ) { point( line.toward ); hastoward = true; } + else if( key == "on" ) { line.on = one(); } else if( key == "distance" ) { number( line.distance ); } else if( key == "color" ) { line.color = one(); } else if( key == "top_color" ) { line.top_color = one(); } else if( key == "elongation" ) { number( line.elongation ); } else if( key == "missing" ) { double value { 0.0 }; number( value ); line.missing = static_cast( value ); } else if( key == "ground" ) { number( line.ground ); } + else if( key == "height" ) { number( line.height ); } + else if( key == "rope_height" ) { number( line.rope_height ); } + else if( key == "insulators" ) { + line.insulators = one(); + if( line.insulators != "new" && line.insulators != "old" && line.insulators != "messenger" && line.insulators != "contact" ) { + report( line.name, "\"insulators\" takes new, old, messenger or contact" ); good = false; + } + } else if( key == "skin" ) { line.skin = one(); } else { report( line.name, "unexpected token \"" + key + "\" in an anchor" ); good = false; } } @@ -250,6 +268,14 @@ void parse_suspensions( std::vector const &Tokens, std::string cons } else { report( entry.name, "\"" + key + "\" needs three numbers: a point in scenery coordinates" ); good = false; } } + else if( key == "shared_pole" ) { + if( list.empty() ) { entry.shared_pole = true; } + else { report( entry.name, "\"shared_pole\" takes no values" ); good = false; } + } + else if( key == "spread" ) { + if( list.size() == 3 && to_number( list[ 0 ], entry.spread[ 0 ] ) && to_number( list[ 1 ], entry.spread[ 1 ] ) && to_number( list[ 2 ], entry.spread[ 2 ] ) ) { entry.has_spread = true; } + else { report( entry.name, "\"spread\" needs three numbers: the point on the console in the plane of the cantilever" ); good = false; } + } else if( key == "umbrella" ) { if( list.size() == 1 ) { entry.umbrella = list.front(); } else { report( entry.name, "\"umbrella\" needs exactly one name" ); good = false; } @@ -270,6 +296,8 @@ void parse_suspensions( std::vector const &Tokens, std::string cons if( entry.has_attach && entry.has_pole ) { report( entry.name, "\"attach\" and \"pole\" can't be used together" ); good = false; } if( entry.card == 0 ) { report( entry.name, "explicit points need a \"scheme\"" ); good = false; } if( false == entry.umbrella.empty() && false == entry.has_pole ) { report( entry.name, "\"umbrella\" needs a \"pole\"" ); good = false; } + if( entry.shared_pole && false == entry.has_pole ) { report( entry.name, "\"shared_pole\" needs a \"pole\"" ); good = false; } + if( entry.has_spread && false == entry.has_pole ) { report( entry.name, "\"spread\" needs a \"pole\"" ); good = false; } } else { if( entry.support.empty() ) { report( entry.name, "missing \"support\"" ); good = false; } @@ -318,9 +346,18 @@ std::string write_suspension( suspension const &Entry ) { if( Entry.has_attach ) { text += " attach " + point( Entry.attach ); } if( Entry.has_pole ) { text += " pole " + Entry.pole_type + " " + point( Entry.pole ); } if( false == Entry.umbrella.empty() ) { text += " umbrella " + Entry.umbrella; } + if( Entry.has_spread ) { text += " spread " + point( Entry.spread ); } + if( Entry.shared_pole ) { text += " shared_pole"; } return text + " endsuspension"; } +std::string const &messenger_holder( chain const &Chain, std::string const &Listed ) { + for( auto const &pair : Chain.messenger_from ) { + if( pair.first == Listed && false == pair.second.empty() ) { return pair.second; } + } + return Listed; +} + std::string write_chain( chain const &Entry ) { auto const number = []( double const Value ) { @@ -337,6 +374,13 @@ std::string write_chain( chain const &Entry ) { if( false == Entry.parallel.empty() ) { text += " parallel " + Entry.parallel; } if( false == Entry.anchor_start.empty() ) { text += " anchor_start " + Entry.anchor_start; } if( false == Entry.anchor_end.empty() ) { text += " anchor_end " + Entry.anchor_end; } + if( false == Entry.anchor_start_contact.empty() ) { text += " anchor_start_contact " + Entry.anchor_start_contact; } + if( false == Entry.anchor_end_contact.empty() ) { text += " anchor_end_contact " + Entry.anchor_end_contact; } + if( false == Entry.middle.empty() ) { + text += " middle " + Entry.middle + ( Entry.middle_device != "0406" ? " middle_device " + Entry.middle_device : std::string() ); + if( Entry.middle_heights[ 0 ] > 0.0 || Entry.middle_heights[ 1 ] > 0.0 ) { text += " middle_height " + number( Entry.middle_heights[ 0 ] ) + " " + number( Entry.middle_heights[ 1 ] ); } + } + for( auto const &pair : Entry.messenger_from ) { text += " messenger_from " + pair.first + " " + pair.second; } text += " suspensions"; for( auto const &name : Entry.suspensions ) { text += " " + name; } return text + " endchain"; @@ -378,12 +422,17 @@ std::string write_anchor( anchor const &Entry ) { std::string text { "node " + number( Entry.range_max, 0 ) + " " + number( Entry.range_min, 0 ) + " " + ( Entry.name.empty() ? std::string( "none" ) : Entry.name ) + " anchor" }; text += " device " + Entry.device; if( false == Entry.network.empty() ) { text += " network " + Entry.network; } - text += " pole " + Entry.pole_type + " " + point( Entry.pole ) + " toward " + point( Entry.toward ) + " distance " + number( Entry.distance, 1 ); + text += " pole " + Entry.pole_type + " " + point( Entry.pole ); + if( false == Entry.on.empty() ) { text += " on " + Entry.on; } + text += " toward " + point( Entry.toward ) + " distance " + number( Entry.distance, 1 ); if( false == Entry.color.empty() ) { text += " color " + Entry.color; } if( false == Entry.top_color.empty() ) { text += " top_color " + Entry.top_color; } if( Entry.elongation != 0.0 ) { text += " elongation " + number( Entry.elongation, 3 ); } if( Entry.missing != 0 ) { text += " missing " + std::to_string( Entry.missing ); } if( Entry.ground != -0.50 ) { text += " ground " + number( Entry.ground, 2 ); } + if( Entry.height > 0.0 ) { text += " height " + number( Entry.height, 3 ); } + if( Entry.rope_height > 0.0 ) { text += " rope_height " + number( Entry.rope_height, 3 ); } + if( false == Entry.insulators.empty() ) { text += " insulators " + Entry.insulators; } if( false == Entry.skin.empty() ) { text += " skin " + Entry.skin; } return text + " endanchor"; } diff --git a/world/catenary_suspension.h b/world/catenary_suspension.h index 55686480..88707dcd 100644 --- a/world/catenary_suspension.h +++ b/world/catenary_suspension.h @@ -38,6 +38,12 @@ struct suspension { // parasol: dwa wpisy ze słupem i tą samą nazwą parasola to dwa wysięgniki na jednym słupie, po przeciwnych stronach. // słup stawia pierwszy z nich; mocowania regulowane (z połówką z nakrętkami) idą na wspólną wysokość std::string umbrella; + // wysięgnik rozstawiony (konsola 4890; para wpisów z tą samą nazwą "umbrella" po tej samej stronie słupa albo jeden): punkt na osi + // płaszczyzny wysięgnika przy słupie (oś słupa przesunięta wzdłuż toru do punktu mocowania na konsoli), "spread x y z" + // słup stawia inny wpis w tym samym miejscu (trzeci wysięgnik przy rozstawionych, w osi słupa): ten tylko z niego korzysta + bool shared_pole { false }; + bool has_spread { false }; + double spread[ 3 ] { 0.0, 0.0, 0.0 }; }; // łańcuch sieci: kolejne przęsła między podwieszeniami z listy (od kotwy do kotwy), wpis @@ -58,12 +64,28 @@ struct chain { double hangers[ 4 ] { 0.4, 4.0, 3.0, 0.04 }; // jak w node traction: najniższy wieszak, odstęp, liczba drutów, odstęp drutów std::string parallel; // przęsło bieżni wspólnej (jak "parallel" w node traction); puste = brak std::string anchor_start, anchor_end; // kotwienia (wpisy "anchor") przed pierwszym i za ostatnim podwieszeniem; puste = brak + // przewody jezdne zakotwione osobno (inne kotwienie, zwykle na innym słupie; 0404 albo przewody rozdzielonego): wtedy + // anchor_start/anchor_end trzyma samą linę nośną (0405, lina rozdzielonego). Puste = całość na anchor_start/anchor_end + std::string anchor_start_contact, anchor_end_contact; + // kotwienie środkowe (karty 63-0406 / 0408): podwieszenie, przy którym lina kotwienia środkowego jest zaciśnięta na linie nośnej, + // i karta (0406 z izolacją, 0408 bez); lina idzie do słupów sąsiednich podwieszeń. Puste = brak + std::string middle; + std::string middle_device { "0406" }; + double middle_heights[ 2 ] { 0.0, 0.0 }; // uchwyty liny kotwienia na słupach przed i za (nad główką); 0 = lina nośna sąsiada + // lina nośna z innego podwieszenia niż przewody jezdne (wysięgniki rozstawione na konsoli 4890, wysięgnik samej liny przy + // odciągu przewodów): w punkcie łańcucha przewody idą przez jego "at", lina przez "rope" podwieszenia + // . Wpis "messenger_from ", przed "suspensions" + std::vector> messenger_from; std::vector suspensions; // nazwy podwieszeń z tego samego pliku, po kolei }; +// podwieszenie, które trzyma linę nośną łańcucha w punkcie Listed (samo Listed, gdy nie ma wpisu messenger_from) +std::string const &messenger_holder( chain const &Chain, std::string const &Listed ); + // kotwienie ciężarowe (urządzenie naprężające z pliku traction_tensioners.txt) na słupie kotwowym, wpis -// "node anchor device network pole x y z toward x y z distance -// [color ] [top_color ] [elongation ] [missing ] [ground ] [skin ] endanchor". +// "node anchor device network pole x y z [on ] toward x y z distance +// [color ] [top_color ] [elongation ] [missing ] [ground ] [height ] [rope_height ] [insulators ] [skin ] +// endanchor". // pole: oś słupa na wysokości główki szyny; toward: punkt, z którego przychodzą druty (urządzenie leży w tym kierunku od // słupa); distance: odległość od kotwienia środkowego albo stałego. Łańcuch przypina się do niego "anchor_start/anchor_end", // a ostatnie przęsło (bez wieszaków) kończy się na uchwytach krańcowych urządzenia @@ -75,12 +97,19 @@ struct anchor { std::string network; // symbol typu sieci (traction_.txt): numer typu i liczba przewodów jezdnych std::string pole_type; double pole[ 3 ] { 0.0, 0.0, 0.0 }; + std::string on; // kotwienie na istniejącym słupie: nazwa podwieszenia (z tego pliku), którego słup to + // jest; słupa nie stawia, typ i punkt bierze z tamtego wpisu (pole tylko zapasowo) double toward[ 3 ] { 0.0, 0.0, 0.0 }; double distance { 700.0 }; std::string color, top_color; double elongation { 0.0 }; int missing { 0 }; double ground { -0.50 }; + double height { 0.0 }; // oś rolek nad główką szyny; 0 = z tablicy montażowej (6,70). Kotwienie stałe: przewody + double rope_height { 0.0 }; // kotwienie stałe: uchwyt liny nośnej nad główką szyny; 0 = jak przy ostatnim słupie + // izolatory w łańcuchach do drutów: puste = auto (rolkowe ESN-2025 na obu przy drutach zaśniedziałych, inaczej żywiczne 7150), + // "new" żywiczne, "old" rolkowe na obu, "messenger" rolkowy tylko na linie nośnej, "contact" rolkowe tylko na przewodach + std::string insulators; std::string skin; }; diff --git a/world/catenary_tensioner.cpp b/world/catenary_tensioner.cpp index a695cf0d..06cbb693 100644 --- a/world/catenary_tensioner.cpp +++ b/world/catenary_tensioner.cpp @@ -89,11 +89,11 @@ tensioner_placement placed( std::string const &Role, std::string const &Part, ve return placement; } -// część liniowa: +X części w kierunku Direction +// część liniowa: biegnie w -X części (modele Jana: 4440 z hole_1 na -X), więc -X części w kierunku Direction tensioner_placement placed_along( std::string const &Role, std::string const &Part, vec const &At, vec const &Direction ) { auto placement { placed( Role, Part, At ) }; - auto const x { unit( Direction ) }; - auto const y { side_of( x ) }; + auto const x { unit( Direction ) * -1.0 }; + auto const y { side_of( x ) * -1.0 }; set_rotation( placement, x, y, cross( x, y ) ); return placement; } @@ -103,8 +103,23 @@ vec place_chain( std::vector const &Links, std::string const &Ro auto const step { unit( Direction ) }; int index { 0 }; for( auto const &link : Links ) { - Result.parts.push_back( placed_along( Role + "." + std::to_string( ++index ), link.name, From, step ) ); - From = From + step * link.length; + // "~część": część odwrócona - jej zero (pin_a) na końcu tego odcinka, biegnie z powrotem (uchwyt 4440 przy rolce ruchomej) + auto const reversed { false == link.name.empty() && link.name[ 0 ] == '~' }; + auto const name { reversed ? link.name.substr( 1 ) : link.name }; + auto const end { From + step * link.length }; + auto placement { placed_along( Role + "." + std::to_string( ++index ), name, reversed ? end : From, reversed ? step * -1.0 : step ) }; + if( link.roll != 0.0 ) { + // obrót wokół osi części (kolumna X): Y i Z obracają się w swojej płaszczyźnie + auto const angle { link.roll * kPi / 180.0 }; + auto const c { std::cos( angle ) }, s { std::sin( angle ) }; + for( int r = 0; r < 3; ++r ) { + auto const y { placement.rotation[ r * 3 + 1 ] }, z { placement.rotation[ r * 3 + 2 ] }; + placement.rotation[ r * 3 + 1 ] = c * y + s * z; + placement.rotation[ r * 3 + 2 ] = -s * y + c * z; + } + } + Result.parts.push_back( placement ); + From = end; } return From; } @@ -178,6 +193,10 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin for( auto k { From }; k + 1 < Words.size(); k += 2 ) { tensioner_link link; link.name = Words[ k ]; + if( auto const at { link.name.find( '@' ) }; at != std::string::npos ) { + if( false == number_of( link.name.substr( at + 1 ), link.roll ) ) { report( "bad roll in \"" + link.name + "\"" ); } + link.name.erase( at ); + } if( false == number_of( Words[ k + 1 ], link.length ) ) { report( "bad length \"" + Words[ k + 1 ] + "\"" ); return; } Links.push_back( link ); } @@ -236,13 +255,118 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin if( keyword == "hanger" ) { links_of( words, 1, device->hanger ); continue; } if( keyword == "messenger" ) { links_of( words, 1, device->messenger ); continue; } if( keyword == "contact" ) { links_of( words, 1, device->contact ); continue; } - if( ( keyword == "messenger_clamp" || keyword == "contact_clamp" || keyword == "equalizer" ) && words.size() >= 3 ) { - auto &link { keyword == "messenger_clamp" ? device->messenger_clamp : keyword == "contact_clamp" ? device->contact_clamp : device->equalizer }; + if( keyword == "cascade" ) { links_of( words, 1, device->cascade ); continue; } + if( keyword == "series" && words.size() >= 4 ) { + // series [holder ] + device->small_pulley = words[ 1 ]; + if( false == number_of( words[ 2 ], device->small_spacing ) || false == number_of( words[ 3 ], device->dead_end ) ) { report( "series: bad numbers" ); } + if( words.size() >= 6 && words[ 4 ] == "holder" ) { device->fixed_holder = words[ 5 ]; } + continue; + } + if( keyword == "messenger_each" ) { links_of( words, 1, device->messenger_each ); continue; } + if( keyword == "contact_each" ) { links_of( words, 1, device->contact_each ); continue; } + if( keyword == "messenger_equalizer" && words.size() >= 3 ) { + // messenger_equalizer [spread ] [z] + device->messenger_equalizer.name = words[ 1 ]; + if( false == number_of( words[ 2 ], device->messenger_equalizer.length ) ) { report( "bad length \"" + words[ 2 ] + "\"" ); } + for( std::size_t k = 3; k < words.size(); ++k ) { + if( words[ k ] == "spread" && k + 1 < words.size() ) { number_of( words[ ++k ], device->messenger_spread ); } + else if( words[ k ] == "z" ) { device->messenger_equalizer_z = true; } + } + continue; + } + if( keyword == "only" && words.size() >= 2 ) { + if( words[ 1 ] == "messenger" || words[ 1 ] == "contact" ) { device->only = words[ 1 ]; } + else { report( "only: messenger or contact" ); } + continue; + } + if( keyword == "split" && words.size() >= 3 ) { device->split_messenger = words[ 1 ]; device->split_contact = words[ 2 ]; continue; } + if( keyword == "frame" && words.size() >= 5 ) { + device->frame = words[ 1 ]; + double side { -1.0 }; + if( false == number_of( words[ 2 ], device->frame_length ) || false == number_of( words[ 3 ], device->frame_y ) || false == number_of( words[ 4 ], side ) ) { report( "frame: bad numbers" ); } + device->frame_side = side < 0.0 ? -1 : 1; + device->frame_owner = words.size() >= 6 && words[ 5 ] == "owner"; + continue; + } + if( keyword == "messenger2" ) { links_of( words, 1, device->messenger2 ); continue; } + if( keyword == "guide" && words.size() >= 3 ) { + device->guide = words[ 1 ]; + if( false == number_of( words[ 2 ], device->guide_length ) ) { report( "bad guide length \"" + words[ 2 ] + "\"" ); } + for( std::size_t k = 3; k + 1 < words.size(); k += 2 ) { + if( words[ k ] == "mount" ) { number_of( words[ k + 1 ], device->guide_mount ); } + else if( words[ k ] == "bottom" ) { number_of( words[ k + 1 ], device->guide_bottom ); } + else { report( "unknown guide keyword \"" + words[ k ] + "\"" ); } + } + continue; + } + if( keyword == "brackets" && words.size() >= 2 ) { + tensioner_device::bracket_set set; + std::size_t k { 1 }; + for( ; k < words.size() && words[ k ] != "top" && words[ k ] != "bottom" && words[ k ] != "hole" && words[ k ] != "bolt"; ++k ) { + set.poles.push_back( words[ k ] ); + } + for( ; k < words.size(); ) { + auto const &key { words[ k ] }; + if( ( key == "top" || key == "bottom" ) && k + 2 < words.size() ) { + ( key == "top" ? set.top_front : set.bottom_front ) = words[ k + 1 ]; + ( key == "top" ? set.top_back : set.bottom_back ) = words[ k + 2 ]; + k += 3; + } + else if( key == "hole" && k + 2 < words.size() ) { number_of( words[ k + 1 ], set.hole_out ); number_of( words[ k + 2 ], set.hole_drop ); k += 3; } + else if( key == "bolt" && k + 1 < words.size() ) { number_of( words[ k + 1 ], set.bolt ); k += 2; } + else { report( "unknown brackets keyword \"" + key + "\"" ); break; } + } + device->brackets.push_back( set ); + continue; + } + if( keyword == "pivot" && words.size() >= 3 ) { + number_of( words[ 1 ], device->pivot_top ); + number_of( words[ 2 ], device->pivot_bottom ); + if( words.size() >= 5 && words[ 3 ] == "rise" ) { number_of( words[ 4 ], device->mount_rise ); } + continue; + } + if( keyword == "ring" && words.size() >= 3 ) { + device->ring = words[ 1 ]; + if( false == number_of( words[ 2 ], device->ring_arm ) ) { report( "bad ring arm \"" + words[ 2 ] + "\"" ); } + for( std::size_t k = 3; k + 1 < words.size(); k += 2 ) { + if( words[ k ] == "turn" ) { number_of( words[ k + 1 ], device->ring_turn ); } + else if( words[ k ] == "at" ) { number_of( words[ k + 1 ], device->ring_at ); } + else { report( "unknown ring keyword \"" + words[ k ] + "\"" ); } + } + continue; + } + if( keyword == "messenger_old" ) { links_of( words, 1, device->messenger_old ); continue; } + if( keyword == "contact_old" ) { links_of( words, 1, device->contact_old ); continue; } + if( ( keyword == "messenger_clamp" || keyword == "contact_clamp" || keyword == "contact_clamp_old" || keyword == "equalizer" ) && words.size() >= 3 ) { + auto &link { keyword == "messenger_clamp" ? device->messenger_clamp : keyword == "contact_clamp" ? device->contact_clamp : keyword == "contact_clamp_old" ? device->contact_clamp_old : device->equalizer }; link.name = words[ 1 ]; if( false == number_of( words[ 2 ], link.length ) ) { report( "bad length \"" + words[ 2 ] + "\"" ); } if( keyword == "equalizer" && words.size() >= 5 && words[ 3 ] == "spread" ) { number_of( words[ 4 ], device->equalizer_spread ); } continue; } + if( keyword == "safety" && words.size() >= 5 ) { + // safety + device->safety_fixed = words[ 1 ]; + device->safety_moving = words[ 2 ]; + number_of( words[ 3 ], device->safety_arm ); + number_of( words[ 4 ], device->safety_offset ); + continue; + } + if( keyword == "safety_series" && words.size() >= 7 ) { + device->series_arm_fixed = words[ 1 ]; + device->series_arm_moving = words[ 2 ]; + device->series_bar = words[ 3 ]; + number_of( words[ 4 ], device->series_arm ); + number_of( words[ 5 ], device->series_bar_length ); + number_of( words[ 6 ], device->safety_offset ); + continue; + } + if( keyword == "title" && words.size() >= 2 ) { + device->title.clear(); + for( std::size_t k = 1; k < words.size(); ++k ) { device->title += ( k > 1 ? " " : "" ) + words[ k ]; } + continue; + } if( keyword == "type" && words.size() >= 2 ) { tensioner_type type; double code; @@ -263,6 +387,11 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin type.contact_hole = words[ k++ ]; type.axis_hole = words[ k++ ]; } + else if( key == "second" && k + 2 < words.size() ) { + type.second_messenger_hole = words[ k++ ]; + type.second_contact_hole = words[ k++ ]; + type.second_axis_hole = words[ k++ ]; + } else if( key == "rod" && k < words.size() ) { type.rod = words[ k++ ]; } else if( key == "clamp" && k < words.size() ) { type.messenger_clamp = words[ k++ ]; } else { report( "type " + words[ 1 ] + ": unknown keyword \"" + key + "\" (a weight has to be defined before the device)" ); break; } @@ -273,6 +402,12 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin report( "unknown keyword \"" + keyword + "\" in a device" ); continue; } + if( keyword == "network" && words.size() >= 4 && words[ 2 ] == "as" ) { + double code; + if( false == number_of( words[ 3 ], code ) ) { report( "bad network code \"" + words[ 3 ] + "\"" ); continue; } + m_networks[ words[ 1 ] ] = static_cast( code ); + continue; + } if( keyword == "table" && words.size() >= 2 ) { // "table base shift_b shift_s ..." - jednowierszowa, wartości innej tablicy przesunięte // o stałą (tablice 0011-6/-7/-8 różnią się od 0011-1 tylko położeniem nominalnym) @@ -341,6 +476,7 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin if( words[ k ] == "diameter" ) { pulley.diameter = value; } else if( words[ k ] == "width" ) { pulley.width = value; } else if( words[ k ] == "grooves" ) { pulley.grooves = static_cast( value ); } + else if( words[ k ] == "sheave" ) { pulley.sheave = words[ k + 1 ]; } else { report( "pulley " + pulley.name + ": unknown keyword \"" + words[ k ] + "\"" ); } } m_pulleys.push_back( pulley ); @@ -377,9 +513,17 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin if( key == "table" ) { device->table = value; } else if( key == "pulley" ) { device->pulley = value; } else if( key == "lever" ) { device->lever = value; } + else if( key == "lever2" ) { device->lever2 = value; } else if( key == "rod" ) { device->rod = value; } + else if( key == "kind" ) { + if( value == "fixed" ) { device->fixed = true; } + else if( value == "split" ) { device->split = true; } + else if( value != "weights" ) { report( "device " + device->name + ": unknown kind \"" + value + "\"" ); } + } else if( key == "rope" ) { device->rope = value; } else if( key == "rope_diameter" ) { number_of( value, device->rope_diameter ); } + else if( key == "rope_eye" && k + 2 < words.size() ) { device->rope_eye = value; number_of( words[ k + 2 ], device->rope_eye_length ); ++k; } + else if( key == "rope_spacer" && k + 2 < words.size() ) { device->rope_spacer = value; number_of( words[ k + 2 ], device->rope_spacer_length ); ++k; } else { report( "device " + device->name + ": unknown keyword \"" + key + "\"" ); } } continue; @@ -402,27 +546,210 @@ tensioner_pulley const *tensioner_catalog::pulley( std::string const &Name ) con for( auto const &entry : m_pulleys ) { if( entry.name == Name ) { return &entry; } } return nullptr; } +tensioner_device::bracket_set const *tensioner_device::brackets_for( std::string const &Pole ) const { + bracket_set const *best { nullptr }; + std::size_t length { 0 }; + for( auto const &set : brackets ) { + for( auto const &pole : set.poles ) { + if( Pole.rfind( pole, 0 ) == 0 && ( best == nullptr || pole.size() > length ) ) { best = &set; length = pole.size(); } + } + } + return best; +} + tensioner_lever const *tensioner_catalog::lever( std::string const &Name ) const { for( auto const &entry : m_levers ) { if( entry.name == Name ) { return &entry; } } return nullptr; } +int tensioner_catalog::code_for( std::string const &Symbol, int const Code ) const { + auto const found { m_networks.find( Symbol ) }; + return ( found != m_networks.end() ? found->second : Code ); +} + tensioner_device const *tensioner_catalog::device( std::string const &Name ) const { for( auto const &entry : m_devices ) { if( entry.name == Name ) { return &entry; } } return nullptr; } +namespace { + +// kotwienie stałe: przegubowy uchwyt (4430) w parze uchwytów konstrukcji na wysokości liny i drugi na wysokości przewodów, +// łańcuchy do drutów wycelowane w ostatnie podwieszenie. Uchwyty w układzie konstrukcji (structure_angle) jak przy ciężarowym +bool solve_fixed( tensioner_device const &Device, tensioner_type const &Type, tensioner_input const &Input, tensioner_result &Result ) { + Result.fixed = true; + vec const out { 1.0, 0.0, 0.0 }; + auto const ca { std::cos( Input.structure_angle ) }, sa { std::sin( Input.structure_angle ) }; + auto const onstructure = [ & ]( vec const &P ) { return vec { ca * P.x - sa * P.y, sa * P.x + ca * P.y, P.z }; }; + auto const fix = [ & ]( std::size_t const From ) { + if( Input.structure_angle == 0.0 ) { return; } + for( auto k { From }; k < Result.parts.size(); ++k ) { + auto &part { Result.parts[ k ] }; + auto const p { onstructure( vec { part.position[ 0 ], part.position[ 1 ], part.position[ 2 ] } ) }; + part.position[ 0 ] = p.x; part.position[ 1 ] = p.y; part.position[ 2 ] = p.z; + for( int c = 0; c < 3; ++c ) { + auto const q { onstructure( vec { part.rotation[ c ], part.rotation[ 3 + c ], part.rotation[ 6 + c ] } ) }; + part.rotation[ c ] = q.x; part.rotation[ 3 + c ] = q.y; part.rotation[ 6 + c ] = q.z; + } + } + }; + auto const *brackets { Input.pole.empty() ? nullptr : Device.brackets_for( Input.pole ) }; + auto const pivotx { brackets != nullptr ? brackets->hole_out : 0.0 }; + // wysokości: przewody - podana albo ostatniego podwieszenia; lina - ostatniego podwieszenia albo 1,70 wyżej + auto const contactheight { Input.height > 0.0 ? Input.height : Input.aimed ? Input.contact_target[ 2 ] : 5.60 }; + auto const messengerheight { Input.rope_height > 0.0 ? Input.rope_height : Input.aimed ? Input.messenger_target[ 2 ] + ( Input.height > 0.0 ? Input.height - Input.contact_target[ 2 ] : 0.0 ) : contactheight + 1.70 }; + Result.rope_height = messengerheight; + Result.height = contactheight; + bool warned { false }; + auto const aimed = [ & ]( vec const &From, double const *Target ) { + if( false == Input.aimed ) { return out; } + vec const to { Target[ 0 ] - From.x, Target[ 1 ] - From.y, Target[ 2 ] - From.z }; + if( to.x < 1.0 ) { + if( false == warned ) { Result.warnings.push_back( "the last suspension is behind or too close to the anchor, the wires are pulled level" ); warned = true; } + return out; + } + return unit( to ); + }; + // para uchwytów wokół przegubu na wysokości Height (górny wprost, dolny odwrócony), z tyłem po drugiej stronie konstrukcji + auto const pair = [ & ]( std::string const &Role, double const Height ) { + if( brackets == nullptr ) { return; } + auto const from { Result.parts.size() }; + for( auto const upside : { false, true } ) { + vec const at { 0.0, 0.0, upside ? Height - Device.pivot_bottom - brackets->hole_drop : Height + Device.pivot_top + brackets->hole_drop }; + auto const name { Role + ( upside ? "_bottom" : "_top" ) }; + if( false == brackets->top_front.empty() ) { + auto placement { placed( name, brackets->top_front, at ) }; + if( upside ) { set_rotation( placement, vec { -1.0, 0.0, 0.0 }, vec { 0.0, 1.0, 0.0 }, vec { 0.0, 0.0, -1.0 } ); } + else { set_rotation( placement, vec { -1.0, 0.0, 0.0 }, vec { 0.0, -1.0, 0.0 }, vec { 0.0, 0.0, 1.0 } ); } + Result.parts.push_back( placement ); + } + auto const depth { Input.depth ? Input.depth( at.z ) : 0.0 }; + if( false == brackets->top_back.empty() && depth > 0.0 ) { + auto placement { placed( name + ".back", brackets->top_back, vec { -depth, 0.0, at.z } ) }; + if( upside ) { set_rotation( placement, vec { 1.0, 0.0, 0.0 }, vec { 0.0, -1.0, 0.0 }, vec { 0.0, 0.0, -1.0 } ); } + Result.parts.push_back( placement ); + std::string const text { "the bracket bolts are shorter than the structure is deep" }; + if( depth > brackets->bolt + 1e-6 && std::find( Result.warnings.begin(), Result.warnings.end(), text ) == Result.warnings.end() ) { Result.warnings.push_back( text ); } + } + } + fix( from ); + }; + // łańcuch od przegubu do drutów: przegub obraca się za drutami, za wyrównywaczem każdy drut osobno + auto const wires = [ & ]( std::string const &Role, double const Height, double const *Target, std::vector const &Links, + tensioner_link const &Equalizer, int const Count, double const Spread, std::vector const &Each, + tensioner_link const &Clamp, bool const Vertical, std::vector &Ends ) { + pair( "bracket_" + Role, Height ); + auto const start { onstructure( vec { pivotx, 0.0, Height } ) }; + auto const way { aimed( start, Target ) }; + std::vector chain { Device.mount.empty() ? std::vector {} : std::vector { Device.mount.front() } }; + chain.insert( chain.end(), Links.begin(), Links.end() ); + auto end { place_chain( chain, Role, start, way, Result ) }; + std::vector starts; + if( Count >= 2 && false == Equalizer.name.empty() ) { + // przewody obok siebie, liny jedna nad drugą (wyrównywacz obrócony o 90 stopni wokół łańcucha) + vec const up { -way.x * way.z, -way.y * way.z, way.x * way.x + way.y * way.y }; + auto const across { Vertical ? unit( up ) : unit( vec { -way.y, way.x, 0.0 } ) }; + auto placement { placed_along( Role + "_equalizer", Equalizer.name, end, way ) }; + if( Vertical ) { set_rotation( placement, way * -1.0, across, cross( way * -1.0, across ) ); } + Result.parts.push_back( placement ); + end = end + way * Equalizer.length; + for( int k = 0; k < Count; ++k ) { starts.push_back( end + across * ( ( k - 0.5 * ( Count - 1 ) ) * Spread ) ); } + } + else { starts.push_back( end ); } + for( std::size_t k = 0; k < starts.size(); ++k ) { + auto const tag { Role + "_each." + std::to_string( k + 1 ) }; + // dwie liny nośne: górna celuje w linę przęsła, dolna o odstęp lin niżej + auto own { way }; + if( Vertical && starts.size() == 2 && Input.aimed ) { + double const lower[ 3 ] { Target[ 0 ], Target[ 1 ], Target[ 2 ] - ( k == 0 ? Input.messenger_stack : 0.0 ) }; + own = aimed( starts[ k ], lower ); + } + auto at { place_chain( Each, tag, starts[ k ], own, Result ) }; + if( false == Clamp.name.empty() ) { + Result.parts.push_back( placed_along( Role + "_clamp." + std::to_string( k + 1 ), Clamp.name, at, own ) ); + at = at + own * Clamp.length; + } + Ends.push_back( at ); + } + }; + std::vector ropes, contacts; + auto messengerclamp { Device.messenger_clamp }; + if( false == Type.messenger_clamp.empty() ) { messengerclamp.name = Type.messenger_clamp; } + // kotwienie samych przewodów (only contact, 0404): bez łańcucha liny + if( Device.only != "contact" ) { + wires( "messenger", messengerheight, Input.messenger_target, Device.messenger, Device.messenger_equalizer, Input.messengers, Device.equalizer_spread, + Device.messenger_each, messengerclamp, true, ropes ); + } + auto const &clamp { Input.old_fittings && false == Device.contact_clamp_old.name.empty() ? Device.contact_clamp_old : Device.contact_clamp }; + // kotwienie środkowe: tylko lina kotwienia (only messenger) + if( Device.only != "messenger" ) { + wires( "contact", contactheight, Input.contact_target, Device.contact, Device.equalizer, Input.contacts, Device.equalizer_spread, Device.contact_each, clamp, false, contacts ); + } + auto const copy = []( vec const &P, double *Out ) { Out[ 0 ] = P.x; Out[ 1 ] = P.y; Out[ 2 ] = P.z; }; + // lina nośna przęsła to górna (dolna o odstęp lin niżej, jak w typie sieci) + if( false == ropes.empty() ) { copy( ropes.back(), Result.messenger_end ); } + if( ropes.size() >= 2 ) { copy( ropes.front(), Result.messenger2_end ); } + for( auto const &end : contacts ) { Result.contact_ends.push_back( { end.x, end.y, end.z } ); } + // obrys dla edytora: od czoła do końca przegubu, na wysokości przewodów + Result.b = pivotx + ( Device.mount.empty() ? 0.0 : Device.mount.front().length ); + Result.s = contactheight; + return true; +} + +} // namespace + bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const &Device, tensioner_input const &Input, tensioner_result &Result ) { Result = tensioner_result(); auto const fail = [ & ]( std::string const &Text ) { Result.warnings.push_back( Text ); return false; }; auto const *type { Device.type( Input.type ) }; if( type == nullptr ) { return fail( "device " + Device.name + " has no data for network type " + std::to_string( Input.type ) ); } + if( Device.fixed ) { return solve_fixed( Device, *type, Input, Result ); } + if( Device.split ) { + // kotwienie rozdzielone: urządzenie liny i urządzenie przewodów (albo ich odmiana dla typu sieci: -) + auto const pick = [ & ]( std::string const &Name ) -> tensioner_device const * { + if( auto const *exact { Catalog.device( Name ) }; exact != nullptr && exact->type( Input.type ) != nullptr ) { return exact; } + for( auto const &other : Catalog.devices() ) { + if( other.name.rfind( Name + "-", 0 ) == 0 && other.type( Input.type ) != nullptr ) { return &other; } + } + return nullptr; + }; + auto const *messengerdevice { pick( Device.split_messenger ) }, *contactdevice { pick( Device.split_contact ) }; + if( messengerdevice == nullptr || contactdevice == nullptr ) { return fail( "device " + Device.name + " has no messenger / contact device for network type " + std::to_string( Input.type ) ); } + tensioner_result rope, wires; + auto contactinput { Input }; + contactinput.missing = 0; // brakujące ciężary - ze stosu liny + if( Input.height > 0.0 ) { + // wysokość podana dla rolek liny; przewody o tyle niżej, co w tablicach + auto const *upper { Catalog.table( messengerdevice->table ) }, *lower { Catalog.table( contactdevice->table ) }; + if( upper != nullptr && lower != nullptr ) { contactinput.height = Input.height - ( upper->height - lower->height ); } + } + if( false == solve_tensioner( Catalog, *messengerdevice, Input, rope ) ) { Result = rope; return false; } + if( false == solve_tensioner( Catalog, *contactdevice, contactinput, wires ) ) { Result = wires; return false; } + Result = rope; + for( auto part : wires.parts ) { part.role = "contact_device." + part.role; Result.parts.push_back( part ); } + Result.rope2 = wires.rope; + Result.contact_ends = wires.contact_ends; + Result.contact_tension = wires.contact_tension; + Result.tension = rope.tension + wires.tension; + Result.nominal_tension = rope.nominal_tension + wires.nominal_tension; + Result.weights = rope.weights + wires.weights; + Result.grounded = rope.grounded || wires.grounded; + for( auto const &warning : wires.warnings ) { + if( std::find( Result.warnings.begin(), Result.warnings.end(), warning ) == Result.warnings.end() ) { Result.warnings.push_back( warning ); } + } + return true; + } auto const *table { Catalog.table( Device.table ) }; if( table == nullptr ) { return fail( "no table " + Device.table ); } auto const *pulley { Catalog.pulley( Device.pulley ) }; if( pulley == nullptr ) { return fail( "no pulley " + Device.pulley ); } - auto const *lever { Catalog.lever( Device.lever ) }; - if( lever == nullptr ) { return fail( "no lever " + Device.lever ); } + // urządzenie tylko liny albo tylko przewodów (kotwienie rozdzielone): bez dźwigni, wszystkie "otwory" w osi na końcu uchwytu + auto const only { false == Device.only.empty() }; + auto const onlymessenger { Device.only == "messenger" }; + static std::pair const nowhere { 0.0, 0.0 }; + auto const *lever { only ? nullptr : Catalog.lever( Device.lever ) }; + if( false == only && lever == nullptr ) { return fail( "no lever " + Device.lever ); } auto const hole = [ & ]( std::string const &Name ) -> std::pair const * { + if( only ) { return &nowhere; } auto const found { lever->holes.find( Name ) }; return found == lever->holes.end() ? nullptr : &found->second; }; @@ -432,6 +759,23 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & if( messengerhole == nullptr || contacthole == nullptr || axishole == nullptr ) { return fail( "lever " + lever->name + " has no hole " + type->messenger_hole + "/" + type->contact_hole + "/" + type->axis_hole ); } + // druga dźwignia (dźwignie szeregowo): lever2 urządzenia albo ta sama część co pierwsza + auto const cascade { false == only && false == type->second_axis_hole.empty() }; + auto const *secondlever { cascade ? Catalog.lever( Device.lever2.empty() ? Device.lever : Device.lever2 ) : nullptr }; + if( cascade && secondlever == nullptr ) { return fail( "no lever " + Device.lever2 ); } + auto const secondholeof = [ & ]( std::string const &Name ) -> std::pair const * { + auto const found { secondlever->holes.find( Name ) }; + return found == secondlever->holes.end() ? nullptr : &found->second; + }; + auto const *secondmessenger { cascade ? secondholeof( type->second_messenger_hole ) : nullptr }; + auto const *secondcontact { cascade ? secondholeof( type->second_contact_hole ) : nullptr }; + auto const *secondaxis { cascade ? secondholeof( type->second_axis_hole ) : nullptr }; + if( cascade && ( secondmessenger == nullptr || secondcontact == nullptr || secondaxis == nullptr ) ) { + return fail( "lever " + secondlever->name + " has no hole " + type->second_messenger_hole + "/" + type->second_contact_hole + "/" + type->second_axis_hole ); + } + if( cascade && ( Device.cascade.empty() || Device.messenger2.empty() ) ) { return fail( "device " + Device.name + " has no cascade/messenger2 chain for the second lever" ); } + Result.cascade = cascade; + auto const series { false == Device.small_pulley.empty() }; auto const rodname { type->rod.empty() ? Device.rod : type->rod }; auto const rodlength { [ & ] { auto const found { Catalog.part_lengths().find( rodname ) }; return found != Catalog.part_lengths().end() ? found->second : table->rod; }() }; @@ -444,6 +788,9 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & s -= 4.0 * Input.elongation; // pręt innej długości niż w tablicy: spód ciężarów niżej o różnicę (uwaga 3 na karcie 0011-1), góra pręta na tej samej linie s -= rodlength - table->rod; + // urządzenie wyżej albo niżej na konstrukcji niż w tablicy: lina ta sama, ciężary przesunięte o tyle samo + auto const height { Input.height > 0.0 ? Input.height : table->height }; + s += height - table->height; // stos ciężarów od dołu, brakujące z góry std::vector stack; @@ -452,12 +799,63 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & if( weight == nullptr ) { return fail( "no weight " + name ); } for( int k = 0; k < count; ++k ) { stack.push_back( weight ); } } + double fullmass { 0.0 }; + for( auto const *weight : stack ) { fullmass += weight->mass; } for( int k = 0; k < Input.missing && false == stack.empty(); ++k ) { stack.pop_back(); } double mass { 0.0 }, stackheight { 0.0 }; for( auto const *weight : stack ) { mass += weight->mass; stackheight += weight->height; } Result.weights = static_cast( stack.size() ); Result.stack_height = stackheight; Result.tension = 4.0 * mass * Input.gravity; + Result.nominal_tension = 4.0 * fullmass * Input.gravity; + + // podział naciągu dźwignią: momenty względem osi; część naciągu na otworze liny + auto const share = [ & ]( std::pair const &Messenger, std::pair const &Contact, std::pair const &Axis ) { + auto const arm_m { std::abs( Messenger.first - Axis.first ) }, arm_c { std::abs( Contact.first - Axis.first ) }; + return arm_m + arm_c > 1e-9 ? arm_c / ( arm_m + arm_c ) : 0.5; + }; + auto const messengershare { only ? ( onlymessenger ? 1.0 : 0.0 ) : share( *messengerhole, *contacthole, *axishole ) }; + auto const secondshare { cascade ? share( *secondmessenger, *secondcontact, *secondaxis ) : 0.0 }; + // części naciągu całej sieci: pierwsza lina, druga lina (szeregowo), przewody. Przesunięcia końców drutów przenoszą się na oś + // pierwszej dźwigni z tymi samymi wagami (dźwignia sztywna: oś = część·przesunięcie liny + reszta·przesunięcie drugiej strony) + auto const first { messengershare }; + auto const second { cascade ? ( 1.0 - messengershare ) * secondshare : 0.0 }; + auto const wires { 1.0 - first - second }; + auto const ropeshare { first + second }; + + // mniejszy naciąg niż w tablicy: sieć krótsza o wydłużenie sprężyste od różnicy naciągów i o nadmiar długości zwisu. + // Wieszaki trzymają przewody pod liną, więc dodatkowy zwis jest wspólny i niesie go naciąg liny i przewodów razem: + // lina zwisa f0·(Hm0 + Hc)/(Hm + Hc), przewody (prosto przy pełnym naciągu) f0·(Hm0 − Hm)/(Hm + Hc), gdzie + // f0 = w·a²/(8·Hm0); nadmiar długości zwisu w przęśle 8·f²/(3·a), przęseł distance/a. Dwie liny szeregowo: każda ma połowę + // sztywności i swoją część naciągu + if( mass > 0.0 && Result.tension < Result.nominal_tension && ( Input.messenger_stiffness > 0.0 || Input.contact_stiffness > 0.0 ) ) { + auto const length { std::max( 0.0, Input.distance ) }; + auto const drop { Result.nominal_tension - Result.tension }; + auto const messenger0 { Result.nominal_tension * ropeshare }, messenger { Result.tension * ropeshare }; + auto const contact0 { Result.nominal_tension * wires }, contact { Result.tension * wires }; + double sagmessengershift { 0.0 }, sagcontactshift { 0.0 }; + if( Input.load > 0.0 && Input.span > 0.0 && messenger0 > 0.0 && messenger + contact > 0.0 ) { + auto const sag0 { Input.load * Input.span * Input.span / ( 8.0 * messenger0 ) }; + auto const sagmessenger { sag0 * ( messenger0 + contact ) / ( messenger + contact ) }; + auto const sagcontact { sag0 * ( messenger0 - messenger ) / ( messenger + contact ) }; + auto const spans { length / Input.span }; + sagmessengershift = spans * 8.0 / ( 3.0 * Input.span ) * ( sagmessenger * sagmessenger - sag0 * sag0 ); + sagcontactshift = spans * 8.0 / ( 3.0 * Input.span ) * sagcontact * sagcontact; + } + // przesunięcie końca jednej grupy drutów o części Part naciągu i sztywności Stiffness + auto const shift = [ & ]( double const Part, double const Stiffness, double const Sag ) { + return Part * ( ( Stiffness > 0.0 ? Part * drop * length / Stiffness : 0.0 ) + Sag ); + }; + if( cascade ) { + Result.shortening = shift( first, 0.5 * Input.messenger_stiffness, sagmessengershift ) + shift( second, 0.5 * Input.messenger_stiffness, sagmessengershift ); + } + else { + Result.shortening = shift( first, Input.messenger_stiffness, sagmessengershift ); + } + Result.shortening += shift( wires, Input.contact_stiffness, sagcontactshift ); + b += Result.shortening; + s += 4.0 * Result.shortening; + } // ciężary nie zejdą niżej niż grunt: lina nie puści rolki bliżej konstrukcji, sieć traci naciąg if( s < Input.ground ) { @@ -470,61 +868,324 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & Result.b = b; Result.s = s; - auto const height { table->height }; + Result.height = height; vec const out { 1.0, 0.0, 0.0 }, down { 0.0, 0.0, -1.0 }; // rolka stała na uchwytach od czoła konstrukcji, rolka ruchoma w b - auto const fixedaxle { place_chain( Device.mount, "mount", vec { 0.0, 0.0, height }, out, Result ) }; - vec const movingaxle { b, 0.0, height }; - auto const radius { 0.5 * pulley->diameter + 0.5 * Device.rope_diameter }; - if( movingaxle.x - fixedaxle.x < 2.0 * radius ) { Result.warnings.push_back( "the pulleys overlap: the network pulled the moving one to the structure" ); } - { - auto fixed { placed( "pulley_fixed", pulley->name, fixedaxle ) }; - Result.parts.push_back( fixed ); - Result.parts.push_back( placed( "pulley_moving", pulley->name, movingaxle ) ); + // uchwyty do konstrukcji: 4450 zaczyna się w otworach uchwytów, hole_out przed czołem + auto const *brackets { Input.pole.empty() ? nullptr : Device.brackets_for( Input.pole ) }; + auto const pivotx { brackets != nullptr ? brackets->hole_out : 0.0 }; + // układ konstrukcji: obrócony o structure_angle wokół pionu przez zero (czoło); części konstrukcji liczone w nim, potem tu + auto const ca { std::cos( Input.structure_angle ) }, sa { std::sin( Input.structure_angle ) }; + auto const onstructure = [ & ]( vec const &P ) { return vec { ca * P.x - sa * P.y, sa * P.x + ca * P.y, P.z }; }; + auto const fix = [ & ]( std::size_t const From ) { + if( Input.structure_angle == 0.0 ) { return; } + for( auto k { From }; k < Result.parts.size(); ++k ) { + auto &part { Result.parts[ k ] }; + auto const at { onstructure( vec { part.position[ 0 ], part.position[ 1 ], part.position[ 2 ] } ) }; + part.position[ 0 ] = at.x; part.position[ 1 ] = at.y; part.position[ 2 ] = at.z; + for( int column = 0; column < 3; ++column ) { + auto const axis { onstructure( vec { part.rotation[ column ], part.rotation[ 3 + column ], part.rotation[ 6 + column ] } ) }; + part.rotation[ column ] = axis.x; part.rotation[ 3 + column ] = axis.y; part.rotation[ 6 + column ] = axis.z; + } + } + }; + // przód: -X modelu przed czoło (obrót o 180 stopni wokół pionu); Upside: do góry nogami (otwór nad zerem) + auto const front = [ & ]( std::string const &Role, std::string const &Part, vec const &At, bool const Upside ) { + if( Part.empty() ) { return; } + auto placement { placed( Role, Part, At ) }; + if( Upside ) { set_rotation( placement, vec { -1.0, 0.0, 0.0 }, vec { 0.0, 1.0, 0.0 }, vec { 0.0, 0.0, -1.0 } ); } + else { set_rotation( placement, vec { -1.0, 0.0, 0.0 }, vec { 0.0, -1.0, 0.0 }, vec { 0.0, 0.0, 1.0 } ); } + Result.parts.push_back( placement ); + }; + // tył: na tylnej ścianie konstrukcji, -X modelu na zewnątrz (w -X urządzenia) + auto const back = [ & ]( std::string const &Role, std::string const &Part, vec const &At, bool const Upside ) { + if( Part.empty() || brackets == nullptr ) { return; } + auto const depth { Input.depth ? Input.depth( At.z ) : 0.0 }; + if( depth <= 0.0 ) { return; } + auto placement { placed( Role, Part, vec { -depth, At.y, At.z } ) }; + if( Upside ) { set_rotation( placement, vec { 1.0, 0.0, 0.0 }, vec { 0.0, -1.0, 0.0 }, vec { 0.0, 0.0, -1.0 } ); } + Result.parts.push_back( placement ); + if( depth > brackets->bolt + 1e-6 ) { + std::string const text { "the bracket bolts are shorter than the structure is deep" }; + if( std::find( Result.warnings.begin(), Result.warnings.end(), text ) == Result.warnings.end() ) { Result.warnings.push_back( text ); } + } + }; + auto const bracketsfrom { Result.parts.size() }; + if( brackets != nullptr ) { + // dwa uchwyty wokół 4450: górny za mount_top, dolny odwrócony za mount_bottom + vec const top { 0.0, 0.0, height - Device.mount_rise + Device.pivot_top + brackets->hole_drop }; + vec const bottom { 0.0, 0.0, height - Device.mount_rise - Device.pivot_bottom - brackets->hole_drop }; + front( "bracket_top", brackets->top_front, top, false ); + back( "bracket_top.back", brackets->top_back, top, false ); + front( "bracket_bottom", brackets->top_front, bottom, true ); + back( "bracket_bottom.back", brackets->top_back, bottom, true ); } - auto const axispoint { place_chain( Device.holder, "holder", movingaxle, out, Result ) }; - - // dźwignia: otwór osi na axispoint; przy otworze liny niżej niż otwór przewodów odwrócona + fix( bracketsfrom ); + // przegub 4450 w otworach uchwytów (przy konstrukcji), dalej urządzenie w stronę drutów + // uchwyt z otworem rolki nad zerem (5040) zaczyna się niżej, rolka zostaje na wysokości z tablicy + auto const fixedaxle { place_chain( Device.mount, "mount", onstructure( vec { pivotx, 0.0, height - Device.mount_rise } ), out, Result ) + vec { 0.0, 0.0, Device.mount_rise } }; + // rolki szeregowe: b to oś dużej ruchomej (skrajnej), uchwyt dźwigni zaczyna się na małej ruchomej + auto const *smallpulley { series ? Catalog.pulley( Device.small_pulley ) : nullptr }; + if( series && smallpulley == nullptr ) { return fail( "no pulley " + Device.small_pulley ); } + vec const movingaxle { series ? b - Device.small_spacing : b, 0.0, height }; + vec const movingbig { b, 0.0, height }; + auto const fixedsmall { fixedaxle + out * Device.small_spacing }; + auto const radius { 0.5 * pulley->diameter + 0.5 * Device.rope_diameter }; + auto const smallradius { series ? 0.5 * smallpulley->diameter + 0.5 * Device.rope_diameter : radius }; + if( series ? movingaxle.x - fixedsmall.x < 2.0 * smallradius : movingaxle.x - fixedaxle.x < 2.0 * radius ) { + Result.warnings.push_back( "the pulleys overlap: the network pulled the moving one to the structure" ); + } + if( series ) { + double b0 { b }, s0 { 0.0 }; + table->lookup( 10.0, Input.distance, b0, s0 ); + auto const travel { b - b0 }; + // lina przechodzi: mała ruchoma d, mała stała 2d, duża ruchoma 3d, duża stała 4d + Result.turn_moving_a = travel / smallradius; + Result.turn_fixed_a = 2.0 * travel / smallradius; + Result.turn_moving_b = 3.0 * travel / radius; + Result.turn_fixed_b = 4.0 * travel / radius; + auto const spun = [ & ]( std::string const &Role, std::string const &Part, vec const &Axle, double const Angle ) { + auto placement { placed( Role, Part, Axle ) }; + auto const c { std::cos( Angle ) }, s { std::sin( Angle ) }; + set_rotation( placement, vec { c, 0.0, -s }, vec { 0.0, 1.0, 0.0 }, vec { s, 0.0, c } ); + Result.parts.push_back( placement ); + }; + spun( "pulley_fixed", pulley->name, fixedaxle, Result.turn_fixed_b ); + spun( "pulley_fixed_small", smallpulley->name, fixedsmall, Result.turn_fixed_a ); + spun( "pulley_moving", smallpulley->name, movingaxle, Result.turn_moving_a ); + spun( "pulley_moving_big", pulley->name, movingbig, Result.turn_moving_b ); + if( false == Device.fixed_holder.empty() ) { Result.parts.push_back( placed_along( "holder_fixed", Device.fixed_holder, fixedaxle, out ) ); } + } + else { + // krążki obracają się o linę przewiniętą od położenia przy +10 °C (bez wydłużenia). Przy przesunięciu rolki ruchomej + // o d lina przechodzi kolejno: ruchoma A d, stała A 2d, ruchoma B 3d, stała B 4d (tyle jadą ciężary); wszystkie + // krążki obracają się w tę samą stronę: przy rolce oddalającej się od konstrukcji lina cofa się w stronę zaczepu, + // co w płaszczyźnie X-Z jest obrotem wokół +Y (Z w stronę X) + double b0 { b }, s0 { 0.0 }; + table->lookup( 10.0, Input.distance, b0, s0 ); + auto const travel { b - b0 }; + auto const turn = [ & ]( double const Times ) { return Times * travel / radius; }; + auto const twin { pulley->grooves >= 2 && false == pulley->sheave.empty() }; + Result.turn_moving_a = turn( 1.0 ); + Result.turn_fixed_a = turn( 2.0 ); + Result.turn_moving_b = turn( pulley->grooves >= 2 ? 3.0 : 1.0 ); + Result.turn_fixed_b = turn( pulley->grooves >= 2 ? 4.0 : 2.0 ); + auto const spun = [ & ]( std::string const &Role, std::string const &Part, vec const &Axle, double const Angle ) { + auto placement { placed( Role, Part, Axle ) }; + auto const c { std::cos( Angle ) }, s { std::sin( Angle ) }; + set_rotation( placement, vec { c, 0.0, -s }, vec { 0.0, 1.0, 0.0 }, vec { s, 0.0, c } ); + Result.parts.push_back( placement ); + }; + spun( "pulley_fixed", pulley->name, fixedaxle, Result.turn_fixed_a ); + spun( "pulley_moving", pulley->name, movingaxle, Result.turn_moving_a ); + if( twin ) { + spun( "sheave_fixed", pulley->sheave, fixedaxle, Result.turn_fixed_b ); + spun( "sheave_moving", pulley->sheave, movingaxle, Result.turn_moving_b ); + } + } + // dźwignia: otwór osi na końcu uchwytu od rolki ruchomej; przy otworze liny niżej niż otwór przewodów odwrócona. + // Dźwignia na sworzniu staje prostopadle do wypadkowej sił z obu łańcuchów, a uchwyt 4440 idzie wzdłuż tej + // wypadkowej (pochylenie tylko w płaszczyźnie X-Z urządzenia; łańcuchy celują dalej w 3D). Otwory zależą od + // pochylenia, a pochylenie od kierunków z otworów, więc kilka przybliżeń (cel daleko - zbiega się od razu) auto const flipped { messengerhole->first < contacthole->first }; auto const sign { flipped ? -1.0 : 1.0 }; - auto const holepoint = [ & ]( std::pair const &Hole ) { - return axispoint + vec { sign * ( Hole.second - axishole->second ), 0.0, sign * ( Hole.first - axishole->first ) }; + Result.messenger_tensions[ 0 ] = Result.tension * first; + Result.messenger_tensions[ 1 ] = Result.tension * second; + Result.messenger_tension = Result.tension * ropeshare; + Result.contact_tension = Result.tension - Result.messenger_tension; + double holderlength { 0.0 }; + for( auto const &link : Device.holder ) { holderlength += link.length; } + // kierunek łańcucha do ostatniego podwieszenia albo poziomo; Short: cel za blisko albo za konstrukcją + auto const aim = [ & ]( vec const &From, double const *Target, bool &Short ) { + Short = false; + if( false == Input.aimed ) { return out; } + vec const to { Target[ 0 ] - From.x, Target[ 1 ] - From.y, Target[ 2 ] - From.z }; + if( to.x < 1.0 ) { Short = true; return out; } + return unit( to ); }; - { - auto placement { placed( "lever", lever->name, holepoint( { 0.0, 0.0 } ) ) }; - set_rotation( placement, vec { sign, 0.0, 0.0 }, vec { 0.0, 1.0, 0.0 }, vec { 0.0, 0.0, sign } ); - Result.parts.push_back( placement ); + vec pull { out }; // kierunek uchwytu i wypadkowej, w płaszczyźnie X-Z + vec axispoint, across, upward; // oś dźwigni i jej osie: across = wzdłuż pull, upward = wzdłuż dźwigni + auto const lever_frame = [ & ]() { + axispoint = movingaxle + pull * holderlength; + across = pull; + upward = vec { -pull.z, 0.0, pull.x }; + }; + auto const holepoint = [ & ]( std::pair const &Hole ) { + return axispoint + across * ( sign * ( Hole.second - axishole->second ) ) + upward * ( sign * ( Hole.first - axishole->first ) ); + }; + lever_frame(); + // druga dźwignia (szeregowo): oś na końcu łańcucha "cascade" z otworu przewodów pierwszej, wzdłuż wypadkowej drugiej + auto const secondsign { cascade && secondmessenger->first < secondcontact->first ? -1.0 : 1.0 }; + double cascadelength { 0.0 }; + for( auto const &link : Device.cascade ) { cascadelength += link.length; } + vec secondpull { out }, secondaxispoint; + auto const secondhole = [ & ]( std::pair const &Hole ) { + vec const up { -secondpull.z, 0.0, secondpull.x }; + return secondaxispoint + secondpull * ( secondsign * ( Hole.second - secondaxis->second ) ) + up * ( secondsign * ( Hole.first - secondaxis->first ) ); + }; + auto const second_frame = [ & ]() { if( cascade ) { secondaxispoint = holepoint( *contacthole ) + secondpull * cascadelength; } }; + second_frame(); + // wypadkowa w płaszczyźnie X-Z; dźwignia nie przechyla się dalej niż do 45 stopni (dziwny cel - zostaje bliżej pionu) + auto const flat_pull = []( vec const &Resultant, vec &Pull ) { + vec const flat { Resultant.x, 0.0, Resultant.z }; + if( length( flat ) < 1e-9 || flat.x < std::abs( flat.z ) ) { return false; } + Pull = unit( flat ); + return true; + }; + if( Input.aimed ) { + for( int pass = 0; pass < 6; ++pass ) { + bool shortm, shortc; + auto const previous { pull }; + auto const wm { aim( holepoint( *messengerhole ), Input.messenger_target, shortm ) }; + vec wc; + if( cascade ) { + // druga dźwignia ciągnie pierwszą wzdłuż swojej wypadkowej + auto const wm2 { aim( secondhole( *secondmessenger ), Input.messenger_target, shortm ) }; + auto const wc2 { aim( secondhole( *secondcontact ), Input.contact_target, shortc ) }; + flat_pull( wm2 * secondshare + wc2 * ( 1.0 - secondshare ), secondpull ); + wc = secondpull; + } + else { wc = aim( holepoint( *contacthole ), Input.contact_target, shortc ); } + if( false == flat_pull( wm * messengershare + wc * ( 1.0 - messengershare ), pull ) ) { break; } + lever_frame(); + second_frame(); + if( length( pull - previous ) < 1e-6 && pass > 0 ) { break; } + } } - // podział naciągu dźwignią: momenty względem osi - auto const armmessenger { std::abs( messengerhole->first - axishole->first ) }; - auto const armcontact { std::abs( contacthole->first - axishole->first ) }; - if( armmessenger + armcontact > 1e-9 ) { - Result.messenger_tension = Result.tension * armcontact / ( armmessenger + armcontact ); - Result.contact_tension = Result.tension - Result.messenger_tension; + Result.lever_tilt = std::atan2( -pull.z, pull.x ); + { + auto const from { Result.parts.size() }; + place_chain( Device.holder, "holder", movingaxle, pull, Result ); + // rolki szeregowe: uchwyt 4440-5 odwrócony tyłem na przód względem uchwytu rolki stałej (Jan 2026-10-25) - obrót o 180 + // stopni wokół jego długiej osi + if( series ) { + for( auto k { from }; k < Result.parts.size(); ++k ) { + auto &r { Result.parts[ k ].rotation }; + for( auto const column : { 1, 2 } ) { for( int row = 0; row < 3; ++row ) { r[ row * 3 + column ] = -r[ row * 3 + column ]; } } + } + } } - // łańcuchy do drutów - { - auto end { place_chain( Device.messenger, "messenger", holepoint( *messengerhole ), out, Result ) }; + // zabezpieczenie: ramiona z osi obu rolek do wspólnej dolnej śruby, para po obu stronach rolek + // zabezpieczenie rolek szeregowych: ramiona z osi małych rolek pochylone tak, żeby ich końce dzielił drążek + if( series && false == Device.series_arm_fixed.empty() ) { + auto const offset { 0.5 * ( ( movingaxle.x - fixedsmall.x ) - Device.series_bar_length ) }; + if( std::abs( offset ) < Device.series_arm ) { + auto const drop { std::sqrt( Device.series_arm * Device.series_arm - offset * offset ) }; + for( auto const side : { -1.0, 1.0 } ) { + vec const aside { 0.0, side * Device.safety_offset, 0.0 }; + auto const a { fixedsmall + aside }, b { movingaxle + aside }; + vec const lowa { a.x + offset, a.y, height - drop }, lowb { b.x - offset, b.y, height - drop }; + auto const role { std::string( side < 0.0 ? "safety_a" : "safety_b" ) }; + Result.parts.push_back( placed_along( role + ".fixed", Device.series_arm_fixed, a, lowa - a ) ); + Result.parts.push_back( placed_along( role + ".moving", Device.series_arm_moving, b, lowb - b ) ); + if( false == Device.series_bar.empty() ) { Result.parts.push_back( placed_along( role + ".bar", Device.series_bar, lowa, lowb - lowa ) ); } + } + } + else { Result.warnings.push_back( "the pulleys are further apart than the safety arms reach" ); } + } + if( false == series && false == Device.safety_fixed.empty() ) { + auto const half { 0.5 * ( movingaxle.x - fixedaxle.x ) }; + if( half < Device.safety_arm ) { + auto const drop { std::sqrt( Device.safety_arm * Device.safety_arm - half * half ) }; + for( auto const side : { -1.0, 1.0 } ) { + vec const offset { 0.0, side * Device.safety_offset, 0.0 }; + vec const bolt { fixedaxle.x + half, offset.y, height - drop }; + auto const role { std::string( side < 0.0 ? "safety_a" : "safety_b" ) }; + Result.parts.push_back( placed_along( role + ".fixed", Device.safety_fixed, fixedaxle + offset, bolt - ( fixedaxle + offset ) ) ); + Result.parts.push_back( placed_along( role + ".moving", Device.safety_moving, movingaxle + offset, bolt - ( movingaxle + offset ) ) ); + } + } + else { Result.warnings.push_back( "the pulleys are further apart than the safety arms reach" ); } + } + + // model dźwigni (Jan): zero w otworze osi II, otwory na drutach odsunięte w -X modelu, czyli w stronę drutów; + // -X modelu idzie więc w stronę drutów (obrót o 180 stopni wokół pionu), odwrócona dodatkowo o 180 wokół Y; + // przechylona razem z układem across/upward + if( false == only ) { + auto placement { placed( "lever", lever->name, holepoint( { 0.0, 0.0 } ) ) }; + set_rotation( placement, across * -sign, vec { 0.0, -1.0, 0.0 }, upward * sign ); + Result.parts.push_back( placement ); + } + if( cascade ) { + place_chain( Device.cascade, "cascade", holepoint( *contacthole ), secondpull, Result ); + auto placement { placed( "lever2", secondlever->name, secondhole( { 0.0, 0.0 } ) ) }; + set_rotation( placement, secondpull * -secondsign, vec { 0.0, -1.0, 0.0 }, vec { -secondpull.z, 0.0, secondpull.x } * secondsign ); + Result.parts.push_back( placement ); + } + + // łańcuchy do drutów: w stronę ostatniego podwieszenia (gdy podane), inaczej poziomo + bool warned { false }; + auto const aimed = [ & ]( vec const &From, double const *Target ) { + bool tooshort; + auto const way { aim( From, Target, tooshort ) }; + if( tooshort && false == warned ) { + Result.warnings.push_back( "the last suspension is behind or too close to the anchor, the wires are pulled level" ); + warned = true; + } + return way; + }; + // lina nośna z otworu dźwigni do uchwytu krańcowego; Role: messenger albo messenger2 + // druga (dolna) lina nośna celuje o odstęp lin niżej niż pierwsza + double const lowertarget[ 3 ] { Input.messenger_target[ 0 ], Input.messenger_target[ 1 ], Input.messenger_target[ 2 ] - Input.messenger_stack }; + auto const rope_chain = [ & ]( vec const &Start, std::vector const &Links, std::string const &Role, double *End, double const *Target ) { + auto const way { aimed( Start, Target ) }; + auto end { place_chain( Links, Role, Start, way, Result ) }; auto const clamp { type->messenger_clamp.empty() ? Device.messenger_clamp.name : type->messenger_clamp }; if( false == clamp.empty() ) { - Result.parts.push_back( placed_along( "messenger_clamp", clamp, end, out ) ); - end = end + out * Device.messenger_clamp.length; + Result.parts.push_back( placed_along( Role + "_clamp", clamp, end, way ) ); + end = end + way * Device.messenger_clamp.length; + } + End[ 0 ] = end.x; + End[ 1 ] = end.y; + End[ 2 ] = end.z; + }; + // izolatory rolkowe tylko w zwykłym złożeniu (szeregowo izolator jest w łańcuchu do drugiej dźwigni) + if( only && onlymessenger && Input.messengers >= 2 && false == Device.messenger_equalizer.name.empty() ) { + // dwie liny z jednego urządzenia (sieć 32: dźwignia równoramienna 6403, 35: wyrównywacz 4410): jedna nad drugą + auto const start { holepoint( *messengerhole ) }; + auto const way { aimed( start, Input.messenger_target ) }; + auto end { place_chain( Device.messenger, "messenger", start, way, Result ) }; + vec const up { -way.x * way.z, -way.y * way.z, way.x * way.x + way.y * way.y }; + auto const across { unit( up ) }; + auto placement { placed_along( "messenger_equalizer", Device.messenger_equalizer.name, end, way ) }; + if( false == Device.messenger_equalizer_z ) { set_rotation( placement, way * -1.0, across, cross( way * -1.0, across ) ); } + Result.parts.push_back( placement ); + end = end + way * Device.messenger_equalizer.length; + auto const clamp { type->messenger_clamp.empty() ? Device.messenger_clamp.name : type->messenger_clamp }; + for( int k = 0; k < 2; ++k ) { + auto const tag { "messenger_each." + std::to_string( k + 1 ) }; + // każda lina celuje w ostatnie podwieszenie osobno (rozstaw dźwigni 6403 zbiega się w przęśle) + auto const from { end + across * ( ( k - 0.5 ) * Device.messenger_spread ) }; + auto const own { aimed( from, k == 1 ? Input.messenger_target : lowertarget ) }; + auto at { place_chain( Device.messenger_each, tag, from, own, Result ) }; + if( false == clamp.empty() ) { + Result.parts.push_back( placed_along( "messenger_clamp." + std::to_string( k + 1 ), clamp, at, own ) ); + at = at + own * Device.messenger_clamp.length; + } + auto *out { k == 1 ? Result.messenger_end : Result.messenger2_end }; + out[ 0 ] = at.x; out[ 1 ] = at.y; out[ 2 ] = at.z; } - Result.messenger_end[ 0 ] = end.x; - Result.messenger_end[ 1 ] = end.y; - Result.messenger_end[ 2 ] = end.z; } - { - auto end { place_chain( Device.contact, "contact", holepoint( *contacthole ), out, Result ) }; + else if( false == only || onlymessenger ) { + rope_chain( holepoint( *messengerhole ), Input.old_messenger && false == cascade && false == Device.messenger_old.empty() ? Device.messenger_old : Device.messenger, "messenger", Result.messenger_end, Input.messenger_target ); + } + if( cascade ) { rope_chain( secondhole( *secondmessenger ), Device.messenger2, "messenger2", Result.messenger2_end, lowertarget ); } + if( false == only || false == onlymessenger ) { + auto const start { cascade ? secondhole( *secondcontact ) : holepoint( *contacthole ) }; + auto const way { aimed( start, Input.contact_target ) }; + auto const &links { Input.old_contact && false == cascade && false == Device.contact_old.empty() ? Device.contact_old : Device.contact }; + auto end { place_chain( links, "contact", start, way, Result ) }; std::vector ends; if( Input.contacts >= 2 && false == Device.equalizer.name.empty() ) { - Result.parts.push_back( placed_along( "equalizer", Device.equalizer.name, end, out ) ); - end = end + out * Device.equalizer.length; + Result.parts.push_back( placed_along( "equalizer", Device.equalizer.name, end, way ) ); + end = end + way * Device.equalizer.length; + // rozstaw przewodów poziomo, w poprzek kierunku łańcucha + auto const across { unit( vec { -way.y, way.x, 0.0 } ) }; for( int k = 0; k < Input.contacts; ++k ) { auto const offset { ( k - 0.5 * ( Input.contacts - 1 ) ) * Device.equalizer_spread }; - ends.push_back( end + vec { 0.0, offset, 0.0 } ); + ends.push_back( end + across * offset ); } } else { @@ -532,9 +1193,10 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & } for( std::size_t k = 0; k < ends.size(); ++k ) { auto at { ends[ k ] }; - if( false == Device.contact_clamp.name.empty() ) { - Result.parts.push_back( placed_along( "contact_clamp." + std::to_string( k + 1 ), Device.contact_clamp.name, at, out ) ); - at = at + out * Device.contact_clamp.length; + auto const &clamp { Input.old_fittings && false == Device.contact_clamp_old.name.empty() ? Device.contact_clamp_old : Device.contact_clamp }; + if( false == clamp.name.empty() ) { + Result.parts.push_back( placed_along( "contact_clamp." + std::to_string( k + 1 ), clamp.name, at, way ) ); + at = at + way * clamp.length; } Result.contact_ends.push_back( { at.x, at.y, at.z } ); } @@ -546,33 +1208,73 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & auto &rope { Result.rope }; auto const point = [ & ]( vec const &P ) { rope.push_back( { P.x, P.y, P.z } ); }; auto const arc = [ & ]( vec const &Center, double const Y, double const From, double const To ) { - // kąt w płaszczyźnie X-Z od +X w stronę +Z; punkty co najwyżej co 15 stopni, bez pierwszego - auto const steps { std::max( 1, static_cast( std::ceil( std::abs( To - From ) / ( kPi / 12.0 ) ) ) ) }; + // kąt w płaszczyźnie X-Z od +X w stronę +Z; punkty co najwyżej co 5 stopni (gładka rura liny), bez pierwszego + auto const steps { std::max( 1, static_cast( std::ceil( std::abs( To - From ) / ( kPi / 36.0 ) - 1e-9 ) ) ) }; for( int k = 1; k <= steps; ++k ) { auto const angle { From + ( To - From ) * k / steps }; point( vec { Center.x + radius * std::cos( angle ), Y, Center.z + radius * std::sin( angle ) } ); } }; - point( vec { fixedaxle.x, 0.0, height - radius } ); - point( vec { movingaxle.x, ya, height - radius } ); - arc( movingaxle, ya, -0.5 * kPi, 0.5 * kPi ); - point( vec { fixedaxle.x, ya, height + radius } ); - arc( fixedaxle, ya, 0.5 * kPi, 1.5 * kPi ); - point( vec { movingaxle.x, yb, height - radius } ); - arc( movingaxle, yb, -0.5 * kPi, 0.5 * kPi ); - point( vec { fixedaxle.x, yb, height + radius } ); - arc( fixedaxle, yb, 0.5 * kPi, kPi ); + if( series ) { + // zaczep na uchwycie stałym, ukośnie pod małą ruchomą, wokół niej w górę, górą do małej stałej, wokół niej w dół, + // dołem do dużej ruchomej, wokół niej w górę, górą do dużej stałej i z jej lewej strony w dół + auto const around = [ & ]( vec const &Center, double const Radius, double const From, double const To ) { + auto const steps { std::max( 1, static_cast( std::ceil( std::abs( To - From ) / ( kPi / 36.0 ) - 1e-9 ) ) ) }; + for( int k = 1; k <= steps; ++k ) { + auto const angle { From + ( To - From ) * k / steps }; + point( vec { Center.x + Radius * std::cos( angle ), 0.0, Center.z + Radius * std::sin( angle ) } ); + } + }; + point( vec { fixedaxle.x + Device.dead_end, 0.0, height } ); + point( vec { movingaxle.x, 0.0, height - smallradius } ); + around( movingaxle, smallradius, -0.5 * kPi, 0.5 * kPi ); + point( vec { fixedsmall.x, 0.0, height + smallradius } ); + around( fixedsmall, smallradius, 0.5 * kPi, 1.5 * kPi ); + point( vec { movingbig.x, 0.0, height - radius } ); + around( movingbig, radius, -0.5 * kPi, 0.5 * kPi ); + point( vec { fixedaxle.x, 0.0, height + radius } ); + around( fixedaxle, radius, 0.5 * kPi, kPi ); + } + else { + point( vec { fixedaxle.x, 0.0, height - radius } ); + point( vec { movingaxle.x, ya, height - radius } ); + arc( movingaxle, ya, -0.5 * kPi, 0.5 * kPi ); + point( vec { fixedaxle.x, ya, height + radius } ); + arc( fixedaxle, ya, 0.5 * kPi, 1.5 * kPi ); + point( vec { movingaxle.x, yb, height - radius } ); + arc( movingaxle, yb, -0.5 * kPi, 0.5 * kPi ); + point( vec { fixedaxle.x, yb, height + radius } ); + arc( fixedaxle, yb, 0.5 * kPi, kPi ); + } // pręt z ciężarami pod liną, łączniki między liną a prętem double hangerlength { 0.0 }; for( auto const &link : Device.hanger ) { hangerlength += link.length; } - auto const columnx { fixedaxle.x - radius }; - vec const ropeend { columnx, yb, s + rodlength + hangerlength }; + // oś stosu: pod rolką stałą; z prowadnicą na uchwytach ustawia ją pierścień jeżdżący po osi prowadnicy (mount 5070 w hole_1 + // uchwytu, Jan 2026-10-24) - lina schodzi z rolki lekko ukośnie + auto const turn { Device.ring_turn * kPi / 180.0 }; + vec toward { -std::cos( turn ), std::sin( turn ), 0.0 }; // od osi stosu do prowadnicy + auto const onbrackets { brackets != nullptr && false == Device.guide.empty() && false == Device.ring.empty() }; + auto const onframe { onbrackets && false == Device.frame.empty() }; + // na ramie 5090: prowadnica w otworze ramy, stos pod liną (Y 0) o ramię pierścienia od prowadnicy, przy konstrukcji albo dalej; + // pierścień obrócony w stronę prowadnicy (Jan 2026-10-24) + if( onframe ) { + auto const offset { std::max( 0.0, Device.ring_arm * Device.ring_arm - Device.frame_y * Device.frame_y ) }; + vec const guide { brackets->hole_out + Device.frame_length, Device.frame_y, 0.0 }; + vec const stack { guide.x + Device.frame_side * std::sqrt( offset ), 0.0, 0.0 }; + toward = unit( vec { guide.x - stack.x, guide.y - stack.y, 0.0 } ); + } + vec const guideaxis { onframe ? vec { brackets->hole_out + Device.frame_length, Device.frame_y, 0.0 } : onbrackets ? vec { brackets->hole_out, 0.0, 0.0 } - toward * Device.guide_mount : vec {} }; + // z prowadnicą stos liczony w układzie konstrukcji (fix na końcu), bez niej - pod rolką, w układzie urządzenia + auto const columnx { onbrackets ? guideaxis.x - toward.x * Device.ring_arm : fixedaxle.x - radius }; + auto const columny { onbrackets ? guideaxis.y - toward.y * Device.ring_arm : yb }; + vec const ropeend { columnx, columny, s + rodlength + hangerlength }; if( ropeend.z > height - 0.05 ) { Result.warnings.push_back( "the weights would hang above the pulley: rod or hangers too long for the table" ); } - point( ropeend ); + point( onbrackets ? onstructure( ropeend ) : ropeend ); + auto const stackfrom { Result.parts.size() }; place_chain( Device.hanger, "hanger", ropeend, down, Result ); { - auto placement { placed( "rod", rodname, vec { columnx, yb, s } ) }; + auto placement { placed( "rod", rodname, vec { columnx, columny, s } ) }; Result.parts.push_back( placement ); } auto z { s }; @@ -589,30 +1291,140 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & number += weight->offset; model = weight->model + "-" + ( number < 10 ? "0" : "" ) + std::to_string( number ); } - auto placement { placed( "weight." + std::to_string( index ), model, vec { columnx, yb, z } ) }; + auto placement { placed( "weight." + std::to_string( index ), model, vec { columnx, columny, z } ) }; auto const angle { weight->turn * ( turns++ ) * kPi / 180.0 }; set_rotation( placement, vec { std::cos( angle ), std::sin( angle ), 0.0 }, vec { -std::sin( angle ), std::cos( angle ), 0.0 }, vec { 0.0, 0.0, 1.0 } ); Result.parts.push_back( placement ); z += weight->height; } if( z > s + rodlength ) { Result.warnings.push_back( "the stack is higher than the rod" ); } + // pierścień prowadzący na stosie, prowadnica w osi jego przesuwu, w stronę konstrukcji + // obrót wokół osi stosu: -X modelu (w stronę prowadnicy) idzie w kierunku toward + auto const turned = [ & ]( std::string const &Role, std::string const &Part, vec const &At ) { + auto placement { placed( Role, Part, At ) }; + set_rotation( placement, toward * -1.0, vec { -toward.y, toward.x, 0.0 } * -1.0, vec { 0.0, 0.0, 1.0 } ); + return placement; + }; + auto const ringz { Device.ring_at < 0.0 ? z : s + std::min( Device.ring_at, z - s ) }; + // prowadnica tylko na uchwytach konstrukcji (bez nich nie ma jej gdzie przykręcić) - wtedy też bez pierścienia + if( onbrackets ) { + Result.parts.push_back( turned( "ring", Device.ring, vec { columnx, columny, ringz } ) ); + } + if( onbrackets ) { + auto const guide { vec { columnx, columny, Device.guide_bottom } + toward * ( Device.ring.empty() ? 0.0 : Device.ring_arm ) }; + Result.parts.push_back( turned( "guide", Device.guide, guide ) ); + Result.has_guide = true; + auto const at { guide + toward * Device.guide_mount }; + double const mount[ 3 ] { at.x, at.y, at.z }; + std::copy( mount, mount + 3, Result.guide_mount ); + std::copy( mount, mount + 3, Result.guide_end ); + Result.guide_end[ 2 ] += Device.guide_length; + if( mount[ 0 ] < 0.0 ) { Result.warnings.push_back( "the guide mounts are behind the face of the structure" ); } + // uchwyty prowadnicy (karta 33-0455): dolny poz. 2 przy spodzie, górny poz. 1 przy końcu; otwory w mount / end + // na ramach 5090: ramy w otworach uchwytów (środek słupa), stawia je i ich uchwyty jedno z dwóch urządzeń (owner) + if( onframe && Device.frame_owner ) { + for( auto const z : { Result.guide_mount[ 2 ], Result.guide_end[ 2 ] } ) { + auto placement { placed_along( z == Result.guide_mount[ 2 ] ? "frame_low" : "frame_high", Device.frame, vec { brackets->hole_out, 0.0, z }, out ) }; + // dolna rama odwrócona o 180 stopni wokół swojej długiej osi (Jan 2026-10-25), jak dolny uchwyt + if( z == Result.guide_mount[ 2 ] ) { + auto &r { placement.rotation }; + for( auto const column : { 1, 2 } ) { for( int row = 0; row < 3; ++row ) { r[ row * 3 + column ] = -r[ row * 3 + column ]; } } + } + Result.parts.push_back( placement ); + } + } + if( onbrackets && ( false == onframe || Device.frame_owner ) ) { + // dolny odwrócony o 180 stopni (karta 33-0455): otwór nad zerem + auto const y { onframe ? 0.0 : at.y }; + vec const low { 0.0, y, Result.guide_mount[ 2 ] - brackets->hole_drop }; + vec const high { 0.0, y, Result.guide_end[ 2 ] + brackets->hole_drop }; + front( "bracket_guide_low", brackets->bottom_front, low, true ); + back( "bracket_guide_low.back", brackets->bottom_back, low, true ); + front( "bracket_guide_high", brackets->top_front, high, false ); + back( "bracket_guide_high.back", brackets->top_back, high, false ); + } + if( false == Device.ring.empty() && ( ringz < Device.guide_bottom || ringz > Device.guide_bottom + Device.guide_length ) ) { + Result.warnings.push_back( "the guide ring is off the guide" ); + } + // mocowania prowadnicy w układzie urządzenia + for( auto *point : { Result.guide_mount, Result.guide_end } ) { + auto const p { onstructure( vec { point[ 0 ], point[ 1 ], point[ 2 ] } ) }; + point[ 0 ] = p.x; point[ 1 ] = p.y; point[ 2 ] = p.z; + } + } + if( onbrackets ) { fix( stackfrom ); } return true; } -void tensioner_rope_parts( tensioner_result &Result, std::string const &Cord, double const Length, double const Girth ) { - for( std::size_t k = 1; k < Result.rope.size(); ++k ) { - vec const from { Result.rope[ k - 1 ][ 0 ], Result.rope[ k - 1 ][ 1 ], Result.rope[ k - 1 ][ 2 ] }; - vec const to { Result.rope[ k ][ 0 ], Result.rope[ k ][ 1 ], Result.rope[ k ][ 2 ] }; - auto const span { length( to - from ) }; - if( span < 1e-6 ) { continue; } - auto const z { unit( to - from ) }; - auto const y { side_of( z ) }; - auto placement { placed( "rope." + std::to_string( k ), Cord, from ) }; - set_rotation( placement, cross( y, z ), y, z ); - placement.scale[ 0 ] = Girth; - placement.scale[ 1 ] = Girth; - placement.scale[ 2 ] = Length > 1e-9 ? span / Length : 1.0; - Result.parts.push_back( placement ); +void tensioner_rope_solid( tensioner_result &Result, double const Radius, int const Sides, std::string const &Eye, double const EyeLength, std::vector &Triangles, + std::string const &Spacer, double const SpacerLength ) { + std::vector points; + for( auto const &point : Result.rope ) { + vec const p { point[ 0 ], point[ 1 ], point[ 2 ] }; + if( points.empty() || length( p - points.back() ) > 1e-6 ) { points.push_back( p ); } + } + if( points.size() < 2 || Radius <= 0.0 ) { return; } + // ucha: zaczep ("eye") w skrajnym punkcie liny, zero ucha w stronę liny; rura zaczyna się i kończy w zerze ucha + if( false == Eye.empty() && EyeLength > 1e-6 ) { + auto const eye = [ & ]( std::string const &Role, vec const &Joint, vec const &Toward ) { + auto const z { unit( Toward ) }; + auto const y { side_of( z ) }; + auto placement { placed( Role, Eye, Joint ) }; + // ucho obrócone o 90 stopni wokół liny względem pierwotnego (Jan 2026-10-25): płaszczyzna ucha pionowo + set_rotation( placement, y, cross( y, z ) * -1.0, z ); + Result.parts.push_back( placement ); + }; + auto const n { points.size() }; + auto const startdir { unit( points[ 1 ] - points[ 0 ] ) }; + auto const enddir { unit( points[ n - 1 ] - points[ n - 2 ] ) }; + auto const start { points[ 0 ] + startdir * std::min( EyeLength, length( points[ 1 ] - points[ 0 ] ) ) }; + auto const end { points[ n - 1 ] - enddir * std::min( EyeLength, length( points[ n - 1 ] - points[ n - 2 ] ) ) }; + eye( "rope_eye.1", start, startdir * -1.0 ); + eye( "rope_eye.2", end, enddir ); + // przekładka ślizgowa na sworzniu ucha przy pręcie: sworzeń wzdłuż X ucha (ucho płaskie w Y-Z modelu), w zaczepie + if( false == Spacer.empty() ) { + auto const z { unit( enddir ) }; + auto const pin { side_of( z ) }; // X ucha (obróconego o 90 stopni) + auto placement { placed( "rope_spacer", Spacer, points[ n - 1 ] - pin * ( 0.5 * SpacerLength ) ) }; + set_rotation( placement, z, cross( pin, z ), pin ); + Result.parts.push_back( placement ); + } + points.front() = start; + points.back() = end; + } + // pierścienie przekroju w każdym punkcie ścieżki, w płaszczyźnie dwusiecznej sąsiednich odcinków; oś odniesienia + // przekroju przenoszona z punktu do punktu bez skręcania (rura się nie skręca na łukach) + auto const count { points.size() }; + auto const sides { std::max( 3, Sides ) }; + std::vector along( count ), across( count ); + for( std::size_t k = 0; k < count; ++k ) { + auto const before { k > 0 ? unit( points[ k ] - points[ k - 1 ] ) : vec {} }; + auto const after { k + 1 < count ? unit( points[ k + 1 ] - points[ k ] ) : vec {} }; + along[ k ] = k == 0 ? after : k + 1 == count ? before : unit( before + after ); + auto const reference { k == 0 ? side_of( along[ 0 ] ) : across[ k - 1 ] }; + across[ k ] = unit( reference - along[ k ] * dot( reference, along[ k ] ) ); + } + auto const ring = [ & ]( std::size_t const Point, int const Side, vec &Position, vec &Normal ) { + auto const angle { 2.0 * kPi * ( Side % sides ) / sides }; + auto const u { across[ Point ] }; + auto const v { cross( along[ Point ], u ) }; + Normal = u * std::cos( angle ) + v * std::sin( angle ); + Position = points[ Point ] + Normal * Radius; + }; + auto const emit = [ & ]( vec const &Position, vec const &Normal ) { + Triangles.push_back( { { Position.x, Position.y, Position.z }, { Normal.x, Normal.y, Normal.z } } ); + }; + Triangles.reserve( Triangles.size() + ( count - 1 ) * sides * 6 ); + for( std::size_t k = 0; k + 1 < count; ++k ) { + for( int side = 0; side < sides; ++side ) { + vec p00, n00, p01, n01, p10, n10, p11, n11; + ring( k, side, p00, n00 ); + ring( k, side + 1, p01, n01 ); + ring( k + 1, side, p10, n10 ); + ring( k + 1, side + 1, p11, n11 ); + emit( p00, n00 ); emit( p11, n11 ); emit( p10, n10 ); + emit( p00, n00 ); emit( p01, n01 ); emit( p11, n11 ); + } } } diff --git a/world/catenary_tensioner.h b/world/catenary_tensioner.h index 0cd5fcef..36f8a0f4 100644 --- a/world/catenary_tensioner.h +++ b/world/catenary_tensioner.h @@ -26,13 +26,18 @@ http://mozilla.org/MPL/2.0/. // Ułożenie modeli (w układzie części): // rolka 4460 zero w osi, koło w płaszczyźnie X-Z, oś wzdłuż Y // części liniowe (uchwyty 4440/4450, łączniki, izolatory, śruby, wyrównywacz, uchwyty krańcowe, łączniki na linie) -// zero w sworzniu od strony konstrukcji/dźwigni, biegną w +X do drugiego sworznia (długość z danych) -// dźwignia 6406 zero w otworze I, otwory w +Z (I na dole), otwór III odsunięty w +X; przy otworze liny poniżej -// otworu przewodów dźwignia odwrócona (obrót o 180 stopni wokół Y) +// zero w sworzniu od strony konstrukcji/dźwigni, biegną w -X do drugiego sworznia (pusty węzeł, +// np. hole_1 uchwytu 4440; długość z danych); "~część" w łańcuchu - postawiona odwrotnie (4440: zero +// w wąskim końcu, widełki z osią rolki na -X; przy rolce ruchomej widełki są od strony słupa) +// dźwignia 6406 zero w otworze osi II (mount_2 = III), otwory w +Z (I na dole), otwory na druty odsunięte w -X +// modelu (w stronę drutów); przy otworze liny poniżej otworu przewodów dźwignia odwrócona (180 wokół Y) // pręt 6401/6402 zero na dole (pod stosem), w +Z do zaczepu na górze (długość z danych) // ciężar 63xx zero w środku spodu, w +Z; kolejne obracane o "turn" wokół Z; kolor numerem wykonania modelu +// prowadnica 5070 zero w osi przesuwu pierścienia na dole, w +Z; mocowania do wsporników w -X (w stronę konstrukcji) +// pierścień 5080 zero w osi stosu, poziomo, punkt przesuwu po prowadnicy w -X (w stronę konstrukcji); ring_at nad spodem stosu // lina linka rozciągana (część typu cord, zero w zaczepie, w +Z, skalowana wzdłuż Z) #pragma once +#include #include #include #include @@ -77,6 +82,9 @@ struct tensioner_pulley { double diameter { 0.220 }; // po dnie rowka double width { 0.080 }; int grooves { 2 }; + // krążek drugiego rowka jako osobny model (4460-11), stawiany w osi rolki; model rolki niesie wtedy oś i krążek + // pierwszego rowka. Oba obracają się osobno o przewiniętą linę; pusty = cała rolka jednym modelem, obracana z rowkiem A + std::string sheave; }; struct tensioner_lever { @@ -88,6 +96,7 @@ struct tensioner_lever { struct tensioner_link { std::string name; double length { 0.0 }; + double roll { 0.0 }; // stopnie wokół osi łańcucha (w danych "@"), np. 90 - sworzeń obrócony do następnej części }; // stos ciężarów i otwory dźwigni dla typu sieci (numer typu z karty 0002 = traction_code) @@ -95,6 +104,9 @@ struct tensioner_type { int code { -1 }; std::vector> stack; // od dołu: ciężar, liczba std::string messenger_hole, contact_hole, axis_hole; + // dźwignie szeregowo (2 liny + 2 przewody na jednym stosie): otwór przewodów pierwszej dźwigni ciągnie łańcuchem "cascade" + // drugą dźwignię za jej otwór osi; druga dzieli dalej na drugą linę nośną i przewody. Puste = jedna dźwignia + std::string second_messenger_hole, second_contact_hole, second_axis_hole; std::string rod; // pręt, gdy inny niż urządzenia std::string messenger_clamp; // uchwyt krańcowy liny nośnej, gdy inny niż urządzenia }; @@ -102,15 +114,88 @@ struct tensioner_type { struct tensioner_device { std::string name; // karta, np. 0461 std::string table, pulley, lever, rod, rope; + std::string lever2; // druga dźwignia (szeregowo), gdy inna niż pierwsza double rope_diameter { 0.010 }; + std::string rope_eye; // ucho (kausza) na obu końcach liny, nieskalowane; pusty = lina bez uch + double rope_eye_length { 0.0 }; // od zera ucha (styk z liną) do punktu "eye" (zaczep), wzdłuż +Z modelu + std::string rope_spacer; // przekładka na sworzniu ucha przy pręcie (6411, oś w +Z modelu); pusta = bez + double rope_spacer_length { 0.0 }; std::vector mount; // od czoła konstrukcji do osi rolki stałej std::vector holder; // od osi rolki ruchomej do osi dźwigni std::vector hanger; // od rolki stałej w dół do pręta (bez liny) std::vector messenger; // od otworu dźwigni do uchwytu krańcowego liny nośnej (bez uchwytu) std::vector contact; // od otworu dźwigni do wyrównywacza albo uchwytu krańcowego przewodu + // dźwignie szeregowo: od otworu przewodów pierwszej do osi drugiej (z izolatorem) i od otworu liny drugiej do uchwytu drugiej + // liny nośnej; przewody ("contact") wtedy z drugiej dźwigni + std::vector cascade, messenger2; + // kotwienie stałe (kind fixed): bez rolek i ciężarów; na konstrukcji dwa przegubowe uchwyty kotwienia (pierwsza część "mount", + // 4430) - liny na wysokości liny nośnej, przewodów na wysokości przewodów, każdy w parze uchwytów poz. 1 (karta 43-0453). + // Za wyrównywaczem łańcuch każdego drutu osobno (…_each), dwie liny nośne przez messenger_equalizer (sieć 32, 35) + bool fixed { false }; + // rolki szeregowe (karty 0421, 0471, 0480): przy konstrukcji duża rolka (pulley) i za nią mała (small_pulley) o small_spacing + // dalej na uchwycie fixed_holder (4440-6), na wózku mała i duża (uchwyt 4440-5 = holder); lina od zaczepu na uchwycie stałym + // (dead_end od osi dużej) przez małą ruchomą, małą stałą, dużą ruchomą i dużą stałą w dół. b z tablicy = oś dużej ruchomej. + // Puste small_pulley = rolki równoległe (podwójne) + std::string small_pulley, fixed_holder; + // kotwienie rozdzielone: urządzenie tylko liny albo tylko przewodów (only), bez dźwigni - łańcuch wprost z uchwytu rolki + // ruchomej; dwa takie składa urządzenie "kind split" (split ). Prowadnica na ramach 5090 (frame + // [owner]): stos pod liną (Y 0), + // pierścień obrócony tak, żeby szekla trafiała w prowadnicę; owner stawia ramy i ich uchwyty + std::string only; + bool split { false }; + std::string split_messenger, split_contact; + std::string frame; + double frame_length { 0.0 }, frame_y { 0.0 }; + int frame_side { -1 }; + bool frame_owner { false }; + double messenger_spread { 0.08 }; // rozstaw dwóch lin za messenger_equalizer + bool messenger_equalizer_z { false }; // otwory messenger_equalizer w Z modelu (dźwignia 6403), a nie w Y (wyrównywacz 4410) + double small_spacing { 0.336 }, dead_end { 0.703 }; + std::vector messenger_each, contact_each; + tensioner_link messenger_equalizer; + // stare złożenia: izolator rolkowy ESN-2025 zamiast 7150 (Jan 2026-10-24: ESN-2025 - 4420 - [wyrównywacz] - uchwyt); + // puste = jak nowe + std::vector messenger_old, contact_old; tensioner_link messenger_clamp, contact_clamp; + tensioner_link contact_clamp_old; // uchwyt krańcowy przewodów w starych złożeniach (5110, stara karta 33-0401); pusty = jak nowe tensioner_link equalizer; // wyrównywacz przewodów jezdnych (tylko przy dwóch przewodach) double equalizer_spread { 0.08 }; // rozstaw przewodów za wyrównywaczem + // zabezpieczenie 4600 wyk. 1: dwa ramiona z osi rolek (stałej i ruchomej) zbiegające się na dolnej śrubie, para po obu stronach rolek + std::string safety_fixed, safety_moving; + double safety_arm { 1.0 }; // rozstaw otworów ramienia + double safety_offset { 0.06 }; // odsunięcie każdej pary od środka rolki w bok + // zabezpieczenie 4600 wyk. 2 (rolki szeregowe, karta 13-0421 poz. 10): ramiona z osi małych rolek (stałej i ruchomej) w dół, + // na końcach drążek; puste = bez niego. safety_series + std::string series_arm_fixed, series_arm_moving, series_bar; + double series_arm { 0.46268 }, series_bar_length { 1.30 }; + // tytuł do wyboru w edytorze (karta z odmianą widoczna na liście mimo "-" w nazwie); puste = bez + std::string title; + // prowadnica ciężarów (5070, karta 33-0455) i pierścień prowadzący na wierzchu stosu (5080). Model prowadnicy: zero w osi + // przesuwu pierścienia na dole, w +Z (długość), punkty "mount"/"end" mocowań do wsporników o guide_mount w -X (w stronę + // konstrukcji). Pierścień: zero w osi stosu, poziomo, "mount" o ring_arm w -X (w osi przesuwu po prowadnicy); pusty = bez nich + std::string guide, ring; + double guide_length { 4.50 }; + double guide_mount { 0.07 }; + double guide_bottom { 0.80 }; // spód prowadnicy nad główką szyny (karta 23-0018-2: 0,80, prowadnica 4,50 do 5,30) + double ring_arm { 0.200 }; // od osi stosu do osi szekli, która chodzi po prowadnicy (karta 23-5080 wyk. 1: 200) + double ring_turn { 0.0 }; // stopnie wokół osi stosu: 0 = prowadnica w stronę konstrukcji, 90 = w bok (+Y) + double ring_at { 1.00 }; // wysokość pierścienia nad spodem stosu (karta 23-0018-2: 1,00 m); ujemna = na wierzchu stosu + // uchwyty do konstrukcji (karta 33-0455): para przód + tył ściśnięta śrubami przez słup, na ścianach wzdłuż drutów (obrócone + // o 90 stopni względem wysięgnika; rozstaw śrub obejmuje słup w poprzek). Przód: zero na czole konstrukcji, śruby w +X modelu + // (w głąb), hole_1 o hole_out przed czołem i hole_drop pod zerem. Tył: ten sam układ, po drugiej stronie. + // Uchwyt przegubowy 4450 (pierwsza część "mount") trzymają dwa: górny wprost za mount_top, dolny odwrócony o 180 stopni za + // mount_bottom (pivot_top / pivot_bottom nad / pod zerem 4450) + struct bracket_set { + std::vector poles; // przedrostki nazw słupów, np. 1611-11 (najdłuższy pasujący wygrywa) + double hole_out { 0.04 }, hole_drop { 0.04 }, bolt { 0.217 }; + std::string top_front, top_back, bottom_front, bottom_back; + }; + std::vector brackets; + double pivot_top { 0.0 }, pivot_bottom { 0.0 }; + // otwór na końcu uchwytu (oś stałej rolki) nad jego zerem: uchwyt schodzi o tyle, żeby rolka została na swojej wysokości + // (5040: hole_1 0,03 nad mount_bottom; wpis "pivot [rise ]") + double mount_rise { 0.0 }; + bracket_set const *brackets_for( std::string const &Pole ) const; std::vector types; tensioner_type const *type( int Code ) const; }; @@ -126,6 +211,8 @@ public: tensioner_device const *device( std::string const &Name ) const; std::vector const &devices() const { return m_devices; } std::map const &part_lengths() const { return m_lengths; } // długości części liniowych (pręt) + // numer typu sieci dla urządzeń: sieć bez własnych danych bierze dane innej (wpis "network as "), inaczej jej kod + int code_for( std::string const &Symbol, int Code ) const; private: std::vector m_tables; @@ -134,19 +221,45 @@ private: std::vector m_levers; std::vector m_devices; std::map m_lengths; + std::map m_networks; }; struct tensioner_input { int type { 3 }; // numer typu sieci (traction_code) int contacts { 2 }; // liczba przewodów jezdnych + int messengers { 1 }; // liczba lin nośnych (kotwienie stałe sieci 32, 35: dwie) + double messenger_stack { 0.04 }; // odstęp pionowy dwóch lin nośnych w przęśle (druga celuje o tyle niżej) double temperature { 10.0 }; // °C double distance { 700.0 }; // m, od kotwienia stałego albo środkowego double elongation { 0.0 }; // m, trwałe wydłużenie sieci od ostatniej regulacji (rolka bliżej konstrukcji) int missing { 0 }; // brakujące ciężary (z góry stosu) + // sieć do skrócenia przy naciągu mniejszym niż od pełnego stosu (brakujące ciężary): sztywności EA wszystkich lin + // nośnych i wszystkich przewodów jezdnych (daN), ciężar sieci na metr wiszący na linie (daN/m) i średnia rozpiętość + // przęseł (m). Lina i przewody skracają się sprężyście, a lina dodatkowo zwisa bardziej, więc urządzenie ściąga je ku + // przęsłom: rolka dalej od konstrukcji, ciężary wyżej. Sztywność 0 = bez tego (tablica jak przy pełnym stosie) + double messenger_stiffness { 0.0 }, contact_stiffness { 0.0 }; + double load { 0.0 }; + double span { 0.0 }; std::string color; // kolor ciężarów (tensioner_color); pusty = szary std::string top_color; // kolor najwyższego ciężaru; pusty = jak reszta + bool old_fittings { false }; // uchwyty krańcowe przewodów 5110 (druty zaśniedziałe) + bool old_messenger { false }, old_contact { false }; // izolatory rolkowe (łańcuchy messenger_old / contact_old) na linie / przewodach + // punkty, w które celują łańcuchy do liny nośnej i do przewodów jezdnych (ostatnie podwieszenie, układ urządzenia); + // bez nich łańcuchy poziomo wzdłuż +X + bool aimed { false }; + double messenger_target[ 3 ] { 0.0, 0.0, 0.0 }; + double contact_target[ 3 ] { 0.0, 0.0, 0.0 }; // stare złożenie (druty zaśniedziałe): uchwyty krańcowe przewodów 5110 zamiast 5120 + double height { 0.0 }; // oś rolek nad główką szyny; 0 = z tablicy. Wyżej/niżej: ciężary o tyle samo (lina ta sama) + double rope_height { 0.0 }; // kotwienie stałe: uchwyt liny nośnej nad główką; 0 = jak drut przy ostatnim podwieszeniu double ground { -0.50 }; // najniższe położenie spodu ciężarów względem główki szyny (grunt, fundament); ZAŁOŻENIE double gravity { 0.981 }; // daN/kg + // konstrukcja: nazwa słupa (dobór uchwytów) i jej grubość od czoła do tyłu (wzdłuż drutów; słup 161x - szerokość wzdłuż toru, + // przez nią idą śruby uchwytów) na wysokości nad główką; puste = bez uchwytów + std::string pole; + std::function depth; + // kąt osi konstrukcji (od czoła w stronę kotwionego przęsła) w układzie urządzenia, radiany wokół +Z: urządzenie od przegubu + // 4450 ciągnie do drutów, a uchwyty, prowadnica, pierścień i stos zostają przy konstrukcji (0 = konstrukcja w linii drutów) + double structure_angle { 0.0 }; }; struct tensioner_placement { @@ -162,19 +275,46 @@ struct tensioner_result { std::vector> rope; // punkty liny (x, y, z) po kolei, od zaczepu przy rolce stałej do pręta double b { 0.0 }, s { 0.0 }; // położenie rolki ruchomej i spodu ciężarów double stack_height { 0.0 }; + double height { 0.0 }; // oś rolek nad główką szyny + double lever_tilt { 0.0 }; // radiany: odchylenie uchwytu dźwigni od poziomu w dół (dźwignia od pionu w stronę konstrukcji u góry) + // obrót krążków (radiany wokół +Y urządzenia) od położenia przy +10 °C: lina przewinięta przez rowek / promień + double turn_fixed_a { 0.0 }, turn_fixed_b { 0.0 }, turn_moving_a { 0.0 }, turn_moving_b { 0.0 }; int weights { 0 }; // zawieszone ciężary double tension { 0.0 }; // daN, całkowity naciąg sieci od ciężarów (zero, gdy ciężary stoją na gruncie) + double nominal_tension { 0.0 }; // daN, naciąg od pełnego stosu (jak w tablicy) + double shortening { 0.0 }; // m, o ile rolka odjechała od tablicy przez mniejszy naciąg (sprężystość i zwis) double messenger_tension { 0.0 }, contact_tension { 0.0 }; double messenger_end[ 3 ] { 0.0, 0.0, 0.0 }; // gdzie kończy się lina nośna (uchwyt krańcowy) + std::vector> rope2; // kotwienie rozdzielone: lina urządzenia przewodów (rope - urządzenia liny) + // dźwignie szeregowo: druga lina nośna (z drugiej dźwigni) i naciągi obu lin osobno (messenger_tension to ich suma) + bool cascade { false }; + bool fixed { false }; // kotwienie stałe (bez ciężarów; b, s, naciąg zero) + double rope_height { 0.0 }; // kotwienie stałe: wysokość uchwytu liny nośnej (height - przewodów) + double messenger2_end[ 3 ] { 0.0, 0.0, 0.0 }; // druga lina nośna: z drugiej dźwigni albo za wyrównywaczem kotwienia stałego + double messenger_tensions[ 2 ] { 0.0, 0.0 }; std::vector> contact_ends; // gdzie kończą się przewody jezdne bool grounded { false }; // ciężary oparte o grunt: sieć bez naciągu od urządzenia + // mocowania prowadnicy do wsporników na konstrukcji (dolne i górne, układ urządzenia); has_guide = false bez prowadnicy + bool has_guide { false }; + double guide_mount[ 3 ] { 0.0, 0.0, 0.0 }, guide_end[ 3 ] { 0.0, 0.0, 0.0 }; std::vector warnings; }; // składa urządzenie. false, gdy brakuje danych (urządzenia, typu, tablicy, rolki, dźwigni, otworów) bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const &Device, tensioner_input const &Input, tensioner_result &Result ); -// lina jako odcinki linki rozciąganej (część Cord o długości spoczynkowej Length, skala w poprzek Girth) -void tensioner_rope_parts( tensioner_result &Result, std::string const &Cord, double Length, double Girth ); +// wierzchołek bryły proceduralnej w układzie urządzenia; kolejne trójki to trójkąty, obchodzone przeciwnie do ruchu +// wskazówek zegara patrząc z zewnątrz (w prawoskrętnym układzie urządzenia) +struct tensioner_vertex { + double position[ 3 ]; + double normal[ 3 ]; +}; + +// lina jako jedna bryła proceduralna: rura o promieniu Radius i Sides ścianach po całej ścieżce Result.rope (łuki na +// rolkach co 5°), bez łączeń. Eye (gdy niepusty): ucho o długości EyeLength (zero w styku z liną, punkt "eye" w +Z w +// zaczepie) na obu końcach, dopisywane do Result.parts; rura skrócona o ucha +// Spacer: przekładka ślizgowa (6411) na sworzniu ucha przy pręcie ciężarów (koniec liny), oś wzdłuż sworznia; pusta = bez +void tensioner_rope_solid( tensioner_result &Result, double Radius, int Sides, std::string const &Eye, double EyeLength, std::vector &Triangles, + std::string const &Spacer = {}, double SpacerLength = 0.0 ); } // namespace catenary