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