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)