mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 10:59:20 +02:00
1700 lines
71 KiB
C++
1700 lines
71 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 "editor/editorSettings.hpp"
|
|
#include "editor/editorGeometry.hpp"
|
|
#include "application/application.h"
|
|
#include "application/editoruilayer.h"
|
|
#include "application/editorprojection.h"
|
|
|
|
#include "simulation/simulation.h"
|
|
#include "simulation/simulationtime.h"
|
|
#include "scene/scenelayers.h"
|
|
#include "utilities/Globals.h"
|
|
#include "utilities/Logs.h"
|
|
#include "world/Track.h"
|
|
#include "world/TrackRails.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>
|
|
#include <sstream>
|
|
|
|
// the script is a text file, one action a line:
|
|
// wait <frames>
|
|
// camera <x> <y> <z> <x> <y> <z> from the first point, looking at the second
|
|
// select <track name>
|
|
// frog [name] logs where the inner rails of each switch cross: distance on both paths, 1:n, throat, nose, check rails
|
|
// guards [name] logs each guard rail and what each of its ends and breaks is cut along; a fault when a cut is not along a rail or a glued end not on the guard rail of the other track
|
|
// railtime lays out the rails of every track, as when its geometry is made, and logs how long it took
|
|
// switches <0|1> sets every switch to the path and moves its blades there at once
|
|
// crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose
|
|
// frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings
|
|
// geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too
|
|
// 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
|
|
// addmodel <x> <z> the model instance nearest the point added to the selection, or taken out (Shift+LMB)
|
|
// selection logs the selected models, with the models of their groups
|
|
// moveselection <x> <y> <z> the selection moved by the offset, as the gizmo moves it
|
|
// turnselection <degrees> the selection turned around the model with the gizmo
|
|
// copy, paste <x> <y> <z>, group, ungroup, drop, delete as the keys of the selection do them
|
|
// 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
|
|
// terrainmode the terrain field of work, with its texture browser
|
|
// terrainsculpt <x> <z> <radius> <metres>, terrainpaint <x> <z> <radius> <palette entry>,
|
|
// terrainlevel <x> <z> <radius> <target height> [mode] as a stroke of the brush
|
|
// terrainadd <count> <spacing> chunks made around the camera, count x count, as the button of the chunks panel does it
|
|
// terrainprobe <x> <z> logs the height of the heightmap terrain and the materials of the chunk
|
|
// terrainconvert converts every material of the terrain files of the scenery
|
|
// terrainmodifier <name> <x0> <z0> <x1> <z1> <formation> <half width> a straight modifier, as the vertical profile makes them
|
|
// terraintouchup <0|1> the brushes touch up over the modifiers, or shape the ground under them
|
|
// terrainrestore <x> <z> <radius> a stroke of the restore brush
|
|
// terrainbake <name> the modifier baked into the ground
|
|
// terrainselect <cx> <cz> [add] the chunk selected with the spacing tool (add: Ctrl)
|
|
// terrainresolution <spacing> [paint 0|1] the selected chunks converted, as the button of the resolution panel does it
|
|
// terrainfill <palette entry> <blur> <rotation> <x> <z> <x> <z> <x> <z>... the area outlined filled, as the fill tool does it
|
|
// terrainturn <palette entry> <degrees> the textures of the palette entry turned
|
|
// terrainwater <index> <material> [size] material and repeat of a body of water; terrainwaterdefault <material>
|
|
// orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given
|
|
// screenshot
|
|
// save
|
|
// log <text>
|
|
// quit
|
|
void editor_mode::selftest_step()
|
|
{
|
|
auto &test{m_selftest};
|
|
if (false == test.loaded)
|
|
{
|
|
test.loaded = true;
|
|
auto const *file{std::getenv("EU07_EDITOR_SELFTEST")};
|
|
if (file == nullptr)
|
|
return;
|
|
std::ifstream stream(file);
|
|
std::string line;
|
|
while (std::getline(stream, line))
|
|
if (false == line.empty() && line.front() != '#')
|
|
test.lines.push_back(line);
|
|
WriteLog("SELFTEST: " + std::to_string(test.lines.size()) + " steps from " + file);
|
|
}
|
|
if (test.wait > 0)
|
|
{
|
|
--test.wait;
|
|
return;
|
|
}
|
|
while (test.next < test.lines.size())
|
|
{
|
|
std::istringstream words(test.lines[test.next++]);
|
|
std::string command;
|
|
words >> command;
|
|
if (command == "wait")
|
|
{
|
|
words >> test.wait;
|
|
return;
|
|
}
|
|
if (command == "camera")
|
|
{
|
|
glm::dvec3 from, to;
|
|
words >> from.x >> from.y >> from.z >> to.x >> to.y >> to.z;
|
|
auto const look{glm::normalize(to - from)};
|
|
Camera.Pos = from;
|
|
Camera.Angle = glm::vec3(static_cast<float>(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast<float>(std::atan2(-look.x, -look.z)), 0.0f);
|
|
Global.pCamera = Camera;
|
|
m_focus_active = false;
|
|
}
|
|
else if (command == "select")
|
|
{
|
|
std::string name;
|
|
words >> name;
|
|
m_node = simulation::Paths.find(name);
|
|
m_track_point = {};
|
|
ui()->set_node(m_node);
|
|
WriteLog("SELFTEST select " + name + (m_node != nullptr ? "" : ": no such track"));
|
|
}
|
|
else if (command == "frog")
|
|
{
|
|
std::string name;
|
|
words >> name;
|
|
for (auto *track : simulation::Paths.sequence())
|
|
{
|
|
if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr)
|
|
continue;
|
|
if (false == name.empty() && track->name() != name)
|
|
continue;
|
|
auto const frog{track->frog()};
|
|
if (false == frog.valid)
|
|
{
|
|
WriteLog(format("SELFTEST frog %s: no frog, paths %.3f and %.3f m, blades %.3f m", track->name().c_str(), track->SwitchExtension->Segments[0]->GetLength(), track->SwitchExtension->Segments[1]->GetLength(), frog.blades[0]));
|
|
continue;
|
|
}
|
|
auto const n{1.0 / std::tan(frog.angle)};
|
|
glm::dvec3 point, across;
|
|
track->SwitchExtension->Segments[0]->plan_frame(frog.at[0], point, across);
|
|
auto const gauge{point + across * (frog.side[0] * 0.5 * std::abs(track->fTrackWidth))};
|
|
auto const slope{std::tan(frog.angle)};
|
|
auto const check{0.5 * track_rails::check_length(slope)};
|
|
WriteLog(format("SELFTEST frog %s: crossing at %.3f / %.3f m (%.3f %.3f %.3f), 1:%.2f, side %d, blades %.3f / %.3f m, throat %.3f, nose %.3f, wing end %.3f, check rails %.3f-%.3f / %.3f-%.3f m", track->name().c_str(),
|
|
frog.at[0], frog.at[1], gauge.x, gauge.y, gauge.z, n, frog.side[0], frog.blades[0], frog.blades[1], frog.at[0] - track_rails::flangeway / slope, frog.at[0] + track_rails::nose_tip / slope, frog.at[0] + track_rails::wing_reach(slope),
|
|
frog.at[0] - 0.25 - check, frog.at[0] - 0.25 + check, frog.at[1] - 0.25 - check, frog.at[1] - 0.25 + check));
|
|
}
|
|
}
|
|
else if (command == "geomhash")
|
|
{
|
|
std::string name;
|
|
words >> name;
|
|
auto const mix = [](std::uint64_t &Hash, float const Value) {
|
|
Hash = (Hash ^ static_cast<std::uint64_t>(static_cast<std::int64_t>(std::llround(Value * 1e4)))) * 1099511628211ull;
|
|
};
|
|
auto const exact = [](std::uint64_t &Hash, float const Value) {
|
|
std::uint32_t bits;
|
|
std::memcpy(&bits, &Value, sizeof(bits));
|
|
Hash = (Hash ^ bits) * 1099511628211ull;
|
|
};
|
|
auto const named = [](std::uint64_t &Hash, material_handle const Material) {
|
|
for (auto const letter : (Material != null_handle ? GfxRenderer->Material(Material)->GetName() : std::string{}) + '|')
|
|
Hash = (Hash ^ static_cast<unsigned char>(letter)) * 1099511628211ull;
|
|
};
|
|
std::uint64_t total{1469598103934665603ull};
|
|
std::uint64_t exacttotal{1469598103934665603ull};
|
|
std::size_t vertices{0};
|
|
int tracks{0};
|
|
for (auto *track : simulation::Paths.sequence())
|
|
{
|
|
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1)
|
|
continue;
|
|
if (false == name.empty() && track->name() != name)
|
|
continue;
|
|
std::uint64_t hash{1469598103934665603ull};
|
|
std::uint64_t exacthash{1469598103934665603ull};
|
|
std::size_t count{0};
|
|
int chunks{0};
|
|
std::vector<TTrack::geometryhandle_sequence const *> sequences{&track->Geometry1, &track->Geometry2, &track->GeometryCut1, &track->GeometryCut2};
|
|
for (auto const &tread : track->GeometryTreads)
|
|
{
|
|
sequences.push_back(&tread.geometry);
|
|
sequences.push_back(&tread.cuts);
|
|
named(exacthash, tread.material);
|
|
named(exacthash, tread.matte);
|
|
}
|
|
for (auto const *sequence : sequences)
|
|
{
|
|
for (auto const &geometry : *sequence)
|
|
{
|
|
++chunks;
|
|
for (auto const &vertex : GfxRenderer->Vertices(geometry))
|
|
{
|
|
for (int i = 0; i < 3; ++i)
|
|
{
|
|
mix(hash, vertex.position[i]);
|
|
mix(hash, vertex.normal[i]);
|
|
exact(exacthash, vertex.position[i]);
|
|
exact(exacthash, vertex.normal[i]);
|
|
}
|
|
mix(hash, vertex.texture.x);
|
|
mix(hash, vertex.texture.y);
|
|
exact(exacthash, vertex.texture.x);
|
|
exact(exacthash, vertex.texture.y);
|
|
++count;
|
|
}
|
|
for (auto const &userdata : GfxRenderer->UserData(geometry))
|
|
{
|
|
for (int i = 0; i < 4; ++i)
|
|
exact(exacthash, userdata.data[i]);
|
|
}
|
|
exact(exacthash, -2.f);
|
|
}
|
|
mix(hash, -1.f);
|
|
exact(exacthash, -1.f);
|
|
}
|
|
WriteLog(format("SELFTEST geom %s: chunks %d, vertices %zu, hash %016llx, exact %016llx", track->name().c_str(), chunks, count, static_cast<unsigned long long>(hash), static_cast<unsigned long long>(exacthash)));
|
|
total = (total ^ hash) * 1099511628211ull;
|
|
exacttotal = (exacttotal ^ exacthash) * 1099511628211ull;
|
|
vertices += count;
|
|
++tracks;
|
|
}
|
|
WriteLog(format("SELFTEST geom total: tracks %d, vertices %zu, hash %016llx, exact %016llx", tracks, vertices, static_cast<unsigned long long>(total), static_cast<unsigned long long>(exacttotal)));
|
|
}
|
|
else if (command == "frogkey")
|
|
{
|
|
std::string name, value;
|
|
words >> name >> value;
|
|
auto *track{simulation::Paths.find(name)};
|
|
if (track == nullptr || track->eType != tt_Switch || track->SwitchExtension == nullptr)
|
|
WriteLog(format("SELFTEST frogkey %s: no such switch", name.c_str()));
|
|
else
|
|
{
|
|
if (value == "on" || value == "off")
|
|
{
|
|
auto const before{editor_track::capture(*track)};
|
|
track->SwitchExtension->m_frogs = (value == "on");
|
|
push_track_snapshot({{track, before}});
|
|
editor_track::commit_parameters(*track);
|
|
}
|
|
std::ostringstream text;
|
|
track->export_as_text(text);
|
|
WriteLog(format("SELFTEST frogkey %s: key %d, written %d, frog %d, crossings %zu, profile '%s'", name.c_str(), track->SwitchExtension->m_frogs ? 1 : 0, text.str().find(" frogs ") != std::string::npos ? 1 : 0,
|
|
track->frog().valid ? 1 : 0, track->rail_crossings().size(), track->m_profile1.first.c_str()));
|
|
}
|
|
}
|
|
else if (command == "guards")
|
|
{
|
|
std::string name;
|
|
words >> name;
|
|
int count{0};
|
|
int faults{0};
|
|
for (auto *track : simulation::Paths.sequence())
|
|
{
|
|
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1)
|
|
continue;
|
|
if (false == name.empty() && track->name() != name)
|
|
continue;
|
|
auto const layout{track->plan_rails(true)};
|
|
auto const size{track->rail_dimensions()};
|
|
auto const guardoffset{-(track_rails::flangeway + size.head)};
|
|
double const laterals[2]{size.low + guardoffset, track_rails::flangeway + size.head + size.low - 0.002 + guardoffset};
|
|
for (int path = 0; path < 2; ++path)
|
|
{
|
|
for (int right = 0; right < 2; ++right)
|
|
{
|
|
auto const side{right ? 1 : -1};
|
|
auto const length{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()};
|
|
auto const drawn = [&](glm::dvec2 const &Miter, double const Square) {
|
|
auto const at{Miter.y == 0.0 ? Square : Miter.x + Miter.y * 0.5 * (laterals[0] + laterals[1])};
|
|
return at > 0.0 && at < length;
|
|
};
|
|
for (auto const &guard : layout.guards[path][right])
|
|
{
|
|
if (false == drawn(guard.frommiter, guard.from) && false == drawn(guard.tomiter, guard.to) && (guard.to <= 0.0 || guard.from >= length))
|
|
continue;
|
|
++count;
|
|
std::string text;
|
|
auto const end = [&](glm::dvec2 const &Miter, bool const Flared, bool const Glued) {
|
|
if (Miter.y == 0.0)
|
|
return std::string{Flared ? "flared" : "square"};
|
|
if (false == drawn(Miter, 0.0))
|
|
return std::string{"past the end of the track"};
|
|
glm::dvec3 points[2];
|
|
for (int i = 0; i < 2; ++i)
|
|
points[i] = track_rails::rail_point(*track, path, side, Miter.x + Miter.y * laterals[i], laterals[i]);
|
|
auto best{1e9};
|
|
double lateral{0.0}, along{0.0};
|
|
TTrack *against{nullptr};
|
|
int againstpath{0}, againstside{0};
|
|
for (auto *other : layout.crossed)
|
|
for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath)
|
|
for (auto const otherside : {-1, 1})
|
|
{
|
|
double d[2], l[2];
|
|
for (int i = 0; i < 2; ++i)
|
|
track_rails::rail_offset(*other, otherpath, otherside, points[i], d[i], l[i]);
|
|
if (std::abs(l[0]) > 0.5 || std::abs(l[1]) > 0.5 || std::abs(l[0] - l[1]) >= best)
|
|
continue;
|
|
best = std::abs(l[0] - l[1]);
|
|
lateral = 0.5 * (l[0] + l[1]);
|
|
along = 0.5 * (d[0] + d[1]);
|
|
against = other;
|
|
againstpath = otherpath;
|
|
againstside = otherside;
|
|
}
|
|
if (against == nullptr)
|
|
{
|
|
++faults;
|
|
return std::string{"cut along nothing FAULT"};
|
|
}
|
|
auto result{format("along %s/%d%+d at %.3f m, %.1f mm off its gauge line, skew %.1f mm", against->name().c_str(), againstpath, againstside, along, lateral * 1000.0, best * 1000.0)};
|
|
auto fault{best > 0.002};
|
|
if (Glued)
|
|
{
|
|
auto const theirsize{against->rail_dimensions()};
|
|
auto const theiroffset{-(track_rails::flangeway + theirsize.head)};
|
|
double const theirlaterals[2]{theirsize.low + theiroffset, track_rails::flangeway + theirsize.head + theirsize.low - 0.002 + theiroffset};
|
|
auto const reach = [&](glm::dvec2 const &Miter, double const Square, bool const Latest) {
|
|
if (Miter.y == 0.0)
|
|
return Square;
|
|
auto const a{Miter.x + Miter.y * theirlaterals[0]};
|
|
auto const b{Miter.x + Miter.y * theirlaterals[1]};
|
|
return Latest ? std::max(a, b) : std::min(a, b);
|
|
};
|
|
auto covered{false};
|
|
for (auto const &theirs : against->plan_rails(true).guards[againstpath][againstside > 0])
|
|
covered |= (along > reach(theirs.frommiter, theirs.from, false) - 0.002 && along < reach(theirs.tomiter, theirs.to, true) + 0.002);
|
|
result += covered ? ", on its guard rail" : ", no guard rail there";
|
|
fault |= (false == covered || std::abs(lateral + track_rails::flangeway) > 0.002);
|
|
}
|
|
if (fault)
|
|
{
|
|
++faults;
|
|
result += " FAULT";
|
|
}
|
|
return result;
|
|
};
|
|
text += "from " + end(guard.frommiter, guard.flarefrom, false == guard.flarefrom) + "; to " + end(guard.tomiter, guard.flareto, false == guard.flareto);
|
|
for (auto const &cut : guard.plan.cuts)
|
|
if (cut.end > 0.0 && cut.start < length)
|
|
text += format("; break %.3f-%.3f: ", cut.end, cut.start) + end(cut.endmiter, false, false) + " / " + end(cut.startmiter, false, false);
|
|
WriteLog(format("SELFTEST guard %s path %d side %+d %.3f-%.3f m: %s", track->name().c_str(), path, side, guard.from, guard.to, text.c_str()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
WriteLog(format("SELFTEST guards: %d guard rails, %d faults", count, faults));
|
|
}
|
|
else if (command == "switches")
|
|
{
|
|
int path{0};
|
|
words >> path;
|
|
int count{0};
|
|
for (auto *track : simulation::Paths.sequence())
|
|
{
|
|
if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr)
|
|
continue;
|
|
track->Switch(path);
|
|
track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset;
|
|
track->RaAnimate();
|
|
++count;
|
|
}
|
|
WriteLog(format("SELFTEST switches: %d switches set to path %d", count, path));
|
|
}
|
|
else if (command == "railtime")
|
|
{
|
|
track_rails::forget();
|
|
auto const started{std::chrono::steady_clock::now()};
|
|
int tracks{0};
|
|
for (auto *track : simulation::Paths.sequence())
|
|
{
|
|
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1)
|
|
continue;
|
|
track->rail_crossings(false);
|
|
++tracks;
|
|
}
|
|
auto const crossed{std::chrono::steady_clock::now()};
|
|
for (auto *track : simulation::Paths.sequence())
|
|
{
|
|
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1)
|
|
continue;
|
|
track->plan_rails(true);
|
|
}
|
|
auto const finished{std::chrono::steady_clock::now()};
|
|
auto const crossings{std::chrono::duration<double, std::milli>(crossed - started).count()};
|
|
auto const elapsed{std::chrono::duration<double, std::milli>(finished - started).count()};
|
|
WriteLog(format("SELFTEST railtime: the rails of %d tracks laid out in %.1f ms, of which their crossings %.1f ms", tracks, elapsed, crossings));
|
|
}
|
|
else if (command == "crossings")
|
|
{
|
|
std::string name;
|
|
words >> name;
|
|
auto const started{std::chrono::steady_clock::now()};
|
|
int tracks{0};
|
|
int found{0};
|
|
int acutes{0};
|
|
int middles{0};
|
|
int mismatched{0};
|
|
for (auto *track : simulation::Paths.sequence())
|
|
{
|
|
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1)
|
|
continue;
|
|
if (false == name.empty() && track->name() != name)
|
|
continue;
|
|
++tracks;
|
|
for (auto const &crossing : track->rail_crossings())
|
|
{
|
|
++found;
|
|
auto const middle{crossing.angle > track_rails::middle_angle && crossing.angle <= track_rails::wide_angle};
|
|
middles += middle ? 1 : 0;
|
|
auto const kind{crossing.angle > track_rails::wide_angle ? "wide" : crossing.facing < 0.0 ? (middle ? "middle acute" : "acute") : (middle ? "middle obtuse" : "obtuse")};
|
|
WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind,
|
|
crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0));
|
|
}
|
|
auto const gauge = [](TTrack *Track, track_rails::rail_crossing const &Crossing) {
|
|
auto const &segment{Track->eType == tt_Switch ? *Track->SwitchExtension->Segments[Crossing.path] : *Track->Segment};
|
|
glm::dvec3 point, across;
|
|
segment.plan_frame(Crossing.at, point, across);
|
|
return point + across * (Crossing.side * 0.5 * std::abs(Track->fTrackWidth));
|
|
};
|
|
for (auto const &crossing : track->rail_crossings(false))
|
|
{
|
|
if (crossing.angle > track_rails::wide_angle || crossing.facing >= 0.0)
|
|
continue;
|
|
++acutes;
|
|
auto const here{gauge(track, crossing)};
|
|
auto partners{0};
|
|
auto noses{crossing.nose ? 1 : 0};
|
|
for (auto const &other : crossing.track->rail_crossings(false))
|
|
{
|
|
if (other.track != track || other.angle > track_rails::wide_angle || other.facing >= 0.0)
|
|
continue;
|
|
auto const there{gauge(crossing.track, other)};
|
|
if (glm::length(glm::dvec2{there.x - here.x, there.z - here.z}) > 0.05)
|
|
continue;
|
|
++partners;
|
|
noses += other.nose ? 1 : 0;
|
|
}
|
|
if (partners != 1 || noses != 1)
|
|
++mismatched;
|
|
WriteLog(format("SELFTEST acute %s %s path %d side %d at %.3f m with %s (%.2f %.2f %.2f): partners %d, noses %d", partners != 1 || noses != 1 ? "mismatch" : "ok", track->name().c_str(), crossing.path, crossing.side, crossing.at, crossing.track->name().c_str(), here.x, here.y, here.z, partners, noses));
|
|
}
|
|
}
|
|
auto const elapsed{std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count()};
|
|
WriteLog(format("SELFTEST crossings: %d rail crossings on %d tracks, %d of them middle, %d acute, %d mismatched, %.1f ms", found, tracks, middles, acutes, mismatched, elapsed));
|
|
}
|
|
else if (command == "sweep")
|
|
{
|
|
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 &tool{m_sweep};
|
|
auto const get = [&](char const *Key, std::string const &Default) { auto const found{values.find(Key)}; return found != values.end() ? found->second : Default; };
|
|
tool.edited = nullptr;
|
|
tool.curve_for = nullptr;
|
|
std::snprintf(tool.model, sizeof(tool.model), "%s", get("model", "").c_str());
|
|
tool.parameters.clear();
|
|
std::stringstream parameters(get("params", ""));
|
|
std::string parameter;
|
|
while (std::getline(parameters, parameter, ','))
|
|
{
|
|
tool.parameters.emplace_back();
|
|
std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", parameter.c_str());
|
|
}
|
|
tool.parameters_for = tool.model;
|
|
auto &settings{tool.settings};
|
|
settings.lateral = std::stod(get("lateral", "0"));
|
|
settings.height = std::stod(get("height", "0"));
|
|
auto const side{get("side", "origin")};
|
|
settings.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0;
|
|
auto const top{get("top", "origin")};
|
|
settings.height_anchor = top == "bottom" ? 1 : top == "top" ? 2 : top == "edge" ? 3 : 0;
|
|
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"));
|
|
settings.to = std::stod(get("to", "-1"));
|
|
auto const length{get("length", "all")};
|
|
tool.length_mode = length == "original" ? 0 : length == "all" ? 2 : 1;
|
|
if (tool.length_mode == 1)
|
|
tool.length = std::stod(length);
|
|
if (values.count("edge"))
|
|
EditorSettings.platform_edge(std::stod(values["edge"]));
|
|
tool.measured.clear();
|
|
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();
|
|
WriteLog("SELFTEST sweep: " + tool.status);
|
|
}
|
|
}
|
|
else if (command == "drag")
|
|
{
|
|
glm::dvec3 from{0.0}, to{0.0};
|
|
words >> from.x >> from.z >> to.x >> to.z;
|
|
m_sweep.open = true;
|
|
if (false == sweep_press_at(from, false))
|
|
{
|
|
WriteLog("SELFTEST drag: the press wasn't taken");
|
|
continue;
|
|
}
|
|
sweep_drag_at(to);
|
|
sweep_release();
|
|
WriteLog("SELFTEST drag: " + m_sweep.status);
|
|
}
|
|
else if (command == "probe")
|
|
{
|
|
// edge of the last laid model as it was made: its distance from the axis and the height of its top over the rail head
|
|
std::string name;
|
|
double step{20.0};
|
|
words >> name >> step;
|
|
if (name == "-")
|
|
name.clear();
|
|
auto *sweep{name.empty() ? m_sweep.edited : simulation::Sweeps.find(name)};
|
|
if (sweep == nullptr)
|
|
{
|
|
WriteLog("SELFTEST probe: nothing laid");
|
|
continue;
|
|
}
|
|
auto const &definition{sweep->definition()};
|
|
double const sign{definition.lateral >= 0.0 ? 1.0 : -1.0};
|
|
struct bin
|
|
{
|
|
double edge{std::numeric_limits<double>::max()};
|
|
std::vector<std::pair<double, double>> points; // lateral, height over the axis
|
|
};
|
|
std::map<int, bin> bins;
|
|
for (auto const &shape : sweep->create_shapes())
|
|
for (auto const &vertex : shape.data().vertices)
|
|
{
|
|
double lateral;
|
|
auto const station{sweep->project(vertex.position, lateral)};
|
|
if (lateral * sign <= 0.0)
|
|
continue;
|
|
auto const axis{sweep->frame_at(station)};
|
|
auto &entry{bins[static_cast<int>(std::floor(station / step))]};
|
|
entry.edge = std::min(entry.edge, std::abs(lateral));
|
|
entry.points.emplace_back(std::abs(lateral), vertex.position.y - axis.position.y);
|
|
}
|
|
WriteLog(format("SELFTEST probe %s: %zu stretches with the model, %.1f - %.1f m of %.1f m", sweep->name().c_str(), bins.size(), sweep->start(), sweep->end(), sweep->length()));
|
|
for (auto const &[index, entry] : bins)
|
|
{
|
|
auto const station{(index + 0.5) * step};
|
|
if (station < sweep->start() || station > sweep->end())
|
|
continue;
|
|
double top{-1000.0};
|
|
for (auto const &point : entry.points)
|
|
if (point.first < entry.edge + 0.15)
|
|
top = std::max(top, point.second);
|
|
auto const at{sweep->frame_at(station)};
|
|
auto const head{0.18 + sweep_node::rail_head(at.roll, definition.lateral)};
|
|
WriteLog(format("SELFTEST probe %s at %.0f m: edge %.3f m from the axis, its top %.3f m over the rail head (cant %.1f deg, gauge widening %.3f m, cant part %.3f m)", sweep->name().c_str(), station, entry.edge, top - head, at.roll,
|
|
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 == "curvedswitch")
|
|
{
|
|
std::string name, label;
|
|
glm::dvec3 at{0.0}, toward{0.0};
|
|
words >> name >> at.x >> at.z >> toward.x >> toward.z >> label;
|
|
m_node = simulation::Paths.find(name);
|
|
ui()->set_node(m_node);
|
|
show_track_tab(track_tab::turnout);
|
|
arm_switch();
|
|
auto &tool{m_switch};
|
|
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
|
|
if (false == label.empty() && tool.templates[i].label.find(label) != std::string::npos)
|
|
{
|
|
tool.armed = i;
|
|
break;
|
|
}
|
|
m_cursor_override = at;
|
|
auto const started{start_switch_placement()};
|
|
m_cursor_override.reset();
|
|
if (false == started)
|
|
{
|
|
WriteLog("SELFTEST curvedswitch: not started, " + tool.status);
|
|
continue;
|
|
}
|
|
tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z};
|
|
finish_switch_placement();
|
|
WriteLog("SELFTEST curvedswitch " + (tool.armed >= 0 ? tool.templates[tool.armed].label : std::string{}) + ": " + tool.status);
|
|
}
|
|
else if (command == "conecheck")
|
|
{
|
|
std::string name;
|
|
words >> name;
|
|
auto *track{simulation::Paths.find(name)};
|
|
if (track == nullptr || track->m_paths.size() < 2)
|
|
{
|
|
WriteLog("SELFTEST conecheck: no switch " + name);
|
|
continue;
|
|
}
|
|
geometry::bezier const main{track->m_paths[0]};
|
|
geometry::bezier const other{track->m_paths[1]};
|
|
std::string text;
|
|
for (int i = 1; i <= 8; ++i)
|
|
{
|
|
auto const t{i * 0.125};
|
|
auto const point{other.point(t)};
|
|
double best{std::numeric_limits<double>::max()}, along{0.0};
|
|
for (int k = 0; k <= 2000; ++k)
|
|
if (auto const distance{geometry::plan_distance(main.point(k / 2000.0), point)}; distance < best)
|
|
{
|
|
best = distance;
|
|
along = k / 2000.0;
|
|
}
|
|
auto const base{main.point(along)};
|
|
auto const direction{glm::normalize(geometry::plan_of(main.first(along)))};
|
|
auto const lateral{(point.x - base.x) * -direction.y + (point.z - base.z) * direction.x};
|
|
auto const &path{track->m_paths[0]};
|
|
auto const roll{glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * along)};
|
|
auto const cone{base.y - lateral * std::tan(roll)};
|
|
text += format(" [t %.3f off %.3f dy %.4f]", t, lateral, point.y - cone);
|
|
}
|
|
WriteLog("SELFTEST conecheck " + name + ":" + text);
|
|
}
|
|
else if (command == "frames")
|
|
{
|
|
std::string name;
|
|
double from{0.0}, to{0.0}, step{0.1};
|
|
words >> name >> from >> to >> step;
|
|
auto *sweep{simulation::Sweeps.find(name)};
|
|
if (sweep == nullptr)
|
|
continue;
|
|
auto const side{sweep->definition().lateral >= 0.0 ? 1 : 0};
|
|
if (step <= 0.0)
|
|
continue;
|
|
for (int i = 0; from + i * step <= to + 1e-9; ++i)
|
|
{
|
|
auto const station{from + i * step};
|
|
auto const at{sweep->frame_at(station)};
|
|
WriteLog(format("SELFTEST frame %s %.2f: %.3f %.3f %.3f widening %.4f cant %.4f setback %.4f", name.c_str(), station, at.position.x, at.position.y, at.position.z, at.widening[side], at.cant[side], sweep->setback(at)));
|
|
}
|
|
}
|
|
else if (command == "switchdrive")
|
|
{
|
|
int index{-1}, side{0};
|
|
words >> index >> side;
|
|
scan_switch_drives();
|
|
m_switch.drive = index;
|
|
m_switch.drive_side = side;
|
|
WriteLog("SELFTEST switchdrive: " + (index >= 0 && index < static_cast<int>(m_switch.drives.size()) ? m_switch.drives[index].name : std::string{"none"}));
|
|
}
|
|
else if (command == "drivetilt")
|
|
{
|
|
std::string name;
|
|
words >> name;
|
|
auto *model{simulation::Instances.find(name)};
|
|
auto *track{simulation::Paths.find(name.substr(0, name.find('_')))};
|
|
if (model == nullptr || track == nullptr)
|
|
{
|
|
WriteLog("SELFTEST drivetilt: no " + name);
|
|
continue;
|
|
}
|
|
auto const &path{track->m_paths[0]};
|
|
auto const transform{glm::dmat3(model->rotation_scale())};
|
|
auto const across{transform * glm::dvec3{1.0, 0.0, 0.0}};
|
|
auto const forward{transform * glm::dvec3{0.0, 0.0, 1.0}};
|
|
auto const direction{glm::normalize(glm::dvec2{forward.x, forward.z})};
|
|
auto const lateral{across.x * -direction.y + across.z * direction.x};
|
|
auto const cone{-lateral * std::tan(glm::radians(static_cast<double>(path.rolls[0])))};
|
|
auto const control{path.points[segment_data::point::control1]};
|
|
auto const grade{control.y / std::hypot(control.x, control.z)};
|
|
WriteLog(format("SELFTEST drivetilt %s: angles %.3f %.3f %.3f, +x rises %.4f (cone %.4f), +z rises %.4f (grade %.4f), origin %.3f %.3f %.3f, points %.3f", name.c_str(), model->Angles().x, model->Angles().y, model->Angles().z, across.y, cone, forward.y, grade,
|
|
model->location().x, model->location().y, model->location().z, path.points[segment_data::point::start].y));
|
|
}
|
|
else if (command == "weather")
|
|
{
|
|
auto const &time{simulation::Time.data()};
|
|
WriteLog(format("SELFTEST weather: %02d:%02d, fog end %.0f m, overcast %.2f", time.wHour, time.wMinute, Global.fFogEnd, Global.Overcast));
|
|
}
|
|
else if (command == "fouling")
|
|
{
|
|
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 &tool{m_fouling};
|
|
tool.scope = std::stoi(values.count("scope") ? values["scope"] : "2");
|
|
tool.height_mode = values["mode"] == "ground" ? 0 : 1;
|
|
tool.height = std::stod(values.count("height") ? values["height"] : "0");
|
|
tool.dirty = true;
|
|
fouling_update();
|
|
WriteLog("SELFTEST fouling: " + tool.status);
|
|
int shown{0};
|
|
for (auto const &found : tool.found)
|
|
if (found.wrong && shown++ < 5)
|
|
WriteLog(format("SELFTEST fouling: %s %.2f m off, %+.3f m high", found.marker.file.c_str(), found.error, found.height_error));
|
|
if (values["place"] == "1")
|
|
{
|
|
fouling_place();
|
|
tool.dirty = true;
|
|
fouling_update();
|
|
WriteLog("SELFTEST fouling after placing: " + tool.status);
|
|
}
|
|
if (values["fix"] == "1")
|
|
{
|
|
fouling_fix();
|
|
tool.dirty = true;
|
|
fouling_update();
|
|
WriteLog("SELFTEST fouling after the fix: " + tool.status);
|
|
}
|
|
}
|
|
else if (command == "hekto")
|
|
{
|
|
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 *track{simulation::Paths.find(values["track"])};
|
|
if (track == nullptr)
|
|
{
|
|
WriteLog("SELFTEST hekto: no such track");
|
|
continue;
|
|
}
|
|
hekto_start(*track, track->m_paths.front().points[segment_data::point::start]);
|
|
auto &tool{m_hekto};
|
|
tool.dirty = true;
|
|
hekto_update();
|
|
if (values.count("km"))
|
|
{
|
|
tool.km = std::stod(values["km"]);
|
|
tool.growth = values["growth"] == "-1" ? -1 : 1;
|
|
}
|
|
tool.side = values["side"] == "alt" ? 0 : values["side"] == "left" ? -1 : 1;
|
|
tool.dirty = true;
|
|
hekto_update();
|
|
auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })};
|
|
WriteLog(format("SELFTEST hekto: km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing. %s", tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast<int>(missing), tool.found.size(), tool.status.c_str()));
|
|
for (std::size_t i = 0; i < tool.posts.size() && i < 4; ++i)
|
|
{
|
|
auto const &post{tool.posts[i]};
|
|
double lateral;
|
|
glm::dvec3 const axis{editor_track::sampled_position(tool.samples, post.chainage)};
|
|
auto const direction{editor_track::sampled_position(tool.samples, post.chainage + 1.0) - axis};
|
|
glm::dvec2 const offset{post.position.x - axis.x, post.position.z - axis.z};
|
|
lateral = glm::length(offset);
|
|
auto const right{(direction.x * offset.y - direction.z * offset.x) * tool.growth >= 0.0};
|
|
WriteLog(format("SELFTEST hekto post %d.%d at %.1f m, %.2f m from the axis, %s of the growing kilometrage, at %.1f %.1f %.1f", post.hectometres / 10, post.hectometres % 10, post.chainage, lateral, right ? "right" : "left", post.position.x, post.position.y,
|
|
post.position.z));
|
|
}
|
|
if (values["place"] == "1")
|
|
{
|
|
hekto_place();
|
|
WriteLog("SELFTEST hekto: " + tool.status);
|
|
}
|
|
}
|
|
else if (command == "tracks")
|
|
{
|
|
int tab{0};
|
|
words >> tab;
|
|
show_track_tab(track_tab::lineside);
|
|
m_sweep.expand = tab == 0;
|
|
m_hekto.expand = tab == 1;
|
|
m_fouling.expand = tab == 2;
|
|
m_parallel.expand = tab == 3;
|
|
m_vehicle.expand = tab == 4;
|
|
}
|
|
else if (command == "curve")
|
|
{
|
|
sweep_curve();
|
|
for (auto const &piece : m_sweep.curve)
|
|
{
|
|
auto const &a{piece.points[segment_data::point::start]};
|
|
auto const &b{piece.points[segment_data::point::end]};
|
|
WriteLog(format("SELFTEST curve piece %.1f %.1f -> %.1f %.1f", a.x, a.z, b.x, b.z));
|
|
}
|
|
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;
|
|
for (std::size_t i = 0; i < m_sweep.outline.size() && i < 45; i += 4)
|
|
{
|
|
ImVec2 screen;
|
|
auto const shown{projection.project(m_sweep.outline[i], screen)};
|
|
auto const clip{projection.clip(m_sweep.outline[i])};
|
|
WriteLog(format("SELFTEST proj %zu: %.1f %.1f %.1f -> %d %.0f %.0f w %.2f", i, m_sweep.outline[i].x, m_sweep.outline[i].y, m_sweep.outline[i].z, shown ? 1 : 0, screen.x, screen.y, clip.w));
|
|
}
|
|
}
|
|
else if (command == "spread")
|
|
{
|
|
std::string name;
|
|
words >> name >> m_spread.stop >> m_spread.reach;
|
|
if (auto *track{simulation::Paths.find(name)})
|
|
{
|
|
std::string names;
|
|
for (auto const *other : track_spread_from(*track))
|
|
names += ' ' + other->name();
|
|
WriteLog("SELFTEST spread " + name + ":" + names + " | prev " + (track->trPrev ? track->trPrev->name() : std::string{"-"}) + " next " + (track->trNext ? track->trNext->name() : std::string{"-"}));
|
|
}
|
|
}
|
|
else if (command == "newscenery")
|
|
{
|
|
WriteLog(restart_for_new_scenery() ? "SELFTEST newscenery: started" : "SELFTEST newscenery: failed");
|
|
return;
|
|
}
|
|
else if (command == "vehicles")
|
|
{
|
|
for (auto *vehicle : simulation::Vehicles.sequence())
|
|
{
|
|
if (vehicle == nullptr)
|
|
continue;
|
|
auto const head{vehicle->HeadPosition()};
|
|
auto const front{vehicle->VectorFront()};
|
|
auto const coupler = [&](int const End) {
|
|
auto const &coupler{vehicle->MoverParameters->Couplers[End]};
|
|
return coupler.Connected != nullptr ? coupler.Connected->Name + "/" + std::to_string(coupler.CouplingFlag) : std::string{"-"};
|
|
};
|
|
WriteLog(format("SELFTEST vehicle %s head %.2f %.2f front %.2f %.2f dir %d coupler0 %s coupler1 %s", vehicle->name().c_str(), head.x, head.z, front.x, front.z, vehicle->iDirection, coupler(0).c_str(), coupler(1).c_str()));
|
|
}
|
|
}
|
|
else if (command == "screenshot")
|
|
{
|
|
Application.queue_screenshot();
|
|
test.wait = 3;
|
|
return;
|
|
}
|
|
else if (command == "save")
|
|
{
|
|
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 == "addmodel")
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
select_node(nearest, true);
|
|
WriteLog("SELFTEST addmodel: " + (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 == "selection")
|
|
{
|
|
std::string text;
|
|
for (auto *node : selection_with_groups())
|
|
text += format(" [%s %.2f %.2f %.2f group %d]", node->name().c_str(), node->location().x, node->location().y, node->location().z, static_cast<int>(node->group()));
|
|
WriteLog("SELFTEST selection:" + text);
|
|
}
|
|
else if (command == "moveselection" && m_node != nullptr)
|
|
{
|
|
glm::dvec3 offset{0.0};
|
|
words >> offset.x >> offset.y >> offset.z;
|
|
selection_snapshots(EditorSnapshot::Action::Move);
|
|
auto const before{m_node->location()};
|
|
m_editor.translate(m_node, before + offset, true);
|
|
selection_follow(m_node->location() - before, glm::vec3(0.0f), m_node->location());
|
|
}
|
|
else if (command == "turnselection" && m_node != nullptr)
|
|
{
|
|
float degrees{0.0f};
|
|
words >> degrees;
|
|
selection_snapshots(EditorSnapshot::Action::Rotate);
|
|
glm::vec3 const turned{0.0f, degrees, 0.0f};
|
|
m_editor.rotate(m_node, turned, 0.0f);
|
|
selection_follow(glm::dvec3(0.0), turned, m_node->location());
|
|
}
|
|
else if (command == "copy")
|
|
copy_selection();
|
|
else if (command == "paste")
|
|
{
|
|
glm::dvec3 point{0.0};
|
|
words >> point.x >> point.y >> point.z;
|
|
paste_clipboard(point);
|
|
}
|
|
else if (command == "group")
|
|
group_selection();
|
|
else if (command == "ungroup")
|
|
ungroup_selection();
|
|
else if (command == "drop")
|
|
drop_selection_to_ground();
|
|
else if (command == "delete")
|
|
delete_selected();
|
|
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;
|
|
std::getline(words, text);
|
|
WriteLog("SELFTEST" + text);
|
|
}
|
|
else if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe")
|
|
{
|
|
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);
|
|
}
|
|
show_track_tab(track_tab::signals);
|
|
signal_refresh(true);
|
|
auto &tool{m_signal};
|
|
auto const standing_named = [&](std::string const &Name) -> signal_standing const * {
|
|
auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.name == Name; })};
|
|
return found != tool.standing.end() ? &*found : nullptr;
|
|
};
|
|
auto const spot_on = [&](signal_template const &Template, int const Facing, int const Side, signal_spot &Spot) {
|
|
auto *track{simulation::Paths.find(values["track"])};
|
|
if (track == nullptr)
|
|
return false;
|
|
auto const path{values.count("path") ? std::stoi(values["path"]) : 0};
|
|
auto const at{values.count("at") ? std::stod(values["at"]) : 0.5};
|
|
geometry::bezier const curve{track->m_paths[std::clamp(path, 0, static_cast<int>(track->m_paths.size()) - 1)]};
|
|
return signal_spot_at(*track, path, curve.point(at), curve.point(at), Facing, Side, Template, Spot);
|
|
};
|
|
if (command == "signal")
|
|
{
|
|
auto const *item{signal_template_of(values["file"])};
|
|
if (item == nullptr)
|
|
{
|
|
WriteLog("SELFTEST signal: no signal template " + values["file"]);
|
|
continue;
|
|
}
|
|
for (int k = 0; k < static_cast<int>(tool.templates.size()); ++k)
|
|
if (&tool.templates[k] == item)
|
|
{
|
|
static char const *const kinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"};
|
|
for (int i = 0; i < 7; ++i)
|
|
if (item->kind == kinds[i])
|
|
{
|
|
tool.kind = i;
|
|
tool.chosen[i] = k;
|
|
}
|
|
}
|
|
if (values.count("name"))
|
|
std::snprintf(tool.name, sizeof(tool.name), "%s", values["name"].c_str());
|
|
if (values.count("plate"))
|
|
std::snprintf(tool.plate, sizeof(tool.plate), "%s", values["plate"].c_str());
|
|
auto const side{values["side"] == "left" ? -1 : 1};
|
|
auto const facing{values["facing"] == "along" ? 1 : values["facing"] == "against" ? -1 : 0};
|
|
signal_spot spot;
|
|
if (false == spot_on(*signal_armed(), facing, side, spot))
|
|
{
|
|
WriteLog("SELFTEST signal: no such track " + values["track"]);
|
|
continue;
|
|
}
|
|
auto const placed{signal_place(spot, *signal_armed())};
|
|
WriteLog(format("SELFTEST signal %s: %s", placed ? "placed" : "failed", tool.status.c_str()));
|
|
}
|
|
else if (command == "signalmove")
|
|
{
|
|
auto const *standing{standing_named(values["name"])};
|
|
auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr};
|
|
if (item == nullptr)
|
|
{
|
|
WriteLog("SELFTEST signalmove: no signal " + values["name"]);
|
|
continue;
|
|
}
|
|
auto const yaw{glm::radians(standing->include.yaw + 180.0)};
|
|
glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)};
|
|
signal_spot probe;
|
|
if (false == spot_on(*item, 1, 1, probe))
|
|
{
|
|
WriteLog("SELFTEST signalmove: no such track " + values["track"]);
|
|
continue;
|
|
}
|
|
auto const facing{glm::dot(probe.travel, travel) >= 0.0 ? 1 : -1};
|
|
auto const sideoftravel{glm::dot(geometry::plan_of(standing->include.location) - geometry::plan_of(standing->spot.axis), glm::dvec2{-travel.y, travel.x}) >= 0.0 ? 1 : -1};
|
|
signal_select(standing->include.instance);
|
|
tool.moving = true;
|
|
tool.moved_valid = spot_on(*item, facing, sideoftravel * facing, tool.moved);
|
|
tool.pressed = {-1000.f, -1000.f};
|
|
signal_release();
|
|
WriteLog("SELFTEST signalmove: " + tool.status);
|
|
}
|
|
else if (command == "signaldelete")
|
|
{
|
|
auto const *standing{standing_named(values["name"])};
|
|
auto const deleted{standing != nullptr && signal_delete(standing->include.instance)};
|
|
WriteLog(std::string{"SELFTEST signaldelete "} + values["name"] + (deleted ? ": removed with its event" : ": not removed by the signal tool"));
|
|
}
|
|
else if (command == "signalrename")
|
|
{
|
|
auto const *standing{standing_named(values["name"])};
|
|
if (standing == nullptr)
|
|
{
|
|
WriteLog("SELFTEST signalrename: no signal " + values["name"]);
|
|
continue;
|
|
}
|
|
select_include(standing->include.instance);
|
|
m_include.values[0] = values["to"];
|
|
apply_include();
|
|
m_include_gesture = false;
|
|
WriteLog("SELFTEST signalrename " + values["name"] + " -> " + values["to"]);
|
|
}
|
|
else
|
|
{
|
|
signal_refresh(true);
|
|
WriteLog(format("SELFTEST signalprobe: %zu signals", tool.standing.size()));
|
|
for (auto const &standing : tool.standing)
|
|
{
|
|
std::string where{"not read"};
|
|
if (standing.track != nullptr)
|
|
{
|
|
auto const &curve{standing.track->m_paths.front()};
|
|
geometry::bezier const bez{curve};
|
|
auto best{std::numeric_limits<double>::max()};
|
|
double parameter{0.0};
|
|
for (int k = 0; k <= 1024; ++k)
|
|
{
|
|
auto const d{geometry::plan_distance(bez.point(k / 1024.0), standing.include.location)};
|
|
if (d < best)
|
|
{
|
|
best = d;
|
|
parameter = k / 1024.0;
|
|
}
|
|
}
|
|
auto const axis{bez.point(parameter)};
|
|
auto tangent{bez.first(parameter)};
|
|
glm::dvec2 travel{tangent.x, tangent.z};
|
|
travel = glm::normalize(travel) * (standing.list == 1 ? -1.0 : 1.0);
|
|
glm::dvec2 const offset{standing.include.location.x - axis.x, standing.include.location.z - axis.z};
|
|
auto const cross{travel.x * offset.y - travel.y * offset.x};
|
|
auto heading{glm::degrees(std::atan2(travel.x, travel.y))};
|
|
auto turn{std::fmod(standing.include.yaw - heading + 720.0, 360.0)};
|
|
auto const &list{editor_track::events(*standing.track, standing.list)};
|
|
auto const bound{std::any_of(list.begin(), list.end(), [&](auto const &Event) { return Event.second != nullptr && ToLower(Event.first) == ToLower(standing.read); })};
|
|
where = std::string{bound ? "bound, " : "unbound, "} + format("%s of %s, %.3f m from the axis, %s of the trains, %.3f m over the axis, model turned %.1f deg from the travel", editor_track::event_keyword(standing.list), standing.track->name().c_str(), glm::length(offset), cross > 0.0 ? "right" : "left",
|
|
standing.include.location.y - axis.y, turn);
|
|
}
|
|
WriteLog(format("SELFTEST signalprobe %s %s at %.3f %.3f %.3f yaw %.2f read %s: %s", standing.name.c_str(), standing.include.file.c_str(), standing.include.location.x, standing.include.location.y, standing.include.location.z, standing.include.yaw,
|
|
standing.read.empty() ? "-" : standing.read.c_str(), where.c_str()));
|
|
}
|
|
}
|
|
}
|
|
else if (command == "orthophoto")
|
|
{
|
|
auto config{m_orthophoto.settings()};
|
|
int fit{1}, inscene{0};
|
|
words >> config.north >> config.east >> config.radius >> fit >> inscene;
|
|
config.drape = fit != 0;
|
|
config.in_scene = inscene != 0;
|
|
m_orthophoto.settings(config);
|
|
m_orthophoto.enabled(true);
|
|
WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : ""));
|
|
}
|
|
else if (command == "terrainmode")
|
|
{
|
|
m_terrain_open = true;
|
|
WriteLog("SELFTEST terrainmode");
|
|
}
|
|
else if (command == "terrainsculpt" || command == "terrainpaint" || command == "terrainlevel")
|
|
{
|
|
double x{0.0}, z{0.0}, radius{10.0}, value{1.0};
|
|
int mode{0};
|
|
words >> x >> z >> radius >> value >> mode;
|
|
auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value)
|
|
: command == "terrainlevel" ? m_streamer.level(x, z, radius, value, 1.0, mode)
|
|
: m_streamer.paint(x, z, radius, 1.0, static_cast<std::uint16_t>(value))};
|
|
forget_ground();
|
|
WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed"));
|
|
}
|
|
else if (command == "terrainmodifier")
|
|
{
|
|
heightmap::modifier modifier;
|
|
heightmap::corridor_point a, b;
|
|
double formation{0.0}, halfwidth{3.5};
|
|
words >> modifier.name >> a.position.x >> a.position.y >> b.position.x >> b.position.y >> formation >> halfwidth;
|
|
a.formation = b.formation = formation;
|
|
a.half_width = b.half_width = halfwidth;
|
|
modifier.points = {a, b};
|
|
m_streamer.modifier(std::move(modifier));
|
|
forget_ground();
|
|
WriteLog(format("SELFTEST terrainmodifier: %zu modifiers", m_streamer.modifiers().size()));
|
|
}
|
|
else if (command == "terrainfill")
|
|
{
|
|
int layer{0};
|
|
words >> layer >> m_paint_feather >> m_paint_rotation;
|
|
m_terrain_layer = layer;
|
|
m_paint_outline.clear();
|
|
double x, z;
|
|
while (words >> x >> z)
|
|
m_paint_outline.emplace_back(x, 0.0, z);
|
|
fill_terrain_area();
|
|
std::string palette;
|
|
for (auto const &entry : m_streamer.manifest().layers)
|
|
palette += format(" [%s %.1f m %.0f deg]", entry.material.c_str(), entry.size, entry.rotation);
|
|
WriteLog("SELFTEST terrainfill: " + m_terrain_status + ", palette" + palette);
|
|
}
|
|
else if (command == "terrainturn")
|
|
{
|
|
int layer{0};
|
|
float degrees{0.f};
|
|
words >> layer >> degrees;
|
|
m_streamer.layer_rotation(static_cast<std::uint16_t>(layer), degrees);
|
|
WriteLog(format("SELFTEST terrainturn %d: %.1f deg", layer, m_streamer.manifest().layers.at(layer).rotation));
|
|
}
|
|
else if (command == "terraintouchup")
|
|
{
|
|
int state{1};
|
|
words >> state;
|
|
m_streamer.touch_up(state != 0);
|
|
WriteLog(format("SELFTEST terraintouchup %d", state));
|
|
}
|
|
else if (command == "terrainrestore")
|
|
{
|
|
double x{0.0}, z{0.0}, radius{10.0};
|
|
words >> x >> z >> radius;
|
|
auto const changed{m_streamer.restore_stitching(x, z, radius, 1.0)};
|
|
forget_ground();
|
|
WriteLog(format("SELFTEST terrainrestore %.1f %.1f: %s, touch-ups left %zu", x, z, changed ? "changed" : "nothing changed", m_streamer.touched_up()));
|
|
}
|
|
else if (command == "terrainbake")
|
|
{
|
|
std::string name;
|
|
words >> name;
|
|
auto const baked{m_streamer.bake_modifier(name)};
|
|
forget_ground();
|
|
WriteLog(format("SELFTEST terrainbake %s: %s, %zu modifiers left", name.c_str(), baked ? "baked" : "not found", m_streamer.modifiers().size()));
|
|
}
|
|
else if (command == "terrainselect")
|
|
{
|
|
int cx{0}, cz{0};
|
|
std::string add;
|
|
words >> cx >> cz >> add;
|
|
if (add != "add")
|
|
m_terrain_selection.clear();
|
|
if (m_streamer.exists({cx, cz}))
|
|
m_terrain_selection.insert({cx, cz});
|
|
WriteLog(format("SELFTEST terrainselect: %zu selected", m_terrain_selection.size()));
|
|
}
|
|
else if (command == "terrainresolution")
|
|
{
|
|
float spacing{1.f};
|
|
int paint{1};
|
|
words >> spacing >> paint;
|
|
m_terrain_convert_spacing = heightmap::valid_spacing(spacing);
|
|
m_terrain_convert_paint = paint != 0;
|
|
convert_selected_chunks();
|
|
WriteLog("SELFTEST terrainresolution: " + m_terrain_status);
|
|
}
|
|
else if (command == "terrainwater" || command == "terrainwaterdefault")
|
|
{
|
|
if (command == "terrainwaterdefault")
|
|
{
|
|
std::string material;
|
|
words >> material;
|
|
m_streamer.water_material(material);
|
|
}
|
|
else
|
|
{
|
|
std::size_t index{0};
|
|
std::string material;
|
|
float size{0.f};
|
|
words >> index >> material >> size;
|
|
auto water{m_streamer.water()};
|
|
if (index < water.size())
|
|
{
|
|
water[index].material = material == "-" ? std::string{} : material;
|
|
water[index].size = size;
|
|
m_streamer.water(std::move(water));
|
|
}
|
|
}
|
|
std::string list;
|
|
for (auto const &body : m_streamer.water())
|
|
list += format(" [%s: %s, %.1f m]", body.name.c_str(), body.material.empty() ? "default" : body.material.c_str(), body.size);
|
|
WriteLog("SELFTEST " + command + ": default " + m_streamer.manifest().water_material + list);
|
|
}
|
|
else if (command == "terrainadd")
|
|
{
|
|
int count{1};
|
|
float spacing{m_terrain_spacing};
|
|
words >> count >> spacing;
|
|
m_terrain_spacing = heightmap::valid_spacing(spacing);
|
|
add_chunks_around(std::clamp(count, 1, 8));
|
|
WriteLog("SELFTEST terrainadd: " + m_terrain_status);
|
|
}
|
|
else if (command == "terrainprobe")
|
|
{
|
|
double x{0.0}, z{0.0}, height{0.0};
|
|
words >> x >> z;
|
|
auto const *chunk{m_streamer.terrain_at(x, z)};
|
|
std::string materials;
|
|
if (chunk != nullptr)
|
|
for (std::size_t slot = 0; slot < std::max<std::size_t>(1, chunk->data().layer_count()); ++slot)
|
|
{
|
|
auto const layer{chunk->data().layers[slot]};
|
|
auto const &palette{m_streamer.manifest().layers};
|
|
materials += (layer < palette.size() ? palette[layer].material : std::string{"?"}) + " ";
|
|
}
|
|
auto const found{m_streamer.height_at(x, z, height)};
|
|
std::string grid;
|
|
if (chunk != nullptr)
|
|
{
|
|
auto const &data{chunk->data()};
|
|
double base{0.0}, adjust{0.0};
|
|
if (false == data.base.empty())
|
|
base = data.height_in(data.base, x, z);
|
|
if (false == data.adjust.empty())
|
|
adjust = data.height_in(data.adjust, x, z);
|
|
grid = format(", spacing %.1f, paint %d, base %s %.3f, touch-up %.3f", data.spacing(), data.paint, data.base.empty() ? "none" : "", base, adjust);
|
|
if (data.paint > 0)
|
|
{
|
|
// weights at the paint sample nearest the point
|
|
auto const corner{data.corner()};
|
|
auto const ix{static_cast<int>(std::lround((x - corner.x) / heightmap::chunk_size * data.paint))};
|
|
auto const iz{static_cast<int>(std::lround((z - corner.y) / heightmap::chunk_size * data.paint))};
|
|
grid += ", weights";
|
|
for (std::size_t slot = 0; slot < data.layer_count(); ++slot)
|
|
grid += format(" %d", data.weight(slot, std::clamp(ix, 0, data.paint), std::clamp(iz, 0, data.paint)));
|
|
}
|
|
}
|
|
WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s%s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str(), grid.c_str()));
|
|
}
|
|
else if (command == "terrainconvert")
|
|
{
|
|
load_ground_files();
|
|
for (auto const &source : m_ground_conversion.files)
|
|
{
|
|
glm::dvec3 low{1e30}, high{-1e30};
|
|
for (auto index = source.first; index < source.first + source.count; ++index)
|
|
for (auto const &vertex : m_ground_conversion.triangles[index])
|
|
{
|
|
low = glm::min(low, vertex);
|
|
high = glm::max(high, vertex);
|
|
}
|
|
WriteLog("SELFTEST terrain file " + source.file + (source.text ? " (text): " : " (binary): ") + source.message +
|
|
format(", from %.1f %.1f %.1f to %.1f %.1f %.1f", low.x, low.y, low.z, high.x, high.y, high.z));
|
|
}
|
|
m_ground_conversion.status = convert_ground();
|
|
WriteLog("SELFTEST terrainconvert: " + m_ground_conversion.status);
|
|
}
|
|
else if (command == "quit")
|
|
{
|
|
if (false == test.quitting)
|
|
{
|
|
WriteLog("SELFTEST done");
|
|
test.quitting = true;
|
|
test.wait = 30;
|
|
--test.next;
|
|
return;
|
|
}
|
|
Application.queue_quit(true);
|
|
return;
|
|
}
|
|
}
|
|
}
|