/* 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 "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/Track.h" #include "imgui/imgui.h" #include "utilities/translation.h" #include #include #include #include #include namespace { using geometry::bezier; 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}; 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; } std::string lower(std::string Text) { std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); return Text; } 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}; } double heading_of(glm::dvec2 const &Direction) { auto const degrees{glm::degrees(std::atan2(Direction.x, Direction.y))}; return degrees < 0.0 ? degrees + 360.0 : degrees; } 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)) { auto const stem{lower(file->path().stem().string())}; 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{lower(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])); } } // namespace void editor_mode::signal_scan() { auto &tool{m_signal}; if (false == EditorIncludes.scanned()) EditorIncludes.scan(); if (tool.revision == EditorIncludes.revision()) return; 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; } 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; } } editor_mode::signal_template const *editor_mode::signal_armed() 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_template_of(std::string const &File) const { auto const file{lower(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{Spot.track->name().empty() ? std::string{"(noname)"} : Spot.track->name()}; 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}; signal_scan(); auto const now{ImGui::GetTime()}; if (false == Force && 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.clear(); if (scene::Layers.empty()) return; 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(lower(event.first), std::make_pair(track, list)); } for (auto const &include : found) { auto const *item{signal_template_of(include.file)}; signal_standing standing; standing.include = include; standing.kind = kind_index(item->kind); standing.name = include.values.empty() ? std::string{} : include.values.front(); if (false == item->read.empty()) { standing.read = editor_includes::substitute(item->read, include.values); if (auto const reader{readers.find(lower(standing.read))}; reader != readers.end()) { standing.track = reader->second.first; standing.list = reader->second.second; } } tool.standing.push_back(std::move(standing)); } 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; signal_select(tool.standing[Intent.index].include.instance); tool.moving = true; tool.moved_valid = false; 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{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; if (found == tool.standing.end() || m_hover.track == nullptr) { tool.moved_valid = false; return; } auto const *item{signal_template_of(found->include.file)}; if (item == nullptr) return; auto const yaw{glm::radians(found->include.yaw + 180.0)}; glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)}; auto const parameter{nearest_on(*m_hover.track, std::clamp(m_hover.path, 0, static_cast(m_hover.track->m_paths.size()) - 1), m_hover.point)}; auto const forward{forward_at(*m_hover.track, std::clamp(m_hover.path, 0, static_cast(m_hover.track->m_paths.size()) - 1), parameter)}; auto const facing{glm::dot(forward, travel) >= 0.0 ? 1 : -1}; auto const sideoftravel{glm::dot(plan_of(found->include.location) - plan_of(m_hover.point), right_of(travel)) >= 0.0 ? 1 : -1}; tool.moved_valid = signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, m_hover.point, facing, sideoftravel * facing, *item, tool.moved); } 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 (false == tool.moved_valid || glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) <= kDragStart) return; tool.moved_valid = false; auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; if (found == tool.standing.end() || false == scene::Layers.tracked(found->include.instance) || false == scene::Layers.removable(found->include.instance)) return; auto const &spot{tool.moved}; auto const &included{scene::Layers.instance(found->include.instance)}; auto values{found->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 before{scene::Layers.directive(found->include.instance)}; std::vector> states; if (false == found->read.empty() && spot.track->Dynamics.empty()) { signal_detach_events(found->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(lower(found->read), nullptr); std::string missing; editor_track::bind_events(*spot.track, missing); spot.track->mark_dirty(); } set_include_directive(found->include.instance, editor_includes::compose_directive(*included.file, values)); if (states.empty()) { EditorSnapshot snap; snap.directives.emplace_back(found->include.instance, std::move(before)); snap.node_name = found->include.file; 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(found->include.instance, std::move(before)); } tool.status = found->read.empty() ? format(STR_C("Signal %s moved"), found->name.c_str()) : format(STR_C("Signal %s moved, read by %s %s of %s"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), spot.track->name().empty() ? "(noname)" : spot.track->name().c_str()); signal_refresh(true); signal_select(found->include.instance); } 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) 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 = 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 lower(Standing.name) == lower(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) == 0) { tool.status = 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{Spot.track->name().empty() ? std::string{"(noname)"} : Spot.track->name()}; std::string reason; if (read.empty()) tool.status = format(STR_C("Signal %s placed (%s), the train doesn't read it"), 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"), 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"), 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(lower(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. 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); return true; } void editor_mode::signal_detach_events(std::string const &Read, std::vector> &States) { auto const name{lower(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 || lower(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 lower(Event.first) == name; }), events.end()); } std::string missing; editor_track::bind_events(*track, missing); track->mark_dirty(); } } 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{lower(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 || lower(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 (lower(event.first) == name) event = {lower(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(); } 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{lower(tool.filter)}; auto const *armed{signal_armed()}; 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 && lower(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{armed != nullptr && group_of(armed->mount, armed->lamps) == keys[g] && armed->kind == first.kind}; if (g % columns != 0) ImGui::SameLine(); ImGui::PushID(g); auto const origin{ImGui::GetCursorScreenPos()}; if (ImGui::InvisibleButton("##tile", tile)) { auto pick{group.front()}; if (armed != nullptr && chosen) pick = tool.chosen[tool.kind]; tool.chosen[tool.kind] = pick; } auto const hovered{ImGui::IsItemHovered()}; draw->AddRectFilled(origin, ImVec2(origin.x + tile.x, origin.y + tile.y), 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::EndTooltip(); } ImGui::PopID(); } ImGui::EndChild(); if (armed == nullptr) return; auto const key{group_of(armed->mount, armed->lamps)}; ImGui::SetNextItemWidth(-1.0f); if (ImGui::BeginCombo("##signalvariant", armed->name.c_str())) { for (int i = 0; i < static_cast(tool.templates.size()); ++i) { auto const &item{tool.templates[i]}; if (item.kind != armed->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 == armed)) 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()) ImGui::SetTooltip("%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")); if (false == armed->description.empty()) ImGui::TextWrapped("%s", armed->description.c_str()); ImGui::TextDisabled("%s", armed->read.empty() ? (armed->kind == "warning" || armed->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(armed->read, {tool.name[0] != '\0' ? std::string{tool.name} : std::string{"?"}}).c_str()).c_str()); } void editor_mode::render_signal_selected() { auto &tool{m_signal}; auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; if (found == tool.standing.end()) return; ImGui::Separator(); ImGui::TextUnformatted(format(STR_C("Selected: %s (%s)"), found->name.c_str(), found->include.file.c_str()).c_str()); 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(), found->track->name().empty() ? "(noname)" : found->track->name().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()); ImGui::TextDisabled("%s", STR_C("Name, plate and the linked signal: in the node properties. Del removes it with its event")); } void editor_mode::render_signal_ui() { auto &tool{m_signal}; signal_scan(); 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; } if (ImGui::BeginTabBar("##signalkinds")) { for (int k = 0; k < kKindCount; ++k) { if (std::none_of(tool.templates.begin(), tool.templates.end(), [&](signal_template const &Item) { return Item.kind == kKinds[k]; })) continue; if (ImGui::BeginTabItem(STR(kKindLabels[k]).c_str())) { tool.kind = k; ImGui::EndTabItem(); } } ImGui::EndTabBar(); } render_signal_palette(); auto const *armed{signal_armed()}; ImGui::Separator(); ImGui::PushItemWidth(140.0f); ImGui::InputText(STR_C("Name"), 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")); if (armed != nullptr && armed->plate) { auto const automatic{format(STR_C("auto: %s"), plate_for({}, tool.name).c_str())}; ImGui::InputTextWithHint(STR_C("Plate"), automatic.c_str(), tool.plate, sizeof(tool.plate)); 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{lower(tool.plate)}; auto const exists{typed.empty() || std::binary_search(plates.begin(), plates.end(), typed)}; if (false == exists) { ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("no such texture")); } if (ImGui::IsItemActive() || (false == exists && false == typed.empty())) { 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; } } } if (armed != nullptr && armed->linked && ImGui::TreeNode(STR_C("Linked signal"))) { ImGui::InputText("##linked", tool.linked, sizeof(tool.linked)); ImGui::TextDisabled("%s", STR_C("(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link")); ImGui::TreePop(); } auto offset{static_cast(EditorSettings.signal_offset())}; if (ImGui::InputFloat(STR_C("From the axis (m)"), &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") { ImGui::InputFloat(STR_C("Over the rails (m)"), &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::PopItemWidth(); if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); render_signal_selected(); } 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) { auto const yaw{glm::radians(standing.include.yaw + 180.0)}; arrow(location, glm::dvec2{std::sin(yaw), std::cos(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); } } }