17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 00:29:19 +02:00
Files
maszyna/application/editormodeturntable.cpp
2026-10-09 14:42:55 +02:00

1169 lines
44 KiB
C++

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