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

feat(editor): track gizmo turns, lengthens and edits the control vectors, joined tracks keep the tangent

W turns the track (about its middle or the selected end) and a selection of tracks, the joined tracks follow.
E lengthens the track at its free end: a straight stays straight, an arc keeps its radius, cant and grade.
The gizmo on a control point moves, turns or lengthens the vector; the control point of the track joined
there turns with it, at a switch the vector only slides along the switch. Straight neighbours dragged along
keep their tangents and become straight again when nothing bent them.
This commit is contained in:
maj00r
2026-10-11 12:59:55 +02:00
parent 2beae15390
commit 89678ead3e
8 changed files with 782 additions and 99 deletions

View File

@@ -262,6 +262,137 @@ void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex)
Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn});
}
std::vector<segment_data> stretched(segment_data const &Path, bool const Atend, double const Length)
{
auto const path{Atend ? Path : turned_around(Path)};
bezier const curve{path};
auto const &start{path.points[segment_data::point::start]};
auto const &end{path.points[segment_data::point::end]};
auto const first{plan_of(curve.first(0.0))};
auto const last{plan_of(curve.first(1.0))};
if (glm::length(first) < 1e-9 || glm::length(last) < 1e-9)
return {};
glm::dvec2 const direction{glm::normalize(first)};
auto const grade{grade_of(curve.first(0.0))};
auto const original{curve.plan_length()};
auto const turn{signed_angle(direction, glm::normalize(last))};
auto const lateral{cross(direction, plan_of(end - start))};
std::vector<segment_data> pieces;
float radius{0.0f};
if (std::abs(turn) < 1e-4 && std::abs(lateral) < 0.01)
pieces = straight_piece(start, direction, grade, Length);
else
{
auto const arcradius{original / std::abs(turn)};
int const side{turn > 0.0 ? 1 : -1};
auto const arc{arc_pieces(start, direction, grade, arcradius, std::abs(turn), side)};
if (std::abs(turn) > 1e-4 && plan_distance(arc.back().points[segment_data::point::end], end) < 0.05 && plan_distance(arc.front().points[segment_data::point::start], start) < 1e-6)
{
pieces = arc_pieces(start, direction, grade, arcradius, std::min(Length / arcradius, glm::radians(330.0)), side);
radius = path.radius;
}
else
{
auto const scale{Length / std::max(original, 1e-6)};
segment_data piece{path};
piece.points[segment_data::point::end] = start + (end - start) * scale;
piece.points[segment_data::point::control1] *= scale;
piece.points[segment_data::point::control2] *= scale;
piece.radius *= static_cast<float>(scale);
return {Atend ? piece : turned_around(piece)};
}
}
double chainage{0.0};
for (auto &piece : pieces)
{
auto const piecelength{bezier{piece}.plan_length()};
auto const from{static_cast<float>(std::clamp(chainage / original, 0.0, 1.0))};
auto const to{static_cast<float>(std::clamp((chainage + piecelength) / original, 0.0, 1.0))};
piece.rolls = {glm::mix(path.rolls[0], path.rolls[1], from), glm::mix(path.rolls[0], path.rolls[1], to)};
piece.radius = radius;
chainage += piecelength;
}
if (false == Atend)
{
std::reverse(pieces.begin(), pieces.end());
for (auto &piece : pieces)
piece = turned_around(piece);
}
return pieces;
}
bool stretch_end(TTrack const &Track, editor_track::point_ref const &Point, int &Path, bool &Atend)
{
if (Track.eType != tt_Normal || false == editor_track::is_supported(Track) || Track.m_paths.empty())
return false;
auto const last{static_cast<int>(Track.m_paths.size()) - 1};
if (Point.valid())
{
if (false == editor_track::is_end(Point.kind) || Point.path > last)
return false;
Path = Point.path;
Atend = Point.kind == editor_track::point_kind::end;
auto const position{editor_track::point_position(Track, Point)};
for (int i = 0; i <= last; ++i)
if (i != Path && (plan_distance(Track.m_paths[i].points[segment_data::point::start], position) < geometry::same_point || plan_distance(Track.m_paths[i].points[segment_data::point::end], position) < geometry::same_point))
return false;
return true;
}
bool const startfree{false == editor_track::is_connected(Track, {0, editor_track::point_kind::start})};
bool const endfree{false == editor_track::is_connected(Track, {last, editor_track::point_kind::end})};
Atend = endfree || false == startfree;
Path = Atend ? last : 0;
return true;
}
std::pair<glm::dvec3, glm::dvec3> stretch_axis(segment_data const &Path, bool const Atend)
{
bezier const curve{Path};
auto const tangent{Atend ? curve.first(1.0) : -curve.first(0.0)};
return {Path.points[Atend ? segment_data::point::end : segment_data::point::start], glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{}};
}
bool mouse_along(glm::dvec3 const &Origin, glm::dvec3 const &Axis, double &Along)
{
screen_projection const projection;
glm::dvec3 origin, direction;
projection.ray(ImGui::GetIO().MousePos, origin, direction);
auto const b{glm::dot(Axis, direction)};
auto const denominator{1.0 - b * b};
if (denominator < 1e-6)
return false;
auto const w{Origin - origin};
Along = (b * glm::dot(direction, w) - glm::dot(Axis, w)) / denominator;
return true;
}
glm::mat4 gizmo_along(glm::dvec3 const &Position, glm::dvec3 const &Axis)
{
auto gizmo{glm::translate(glm::mat4(1.0f), glm::vec3(Position))};
if (glm::length(Axis) < 1e-9)
return gizmo;
auto const z{glm::normalize(Axis)};
auto x{glm::cross(glm::dvec3{0.0, 1.0, 0.0}, z)};
x = glm::length(x) > 1e-6 ? glm::normalize(x) : glm::dvec3{1.0, 0.0, 0.0};
auto const y{glm::cross(z, x)};
gizmo[0] = glm::vec4(glm::vec3(x), 0.0f);
gizmo[1] = glm::vec4(glm::vec3(y), 0.0f);
gizmo[2] = glm::vec4(glm::vec3(z), 0.0f);
return gizmo;
}
editor_track::point_ref stretch_remap(editor_track::point_ref Point, int const Path, std::size_t const Pieces)
{
if (false == Point.valid() || Point.path < Path)
return Point;
auto const extra{static_cast<int>(Pieces) - 1};
if (Point.path > Path)
Point.path += extra;
else if (Point.kind == editor_track::point_kind::end || Point.kind == editor_track::point_kind::control2)
Point.path += extra;
return Point;
}
}
TTrack *editor_mode::selected_track() const
@@ -395,6 +526,214 @@ void editor_mode::draw_track_overlay() const
}
}
editor_mode::gizmo_operation editor_mode::track_gizmo_operation(TTrack const &Track, bool const Pointmode) const
{
if (m_gizmo_op == gizmo_operation::translate || (Pointmode && false == editor_track::is_end(m_track_point.kind)))
return m_gizmo_op;
if (m_gizmo_op == gizmo_operation::rotate)
return gizmo_operation::rotate;
int path{0};
bool atend{true};
return stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{}, path, atend) ? gizmo_operation::scale : gizmo_operation::translate;
}
glm::mat4 editor_mode::track_gizmo_frame(TTrack const &Track, bool const Pointmode, gizmo_operation const Operation) const
{
auto anchor{Pointmode ? editor_track::point_position(Track, m_track_point) : editor_track::pivot(Track)};
glm::dvec3 axis{0.0};
if (Operation == gizmo_operation::scale)
{
int path{0};
bool atend{true};
if (Pointmode && false == editor_track::is_end(m_track_point.kind))
axis = anchor - editor_track::point_position(Track, editor_track::end_of(m_track_point));
else if (stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{}, path, atend))
std::tie(anchor, axis) = stretch_axis(Track.m_paths[path], atend);
}
return gizmo_along(anchor - glm::dvec3{GfxRenderer->Camera_Position()}, axis);
}
void editor_mode::track_gizmo_hold()
{
auto &drag{m_track_gizmo_drag};
drag.held.clear();
for (auto *track : m_track_drag)
{
if (drag.operation == gizmo_operation::scale && false == drag.control && track == selected_track())
continue;
auto held{editor_track::hold_controls(*track)};
if (false == held.empty())
drag.held.emplace_back(track, std::move(held));
}
}
void editor_mode::track_gizmo_settle()
{
for (auto &entry : m_track_gizmo_drag.held)
editor_track::settle_controls(*entry.first, entry.second);
m_track_gizmo_drag.held.clear();
}
void editor_mode::track_gizmo_stretch(TTrack &Track, double const Length)
{
auto &drag{m_track_gizmo_drag};
auto const pieces{stretched(drag.paths[drag.path], drag.atend, Length)};
if (pieces.empty())
return;
auto paths{drag.paths};
paths.erase(paths.begin() + drag.path);
paths.insert(paths.begin() + drag.path, pieces.begin(), pieces.end());
Track.m_paths = std::move(paths);
drag.pieces = pieces.size();
drag.stretch = Length;
if (drag.point.valid())
m_track_point = stretch_remap(drag.point, drag.path, drag.pieces);
auto joints{m_track_joints};
for (auto &joint : joints)
joint.own = stretch_remap(joint.own, drag.path, drag.pieces);
editor_track::follow(Track, joints);
m_track_dirty = true;
}
void editor_mode::track_gizmo_control(TTrack &Track, glm::dvec3 Control)
{
auto const &drag{m_track_gizmo_drag};
if (drag.fixed != glm::dvec3{})
Control = drag.fixed * std::max(0.5, glm::dot(Control, drag.fixed));
if (glm::length(Control) < 0.5)
Control = glm::length(Control) > 1e-9 ? glm::normalize(Control) * 0.5 : drag.axis * 0.5;
editor_track::move_point(Track, drag.point, editor_track::point_position(Track, editor_track::end_of(drag.point)) + Control);
editor_track::align_controls(Track, drag.point, drag.partners);
m_track_dirty = true;
}
void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation)
{
bool const controlmode{Pointmode && false == editor_track::is_end(m_track_point.kind)};
auto &drag{m_track_gizmo_drag};
m_track_gizmo_using = true;
m_track_pivot = editor_track::pivot(Track);
m_track_drag = {&Track};
m_track_drag_points.clear();
m_track_joints.clear();
m_track_snap = {};
drag = {};
drag.operation = Operation;
drag.control = controlmode;
drag.point = Pointmode ? m_track_point : editor_track::point_ref{};
bool const whole{false == Pointmode || (false == controlmode && Operation != gizmo_operation::translate)};
if (whole && m_track_drag_connected)
{
m_track_joints = editor_track::joints(Track);
for (auto const &joint : m_track_joints)
if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end())
m_track_drag.emplace_back(joint.other);
}
if (Pointmode && false == controlmode && Operation == gizmo_operation::translate && m_track_drag_connected)
{
for (auto const &connection : editor_track::connected_points(Track, editor_track::point_position(Track, m_track_point)))
{
std::string reason;
if (false == editor_track::can_edit_geometry(*connection.first, reason))
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);
}
}
if (controlmode)
{
for (auto const &partner : editor_track::partners(Track, editor_track::end_of(m_track_point)))
{
std::string reason;
auto const &path{partner.first->m_paths[partner.second.path]};
if (partner.first->eType != tt_Normal || false == editor_track::can_edit_geometry(*partner.first, reason))
{
bool const start{partner.second.kind == editor_track::point_kind::start};
auto const theirs{path.points[start ? segment_data::point::control1 : segment_data::point::control2]};
auto const chord{path.points[start ? segment_data::point::end : segment_data::point::start] - path.points[start ? segment_data::point::start : segment_data::point::end]};
auto const along{theirs != glm::dvec3{} ? theirs : chord};
if (glm::length(along) > 1e-9)
drag.fixed = -glm::normalize(along);
continue;
}
if (false == m_track_drag_connected && partner.first != &Track)
continue;
drag.partners.push_back(partner);
if (std::find(m_track_drag.begin(), m_track_drag.end(), partner.first) == m_track_drag.end())
m_track_drag.emplace_back(partner.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));
track_gizmo_hold();
if (controlmode)
{
drag.origin = editor_track::point_position(Track, m_track_point);
auto const control{drag.origin - editor_track::point_position(Track, editor_track::end_of(m_track_point))};
drag.length = glm::length(control);
drag.axis = drag.length > 1e-9 ? control / drag.length : glm::dvec3{0.0, 0.0, 1.0};
}
else if (Operation == gizmo_operation::scale)
{
stretch_end(Track, drag.point, drag.path, drag.atend);
drag.paths = Track.m_paths;
std::tie(drag.origin, drag.axis) = stretch_axis(Track.m_paths[drag.path], drag.atend);
drag.length = bezier{Track.m_paths[drag.path]}.plan_length();
}
drag.stretch = drag.length;
}
void editor_mode::track_gizmo_turn(TTrack &Track, double const Angle)
{
auto &drag{m_track_gizmo_drag};
auto const step{std::remainder(Angle - drag.angle, 2.0 * glm::pi<double>())};
if (std::abs(step) < 1e-9 || (drag.control && drag.fixed != glm::dvec3{}))
return;
drag.angle += step;
if (drag.control)
{
auto const control{editor_track::point_position(Track, drag.point) - editor_track::point_position(Track, editor_track::end_of(drag.point))};
auto const plan{turned(plan_of(control), -step)};
track_gizmo_control(Track, {plan.x, control.y, plan.y});
return;
}
editor_track::rotate_y(Track, drag.point.valid() ? editor_track::point_position(Track, drag.point) : m_track_pivot, step);
editor_track::follow(Track, m_track_joints);
m_track_dirty = true;
}
void editor_mode::track_gizmo_length(TTrack &Track, double Length)
{
auto &drag{m_track_gizmo_drag};
Length = std::max(drag.control ? 0.5 : 1.0, Length);
if (std::abs(Length - drag.stretch) < 1e-6)
return;
if (drag.control)
{
drag.stretch = Length;
track_gizmo_control(Track, drag.axis * Length);
}
else if (drag.path >= 0)
track_gizmo_stretch(Track, Length);
}
void editor_mode::track_gizmo_end(TTrack &Track)
{
if (m_track_snap.track != nullptr && m_track_gizmo_drag.operation == gizmo_operation::translate && m_track_point.valid())
editor_track::snap_point(Track, m_track_point, m_track_snap, m_track_align_tangent);
track_gizmo_settle();
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();
m_track_joints.clear();
}
void editor_mode::render_track_gizmo()
{
auto *track = selected_track();
@@ -405,54 +744,49 @@ void editor_mode::render_track_gizmo()
return;
}
bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast<int>(track->m_paths.size());
bool const controlmode = pointmode && false == editor_track::is_end(m_track_point.kind);
gizmo_frame const frame;
auto &drag{m_track_gizmo_drag};
auto const operation{m_track_gizmo_using ? drag.operation : track_gizmo_operation(*track, pointmode)};
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 - frame.camera));
}
m_track_gizmo = track_gizmo_frame(*track, pointmode, operation);
glm::mat4 delta(1.0f);
frame.manipulate(ImGuizmo::TRANSLATE, m_track_gizmo, m_gizmo_snap, &delta);
auto const gizmo{operation == gizmo_operation::rotate ? ImGuizmo::ROTATE_Y : operation == gizmo_operation::scale ? ImGuizmo::SCALE_Z : ImGuizmo::TRANSLATE};
ImGuizmo::AllowAxisFlip(operation != gizmo_operation::scale);
frame.manipulate(gizmo, m_track_gizmo, operation == gizmo_operation::rotate ? 5.0f : operation == gizmo_operation::scale ? 0.0f : m_gizmo_snap, &delta, operation == gizmo_operation::scale ? ImGuizmo::LOCAL : ImGuizmo::WORLD);
ImGuizmo::AllowAxisFlip(true);
if (ImGuizmo::IsUsing())
{
if (false == m_track_gizmo_using)
{
m_track_gizmo_using = true;
m_track_pivot = editor_track::pivot(*track);
m_track_drag = {track};
m_track_drag_points.clear();
m_track_joints.clear();
m_track_snap = {};
if (false == pointmode && m_track_drag_connected)
{
m_track_joints = editor_track::joints(*track);
for (auto const &joint : m_track_joints)
if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end())
m_track_drag.emplace_back(joint.other);
}
if (pointmode && editor_track::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));
track_gizmo_begin(*track, pointmode, operation);
if (operation == gizmo_operation::scale && false == mouse_along(drag.origin, drag.axis, drag.grabbed))
drag.grabbed = 0.0;
}
if (pointmode)
if (operation == gizmo_operation::rotate)
track_gizmo_turn(*track, std::atan2(static_cast<double>(m_track_gizmo[2][0]), static_cast<double>(m_track_gizmo[0][0])));
else if (operation == gizmo_operation::scale)
{
double along{0.0};
if (mouse_along(drag.origin, drag.axis, along))
{
auto length{drag.length + along - drag.grabbed};
if (Global.ctrlState && m_gizmo_snap > 0.0f)
length = std::round(length / m_gizmo_snap) * m_gizmo_snap;
track_gizmo_length(*track, length);
}
}
else if (controlmode)
{
glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]);
track_gizmo_control(*track, position - editor_track::point_position(*track, editor_track::end_of(m_track_point)));
}
else if (pointmode)
{
glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]);
editor_track::move_point(*track, m_track_point, position);
@@ -480,19 +814,7 @@ void editor_mode::render_track_gizmo()
commit_track_drag(false);
}
else if (m_track_gizmo_using)
{
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();
m_track_joints.clear();
}
track_gizmo_end(*track);
}
void editor_mode::commit_track_drag(bool const Force)
@@ -3605,12 +3927,24 @@ std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All)
hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "done"}};
else if (m_track_point.valid())
{
hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", "moves the point along the axes"}, {"Esc", "releases the point"}};
char const *action{"moves the point along the axes"};
if (false == editor_track::is_end(m_track_point.kind))
action = m_gizmo_op == gizmo_operation::rotate ? "turns the control vector, the joined track keeps the tangent" : m_gizmo_op == gizmo_operation::scale ? "length of the control vector" : "moves the control point, the joined track keeps the tangent";
else if (m_gizmo_op == gizmo_operation::rotate)
action = "turns the track about the point, the joined tracks follow";
else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, true) == gizmo_operation::scale)
action = "lengthens the track at this end, the curve keeps its radius";
hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", action}, {"Q W E", "gizmo: move, turn, lengthen"}, {"Esc", "releases the point"}};
gizmo = true;
}
else
{
hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", "moves the whole track"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"L", "new track, also from a free end"}, {"Esc", "deselects"}, {"Del", "deletes the track"}};
char const *action{"moves the whole track"};
if (m_gizmo_op == gizmo_operation::rotate)
action = "turns the track, the joined tracks follow";
else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, false) == gizmo_operation::scale)
action = "lengthens the track at its free end, the curve keeps its radius";
hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", action}, {"Q W E", "gizmo: move, turn, lengthen"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"L", "new track, also from a free end"}, {"Esc", "deselects"}, {"Del", "deletes the track"}};
gizmo = true;
}
if (auto const *meaning{typed_meaning()}; meaning != nullptr)
@@ -3736,9 +4070,31 @@ std::string editor_mode::track_readout() const
return format("L %.3f m (%+.3f) azimuth %.4f deg", plan_distance(state.preview_start, state.preview_end), plan_distance(state.preview_start, state.preview_end) - grabbed.length,
azimuth(state.preview_start, state.preview_end));
}
if (m_track_set_using && m_track_gizmo_drag.operation == gizmo_operation::rotate)
return format(STR_C("turned %+.2f deg"), glm::degrees(m_track_gizmo_drag.angle));
auto const *track{selected_track()};
if (m_track_gizmo_using && track != nullptr)
{
auto const &drag{m_track_gizmo_drag};
if (drag.control)
{
auto const control{editor_track::point_position(*track, m_track_point) - editor_track::point_position(*track, editor_track::end_of(m_track_point))};
auto text{format(STR_C("control %.2f m azimuth %.2f deg"), glm::length(control), azimuth(glm::dvec3{0.0}, control))};
if (drag.operation == gizmo_operation::rotate)
text += format(STR_C(" turned %+.2f deg"), glm::degrees(drag.angle));
if (drag.fixed != glm::dvec3{})
text += "\n" + STR("along the switch");
return text;
}
if (drag.operation == gizmo_operation::scale && drag.path >= 0)
{
auto text{format("L %.3f m (%+.3f)", drag.stretch, drag.stretch - drag.length)};
if (drag.path < static_cast<int>(drag.paths.size()) && std::abs(drag.paths[drag.path].radius) > 0.5f)
text += format(" R %.1f m", std::abs(drag.paths[drag.path].radius));
return text;
}
if (drag.operation == gizmo_operation::rotate)
return format(STR_C("turned %+.2f deg"), glm::degrees(drag.angle));
if (m_track_point.valid() && m_track_point.path < static_cast<int>(track->m_paths.size()))
{
auto const &path{track->m_paths[m_track_point.path]};