17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 17:26:22 +01:00
Files
maszyna/application/editormodeturntable.cpp
maj00r 088eeaa743 fix(editor): a scenery saved under another name keeps one file of turntable controls, not the old one beside its own (#251)
The editor writes the turntable controls on each save to <scenery>_obrotnice.scm and includes it. A copy of a
scenery, saved under another name, still included the file of the scenery it came from. Its save wrote a file of
its own and included it too, so each turntable had its events twice.

Now the save leaves out of the scenery the other files of turntable controls it includes, told by the line the
editor writes at their start. Their files stay on the disk, for the scenery they were made for. A file of
turntables made by hand, without that line, stays included.

Co-authored-by: maj00r <maj00r@host>
2026-11-07 12:32:02 +01:00

1199 lines
46 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/Logs.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());
}
// the first line of the file of the turntable controls, which tells it from the files of the scenery made by hand
char const *const kControlsHeader{"// made by the scenery editor on each save: T turns each turntable to its next position, Shift+T to the previous one"};
// true if the file is one of the turntable controls the editor writes
bool controls_file(std::string const &Path)
{
std::ifstream input{Path, std::ios_base::binary};
std::string first;
std::getline(input, first);
if (false == first.empty() && first.back() == '\r')
first.pop_back();
return first == kControlsHeader;
}
} // 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{std::string{kControlsHeader} + "\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;
}
// the controls written for this scenery under another name, the file it was copied from, give way to these: with both
// included each turntable would have its events twice. their files stay on the disk, for the scenery they were made for
for (std::size_t handle = 1; handle <= scene::Layers.size(); ++handle)
{
auto const layer{static_cast<scene::layer_handle>(handle)};
if (false == scene::Layers.listed(layer) || scene::Layers.layer(layer).removed)
continue;
auto name{scene::Layers.layer(layer).name};
std::replace(name.begin(), name.end(), '\\', '/');
auto const lower{ToLower(name)};
if (lower == ToLower(file) || lower.ends_with("/" + ToLower(file)) || false == lower.ends_with("_obrotnice.scm"))
continue;
if (controls_file(Global.asCurrentSceneryPath + scene::Layers.layer(layer).name) && scene::Layers.remove(layer))
WriteLog("Editor: " + name + " left out of the scenery, the turntable controls are in " + file + " now");
}
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;
}