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

feat(editor): insert switches by dragging, paths from free ends

This commit is contained in:
maj00r
2026-10-02 23:58:48 +02:00
parent 32798a32bb
commit 1985758a86
5 changed files with 415 additions and 3 deletions

View File

@@ -1583,6 +1583,7 @@ void editor_mode::render_straights_ui()
}
if (false == state.status.empty())
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str());
render_switch_ui();
if (false == state.set.empty())
{
@@ -2091,8 +2092,14 @@ void editor_mode::delete_selected_track()
void editor_mode::draw_track_hints()
{
std::string hint;
if (m_track_tab == track_tab::straights)
hint = current_straight().tracks.empty() ? "LMB: select a straight or a curve" : "Drag the ends or the middle of the straight Ctrl+drag: break Shift+drag: S-curve Alt+click: add to the set Del: delete path";
if (m_extend.active)
hint = "Drag: curve through the cursor, along the end: straight Ctrl: straight";
else if (m_switch.placing)
hint = "Drag along the straight to set the direction of the switch, sideways to set its side";
else if (m_switch.armed >= 0 && m_track_tab == track_tab::straights)
hint = "Press on the selected straight where the switch starts and drag";
else if (m_track_tab == track_tab::straights)
hint = current_straight().tracks.empty() ? "LMB: select a straight or a curve" : "Drag the ends or the middle Ctrl+drag: break Shift+drag: S-curve Alt+click: add to the set Drag from a red ring: new path Del: delete";
else if (m_track_tab == track_tab::route)
hint = m_route.chain.tracks.empty() ? "LMB: select a curve" : "Drag the vertex (yellow) or the radius grip (green), the change is applied on release Ctrl+Z: undo";
else
@@ -2105,3 +2112,333 @@ void editor_mode::draw_track_hints()
drawlist->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 150), 4.0f);
drawlist->AddText(position, IM_COL32(255, 255, 255, 230), hint.c_str());
}
void editor_mode::update_build_tools()
{
glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()};
if (m_extend.active)
m_extend.mouse = ground;
if (m_switch.placing)
m_switch.mouse = ground;
}
bool editor_mode::start_extend()
{
auto *track{selected_track()};
if (track == nullptr || track->eType != tt_Normal || false == editor_track::is_supported(*track) || false == track->Dynamics.empty())
return false;
screen_projection const projection;
ImVec2 const mouse = ImGui::GetIO().MousePos;
auto const &path{track->m_paths.front()};
for (auto const atend : {false, true})
{
editor_track::point_ref const point{0, atend ? editor_track::point_kind::end : editor_track::point_kind::start};
if (editor_track::is_connected(*track, point))
continue;
auto const position{editor_track::point_position(*track, point)};
ImVec2 screen;
if (false == projection.project(position, screen))
continue;
if ((screen.x - mouse.x) * (screen.x - mouse.x) + (screen.y - mouse.y) * (screen.y - mouse.y) > kHandleRadius * kHandleRadius * 1.5f)
continue;
auto const &start{path.points[segment_data::point::start]};
auto const &end{path.points[segment_data::point::end]};
auto const &control1{path.points[segment_data::point::control1]};
auto const &control2{path.points[segment_data::point::control2]};
glm::dvec3 tangent;
if (atend)
tangent = control2 != glm::dvec3{} ? -control2 : end - (start + control1);
else
tangent = -(control1 != glm::dvec3{} ? control1 : (end + control2) - start);
glm::dvec2 const planar{tangent.x, tangent.z};
if (glm::length(planar) < 1e-6)
return false;
m_extend.active = true;
m_extend.track = track;
m_extend.point = position;
m_extend.direction = glm::normalize(planar);
m_extend.grade = tangent.y / glm::length(planar);
m_extend.mouse = position;
return true;
}
return false;
}
std::vector<segment_data> editor_mode::extend_pieces() const
{
std::vector<segment_data> result;
auto const &tool{m_extend};
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
auto const chord{glm::length(offset)};
if (chord < 0.5)
return result;
auto const &direction{tool.direction};
auto const along{glm::dot(offset, direction)};
auto const lateral{direction.x * offset.y - direction.y * offset.x};
auto const height = [&](double const Distance) { return tool.point.y + tool.grade * Distance; };
if (ImGui::GetIO().KeyCtrl || std::abs(lateral) < 0.005 * chord + 0.05)
{
if (along < 0.5)
return result;
segment_data path;
path.points[segment_data::point::start] = tool.point;
path.points[segment_data::point::end] = {tool.point.x + direction.x * along, height(along), tool.point.z + direction.y * along};
result.push_back(path);
return result;
}
auto const angle{2.0 * std::atan2(std::abs(lateral), along)};
auto const radius{chord / (2.0 * std::sin(angle * 0.5))};
double const side{lateral > 0.0 ? 1.0 : -1.0};
glm::dvec2 const centre{glm::dvec2{tool.point.x, tool.point.z} + glm::dvec2{-direction.y, direction.x} * (side * radius)};
auto const count{std::max(1, static_cast<int>(std::ceil(angle / glm::radians(10.0) - 1e-9)))};
auto const step{angle / count};
auto const handle{4.0 / 3.0 * std::tan(step / 4.0) * radius};
auto const turn = [&](glm::dvec2 const &Vector, double const Angle) {
auto const c{std::cos(Angle)};
auto const s{std::sin(Angle)};
return glm::dvec2{Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c};
};
glm::dvec2 const radial{glm::dvec2{tool.point.x, tool.point.z} - centre};
for (int i = 0; i < count; ++i)
{
auto const a0{side * step * i};
auto const a1{side * step * (i + 1)};
auto const p0{centre + turn(radial, a0)};
auto const p3{centre + turn(radial, a1)};
auto const d0{turn(direction, a0)};
auto const d3{turn(direction, a1)};
auto const y0{height(radius * step * i)};
auto const y3{height(radius * step * (i + 1))};
segment_data path;
path.points[segment_data::point::start] = {p0.x, y0, p0.y};
path.points[segment_data::point::end] = {p3.x, y3, p3.y};
path.points[segment_data::point::control1] = {d0.x * handle, tool.grade * handle, d0.y * handle};
path.points[segment_data::point::control2] = {-d3.x * handle, -tool.grade * handle, -d3.y * handle};
path.radius = static_cast<float>(radius);
result.push_back(path);
}
return result;
}
void editor_mode::finish_extend()
{
auto const pieces{extend_pieces()};
auto *style{m_extend.track};
m_extend.active = false;
if (pieces.empty() || style == nullptr)
return;
std::vector<TTrack *> created;
for (auto const &piece : pieces)
created.push_back(editor_track::create_path(*style, piece));
editor_track::commit(created);
push_track_snapshot({}, created);
m_node = created.back();
ui()->set_node(m_node);
straight_refresh();
}
std::vector<segment_data> editor_mode::switch_preview() const
{
auto const &tool{m_switch};
if (false == tool.placing || tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()))
return {};
auto const &line{tool.line};
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
auto const forward{glm::dot(offset, line.direction) >= 0.0 ? 1 : -1};
glm::dvec2 const direction{line.direction * static_cast<double>(forward)};
auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1};
return editor_track::place_switch(tool.templates[tool.armed], tool.point, direction, side, line.grade * forward);
}
bool editor_mode::start_switch_placement()
{
auto &tool{m_switch};
if (tool.armed < 0 || m_track_tab != track_tab::straights)
return false;
auto const &line{current_straight()};
if (line.tracks.empty())
return false;
glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()};
tool.along = std::clamp(glm::dot(glm::dvec2{ground.x - line.start.x, ground.z - line.start.z}, line.direction), 0.0, line.length);
tool.point = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * tool.along;
tool.mouse = ground;
tool.line = line;
tool.placing = true;
return true;
}
void editor_mode::finish_switch_placement()
{
auto &tool{m_switch};
tool.placing = false;
if (tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()))
return;
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
auto const forward{glm::dot(offset, tool.line.direction) >= 0.0 ? 1 : -1};
glm::dvec2 const direction{tool.line.direction * static_cast<double>(forward)};
auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1};
insert_switch(tool.line, tool.along, forward, side);
}
void editor_mode::insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side)
{
auto &state{m_straights};
state.status.clear();
auto const &shape{m_switch.templates[m_switch.armed]};
auto const from{Direction > 0 ? Along : Along - shape.length};
auto const to{from + shape.length};
if (from < -0.01 || to > Line.length + 0.01)
{
state.status = "The switch doesn't fit on the straight here";
return;
}
struct span
{
TTrack *track;
int path;
double from, to;
bool forward;
};
auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - Line.start.x, Point.z - Line.start.z}, Line.direction); };
std::vector<span> spans;
for (std::size_t i = 0; i < Line.tracks.size(); ++i)
{
auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]};
auto const a{along(path.points[segment_data::point::start])};
auto const b{along(path.points[segment_data::point::end])};
spans.push_back({Line.tracks[i], Line.paths[i], std::min(a, b), std::max(a, b), a < b});
}
std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; });
int first{-1}, last{-1};
for (int i = 0; i < static_cast<int>(spans.size()); ++i)
{
if (spans[i].to > from + 1e-3 && spans[i].from < to - 1e-3)
{
if (first < 0)
first = i;
last = i;
}
}
if (first < 0)
return;
editor_track::chain chain;
for (int i = first; i <= last; ++i)
{
if (spans[i].track->eType != tt_Normal || false == spans[i].track->Dynamics.empty())
{
state.status = "The switch would overlap another switch or a path with vehicles";
return;
}
chain.tracks.push_back(spans[i].track);
chain.forward.push_back(spans[i].forward);
}
auto const before{std::max(0.0, from - spans[first].from)};
auto const after{std::max(0.0, spans[last].to - to)};
bool const hasbefore{before > 0.01};
bool const hasafter{after > 0.01};
if (false == hasbefore && false == hasafter)
{
state.status = "The switch would replace the paths completely, place it so that a piece of the straight remains";
return;
}
auto const count{std::max<std::size_t>(chain.tracks.size(), (hasbefore ? 1 : 0) + (hasafter ? 1 : 0))};
std::size_t countbefore{0};
if (hasbefore && hasafter)
countbefore = std::clamp<std::size_t>(static_cast<std::size_t>(std::round(count * before / (before + after))), 1, count - 1);
else if (hasbefore)
countbefore = count;
auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; };
std::vector<segment_data> pieces;
auto const straight = [&](double const From, double const To, std::size_t const Count) {
for (std::size_t i = 0; i < Count; ++i)
{
segment_data path;
path.points[segment_data::point::start] = point(From + (To - From) * i / Count);
path.points[segment_data::point::end] = point(From + (To - From) * (i + 1) / Count);
pieces.push_back(path);
}
};
if (hasbefore)
straight(spans[first].from, from, countbefore);
if (hasafter)
straight(to, spans[last].to, count - countbefore);
std::vector<std::pair<TTrack *, editor_track::state>> states;
for (auto *track : chain.tracks)
states.emplace_back(track, editor_track::capture(*track));
auto created{editor_track::relay(chain, pieces)};
glm::dvec2 const direction{Line.direction * static_cast<double>(Direction)};
auto const paths{editor_track::place_switch(shape, point(Along), direction, Side, Line.grade * Direction)};
auto *track{editor_track::create_switch(shape, paths, *spans[first].track)};
if (track != nullptr)
{
editor_track::commit({track});
created.push_back(track);
}
push_track_snapshot(std::move(states), std::move(created));
straight_refresh();
state.status = "Switch inserted";
}
void editor_mode::draw_build_overlay() const
{
screen_projection const projection;
ImDrawList *drawlist = ImGui::GetBackgroundDrawList();
auto const drawpath = [&](segment_data const &Path, ImU32 const Color) {
auto previous{path_point(Path, 0.0)};
for (int i = 1; i <= 24; ++i)
{
auto const next{path_point(Path, i / 24.0)};
projection.line(drawlist, previous, next, Color, 3.0f);
previous = next;
}
};
if (m_extend.active)
for (auto const &piece : extend_pieces())
drawpath(piece, IM_COL32(255, 210, 60, 255));
if (m_switch.placing)
for (auto const &piece : switch_preview())
drawpath(piece, IM_COL32(255, 210, 60, 255));
auto const *track{selected_track()};
if (track == nullptr || track->eType != tt_Normal || m_extend.active)
return;
for (auto const atend : {false, true})
{
editor_track::point_ref const point{0, atend ? editor_track::point_kind::end : editor_track::point_kind::start};
if (editor_track::is_connected(*track, point))
continue;
ImVec2 screen;
if (projection.project(editor_track::point_position(*track, point), screen))
drawlist->AddCircle(screen, 10.0f, IM_COL32(240, 60, 60, 255), 16, 3.0f);
}
}
void editor_mode::render_switch_ui()
{
auto &tool{m_switch};
if (false == ImGui::CollapsingHeader("Switches", ImGuiTreeNodeFlags_DefaultOpen))
return;
if (false == tool.collected)
{
tool.templates = editor_track::standard_switch_templates();
auto const found{editor_track::find_switch_templates()};
tool.templates.insert(tool.templates.end(), found.begin(), found.end());
tool.collected = true;
}
ImGui::TextDisabled(tool.armed >= 0 ? "Press on the straight where the switch starts and drag: along sets its direction, sideways its side" : "Click a template, then drag on the selected straight");
ImGui::BeginChild("##switchtemplates", ImVec2(0.0f, 150.0f), true);
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
{
auto const &entry{tool.templates[i]};
auto label{entry.source != nullptr ? "Scenery: " + entry.label + " (" + std::to_string(entry.count) + "x)" : "PLK: " + entry.label};
label += "##switch" + std::to_string(i);
if (ImGui::Selectable(label.c_str(), tool.armed == i))
tool.armed = (tool.armed == i ? -1 : i);
}
ImGui::EndChild();
if (ImGui::SmallButton("Collect again from the scenery"))
{
tool.collected = false;
tool.armed = -1;
}
}