diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 048b005c69..768f16f567 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -562,6 +562,11 @@ void GLGizmoTextureDisplacement::on_shutdown() m_shaded_island_delta = Eigen::Matrix::Identity(); m_island_drag_active = false; m_island_move_set.clear(); + // The pane closes below, taking the highlight with it, and the view must not open the next session in a + // Fast the user never picked. Set directly: nothing is to be queued against a gizmo that is closing. + if (m_island_view.switched) + m_use_shaded_preview = false; + m_island_view = IslandViewFollow{}; m_adjust_texture_mode = false; m_seam_edit_mode = false; m_adjust_drag_handle = AdjustHandle::None; @@ -727,18 +732,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo() // The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane // changes its selection in its own mouse handling, and a compare of a few ints per frame is free. - { - const TextureDisplacementLayer *al = active_layer(); - const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas(); - if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM && - uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) { - if (uv_canvas->selected_islands() != m_island_overlay_selection) - rebuild_island_overlay(uv_canvas->selected_islands()); - render_island_overlay(); - } else if (m_island_overlay_glmodel.is_initialized()) { - m_island_overlay_glmodel.reset(); - m_island_overlay_selection.clear(); - } + if (const std::vector *islands = island_highlight_selection()) { + if (*islands != m_island_overlay_selection) + rebuild_island_overlay(*islands); + render_island_overlay(); + } else if (m_island_overlay_glmodel.is_initialized()) { + m_island_overlay_glmodel.reset(); + m_island_overlay_selection.clear(); } // Diagnostic overlays, drawn on top of whatever preview is active (both pull toward the camera @@ -1926,6 +1926,16 @@ void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay) shader->stop_using(); } +const std::vector *GLGizmoTextureDisplacement::island_highlight_selection() +{ + const TextureDisplacementLayer *al = active_layer(); + const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas(); + if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM && + uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) + return &uv_canvas->selected_islands(); + return nullptr; +} + void GLGizmoTextureDisplacement::rebuild_island_overlay(const std::vector &selection) { m_island_overlay_glmodel.reset(); @@ -2651,6 +2661,37 @@ void GLGizmoTextureDisplacement::apply_view_mode(int mode) m_parent.set_as_dirty(); } +int GLGizmoTextureDisplacement::view_mode() const +{ + return m_use_shaded_preview ? 1 : + m_uv_check_mode == UVCheckMode::Checker ? 2 : + m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0; +} + +int GLGizmoTextureDisplacement::IslandViewFollow::step(bool shown_now, int view, bool debug_open) +{ + shown = shown_now; + if (!shown) { + // The view is back to Normal already if a debug stage took it, and Normal applied over a stage would + // queue a preview job that replaces the stage being inspected. + const bool restore = switched && view == 1; + switched = keep_normal = false; + return restore ? 0 : -1; + } + if (view != 0 || keep_normal || debug_open) + return -1; + switched = true; + return 1; +} + +void GLGizmoTextureDisplacement::follow_island_highlight() +{ + const std::vector *islands = island_highlight_selection(); + const int view = m_island_view.step(islands != nullptr && !islands->empty(), view_mode(), m_debug_stage >= 0); + if (view >= 0) + apply_view_mode(view); +} + void GLGizmoTextureDisplacement::push_uv_pane_state() { UVEditorCanvas *canvas = wxGetApp().plater()->get_uv_editor_canvas(); @@ -2891,7 +2932,8 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value) rebuild_preview(); } } - // FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet. + // FrameAll/ToggleSnap are handled inside the canvas; ProjectFromView is not wired yet. SelectionChanged + // only asks for this frame: follow_island_highlight() and the overlay read the selection back themselves. m_parent.set_as_dirty(); } @@ -5835,6 +5877,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float GizmoImguiBegin(get_name(), flags); ensure_panel_icons(); process_uv_commands(); // clicks from the UV editor pane, run here where the GL context is current + // After the pane's commands, which can close it or change the view, and before the View row, so the row + // lights the view this frame ends up in and a click on it has the last word. + follow_island_highlight(); const float previous_body_h = m_panel_body_h; const float previous_footer_h = m_panel_footer_h; @@ -6323,10 +6368,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float // ---- View: Normal / Fast / Checker / Distortion as one group, Wireframe on its own ---- // The underlying state stays m_use_shaded_preview + m_uv_check_mode. { - const int cur_mode = m_use_shaded_preview ? 1 : - m_uv_check_mode == UVCheckMode::Checker ? 2 : - m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0; + const int cur_mode = view_mode(); int new_mode = cur_mode; + int picked = -1; // the button clicked, the active one included bool wf_toggle = false; bool open_uv_editor = false; // Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second @@ -6349,19 +6393,19 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f))); if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"), _L("Normal - the real geometry, exactly what Bake will produce. Slower to update"))) - new_mode = 0; + new_mode = picked = 0; ImGui::SameLine(0.f, gap_s); if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"), _L("Fast - the relief is only shaded on, not built, and only for the layer you are " "editing. Updates instantly while you paint"))) - new_mode = 1; + new_mode = picked = 1; ImGui::SameLine(0.f, gap_s); if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"), _L("Checker - a test grid instead of the texture. Where the squares stay square the " "texture is undistorted; where they stretch, it will too. Opens the UV editor if " "it is closed"), uv_view_na)) { - new_mode = 2; + new_mode = picked = 2; open_uv_editor = true; } ImGui::SameLine(0.f, gap_s); @@ -6369,7 +6413,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float _L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if " "it is closed"), uv_view_na)) { - new_mode = 3; + new_mode = picked = 3; open_uv_editor = true; } vsep(icon_md); @@ -6393,6 +6437,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float if (new_mode == cur_mode) update_uv_editor(); // otherwise apply_view_mode() below does it } + if (picked >= 0) + m_island_view.picked(picked); if (new_mode != cur_mode) apply_view_mode(new_mode); if (wf_toggle) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 631254313c..5b754edfe9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -197,6 +197,27 @@ public: }; static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint); + // The view while the UV editor's selected islands are highlighted on the model. The highlight lies on the + // undisplaced surface, so the Normal view's relief hides it wherever it rises: Normal gives way to Fast + // while islands are selected and comes back once none are. A view the user clicks in the View row is + // theirs - the pane's background buttons are not a pick, Height only meaning "the texture view". + struct IslandViewFollow + { + bool shown = false; // the highlight, as of the last step() + bool switched = false; // Fast was switched on for the highlight, so Normal comes back once it is gone + bool keep_normal = false; // the user picked Normal in the View row while the highlight was shown + // The view to apply this frame (0 Normal, 1 Fast), or -1 for none. `view` is the one on screen, numbered as + // apply_view_mode() takes it; `debug_open` a debug stage on screen, which holds Normal itself. + int step(bool shown_now, int view, bool debug_open); + // A View row button clicked, the active one included. The row is drawn after step() in the same frame, + // so `shown` is this frame's. + void picked(int view) + { + switched = false; + keep_normal = shown && view == 0; + } + }; + void render_painter_gizmo() override; // Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/ @@ -513,6 +534,11 @@ private: // The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's // background buttons, so both switch views the same way. void apply_view_mode(int mode); + // The view on screen, numbered as apply_view_mode() takes it. + int view_mode() const; + // Run once a frame, after the pane's commands: switches the view as m_island_view says. + void follow_island_highlight(); + IslandViewFollow m_island_view; // The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes. static constexpr int UV_THUMB_PX = 64; const void *m_uv_thumb_source = nullptr; @@ -744,6 +770,8 @@ private: // in place. Rebuilt whenever the pane's selection differs from the one it was built for. GLModel m_island_overlay_glmodel; std::vector m_island_overlay_selection; + // The pane's selection while the overlay applies - the pane open over an unwrapped LSCM layer - else null. + const std::vector *island_highlight_selection(); void rebuild_island_overlay(const std::vector &selection); void render_island_overlay(); // Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept diff --git a/src/slic3r/GUI/UVEditorCanvas.cpp b/src/slic3r/GUI/UVEditorCanvas.cpp index 2432026689..a4a9901822 100644 --- a/src/slic3r/GUI/UVEditorCanvas.cpp +++ b/src/slic3r/GUI/UVEditorCanvas.cpp @@ -939,7 +939,8 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt) m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair{ -1, -1 }; m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan; } else { - const int hit = island_at(uv); + const std::vector selection_before = m_selection; + const int hit = island_at(uv); if (hit >= 0) { if (evt.ShiftDown()) { // Shift adds to the selection (and makes the clicked one the new primary). @@ -969,6 +970,10 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt) m_selected_island = -1; } m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan; + // The owner shows the selection on the model and would not hear of this click otherwise in time: a + // click that clears the selection starts no gesture, and one that sets it reports only on release. + if (m_selection != selection_before) + run_command(Command::SelectionChanged); } grab_mouse(); Refresh(); diff --git a/src/slic3r/GUI/UVEditorCanvas.hpp b/src/slic3r/GUI/UVEditorCanvas.hpp index 2fc1055cb8..a01d058983 100644 --- a/src/slic3r/GUI/UVEditorCanvas.hpp +++ b/src/slic3r/GUI/UVEditorCanvas.hpp @@ -108,10 +108,12 @@ public: // the command callback - the canvas has the selection and the view, the gizmo has the layer. // `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into // SelectMode / Background) and is 0 otherwise. + // SelectionChanged is sent by the canvas itself when a click changes the island selection. It carries + // nothing: the owner reads selected_islands() back, and only needs telling that it should look. enum class Command { FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected, Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits, - SetBackground, PickTexture, PaneClosed + SetBackground, PickTexture, PaneClosed, SelectionChanged }; void run_command(Command cmd, float value = 0.f); using CommandFn = std::function; diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index db56a510ac..2fe6512d9e 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(${_TEST_NAME}_tests test_plugin_json_depth.cpp test_shortcuts.cpp test_texture_color_palette.cpp + test_texture_island_view.cpp test_file_url.cpp test_user_manager.cpp ../fff_print/test_helpers.cpp diff --git a/tests/slic3rutils/test_texture_island_view.cpp b/tests/slic3rutils/test_texture_island_view.cpp new file mode 100644 index 0000000000..9655c86115 --- /dev/null +++ b/tests/slic3rutils/test_texture_island_view.cpp @@ -0,0 +1,99 @@ +// How the texture displacement gizmo's view follows the UV editor's island highlight. The rules live in +// libslic3r_gui; this is the suite that links it. Same Windows include prologue as +// test_texture_color_palette.cpp (wx pulls in ; keep WIN32_LEAN_AND_MEAN / NOMINMAX ahead of it). +#ifdef WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif + +#include +#include + +#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" + +using Follow = Slic3r::GUI::GLGizmoTextureDisplacement::IslandViewFollow; + +namespace { + +// The View row's indices, and step()'s "apply nothing". +constexpr int NORMAL = 0, FAST = 1, CHECKER = 2, DISTORTION = 3, NONE = -1; + +} // namespace + +TEST_CASE("a Normal view gives way to Fast while islands are selected and comes back once none are", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + CHECK(f.step(false, NORMAL, false) == NONE); + CHECK(f.step(true, NORMAL, false) == FAST); + CHECK(f.step(true, FAST, false) == NONE); + CHECK(f.step(false, FAST, false) == NORMAL); + CHECK(f.step(false, NORMAL, false) == NONE); +} + +TEST_CASE("a Fast, Checker or Distortion view is left alone by the island highlight", "[TextureIslandView][TextureDisplacement]") +{ + const int view = GENERATE(FAST, CHECKER, DISTORTION); + Follow f; + CHECK(f.step(true, view, false) == NONE); + CHECK(f.step(false, view, false) == NONE); +} + +TEST_CASE("arriving in Normal with islands already selected switches to Fast", "[TextureIslandView][TextureDisplacement]") +{ + // The pane's Height button over Checker lands in Normal with the islands still selected. + Follow f; + CHECK(f.step(true, CHECKER, false) == NONE); + CHECK(f.step(true, NORMAL, false) == FAST); + CHECK(f.step(false, FAST, false) == NORMAL); +} + +TEST_CASE("Normal picked while islands are selected stays until they are deselected", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + REQUIRE(f.step(true, NORMAL, false) == FAST); + f.picked(NORMAL); + CHECK(f.step(true, NORMAL, false) == NONE); + // A debug stage opening and closing over it changes nothing. + CHECK(f.step(true, NORMAL, true) == NONE); + CHECK(f.step(true, NORMAL, false) == NONE); + CHECK(f.step(false, NORMAL, false) == NONE); + CHECK(f.step(true, NORMAL, false) == FAST); +} + +TEST_CASE("Fast picked while it was switched on stays after deselecting", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + REQUIRE(f.step(true, NORMAL, false) == FAST); + f.picked(FAST); + CHECK(f.step(false, FAST, false) == NONE); +} + +TEST_CASE("Normal picked with nothing selected does not hold off the next switch", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + CHECK(f.step(false, NORMAL, false) == NONE); + f.picked(NORMAL); + CHECK(f.step(true, NORMAL, false) == FAST); +} + +TEST_CASE("a debug stage holds Normal while it is open, and the switch follows once it closes", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + REQUIRE(f.step(true, NORMAL, false) == FAST); + // The stage takes Normal itself, with the islands still selected. + CHECK(f.step(true, NORMAL, true) == NONE); + CHECK(f.step(true, NORMAL, false) == FAST); +} + +TEST_CASE("islands deselected while a debug stage is open apply no view over it", "[TextureIslandView][TextureDisplacement]") +{ + Follow f; + REQUIRE(f.step(true, NORMAL, false) == FAST); + CHECK(f.step(false, NORMAL, true) == NONE); + CHECK(f.step(false, NORMAL, false) == NONE); +}