diff --git a/application/editormode.cpp b/application/editormode.cpp index 31b78e82..35fcea4f 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1591,6 +1591,14 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; } + // while the plan tool is up the left button belongs to it: it lays and moves the points of the + // track layout, so it must not also pick or place scenery nodes + if (Global.editor_ortho && Button == GLFW_MOUSE_BUTTON_LEFT) + { + m_input.mouse.button(Button, Action); + return; + } + // in chunk-edit mode the left button adds a neighbouring chunk (Shift = delete the clicked one) if (m_chunk_edit && Button == GLFW_MOUSE_BUTTON_LEFT) { diff --git a/application/planpanel.cpp b/application/planpanel.cpp index 87e31124..e0bf2b9a 100644 --- a/application/planpanel.cpp +++ b/application/planpanel.cpp @@ -10,58 +10,49 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/planpanel.h" +#include "application/editormode.h" +#include "rendering/renderer.h" #include "utilities/Globals.h" namespace { -// screen placement of the canvas together with the plan point it is centred on; rebuilt every frame -// from the panel's view state, so the transform never goes stale against a resized window -struct canvas_view +// the plan works in the project's cartesian frame, the scenery in the engine's world space. with no +// georeference set the two share an origin, so the mapping is just the naming of the axes: easting +// runs along world x, northing against world z +glm::dvec3 plan_to_world(double const X, double const Y) { - ImVec2 origin{0.f, 0.f}; - ImVec2 size{0.f, 0.f}; - double viewx{0.0}; - double viewy{0.0}; - double scale{1.0}; + return {X, 0.0, -Y}; +} - ImVec2 centre() const { return {origin.x + size.x * 0.5f, origin.y + size.y * 0.5f}; } - - // northing grows towards the top of the screen, so the vertical axis is flipped - ImVec2 to_screen(double const X, double const Y) const - { - auto const middle{centre()}; - return {middle.x + static_cast((X - viewx) * scale), middle.y - static_cast((Y - viewy) * scale)}; - } - - void to_plan(ImVec2 const &Point, double &X, double &Y) const - { - auto const middle{centre()}; - X = viewx + (Point.x - middle.x) / scale; - Y = viewy - (Point.y - middle.y) / scale; - } -}; - -// grid spacing is picked so the cells stay roughly the same size on screen whatever the zoom, and -// always lands on a round number of metres a surveyor would recognise -double grid_step(double const Scale) +void world_to_plan(glm::dvec3 const &World, double &X, double &Y) { - auto const target{80.0 / std::max(Scale, 1e-6)}; // metres per cell at the wanted screen size - auto const magnitude{std::pow(10.0, std::floor(std::log10(std::max(target, 1e-6))))}; - auto const steps = {1.0, 2.0, 5.0, 10.0}; - for (auto const step : steps) + X = World.x; + Y = -World.z; +} + +// places a world point on screen using the camera of the most recent colour pass. the engine renders +// camera-relative, so the view matrix carries rotation only and the position is subtracted here +bool world_to_screen(glm::dvec3 const &World, ImVec2 &Screen) +{ + auto const relative{glm::vec3(World - GfxRenderer->Camera_Position())}; + auto const clip{GfxRenderer->Camera_Projection_Matrix() * GfxRenderer->Camera_View_Matrix() * glm::vec4(relative, 1.0f)}; + + if (clip.w <= 0.0f) { - if (magnitude * step >= target) - { - return magnitude * step; - } + return false; // behind the camera } - return magnitude * 10.0; + + auto const ndc{glm::vec3(clip) / clip.w}; + auto const display{ImGui::GetIO().DisplaySize}; + Screen = {(ndc.x * 0.5f + 0.5f) * display.x, (0.5f - ndc.y * 0.5f) * display.y}; + + return true; } ImU32 element_colour(int const Kind, bool const Active) { - auto const alpha{Active ? 255 : 90}; + auto const alpha{Active ? 255 : 110}; switch (Kind) { case 1: // arc @@ -77,8 +68,8 @@ ImU32 element_colour(int const Kind, bool const Active) plan_panel::plan_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) { - size_min = {520, 460}; - size_max = {2000, 1600}; + size_min = {420, 240}; + size_max = {900, 900}; m_document.niwelety.push_back({"niweleta 1", {}, {}}); solve(); } @@ -92,118 +83,31 @@ void plan_panel::update() void plan_panel::render_contents() { - ImGui::Text("view: top-down, %.0f m across", Global.editor_ortho_extent * 2.0f); - ImGui::SameLine(); - ImGui::TextDisabled("(wheel over the scenery zooms it)"); + handle_scene(); + draw_on_scene(); render_toolbar(); - render_canvas(); render_gaps(); render_storage(); } -void plan_panel::render_toolbar() +void plan_panel::handle_scene() { - auto *niweleta{current_niweleta()}; - - if (ImGui::Button("New niweleta")) + // clicks that land on a panel belong to the panel, not to the ground under it + if (ImGui::GetIO().WantCaptureMouse) { - m_document.niwelety.push_back({"niweleta " + std::to_string(m_document.niwelety.size() + 1), {}, {}}); - m_niweleta = m_document.niwelety.size() - 1; - m_pending = false; - solve(); - } - ImGui::SameLine(); - if (ImGui::Button("Undo point")) - { - drop_last_vertex(); - } - ImGui::SameLine(); - if (ImGui::Button("Frame all")) - { - frame_all(); - } - ImGui::SameLine(); - ImGui::Checkbox("Draw", &m_drawing); - - if (m_document.niwelety.size() > 1) - { - auto const preview{m_document.niwelety[m_niweleta].name}; - ImGui::SetNextItemWidth(220.0f); - if (ImGui::BeginCombo("Edited", preview.c_str())) - { - for (std::size_t i = 0; i < m_document.niwelety.size(); ++i) - { - if (ImGui::Selectable(m_document.niwelety[i].name.c_str(), i == m_niweleta)) - { - m_niweleta = i; - m_pending = false; - } - } - ImGui::EndCombo(); - } + m_draggedvertex = -1; + return; } - ImGui::Text("straights: %d, points: %d", niweleta != nullptr ? static_cast(niweleta->straights.size()) : 0, static_cast(vertex_count())); - ImGui::Separator(); -} + double cursorx{0.0}; + double cursory{0.0}; + world_to_plan(Global.pCamera.Pos + GfxRenderer->Mouse_Position(), cursorx, cursory); -void plan_panel::render_canvas() -{ - auto const available{ImGui::GetContentRegionAvail()}; - // the tables below the canvas need room of their own, but the canvas takes whatever is left - ImVec2 const canvassize{std::max(available.x, 240.0f), std::max(available.y - 210.0f, 200.0f)}; - - canvas_view view; - view.origin = ImGui::GetCursorScreenPos(); - view.size = canvassize; - view.viewx = m_viewx; - view.viewy = m_viewy; - view.scale = m_scale; - - ImGui::InvisibleButton("plan_canvas", canvassize); - auto const hovered{ImGui::IsItemHovered()}; auto const mouse{ImGui::GetIO().MousePos}; - double mousex{0.0}; - double mousey{0.0}; - view.to_plan(mouse, mousex, mousey); - - // zoom around the cursor, so the ground under it stays put - if (hovered && ImGui::GetIO().MouseWheel != 0.0f) - { - m_scale = std::clamp(m_scale * std::pow(1.15, ImGui::GetIO().MouseWheel), 0.002, 40.0); - view.scale = m_scale; - auto const middle{view.centre()}; - m_viewx = mousex - (mouse.x - middle.x) / m_scale; - m_viewy = mousey + (mouse.y - middle.y) / m_scale; - view.viewx = m_viewx; - view.viewy = m_viewy; - } - - // panning latches on, so the view keeps following the cursor once it leaves the canvas - if (hovered && ImGui::IsMouseDragging(1)) - { - m_panning = true; - } - if (m_panning) - { - if (false == ImGui::IsMouseDragging(1)) - { - m_panning = false; - } - else - { - auto const delta{ImGui::GetIO().MouseDelta}; - m_viewx -= delta.x / m_scale; - m_viewy += delta.y / m_scale; - view.viewx = m_viewx; - view.viewy = m_viewy; - } - } - - // grab an existing point before laying a new one, so corners can be nudged into place - if (hovered && ImGui::IsMouseClicked(0)) + // grabbing an existing point comes first, so corners can be nudged into place + if (ImGui::IsMouseClicked(0)) { m_draggedvertex = -1; for (std::size_t i = 0; i < vertex_count(); ++i) @@ -211,8 +115,12 @@ void plan_panel::render_canvas() double x{0.0}; double y{0.0}; vertex_position(i, x, y); - auto const point{view.to_screen(x, y)}; - if (std::abs(point.x - mouse.x) <= 6.0f && std::abs(point.y - mouse.y) <= 6.0f) + ImVec2 point; + if (false == world_to_screen(plan_to_world(x, y), point)) + { + continue; + } + if (std::abs(point.x - mouse.x) <= 8.0f && std::abs(point.y - mouse.y) <= 8.0f) { m_draggedvertex = static_cast(i); break; @@ -220,46 +128,29 @@ void plan_panel::render_canvas() } if (m_draggedvertex == -1 && m_drawing) { - append_vertex(mousex, mousey); + append_vertex(cursorx, cursory); } } + if (m_draggedvertex != -1) { if (ImGui::IsMouseDown(0)) { - move_vertex(static_cast(m_draggedvertex), mousex, mousey); + move_vertex(static_cast(m_draggedvertex), cursorx, cursory); } else { m_draggedvertex = -1; } } +} - auto *drawlist{ImGui::GetWindowDrawList()}; - auto const canvasend{ImVec2(view.origin.x + canvassize.x, view.origin.y + canvassize.y)}; - drawlist->PushClipRect(view.origin, canvasend, true); - drawlist->AddRectFilled(view.origin, canvasend, IM_COL32(24, 26, 30, 255)); +void plan_panel::draw_on_scene() +{ + // the background list paints over the rendered scenery but under every window, so the controls + // stay readable on top of the drawing + auto *drawlist{ImGui::GetBackgroundDrawList()}; - // grid, with the project origin picked out - auto const step{grid_step(m_scale)}; - double left{0.0}; - double top{0.0}; - double right{0.0}; - double bottom{0.0}; - view.to_plan(view.origin, left, top); - view.to_plan(canvasend, right, bottom); - for (auto x = std::floor(left / step) * step; x <= right; x += step) - { - auto const colour{std::abs(x) < step * 0.5 ? IM_COL32(90, 90, 110, 255) : IM_COL32(48, 50, 58, 255)}; - drawlist->AddLine(view.to_screen(x, top), view.to_screen(x, bottom), colour); - } - for (auto y = std::floor(bottom / step) * step; y <= top; y += step) - { - auto const colour{std::abs(y) < step * 0.5 ? IM_COL32(90, 90, 110, 255) : IM_COL32(48, 50, 58, 255)}; - drawlist->AddLine(view.to_screen(left, y), view.to_screen(right, y), colour); - } - - // solved geometry: two rails per centreline, coloured by the kind of element they came from std::vector points; for (std::size_t n = 0; n < m_solved.size(); ++n) { @@ -274,42 +165,109 @@ void plan_panel::render_canvas() points.reserve(polyline.points.size()); for (auto const &point : polyline.points) { - points.push_back(view.to_screen(point.x, point.y)); + ImVec2 screen; + if (false == world_to_screen(plan_to_world(point.x, point.y), screen)) + { + continue; + } + points.push_back(screen); } - drawlist->AddPolyline(points.data(), static_cast(points.size()), element_colour(polyline.kind, active), false, active ? 2.0f : 1.0f); + if (points.size() < 2) + { + continue; + } + drawlist->AddPolyline(points.data(), static_cast(points.size()), element_colour(polyline.kind, active), false, active ? 2.5f : 1.5f); } } - // editable points of the niweleta being drawn, plus the corner still waiting for its partner + // editable points of the niweleta being drawn for (std::size_t i = 0; i < vertex_count(); ++i) { double x{0.0}; double y{0.0}; vertex_position(i, x, y); - auto const point{view.to_screen(x, y)}; - drawlist->AddCircleFilled(point, 4.0f, static_cast(i) == m_draggedvertex ? IM_COL32(255, 240, 120, 255) : IM_COL32(235, 235, 235, 255)); + ImVec2 screen; + if (false == world_to_screen(plan_to_world(x, y), screen)) + { + continue; + } + drawlist->AddCircleFilled(screen, 5.0f, static_cast(i) == m_draggedvertex ? IM_COL32(255, 240, 120, 255) : IM_COL32(240, 240, 240, 255)); } + + // the corner still waiting for the one that closes its straight, and the rubber band to the cursor + auto const mouse{ImGui::GetIO().MousePos}; + auto const overscenery{false == ImGui::GetIO().WantCaptureMouse}; if (m_pending) { - auto const point{view.to_screen(m_pendingx, m_pendingy)}; - drawlist->AddCircleFilled(point, 4.0f, IM_COL32(255, 240, 120, 255)); - if (hovered) + ImVec2 screen; + if (world_to_screen(plan_to_world(m_pendingx, m_pendingy), screen)) { - drawlist->AddLine(point, mouse, IM_COL32(255, 240, 120, 160)); + drawlist->AddCircleFilled(screen, 5.0f, IM_COL32(255, 240, 120, 255)); + if (overscenery) + { + drawlist->AddLine(screen, mouse, IM_COL32(255, 240, 120, 170), 1.5f); + } } } - else if (hovered && m_drawing && m_draggedvertex == -1 && vertex_count() > 0) + else if (overscenery && m_drawing && m_draggedvertex == -1 && vertex_count() > 0) { double x{0.0}; double y{0.0}; vertex_position(vertex_count() - 1, x, y); - drawlist->AddLine(view.to_screen(x, y), mouse, IM_COL32(255, 240, 120, 160)); + ImVec2 screen; + if (world_to_screen(plan_to_world(x, y), screen)) + { + drawlist->AddLine(screen, mouse, IM_COL32(255, 240, 120, 170), 1.5f); + } + } +} + +void plan_panel::render_toolbar() +{ + auto *niweleta{current_niweleta()}; + + ImGui::Text("top-down view, %.0f m across", Global.editor_ortho_extent * 2.0f); + ImGui::TextDisabled("click the scenery to lay points, drag a point to move it, wheel zooms"); + ImGui::Separator(); + + if (ImGui::Button("New niweleta")) + { + m_document.niwelety.push_back({"niweleta " + std::to_string(m_document.niwelety.size() + 1), {}, {}}); + m_niweleta = m_document.niwelety.size() - 1; + m_pending = false; + solve(); + } + ImGui::SameLine(); + if (ImGui::Button("Undo point")) + { + drop_last_vertex(); + } + ImGui::SameLine(); + if (ImGui::Button("Go to plan")) + { + go_to_plan(); + } + ImGui::SameLine(); + ImGui::Checkbox("Draw", &m_drawing); + + if (m_document.niwelety.size() > 1) + { + ImGui::SetNextItemWidth(220.0f); + if (ImGui::BeginCombo("Edited", m_document.niwelety[m_niweleta].name.c_str())) + { + for (std::size_t i = 0; i < m_document.niwelety.size(); ++i) + { + if (ImGui::Selectable(m_document.niwelety[i].name.c_str(), i == m_niweleta)) + { + m_niweleta = i; + m_pending = false; + } + } + ImGui::EndCombo(); + } } - drawlist->PopClipRect(); - - ImGui::Text("cursor: %.1f, %.1f m grid: %g m scale: %.3g px/m", mousex, mousey, step, m_scale); - ImGui::TextDisabled("left click lays a point, drag a point to move it, right drag pans, wheel zooms"); + ImGui::Text("straights: %d, points: %d", niweleta != nullptr ? static_cast(niweleta->straights.size()) : 0, static_cast(vertex_count())); } void plan_panel::render_gaps() @@ -326,7 +284,7 @@ void plan_panel::render_gaps() return; } - ImGui::BeginChild("plan_gaps", ImVec2(0.0f, 120.0f), false); + ImGui::BeginChild("plan_gaps", ImVec2(0.0f, 130.0f), false); for (std::size_t gap = 0; gap < niweleta->fits.size(); ++gap) { auto &fit{niweleta->fits[gap]}; @@ -377,7 +335,7 @@ void plan_panel::render_gaps() void plan_panel::render_storage() { ImGui::Separator(); - ImGui::SetNextItemWidth(280.0f); + ImGui::SetNextItemWidth(240.0f); ImGui::InputText("##path", m_path, sizeof(m_path)); ImGui::SameLine(); if (ImGui::Button("Save")) @@ -399,7 +357,7 @@ void plan_panel::render_storage() m_pending = false; m_draggedvertex = -1; solve(); - frame_all(); + go_to_plan(); m_status = "loaded " + std::string(m_path); } else @@ -561,7 +519,7 @@ void plan_panel::drop_last_vertex() solve(); } -void plan_panel::frame_all() +void plan_panel::go_to_plan() { auto minx{std::numeric_limits::max()}; auto miny{std::numeric_limits::max()}; @@ -581,16 +539,15 @@ void plan_panel::frame_all() if (minx > maxx) { - // nothing drawn yet: sit on the project origin at a scale that shows a few hundred metres - m_viewx = 0.0; - m_viewy = 0.0; - m_scale = 2.0; - return; + return; // nothing drawn yet, so there is nowhere to go } - m_viewx = (minx + maxx) * 0.5; - m_viewy = (miny + maxy) * 0.5; + auto &camera{editor_mode::get_camera()}; + auto const centre{plan_to_world((minx + maxx) * 0.5, (miny + maxy) * 0.5)}; + camera.Pos.x = centre.x; + camera.Pos.z = centre.z; + // show the whole drawing with a little room around it auto const span{std::max({maxx - minx, maxy - miny, 50.0})}; - m_scale = std::clamp(400.0 / span, 0.002, 40.0); + Global.editor_ortho_extent = std::clamp(static_cast(span * 0.6), 5.0f, 20000.0f); } diff --git a/application/planpanel.h b/application/planpanel.h index 61d751bf..d5976cab 100644 --- a/application/planpanel.h +++ b/application/planpanel.h @@ -14,10 +14,10 @@ http://mozilla.org/MPL/2.0/. #include "maj0sted/app/editor_document.hpp" #include "maj0sted/web/editor.hpp" -// drawing board for track layout: straights laid down by hand and the curves fitted into the gaps -// between them, in the project's own cartesian frame (EPSG:2180, metres). none of the geometry is -// computed here - the panel collects the input, hands the whole document to maj0sted and paints -// what comes back. scenery nodes are not touched. +// track layout tool: straights laid down by hand on the scenery itself and the curves fitted into +// the gaps between them. the panel holds only the controls - the drawing happens in the viewport, +// which the tool puts into a top-down orthographic view for the purpose. none of the geometry is +// computed here: maj0sted solves the whole document and the panel paints what comes back. class plan_panel : public ui_panel { @@ -30,9 +30,11 @@ class plan_panel : public ui_panel private: // methods void render_toolbar(); - void render_canvas(); void render_gaps(); void render_storage(); + // paints the solved plan over the rendered scenery and takes the clicks that build it + void handle_scene(); + void draw_on_scene(); // re-runs the solver over the whole document; cheap enough to do on every edit void solve(); maj0sted::web::NiweletaSpec *current_niweleta(); @@ -48,23 +50,18 @@ class plan_panel : public ui_panel void append_vertex(double const X, double const Y); void move_vertex(std::size_t const Index, double const X, double const Y); void drop_last_vertex(); - // centres the view on the drawing, or on a default patch of ground when nothing is drawn yet - void frame_all(); + // takes the camera to the middle of the drawing + void go_to_plan(); // members maj0sted::app::EditorDocument m_document; std::vector m_solved; std::size_t m_niweleta{0}; // niweleta being drawn into int m_draggedvertex{-1}; - bool m_drawing{true}; // whether a click on the canvas lays track or only picks vertices - bool m_panning{false}; + bool m_drawing{true}; // whether a click on the scenery lays track or only picks existing points // first corner of a niweleta has nothing to attach to, so it waits for the second click bool m_pending{false}; double m_pendingx{0.0}; double m_pendingy{0.0}; - // view: the plan point sitting at the centre of the canvas, and the zoom in pixels per metre - double m_viewx{0.0}; - double m_viewy{0.0}; - double m_scale{2.0}; std::string m_status; char m_path[256]{"editor/plan.m0s"}; };