mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 02:49:19 +02:00
feat(editor): edit tracks loaded from the scenery
This commit is contained in:
@@ -170,9 +170,11 @@ set(SOURCES
|
||||
"editor/editorModelSets.cpp"
|
||||
"editor/editorIncludeInfo.cpp"
|
||||
"editor/editorTerrain.cpp"
|
||||
"editor/editorTrack.cpp"
|
||||
"editor/editorTerrainStreamer.cpp"
|
||||
"editor/editorOrthophoto.cpp"
|
||||
"application/editormode.cpp"
|
||||
"application/editormodetrack.cpp"
|
||||
"input/editormouseinput.cpp"
|
||||
"application/editoruilayer.cpp"
|
||||
"application/editoruipanels.cpp"
|
||||
|
||||
@@ -339,6 +339,7 @@ editor_mode::editor_mode() {
|
||||
ui()->set_fill_options([this]() { render_area_fill(); });
|
||||
ui()->set_gizmo_options([this]() { render_gizmo_options(); });
|
||||
ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); });
|
||||
ui()->set_track_options([this]() { render_track_ui(); });
|
||||
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
|
||||
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out) {
|
||||
gather_ground_triangles(Min, Max, true, 50.0f, Out);
|
||||
@@ -680,6 +681,12 @@ void editor_mode::undo_last()
|
||||
return;
|
||||
}
|
||||
|
||||
if (snap.action == EditorSnapshot::Action::TrackEdit)
|
||||
{
|
||||
restore_track_snapshot(snap, g_redo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (snap.action == EditorSnapshot::Action::Delete)
|
||||
{
|
||||
// undo delete -> recreate model
|
||||
@@ -769,6 +776,12 @@ void editor_mode::redo_last()
|
||||
return;
|
||||
}
|
||||
|
||||
if (snap.action == EditorSnapshot::Action::TrackEdit)
|
||||
{
|
||||
restore_track_snapshot(snap, m_history);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle delete redo (re-delete) separately
|
||||
if (snap.action == EditorSnapshot::Action::Delete)
|
||||
{
|
||||
@@ -996,6 +1009,10 @@ bool editor_mode::update()
|
||||
// --- ImGuizmo: in-viewport transform gizmo for the selected node ---
|
||||
render_gizmo();
|
||||
|
||||
// --- path editing: course and point handles of the selected path ---
|
||||
if (selected_track())
|
||||
draw_track_overlay();
|
||||
|
||||
// --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) ---
|
||||
if (ui()->mode() == nodebank_panel::FILL)
|
||||
draw_area_fill_outline();
|
||||
@@ -2232,6 +2249,13 @@ void editor_mode::render_gizmo()
|
||||
return;
|
||||
}
|
||||
|
||||
// paths aren't transformed as a whole node, they're edited through their source data
|
||||
if (selected_track())
|
||||
{
|
||||
render_track_gizmo();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGuizmo::BeginFrame();
|
||||
ImGuizmo::SetOrthographic(false);
|
||||
|
||||
@@ -2504,6 +2528,13 @@ void editor_mode::exit()
|
||||
FreeFlyModeFlag = m_statebackup.freefly;
|
||||
Global.pCamera = m_statebackup.camera;
|
||||
|
||||
commit_track_drag(true);
|
||||
m_track_gizmo_using = false;
|
||||
m_track_drag.clear();
|
||||
m_track_drag_points.clear();
|
||||
m_track_snap = {};
|
||||
m_track_point = {};
|
||||
|
||||
g_redo.clear();
|
||||
m_history.clear();
|
||||
m_fill_last.clear();
|
||||
@@ -2647,6 +2678,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
}
|
||||
break;
|
||||
|
||||
case GLFW_KEY_ESCAPE:
|
||||
// track mode: release the selected point handle, so the gizmo moves the whole path again
|
||||
if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK)
|
||||
m_track_point = {};
|
||||
break;
|
||||
|
||||
case GLFW_KEY_BACKSPACE:
|
||||
// area fill: remove the last outline point
|
||||
if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty())
|
||||
@@ -2735,6 +2772,20 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
return;
|
||||
}
|
||||
|
||||
// in track mode the left button selects a point handle of the selected path, or another path
|
||||
if (mode == nodebank_panel::TRACK)
|
||||
{
|
||||
if (false == ImGuizmo::IsOver() && false == pick_track_handle())
|
||||
{
|
||||
GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) {
|
||||
if (viewport_click())
|
||||
select_track(node);
|
||||
});
|
||||
}
|
||||
m_input.mouse.button(Button, Action);
|
||||
return;
|
||||
}
|
||||
|
||||
// delegate node picking behaviour depending on current panel mode.
|
||||
// NOTE: the pick is resolved a few frames later, often after the button was already released,
|
||||
// so the callback mustn't depend on the button still being held
|
||||
@@ -2879,7 +2930,8 @@ void editor_mode::render_change_history(){
|
||||
s.action == EditorSnapshot::Action::Delete ? "DEL" :
|
||||
s.action == EditorSnapshot::Action::Move ? "MOV" :
|
||||
s.action == EditorSnapshot::Action::Rotate ? "ROT" :
|
||||
s.action == EditorSnapshot::Action::Scale ? "SCA" : "OTH",
|
||||
s.action == EditorSnapshot::Action::Scale ? "SCA" :
|
||||
s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : "OTH",
|
||||
s.node_name.empty() ? "(noname)" : s.node_name.c_str(),
|
||||
s.position.x, s.position.y, s.position.z);
|
||||
|
||||
|
||||
@@ -19,12 +19,16 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "editor/editorTerrain.hpp"
|
||||
#include "editor/editorTerrainStreamer.hpp"
|
||||
#include "editor/editorOrthophoto.hpp"
|
||||
#include "editor/editorTrack.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class TAnimModel;
|
||||
class TTrack;
|
||||
|
||||
class editor_mode : public application_mode
|
||||
{
|
||||
@@ -85,7 +89,7 @@ class editor_mode : public application_mode
|
||||
|
||||
struct EditorSnapshot
|
||||
{
|
||||
enum class Action { Move, Rotate, Scale, Add, Delete, Other };
|
||||
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other };
|
||||
|
||||
Action action{Action::Other};
|
||||
std::string node_name; // node identifier (basic_node::name())
|
||||
@@ -98,6 +102,8 @@ class editor_mode : public application_mode
|
||||
UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes
|
||||
scene::layer_handle layer{null_handle}; // scenery layer of the node, so a recreated node returns to it
|
||||
scene::instance_handle instance{0}; // include of a scenery template which was placed or removed, instead of a node
|
||||
// TrackEdit: state of all paths modified by the change (the edited path and neighbours dragged along)
|
||||
std::vector<std::pair<TTrack *, editor_track::state>> tracks;
|
||||
|
||||
};
|
||||
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
|
||||
@@ -268,4 +274,37 @@ class editor_mode : public application_mode
|
||||
bool m_gizmo_local{false}; // manipulate in the object's local space instead of world space
|
||||
gizmo_operation m_gizmo_op{gizmo_operation::translate}; // current transform mode (translate/rotate/scale)
|
||||
float m_gizmo_snap{1.0f}; // translation snap step (metres) applied while Ctrl is held
|
||||
|
||||
// path (track) editing, implemented in editormodetrack.cpp
|
||||
TTrack *selected_track() const;
|
||||
// settings and parameters of the selected path, drawn in the node bank tab of the track mode
|
||||
void render_track_ui();
|
||||
// curve and point handles of the selected path
|
||||
void draw_track_overlay() const;
|
||||
// selects the point handle of the selected path located under the cursor. returns: true if a handle was hit
|
||||
bool pick_track_handle();
|
||||
// selects the path picked in the viewport
|
||||
void select_track(scene::basic_node *Node);
|
||||
// gizmo for the selected path: moves the selected point handle, or the whole path
|
||||
void render_track_gizmo();
|
||||
// rebuilds paths modified by the ongoing drag; unless forced, at most a few times per second
|
||||
void commit_track_drag(bool const Force);
|
||||
// records provided state of paths in the undo history
|
||||
void push_track_snapshot(std::vector<std::pair<TTrack *, editor_track::state>> States);
|
||||
// restores state of paths stored in the history, moving their current state to the opposite stack
|
||||
void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite);
|
||||
editor_track::point_ref m_track_point; // selected point handle of the selected path
|
||||
std::vector<TTrack *> m_track_drag; // paths modified by the ongoing gizmo drag
|
||||
std::vector<std::pair<TTrack *, editor_track::point_ref>> m_track_drag_points; // end points of neighbours dragged along
|
||||
editor_track::snap_target m_track_snap; // connection made at the end of the ongoing drag
|
||||
glm::dvec3 m_track_pivot{0.0}; // rotation pivot of the ongoing drag
|
||||
glm::mat4 m_track_gizmo{1.0f}; // gizmo matrix, kept for the duration of a drag
|
||||
bool m_track_gizmo_using{false};
|
||||
bool m_track_dirty{false}; // drag changes waiting for rebuild
|
||||
std::chrono::steady_clock::time_point m_track_last_commit;
|
||||
editor_track::state m_track_field_before; // state of the path before the ongoing field edit
|
||||
std::array<std::array<char, 256>, 3> m_track_materials{}; // material names being edited
|
||||
float m_track_snap_radius{1.0f};
|
||||
bool m_track_align_tangent{true};
|
||||
bool m_track_drag_connected{true};
|
||||
};
|
||||
|
||||
613
application/editormodetrack.cpp
Normal file
613
application/editormodetrack.cpp
Normal file
@@ -0,0 +1,613 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
// path (track) editing part of the editor mode
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "application/editormode.h"
|
||||
#include "application/editoruilayer.h"
|
||||
|
||||
#include "utilities/Globals.h"
|
||||
#include "rendering/renderer.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "imgui/ImGuizmo.h"
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// projects world points onto the screen, with the same camera-relative view and clean perspective the gizmo uses
|
||||
class screen_projection
|
||||
{
|
||||
public:
|
||||
screen_projection()
|
||||
{
|
||||
ImGuiIO const &io = ImGui::GetIO();
|
||||
m_size = io.DisplaySize;
|
||||
m_camera = GfxRenderer->Camera_Position();
|
||||
float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor);
|
||||
float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f;
|
||||
m_viewprojection = glm::perspective(fovy, aspect, 0.1f, 10000.0f) * GfxRenderer->Camera_View_Matrix();
|
||||
}
|
||||
|
||||
glm::vec4 clip(glm::dvec3 const &Point) const
|
||||
{
|
||||
return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f);
|
||||
}
|
||||
ImVec2 screen(glm::vec4 const &Clip) const
|
||||
{
|
||||
return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y);
|
||||
}
|
||||
// returns: true if the point is in front of the camera
|
||||
bool project(glm::dvec3 const &Point, ImVec2 &Screen) const
|
||||
{
|
||||
auto const c = clip(Point);
|
||||
if (c.w < kNear)
|
||||
return false;
|
||||
Screen = screen(c);
|
||||
return true;
|
||||
}
|
||||
// draws a line, clipped against the near plane so points behind the camera don't flip across the screen
|
||||
void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const
|
||||
{
|
||||
glm::vec4 a = clip(A);
|
||||
glm::vec4 b = clip(B);
|
||||
if (a.w < kNear && b.w < kNear)
|
||||
return;
|
||||
if (a.w < kNear)
|
||||
a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w));
|
||||
else if (b.w < kNear)
|
||||
b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w));
|
||||
Drawlist->AddLine(screen(a), screen(b), Color, Thickness);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr float kNear{0.1f};
|
||||
ImVec2 m_size;
|
||||
glm::dvec3 m_camera;
|
||||
glm::mat4 m_viewprojection;
|
||||
};
|
||||
|
||||
// point on the course of a path, matching the curve TSegment builds from the same data
|
||||
glm::dvec3 path_point(segment_data const &Path, double const T)
|
||||
{
|
||||
auto const &p0 = Path.points[segment_data::point::start];
|
||||
auto const &p3 = Path.points[segment_data::point::end];
|
||||
auto const &cp1 = Path.points[segment_data::point::control1];
|
||||
auto const &cp2 = Path.points[segment_data::point::control2];
|
||||
if (cp1 == glm::dvec3{} && cp2 == glm::dvec3{})
|
||||
return glm::mix(p0, p3, T);
|
||||
auto const p1 = p0 + cp1;
|
||||
auto const p2 = p3 + cp2;
|
||||
double const u = 1.0 - T;
|
||||
return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3;
|
||||
}
|
||||
|
||||
char const *point_label(editor_track::point_kind const Kind)
|
||||
{
|
||||
switch (Kind)
|
||||
{
|
||||
case editor_track::point_kind::start: return "Start";
|
||||
case editor_track::point_kind::control1: return "Control 1";
|
||||
case editor_track::point_kind::control2: return "Control 2";
|
||||
default: return "End";
|
||||
}
|
||||
}
|
||||
|
||||
bool is_end(editor_track::point_kind const Kind)
|
||||
{
|
||||
return Kind == editor_track::point_kind::start || Kind == editor_track::point_kind::end;
|
||||
}
|
||||
|
||||
editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, editor_track::point_kind::control1, editor_track::point_kind::control2, editor_track::point_kind::end};
|
||||
|
||||
// screen distance within which a click grabs a point handle
|
||||
float const kHandleRadius{10.0f};
|
||||
|
||||
} // namespace
|
||||
|
||||
TTrack *editor_mode::selected_track() const
|
||||
{
|
||||
return dynamic_cast<TTrack *>(m_node);
|
||||
}
|
||||
|
||||
void editor_mode::select_track(scene::basic_node *Node)
|
||||
{
|
||||
auto *track = dynamic_cast<TTrack *>(Node);
|
||||
if (track == nullptr)
|
||||
{
|
||||
// a click beside the selected path releases its point handle
|
||||
m_track_point = {};
|
||||
return;
|
||||
}
|
||||
// paths are long, so the distance is measured to the nearest point of the path rather than its centre
|
||||
glm::dvec3 const camera{Global.pCamera.Pos};
|
||||
if (glm::distance(glm::dvec3(track->get_nearest_point(camera)), camera) > static_cast<double>(kMaxPlacementDistance))
|
||||
return;
|
||||
|
||||
if (track != m_node)
|
||||
m_track_point = {};
|
||||
m_node = track;
|
||||
ui()->set_node(m_node);
|
||||
}
|
||||
|
||||
bool editor_mode::pick_track_handle()
|
||||
{
|
||||
auto *track = selected_track();
|
||||
if (track == nullptr || false == editor_track::is_supported(*track))
|
||||
return false;
|
||||
|
||||
screen_projection const projection;
|
||||
ImVec2 const mouse = ImGui::GetIO().MousePos;
|
||||
float best = kHandleRadius * kHandleRadius;
|
||||
editor_track::point_ref hit;
|
||||
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
||||
{
|
||||
for (auto const kind : kPointKinds)
|
||||
{
|
||||
editor_track::point_ref const point{i, kind};
|
||||
ImVec2 screen;
|
||||
if (false == projection.project(editor_track::point_position(*track, point), screen))
|
||||
continue;
|
||||
float const dx = screen.x - mouse.x;
|
||||
float const dy = screen.y - mouse.y;
|
||||
// end points take precedence over control points placed at the same spot
|
||||
float const distance = (dx * dx + dy * dy) * (is_end(kind) ? 0.5f : 1.0f);
|
||||
if (distance < best)
|
||||
{
|
||||
best = distance;
|
||||
hit = point;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (false == hit.valid())
|
||||
return false;
|
||||
m_track_point = hit;
|
||||
return true;
|
||||
}
|
||||
|
||||
void editor_mode::draw_track_overlay() const
|
||||
{
|
||||
auto const *track = selected_track();
|
||||
if (track == nullptr)
|
||||
return;
|
||||
|
||||
screen_projection const projection;
|
||||
ImDrawList *drawlist = ImGui::GetBackgroundDrawList();
|
||||
bool const trackmode = ui()->mode() == nodebank_panel::TRACK;
|
||||
|
||||
// course of the path, from its source data so it follows the edits before the path is rebuilt
|
||||
ImU32 const coursecolor = IM_COL32(40, 220, 255, 220);
|
||||
int constexpr samples{32};
|
||||
for (auto const &path : track->m_paths)
|
||||
{
|
||||
auto previous = path_point(path, 0.0);
|
||||
for (int i = 1; i <= samples; ++i)
|
||||
{
|
||||
auto const next = path_point(path, static_cast<double>(i) / samples);
|
||||
projection.line(drawlist, previous, next, coursecolor, 2.5f);
|
||||
previous = next;
|
||||
}
|
||||
}
|
||||
if (false == trackmode || false == editor_track::is_supported(*track))
|
||||
return;
|
||||
|
||||
// control vectors
|
||||
ImU32 const vectorcolor = IM_COL32(200, 200, 200, 160);
|
||||
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
||||
{
|
||||
projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::start}), editor_track::point_position(*track, {i, editor_track::point_kind::control1}), vectorcolor, 1.0f);
|
||||
projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::end}), editor_track::point_position(*track, {i, editor_track::point_kind::control2}), vectorcolor, 1.0f);
|
||||
}
|
||||
// handles: end points coloured by connection state (green: connected, red: free), control points as squares
|
||||
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
||||
{
|
||||
for (auto const kind : kPointKinds)
|
||||
{
|
||||
editor_track::point_ref const point{i, kind};
|
||||
ImVec2 screen;
|
||||
if (false == projection.project(editor_track::point_position(*track, point), screen))
|
||||
continue;
|
||||
bool const selected = (point == m_track_point);
|
||||
if (is_end(kind))
|
||||
{
|
||||
ImU32 const color = editor_track::is_connected(*track, point) ? IM_COL32(60, 220, 60, 255) : IM_COL32(240, 60, 60, 255);
|
||||
drawlist->AddCircleFilled(screen, 6.0f, color);
|
||||
}
|
||||
else
|
||||
{
|
||||
drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 4.0f), ImVec2(screen.x + 4.0f, screen.y + 4.0f), IM_COL32(230, 230, 230, 255));
|
||||
}
|
||||
if (selected)
|
||||
drawlist->AddCircle(screen, 10.0f, IM_COL32(255, 220, 40, 255), 16, 2.5f);
|
||||
}
|
||||
}
|
||||
// connection to be made when the ongoing drag ends
|
||||
if (m_track_snap.track != nullptr)
|
||||
{
|
||||
ImVec2 screen;
|
||||
if (projection.project(m_track_snap.position, screen))
|
||||
drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f);
|
||||
projection.line(drawlist, m_track_snap.position, m_track_snap.position + m_track_snap.direction * 5.0, IM_COL32(255, 60, 255, 200), 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::render_track_gizmo()
|
||||
{
|
||||
auto *track = selected_track();
|
||||
std::string reason;
|
||||
if (track == nullptr || false == editor_track::can_edit_geometry(*track, reason))
|
||||
{
|
||||
m_track_gizmo_using = false;
|
||||
return;
|
||||
}
|
||||
bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast<int>(track->m_paths.size());
|
||||
|
||||
ImGuizmo::BeginFrame();
|
||||
ImGuizmo::SetOrthographic(false);
|
||||
ImGuiIO const &io = ImGui::GetIO();
|
||||
ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y);
|
||||
|
||||
// same camera-relative view and clean perspective as the transform gizmo of the other nodes
|
||||
glm::mat4 const view = GfxRenderer->Camera_View_Matrix();
|
||||
glm::dvec3 const camerapos = GfxRenderer->Camera_Position();
|
||||
float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor);
|
||||
float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f;
|
||||
glm::mat4 const projection = glm::perspective(fovy, aspect, 0.1f, 10000.0f);
|
||||
|
||||
// point handles only move; the whole path moves or turns around the vertical axis
|
||||
ImGuizmo::OPERATION const operation = (false == pointmode && m_gizmo_op == gizmo_operation::rotate) ? ImGuizmo::ROTATE_Y : ImGuizmo::TRANSLATE;
|
||||
|
||||
if (false == m_track_gizmo_using)
|
||||
{
|
||||
auto const anchor = pointmode ? editor_track::point_position(*track, m_track_point) : editor_track::pivot(*track);
|
||||
m_track_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - camerapos));
|
||||
}
|
||||
|
||||
glm::vec3 snapvalue(operation == ImGuizmo::ROTATE_Y ? 5.0f : m_gizmo_snap);
|
||||
float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr;
|
||||
glm::mat4 delta(1.0f);
|
||||
ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), operation, ImGuizmo::WORLD, glm::value_ptr(m_track_gizmo), glm::value_ptr(delta), snap);
|
||||
|
||||
if (ImGuizmo::IsUsing())
|
||||
{
|
||||
if (false == m_track_gizmo_using)
|
||||
{
|
||||
// drag start: gather the paths it modifies and record their state for undo
|
||||
m_track_gizmo_using = true;
|
||||
m_track_pivot = editor_track::pivot(*track);
|
||||
m_track_drag = {track};
|
||||
m_track_drag_points.clear();
|
||||
m_track_snap = {};
|
||||
if (pointmode && is_end(m_track_point.kind) && m_track_drag_connected)
|
||||
{
|
||||
for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point)))
|
||||
{
|
||||
std::string neighbourreason;
|
||||
if (false == editor_track::can_edit_geometry(*connection.first, neighbourreason))
|
||||
continue;
|
||||
m_track_drag_points.emplace_back(connection);
|
||||
if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end())
|
||||
m_track_drag.emplace_back(connection.first);
|
||||
}
|
||||
}
|
||||
std::vector<std::pair<TTrack *, editor_track::state>> states;
|
||||
for (auto *dragged : m_track_drag)
|
||||
states.emplace_back(dragged, editor_track::capture(*dragged));
|
||||
push_track_snapshot(std::move(states));
|
||||
}
|
||||
|
||||
if (operation == ImGuizmo::ROTATE_Y)
|
||||
{
|
||||
double const angle = std::atan2(delta[2][0], delta[0][0]);
|
||||
if (std::abs(angle) > 1e-9)
|
||||
{
|
||||
editor_track::rotate_y(*track, m_track_pivot, angle);
|
||||
m_track_dirty = true;
|
||||
}
|
||||
}
|
||||
else if (pointmode)
|
||||
{
|
||||
glm::dvec3 const position = camerapos + glm::dvec3(m_track_gizmo[3]);
|
||||
editor_track::move_point(*track, m_track_point, position);
|
||||
for (auto const &connection : m_track_drag_points)
|
||||
editor_track::move_point(*connection.first, connection.second, position);
|
||||
m_track_dirty = true;
|
||||
// a free end point can be connected with another path; a joint moved as a whole stays as it is
|
||||
if (is_end(m_track_point.kind) && m_track_drag_points.empty())
|
||||
{
|
||||
std::vector<TTrack const *> const exclude(m_track_drag.begin(), m_track_drag.end());
|
||||
m_track_snap = editor_track::find_snap_target(*track, position, m_track_snap_radius, exclude);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
glm::dvec3 const offset{delta[3]};
|
||||
if (glm::length(offset) > 1e-9)
|
||||
{
|
||||
editor_track::translate(*track, offset);
|
||||
m_track_dirty = true;
|
||||
}
|
||||
}
|
||||
commit_track_drag(false);
|
||||
}
|
||||
else if (m_track_gizmo_using)
|
||||
{
|
||||
// drag end: make the offered connection and rebuild everything for good
|
||||
if (m_track_snap.track != nullptr && pointmode)
|
||||
{
|
||||
editor_track::snap_point(*track, m_track_point, m_track_snap, m_track_align_tangent);
|
||||
m_track_dirty = true;
|
||||
}
|
||||
m_track_snap = {};
|
||||
commit_track_drag(true);
|
||||
m_track_gizmo_using = false;
|
||||
m_track_drag.clear();
|
||||
m_track_drag_points.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::commit_track_drag(bool const Force)
|
||||
{
|
||||
if (false == m_track_dirty || m_track_drag.empty())
|
||||
return;
|
||||
// each rebuild re-uploads geometry of the affected sections, so it isn't done on every frame of a drag
|
||||
auto const now = std::chrono::steady_clock::now();
|
||||
if (false == Force && now - m_track_last_commit < std::chrono::milliseconds(100))
|
||||
return;
|
||||
editor_track::commit(m_track_drag);
|
||||
m_track_dirty = false;
|
||||
m_track_last_commit = now;
|
||||
}
|
||||
|
||||
void editor_mode::push_track_snapshot(std::vector<std::pair<TTrack *, editor_track::state>> States)
|
||||
{
|
||||
if (States.empty())
|
||||
return;
|
||||
|
||||
if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size)
|
||||
m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1));
|
||||
|
||||
auto *track = States.front().first;
|
||||
EditorSnapshot snap;
|
||||
snap.action = EditorSnapshot::Action::TrackEdit;
|
||||
snap.node_name = track->name();
|
||||
snap.node_ptr = track;
|
||||
snap.position = track->location();
|
||||
snap.uuid = track->uuid;
|
||||
snap.tracks = std::move(States);
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
}
|
||||
|
||||
void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite)
|
||||
{
|
||||
if (Snapshot.tracks.empty())
|
||||
return;
|
||||
|
||||
EditorSnapshot current{Snapshot};
|
||||
std::vector<TTrack *> tracks;
|
||||
for (auto &entry : current.tracks)
|
||||
{
|
||||
tracks.emplace_back(entry.first);
|
||||
entry.second = editor_track::capture(*entry.first);
|
||||
}
|
||||
Opposite.push_back(std::move(current));
|
||||
|
||||
for (auto const &entry : Snapshot.tracks)
|
||||
editor_track::apply(*entry.first, entry.second);
|
||||
editor_track::commit(tracks);
|
||||
|
||||
m_track_snap = {};
|
||||
m_track_dirty = false;
|
||||
if (m_node != tracks.front())
|
||||
m_track_point = {};
|
||||
m_node = tracks.front();
|
||||
ui()->set_node(m_node);
|
||||
}
|
||||
|
||||
void editor_mode::render_track_ui()
|
||||
{
|
||||
ImGui::TextDisabled("LMB: select path or point handle Esc: release point\nGizmo moves the selected point, otherwise the whole path (W: turn)");
|
||||
auto *track = selected_track();
|
||||
if (track == nullptr)
|
||||
{
|
||||
ImGui::TextDisabled("No path selected");
|
||||
return;
|
||||
}
|
||||
|
||||
char const *type = track->eType == tt_Normal ? "normal" : track->eType == tt_Switch ? "switch" : track->eType == tt_Cross ? "cross" : track->eType == tt_Table ? "turntable" : track->eType == tt_Tributary ? "tributary" : "unknown";
|
||||
char const *category = (track->iCategoryFlag & 1) ? "rail" : (track->iCategoryFlag & 2) ? "road" : (track->iCategoryFlag & 4) ? "river" : "other";
|
||||
ImGui::Text("%s (%s %s, %.2f m)", track->name().empty() ? "(noname)" : track->name().c_str(), category, type, track->Length());
|
||||
|
||||
if (false == editor_track::is_supported(*track))
|
||||
{
|
||||
ImGui::TextDisabled("Editing of this path type isn't supported");
|
||||
return;
|
||||
}
|
||||
std::string reason;
|
||||
bool const geometry = editor_track::can_edit_geometry(*track, reason);
|
||||
if (false == geometry)
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "Geometry locked: %s", reason.c_str());
|
||||
|
||||
auto const before = editor_track::capture(*track);
|
||||
// records undo state when a field edit starts, and rebuilds the path once it's finished
|
||||
enum class rebuild { none, parameters, geometry };
|
||||
auto const finish_edit = [&](rebuild const Rebuild) {
|
||||
if (ImGui::IsItemActivated())
|
||||
m_track_field_before = before;
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
push_track_snapshot({{track, m_track_field_before}});
|
||||
switch (Rebuild)
|
||||
{
|
||||
case rebuild::geometry: editor_track::commit({track}); break;
|
||||
case rebuild::parameters: editor_track::commit_parameters(*track); break;
|
||||
default: track->mark_dirty(); break;
|
||||
}
|
||||
}
|
||||
};
|
||||
// applies a change made with a widget which reports it at once, like a combo box
|
||||
auto const apply_now = [&](rebuild const Rebuild) {
|
||||
push_track_snapshot({{track, before}});
|
||||
if (Rebuild == rebuild::parameters)
|
||||
editor_track::commit_parameters(*track);
|
||||
else
|
||||
track->mark_dirty();
|
||||
};
|
||||
|
||||
if (ImGui::CollapsingHeader("Path parameters", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::PushItemWidth(120.0f);
|
||||
double velocity = editor_track::velocity(*track);
|
||||
if (ImGui::InputDouble("Velocity (km/h, -1: none)", &velocity, 0.0, 0.0, "%.1f"))
|
||||
editor_track::velocity(*track, velocity);
|
||||
finish_edit(rebuild::none);
|
||||
if (ImGui::InputFloat("Friction", &track->fFriction, 0.0f, 0.0f, "%.3f"))
|
||||
track->fFriction = std::max(0.0f, track->fFriction);
|
||||
finish_edit(rebuild::none);
|
||||
ImGui::InputFloat("Sound distance", &track->fSoundDistance, 0.0f, 0.0f, "%.1f");
|
||||
finish_edit(rebuild::none);
|
||||
ImGui::InputInt("Quality", &track->iQualityFlag, 0);
|
||||
finish_edit(rebuild::none);
|
||||
int damage = track->iDamageFlag;
|
||||
if (ImGui::InputInt("Damage flags", &damage, 0))
|
||||
editor_track::damage(*track, damage);
|
||||
finish_edit(rebuild::none);
|
||||
|
||||
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
|
||||
int environment = std::clamp(static_cast<int>(track->eEnvironment), 0, 5);
|
||||
if (ImGui::Combo("Environment", &environment, environments, IM_ARRAYSIZE(environments)))
|
||||
{
|
||||
track->eEnvironment = static_cast<TEnvironmentType>(environment);
|
||||
apply_now(rebuild::none);
|
||||
}
|
||||
|
||||
if (ImGui::InputFloat("Width", &track->fTrackWidth, 0.0f, 0.0f, "%.3f"))
|
||||
track->fTrackWidth = std::max(0.01f, track->fTrackWidth);
|
||||
finish_edit(rebuild::parameters);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
if (track->m_visible)
|
||||
{
|
||||
// material names are edited in local buffers, refreshed from the path whenever no field is being edited
|
||||
material_handle *materials[] = {&track->m_material1, &track->m_material2, track->SwitchExtension ? &track->SwitchExtension->m_material3 : nullptr};
|
||||
char const *labels[] = {"Texture 1 (rails/surface)", "Texture 2 (trackbed/side)", "Trackbed (switch)"};
|
||||
if (false == ImGui::IsAnyItemActive())
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{};
|
||||
std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1);
|
||||
m_track_materials[i].back() = '\0';
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
if (materials[i] == nullptr || (i == 2 && track->eType != tt_Switch))
|
||||
continue;
|
||||
ImGui::PushItemWidth(-160.0f);
|
||||
if (ImGui::InputText(labels[i], m_track_materials[i].data(), m_track_materials[i].size(), ImGuiInputTextFlags_EnterReturnsTrue))
|
||||
{
|
||||
*materials[i] = editor_track::fetch_material(m_track_materials[i].data());
|
||||
apply_now(rebuild::parameters);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
}
|
||||
ImGui::TextDisabled("Enter applies a texture name, \"none\" removes the texture");
|
||||
|
||||
ImGui::PushItemWidth(120.0f);
|
||||
if (ImGui::InputFloat("Texture length", &track->fTexLength, 0.0f, 0.0f, "%.2f"))
|
||||
track->fTexLength = std::max(0.01f, track->fTexLength);
|
||||
finish_edit(rebuild::parameters);
|
||||
float height = editor_track::texture_height(*track);
|
||||
if (ImGui::InputFloat("Trackbed height", &height, 0.0f, 0.0f, "%.3f"))
|
||||
editor_track::texture_height(*track, height);
|
||||
finish_edit(rebuild::parameters);
|
||||
ImGui::InputFloat("Trackbed width", &track->fTexWidth, 0.0f, 0.0f, "%.3f");
|
||||
finish_edit(rebuild::parameters);
|
||||
ImGui::InputFloat("Trackbed slope", &track->fTexSlope, 0.0f, 0.0f, "%.3f");
|
||||
finish_edit(rebuild::parameters);
|
||||
ImGui::PopItemWidth();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Path geometry", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::PushItemWidth(120.0f);
|
||||
ImGui::InputFloat("Snap radius (m)", &m_track_snap_radius, 0.0f, 0.0f, "%.2f");
|
||||
m_track_snap_radius = std::max(0.0f, m_track_snap_radius);
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::Checkbox("Align with snapped path", &m_track_align_tangent);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Drag connected ends", &m_track_drag_connected);
|
||||
|
||||
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
||||
{
|
||||
ImGui::PushID(i);
|
||||
if (track->m_paths.size() > 1)
|
||||
ImGui::Text("Path %d", i + 1);
|
||||
// point selection, same as clicking the handle in the viewport
|
||||
for (auto const kind : kPointKinds)
|
||||
{
|
||||
editor_track::point_ref const point{i, kind};
|
||||
if (kind != editor_track::point_kind::start)
|
||||
ImGui::SameLine();
|
||||
std::string label = point_label(kind);
|
||||
if (is_end(kind))
|
||||
label += editor_track::is_connected(*track, point) ? " (+)" : " (-)";
|
||||
if (ImGui::Selectable(label.c_str(), m_track_point == point, 0, ImVec2(90.0f, 0.0f)))
|
||||
m_track_point = (m_track_point == point ? editor_track::point_ref{} : point);
|
||||
}
|
||||
|
||||
auto &path = track->m_paths[i];
|
||||
if (geometry)
|
||||
{
|
||||
ImGui::PushItemWidth(-160.0f);
|
||||
for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end})
|
||||
{
|
||||
auto position = path.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end];
|
||||
if (ImGui::InputScalarN(point_label(kind), ImGuiDataType_Double, glm::value_ptr(position), 3, nullptr, nullptr, "%.3f"))
|
||||
editor_track::move_point(*track, {i, kind}, position); // keeps the common start of a switch together
|
||||
finish_edit(rebuild::geometry);
|
||||
}
|
||||
ImGui::InputScalarN("Control 1 (relative)", ImGuiDataType_Double, glm::value_ptr(path.points[segment_data::point::control1]), 3, nullptr, nullptr, "%.3f");
|
||||
finish_edit(rebuild::geometry);
|
||||
ImGui::InputScalarN("Control 2 (relative)", ImGuiDataType_Double, glm::value_ptr(path.points[segment_data::point::control2]), 3, nullptr, nullptr, "%.3f");
|
||||
finish_edit(rebuild::geometry);
|
||||
ImGui::InputFloat2("Roll (deg)", path.rolls.data(), "%.2f");
|
||||
finish_edit(rebuild::geometry);
|
||||
ImGui::InputFloat("Radius", &path.radius, 0.0f, 0.0f, "%.1f");
|
||||
finish_edit(rebuild::geometry);
|
||||
ImGui::PopItemWidth();
|
||||
if (ImGui::Button("Straighten"))
|
||||
{
|
||||
push_track_snapshot({{track, before}});
|
||||
path.points[segment_data::point::control1] = glm::dvec3{};
|
||||
path.points[segment_data::point::control2] = glm::dvec3{};
|
||||
path.radius = 0.0f;
|
||||
editor_track::commit({track});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const &start = path.points[segment_data::point::start];
|
||||
auto const &end = path.points[segment_data::point::end];
|
||||
ImGui::TextDisabled("Start (%.2f, %.2f, %.2f) End (%.2f, %.2f, %.2f)", start.x, start.y, start.z, end.x, end.y, end.z);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,10 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode)
|
||||
if (m_filloptions)
|
||||
m_filloptions();
|
||||
break;
|
||||
case nodebank_panel::TRACK:
|
||||
if (m_trackoptions)
|
||||
m_trackoptions();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ class editor_ui : public ui_layer
|
||||
{
|
||||
m_filloptions = std::move(Renderer);
|
||||
}
|
||||
// draws the path editing settings in its node bank tab (the editing state lives in the editor mode)
|
||||
void set_track_options(std::function<void()> Renderer)
|
||||
{
|
||||
m_trackoptions = std::move(Renderer);
|
||||
}
|
||||
// draws the gizmo settings in the toolset window (the gizmo state lives in the editor mode)
|
||||
void set_gizmo_options(std::function<void()> Renderer)
|
||||
{
|
||||
@@ -77,6 +82,7 @@ class editor_ui : public ui_layer
|
||||
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
|
||||
model_set_ref m_insertset;
|
||||
std::function<void()> m_filloptions;
|
||||
std::function<void()> m_trackoptions;
|
||||
std::function<void()> m_gizmooptions;
|
||||
scene::basic_node *m_node{nullptr}; // currently bound scene node, if any
|
||||
};
|
||||
|
||||
@@ -542,7 +542,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[] = {{"Select", MODIFY}, {"Track", TRACK}, {"Insert", ADD}, {"Brush", BRUSH}, {"Area fill", FILL}, {"Copy to bank", COPY}};
|
||||
if (ImGui::BeginTabBar("##editmodes", ImGuiTabBarFlags_FittingPolicyResizeDown))
|
||||
{
|
||||
for (auto const &tab : modes)
|
||||
|
||||
@@ -122,7 +122,8 @@ class nodebank_panel : public ui_panel
|
||||
COPY,
|
||||
ADD,
|
||||
BRUSH,
|
||||
FILL
|
||||
FILL,
|
||||
TRACK
|
||||
};
|
||||
edit_mode mode = MODIFY;
|
||||
|
||||
|
||||
645
editor/editorTrack.cpp
Normal file
645
editor/editorTrack.cpp
Normal file
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
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 "editor/editorTrack.hpp"
|
||||
|
||||
#include "world/Track.h"
|
||||
#include "scene/scene.h"
|
||||
#include "simulation/simulation.h"
|
||||
#include "rendering/renderer.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/utilities.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// end points closer than this are treated as the same point
|
||||
double const kSamePoint{0.05};
|
||||
|
||||
segment_data::point point_index(editor_track::point_kind const Kind)
|
||||
{
|
||||
switch (Kind)
|
||||
{
|
||||
case editor_track::point_kind::start: return segment_data::point::start;
|
||||
case editor_track::point_kind::control1: return segment_data::point::control1;
|
||||
case editor_track::point_kind::control2: return segment_data::point::control2;
|
||||
default: return segment_data::point::end;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_end(editor_track::point_kind const Kind)
|
||||
{
|
||||
return Kind == editor_track::point_kind::start || Kind == editor_track::point_kind::end;
|
||||
}
|
||||
|
||||
bool has_switch_paths(TTrack const &Track)
|
||||
{
|
||||
return Track.SwitchExtension != nullptr && (Track.eType == tt_Switch || Track.eType == tt_Cross);
|
||||
}
|
||||
|
||||
void add_unique(std::vector<TTrack *> &Tracks, TTrack *Track)
|
||||
{
|
||||
if (Track != nullptr && std::find(Tracks.begin(), Tracks.end(), Track) == Tracks.end())
|
||||
Tracks.emplace_back(Track);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
editor_track::state editor_track::capture(TTrack const &Track)
|
||||
{
|
||||
state result;
|
||||
result.paths = Track.m_paths;
|
||||
result.velocity = Track.fVelocity;
|
||||
result.switchvelocity = Track.SwitchExtension ? Track.SwitchExtension->fVelocity : -1.f;
|
||||
result.width = Track.fTrackWidth;
|
||||
result.friction = Track.fFriction;
|
||||
result.sounddistance = Track.fSoundDistance;
|
||||
result.quality = Track.iQualityFlag;
|
||||
result.damage = Track.iDamageFlag;
|
||||
result.environment = static_cast<int>(Track.eEnvironment);
|
||||
result.material1 = Track.m_material1;
|
||||
result.material2 = Track.m_material2;
|
||||
result.material3 = Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle;
|
||||
result.texlength = Track.fTexLength;
|
||||
result.texheight = texture_height(Track);
|
||||
result.texwidth = Track.fTexWidth;
|
||||
result.texslope = Track.fTexSlope;
|
||||
return result;
|
||||
}
|
||||
|
||||
void editor_track::apply(TTrack &Track, state const &State)
|
||||
{
|
||||
Track.m_paths = State.paths;
|
||||
Track.fVelocity = State.velocity;
|
||||
if (Track.SwitchExtension)
|
||||
{
|
||||
Track.SwitchExtension->fVelocity = State.switchvelocity;
|
||||
Track.SwitchExtension->m_material3 = State.material3;
|
||||
}
|
||||
Track.fTrackWidth = State.width;
|
||||
Track.fTrackWidth2 = State.width;
|
||||
Track.fFriction = State.friction;
|
||||
Track.fSoundDistance = State.sounddistance;
|
||||
Track.iQualityFlag = State.quality;
|
||||
damage(Track, State.damage);
|
||||
Track.eEnvironment = static_cast<TEnvironmentType>(State.environment);
|
||||
Track.m_material1 = State.material1;
|
||||
Track.m_material2 = State.material2;
|
||||
Track.fTexLength = State.texlength;
|
||||
texture_height(Track, State.texheight);
|
||||
Track.fTexWidth = State.texwidth;
|
||||
Track.fTexSlope = State.texslope;
|
||||
}
|
||||
|
||||
bool editor_track::is_supported(TTrack const &Track)
|
||||
{
|
||||
if ((Track.iCategoryFlag & 0x80) != 0)
|
||||
return false; // auto-generated helper path
|
||||
if (Track.eType == tt_Normal)
|
||||
return Track.Segment != nullptr;
|
||||
if (Track.eType == tt_Switch)
|
||||
return Track.SwitchExtension != nullptr && Track.m_paths.size() >= 2;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool editor_track::can_edit_geometry(TTrack const &Track, std::string &Reason)
|
||||
{
|
||||
if (false == is_supported(Track))
|
||||
{
|
||||
Reason = "Editing of this path type isn't supported";
|
||||
return false;
|
||||
}
|
||||
if (Global.NvRenderer)
|
||||
{
|
||||
Reason = "Path geometry can't be rebuilt with the experimental renderer";
|
||||
return false;
|
||||
}
|
||||
if (false == Track.Dynamics.empty())
|
||||
{
|
||||
Reason = "There are vehicles placed on this path";
|
||||
return false;
|
||||
}
|
||||
Reason.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void editor_track::commit(std::vector<TTrack *> const &Tracks)
|
||||
{
|
||||
if (Tracks.empty())
|
||||
return;
|
||||
|
||||
// paths whose render geometry depends on the modified ones
|
||||
std::vector<TTrack *> affected;
|
||||
for (auto *track : Tracks)
|
||||
{
|
||||
add_unique(affected, track);
|
||||
for (auto *neighbour : neighbours(*track))
|
||||
add_unique(affected, neighbour);
|
||||
}
|
||||
// take the paths out of the scene while their ends still match the old segments...
|
||||
for (auto *track : Tracks)
|
||||
{
|
||||
disconnect(*track);
|
||||
simulation::Region->erase_and_unregister(track);
|
||||
}
|
||||
// ...rebuild the segments and put the paths back in their new place...
|
||||
for (auto *track : Tracks)
|
||||
{
|
||||
track->init_segments(false);
|
||||
track->update_location();
|
||||
track->m_area.radius = -1.f; // forces radius recalculation
|
||||
simulation::Region->insert_and_register(track);
|
||||
}
|
||||
// ...connect them with whatever they touch now...
|
||||
for (auto *track : Tracks)
|
||||
join(*track);
|
||||
for (auto *track : Tracks)
|
||||
for (auto *neighbour : neighbours(*track))
|
||||
add_unique(affected, neighbour);
|
||||
// ...and update the visuals
|
||||
for (auto *track : affected)
|
||||
update_transition(*track);
|
||||
for (auto *track : affected)
|
||||
rebuild_geometry(*track);
|
||||
for (auto *track : Tracks)
|
||||
track->mark_dirty();
|
||||
}
|
||||
|
||||
void editor_track::commit_parameters(TTrack &Track)
|
||||
{
|
||||
Track.fTrackWidth2 = Track.fTrackWidth;
|
||||
// segments are re-created from unchanged data, but road texture proportions and roll fix depend on parameters
|
||||
Track.init_segments(false);
|
||||
|
||||
std::vector<TTrack *> affected{&Track};
|
||||
for (auto *neighbour : neighbours(Track))
|
||||
add_unique(affected, neighbour);
|
||||
for (auto *track : affected)
|
||||
update_transition(*track);
|
||||
for (auto *track : affected)
|
||||
rebuild_geometry(*track);
|
||||
Track.mark_dirty();
|
||||
}
|
||||
|
||||
glm::dvec3 editor_track::point_position(TTrack const &Track, point_ref const &Point)
|
||||
{
|
||||
if (Point.path < 0 || Point.path >= static_cast<int>(Track.m_paths.size()))
|
||||
return Track.location();
|
||||
|
||||
auto const &path{Track.m_paths[Point.path]};
|
||||
auto const &start{path.points[segment_data::point::start]};
|
||||
auto const &end{path.points[segment_data::point::end]};
|
||||
switch (Point.kind)
|
||||
{
|
||||
case point_kind::start: return start;
|
||||
case point_kind::end: return end;
|
||||
case point_kind::control1:
|
||||
{
|
||||
auto const &control{path.points[segment_data::point::control1]};
|
||||
return control == glm::dvec3{} ? start + (end - start) / 3.0 : start + control;
|
||||
}
|
||||
case point_kind::control2:
|
||||
{
|
||||
auto const &control{path.points[segment_data::point::control2]};
|
||||
return control == glm::dvec3{} ? end + (start - end) / 3.0 : end + control;
|
||||
}
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
void editor_track::move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position)
|
||||
{
|
||||
if (Point.path < 0 || Point.path >= static_cast<int>(Track.m_paths.size()))
|
||||
return;
|
||||
|
||||
auto &path{Track.m_paths[Point.path]};
|
||||
switch (Point.kind)
|
||||
{
|
||||
case point_kind::start:
|
||||
case point_kind::end:
|
||||
{
|
||||
auto &point{path.points[point_index(Point.kind)]};
|
||||
auto const oldposition{point};
|
||||
point = Position;
|
||||
// control vectors are relative to their end points, so they follow them automatically.
|
||||
// other paths of the same piece sharing this point (the common start of a switch) follow as well
|
||||
for (auto &otherpath : Track.m_paths)
|
||||
{
|
||||
if (&otherpath == &path)
|
||||
continue;
|
||||
for (auto const index : {segment_data::point::start, segment_data::point::end})
|
||||
if (glm::distance(otherpath.points[index], oldposition) < kSamePoint)
|
||||
otherpath.points[index] = Position;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case point_kind::control1:
|
||||
{
|
||||
auto control{Position - path.points[segment_data::point::start]};
|
||||
path.points[segment_data::point::control1] = (glm::length(control) < 0.01 ? glm::dvec3{} : control);
|
||||
break;
|
||||
}
|
||||
case point_kind::control2:
|
||||
{
|
||||
auto control{Position - path.points[segment_data::point::end]};
|
||||
path.points[segment_data::point::control2] = (glm::length(control) < 0.01 ? glm::dvec3{} : control);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void editor_track::translate(TTrack &Track, glm::dvec3 const &Offset)
|
||||
{
|
||||
for (auto &path : Track.m_paths)
|
||||
{
|
||||
path.points[segment_data::point::start] += Offset;
|
||||
path.points[segment_data::point::end] += Offset;
|
||||
}
|
||||
}
|
||||
|
||||
void editor_track::rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle)
|
||||
{
|
||||
auto const c{std::cos(Angle)};
|
||||
auto const s{std::sin(Angle)};
|
||||
auto const rotate = [&](glm::dvec3 const &Vector) { return glm::dvec3{Vector.x * c + Vector.z * s, Vector.y, -Vector.x * s + Vector.z * c}; };
|
||||
|
||||
for (auto &path : Track.m_paths)
|
||||
{
|
||||
for (auto const index : {segment_data::point::start, segment_data::point::end})
|
||||
path.points[index] = Pivot + rotate(path.points[index] - Pivot);
|
||||
for (auto const index : {segment_data::point::control1, segment_data::point::control2})
|
||||
path.points[index] = rotate(path.points[index]);
|
||||
}
|
||||
}
|
||||
|
||||
glm::dvec3 editor_track::pivot(TTrack const &Track)
|
||||
{
|
||||
return Track.location();
|
||||
}
|
||||
|
||||
std::vector<TTrack *> editor_track::neighbours(TTrack const &Track)
|
||||
{
|
||||
std::vector<TTrack *> result;
|
||||
add_unique(result, Track.trPrev);
|
||||
add_unique(result, Track.trNext);
|
||||
if (has_switch_paths(Track))
|
||||
{
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
add_unique(result, Track.SwitchExtension->pPrevs[i]);
|
||||
add_unique(result, Track.SwitchExtension->pNexts[i]);
|
||||
}
|
||||
}
|
||||
result.erase(std::remove(result.begin(), result.end(), &Track), result.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::pair<TTrack *, editor_track::point_ref>> editor_track::connected_points(TTrack const &Track, glm::dvec3 const &Position)
|
||||
{
|
||||
std::vector<std::pair<TTrack *, point_ref>> result;
|
||||
for (auto *neighbour : neighbours(Track))
|
||||
{
|
||||
for (int i = 0; i < static_cast<int>(neighbour->m_paths.size()); ++i)
|
||||
{
|
||||
for (auto const kind : {point_kind::start, point_kind::end})
|
||||
{
|
||||
if (glm::distance(neighbour->m_paths[i].points[point_index(kind)], Position) < kSamePoint)
|
||||
result.emplace_back(neighbour, point_ref{i, kind});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool editor_track::is_connected(TTrack const &Track, point_ref const &Point)
|
||||
{
|
||||
if (false == is_end(Point.kind))
|
||||
return false;
|
||||
return false == connected_points(Track, point_position(Track, Point)).empty();
|
||||
}
|
||||
|
||||
editor_track::snap_target editor_track::find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector<TTrack const *> const &Exclude)
|
||||
{
|
||||
snap_target result;
|
||||
result.distance = std::numeric_limits<double>::max();
|
||||
|
||||
auto const excluded = [&](TTrack const *Other) { return std::find(Exclude.begin(), Exclude.end(), Other) != Exclude.end(); };
|
||||
auto const category{Track.iCategoryFlag & 15};
|
||||
// NOTE: copy, the region returns its scratchpad
|
||||
auto const sections{simulation::Region->sections(Position, static_cast<float>(Radius))};
|
||||
for (auto *section : sections)
|
||||
{
|
||||
for (auto const &cell : section->m_cells)
|
||||
{
|
||||
for (auto *other : cell.m_directories.paths)
|
||||
{
|
||||
if (other == &Track || excluded(other) || (other->iCategoryFlag & 15) != category || false == is_supported(*other))
|
||||
continue;
|
||||
for (int i = 0; i < static_cast<int>(other->m_paths.size()); ++i)
|
||||
{
|
||||
auto const &path{other->m_paths[i]};
|
||||
for (auto const kind : {point_kind::start, point_kind::end})
|
||||
{
|
||||
auto const &point{path.points[point_index(kind)]};
|
||||
auto const distance{glm::distance(point, Position)};
|
||||
if (distance > Radius || distance >= result.distance)
|
||||
continue;
|
||||
// skip ends already connected with something which stays in place
|
||||
auto const connections{connected_points(*other, point)};
|
||||
if (std::any_of(connections.begin(), connections.end(), [&](auto const &Connection) { return false == excluded(Connection.first); }))
|
||||
continue;
|
||||
|
||||
glm::dvec3 direction;
|
||||
if (kind == point_kind::start)
|
||||
{
|
||||
auto const &control{path.points[segment_data::point::control1]};
|
||||
direction = control != glm::dvec3{} ? control : path.points[segment_data::point::end] - point;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const &control{path.points[segment_data::point::control2]};
|
||||
direction = control != glm::dvec3{} ? control : path.points[segment_data::point::start] - point;
|
||||
}
|
||||
if (glm::length(direction) < 1e-6)
|
||||
continue;
|
||||
|
||||
result.track = other;
|
||||
result.point = point_ref{i, kind};
|
||||
result.position = point;
|
||||
result.direction = glm::normalize(direction);
|
||||
result.distance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void editor_track::snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent)
|
||||
{
|
||||
if (Target.track == nullptr || false == is_end(Point.kind))
|
||||
return;
|
||||
|
||||
move_point(Track, Point, Target.position);
|
||||
if (false == Aligntangent)
|
||||
return;
|
||||
|
||||
// the path continues the target, so it leaves the shared point in the opposite direction
|
||||
auto &path{Track.m_paths[Point.path]};
|
||||
auto const &start{path.points[segment_data::point::start]};
|
||||
auto const &end{path.points[segment_data::point::end]};
|
||||
auto &control{path.points[Point.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2]};
|
||||
auto const length{control != glm::dvec3{} ? glm::length(control) : glm::distance(start, end) / 3.0};
|
||||
control = -Target.direction * length;
|
||||
}
|
||||
|
||||
std::string editor_track::material_name(material_handle const Material)
|
||||
{
|
||||
if (Material == null_handle)
|
||||
return "none";
|
||||
auto name{GfxRenderer->Material(Material)->GetName()};
|
||||
if (name.find(paths::textures) == 0)
|
||||
name.erase(0, std::string{paths::textures}.size());
|
||||
return name;
|
||||
}
|
||||
|
||||
material_handle editor_track::fetch_material(std::string const &Name)
|
||||
{
|
||||
if (Name.empty() || Name == "none")
|
||||
return null_handle;
|
||||
auto name{Name};
|
||||
replace_slashes(name);
|
||||
return GfxRenderer->Fetch_Material(name);
|
||||
}
|
||||
|
||||
float editor_track::texture_height(TTrack const &Track)
|
||||
{
|
||||
return (Track.fTexHeight1 - Track.fTexHeightOffset) * (Track.iCategoryFlag & 4 ? -1.f : 1.f);
|
||||
}
|
||||
|
||||
void editor_track::texture_height(TTrack &Track, float const Height)
|
||||
{
|
||||
Track.fTexHeight1 = Height * (Track.iCategoryFlag & 4 ? -1.f : 1.f) + Track.fTexHeightOffset;
|
||||
}
|
||||
|
||||
double editor_track::velocity(TTrack const &Track)
|
||||
{
|
||||
if (Track.SwitchExtension && Track.eType == tt_Switch)
|
||||
return Track.SwitchExtension->fVelocity;
|
||||
return Track.fVelocity;
|
||||
}
|
||||
|
||||
void editor_track::velocity(TTrack &Track, double const Velocity)
|
||||
{
|
||||
if (Track.SwitchExtension && Track.eType == tt_Switch)
|
||||
{
|
||||
// same rules as TTrack::Switch()
|
||||
Track.SwitchExtension->fVelocity = static_cast<float>(Velocity);
|
||||
if (Velocity <= -2.0)
|
||||
Track.fVelocity = Track.SwitchExtension->CurrentIndex ? -Velocity : -1.0;
|
||||
else
|
||||
Track.fVelocity = Velocity;
|
||||
return;
|
||||
}
|
||||
Track.fVelocity = Velocity;
|
||||
}
|
||||
|
||||
void editor_track::damage(TTrack &Track, int const Damage)
|
||||
{
|
||||
Track.iDamageFlag = Damage;
|
||||
if (Damage & 128)
|
||||
Track.iAction |= 0x80;
|
||||
else
|
||||
Track.iAction &= ~0x80;
|
||||
}
|
||||
|
||||
void editor_track::disconnect(TTrack &Track)
|
||||
{
|
||||
for (auto *neighbour : neighbours(Track))
|
||||
{
|
||||
if (has_switch_paths(*neighbour))
|
||||
{
|
||||
auto &extension{*neighbour->SwitchExtension};
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
if (extension.pPrevs[i] == &Track)
|
||||
extension.pPrevs[i] = nullptr;
|
||||
if (extension.pNexts[i] == &Track)
|
||||
extension.pNexts[i] = nullptr;
|
||||
}
|
||||
bind_switch_path(*neighbour, extension.CurrentIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (neighbour->trPrev == &Track)
|
||||
neighbour->trPrev = nullptr;
|
||||
if (neighbour->trNext == &Track)
|
||||
neighbour->trNext = nullptr;
|
||||
}
|
||||
}
|
||||
Track.trPrev = nullptr;
|
||||
Track.trNext = nullptr;
|
||||
if (has_switch_paths(Track))
|
||||
{
|
||||
auto &extension{*Track.SwitchExtension};
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
extension.pPrevs[i] = nullptr;
|
||||
extension.pNexts[i] = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void editor_track::join(TTrack &Track)
|
||||
{
|
||||
TTrack *other;
|
||||
int endpointid;
|
||||
switch (Track.eType)
|
||||
{
|
||||
case tt_Normal:
|
||||
{
|
||||
if (Track.trPrev == nullptr)
|
||||
{
|
||||
std::tie(other, endpointid) = simulation::Region->find_path(Track.Segment->FastGetPoint_0(), &Track);
|
||||
connect(Track, true, other, endpointid);
|
||||
}
|
||||
if (Track.trNext == nullptr)
|
||||
{
|
||||
std::tie(other, endpointid) = simulation::Region->find_path(Track.Segment->FastGetPoint_1(), &Track);
|
||||
connect(Track, false, other, endpointid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case tt_Switch:
|
||||
{
|
||||
// like during scenery load, switches are connected from the side of the regular paths
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
auto const &segment{Track.SwitchExtension->Segments[i]};
|
||||
for (auto const &point : {segment->FastGetPoint_0(), segment->FastGetPoint_1()})
|
||||
{
|
||||
std::tie(other, endpointid) = simulation::Region->find_path(point, &Track);
|
||||
if (other != nullptr && other->eType == tt_Normal)
|
||||
join(*other);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void editor_track::connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid)
|
||||
{
|
||||
if (Other == nullptr)
|
||||
return;
|
||||
|
||||
switch (Endpointid)
|
||||
{
|
||||
case 0:
|
||||
if (Prevside)
|
||||
Track.ConnectPrevPrev(Other, 0);
|
||||
else
|
||||
Track.ConnectNextPrev(Other, 0);
|
||||
break;
|
||||
case 1:
|
||||
if (Prevside)
|
||||
Track.ConnectPrevNext(Other, 1);
|
||||
else
|
||||
Track.ConnectNextNext(Other, 1);
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
{
|
||||
if (false == has_switch_paths(*Other))
|
||||
break;
|
||||
// same connections as in path_table::InitTracks(), but made without moving the switch blades
|
||||
auto const path{(Endpointid - 2) / 2};
|
||||
auto const atend{((Endpointid - 2) % 2) != 0};
|
||||
bind_switch_path(*Other, path);
|
||||
if (Prevside)
|
||||
{
|
||||
if (atend)
|
||||
Track.ConnectPrevNext(Other, path ? 3 : 1);
|
||||
else
|
||||
Track.ConnectPrevPrev(Other, path ? 2 : 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (atend)
|
||||
Track.ConnectNextNext(Other, path ? 3 : 1);
|
||||
else
|
||||
Track.ConnectNextPrev(Other, path ? 2 : 0);
|
||||
}
|
||||
store_switch_path(*Other, path);
|
||||
bind_switch_path(*Other, Other->SwitchExtension->CurrentIndex);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void editor_track::update_transition(TTrack &Track)
|
||||
{
|
||||
// same rule as TTrack::ConnectNextPrev(): transition is drawn by the path whose end 2 meets start of a different path
|
||||
Track.iTrapezoid &= ~2;
|
||||
auto const *next{Track.trNext};
|
||||
if (next == nullptr || Track.iNextDirection != 0)
|
||||
return;
|
||||
if (Track.m_visible && next->m_visible && Track.eType == tt_Normal && next->eType == tt_Normal)
|
||||
{
|
||||
if (Track.fTrackWidth != next->fTrackWidth || Track.fTexHeight1 != next->fTexHeight1 || Track.fTexWidth != next->fTexWidth || Track.fTexSlope != next->fTexSlope)
|
||||
Track.iTrapezoid |= 2;
|
||||
}
|
||||
}
|
||||
|
||||
void editor_track::bind_switch_path(TTrack &Switch, int const Path)
|
||||
{
|
||||
auto &extension{*Switch.SwitchExtension};
|
||||
auto const path{Path & 1};
|
||||
Switch.Segment = extension.Segments[path];
|
||||
Switch.trNext = extension.pNexts[path];
|
||||
Switch.trPrev = extension.pPrevs[path];
|
||||
Switch.iNextDirection = extension.iNextDirection[path];
|
||||
Switch.iPrevDirection = extension.iPrevDirection[path];
|
||||
}
|
||||
|
||||
void editor_track::store_switch_path(TTrack &Switch, int const Path)
|
||||
{
|
||||
// same as TTrack::SetConnections(), without switching back to the base position
|
||||
auto &extension{*Switch.SwitchExtension};
|
||||
auto const path{Path & 1};
|
||||
extension.pNexts[path] = Switch.trNext;
|
||||
extension.pPrevs[path] = Switch.trPrev;
|
||||
extension.iNextDirection[path] = Switch.iNextDirection;
|
||||
extension.iPrevDirection[path] = Switch.iPrevDirection;
|
||||
if (Switch.eType == tt_Switch)
|
||||
{
|
||||
// switch paths share their start point, so they share the path connected there as well
|
||||
extension.pPrevs[path ^ 1] = Switch.trPrev;
|
||||
extension.iPrevDirection[path ^ 1] = Switch.iPrevDirection;
|
||||
}
|
||||
}
|
||||
|
||||
void editor_track::rebuild_geometry(TTrack &Track)
|
||||
{
|
||||
auto §ion{simulation::Region->section(Track.location())};
|
||||
// sections which weren't displayed yet create geometry of their content on their own
|
||||
Track.rebuild_geometry(section.m_geometrycreated ? section.m_geometrybank : gfx::geometrybank_handle{});
|
||||
}
|
||||
136
editor/editorTrack.hpp
Normal file
136
editor/editorTrack.hpp
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "world/Segment.h"
|
||||
#include "utilities/Classes.h"
|
||||
|
||||
class TTrack;
|
||||
|
||||
// Editing of paths (tracks, roads, rivers) loaded from the scenery.
|
||||
//
|
||||
// The source of truth for path geometry is TTrack::m_paths (the same data the scenery export writes).
|
||||
// Edits modify m_paths and then commit() rebuilds everything derived from it: the segments, placement
|
||||
// in the scene cells, connections with neighbouring paths and the render geometry.
|
||||
class editor_track
|
||||
{
|
||||
public:
|
||||
// identifies an editable point of a path
|
||||
enum class point_kind { start, control1, control2, end };
|
||||
struct point_ref
|
||||
{
|
||||
int path{-1}; // index into TTrack::m_paths
|
||||
point_kind kind{point_kind::start};
|
||||
bool valid() const { return path >= 0; }
|
||||
bool operator==(point_ref const &Other) const { return path == Other.path && kind == Other.kind; }
|
||||
};
|
||||
|
||||
// snapshot of the editable state of a path, used by the undo history
|
||||
struct state
|
||||
{
|
||||
std::vector<segment_data> paths;
|
||||
double velocity{-1.0};
|
||||
float switchvelocity{-1.f};
|
||||
float width{1.435f};
|
||||
float friction{0.15f};
|
||||
float sounddistance{-1.f};
|
||||
int quality{20};
|
||||
int damage{0};
|
||||
int environment{0};
|
||||
material_handle material1{0};
|
||||
material_handle material2{0};
|
||||
material_handle material3{0};
|
||||
float texlength{4.f};
|
||||
float texheight{0.6f};
|
||||
float texwidth{0.9f};
|
||||
float texslope{0.9f};
|
||||
};
|
||||
static state capture(TTrack const &Track);
|
||||
// restores provided state; follow with commit() to rebuild the path
|
||||
static void apply(TTrack &Track, state const &State);
|
||||
|
||||
// checks whether geometry of the path can be modified. returns: true if it can, otherwise false and the reason
|
||||
static bool can_edit_geometry(TTrack const &Track, std::string &Reason);
|
||||
// checks whether the path type is supported by the editor at all
|
||||
static bool is_supported(TTrack const &Track);
|
||||
|
||||
// rebuilds provided paths after modification of their source data (m_paths): segments, placement in the region,
|
||||
// connections with neighbours and render geometry of the paths and all their old and new neighbours
|
||||
static void commit(std::vector<TTrack *> const &Tracks);
|
||||
// rebuilds the path after change of parameters which don't affect its course (width, textures, etc)
|
||||
static void commit_parameters(TTrack &Track);
|
||||
|
||||
// world position of specified point. for control points it's the absolute position of the handle;
|
||||
// a zero (straight) control vector is presented at 1/3 of the chord so it can be grabbed
|
||||
static glm::dvec3 point_position(TTrack const &Track, point_ref const &Point);
|
||||
// moves specified point to provided world position. moving an end point carries its control vector along.
|
||||
// switches keep the start points of both their paths together
|
||||
static void move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position);
|
||||
// moves the whole path by provided offset
|
||||
static void translate(TTrack &Track, glm::dvec3 const &Offset);
|
||||
// rotates the whole path around vertical axis going through provided pivot point
|
||||
static void rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle);
|
||||
// pivot used for rotation of the whole path
|
||||
static glm::dvec3 pivot(TTrack const &Track);
|
||||
|
||||
// lists paths currently connected to provided path
|
||||
static std::vector<TTrack *> neighbours(TTrack const &Track);
|
||||
// lists end points of other paths connected to the end point located at provided position,
|
||||
// as pairs of the neighbour and its matching point
|
||||
static std::vector<std::pair<TTrack *, point_ref>> connected_points(TTrack const &Track, glm::dvec3 const &Position);
|
||||
// checks whether end point located at provided position is connected to another path
|
||||
static bool is_connected(TTrack const &Track, point_ref const &Point);
|
||||
|
||||
// candidate to connect a dragged end point with
|
||||
struct snap_target
|
||||
{
|
||||
TTrack *track{nullptr};
|
||||
point_ref point;
|
||||
glm::dvec3 position{0.0};
|
||||
glm::dvec3 direction{0.0}; // direction from the point into the target path
|
||||
double distance{0.0};
|
||||
};
|
||||
// finds the nearest free end point of another path of the same category as provided path, within specified radius.
|
||||
// paths listed in Exclude are skipped, and connections to them don't count as occupying an end point
|
||||
static snap_target find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector<TTrack const *> const &Exclude);
|
||||
// places the point at the target and optionally aligns the control vector with the target path
|
||||
static void snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent);
|
||||
|
||||
// names of the materials used by the path; "none" for missing ones
|
||||
static std::string material_name(material_handle const Material);
|
||||
static material_handle fetch_material(std::string const &Name);
|
||||
|
||||
// height of the trackbed edge as written in the scenery (without roll fix adjustment)
|
||||
static float texture_height(TTrack const &Track);
|
||||
static void texture_height(TTrack &Track, float const Height);
|
||||
static double velocity(TTrack const &Track);
|
||||
static void velocity(TTrack &Track, double const Velocity);
|
||||
static void damage(TTrack &Track, int const Damage);
|
||||
|
||||
private:
|
||||
// unlinks the path from all its neighbours
|
||||
static void disconnect(TTrack &Track);
|
||||
// links the path with paths whose free end points match its own free end points
|
||||
static void join(TTrack &Track);
|
||||
// links specified end of a regular path with matching end of another path
|
||||
static void connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid);
|
||||
// recalculates the trackbed transition flag
|
||||
static void update_transition(TTrack &Track);
|
||||
// sets active connections of a switch to specified path, without moving the blades
|
||||
static void bind_switch_path(TTrack &Switch, int const Path);
|
||||
static void store_switch_path(TTrack &Switch, int const Path);
|
||||
// rebuilds render geometry of the path in the bank of the section it's placed in
|
||||
static void rebuild_geometry(TTrack &Track);
|
||||
};
|
||||
@@ -709,6 +709,8 @@ void TTrack::init_segments( bool const Initial ) {
|
||||
// so repeated initialization doesn't accumulate it
|
||||
fTexHeight1 -= fTexHeightOffset;
|
||||
fTexHeightOffset = 0.f;
|
||||
// the roll flag is re-evaluated below, the transition flag is set again when the path gets connected
|
||||
iTrapezoid = 0;
|
||||
|
||||
switch (eType) {
|
||||
// Ra: łuki segmentowane co 5m albo 314-kątem foremnym
|
||||
@@ -1355,10 +1357,9 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const
|
||||
// wypełnianie tablic VBO
|
||||
// discards references to current render geometry and generates it anew in specified bank
|
||||
// NOTE: geometry banks can't release chunks, so the old data stays in the bank, unused
|
||||
// with null bank the geometry is only discarded, to be created later along with the rest of the owning section
|
||||
void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) {
|
||||
|
||||
if( Bank == null_handle ) { return; }
|
||||
|
||||
Geometry1.clear();
|
||||
Geometry2.clear();
|
||||
if( SwitchExtension ) {
|
||||
@@ -1366,6 +1367,8 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) {
|
||||
}
|
||||
m_sleeper_local_transforms.clear();
|
||||
|
||||
if( Bank == null_handle ) { return; }
|
||||
|
||||
create_geometry( Bank );
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user