From 8ecfee6a2160d5a30d622573da62d82a6a10f476 Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Fri, 9 Oct 2026 09:35:35 +0200 Subject: [PATCH] Save where the panel was left, and recover a texture the archive lacks The project carried the layer stack but not the user's place in it: a reopened project landed on slot 0 in Standard, whatever they had been editing. The active layer and Standard/Pro now live on the volume, next to the rest of its options, and come back with it. Docked/undocked goes to the application config instead - it describes the user's window, not the model, so opening someone else's project must not move their panel. A layer whose image_data was empty at save time was written with no texture and came back with none. It now falls back to the file the layer came from, which the importer put in the user's own texture library, so a project reopened on the same machine keeps its pixels. The texture is always stored as .png: the importer converts whatever the user picked, so the extension was being taken from a name that no longer described the bytes. Also adds v2_flip_edges to the cereal field list, which the undo stack was silently dropping. --- src/libslic3r/Format/bbs_3mf.cpp | 44 +++++++++++++------ src/libslic3r/TextureDisplacement.cpp | 6 +++ src/libslic3r/TextureDisplacement.hpp | 9 +++- .../GUI/Gizmos/GLGizmoTextureDisplacement.cpp | 30 +++++++++++++ .../GUI/Gizmos/GLGizmoTextureDisplacement.hpp | 10 ++++- tests/libslic3r/test_texture_displacement.cpp | 7 +++ 6 files changed, 89 insertions(+), 17 deletions(-) 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;