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)