From b97e4aff38575e5c94ab8ee79c7016ecad205b3b Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Tue, 21 Jul 2026 10:34:29 +0800 Subject: [PATCH 1/8] feature: add Fit Camera button and shortcut for viewport - Add ZoomToFit() to GLCanvas3D that prioritizes selection, then volumes, then bed depending on context - Render a Fit Camera icon button next to the 3D navigator in the viewport via ImGui (light/dark/hover variants) - Bind 'Z' key as global shortcut for fit camera, with guard against text entry focus, running gizmos, and ImGui keyboard capture - Add Fit Camera entry to the View menu - Add 4 fit_camera SVG icons and register them in GLGizmosManager icon textures - Fix init_icon_textures() to skip reloading if already initialized - Document 'Z' shortcut in KBShortcutsDialog - Add zh_CN translations for the new UI strings --- .../i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 11 ++- resources/images/fit_camera.svg | 5 ++ resources/images/fit_camera_dark.svg | 5 ++ resources/images/fit_camera_dark_hover.svg | 5 ++ resources/images/fit_camera_hover.svg | 5 ++ src/slic3r/GUI/GLCanvas3D.cpp | 76 ++++++++++++++++ src/slic3r/GUI/GLCanvas3D.hpp | 8 ++ src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 25 ++++++ src/slic3r/GUI/Gizmos/GLGizmosManager.hpp | 4 + src/slic3r/GUI/KBShortcutsDialog.cpp | 1 + src/slic3r/GUI/MainFrame.cpp | 89 +++++++++++++++++++ src/slic3r/GUI/MainFrame.hpp | 4 + 12 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 resources/images/fit_camera.svg create mode 100644 resources/images/fit_camera_dark.svg create mode 100644 resources/images/fit_camera_dark_hover.svg create mode 100644 resources/images/fit_camera_hover.svg diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index abef0c29816..6ce3eaa7e10 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15348,4 +15348,13 @@ msgid "Other Colors" msgstr "其他颜色" msgid "Multiple Color" -msgstr "多色" \ No newline at end of file +msgstr "多色" + +msgid "Fit camera to scene or selected object." +msgstr "使相机适配场景或选定对象" + +msgid "Fit Camera" +msgstr "相机适配" + +msgid "Camera view - Fit to scene or selection" +msgstr "摄像机视角 - 适应场景或选择区域" diff --git a/resources/images/fit_camera.svg b/resources/images/fit_camera.svg new file mode 100644 index 00000000000..3945926c8ef --- /dev/null +++ b/resources/images/fit_camera.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/fit_camera_dark.svg b/resources/images/fit_camera_dark.svg new file mode 100644 index 00000000000..2f0842b526f --- /dev/null +++ b/resources/images/fit_camera_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/fit_camera_dark_hover.svg b/resources/images/fit_camera_dark_hover.svg new file mode 100644 index 00000000000..33a002bfc70 --- /dev/null +++ b/resources/images/fit_camera_dark_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/fit_camera_hover.svg b/resources/images/fit_camera_hover.svg new file mode 100644 index 00000000000..a31a62768f0 --- /dev/null +++ b/resources/images/fit_camera_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 3c4943628a0..b4912b83701 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1812,6 +1812,25 @@ void GLCanvas3D::zoom_to_plate(int plate_idx) } } +void GLCanvas3D::ZoomToFit() +{ + select_view("plate"); + + if (!m_selection.is_empty()) + { + zoom_to_selection(); + return; + } + + if (m_canvas_type == ECanvasType::CanvasAssembleView) + { + zoom_to_volumes(); + return; + } + + zoom_to_bed(); +} + void GLCanvas3D::select_view(const std::string& direction) { wxGetApp().plater()->get_camera().select_view(direction); @@ -5901,6 +5920,63 @@ void GLCanvas3D::_render_3d_navigator() request_extra_frame(); } + + const float fitButtonSize = ImGui::GetFontSize() * 2.5f; + const float fitButtonGap = 8.0f * sc; + const float fitButtonLeft = viewManipulateLeft + size + fitButtonGap; + const float fitButtonTop = viewManipulateTop - fitButtonSize - 20.0f * sc; + RenderFitCameraButton(fitButtonLeft, fitButtonTop, fitButtonSize); +} + +void GLCanvas3D::RenderFitCameraButton(float left, float top, float buttonSize) +{ + if (buttonSize <= 0.0f) + { + return; + } + + const GLGizmosManager::MENU_ICON_NAME normalIcon = m_is_dark ? + GLGizmosManager::IC_FIT_CAMERA_DARK : GLGizmosManager::IC_FIT_CAMERA; + const GLGizmosManager::MENU_ICON_NAME hoverIcon = m_is_dark ? + GLGizmosManager::IC_FIT_CAMERA_DARK_HOVER : GLGizmosManager::IC_FIT_CAMERA_HOVER; + + if (!m_gizmos.init_icon_textures()) + { + return; + } + + const ImTextureID normalId = m_gizmos.get_icon_texture_id(normalIcon); + const ImTextureID hoverId = m_gizmos.get_icon_texture_id(hoverIcon); + if (normalId == nullptr || hoverId == nullptr) + { + return; + } + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.set_next_window_pos(left, top, ImGuiCond_Always, 0.0f, 0.0f); + imgui.set_next_window_size(buttonSize, buttonSize, ImGuiCond_Always); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + imgui.begin(_L("Fit camera"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoSavedSettings); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + if (ImGui::ImageButton3(normalId, hoverId, ImVec2(buttonSize, buttonSize))) + { + ZoomToFit(); + } + + if (ImGui::IsItemHovered()) + { + imgui.tooltip(_L("Fit camera to scene or selected object."), ImGui::GetFontSize() * 20.0f); + } + + ImGui::PopStyleVar(2); + imgui.end(); + ImGui::PopStyleVar(); } #define ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT 0 diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index bd2df0bcec3..daf4d7495db 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -855,6 +855,7 @@ class GLCanvas3D void zoom_to_gcode(); //BBS -1 for current plate void zoom_to_plate(int plate_idx = -1); + void ZoomToFit(); void select_view(const std::string& direction); //BBS: add part plate related logic void select_plate(); @@ -1209,6 +1210,13 @@ class GLCanvas3D bool _render_orient_menu(float left, float right, float bottom, float top); bool _render_arrange_menu(float left, float right, float bottom, float top); void _render_3d_navigator(); + /** + * @brief Renders the fit-camera button next to the 3D navigator. + * @param left Left position in ImGui screen coordinates. + * @param top Top position in ImGui screen coordinates. + * @param buttonSize Width and height of the square button. + */ + void RenderFitCameraButton(float left, float top, float buttonSize); // render thumbnail using the default framebuffer void render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, ModelObjectPtrs& model_objects, const GLVolumeCollection& volumes, std::vector& extruder_colors, GLShaderProgram* shader, Camera::EType camera_type); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 1b2bb4bad74..4ed7677512b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -245,6 +245,11 @@ bool GLGizmosManager::init() bool GLGizmosManager::init_icon_textures() { + if (!icon_list.empty()) + { + return true; + } + ImTextureID texture_id; icon_list.clear(); @@ -278,6 +283,26 @@ bool GLGizmosManager::init_icon_textures() else return false; + if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/fit_camera.svg", 64, 64, texture_id)) + icon_list.insert(std::make_pair(static_cast(IC_FIT_CAMERA), texture_id)); + else + return false; + + if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/fit_camera_hover.svg", 64, 64, texture_id)) + icon_list.insert(std::make_pair(static_cast(IC_FIT_CAMERA_HOVER), texture_id)); + else + return false; + + if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/fit_camera_dark.svg", 64, 64, texture_id)) + icon_list.insert(std::make_pair(static_cast(IC_FIT_CAMERA_DARK), texture_id)); + else + return false; + + if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/fit_camera_dark_hover.svg", 64, 64, texture_id)) + icon_list.insert(std::make_pair(static_cast(IC_FIT_CAMERA_DARK_HOVER), texture_id)); + else + return false; + return true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index f68d4038add..240314dd810 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -169,6 +169,10 @@ class GLGizmosManager : public Slic3r::ObjectBase IC_TOOLBAR_RESET_ZERO_HOVER, IC_TOOLBAR_TOOLTIP, IC_TOOLBAR_TOOLTIP_HOVER, + IC_FIT_CAMERA, + IC_FIT_CAMERA_HOVER, + IC_FIT_CAMERA_DARK, + IC_FIT_CAMERA_DARK_HOVER, IC_NAME_COUNT, }; diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 7e53c2aec4d..6e34ca71e5e 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -242,6 +242,7 @@ void KBShortcutsDialog::fill_shortcuts() {ctrl + "4", L("Camera view - Behind")}, {ctrl + "5", L("Camera Angle - Left side")}, {ctrl + "6", L("Camera Angle - Right side")}, + {"Z", into_u8(_L("Camera view - Fit to scene or selection"))}, {ctrl + "A", L("Select all objects")}, {ctrl + "D", L("Delete all")}, diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index ba317d2be97..06f30f9baa3 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include //#include #include @@ -97,6 +98,54 @@ wxDEFINE_EVENT(EVT_BACKUP_POST, wxCommandEvent); wxDEFINE_EVENT(EVT_LOAD_URL, wxCommandEvent); wxDEFINE_EVENT(EVT_LOAD_PRINTER_URL, LoadPrinterViewEvent); +/** + * @brief Checks whether a wxWidgets text input currently owns the focus. + * @return True when keyboard input belongs to a text control. + */ +static bool IsTextEntryFocused() +{ + for (wxWindow* window = wxWindow::FindFocus(); window != nullptr; window = window->GetParent()) + { + if (dynamic_cast(window) != nullptr) + { + return true; + } + } + + return false; +} + +/** + * @brief Checks whether the Fit Camera shortcut must be passed through. + * @param plater Current plater instance. + * @return True when the shortcut must not trigger Fit Camera. + */ +static bool ShouldSkipFitCameraShortcut(Plater* plater) +{ + if (plater == nullptr || IsTextEntryFocused()) + { + return true; + } + + ImGuiWrapper* const imgui = wxGetApp().imgui(); + if (imgui != nullptr && (imgui->want_keyboard() || imgui->want_text_input())) + { + return true; + } + + GLCanvas3D* const currentCanvas = plater->get_current_canvas3D(); + if (currentCanvas == nullptr) + { + return true; + } + + GLCanvas3D* const viewCanvas = plater->get_view3D_canvas3D(); + GLCanvas3D* const assembleCanvas = plater->get_assmeble_canvas3D(); + return (viewCanvas != nullptr && viewCanvas->get_gizmos_manager().is_running()) || + (assembleCanvas != nullptr && assembleCanvas->get_gizmos_manager().is_running()) || + currentCanvas->get_gizmos_manager().is_running(); +} + enum class ERescaleTarget { Mainframe, @@ -618,6 +667,23 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ if (m_plater) { m_plater->add_file(); } return; } + + const bool isFitCameraShortcut = !evt.HasAnyModifiers() && + (evt.GetKeyCode() == 'Z' || evt.GetKeyCode() == 'z'); + if (isFitCameraShortcut) + { + if (ShouldSkipFitCameraShortcut(m_plater) || !can_change_view()) + { + evt.Skip(); + } + else + { + ZoomCameraToFit(); + } + + return; + } + evt.Skip(); }); @@ -2375,6 +2441,15 @@ static void add_common_view_menu_items(wxMenu* view_menu, MainFrame* mainFrame, "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); append_menu_item(view_menu, wxID_ANY, _L("Right") + "\t" + ctrl + "6", _L("Right View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("right"); }, "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); + +#ifdef __APPLE__ + const wxString fitCameraLabel = _L("Fit Camera"); +#else + const wxString fitCameraLabel = _L("Fit Camera") + "\tZ"; +#endif + append_menu_item(view_menu, wxID_ANY, fitCameraLabel, _L("Fit camera to scene or selected object."), + [mainFrame](wxCommandEvent&) { mainFrame->ZoomCameraToFit(); }, + "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); } void MainFrame::init_menubar_as_editor() @@ -3560,6 +3635,20 @@ void MainFrame::select_view(const std::string& direction) m_plater->select_view(direction); } +void MainFrame::ZoomCameraToFit() const +{ + if (m_plater == nullptr || !can_change_view()) + { + return; + } + + GLCanvas3D* const canvas = m_plater->canvas3D(); + if (canvas != nullptr) + { + canvas->ZoomToFit(); + } +} + // #ys_FIXME_to_delete void MainFrame::on_presets_changed(SimpleEvent &event) { diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index d32d28a30e4..fe996ff18de 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -320,6 +320,10 @@ class MainFrame : public DPIFrame void request_select_tab(TabPosition pos); int get_calibration_curr_tab(); void select_view(const std::string& direction); + /** + * @brief Fits the active canvas camera to the scene or selection. + */ + void ZoomCameraToFit() const; // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig void on_config_changed(DynamicPrintConfig* cfg) const ; void set_print_button_to_default(PrintSelectType select_type); From 369bdb9218f1e7439c1d0eae461c944a9f392686 Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Wed, 22 Jul 2026 11:45:18 +0800 Subject: [PATCH 2/8] feat: auto-arrange imported objects in a straight line for assembly view Add Model::InitializeAssemblyPositions() to place newly imported objects sequentially along the X-axis with 10mm spacing, matching BambuStudio's assembly view behavior. Multiple instances of the same object stack along the Y-axis. All objects are auto-grounded on Z-axis. Apply assembly position initialization to all object creation paths: - Handy model insertion (GUI_Factories) - Internal mesh loading (GUI_ObjectList) - File import for raw geometry formats: STL/OBJ/GLB/GLTF/FBX (Plater) - Copy/paste (Selection) - Text emboss job (EmbossJob) - Fill bed job (FillBedJob) 3MF project files are intentionally excluded as they preserve their own position data. --- src/libslic3r/Model.cpp | 116 +++++++++++++++++++++++++++++ src/libslic3r/Model.hpp | 23 +++++- src/slic3r/GUI/GUI_Factories.cpp | 20 ++++- src/slic3r/GUI/GUI_ObjectList.cpp | 4 +- src/slic3r/GUI/Jobs/EmbossJob.cpp | 1 + src/slic3r/GUI/Jobs/FillBedJob.cpp | 1 + src/slic3r/GUI/Plater.cpp | 35 +++++++++ src/slic3r/GUI/Selection.cpp | 5 +- 8 files changed, 196 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 2ad2daaeca8..9ce444a9291 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -504,6 +504,85 @@ ModelObject* Model::add_object(const ModelObject &other) return new_object; } +bool Model::InitializeAssemblyPositions(const ModelObjectPtrs& modelObjects) +{ + constexpr double ASSEMBLY_OBJECT_GAP = 10.0; + + if (modelObjects.empty()) + { + return false; + } + + ModelObjectPtrs validatedObjects; + validatedObjects.reserve(modelObjects.size()); + for (ModelObject* modelObject : modelObjects) + { + if (modelObject == nullptr || modelObject->instances.empty() || + std::find(objects.begin(), objects.end(), modelObject) == objects.end() || + std::find(validatedObjects.begin(), validatedObjects.end(), modelObject) != validatedObjects.end()) + { + return false; + } + + const BoundingBoxf3 rawBox = modelObject->raw_mesh_bounding_box(); + if (!rawBox.defined) + { + return false; + } + + for (const ModelInstance* instance : modelObject->instances) + { + if (instance == nullptr) + { + return false; + } + } + + validatedObjects.push_back(modelObject); + } + + BoundingBoxf3 sceneBox = CalculateAssemblyBoundingBox(validatedObjects); + for (ModelObject* modelObject : validatedObjects) + { + const BoundingBoxf3 rawBox = modelObject->raw_mesh_bounding_box(); + std::vector instanceBoxes; + instanceBoxes.reserve(modelObject->instances.size()); + + double objectWidth = 0.0; + for (ModelInstance* instance : modelObject->instances) + { + if (!instance->is_assemble_initialized()) + { + instance->set_assemble_transformation(instance->get_transformation()); + } + + const BoundingBoxf3 instanceBox = + rawBox.transformed(instance->get_assemble_transformation().get_matrix_no_offset()); + instanceBoxes.push_back(instanceBox); + objectWidth = std::max(objectWidth, instanceBox.size().x()); + } + + const double objectCenterX = sceneBox.defined ? sceneBox.max.x() + ASSEMBLY_OBJECT_GAP + objectWidth * 0.5 : 0.0; + const double firstInstanceCenterY = sceneBox.defined ? sceneBox.center().y() : 0.0; + double previousInstanceMaxY = 0.0; + + for (size_t instanceIndex = 0; instanceIndex < modelObject->instances.size(); ++instanceIndex) + { + const BoundingBoxf3& instanceBox = instanceBoxes[instanceIndex]; + const double instanceCenterY = instanceIndex == 0 ? firstInstanceCenterY : + previousInstanceMaxY + ASSEMBLY_OBJECT_GAP + instanceBox.size().y() * 0.5; + const Vec3d assemblyOffset(objectCenterX - instanceBox.center().x(), + instanceCenterY - instanceBox.center().y(), -instanceBox.min.z()); + modelObject->instances[instanceIndex]->set_assemble_offset(assemblyOffset); + previousInstanceMaxY = instanceCenterY + instanceBox.size().y() * 0.5; + } + + sceneBox.merge(modelObject->CalculateAssemblyBoundingBox()); + } + + return true; +} + void Model::delete_object(size_t idx) { ModelObjectPtrs::iterator i = this->objects.begin() + idx; @@ -663,6 +742,23 @@ BoundingBoxf3 Model::bounding_box_exact() const return bb; } +BoundingBoxf3 Model::CalculateAssemblyBoundingBox(const ModelObjectPtrs& excludedObjects) const +{ + BoundingBoxf3 assemblyBox; + for (const ModelObject* modelObject : objects) + { + if (modelObject == nullptr || + std::find(excludedObjects.begin(), excludedObjects.end(), modelObject) != excludedObjects.end()) + { + continue; + } + + assemblyBox.merge(modelObject->CalculateAssemblyBoundingBox()); + } + + return assemblyBox; +} + double Model::max_z() const { double z = 0; @@ -1440,6 +1536,26 @@ const BoundingBoxf3& ModelObject::bounding_box_exact() const return m_bounding_box_exact; } +BoundingBoxf3 ModelObject::CalculateAssemblyBoundingBox() const +{ + BoundingBoxf3 assemblyBox; + const BoundingBoxf3 rawBox = raw_mesh_bounding_box(); + if (!rawBox.defined) + { + return assemblyBox; + } + + for (const ModelInstance* instance : instances) + { + if (instance != nullptr) + { + assemblyBox.merge(rawBox.transformed(instance->get_assemble_transformation().get_matrix())); + } + } + + return assemblyBox; +} + double ModelObject::min_z() const { const_cast(this)->update_min_max_z(); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 15551193790..69e8e129792 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -444,6 +444,11 @@ class ModelObject final : public ObjectBase const BoundingBoxf3& bounding_box_approx() const; // Returns an exact bounding box of the transformed instances. The result it is being cached. const BoundingBoxf3& bounding_box_exact() const; + /** + * @brief Calculates the bounding box of all instances in assembly-view coordinates. + * @return The merged assembly-view bounding box. + */ + BoundingBoxf3 CalculateAssemblyBoundingBox() const; // Return minimum / maximum of a printable object transformed into the world coordinate system. // All instances share the same min / max Z. double min_z() const; @@ -1281,7 +1286,11 @@ class ModelInstance final : public ObjectBase m_assemble_transformation.set_matrix(transform); } Vec3d get_assemble_offset() const {return m_assemble_transformation.get_offset(); } - void set_assemble_offset(const Vec3d& offset) { m_assemble_transformation.set_offset(offset); } + void set_assemble_offset(const Vec3d& offset) + { + m_assemble_initialized = true; + m_assemble_transformation.set_offset(offset); + } void set_assemble_rotation(const Vec3d &rotation) { m_assemble_transformation.set_rotation(rotation); } void rotate_assemble(double angle, const Vec3d& axis) { m_assemble_transformation.set_rotation(m_assemble_transformation.get_rotation() + Geometry::extract_euler_angles(Eigen::Quaterniond(Eigen::AngleAxisd(angle, axis)).toRotationMatrix())); @@ -1616,6 +1625,12 @@ class Model final : public ObjectBase ModelObject* add_object(const char *name, const char *path, const TriangleMesh &mesh); ModelObject* add_object(const char *name, const char *path, TriangleMesh &&mesh); ModelObject* add_object(const ModelObject &other); + /** + * @brief Initializes newly created objects as one assembly-view layout batch. + * @param modelObjects Objects owned by this model, in their desired layout order. + * @return True when every assembly position was initialized. + */ + bool InitializeAssemblyPositions(const ModelObjectPtrs& modelObjects); void delete_object(size_t idx); bool delete_object(ObjectID id); bool delete_object(ModelObject* object); @@ -1640,6 +1655,12 @@ class Model final : public ObjectBase BoundingBoxf3 bounding_box_approx() const; // Returns exact axis aligned bounding box of this model. BoundingBoxf3 bounding_box_exact() const; + /** + * @brief Calculates the assembly-view bounding box for this model. + * @param excludedObjects Objects to exclude from the result. + * @return The merged assembly-view bounding box. + */ + BoundingBoxf3 CalculateAssemblyBoundingBox(const ModelObjectPtrs& excludedObjects = {}) const; // Return maximum height of all printable objects. double max_z() const; // Set the print_volume_state of PrintObject::instances, diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 87a6d909cb5..b0eedadfa8c 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -599,7 +599,25 @@ wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeTyp } else return; input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name)); - plater()->load_files(input_files, LoadStrategy::LoadModel); + Plater* currentPlater = plater(); + if (currentPlater == nullptr) + { + return; + } + + const std::vector loadedObjectIndexes = currentPlater->load_files(input_files, LoadStrategy::LoadModel); + Model& model = currentPlater->model(); + ModelObjectPtrs loadedObjects; + loadedObjects.reserve(loadedObjectIndexes.size()); + for (std::vector::const_reverse_iterator objectIndex = loadedObjectIndexes.rbegin(); + objectIndex != loadedObjectIndexes.rend(); ++objectIndex) + { + if (*objectIndex < model.objects.size()) + { + loadedObjects.push_back(model.objects[*objectIndex]); + } + } + model.InitializeAssemblyPositions(loadedObjects); // Suggest to change settings for stringhell // This serves as mini tutorial for new users diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index b1130ea33c1..70619122dc4 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2465,9 +2465,7 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name new_object->ensure_on_bed(); - //BBS init assmeble transformation - Geometry::Transformation t = new_object->instances[0]->get_transformation(); - new_object->instances[0]->set_assemble_transformation(t); + model.InitializeAssemblyPositions({new_object}); object_idxs.push_back(model.objects.size() - 1); #ifdef _DEBUG diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index a6e7dd6c6d7..3628f4c7cf3 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -371,6 +371,7 @@ void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) Slic3r::Geometry::Transformation tr(m_transformation); new_object->instances.front()->set_transformation(tr); new_object->ensure_on_bed(); + model.InitializeAssemblyPositions({new_object}); // Actualize right panel and set inside of selection app.obj_list()->paste_objects_into_list({model.objects.size() - 1}); diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index b4aa69c4e9b..8c45cc63594 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -178,6 +178,7 @@ void FillBedJob::prepare() ModelObject* newObj = m_plater->model().add_object(*mo); newObj->name = mo->name +" "+ std::to_string(p.itemid); for (ModelInstance *newInst : newObj->instances) { newInst->apply_arrange_result(p.translation.cast(), p.rotation); } + m_plater->model().InitializeAssemblyPositions({newObj}); //m_plater->sidebar().obj_list()->paste_objects_into_list({m_plater->model().objects.size()-1}); }; m_selected.emplace_back(ap); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cc0e8ecef11..0d67d2d038e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10370,6 +10370,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ const float INIT_MODEL_RATIO = 0.75; const float CENTER_AROUND_ORIGIN_RATIO = 0.8; const float LOAD_MODEL_RATIO = 0.9; + bool hasRawGeometryImport = false; for (size_t i = 0; i < input_files.size(); ++i) { int file_percent = 0; @@ -10394,6 +10395,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ // const bool type_zip_amf = !type_3mf && std::regex_match(path.string(), pattern_zip_amf); const bool type_any_amf = !type_3mf && std::regex_match(path.string(), pattern_any_amf); // const bool type_prusa = std::regex_match(path.string(), pattern_prusa); + const bool shouldInitializeAssemblyPosition = boost::algorithm::iends_with(path.string(), ".stl") || + boost::algorithm::iends_with(path.string(), ".obj") || + boost::algorithm::iends_with(path.string(), ".glb") || + boost::algorithm::iends_with(path.string(), ".gltf") || + boost::algorithm::iends_with(path.string(), ".fbx"); + hasRawGeometryImport = hasRawGeometryImport || shouldInitializeAssemblyPosition; Slic3r::Model model; // BBS: add auxiliary files related logic @@ -11185,6 +11192,20 @@ std::vector Plater::priv::load_files(const std::vector& input_ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", before load_model_objects, count %1%")%model.objects.size(); auto loaded_idxs = load_model_objects(model.objects, is_project_file); obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); + if (shouldInitializeAssemblyPosition) + { + ModelObjectPtrs loadedObjects; + loadedObjects.reserve(loaded_idxs.size()); + for (std::vector::const_reverse_iterator objectIndex = loaded_idxs.rbegin(); + objectIndex != loaded_idxs.rend(); ++objectIndex) + { + if (*objectIndex < q->model().objects.size()) + { + loadedObjects.push_back(q->model().objects[*objectIndex]); + } + } + q->model().InitializeAssemblyPositions(loadedObjects); + } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished load_model_objects"); wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); @@ -11220,6 +11241,20 @@ std::vector Plater::priv::load_files(const std::vector& input_ auto loaded_idxs = load_model_objects(new_model->objects); obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); + if (hasRawGeometryImport) + { + ModelObjectPtrs loadedObjects; + loadedObjects.reserve(loaded_idxs.size()); + for (std::vector::const_reverse_iterator objectIndex = loaded_idxs.rbegin(); + objectIndex != loaded_idxs.rend(); ++objectIndex) + { + if (*objectIndex < q->model().objects.size()) + { + loadedObjects.push_back(q->model().objects[*objectIndex]); + } + } + q->model().InitializeAssemblyPositions(loadedObjects); + } } if (new_model) delete new_model; diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 074d33850fb..7aea8f4fa5b 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -3170,11 +3170,8 @@ void Selection::paste_objects_from_clipboard() for (ModelInstance* inst : dst_object->instances) { inst->set_offset(displacement); - - //BBS init asssmble transformation - Geometry::Transformation t = inst->get_transformation(); - inst->set_assemble_transformation(t); } + m_model->InitializeAssemblyPositions({dst_object}); object_idxs.push_back(m_model->objects.size() - 1); #ifdef _DEBUG From 389be0a17abfebb2338855f595c31f8285dbe87e Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Wed, 22 Jul 2026 17:17:52 +0800 Subject: [PATCH 3/8] refactor: unify "Fit Camera" labels to "Fit in all view" Consolidate three duplicate i18n strings ("Fit camera to scene or selected object.", "Fit Camera", "Camera view - Fit to scene or selection") into a single "Fit in all view" label, used consistently across tooltip, shortcut dialog, and menu item. --- localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 8 +------- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/KBShortcutsDialog.cpp | 2 +- src/slic3r/GUI/MainFrame.cpp | 6 +++--- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 6ce3eaa7e10..f1c28c641a6 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15350,11 +15350,5 @@ msgstr "其他颜色" msgid "Multiple Color" msgstr "多色" -msgid "Fit camera to scene or selected object." -msgstr "使相机适配场景或选定对象" - -msgid "Fit Camera" +msgid "Fit in all view" msgstr "相机适配" - -msgid "Camera view - Fit to scene or selection" -msgstr "摄像机视角 - 适应场景或选择区域" diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index b4912b83701..04193d39ba3 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -5971,7 +5971,7 @@ void GLCanvas3D::RenderFitCameraButton(float left, float top, float buttonSize) if (ImGui::IsItemHovered()) { - imgui.tooltip(_L("Fit camera to scene or selected object."), ImGui::GetFontSize() * 20.0f); + imgui.tooltip(_L("Fit in all view"), ImGui::GetFontSize() * 20.0f); } ImGui::PopStyleVar(2); diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 6e34ca71e5e..c6ae085de14 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -242,7 +242,7 @@ void KBShortcutsDialog::fill_shortcuts() {ctrl + "4", L("Camera view - Behind")}, {ctrl + "5", L("Camera Angle - Left side")}, {ctrl + "6", L("Camera Angle - Right side")}, - {"Z", into_u8(_L("Camera view - Fit to scene or selection"))}, + {"Z", into_u8(_L("Fit in all view"))}, {ctrl + "A", L("Select all objects")}, {ctrl + "D", L("Delete all")}, diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 06f30f9baa3..edc1896f22f 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2443,11 +2443,11 @@ static void add_common_view_menu_items(wxMenu* view_menu, MainFrame* mainFrame, "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); #ifdef __APPLE__ - const wxString fitCameraLabel = _L("Fit Camera"); + const wxString fitCameraLabel = _L("Fit in all view"); #else - const wxString fitCameraLabel = _L("Fit Camera") + "\tZ"; + const wxString fitCameraLabel = _L("Fit in all view") + "\tZ"; #endif - append_menu_item(view_menu, wxID_ANY, fitCameraLabel, _L("Fit camera to scene or selected object."), + append_menu_item(view_menu, wxID_ANY, fitCameraLabel, _L("Fit in all view"), [mainFrame](wxCommandEvent&) { mainFrame->ZoomCameraToFit(); }, "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); } From 9ecca97deeeb23719b9656beb52d854e80c4d8f5 Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Thu, 23 Jul 2026 10:05:43 +0800 Subject: [PATCH 4/8] refactor: address review findings for Fit Camera and assembly layout - perf: batch InitializeAssemblyPositions in FillBedJob::finalize (was per-item in the setter -> O(N^2) full-scene traversal) - fix: iterate loaded objects forward so assembly-view order matches load order (was reversed via rbegin/rend) at all three call sites - refactor: drop _L() on the Fit Camera ImGui window ID (hidden by NoTitleBar, polluted the translation catalog); use a plain std::string ID, also resolving begin() overload ambiguity --- src/slic3r/GUI/GLCanvas3D.cpp | 8 ++++---- src/slic3r/GUI/GUI_Factories.cpp | 7 +++---- src/slic3r/GUI/Jobs/FillBedJob.cpp | 16 +++++++++++----- src/slic3r/GUI/Plater.cpp | 14 ++++++-------- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 04193d39ba3..4f6d7b12f99 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -5957,10 +5957,10 @@ void GLCanvas3D::RenderFitCameraButton(float left, float top, float buttonSize) imgui.set_next_window_size(buttonSize, buttonSize, ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - imgui.begin(_L("Fit camera"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoSavedSettings); + imgui.begin(std::string("FitCameraButtonWindow"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoSavedSettings); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index b0eedadfa8c..a72223fcaf5 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -609,12 +609,11 @@ wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeTyp Model& model = currentPlater->model(); ModelObjectPtrs loadedObjects; loadedObjects.reserve(loadedObjectIndexes.size()); - for (std::vector::const_reverse_iterator objectIndex = loadedObjectIndexes.rbegin(); - objectIndex != loadedObjectIndexes.rend(); ++objectIndex) + for (const size_t objectIndex : loadedObjectIndexes) { - if (*objectIndex < model.objects.size()) + if (objectIndex < model.objects.size()) { - loadedObjects.push_back(model.objects[*objectIndex]); + loadedObjects.push_back(model.objects[objectIndex]); } } model.InitializeAssemblyPositions(loadedObjects); diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index 8c45cc63594..841fc8e0df0 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -173,12 +173,11 @@ void FillBedJob::prepare() ap.poly = m_selected.front().poly; ap.bed_idx = PartPlateList::MAX_PLATES_COUNT; ap.itemid = -1; - ap.setter = [this, mi](const ArrangePolygon &p) { + ap.setter = [this](const ArrangePolygon &p) { ModelObject *mo = m_plater->model().objects[m_object_idx]; ModelObject* newObj = m_plater->model().add_object(*mo); newObj->name = mo->name +" "+ std::to_string(p.itemid); for (ModelInstance *newInst : newObj->instances) { newInst->apply_arrange_result(p.translation.cast(), p.rotation); } - m_plater->model().InitializeAssemblyPositions({newObj}); //m_plater->sidebar().obj_list()->paste_objects_into_list({m_plater->model().objects.size()-1}); }; m_selected.emplace_back(ap); @@ -280,7 +279,8 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) return s + int(ap.priority == 0 && ap.bed_idx == 0); }); - int oldSize = m_plater->model().objects.size(); + Model& model = m_plater->model(); + const size_t oldObjectCount = model.objects.size(); if (added_cnt > 0) { //BBS: adjust the selected instances @@ -306,9 +306,15 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":selected: bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale(ap.translation(X)) % unscale(ap.translation(Y)); } - int newSize = m_plater->model().objects.size(); + const size_t newObjectCount = model.objects.size(); + if (newObjectCount > oldObjectCount) + { + ModelObjectPtrs newObjects(model.objects.begin() + oldObjectCount, model.objects.end()); + model.InitializeAssemblyPositions(newObjects); + } + auto obj_list = m_plater->sidebar().obj_list(); - for (size_t i = oldSize; i < newSize; i++) { + for (size_t i = oldObjectCount; i < newObjectCount; i++) { obj_list->add_object_to_list(i, true, true, false); obj_list->update_printable_state(i, 0); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0d67d2d038e..6f0f4e209a6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -11196,12 +11196,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ { ModelObjectPtrs loadedObjects; loadedObjects.reserve(loaded_idxs.size()); - for (std::vector::const_reverse_iterator objectIndex = loaded_idxs.rbegin(); - objectIndex != loaded_idxs.rend(); ++objectIndex) + for (const size_t objectIndex : loaded_idxs) { - if (*objectIndex < q->model().objects.size()) + if (objectIndex < q->model().objects.size()) { - loadedObjects.push_back(q->model().objects[*objectIndex]); + loadedObjects.push_back(q->model().objects[objectIndex]); } } q->model().InitializeAssemblyPositions(loadedObjects); @@ -11245,12 +11244,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ { ModelObjectPtrs loadedObjects; loadedObjects.reserve(loaded_idxs.size()); - for (std::vector::const_reverse_iterator objectIndex = loaded_idxs.rbegin(); - objectIndex != loaded_idxs.rend(); ++objectIndex) + for (const size_t objectIndex : loaded_idxs) { - if (*objectIndex < q->model().objects.size()) + if (objectIndex < q->model().objects.size()) { - loadedObjects.push_back(q->model().objects[*objectIndex]); + loadedObjects.push_back(q->model().objects[objectIndex]); } } q->model().InitializeAssemblyPositions(loadedObjects); From f879104307813aa6429995ac86c41d0620c61265 Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Thu, 23 Jul 2026 14:15:12 +0800 Subject: [PATCH 5/8] fix: skip invalid objects/instances in InitializeAssemblyPositions A single invalid object (empty mesh, null/duplicate instance, etc.) previously made InitializeAssemblyPositions return false and leave the whole batch uninitialized. Rework it to skip invalid items and continue with the valid ones. - Return void instead of bool; no caller used the result. - Skip null/undefined-mesh objects and null/undefined-box instances instead of aborting the batch. - Pre-size instanceBoxes and keep them index-aligned with instances; skipped slots stay defined==false and are filtered consistently. - Use a firstValidInstance flag instead of instanceIndex == 0 so Y stacking is correct when instances[0] is null or degenerate. - Bind rawBox to raw_mesh_bounding_box()'s cached const reference. --- src/libslic3r/Model.cpp | 67 ++++++++++++++++++++++++++--------------- src/libslic3r/Model.hpp | 4 +-- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 9ce444a9291..5662f4cdb0b 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -504,13 +504,13 @@ ModelObject* Model::add_object(const ModelObject &other) return new_object; } -bool Model::InitializeAssemblyPositions(const ModelObjectPtrs& modelObjects) +void Model::InitializeAssemblyPositions(const ModelObjectPtrs& modelObjects) { constexpr double ASSEMBLY_OBJECT_GAP = 10.0; if (modelObjects.empty()) { - return false; + return; } ModelObjectPtrs validatedObjects; @@ -521,21 +521,13 @@ bool Model::InitializeAssemblyPositions(const ModelObjectPtrs& modelObjects) std::find(objects.begin(), objects.end(), modelObject) == objects.end() || std::find(validatedObjects.begin(), validatedObjects.end(), modelObject) != validatedObjects.end()) { - return false; + continue; } - const BoundingBoxf3 rawBox = modelObject->raw_mesh_bounding_box(); + const BoundingBoxf3& rawBox = modelObject->raw_mesh_bounding_box(); if (!rawBox.defined) { - return false; - } - - for (const ModelInstance* instance : modelObject->instances) - { - if (instance == nullptr) - { - return false; - } + continue; } validatedObjects.push_back(modelObject); @@ -544,43 +536,70 @@ bool Model::InitializeAssemblyPositions(const ModelObjectPtrs& modelObjects) BoundingBoxf3 sceneBox = CalculateAssemblyBoundingBox(validatedObjects); for (ModelObject* modelObject : validatedObjects) { - const BoundingBoxf3 rawBox = modelObject->raw_mesh_bounding_box(); - std::vector instanceBoxes; - instanceBoxes.reserve(modelObject->instances.size()); + const BoundingBoxf3& rawBox = modelObject->raw_mesh_bounding_box(); + std::vector instanceBoxes(modelObject->instances.size()); double objectWidth = 0.0; - for (ModelInstance* instance : modelObject->instances) + bool hasValidInstance = false; + for (size_t instanceIndex = 0; instanceIndex < modelObject->instances.size(); ++instanceIndex) { + ModelInstance* instance = modelObject->instances[instanceIndex]; + if (instance == nullptr) + { + continue; + } + if (!instance->is_assemble_initialized()) { instance->set_assemble_transformation(instance->get_transformation()); } - const BoundingBoxf3 instanceBox = - rawBox.transformed(instance->get_assemble_transformation().get_matrix_no_offset()); - instanceBoxes.push_back(instanceBox); + BoundingBoxf3& instanceBox = instanceBoxes[instanceIndex]; + instanceBox = rawBox.transformed(instance->get_assemble_transformation().get_matrix_no_offset()); + if (!instanceBox.defined) + { + continue; + } + objectWidth = std::max(objectWidth, instanceBox.size().x()); + hasValidInstance = true; + } + + if (!hasValidInstance) + { + continue; } const double objectCenterX = sceneBox.defined ? sceneBox.max.x() + ASSEMBLY_OBJECT_GAP + objectWidth * 0.5 : 0.0; const double firstInstanceCenterY = sceneBox.defined ? sceneBox.center().y() : 0.0; double previousInstanceMaxY = 0.0; + bool firstValidInstance = true; for (size_t instanceIndex = 0; instanceIndex < modelObject->instances.size(); ++instanceIndex) { + ModelInstance* instance = modelObject->instances[instanceIndex]; + if (instance == nullptr) + { + continue; + } + const BoundingBoxf3& instanceBox = instanceBoxes[instanceIndex]; - const double instanceCenterY = instanceIndex == 0 ? firstInstanceCenterY : + if (!instanceBox.defined) + { + continue; + } + + const double instanceCenterY = firstValidInstance ? firstInstanceCenterY : previousInstanceMaxY + ASSEMBLY_OBJECT_GAP + instanceBox.size().y() * 0.5; const Vec3d assemblyOffset(objectCenterX - instanceBox.center().x(), instanceCenterY - instanceBox.center().y(), -instanceBox.min.z()); - modelObject->instances[instanceIndex]->set_assemble_offset(assemblyOffset); + instance->set_assemble_offset(assemblyOffset); previousInstanceMaxY = instanceCenterY + instanceBox.size().y() * 0.5; + firstValidInstance = false; } sceneBox.merge(modelObject->CalculateAssemblyBoundingBox()); } - - return true; } void Model::delete_object(size_t idx) diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 69e8e129792..3402a2f625c 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -1628,9 +1628,9 @@ class Model final : public ObjectBase /** * @brief Initializes newly created objects as one assembly-view layout batch. * @param modelObjects Objects owned by this model, in their desired layout order. - * @return True when every assembly position was initialized. + * Invalid objects and instances are skipped. */ - bool InitializeAssemblyPositions(const ModelObjectPtrs& modelObjects); + void InitializeAssemblyPositions(const ModelObjectPtrs& modelObjects); void delete_object(size_t idx); bool delete_object(ObjectID id); bool delete_object(ModelObject* object); From a338da5d66e748e9dde75cb066d3520e9c0ddbe0 Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Thu, 23 Jul 2026 15:00:38 +0800 Subject: [PATCH 6/8] test(gui): enable Fit Camera Z accelerator on macOS --- src/slic3r/GUI/MainFrame.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index edc1896f22f..7166a1d77c2 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2442,11 +2442,7 @@ static void add_common_view_menu_items(wxMenu* view_menu, MainFrame* mainFrame, append_menu_item(view_menu, wxID_ANY, _L("Right") + "\t" + ctrl + "6", _L("Right View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("right"); }, "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); -#ifdef __APPLE__ - const wxString fitCameraLabel = _L("Fit in all view"); -#else const wxString fitCameraLabel = _L("Fit in all view") + "\tZ"; -#endif append_menu_item(view_menu, wxID_ANY, fitCameraLabel, _L("Fit in all view"), [mainFrame](wxCommandEvent&) { mainFrame->ZoomCameraToFit(); }, "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); From a6572188352dac32a983b2ba5a8af19b70f734cc Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Thu, 23 Jul 2026 18:34:01 +0800 Subject: [PATCH 7/8] fix: skip macOS menu accelerator for Fit Camera shortcut Registering the bare "Z" as a menu keyEquivalent on macOS would route the shortcut through the menu's wxCommandEvent, which only checks can_change_view() and bypasses the stricter ShouldSkipFitCameraShortcut guard (text-entry focus / ImGui wants keyboard / gizmo running) used in the wxEVT_CHAR_HOOK handler. On macOS the shortcut is therefore left to the CharHook path only, and the menu label omits the "\tZ" accelerator. Windows/Linux keep showing it. --- src/slic3r/GUI/MainFrame.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7166a1d77c2..edc1896f22f 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2442,7 +2442,11 @@ static void add_common_view_menu_items(wxMenu* view_menu, MainFrame* mainFrame, append_menu_item(view_menu, wxID_ANY, _L("Right") + "\t" + ctrl + "6", _L("Right View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("right"); }, "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); +#ifdef __APPLE__ + const wxString fitCameraLabel = _L("Fit in all view"); +#else const wxString fitCameraLabel = _L("Fit in all view") + "\tZ"; +#endif append_menu_item(view_menu, wxID_ANY, fitCameraLabel, _L("Fit in all view"), [mainFrame](wxCommandEvent&) { mainFrame->ZoomCameraToFit(); }, "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); From 0542a36b79f7d5a2aea1a872d8669ce3ed0c94d5 Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Tue, 4 Aug 2026 15:02:23 +0800 Subject: [PATCH 8/8] feat: add MainFrame::ZoomCameraToFit to fit camera to scene/selection Declare ZoomCameraToFit() on MainFrame as the entry point for fitting the active canvas camera to the full scene or current selection. --- src/slic3r/GUI/MainFrame.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index d32d28a30e4..20e2c31ec92 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -320,6 +320,10 @@ class MainFrame : public DPIFrame void request_select_tab(TabPosition pos); int get_calibration_curr_tab(); void select_view(const std::string& direction); + /** + * @brief Fits the active canvas camera to the scene or selection. + */ + void ZoomCameraToFit() const; // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig void on_config_changed(DynamicPrintConfig* cfg) const ; void set_print_button_to_default(PrintSelectType select_type);