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();