diff --git a/resources/images/toolbar_image.svg b/resources/images/toolbar_image.svg index 0cb9c793c6..fb0830814c 100644 --- a/resources/images/toolbar_image.svg +++ b/resources/images/toolbar_image.svg @@ -1 +1 @@ - + diff --git a/resources/shaders/110/texture_displacement_shaded.fs b/resources/shaders/110/texture_displacement_shaded.fs index 0b9192749c..6737b6e560 100644 --- a/resources/shaders/110/texture_displacement_shaded.fs +++ b/resources/shaders/110/texture_displacement_shaded.fs @@ -51,6 +51,11 @@ uniform float tex_aspect; uniform float rotation_rad; uniform vec2 uv_offset; uniform bool invert; +// False means the layer places a single copy rather than repeating: outside that copy there is +// simply no texture. DecodedHeightTexture::sample() has always returned 0 there, so the bake got +// this right while the preview wrapped the image and showed a tiled pattern that was never going +// to be printed. +uniform bool tile_enabled; uniform float midlevel; // the height that means "don't move"; needed by the parallax step uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor) uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin @@ -218,6 +223,14 @@ int nearest_palette_entry(vec3 rgb) return best; } +// True where this uv has texture on it: everywhere when the layer repeats, and only inside the +// single placed copy when it does not. The bake's own sampler draws the same line (see +// DecodedHeightTexture::sample), so the preview now stops at the same edge it does. +bool in_tile(vec2 uv) +{ + return tile_enabled || (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0); +} + void main() { if (any(lessThan(clipping_planes_dots, ZERO))) @@ -243,9 +256,9 @@ void main() ? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr : vertex_uv; color_uv = uv; - have_uv = true; + have_uv = in_tile(uv); float h = texture2D(height_tex, uv).r; - float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); + float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * (have_uv ? 1.0 : 0.0); vec3 sigmaS = dFdx(world_pos.xyz); vec3 sigmaT = dFdy(world_pos.xyz); vec3 R1 = cross(sigmaT, triangle_normal); @@ -262,10 +275,12 @@ void main() // Parallax occlusion mapping: march the view ray through the height shell and shade at the // first point where it drops below the displaced surface (see header). - float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); + // uv first: outside a single placed copy there is no shell to march through. + vec2 uv = project_uv(tex_pos, triangle_normal); + float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * + (in_tile(uv) ? 1.0 : 0.0); vec3 view_dir = normalize(eye_model_pos - tex_pos); float v_dot_n = dot(view_dir, triangle_normal); - vec2 uv = project_uv(tex_pos, triangle_normal); // The shell the displaced surface lives inside, as signed heights along the normal. Taken from // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel, @@ -302,7 +317,7 @@ void main() } color_uv = uv; // after the parallax march, so colour and relief stay registered - have_uv = true; + have_uv = in_tile(uv); float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r; float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r; @@ -311,7 +326,8 @@ void main() vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y)); float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; - float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0); + float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0) * + (have_uv ? 1.0 : 0.0); float cs = cos(rotation_rad); float sn = sin(rotation_rad); diff --git a/resources/shaders/140/texture_displacement_shaded.fs b/resources/shaders/140/texture_displacement_shaded.fs index 911a919aaf..b5c68c0069 100644 --- a/resources/shaders/140/texture_displacement_shaded.fs +++ b/resources/shaders/140/texture_displacement_shaded.fs @@ -110,6 +110,11 @@ uniform float tex_aspect; uniform float rotation_rad; uniform vec2 uv_offset; uniform bool invert; +// False means the layer places a single copy rather than repeating: outside that copy there is +// simply no texture. DecodedHeightTexture::sample() has always returned 0 there, so the bake got +// this right while the preview wrapped the image and showed a tiled pattern that was never going +// to be printed. +uniform bool tile_enabled; uniform float midlevel; // the height that means "don't move"; needed by the parallax step uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor) uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin @@ -284,6 +289,14 @@ int nearest_palette_entry(vec3 rgb) return best; } +// True where this uv has texture on it: everywhere when the layer repeats, and only inside the +// single placed copy when it does not. The bake's own sampler draws the same line (see +// DecodedHeightTexture::sample), so the preview now stops at the same edge it does. +bool in_tile(vec2 uv) +{ + return tile_enabled || (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0); +} + void main() { if (any(lessThan(clipping_planes_dots, ZERO))) @@ -321,9 +334,9 @@ void main() ? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr : vertex_uv; color_uv = uv; - have_uv = true; + have_uv = in_tile(uv); float h = texture(height_tex, uv).r; - float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); + float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * (have_uv ? 1.0 : 0.0); vec3 sigmaS = dFdx(world_pos.xyz); vec3 sigmaT = dFdy(world_pos.xyz); vec3 R1 = cross(sigmaT, triangle_normal); @@ -343,10 +356,12 @@ void main() // Parallax occlusion mapping: march the view ray through the height shell and shade at the // first point where it drops below the displaced surface (see header). - float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); + // uv first: outside a single placed copy there is no shell to march through. + vec2 uv = project_uv(tex_pos, triangle_normal); + float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * + (in_tile(uv) ? 1.0 : 0.0); vec3 view_dir = normalize(eye_model_pos - tex_pos); float v_dot_n = dot(view_dir, triangle_normal); - vec2 uv = project_uv(tex_pos, triangle_normal); // The shell the displaced surface lives inside, as signed heights along the normal. Taken from // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel, @@ -383,7 +398,7 @@ void main() } color_uv = uv; // after the parallax march, so colour and relief stay registered - have_uv = true; + have_uv = in_tile(uv); float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r; float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r; @@ -396,7 +411,8 @@ void main() // uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space // gradient into a real surface slope. float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; - float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0); + float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0) * + (have_uv ? 1.0 : 0.0); // uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the // gradient back into the axes' frame. float cs = cos(rotation_rad); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp index 8ef041ec13..e0d36f8510 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp @@ -103,11 +103,23 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo const float approx_height = m_imgui->scaled(22.f); y = std::min(y, bottom_limit - approx_height); - GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f); + + // Docked (the default) it is pinned to the right edge of the 3D canvas and cannot be moved, the + // same place and the same rule as the displacement panel, so the two tools do not put their + // controls in different corners. Undocked it becomes an ordinary floating window with a title + // bar to drag it by; the position is deliberately not seeded on undock, so it stays exactly + // where it was and the user simply gains the ability to move it. + ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse; + if (!m_undocked) { + flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar; + // Right-aligned (pivot 1), so the width the window auto-resized to last frame does not need + // to be known here. + float right = float(m_parent.get_canvas_size().get_width()) - m_imgui->scaled(0.5f); + GizmoImguiSetNextWIndowPos(right, y, 0.f, 0.f, ImGuiCond_Always, 1.0f, 0.0f); + } ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); - GizmoImguiBegin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); + GizmoImguiBegin(get_name(), flags); ensure_panel_icons(); const ImGuiStyle &style = ImGui::GetStyle(); @@ -138,6 +150,28 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo return ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0)); }; + // Borderless icon button, for the dock toggle: always the grey monochrome variant. + const auto icon_button = [&](int uid, const std::string &iconfile, const wxString &label, + const wxString &tip_text) -> bool { + const auto it = m_panel_icon_map.find(iconfile); + const float pad = std::max(1.f, std::round(icon_sm * 0.14f)); + bool clicked = false; + ImGui::PushID(uid); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f)); + if (it != m_panel_icon_map.end() && !it->second.empty() && it->second[0]->is_valid()) { + const IconManager::Icon &ic = *it->second[0]; + clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id, + ImVec2(icon_sm - 2.f * pad, icon_sm - 2.f * pad), ic.tl, ic.br, int(pad)); + } else { + clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, icon_sm)); + } + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::PopID(); + hover_tip(tip_text); + return clicked; + }; // Framed icon toggle, the displacement panel's one cut down to what this window needs. const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, const wxString &label, const wxString &tip_text) -> bool { @@ -166,9 +200,19 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo TextureDisplacementLayer *layer = mv != nullptr ? active_layer() : nullptr; - m_imgui->push_bold_font(); - m_imgui->text(wxString::FromUTF8(get_name(false))); - m_imgui->pop_bold_font(); + { + const float x0 = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + m_imgui->push_bold_font(); + m_imgui->text(wxString::FromUTF8(get_name(false))); + m_imgui->pop_bold_font(); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + panel_w - icon_sm)); + if (icon_button(920, "canvas_drag.svg", 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 " + "against the right edge."))) + m_undocked = !m_undocked; + } ImGui::Separator(); // The picture, shown whole and the full width of the window: it is the thing being placed, so a @@ -179,7 +223,10 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo m_picker_slot = slot; m_picker_open_request = true; }; - if (layer == nullptr) { + // A layer with no picture on it is the starting state, not a broken one: the tool creates the + // layer empty and the panel stays on the frame below until a picture has been chosen. + const bool has_picture = layer != nullptr && !layer->empty(); + if (!has_picture) { const float box_h = std::round(panel_w * 0.6f); const ImVec2 p0 = ImGui::GetCursorScreenPos(); m_imgui->disabled_begin(busy || mv == nullptr); @@ -195,8 +242,9 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo dl->AddLine(ImVec2(cx, cy - arm), ImVec2(cx, cy + arm), col, thick); hover_tip(_u8L("Choose a picture to put on the model.")); if (clicked) { - add_texture_layer(); // seeds the first library image that has colour in it - open_library(m_active_layer_slot); // ... and offers the library, Import included, at once + if (layer == nullptr) + add_texture_layer(); // created with no picture on it, on purpose + open_library(layer != nullptr ? layer->slot : m_active_layer_slot); } ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w); ImGui::TextDisabled("%s", _u8L("Choose a picture, then click the model to put it there.").c_str()); @@ -448,8 +496,8 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo m_imgui->pop_bold_font(); GLGizmoUtils::pop_orca_button_style(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - m_imgui->tooltip(layer == nullptr ? _u8L("Load a picture first.") : - !painted ? _u8L("Click the model to place the picture first.") : + m_imgui->tooltip(!has_picture ? _u8L("Choose a picture first.") : + !painted ? _u8L("Click the model to place the picture first.") : no_colors ? _u8L("This picture is grayscale, so there is nothing to print. " "Choose a color picture.") : _u8L("Prints the picture where you placed it, in the nearest of your " diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 431b3e6ed1..5ccaf87240 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -1564,6 +1564,9 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh() shader->set_uniform("height_tex", 0); shader->set_uniform("height_tex_texel", Vec2f(1.f / float(tex->get_width()), 1.f / float(tex->get_height()))); shader->set_uniform("depth_mm", layer->depth_mm); + // Without this the preview wrapped the image and showed a repeating pattern where the bake + // places a single copy. + shader->set_uniform("tile_enabled", layer->tile_enabled); shader->set_uniform("tiling_scale", layer->tiling_scale); // Read off the uploaded texture rather than the decoded one: they are the same image, and this is // the aspect the sampler will actually see. @@ -3724,31 +3727,26 @@ void GLGizmoTextureDisplacement::add_texture_layer() TextureDisplacementLayer layer; layer.slot = free_slot; - // 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. + // 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. // - // 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(); - 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; - // One copy, not a repeating pattern: a photo or a logo is placed once. The panel's Tile - // checkbox is there for the cases that do want it, so this is a default and not a pin. - layer.tile_enabled = false; - } - layer.name = entry.name; - layer.path = entry.path; - layer.image_data = tex->image_data; - break; + // Not in the Image tool: there the layer deliberately starts with no picture at all, so the panel + // stays on its empty frame until the user has chosen one. Picking a shipped height map for them + // would be choosing the wrong thing twice over - it is not their picture, and most of that + // library is grayscale, which an image bake cannot print. + if (image_mode()) { + // One copy, not a repeating pattern: a photo or a logo is placed once. The panel's Tile + // checkbox is there for the cases that do want it, so this is a default and not a pin. + layer.tile_enabled = false; + } else { + 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; + } } 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. @@ -3944,15 +3942,19 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane return n; }; - ImGui::TextDisabled("%s", _u8L("Built-in").c_str()); - { + // The shipped library is grayscale height maps, which is what the displacement tool wants and the + // opposite of what an image bake can use: it prints colour, and a height map has none. So the + // Image tool offers only the user's own pictures, and the section is left out entirely rather + // than shown full of things that would all warn when picked. + if (!image_mode()) { + ImGui::TextDisabled("%s", _u8L("Built-in").c_str()); const ImVec2 origin = ImGui::GetCursorScreenPos(); const int count = grid(origin, false); end_grid(origin, count); if (count == 0) ImGui::TextDisabled("%s", _u8L("No textures found.").c_str()); } - ImGui::TextDisabled("%s", _u8L("My textures").c_str()); + ImGui::TextDisabled("%s", image_mode() ? _u8L("My images").c_str() : _u8L("My textures").c_str()); { const ImVec2 origin = ImGui::GetCursorScreenPos(); const int count = grid(origin, true); @@ -3960,15 +3962,20 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane if (ImGui::Button("+##import_texture", ImVec2(tile, tile))) import_clicked = true; if (ImGui::IsItemHovered()) - m_imgui->tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer " - "bakes from and saved to your personal texture folder, kept separate from the " - "textures shipped with OrcaSlicer."), + m_imgui->tooltip(image_mode() ? + _u8L("Import a picture from your computer. Its colors are kept, and it is saved to " + "your personal folder so it is here next time.") : + _u8L("Import your own image as a height map. It is converted to the format the " + "slicer bakes from and saved to your personal texture folder, kept separate " + "from the textures shipped with OrcaSlicer."), m_imgui->scaled(20.f)); end_grid(origin, count + 1); } ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w); - ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str()); + ImGui::TextDisabled("%s", image_mode() ? + _u8L("PNG, JPG or BMP. Hover one to remove it.").c_str() : + _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str()); ImGui::PopTextWrapPos(); if (chosen || import_clicked)