17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-19 00:19:19 +02:00

feat(editor): a model or an include standing by a track bent along it (B, or the Bend along the track section of the toolset): it stays where it stood and follows the track, through the joined tracks as far as it reaches; reference point at its start, origin, middle, end or a point picked on the model, across at its origin, the side facing the track, the far side or its middle, up at its origin, bottom, top or the top of its edge by the track; the blue cross of a bent model dragged for its offset, with Shift for its height and with Ctrl along the track, the values shown at the cursor; a bent model clicked to edit; one step of the undo history; scale of the model kept, parts lit only at night left out; edges of platforms told by the name of the model and by the overhang of their edge over the foot: put 1.725 m from the axis at the height the name gives, turned with the edge to the track when they stood the other way; platforms stand on the plane of the rail heads carried on to their edge (PKP PLK ST-T2), the gauge widening led in from the tracks beyond the ends and from the other path of a switch; gaps between the pieces of a sweep filled with the paths which join them; Platforms and models tab left out of the track editor; laid track eased into the curvature of the one it joins with a transition curve; File > Open scenery... starts the editor again with the scenery picked from a list; the camera saved with the scenery; T and Shift+T turn each turntable to its next and previous position by event launchers written on each save; a turntable track deleted with its template; a scenery with no trainset gets a WMB10 on its first track; vehicle placed where the track is clicked; gauge check takes in the bent models; Polish translation of the editor

This commit is contained in:
maj00r
2026-10-08 00:51:40 +02:00
parent b451f80c6d
commit d4791f6a29
18 changed files with 4180 additions and 552 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@ http://mozilla.org/MPL/2.0/.
#include <chrono>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@@ -127,7 +128,7 @@ class editor_mode : public application_mode, private editor_track::observer
struct stored_profile;
struct EditorSnapshot
{
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array };
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend };
// copy of a model an array made
struct array_copy
{
@@ -313,6 +314,17 @@ class editor_mode : public application_mode, private editor_track::observer
bool m_newscenery_asked{false};
void render_new_scenery_popup();
bool restart_for_new_scenery();
bool restart_editor(std::string const &Scenery);
bool m_openscenery_asked{false};
bool m_opened_at_track{false};
bool m_turntable_included{false};
bool turntable_controls(std::vector<std::string> &Rootstatements);
void starter_trainset(std::vector<std::string> &Trailing);
std::optional<std::pair<glm::dvec3, glm::vec3>> m_saved_camera;
char m_openscenery_search[64]{};
std::vector<std::string> m_openscenery_list;
std::string m_openscenery_choice;
void render_open_scenery_popup();
bool m_orthophoto_window{false};
// origin of the scenery in PUWG 1992 the scenery file gives in its //$g line, if it does
bool m_georeference_read{false};
@@ -702,6 +714,7 @@ class editor_mode : public application_mode, private editor_track::observer
void scan_turntables();
std::string turntable_name_for_new() const;
bool turntable_read(TTrack *Table);
std::vector<scene::instance_handle> turntable_includes(TTrack const &Table);
glm::dvec3 turntable_centre() const;
std::vector<double> turntable_lines() const;
double turntable_snap(double const Heading) const;
@@ -756,6 +769,7 @@ class editor_mode : public application_mode, private editor_track::observer
void lay_enter();
bool lay_heading(glm::dvec2 &Heading) const;
std::vector<segment_data> lay_pieces(std::vector<glm::dvec3> const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const;
alignment::result lay_shape(std::vector<glm::dvec3> const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const;
void render_switch_ui();
bool start_switch_placement();
void finish_switch_placement();
@@ -1192,7 +1206,7 @@ class editor_mode : public application_mode, private editor_track::observer
std::vector<std::string> parameter_labels;
std::vector<bool> parameter_placement; // filled in with zeros: the template is placed in its own space
std::vector<std::vector<std::string>> parameter_choices; // variants of the textures a parameter is a part of the name of
int length_mode{0}; // 0: one copy as long as the original, 1: the length given, 2: the whole stretch of the line
int length_mode{2}; // 0: one copy as long as the original, 1: the length given, 2: the whole stretch of the line
double length{200.0};
sweep_node::state settings;
int axis{0}; // 0: the longer one of the model, 1: z, 2: x
@@ -1255,6 +1269,65 @@ class editor_mode : public application_mode, private editor_track::observer
void sweeps_committed(std::vector<TTrack *> const &Tracks);
void sweeps_split(TTrack &Original, TTrack &Created);
struct bend_source
{
TAnimModel *model{nullptr};
scene::instance_handle instance{0};
std::string file;
std::string skin{"none"};
std::vector<std::string> parameters;
glm::dmat4 transform{1.0};
glm::dvec3 scale{1.0};
scene::layer_handle layer{null_handle};
};
struct bend_tool
{
int axis{0};
bool turned{false};
int along_anchor{1};
int side_anchor{0};
int height_anchor{0};
glm::dvec3 point{0.0};
bool picking{false};
void const *tracks_for{nullptr};
glm::dvec3 tracks_at{0.0};
std::vector<std::pair<TTrack *, double>> tracks;
int track{0};
sweep_node *edited{nullptr};
std::string signature;
bool marker_valid{false};
glm::dvec3 marker{0.0};
std::string status;
int dragging{0};
sweep_node::state drag_state;
glm::dvec3 drag_anchor{0.0};
double drag_station{0.0};
std::string drag_label;
sweep_node *hover{nullptr};
bool over_marker{false};
std::string label;
std::string details;
};
bend_tool m_bend;
bool bend_drag_start(int const Mods);
void bend_drag_update();
sweep_node *sweep_under(glm::dvec3 const &Point) const;
bool bend_click();
bool bend_shortcut();
bool bend_source_of_selection(bend_source &Source) const;
void bend_find_tracks(glm::dvec3 const &Point, void const *Key);
bool bend_definition(bend_source const &Source, TTrack &Track, sweep_node::state &State, std::vector<TTrack *> &Tracks);
void bend_selection();
void bend_edit(sweep_node::state const &State);
void bend_reanchor(sweep_node::state &State, int const Side, int const Height, glm::dvec3 const &Point) const;
double bend_fixed(sweep_node const &Sweep, glm::dvec3 const &Point) const;
void bend_stretch(sweep_node const &Sweep, double const Fixed, double const Length, glm::dvec3 const &Point, sweep_node::state &State) const;
void bend_pick(glm::dvec3 const &Point);
glm::dvec3 bend_marker(sweep_node const &Sweep) const;
void restore_bend(EditorSnapshot &Snapshot, bool const Undo);
void render_bend();
void draw_bend_overlay() const;
// script of editor actions given in the EU07_EDITOR_SELFTEST file, run a step at a time, for testing without a person
struct selftest
{

View File

@@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/renderer.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "world/Sweep.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include "utilities/translation.h"
@@ -324,6 +325,33 @@ template <typename Visitor> void visit_triangles(TSubModel *Submodel, glm::dmat4
}
}
// the triangle enters the gauge when any point of a grid spread over it does; the deepest such point goes to the hit
void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, editor_mode::gauge_hit &Hit)
{
auto const middle{(A + B + C) / 3.0};
if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)})))
return;
auto const edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})};
auto const steps{std::clamp(static_cast<int>(std::ceil(edge / 0.5)), 1, 24)};
for (int u = 0; u <= steps; ++u)
for (int v = 0; u + v <= steps; ++v)
{
auto const point{A + (B - A) * (static_cast<double>(u) / steps) + (C - A) * (static_cast<double>(v) / steps)};
auto const found{Space.intrusion(point)};
if (found.depth <= 0.0)
continue;
if (found.depth > Hit.depth)
{
Hit.depth = found.depth;
Hit.point = point;
Hit.track = found.span->track;
Hit.path = found.span->path;
Hit.direction = found.direction;
}
return;
}
}
// the deepest place of the model in the gauge, added to the hits when it enters it
void scan_model(corridor const &Space, TAnimModel &Instance, std::vector<editor_mode::gauge_hit> &Hits)
{
@@ -332,31 +360,23 @@ void scan_model(corridor const &Space, TAnimModel &Instance, std::vector<editor_
if (false == Space.nearby(Instance.location(), Instance.radius()))
return;
editor_mode::gauge_hit hit{Instance.name(), Instance.location()};
// the triangle enters the gauge when any point of a grid spread over it does
visit_triangles(Instance.Model()->GetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) {
auto const middle{(A + B + C) / 3.0};
if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)})))
return;
auto const edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})};
auto const steps{std::clamp(static_cast<int>(std::ceil(edge / 0.5)), 1, 24)};
for (int u = 0; u <= steps; ++u)
for (int v = 0; u + v <= steps; ++v)
{
auto const point{A + (B - A) * (static_cast<double>(u) / steps) + (C - A) * (static_cast<double>(v) / steps)};
auto const found{Space.intrusion(point)};
if (found.depth <= 0.0)
continue;
if (found.depth > hit.depth)
{
hit.depth = found.depth;
hit.point = point;
hit.track = found.span->track;
hit.path = found.span->path;
hit.direction = found.direction;
}
return;
}
});
visit_triangles(Instance.Model()->GetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { scan_triangle(Space, A, B, C, hit); });
if (hit.depth > 0.0)
Hits.push_back(std::move(hit));
}
// the same for the copies of a model laid along a curve
void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector<editor_mode::gauge_hit> &Hits)
{
if (Sweep.m_editorremoved || false == Space.nearby(Sweep.location(), Sweep.radius()))
return;
editor_mode::gauge_hit hit{Sweep.name(), Sweep.location()};
for (auto const &shape : Sweep.create_shapes())
{
auto const &vertices{shape.data().vertices};
for (std::size_t i = 0; i + 2 < vertices.size(); i += 3)
scan_triangle(Space, vertices[i].position, vertices[i + 1].position, vertices[i + 2].position, hit);
}
if (hit.depth > 0.0)
Hits.push_back(std::move(hit));
}
@@ -372,6 +392,9 @@ void scan_models(corridor const &Space, std::vector<editor_mode::gauge_hit> &Hit
for (auto *instance : simulation::Instances.sequence())
if (instance != nullptr)
scan_model(Space, *instance, Hits);
for (auto *sweep : simulation::Sweeps.sequence())
if (sweep != nullptr)
scan_sweep(Space, *sweep, Hits);
sort_hits(Hits);
}
@@ -579,14 +602,22 @@ void editor_mode::step_gauge_map()
{
auto &scan{*m_gauge.scan};
auto const &instances{simulation::Instances.sequence()};
auto const &sweeps{simulation::Sweeps.sequence()};
auto const total{instances.size() + sweeps.size()};
auto const until{std::chrono::steady_clock::now() + gauge_slice};
while (scan.next < instances.size() && std::chrono::steady_clock::now() < until)
while (scan.next < total && std::chrono::steady_clock::now() < until)
{
if (auto *instance{instances[scan.next++]})
scan_model(*scan.space, *instance, m_gauge.map.hits);
auto const next{scan.next++};
if (next < instances.size())
{
if (auto *instance{instances[next]})
scan_model(*scan.space, *instance, m_gauge.map.hits);
}
else if (auto *sweep{sweeps[next - instances.size()]})
scan_sweep(*scan.space, *sweep, m_gauge.map.hits);
}
scan.progress = instances.empty() ? 1.f : static_cast<float>(scan.next) / instances.size();
if (scan.next >= instances.size())
scan.progress = total == 0 ? 1.f : static_cast<float>(scan.next) / total;
if (scan.next >= total)
finish_gauge_map();
}

View File

@@ -14,12 +14,24 @@ http://mozilla.org/MPL/2.0/.
#include "application/editorprojection.h"
#include "simulation/simulation.h"
#include "scene/scenelayers.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "world/Track.h"
#include "world/MemCell.h"
#include "world/Event.h"
#include "vehicle/DynObj.h"
#include "model/AnimModel.h"
#include "model/Model3d.h"
#include "rendering/renderer.h"
#include "world/Sweep.h"
#include "input/keyboardinput.h"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
#include "editor/editorFormat.hpp"
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <map>
@@ -32,6 +44,23 @@ http://mozilla.org/MPL/2.0/.
// sweep <key>=<value>... model, params (separated by commas), lateral, height, side (origin, near, far,
// centre), top (origin, bottom, top, edge), mode (bend, repeat), step,
// length (original, all, or metres), from, to, widen (0, 1), face (0, 1), tilt (0, 1)
// selectmodel <x> <z> the model instance nearest the point
// bend <key>=<value>... the selected model bent along a track: track (index on the list), axis (auto, z, x),
// turned (0, 1), along (start, origin, middle, end, point), side (origin, near, far,
// middle, point), height (origin, bottom, top, edge, point), point=<x>,<y>,<z>,
// pick=<x>,<y>,<z> (a point of the scene picked as the reference point), go (0, 1)
// selectinclude <x> <z> the include placed nearest the point
// bendset <key>=<value>... the bent model being edited: length, lateral, height, along, side, height
// bendprobe logs the bent model being edited
// undo, redo
// key <S|W|CTRL|RMB> <press|release> [ctrl] a key or right button event, as the window gives it
// lineside <vehicle|hekto|fouling|parallel> the tab of the objects along the track in the track window
// event <name> queues the event
// memcell <name> logs the memory cell
// bendpanel unfolds the bend section of the toolset
// openscenerypopup the window of File / Open scenery, its list searched for zz_
// openscenery <scenery file> the editor starts again with the scenery
// checkspans logs whether the text the scenery files hold at each node's place starts and ends it
// spread <track> <stop 0..2> <reach> logs the paths the spread from the track takes
// newscenery the editor starts again with the wizard of a new scenery
// vehicles logs each vehicle: its head, facing, and what its couplers hold
@@ -123,6 +152,7 @@ void editor_mode::selftest_step()
settings.bend = get("mode", "bend") != "repeat";
settings.step = std::stod(get("step", "0"));
settings.widen = get("widen", "0") == "1";
settings.platform = get("platform", "0") == "1";
settings.face = get("face", "1") == "1";
settings.tilt = get("tilt", "0") == "1";
settings.from = std::stod(get("from", "0"));
@@ -135,6 +165,10 @@ void editor_mode::selftest_step()
tool.open = true;
sweep_curve();
sweep_measure();
if (values.count("widen"))
settings.widen = get("widen", "0") == "1";
if (values.count("platform"))
settings.platform = get("platform", "0") == "1";
if (get("create", "1") == "1")
{
sweep_create();
@@ -202,6 +236,37 @@ void editor_mode::selftest_step()
at.widening[definition.lateral >= 0.0 ? 1 : 0], std::max(0.0, at.cant[definition.lateral >= 0.0 ? 1 : 0])));
}
}
else if (command == "profile")
{
std::string name;
double station{0.0};
words >> name >> station;
auto *sweep{simulation::Sweeps.find(name)};
if (sweep == nullptr)
{
WriteLog("SELFTEST profile: no such sweep");
continue;
}
auto const &definition{sweep->definition()};
std::map<int, std::pair<double, double>> columns;
for (auto const &shape : sweep->create_shapes())
for (auto const &vertex : shape.data().vertices)
{
double lateral;
auto const at{sweep->project(vertex.position, lateral)};
if (std::abs(at - station) > 1.0)
continue;
auto const axis{sweep->frame_at(at)};
auto const height{vertex.position.y - axis.position.y - 0.18 - sweep_node::rail_head(axis.roll, definition.lateral)};
auto &column{columns.try_emplace(static_cast<int>(std::floor(lateral / 0.1)), std::numeric_limits<double>::max(), -std::numeric_limits<double>::max()).first->second};
column.first = std::min(column.first, height);
column.second = std::max(column.second, height);
}
std::string text;
for (auto const &[index, column] : columns)
text += format(" [%.1f %.2f..%.2f]", index * 0.1, column.first, column.second);
WriteLog(format("SELFTEST profile %s at %.0f m:", name.c_str(), station) + text);
}
else if (command == "fouling")
{
std::map<std::string, std::string> values;
@@ -309,6 +374,46 @@ void editor_mode::selftest_step()
if (false == m_sweep.outline.empty())
WriteLog(format("SELFTEST curve outline %zu points, %.1f %.1f -> %.1f %.1f, %.1f m", m_sweep.outline.size(), m_sweep.outline.front().x, m_sweep.outline.front().z, m_sweep.outline.back().x, m_sweep.outline.back().z, m_sweep.curve_length));
}
else if (command == "lay")
{
std::string name, side;
words >> name >> side;
auto *track{simulation::Paths.find(name)};
if (track == nullptr || track->m_paths.empty())
{
WriteLog("SELFTEST lay: no track " + name);
return;
}
auto const &path{track->m_paths.front()};
auto const atend{side == "end"};
editor_track::snap_target start;
start.track = track;
start.point = {0, atend ? editor_track::point_kind::end : editor_track::point_kind::start};
start.position = path.points[atend ? segment_data::point::end : segment_data::point::start];
auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]};
start.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[atend ? segment_data::point::start : segment_data::point::end] - start.position);
m_lay.points = {start.position};
m_lay.start = start;
for (glm::dvec3 point{0.0, start.position.y, 0.0}; words >> point.x >> point.z;)
m_lay.points.push_back(point);
lay_finish({});
WriteLog("SELFTEST lay: " + m_lay.status);
}
else if (command == "analyse")
{
std::string name;
words >> name;
editor_track::curve curve;
if (auto *track{simulation::Paths.find(name)}; track != nullptr && editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve))
{
std::string arcs;
for (auto const &arc : curve.arcs)
arcs += format(" R%.0f/%.1fdeg/after %.1fm", arc.radius, glm::degrees(arc.turn), arc.transition);
WriteLog(format("SELFTEST analyse %s: R %.0f, compound %d, in %.1f m, out %.1f m, turn %.1f deg,", name.c_str(), curve.radius, curve.compound ? 1 : 0, curve.transition_in, curve.transition_out, glm::degrees(curve.turn)) + arcs);
}
else
WriteLog("SELFTEST analyse " + name + ": no curve");
}
else if (command == "projtest")
{
screen_projection const projection;
@@ -362,6 +467,330 @@ void editor_mode::selftest_step()
{
save();
}
else if (command == "turntable")
{
glm::dvec3 centre{0.0};
double yaw{0.0};
words >> centre.x >> centre.y >> centre.z >> yaw;
turntable_place(centre, yaw);
for (double angle; words >> angle;)
{
m_turntable.heading = m_turntable.table_yaw + angle;
m_turntable.exit_length = 10.0;
track_intent intent;
intent.what = track_intent::kind::turntable_exit;
turntable_press(intent);
}
WriteLog("SELFTEST turntable: " + (m_turntable.error.empty() ? m_turntable.status : m_turntable.error));
}
else if (command == "selectmodel")
{
glm::dvec3 point{0.0};
words >> point.x >> point.z;
TAnimModel *nearest{nullptr};
auto best{50.0};
for (auto *model : simulation::Instances.sequence())
{
if (model == nullptr)
continue;
auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})};
if (distance < best)
{
best = distance;
nearest = model;
}
}
m_node = nearest;
select_include(0);
ui()->set_node(m_node);
WriteLog("SELFTEST selectmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"}));
}
else if (command == "selectinclude")
{
glm::dvec3 point{0.0};
words >> point.x >> point.z;
scene::instance_handle nearest{0};
auto best{50.0};
m_node = nullptr;
for (scene::instance_handle instance = 1; instance <= scene::Layers.instance_count(); ++instance)
{
if (false == scene::Layers.removable(instance))
continue;
select_include(instance);
bend_source source;
if (false == bend_source_of_selection(source))
continue;
auto const at{glm::dvec3(source.transform[3])};
auto const distance{glm::length(glm::dvec2{at.x - point.x, at.z - point.z})};
if (distance < best)
{
best = distance;
nearest = instance;
}
}
select_include(nearest);
WriteLog("SELFTEST selectinclude: " + (nearest != 0 ? *scene::Layers.instance(nearest).file : std::string{"none"}));
}
else if (command == "bend" || command == "bendset")
{
std::map<std::string, std::string> values;
std::string pair;
while (words >> pair)
{
auto const equals{pair.find('=')};
if (equals != std::string::npos)
values[pair.substr(0, equals)] = pair.substr(equals + 1);
}
auto const vector = [](std::string Text) {
std::replace(Text.begin(), Text.end(), ',', ' ');
std::istringstream numbers(Text);
glm::dvec3 result{0.0};
numbers >> result.x >> result.y >> result.z;
return result;
};
auto const index = [](std::string const &Text, std::vector<std::string> const &Names) {
return static_cast<int>(std::find(Names.begin(), Names.end(), Text) - Names.begin());
};
auto &tool{m_bend};
auto const along{values.count("along") ? index(values["along"], {"start", "origin", "middle", "end", "point"}) : tool.along_anchor};
auto const side{values.count("side") ? index(values["side"], {"origin", "near", "far", "middle", "point"}) : tool.side_anchor};
auto const height{values.count("height") ? index(values["height"], {"origin", "bottom", "top", "edge", "point"}) : tool.height_anchor};
auto const point{values.count("point") ? vector(values["point"]) : tool.point};
if (command == "bend")
{
tool.edited = nullptr;
tool.along_anchor = along;
tool.side_anchor = side;
tool.height_anchor = height;
tool.point = point;
tool.axis = values.count("axis") ? index(values["axis"], {"auto", "z", "x"}) : 0;
tool.turned = values["turned"] == "1";
bend_source source;
if (false == bend_source_of_selection(source))
{
std::string reason;
auto const editable{m_node != nullptr && scene::Layers.editable(m_node, &reason)};
WriteLog(format("SELFTEST bend: nothing to bend (node %p template %d editable %d %s, instance %d)", static_cast<void *>(m_node), m_node != nullptr && m_node->from_template() ? 1 : 0, editable ? 1 : 0, reason.c_str(), static_cast<int>(m_instance)));
continue;
}
tool.tracks_for = nullptr;
bend_find_tracks(glm::dvec3(source.transform[3]), source.model != nullptr ? static_cast<void const *>(source.model) : reinterpret_cast<void const *>(static_cast<std::uintptr_t>(source.instance)));
tool.track = values.count("track") ? std::stoi(values["track"]) : 0;
if (values.count("pick"))
bend_pick(vector(values["pick"]));
if (tool.tracks.empty())
{
WriteLog("SELFTEST bend: no track nearby");
continue;
}
sweep_node::state state;
std::vector<TTrack *> tracks;
bend_definition(source, *tool.tracks[tool.track].first, state, tracks);
WriteLog(format("SELFTEST bend: track %s, %zu paths, anchor before %.3f %.3f %.3f, lateral %.3f height %.3f from %.3f to %.3f, axis %s flip %d; %s", tool.tracks[tool.track].first->name().c_str(), tracks.size(),
tool.marker.x, tool.marker.y, tool.marker.z, state.lateral, state.height, state.from, state.to, state.along_x ? "x" : "z", state.flip ? 1 : 0, tool.status.c_str()));
if (values["go"] == "1")
{
bend_selection();
WriteLog("SELFTEST bend: " + tool.status);
}
}
else if (auto *sweep{tool.edited})
{
auto state{sweep->definition()};
tool.along_anchor = along;
if (side != tool.side_anchor || height != tool.height_anchor || point != tool.point)
{
tool.side_anchor = side;
tool.height_anchor = height;
tool.point = point;
bend_reanchor(state, side, height, point);
}
if (values.count("lateral"))
state.lateral = std::stod(values["lateral"]);
if (values.count("up"))
state.height = std::stod(values["up"]);
if (values.count("widen"))
state.widen = values["widen"] == "1";
if (values.count("pick"))
{
bend_pick(vector(values["pick"]));
state = sweep->definition();
}
if (values.count("length"))
{
bend_stretch(*sweep, bend_fixed(*sweep, state.point), std::stod(values["length"]), state.point, state);
}
bend_edit(state);
WriteLog("SELFTEST bendset done");
}
}
else if (command == "bendprobe")
{
auto *sweep{m_bend.edited};
if (sweep == nullptr)
{
WriteLog("SELFTEST bendprobe: nothing bent");
continue;
}
glm::dvec3 low, high, shift;
sweep->bounds(low, high, shift);
auto const &state{sweep->definition()};
auto const marker{bend_marker(*sweep)};
glm::dvec3 boxlow{std::numeric_limits<double>::max()}, boxhigh{-std::numeric_limits<double>::max()};
std::size_t vertices{0};
for (auto const &shape : sweep->create_shapes())
for (auto const &vertex : shape.data().vertices)
{
boxlow = glm::min(boxlow, vertex.position);
boxhigh = glm::max(boxhigh, vertex.position);
++vertices;
}
WriteLog(format("SELFTEST bendprobe %s: stations %.3f - %.3f, along the reference line %.3f m, model %.3f m, lateral %.3f height %.3f anchors %d/%d point %.3f %.3f %.3f, marker %.3f %.3f %.3f, box %.2f %.2f %.2f - %.2f %.2f %.2f, %zu vertices, removed %d",
sweep->name().c_str(), sweep->start(), sweep->end(), sweep->distance_at(sweep->end()) - sweep->distance_at(sweep->start()), high.x - low.x, state.lateral, state.height, state.side_anchor, state.height_anchor, state.point.x,
state.point.y, state.point.z, marker.x, marker.y, marker.z, boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z, vertices, sweep->m_editorremoved ? 1 : 0));
}
else if (command == "modelbox")
{
auto *model{dynamic_cast<TAnimModel *>(m_node)};
bend_source source;
if (model == nullptr || false == bend_source_of_selection(source))
{
WriteLog("SELFTEST modelbox: no model selected");
continue;
}
scene::node_data data;
sweep_node probe{data};
sweep_node::state state;
state.model = source.file;
state.skin = source.skin;
state.scale = source.scale;
probe.define(state);
glm::dvec3 boxlow{std::numeric_limits<double>::max()}, boxhigh{-std::numeric_limits<double>::max()};
for (auto const &item : probe.items())
{
auto const transform{source.transform * glm::scale(glm::dmat4(1.0), source.scale) * item.transform};
std::vector<TSubModel *> pending{item.model->GetSMRoot()};
while (false == pending.empty())
{
auto *submodel{pending.back()};
pending.pop_back();
for (; submodel != nullptr; submodel = submodel->Next)
{
if (submodel->Child != nullptr)
pending.push_back(submodel->Child);
if (submodel->eType != GL_TRIANGLES || submodel->fSquareMinDist > 0.f || (submodel->m_geometry.handle.bank == 0 && submodel->m_geometry.handle.chunk == 0))
continue;
glm::dmat4 local{1.0};
for (auto *parent = submodel; parent != nullptr; parent = parent->Parent)
if ((parent->iFlags & 0xC000) != 0 && parent->GetMatrix() != nullptr)
local = glm::dmat4(glm::make_mat4(parent->GetMatrix()->readArray())) * local;
for (auto const &vertex : GfxRenderer->Vertices(submodel->m_geometry.handle))
{
auto const world{glm::dvec3(transform * local * glm::dvec4(glm::dvec3(vertex.position), 1.0))};
boxlow = glm::min(boxlow, world);
boxhigh = glm::max(boxhigh, world);
}
}
}
}
WriteLog(format("SELFTEST modelbox: %.2f %.2f %.2f - %.2f %.2f %.2f", boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z));
}
else if (command == "key")
{
std::string key, action, mods;
words >> key >> action >> mods;
if (key == "RMB")
m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT, action == "press" ? GLFW_PRESS : GLFW_RELEASE);
else
{
auto const code{key == "S" ? GLFW_KEY_S : key == "W" ? GLFW_KEY_W : key == "CTRL" ? GLFW_KEY_LEFT_CONTROL : 0};
on_key(code, 0, action == "press" ? GLFW_PRESS : GLFW_RELEASE, mods == "ctrl" ? GLFW_MOD_CONTROL : 0);
}
WriteLog(format("SELFTEST key %s %s %s: camera %.2f %.2f %.2f, key S %d, right button %d", key.c_str(), action.c_str(), mods.c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z, static_cast<int>(input::keys[GLFW_KEY_S]), m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT)));
}
else if (command == "lineside")
{
std::string tab;
words >> tab;
show_track_tab(track_tab::lineside);
(tab == "vehicle" ? m_vehicle.expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true;
}
else if (command == "event")
{
std::string name;
words >> name;
auto *event{simulation::Events.FindEvent(name)};
if (event != nullptr)
simulation::Events.AddToQuery(event, nullptr);
WriteLog("SELFTEST event " + name + (event != nullptr ? " queued" : ": no such event"));
}
else if (command == "memcell")
{
std::string name;
words >> name;
auto *cell{simulation::Memory.find(name)};
WriteLog("SELFTEST memcell " + name + (cell != nullptr ? format(": %s %.1f %.1f", cell->Text().c_str(), cell->Value1(), cell->Value2()) : std::string{": none"}));
}
else if (command == "bendpanel")
{
ui()->expand_bend();
}
else if (command == "newscenerypopup")
{
m_newscenery_asked = true;
}
else if (command == "openscenerypopup")
{
m_openscenery_asked = true;
std::snprintf(m_openscenery_search, sizeof(m_openscenery_search), "%s", "zz_");
}
else if (command == "openscenery")
{
std::string name;
words >> name;
WriteLog(restart_editor(name) ? "SELFTEST openscenery: started " + name : "SELFTEST openscenery: failed");
}
else if (command == "gauge")
{
std::string name;
words >> name;
if (auto *track{simulation::Paths.find(name)})
{
scan_gauge(*track);
WriteLog(format("SELFTEST gauge %s: %d hits", name.c_str(), static_cast<int>(m_gauge.line.hits.size())));
for (auto const &hit : m_gauge.line.hits)
WriteLog(format("SELFTEST gauge hit %s depth %.3f at %.2f %.2f %.2f", hit.model.c_str(), hit.depth, hit.point.x, hit.point.y, hit.point.z));
}
else
WriteLog("SELFTEST gauge " + name + ": no such track");
}
else if (command == "deletetrack")
{
delete_selected_track();
WriteLog("SELFTEST deletetrack");
}
else if (command == "ttincludes")
{
std::string name;
words >> name;
auto const *track{simulation::Paths.find(name)};
WriteLog(format("SELFTEST ttincludes %s: track %s, includes %d", name.c_str(), track == nullptr ? "none" : track->m_editorremoved ? "removed" : "present",
track != nullptr ? static_cast<int>(turntable_includes(*track).size()) : -1));
}
else if (command == "undo")
{
undo_last();
WriteLog("SELFTEST undo");
}
else if (command == "redo")
{
redo_last();
WriteLog("SELFTEST redo");
}
else if (command == "checkspans")
{
WriteLog("SELFTEST spans: " + scene::Layers.check_sources());
}
else if (command == "log")
{
std::string text;

File diff suppressed because it is too large Load Diff

View File

@@ -81,6 +81,99 @@ void item_tooltip(char const *Text)
ImGui::SetTooltip("%s", STR_C(Text));
}
// the curve a track ends with at Joint, as it goes on beyond the joint, and a transition curve leading out of it to the straight
struct easing
{
double curvature{0.0}; // positive turning from x towards z
double length{0.0};
glm::dvec3 end{0.0};
glm::dvec2 direction{0.0};
std::vector<segment_data> pieces;
};
easing ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits)
{
easing result;
for (auto const &path : Neighbour.m_paths)
for (auto const atend : {false, true})
{
if (glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], Joint) > 0.05)
continue;
if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{})
return result;
bezier const curve{path};
auto const first{curve.first(atend ? 1.0 : 0.0)};
auto const second{curve.second(atend ? 1.0 : 0.0)};
auto const speed{std::hypot(first.x, first.z)};
if (speed < 1e-9)
return result;
auto curvature{(first.x * second.z - first.z * second.x) / (speed * speed * speed)};
glm::dvec2 direction{first.x / speed, first.z / speed};
double roll{path.rolls[1]};
if (false == atend)
{
curvature = -curvature;
direction = -direction;
roll = -path.rolls[0];
}
result.curvature = curvature;
if (std::abs(curvature) < 1e-4)
return result;
result.length = std::max(20.0, alignment::recommend(Speed, 1.0 / std::abs(curvature), Limits).transition);
auto const count{static_cast<int>(std::ceil(result.length / 10.0))};
auto const piece{result.length / count};
glm::dvec3 point{Joint};
for (int i = 0; i < count; ++i)
{
segment_data part;
part.points[segment_data::point::start] = point;
auto const from{direction};
int const steps{static_cast<int>(std::ceil(piece / 0.05))};
auto const step{piece / steps};
for (int j = 0; j < steps; ++j)
{
auto const along{i * piece + (j + 0.5) * step};
auto const angle{curvature * (1.0 - along / result.length) * step};
auto const half{turned(direction, angle * 0.5)};
point += glm::dvec3{half.x * step, Grade * step, half.y * step};
direction = turned(direction, angle);
}
part.points[segment_data::point::end] = point;
part.points[segment_data::point::control1] = glm::dvec3{from.x, Grade, from.y} * (piece / 3.0);
part.points[segment_data::point::control2] = -glm::dvec3{direction.x, Grade, direction.y} * (piece / 3.0);
part.rolls = {static_cast<float>(roll * (1.0 - i * piece / result.length)), static_cast<float>(roll * (1.0 - (i + 1) * piece / result.length))};
result.pieces.push_back(part);
}
result.end = point;
result.direction = direction;
return result;
}
return result;
}
segment_data turned_around(segment_data const &Path)
{
segment_data result{Path};
result.points[segment_data::point::start] = Path.points[segment_data::point::end];
result.points[segment_data::point::end] = Path.points[segment_data::point::start];
result.points[segment_data::point::control1] = Path.points[segment_data::point::control2];
result.points[segment_data::point::control2] = Path.points[segment_data::point::control1];
result.rolls = {-Path.rolls[1], -Path.rolls[0]};
return result;
}
// curvature of the shape at Chainage, positive turning from x towards z
double curvature_at(alignment::result const &Shape, double const Chainage)
{
auto const a{std::clamp(Chainage - 0.5, 0.0, Shape.length)};
auto const b{std::clamp(Chainage + 0.5, 0.0, Shape.length)};
if (b - a < 1e-3)
return 0.0;
auto const from{plan_of(alignment::evaluate(Shape, a).direction)};
auto const to{plan_of(alignment::evaluate(Shape, b).direction)};
return signed_angle(glm::normalize(from), glm::normalize(to)) / (b - a);
}
// circular curve from a point along a direction, cut into pieces of 90 degrees at most. Side: 1 to the left, -1 to the right
void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex)
{
@@ -477,7 +570,7 @@ std::array<editor_mode::track_mode, 11> const &editor_mode::track_modes()
{"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it"},
{"Straight", "G", track_tab::straights, "The whole straight through the selected path: drag its ends or the middle, break it, shift it"},
{"Curve", "C", track_tab::route, "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves"},
{"Objects", "B", track_tab::lineside, "Along the track: platforms and other models, hectometre posts, fouling point markers, a parallel track, a vehicle to drive"},
{"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, a parallel track, a vehicle to drive"},
{"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line"},
{"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows"},
{"Joints", "J", track_tab::joints, "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints"},
@@ -612,8 +705,6 @@ std::string editor_mode::track_mode_name() const
case track_tab::speed: return STR_C("Speed check: LMB on a row shows the path");
case track_tab::joints: return STR_C("Joints: LMB on a row shows the place");
case track_tab::lineside:
if (m_lineside_tab == 0)
return m_sweep.dragging ? STR_C("Model along the track: release where it ends") : STR_C("Model along the track: LMB by the track where it starts, drag to the end");
return STR_C("Objects along the track");
case track_tab::infra: return STR_C("Infrastructure along the track");
case track_tab::turntable:
@@ -3310,6 +3401,13 @@ void editor_mode::delete_selected_track()
ui()->set_status(STR_C("The switch is replaced by its main track, Shift+Del removes all of it"), false);
}
push_track_snapshot({}, created, {track});
// the model and the events of a turntable can't stay without its track
if (track->eType == tt_Table)
for (auto const instance : turntable_includes(*track))
{
scene::Layers.removed(instance, true);
m_history.back().instances.push_back(instance);
}
if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end())
m_route = {};
m_node = nullptr;
@@ -3368,8 +3466,6 @@ std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All)
hints = {{"Drag a point", "in the profile window: changes the grade"}, {"Double click", "on the line: adds a point"}, {"Apply", "writes the heights into the tracks"}};
else if (m_track_window_open && m_track_tab == track_tab::speed)
hints = {{"LMB on a row", "show the path"}};
else if (m_track_window_open && m_track_tab == track_tab::lineside && m_lineside_tab == 0)
hints = {{"LMB by the track", "where it starts, the side of the track it goes on"}, {"Drag", "to where it ends, a click: one as long as the original"}, {"Release", "lays it"}, {"LMB away from the track", "selects another track"}};
else if (m_track_window_open && m_track_tab == track_tab::lineside)
hints = {{"LMB on a track", "the line the objects go along"}};
else if (m_track_window_open && m_track_tab == track_tab::joints)
@@ -4118,12 +4214,6 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
Length = 0.0;
if (Points.size() < 2)
return {};
alignment::design design;
design.norms = m_route.design.norms;
design.shape = m_route.design.shape;
design.speed = tool.style.velocity > 0.0 ? tool.style.velocity : 100.0;
design.start = Points.front();
design.end = Points.back();
auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])};
auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])};
if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3)
@@ -4131,8 +4221,75 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
Error = STR_C("Two points lie on one another");
return {};
}
design.start_direction = glm::normalize(startdirection);
design.end_direction = glm::normalize(enddirection);
alignment::design design;
auto const shape{lay_shape(Points, glm::normalize(startdirection), glm::normalize(enddirection), design)};
if (false == shape.valid)
{
Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front();
return {};
}
Error = shape.warnings.empty() ? std::string{} : shape.warnings.front();
auto const count = [&](alignment::result const &Shape, alignment::design const &Design) {
return std::max(alignment::minimum_pieces(Shape, Design), static_cast<std::size_t>(std::ceil(Shape.length / std::max(5.0, tool.piece_length))));
};
// the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them
auto const grade{(Points.back().y - Points.front().y) / std::max(1.0, plan_distance(Points.front(), Points.back()))};
auto const eased = [&](editor_track::snap_target const &Joint, double const Chainage, double const Sign) {
if (Joint.track == nullptr || Joint.track->eType != tt_Normal)
return easing{};
auto result{ease_out_of(*Joint.track, Joint.position, grade * Sign, design.speed, design.norms)};
auto const own{Sign * curvature_at(shape, Chainage)};
if (std::abs(own - result.curvature) <= 0.05 * std::abs(result.curvature))
result.pieces.clear();
return result;
};
auto const in{eased(tool.start, 0.0, 1.0)};
auto const out{eased(End, shape.length, -1.0)};
if (false == in.pieces.empty() || false == out.pieces.empty())
{
auto points{Points};
auto starting{design.start_direction};
auto ending{design.end_direction};
if (false == in.pieces.empty())
{
points.front() = in.end;
starting = in.direction;
}
if (false == out.pieces.empty())
{
points.back() = out.end;
ending = -out.direction;
}
alignment::design easeddesign;
auto const easedshape{lay_shape(points, starting, ending, easeddesign)};
if (easedshape.valid)
{
auto result{in.pieces};
auto const middle{alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign))};
result.insert(result.end(), middle.begin(), middle.end());
for (auto piece{out.pieces.rbegin()}; piece != out.pieces.rend(); ++piece)
result.push_back(turned_around(*piece));
Length = easedshape.length + (in.pieces.empty() ? 0.0 : in.length) + (out.pieces.empty() ? 0.0 : out.length);
Error = easedshape.warnings.empty() ? std::string{} : easedshape.warnings.front();
return result;
}
Error = STR("The curvature changes abruptly where the track joins, there's no room for a transition curve") + (easedshape.errors.empty() ? std::string{} : ": " + easedshape.errors.front());
}
Length = shape.length;
return alignment::pieces(shape, design, count(shape, design));
}
alignment::result editor_mode::lay_shape(std::vector<glm::dvec3> const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &design) const
{
auto const &tool{m_lay};
design = {};
design.norms = m_route.design.norms;
design.shape = m_route.design.shape;
design.speed = tool.style.velocity > 0.0 ? tool.style.velocity : 100.0;
design.start = Points.front();
design.end = Points.back();
design.start_direction = Startdirection;
design.end_direction = Enddirection;
alignment::vertex corner;
corner.radius = std::max(1.0, tool.radius);
@@ -4171,16 +4328,7 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction));
}
}
auto const shape{alignment::compute(design)};
if (false == shape.valid)
{
Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front();
return {};
}
Error = shape.warnings.empty() ? std::string{} : shape.warnings.front();
Length = shape.length;
auto const count{std::max(alignment::minimum_pieces(shape, design), static_cast<std::size_t>(std::ceil(shape.length / std::max(5.0, tool.piece_length))))};
return alignment::pieces(shape, design, count);
return alignment::compute(design);
}
void editor_mode::lay_click()
@@ -4232,6 +4380,23 @@ void editor_mode::lay_finish(editor_track::snap_target const &End)
tool.status = format(STR_C("Laid %zu paths, %.1f m"), created.size(), length);
if (false == error.empty())
tool.status += "\n" + error;
{
std::vector<std::pair<TTrack *, bool>> run;
if (tool.start.track != nullptr && tool.start.track->eType == tt_Normal)
run.emplace_back(tool.start.track, tool.start.point.kind == editor_track::point_kind::end);
for (auto *track : created)
run.emplace_back(track, true);
if (End.track != nullptr && End.track->eType == tt_Normal)
run.emplace_back(End.track, End.point.kind == editor_track::point_kind::start);
editor_track::curve curve;
if (run.size() > created.size() && editor_track::analyse_curve(run, m_route.design.norms.gauge, curve) && curve.compound)
for (std::size_t i = 1; i < curve.arcs.size(); ++i)
if (curve.arcs[i].transition < 1.0)
{
tool.status += "\n" + format(STR_C("Compound curve: R %.0f m meets R %.0f m with no transition curve between them"), curve.arcs[i - 1].radius, curve.arcs[i].radius);
break;
}
}
if (scene::Layers.active() == null_handle && style == nullptr)
tool.status += "\nThe scenery isn't opened for editing, the new track won't be saved";
WriteLog("Editor: " + tool.status, logtype::generic);

View File

@@ -232,8 +232,6 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
}
if (m_track_tab == track_tab::turntable)
return turntable_intent(ground, shift);
if (m_track_tab == track_tab::lineside && sweep_grabs(ground))
return make(kind::sweep, "Press: where the model starts, drag to its end");
if (m_track_tab == track_tab::path)
{
if (shift && control && m_hover.track != nullptr && rail(*m_hover.track))
@@ -598,11 +596,6 @@ void editor_mode::render_track_context()
show_track_tab(track_tab::lineside);
m_parallel.expand = true;
}
if (ImGui::MenuItem(STR_C("Model along the track")))
{
show_track_tab(track_tab::lineside);
m_sweep.expand = true;
}
if (ImGui::MenuItem(STR_C("Place a vehicle here")))
{
show_track_tab(track_tab::lineside);
@@ -1332,8 +1325,7 @@ void editor_mode::render_lineside_ui()
bool *expand;
std::function<void()> render;
};
std::array<tab, 5> const tabs{{
{"Platforms and models", &m_sweep.open, &m_sweep.expand, [this]() { render_sweep_ui(); }},
std::array<tab, 4> const tabs{{
{"Hectometre posts", &m_hekto.open, &m_hekto.expand, [this]() { render_hekto_ui(); }},
{"Fouling points", &m_fouling.open, &m_fouling.expand, [this]() { render_fouling_ui(); }},
{"Parallel track", &m_parallel.open, &m_parallel.expand, [this]() { render_parallel_ui(); }},

View File

@@ -27,7 +27,9 @@ http://mozilla.org/MPL/2.0/.
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <optional>
#include <regex>
#include <sstream>
@@ -536,6 +538,26 @@ void editor_mode::turntable_find_fits()
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};
@@ -1023,3 +1045,122 @@ void editor_mode::draw_turntable_overlay() const
label(projection, cursor, format("%.1f deg", yaw));
}
}
bool editor_mode::turntable_controls(std::vector<std::string> &Rootstatements)
{
if (scene::Layers.empty())
return false;
std::string root{scene::Layers.layer(scene::layer_handle{1}).name};
if (auto const slash{root.find_last_of("/\\")}; slash != std::string::npos)
root.erase(0, slash + 1);
if (auto const dot{root.rfind('.')}; dot != std::string::npos)
root.erase(dot);
if (root.empty())
return false;
auto const file{root + "_obrotnice.scm"};
std::ostringstream text;
auto tables{0};
for (auto const *table : simulation::Paths.sequence())
{
if (table == nullptr || table->eType != tt_Table || table->m_editorremoved || table->m_paths.empty() || table->name().empty())
continue;
auto const &name{table->name()};
std::vector<int> slots;
std::string events;
scan_turntables();
for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count() && slots.empty(); ++handle)
{
if (false == scene::Layers.tracked(handle))
continue;
auto const &included{scene::Layers.instance(handle)};
if (included.removed || included.dead || included.file == nullptr)
continue;
auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })};
if (kind == m_turntable.templates.end() || kind->angles == 0)
continue;
std::string target;
std::vector<std::string> values;
if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || kind->track > static_cast<int>(values.size()) || ToLower(values[kind->track - 1]) != name)
continue;
{
std::ifstream input{Global.asCurrentSceneryPath + kind->file, std::ios_base::binary};
events.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
events = ToLower(events);
std::replace_if(events.begin(), events.end(), [](char const Character) { return Character == '\t' || Character == '\r' || Character == '\n'; }, ' ');
auto const parameter{"(p" + std::to_string(kind->track) + ")"};
for (auto at{events.find(parameter)}; at != std::string::npos; at = events.find(parameter, at + name.size()))
events.replace(at, parameter.size(), name);
}
std::vector<double> seen;
for (int i = 0; i < kind->positions && kind->angles - 1 + i < static_cast<int>(values.size()); ++i)
if (double angle; numeric(values[kind->angles - 1 + i], angle) && std::none_of(seen.begin(), seen.end(), [&](double const Other) { return std::abs(circular(Other - angle)) < 0.01; }))
{
seen.push_back(angle);
slots.push_back(i + 1);
}
}
if (slots.empty())
for (int position = 1; position <= 64; ++position)
slots.push_back(position);
std::vector<int> positions;
for (auto const position : slots)
{
// the events of a turntable placed in this session get loaded only with the scenery loaded again
auto const event{name + "_tor" + std::to_string(position)};
if (events.find(" " + event + " ") != std::string::npos || simulation::Events.FindEvent(event) != nullptr)
positions.push_back(position);
}
if (positions.size() < 2)
continue;
++tables;
auto const count{positions.size()};
auto const centre{centre_of(*table)};
auto const at{format("%.3f %.3f %.3f", centre.x, centre.y, centre.z)};
text << "node -1 0 " << name << "_keylock memcell " << at << " none 0 0 none endmemcell\r\n";
text << "event " << name << "_lock updatevalues 0 " << name << "_keylock * 1 * endevent\r\n";
text << "event " << name << "_unlock updatevalues 1.5 " << name << "_keylock * 0 * endevent\r\n";
for (auto const forward : {true, false})
{
auto const step{std::string{forward ? "_next" : "_prev"}};
text << "event " << name << step << " multiple 0 " << name << "_keylock " << name << "_lock " << name << "_unlock " << name << step << '0';
for (auto const position : positions)
text << ' ' << name << step << position;
text << " condition memcompare * 0 * endevent\r\n";
// the turntable starts with no position set, the first step takes it to the first one
text << "event " << name << step << "0 multiple 0 " << name << ' ' << name << "_tor" << positions.front() << " condition memcompare * 0 * endevent\r\n";
for (std::size_t i = 0; i < count; ++i)
{
auto const target{positions[forward ? (i + 1) % count : (i + count - 1) % count]};
text << "event " << name << step << positions[i] << " multiple 0 " << name << ' ' << name << "_tor" << target << " condition memcompare * " << positions[i] << " * endevent\r\n";
}
}
text << "node -1 0 " << name << "_key eventlauncher " << at << " 150 t 0 " << name << "_next " << name << "_prev end\r\n";
}
auto const path{Global.asCurrentSceneryPath + file};
std::error_code error;
auto const exists{std::filesystem::exists(path, error)};
if (tables == 0 && false == exists)
return false;
auto const content{"// made by the scenery editor on each save: T turns each turntable to its next position, Shift+T to the previous one\r\n" + text.str()};
std::string current;
{
std::ifstream input{path, std::ios_base::binary};
current.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
}
if (current != content)
{
std::ofstream output{path, std::ios_base::binary | std::ios_base::trunc};
output << content;
}
auto included{m_turntable_included};
for (std::size_t handle = 1; handle <= scene::Layers.size() && false == included; ++handle)
{
auto name{scene::Layers.layer(static_cast<scene::layer_handle>(handle)).name};
std::replace(name.begin(), name.end(), '\\', '/');
included = ToLower(name) == ToLower(file) || ToLower(name).ends_with("/" + ToLower(file));
}
if (included)
return false;
Rootstatements.push_back("include " + file + " end");
return true;
}

View File

@@ -183,10 +183,15 @@ void editor_mode::render_vehicle_ui()
{
auto &tool{m_vehicle};
auto *selected{selected_track()};
if (selected != nullptr && selected != tool.track)
vehicle_start(*selected, bezier{selected->m_paths.front()}.point(0.5));
if (selected != nullptr && selected == tool.track && m_hover.track == selected && ImGui::IsMouseClicked(0) && false == ImGui::GetIO().WantCaptureMouse)
vehicle_start(*selected, m_hover.point);
else if (selected != nullptr && selected != tool.track)
vehicle_start(*selected, m_hover.track == selected ? m_hover.point : bezier{selected->m_paths.front()}.point(0.5));
if (tool.track == nullptr)
{
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("LMB on a track: where the vehicle stands"));
return;
}
if (tool.bank == nullptr)
{
tool.bank = std::make_shared<ui::vehicles_bank>();

View File

@@ -54,30 +54,30 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode)
switch (Mode)
{
case nodebank_panel::MODIFY:
ImGui::TextDisabled("LMB: select node F: focus End: drop to ground Del: delete");
ImGui::TextDisabled(STR_C("LMB: select node F: focus End: drop to ground Del: delete"));
break;
case nodebank_panel::COPY:
ImGui::TextDisabled("LMB: copy the clicked model to the node bank");
ImGui::TextDisabled(STR_C("LMB: copy the clicked model to the node bank"));
break;
case nodebank_panel::ADD:
{
ImGui::TextDisabled("LMB: insert a model at the cursor");
ImGui::Checkbox("Random model from set", &m_insertrandom);
ImGui::TextDisabled(STR_C("LMB: insert a model at the cursor"));
ImGui::Checkbox(STR_C("Random model from set"), &m_insertrandom);
if (m_insertrandom)
{
m_nodebankpanel.set_combo("Set##insert", m_insertset, nullptr);
m_nodebankpanel.set_combo(STR_C("Set##insert"), m_insertset, nullptr);
auto const count{m_nodebankpanel.set_entries(m_insertset).size()};
ImGui::TextDisabled("%zu templates in set%s", count, count == 0 ? ", the node bank selection is used" : "");
ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : "");
}
else
{
ImGui::TextDisabled("Inserts the template selected in the node bank");
ImGui::TextDisabled(STR_C("Inserts the template selected in the node bank"));
}
m_functionspanel.render_controls();
break;
}
case nodebank_panel::BRUSH:
ImGui::TextDisabled("Hold LMB: paint models along the cursor path");
ImGui::TextDisabled(STR_C("Hold LMB: paint models along the cursor path"));
m_brushobjects.render_options(m_nodebankpanel);
m_functionspanel.render_controls();
break;
@@ -92,22 +92,32 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode)
void editor_ui::render_header_sections()
{
if (ImGui::CollapsingHeader("Gizmo", ImGuiTreeNodeFlags_DefaultOpen))
if (ImGui::CollapsingHeader(STR_C("Gizmo"), ImGuiTreeNodeFlags_DefaultOpen))
{
if (m_gizmooptions)
m_gizmooptions();
}
if (ImGui::CollapsingHeader("Node properties", ImGuiTreeNodeFlags_DefaultOpen))
if (ImGui::CollapsingHeader(STR_C("Node properties"), ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Indent();
m_itempropertiespanel.render_body();
ImGui::Unindent();
}
if (ImGui::CollapsingHeader("Array"))
if (ImGui::CollapsingHeader(STR_C("Array")))
{
if (m_arrayoptions)
m_arrayoptions();
}
if (m_bendexpand)
{
ImGui::SetNextItemOpen(true);
m_bendexpand = false;
}
if (ImGui::CollapsingHeader(STR_C("Bend along the track")))
{
if (m_bendoptions)
m_bendoptions();
}
}
void editor_ui::render_menu_contents()
@@ -118,9 +128,13 @@ void editor_ui::render_menu_contents()
m_newscenery();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The editor starts again with the wizard of a new scenery: its name and its centre on the map"));
if (ImGui::MenuItem(STR_C("Open scenery...")) && m_openscenery)
m_openscenery();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The editor starts again with another scenery of the scenery folder"));
// changes go to the scenery files only if the scenery was loaded with its sources tracked
auto const editsession{false == scene::Layers.empty()};
if (ImGui::MenuItem(STR_C("Save"), "Ctrl+S", false, editsession) && m_save)
if (ImGui::MenuItem(STR_C("Save"), STR_C("Ctrl+S"), false, editsession) && m_save)
{
m_save();
}
@@ -128,7 +142,7 @@ void editor_ui::render_menu_contents()
{
ImGui::SetTooltip("%s", STR_C("Start the simulator with -edit <scenery file> to save changes to the scenery files"));
}
if (ImGui::MenuItem(STR_C("Export scenery dump"), "Ctrl+Shift+F11") && m_export)
if (ImGui::MenuItem(STR_C("Export scenery dump"), STR_C("Ctrl+Shift+F11")) && m_export)
{
m_export();
}
@@ -174,6 +188,8 @@ void editor_ui::render_rotation_controls()
void editor_ui::set_node(scene::basic_node *Node)
{
m_node = Node;
m_itempropertiespanel.update(m_node);
m_functionspanel.update(m_node);
}
void editor_ui::set_include(include_selection *Include)

View File

@@ -71,9 +71,18 @@ class editor_ui : public ui_layer
{
m_arrayoptions = std::move(Renderer);
}
void set_bend_options(std::function<void()> Renderer)
{
m_bendoptions = std::move(Renderer);
}
void expand_bend()
{
m_bendexpand = true;
}
// actions of the file menu, performed by the editor mode
void set_file_actions(std::function<void()> Save, std::function<void()> Export);
void set_new_scenery(std::function<void()> New) { m_newscenery = std::move(New); }
void set_open_scenery(std::function<void()> Open) { m_openscenery = std::move(Open); }
// shows outcome of an operation in the layers window
void set_status(std::string const &Status, bool const Error = false);
@@ -91,6 +100,7 @@ class editor_ui : public ui_layer
std::function<void()> m_save;
std::function<void()> m_export;
std::function<void()> m_newscenery;
std::function<void()> m_openscenery;
brush_object_list m_brushobjects;
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
model_set_ref m_insertset;
@@ -98,6 +108,8 @@ class editor_ui : public ui_layer
std::function<void()> m_menuoptions;
std::function<void()> m_gizmooptions;
std::function<void()> m_arrayoptions;
std::function<void()> m_bendoptions;
bool m_bendexpand{false};
scene::basic_node *m_node{nullptr}; // currently bound scene node, if any
bool m_track{false};
};

View File

@@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/renderer.h"
#include "simulation/simulation.h"
#include "editor/editorModelSets.hpp"
#include "editor/editorFormat.hpp"
namespace
{
@@ -369,11 +370,11 @@ void itemproperties_panel::render_name_editor()
m_namednode = m_node;
std::snprintf(m_namebuffer, sizeof(m_namebuffer), "%s", picked->name().c_str());
}
auto const entered{ImGui::InputTextWithHint("name", "(none)", m_namebuffer, IM_ARRAYSIZE(m_namebuffer), ImGuiInputTextFlags_EnterReturnsTrue)};
auto const entered{ImGui::InputTextWithHint(STR_C("name"), STR_C("(none)"), m_namebuffer, IM_ARRAYSIZE(m_namebuffer), ImGuiInputTextFlags_EnterReturnsTrue)};
m_nameactive = ImGui::IsItemActive();
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it");
ImGui::SetTooltip(STR_C("Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it"));
}
if (entered || ImGui::IsItemDeactivatedAfterEdit())
{
@@ -420,7 +421,7 @@ void itemproperties_panel::render_transform_editor()
if (typeid(*m_node) != typeid(TAnimModel)) { return; }
auto *picked = static_cast<TAnimModel *>(m_node);
if (false == ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
if (false == ImGui::CollapsingHeader(STR_C("Transform"), ImGuiTreeNodeFlags_DefaultOpen))
{
return;
}
@@ -442,7 +443,7 @@ void itemproperties_panel::render_transform_editor()
clamp_circular(picked->Angles().x),
clamp_circular(picked->Angles().y),
clamp_circular(picked->Angles().z)};
if (ImGui::DragFloat3("rotation (deg)", &angles.x, 0.5f, 0.0f, 360.0f, "%.3f"))
if (ImGui::DragFloat3(STR_C("rotation (deg)"), &angles.x, 0.5f, 0.0f, 360.0f, "%.3f"))
{
picked->Angles(angles);
}
@@ -451,15 +452,15 @@ void itemproperties_panel::render_transform_editor()
// Scale — per-axis float, 1.000 display. Clamped to a reasonable positive range.
{
glm::vec3 scale = picked->Scale();
if (ImGui::DragFloat3("scale (x,y,z)", &scale.x, 0.01f, 0.001f, 100.0f, "%.3f"))
if (ImGui::DragFloat3(STR_C("scale (x,y,z)"), &scale.x, 0.01f, 0.001f, 100.0f, "%.3f"))
{
picked->Scale(scale);
}
}
if (ImGui::Button("reset rotation")) { picked->Angles(glm::vec3(0.0f)); }
if (ImGui::Button(STR_C("reset rotation"))) { picked->Angles(glm::vec3(0.0f)); }
ImGui::SameLine();
if (ImGui::Button("reset scale")) { picked->Scale(glm::vec3(1.0f)); }
if (ImGui::Button(STR_C("reset scale"))) { picked->Scale(glm::vec3(1.0f)); }
}
bool itemproperties_panel::render_group()
@@ -474,7 +475,7 @@ bool itemproperties_panel::render_group()
return false;
}
if (false == ImGui::CollapsingHeader("Parent Group"))
if (false == ImGui::CollapsingHeader(STR_C("Parent Group")))
{
return false;
}
@@ -501,14 +502,14 @@ std::string *brush_object_list::GetRandomObject()
void brush_object_list::render_options(nodebank_panel &Bank)
{
ImGui::SliderFloat("Spacing", &spacing, 0.1f, 20.0f, "%.1f m");
ImGui::Checkbox("Random model from set", &useRandom);
ImGui::SliderFloat(STR_C("Spacing"), &spacing, 0.1f, 20.0f, "%.1f m");
ImGui::Checkbox(STR_C("Random model from set"), &useRandom);
if (false == useRandom)
{
ImGui::TextDisabled("Paints the template selected in the node bank");
ImGui::TextDisabled(STR_C("Paints the template selected in the node bank"));
return;
}
Bank.set_combo("Preset", source, "Manual list");
Bank.set_combo(STR_C("Preset"), source, STR_C("Manual list"));
if (source.kind == model_set_ref::source::manual)
{
Bank.manual_list("brushset", Objects, idx);
@@ -516,7 +517,7 @@ void brush_object_list::render_options(nodebank_panel &Bank)
else
{
auto const count{Bank.set_entries(source).size()};
ImGui::TextDisabled("%zu templates in set%s", count, count == 0 ? ", the node bank selection is used" : "");
ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : "");
}
}
@@ -608,7 +609,7 @@ void nodebank_panel::render()
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{
if (ImGui::Button("Reload node bank"))
if (ImGui::Button(STR_C("Reload node bank")))
{
nodebank_reload();
}
@@ -624,7 +625,7 @@ void nodebank_panel::render()
}
// edit modes as tabs, each with its own settings
std::pair<char const *, edit_mode> const modes[] = {{"Select", MODIFY}, {"Insert", ADD}, {"Brush", BRUSH}, {"Area fill", FILL}, {"Copy to bank", COPY}};
std::pair<char const *, edit_mode> const modes[] = {{STR_C("Select"), MODIFY}, {STR_C("Insert"), ADD}, {STR_C("Brush"), BRUSH}, {STR_C("Area fill"), FILL}, {STR_C("Copy to bank"), COPY}};
if (ImGui::BeginTabBar("##editmodes", ImGuiTabBarFlags_FittingPolicyResizeDown))
{
for (auto const &tab : modes)
@@ -646,7 +647,7 @@ void nodebank_panel::render()
ImGui::Separator();
ImGui::PushItemWidth(-1);
ImGui::InputTextWithHint("Search", "Search node bank", m_nodesearch, IM_ARRAYSIZE(m_nodesearch));
ImGui::InputTextWithHint(STR_C("Search"), STR_C("Search node bank"), m_nodesearch, IM_ARRAYSIZE(m_nodesearch));
// the list takes the rest of the window, but keeps a usable height when the sections above are expanded (the window scrolls then)
auto const listheight{std::max(ImGui::GetContentRegionAvail().y, ImGui::GetTextLineHeightWithSpacing() * 10.0f)};
if (ImGui::ListBoxHeader("##nodebank", ImVec2(-1, listheight)))
@@ -678,7 +679,7 @@ void nodebank_panel::render()
}
}
// scenery templates. placing one adds an include directive to a scenery file, so they take scenery opened for editing
if (Global.editor_session && ImGui::CollapsingHeader("Scenery templates (.inc)"))
if (Global.editor_session && ImGui::CollapsingHeader(STR_C("Scenery templates (.inc)")))
{
if (false == EditorIncludes.scanned())
{
@@ -738,11 +739,11 @@ bool nodebank_panel::set_combo(char const *Label, model_set_ref &Ref, char const
changed = true;
}
}
ImGui::TextDisabled("User sets");
ImGui::TextDisabled(STR_C("User sets"));
auto const &sets{EditorModelSets.sets()};
if (sets.empty())
{
ImGui::TextDisabled(" (none yet, see Model sets...)");
ImGui::TextDisabled(STR_C(" (none yet, see Model sets...)"));
}
for (auto const &set : sets)
{
@@ -753,7 +754,7 @@ bool nodebank_panel::set_combo(char const *Label, model_set_ref &Ref, char const
changed = true;
}
}
ImGui::TextDisabled("Node bank groups");
ImGui::TextDisabled(STR_C("Node bank groups"));
auto const groups{group_names()};
for (int idx = 0; idx < static_cast<int>(groups.size()); ++idx)
{
@@ -781,33 +782,33 @@ void nodebank_panel::manual_list(char const *Id, std::vector<std::string> &List,
}
if (List.empty())
{
ImGui::TextDisabled("(empty)");
ImGui::TextDisabled(STR_C("(empty)"));
}
ImGui::ListBoxFooter();
}
if (ImGui::Button("Add selected"))
if (ImGui::Button(STR_C("Add selected")))
{
if (node_selected())
List.push_back(*m_selectedtemplate);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Adds the template selected in the node bank list");
ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list"));
}
ImGui::SameLine();
if (ImGui::Button("Remove") && Selected >= 0 && Selected < static_cast<int>(List.size()))
if (ImGui::Button(STR_C("Remove")) && Selected >= 0 && Selected < static_cast<int>(List.size()))
{
List.erase(List.begin() + Selected);
Selected = -1;
}
ImGui::SameLine();
if (ImGui::Button("Clear"))
if (ImGui::Button(STR_C("Clear")))
{
List.clear();
Selected = -1;
}
ImGui::SameLine();
if (ImGui::Button("Save as set") && false == List.empty())
if (ImGui::Button(STR_C("Save as set")) && false == List.empty())
{
open_sets_window(EditorModelSets.create("New set", List));
}
@@ -891,7 +892,7 @@ void nodebank_panel::open_sets_window(int const Setid)
void nodebank_panel::render_sets_window()
{
ImGui::SetNextWindowSize(ImVec2S(600, 380), ImGuiCond_FirstUseEver);
if (false == ImGui::Begin("Model sets", &m_setsopen, ImGuiWindowFlags_NoCollapse))
if (false == ImGui::Begin(STR_C("Model sets"), &m_setsopen, ImGuiWindowFlags_NoCollapse))
{
ImGui::End();
return;
@@ -919,10 +920,10 @@ void nodebank_panel::render_sets_window()
}
if (sets.empty())
{
ImGui::TextDisabled("No sets yet");
ImGui::TextDisabled(STR_C("No sets yet"));
}
ImGui::EndChild();
if (ImGui::Button("New"))
if (ImGui::Button(STR_C("New")))
{
open_sets_window(EditorModelSets.create("New set"));
}
@@ -930,27 +931,27 @@ void nodebank_panel::render_sets_window()
if (edited != nullptr)
{
ImGui::SameLine();
if (ImGui::Button("Duplicate"))
if (ImGui::Button(STR_C("Duplicate")))
{
open_sets_window(EditorModelSets.create(edited->name + " copy", edited->templates));
edited = EditorModelSets.find(m_setsselected);
}
ImGui::SameLine();
if (ImGui::Button("Delete"))
if (ImGui::Button(STR_C("Delete")))
{
ImGui::OpenPopup("##deleteset");
}
if (ImGui::BeginPopup("##deleteset"))
{
ImGui::Text("Delete set \"%s\"?", edited->name.c_str());
if (ImGui::Button("Delete##confirm"))
ImGui::Text(STR_C("Delete set \"%s\"?"), edited->name.c_str());
if (ImGui::Button(STR_C("Delete##confirm")))
{
EditorModelSets.remove(edited->id);
edited = nullptr;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
@@ -965,8 +966,8 @@ void nodebank_panel::render_sets_window()
ImGui::BeginGroup();
if (edited == nullptr)
{
ImGui::TextDisabled("Create a set with \"New\", or use \"Save as set\" on a brush or area fill list.");
ImGui::TextDisabled("Sets can be chosen in Insert (random model), Brush and Area fill.");
ImGui::TextDisabled(STR_C("Create a set with \"New\", or use \"Save as set\" on a brush or area fill list."));
ImGui::TextDisabled(STR_C("Sets can be chosen in Insert (random model), Brush and Area fill."));
}
else
{
@@ -980,7 +981,7 @@ void nodebank_panel::render_sets_window()
auto const entered{ImGui::InputText("##setname", m_setsname, IM_ARRAYSIZE(m_setsname), ImGuiInputTextFlags_EnterReturnsTrue)};
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button("Rename") || entered)
if (ImGui::Button(STR_C("Rename")) || entered)
{
EditorModelSets.rename(edited->id, m_setsname);
m_setsnameid = 0; // reload, the name could have been adjusted to stay unique
@@ -995,42 +996,42 @@ void nodebank_panel::render_sets_window()
}
if (edited->templates.empty())
{
ImGui::TextDisabled("(empty) select a template in the node bank and press \"Add selected\"");
ImGui::TextDisabled(STR_C("(empty) select a template in the node bank and press \"Add selected\""));
}
ImGui::EndChild();
auto const setid{edited->id};
if (ImGui::Button("Add selected"))
if (ImGui::Button(STR_C("Add selected")))
{
if (node_selected())
EditorModelSets.add(setid, {*m_selectedtemplate});
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Adds the template selected in the node bank list");
ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list"));
}
ImGui::SameLine();
if (ImGui::Button("Remove") && m_setsentry >= 0)
if (ImGui::Button(STR_C("Remove")) && m_setsentry >= 0)
{
EditorModelSets.erase(setid, static_cast<std::size_t>(m_setsentry));
m_setsentry = -1;
}
ImGui::SameLine();
if (ImGui::Button("Clear"))
if (ImGui::Button(STR_C("Clear")))
{
ImGui::OpenPopup("##clearset");
}
if (ImGui::BeginPopup("##clearset"))
{
ImGui::TextUnformatted("Remove all templates from this set?");
if (ImGui::Button("Clear##confirm"))
ImGui::TextUnformatted(STR_C("Remove all templates from this set?"));
if (ImGui::Button(STR_C("Clear##confirm")))
{
EditorModelSets.clear(setid);
m_setsentry = -1;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
@@ -1053,7 +1054,7 @@ void nodebank_panel::render_sets_window()
}
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button("Add group"))
if (ImGui::Button(STR_C("Add group")))
{
std::vector<std::string> templates;
for (auto const *entry : set_entries(m_setsgroup))
@@ -1191,16 +1192,16 @@ void functions_panel::render()
void functions_panel::render_controls()
{
// order matches rotation_mode
char const *const modes[] = {"Random", "Fixed", "Default (from template)"};
char const *const modes[] = {STR_C("Random"), STR_C("Fixed"), STR_C("Default (from template)")};
auto current{static_cast<int>(rot_mode)};
if (ImGui::Combo("Rotation", &current, modes, IM_ARRAYSIZE(modes)))
if (ImGui::Combo(STR_C("Rotation"), &current, modes, IM_ARRAYSIZE(modes)))
{
rot_mode = static_cast<rotation_mode>(current);
}
if (rot_mode == FIXED)
{
// ImGui::Checkbox("Get rotation from last object", &rot_from_last);
ImGui::SliderFloat("Rotation value", &rot_value, 0.0f, 360.0f, "%.1f deg");
// ImGui::Checkbox(STR_C("Get rotation from last object"), &rot_from_last);
ImGui::SliderFloat(STR_C("Rotation value"), &rot_value, 0.0f, 360.0f, STR_C("%.1f deg"));
}
}
@@ -1280,7 +1281,7 @@ void itemproperties_panel::render_include()
auto &include{*m_include};
include.active = false;
ImGui::Text("include: %s", Bezogonkow(include.target).c_str());
ImGui::Text(STR_C("include: %s"), Bezogonkow(include.target).c_str());
if (false == include.info.name.empty())
{
ImGui::TextDisabled("%s", include.info.name.c_str());
@@ -1294,7 +1295,7 @@ void itemproperties_panel::render_include()
}
if (include.values.empty())
{
ImGui::TextDisabled("The template takes no parameters.");
ImGui::TextDisabled(STR_C("The template takes no parameters."));
return;
}
@@ -1339,7 +1340,7 @@ void itemproperties_panel::render_include()
ImGui::PopItemWidth();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
ImGui::TextWrapped("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again.");
ImGui::TextWrapped(STR_C("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again."));
ImGui::PopStyleColor();
}
@@ -1365,7 +1366,7 @@ void layers_panel::render()
if (scene::Layers.empty())
{
// layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping
ImGui::TextDisabled("Scenery layers are available in an edit session.\nStart the simulator with: -edit <scenery file>");
ImGui::TextDisabled(STR_C("Scenery layers are available in an edit session.\nStart the simulator with: -edit <scenery file>"));
}
else
{
@@ -1375,32 +1376,32 @@ void layers_panel::render()
}
auto const selected{m_selected != null_handle};
auto const removed{selected && scene::Layers.layer(m_selected).removed};
std::string reason{"Select a layer first"};
std::string reason{STR("Select a layer first")};
if (ImGui::Button("New..."))
if (ImGui::Button(STR_C("New...")))
{
m_newname[0] = '\0';
m_newparent = (selected && false == removed && false == scene::Layers.layer(m_selected).binary ? m_selected : scene::layer_handle{1});
m_popuperror.clear();
ImGui::OpenPopup("New layer");
ImGui::OpenPopup(STR_C("New layer"));
}
ImGui::SameLine();
if (removed)
{
if (ImGui::Button("Restore"))
if (ImGui::Button(STR_C("Restore")))
{
scene::Layers.restore(m_selected);
report("Layer \"" + layer_name(m_selected) + "\" restored");
report(format(STR_C("Layer \"%s\" restored"), layer_name(m_selected).c_str()));
}
}
else if (action_button("Remove...", selected && scene::Layers.can_remove(m_selected, reason), reason))
else if (action_button(STR_C("Remove..."), selected && scene::Layers.can_remove(m_selected, reason), reason))
{
ImGui::OpenPopup("Remove layer");
ImGui::OpenPopup(STR_C("Remove layer"));
}
ImGui::SameLine();
// the scenario file is what holds the scenery together, it stays where it is
auto const mergeable{selected && false == removed && (scene::Layers.layer(m_selected).created || false == scene::Layers.layer(m_selected).sites.empty())};
if (action_button("Merge into...", mergeable, selected && false == removed ? "The scenario file can't be merged into another layer" : "Select a layer first"))
if (action_button(STR_C("Merge into..."), mergeable, selected && false == removed ? STR("The scenario file can't be merged into another layer") : STR("Select a layer first")))
{
// the layer which includes the selected one is the most likely target
m_mergetarget = scene::Layers.resolve(scene::Layers.layer(m_selected).parent);
@@ -1408,16 +1409,16 @@ void layers_panel::render()
{
m_mergetarget = null_handle;
}
ImGui::OpenPopup("Merge layer");
ImGui::OpenPopup(STR_C("Merge layer"));
}
ImGui::SameLine();
if (ImGui::Button("Save") && save)
if (ImGui::Button(STR_C("Save")) && save)
{
save();
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Writes changed nodes and layers to the scenery files (Ctrl+S)");
ImGui::SetTooltip(STR_C("Writes changed nodes and layers to the scenery files (Ctrl+S)"));
}
ImGui::Separator();
@@ -1447,7 +1448,7 @@ void layers_panel::render_list()
{
// the list takes the window except for the room left for the status
ImGui::BeginChild("##layers", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing() * 2.5f));
ImGui::TextDisabled("visible, locked, active, layer file");
ImGui::TextDisabled(STR_C("visible, locked, active, layer file"));
for (auto const handle : scene::Layers.tree())
{
@@ -1489,7 +1490,7 @@ void layers_panel::render_list()
{
if (std::string reason; false == scene::Layers.accepts(handle, &reason))
{
report("Layer \"" + layer_name(handle) + "\" can't take new nodes" + (reason.empty() ? "" : ": " + reason), true);
report(format(STR_C("Layer \"%s\" can't take new nodes"), layer_name(handle).c_str()) + (reason.empty() ? "" : ": " + reason), true);
}
else
{
@@ -1498,7 +1499,7 @@ void layers_panel::render_list()
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Active layer: nodes created in the editor are placed in it");
ImGui::SetTooltip(STR_C("Active layer: nodes created in the editor are placed in it"));
}
ImGui::SameLine();
auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"};
@@ -1524,7 +1525,7 @@ void layers_panel::render_list()
}
if (ImGui::IsItemHovered())
{
std::string content{layer.created ? "New layer, its file is created on save" : isremoved ? "Dropped from the scenery on save" : "Scenery file"};
std::string content{layer.created ? STR("New layer, its file is created on save") : isremoved ? STR("Dropped from the scenery on save") : STR("Scenery file")};
if (scene::Layers.valid(layer.parent))
{
content += "\nincluded by: " + layer_name(scene::Layers.resolve(layer.parent));
@@ -1554,10 +1555,10 @@ void layers_panel::render_list()
void layers_panel::render_popups()
{
if (ImGui::BeginPopupModal("New layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
if (ImGui::BeginPopupModal(STR_C("New layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("File of the new layer, in the scenery directory:");
ImGui::InputTextWithHint("##name", "name.scm", m_newname, IM_ARRAYSIZE(m_newname));
ImGui::TextUnformatted(STR_C("File of the new layer, in the scenery directory:"));
ImGui::InputTextWithHint("##name", STR_C("name.scm"), m_newname, IM_ARRAYSIZE(m_newname));
if (ImGui::BeginCombo("Included by", layer_name(m_newparent).c_str()))
{
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
@@ -1578,25 +1579,25 @@ void layers_panel::render_popups()
{
ImGui::TextColored(status_errorcolor, "%s", m_popuperror.c_str());
}
if (ImGui::Button("Create"))
if (ImGui::Button(STR_C("Create")))
{
auto const created{scene::Layers.create(m_newname, m_newparent, m_popuperror)};
if (created != null_handle)
{
m_selected = created;
report("Layer \"" + layer_name(created) + "\" created and made active. Its file is written on save.");
report(format(STR_C("Layer \"%s\" created and made active. Its file is written on save."), layer_name(created).c_str()));
ImGui::CloseCurrentPopup();
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("Remove layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
if (ImGui::BeginPopupModal(STR_C("Remove layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
if (false == scene::Layers.listed(m_selected))
{
@@ -1614,30 +1615,30 @@ void layers_panel::render_popups()
++included;
}
}
ImGui::Text("Remove layer \"%s\" from the scenery?", layer_name(m_selected).c_str());
ImGui::Text(STR_C("Remove layer \"%s\" from the scenery?"), layer_name(m_selected).c_str());
ImGui::TextUnformatted(layer.created ? "The layer wasn't saved yet, so it leaves no file behind." : "Its include directive is erased on save. The file itself stays on the disk.");
if (included > 0)
{
ImGui::Text("%d layer(s) it includes are removed with it.", included);
ImGui::Text(STR_C("%d layer(s) it includes are removed with it."), included);
}
if (auto const items{layer_content(layer)}; false == items.empty())
{
ImGui::Separator();
ImGui::TextUnformatted("The layer holds:");
ImGui::TextUnformatted(STR_C("The layer holds:"));
ImGui::TextUnformatted(items.c_str());
ImGui::TextDisabled("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells.");
ImGui::TextDisabled(STR_C("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells."));
}
if (ImGui::Button("Remove"))
if (ImGui::Button(STR_C("Remove")))
{
auto const name{layer_name(m_selected)};
if (scene::Layers.remove(m_selected))
{
report("Layer \"" + name + "\" removed. It can be restored until the scenery is saved.");
report(format(STR_C("Layer \"%s\" removed. It can be restored until the scenery is saved."), name.c_str()));
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
@@ -1645,7 +1646,7 @@ void layers_panel::render_popups()
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("Merge layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
if (ImGui::BeginPopupModal(STR_C("Merge layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
if (false == scene::Layers.listed(m_selected))
{
@@ -1653,7 +1654,7 @@ void layers_panel::render_popups()
}
else
{
ImGui::Text("Move content of layer \"%s\" to:", layer_name(m_selected).c_str());
ImGui::Text(STR_C("Move content of layer \"%s\" to:"), layer_name(m_selected).c_str());
std::string reason;
auto targets{0};
if (ImGui::BeginCombo("##target", layer_name(m_mergetarget).c_str()))
@@ -1673,27 +1674,27 @@ void layers_panel::render_popups()
}
if (targets == 0)
{
ImGui::TextDisabled("no layer can take it");
ImGui::TextDisabled(STR_C("no layer can take it"));
}
ImGui::EndCombo();
}
ImGui::TextDisabled("The text of the file is moved on save, the emptied file stays on the disk.\n"
ImGui::TextDisabled(STR_C("The text of the file is moved on save, the emptied file stays on the disk.\n"
"Merged into the layer which includes it, the content stays where the include was.\n"
"Merged into any other layer it's placed at its end, which changes the load order.");
"Merged into any other layer it's placed at its end, which changes the load order."));
auto const possible{m_mergetarget != null_handle && scene::Layers.can_merge(m_selected, m_mergetarget, reason)};
if (action_button("Merge", possible, m_mergetarget != null_handle ? reason : "Pick the target layer"))
if (action_button(STR_C("Merge"), possible, m_mergetarget != null_handle ? reason : STR("Pick the target layer")))
{
auto const name{layer_name(m_selected)};
auto const target{m_mergetarget};
if (scene::Layers.merge(m_selected, target))
{
report("Layer \"" + name + "\" merged into \"" + layer_name(target) + "\". The files are changed on save.");
report(format(STR_C("Layer \"%s\" merged into \"%s\". The files are changed on save."), name.c_str(), layer_name(target).c_str()));
m_selected = target;
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
@@ -1740,24 +1741,24 @@ void includes_panel::render_list()
auto const &entries{EditorIncludes.entries()};
ImGui::PushItemWidth(ImGui::GetFontSize() * 12.0f);
auto changed{ImGui::InputTextWithHint("##filter", "Search templates", m_filter, IM_ARRAYSIZE(m_filter))};
auto changed{ImGui::InputTextWithHint("##filter", STR_C("Search templates"), m_filter, IM_ARRAYSIZE(m_filter))};
ImGui::PopItemWidth();
ImGui::SameLine();
changed |= ImGui::Checkbox("Described", &m_describedonly);
changed |= ImGui::Checkbox(STR_C("Described"), &m_describedonly);
if (false == scene::Layers.templates().empty())
{
// known only for scenery opened for editing
ImGui::SameLine();
changed |= ImGui::Checkbox("Used by the scenery", &m_usedonly);
changed |= ImGui::Checkbox(STR_C("Used by the scenery"), &m_usedonly);
}
ImGui::SameLine();
if (ImGui::Button("Rescan"))
if (ImGui::Button(STR_C("Rescan")))
{
EditorIncludes.scan();
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Looks through the scenery directory again, for the templates added or changed outside of the editor");
ImGui::SetTooltip(STR_C("Looks through the scenery directory again, for the templates added or changed outside of the editor"));
}
if (changed || m_listrevision != EditorIncludes.revision())
@@ -1782,7 +1783,7 @@ void includes_panel::render_list()
m_listed.emplace_back(idx);
}
}
ImGui::TextDisabled("%d template(s) in the scenery directory, %d described, %d ready for the node bank", static_cast<int>(entries.size()), m_described,
ImGui::TextDisabled(STR_C("%d template(s) in the scenery directory, %d described, %d ready for the node bank"), static_cast<int>(entries.size()), m_described,
static_cast<int>(EditorIncludes.ready().size()));
ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 9.0f), true);
@@ -1805,7 +1806,7 @@ void includes_panel::render_list()
}
if (false == entry.complete && false == entry.issue.empty() && ImGui::IsItemHovered())
{
ImGui::SetTooltip("Not in the node bank: %s", Bezogonkow(entry.issue).c_str());
ImGui::SetTooltip(STR_C("Not in the node bank: %s"), Bezogonkow(entry.issue).c_str());
}
if (clicked)
{
@@ -1831,13 +1832,13 @@ void includes_panel::render_description()
ImGui::Columns(4, "##parameters", false);
ImGui::SetColumnWidth(0, ImGui::CalcTextSize("(p000) ").x);
ImGui::SetColumnWidth(1, ImGui::CalcTextSize("texture ").x + ImGui::GetFrameHeight() * 2.0f);
ImGui::TextDisabled("param");
ImGui::TextDisabled(STR_C("param"));
ImGui::NextColumn();
ImGui::TextDisabled("role");
ImGui::TextDisabled(STR_C("role"));
ImGui::NextColumn();
ImGui::TextDisabled("label");
ImGui::TextDisabled(STR_C("label"));
ImGui::NextColumn();
ImGui::TextDisabled("default");
ImGui::TextDisabled(STR_C("default"));
ImGui::NextColumn();
for (auto &parameter : m_info.parameters)
{
@@ -1883,35 +1884,35 @@ void includes_panel::render_description()
ImGui::PushTextWrapPos(0.0f);
if (std::string issue; editor_includes::complete(m_info, m_parameters, &issue))
{
ImGui::TextUnformatted("Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates).");
ImGui::TextUnformatted(STR_C("Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates)."));
}
else
{
ImGui::TextColored(status_errorcolor, "Not for the node bank yet: %s.", issue.c_str());
ImGui::TextColored(status_errorcolor, STR_C("Not for the node bank yet: %s."), issue.c_str());
}
ImGui::PopTextWrapPos();
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\n"
ImGui::SetTooltip(STR_C("A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\n"
"Insert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\n"
"The other parameters receive their default values.");
"The other parameters receive their default values."));
}
if (ImGui::Button("Suggest roles"))
if (ImGui::Button(STR_C("Suggest roles")))
{
editor_includes::suggest(m_file, m_info);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are");
ImGui::SetTooltip(STR_C("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are"));
}
ImGui::SameLine();
if (ImGui::Button("Add parameter"))
if (ImGui::Button(STR_C("Add parameter")))
{
m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1);
}
ImGui::SameLine();
if (ImGui::Button("Save to file"))
if (ImGui::Button(STR_C("Save to file")))
{
std::string error;
m_statuserror = false == editor_includes::save(m_file, m_info, error);
@@ -1922,7 +1923,7 @@ void includes_panel::render_description()
}
}
ImGui::SameLine();
if (ImGui::Button("Reload"))
if (ImGui::Button(STR_C("Reload")))
{
open(m_file);
}
@@ -1944,11 +1945,11 @@ void includes_panel::render()
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
{
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
ImGui::TextWrapped("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; "
"the description is stored in the template file, as //$e comment lines.");
ImGui::TextWrapped(STR_C("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; "
"the description is stored in the template file, as //$e comment lines."));
if (false == Global.editor_session)
{
ImGui::TextWrapped("The node bank offers the templates in a scenery opened for editing (-edit).");
ImGui::TextWrapped(STR_C("The node bank offers the templates in a scenery opened for editing (-edit)."));
}
ImGui::PopStyleColor();