/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "application/editormode.h" #include "application/editormodeshared.h" #include "application/editorprojection.h" #include "application/editoruilayer.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" #include "model/AnimModel.h" #include "scene/scene.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/translation.h" #include "utilities/utilities.h" #include "world/Track.h" #include "imgui/imgui.h" #include #include #include #include #include #include #include #include #include namespace { using geometry::arc_pieces; using geometry::plan_distance; using geometry::plan_of; using geometry::signed_angle; double constexpr kRimTolerance{0.05}; double constexpr kMinimumRadius{80.0}; glm::dvec2 heading_vector(double const Heading) { return {std::sin(glm::radians(Heading)), std::cos(glm::radians(Heading))}; } double heading_of(glm::dvec2 const &Direction) { return glm::degrees(std::atan2(Direction.x, Direction.y)); } double circular(double Angle) { Angle = std::fmod(Angle, 360.0); if (Angle <= -180.0) Angle += 360.0; if (Angle > 180.0) Angle -= 360.0; return Angle; } double line_angle(double const Angle) { auto result{circular(Angle)}; if (result >= 90.0) result -= 180.0; if (result < -90.0) result += 180.0; return result; } bool numeric(std::string const &Text, double &Value) { char *end{nullptr}; Value = std::strtod(Text.c_str(), &end); return false == Text.empty() && *end == '\0'; } std::string normalized(std::string File) { std::replace(File.begin(), File.end(), '\\', '/'); return ToLower(File); } std::vector fixed_angles(std::string const &File) { std::vector result; std::ifstream input{Global.asCurrentSceneryPath + File, std::ios_base::binary}; if (false == input.is_open()) return result; std::string const content{std::istreambuf_iterator(input), std::istreambuf_iterator()}; static std::regex const pattern{R"(event\s+\S*tor(\d+)rot\s+animation\s+\S+\s+\S+\s+rotate\s+\S+\s+\S+\s+\S+\s+(\S+))", std::regex::icase}; for (auto match{std::sregex_iterator(content.begin(), content.end(), pattern)}; match != std::sregex_iterator(); ++match) { double angle; if (numeric((*match)[2].str(), angle) && std::none_of(result.begin(), result.end(), [&](double const Angle) { return std::abs(line_angle(Angle - angle)) < 0.01; })) result.push_back(angle); } return result; } glm::dvec3 centre_of(TTrack const &Table) { auto const &path{Table.m_paths.front()}; return glm::mix(path.points[segment_data::point::start], path.points[segment_data::point::end], 0.5); } double length_of(TTrack const &Table) { auto const &path{Table.m_paths.front()}; return plan_distance(path.points[segment_data::point::start], path.points[segment_data::point::end]); } struct rim_end { double heading{0.0}; TTrack *track{nullptr}; }; std::vector rim_ends(TTrack const *Table, glm::dvec3 const &Centre, double const Rim) { std::vector result; for (auto *section : simulation::Region->sections(Centre, static_cast(Rim + 10.0))) for (auto const &cell : section->m_cells) for (auto *other : cell.m_directories.paths) { if (other == Table || other->m_editorremoved || (other->iCategoryFlag & 15) != 1 || other->eType == tt_Table) continue; for (auto const &path : other->m_paths) for (auto const &point : {path.points[segment_data::point::start], path.points[segment_data::point::end]}) { if (std::abs(plan_distance(point, Centre) - Rim) > kRimTolerance || std::abs(point.y - Centre.y) > 0.5) continue; auto const heading{heading_of(plan_of(point - Centre))}; if (std::none_of(result.begin(), result.end(), [&](rim_end const &End) { return End.track == other && std::abs(circular(End.heading - heading)) < 0.01; })) result.push_back({heading, other}); } } return result; } void draw_circle(screen_projection const &Projection, ImDrawList *Drawlist, glm::dvec3 const &Centre, double const Radius, ImU32 const Colour, float const Thickness) { int const count{48}; glm::dvec3 previous{Centre.x, Centre.y, Centre.z + Radius}; for (int i = 1; i <= count; ++i) { auto const angle{glm::two_pi() * i / count}; glm::dvec3 const next{Centre.x + Radius * std::sin(angle), Centre.y, Centre.z + Radius * std::cos(angle)}; Projection.line(Drawlist, previous, next, Colour, Thickness); previous = next; } } void draw_pieces(screen_projection const &Projection, ImDrawList *Drawlist, std::vector const &Pieces, ImU32 const Colour, float const Thickness) { for (auto const &piece : Pieces) { geometry::bezier const curve{piece}; auto const count{std::clamp(static_cast(curve.plan_length() / 2.0), 2, 48)}; auto previous{curve.point(0.0)}; for (int k = 1; k <= count; ++k) { auto const next{curve.point(static_cast(k) / count)}; Projection.line(Drawlist, previous, next, Colour, Thickness); previous = next; } } } void label(screen_projection const &Projection, glm::dvec3 const &Point, std::string const &Text) { ImVec2 at; if (false == Projection.project(Point, at)) return; auto *drawlist{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; auto const size{ImGui::CalcTextSize(Text.c_str())}; drawlist->AddRectFilled(ImVec2(at.x + 8.f, at.y - 4.f), ImVec2(at.x + 16.f + size.x, at.y + size.y + 4.f), IM_COL32(0, 0, 0, 170), 4.f); drawlist->AddText(ImVec2(at.x + 12.f, at.y), IM_COL32(255, 255, 255, 235), Text.c_str()); } } // namespace void editor_mode::scan_turntables() { auto &tool{m_turntable}; if (tool.scanned) return; tool.scanned = true; tool.templates.clear(); if (false == EditorIncludes.scanned()) EditorIncludes.scan(); for (auto const &entry : EditorIncludes.entries()) { if (entry.category != "turntable") continue; include_info info; std::string error; if (false == editor_includes::load(entry.file, info, error) || info.turntable_length <= 0.0) continue; turntable_template item; item.file = entry.file; item.name = info.name.empty() ? entry.file : info.name; item.length = info.turntable_length; item.track = editor_includes::parameter_with_role(info, "track"); item.yaw = editor_includes::parameter_with_role(info, "rot.y"); if (info.turntable_angles > 0 && info.turntable_positions > 0) { item.angles = info.turntable_angles; item.positions = info.turntable_positions; } else item.fixed = fixed_angles(entry.file); if (item.track == 0 || item.yaw == 0 || (item.angles == 0 && item.fixed.empty())) continue; tool.templates.push_back(std::move(item)); } std::stable_sort(tool.templates.begin(), tool.templates.end(), [](turntable_template const &A, turntable_template const &B) { return (A.angles > 0) != (B.angles > 0) ? A.angles > 0 : A.name < B.name; }); tool.chosen = tool.templates.empty() ? -1 : 0; } std::string editor_mode::turntable_name_for_new() const { if (m_turntable.name[0] != '\0') return ToLower(m_turntable.name); for (int i = 1;; ++i) { auto const name{"obr" + std::to_string(i)}; if (simulation::Paths.find(name) == nullptr && simulation::Instances.find(name) == nullptr) return name; } } bool editor_mode::turntable_read(TTrack *Table) { auto &tool{m_turntable}; if (Table == nullptr || Table->eType != tt_Table || Table->m_editorremoved || Table->m_paths.empty()) { tool.table = nullptr; tool.include = 0; tool.kind = -1; tool.exits.clear(); tool.fits.clear(); return false; } if (tool.table != Table) { tool.fits.clear(); tool.hovered = -1; tool.status.clear(); tool.error.clear(); } scan_turntables(); tool.table = Table; tool.length = length_of(*Table); auto const &path{Table->m_paths.front()}; tool.table_yaw = heading_of(plan_of(path.points[segment_data::point::start] - centre_of(*Table))); tool.include = 0; tool.kind = -1; tool.exits.clear(); for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count() && tool.include == 0; ++handle) { if (false == scene::Layers.tracked(handle)) continue; auto const &included{scene::Layers.instance(handle)}; if (included.removed || included.dead || included.file == nullptr) continue; auto const file{normalized(*included.file)}; auto const kind{std::find_if(tool.templates.begin(), tool.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == file; })}; if (kind == tool.templates.end()) continue; std::string target; std::vector values; if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || kind->track > static_cast(values.size()) || ToLower(values[kind->track - 1]) != Table->name()) continue; tool.include = handle; tool.kind = static_cast(kind - tool.templates.begin()); if (double yaw; kind->yaw <= static_cast(values.size()) && numeric(values[kind->yaw - 1], yaw)) tool.table_yaw = yaw; if (kind->angles > 0) { for (int i = 0; i < kind->positions && kind->angles - 1 + i < static_cast(values.size()); ++i) if (double angle; numeric(values[kind->angles - 1 + i], angle) && std::none_of(tool.exits.begin(), tool.exits.end(), [&](double const Exit) { return std::abs(line_angle(Exit - angle)) < 0.01; })) tool.exits.push_back(angle); } else tool.exits = kind->fixed; } return true; } glm::dvec3 editor_mode::turntable_centre() const { return m_turntable.table != nullptr ? centre_of(*m_turntable.table) : glm::dvec3{0.0}; } std::vector editor_mode::turntable_lines() const { std::vector result; for (auto const angle : m_turntable.exits) result.push_back(m_turntable.table_yaw + angle); return result; } double editor_mode::turntable_snap(double const Heading) const { auto const &tool{m_turntable}; auto const fixed{tool.kind >= 0 && tool.templates[tool.kind].angles == 0}; std::optional nearest; for (auto const line : turntable_lines()) for (auto const heading : {line, line + 180.0}) if (false == nearest.has_value() || std::abs(circular(heading - Heading)) < std::abs(circular(*nearest - Heading))) nearest = heading; if (nearest.has_value() && (fixed || std::abs(circular(*nearest - Heading)) < 1.0)) return circular(*nearest); auto const step{tool.step > 0.f ? static_cast(tool.step) : 0.1}; return circular(tool.table_yaw + std::round(circular(Heading - tool.table_yaw) / step) * step); } bool editor_mode::turntable_exit_at(glm::dvec3 const &Ground, double &Heading, double &Length) const { auto const &tool{m_turntable}; if (tool.table == nullptr) return false; auto const centre{turntable_centre()}; auto const rim{tool.length * 0.5}; auto const distance{plan_distance(Ground, centre)}; if (distance < rim + 1.0 || distance > rim + tool.reach) return false; Heading = turntable_snap(heading_of(plan_of(Ground - centre))); Length = std::max(5.0, std::round((distance - rim) * 2.0) * 0.5); for (auto const &end : rim_ends(tool.table, centre, rim)) if (std::abs(circular(end.heading - Heading)) < 0.05) return false; return true; } bool editor_mode::turntable_fit_end(editor_track::snap_target const &End, turntable_fit &Fit) const { auto const &tool{m_turntable}; Fit = {}; Fit.end = End; auto const centre{turntable_centre()}; auto const rim{tool.length * 0.5}; glm::dvec2 const c{plan_of(centre)}; auto const into{plan_of(End.direction)}; if (tool.table == nullptr || glm::length(into) < 1e-6) return false; glm::dvec2 const t{glm::normalize(into)}; auto target{End.position}; glm::dvec2 e{plan_of(target)}; struct shape { glm::dvec2 radial{0.0}; double radius{0.0}; double first{0.0}; double turn{0.0}; double second{0.0}; }; auto const connect = [&](glm::dvec2 const &Radial, double const Radius) -> std::optional { auto const rimpoint{c + Radial * rim}; auto const delta{signed_angle(Radial, t)}; auto const offset{e - rimpoint}; if (std::abs(delta) < 1e-5) { auto const along{glm::dot(offset, Radial)}; if (along < 0.5 || std::abs(geometry::cross(Radial, offset)) > 0.02) return std::nullopt; return shape{Radial, 0.0, along, 0.0, 0.0}; } if (std::abs(delta) > glm::radians(90.0)) return std::nullopt; auto const a{geometry::cross(t, offset) / geometry::cross(t, Radial)}; auto const s{geometry::cross(Radial, offset) / geometry::cross(Radial, t)}; auto const tangent{Radius * std::tan(std::abs(delta) * 0.5)}; if (a - tangent < -1e-3 || s - tangent < -1e-3) return std::nullopt; return shape{Radial, Radius, std::max(0.0, a - tangent), delta, std::max(0.0, s - tangent)}; }; std::optional best; std::string issue; auto const better = [&](shape const &Candidate) { if (false == best.has_value()) return true; auto const straight = [](shape const &Shape) { return Shape.radius == 0.0; }; if (straight(Candidate) != straight(*best)) return straight(Candidate); return Candidate.radius > best->radius + 1e-6; }; auto const solve = [&]() { auto const fixed{tool.kind >= 0 && tool.templates[tool.kind].angles == 0}; if (fixed) { for (auto const line : turntable_lines()) for (auto const heading : {line, line + 180.0}) { auto const radial{heading_vector(heading)}; for (int i = 0; tool.radius * std::pow(0.9, i) >= kMinimumRadius; ++i) if (auto const candidate{connect(radial, tool.radius * std::pow(0.9, i))}) { if (better(*candidate)) best = candidate; break; } } if (false == best.has_value()) issue = STR("It doesn't line up with any position of the turntable"); } else if (std::abs(geometry::cross(t, c - e)) < 0.02 && glm::dot(t, e - c) > rim + 0.5) { best = connect(t, 0.0); } else { for (int r = 0; tool.radius * std::pow(0.9, r) >= kMinimumRadius && false == best.has_value(); ++r) { auto const radius{tool.radius * std::pow(0.9, r)}; auto const gap = [&](double const S) { auto const vertex{e - t * S}; auto const radial{vertex - c}; if (glm::length(radial) < 1e-6) return -1.0; auto const delta{signed_angle(glm::normalize(radial), t)}; return S - radius * std::tan(std::abs(delta) * 0.5); }; auto const limit{plan_distance(End.position, centre) + 400.0}; double low{0.0}; std::optional high; for (int i = 1; i * 0.25 <= limit; ++i) { auto const s{i * 0.25}; if (gap(s) >= 0.0) { high = s; break; } low = s; } if (false == high.has_value()) continue; auto upper{*high}; for (int i = 0; i < 50; ++i) { auto const middle{(low + upper) * 0.5}; (gap(middle) >= 0.0 ? upper : low) = middle; } auto const vertex{e - t * upper}; if (glm::length(vertex - c) <= rim) continue; best = connect(glm::normalize(vertex - c), radius); } if (false == best.has_value()) issue = format(STR_C("Too close to the turntable for a curve of %.0f m or more"), kMinimumRadius); } }; solve(); auto *own{End.track}; if (false == best.has_value() && own != nullptr && own->eType == tt_Normal && own->m_paths.size() == 1 && own->Dynamics.empty()) { auto const &path{own->m_paths.front()}; auto const opposite{End.point.kind == editor_track::point_kind::start ? path.points[segment_data::point::end] : path.points[segment_data::point::start]}; auto const straight{std::abs(geometry::cross(t, plan_of(opposite - End.position))) < 0.01 && glm::dot(t, plan_of(opposite - End.position)) > 1.0}; std::string reason; if (straight && editor_track::can_edit_geometry(*own, reason)) { auto const first{issue}; target = opposite; e = plan_of(target); solve(); if (best.has_value()) Fit.replaces = own; else { target = End.position; e = plan_of(target); issue = first; } } } if (false == best.has_value()) { Fit.issue = issue.empty() ? STR("It points away from the turntable") : issue; return false; } auto const &shape{*best}; Fit.heading = heading_of(shape.radial); Fit.radius = shape.radius; Fit.length = shape.first + shape.radius * std::abs(shape.turn) + shape.second; auto const grade{Fit.length > 1e-6 ? (target.y - centre.y) / Fit.length : 0.0}; glm::dvec3 point{c.x + shape.radial.x * rim, centre.y, c.y + shape.radial.y * rim}; auto const straight = [&](glm::dvec2 const &Direction, double const Length) { segment_data piece; piece.points[segment_data::point::start] = point; point = {point.x + Direction.x * Length, point.y + grade * Length, point.z + Direction.y * Length}; piece.points[segment_data::point::end] = point; Fit.pieces.push_back(piece); }; if (shape.first > 0.05) straight(shape.radial, shape.first); if (shape.radius > 0.0) { auto const arc{arc_pieces(point, shape.radial, grade, shape.radius, std::abs(shape.turn), shape.turn > 0.0 ? 1 : -1)}; Fit.pieces.insert(Fit.pieces.end(), arc.begin(), arc.end()); point = arc.back().points[segment_data::point::end]; } if (shape.second > 0.05) straight(t, shape.second); if (false == Fit.pieces.empty()) Fit.pieces.back().points[segment_data::point::end] = target; return false == Fit.pieces.empty(); } void editor_mode::turntable_find_fits() { auto &tool{m_turntable}; tool.fits.clear(); tool.hovered = -1; tool.error.clear(); if (tool.table == nullptr) return; auto const centre{turntable_centre()}; auto const rim{tool.length * 0.5}; for (auto const &end : editor_track::free_ends(nullptr, 1, centre, rim + tool.reach, {tool.table})) { if (std::abs(plan_distance(end.position, centre) - rim) <= kRimTolerance) continue; turntable_fit fit; turntable_fit_end(end, fit); fit.chosen = fit.issue.empty(); tool.fits.push_back(std::move(fit)); } std::sort(tool.fits.begin(), tool.fits.end(), [](turntable_fit const &A, turntable_fit const &B) { return A.end.distance < B.end.distance; }); tool.status = tool.fits.empty() ? format(STR_C("No free ends within %.0f m of the rim"), tool.reach) : std::string{}; } std::vector editor_mode::turntable_includes(TTrack const &Table) { scan_turntables(); std::vector result; for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) { if (false == scene::Layers.tracked(handle)) continue; auto const &included{scene::Layers.instance(handle)}; if (included.removed || included.dead || included.file == nullptr) continue; auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })}; std::string target; std::vector values; if (kind != m_turntable.templates.end() && editor_includes::parse_directive(scene::Layers.directive(handle), target, values) && kind->track <= static_cast(values.size()) && ToLower(values[kind->track - 1]) == Table.name()) result.push_back(handle); } return result; } void editor_mode::turntable_build(std::vector const &Fits) { auto &tool{m_turntable}; if (tool.table == nullptr || Fits.empty()) return; std::vector created; std::vector removed; for (auto const *fit : Fits) { if (false == fit->issue.empty() || fit->end.track == nullptr || fit->end.track->m_editorremoved) continue; for (auto const &piece : fit->pieces) created.push_back(editor_track::create_path(*fit->end.track, piece)); if (fit->replaces != nullptr && false == fit->replaces->m_editorremoved) { editor_track::retire(*fit->replaces); removed.push_back(fit->replaces); } } if (created.empty()) return; editor_track::commit(created); push_track_snapshot({}, created, removed); turntable_store_exits(); tool.status = format(STR_C("%d track(s) led to the turntable"), static_cast(Fits.size())); } void editor_mode::turntable_store_exits() { auto &tool{m_turntable}; if (tool.table == nullptr || tool.include == 0 || tool.kind < 0) { turntable_read(tool.table); return; } auto const &kind{tool.templates[tool.kind]}; if (kind.angles == 0) { turntable_read(tool.table); return; } std::vector lines; for (auto const &end : rim_ends(tool.table, turntable_centre(), tool.length * 0.5)) { auto const angle{line_angle(end.heading - tool.table_yaw)}; if (std::none_of(lines.begin(), lines.end(), [&](double const Line) { return std::abs(line_angle(Line - angle)) < 0.01; })) lines.push_back(angle); } std::sort(lines.begin(), lines.end()); if (static_cast(lines.size()) > kind.positions) tool.error = format(STR_C("The turntable has %d positions, %d track lines lead to it: the rest won't be reached"), kind.positions, static_cast(lines.size())); std::string target; std::vector values; if (false == editor_includes::parse_directive(scene::Layers.directive(tool.include), target, values)) return; for (int i = 0; i < kind.positions && kind.angles - 1 + i < static_cast(values.size()); ++i) values[kind.angles - 1 + i] = editor_includes::number(i < static_cast(lines.size()) ? lines[i] : lines.empty() ? 0.0 : lines.front()); auto const directive{editor_includes::compose_directive(target, values)}; auto before{scene::Layers.directive(tool.include)}; if (directive != before) { EditorSnapshot snap; snap.node_name = target; snap.directives.emplace_back(tool.include, std::move(before)); set_include_directive(tool.include, directive); trim_history(); m_history.push_back(std::move(snap)); g_redo.clear(); } turntable_read(tool.table); } void editor_mode::turntable_place(glm::dvec3 const &Centre, double const Yaw) { auto &tool{m_turntable}; scan_turntables(); tool.error.clear(); if (tool.chosen < 0 || tool.chosen >= static_cast(tool.templates.size())) return; auto const &kind{tool.templates[tool.chosen]}; auto const name{turntable_name_for_new()}; if (simulation::Paths.find(name) != nullptr) { tool.error = format(STR_C("The name %s is taken by another path"), name.c_str()); return; } auto const rim{kind.length * 0.5}; auto const direction{heading_vector(Yaw)}; segment_data path; path.points[segment_data::point::start] = {Centre.x + direction.x * rim, Centre.y, Centre.z + direction.y * rim}; path.points[segment_data::point::end] = {Centre.x - direction.x * rim, Centre.y, Centre.z - direction.y * rim}; auto *table{editor_track::create_turntable(path, tool.snap.track, name)}; editor_track::commit({table}); push_track_snapshot({}, {table}); template_item item; item.file = kind.file; item.location = Centre; item.yaw = Yaw; item.described = true; item.track = table->name(); std::string error; if (place_templates({item}, error) == 0) tool.error = error; tool.name[0] = '\0'; m_node = table; m_track_point = {}; ui()->set_node(m_node); turntable_read(table); tool.status = format(STR_C("Turntable %s: click around it for the tracks out of it, or fit the tracks around. It turns after the scenery is saved and loaded again"), table->name().c_str()); } void editor_mode::turntable_press(track_intent const &Intent) { using kind = track_intent::kind; auto &tool{m_turntable}; auto const &io{ImGui::GetIO()}; switch (Intent.what) { case kind::turntable_place: tool.placing = true; tool.pressed = {io.MousePos.x, io.MousePos.y}; tool.snap = Intent.snap; tool.centre = Intent.position; return; case kind::turntable_exit: { if (tool.table == nullptr) return; auto const centre{turntable_centre()}; auto const rim{tool.length * 0.5}; auto const direction{heading_vector(tool.heading)}; segment_data piece; piece.points[segment_data::point::start] = {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; piece.points[segment_data::point::end] = {centre.x + direction.x * (rim + tool.exit_length), centre.y, centre.z + direction.y * (rim + tool.exit_length)}; auto const ends{rim_ends(tool.table, centre, rim)}; auto *track{ends.empty() ? editor_track::create_path(m_lay.style, piece) : editor_track::create_path(*ends.front().track, piece)}; editor_track::commit({track}); push_track_snapshot({}, {track}); turntable_store_exits(); tool.status = format(STR_C("Track out of the turntable at %.1f deg, %.1f m"), circular(tool.heading - tool.table_yaw), tool.exit_length); return; } case kind::turntable_fit: { turntable_fit fit; if (turntable_fit_end(Intent.snap, fit)) turntable_build({&fit}); else tool.error = fit.issue; return; } default: return; } } void editor_mode::turntable_finish_placement() { auto &tool{m_turntable}; if (false == tool.placing) return; tool.placing = false; scan_turntables(); if (tool.chosen < 0) return; auto const rim{tool.templates[tool.chosen].length * 0.5}; auto const &io{ImGui::GetIO()}; auto const dragged{glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - tool.pressed) >= kDragPixels}; if (tool.snap.track != nullptr && false == dragged) { auto const into{glm::normalize(plan_of(tool.snap.direction))}; glm::dvec3 const centre{tool.snap.position.x - into.x * rim, tool.snap.position.y, tool.snap.position.z - into.y * rim}; tool.yaw = heading_of(into); turntable_place(centre, tool.yaw); return; } tool.snap = {}; if (dragged) { auto const cursor{cursor_level(tool.centre.y)}; if (plan_distance(cursor, tool.centre) > 0.5) tool.yaw = std::round(heading_of(plan_of(cursor - tool.centre)) * 10.0) / 10.0; } turntable_place(tool.centre, tool.yaw); } editor_mode::track_intent editor_mode::turntable_intent(glm::dvec3 const &Ground, bool const Shift) { using kind = track_intent::kind; auto &tool{m_turntable}; track_intent intent; intent.position = Ground; auto const make = [&](kind const What, std::string const &Action) { intent.what = What; intent.action = Action; return intent; }; auto const &io{ImGui::GetIO()}; glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; if (tool.table != selected_track()) turntable_read(selected_track()); if (tool.table != nullptr) { auto const centre{turntable_centre()}; auto const rim{tool.length * 0.5}; auto const end{snap_free_end(Ground, mouse, nullptr, 1, {tool.table})}; if (end.track != nullptr && std::abs(plan_distance(end.position, centre) - rim) > kRimTolerance && plan_distance(end.position, centre) <= rim + tool.reach) { intent.snap = end; intent.track = end.track; intent.position = end.position; return make(kind::turntable_fit, STR("Click: lead this track to the turntable")); } double heading; double length; if (turntable_exit_at(cursor_level(centre.y), heading, length)) { tool.heading = heading; tool.exit_length = length; auto const direction{heading_vector(heading)}; intent.position = {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; return make(kind::turntable_exit, format(STR_C("Click: a track out of the turntable, %.1f deg, %.1f m"), circular(heading - tool.table_yaw), length)); } } for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->eType != tt_Table || track->m_editorremoved || track->m_paths.empty()) continue; if (plan_distance(centre_of(*track), Ground) <= length_of(*track) * 0.5) { intent.track = track; return make(kind::select, track == tool.table ? std::string{} : STR("Click: this turntable")); } } scan_turntables(); if (tool.chosen < 0) return intent; if (false == Shift) { auto const end{snap_free_end(Ground, mouse, nullptr, 1, {})}; if (end.track != nullptr) { intent.snap = end; intent.position = end.position; return make(kind::turntable_place, STR("Click: a turntable at the end of this track")); } } return make(kind::turntable_place, STR("Press: the centre of a new turntable, drag: the direction of its bridge")); } void editor_mode::render_turntable_ui() { auto &tool{m_turntable}; scan_turntables(); auto *selected{selected_track()}; if (tool.table != selected) turntable_read(selected); ImVec4 const warncolour{1.f, 0.75f, 0.3f, 1.f}; if (tool.table != nullptr) { auto &table{*tool.table}; ImGui::Text(STR_C("Turntable %s, bridge %.1f m"), table.name().c_str(), tool.length); if (tool.kind >= 0) { auto const &kind{tool.templates[tool.kind]}; ImGui::TextDisabled("%s", kind.name.c_str()); ImGui::SameLine(); ImGui::TextDisabled(kind.angles > 0 ? STR_C("(positions set by the tracks around, up to %d)") : STR_C("(%d positions fixed by the template)"), kind.angles > 0 ? kind.positions : static_cast(kind.fixed.size())); } else ImGui::TextColored(warncolour, "%s", STR_C("No turntable template takes this path as its track: the positions aren't known")); render_track_name(table); auto const centre{turntable_centre()}; auto const ends{rim_ends(&table, centre, tool.length * 0.5)}; if (false == tool.exits.empty() && ImGui::TreeNodeEx(format(STR_C("Positions (%d)"), static_cast(tool.exits.size())).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { for (std::size_t i = 0; i < tool.exits.size(); ++i) { auto const heading{tool.table_yaw + tool.exits[i]}; auto const occupied = [&](double const Heading) { return std::any_of(ends.begin(), ends.end(), [&](rim_end const &End) { return std::abs(circular(End.heading - Heading)) < 0.05; }); }; auto const front{occupied(heading)}; auto const back{occupied(heading + 180.0)}; ImGui::Text("%2d %7.2f deg", static_cast(i + 1), tool.exits[i]); ImGui::SameLine(); if (front || back) ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.f), "%s%s%s", front ? STR_C("track") : "", front && back ? " + " : "", back ? STR_C("track on the other end") : ""); else ImGui::TextDisabled("%s", STR_C("no track")); } ImGui::TreePop(); } ImGui::Separator(); ImGui::TextUnformatted(STR_C("Tracks around")); ImGui::PushItemWidth(110.f); ImGui::InputFloat(STR_C("Reach from the rim, m"), &tool.reach, 10.f, 50.f, "%.0f"); tool.reach = std::clamp(tool.reach, 5.f, 500.f); ImGui::InputFloat(STR_C("Radius of the curves, m"), &tool.radius, 10.f, 50.f, "%.0f"); tool.radius = std::clamp(tool.radius, static_cast(kMinimumRadius), 2000.f); if (tool.kind < 0 || tool.templates[tool.kind].angles > 0) { ImGui::InputFloat(STR_C("Angle step of a new track, deg (0: free)"), &tool.step, 0.5f, 2.5f, "%.2f"); tool.step = std::clamp(tool.step, 0.f, 45.f); } ImGui::PopItemWidth(); ImGui::TextDisabled("%s", STR_C("Click around the turntable: a track out of it, as long as the cursor is far.\nClick a free end near it: that track is led to the rim, straight or with a curve")); if (ImGui::Button(STR_C("Find the free ends around"))) turntable_find_fits(); if (false == tool.fits.empty()) { int chosen{0}; for (auto const &fit : tool.fits) chosen += fit.chosen && fit.issue.empty() ? 1 : 0; ImGui::SameLine(); if (ImGui::Button(format(STR_C("Lead the chosen to the turntable (%d)"), chosen).c_str())) { std::vector fits; for (auto const &fit : tool.fits) if (fit.chosen && fit.issue.empty()) fits.push_back(&fit); turntable_build(fits); turntable_find_fits(); } tool.hovered = -1; ImGui::BeginChild("##turntablefits", ImVec2(0.f, 180.f), true); for (int i = 0; i < static_cast(tool.fits.size()); ++i) { auto &fit{tool.fits[i]}; ImGui::PushID(i); if (fit.issue.empty()) ImGui::Checkbox("##chosen", &fit.chosen); else ImGui::TextDisabled(" - "); ImGui::SameLine(); auto const name{fit.end.track->name().empty() ? std::string{"(noname)"} : fit.end.track->name()}; if (ImGui::Selectable(name.c_str(), false, ImGuiSelectableFlags_None, ImVec2(ImGui::CalcTextSize(name.c_str()).x, 0.f))) focus_track(*fit.end.track, fit.end.point.path, fit.end.position); if (ImGui::IsItemHovered()) tool.hovered = i; ImGui::SameLine(); if (false == fit.issue.empty()) ImGui::TextColored(warncolour, "%s", fit.issue.c_str()); else if (fit.radius > 0.0) ImGui::TextDisabled(STR_C("%.1f deg, curve R %.0f m, %.1f m"), circular(fit.heading - tool.table_yaw), fit.radius, fit.length); else ImGui::TextDisabled(STR_C("%.1f deg, straight %.1f m"), circular(fit.heading - tool.table_yaw), fit.length); if (fit.replaces != nullptr) { ImGui::SameLine(); ImGui::TextDisabled(STR_C("in place of its last straight %s"), fit.replaces->name().c_str()); } ImGui::PopID(); } ImGui::EndChild(); } } else { if (selected != nullptr && selected->eType == tt_Table) ImGui::TextColored(warncolour, "%s", STR_C("This turntable can't be read")); ImGui::TextUnformatted(STR_C("New turntable")); if (tool.templates.empty()) ImGui::TextColored(warncolour, "%s", STR_C("No turntables: no template is described with category: turntable and its turntable: {length}")); for (int i = 0; i < static_cast(tool.templates.size()); ++i) { auto const &kind{tool.templates[i]}; auto const text{kind.angles > 0 ? format(STR_C("%s %.0f m, up to %d positions set by the tracks"), kind.name.c_str(), kind.length, kind.positions) : format(STR_C("%s %.0f m, %d fixed positions"), kind.name.c_str(), kind.length, static_cast(kind.fixed.size()))}; if (ImGui::Selectable((text + "##turntable" + std::to_string(i)).c_str(), tool.chosen == i)) tool.chosen = i; if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", kind.file.c_str()); } ImGui::SetNextItemWidth(160.f); ImGui::InputTextWithHint(STR_C("Name##turntablename"), turntable_name_for_new().c_str(), tool.name, sizeof(tool.name), ImGuiInputTextFlags_CharsNoBlank); ImGui::TextDisabled("%s", STR_C("Press where the centre goes and drag to turn the bridge.\nClick a free end of a track: the turntable lines up with it, at its end. Shift: no lining up")); } if (false == tool.error.empty()) ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", tool.error.c_str()); else if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); } void editor_mode::draw_turntable_overlay() const { using kind = track_intent::kind; auto const &tool{m_turntable}; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; auto const camera{glm::dvec3{Global.pCamera.Pos}}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->eType != tt_Table || track->m_editorremoved || track->m_paths.empty() || track == tool.table) continue; auto const centre{centre_of(*track)}; if (glm::distance(centre, camera) < 1500.0) draw_circle(projection, drawlist, centre, length_of(*track) * 0.5, IM_COL32(255, 255, 255, 90), 1.5f); } if (tool.table != nullptr) { auto const centre{turntable_centre()}; auto const rim{tool.length * 0.5}; draw_circle(projection, drawlist, centre, rim, overlay_color::selected, 2.5f); auto const ends{rim_ends(tool.table, centre, rim)}; for (auto const line : turntable_lines()) for (auto const heading : {line, line + 180.0}) { auto const direction{heading_vector(heading)}; auto const occupied{std::any_of(ends.begin(), ends.end(), [&](rim_end const &End) { return std::abs(circular(End.heading - heading)) < 0.05; })}; glm::dvec3 const from{centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; glm::dvec3 const to{centre.x + direction.x * (rim + 6.0), centre.y, centre.z + direction.y * (rim + 6.0)}; projection.line(drawlist, from, to, occupied ? IM_COL32(90, 230, 110, 230) : IM_COL32(255, 255, 255, 150), occupied ? 2.5f : 1.5f); } for (int i = 0; i < static_cast(tool.fits.size()); ++i) { auto const &fit{tool.fits[i]}; auto const colour{false == fit.issue.empty() ? IM_COL32(255, 150, 60, 200) : fit.chosen ? IM_COL32(90, 230, 110, 230) : IM_COL32(200, 200, 200, 140)}; draw_pieces(projection, drawlist, fit.pieces, colour, i == tool.hovered ? 4.f : 2.f); ImVec2 at; if (projection.project(fit.end.position, at)) drawlist->AddCircle(at, i == tool.hovered ? 10.f : 6.f, colour, 16, 2.f); } } auto const &intent{m_intent}; switch (intent.what) { case kind::turntable_exit: { auto const centre{turntable_centre()}; auto const direction{heading_vector(tool.heading)}; auto const rim{tool.length * 0.5}; glm::dvec3 const to{centre.x + direction.x * (rim + tool.exit_length), centre.y, centre.z + direction.y * (rim + tool.exit_length)}; projection.line(drawlist, intent.position, to, overlay_color::grip, 3.f); break; } case kind::turntable_fit: { turntable_fit fit; auto const fits{turntable_fit_end(intent.snap, fit)}; draw_pieces(projection, drawlist, fit.pieces, fits ? overlay_color::grip : overlay_color::invalid, 3.f); if (false == fits) label(projection, intent.position, fit.issue); break; } case kind::turntable_place: { if (tool.chosen < 0 || tool.chosen >= static_cast(tool.templates.size())) break; auto const rim{tool.templates[tool.chosen].length * 0.5}; auto centre{intent.position}; auto yaw{tool.yaw}; if (intent.snap.track != nullptr) { auto const into{glm::normalize(plan_of(intent.snap.direction))}; centre = {intent.snap.position.x - into.x * rim, intent.snap.position.y, intent.snap.position.z - into.y * rim}; yaw = heading_of(into); } auto const direction{heading_vector(yaw)}; draw_circle(projection, drawlist, centre, rim, IM_COL32(255, 255, 255, 170), 1.5f); projection.line(drawlist, {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}, {centre.x - direction.x * rim, centre.y, centre.z - direction.y * rim}, IM_COL32(255, 255, 255, 170), 2.f); break; } default: break; } if (tool.placing && tool.chosen >= 0 && tool.chosen < static_cast(tool.templates.size())) { auto const rim{tool.templates[tool.chosen].length * 0.5}; auto const cursor{cursor_level(tool.centre.y)}; auto const &io{ImGui::GetIO()}; auto const dragged{glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - tool.pressed) >= kDragPixels}; auto centre{tool.centre}; auto yaw{tool.yaw}; if (tool.snap.track != nullptr && false == dragged) { auto const into{glm::normalize(plan_of(tool.snap.direction))}; centre = {tool.snap.position.x - into.x * rim, tool.snap.position.y, tool.snap.position.z - into.y * rim}; yaw = heading_of(into); } else if (dragged && plan_distance(cursor, centre) > 0.5) yaw = std::round(heading_of(plan_of(cursor - centre)) * 10.0) / 10.0; auto const direction{heading_vector(yaw)}; draw_circle(projection, drawlist, centre, rim, overlay_color::marked, 2.5f); projection.line(drawlist, {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}, {centre.x - direction.x * rim, centre.y, centre.z - direction.y * rim}, overlay_color::marked, 3.f); if (dragged) label(projection, cursor, format("%.1f deg", yaw)); } } bool editor_mode::turntable_controls(std::vector &Rootstatements) { if (scene::Layers.empty()) return false; std::string root{scene::Layers.layer(scene::layer_handle{1}).name}; if (auto const slash{root.find_last_of("/\\")}; slash != std::string::npos) root.erase(0, slash + 1); if (auto const dot{root.rfind('.')}; dot != std::string::npos) root.erase(dot); if (root.empty()) return false; auto const file{root + "_obrotnice.scm"}; std::ostringstream text; auto tables{0}; std::vector centres; for (auto const *table : simulation::Paths.sequence()) if (table != nullptr && table->eType == tt_Table && false == table->m_editorremoved && false == table->m_paths.empty() && false == table->name().empty()) centres.push_back(centre_of(*table)); for (auto const *table : simulation::Paths.sequence()) { if (table == nullptr || table->eType != tt_Table || table->m_editorremoved || table->m_paths.empty() || table->name().empty()) continue; auto const &name{table->name()}; std::vector slots; std::string events; scan_turntables(); for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count() && slots.empty(); ++handle) { if (false == scene::Layers.tracked(handle)) continue; auto const &included{scene::Layers.instance(handle)}; if (included.removed || included.dead || included.file == nullptr) continue; auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })}; if (kind == m_turntable.templates.end() || kind->angles == 0) continue; std::string target; std::vector values; if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || kind->track > static_cast(values.size()) || ToLower(values[kind->track - 1]) != name) continue; { std::ifstream input{Global.asCurrentSceneryPath + kind->file, std::ios_base::binary}; events.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); events = ToLower(events); std::replace_if(events.begin(), events.end(), [](char const Character) { return Character == '\t' || Character == '\r' || Character == '\n'; }, ' '); auto const parameter{"(p" + std::to_string(kind->track) + ")"}; for (auto at{events.find(parameter)}; at != std::string::npos; at = events.find(parameter, at + name.size())) events.replace(at, parameter.size(), name); } std::vector seen; for (int i = 0; i < kind->positions && kind->angles - 1 + i < static_cast(values.size()); ++i) if (double angle; numeric(values[kind->angles - 1 + i], angle) && std::none_of(seen.begin(), seen.end(), [&](double const Other) { return std::abs(circular(Other - angle)) < 0.01; })) { seen.push_back(angle); slots.push_back(i + 1); } } if (slots.empty()) for (int position = 1; position <= 64; ++position) slots.push_back(position); std::vector positions; for (auto const position : slots) { // the events of a turntable placed in this session get loaded only with the scenery loaded again auto const event{name + "_tor" + std::to_string(position)}; if (events.find(" " + event + " ") != std::string::npos || simulation::Events.FindEvent(event) != nullptr) positions.push_back(position); } if (positions.size() < 2) continue; ++tables; auto const count{positions.size()}; auto const centre{centre_of(*table)}; auto const at{format("%.3f %.3f %.3f", centre.x, centre.y, centre.z)}; text << "node -1 0 " << name << "_keylock memcell " << at << " none 0 0 none endmemcell\r\n"; text << "event " << name << "_lock updatevalues 0 " << name << "_keylock * 1 * endevent\r\n"; text << "event " << name << "_unlock updatevalues 1.5 " << name << "_keylock * 0 * endevent\r\n"; for (auto const forward : {true, false}) { auto const step{std::string{forward ? "_next" : "_prev"}}; text << "event " << name << step << " multiple 0 " << name << "_keylock " << name << "_lock " << name << "_unlock " << name << step << '0'; for (auto const position : positions) text << ' ' << name << step << position; text << " condition memcompare * 0 * endevent\r\n"; // the turntable starts with no position set, the first step takes it to the first one text << "event " << name << step << "0 multiple 0 " << name << ' ' << name << "_tor" << positions.front() << " condition memcompare * 0 * endevent\r\n"; for (std::size_t i = 0; i < count; ++i) { auto const target{positions[forward ? (i + 1) % count : (i + count - 1) % count]}; text << "event " << name << step << positions[i] << " multiple 0 " << name << ' ' << name << "_tor" << target << " condition memcompare * " << positions[i] << " * endevent\r\n"; } } auto range{150.0}; for (auto const &other : centres) if (auto const distance{glm::distance(other, centre)}; distance > 0.5) range = std::min(range, distance * 0.5); text << "node -1 0 " << name << "_key eventlauncher " << at << ' ' << format("%.1f", range) << " t 0 " << name << "_next " << name << "_prev end\r\n"; } auto const path{Global.asCurrentSceneryPath + file}; std::error_code error; auto const exists{std::filesystem::exists(path, error)}; if (tables == 0 && false == exists) return false; auto const content{"// made by the scenery editor on each save: T turns each turntable to its next position, Shift+T to the previous one\r\n" + text.str()}; std::string current; { std::ifstream input{path, std::ios_base::binary}; current.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); } if (current != content) { std::ofstream output{path, std::ios_base::binary | std::ios_base::trunc}; output << content; } auto included{m_turntable_included}; for (std::size_t handle = 1; handle <= scene::Layers.size() && false == included; ++handle) { auto name{scene::Layers.layer(static_cast(handle)).name}; std::replace(name.begin(), name.end(), '\\', '/'); included = ToLower(name) == ToLower(file) || ToLower(name).ends_with("/" + ToLower(file)); } if (included) return false; Rootstatements.push_back("include " + file + " end"); return true; }