diff --git a/resources/images/toolbar_image.svg b/resources/images/toolbar_image.svg
index fb0830814c..3f68aa3577 100644
--- a/resources/images/toolbar_image.svg
+++ b/resources/images/toolbar_image.svg
@@ -1 +1 @@
-
+
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp
index e0d36f8510..8f337f2e04 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp
@@ -11,6 +11,7 @@
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
+#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp"
@@ -19,6 +20,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -52,6 +54,31 @@ wxString GLGizmoImage::handle_snapshot_action_name(bool shift_down, GLGizmoPaint
return erasing ? _L("Remove image from the area") : _L("Place image");
}
+bool GLGizmoTextureDisplacement::drop_image_file(const std::string &path)
+{
+ if (!image_mode() || texture_volume() == nullptr)
+ return false;
+
+ // Imported rather than just read: the same thing the panel's Import tile does, so a dropped
+ // picture lands in "My images" and is there the next time without hunting for the file again.
+ std::string error;
+ const std::optional entry = import_texture_to_library(path, error);
+ if (!entry) {
+ show_error(nullptr, error);
+ return false;
+ }
+
+ // The tool starts with no layer at all, so a drop before anything is loaded has to make one.
+ if (active_layer() == nullptr)
+ add_texture_layer();
+ TextureDisplacementLayer *layer = active_layer();
+ if (layer == nullptr)
+ return false;
+ set_layer_texture(*layer, *entry);
+ m_parent.set_as_dirty();
+ return true;
+}
+
// ---------------------------------------------------------------------------------------------
// The Image tool's window.
//
@@ -226,6 +253,16 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo
// 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();
+ const bool placed = mv != nullptr && mv->is_texture_displacement_painted();
+
+ // The handle comes up by itself the first moment there is something to anchor it to, so the
+ // picture can be nudged straight after the click that placed it without hunting for a toggle.
+ // Armed once: turning it back off has to stay off, not be undone on the next frame.
+ if (has_picture && placed && !m_image_handle_armed) {
+ m_image_handle_armed = true;
+ if (update_adjust_anchor())
+ m_adjust_texture_mode = true;
+ }
if (!has_picture) {
const float box_h = std::round(panel_w * 0.6f);
const ImVec2 p0 = ImGui::GetCursorScreenPos();
@@ -282,9 +319,8 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo
// the angle below. No brush, no erase toggle - on a flat side or a cube face the click alone
// is the whole gesture, and the slider is there for the cases where it is not.
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
- ImGui::TextDisabled("%s", mv != nullptr && mv->is_texture_displacement_painted() ?
- _u8L("Click the model to move the picture somewhere else.").c_str() :
- _u8L("Click the model to put the picture there.").c_str());
+ ImGui::TextDisabled("%s", placed ? _u8L("Click the model to move the picture somewhere else.").c_str() :
+ _u8L("Click the model to put the picture there.").c_str());
ImGui::PopTextWrapPos();
float fill_angle = m_smart_fill_angle;
if (float_row("##fill_angle", _L("Spread"), &fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false))
@@ -461,7 +497,7 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo
ImGui::Separator();
{
- const bool painted = mv != nullptr && mv->is_texture_displacement_painted();
+ const bool painted = placed;
const bool no_colors = mv != nullptr && !any_layer_colors(*mv);
// Colour is the only thing this bake produces, so with a grayscale picture there would be
// nothing to show for it - and it is not a cheap no-op: the bake still refines the painted
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
index 5ccaf87240..1adf4767c3 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
@@ -557,6 +557,7 @@ void GLGizmoTextureDisplacement::on_shutdown()
m_seam_edit_mode = false;
m_adjust_drag_handle = AdjustHandle::None;
m_adjust_anchor_valid = false;
+ m_image_handle_armed = false;
// The pane is request-only: closing the gizmo drops the request, so reopening it later doesn't
// silently reopen the pane too.
m_show_uv_editor = false;
@@ -761,8 +762,16 @@ bool GLGizmoTextureDisplacement::on_mouse(const wxMouseEvent &mouse_event)
{
if (m_seam_edit_mode)
return on_mouse_seam(mouse_event);
- if (m_adjust_texture_mode)
- return on_mouse_adjust_texture(mouse_event);
+ if (m_adjust_texture_mode) {
+ if (on_mouse_adjust_texture(mouse_event))
+ return true;
+ // The displacement tool turns this mode on deliberately, so there a click off the handle
+ // doing nothing is the point. The Image tool has it on from the start, so the same click has
+ // to fall through and place the picture, or turning it on would disable the tool's one
+ // gesture.
+ if (!image_mode())
+ return false;
+ }
const bool handled = GLGizmoPainterBase::on_mouse(mouse_event);
// A consumed drag/click is a paint (or erase) event: the base class has already updated the live
@@ -3590,6 +3599,11 @@ void GLGizmoTextureDisplacement::update_model_object()
wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin());
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
rebuild_preview();
+ // The on-canvas handle is anchored to the paint patch, so a stroke that moved the paint has
+ // to move it with them. Matters most in the Image tool, where the handle is on from the
+ // start and a click moves the picture to a different part of the model.
+ if (m_adjust_texture_mode)
+ update_adjust_anchor();
}
}
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
index 8e3d655b62..58cac77757 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
@@ -222,6 +222,11 @@ public:
// rotation handles instead of painting); otherwise forwards to the normal painting handling.
bool on_mouse(const wxMouseEvent &mouse_event) override;
+ // Loads `path` as the Image tool's picture, importing it into the user's library on the way so
+ // it is there next time. For a drop onto the 3D view; see PlaterDropTarget::OnDropFiles(). False
+ // when this is not the Image tool, or the file could not be read.
+ bool drop_image_file(const std::string &path);
+
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
@@ -302,6 +307,9 @@ private:
// only so that it can reach the same private machinery the displacement panel uses, and keeping
// it out of this file leaves that panel's 1700 lines alone.
void render_image_input_window(float x, float y, float bottom_limit);
+ // Set once the Image tool has switched its placement handle on for the current picture, so that
+ // turning the handle back off stays off instead of being re-armed on the next frame.
+ bool m_image_handle_armed = false;
// 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
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 8339e4d9d9..66e8b3396d 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -67,6 +67,7 @@
#include "slic3r/GUI/Field.hpp"
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
#include
+#include
#include "slic3r/GUI/Widgets/StaticLine.hpp"
#include "slic3r/GUI/GUI_ObjectSettings.hpp"
#include "slic3r/GUI/ColorDecomposeDialog.hpp"
@@ -246,6 +247,7 @@
#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles
#include "ProjectDirtyStateManager.hpp"
#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification
+#include "Gizmos/GLGizmoImage.hpp" // Drop an image onto the Image tool
#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file
#include "Gizmos/GizmoObjectManipulation.hpp"
@@ -7125,6 +7127,25 @@ private:
};
namespace {
+// True when `file` is a picture and the Image gizmo is the one currently open, in which case it has
+// taken the file. Mirrors emboss_svg() below, which does the same for an SVG and the text tool.
+bool drop_image_on_gizmo(Plater &plater, const wxString &file)
+{
+ static const std::set image_ext = { "png", "jpg", "jpeg", "bmp" };
+ const wxString ext = wxFileName(file).GetExt().Lower();
+ if (image_ext.count(ext) == 0)
+ return false;
+
+ GLCanvas3D *canvas = plater.canvas3D();
+ if (canvas == nullptr)
+ return false;
+ GLGizmosManager &gizmos = canvas->get_gizmos_manager();
+ if (gizmos.get_current_type() != GLGizmosManager::Image)
+ return false;
+ auto *image = dynamic_cast(gizmos.get_gizmo(GLGizmosManager::Image));
+ return image != nullptr && image->drop_image_file(into_u8(file));
+}
+
bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position)
{
std::string svg_file_str = into_u8(svg_file);
@@ -7795,6 +7816,11 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi
if (wxGetApp().is_editor())
m_plater.select_view_3D("3D");
+ // A picture dropped while the Image tool is open is that tool's input, not a model to load. Only
+ // then: with the tool closed a dropped PNG goes on doing whatever it did before.
+ if (filenames.size() == 1 && drop_image_on_gizmo(m_plater, filenames.Last()))
+ return true;
+
// When only one .svg file is dropped on scene
if (filenames.size() == 1) {
const wxString &filename = filenames.Last();