17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 16:14:52 +02:00
Files
maszyna/application/editormodeturntable.cpp
maj00r ff1710d9ea feat(editor): labels for a layman, light at the start; whole sceneries fitted to the terrain model
Labels:
- every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the
  sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track,
  grade), sides told from directions, degrees and per mille as signs, messages built from one format string
- plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header
- lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped
- roads: the notes and errors naming a road translated; the build options and the layout fold the texture length,
  kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the
  lanes start folded

Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once:
- fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone
  past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way
  is one point of intersection, a curve with no room is as sharp as the room lets it be
- the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what
  the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run
- apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into
  pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the
  ends of the route are brought to its height
- Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm
2026-10-19 15:27:50 +02:00

1245 lines
48 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/editorgestures.h"
#include "application/editormodeshared.h"
#include "application/editorprojection.h"
#include "application/editoruilayer.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorIncludeInfo.hpp"
#include "model/AnimModel.h"
#include "scene/scene.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "utilities/Globals.h"
#include "utilities/translation.h"
#include "utilities/utilities.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include <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};
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 placed; it turns after the scenery is saved and loaded again"), table->name().c_str());
}
void editor_mode::turntable_press(track_intent const &Intent)
{
using kind = track_intent::kind;
auto &tool{m_turntable};
auto const &io{ImGui::GetIO()};
switch (Intent.what)
{
case kind::turntable_place:
tool.placing = true;
tool.pressed = {io.MousePos.x, io.MousePos.y};
tool.snap = Intent.snap;
tool.centre = Intent.position;
return;
case kind::turntable_exit:
{
if (tool.table == nullptr)
return;
auto const centre{turntable_centre()};
auto const rim{tool.length * 0.5};
auto const direction{heading_vector(tool.heading)};
segment_data piece;
piece.points[segment_data::point::start] = {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim};
piece.points[segment_data::point::end] = {centre.x + direction.x * (rim + tool.exit_length), centre.y, centre.z + direction.y * (rim + tool.exit_length)};
auto const ends{rim_ends(tool.table, centre, rim)};
auto *track{ends.empty() ? editor_track::create_path(m_lay.style, piece) : editor_track::create_path(*ends.front().track, piece)};
editor_track::commit({track});
push_track_snapshot({}, {track});
turntable_store_exits();
tool.status = format(STR_C("Track out of the turntable at %.1f deg, %.1f m"), circular(tool.heading - tool.table_yaw), tool.exit_length);
return;
}
case kind::turntable_fit:
{
turntable_fit fit;
if (turntable_fit_end(Intent.snap, fit))
turntable_build({&fit});
else
tool.error = fit.issue;
return;
}
default: return;
}
}
void editor_mode::turntable_finish_placement()
{
auto &tool{m_turntable};
if (false == tool.placing)
return;
tool.placing = false;
scan_turntables();
if (tool.chosen < 0)
return;
auto const rim{tool.templates[tool.chosen].length * 0.5};
auto const &io{ImGui::GetIO()};
auto const dragged{glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - tool.pressed) >= kDragPixels};
if (tool.snap.track != nullptr && false == dragged)
{
auto const into{glm::normalize(plan_of(tool.snap.direction))};
glm::dvec3 const centre{tool.snap.position.x - into.x * rim, tool.snap.position.y, tool.snap.position.z - into.y * rim};
tool.yaw = heading_of(into);
turntable_place(centre, tool.yaw);
return;
}
tool.snap = {};
if (dragged)
{
auto const cursor{cursor_level(tool.centre.y)};
if (plan_distance(cursor, tool.centre) > 0.5)
tool.yaw = std::round(heading_of(plan_of(cursor - tool.centre)) * 10.0) / 10.0;
}
turntable_place(tool.centre, tool.yaw);
}
editor_mode::track_intent editor_mode::turntable_intent(glm::dvec3 const &Ground, bool const Shift)
{
using kind = track_intent::kind;
auto &tool{m_turntable};
track_intent intent;
intent.position = Ground;
auto const make = [&](kind const What, std::string const &Action) {
intent.what = What;
intent.action = Action;
return intent;
};
auto const &io{ImGui::GetIO()};
glm::vec2 const mouse{io.MousePos.x, io.MousePos.y};
if (tool.table != selected_track())
turntable_read(selected_track());
if (tool.table != nullptr)
{
auto const centre{turntable_centre()};
auto const rim{tool.length * 0.5};
auto const end{snap_free_end(Ground, mouse, nullptr, 1, {tool.table})};
if (end.track != nullptr && std::abs(plan_distance(end.position, centre) - rim) > kRimTolerance && plan_distance(end.position, centre) <= rim + tool.reach)
{
intent.snap = end;
intent.track = end.track;
intent.position = end.position;
return make(kind::turntable_fit, STR("Click: lead this track to the turntable"));
}
double heading;
double length;
if (turntable_exit_at(cursor_level(centre.y), heading, length))
{
tool.heading = heading;
tool.exit_length = length;
auto const direction{heading_vector(heading)};
intent.position = {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim};
return make(kind::turntable_exit, format(STR_C("Click: a track out of the turntable, %.1f deg, %.1f m"), circular(heading - tool.table_yaw), length));
}
}
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->eType != tt_Table || track->m_editorremoved || track->m_paths.empty())
continue;
if (plan_distance(centre_of(*track), Ground) <= length_of(*track) * 0.5)
{
intent.track = track;
return make(kind::select, track == tool.table ? std::string{} : STR("Click: this turntable"));
}
}
scan_turntables();
if (tool.chosen < 0)
return intent;
if (false == Shift)
{
auto const end{snap_free_end(Ground, mouse, nullptr, 1, {})};
if (end.track != nullptr)
{
intent.snap = end;
intent.position = end.position;
return make(kind::turntable_place, STR("Click: a turntable at the end of this track"));
}
}
return make(kind::turntable_place, STR("Press: the centre of a new turntable, drag: the direction of its bridge"));
}
void editor_mode::render_turntable_ui()
{
auto &tool{m_turntable};
scan_turntables();
auto *selected{selected_track()};
if (tool.table != selected)
turntable_read(selected);
ImVec4 const warncolour{1.f, 0.75f, 0.3f, 1.f};
if (tool.table != nullptr)
{
auto &table{*tool.table};
ImGui::Text(STR_C("Turntable %s, bridge %.1f m"), table.name().c_str(), tool.length);
if (tool.kind >= 0)
{
auto const &kind{tool.templates[tool.kind]};
ImGui::TextDisabled("%s", kind.name.c_str());
ImGui::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())
{
// the positions round the pit: a track out of the end of a position in green, a free end as a grey tick, the numbers
// past them, the bridge at the first position
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 font{ImGui::GetFontSize()};
auto const width{ImGui::GetContentRegionAvail().x};
auto const height{std::round(font * 10.0f)};
auto const top{ImGui::GetCursorScreenPos()};
ImGui::Dummy(ImVec2(width, height));
auto *list{ImGui::GetWindowDrawList()};
list->AddRectFilled(top, ImVec2(top.x + width, top.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg), std::round(font * 0.35f));
ImVec2 const middle{std::round(top.x + width * 0.5f), std::round(top.y + height * 0.5f)};
auto const rim{height * 0.22f};
auto const point = [&](double const Heading, float const Distance) {
auto const direction{heading_vector(Heading)};
return ImVec2(middle.x + static_cast<float>(direction.x) * Distance, middle.y - static_cast<float>(direction.y) * Distance);
};
list->AddCircleFilled(middle, rim, IM_COL32(40, 44, 52, 255), 48);
list->AddCircle(middle, rim, ImGui::GetColorU32(ImGuiCol_Text, 0.7f), 48, 2.0f);
auto const bridge{tool.table_yaw + tool.exits.front()};
list->AddLine(point(bridge, rim * 0.95f), point(bridge + 180.0, rim * 0.95f), IM_COL32(150, 160, 175, 255), std::round(font * 0.55f));
list->AddLine(point(bridge, rim * 0.95f), point(bridge + 180.0, rim * 0.95f), ImGui::GetColorU32(ImGuiCol_Text), 1.5f);
list->AddCircleFilled(middle, 3.0f, ImGui::GetColorU32(ImGuiCol_Text));
for (std::size_t i = 0; i < tool.exits.size(); ++i)
for (auto const back : {false, true})
{
auto const heading{tool.table_yaw + tool.exits[i] + (back ? 180.0 : 0.0)};
auto const taken{occupied(heading)};
if (taken)
list->AddLine(point(heading, rim), point(heading, rim * 1.5f), IM_COL32(90, 230, 110, 255), 3.0f);
else
list->AddLine(point(heading, rim), point(heading, rim * 1.15f), ImGui::GetColorU32(ImGuiCol_TextDisabled), 1.5f);
if (back)
continue;
auto const number{std::to_string(i + 1)};
auto const size{ImGui::CalcTextSize(number.c_str())};
auto const at{point(heading, rim * (taken ? 1.5f : 1.15f) + font * 0.75f)};
list->AddText(ImVec2(std::round(at.x - size.x * 0.5f), std::round(at.y - size.y * 0.5f)), taken ? IM_COL32(150, 240, 160, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled), number.c_str());
}
ImGui::TextDisabled("%s", STR_C("green: a track out of the position"));
}
if (false == tool.exits.empty() && ImGui::TreeNodeEx(format(STR_C("Positions (%d)"), static_cast<int>(tool.exits.size())).c_str()))
{
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°", 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::SeparatorText(STR_C("Tracks around"));
ImGui::PushID("turntablemore");
if (ImGui::TreeNode(STR_C("More settings")))
{
if (begin_fields("##turntablefields", 0.5f))
{
field_row("Reach from the rim, m");
ImGui::InputFloat("##turntablereach", &tool.reach, 10.f, 50.f, "%.0f");
tool.reach = std::clamp(tool.reach, 5.f, 500.f);
field_row("Radius of the curves, m");
ImGui::InputFloat("##turntableradius", &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)
{
field_row("Angle step of a new track, deg (0: free)");
ImGui::InputFloat("##turntablestep", &tool.step, 0.5f, 2.5f, "%.2f");
tool.step = std::clamp(tool.step, 0.f, 45.f);
}
ImGui::EndTable();
}
ImGui::TreePop();
}
ImGui::PopID();
gesture_rows({{"LMB around the turntable", "a track out of it, as long as the cursor is far"}, {"LMB on 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"), ImVec2(-FLT_MIN, 0.f)))
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::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.42f, 0.24f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.52f, 0.30f, 1.0f));
ImGui::BeginDisabled(chosen == 0);
auto const lead{ImGui::Button(format(STR_C("Lead the chosen to the turntable (%d)"), chosen).c_str(), ImVec2(-FLT_MIN, 0.f))};
ImGui::EndDisabled();
ImGui::PopStyleColor(2);
if (lead)
{
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{STR_C("(no name)")} : 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]};
// the name, and under it the size and the positions
auto const details{kind.angles > 0 ? format(STR_C("%.0f m, up to %d positions set by the tracks"), kind.length, kind.positions)
: format(STR_C("%.0f m, %d fixed positions"), kind.length, static_cast<int>(kind.fixed.size()))};
auto const line{ImGui::GetTextLineHeight()};
auto const at{ImGui::GetCursorScreenPos()};
if (ImGui::Selectable(("##turntable" + std::to_string(i)).c_str(), tool.chosen == i, ImGuiSelectableFlags_None, ImVec2(0.0f, line * 2.0f + 2.0f)))
tool.chosen = i;
auto *list{ImGui::GetWindowDrawList()};
list->AddText(ImVec2(at.x + 4.0f, at.y), ImGui::GetColorU32(ImGuiCol_Text), kind.name.c_str());
list->AddText(ImVec2(at.x + 4.0f, at.y + line + 1.0f), ImGui::GetColorU32(ImGuiCol_TextDisabled), details.c_str());
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", kind.file.c_str());
}
if (begin_fields("##turntablenamefields", 0.3f))
{
field_row("Name##turntablename");
ImGui::InputTextWithHint("##turntablename", turntable_name_for_new().c_str(), tool.name, sizeof(tool.name), ImGuiInputTextFlags_CharsNoBlank);
ImGui::EndTable();
}
gesture_rows({{"Drag", "from where the centre goes, turning the bridge"}, {"LMB on a free end", "the turntable lines up with it, at its end"}, {"Shift+LMB", "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°", 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};
std::vector<glm::dvec3> centres;
for (auto const *table : simulation::Paths.sequence())
if (table != nullptr && table->eType == tt_Table && false == table->m_editorremoved && false == table->m_paths.empty() && false == table->name().empty())
centres.push_back(centre_of(*table));
for (auto const *table : simulation::Paths.sequence())
{
if (table == nullptr || table->eType != tt_Table || table->m_editorremoved || table->m_paths.empty() || table->name().empty())
continue;
auto const &name{table->name()};
std::vector<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";
}
}
auto range{150.0};
for (auto const &other : centres)
if (auto const distance{glm::distance(other, centre)}; distance > 0.5)
range = std::min(range, distance * 0.5);
text << "node -1 0 " << name << "_key eventlauncher " << at << ' ' << format("%.1f", range) << " t 0 " << name << "_next " << name << "_prev end\r\n";
}
auto const path{Global.asCurrentSceneryPath + file};
std::error_code error;
auto const exists{std::filesystem::exists(path, error)};
if (tables == 0 && false == exists)
return false;
auto const content{"// made by the scenery editor on each save: T turns each turntable to its next position, Shift+T to the previous one\r\n" + text.str()};
std::string current;
{
std::ifstream input{path, std::ios_base::binary};
current.assign(std::istreambuf_iterator<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;
}