diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 0241692e7b..d5895603e6 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include #include #include @@ -64,6 +63,7 @@ #include #include #include +#include #include #include #include @@ -3346,16 +3346,34 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) add_error("Malformed texture displacement data: " + json_path); return; } + // A layer the archive carries no image for - saved while its texture had not been loaded, or + // written by a build that stored only the path - falls back to the file it came from. The + // importer copies every texture into the user's own library, so that path resolves on the + // machine the project was made on and quietly resolves nowhere else, which is the old + // behaviour: settings restored, pixels not. + const auto load_image_from_disk = [](TextureDisplacementLayer &layer) { + boost::system::error_code ec; + if (layer.path.empty() || !boost::filesystem::is_regular_file(layer.path, ec) || ec) + return; + boost::nowide::ifstream file(layer.path, std::ios::binary); + if (!file.good()) + return; + std::vector bytes((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (!bytes.empty()) + layer.image_data = std::make_shared>(std::move(bytes)); + }; + for (TextureDisplacementLayer &layer : layers) { - if (layer.path_in_3mf.empty()) - continue; - const auto image = m_texture_displacement_files.find(layer.path_in_3mf); - if (image == m_texture_displacement_files.end()) { + if (!layer.path_in_3mf.empty()) { + if (const auto image = m_texture_displacement_files.find(layer.path_in_3mf); + image != m_texture_displacement_files.end()) { + layer.image_data = std::make_shared>(image->second.begin(), + image->second.end()); + continue; + } add_error("Missing texture displacement image: " + layer.path_in_3mf); - continue; } - layer.image_data = std::make_shared>(image->second.begin(), - image->second.end()); + load_image_from_disk(layer); } volume.texture_displacement_layers = std::move(layers); volume.texture_displacement_options = options; @@ -8183,12 +8201,10 @@ static void add_texture_displacement(std::stringstream &stream, const ModelVolum layer.path_in_3mf.clear(); if (!layer.image_data || layer.image_data->empty()) continue; - std::string ext = boost::filesystem::path(layer.path).extension().string(); - boost::to_lower(ext); - if (ext != ".png" && ext != ".jpg" && ext != ".jpeg") - ext = ".png"; // the library ships PNG; anything unrecognised is stored under a type a reader expects - const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ext; - // No deflate: PNG and JPEG are already compressed, so a second pass only costs time. + // Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit + // greyscale PNG, so image_data is PNG whatever layer.path happens to be named. + const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png"; + // No deflate: a PNG is already compressed, so a second pass only costs time. if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(), MZ_NO_COMPRESSION)) layer.path_in_3mf = path; diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index c89ee713e3..1b910e1006 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -5207,6 +5207,8 @@ std::string texture_displacement_layers_to_json(const std::vector out_layers; diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index 7c724b900b..c6ebe6c416 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -412,12 +412,19 @@ struct TextureDisplacementOptions // finer than the mesh leaves behind, without eating features that are genuinely a facet wide. int color_despeckle = 2; + // Where the panel was left, rather than anything the bake reads. It lives with the volume because + // that is what it describes: reopening a project should put the user back on the layer they were + // editing, in the mode they were editing it in, not on slot 0 in Standard. + int active_slot = 0; + int panel_mode = 0; // 0 Standard, 1 Pro + template void serialize(Archive &ar) { int mix_mode = int(color_mix_mode); ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border, pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, - v2_relocate, color_mix_enabled, mix_mode, color_despeckle); + v2_relocate, v2_flip_edges, color_mix_enabled, mix_mode, color_despeckle, + active_slot, panel_mode); color_mix_mode = ColorMixMode(mix_mode); } }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index c467a7768d..3a4a32d00b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -3329,6 +3329,7 @@ const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const void GLGizmoTextureDisplacement::update_model_object() { + store_panel_state(); bool updated = false; ModelObject *mo = m_c->selection_info()->model_object(); int idx = -1; @@ -3364,6 +3365,24 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update) { wxBusyCursor wait; + // Opening the gizmo puts the panel back where it was left: the layer and the mode come off the + // volume, because they are the user's place in this model's stack, and the dock state off the + // application config, because it is about their window. Only on open - this also runs on an + // undo/redo and on a layer switch, where overriding what the user is doing now would be wrong. + if (first_update) { + if (const ModelVolume *mv = texture_volume(); mv != nullptr) { + const TextureDisplacementOptions &opts = mv->texture_displacement_options; + m_panel_mode = std::clamp(opts.panel_mode, 0, 1); + // Only if that slot still holds a layer: the stack can have been edited by anything that + // does not go through the panel, and a selection pointing at nothing shows an empty panel. + const auto &stack = mv->texture_displacement_layers; + if (std::any_of(stack.begin(), stack.end(), + [&opts](const TextureDisplacementLayer &l) { return l.slot == opts.active_slot; })) + m_active_layer_slot = opts.active_slot; + } + m_undocked = wxGetApp().app_config->get_bool(UNDOCKED_CONFIG_KEY); + } + const ModelObject *mo = m_c->selection_info()->model_object(); m_triangle_selectors.clear(); @@ -3409,6 +3428,14 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update) rebuild_preview(); } +void GLGizmoTextureDisplacement::store_panel_state() +{ + if (ModelVolume *mv = texture_volume(); mv != nullptr) { + mv->texture_displacement_options.active_slot = m_active_layer_slot; + mv->texture_displacement_options.panel_mode = m_panel_mode; + } +} + void GLGizmoTextureDisplacement::set_active_layer(int slot) { if (slot == m_active_layer_slot) @@ -3417,6 +3444,7 @@ void GLGizmoTextureDisplacement::set_active_layer(int slot) // reflect - otherwise they would be silently lost. update_model_object(); m_active_layer_slot = slot; + store_panel_state(); update_from_model_object(false); // The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync // instead of leaving it pointing at the previous layer's (now stale) paint patch. @@ -5590,6 +5618,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "not what you wanted and you know why.")); if (clicked && !on) { m_panel_mode = m; + store_panel_state(); if (!pro_mode()) { // Leaving the subdivision preview open would strand a wireframe whose controls just // disappeared, so close it as part of the switch. @@ -5612,6 +5641,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float _L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside " "the toolbar."))) m_undocked = !m_undocked; + wxGetApp().app_config->set_bool(UNDOCKED_CONFIG_KEY, m_undocked); } ImGui::Separator(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 2c6f5bc6c7..32dd66c99c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -206,6 +206,9 @@ 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); + // Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file + // can carry it. Cheap and idempotent - called wherever either of the two changes. + void store_panel_state(); // 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 @@ -904,9 +907,12 @@ private: PatchUnwrap m_uv_editor_unwrap; // When set, the panel is a free-floating window the user can drag anywhere (with a title bar to - // grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo - // open/close within a session, so the choice sticks while working. + // grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application + // config rather than in the project: it describes the user's window, not the model, so opening + // someone else's project must not move their panel. bool m_undocked = false; + // The config key behind m_undocked. + static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked"; // Smooth scrolling for the panel body (everything between the header and the pinned Bake footer). // ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard diff --git a/tests/libslic3r/test_texture_displacement.cpp b/tests/libslic3r/test_texture_displacement.cpp index 29e6540c0a..0e617a17f1 100644 --- a/tests/libslic3r/test_texture_displacement.cpp +++ b/tests/libslic3r/test_texture_displacement.cpp @@ -2187,6 +2187,8 @@ TEST_CASE("Texture displacement layers survive a JSON round trip", "[TextureDisp options.v2_max_triangles_k = 250; options.color_mix_mode = ColorMixMode::XYDither; options.color_despeckle = 4; + options.active_slot = 3; + options.panel_mode = 1; const std::string json = texture_displacement_layers_to_json(layers, options); @@ -2240,6 +2242,9 @@ TEST_CASE("Texture displacement layers survive a JSON round trip", "[TextureDisp CHECK(read_options.v2_max_triangles_k == options.v2_max_triangles_k); CHECK(read_options.color_mix_mode == options.color_mix_mode); CHECK(read_options.color_despeckle == options.color_despeckle); + // Where the panel was left comes back with the rest, so a reopened project is on the same layer. + CHECK(read_options.active_slot == options.active_slot); + CHECK(read_options.panel_mode == options.panel_mode); } TEST_CASE("Texture displacement JSON keeps defaults for keys it does not carry", "[TextureDisplacement]") @@ -2266,6 +2271,8 @@ TEST_CASE("Texture displacement JSON keeps defaults for keys it does not carry", CHECK_THAT(l.lscm_seam_angle_deg, WithinAbs(fresh.lscm_seam_angle_deg, 1e-6f)); CHECK(options.color_mix_mode == defaults.color_mix_mode); CHECK(options.pipeline_v2 == defaults.pipeline_v2); + CHECK(options.active_slot == defaults.active_slot); + CHECK(options.panel_mode == defaults.panel_mode); std::vector unused_layers; TextureDisplacementOptions unused_options;