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