/* 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/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" #include "editor/editorSettings.hpp" #include "rendering/renderer.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/utilities.h" #include "world/Event.h" #include "world/Track.h" #include "imgui/imgui.h" #include "utilities/translation.h" #include #include #include #include #include #include namespace { using geometry::bezier; using geometry::heading_of; using geometry::plan_of; double const kRailTop{0.18}; double const kSignalRange{1500.0}; double const kSignalHead{4.0}; float const kSignalPick{12.0f}; float const kDragStart{6.0f}; double const kDragTravel{1.0}; double const kRefreshPeriod{2.0}; double const kSignalReach{10.0}; // a track laid in the place of a deleted one runs within it of its axis double const kOrphanReach{1.0}; std::size_t const kOrphanRecords{64}; char const *const kKinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"}; char const *const kKindLabels[] = {"Semaphore", "Automatic block", "Warning signal", "Shunting signal", "Stop signal", "Substitute signal", "Repeater"}; int const kKindCount{7}; int kind_index(std::string const &Kind) { for (int i = 0; i < kKindCount; ++i) if (Kind == kKinds[i]) return i; return -1; } double nearest_on(TTrack const &Track, int const Path, glm::dvec3 const &Point) { bezier const curve{Track.m_paths[Path]}; auto parameter{0.0}; auto best{std::numeric_limits::max()}; for (int k = 0; k <= 256; ++k) { auto const distance{geometry::plan_distance(curve.point(k / 256.0), Point)}; if (distance < best) { best = distance; parameter = k / 256.0; } } return parameter; } glm::dvec2 forward_at(TTrack const &Track, int const Path, double const Parameter) { auto const tangent{bezier{Track.m_paths[Path]}.first(Parameter)}; glm::dvec2 const planar{tangent.x, tangent.z}; return glm::length(planar) > 1e-9 ? glm::normalize(planar) : glm::dvec2{0.0, 1.0}; } glm::dvec2 right_of(glm::dvec2 const &Direction) { return {-Direction.y, Direction.x}; } glm::dvec2 travel_of(double const Yaw) { auto const yaw{glm::radians(Yaw + 180.0)}; return {std::sin(yaw), std::cos(yaw)}; } int travel_side(int const Side, int const List) { return Side * (List == 2 ? 1 : -1); } int lean_wanted(int const Side) { return Side > 0 ? 1 : 2; } int lean_of(std::string const &Lean) { return Lean == "left" ? 1 : Lean == "right" ? 2 : 0; } std::size_t edit_distance(std::string const &A, std::string const &B) { std::vector row(B.size() + 1); for (std::size_t j = 0; j <= B.size(); ++j) row[j] = j; for (std::size_t i = 1; i <= A.size(); ++i) { auto diagonal{row[0]}; row[0] = i; for (std::size_t j = 1; j <= B.size(); ++j) { auto const above{row[j]}; row[j] = std::min({row[j] + 1, row[j - 1] + 1, diagonal + (A[i - 1] == B[j - 1] ? 0 : 1)}); diagonal = above; } } return row[B.size()]; } ImU32 lamp_colour(char const Lamp) { switch (Lamp) { case 'z': return IM_COL32(40, 210, 80, 255); case 'c': return IM_COL32(230, 40, 40, 255); case 'p': return IM_COL32(255, 160, 30, 255); case 'b': return IM_COL32(235, 235, 235, 255); case 'n': return IM_COL32(60, 110, 255, 255); default: return IM_COL32(70, 70, 70, 255); } } std::string group_of(std::string const &Mount, std::string const &Lamps) { return Mount + "|" + Lamps; } std::vector const &plate_textures() { static std::vector plates; static bool scanned{false}; if (scanned) return plates; scanned = true; std::error_code error; for (std::filesystem::directory_iterator file{"textures/tabl", error}, end; file != end; file.increment(error)) { std::string stem; try { stem = ToLower(file->path().stem().string()); } catch (std::exception const &) { continue; } if (std::find(plates.begin(), plates.end(), stem) == plates.end()) plates.push_back(stem); } std::sort(plates.begin(), plates.end()); return plates; } std::string plate_for(std::string const &Plate, std::string const &Name) { if (false == Plate.empty()) return Plate; auto const &plates{plate_textures()}; auto const name{ToLower(Name)}; return std::binary_search(plates.begin(), plates.end(), name) ? name : std::string{"0_nothing"}; } void draw_signal_icon(ImDrawList *Draw, ImVec2 const Origin, ImVec2 const Size, std::string const &Mount, std::string const &Lamps, ImU32 const Frame) { auto const lamps{std::max(1, static_cast(Lamps.size()))}; auto const centre{Origin.x + Size.x * 0.5f}; auto const bottom{Origin.y + Size.y - 4.0f}; auto const lamp{std::min(7.0f, (Size.y - 26.0f) / (lamps * 2.2f))}; auto const headwidth{lamp * 2.0f + 6.0f}; auto const headheight{lamps * lamp * 2.2f + 4.0f}; ImU32 const pole{IM_COL32(150, 150, 150, 255)}; float headtop; if (Mount == "dwarf") { headtop = bottom - headheight; Draw->AddRectFilled(ImVec2(centre - headwidth * 0.7f, bottom - 3.0f), ImVec2(centre + headwidth * 0.7f, bottom), pole); } else if (Mount == "gantry") { headtop = Origin.y + 10.0f; Draw->AddLine(ImVec2(Origin.x + 4.0f, Origin.y + 6.0f), ImVec2(Origin.x + Size.x - 4.0f, Origin.y + 6.0f), pole, 3.0f); Draw->AddLine(ImVec2(centre, Origin.y + 6.0f), ImVec2(centre, headtop), pole, 2.0f); } else { headtop = Origin.y + 6.0f; Draw->AddLine(ImVec2(centre, headtop + headheight), ImVec2(centre, bottom), pole, 3.0f); Draw->AddLine(ImVec2(centre - 6.0f, bottom), ImVec2(centre + 6.0f, bottom), pole, 2.0f); } if (Lamps.empty()) { Draw->AddLine(ImVec2(centre, headtop), ImVec2(centre, headtop + headheight), pole, 3.0f); Draw->AddLine(ImVec2(centre, headtop + 6.0f), ImVec2(centre + 14.0f, headtop - 2.0f), IM_COL32(230, 60, 60, 255), 4.0f); return; } Draw->AddRectFilled(ImVec2(centre - headwidth * 0.5f, headtop), ImVec2(centre + headwidth * 0.5f, headtop + headheight), IM_COL32(25, 25, 25, 255), 3.0f); Draw->AddRect(ImVec2(centre - headwidth * 0.5f, headtop), ImVec2(centre + headwidth * 0.5f, headtop + headheight), Frame, 3.0f); for (int i = 0; i < static_cast(Lamps.size()); ++i) Draw->AddCircleFilled(ImVec2(centre, headtop + 2.0f + lamp * 1.1f + i * lamp * 2.2f), lamp, lamp_colour(Lamps[i])); } // the point of an axis of the track nearest in the plan to the one given: how far, on which path, where on it double plan_gap(TTrack const &Track, glm::dvec3 const &Point, int &Path, double &Parameter) { auto best{std::numeric_limits::max()}; for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) { auto const parameter{nearest_on(Track, path, Point)}; auto const distance{geometry::plan_distance(bezier{Track.m_paths[path]}.point(parameter), Point)}; if (distance < best) { best = distance; Path = path; Parameter = parameter; } } return best; } bool plan_overlaps(TTrack const &A, TTrack const &B, double const Margin) { auto const box = [](TTrack const &Track, glm::dvec2 &Low, glm::dvec2 &High) { Low = glm::dvec2{std::numeric_limits::max()}; High = -Low; for (auto const &path : Track.m_paths) { bezier const curve{path}; for (auto const &point : {curve.p0, curve.p1, curve.p2, curve.p3}) { Low = glm::min(Low, plan_of(point)); High = glm::max(High, plan_of(point)); } } }; glm::dvec2 low_a, high_a, low_b, high_b; box(A, low_a, high_a); box(B, low_b, high_b); return low_a.x - Margin <= high_b.x && low_b.x - Margin <= high_a.x && low_a.y - Margin <= high_b.y && low_b.y - Margin <= high_a.y; } std::string listed(std::vector const &Names) { std::string text; for (auto const &name : Names) text += (text.empty() ? "" : ", ") + name; return text; } } // namespace bool editor_mode::signal_scan(bool const Wait) { auto &tool{m_signal}; if (Wait) EditorIncludes.wait(); else EditorIncludes.start(); if (false == EditorIncludes.scanned() || tool.revision == EditorIncludes.revision()) return false; tool.revision = EditorIncludes.revision(); std::string chosen[kKindCount]; for (int i = 0; i < kKindCount; ++i) if (tool.chosen[i] >= 0 && tool.chosen[i] < static_cast(tool.templates.size())) chosen[i] = tool.templates[tool.chosen[i]].file; tool.templates.clear(); for (auto const &entry : EditorIncludes.entries()) { if (entry.category != "signal" || false == entry.complete) continue; include_info info; std::string error; if (false == editor_includes::load(entry.file, info, error) || kind_index(info.signal_kind) < 0) continue; signal_template item; item.file = entry.file; item.name = info.name.empty() ? entry.file : info.name; item.description = info.description; item.kind = info.signal_kind; item.mount = info.signal_mount.empty() ? std::string{"mast"} : info.signal_mount; item.lamps = info.signal_lamps; item.lean = info.signal_lean; item.read = info.signal_read; for (auto const ¶meter : info.parameters) { item.parameters = std::max(item.parameters, parameter.id); if (parameter.id == 6 && parameter.role == "texture") item.plate = true; if (parameter.id == 7) item.linked = true; } editor_includes::signal_speeds(entry.file, item.speeds); if (item.kind == "block") item.aspects = editor_includes::block_aspects(entry.file); tool.templates.push_back(std::move(item)); } std::sort(tool.templates.begin(), tool.templates.end(), [](signal_template const &A, signal_template const &B) { return std::make_tuple(kind_index(A.kind), A.lamps.empty(), A.mount != "mast", A.lamps.size(), A.lamps, A.name.size(), A.name) < std::make_tuple(kind_index(B.kind), B.lamps.empty(), B.mount != "mast", B.lamps.size(), B.lamps, B.name.size(), B.name); }); for (int i = 0; i < kKindCount; ++i) { tool.chosen[i] = -1; std::unordered_map sizes; for (auto const &item : tool.templates) if (item.kind == kKinds[i]) ++sizes[group_of(item.mount, item.lamps)]; auto best{-1}; for (int k = 0; k < static_cast(tool.templates.size()); ++k) { auto const &item{tool.templates[k]}; if (item.kind != kKinds[i]) continue; if (item.file == chosen[i]) { best = k; break; } if (best < 0 || sizes[group_of(item.mount, item.lamps)] > sizes[group_of(tool.templates[best].mount, tool.templates[best].lamps)]) best = k; } tool.chosen[i] = best; } return true; } editor_mode::signal_template const *editor_mode::signal_current() const { auto const &tool{m_signal}; if (tool.kind < 0 || tool.kind >= kKindCount) return nullptr; auto const index{tool.chosen[tool.kind]}; return index >= 0 && index < static_cast(tool.templates.size()) ? &tool.templates[index] : nullptr; } editor_mode::signal_template const *editor_mode::signal_armed() const { return m_signal.armed ? signal_current() : nullptr; } editor_mode::signal_template const *editor_mode::signal_template_of(std::string const &File) const { auto const file{ToLower(File)}; for (auto const &item : m_signal.templates) if (item.file == file) return &item; return nullptr; } bool editor_mode::signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const { if (Track.m_paths.empty() || Track.m_editorremoved) return false; auto const path{std::clamp(Path, 0, static_cast(Track.m_paths.size()) - 1)}; auto const parameter{nearest_on(Track, path, Point)}; auto const axis{bezier{Track.m_paths[path]}.point(parameter)}; auto const forward{forward_at(Track, path, parameter)}; auto const right{right_of(forward)}; auto side{Side}; if (side == 0) side = glm::dot(plan_of(Cursor) - plan_of(axis), right) >= 0.0 ? 1 : -1; auto facing{Facing}; if (facing == 0) facing = side; Spot.track = &Track; Spot.path = path; Spot.axis = axis; Spot.travel = forward * static_cast(facing); Spot.side = side; Spot.list = facing > 0 ? 2 : 1; Spot.offset = Template.mount == "gantry" ? 0.0 : EditorSettings.signal_offset(); auto const across{right * static_cast(side) * Spot.offset}; Spot.position = {axis.x + across.x, axis.y + kRailTop + (Template.mount == "gantry" ? static_cast(m_signal.height) : 0.0), axis.z + across.y}; auto const yaw{heading_of(Spot.travel) + 180.0}; Spot.yaw = yaw >= 360.0 ? yaw - 360.0 : yaw; return true; } std::string editor_mode::signal_intent_label(signal_template const &Template, signal_spot const &Spot) const { auto const name{m_signal.name[0] != '\0' ? std::string{m_signal.name} : std::string{"?"}}; auto const right{glm::dot(right_of(Spot.travel), plan_of(Spot.position) - plan_of(Spot.axis)) >= 0.0}; auto const where{right ? STR(" on the right of the trains") : STR(" on the left of the trains")}; auto variant{signal_variant(Template, Spot)}; variant = variant.substr(0, variant.rfind('.')); if (Template.read.empty()) return format(STR_C("Click: %s %s (%s)%s, the train doesn't read it"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag"); auto const event{editor_includes::substitute(Template.read, {name})}; auto const path{name_of(*Spot.track)}; return format(STR_C("Click: %s %s (%s)%s, read by %s %s of %s"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str(), editor_track::event_keyword(Spot.list), event.c_str(), path.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag"); } void editor_mode::signal_refresh(bool const Force) { auto &tool{m_signal}; auto const scanned{signal_scan(false)}; auto const now{ImGui::GetTime()}; if (false == Force && false == scanned && tool.standing_history == m_history.size() && tool.standing_redo == g_redo.size() && now - tool.standing_time < kRefreshPeriod) return; tool.standing_history = m_history.size(); tool.standing_redo = g_redo.size(); tool.standing_time = now; tool.standing = signal_standing_now(); if (tool.selected != 0 && std::none_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })) tool.selected = 0; } int editor_mode::signal_hit() const { screen_projection const projection; auto const &mouse{ImGui::GetIO().MousePos}; auto best{kSignalPick * kSignalPick}; int result{-1}; glm::dvec3 const camera{Global.pCamera.Pos}; for (int i = 0; i < static_cast(m_signal.standing.size()); ++i) { auto const &location{m_signal.standing[i].include.location}; if (glm::distance(location, camera) > kSignalRange) continue; ImVec2 foot, head; if (false == projection.project(location, foot) || false == projection.project(location + glm::dvec3{0.0, kSignalHead, 0.0}, head)) continue; ImVec2 const span{head.x - foot.x, head.y - foot.y}; auto const length2{span.x * span.x + span.y * span.y}; auto const t{length2 > 1e-6f ? std::clamp(((mouse.x - foot.x) * span.x + (mouse.y - foot.y) * span.y) / length2, 0.0f, 1.0f) : 0.0f}; auto const dx{foot.x + span.x * t - mouse.x}; auto const dy{foot.y + span.y * t - mouse.y}; auto const distance2{dx * dx + dy * dy}; if (distance2 < best) { best = distance2; result = i; } } return result; } void editor_mode::signal_press(track_intent const &Intent) { auto &tool{m_signal}; auto const &mouse{ImGui::GetIO().MousePos}; tool.pressed = {mouse.x, mouse.y}; tool.facing = 0; tool.status.clear(); if (Intent.what == track_intent::kind::signal_move) { if (Intent.index < 0 || Intent.index >= static_cast(tool.standing.size())) return; auto const &standing{tool.standing[Intent.index]}; tool.grab = plan_of(standing.include.location) - plan_of(cursor_level(standing.include.location.y)); tool.home_valid = signal_standing_spot(standing, tool.home); tool.moving = true; tool.moved_valid = false; signal_select(standing.include.instance); return; } auto const *armed{signal_armed()}; if (armed == nullptr || Intent.track == nullptr) return; if (false == signal_spot_at(*Intent.track, Intent.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, tool.press)) return; tool.placing = true; } void editor_mode::signal_move_update() { auto &tool{m_signal}; auto const &mouse{ImGui::GetIO().MousePos}; auto const dragged{glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) > kDragStart}; if (tool.placing) { auto const *armed{signal_armed()}; if (armed == nullptr || tool.press.track == nullptr || tool.press.track->m_editorremoved) { tool.placing = false; return; } if (false == dragged) return; auto const cursor{cursor_level(tool.press.axis.y)}; auto const forward{tool.press.travel * static_cast(tool.press.list == 2 ? 1 : -1)}; auto const along{glm::dot(plan_of(cursor) - plan_of(tool.press.axis), forward)}; if (std::abs(along) < kDragTravel) return; tool.facing = along > 0.0 ? 1 : -1; signal_spot spot; if (signal_spot_at(*tool.press.track, tool.press.path, tool.press.axis, cursor, tool.facing, tool.press.side, *armed, spot)) tool.press = spot; return; } if (false == tool.moving) return; if (false == dragged) return; auto const *found{signal_standing_of(tool.selected)}; if (found == nullptr) { tool.moved_valid = false; return; } auto const &location{found->include.location}; auto const cursor{cursor_level(location.y)}; glm::dvec3 const held{cursor.x + tool.grab.x, location.y, cursor.z + tool.grab.y}; // the track under the cursor; off every track, as it is when the signal is dragged beside its track, the track the // signal stands by auto *track{m_hover.track}; auto path{m_hover.path}; if (track == nullptr && tool.home_valid) { track = tool.home.track; path = tool.home.path; } tool.moved_valid = track != nullptr && signal_drop_at(*found, *track, path, held, 0, tool.moved); } std::vector editor_mode::signal_standing_now() const { std::vector standing; if (scene::Layers.empty() || m_signal.templates.empty()) return standing; auto const found{standing_templates([this](std::string const &File) { return signal_template_of(File) != nullptr; })}; std::unordered_map> readers; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved) continue; for (int list = 0; list < 6; ++list) for (auto const &event : editor_track::events(*track, list)) if (false == event.first.empty()) readers.emplace(ToLower(event.first), std::make_pair(track, list)); } for (auto const &include : found) { auto const *item{signal_template_of(include.file)}; if (item == nullptr) continue; signal_standing signal; signal.include = include; signal.kind = kind_index(item->kind); signal.name = include.values.empty() ? std::string{} : include.values.front(); if (false == item->read.empty()) { signal.read = editor_includes::substitute(item->read, include.values); if (auto const reader{readers.find(ToLower(signal.read))}; reader != readers.end()) { signal.track = reader->second.first; signal.list = reader->second.second; } } standing.push_back(std::move(signal)); } return standing; } editor_mode::signal_standing const *editor_mode::signal_standing_of(scene::instance_handle const Instance) const { auto const &standing{m_signal.standing}; auto const found{std::find_if(standing.begin(), standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == Instance; })}; return found != standing.end() ? &*found : nullptr; } bool editor_mode::signal_drop_at(signal_standing const &Standing, TTrack &Track, int const Path, glm::dvec3 const &Point, int const Side, signal_spot &Spot) const { auto const *item{signal_template_of(Standing.include.file)}; if (item == nullptr || Track.m_paths.empty() || Track.m_editorremoved) return false; auto const path{std::clamp(Path, 0, static_cast(Track.m_paths.size()) - 1)}; auto const forward{forward_at(Track, path, nearest_on(Track, path, Point))}; auto const facing{glm::dot(forward, travel_of(Standing.include.yaw)) >= 0.0 ? 1 : -1}; return signal_spot_at(Track, path, Point, Point, facing, Side * facing, *item, Spot); } bool editor_mode::signal_standing_spot(signal_standing const &Standing, signal_spot &Spot) const { auto const &location{Standing.include.location}; TTrack *track{nullptr}; int path{0}; auto best{kSignalReach}; auto const consider = [&](TTrack *Track) { if (Track == nullptr || Track->m_editorremoved) return; for (int p = 0; p < static_cast(Track->m_paths.size()); ++p) { bezier const curve{Track->m_paths[p]}; auto const low{glm::min(glm::min(plan_of(curve.p0), plan_of(curve.p1)), glm::min(plan_of(curve.p2), plan_of(curve.p3)))}; auto const high{glm::max(glm::max(plan_of(curve.p0), plan_of(curve.p1)), glm::max(plan_of(curve.p2), plan_of(curve.p3)))}; if (glm::length(glm::max(glm::max(low - plan_of(location), plan_of(location) - high), glm::dvec2{0.0})) >= best) continue; auto const distance{geometry::plan_distance(curve.point(nearest_on(*Track, p, location)), location)}; if (distance < best) { best = distance; track = Track; path = p; } } }; consider(Standing.track); if (track == nullptr) for (auto *candidate : simulation::Paths.sequence()) consider(candidate); if (track == nullptr) return false; auto const axis{bezier{track->m_paths[path]}.point(nearest_on(*track, path, location))}; auto const side{glm::dot(plan_of(location) - plan_of(axis), right_of(travel_of(Standing.include.yaw))) >= 0.0 ? 1 : -1}; if (false == signal_drop_at(Standing, *track, path, location, side, Spot)) return false; Spot.offset = geometry::plan_distance(location, axis); Spot.position = location; Spot.yaw = Standing.include.yaw; return true; } void editor_mode::signal_release() { auto &tool{m_signal}; if (tool.placing) { tool.placing = false; if (auto const *armed{signal_armed()}; armed != nullptr && tool.press.track != nullptr && false == tool.press.track->m_editorremoved) signal_place(tool.press, *armed); return; } if (false == tool.moving) return; tool.moving = false; auto const &mouse{ImGui::GetIO().MousePos}; if (glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) <= kDragStart) return; if (false == tool.moved_valid) { tool.status = refused(STR_C("Drop the signal by a track")); return; } tool.moved_valid = false; if (auto const *found{signal_standing_of(tool.selected)}) signal_move_to(*found, tool.moved, STR_C("Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s moved, Ctrl+Z takes it back")); } bool editor_mode::signal_move_to(signal_standing const &Standing, signal_spot const &Spot, char const *Read, char const *Unread) { auto &tool{m_signal}; auto const instance{Standing.include.instance}; if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance)) { tool.status = refused(STR_C("This signal is in a file the editor doesn't change, it can't be moved")); return false; } auto const &included{scene::Layers.instance(instance)}; auto file{*included.file}; signal_spot before; auto const *item{signal_template_of(Standing.include.file)}; if (item != nullptr && signal_standing_spot(Standing, before) && travel_side(before.side, before.list) != travel_side(Spot.side, Spot.list)) file = signal_variant(*item, Spot); auto values{Standing.include.values}; auto const local{Spot.position - included.context.offset}; values[1] = editor_includes::number(local.x); values[2] = editor_includes::number(local.y); values[3] = editor_includes::number(local.z); values[4] = editor_includes::number(Spot.yaw); auto const *next{signal_template_of(file)}; auto const read{next != nullptr && false == next->read.empty() ? editor_includes::substitute(next->read, values) : Standing.read}; auto directive{scene::Layers.directive(instance)}; std::vector> states; if (false == Standing.read.empty() && Spot.track->Dynamics.empty()) { signal_detach_events(Standing.read, states); if (std::none_of(states.begin(), states.end(), [&](auto const &State) { return State.first == Spot.track; })) states.emplace_back(Spot.track, editor_track::capture(*Spot.track)); editor_track::events(*Spot.track, Spot.list).emplace_back(ToLower(read), nullptr); std::string missing; editor_track::bind_events(*Spot.track, missing); Spot.track->mark_dirty(); } auto const name{Standing.name}; auto const node{Standing.include.file}; set_include_directive(instance, editor_includes::compose_directive(file, values)); if (states.empty()) { EditorSnapshot snap; snap.directives.emplace_back(instance, std::move(directive)); snap.node_name = node; trim_history(); m_history.push_back(std::move(snap)); g_redo.clear(); } else { push_track_snapshot(std::move(states)); m_history.back().directives.emplace_back(instance, std::move(directive)); } signal_refresh(true); signal_select(instance); auto const *moved{signal_standing_of(instance)}; tool.status = moved != nullptr && moved->track != nullptr ? format(Read, name.c_str(), editor_track::event_keyword(moved->list), moved->read.c_str(), name_of(*moved->track).c_str()) : format(Unread, name.c_str()); return true; } bool editor_mode::signal_flip(scene::instance_handle const Instance) { auto const *standing{signal_standing_of(Instance)}; signal_spot spot; if (standing == nullptr || false == signal_standing_spot(*standing, spot)) { m_signal.status = refused(STR_C("No track by the signal to take it across")); return false; } auto const across{plan_of(spot.position) - plan_of(spot.axis)}; auto const mirrored{plan_of(spot.axis) + spot.travel * glm::dot(across, spot.travel) - right_of(spot.travel) * glm::dot(across, right_of(spot.travel))}; spot.side = -spot.side; spot.position = {mirrored.x, spot.position.y, mirrored.y}; return signal_move_to(*standing, spot, STR_C("Signal %s moved to the other side of the track, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s moved to the other side of the track, Ctrl+Z takes it back")); } bool editor_mode::signal_turn(scene::instance_handle const Instance) { auto const *standing{signal_standing_of(Instance)}; signal_spot spot; if (standing == nullptr || false == signal_standing_spot(*standing, spot)) { m_signal.status = refused(STR_C("No track by the signal to turn it to")); return false; } auto const *item{signal_template_of(standing->include.file)}; auto const position{spot.position}; if (item == nullptr || false == signal_spot_at(*spot.track, spot.path, spot.axis, spot.axis, spot.list == 2 ? -1 : 1, spot.side, *item, spot)) return false; spot.position = position; spot.yaw = std::fmod(standing->include.yaw + 180.0, 360.0); return signal_move_to(*standing, spot, STR_C("Signal %s turned to the trains of the other direction, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s turned to the trains of the other direction, Ctrl+Z takes it back")); } bool editor_mode::signal_swap(scene::instance_handle const Instance, std::string const &File) { auto &tool{m_signal}; auto const *standing{signal_standing_of(Instance)}; if (standing == nullptr) return false; auto const name{standing->name}; auto const before{standing->include.file}; signal_changes changes; changes.files.emplace_back(Instance, File); std::string error; signal_batch(changes, {}, {}, error); signal_refresh(true); auto const *swapped{signal_standing_of(Instance)}; if (false == error.empty() || swapped == nullptr || swapped->include.file == before) { tool.status = error.empty() ? format(STR_C("Signal %s can't take the template %s"), name.c_str(), File.c_str()) : error; return false; } signal_select(Instance); tool.status = format(STR_C("Signal %s got the template %s, Ctrl+Z takes it back"), name.c_str(), swapped->include.file.c_str()); return true; } std::string editor_mode::signal_variant(signal_template const &Template, signal_spot const &Spot) const { auto file{Template.file}; auto const wanted{lean_wanted(glm::dot(right_of(Spot.travel), plan_of(Spot.position) - plan_of(Spot.axis)) >= 0.0 ? 1 : -1)}; if (lean_of(Template.lean) == 0 || lean_of(Template.lean) == wanted) return file; auto const group{group_of(Template.mount, Template.lamps)}; auto best{std::numeric_limits::max()}; for (auto const &other : m_signal.templates) { if (other.kind != Template.kind || group_of(other.mount, other.lamps) != group || lean_of(other.lean) != wanted || other.speeds != Template.speeds) continue; auto const distance{edit_distance(other.name, Template.name)}; if (distance < best) { best = distance; file = other.file; } } return file; } std::string editor_mode::signal_next_name(std::string const &Name) const { if (Name.empty()) return Name; auto digits{Name.size()}; while (digits > 0 && std::isdigit(static_cast(Name[digits - 1]))) --digits; if (digits < Name.size() && Name.size() - digits < 9) return Name.substr(0, digits) + std::to_string(std::stoi(Name.substr(digits)) + 1); auto const last{static_cast(Name.back())}; if (std::isalpha(last) && last != 'z' && last != 'Z') return Name.substr(0, Name.size() - 1) + static_cast(last + 1); return Name + "2"; } bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &Template) { auto &tool{m_signal}; std::string name{tool.name}; if (name.empty() || name.find_first_of(" \t\";") != std::string::npos) { tool.status = refused(STR_C("Give the signal a name without spaces first")); return false; } signal_refresh(true); auto const taken = [&](std::string const &Name) { return std::any_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return ToLower(Standing.name) == ToLower(Name); }); }; for (int guard = 0; taken(name) && guard < 1000; ++guard) name = signal_next_name(name); auto const file{signal_variant(Template, Spot)}; template_item item; item.file = file; item.location = Spot.position; item.yaw = Spot.yaw; item.described = true; item.values["name"] = name; if (Template.plate) item.values["#6"] = plate_for(tool.plate, name); if (Template.linked) item.values["#7"] = tool.linked[0] != '\0' ? std::string{tool.linked} : std::string{"none"}; std::string error; if (place_templates({item}, error, output_layer("srk")) == 0) { tool.status = refused(STR_C("The signal couldn't be placed: ") + error); return false; } auto const instance{m_history.back().instances.front()}; auto const *placed{signal_template_of(file)}; auto const read{placed != nullptr && false == placed->read.empty() ? editor_includes::substitute(placed->read, {name}) : std::string{}}; auto const path{name_of(*Spot.track)}; std::string reason; if (read.empty()) tool.status = format(STR_C("Signal %s placed (%s), the train doesn't read it, Ctrl+Z takes it back"), name.c_str(), file.c_str()); else if (false == Spot.track->Dynamics.empty()) tool.status = format(STR_C("Signal %s placed (%s), but vehicles stand on the path: add %s to its %s by hand, Ctrl+Z takes it back"), name.c_str(), file.c_str(), read.c_str(), editor_track::event_keyword(Spot.list)); else if (false == scene::Layers.editable(Spot.track, &reason)) tool.status = format(STR_C("Signal %s placed (%s), but the path can't take its event: %s, Ctrl+Z takes it back"), name.c_str(), file.c_str(), reason.c_str()); else { auto snap{std::move(m_history.back())}; m_history.pop_back(); auto const before{editor_track::capture(*Spot.track)}; editor_track::events(*Spot.track, Spot.list).emplace_back(ToLower(read), nullptr); std::string missing; editor_track::bind_events(*Spot.track, missing); Spot.track->mark_dirty(); push_track_snapshot({{Spot.track, before}}); m_history.back().instances = std::move(snap.instances); tool.status = format(STR_C("Signal %s placed (%s), read by %s %s of %s, Ctrl+Z takes it back. Its logic works after the scenery is saved and opened again"), name.c_str(), file.c_str(), editor_track::event_keyword(Spot.list), read.c_str(), path.c_str()); } std::snprintf(tool.name, sizeof(tool.name), "%s", signal_next_name(name).c_str()); signal_refresh(true); signal_select(instance); first_step_done(first_step::signal); return true; } void editor_mode::signal_detach_events(std::string const &Read, std::vector> &States) { auto const name{ToLower(Read)}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved) continue; bool carries{false}; for (int list = 0; list < 6 && false == carries; ++list) for (auto const &event : editor_track::events(*track, list)) carries = carries || ToLower(event.first) == name; if (false == carries) continue; if (std::none_of(States.begin(), States.end(), [&](auto const &State) { return State.first == track; })) States.emplace_back(track, editor_track::capture(*track)); for (int list = 0; list < 6; ++list) { auto &events{editor_track::events(*track, list)}; events.erase(std::remove_if(events.begin(), events.end(), [&](auto const &Event) { return ToLower(Event.first) == name; }), events.end()); } std::string missing; editor_track::bind_events(*track, missing); track->mark_dirty(); } } void editor_mode::orphans_register(std::vector const &Removed, std::vector const &Kept) { auto const carries = [](TTrack &Track) { for (int list = 0; list < 6; ++list) for (auto const &event : editor_track::events(Track, list)) if (false == event.first.empty()) return true; return false == Track.Isolated.empty(); }; // a record of a track removed again, after an undo brought it back, is the step's to make anew m_orphans.erase(std::remove_if(m_orphans.begin(), m_orphans.end(), [&](orphan_record const &Record) { return std::find(Removed.begin(), Removed.end(), Record.track) != Removed.end(); }), m_orphans.end()); // a track which took the circuits over, removed by the step, leaves them to what the step made for (auto &record : m_orphans) if (std::any_of(record.given.begin(), record.given.end(), [&](TTrack const *Track) { return std::find(Removed.begin(), Removed.end(), Track) != Removed.end(); })) { record.circuits.clear(); record.given.clear(); } std::vector gone; for (auto *track : Removed) if (track != nullptr && track->m_editorremoved && false == track->m_paths.empty() && carries(*track)) gone.push_back(track); if (gone.empty()) return; // what the step moved onto the tracks left isn't lost std::unordered_set carried; for (auto *track : simulation::Paths.sequence()) if (track != nullptr && false == track->m_editorremoved) for (int list = 0; list < 6; ++list) for (auto const &event : editor_track::events(*track, list)) carried.insert(ToLower(event.first)); std::unordered_set circuits; for (auto *track : Kept) if (track != nullptr && false == track->m_editorremoved) for (auto const *isolated : track->Isolated) if (isolated != nullptr) circuits.insert(ToLower(isolated->asName)); for (auto *track : gone) { orphan_record record; record.track = track; record.fresh = true; for (int list = 0; list < 6; ++list) for (auto const &event : editor_track::events(*track, list)) { auto const name{ToLower(event.first)}; if (false == name.empty() && carried.count(name) == 0 && std::none_of(record.events.begin(), record.events.end(), [&](auto const &Event) { return Event.first == name; })) record.events.emplace_back(name, list); } for (auto const *isolated : track->Isolated) if (isolated != nullptr && circuits.count(ToLower(isolated->asName)) == 0) record.circuits.push_back(isolated->asName); if (record.events.empty() && record.circuits.empty()) continue; m_orphans.push_back(std::move(record)); } if (m_orphans.size() > kOrphanRecords) m_orphans.erase(m_orphans.begin(), m_orphans.begin() + static_cast(m_orphans.size() - kOrphanRecords)); } void editor_mode::orphans_take_over(std::vector const &Created, std::vector const &Changed) { // a record waits while its track stays deleted, so the step laying another one in its place, undone and laid again, gets it again if (std::none_of(m_orphans.begin(), m_orphans.end(), [](orphan_record const &Record) { return Record.track->m_editorremoved; })) return; std::vector candidates; for (auto const *tracks : {&Created, &Changed}) for (auto *track : *tracks) if (track != nullptr && false == track->m_editorremoved && false == track->m_paths.empty() && track->Dynamics.empty() && editor_track::is_supported(*track) && std::find(candidates.begin(), candidates.end(), track) == candidates.end()) candidates.push_back(track); if (candidates.empty() && std::none_of(m_orphans.begin(), m_orphans.end(), [](orphan_record const &Record) { return Record.fresh; })) return; std::unordered_set carried; for (auto *track : simulation::Paths.sequence()) if (track != nullptr && false == track->m_editorremoved) for (int list = 0; list < 6; ++list) for (auto const &event : editor_track::events(*track, list)) carried.insert(ToLower(event.first)); // not the signals standing as last listed, nor the templates once found: the step taken may hold one of them; looked for once an event // waits std::vector signals; std::unordered_map readers; auto known{false}; auto const reader_of = [&](std::string const &Name) -> signal_standing const * { if (false == known) { known = true; if (m_signal.templates.empty()) signal_scan(false); signals = signal_standing_now(); for (auto const &standing : signals) if (false == standing.read.empty()) readers.emplace(ToLower(standing.read), &standing); } auto const found{readers.find(Name)}; return found != readers.end() ? found->second : nullptr; }; std::vector taken; std::vector left; std::size_t taken_events{0}; std::size_t left_events{0}; std::vector taken_circuits; std::vector left_circuits; std::size_t deleted{0}; for (auto &record : m_orphans) { if (false == record.track->m_editorremoved) { record.fresh = false; continue; } if (candidates.empty() && false == record.fresh) continue; auto const &old{*record.track}; deleted += record.fresh ? 1 : 0; for (auto const &[name, list] : record.events) { if (carried.count(name) > 0) continue; auto const *standing{reader_of(name)}; auto got{false}; // an event no longer there, of a signal taken away, stays off the tracks if (false == candidates.empty() && simulation::Events.FindEvent(name) != nullptr) { // the place on the old axis: by the signal reading it, else the middle of the track int old_path{0}; double old_parameter{0.5}; auto const by_signal{standing != nullptr && plan_gap(old, standing->include.location, old_path, old_parameter) <= kSignalReach}; if (false == by_signal) { old_path = 0; old_parameter = 0.5; } auto const anchor{bezier{old.m_paths[old_path]}.point(old_parameter)}; auto const old_forward{forward_at(old, old_path, old_parameter)}; TTrack *best{nullptr}; int best_path{0}; double best_parameter{0.0}; auto nearest{kOrphanReach}; for (auto *track : candidates) { int path{0}; double parameter{0.0}; auto const gap{plan_gap(*track, anchor, path, parameter)}; if (gap <= nearest && std::abs(glm::dot(forward_at(*track, path, parameter), old_forward)) > 0.9) { nearest = gap; best = track; best_path = path; best_parameter = parameter; } } if (best != nullptr) { // laid the other way round, the trains running towards its end read it at its start auto const reversed{glm::dot(forward_at(*best, best_path, best_parameter), old_forward) < 0.0}; auto target{reversed && list % 3 != 0 ? list + (list % 3 == 1 ? 1 : -1) : list}; if (by_signal) { auto const &location{standing->include.location}; auto const axis{bezier{best->m_paths[best_path]}.point(nearest_on(*best, best_path, location))}; auto const side{glm::dot(plan_of(location) - plan_of(axis), right_of(travel_of(standing->include.yaw))) >= 0.0 ? 1 : -1}; signal_spot spot; if (signal_drop_at(*standing, *best, best_path, location, side, spot)) target = spot.list + (list >= 3 ? 3 : 0); } editor_track::events(*best, target).emplace_back(name, nullptr); std::string missing; editor_track::bind_events(*best, missing); best->mark_dirty(); carried.insert(name); got = true; } } if (got) { if (standing != nullptr) taken.push_back(standing->name); else ++taken_events; } else if (record.fresh) { if (standing != nullptr) left.push_back(standing->name); else ++left_events; } } if (false == record.circuits.empty() && std::all_of(record.given.begin(), record.given.end(), [](TTrack const *Track) { return Track->m_editorremoved; })) { auto got{false}; // a track lying in the place of the old one for the most part: a new one, in the circuit of the track it was made from, // or one without a circuit for (auto *track : candidates) { auto const made{std::find(Created.begin(), Created.end(), track) != Created.end()}; if ((false == made && false == track->Isolated.empty()) || false == plan_overlaps(*track, old, 2.0)) continue; auto const same{track->Isolated.size() == record.circuits.size() && std::equal(record.circuits.begin(), record.circuits.end(), track->Isolated.begin(), [](std::string const &Name, TIsolated const *Isolated) { return Isolated != nullptr && ToLower(Isolated->asName) == ToLower(Name); })}; if (same) continue; std::size_t on{0}; std::size_t all{0}; for (auto const &path : track->m_paths) { bezier const curve{path}; for (int k = 0; k <= 32; ++k) { int old_path{0}; double old_parameter{0.0}; ++all; if (plan_gap(old, curve.point(k / 32.0), old_path, old_parameter) <= kOrphanReach) ++on; } } if (all == 0 || on * 2 < all) continue; editor_track::isolated(*track, record.circuits); if (std::find(record.given.begin(), record.given.end(), track) == record.given.end()) record.given.push_back(track); got = true; } if (got) taken_circuits.insert(taken_circuits.end(), record.circuits.begin(), record.circuits.end()); else if (record.fresh) left_circuits.insert(left_circuits.end(), record.circuits.begin(), record.circuits.end()); } record.fresh = false; } auto const parts = [](std::vector const &Signals, std::size_t const Events, std::vector const &Circuits) { std::vector part; if (false == Signals.empty()) part.push_back(format(STR_P("the read of signal %s", "the reads of signals %s", static_cast(Signals.size())), listed(Signals).c_str())); if (Events > 0) part.push_back(format(STR_P("%zu event", "%zu events", static_cast(Events)), Events)); if (false == Circuits.empty()) part.push_back(format(STR_P("the track circuit %s", "the track circuits %s", static_cast(Circuits.size())), listed(Circuits).c_str())); return listed(part); }; std::string note; if (auto const got{parts(taken, taken_events, taken_circuits)}; false == got.empty()) note = format(STR_C("Taken over from the deleted track: %s"), got.c_str()); if (auto const lost{parts(left, left_events, left_circuits)}; false == lost.empty()) note += (note.empty() ? "" : "\n") + format(STR_P("The deleted track carried %s: a track laid in its place takes it over, Ctrl+Z brings it back", "The deleted tracks carried %s: tracks laid in their place take it over, Ctrl+Z brings them back", static_cast(std::max(deleted, 1))), lost.c_str()); if (false == note.empty()) m_orphan_note += (m_orphan_note.empty() ? "" : "\n") + note; } std::size_t editor_mode::signal_reattach(std::vector const &Instances) { signal_refresh(true); std::vector> states; std::size_t attached{0}; for (auto const instance : Instances) { auto const *standing{signal_standing_of(instance)}; if (standing == nullptr || standing->read.empty() || standing->track != nullptr || simulation::Events.FindEvent(ToLower(standing->read)) == nullptr) continue; signal_spot spot; if (false == signal_standing_spot(*standing, spot) || spot.track == nullptr || false == spot.track->Dynamics.empty()) continue; if (std::none_of(states.begin(), states.end(), [&](auto const &State) { return State.first == spot.track; })) states.emplace_back(spot.track, editor_track::capture(*spot.track)); auto &events{editor_track::events(*spot.track, spot.list)}; if (std::none_of(events.begin(), events.end(), [&](auto const &Event) { return ToLower(Event.first) == ToLower(standing->read); })) events.emplace_back(ToLower(standing->read), nullptr); std::string missing; editor_track::bind_events(*spot.track, missing); spot.track->mark_dirty(); ++attached; } if (states.empty()) return 0; push_track_snapshot(std::move(states)); signal_refresh(true); return attached; } bool editor_mode::signal_delete(scene::instance_handle const Instance) { if (false == scene::Layers.tracked(Instance) || false == scene::Layers.removable(Instance)) return false; signal_scan(); auto const &included{scene::Layers.instance(Instance)}; auto const *item{signal_template_of(*included.file)}; if (item == nullptr || item->read.empty()) return false; std::string file; std::vector values; if (false == editor_includes::parse_directive(scene::Layers.directive(Instance), file, values)) return false; auto const read{editor_includes::substitute(item->read, values)}; std::vector> states; signal_detach_events(read, states); if (states.empty()) return false; scene::Layers.removed(Instance, true); push_track_snapshot(std::move(states)); m_history.back().instances.push_back(Instance); ui()->set_status(format(STR_C("Signal %s removed with its event %s, Ctrl+Z brings both back"), values.empty() ? "" : values.front().c_str(), read.c_str())); select_include(0); m_signal.selected = 0; signal_refresh(true); return true; } void editor_mode::signal_renamed(std::string const &Before, std::string const &After, EditorSnapshot &Snapshot) { auto const name{ToLower(Before)}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved) continue; bool carries{false}; for (int list = 0; list < 6 && false == carries; ++list) for (auto const &event : editor_track::events(*track, list)) carries = carries || ToLower(event.first) == name; if (false == carries) continue; if (std::none_of(Snapshot.tracks.begin(), Snapshot.tracks.end(), [&](auto const &State) { return State.first == track; })) Snapshot.tracks.emplace_back(track, editor_track::capture(*track)); for (int list = 0; list < 6; ++list) for (auto &event : editor_track::events(*track, list)) if (ToLower(event.first) == name) event = {ToLower(After), nullptr}; std::string missing; editor_track::bind_events(*track, missing); track->mark_dirty(); } if (Snapshot.tracks.empty() || Snapshot.instance == 0) return; Snapshot.directives.emplace_back(Snapshot.instance, std::move(Snapshot.serialized)); Snapshot.instance = 0; Snapshot.serialized.clear(); Snapshot.action = EditorSnapshot::Action::TrackEdit; Snapshot.node_ptr = Snapshot.tracks.front().first; Snapshot.node_name = Snapshot.tracks.front().first->name(); } bool editor_mode::standing_signals(std::vector &Signals, bool const Wait) { Signals.clear(); signal_scan(Wait); if (m_signal.templates.empty()) return false; signal_refresh(true); for (auto const &standing : m_signal.standing) { if (standing.kind < 0) continue; std::string file; std::vector values; auto const *item{signal_template_of(standing.include.file)}; auto const linked{item != nullptr && item->linked && editor_includes::parse_directive(scene::Layers.directive(standing.include.instance), file, values) && values.size() > 6}; Signals.push_back({standing.include.instance, standing.name, kKinds[standing.kind], standing.include.location, standing.include.yaw, linked ? values[6] : std::string{}}); auto &signal{Signals.back()}; signal.file = standing.include.file; signal.track = standing.track; signal.facing = standing.track == nullptr ? 0 : standing.list % 3 == 2 ? 1 : standing.list % 3 == 1 ? -1 : 0; signal.read = standing.read; } return true; } bool editor_mode::signal_heads(std::vector &Heads, bool const Wait) { Heads.clear(); signal_scan(Wait); if (m_signal.templates.empty()) return false; for (int index = 0; index < static_cast(m_signal.templates.size()); ++index) { auto const &item{m_signal.templates[index]}; if (item.lamps.empty()) continue; auto const kind{kind_index(item.kind)}; Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, kind >= 0 && m_signal.chosen[kind] == index, item.aspects, item.lean}); } return true; } std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) { auto &tool{m_signal}; signal_scan(); signal_refresh(true); std::vector items; std::vector> reads; for (auto const &order : Changes.placed) { auto const kind{kind_index(order.kind)}; auto const *item{kind >= 0 && tool.chosen[kind] >= 0 && tool.chosen[kind] < static_cast(tool.templates.size()) ? &tool.templates[tool.chosen[kind]] : nullptr}; if (auto const *wanted{order.file.empty() ? nullptr : signal_template_of(order.file)}; wanted != nullptr && wanted->kind == order.kind) item = wanted; if (item == nullptr) { Error = format(STR_C("no template of the kind %s"), STR(kKindLabels[std::max(0, kind)]).c_str()); break; } signal_spot spot; if (order.track == nullptr || false == signal_spot_at(*order.track, order.path, order.point, order.point, order.facing, order.side, *item, spot)) continue; template_item placing; placing.file = signal_variant(*item, spot); placing.location = spot.position; placing.yaw = spot.yaw; placing.described = true; placing.values["name"] = order.name; auto const *variant{signal_template_of(placing.file)}; if (variant == nullptr) variant = item; if (variant->plate) placing.values["#6"] = plate_for(std::string{}, order.plate); if (variant->linked) placing.values["#7"] = order.linked.empty() ? std::string{"none"} : order.linked; items.push_back(std::move(placing)); reads.emplace_back(spot, variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, {order.name})); } if (false == Error.empty()) { // nothing changed but the tracks cut for them, a step of undo if (false == States.empty() || false == Created.empty()) push_track_snapshot(std::move(States), std::move(Created)); return 0; } // the names passed round among the signals go through ones of their own first, or a read event renamed would be renamed again items.insert(items.end(), Changes.others.begin(), Changes.others.end()); EditorSnapshot step; step.tracks = std::move(States); std::vector> directives; std::unordered_set changed; auto const record = [&](scene::instance_handle const Instance, std::string const &Before) { if (changed.insert(Instance).second) directives.emplace_back(Instance, Before); }; for (auto const temporary : {true, false}) for (auto const &rename : Changes.renamed) { auto const *standing{signal_standing_of(rename.instance)}; auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; auto const before{scene::Layers.directive(rename.instance)}; std::string file; std::vector values; if (item == nullptr || false == editor_includes::parse_directive(before, file, values) || values.empty()) continue; if (temporary && ToLower(values[0]) == ToLower(rename.name)) continue; auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; values[0] = rename.name + (temporary ? "~" : ""); if (item->plate && values.size() > 5 && false == rename.plate.empty()) values[5] = plate_for(std::string{}, rename.plate); if (item->linked && values.size() > 6 && false == rename.linked.empty()) values[6] = rename.linked; auto const read_after{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; auto const directive{editor_includes::compose_directive(file, values)}; if (directive == before) continue; record(rename.instance, before); set_include_directive(rename.instance, directive); if (read_before != read_after) signal_renamed(read_before, read_after, step); } for (auto const &[instance, values] : Changes.values) { auto const before{scene::Layers.directive(instance)}; std::string file; std::vector standing; if (false == editor_includes::parse_directive(before, file, standing)) continue; if (auto const directive{editor_includes::compose_directive(file, values)}; directive != before) { record(instance, before); set_include_directive(instance, directive); } } for (auto const &[instance, wanted] : Changes.files) { auto const *standing{signal_standing_of(instance)}; auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; auto const *chosen{signal_template_of(wanted)}; auto const before{scene::Layers.directive(instance)}; std::string file; std::vector values; if (item == nullptr || chosen == nullptr || chosen->kind != item->kind || false == editor_includes::parse_directive(before, file, values) || values.empty()) continue; signal_spot spot; auto const *variant{signal_standing_spot(*standing, spot) ? signal_template_of(signal_variant(*chosen, spot)) : chosen}; if (variant == nullptr) variant = chosen; if (variant->file == item->file) continue; include_info had; include_info info; std::string missing; editor_includes::load(item->file, had, missing); editor_includes::load(variant->file, info, missing); auto const role_of = [](include_info &Info, int const Id) { return Info.parameter(Id).role; }; std::vector after(std::max(variant->parameters, 1)); for (int id = 1; id <= static_cast(after.size()); ++id) { auto &value{after[id - 1]}; auto const role{role_of(info, id)}; for (int old = 1; old <= static_cast(values.size()) && value.empty(); ++old) if (role != "free" && role != "text" && role_of(had, old) == role) value = values[old - 1]; if (id == 7 && variant->linked) value = item->linked && values.size() > 6 ? values[6] : std::string{"none"}; if (value.empty() && id == 6 && variant->plate) value = plate_for(std::string{}, values[0].substr(values[0].rfind('_') + 1)); if (value.empty()) value = info.parameter(id).value.empty() ? std::string{"none"} : info.parameter(id).value; } auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; auto const read_after{variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, after)}; record(instance, before); set_include_directive(instance, editor_includes::compose_directive(variant->file, after)); if (false == read_before.empty() && false == read_after.empty() && ToLower(read_before) != ToLower(read_after)) signal_renamed(read_before, read_after, step); } std::unordered_set const created(Created.begin(), Created.end()); std::vector removed; for (auto const instance : Changes.removed) { if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance) || scene::Layers.instance(instance).removed) continue; auto const *standing{signal_standing_of(instance)}; auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; std::string file; std::vector values; if (item != nullptr && false == item->read.empty() && editor_includes::parse_directive(scene::Layers.directive(instance), file, values)) { std::vector> states; signal_detach_events(editor_includes::substitute(item->read, values), states); for (auto &state : states) if (created.count(state.first) == 0 && std::none_of(step.tracks.begin(), step.tracks.end(), [&](auto const &Taken) { return Taken.first == state.first; })) step.tracks.push_back(std::move(state)); } scene::Layers.removed(instance, true); removed.push_back(instance); } std::vector instances; if (Error.empty() && false == items.empty()) { if (place_templates(items, Error, output_layer("srk")) == 0) reads.clear(); else { auto placed{std::move(m_history.back())}; m_history.pop_back(); instances = std::move(placed.instances); } } else reads.clear(); for (auto const &[spot, read] : reads) { if (read.empty() || false == spot.track->Dynamics.empty() || false == scene::Layers.editable(spot.track)) continue; if (created.count(spot.track) == 0 && std::none_of(step.tracks.begin(), step.tracks.end(), [&](auto const &State) { return State.first == spot.track; })) step.tracks.emplace_back(spot.track, editor_track::capture(*spot.track)); editor_track::events(*spot.track, spot.list).emplace_back(ToLower(read), nullptr); std::string missing; editor_track::bind_events(*spot.track, missing); spot.track->mark_dirty(); } auto const count{instances.size()}; instances.insert(instances.end(), removed.begin(), removed.end()); if (step.tracks.empty() && Created.empty() && instances.empty() && directives.empty()) return 0; if (step.tracks.empty() && Created.empty()) { EditorSnapshot only; auto const *file{scene::Layers.instance(directives.empty() ? instances.front() : directives.front().first).file}; only.node_name = file != nullptr ? *file : std::string{}; trim_history(); m_history.push_back(std::move(only)); g_redo.clear(); } else push_track_snapshot(std::move(step.tracks), std::move(Created)); auto &pushed{m_history.back()}; pushed.instances = std::move(instances); pushed.directives.insert(pushed.directives.end(), directives.begin(), directives.end()); signal_refresh(true); return count; } void editor_mode::signal_select(scene::instance_handle const Instance) { m_signal.selected = Instance; if (Instance != 0) select_include(Instance); } void editor_mode::render_signal_palette() { auto &tool{m_signal}; ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##signalfilter", STR_C("filter: name, w24, sbl..."), tool.filter, sizeof(tool.filter)); auto const filter{ToLower(tool.filter)}; auto const *current{signal_current()}; std::vector> groups; std::vector keys; for (int i = 0; i < static_cast(tool.templates.size()); ++i) { auto const &item{tool.templates[i]}; if (item.kind != kKinds[tool.kind]) continue; if (false == filter.empty() && item.name.find(filter) == std::string::npos && ToLower(item.description).find(filter) == std::string::npos) continue; auto const key{group_of(item.mount, item.lamps)}; auto const found{std::find(keys.begin(), keys.end(), key)}; if (found == keys.end()) { keys.push_back(key); groups.push_back({i}); } else groups[found - keys.begin()].push_back(i); } if (groups.empty()) { ImGui::TextDisabled("%s", STR_C("No template of this kind matches the filter")); return; } ImVec2 const tile{52.0f, 86.0f}; auto const spacing{ImGui::GetStyle().ItemSpacing.x}; ImGui::BeginChild("##signaltiles", ImVec2(0.0f, std::min(2.0f * (tile.y + 8.0f) + 20.0f, ImGui::GetContentRegionAvail().y * 0.45f)), true); auto const columns{std::max(1, static_cast((ImGui::GetContentRegionAvail().x + spacing) / (tile.x + spacing)))}; auto *draw{ImGui::GetWindowDrawList()}; for (int g = 0; g < static_cast(groups.size()); ++g) { auto const &group{groups[g]}; auto const &first{tool.templates[group.front()]}; auto const chosen{current != nullptr && group_of(current->mount, current->lamps) == keys[g] && current->kind == first.kind}; if (g % columns != 0) ImGui::SameLine(); ImGui::PushID(g); auto const origin{ImGui::GetCursorScreenPos()}; if (ImGui::InvisibleButton("##tile", tile)) { if (chosen && tool.armed) tool.armed = false; else { tool.chosen[tool.kind] = chosen ? tool.chosen[tool.kind] : group.front(); tool.armed = true; } } auto const hovered{ImGui::IsItemHovered()}; auto const armed{chosen && tool.armed}; draw->AddRectFilled(origin, ImVec2(origin.x + tile.x, origin.y + tile.y), armed ? IM_COL32(45, 110, 60, 255) : chosen ? IM_COL32(60, 90, 140, 255) : hovered ? IM_COL32(60, 60, 60, 255) : IM_COL32(40, 40, 40, 255), 4.0f); draw_signal_icon(draw, origin, ImVec2(tile.x, tile.y - 14.0f), first.mount, first.lamps, chosen ? overlay_color::selected : IM_COL32(120, 120, 120, 255)); auto const caption{first.lamps.empty() ? first.name : first.lamps}; auto const captionsize{ImGui::CalcTextSize(caption.c_str())}; draw->AddText(ImVec2(origin.x + std::max(2.0f, (tile.x - captionsize.x) * 0.5f), origin.y + tile.y - 15.0f), IM_COL32(220, 220, 220, 255), caption.c_str()); if (group.size() > 1) draw->AddText(ImVec2(origin.x + 3.0f, origin.y + 2.0f), IM_COL32(170, 170, 170, 255), std::to_string(group.size()).c_str()); if (hovered) { ImGui::BeginTooltip(); ImGui::TextUnformatted(first.description.empty() ? first.name.c_str() : first.description.c_str()); ImGui::TextDisabled(STR_C("%zu variants, e.g. %s"), group.size(), first.file.c_str()); ImGui::TextDisabled("%s", first.read.empty() ? STR_C("The train doesn't read it") : STR_C("The train reads it by an event of the path")); ImGui::TextDisabled("%s", armed ? STR_C("Click: stop placing") : STR_C("Click: place it by a track, the signalling of the station places its signals of this kind with it too")); ImGui::EndTooltip(); } ImGui::PopID(); } ImGui::EndChild(); if (current == nullptr) return; auto const key{group_of(current->mount, current->lamps)}; ImGui::SetNextItemWidth(-1.0f); if (ImGui::BeginCombo("##signalvariant", current->name.c_str())) { for (int i = 0; i < static_cast(tool.templates.size()); ++i) { auto const &item{tool.templates[i]}; if (item.kind != current->kind || group_of(item.mount, item.lamps) != key) continue; auto const label{item.name + (item.lean == "left" ? std::string{" \xE2\x86\x96"} : item.lean == "right" ? std::string{" \xE2\x86\x97"} : std::string{}) + "##" + item.file}; if (ImGui::Selectable(label.c_str(), &item == current)) tool.chosen[tool.kind] = i; if (ImGui::IsItemHovered() && false == item.description.empty()) ImGui::SetTooltip("%s\n%s", item.description.c_str(), item.file.c_str()); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) { auto const read{current->read.empty() ? std::string{current->kind == "warning" || current->kind == "repeater" ? STR_C("The train doesn't read it: it repeats what the signal ahead of it shows") : STR_C("The train doesn't read it")} : format(STR_C("The train reads it by %s on the path it stands by"), editor_includes::substitute(current->read, {tool.name[0] != '\0' ? std::string{tool.name} : std::string{"?"}}).c_str())}; ImGui::SetTooltip("%s\n%s", STR_C("Variant of the chosen type. A head leaning to one side is swapped for its counterpart leaning towards the track on the other side"), read.c_str()); } if (false == current->description.empty()) ImGui::TextWrapped("%s", current->description.c_str()); if (tool.armed) { if (ImGui::Button(STR_C("Stop placing (Esc)"), ImVec2(-1.0f, 0.0f))) tool.armed = false; } else if (main_button(STR_C("Place by a track"), ImVec2(-1.0f, 0.0f))) tool.armed = true; item_tooltip("A click by a track places the chosen signal on that side, a press dragged along it turns it to the trains running that way. The signalling of the station places its signals of each kind with the template chosen here"); } void editor_mode::render_signal_selected() { auto const *found{m_signal.selected == m_instance ? signal_standing_of(m_signal.selected) : nullptr}; if (found == nullptr) return; ImGui::TextUnformatted(format(STR_C("Selected: %s (%s)"), found->name.c_str(), found->include.file.c_str()).c_str()); item_tooltip("Name, plate and the linked signal: in the node properties. Del removes it with its event"); if (found->read.empty()) ImGui::TextDisabled("%s", STR_C("The train doesn't read it")); else if (found->track != nullptr) ImGui::TextDisabled(STR_C("Read by %s %s of %s"), editor_track::event_keyword(found->list), found->read.c_str(), name_of(*found->track).c_str()); else ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No path has %s among its events: the train doesn't see it"), found->read.c_str()); auto const instance{found->include.instance}; if (auto const *item{signal_template_of(found->include.file)}; item != nullptr) { auto const shown = [](signal_template const &Template) { return Template.lamps.empty() ? Template.name : Template.name + " " + Template.lamps; }; std::string swap; ImGui::SetNextItemWidth(-1.0f); if (ImGui::BeginCombo("##signalswap", format(STR_C("Template: %s"), shown(*item).c_str()).c_str(), ImGuiComboFlags_HeightLarge)) { for (auto const &other : m_signal.templates) if (other.kind == item->kind && other.mount == item->mount && ImGui::Selectable((shown(other) + "##" + other.file).c_str(), other.file == item->file) && other.file != item->file) swap = other.file; ImGui::EndCombo(); } item_tooltip("Another template of its kind and mount for the signal: the name, the plate, the place and the event the train reads it by stay. A head leaning to one side is swapped for its counterpart leaning towards the track. Ctrl+Z takes it back"); if (false == swap.empty()) { signal_swap(instance, swap); return; } } auto const width{(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f}; if (ImGui::Button(STR_C("To the other side"), ImVec2(width, 0.0f))) signal_flip(instance); item_tooltip("Takes the signal across the track, it keeps facing the same trains. Dragging it over the track does the same"); ImGui::SameLine(); if (ImGui::Button(STR_C("Turn around"), ImVec2(width, 0.0f))) signal_turn(instance); item_tooltip("Turns the signal to the trains of the other direction, they read it by the other event of the path"); ImGui::Separator(); } void editor_mode::select_signal(scene::instance_handle const Instance) { signal_refresh(true); signal_select(Instance); } void editor_mode::render_signal_templates() { ImGui::Spacing(); if (ImGui::CollapsingHeader(format("%s###srksignals", STR_C("Signals")).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) render_signal_ui(); else m_signal.armed = false; } void editor_mode::render_signal_ui() { auto &tool{m_signal}; signal_scan(false); if (tool.templates.empty() && EditorIncludes.searching()) { ImGui::TextDisabled("%s", STR_C("Looking for the templates...")); return; } if (tool.templates.empty()) { ImGui::TextWrapped("%s", STR_C("No signal templates: no template of the scenery directory is described with category: signal. tools/editor/signal_headers.py writes the descriptions into the signal templates.")); return; } std::vector labels; labels.reserve(kKindCount); std::vector kinds; for (int k = 0; k < kKindCount; ++k) if (std::any_of(tool.templates.begin(), tool.templates.end(), [&](signal_template const &Item) { return Item.kind == kKinds[k]; })) { labels.push_back(STR(kKindLabels[k])); kinds.push_back({k, labels.back().c_str()}); } if (false == kinds.empty() && std::none_of(kinds.begin(), kinds.end(), [&](segment_option const &Option) { return Option.value == tool.kind; })) tool.kind = kinds.front().value; choice_chips("signalkinds", tool.kind, kinds); render_signal_palette(); auto const *armed{signal_current()}; if (begin_fields("##signalnames", 0.3f)) { field_row("Name"); ImGui::InputText("##signalname", tool.name, sizeof(tool.name)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Name of the next signal; it counts up after each one placed. Its events and memory cell are named after it")); ImGui::EndTable(); } ImGui::PushID("signalmore"); if (ImGui::TreeNode(STR_C("More settings"))) { bool missing{false}; if (begin_fields("##signalplace", 0.45f)) { if (armed != nullptr && armed->plate) { auto const automatic{format(STR_C("auto: %s"), plate_for({}, tool.name).c_str())}; field_row("Plate"); ImGui::InputTextWithHint("##signalplate", automatic.c_str(), tool.plate, sizeof(tool.plate)); auto const typing{ImGui::IsItemActive()}; if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Texture of the plate from textures/tabl, e.g. a-12 or tob. Left empty: the one named like the signal, if there is one")); auto const &plates{plate_textures()}; auto const typed{ToLower(tool.plate)}; auto const exists{typed.empty() || std::binary_search(plates.begin(), plates.end(), typed)}; if (false == exists) ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("no such texture")); missing = typing || (false == exists && false == typed.empty()); } if (armed != nullptr && armed->linked) { field_row("Linked signal"); ImGui::InputText("##linked", tool.linked, sizeof(tool.linked)); item_tooltip("(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link"); } auto offset{static_cast(EditorSettings.signal_offset())}; field_row("From the axis (m)"); if (ImGui::InputFloat("##signaloffset", &offset, 0.05f, 0.25f, "%.2f")) { EditorSettings.signal_offset(std::clamp(static_cast(offset), 0.0, 10.0)); EditorSettings.save(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Distance of the mast from the axis of the track. 2.25 m is what the signals of the sceneries stand at")); if (armed != nullptr && armed->mount == "gantry") { field_row("Over the rails (m)"); ImGui::InputFloat("##signalheight", &tool.height, 0.1f, 0.5f, "%.2f"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("A head for a gantry hangs over the axis of the track this high over the rail heads")); } ImGui::EndTable(); } if (missing) { auto const &plates{plate_textures()}; auto const typed{ToLower(tool.plate)}; int shown{0}; for (auto const &plate : plates) { if (typed.empty() || plate.compare(0, typed.size(), typed) != 0 || plate == typed) continue; if (shown % 6 != 0) ImGui::SameLine(); if (ImGui::SmallButton(plate.c_str())) std::snprintf(tool.plate, sizeof(tool.plate), "%s", plate.c_str()); if (++shown >= 18) break; } } ImGui::TreePop(); } ImGui::PopID(); if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); } void editor_mode::draw_signal_overlay() const { auto const &tool{m_signal}; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; glm::dvec3 const camera{Global.pCamera.Pos}; auto const arrow = [&](glm::dvec3 const &From, glm::dvec2 const &Direction, ImU32 const Colour) { glm::dvec3 const tip{From.x + Direction.x * 6.0, From.y, From.z + Direction.y * 6.0}; ImVec2 a, b; if (false == projection.project(From, a) || false == projection.project(tip, b)) return; drawlist->AddLine(a, b, Colour, 2.5f); auto const dx{b.x - a.x}; auto const dy{b.y - a.y}; auto const size{std::sqrt(dx * dx + dy * dy)}; if (size < 1.0f) return; ImVec2 const unit{dx / size, dy / size}; ImVec2 const side{-unit.y, unit.x}; drawlist->AddTriangleFilled(ImVec2(b.x + unit.x * 9.0f, b.y + unit.y * 9.0f), ImVec2(b.x + side.x * 6.0f, b.y + side.y * 6.0f), ImVec2(b.x - side.x * 6.0f, b.y - side.y * 6.0f), Colour); }; auto const mast = [&](glm::dvec3 const &Foot, ImU32 const Colour, float const Width) { projection.line(drawlist, Foot, Foot + glm::dvec3{0.0, kSignalHead, 0.0}, Colour, Width); }; for (int i = 0; i < static_cast(tool.standing.size()); ++i) { auto const &standing{tool.standing[i]}; auto const &location{standing.include.location}; if (glm::distance(location, camera) > kSignalRange) continue; auto const selected{standing.include.instance == tool.selected}; auto const hovered{m_intent.what == track_intent::kind::signal_move && m_intent.index == i}; auto const unread{false == standing.read.empty() && standing.track == nullptr}; auto const colour{selected ? overlay_color::selected : hovered ? overlay_color::highlight : unread ? overlay_color::invalid : IM_COL32(255, 210, 60, 160)}; mast(location, colour, selected || hovered ? 3.0f : 1.5f); ImVec2 head; if ((selected || hovered || unread) && projection.project(location + glm::dvec3{0.0, kSignalHead, 0.0}, head)) { auto const label{standing.name + (unread ? std::string{" "} + STR(" not read") : std::string{})}; drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), colour, label.c_str()); } if (selected || hovered) arrow(location, travel_of(standing.include.yaw), colour); } auto const preview = [&](signal_spot const &Spot, ImU32 const Colour) { projection.line(drawlist, Spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, Spot.position, Colour, 2.0f); mast(Spot.position, Colour, 3.0f); arrow(Spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, Spot.travel, Colour); ImVec2 head; auto const *armed{signal_armed()}; if (armed != nullptr && false == armed->lamps.empty() && projection.project(Spot.position + glm::dvec3{0.0, kSignalHead, 0.0}, head)) { auto const lamps{static_cast(armed->lamps.size())}; drawlist->AddRectFilled(ImVec2(head.x - 6.0f, head.y - 4.0f), ImVec2(head.x + 6.0f, head.y + lamps * 10.0f), IM_COL32(20, 20, 20, 220), 3.0f); for (int i = 0; i < lamps; ++i) drawlist->AddCircleFilled(ImVec2(head.x, head.y + 1.0f + i * 10.0f), 4.0f, lamp_colour(armed->lamps[i])); } }; if (tool.placing) preview(tool.press, overlay_color::grip); else if (tool.moving && tool.moved_valid) preview(tool.moved, overlay_color::selected); else if (m_intent.what == track_intent::kind::signal_place && m_hover.track != nullptr) { if (auto const *armed{signal_armed()}; armed != nullptr) { signal_spot spot; if (signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot)) preview(spot, overlay_color::marked); } } }