From 97b0d2c8798308f19237968d9755d7336d52a479 Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Mon, 5 Oct 2026 08:35:06 +0200 Subject: [PATCH] Explain the texture displacement views that cannot be used, and show them Checker and Distortion are views of a layer's unwrap, so they mean nothing on any other mapping - but they were offered on all of them and simply drew nothing when picked. Fade them out on anything but Unwrap (LSCM), with the reason in the tooltip, and have a click bring the UV editor up on the view it selected, since that pane is where the unwrap is actually worked on. Neither view was visible even when it did apply. The overlay is built from the base patch and drawn with a polygon offset, which biases depth values rather than moving geometry, so it cannot win against the displaced preview standing in front of it. Leave that preview out while a UV-check view is on and draw the undisplaced surface instead, which is what the offset assumes and what the mapping being inspected belongs to. In the UV editor, a tool that cannot be used right now is faded rather than disabled. A disabled window gets no mouse events on GTK or MSW, so every one of those tools - Cut, Join, Unjoin, Clear seams, Clear UV edits, the select modes, Snap, Frame - silently had no tooltip in the state where the user most needs to know what is missing. Each now says what to do instead. The tile size, the island statistics, the status line and the three select modes gained tooltips of their own; the select modes now name the gestures they enable, which were documented nowhere. --- .../GUI/Gizmos/GLGizmoTextureDisplacement.cpp | 63 +++++++-- src/slic3r/GUI/UVEditorCanvas.cpp | 120 ++++++++++++++---- 2 files changed, 142 insertions(+), 41 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 5c322a1f81..5459685a04 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -489,7 +489,15 @@ void GLGizmoTextureDisplacement::render_painter_gizmo() // volume for a shaded pass that then draws nothing is what made the model vanish - most obviously // with zero layers, but equally with a layer that has no texture picked yet. const bool use_shaded = m_use_shaded_preview && m_shaded_preview_glmodel.is_initialized() && shaded_preview_ready(); - const bool use_true_preview = !use_shaded && m_preview_glmodel.is_initialized(); + // Checker/Distortion are built from the *base* patch and drawn with a polygon offset, which biases + // depth values - it does not move the geometry. It therefore cannot win against a surface that + // genuinely stands in front, and the displaced preview does exactly that: it rises above the base + // surface by the layer's depth. Drawn underneath a UV-check overlay it simply occludes it, which is + // why those two modes looked like they did nothing. Leave it out and let the undisplaced volume show + // through instead (toggle_model_objects_visibility below) - that one *is* coincident with the + // overlay, which is what the offset assumes, and it is the surface whose mapping is being inspected. + const bool use_true_preview = !use_shaded && m_uv_check_mode == UVCheckMode::None && + m_preview_glmodel.is_initialized(); // In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is // looking at, so the opaque paint-selection highlight must not be drawn on top of it - same // reasoning as skipping it for the shaded preview (see bug #12). Without this the painted area @@ -517,7 +525,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo() render_triangles(selection); glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); } - } else if (show_paint_overlay) { + } else { + // render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the + // selector's colours), not an overlay on top of one - so it still has to run under a UV-check + // overlay, or nothing draws the surface at all and the checker floats alone over an empty scene. + // Deliberately without the depth bias the branch above applies: the checker/heatmap is drawn later + // with its own -1 offset and has to win against this. Biasing both by the same amount is what made + // the painted area cover the checker and is why this call used to be skipped outright. render_triangles(selection); } @@ -5710,14 +5724,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float const int cur_mode = m_use_shaded_preview ? 1 : m_uv_check_mode == UVCheckMode::Checker ? 2 : m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0; - int new_mode = cur_mode; - bool wf_toggle = false; - const wxString distortion_na = active == nullptr ? _L("Add a layer first.") : - active->projection_method != TextureProjectionMethod::LSCM ? - _L("Needs the active layer mapped with Unwrap (LSCM).") : - wxString(); - // Distortion over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal. - if (cur_mode == 3 && !distortion_na.empty()) + int new_mode = cur_mode; + 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 + // its stretch - so they only mean anything for a layer mapped with Unwrap (LSCM). On the default + // triplanar mapping (or cylindrical / spherical / from view) they are faded out with the reason in the + // tooltip, rather than being offered and then showing nothing. + const wxString uv_view_na = active == nullptr ? _L("Add a layer first.") : + active->projection_method != TextureProjectionMethod::LSCM ? + _L("Only for a layer mapped with Unwrap (LSCM) - set the " + "active layer's Mapping to Unwrap to use this view.") : + wxString(); + // Either view over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal. + if ((cur_mode == 2 || cur_mode == 3) && !uv_view_na.empty()) new_mode = 0; const float x0 = ImGui::GetCursorPosX(); @@ -5736,12 +5756,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float 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"))) - new_mode = 2; + "texture is undistorted; where they stretch, it will too. Opens the UV editor if " + "it is closed"), + uv_view_na)) { + new_mode = 2; + open_uv_editor = true; + } ImGui::SameLine(0.f, gap_s); if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"), - _L("Distortion - blue-to-red stretch heatmap over the unwrap"), distortion_na)) - new_mode = 3; + _L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if " + "it is closed"), + uv_view_na)) { + new_mode = 3; + open_uv_editor = true; + } vsep(icon_md); if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"), _L("Wireframe - overlay the mesh edges; independent of the view above"))) @@ -5756,6 +5784,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float hover_tip(_u8L("Rebuilds the preview as soon as anything changes. Turn it off on a heavy model if painting " "or dragging a slider starts to stutter - the preview then waits until you let go.")); + // Both are views of the unwrap, so picking one brings the UV editor up with it - including when that + // view is already the active one and only the pane is missing. + if (open_uv_editor && !m_show_uv_editor) { + m_show_uv_editor = true; + if (new_mode == cur_mode) + update_uv_editor(); // otherwise apply_view_mode() below does it + } if (new_mode != cur_mode) apply_view_mode(new_mode); if (wf_toggle) { diff --git a/src/slic3r/GUI/UVEditorCanvas.cpp b/src/slic3r/GUI/UVEditorCanvas.cpp index dd0432bc95..e12829c5bf 100644 --- a/src/slic3r/GUI/UVEditorCanvas.cpp +++ b/src/slic3r/GUI/UVEditorCanvas.cpp @@ -1712,7 +1712,7 @@ public: bool toggle, bool accent = false, int size_dip = 26) : wxWindow(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxFULL_REPAINT_ON_RESIZE) , m_icon_name(icon), m_icon_dip(size_dip >= 26 ? 16 : 14), m_label(label), m_toggle(toggle), m_accent(accent) - , m_size_dip(size_dip) + , m_size_dip(size_dip), m_tip(tip) { SetBackgroundStyle(wxBG_STYLE_PAINT); SetToolTip(tip); @@ -1725,7 +1725,7 @@ public: Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent &) { m_hover = true; Refresh(); }); Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &) { m_hover = false; m_pressed = false; Refresh(); }); Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) { - if (IsEnabled()) { + if (usable()) { m_pressed = true; Refresh(); } @@ -1734,7 +1734,7 @@ public: const bool was_pressed = m_pressed; m_pressed = false; Refresh(); - if (!was_pressed || !IsEnabled() || !GetClientRect().Contains(e.GetPosition())) + if (!was_pressed || !usable() || !GetClientRect().Contains(e.GetPosition())) return; if (m_toggle) m_on = !m_on; @@ -1774,6 +1774,30 @@ public: Refresh(); return changed; } + // Soft-disable: the button is drawn faded and swallows clicks, but stays a live window, so hovering it + // still raises its tooltip - now with `reason` appended, saying what to do to make it usable. A window + // really disabled with Enable(false) gets no mouse events at all on GTK and MSW, which leaves the user + // guessing; this is the same trade-off the gizmo panel's icon_toggle() makes with its `unavailable`. + // An empty reason makes the button usable again. + void SetUnavailable(const wxString &reason) + { + if (reason == m_unavailable) + return; + m_unavailable = reason; + SetToolTip(reason.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + reason); + if (!m_unavailable.empty()) + m_pressed = false; // a reason appearing mid-press cancels the press + Refresh(); + } + // Replaces the plain tooltip, keeping whatever reason is currently appended to it. + void SetTip(const wxString &tip) + { + if (tip == m_tip) + return; + m_tip = tip; + SetToolTip(m_unavailable.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + m_unavailable); + } + bool usable() const { return IsEnabled() && m_unavailable.empty(); } protected: wxSize DoGetBestSize() const override @@ -1792,7 +1816,7 @@ private: const PaneColors c = PaneColors::current(); const wxRect r = GetClientRect(); const wxColour teal(0x00, 0x96, 0x88); - const bool enabled = IsEnabled(); + const bool enabled = usable(); wxColour fill = c.bg, border = c.frame, text = c.ink; if (m_accent) { @@ -1852,6 +1876,8 @@ private: bool m_toggle = false; bool m_accent = false; int m_size_dip = 26; + wxString m_tip; // the tooltip without any m_unavailable reason appended + wxString m_unavailable; // non-empty: faded and unclickable, and why (see SetUnavailable()) bool m_on = false; bool m_badge = false; bool m_hover = false; @@ -1897,6 +1923,9 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) }); m_layer_name->SetMinSize(wxSize(FromDIP(30), -1)); m_tile = text(wxEmptyString, c.dim); + m_tile->SetToolTip(_L("The active layer's tile size: how much of the model one repeat of the texture covers. The " + "canvas is measured in tiles, so one grid cell is one repeat. Change it with Tiling in the " + "layer's settings.")); header->Add(m_thumb, 0, wxALIGN_CENTER_VERTICAL); header->Add(m_layer_name, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); header->Add(m_tile, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); @@ -1950,9 +1979,19 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) strip->Add(r, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(11)); strip->AddSpacer(FromDIP(7)); }; - m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", _L("Island - move, rotate and scale whole islands"), true); - m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", _L("Vertex - drag vertices to reshape; Shift/Ctrl to multi-select"), true); - m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", _L("Edge - drag edges to reshape; Shift/Ctrl to multi-select"), true); + m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", + _L("Island - work on whole islands. Click one to select it, then drag to move it, right-drag to " + "rotate it, or press R to rotate and S to scale with the mouse (click or Enter to confirm, Esc " + "to cancel)."), + true); + m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", + _L("Vertex - drag vertices to reshape an island by hand; Shift adds to the selection, Ctrl " + "toggles one in or out of it."), + true); + m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", + _L("Edge - drag edges to reshape an island by hand; Shift adds to the selection, Ctrl toggles " + "one in or out of it."), + true); strip_rule(); m_mark_seams = tool(ID_UV_MARK_SEAMS, "texture_displacement_uv_seam", _L("Mark seams - click edges on the model to cut the unwrap along them. The edge under the cursor is " @@ -1973,7 +2012,11 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) m_clear_edits = tool(ID_UV_CLEAR_EDITS, "texture_displacement_uv_clear_edits", _L("Clear UV edits - discard all manual vertex/edge moves and return the unwrap to its automatic shape"), false); m_snap = tool(ID_UV_SNAP, "texture_displacement_uv_snap", _L("Snap - stick islands together when dragging one against another"), true); - m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", _L("Frame all islands (Home)"), false); + m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", + _L("Frame all islands, fitting every one of them in view (Home or F).\n" + "Elsewhere on the canvas: scroll to zoom around the cursor, and middle-drag - or drag empty space - " + "to pan."), + false); strip->AddSpacer(FromDIP(4)); m_canvas = new UVEditorCanvas(this); @@ -1984,6 +2027,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) // ---- status line: the current gesture on the left, the unwrap summary on the right ---- m_status = text(wxEmptyString, c.dim, wxST_ELLIPSIZE_END); + m_status->SetToolTip(_L("What is selected, and the exact figures of the move, rotation or scale while you drag one.")); m_status->SetMinSize(wxSize(FromDIP(40), -1)); m_stats = text(wxEmptyString, c.dim); auto *status = new wxBoxSizer(wxHORIZONTAL); @@ -2059,19 +2103,25 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s) } else { m_thumb->SetBitmap(wxNullBitmap); } - m_thumb->Enable(s.has_layer); + // Every tool that cannot be used right now is faded with the reason appended to its tooltip, rather than + // being hard-disabled (which would hide the tooltip too - see UVToolButton::SetUnavailable()). + const wxString no_layer = s.has_layer ? wxString() : + _L("The pane follows the active texture layer, and that layer has to be mapped " + "with Unwrap (LSCM). Add a layer and set its Mapping to Unwrap."); + m_thumb->SetUnavailable(no_layer); + m_layer_name->SetToolTip(s.has_layer ? m_thumb->GetToolTipText() : no_layer); for (int i = 0; i < 3; ++i) { m_background[i]->SetValue(int(s.background) == i); - m_background[i]->Enable(s.has_layer); + m_background[i]->SetUnavailable(no_layer); } - m_unwrap->Enable(s.has_layer); + m_unwrap->SetUnavailable(no_layer); m_unwrap->SetBadge(s.unwrap_stale); - m_unwrap->SetToolTip(s.unwrap_stale ? - _L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. " - "Press to unwrap again.") : - _L("Flatten the painted area into UV islands. It is computed only when you press this, not on " - "every edit - so paint, change the seam angle or mark seams first, then press Unwrap.")); + m_unwrap->SetTip(s.unwrap_stale ? + _L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. " + "Press to unwrap again.") : + _L("Flatten the painted area into UV islands. It is computed only when you press this, not on " + "every edit - so paint, change the seam angle or mark seams first, then press Unwrap.")); if (m_seam_angle->GetValue() != int(std::lround(s.seam_angle_deg))) m_seam_angle->SetValue(int(std::lround(s.seam_angle_deg))); @@ -2080,15 +2130,26 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s) m_connect->Enable(s.has_layer); m_mark_seams->SetValue(s.mark_seams); - m_mark_seams->Enable(s.has_layer); + m_mark_seams->SetUnavailable(no_layer); m_seam_path->SetValue(s.seam_path); - m_seam_path->Enable(s.has_layer && s.mark_seams); - m_clear_seams->Enable(s.has_layer && s.has_seams); - m_clear_edits->Enable(s.has_layer && s.has_uv_edits); + m_seam_path->SetUnavailable(!no_layer.empty() ? no_layer : + s.mark_seams ? wxString() : + _L("Turn Mark seams on first - Path is a quicker way of marking them.")); + m_clear_seams->SetUnavailable(!no_layer.empty() ? no_layer : + s.has_seams ? wxString() : + _L("No seams are marked on this layer.")); + m_clear_edits->SetUnavailable(!no_layer.empty() ? no_layer : + s.has_uv_edits ? wxString() : + _L("No islands have been reshaped by hand, so there is nothing to " + "discard.")); m_stats->SetLabel(s.unwrapped ? wxString::Format(_L("%d islands, %s faces"), s.island_count, wxString(std::to_string(s.face_count))) : wxString()); + m_stats->SetToolTip(s.unwrapped ? _L("How the painted area came out of the unwrap: the number of separate pieces it " + "was cut into (at the seams and at edges sharper than the seam angle), and how " + "many triangles they hold in total.") : + wxString()); refresh_selection_tools(); if (relayout) Layout(); @@ -2098,19 +2159,24 @@ void UVEditorPanel::refresh_selection_tools() { const bool has_islands = m_canvas->has_islands(); const int mode = int(m_canvas->select_mode()); + // Faded rather than hard-disabled, so the tooltip still says what is missing (see apply_state()). + const wxString not_unwrapped = has_islands ? wxString() : _L("Press Unwrap first - there are no islands to work on yet."); for (int i = 0; i < 3; ++i) { m_select[i]->SetValue(i == mode); - m_select[i]->Enable(has_islands); + m_select[i]->SetUnavailable(not_unwrapped); } const bool island_picked = has_islands && m_canvas->select_mode() == UVEditorCanvas::SelectMode::Island && m_canvas->selected_island() >= 0; - m_avg_scale->Enable(has_islands); - m_cut->Enable(island_picked); - m_join->Enable(island_picked); - m_unjoin->Enable(island_picked); - m_snap->Enable(has_islands); + const wxString no_island = !not_unwrapped.empty() ? not_unwrapped : + island_picked ? wxString() : + _L("Click an island on the canvas first, in Island mode."); + m_avg_scale->SetUnavailable(not_unwrapped); + m_cut->SetUnavailable(no_island); + m_join->SetUnavailable(no_island); + m_unjoin->SetUnavailable(no_island); + m_snap->SetUnavailable(not_unwrapped); m_snap->SetValue(m_canvas->snap_enabled()); - m_frame->Enable(has_islands); + m_frame->SetUnavailable(not_unwrapped); } void UVEditorPanel::on_tool(wxCommandEvent &evt)