diff --git a/docs/HLSD/keyboard-shortcuts.md b/docs/HLSD/keyboard-shortcuts.md index 7cfc72c30f..f3d9ff6f3b 100644 --- a/docs/HLSD/keyboard-shortcuts.md +++ b/docs/HLSD/keyboard-shortcuts.md @@ -124,7 +124,9 @@ Control on the Global page, and from the `?` key on the page of the view that re the dialog should list it; the row's section heading is the `section_table` entry above it, so a new section needs an entry there too. Pick a default that does not collide inside its contexts; the `[Shortcuts]` tests check every default against the - others. + others. A default `KeyChord{}` leaves the action unbound: it is listed in the dialog + for the user to bind and matches nothing until they do, which is the honest answer + when every key its contexts can reach is already spoken for. 2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`, `GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's `on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor. diff --git a/resources/images/toolbar_image.svg b/resources/images/toolbar_image.svg new file mode 100644 index 0000000000..0cb9c793c6 --- /dev/null +++ b/resources/images/toolbar_image.svg @@ -0,0 +1 @@ + diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 65c8384ef2..20e255ad57 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -186,6 +186,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoFlatten.hpp GUI/Gizmos/GLGizmoFuzzySkin.cpp GUI/Gizmos/GLGizmoFuzzySkin.hpp + GUI/Gizmos/GLGizmoImage.cpp + GUI/Gizmos/GLGizmoImage.hpp GUI/Gizmos/GLGizmoMeasure.cpp GUI/Gizmos/GLGizmoMeasure.hpp GUI/Gizmos/GLGizmoMeshBoolean.cpp diff --git a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp new file mode 100644 index 0000000000..1349f7c2a8 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp @@ -0,0 +1,39 @@ +#include "GLGizmoImage.hpp" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/Shortcuts.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" + +#include +#include + +namespace Slic3r::GUI { + +GLGizmoImage::GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id) + : GLGizmoTextureDisplacement(parent, icon_filename, sprite_id) +{ +} + +bool GLGizmoImage::on_init() +{ + if (!GLGizmoTextureDisplacement::on_init()) + return false; + // Everything the base set up stays - the brush descriptions and the shortcut lists are the same + // tools - except the key that opens it, which is this tool's own. + m_shortcut = Shortcut::GizmoImage; + return true; +} + +std::string GLGizmoImage::on_get_name() const +{ + return _u8L("Image"); +} + +wxString GLGizmoImage::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const +{ + // Same paint, same masks; only the undo step's wording follows the tool the user is in. + const bool erasing = shift_down || (m_erase_mode != (button_down == GLGizmoPainterBase::Button::Right)); + return erasing ? _L("Erase image paint") : _L("Paint image"); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoImage.hpp b/src/slic3r/GUI/Gizmos/GLGizmoImage.hpp new file mode 100644 index 0000000000..a3e78bd2ec --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoImage.hpp @@ -0,0 +1,46 @@ +#ifndef slic3r_GLGizmoImage_hpp_ +#define slic3r_GLGizmoImage_hpp_ + +#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" +#include "slic3r/GUI/I18N.hpp" + +#include +#include + +namespace Slic3r::GUI { + +class GLCanvas3D; + +// "Put this picture on the model": the texture-displacement gizmo with the relief taken out of it. +// +// There is no second engine here and no copy of one. It is GLGizmoTextureDisplacement with +// image_mode() on, which pins every layer to zero depth and colour on (see +// apply_image_mode_presets()) and leaves out the controls that only mean something once the surface +// moves. Painting, the layer stack, the texture library, the previews and the bake are the very same +// code, reached the very same way - a zero-depth bake refines the painted area and writes the +// colours exactly as a relief bake does, it just displaces by nothing. +// +// Its own toolbar button because the two are different jobs, not two settings of one: a user who +// wants a photo on a part should not have to know what a height map is, or find the one slider that +// has to be zero. +class GLGizmoImage : public GLGizmoTextureDisplacement +{ +public: + GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id); + +protected: + bool image_mode() const override { return true; } + + bool on_init() override; + std::string on_get_name() const override; + + wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; + + std::string get_gizmo_entering_text() const override { return _u8L("Entering Image painting"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Image painting"); } + std::string get_action_snapshot_name() const override { return _u8L("Image editing"); } +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_GLGizmoImage_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 96af821505..660bd76f62 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -3723,16 +3723,29 @@ void GLGizmoTextureDisplacement::add_texture_layer() TextureDisplacementLayer layer; layer.slot = free_slot; - // Start the layer off on the first library texture rather than on nothing at all: a textureless - // layer looks broken (painting on it appears to do nothing, because there is no height map to - // displace by). The user swaps it for another from the layer's own picker. + // Start the layer off on a library texture rather than on nothing at all: a textureless layer + // looks broken (painting on it appears to do nothing, because there is no height map to displace + // by). The user swaps it for another from the layer's own picker. + // + // The Image tool needs that first texture to have colour in it, since colour is the only thing it + // bakes - most of the shipped library is grayscale height maps, and landing on one of those would + // open the tool on an image it then warns about. const std::vector &library = texture_library(); - if (!library.empty()) - if (const LibraryTexture *tex = get_library_texture(library.front().path)) { - layer.name = library.front().name; - layer.path = library.front().path; - layer.image_data = tex->image_data; + for (const TextureLibraryEntry &entry : library) { + const LibraryTexture *tex = get_library_texture(entry.path); + if (tex == nullptr) + continue; + if (image_mode()) { + TextureDisplacementLayer probe; + probe.image_data = tex->image_data; + if (!height_texture_has_color(probe)) + continue; } + layer.name = entry.name; + layer.path = entry.path; + layer.image_data = tex->image_data; + break; + } mv->texture_displacement_layers.push_back(std::move(layer)); // A new layer opens with every setting showing; there is nothing on it yet to hide settings from. m_layer_expanded[size_t(free_slot)] = true; @@ -5442,6 +5455,40 @@ bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv) return changed; } +// Pinned into the volume rather than applied on the way to the bake, exactly as Standard mode pins +// displace_border and smooth_enabled: the preview, the bake, the triangle estimate and the UV pane all +// read the layers straight off the volume, and a value that only some of them saw would put the +// preview and the bake out of step. +// +// The two tools share one layer stack - that is what "not a fork" means here - so opening the Image +// tool on a volume whose layers carry relief flattens them, and they stay flat when the displacement +// tool is reopened. Only that one number changes: the paint, the image, the tiling, the mapping and +// any relief already baked into the mesh are untouched. +bool GLGizmoTextureDisplacement::apply_image_mode_presets(ModelVolume *mv) +{ + if (mv == nullptr) + return false; + bool changed = false; + for (TextureDisplacementLayer &layer : mv->texture_displacement_layers) { + // Zero depth is the whole of "no relief": the bake still refines the painted area and still + // writes the colours, every layer just contributes a displacement of 0 (see + // make_combined_displacement_sampler()). Nothing special-cases it. + if (layer.depth_mm != 0.f) { + layer.depth_mm = 0.f; + changed = true; + } + // Colour is the only thing an image bake produces, so it is on wherever there is colour to + // use rather than being a checkbox. A grayscale picture leaves it off, which is what makes + // the panel's "this image has no colors" note the honest answer instead of a silent no-op. + const bool wanted = height_texture_has_color(layer); + if (layer.color_enabled != wanted) { + layer.color_enabled = wanted; + changed = true; + } + } + return changed; +} + void GLGizmoTextureDisplacement::bake_standard() { ModelVolume *mv = texture_volume(); @@ -5798,9 +5845,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float const float previous_body_h = m_panel_body_h; const float previous_footer_h = m_panel_footer_h; + // The Image tool is Standard with the relief taken out of it; there is no Pro there to switch to. + if (image_mode()) + m_panel_mode = 0; // Pinned every frame while Standard is active, so what Preview shows is always what Bake will do. if (!pro_mode() && apply_standard_mode_presets(mv)) m_preview_params_dirty = true; + if (image_mode() && apply_image_mode_presets(mv)) + m_preview_params_dirty = true; // Layout follows the Option C design: a fixed-width panel, a header pinned at the top, a body that // scrolls as one, and a footer with Bake pinned at the bottom. Rows are laid out against panel_w and @@ -5955,21 +6007,26 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float { const float x0 = ImGui::GetCursorPosX(); ImGui::AlignTextToFramePadding(); - heading(_L("Texture displacement")); + // The tool's own name, so the Image panel does not announce itself as the one it is built on. + heading(wxString::FromUTF8(get_name(false))); const std::string labels[2] = { _u8L("Standard"), _u8L("Pro") }; const float seg_pad = m_imgui->scaled(0.5f); const float seg_w[2] = { ImGui::CalcTextSize(labels[0].c_str()).x + 2.f * seg_pad, ImGui::CalcTextSize(labels[1].c_str()).x + 2.f * seg_pad }; - ImGui::SameLine(); - ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + panel_w - (seg_w[0] + seg_w[1] + gap_s + icon_sm))); - // Standard / Pro is a mode, not an option: Standard hides every mesh-preparation control and folds - // the whole recipe into Bake, Pro shows all of it and hands the ordering to the user. + // the whole recipe into Bake, Pro shows all of it and hands the ordering to the user. The Image + // tool is the restricted case of Standard, so it offers neither and the segment is left out. + const int mode_buttons = image_mode() ? 0 : 2; + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), + x0 + panel_w - icon_sm - + (mode_buttons > 0 ? seg_w[0] + seg_w[1] + gap_s : 0.f))); + const ImVec2 seg_min = ImGui::GetCursorScreenPos(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, style.ItemSpacing.y)); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f); m_imgui->disabled_begin(busy); - for (int m = 0; m < 2; ++m) { + for (int m = 0; m < mode_buttons; ++m) { if (m > 0) ImGui::SameLine(); const bool on = m_panel_mode == m; @@ -6000,10 +6057,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float } m_imgui->disabled_end(); ImGui::PopStyleVar(2); - ImGui::GetWindowDrawList()->AddRect(seg_min, ImVec2(ImGui::GetItemRectMax().x, seg_min.y + frame_h), - ImGui::GetColorU32(col_frame), style.FrameRounding); - - ImGui::SameLine(0.f, gap_s); + if (mode_buttons > 0) { + ImGui::GetWindowDrawList()->AddRect(seg_min, ImVec2(ImGui::GetItemRectMax().x, seg_min.y + frame_h), + ImGui::GetColorU32(col_frame), style.FrameRounding); + ImGui::SameLine(0.f, gap_s); + } if (icon_button(806, "canvas_drag.svg", icon_sm, m_undocked ? _L("Dock panel") : _L("Undock panel"), _L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside " "the toolbar."))) @@ -6276,7 +6334,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float // ---- Texture layers: the active layer as an open card, every other one as a single row ---- { ImGui::AlignTextToFramePadding(); - heading(_L("Texture layers")); + heading(image_mode() ? _L("Images") : _L("Texture layers")); if (mv != nullptr) { const std::string count = std::to_string(mv->texture_displacement_layers.size()) + " / " + std::to_string(TEXTURE_DISPLACEMENT_MAX_LAYERS); @@ -6451,9 +6509,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float // The three controls nearly every layer needs; everything else waits behind "More settings". // Depth and tile size are logarithmic, so both ends of their wide ranges stay usable. - m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad); - hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in " - "millimetres. Black does not move it at all, unless Midlevel says otherwise.")); + // Depth is the one the Image tool has no use for: it is pinned to 0 there, and a slider + // that must not be moved is worse than none. + if (!image_mode()) { + m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad); + hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in " + "millimetres. Black does not move it at all, unless Midlevel says otherwise.")); + } m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad); hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern " "more often and makes its detail finer; with Tile off, this is the size of the " @@ -6474,65 +6536,87 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float } // Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards; - // at 0.5 mid-grey is neutral and darker texels cut inwards. - m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad); - hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes " - "outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones " - "still push out - one image both embosses and engraves.\n\n" - "What cuts in has to fit: inside a sharp corner or through a thin wall, a deep " - "cut can pass through the other side.")); - if (layer.midlevel > 0.f && layer.depth_mm > 1.f) { - ImGui::PushTextWrapPos(content_rx - card_pad); - m_imgui->warning_text(_L("Deep inward displacement may self-intersect.")); - ImGui::PopTextWrapPos(); + // at 0.5 mid-grey is neutral and darker texels cut inwards. Every setting from here to + // Smoothing describes relief only, so the Image tool leaves the lot out. + if (!image_mode()) { + m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad); + hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes " + "outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones " + "still push out - one image both embosses and engraves.\n\n" + "What cuts in has to fit: inside a sharp corner or through a thin wall, a deep " + "cut can pass through the other side.")); + if (layer.midlevel > 0.f && layer.depth_mm > 1.f) { + ImGui::PushTextWrapPos(content_rx - card_pad); + m_imgui->warning_text(_L("Deep inward displacement may self-intersect.")); + ImGui::PopTextWrapPos(); + } } + // Blurs the image itself, so it is as much a colour control as a relief one - the Image + // tool keeps it for exactly that. m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad); hover_tip(_u8L("Blurs the image before it is used, which rounds off hard steps and removes " "speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it " "costs fine detail.")); - // Edge fade: the relief flattens toward the boundary of the painted area. - m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing); - hover_tip(_u8L("Flattens the relief as it approaches the edge of the painted area, so it " - "blends into the bare surface instead of stopping at a step.")); - ImGui::SameLine(); - m_imgui->disabled_begin(!layer.edge_smoothing); - ImGui::SetNextItemWidth(-card_pad); - m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", - ImGuiSliderFlags_AlwaysClamp); - m_imgui->disabled_end(); - hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values " - "soften a narrow band at the edge; 1 flattens almost all of it.")); + if (!image_mode()) { + // Edge fade: the relief flattens toward the boundary of the painted area. + m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing); + hover_tip(_u8L("Flattens the relief as it approaches the edge of the painted area, so it " + "blends into the bare surface instead of stopping at a step.")); + ImGui::SameLine(); + m_imgui->disabled_begin(!layer.edge_smoothing); + ImGui::SetNextItemWidth(-card_pad); + m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", + ImGuiSliderFlags_AlwaysClamp); + m_imgui->disabled_end(); + hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values " + "soften a narrow band at the edge; 1 flattens almost all of it.")); + } // Invert and Colours share a row. { const float x0 = ImGui::GetCursorPosX(); - m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Invert"), layer.invert); - hover_tip(_u8L("Turns the relief inside out: what stood out is cut in, and the other way " - "round. The same as using a negative of the image.")); - ImGui::SameLine(); - ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + 0.5f * (ImGui::GetWindowContentRegionMax().x - card_pad - x0))); + if (!image_mode()) { + m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Invert"), layer.invert); + hover_tip(_u8L("Turns the relief inside out: what stood out is cut in, and the other way " + "round. The same as using a negative of the image.")); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + 0.5f * (ImGui::GetWindowContentRegionMax().x - card_pad - x0))); + } // Only offered for a texture that actually has colour - the shipped library is grayscale, // and a checkbox that silently does nothing on nine textures out of ten is worse than no // checkbox. Disabled rather than hidden so it is clear the feature exists and what it wants. + // + // In the Image tool colour is not a choice at all - it is the only thing the bake produces - + // so the checkbox gives way to a warning on the one case that cannot work: a picture with no + // colour in it, which would bake to nothing. const bool has_color = decode_height_texture(layer).has_color(); - bool color_enabled = layer.color_enabled && has_color; - m_imgui->disabled_begin(!has_color); - if (m_imgui->bbl_checkbox(_L("Colors"), color_enabled)) { - layer.color_enabled = color_enabled; - m_preview_params_dirty = true; + const bool color_enabled = image_mode() ? has_color : (layer.color_enabled && has_color); + if (image_mode()) { + if (!has_color) { + ImGui::PushTextWrapPos(content_rx - card_pad); + m_imgui->warning_text(_L("This image is grayscale - pick one with colors in it.")); + ImGui::PopTextWrapPos(); + } + } else { + bool enable = color_enabled; + m_imgui->disabled_begin(!has_color); + if (m_imgui->bbl_checkbox(_L("Colors"), enable)) { + layer.color_enabled = enable; + m_preview_params_dirty = true; + } + m_imgui->disabled_end(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + m_imgui->tooltip(has_color ? + _u8L("Prints the painted area in the texture's colors as well as its " + "relief. Each color is matched to the nearest of your loaded " + "filaments; anything you did not paint keeps the object's own.") : + _u8L("This texture is a grayscale height map, so it has no colors to " + "apply. Import a color image to use this."), + wrap_w); } - m_imgui->disabled_end(); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - m_imgui->tooltip(has_color ? - _u8L("Prints the painted area in the texture's colors as well as its " - "relief. Each color is matched to the nearest of your loaded " - "filaments; anything you did not paint keeps the object's own.") : - _u8L("This texture is a grayscale height map, so it has no colors to " - "apply. Import a color image to use this."), - wrap_w); // The rest of colour belongs to the whole stack, not to this layer, so it only appears once - // under whichever layer turned colour on. @@ -6720,40 +6804,51 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float } } - // Blend, and placement on the model. + // Blend, and placement on the model. Blend combines *displacements*, so with every + // layer pinned to zero depth it has nothing to combine - the Image tool shows only the + // placement handle. Colour does not blend either way: a higher layer simply paints over + // a lower one where they overlap. { const float x0 = ImGui::GetCursorPosX(); - ImGui::AlignTextToFramePadding(); - ImGui::TextDisabled("%s", _u8L("Blend").c_str()); - ImGui::SameLine(); - ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); - const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement"); - const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement"); - const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement"); - const std::string blend_divide = _u8L("Divide"); - const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() }; - // The first layer has nothing before it to combine with - build_texture_displacement() makes it - // add regardless - so its combo shows Add and cannot be changed. - const bool base_layer = li == 0; - int blend_mode = base_layer ? 0 : static_cast(layer.blend_mode); - ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm)); - m_imgui->disabled_begin(base_layer); - if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) { - layer.blend_mode = static_cast(blend_mode); - m_preview_params_dirty = true; - } - m_imgui->disabled_end(); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - m_imgui->tooltip(base_layer ? - _u8L("The base layer has nothing beneath it to combine with, so it always adds.") : - _u8L("What this layer does where it overlaps the ones below. Add and " - "Subtract pile relief on or carve it away. Multiply and Divide scale " - "what is underneath, which turns this layer into a mask over it - " - "there, Depth acts as a strength, and 1 mm leaves white areas " - "untouched."), - wrap_w); + if (!image_mode()) { + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", _u8L("Blend").c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement"); + const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement"); + const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement"); + const std::string blend_divide = _u8L("Divide"); + const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() }; + // The first layer has nothing before it to combine with - build_texture_displacement() makes it + // add regardless - so its combo shows Add and cannot be changed. + const bool base_layer = li == 0; + int blend_mode = base_layer ? 0 : static_cast(layer.blend_mode); + ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm)); + m_imgui->disabled_begin(base_layer); + if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) { + layer.blend_mode = static_cast(blend_mode); + m_preview_params_dirty = true; + } + m_imgui->disabled_end(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + m_imgui->tooltip(base_layer ? + _u8L("The base layer has nothing beneath it to combine with, so it always adds.") : + _u8L("What this layer does where it overlaps the ones below. Add and " + "Subtract pile relief on or carve it away. Multiply and Divide scale " + "what is underneath, which turns this layer into a mask over it - " + "there, Depth acts as a strength, and 1 mm leaves white areas " + "untouched."), + wrap_w); - ImGui::SameLine(0.f, gap_s); + ImGui::SameLine(0.f, gap_s); + } else { + // On its own, the handle toggle needs the label the Blend row would have carried. + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", _u8L("Placement").c_str()); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + } if (icon_toggle(842, "texture_displacement_adjust.svg", m_adjust_texture_mode, icon_sm, _L("Adjust placement"), _L("Adjust placement - drag the handle on the model to move the texture"))) { if (!m_adjust_texture_mode) { @@ -6847,9 +6942,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float hover_tip(_u8L("Pick a different image for this layer. The paint, depth and tiling stay as they are.")); ImGui::SameLine(); + // Depth is what a closed relief layer is summarised by; with it pinned to zero that says + // nothing, so the Image tool reports the one state a closed row still has. char depth_text[32]; std::snprintf(depth_text, sizeof(depth_text), "%.3f mm", layer.depth_mm); - const std::string summary = painted ? std::string(depth_text) : _u8L("not painted"); + const std::string summary = !painted ? _u8L("not painted") : + image_mode() ? _u8L("painted") : + std::string(depth_text); const float sum_w = ImGui::CalcTextSize(summary.c_str()).x; const float right_edge = ImGui::GetWindowContentRegionMax().x - inner; const float name_w = std::max(0.f, right_edge - ImGui::GetCursorPosX() - 2.f * gap_s - sum_w - frame_h); @@ -6903,7 +7002,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float char depth_text[32]; std::snprintf(depth_text, sizeof(depth_text), "%.3f mm", l.depth_mm); - const std::string summary = slot_painted(l.slot) ? std::string(depth_text) : _u8L("not painted"); + const std::string summary = !slot_painted(l.slot) ? _u8L("not painted") : + image_mode() ? _u8L("painted") : + std::string(depth_text); const float sum_w = ImGui::CalcTextSize(summary.c_str()).x; const float text_y = mn.y + 0.5f * (row_h - ImGui::GetFontSize()); fg->AddText(ImVec2(mx.x - inner - sum_w, text_y), ImGui::GetColorU32(ImGuiCol_TextDisabled), summary.c_str()); @@ -6914,8 +7015,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float if (mv != nullptr) { const bool full = mv->texture_displacement_layers.size() >= TEXTURE_DISPLACEMENT_MAX_LAYERS; - const std::string label = full ? Slic3r::format(_u8L("All %1% layers used"), TEXTURE_DISPLACEMENT_MAX_LAYERS) : - "+ " + _u8L("Add layer"); + const std::string label = full ? Slic3r::format(_u8L("All %1% layers used"), TEXTURE_DISPLACEMENT_MAX_LAYERS) : + image_mode() ? "+ " + _u8L("Add image") : + "+ " + _u8L("Add layer"); m_imgui->disabled_begin(busy || full); if (ImGui::Button((label + "##add_layer").c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0.f))) add_texture_layer(); @@ -7305,6 +7407,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float hover_tip(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, " "budget %2% k. Untick Auto to set them yourself."), rec.edge_mm, rec.budget_k)); + else if (image_mode()) + // Same number, different thing to look at: with no relief to carry, the mesh under the + // paint is only ever as fine as the color edges printed along it need it to be. + hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. Each triangle " + "prints in one filament, so this is how sharp an edge between two colors " + "can come out. Smaller costs triangles fast: halving it needs four times " + "as many, and once they no longer fit the budget the bake simplifies back " + "down and the sharpness goes with it.")); else hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. It has to be " "smaller than the detail you want out of the texture - a 0.5 mm groove needs " @@ -7385,7 +7495,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float ImGui::SameLine(x0 + panel_w - button_w * 2 - style.ItemSpacing.x); - const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted(); + // Colour is the only thing an image bake produces, so with no colouring layer there would be + // nothing to show for it - and it is not a cheap no-op: the bake would still refine the painted + // area and replace the mesh with the result. + const bool no_colors = image_mode() && mv != nullptr && !any_layer_colors(*mv); + const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted() && !no_colors; const std::string bake_label = m_prepare_in_progress ? _u8L("Preparing...") : m_bake_in_progress ? _u8L("Baking...") : into_u8(m_desc.at("bake")); @@ -7410,6 +7524,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "strokes on the model mark seams instead of painting - turn " "it off in the pane to paint.") : _u8L("Nothing is painted yet.")) : + no_colors ? + _u8L("None of the images have colors in them, so there is nothing to print. Pick a " + "color picture.") : + image_mode() ? + _u8L("Prints the painted area in the picture's colors. Each color is matched to the " + "nearest of your loaded filaments; the surface itself is left exactly where it " + "is, and anything you did not paint keeps the object's own color.") : pro_mode() ? _u8L("Turn the painted height maps into real geometry, by moving the vertices that are " "already there. Use Subdivide first if the mesh is too coarse to show the detail.") : diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 48e0ab72f6..d369b1dc75 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -204,6 +204,14 @@ public: bool on_mouse(const wxMouseEvent &mouse_event) override; protected: + // True in the Image tool (see GLGizmoImage): this same gizmo restricted to the one case where + // the surface does not move at all - a colour picture laid on the model. Everything behind the + // panel is shared and unchanged: the same layers, the same paint masks, the same bake. This flag + // only tells the panel which of the two tools it is drawing, and makes it pin the values the + // restricted case fixes (see apply_image_mode_presets()). + virtual bool image_mode() const { return false; } + + bool on_init() override; void on_render_input_window(float x, float y, float bottom_limit) override; std::string on_get_name() const override; // Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn @@ -221,8 +229,11 @@ protected: EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; } EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; } + // Paint / Erase, from the panel. See get_left_button_state_type(). Protected because a subclass + // overriding handle_snapshot_action_name() has to read the same flag to name the undo step. + bool m_erase_mode = false; + private: - bool on_init() override; void update_model_object() override; void update_from_model_object(bool first_update) override; void on_opening() override {} @@ -262,6 +273,11 @@ private: // Standard is active so what Preview shows is always what Bake will do. Returns true if it actually // changed something, so the caller can invalidate the preview. bool apply_standard_mode_presets(ModelVolume *mv); + // Image mode's fixed values, on top of Standard's and pinned the same way (every frame, so the + // preview cannot disagree with the bake): zero depth on every layer, and colour on wherever the + // picture has any. Those two are what turn the displacement bake into "put this picture on the + // model" without a second code path. Returns true if it actually changed something. + bool apply_image_mode_presets(ModelVolume *mv); // Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace. // The order matters and is the whole reason this is one button - a height map can only move // existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop @@ -399,8 +415,6 @@ private: void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry); void import_custom_texture(TextureDisplacementLayer &layer); - // Paint / Erase, from the panel. See get_left_button_state_type(). - bool m_erase_mode = false; // Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation. // Panel state only, not saved with the project. std::array m_layer_expanded{}; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index d840754ac2..bedc9c97ed 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -22,6 +22,7 @@ //#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoImage.hpp" #include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" #include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" @@ -201,6 +202,11 @@ void GLGizmosManager::switch_gizmos_icon_filename() // light/dark switch. gizmo->set_icon_filename("toolbar_texture_displacement.svg"); break; + case(EType::Image): + // Drawn flat in Orca's menu-icon palette, which reads on either background, so one file + // serves both themes here too. + gizmo->set_icon_filename("toolbar_image.svg"); + break; case(EType::MeshBoolean): gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg"); break; @@ -252,6 +258,8 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation)); // One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes. m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement)); + // The same gizmo restricted to the zero-displacement case; see GLGizmoImage. + m_gizmos.emplace_back(new GLGizmoImage(m_parent, "toolbar_image.svg", EType::Image)); m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss)); m_gizmos.emplace_back(new GLGizmoSVG(m_parent)); m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure)); @@ -585,6 +593,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == TextureDisplacement) return dynamic_cast(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == Image) + return dynamic_cast(m_gizmos[Image].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == MeshBoolean) return dynamic_cast(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == BrimEars) @@ -599,6 +609,7 @@ bool GLGizmosManager::is_paint_gizmo() m_current == EType::MmSegmentation || m_current == EType::FuzzySkin || m_current == EType::TextureDisplacement || + m_current == EType::Image || m_current == EType::Seam; } @@ -1550,6 +1561,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type) return "Fuzzy Skin Painting"; case GLGizmosManager::EType::TextureDisplacement: return "Texture Displacement"; + case GLGizmosManager::EType::Image: + return "Image"; default: return ""; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 0914f8d958..85be679d71 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -96,6 +96,7 @@ public: FuzzySkin, MmSegmentation, TextureDisplacement, + Image, Emboss, Svg, Measure, diff --git a/src/slic3r/GUI/Shortcuts.cpp b/src/slic3r/GUI/Shortcuts.cpp index df8d7c00ca..c8d3e71345 100644 --- a/src/slic3r/GUI/Shortcuts.cpp +++ b/src/slic3r/GUI/Shortcuts.cpp @@ -114,6 +114,9 @@ constexpr std::array shortcut_table = {{ SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }), SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }), SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }), + // Unbound by default: every single letter this context can reach is already taken, and a default + // that shadows one of them is worse than none. The shortcuts dialog lists it for binding. + SHORTCUT(GizmoImage, "gizmo_image", L("Gizmo image painting"), PLATER, KeyChord{}), // Sliders SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }), diff --git a/src/slic3r/GUI/Shortcuts.hpp b/src/slic3r/GUI/Shortcuts.hpp index aba315d521..311d2fb914 100644 --- a/src/slic3r/GUI/Shortcuts.hpp +++ b/src/slic3r/GUI/Shortcuts.hpp @@ -38,7 +38,7 @@ enum class Shortcut : uint8_t { MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight, // Gizmos GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin, - GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement, + GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement, GizmoImage, // Sliders GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd, // Painting tools