@@ -261,4 +267,3 @@
-
diff --git a/resources/web/homepage/js/home.js b/resources/web/homepage/js/home.js
index 72789cc088..f522c44bed 100644
--- a/resources/web/homepage/js/home.js
+++ b/resources/web/homepage/js/home.js
@@ -173,6 +173,7 @@ function GotoMenu( strMenu )
function SetOrcaLoginInfo( strAvatar, strName )
{
$("#OrcaLogin1").hide();
+ $("#OrcaStatusText").hide();
$("#UserName").text(strName);
@@ -196,6 +197,7 @@ function SetOrcaUserOffline()
$("#OrcaLogin1").show();
$("#OrcaLogin1").css("display","flex");
+ $("#OrcaStatusText").show();
}
function SetMallUrl( strUrl )
@@ -437,6 +439,8 @@ function SetBambuLoginInfo(strAvatar, strName) {
$("#BambuLogin2").show();
$("#BambuLogin2").css("display", "flex");
$(".bambu-status-dot").addClass("online");
+ $("#BambuStatusText").text("Connected");
+ $("#BambuStatusText").attr("tid", "orca11");
}
function SetBambuUserOffline() {
@@ -448,6 +452,8 @@ function SetBambuUserOffline() {
$("#BambuLogin1").css("display", "flex");
}
$(".bambu-status-dot").removeClass("online");
+ $("#BambuStatusText").text("Not connected");
+ $("#BambuStatusText").attr("tid", "orca10");
}
function OnBambuLoginOrRegister() { SendSimpleCommand("homepage_bambu_login_or_register"); }
diff --git a/scripts/build_flatpak_with_docker.sh b/scripts/build_flatpak_with_docker.sh
index 898ec2c56a..9e8805af2f 100755
--- a/scripts/build_flatpak_with_docker.sh
+++ b/scripts/build_flatpak_with_docker.sh
@@ -22,7 +22,7 @@ ARCH="$(uname -m)"
NO_DEBUG_INFO=false
FORCE_PULL=false
FORCE_CLEAN=true
-CONTAINER_IMAGE="ghcr.io/flathub-infra/flatpak-github-actions:gnome-49"
+CONTAINER_IMAGE="ghcr.io/flathub-infra/flatpak-github-actions:gnome-50"
normalize_arch() {
case "$1" in
@@ -142,6 +142,16 @@ fi
DOCKER_RUN_ARGS=(run --rm -i --privileged)
+# When building from a git worktree, $PROJECT_ROOT/.git is a file pointing to the
+# main repo's git dir (outside $PROJECT_ROOT). The git commands and flatpak-builder
+# inside the container need that path to resolve, so bind-mount the common git dir
+# read-only at its original absolute path. No-op for a normal clone.
+GIT_COMMON_DIR="$(git -C "$PROJECT_ROOT" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)"
+if [ -n "$GIT_COMMON_DIR" ] && [ "$GIT_COMMON_DIR" != "$PROJECT_ROOT/.git" ]; then
+ echo " Git worktree detected; mounting common git dir read-only: $GIT_COMMON_DIR"
+ DOCKER_RUN_ARGS+=(-v "$GIT_COMMON_DIR":"$GIT_COMMON_DIR":ro)
+fi
+
# Pass build parameters as env vars so the inner script doesn't need
# variable expansion from the outer shell (avoids quoting issues).
echo "=== Starting Flatpak build inside container ==="
@@ -167,16 +177,16 @@ format_duration() {
overall_start=$(date +%s)
install_start=$overall_start
-# The workspace and .flatpak-builder cache are bind-mounted from the host.
-# Git inside the container may reject cached source repos as unsafe due to
-# ownership mismatch, which breaks flatpak-builder when it reuses git sources.
-git config --global --add safe.directory /src
-git config --global --add safe.directory '/src/.flatpak-builder/git/*'
+# This container runs as root, but the bind-mounted workspace and .flatpak-builder
+# cache are host-user-owned, so git's dubious-ownership check rejects them and
+# breaks flatpak-builder's git checkouts (e.g. wxWidgets). Trust every repo: safe
+# in this ephemeral build container, and covers the workspace, mirrors and builds.
+git config --global --add safe.directory '*'
# Install required SDK extensions (not pre-installed in the container image)
flatpak install -y --noninteractive --arch="$BUILD_ARCH" flathub \
- org.gnome.Platform//49 \
- org.gnome.Sdk//49 \
+ org.gnome.Platform//50 \
+ org.gnome.Sdk//50 \
org.freedesktop.Sdk.Extension.llvm21//25.08 || true
install_end=$(date +%s)
diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml
index 8436a478cc..9ec11fa930 100644
--- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml
+++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml
@@ -45,6 +45,12 @@
#00695C
+
+ https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds
+
+ See the release page for detailed changelog.
+
+
https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/v2.3.2
diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
index bb42b48607..8c1f734e1c 100644
--- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
+++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
@@ -1,6 +1,6 @@
app-id: com.orcaslicer.OrcaSlicer
runtime: org.gnome.Platform
-runtime-version: "49"
+runtime-version: "50"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.llvm21
@@ -24,6 +24,11 @@ finish-args:
- --filesystem=xdg-run/gvfs
- --filesystem=/run/media
- --filesystem=/media
+ # Network shares (CIFS/NFS) are commonly mounted under /mnt, which the default
+ # finish-args don't grant; without it, opening a model from such a share fails
+ # with "The file does not contain any geometry data."
+ # https://github.com/flathub/com.orcaslicer.OrcaSlicer/issues/2
+ - --filesystem=/mnt
- --filesystem=/run/spnav.sock:ro
# Allow read-only access to OrcaSlicer's legacy config and cache directories (if they exist) for migration purposes.
- --filesystem=~/.var/app/io.github.orcaslicer.OrcaSlicer:ro
@@ -115,7 +120,12 @@ modules:
- -DwxUSE_ZLIB=sys
- -DwxUSE_LIBJPEG=sys
- -DwxUSE_LIBTIFF=OFF
+ # sys, not builtin (unlike the static deps build): wxWidgets installs the
+ # builtin libwxwebp*.so only for static builds, so a shared build leaves
+ # them missing at runtime. The GNOME runtime provides libwebp.
+ - -DwxUSE_LIBWEBP=sys
- -DwxUSE_EXPAT=sys
+ - -DwxUSE_NANOSVG=OFF
- -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld
- -DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=lld
- -DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=lld
diff --git a/scripts/flatpak/setup_env_ubuntu24.04.sh b/scripts/flatpak/setup_env_ubuntu24.04.sh
index 68bf325497..a0ccbb9b32 100755
--- a/scripts/flatpak/setup_env_ubuntu24.04.sh
+++ b/scripts/flatpak/setup_env_ubuntu24.04.sh
@@ -3,7 +3,7 @@
sudo apt update
sudo apt install build-essential flatpak flatpak-builder gnome-software-plugin-flatpak -y
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
-flatpak install flathub org.gnome.Platform//48 org.gnome.Sdk//48
+flatpak install flathub org.gnome.Platform//50 org.gnome.Sdk//50 org.freedesktop.Sdk.Extension.llvm21//25.08
##
diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp
index 0a1ee7a214..7107e1ba1e 100644
--- a/src/OrcaSlicer.cpp
+++ b/src/OrcaSlicer.cpp
@@ -23,6 +23,7 @@
#include
#include
#include
+#include
#if defined(__linux__) || defined(__LINUX__)
#include
@@ -4456,7 +4457,7 @@ int CLI::run(int argc, char **argv)
size_t num_objects = model.objects.size();
for (size_t i = 0; i < num_objects; ++ i) {
ModelObjectPtrs new_objects;
- model.objects.front()->split(&new_objects);
+ model.objects.front()->split(&new_objects, false); // TODO: add cli option to enable this?
model.delete_object(size_t(0));
}
}
@@ -6435,6 +6436,7 @@ int CLI::run(int argc, char **argv)
glfwGetVersion(&gl_major, &gl_minor, &gl_verbos);
BOOST_LOG_TRIVIAL(info) << boost::format("opengl version %1%.%2%.%3%")%gl_major %gl_minor %gl_verbos;
+ bool thumbnail_opengl_ready = false;
glfwSetErrorCallback(glfw_callback);
int ret = glfwInit();
if (ret == GLFW_FALSE) {
@@ -6463,13 +6465,22 @@ int CLI::run(int argc, char **argv)
GLFWwindow* window = glfwCreateWindow(640, 480, "base_window", NULL, NULL);
if (window == NULL)
{
- BOOST_LOG_TRIVIAL(error) << "Failed to create GLFW window" << std::endl;
+ BOOST_LOG_TRIVIAL(error) << "Failed to create GLFW window; skipping thumbnail rendering for CLI export" << std::endl;
}
- else
+ else {
glfwMakeContextCurrent(window);
+ thumbnail_opengl_ready = true;
+ }
}
//opengl manager related logic
+ if (!thumbnail_opengl_ready) {
+ BOOST_LOG_TRIVIAL(error) << "OpenGL context unavailable; skip thumbnail generating" << std::endl;
+ need_create_thumbnail_group = false;
+ need_create_no_light_group = false;
+ need_create_top_group = false;
+ }
+ else
{
GUI::OpenGLManager opengl_mgr;
bool opengl_valid = opengl_mgr.init_gl(false);
@@ -7449,6 +7460,13 @@ extern "C" {
#else /* _MSC_VER */
int main(int argc, char **argv)
{
+#ifndef _WIN32
+ // Ignore SIGPIPE so a write to a closed socket (e.g. a dropped printer
+ // network connection) returns EPIPE to the caller instead of terminating
+ // the whole process. Without this, losing the printer link kills
+ // OrcaSlicer with SIGPIPE (exit 141) and produces no crash report.
+ std::signal(SIGPIPE, SIG_IGN);
+#endif
return CLI().run(argc, argv);
}
#endif /* _MSC_VER */
diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp
index 55918e86c2..b35a754c2b 100644
--- a/src/libslic3r/AppConfig.cpp
+++ b/src/libslic3r/AppConfig.cpp
@@ -212,8 +212,14 @@ void AppConfig::set_defaults()
if (get("camera_navigation_style").empty())
set("camera_navigation_style", "0");
- if (get("swap_mouse_buttons").empty())
- set_bool("swap_mouse_buttons", false);
+ if (get("left_mouse_drag_action").empty())
+ set("left_mouse_drag_action", "2");
+
+ if (get("middle_mouse_drag_action").empty())
+ set("middle_mouse_drag_action", "1");
+
+ if (get("right_mouse_drag_action").empty())
+ set("right_mouse_drag_action", "1");
if (get("reverse_mouse_wheel_zoom").empty())
set_bool("reverse_mouse_wheel_zoom", false);
@@ -230,6 +236,29 @@ void AppConfig::set_defaults()
if (get("camera_orbit_mult").empty())
set("camera_orbit_mult", "1.0");
+ if (get(SETTING_OPENGL_AA_SAMPLES).empty())
+ set(SETTING_OPENGL_AA_SAMPLES, "4");
+
+ if (get(SETTING_OPENGL_FXAA_ENABLED).empty())
+ set_bool(SETTING_OPENGL_FXAA_ENABLED, false);
+
+ if (get(SETTING_OPENGL_FPS_CAP).empty())
+ set(SETTING_OPENGL_FPS_CAP, "0");
+ else {
+ int fps_cap = 0;
+ try {
+ fps_cap = std::stoi(get(SETTING_OPENGL_FPS_CAP));
+ }
+ catch (...) {
+ fps_cap = 0;
+ }
+ fps_cap = std::max(0, std::min(fps_cap, 240));
+ set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap));
+ }
+
+ if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
+ set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
+
if (get("export_sources_full_pathnames").empty())
set_bool("export_sources_full_pathnames", false);
diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp
index 06b919484f..33f243905a 100644
--- a/src/libslic3r/AppConfig.hpp
+++ b/src/libslic3r/AppConfig.hpp
@@ -30,6 +30,10 @@ using namespace nlohmann;
#define SETTING_NETWORK_PLUGIN_REMIND_LATER "network_plugin_remind_later"
#define SETTING_USE_ENCRYPTED_TOKEN_FILE "use_encrypted_token_file"
#define SETTING_CLOUD_PROVIDERS "cloud_providers"
+#define SETTING_OPENGL_AA_SAMPLES "opengl_antialiasing_samples"
+#define SETTING_OPENGL_FXAA_ENABLED "opengl_fxaa_enabled"
+#define SETTING_OPENGL_FPS_CAP "opengl_fps_cap"
+#define SETTING_OPENGL_SHOW_FPS_OVERLAY "opengl_show_fps_overlay"
#if defined(_WIN32) || defined(_WIN64)
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp
index 8611ec302d..f8b1483773 100644
--- a/src/libslic3r/CutUtils.cpp
+++ b/src/libslic3r/CutUtils.cpp
@@ -63,7 +63,7 @@ static void add_cut_volume(TriangleMesh& mesh, ModelObject* object, const ModelV
vol->cut_info = src_volume->cut_info;
}
-static void process_volume_cut( ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix,
+static void process_volume_cut( const ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix,
ModelObjectCutAttributes attributes, TriangleMesh& upper_mesh, TriangleMesh& lower_mesh)
{
const auto volume_matrix = volume->get_matrix();
@@ -178,7 +178,7 @@ static void process_modifier_cut(ModelVolume* volume, const Transform3d& instanc
lower->add_volume(*volume);
}
-static void process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix,
+static void process_solid_part_cut(const ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix,
ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower)
{
// Perform cut
@@ -279,9 +279,22 @@ void Cut::post_process(ModelObject* upper, ModelObject* lower, ModelObjectPtrs&
}
-void Cut::finalize(const ModelObjectPtrs& objects)
+void Cut::finalize(const ModelObjectPtrs& objects, const std::vector>& saved_paintings)
{
- //clear model from temporarry objects
+ // Paint volumes
+ for (const auto& saved_painting : saved_paintings) {
+ if (saved_painting) {
+ for (const auto object : objects) {
+ for (const auto volume : object->volumes) {
+ if (volume->is_model_part() && !volume->is_cut_connector()) {
+ volume->restore_painting(saved_painting, true);
+ }
+ }
+ }
+ }
+ }
+
+ //clear model from temporary objects
m_model.clear_objects();
// add to model result objects
@@ -320,7 +333,17 @@ const ModelObjectPtrs& Cut::perform_with_plane()
const Transformation cut_transformation = Transformation(m_cut_matrix);
const Transform3d inverse_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * translation_transform(-1. * cut_transformation.get_offset());
+ std::vector> saved_paintings;
for (ModelVolume* volume : mo->volumes) {
+ // Save painting data before reset_extra_facets() discards it.
+ if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) {
+ saved_paintings.emplace_back(volume->save_painting());
+ if (saved_paintings.back()) {
+ // Transform mesh to cut space (same transform as process_volume_cut applies)
+ saved_paintings.back()->mesh.transform(instance_matrix * volume->get_matrix(), true);
+ }
+ }
+
volume->reset_extra_facets();
if (!volume->is_model_part()) {
@@ -376,9 +399,9 @@ const ModelObjectPtrs& Cut::perform_with_plane()
}
}
- BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end";
+ finalize(cut_object_ptrs, saved_paintings);
- finalize(cut_object_ptrs);
+ BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end";
return m_model.objects;
}
@@ -431,7 +454,7 @@ static void merge_solid_parts_inside_object(ModelObjectPtrs& objects)
}
-const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowels_count)
+const ModelObjectPtrs& Cut::perform_by_contour(const ModelObject* src_object, std::vector parts, int dowels_count)
{
ModelObject* cut_mo = m_model.objects.front();
@@ -446,6 +469,19 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowe
lower->name = lower->name + "_B";
}
+ // Save painting data so we later can remap it.
+ std::vector> saved_paintings;
+ if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) {
+ const auto instance_matrix = src_object->instances[m_instance]->get_transformation().get_matrix_no_offset();
+ for (const auto volume : src_object->volumes) {
+ saved_paintings.emplace_back(volume->save_painting());
+ if (saved_paintings.back()) {
+ // Transform mesh to cut space (same transform as process_volume_cut applies)
+ saved_paintings.back()->mesh.transform(instance_matrix * volume->get_matrix(), true);
+ }
+ }
+ }
+
const size_t cut_parts_cnt = parts.size();
bool has_modifiers = false;
@@ -475,7 +511,7 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowe
merge_solid_parts_inside_object(cut_object_ptrs);
// replace initial objects in model with cut object
- finalize(cut_object_ptrs);
+ finalize(cut_object_ptrs, saved_paintings);
}
else if (volumes.size() > cut_parts_cnt) {
// Means that object is cut with connectors
@@ -506,7 +542,7 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowe
merge_solid_parts_inside_object(cut_object_ptrs);
// replace initial objects in model with cut object
- finalize(cut_object_ptrs);
+ finalize(cut_object_ptrs, saved_paintings);
// Add Dowel-connectors as separate objects to model
if (cut_connectors_obj.size() >= 3)
@@ -518,7 +554,12 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowe
}
-const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Transform3d& rotation_m, bool keep_as_parts/* = false*/)
+const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove,
+ const Transform3d& rotation_m,
+ const int groove_count,
+ const float groove_gap,
+ const float m_radius,
+ bool keep_as_parts /* = false*/)
{
ModelObject* cut_mo = m_model.objects.front();
@@ -532,6 +573,20 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran
upper->name = upper->name + "_A";
lower->name = lower->name + "_B";
}
+
+ // Save painting data so we later can remap it.
+ std::vector> saved_paintings;
+ if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) {
+ const auto instance_matrix = cut_mo->instances[m_instance]->get_transformation().get_matrix_no_offset();
+ for (const auto volume : cut_mo->volumes) {
+ saved_paintings.emplace_back(volume->save_painting());
+ if (saved_paintings.back()) {
+ // Transform mesh to cut space (same transform as process_volume_cut applies)
+ saved_paintings.back()->mesh.transform(instance_matrix * volume->get_matrix(), true);
+ }
+ }
+ }
+
const double groove_half_depth = 0.5 * double(groove.depth);
Model tmp_model_for_cut = Model();
@@ -565,61 +620,108 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran
reset_instance_transformation(object, m_instance);
};
- // cut by upper plane
-
- const Transform3d cut_matrix_upper = translation_transform(rotation_m * (groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix;
+ // cut by upper plane (+Z)
{
+ const Transform3d cut_matrix_upper = translation_transform(rotation_m * (groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix;
+
cut(tmp_object, cut_matrix_upper, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
add_volumes_from_cut(upper, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
}
- // cut by lower plane
-
- const Transform3d cut_matrix_lower = translation_transform(rotation_m * (-groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix;
+ // cut by lower plane (-Z)
{
+ const Transform3d cut_matrix_lower = translation_transform(rotation_m * (-groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix;
+
cut(tmp_object, cut_matrix_lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
}
- // cut middle part with 2 angles and add parts to related upper/lower objects
+ // Compute same slot outer width used in preview plane
+ const float groove_width = calculate_groove_width(groove, m_radius);
- const double h_side_shift = 0.5 * double(groove.width + groove.depth / tan(groove.flaps_angle));
+ ModelObject* groove_object{nullptr};
- // cut by angle1 plane
- {
- const Transform3d cut_matrix_angle1 = translation_transform(rotation_m * (-h_side_shift * Vec3d::UnitX())) * m_cut_matrix * rotation_transform(Vec3d(0, -groove.flaps_angle, -groove.angle));
+ // multiple cuts
+ for (int i = 0; i < groove_count; i++) {
+ bool is_first_groove = i == 0;
+ bool is_last_groove = i == groove_count - 1;
- cut(tmp_object, cut_matrix_angle1, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
- add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
+ // Calculate the x-axis offset for this dovetail
+ float groove_offset_factor_start = -.5 * ((groove_count - 1));
+ float groove_offset_factor = groove_offset_factor_start + i;
+
+ float offset_x = groove_offset_factor * (groove_gap + groove_width);
+
+
+ tmp_object->clone_for_cut(&groove_object);
+ for (ModelVolume* volume : tmp_object->volumes) {
+ ModelVolume* new_vol = groove_object->add_volume(*volume);
+ new_vol->reset_from_upper();
+ }
+
+ // isolate area of current groove
+ if (!is_first_groove) {
+ float left_cut_position = (-groove_gap / 2.f) - (groove_width / 2.f) + offset_x;
+
+ const Transform3d cut_matrix_left = translation_transform(rotation_m * (left_cut_position * Vec3d::UnitX())) *
+ m_cut_matrix * rotation_transform(Vec3d(0, M_PI / 2.0, 0));
+
+ cut(groove_object, cut_matrix_left, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
+ }
+ if (!is_last_groove) {
+ float right_cut_position = (groove_gap / 2.f) + (groove_width / 2.f) + offset_x;
+
+ const Transform3d cut_matrix_right = translation_transform(rotation_m * (right_cut_position * Vec3d::UnitX())) *
+ m_cut_matrix * rotation_transform(Vec3d(0, M_PI / 2.0, 0));
+ cut(groove_object, cut_matrix_right, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
+ }
+
+ const Transform3d groove_translation = translation_transform(rotation_m * (offset_x * Vec3d::UnitX()));
+ // cut middle part with 2 angles and add parts to related upper/lower objects
+ const double h_side_shift = 0.5 * double(groove.width + groove.depth / tan(groove.flaps_angle));
+
+ // cut by angle1 plane
+ {
+ const Transform3d cut_matrix_angle1 = groove_translation * translation_transform(rotation_m * (-h_side_shift * Vec3d::UnitX())) *
+ m_cut_matrix * rotation_transform(Vec3d(0, -groove.flaps_angle, -groove.angle));
+
+ cut(groove_object, cut_matrix_angle1, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
+ add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
+ }
+
+ // cut by angle2 plane
+ {
+ const Transform3d cut_matrix_angle2 = groove_translation * translation_transform(rotation_m * (h_side_shift * Vec3d::UnitX())) *
+ m_cut_matrix * rotation_transform(Vec3d(0, groove.flaps_angle, groove.angle));
+
+ cut(groove_object, cut_matrix_angle2, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
+ add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
+ }
+
+ // apply tolerance to the middle part
+ {
+ const double h_groove_shift_tolerance = groove_half_depth - (double)groove.depth_tolerance;
+
+ const Transform3d cut_matrix_lower_tolerance = groove_translation * translation_transform(rotation_m * (-h_groove_shift_tolerance * Vec3d::UnitZ())) *
+ m_cut_matrix;
+ cut(groove_object, cut_matrix_lower_tolerance, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
+
+ const double h_side_shift_tolerance = h_side_shift - 0.5 * double(groove.width_tolerance);
+
+ const Transform3d cut_matrix_angle1_tolerance = groove_translation * translation_transform(rotation_m * (-h_side_shift_tolerance * Vec3d::UnitX())) *
+ m_cut_matrix * rotation_transform(Vec3d(0, -groove.flaps_angle, -groove.angle));
+ cut(groove_object, cut_matrix_angle1_tolerance, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
+
+ const Transform3d cut_matrix_angle2_tolerance = groove_translation * translation_transform(rotation_m * (h_side_shift_tolerance * Vec3d::UnitX())) *
+ m_cut_matrix * rotation_transform(Vec3d(0, groove.flaps_angle, groove.angle));
+ cut(groove_object, cut_matrix_angle2_tolerance, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
+ }
+
+ add_volumes_from_cut(upper, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
+
+ groove_object->clear_volumes();
}
- // cut by angle2 plane
- {
- const Transform3d cut_matrix_angle2 = translation_transform(rotation_m * (h_side_shift * Vec3d::UnitX())) * m_cut_matrix * rotation_transform(Vec3d(0, groove.flaps_angle, groove.angle));
-
- cut(tmp_object, cut_matrix_angle2, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
- add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
- }
-
- // apply tolerance to the middle part
- {
- const double h_groove_shift_tolerance = groove_half_depth - (double)groove.depth_tolerance;
-
- const Transform3d cut_matrix_lower_tolerance = translation_transform(rotation_m * (-h_groove_shift_tolerance * Vec3d::UnitZ())) * m_cut_matrix;
- cut(tmp_object, cut_matrix_lower_tolerance, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
-
- const double h_side_shift_tolerance = h_side_shift - 0.5 * double(groove.width_tolerance);
-
- const Transform3d cut_matrix_angle1_tolerance = translation_transform(rotation_m * (-h_side_shift_tolerance * Vec3d::UnitX())) * m_cut_matrix * rotation_transform(Vec3d(0, -groove.flaps_angle, -groove.angle));
- cut(tmp_object, cut_matrix_angle1_tolerance, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
-
- const Transform3d cut_matrix_angle2_tolerance = translation_transform(rotation_m * (h_side_shift_tolerance * Vec3d::UnitX())) * m_cut_matrix * rotation_transform(Vec3d(0, groove.flaps_angle, groove.angle));
- cut(tmp_object, cut_matrix_angle2_tolerance, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
- }
-
- // this part can be added to the upper object now
- add_volumes_from_cut(upper, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
-
ModelObjectPtrs cut_object_ptrs;
if (keep_as_parts) {
@@ -658,10 +760,24 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran
merge_solid_parts_inside_object(cut_object_ptrs);
}
- finalize(cut_object_ptrs);
+ finalize(cut_object_ptrs, saved_paintings);
return m_model.objects;
}
+float Cut::calculate_groove_width (const Cut::Groove& groove, const float m_radius)
+{
+ // Compute same slot outer width used in preview plane
+ const double flap_width = is_approx(groove.flaps_angle, 0.f) ? groove.depth : groove.depth / sin(groove.flaps_angle);
+ const double total_flap_width = 2.0 * flap_width * cos(groove.flaps_angle);
+ const double slot_neck_half_width = 0.5f * (groove.width);
+ const double slot_mouth_half_width = 0.5 * (groove.width + total_flap_width);
+ const double plane_half_height = 0.5f* (1.5f * (1.5f *m_radius));
+ const double flap_taper_offset = plane_half_height * tan(groove.angle);
+ const double slot_outer_x_max = std::max(slot_mouth_half_width + flap_taper_offset, slot_neck_half_width + flap_taper_offset);
+
+ return float(2.0 * slot_outer_x_max);
+}
+
} // namespace Slic3r
diff --git a/src/libslic3r/CutUtils.hpp b/src/libslic3r/CutUtils.hpp
index 2c477a3e2b..b7f31d7b9a 100644
--- a/src/libslic3r/CutUtils.hpp
+++ b/src/libslic3r/CutUtils.hpp
@@ -11,7 +11,7 @@ namespace Slic3r {
using ModelObjectPtrs = std::vector;
-enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, KeepAsParts, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels, InvalidateCutInfo };
+enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, KeepAsParts, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels, InvalidateCutInfo, KeepPaint };
using ModelObjectCutAttributes = enum_bitmask;
ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute);
@@ -25,7 +25,7 @@ class Cut {
void post_process(ModelObject* object, ModelObjectPtrs& objects, bool keep, bool place_on_cut, bool flip);
void post_process(ModelObject* upper_object, ModelObject* lower_object, ModelObjectPtrs& objects);
- void finalize(const ModelObjectPtrs& objects);
+ void finalize(const ModelObjectPtrs& objects, const std::vector>& saved_paintings);
public:
@@ -56,10 +56,16 @@ public:
};
const ModelObjectPtrs& perform_with_plane();
- const ModelObjectPtrs& perform_by_contour(std::vector parts, int dowels_count);
- const ModelObjectPtrs& perform_with_groove(const Groove& groove, const Transform3d& rotation_m, bool keep_as_parts = false);
+ const ModelObjectPtrs& perform_by_contour(const ModelObject* src_object, std::vector parts, int dowels_count);
+ const ModelObjectPtrs& perform_with_groove(const Groove& groove,
+ const Transform3d& rotation_m,
+ const int groove_count,
+ const float groove_gap,
+ const float m_radius,
+ bool keep_as_parts = false);
-}; // namespace Cut
+ static float calculate_groove_width(const Cut::Groove& groove, const float m_radius);
+ }; // namespace Cut
} // namespace Slic3r
diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp
index 74e6d14d14..d47518bbca 100644
--- a/src/libslic3r/Fill/Fill.cpp
+++ b/src/libslic3r/Fill/Fill.cpp
@@ -271,6 +271,9 @@ struct SurfaceFillParams
// Params for Lateral honeycomb
float infill_overhang_angle = 60.f;
+ // For Gyroid: when true, use the parameterized "optimized" wave.
+ bool gyroid_optimized = false;
+
bool operator<(const SurfaceFillParams &rhs) const {
#define RETURN_COMPARE_NON_EQUAL(KEY) if (this->KEY < rhs.KEY) return true; if (this->KEY > rhs.KEY) return false;
#define RETURN_COMPARE_NON_EQUAL_TYPED(TYPE, KEY) if (TYPE(this->KEY) < TYPE(rhs.KEY)) return true; if (TYPE(this->KEY) > TYPE(rhs.KEY)) return false;
@@ -303,6 +306,7 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(symmetric_infill_y_axis);
RETURN_COMPARE_NON_EQUAL(infill_lock_depth);
RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
+ RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
return false;
}
@@ -330,7 +334,8 @@ struct SurfaceFillParams
this->lateral_lattice_angle_2 == rhs.lateral_lattice_angle_2 &&
this->infill_lock_depth == rhs.infill_lock_depth &&
this->skin_infill_depth == rhs.skin_infill_depth &&
- this->infill_overhang_angle == rhs.infill_overhang_angle;
+ this->infill_overhang_angle == rhs.infill_overhang_angle &&
+ this->gyroid_optimized == rhs.gyroid_optimized;
}
};
@@ -920,6 +925,12 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p
// Orca: apply fill multiline only for sparse infill
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
+ // Pass through gyroid_optimized only when the effective pattern is Gyroid,
+ // so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
+ // (which would unnecessarily split fill batching).
+ // Stored on SurfaceFillParams; copied to FillParams during conversion.
+ params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized;
+
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
region_config.sparse_infill_rotate_template.value);
@@ -1273,6 +1284,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.lateral_lattice_angle_1 = surface_fill.params.lateral_lattice_angle_1;
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
+ params.gyroid_optimized = surface_fill.params.gyroid_optimized;
// BBS
params.flow = surface_fill.params.flow;
@@ -1468,6 +1480,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
+ params.gyroid_optimized = surface_fill.params.gyroid_optimized;
for (ExPolygon &expoly : surface_fill.expolygons) {
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp
index 47195c850d..ef6a6bc804 100644
--- a/src/libslic3r/Fill/FillBase.hpp
+++ b/src/libslic3r/Fill/FillBase.hpp
@@ -79,6 +79,9 @@ struct FillParams
// Layer height for Concentric infill with Arachne.
coordf_t layer_height { 0.f };
+ // For Gyroid: when true, use the parameterized "optimized" variant.
+ bool gyroid_optimized { false };
+
// For Lateral lattice
coordf_t lateral_lattice_angle_1 { 0.f };
coordf_t lateral_lattice_angle_2 { 0.f };
diff --git a/src/libslic3r/Fill/FillGyroid.cpp b/src/libslic3r/Fill/FillGyroid.cpp
index 8700c1746e..c740dd2b4b 100644
--- a/src/libslic3r/Fill/FillGyroid.cpp
+++ b/src/libslic3r/Fill/FillGyroid.cpp
@@ -1,4 +1,5 @@
#include "../ClipperUtils.hpp"
+#include "../MarchingSquares.hpp"
#include "../ShortestPath.hpp"
#include "../Surface.hpp"
#include
@@ -7,6 +8,99 @@
#include "FillBase.hpp"
#include "FillGyroid.hpp"
+// ---------------------------------------------------------------------------
+// Marching-squares scalar field for the optimized gyroid branch.
+// Modeled after FillTpmsFK.cpp's ScalarField.
+//
+// The gyroid scalar field is the standard implicit equation
+// F(x,y,z) = sin(fx*x)cos(fy*y) + sin(fy*y)cos(fz*z) + sin(fz*z)cos(fx*x)
+// Marching squares extracts the iso-zero contour, which gives smoother
+// transitions between vertical and horizontal regimes than the analytical
+// asin-based wave generator. Setting fz = omega * baseline anisotropically
+// tightens the wave along the layer-stacking axis, shortening the effective
+// vertical strand length and improving column-buckling resistance under
+// Z-axis compression.
+// ---------------------------------------------------------------------------
+namespace marchsq {
+using namespace Slic3r;
+
+using coordr_t = long;
+using Pointf = Vec2d;
+
+struct GyroidField
+{
+ static constexpr float gsizef = 0.40f;
+ static constexpr float rsizef = 0.004f;
+ const coord_t rsize = scaled(rsizef);
+ const coordr_t gsize = std::round(gsizef / rsizef);
+ Point size;
+ Point offs;
+ coordf_t z;
+ float fx;
+ float fy;
+ float fz;
+ float isoval = 0.0f;
+
+ explicit GyroidField(const BoundingBox bb, const coordf_t z, const float period, const float omega = 1.0f)
+ : size{bb.size()}, offs{bb.min}, z{z}
+ {
+ const float baseline = float(2.0 * PI) / std::max(period, 1e-3f);
+ fx = baseline;
+ fy = baseline;
+ fz = omega * baseline;
+ }
+
+ float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const
+ {
+ const float a = fx * float(x);
+ const float b = fy * float(y);
+ const float c = fz * float(z_arg);
+ return std::sin(a) * std::cos(b) + std::sin(b) * std::cos(c) + std::sin(c) * std::cos(a);
+ }
+
+ float get_scalar(Coord p) const
+ {
+ Pointf pf = to_Pointf(p);
+ return get_scalar(pf.x(), pf.y(), z);
+ }
+
+ inline coord_t to_coord (const coordr_t& x) const { return x * rsize; }
+ inline coordr_t to_coordr(const coord_t& x) const { return x / rsize; }
+ inline Point to_Point (const Coord& p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
+ inline Coord to_Coord (const Point& p) const { return Coord(to_coordr(p.y() - offs.y()), to_coordr(p.x() - offs.x())); }
+ inline Pointf to_Pointf(const Point& p) const { return Pointf(unscaled(p.x()), unscaled(p.y())); }
+ inline Pointf to_Pointf(const Coord& p) const { return to_Pointf(to_Point(p)); }
+};
+
+template<> struct _RasterTraits
+{
+ using ValueType = float;
+ static float get (const GyroidField& sf, size_t row, size_t col) { return sf.get_scalar(Coord(row, col)); }
+ static size_t rows(const GyroidField& sf) { return sf.to_coordr(sf.size.y()); }
+ static size_t cols(const GyroidField& sf) { return sf.to_coordr(sf.size.x()); }
+};
+
+inline Polylines get_gyroid_polylines(const GyroidField& sf, const double tolerance = SCALED_EPSILON)
+{
+ std::vector rings = execute_with_policy(ex_tbb, sf, sf.isoval, {sf.gsize, sf.gsize});
+ Polylines polys;
+ polys.reserve(rings.size());
+ for (const Ring& ring : rings) {
+ Polyline poly;
+ Points& pts = poly.points;
+ pts.reserve(ring.size() + 1);
+ for (const Coord& crd : ring)
+ pts.emplace_back(sf.to_Point(crd));
+ pts.push_back(pts.front());
+ if (tolerance >= 0.0)
+ poly.simplify(tolerance);
+ polys.emplace_back(poly);
+ }
+ return polys;
+}
+
+} // namespace marchsq
+
namespace Slic3r {
static inline double f(double x, double z_sin, double z_cos, bool vertical, bool flip)
@@ -102,6 +196,49 @@ static std::vector make_one_period(double width, double scaleFactor, doub
return points;
}
+// ---------------------------------------------------------------------------
+// "Optimized" gyroid wave: marching-squares variant gated on
+// params.gyroid_optimized. The wave shape is extracted from the gyroid
+// implicit scalar field (see marchsq::GyroidField above) at iso=0, with
+// the Z dimension's spatial frequency multiplied by an Euler-Bernoulli
+// buckling-derived factor so the vertical strands become shorter columns,
+// raising the critical buckling load against Z-axis compression.
+//
+// The formula is INVERTED from a naive "scale with density" derivation:
+// at LOW density the gyroid strands are long and slender (prime buckling
+// targets), so they need the most shortening; at high density the strands
+// are already short and need little extra help. omega is therefore the
+// inverse-square-root of density_adjusted:
+//
+// omega = sqrt(1 / density_adj) / sqrt(1 + layer_h/spacing),
+// clamped [1.0, 2.0]
+//
+// fx and fy are left at the baseline frequency, so the per-XY-slice line
+// length per unit area is preserved -> mass at the same `sparse_infill_density`
+// setting matches the standard gyroid path. Strength gain comes purely from
+// the shorter vertical column length (P_cr proportional to 1/L^2).
+//
+// Empirical Python sim (sim_gyroid_compare.py) at layer_h=0.20, spacing=0.45:
+//
+// density omega line/std strength/std strength_per_mass
+// 10% 2.00 1.00 2.84 2.84
+// 15% 1.38 1.00 1.89 1.89
+// 20% 1.19 1.00 1.42 1.42
+// 30% 1.00 1.00 1.00 1.00
+// 50%+ 1.00 1.00 1.00 1.00
+//
+// When gyroid_optimized is false, behavior is byte-identical to the
+// standard parametric gyroid path below.
+// ---------------------------------------------------------------------------
+
+static inline double compute_omega_factor(double density_adjusted, double line_spacing, double layer_height)
+{
+ double lh_ratio = (line_spacing > 0.) ? layer_height / line_spacing : 0.5;
+ double correction = 1.0 / std::sqrt(1.0 + lh_ratio);
+ double raw = std::sqrt(1.0 / std::max(density_adjusted, 0.1)) * correction;
+ return std::clamp(raw, 1.0, 2.0);
+}
+
static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
{
const double scaleFactor = scale_(line_spacing) / density_adjusted;
@@ -173,16 +310,41 @@ void FillGyroid::_fill_surface_single(
bb.offset(expand);
// generate pattern
- Polylines polylines = make_gyroid_waves(
- scale_(this->z),
- density_adjusted,
- this->spacing,
- ceil(bb.size()(0) / distance) + 1.,
- ceil(bb.size()(1) / distance) + 1.);
+ Polylines polylines;
+ if (params.gyroid_optimized) {
+ // Marching-squares path on the gyroid implicit field. Base period matches
+ // the standard parametric path's wavelength: 2*pi * spacing / density_adj.
+ // omega >= 1 always, so fz >= baseline -> shorter vertical wavelength ->
+ // shorter effective column length -> higher buckling resistance.
+ //
+ // Mass: fx and fy are left at baseline (same as standard), so the
+ // per-XY-slice line length per unit area is approximately preserved.
+ // Empirically (sim_gyroid_compare.py) the optimized line/std ratio is
+ // ~1.000 across densities, so no period compensation is needed.
+ const double lh = (params.layer_height > 0.) ? double(params.layer_height) : double(this->spacing);
+ const double omega = compute_omega_factor(density_adjusted, this->spacing * params.multiline, lh);
- // shift the polyline to the grid origin
- for (Polyline &pl : polylines)
- pl.translate(bb.min);
+ const float density_factor = std::max(0.001f, float(params.density * DensityAdjust / params.multiline));
+ const float period = float(2.0 * M_PI) * float(this->spacing) / density_factor;
+
+ // bb is already expanded above by 10 * scale_(spacing) for edge artifacts;
+ // skip a second offset here to avoid raster-area bloat in the marching squares pass.
+ marchsq::GyroidField sf(bb, this->z, period, float(omega));
+ polylines = marchsq::get_gyroid_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
+ } else {
+ polylines = make_gyroid_waves(
+ scale_(this->z),
+ density_adjusted,
+ this->spacing,
+ ceil(bb.size()(0) / distance) + 1.,
+ ceil(bb.size()(1) / distance) + 1.);
+
+ // The parametric generator produces wave coords relative to the grid origin;
+ // shift them into absolute layer coords. The marching-squares branch above
+ // already emits absolute coords via GyroidField::to_Point, so it skips this.
+ for (Polyline &pl : polylines)
+ pl.translate(bb.min);
+ }
// Apply multiline offset if needed
multiline_fill(polylines, params, spacing);
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index 34495abd4c..542600556b 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -1163,10 +1163,15 @@ static std::vector get_path_of_change_filament(const Print& print)
const bool is_ramming = (gcodegen.config().single_extruder_multi_material) ||
(!gcodegen.config().single_extruder_multi_material &&
gcodegen.config().filament_multitool_ramming.get_at(tcr.initial_tool));
+ // Orca: user-facing override (Printer Settings > Wipe tower > "Tool change on wipe tower").
+ // Forces the toolhead to travel over the wipe tower before issuing Tx even on multi-toolhead
+ // printers without ramming, where Orca would otherwise emit Tx in place (potentially over the part).
+ const bool tool_change_on_wipe_tower = gcodegen.config().tool_change_on_wipe_tower.value;
const bool should_travel_to_tower = !tcr.priming && (tcr.force_travel // wipe tower says so
|| !needs_toolchange // this is just finishing the tower with no toolchange
|| will_go_down // Make sure to move to prime tower before moving down
- || is_ramming);
+ || is_ramming
+ || tool_change_on_wipe_tower);
if (should_travel_to_tower || gcodegen.m_need_change_layer_lift_z) {
// FIXME: It would be better if the wipe tower set the force_travel flag for all toolchanges,
@@ -5887,6 +5892,9 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
// if polyline was shorter than the clipping distance we'd get a null polyline, so
// we discard it in that case
const double seam_gap = scale_(m_config.seam_gap.get_abs_value(nozzle_diameter));
+ const bool seam_gap_applied = enable_seam_slope || m_enable_loop_clipping;
+ const double seam_gap_distance_mm = seam_gap_applied ? unscale_(seam_gap) : 0.0;
+ double seam_scarf_distance_mm = 0.0;
const double clip_length = m_enable_loop_clipping && !enable_seam_slope ? seam_gap : 0;
// get paths
@@ -6035,6 +6043,7 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
const double slope_min_length = slope_entire_loop ? loop_length : std::min(m_config.seam_slope_min_length.value, loop_length);
const int slope_steps = m_config.seam_slope_steps;
const double slope_max_segment_length = scale_(slope_min_length / slope_steps);
+ seam_scarf_distance_mm = slope_min_length;
// Calculate the sloped loop
ExtrusionLoopSloped new_loop(paths, seam_gap, slope_min_length, slope_max_segment_length, start_slope_ratio, loop.loop_role());
@@ -6059,6 +6068,11 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
}
}
+ if (description == "perimeter") {
+ m_processor.result().print_statistics.total_seam_gap_distance += static_cast(seam_gap_distance_mm);
+ m_processor.result().print_statistics.total_seam_scarf_distance += static_cast(seam_scarf_distance_mm);
+ }
+
// BBS
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
m_wipe.path = Polyline();
diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp
index 41e612fdab..8cfb99a46b 100644
--- a/src/libslic3r/GCode/CoolingBuffer.cpp
+++ b/src/libslic3r/GCode/CoolingBuffer.cpp
@@ -719,6 +719,9 @@ std::string CoolingBuffer::apply_layer_cooldown(
// Second generate the adjusted G-code.
std::string new_gcode;
new_gcode.reserve(gcode.size() * 2);
+ // ORCA: per-printer minimum non-zero fan PWM. Applied at every part-cooling fan emission below so any
+ // non-zero command is raised to at least this percent (0 disables the clamp).
+ const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value));
bool overhang_fan_control= false;
int overhang_fan_speed = 0;
bool internal_bridge_fan_control= false; // ORCA: Add support for separate internal bridge fan speed control
@@ -727,7 +730,7 @@ std::string CoolingBuffer::apply_layer_cooldown(
int supp_interface_fan_speed = 0;
bool ironing_fan_control= false; // ORCA: Add support for ironing fan speed control
int ironing_fan_speed = 0; // ORCA: Add support for ironing fan speed control
- auto change_extruder_set_fan = [ this, layer_id, layer_time, &new_gcode,
+ auto change_extruder_set_fan = [ this, layer_id, layer_time, &new_gcode, part_cooling_fan_min_pwm,
&overhang_fan_control, &overhang_fan_speed,
&internal_bridge_fan_control, &internal_bridge_fan_speed,
&supp_interface_fan_control, &supp_interface_fan_speed,
@@ -743,11 +746,11 @@ std::string CoolingBuffer::apply_layer_cooldown(
int full_fan_speed_layer = EXTRUDER_CONFIG(full_fan_speed_layer);
supp_interface_fan_speed = EXTRUDER_CONFIG(support_material_interface_fan_speed);
- if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) {
- // When ramping up fan speed from close_fan_the_first_x_layers to full_fan_speed_layer, force close_fan_the_first_x_layers above zero,
- // so there will be a zero fan speed at least at the 1st layer.
- close_fan_the_first_x_layers = 1;
- }
+ // ORCA: previously a silent override forced `close_fan_the_first_x_layers` from 0 up to 1 whenever a ramp
+ // was configured (`full_fan_speed_layer > 0`), so the first printed layer always had the fan disabled.
+ // That hid the user's literal "no cooling for the first 0 layers" setting and produced a non-zero starting
+ // factor on the ramp denominator. The override has been removed: with N=0 and M>0 the ramp now genuinely
+ // starts on layer 0 at a factor of 1/M and reaches 100% at layer M-1, matching the intent of the option.
if (int(layer_id) >= close_fan_the_first_x_layers) {
float fan_max_speed = EXTRUDER_CONFIG(fan_max_speed);
float slow_down_layer_time = float(EXTRUDER_CONFIG(slow_down_layer_time));
@@ -806,7 +809,7 @@ std::string CoolingBuffer::apply_layer_cooldown(
m_fan_speed = fan_speed_new;
m_current_fan_speed = fan_speed_new;
if (immediately_apply)
- new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed);
+ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm);
}
//BBS
if (additional_fan_speed_new != m_additional_fan_speed) {
@@ -980,26 +983,26 @@ std::string CoolingBuffer::apply_layer_cooldown(
if (need_set_fan) {
if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
- new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, overhang_fan_speed);
+ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, overhang_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = overhang_fan_speed;
} else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control
- new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, internal_bridge_fan_speed);
+ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, internal_bridge_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = internal_bridge_fan_speed;
}
else if (fan_speed_change_requests[CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START]){
- new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, supp_interface_fan_speed);
+ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, supp_interface_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = supp_interface_fan_speed;
}
else if (fan_speed_change_requests[CoolingLine::TYPE_IRONING_FAN_START]){
- new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, ironing_fan_speed);
+ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, ironing_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = ironing_fan_speed;
}
else if(fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] && m_current_fan_speed != -1){
- new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed);
+ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed, part_cooling_fan_min_pwm);
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
}
else
- new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed);
+ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm);
need_set_fan = false;
}
pos = line_end;
diff --git a/src/libslic3r/GCode/FanMover.cpp b/src/libslic3r/GCode/FanMover.cpp
index 63e47f3f73..112066bb85 100644
--- a/src/libslic3r/GCode/FanMover.cpp
+++ b/src/libslic3r/GCode/FanMover.cpp
@@ -230,7 +230,10 @@ void FanMover::_remove_slow_fan(int16_t min_speed, float past_sec) {
std::string FanMover::_set_fan(int16_t speed) {
//const Tool* tool = m_writer.get_tool(m_currrent_extruder < 20 ? m_currrent_extruder : 0);
- return GCodeWriter::set_fan(m_writer.config.gcode_flavor.value, speed);
+ // ORCA: apply the per-printer non-zero fan PWM floor so reposted fan commands respect the clamp too.
+ const int floor_pct = m_writer.config.part_cooling_fan_min_pwm.value;
+ const unsigned int part_cooling_fan_min_pwm = floor_pct > 0 ? static_cast(floor_pct) : 0u;
+ return GCodeWriter::set_fan(m_writer.config.gcode_flavor.value, speed, part_cooling_fan_min_pwm);
}
diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp
index 1ca9c3ffc4..0ab32a6780 100644
--- a/src/libslic3r/GCode/GCodeProcessor.cpp
+++ b/src/libslic3r/GCode/GCodeProcessor.cpp
@@ -1230,8 +1230,15 @@ void GCodeProcessor::run_post_process()
}
};
+ // Orca: track the current layer during the post-processing pass so that preheat M104s emitted
+ // for tool changes on the first layer use the correct first-layer temperature. The member
+ // m_layer_id is populated during the analysis pass and ends at the total layer count, so it
+ // cannot be used here — it would always select the "other layers" temperature for multi-layer
+ // prints.
+ unsigned int current_layer_id = 0;
+
// add lines M104 to exported gcode
- auto process_line_T = [this, &export_line](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) {
+ auto process_line_T = [this, &export_line, ¤t_layer_id](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) {
const std::string cmd = GCodeReader::GCodeLine::extract_cmd(gcode_line);
int tool_number = -1;
@@ -1257,8 +1264,13 @@ void GCodeProcessor::run_post_process()
export_line.insert_lines(
backtrace, cmd,
// line inserter
- [tool_number, this](unsigned int id, const std::vector& time_diffs) {
- const int temperature = int(m_layer_id != 1 ? m_filament_nozzle_temp[tool_number] :
+ [tool_number, this, ¤t_layer_id](unsigned int id, const std::vector& time_diffs) {
+ // Orca: use the locally-tracked layer index (current_layer_id) rather than
+ // the stale m_layer_id from the analysis pass. current_layer_id == 0 means
+ // we haven't reached the first ;LAYER_CHANGE marker yet (e.g. tool change
+ // inside start gcode); == 1 means we are inside the first printed layer.
+ // Both cases should use the first-layer nozzle temperature.
+ const int temperature = int(current_layer_id > 1 ? m_filament_nozzle_temp[tool_number] :
m_filament_nozzle_temp_first_layer[tool_number]);
// Orca: M104.1 for XL printers, I can't find the documentation for this so I copied the C++ comments from
// Prusa-Firmware-Buddy here
@@ -1351,6 +1363,19 @@ void GCodeProcessor::run_post_process()
if (eol) {
++line_id;
const unsigned int internal_g1_lines_counter = export_line.update(gcode_line, line_id, g1_lines_counter);
+ // Orca: track the current layer for preheat temperature selection.
+ // The line is ";" + reserved_tag(Layer_Change) + EOL; match it independent of
+ // BBL vs. compatible flavor (which differ in the tag text).
+ if (gcode_line.size() > 1 && gcode_line.front() == ';') {
+ std::string_view tag_line(gcode_line);
+ // strip trailing CR/LF
+ while (!tag_line.empty() && (tag_line.back() == '\n' || tag_line.back() == '\r'))
+ tag_line.remove_suffix(1);
+ // strip leading ';'
+ tag_line.remove_prefix(1);
+ if (tag_line == reserved_tag(ETags::Layer_Change))
+ ++current_layer_id;
+ }
// replace placeholder lines
bool processed = process_placeholders(gcode_line);
if (processed)
@@ -3796,9 +3821,10 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes
return;
EMoveType type = move_type(delta_pos);
+ const float delta_xyz = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z]));
+ m_travel_dist = delta_xyz;
+
if (type == EMoveType::Extrude) {
- const float delta_xyz = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z]));
- m_travel_dist = delta_xyz;
float volume_extruded_filament = area_filament_cross_section * delta_pos[E];
float area_toolpath_cross_section = volume_extruded_filament / delta_xyz;
@@ -5420,6 +5446,11 @@ void GCodeProcessor::process_filament_change(int id)
int next_extruder_id = m_filament_maps[id];
int next_filament_id = id;
float extra_time = 0;
+ unsigned int filament_changes_delta = 0;
+ unsigned int extruder_changes_delta = 0;
+ float filament_load_time_delta = 0.0f;
+ float filament_unload_time_delta = 0.0f;
+ float tool_change_time_delta = 0.0f;
if (prev_filament_id == next_filament_id)
return;
@@ -5432,12 +5463,14 @@ void GCodeProcessor::process_filament_change(int id)
assert(prev_extruder_id != -1);
process_filaments(CustomGCode::ToolChange);
m_filament_id[next_extruder_id] = next_filament_id;
- m_result.lock();
- m_result.print_statistics.total_filament_changes += 1;
- m_result.unlock();
- extra_time += get_filament_unload_time(static_cast(prev_filament_id));
+ filament_changes_delta += 1;
+ const float filament_unload_time = get_filament_unload_time(static_cast(prev_filament_id));
+ extra_time += filament_unload_time;
+ filament_unload_time_delta += filament_unload_time;
m_time_processor.extruder_unloaded = false;
- extra_time += get_filament_load_time(static_cast(next_filament_id));
+ const float filament_load_time = get_filament_load_time(static_cast(next_filament_id));
+ extra_time += filament_load_time;
+ filament_load_time_delta += filament_load_time;
}
else {
if (prev_extruder_id == -1) {
@@ -5445,7 +5478,9 @@ void GCodeProcessor::process_filament_change(int id)
m_extruder_id = next_extruder_id;
m_filament_id[next_extruder_id] = next_filament_id;
m_time_processor.extruder_unloaded = false;
- extra_time += get_filament_load_time(static_cast(next_filament_id));
+ const float filament_load_time = get_filament_load_time(static_cast(next_filament_id));
+ extra_time += filament_load_time;
+ filament_load_time_delta += filament_load_time;
}
else {
//first process cache generated by last extruder
@@ -5456,24 +5491,39 @@ void GCodeProcessor::process_filament_change(int id)
//no filament in current extruder
m_filament_id[next_extruder_id] = next_filament_id;
m_time_processor.extruder_unloaded = false;
- extra_time += get_filament_load_time(static_cast(next_filament_id));
+ const float filament_load_time = get_filament_load_time(static_cast(next_filament_id));
+ extra_time += filament_load_time;
+ filament_load_time_delta += filament_load_time;
}
else if (m_last_filament_id[next_extruder_id] != next_filament_id) {
//need to change filament
m_filament_id[next_extruder_id] = next_filament_id;
- m_result.lock();
- m_result.print_statistics.total_filament_changes += 1;
- m_result.unlock();
- extra_time += get_filament_unload_time(static_cast(prev_filament_id));
+ filament_changes_delta += 1;
+ const float filament_unload_time = get_filament_unload_time(static_cast(prev_filament_id));
+ extra_time += filament_unload_time;
+ filament_unload_time_delta += filament_unload_time;
m_time_processor.extruder_unloaded = false;
- extra_time += get_filament_load_time(static_cast(next_filament_id));
+ const float filament_load_time = get_filament_load_time(static_cast(next_filament_id));
+ extra_time += filament_load_time;
+ filament_load_time_delta += filament_load_time;
}
- m_result.lock();
- m_result.print_statistics.total_extruder_changes++;
- m_result.unlock();
- extra_time += get_extruder_change_time(next_extruder_id);
+ extruder_changes_delta += 1;
+ const float tool_change_time = get_extruder_change_time(next_extruder_id);
+ extra_time += tool_change_time;
+ tool_change_time_delta += tool_change_time;
}
}
+
+ if (filament_changes_delta > 0 || extruder_changes_delta > 0 || filament_load_time_delta > 0.0f || filament_unload_time_delta > 0.0f || tool_change_time_delta > 0.0f) {
+ m_result.lock();
+ m_result.print_statistics.total_filament_changes += filament_changes_delta;
+ m_result.print_statistics.total_extruder_changes += extruder_changes_delta;
+ m_result.print_statistics.total_filament_load_time += filament_load_time_delta;
+ m_result.print_statistics.total_filament_unload_time += filament_unload_time_delta;
+ m_result.print_statistics.total_tool_change_time += tool_change_time_delta;
+ m_result.unlock();
+ }
+
m_cp_color.current = m_extruder_colors[next_filament_id];
simulate_st_synchronize(extra_time);
// store tool change move
@@ -5518,6 +5568,11 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
m_line_id + 1 :
((type == EMoveType::Seam) ? m_last_line_id : m_line_id);
+ if (type == EMoveType::Travel) {
+ m_result.print_statistics.total_travel_moves++;
+ m_result.print_statistics.total_travel_distance += m_travel_dist;
+ }
+
m_result.moves.push_back({
m_last_line_id,
type,
diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp
index 867c8561b1..546df6fbb2 100644
--- a/src/libslic3r/GCode/GCodeProcessor.hpp
+++ b/src/libslic3r/GCode/GCodeProcessor.hpp
@@ -78,6 +78,13 @@ class Print;
std::array(ETimeMode::Count)> modes;
unsigned int total_filament_changes;
unsigned int total_extruder_changes;
+ float total_filament_load_time;
+ float total_filament_unload_time;
+ float total_tool_change_time;
+ float total_travel_distance;
+ unsigned int total_travel_moves;
+ float total_seam_gap_distance;
+ float total_seam_scarf_distance;
PrintEstimatedStatistics() { reset(); }
@@ -95,6 +102,13 @@ class Print;
used_filaments_per_role.clear();
total_filament_changes = 0;
total_extruder_changes = 0;
+ total_filament_load_time = 0.0f;
+ total_filament_unload_time = 0.0f;
+ total_tool_change_time = 0.0f;
+ total_travel_distance = 0.0f;
+ total_travel_moves = 0;
+ total_seam_gap_distance = 0.0f;
+ total_seam_scarf_distance = 0.0f;
}
};
diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp
index 238de9a5cf..307e099f2c 100644
--- a/src/libslic3r/GCodeWriter.cpp
+++ b/src/libslic3r/GCodeWriter.cpp
@@ -881,10 +881,10 @@ std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, c
// Determine number of segments based on Resolution
// --------------------------------------------------------------------
const double ref_resolution = 0.01; // reference resolution in mm
- const double ref_segments = 16.0; // reference number of segments at reference resolution
+ const double ref_segments = 8.0; // reference number of segments at reference resolution
- // number of linear segments to use for approximating the arc, clamp between 4 and 24
- const int segments = std::clamp(int(std::round(ref_segments * (ref_resolution / m_resolution))), 4, 24);
+ // number of linear segments to use for approximating the arc, clamp between 4 and 16
+ const int segments = std::clamp(int(std::round(ref_segments * (ref_resolution / m_resolution))), 4, 16);
// --------------------------------------------------------------------
const double px = m_pos(0) - m_x_offset; // take plate offset into consideration
@@ -1109,9 +1109,13 @@ std::string GCodeWriter::unlift()
return gcode;
}
-std::string GCodeWriter::set_fan(const GCodeFlavor gcode_flavor, unsigned int speed)
+std::string GCodeWriter::set_fan(const GCodeFlavor gcode_flavor, unsigned int speed, unsigned int part_cooling_fan_min_pwm)
{
std::ostringstream gcode;
+ // ORCA: clamp non-zero fan commands up to the configured PWM floor so fans that can't spool at low duty
+ // cycles still start reliably. Zero (fan off) is preserved exactly so disable-fan commands are never altered.
+ if (speed > 0 && part_cooling_fan_min_pwm > 0 && speed < part_cooling_fan_min_pwm)
+ speed = part_cooling_fan_min_pwm;
if (speed == 0) {
switch (gcode_flavor) {
case gcfTeacup:
@@ -1146,7 +1150,9 @@ std::string GCodeWriter::set_fan(const GCodeFlavor gcode_flavor, unsigned int sp
std::string GCodeWriter::set_fan(unsigned int speed) const
{
//BBS
- return GCodeWriter::set_fan(this->config.gcode_flavor, speed);
+ // ORCA: pick up the per-printer PWM floor from the active config.
+ return GCodeWriter::set_fan(this->config.gcode_flavor, speed,
+ static_cast(std::max(0, this->config.part_cooling_fan_min_pwm.value)));
}
//BBS: set additional fan speed for BBS machine only
diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp
index 1c8da2c42f..7f21eb9d79 100644
--- a/src/libslic3r/GCodeWriter.hpp
+++ b/src/libslic3r/GCodeWriter.hpp
@@ -98,7 +98,9 @@ public:
void set_xy_offset(double x, double y) { m_x_offset = x; m_y_offset = y; }
Vec2f get_xy_offset() { return Vec2f{m_x_offset, m_y_offset}; };
// To be called by the CoolingBuffer from another thread.
- static std::string set_fan(const GCodeFlavor gcode_flavor, unsigned int speed);
+ // ORCA: `part_cooling_fan_min_pwm` (0-100, default 0) is a floor applied only when `speed` is non-zero, used to overcome
+ // PWM start-up thresholds on fans that won't spool below a certain duty cycle. A `speed` of 0 is always honoured.
+ static std::string set_fan(const GCodeFlavor gcode_flavor, unsigned int speed, unsigned int part_cooling_fan_min_pwm = 0);
// To be called by the main thread. It always emits the G-code, it does not remember the previous state.
// Keeping the state is left to the CoolingBuffer, which runs asynchronously on another thread.
std::string set_fan(unsigned int speed) const;
diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp
index 0ae7524fa0..e610a9ed4f 100644
--- a/src/libslic3r/Geometry.hpp
+++ b/src/libslic3r/Geometry.hpp
@@ -298,7 +298,11 @@ bool directions_perpendicular(double angle1, double angle2, double max_diff = 0)
template bool contains(const std::vector &vector, const Point &point);
template T rad2deg(T angle) { return T(180.0) * angle / T(PI); }
double rad2deg_dir(double angle);
-template constexpr T deg2rad(const T angle) { return T(PI) * angle / T(180.0); }
+template constexpr T deg2rad(const T angle)
+{
+ static_assert(std::is_floating_point::value, "Why do you want to calculate angle in integer?");
+ return T(PI) * angle / T(180.0);
+}
template T angle_to_0_2PI(T angle)
{
static const T TWO_PI = T(2) * T(PI);
diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp
index 67959a508a..0a80c019ae 100644
--- a/src/libslic3r/Layer.cpp
+++ b/src/libslic3r/Layer.cpp
@@ -159,6 +159,12 @@ bool Layer::is_perimeter_compatible(const PrintRegion& a, const PrintRegion& b)
&& config.detect_thin_wall == other_config.detect_thin_wall
&& config.infill_wall_overlap == other_config.infill_wall_overlap
&& config.top_bottom_infill_wall_overlap == other_config.top_bottom_infill_wall_overlap
+ // Orca: these flags directly change the effective wall count produced by the perimeter
+ // generator. If two regions disagree on any of them, merging their slices into one shared make_perimeters
+ // call would silently use the first region's flag for both.
+ && config.only_one_wall_first_layer == other_config.only_one_wall_first_layer
+ && config.only_one_wall_top == other_config.only_one_wall_top
+ && config.min_width_top_surface == other_config.min_width_top_surface
&& config.seam_slope_type == other_config.seam_slope_type
&& config.seam_slope_conditional == other_config.seam_slope_conditional
&& config.scarf_angle_threshold == other_config.scarf_angle_threshold
diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp
index e3b2ada837..0067684f15 100644
--- a/src/libslic3r/Model.cpp
+++ b/src/libslic3r/Model.cpp
@@ -1980,6 +1980,49 @@ void ModelVolume::reset_extra_facets()
this->fuzzy_skin_facets.reset();
}
+std::optional ModelVolume::save_painting() const
+{
+ if (is_any_painted() && is_model_part() && !mesh().empty()) {
+ TriangleSelector::SavedPainting sp;
+ sp.mesh = mesh();
+ sp.supported = supported_facets.get_data();
+ sp.seam = seam_facets.get_data();
+ sp.mmu = mmu_segmentation_facets.get_data();
+ sp.fuzzy = fuzzy_skin_facets.get_data();
+ return sp;
+ }
+
+ return {};
+}
+
+void ModelVolume::restore_painting(const std::optional& saved, const bool keep_existing_paint)
+{
+ if (!keep_existing_paint) {
+ reset_extra_facets();
+ }
+
+ if (!saved) {
+ return;
+ }
+
+ auto remap_one = [&](const TriangleSelector::TriangleSplittingData& src_data,
+ FacetsAnnotation& target_facets) {
+ if (src_data.bitstream.empty())
+ return;
+ auto result =
+ TriangleSelector::remap_painting(saved->mesh.its, src_data, mesh().its, Geometry::translation_transform(mesh().get_init_shift()),
+ keep_existing_paint ?
+ std::optional>{std::ref(target_facets.get_data())} :
+ std::optional>{});
+ if (!result.bitstream.empty())
+ target_facets.set_data(std::move(result));
+ };
+ remap_one(saved->supported, supported_facets);
+ remap_one(saved->seam, seam_facets);
+ remap_one(saved->mmu, mmu_segmentation_facets);
+ remap_one(saved->fuzzy, fuzzy_skin_facets);
+}
+
static void invalidate_translations(ModelObject* object, const ModelInstance* src_instance)
{
if (!object->origin_translation.isApprox(Vec3d::Zero()) && src_instance->get_offset().isApprox(Vec3d::Zero())) {
@@ -1993,13 +2036,14 @@ static void invalidate_translations(ModelObject* object, const ModelInstance* sr
}
}
-void ModelObject::split(ModelObjectPtrs* new_objects)
+void ModelObject::split(ModelObjectPtrs* new_objects, const bool remap_paint)
{
std::vector all_meshes;
std::vector all_transfos;
std::vector> volume_mesh_counts;
all_meshes.reserve(this->volumes.size() * 5);
bool is_multi_volume_object = (this->volumes.size() > 1);
+ std::optional saved_painting;
for (int volume_idx = 0; volume_idx < this->volumes.size(); volume_idx++) {
ModelVolume* volume = this->volumes[volume_idx];
@@ -2011,6 +2055,10 @@ void ModelObject::split(ModelObjectPtrs* new_objects)
volume->text_configuration.reset();
if (!is_multi_volume_object) {
+ if (remap_paint) {
+ // Save painting so we could restore them after the mesh split
+ saved_painting = volume->save_painting();
+ }
//BBS: not multi volume object, then split mesh.
std::vector volume_meshes = volume->mesh().split();
int mesh_count = 0;
@@ -2078,10 +2126,19 @@ void ModelObject::split(ModelObjectPtrs* new_objects)
ModelVolume* new_vol = new_object->add_volume(*volume, std::move(mesh));
if (is_multi_volume_object) {
- // BBS: volume geometry not changed, so we can keep the color paint facets
- if (new_vol->mmu_segmentation_facets.timestamp() == volume->mmu_segmentation_facets.timestamp())
- new_vol->mmu_segmentation_facets.reset(); // BBS: let next assign take effect
- new_vol->mmu_segmentation_facets.assign(volume->mmu_segmentation_facets);
+ // BBS: volume geometry not changed, so we can keep the paint facets
+#define COPY_FACETS(f) \
+ if (new_vol->f.timestamp() == volume->f.timestamp()) \
+ new_vol->f.reset(); /* BBS: let next assign take effect */ \
+ new_vol->f.assign(volume->f)
+
+ COPY_FACETS(supported_facets);
+ COPY_FACETS(seam_facets);
+ COPY_FACETS(mmu_segmentation_facets);
+ COPY_FACETS(fuzzy_skin_facets);
+ } else if (saved_painting) {
+ // Geometry changed, attempt to remap them to the new mesh
+ new_vol->restore_painting(saved_painting);
}
// BBS: clear volume's config, as we already set them into object
@@ -2682,7 +2739,7 @@ std::string ModelVolume::type_to_string(const ModelVolumeType t)
// Split this volume, append the result to the object owning this volume.
// Return the number of volumes created from this one.
// This is useful to assign different materials to different volumes of an object.
-size_t ModelVolume::split(unsigned int max_extruders)
+size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
{
std::vector meshes = this->mesh().split();
if (meshes.size() <= 1)
@@ -2692,6 +2749,9 @@ size_t ModelVolume::split(unsigned int max_extruders)
if (text_configuration.has_value())
text_configuration.reset();
+ std::optional saved_painting = remap_paint ? save_painting() :
+ std::optional{};
+
size_t idx = 0;
size_t ivolume = std::find(this->object->volumes.begin(), this->object->volumes.end(), this) - this->object->volumes.begin();
const std::string name = this->name;
@@ -2714,11 +2774,7 @@ size_t ModelVolume::split(unsigned int max_extruders)
this->source = ModelVolume::Source();
// BBS: reset facet annotations
- this->mmu_segmentation_facets.reset();
- this->exterior_facets.reset();
- this->supported_facets.reset();
- this->seam_facets.reset();
- this->fuzzy_skin_facets.reset();
+ this->reset_extra_facets();
}
else
this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), new ModelVolume(object, *this, std::move(mesh)));
@@ -2731,6 +2787,8 @@ size_t ModelVolume::split(unsigned int max_extruders)
this->object->volumes[ivolume]->config.set("extruder", this->extruder_id());
//this->object->volumes[ivolume]->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter));
this->object->volumes[ivolume]->m_is_splittable = 0;
+ this->object->volumes[ivolume]->restore_painting(saved_painting);
+
++ idx;
}
diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp
index 5974ca4cd6..d8697adb41 100644
--- a/src/libslic3r/Model.hpp
+++ b/src/libslic3r/Model.hpp
@@ -516,7 +516,7 @@ public:
void delete_connectors();
void clone_for_cut(ModelObject **obj);
- void split(ModelObjectPtrs*new_objects);
+ void split(ModelObjectPtrs*new_objects, bool remap_paint);
void merge();
// BBS: Boolean opts - Musang King
@@ -731,6 +731,7 @@ public:
void assign(const FacetsAnnotation &rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } }
void assign(FacetsAnnotation &&rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } }
const TriangleSelector::TriangleSplittingData &get_data() const noexcept { return m_data; }
+ void set_data(TriangleSelector::TriangleSplittingData &&data) { m_data = std::move(data); this->touch(); }
bool set(const TriangleSelector& selector);
indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
// BBS
@@ -877,13 +878,18 @@ public:
// List of mesh facets painted for fuzzy skin.
FacetsAnnotation fuzzy_skin_facets;
+ // Save painting data before reset_extra_facets() discards it.
+ // Used for replacing mesh without losing painting data.
+ // Only for model parts (not modifiers/connectors).
+ std::optional save_painting() const;
+
+ // Remap painting data from previous saved source to this mesh
+ void restore_painting(const std::optional& saved, bool keep_existing_paint = false);
+
// BBS: quick access for volume extruders, 1 based
mutable std::vector mmuseg_extruders;
mutable Timestamp mmuseg_ts;
- // List of exterior faces
- FacetsAnnotation exterior_facets;
-
// Is set only when volume is Embossed Text type
// Contain information how to re-create volume
std::optional text_configuration;
@@ -924,7 +930,7 @@ public:
// Split this volume, append the result to the object owning this volume.
// Return the number of volumes created from this one.
// This is useful to assign different materials to different volumes of an object.
- size_t split(unsigned int max_extruders);
+ size_t split(unsigned int max_extruders, bool remap_paint);
void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); }
void translate(const Vec3d& displacement);
void scale(const Vec3d& scaling_factors);
@@ -1007,6 +1013,7 @@ public:
bool is_seam_painted() const { return !this->seam_facets.empty(); }
bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); }
bool is_fuzzy_skin_painted() const { return !this->fuzzy_skin_facets.empty(); }
+ bool is_any_painted() const { return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted(); }
// Orca: Implement prusa's filament shrink compensation approach
// Returns 0-based indices of extruders painted by multi-material painting gizmo.
diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp
index 7b238ea893..f6e6ee14dd 100644
--- a/src/libslic3r/Preset.cpp
+++ b/src/libslic3r/Preset.cpp
@@ -581,14 +581,6 @@ void Preset::load_info(const std::string& file)
catch (...) {
return;
}
-
- //TODO: workaround for current info file convert, will remove it later
- if (this->updated_time == 0) {
- this->updated_time = (long long)Slic3r::Utils::get_current_time_utc();
- //this->sync_info = "update";
- BOOST_LOG_TRIVIAL(info) << boost::format("old info file, updated time to %1%") % this->updated_time;
- save_info();
- }
}
void Preset::save_info(std::string file)
@@ -1014,6 +1006,7 @@ static std::vector s_Preset_print_options{
"is_infill_first",
"sparse_infill_density",
"fill_multiline",
+ "gyroid_optimized",
"sparse_infill_pattern",
"lateral_lattice_angle_1",
"lateral_lattice_angle_2",
@@ -1335,7 +1328,7 @@ static std::vector s_Preset_machine_limits_options {
static std::vector s_Preset_printer_options {
"printer_technology",
"printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
- "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs",
+ "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod",
@@ -1352,7 +1345,7 @@ static std::vector s_Preset_printer_options {
"use_relative_e_distances", "extruder_type", "use_firmware_retraction", "printer_notes",
"grab_length", "support_object_skip_flush", "physical_extruder_map",
"cooling_tube_retraction",
- "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming",
+ "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower",
"z_offset",
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
"bed_temperature_formula", "nozzle_flush_dataset",
@@ -2205,19 +2198,29 @@ bool PresetCollection::load_user_preset(std::string name, std::mapsecond;
+ } else {
+ const auto inherits_iter = preset_values.find(BBL_JSON_KEY_INHERITS);
+ const bool preset_inherits_from_parent = inherits_iter != preset_values.end() && !inherits_iter->second.empty();
+ if (preset_inherits_from_parent) {
+ // This indicates that there is inherits exists but there is no base_id
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__
+ << boost::format("can not find base_id, not loading for user preset %1%") % canonical_name;
+ unlock();
+ return false;
+ }
}
- std::string cloud_base_id = preset_values[BBL_JSON_KEY_BASE_ID];
//filament_id
std::string cloud_filament_id;
if ((m_type == Preset::TYPE_FILAMENT) && preset_values.find(BBL_JSON_KEY_FILAMENT_ID) != preset_values.end()) {
cloud_filament_id = preset_values[BBL_JSON_KEY_FILAMENT_ID];
- BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << canonical_name << " filament_id: " << cloud_filament_id << " base_id: " << cloud_base_id;
+ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << canonical_name << " filament_id: " << cloud_filament_id << " base_id: " << based_id;
}
DynamicPrintConfig new_config, cloud_config;
@@ -2279,6 +2282,10 @@ bool PresetCollection::load_user_preset(std::string name, std::mapname == m_edited_preset.name && iter->is_dirty) {
// Keep modifies when update from remote
new_config.apply_only(m_edited_preset.config, m_edited_preset.config.diff(iter->config));
+ } else if (iter->name == m_edited_preset.name) {
+ // Preset is not dirty (no local unsaved changes) — also update the edited preset
+ // to prevent a false "dirty" indication (orange highlight) after a silent cloud sync
+ m_edited_preset.config = new_config;
}
iter->config = new_config;
iter->updated_time = cloud_update_time;
@@ -2286,7 +2293,7 @@ bool PresetCollection::load_user_preset(std::string name, std::mapversion = cloud_version.value();
iter->user_id = cloud_user_id;
iter->setting_id = cloud_setting_id;
- iter->base_id = cloud_base_id;
+ iter->base_id = based_id;
iter->filament_id = cloud_filament_id;
update_alias(*iter);
//presets_loaded.emplace_back(*it->second);
@@ -2305,7 +2312,7 @@ bool PresetCollection::load_user_preset(std::string name, std::map(BBL_JSON_KEY_INHERITS)->value.empty()) {
- if (alias_name.empty()) {
- size_t end_pos = preset_name.find_first_of("@");
- if (end_pos != std::string::npos) {
- alias_name = preset_name.substr(0, end_pos);
- boost::trim_right(alias_name);
- }
+ std::string bare_preset_name = get_preset_bare_name(preset.name);
+ std::string alias_name = bare_preset_name;
+
+ const bool is_root_filament_preset =
+ m_type == Preset::Type::TYPE_FILAMENT &&
+ preset.config.has(BBL_JSON_KEY_INHERITS) &&
+ preset.config.option(BBL_JSON_KEY_INHERITS)->value.empty();
+ if (is_root_filament_preset) {
+ const size_t suffix_separator_pos = bare_preset_name.find_first_of("@");
+ if (suffix_separator_pos != std::string::npos) {
+ alias_name = bare_preset_name.substr(0, suffix_separator_pos);
+ boost::trim_right(alias_name);
+ if (alias_name.empty())
+ alias_name = bare_preset_name;
}
}
- else {
- alias_name = preset_name;
- }
preset.alias = std::move(alias_name);
m_map_alias_to_profile_name[preset.alias].push_back(preset.name);
diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp
index 1bbf1747e6..3af0b3c1ba 100644
--- a/src/libslic3r/PresetBundle.cpp
+++ b/src/libslic3r/PresetBundle.cpp
@@ -614,13 +614,24 @@ VendorType PresetBundle::get_current_vendor_type()
{
auto t = VendorType::Unknown;
auto config = &printers.get_edited_preset().config;
+ const auto* printer_model = config->opt("printer_model");
+ if (printer_model == nullptr) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": printer_model is "
+ << (config->has("printer_model") ? "not a string" : "missing")
+ << ", vendor type is Unknown";
+ return t;
+ }
+
std::string vendor_name;
- for (auto vendor_profile : vendors) {
- for (auto vendor_model : vendor_profile.second.models)
- if (vendor_model.name == config->opt_string("printer_model")) {
+ for (const auto& vendor_profile : vendors) {
+ for (const auto& vendor_model : vendor_profile.second.models) {
+ if (vendor_model.name == printer_model->value) {
vendor_name = vendor_profile.first;
break;
}
+ }
+ if (!vendor_name.empty())
+ break;
}
if (!vendor_name.empty())
{
@@ -2622,6 +2633,9 @@ void PresetBundle::update_selections(AppConfig &config)
break;
this->filament_presets.emplace_back(remove_ini_suffix(f_name));
}
+
+ update_filament_count();
+
std::vector filament_colors;
auto f_colors = config.get_printer_setting(initial_printer_profile_name, "filament_colors");
if (!f_colors.empty()) {
@@ -2763,6 +2777,8 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
this->filament_presets.emplace_back(remove_ini_suffix(f_name));
}
+ update_filament_count();
+
// Load data from AppConfig to ProjectConfig when Studio is initialized.
std::vector filament_colors;
auto f_colors = config.get_printer_setting(initial_printer_profile_name, "filament_colors");
@@ -3782,11 +3798,33 @@ int PresetBundle::get_printer_extruder_count() const
{
const Preset& printer_preset = this->printers.get_edited_preset();
- int count = printer_preset.config.option("nozzle_diameter")->values.size();
+ const auto* nozzle_diameter = printer_preset.config.option("nozzle_diameter");
+ if (nozzle_diameter == nullptr) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": nozzle_diameter is missing, using 1 extruder";
+ return 1;
+ }
+ if (nozzle_diameter->values.empty()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": nozzle_diameter is empty, using 1 extruder";
+ return 1;
+ }
+
+ int count = int(nozzle_diameter->values.size());
return count;
}
+void PresetBundle::update_filament_count()
+{
+ if (printers.get_edited_preset().printer_technology() != ptFFF)
+ return;
+ const size_t num_extruders = static_cast(get_printer_extruder_count());
+ if (filament_presets.size() >= num_extruders)
+ return;
+ filament_presets.resize(num_extruders, filament_presets.empty()
+ ? filaments.first_visible().name
+ : filament_presets.back());
+}
+
bool PresetBundle::support_different_extruders()
{
Preset& printer_preset = this->printers.get_edited_preset();
diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp
index 2351084ade..220de95b78 100644
--- a/src/libslic3r/PresetBundle.hpp
+++ b/src/libslic3r/PresetBundle.hpp
@@ -372,6 +372,12 @@ public:
int get_printer_extruder_count() const;
bool support_different_extruders();
+ // Orca: Ensure filament_presets has at least one slot per nozzle on FFF printers.
+ // Called from (load|update)_selections before the parallel project_config arrays
+ // (filament_colour/colour_type/map) are sized off filament_presets.size(), so a
+ // short saved filament list doesn't truncate the loaded colors.
+ void update_filament_count();
+
// Load user configuration and store it into the user profiles.
// This method is called by the configuration wizard.
void load_config_from_wizard(const std::string &name, DynamicPrintConfig config, Semver file_version)
diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp
index 6b4ae47457..8cfd4ca7a2 100644
--- a/src/libslic3r/Print.cpp
+++ b/src/libslic3r/Print.cpp
@@ -134,6 +134,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"fan_cooling_layer_time",
"full_fan_speed_layer",
"fan_kickstart",
+ "part_cooling_fan_min_pwm",
"fan_speedup_overhangs",
"fan_speedup_time",
"filament_colour",
@@ -360,6 +361,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "wipe_tower_filament"
|| opt_key == "wiping_volumes_extruders"
|| opt_key == "enable_filament_ramming"
+ || opt_key == "tool_change_on_wipe_tower"
|| opt_key == "purge_in_prime_tower"
|| opt_key == "z_offset"
|| opt_key == "support_multi_bed_types"
@@ -1785,46 +1787,47 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
};
std::string warning_key;
+ const auto max_junction_deviation = m_config.machine_max_junction_deviation.values[0];
+ const bool ignore_jerk_validation = m_config.gcode_flavor == gcfMarlinFirmware && max_junction_deviation > 0;
+
// check jerk
- if (m_default_object_config.default_jerk == 1 || m_default_object_config.outer_wall_jerk == 1 ||
- m_default_object_config.inner_wall_jerk == 1) {
- warning->string = L("Setting the jerk speed too low could lead to artifacts on curved surfaces");
- if (m_default_object_config.outer_wall_jerk == 1)
- warning_key = "outer_wall_jerk";
- else if (m_default_object_config.inner_wall_jerk == 1)
- warning_key = "inner_wall_jerk";
- else
- warning_key = "default_jerk";
+ if (!ignore_jerk_validation) {
+ if (m_default_object_config.default_jerk == 1 || m_default_object_config.outer_wall_jerk == 1 ||
+ m_default_object_config.inner_wall_jerk == 1) {
+ warning->string = L("Setting the jerk speed too low could lead to artifacts on curved surfaces");
+ if (m_default_object_config.outer_wall_jerk == 1)
+ warning_key = "outer_wall_jerk";
+ else if (m_default_object_config.inner_wall_jerk == 1)
+ warning_key = "inner_wall_jerk";
+ else
+ warning_key = "default_jerk";
- warning->opt_key = warning_key;
- }
+ warning->opt_key = warning_key;
+ }
- if (warning_key.empty() && m_default_object_config.default_jerk > 0) {
- std::vector jerk_to_check = {"default_jerk", "outer_wall_jerk", "inner_wall_jerk", "infill_jerk",
- "top_surface_jerk", "initial_layer_jerk", "travel_jerk"};
- const auto max_jerk = std::min(m_config.machine_max_jerk_x.values[0], m_config.machine_max_jerk_y.values[0]);
- warning_key.clear();
- if (m_default_object_config.default_jerk > 0)
- warning_key = check_motion_ability_object_setting(jerk_to_check, max_jerk);
- if (!warning_key.empty()) {
- warning->string = L(
- "The jerk setting exceeds the printer's maximum jerk (machine_max_jerk_x/machine_max_jerk_y).\nOrca will "
- "automatically cap the jerk speed to ensure it doesn't surpass the printer's capabilities.\nYou can adjust the "
- "maximum jerk setting in your printer's configuration to get higher speeds.");
- warning->opt_key = warning_key;
- }
+ if (warning_key.empty() && m_default_object_config.default_jerk > 0) {
+ std::vector jerk_to_check = {"default_jerk", "outer_wall_jerk", "inner_wall_jerk", "infill_jerk",
+ "top_surface_jerk", "initial_layer_jerk", "travel_jerk"};
+ const auto max_jerk = std::min(m_config.machine_max_jerk_x.values[0], m_config.machine_max_jerk_y.values[0]);
+ warning_key.clear();
+ warning_key = check_motion_ability_object_setting(jerk_to_check, max_jerk);
+ if (!warning_key.empty()) {
+ warning->string = L(
+ "The jerk setting exceeds the printer's maximum jerk (machine_max_jerk_x/machine_max_jerk_y).\n"
+ "Orca will automatically cap the jerk speed to ensure it doesn't surpass the printer's capabilities.\n"
+ "You can adjust the maximum jerk setting in your printer's configuration to get higher speeds.");
+ warning->opt_key = warning_key;
+ }
+ }
}
// Check junction deviation
- const auto max_junction_deviation = m_config.machine_max_junction_deviation.values[0];
// Orca: Only marlin FW supports max junction deviation. Dont display warning if firmware is not supporting it.
const bool support_max_junction_deviation = ( m_config.gcode_flavor == gcfMarlinFirmware);
if (warning_key.empty() && m_default_object_config.default_junction_deviation.value > max_junction_deviation && support_max_junction_deviation) {
- warning->string = L( "Junction deviation setting exceeds the printer's maximum value "
- "(machine_max_junction_deviation).\nOrca will "
- "automatically cap the junction deviation to ensure it doesn't surpass the printer's "
- "capabilities.\nYou can adjust the "
- "machine_max_junction_deviation value in your printer's configuration to get higher limits.");
+ warning->string = L( "Junction deviation setting exceeds the printer's maximum value (machine_max_junction_deviation).\n"
+ "Orca will automatically cap the junction deviation to ensure it doesn't surpass the printer's capabilities.\n"
+ "You can adjust the machine_max_junction_deviation value in your printer's configuration to get higher limits.");
warning->opt_key = warning_key;
}
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index 5f2e58c9f7..6a1586f025 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -2931,6 +2931,19 @@ void PrintConfigDef::init_fff_params()
def->max = 10; // Maximum number of lines for infill pattern
def->set_default_value(new ConfigOptionInt(1));
+ // Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z
+ // (vertical) axis at low infill density to shorten the effective column length under
+ // Z-axis compression. Filament use at the same `sparse_infill_density` setting is
+ // preserved. No effect above ~30% density (formula clamps to no-op).
+ def = this->add("gyroid_optimized", coBool);
+ def->label = L("Z-buckling bias optimization (experimental)");
+ def->category = L("Strength");
+ def->tooltip = L("Tightens the gyroid wave along the Z (vertical) axis at low infill density "
+ "to shorten the effective vertical column length and improve Z-axis compression "
+ "buckling resistance. Filament use is preserved. No effect at ~30% sparse infill "
+ "density and above. Only applies when Sparse infill pattern is set to Gyroid.");
+ def->set_default_value(new ConfigOptionBool(false));
+
def = this->add("sparse_infill_pattern", coEnum);
def->label = L("Sparse infill pattern");
def->category = L("Strength");
@@ -3743,6 +3756,28 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0));
+ // ORCA: minimum non-zero part cooling fan speed.
+ def = this->add("part_cooling_fan_min_pwm", coInt);
+ def->label = L("Minimum non-zero part cooling fan speed");
+ def->tooltip = L("Some part-cooling fans cannot start spinning when commanded below a certain PWM duty cycle. "
+ "When set above 0, any non-zero part-cooling fan command will be raised to at least this percentage "
+ "so the fan reliably starts. A fan command of 0 (fan off) is always honoured exactly. "
+ "This clamp is applied after every other fan calculation (first-layer ramp, layer-time interpolation, "
+ "overhang/bridge/support-interface/ironing overrides), so scaling still operates within the range "
+ "[this value, 100%]."
+ "\nIf your firmware already disables the fan below a threshold (for example Klipper's "
+ "[fan] off_below: 0.10 shuts the fan off whenever the commanded duty cycle is below 10%), "
+ "this option and the firmware threshold should ideally be set to the same value. Matching them "
+ "(e.g. off_below: 0.10 in Klipper and 10% here) guarantees the slicer never emits a non-zero "
+ "value that the firmware would silently drop, and the fan never receives a value below the one "
+ "you know it can actually spool at."
+ "\nSet to 0 to deactivate.");
+ def->sidetext = L("%");
+ def->min = 0;
+ def->max = 100;
+ def->mode = comAdvanced;
+ def->set_default_value(new ConfigOptionInt(0));
+
def = this->add("time_cost", coFloat);
def->label = L("Time cost");
@@ -4256,7 +4291,7 @@ void PrintConfigDef::init_fff_params()
def->min = 0;
def->max = 90;
def->mode = comExpert;
- def->set_default_value(new ConfigOptionFloat(0));
+ def->set_default_value(new ConfigOptionFloat(35));
def = this->add("zaa_dont_alternate_fill_direction", coBool);
def->label = L("Don't alternate fill direction");
@@ -5953,6 +5988,16 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
+ def = this->add("tool_change_on_wipe_tower", coBool);
+ def->label = L("Tool change on wipe tower");
+ def->tooltip = L("Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). "
+ "Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. "
+ "By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, "
+ "which can result in the Tx command being issued above the printed part. "
+ "Enable this option if you want the tool change to always be issued above the wipe tower instead.");
+ def->mode = comAdvanced;
+ def->set_default_value(new ConfigOptionBool(false));
+
def = this->add("wipe_tower_no_sparse_layers", coBool);
def->label = L("No sparse layers (beta)");
@@ -6872,7 +6917,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.emplace_back(L("Cone"));
def->enum_labels.emplace_back(L("Rib"));
def->mode = comAdvanced;
- def->set_default_value(new ConfigOptionEnum(wtwRectangle));
+ def->set_default_value(new ConfigOptionEnum(wtwRib));
def = this->add("wipe_tower_extra_rib_length", coFloat);
def->label = L("Extra rib length");
@@ -9736,11 +9781,19 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
case coStrings:
{
ConfigOptionStrings * opt = this->option(key);
+ if (!opt) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
+ break;
+ }
std::vector new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
+ if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
+ continue;
+ }
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
@@ -9749,11 +9802,19 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
case coInts:
{
ConfigOptionInts * opt = this->option(key);
+ if (!opt) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
+ break;
+ }
std::vector new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
+ if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
+ continue;
+ }
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
@@ -9762,11 +9823,19 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
case coFloats:
{
ConfigOptionFloats * opt = this->option(key);
+ if (!opt) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
+ break;
+ }
std::vector new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
+ if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
+ continue;
+ }
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
@@ -9775,11 +9844,19 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
case coPercents:
{
ConfigOptionPercents * opt = this->option(key);
+ if (!opt) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
+ break;
+ }
std::vector new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
+ if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
+ continue;
+ }
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
@@ -9788,11 +9865,19 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
case coFloatsOrPercents:
{
ConfigOptionFloatsOrPercents * opt = this->option(key);
+ if (!opt) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
+ break;
+ }
std::vector new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
+ if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
+ continue;
+ }
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
@@ -9801,11 +9886,19 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
case coBools:
{
ConfigOptionBools * opt = this->option(key);
+ if (!opt) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
+ break;
+ }
std::vector new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
+ if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
+ continue;
+ }
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
@@ -9814,11 +9907,19 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
case coEnums:
{
ConfigOptionEnumsGeneric * opt = this->option(key);
+ if (!opt) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
+ break;
+ }
std::vector new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
+ if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
+ continue;
+ }
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp
index b21897b6f6..56cd47f31d 100644
--- a/src/libslic3r/PrintConfig.hpp
+++ b/src/libslic3r/PrintConfig.hpp
@@ -1160,6 +1160,7 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca:
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
((ConfigOptionInt, fill_multiline))
+ ((ConfigOptionBool, gyroid_optimized))
// Ironing options
((ConfigOptionEnum, ironing_type))
((ConfigOptionEnum, ironing_pattern))
@@ -1336,6 +1337,10 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloat, fan_kickstart))
((ConfigOptionBool, fan_speedup_overhangs))
((ConfigOptionFloat, fan_speedup_time))
+ // ORCA: minimum PWM (as a percent 0-100) emitted when the part-cooling fan is asked for a non-zero speed.
+ // Used to overcome the PWM start-up threshold on fans that cannot spool below a certain duty cycle.
+ // A value of 0 (the default) leaves behaviour unchanged. A fan command of 0 (off) is always honoured.
+ ((ConfigOptionInt, part_cooling_fan_min_pwm))
((ConfigOptionFloats, filament_diameter))
((ConfigOptionBoolsNullable, filament_adaptive_volumetric_speed))
((ConfigOptionStrings, volumetric_speed_coefficients))
@@ -1489,6 +1494,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionEnum, wipe_tower_type))
((ConfigOptionBool, purge_in_prime_tower))
((ConfigOptionBool, enable_filament_ramming))
+ ((ConfigOptionBool, tool_change_on_wipe_tower))
((ConfigOptionBool, support_multi_bed_types))
// Small Area Infill Flow Compensation
diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp
index 1e59b44e30..ee37acb91e 100644
--- a/src/libslic3r/PrintObjectSlice.cpp
+++ b/src/libslic3r/PrintObjectSlice.cpp
@@ -21,6 +21,32 @@ namespace Slic3r {
bool PrintObject::clip_multipart_objects = true;
bool PrintObject::infill_only_where_needed = false;
+static coordf_t compute_slice_z(PrintObject* print_object, size_t i_layer, coordf_t lo, coordf_t hi)
+{
+ bool zaa_active = false;
+ coordf_t z_offset = 0.0;
+
+ size_t num_regions = print_object->num_printing_regions();
+ for (size_t rid = 0; rid < num_regions; ++rid) {
+ const auto& rcfg = print_object->printing_region(rid).config();
+ if (rcfg.zaa_enabled) {
+ if (!zaa_active || rcfg.zaa_min_z < z_offset)
+ z_offset = rcfg.zaa_min_z;
+ zaa_active = true;
+ }
+ }
+
+ if (!zaa_active || i_layer == 0) {
+ return 0.5 * (lo + hi);
+ }
+
+ coordf_t slice_z = lo + z_offset;
+ if ((slice_z < lo && !is_approx(slice_z, lo)) || (slice_z > hi && !is_approx(slice_z, hi))) {
+ throw RuntimeError("Bad min Z value");
+ }
+ return slice_z;
+}
+
LayerPtrs new_layers(
PrintObject *print_object,
// Object layers (pairs of bottom/top Z coordinate), without the raft.
@@ -34,24 +60,8 @@ LayerPtrs new_layers(
for (size_t i_layer = 0; i_layer < object_layers.size(); i_layer += 2) {
coordf_t lo = object_layers[i_layer];
coordf_t hi = object_layers[i_layer + 1];
- coordf_t slice_z = 0.5 * (lo + hi);
- bool zaa_active = false;
- coordf_t z_offset = 0.0;
- size_t num_regions = print_object->num_printing_regions();
- for (size_t rid = 0; rid < num_regions; ++rid) {
- const auto &rcfg = print_object->printing_region(rid).config();
- if (rcfg.zaa_enabled) {
- if (!zaa_active || rcfg.zaa_min_z < z_offset)
- z_offset = rcfg.zaa_min_z;
- zaa_active = true;
- }
- }
- if (zaa_active) {
- slice_z = lo + z_offset;
- if ((slice_z < lo && !is_approx(slice_z, lo)) || (slice_z > hi && !is_approx(slice_z, hi))) {
- throw RuntimeError("Bad min Z value");
- }
- }
+ coordf_t slice_z = compute_slice_z(print_object, i_layer, lo, hi);
+
Layer *layer = new Layer(id ++, print_object, hi - lo, hi + zmin, slice_z);
out.emplace_back(layer);
if (prev != nullptr) {
diff --git a/src/libslic3r/Thread.hpp b/src/libslic3r/Thread.hpp
index f9dc07456c..c407159072 100644
--- a/src/libslic3r/Thread.hpp
+++ b/src/libslic3r/Thread.hpp
@@ -46,11 +46,23 @@ void name_tbb_thread_pool_threads_set_locale();
template
inline boost::thread create_thread(boost::thread::attributes &attrs, Fn &&fn)
{
- // Duplicating the stack allocation size of Thread Building Block worker
- // threads of the thread pool: allocate 4MB on a 64bit system, allocate 2MB
- // on a 32bit system by default.
-
- attrs.set_stack_size((sizeof(void*) == 4) ? (2048 * 1024) : (4096 * 1024));
+ // Stack size for our worker threads. Originally duplicated TBB's pool
+ // default (4 MB), but the Emboss text-cut path calls into CGAL's
+ // Polygon_mesh_processing::corefine, which falls back from filtered
+ // interval arithmetic to exact rational arithmetic (mpq_class) on
+ // near-degenerate input, and the constrained 2D triangulation walker
+ // (Triangulation_2::march_locate_2D) can recurse deeply enough to
+ // exceed 4 MB on real models -- producing a SIGBUS at the next thread's
+ // stack guard page on macOS / Linux.
+ //
+ // 16 MB chosen as 4x defensive headroom over the observed crash
+ // threshold (n=1 reproducer at exactly 4 MB on macOS arm64). All three
+ // platforms defer-commit reserved stack pages: macOS / Linux mmap the
+ // stack and only fault in pages on touch; Boost.Thread on Win32 passes
+ // STACK_SIZE_PARAM_IS_A_RESERVATION to _beginthreadex, so the value is
+ // a reserve, not the initial commit. Resident memory therefore stays
+ // proportional to actual stack depth on every target.
+ attrs.set_stack_size(16 * 1024 * 1024);
return boost::thread{attrs, std::forward(fn)};
}
diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp
index 150c456b8b..2c1c0da23f 100644
--- a/src/libslic3r/TriangleMeshSlicer.cpp
+++ b/src/libslic3r/TriangleMeshSlicer.cpp
@@ -2348,7 +2348,7 @@ void cut_mesh(const indexed_triangle_set& mesh, float z, indexed_triangle_set* u
IntersectionLines upper_lines, lower_lines;
std::vector upper_slice_vertices, lower_slice_vertices;
std::vector facets_edge_ids = its_face_edge_ids(mesh);
- std::map section_vertices_map;
+ std::map section_vertices_map; // vertices on the cut plane
for (int facet_idx = 0; facet_idx < int(mesh.indices.size()); ++ facet_idx) {
const stl_triangle_vertex_indices &facet = mesh.indices[facet_idx];
diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp
index a6d19f505c..3b032cc57e 100644
--- a/src/libslic3r/TriangleSelector.cpp
+++ b/src/libslic3r/TriangleSelector.cpp
@@ -1,9 +1,11 @@
#include "TriangleSelector.hpp"
#include "Model.hpp"
+#include "AABBTreeIndirect.hpp"
#include
#include
#include
+#include
#include
#ifndef NDEBUG
@@ -168,24 +170,69 @@ void TriangleSelector::Triangle::set_division(int sides_to_split, int special_si
this->special_side_idx = char(special_side_idx);
}
-inline bool is_point_inside_triangle(const Vec3f &pt, const Vec3f &p1, const Vec3f &p2, const Vec3f &p3)
+// Pre-computed barycentric resolver
+// Real-time collision detection, Ericson, Chapter 3.4
+// Cache inspired by Don Hatch at https://gamedev.stackexchange.com/questions/23743/whats-the-most-efficient-way-to-find-barycentric-coordinates/23745#comment390123_23745
+struct Barycentric
{
- // Real-time collision detection, Ericson, Chapter 3.4
- auto barycentric = [&pt, &p1, &p2, &p3]() -> Vec3f {
- std::array v = {p2 - p1, p3 - p1, pt - p1};
- float d00 = v[0].dot(v[0]);
- float d01 = v[0].dot(v[1]);
- float d11 = v[1].dot(v[1]);
- float d20 = v[2].dot(v[0]);
- float d21 = v[2].dot(v[1]);
- float denom = d00 * d11 - d01 * d01;
+public:
+ Barycentric(const Vec3f& a, const Vec3f& b, const Vec3f& c)
+ :a_(a)
+ {
+ // Pre-compute denominator
+ const Vec3f v0 = b - a;
+ const Vec3f v1 = c - a;
+ const float d00 = v0.dot(v0);
+ const float d01 = v0.dot(v1);
+ const float d11 = v1.dot(v1);
+ const float inv_denom_ = 1.0f / (d00 * d11 - d01 * d01);
+
+ x1_ = (d11 * v0 - d01 * v1) * inv_denom_;
+ x2_ = (d00 * v1 - d01 * v0) * inv_denom_;
+ }
- Vec3f barycentric_cords(1.f, (d11 * d20 - d01 * d21) / denom, (d00 * d21 - d01 * d20) / denom);
- barycentric_cords.x() = barycentric_cords.x() - barycentric_cords.y() - barycentric_cords.z();
- return barycentric_cords;
- };
+ Vec3f calc(const Vec3f& p) const
+ {
+ const Vec3f v2 = p - a_;
+ const float v = v2.dot(x1_);
+ const float w = v2.dot(x2_);
+ const float u = 1.f - v - w;
- Vec3f barycentric_cords = barycentric();
+ return {u, v, w};
+ }
+
+ bool is_point_inside_triangle(const Vec3f& p) const
+ {
+ const Vec3f barycentric_cords = calc(p);
+ return std::all_of(begin(barycentric_cords), end(barycentric_cords), [](float cord) { return 0.f <= cord && cord <= 1.0; });
+ }
+
+ static Vec3f calc(const Vec3f& pt, const Vec3f& p1, const Vec3f& p2, const Vec3f& p3)
+ {
+ const std::array vec = {p2 - p1, p3 - p1, pt - p1};
+ const float d00 = vec[0].dot(vec[0]);
+ const float d01 = vec[0].dot(vec[1]);
+ const float d11 = vec[1].dot(vec[1]);
+ const float d20 = vec[2].dot(vec[0]);
+ const float d21 = vec[2].dot(vec[1]);
+ const float denom = d00 * d11 - d01 * d01;
+
+ const float v = (d11 * d20 - d01 * d21) / denom;
+ const float w = (d00 * d21 - d01 * d20) / denom;
+ const float u = 1.f - v - w;
+
+ return {u, v, w};
+ }
+
+private:
+ Vec3f a_;
+ Vec3f x1_;
+ Vec3f x2_;
+};
+
+static bool is_point_inside_triangle(const Vec3f &pt, const Vec3f &p1, const Vec3f &p2, const Vec3f &p3)
+{
+ Vec3f barycentric_cords = Barycentric::calc(pt, p1, p2, p3);
return std::all_of(begin(barycentric_cords), end(barycentric_cords), [](float cord) { return 0.f <= cord && cord <= 1.0; });
}
@@ -233,7 +280,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c
return this->select_unsplit_triangle(hit, facet_idx, neighbors);
}
-void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg)
+void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const bool select_partially)
{
assert(facet_start < m_orig_size_indices);
@@ -296,7 +343,7 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&c
Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast());
float world_normal_z = (normal_matrix* facet_normal).normalized().z();
if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) {
- if (select_triangle(facet, new_state, triangle_splitting)) {
+ if (select_triangle(facet, new_state, triangle_splitting, select_partially)) {
// add neighboring facets to list to be processed later
for (int neighbor_idx : m_neighbors[facet])
if (neighbor_idx >= 0 && m_cursor->is_facet_visible(neighbor_idx, m_face_normals))
@@ -552,7 +599,7 @@ void TriangleSelector::bucket_fill_select_triangles(const Vec3f& hit, int facet_
// This is done by an actual recursive call. Returns false if the triangle is
// outside the cursor.
// Called by select_patch() and by itself.
-bool TriangleSelector::select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting)
+bool TriangleSelector::select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting, bool select_partially)
{
assert(facet_idx < int(m_triangles.size()));
@@ -562,7 +609,7 @@ bool TriangleSelector::select_triangle(int facet_idx, EnforcerBlockerType type,
Vec3i32 neighbors = m_neighbors[facet_idx];
assert(this->verify_triangle_neighbors(m_triangles[facet_idx], neighbors));
- if (! select_triangle_recursive(facet_idx, neighbors, type, triangle_splitting))
+ if (! select_triangle_recursive(facet_idx, neighbors, type, triangle_splitting, select_partially))
return false;
// In case that all children are leafs and have the same state now,
@@ -902,7 +949,7 @@ Vec3i32 TriangleSelector::child_neighbors_propagated(const Triangle &tr, const V
return out;
}
-bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &neighbors, EnforcerBlockerType type, bool triangle_splitting)
+bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &neighbors, EnforcerBlockerType type, bool triangle_splitting, bool select_partially)
{
assert(facet_idx < int(m_triangles.size()));
@@ -935,8 +982,10 @@ bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &n
if (triangle_splitting)
split_triangle(facet_idx, neighbors);
- else if (!m_triangles[facet_idx].is_split())
+ if ((!triangle_splitting || select_partially) && !m_triangles[facet_idx].is_split()) {
m_triangles[facet_idx].set_state(type);
+ return true;
+ }
tr = &m_triangles[facet_idx]; // might have been invalidated by split_triangle().
int num_of_children = tr->number_of_split_sides() + 1;
@@ -946,7 +995,7 @@ bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &n
assert(tr->children[i] < int(m_triangles.size()));
// Recursion, deep first search over the children of this triangle.
// All children of this triangle were created by splitting a single source triangle of the original mesh.
- select_triangle_recursive(tr->children[i], this->child_neighbors(*tr, neighbors, i), type, triangle_splitting);
+ select_triangle_recursive(tr->children[i], this->child_neighbors(*tr, neighbors, i), type, triangle_splitting, select_partially);
tr = &m_triangles[facet_idx]; // might have been invalidated
}
}
@@ -2208,4 +2257,271 @@ std::vector TriangleSelector::extract_used_facet_states(con
return out;
}
+static bool segments_intersect_proj(const Vec3f& p1, const Vec3f& p2, const Vec3f& p3, const Vec3f& p4, const std::pair& proj)
+{
+ auto cross2d = [](float ax, float ay, float bx, float by) -> float { return ax * by - ay * bx; };
+ const auto [u_axis, v_axis] = proj;
+ const auto u1 = p1(u_axis), v1 = p1(v_axis);
+ const auto u2 = p2(u_axis), v2 = p2(v_axis);
+ const auto u3 = p3(u_axis), v3 = p3(v_axis);
+ const auto u4 = p4(u_axis), v4 = p4(v_axis);
+ const float ru = u2 - u1, rv = v2 - v1;
+ const float su = u4 - u3, sv = v4 - v3;
+ const float denom = cross2d(ru, rv, su, sv);
+ if (std::abs(denom) < 1e-10f)
+ return false;
+ const float dpu = u3 - u1, dpv = v3 - v1;
+ const float t1 = cross2d(dpu, dpv, su, sv) / denom;
+ const float t2 = cross2d(dpu, dpv, ru, rv) / denom;
+ return 0.f <= t1 && t1 <= 1.0f && 0.f <= t2 && t2 <= 1.f;
+};
+
+class TriangleCursor : public TriangleSelector::SinglePointCursor
+{
+public:
+ TriangleCursor() = delete;
+ explicit TriangleCursor(const Vec3f& center_,
+ const Vec3f& source_,
+ float radius_world,
+ const Transform3d& trafo_,
+ const TriangleSelector::ClippingPlane& clipping_plane_,
+ const std::array& vertices)
+ : TriangleSelector::SinglePointCursor(center_, source_, radius_world, trafo_, clipping_plane_)
+ , barycentric_(vertices[0], vertices[1], vertices[2])
+ {}
+ ~TriangleCursor() override = default;
+
+ static std::unique_ptr build_cursor(const TriangleSelector& source_selector, const TriangleSelector::Triangle& tri)
+ {
+ const Vec3f& pv0 = source_selector.m_vertices[tri.verts_idxs[0]].v;
+ const Vec3f& pv1 = source_selector.m_vertices[tri.verts_idxs[1]].v;
+ const Vec3f& pv2 = source_selector.m_vertices[tri.verts_idxs[2]].v;
+
+ // Calculate the centroid of the triangle
+ const Vec3f center = (pv0 + pv1 + pv2) / 3.f;
+
+ // Calculate the norm of the plane
+ const Vec3f& norm = source_selector.m_face_normals[tri.source_triangle];
+
+ // Calculate the min distance from the centroid to every edges
+ const float radius = std::max(std::min(std::min(point_to_line_dist(center, pv0, pv1), point_to_line_dist(center, pv0, pv2)),
+ point_to_line_dist(center, pv1, pv2)), 0.1f);
+
+ return std::make_unique(center, center + norm, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane(),
+ std::array{pv0, pv1, pv2});
+ }
+
+ bool is_mesh_point_inside(const Vec3f& point) const override
+ {
+ return barycentric_.is_point_inside_triangle(point);
+ }
+
+ bool is_edge_inside_cursor(const TriangleSelector::Triangle& tr, const std::vector& vertices) const override
+ {
+ std::array pts_proj; // Projected point onto the plane
+ std::array pts_dist; // Distance to the plane, positive means above, negative means below the plane
+ for (int i = 0; i < 3; ++i) {
+ Vec3f p = vertices[tr.verts_idxs[i]].v;
+ if (!this->uniform_scaling)
+ p = this->trafo * p;
+
+ const Vec3f diff = p - center;
+ pts_dist[i] = diff.dot(dir);
+ pts_proj[i] = p - pts_dist[i] * dir;
+ }
+
+ for (int side = 0; side < 3; ++side) {
+ const int idx_a = side;
+ const int idx_b = side < 2 ? side + 1 : 0;
+
+ // If both ends at the same side and farther than tolerance, skip
+ const float dist_a = pts_dist[idx_a];
+ const float dist_b = pts_dist[idx_b];
+ if ((dist_a > tolerance && dist_b > tolerance) || (dist_a < -tolerance && dist_b < -tolerance)) {
+ continue;
+ }
+
+ // Find the projected line segment which has distance to the plane within the tolerance
+ Vec3f pt_a = pts_proj[idx_a];
+ Vec3f pt_b = pts_proj[idx_b];
+ if (std::abs(dist_a) > tolerance) {
+ pt_a = (tolerance - dist_b) / (dist_a - dist_b) * (pts_proj[idx_a] - pts_proj[idx_b]) + pts_proj[idx_b];
+ }
+ if (std::abs(dist_b) > tolerance) {
+ pt_b = (tolerance - dist_a) / (dist_b - dist_a) * (pts_proj[idx_b] - pts_proj[idx_a]) + pts_proj[idx_a];
+ }
+
+ // If any projected end is inside the triangle, then is in
+ if (barycentric_.is_point_inside_triangle(pt_a) ||
+ barycentric_.is_point_inside_triangle(pt_b)) {
+ return true;
+ }
+
+ // Otherwise see if the segment (pt_a, pt_b) intersects the triangle
+ {
+ const Vec3f uvw_a = barycentric_.calc(pt_a);
+ const Vec3f uvw_b = barycentric_.calc(pt_b);
+ const Vec3f uvw_0 {1.f,0.f,0.f};
+ const Vec3f uvw_1 {0.f,1.f,0.f};
+ const Vec3f uvw_2 {0.f,0.f,1.f};
+ constexpr std::pair proj{0, 1};
+
+ if (segments_intersect_proj(uvw_a, uvw_b, uvw_0, uvw_1, proj)||
+ segments_intersect_proj(uvw_a, uvw_b, uvw_0, uvw_2, proj)||
+ segments_intersect_proj(uvw_a, uvw_b, uvw_1, uvw_2, proj)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ bool is_facet_visible(int facet_idx, const std::vector& face_normals) const override
+ {
+ const Vec3f& n = face_normals[facet_idx];
+
+ return check_normal(n, this->dir);
+ }
+
+ static bool check_normal(const Vec3f& facet_norm, const Vec3f& camera_dir)
+ {
+ const float a = -(facet_norm.dot(camera_dir));
+ return std::clamp(a, 0.f, 1.f) >= facet_angle_limit;
+ }
+
+ static constexpr float tolerance = 0.01f;
+
+private:
+ const Barycentric barycentric_;
+ static const double facet_angle_limit;
+
+ static float point_to_line_dist(const Vec3f& p, const Vec3f& a, const Vec3f& b)
+ {
+ const Eigen::ParametrizedLine line = Eigen::ParametrizedLine::Through(a, b);
+ return line.distance(p);
+ }
+};
+const double TriangleCursor::facet_angle_limit = cos(Geometry::deg2rad(5.0));
+
+
+// Remap painting data from source mesh to target mesh using spatial mapping.
+TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting(
+ const indexed_triangle_set& source_its,
+ const TriangleSplittingData& source_painting,
+ const indexed_triangle_set& target_its,
+ const Transform3d& target_transform,
+ const std::optional>& existing_painting)
+{
+ TriangleSelector::TriangleSplittingData result;
+ if (source_painting.bitstream.empty())
+ return result;
+
+ // 1. Deserialize source painting
+ TriangleMesh source_mesh(source_its);
+ TriangleSelector source_selector(source_mesh);
+ source_selector.deserialize(source_painting, false);
+
+ // 2. Extract painted geometry
+ std::vector> painted_triangles;
+ painted_triangles.reserve(source_selector.m_triangles.size());
+ for (const Triangle& tr : source_selector.m_triangles) {
+ if (tr.valid() && !tr.is_split() && tr.get_state() != EnforcerBlockerType::NONE) {
+ painted_triangles.push_back(std::ref(tr));
+ }
+ }
+
+ if (painted_triangles.empty())
+ return result;
+
+ // 3. Build AABB tree of target mesh so we could find nearest face quickly
+ TriangleMesh target_mesh(target_its);
+ target_mesh.transform(target_transform);
+ AABBTreeIndirect::Tree3f target_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(target_mesh.its.vertices, target_mesh.its.indices);
+
+ // Helper: check overlap between a paint triangle and a target triangle.
+ // Uses 3D barycentric point-in-triangle tests and dominant-axis 2D projection
+ // for edge-edge intersection to handle all triangle orientations correctly.
+ auto check_overlap = [&](const Vec3f& pa, const Vec3f& pb, const Vec3f& pc,
+ const Vec3f& ta, const Vec3f& tb, const Vec3f& tc) -> bool {
+ // Check if any target vertex is inside the paint triangle
+ if (is_point_inside_triangle(ta, pa, pb, pc)) return true;
+ if (is_point_inside_triangle(tb, pa, pb, pc)) return true;
+ if (is_point_inside_triangle(tc, pa, pb, pc)) return true;
+
+ // Check if any paint vertex is inside the target triangle
+ if (is_point_inside_triangle(pa, ta, tb, tc)) return true;
+ if (is_point_inside_triangle(pb, ta, tb, tc)) return true;
+ if (is_point_inside_triangle(pc, ta, tb, tc)) return true;
+
+ // Check edge-edge intersections using dominant-axis 2D projection.
+ // Project onto the plane (XY, XZ, or YZ) where the triangles have the
+ // most area, avoiding degenerate projections for vertical/angled surfaces.
+ Vec3f n1 = (pb - pa).cross(pc - pa);
+ Vec3f n2 = (tb - ta).cross(tc - ta);
+ Vec3f n_abs = (n1.cwiseAbs() + n2.cwiseAbs());
+ int axis = (n_abs.x() >= n_abs.y() && n_abs.x() >= n_abs.z()) ? 0
+ : (n_abs.y() >= n_abs.z()) ? 1 : 2;
+ const int u_axis = (axis + 1) % 3;
+ const int v_axis = (axis + 2) % 3;
+ const std::pair proj{u_axis, v_axis};
+
+ if (segments_intersect_proj(pa, pb, ta, tb, proj)) return true;
+ if (segments_intersect_proj(pa, pb, tb, tc, proj)) return true;
+ if (segments_intersect_proj(pa, pb, tc, ta, proj)) return true;
+ if (segments_intersect_proj(pb, pc, ta, tb, proj)) return true;
+ if (segments_intersect_proj(pb, pc, tb, tc, proj)) return true;
+ if (segments_intersect_proj(pb, pc, tc, ta, proj)) return true;
+ if (segments_intersect_proj(pc, pa, ta, tb, proj)) return true;
+ if (segments_intersect_proj(pc, pa, tb, tc, proj)) return true;
+ if (segments_intersect_proj(pc, pa, tc, ta, proj)) return true;
+
+ return false;
+ };
+
+ // 4. For each painted face, we find the nearest target face, and apply the TriangleCursor to paint it
+ TriangleSelector target_selector(target_mesh);
+ if (existing_painting) {
+ // Restore existing painting first, if given
+ target_selector.deserialize(existing_painting->get(), false);
+ }
+ for (auto tri_ref : painted_triangles) {
+ const Triangle& tri = tri_ref.get();
+ const Vec3f& pv0 = source_selector.m_vertices[tri.verts_idxs[0]].v;
+ const Vec3f& pv1 = source_selector.m_vertices[tri.verts_idxs[1]].v;
+ const Vec3f& pv2 = source_selector.m_vertices[tri.verts_idxs[2]].v;
+
+ // Find ALL target faces whose bounding boxes overlap with the paint
+ // triangle's bounding box, not just the nearest one.
+ Eigen::AlignedBox3f pt_bbox;
+ pt_bbox.extend(pv0);
+ pt_bbox.extend(pv1);
+ pt_bbox.extend(pv2);
+ pt_bbox.min() -= Eigen::Vector3f::Constant(TriangleCursor::tolerance);
+ pt_bbox.max() += Eigen::Vector3f::Constant(TriangleCursor::tolerance);
+
+ AABBTreeIndirect::traverse(target_tree, AABBTreeIndirect::intersecting(pt_bbox), [&](const AABBTreeIndirect::Tree3f::Node& node) -> bool {
+ size_t face_idx = node.idx;
+ if (face_idx >= target_mesh.its.indices.size())
+ return true;
+
+ const Vec3f& norm_a = source_selector.m_face_normals[tri.source_triangle];
+ const Vec3f& norm_b = target_selector.m_face_normals[face_idx];
+
+ const Vec3i32& face = target_mesh.its.indices[face_idx];
+ const Vec3f& ta = target_mesh.its.vertices[face(0)];
+ const Vec3f& tb = target_mesh.its.vertices[face(1)];
+ const Vec3f& tc = target_mesh.its.vertices[face(2)];
+
+ if (TriangleCursor::check_normal(norm_b, -norm_a) && check_overlap(pv0, pv1, pv2, ta, tb, tc)) {
+ // Paint this face
+ target_selector.select_patch(face_idx, TriangleCursor::build_cursor(source_selector, tri), tri.get_state(),
+ Transform3d::Identity(), true, 0.f, true);
+ }
+ return true; // continue traversal
+ });
+ }
+
+ return target_selector.serialize();
+}
+
} // namespace Slic3r
diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp
index 50bbdd4ed0..11517f5c6c 100644
--- a/src/libslic3r/TriangleSelector.hpp
+++ b/src/libslic3r/TriangleSelector.hpp
@@ -308,7 +308,8 @@ public:
EnforcerBlockerType new_state, // enforcer or blocker?
const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation
bool triangle_splitting, // If triangles will be split base on the cursor or not
- float highlight_by_angle_deg = 0.f); // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees.
+ float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees.
+ bool select_partially = false); // Select a triangle if it's partially in the cursor but too small to be subdivided
void seed_fill_select_triangles(const Vec3f &hit, // point where to start
int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to
@@ -372,6 +373,25 @@ public:
// The operation may merge split triangles if they are being assigned the same color.
void seed_fill_apply_on_triangles(EnforcerBlockerType new_state);
+ // Saved painting data for remapping after mesh change.
+ struct SavedPainting {
+ TriangleMesh mesh; // Original mesh
+ TriangleSplittingData supported;
+ TriangleSplittingData seam;
+ TriangleSplittingData mmu;
+ TriangleSplittingData fuzzy;
+ };
+
+ // Remap painting data from source mesh to target mesh using spatial mapping.
+ // `target_transform` should transform the target mesh into source's coordinate space.
+ // If `existing_painting` is present, the result will be a combine of `existing_painting` and remapped `source_painting`.
+ static TriangleSplittingData remap_painting(
+ const indexed_triangle_set& source_its,
+ const TriangleSplittingData& source_painting,
+ const indexed_triangle_set& target_its,
+ const Transform3d& target_transform,
+ const std::optional>& existing_painting);
+
protected:
// Triangle and info about how it's split.
class Triangle {
@@ -477,8 +497,8 @@ protected:
// Private functions:
private:
- bool select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting);
- bool select_triangle_recursive(int facet_idx, const Vec3i32 &neighbors, EnforcerBlockerType type, bool triangle_splitting);
+ bool select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting, bool select_partially);
+ bool select_triangle_recursive(int facet_idx, const Vec3i32 &neighbors, EnforcerBlockerType type, bool triangle_splitting, bool select_partially);
void undivide_triangle(int facet_idx);
void split_triangle(int facet_idx, const Vec3i32 &neighbors);
void remove_useless_children(int facet_idx); // No hidden meaning. Triangles are meant.
@@ -521,11 +541,10 @@ private:
int m_free_triangles_head { -1 };
int m_free_vertices_head { -1 };
+
+ friend class TriangleCursor;
};
-
-
-
} // namespace Slic3r
#endif // libslic3r_TriangleSelector_hpp_
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index d40cf40518..c7afa29d95 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -331,6 +331,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Mouse3DController.hpp
GUI/MsgDialog.cpp
GUI/MsgDialog.hpp
+ GUI/MultiChoiceDialog.hpp
+ GUI/MultiChoiceDialog.cpp
GUI/MultiMachine.cpp
GUI/MultiMachine.hpp
GUI/MultiMachineManagerPage.cpp
@@ -498,6 +500,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Widgets/Button.hpp
GUI/Widgets/CheckBox.cpp
GUI/Widgets/CheckBox.hpp
+ GUI/Widgets/CheckList.cpp
+ GUI/Widgets/CheckList.hpp
GUI/Widgets/ComboBox.cpp
GUI/Widgets/ComboBox.hpp
GUI/Widgets/DialogButtons.cpp
@@ -685,6 +689,8 @@ if (APPLE)
GUI/Mouse3DHandlerMac.mm
GUI/InstanceCheckMac.mm
GUI/InstanceCheckMac.h
+ GUI/DeepLinkHandlerMac.mm
+ GUI/DeepLinkHandlerMac.h
GUI/GUI_UtilsMac.mm
GUI/wxMediaCtrl2.mm
GUI/wxMediaCtrl2.h
diff --git a/src/slic3r/GUI/BBLTopbar.cpp b/src/slic3r/GUI/BBLTopbar.cpp
index 9b1c6ab97d..7a5bece0af 100644
--- a/src/slic3r/GUI/BBLTopbar.cpp
+++ b/src/slic3r/GUI/BBLTopbar.cpp
@@ -58,7 +58,7 @@ CenteredTitle::CenteredTitle(wxWindow* parent)
wxRect rect = GetClientRect();
wxString ellipsized = wxControl::Ellipsize(m_title, dc, wxELLIPSIZE_END, wxMax(0, rect.GetWidth() - FromDIP(8)));
- int y = rect.y + (rect.height - textHeight) / 2;
+ int y = rect.y + (rect.height - textHeight) / 2 + 1;
int x = rect.x + ((ellipsized != m_title) // is ellipsized
? FromDIP(4) // align to left when clipped
: (rect.width - dc.GetTextExtent(m_title).GetWidth()) / 2); // centered when has available space
@@ -115,15 +115,14 @@ void BBLTopbarArt::DrawBackground(wxDC& dc, wxWindow* wnd, const wxRect& rect)
void BBLTopbarArt::DrawButton(wxDC& dc, wxWindow* wnd, const wxAuiToolBarItem& item, const wxRect& rect)
{
int textWidth = 0, textHeight = 0;
+ wxFontMetrics fm;
if (m_flags & wxAUI_TB_TEXT)
{
dc.SetFont(m_font);
- int tx, ty;
-
- dc.GetTextExtent(wxT("ABCDHgj"), &tx, &textHeight);
- textWidth = 0;
- dc.GetTextExtent(item.GetLabel(), &textWidth, &ty);
+ fm = dc.GetFontMetrics();
+ textHeight = fm.ascent + fm.descent;
+ dc.GetTextExtent(item.GetLabel(), &textWidth, nullptr);
}
int bmpX = 0, bmpY = 0;
@@ -158,9 +157,7 @@ void BBLTopbarArt::DrawButton(wxDC& dc, wxWindow* wnd, const wxAuiToolBarItem& i
(bmpSize.y / 2);
textX = bmpX + wnd->FromDIP(3) + bmpSize.x;
- textY = rect.y +
- (rect.height / 2) -
- (textHeight / 2);
+ textY = bmpY + bmpSize.y / 2 - textHeight / 2 - fm.externalLeading + 1;
}
@@ -228,7 +225,9 @@ BBLTopbar::BBLTopbar(wxWindow* pwin, wxFrame* parent)
void BBLTopbar::Init(wxFrame* parent)
{
- SetArtProvider(new BBLTopbarArt());
+ auto* art = new BBLTopbarArt();
+ art->SetFont(Label::Body_12);
+ SetArtProvider(art);
m_frame = parent;
m_skip_popup_file_menu = false;
m_skip_popup_dropdown_menu = false;
@@ -321,6 +320,7 @@ void BBLTopbar::Init(wxFrame* parent)
//this->AddStretchSpacer(1);
m_title_ctrl = new CenteredTitle(this);
+ m_title_ctrl->SetFont(Label::Body_12);
wxAuiToolBarItem* title_item = this->AddControl(m_title_ctrl, "");
title_item->SetProportion(1);
diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp
index dbb0e8d525..1df4dba9c2 100644
--- a/src/slic3r/GUI/ConfigManipulation.cpp
+++ b/src/slic3r/GUI/ConfigManipulation.cpp
@@ -605,6 +605,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral;
+ // gyroid_optimized only applies when the sparse infill pattern is gyroid;
+ // hide the whole line otherwise.
+ toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid);
+
// If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
if (have_infill) {
toggle_field("fill_multiline", have_multiline_infill_pattern);
diff --git a/src/slic3r/GUI/DeepLinkHandlerMac.h b/src/slic3r/GUI/DeepLinkHandlerMac.h
new file mode 100644
index 0000000000..e9877e3868
--- /dev/null
+++ b/src/slic3r/GUI/DeepLinkHandlerMac.h
@@ -0,0 +1,16 @@
+#ifndef slic3r_GUI_DeepLinkHandlerMac_h_
+#define slic3r_GUI_DeepLinkHandlerMac_h_
+
+namespace Slic3r {
+namespace GUI {
+
+// Re-registers a Cocoa Apple Event handler for kInternetEventClass/kAEGetURL.
+// Works around a regression observed after upgrading to wxWidgets 3.3.2 on
+// macOS Tahoe (#13119) where wxWidgets' built-in handler is registered but
+// never fires for orcaslicer:// deep links.
+void register_mac_deep_link_handler();
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif
diff --git a/src/slic3r/GUI/DeepLinkHandlerMac.mm b/src/slic3r/GUI/DeepLinkHandlerMac.mm
new file mode 100644
index 0000000000..fd3d8272ff
--- /dev/null
+++ b/src/slic3r/GUI/DeepLinkHandlerMac.mm
@@ -0,0 +1,41 @@
+#import
+#import
+
+#include
+
+#include "DeepLinkHandlerMac.h"
+#include "GUI_App.hpp"
+
+@interface OrcaDeepLinkHandler : NSObject
+- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)reply;
+@end
+
+@implementation OrcaDeepLinkHandler
+- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)reply
+{
+ NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
+ if (url == nil || url.length == 0)
+ return;
+ BOOST_LOG_TRIVIAL(info) << "Deep link received: " << [url UTF8String];
+ Slic3r::GUI::wxGetApp().MacOpenURL(wxString::FromUTF8([url UTF8String]));
+}
+@end
+
+namespace Slic3r {
+namespace GUI {
+
+void register_mac_deep_link_handler()
+{
+ static OrcaDeepLinkHandler *handler = nil;
+ if (handler == nil)
+ handler = [[OrcaDeepLinkHandler alloc] init];
+
+ [[NSAppleEventManager sharedAppleEventManager]
+ setEventHandler:handler
+ andSelector:@selector(handleGetURLEvent:withReplyEvent:)
+ forEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp
index 0e197a236b..63311b99af 100644
--- a/src/slic3r/GUI/DeviceCore/DevManager.cpp
+++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp
@@ -1,4 +1,7 @@
#include
+
+#include
+
#include "DevManager.h"
#include "DevUtil.h"
@@ -151,11 +154,15 @@ namespace Slic3r
{
keep_alive();
MachineObject* obj = this->get_selected_machine();
+ if (!obj) {
+ BOOST_LOG_TRIVIAL(warning) << "DeviceManager::check_pushing selected machine not found";
+ return;
+ }
std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
auto internal = std::chrono::duration_cast(start - obj->last_update_time);
- if (obj && !obj->is_support_mqtt_alive)
+ if (!obj->is_support_mqtt_alive)
{
if (internal.count() > TIMEOUT_FOR_STRAT && internal.count() < 1000 * 60 * 60 * 300)
{
@@ -916,18 +923,33 @@ namespace Slic3r
const auto cloud_provider = Slic3r::GUI::wxGetApp().get_printer_cloud_provider();
if (Slic3r::GUI::wxGetApp().is_user_login(cloud_provider))
{
- m_manager->check_pushing();
- try
- {
- agent->refresh_connection(cloud_provider);
+ try {
+ m_manager->check_pushing();
+ } catch (const std::exception& e) {
+ BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer check_pushing exception="
+ << e.what();
+ } catch (...) {
+ BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer check_pushing unknown exception";
}
- catch (...)
- {
- ;
+
+ try {
+ agent->refresh_connection(cloud_provider);
+ } catch (const std::exception& e) {
+ BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer refresh_connection exception="
+ << e.what();
+ } catch (...) {
+ BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer refresh_connection unknown exception";
}
}
// certificate
- agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
+ try {
+ agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
+ } catch (const std::exception& e) {
+ BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer install_device_cert exception="
+ << e.what();
+ } catch (...) {
+ BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer install_device_cert unknown exception";
+ }
}
}
diff --git a/src/slic3r/GUI/DeviceErrorDialog.cpp b/src/slic3r/GUI/DeviceErrorDialog.cpp
index 008c99474d..aadc39abd9 100644
--- a/src/slic3r/GUI/DeviceErrorDialog.cpp
+++ b/src/slic3r/GUI/DeviceErrorDialog.cpp
@@ -175,7 +175,7 @@ void DeviceErrorDialog::init_button_list()
init_button(PROBLEM_SOLVED_RESUME, _L("Problem Solved and Resume"));
init_button(TURN_OFF_FIRE_ALARM, _L("Got it, Turn off the Fire Alarm."));
init_button(RETRY_PROBLEM_SOLVED, _L("Retry (problem solved)"));
- init_button(CANCLE, _L("Cancel"));
+ init_button(CANCEL, _L("Cancel"));
init_button(STOP_DRYING, _L("Stop Drying"));
init_button(PROCEED, _L("Proceed"));
init_button(DBL_CHECK_CANCEL, _L("Cancel"));
@@ -445,7 +445,7 @@ void DeviceErrorDialog::on_button_click(ActionButton btn_id)
m_obj->command_ams_control("resume");
break;
}
- case DeviceErrorDialog::CANCLE: {
+ case DeviceErrorDialog::CANCEL: {
break;
}
case DeviceErrorDialog::STOP_DRYING: {
diff --git a/src/slic3r/GUI/DeviceErrorDialog.hpp b/src/slic3r/GUI/DeviceErrorDialog.hpp
index cca8af14c1..af0b850e4b 100644
--- a/src/slic3r/GUI/DeviceErrorDialog.hpp
+++ b/src/slic3r/GUI/DeviceErrorDialog.hpp
@@ -43,7 +43,7 @@ public:
RETRY_PROBLEM_SOLVED = 34,
STOP_DRYING = 35,
- CANCLE = 37,
+ CANCEL = 37,
REMOVE_CLOSE_BTN = 39, // special case, do not show close button
PROCEED = 41,
diff --git a/src/slic3r/GUI/ExtraRenderers.cpp b/src/slic3r/GUI/ExtraRenderers.cpp
index 4c16ce8deb..18811ef241 100644
--- a/src/slic3r/GUI/ExtraRenderers.cpp
+++ b/src/slic3r/GUI/ExtraRenderers.cpp
@@ -324,9 +324,18 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR
else
c_editor->SetSelection(atoi(data.GetText().c_str()) - 1);
- // Open the dropdown immediately when the editor is focused.
c_editor->Bind(wxEVT_SET_FOCUS, [c_editor](wxFocusEvent& evt) {
+#ifdef __WXGTK__
+ // On wxGTK the data-view editor may receive focus before its native
+ // window is mapped. Opening the popup one event later avoids creating
+ // the GTK popup without a valid toplevel parent.
+ c_editor->CallAfter([c_editor]() {
+ if (c_editor->IsShownOnScreen())
+ c_editor->ForceDropdownOpen();
+ });
+#else
c_editor->ForceDropdownOpen();
+#endif
evt.Skip();
});
@@ -343,7 +352,10 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR
bool BitmapChoiceRenderer::GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value)
{
- ::ComboBox*c = static_cast<::ComboBox *>(ctrl);
+ auto* c = dynamic_cast<::ComboBox*>(ctrl);
+ if (!c)
+ return false;
+
int selection = c->GetSelection();
if (selection < 0)
return false;
@@ -389,4 +401,3 @@ wxSize TextRenderer::GetSize() const
return GetTextExtent(m_value);
}
-
diff --git a/src/slic3r/GUI/ExtraRenderers.hpp b/src/slic3r/GUI/ExtraRenderers.hpp
index 18f1f49182..59a1f040ee 100644
--- a/src/slic3r/GUI/ExtraRenderers.hpp
+++ b/src/slic3r/GUI/ExtraRenderers.hpp
@@ -28,6 +28,15 @@ public:
m_bmp(other.m_bmp)
{ }
+ DataViewBitmapText& operator=(const DataViewBitmapText &other)
+ {
+ if (this != &other) {
+ m_text = other.m_text;
+ m_bmp = other.m_bmp;
+ }
+ return *this;
+ }
+
void SetText(const wxString &text) { m_text = text; }
wxString GetText() const { return m_text; }
void SetBitmap(const wxBitmap &bmp) { m_bmp = bmp; }
diff --git a/src/slic3r/GUI/FilamentMapDialog.cpp b/src/slic3r/GUI/FilamentMapDialog.cpp
index 73f8041a28..a514494af1 100644
--- a/src/slic3r/GUI/FilamentMapDialog.cpp
+++ b/src/slic3r/GUI/FilamentMapDialog.cpp
@@ -215,7 +215,7 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent,
main_sizer->Add(bottom_panel, 0, wxEXPAND);
m_ok_btn->Bind(wxEVT_BUTTON, &FilamentMapDialog::on_ok, this);
- m_cancel_btn->Bind(wxEVT_BUTTON, &FilamentMapDialog::on_cancle, this);
+ m_cancel_btn->Bind(wxEVT_BUTTON, &FilamentMapDialog::on_cancel, this);
SetEscapeId(wxID_CANCEL);
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_ESCAPE) {
@@ -286,7 +286,7 @@ void FilamentMapDialog::on_ok(wxCommandEvent &event)
EndModal(wxID_OK);
}
-void FilamentMapDialog::on_cancle(wxCommandEvent &event) { EndModal(wxID_CANCEL); }
+void FilamentMapDialog::on_cancel(wxCommandEvent &event) { EndModal(wxID_CANCEL); }
void FilamentMapDialog::update_panel_status(PageType page)
{
diff --git a/src/slic3r/GUI/FilamentMapDialog.hpp b/src/slic3r/GUI/FilamentMapDialog.hpp
index bafc6ab3f2..3272f203b3 100644
--- a/src/slic3r/GUI/FilamentMapDialog.hpp
+++ b/src/slic3r/GUI/FilamentMapDialog.hpp
@@ -60,7 +60,7 @@ public:
void set_modal_btn_labels(const wxString& left_label, const wxString& right_label);
private:
void on_ok(wxCommandEvent &event);
- void on_cancle(wxCommandEvent &event);
+ void on_cancel(wxCommandEvent &event);
void on_switch_mode(wxCommandEvent &event);
void on_checkbox(wxCommandEvent &event);
diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp
index 9a7c5946aa..2df840e5fe 100644
--- a/src/slic3r/GUI/GCodeViewer.cpp
+++ b/src/slic3r/GUI/GCodeViewer.cpp
@@ -46,6 +46,7 @@
#include
#include
#include
+#include
#include
@@ -97,7 +98,7 @@ static std::string get_view_type_string(libvgcode::EViewType view_type)
return _u8L("Filament");
else if (view_type == libvgcode::EViewType::LayerTimeLinear)
return _u8L("Layer Time");
-else if (view_type == libvgcode::EViewType::LayerTimeLogarithmic)
+ else if (view_type == libvgcode::EViewType::LayerTimeLogarithmic)
return _u8L("Layer Time (log)");
// ORCA: Add Pressure Advance visualization support
else if (view_type == libvgcode::EViewType::PressureAdvance)
@@ -127,6 +128,29 @@ static int find_close_layer_idx(const std::vector &zs, double &z, double
return -1;
}
+static std::string format_compact_weight(double value_in_grams, bool imperial_units)
+{
+ char buffer[64];
+ if (imperial_units) {
+ ::sprintf(buffer, "%.2f oz", value_in_grams / GizmoObjectManipulation::oz_to_g);
+ return buffer;
+ }
+
+ const double abs_value = value_in_grams < 0.0 ? -value_in_grams : value_in_grams;
+ const char* unit = "g";
+ double scaled_value = abs_value;
+ if (scaled_value >= 1000000.0) {
+ scaled_value /= 1000000.0;
+ unit = "t";
+ } else if (scaled_value >= 1000.0) {
+ scaled_value /= 1000.0;
+ unit = "kg";
+ }
+
+ ::sprintf(buffer, "%s%.2f%s", value_in_grams < 0.0 ? "-" : "", scaled_value, unit);
+ return buffer;
+}
+
#if ENABLE_ACTUAL_SPEED_DEBUG
int GCodeViewer::SequentialView::ActualSpeedImguiWidget::plot(const char* label, const std::array& frame_size)
{
@@ -1294,6 +1318,25 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
//BBS: move the id to the end of reset
m_last_result_id = gcode_result.id;
m_gcode_result = &gcode_result;
+ m_move_type_counts.fill(0);
+ for (auto& move_type_times : m_move_type_times)
+ move_type_times.fill(0.0f);
+ m_move_type_distances.fill(0.0f);
+ for (const GCodeProcessorResult::MoveVertex& move : gcode_result.moves) {
+ if (move.internal_only)
+ continue;
+
+ const size_t move_type = static_cast(move.type);
+ if (move_type < m_move_type_counts.size()) {
+ ++m_move_type_counts[move_type];
+ for (size_t mode = 0; mode < move.time.size(); ++mode)
+ m_move_type_times[move_type][mode] += move.time[mode];
+ if (move.type == EMoveType::Retract || move.type == EMoveType::Unretract)
+ m_move_type_distances[move_type] += std::fabs(move.delta_extruder);
+ else
+ m_move_type_distances[move_type] += move.travel_dist;
+ }
+ }
m_only_gcode_in_preview = only_gcode;
m_sequential_view.gcode_window.load_gcode(gcode_result.filename, gcode_result.lines_ends);
@@ -1456,6 +1499,29 @@ void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data)
{
m_loaded_as_preview = true;
+ m_move_type_counts.fill(0);
+ for (auto& move_type_times : m_move_type_times)
+ move_type_times.fill(0.0f);
+ m_move_type_distances.fill(0.0f);
+ const size_t normal_time_mode_idx = static_cast(PrintEstimatedStatistics::ETimeMode::Normal);
+ for (size_t i = 0; i < data.vertices.size(); ++i) {
+ const libvgcode::PathVertex& vertex = data.vertices[i];
+ const size_t move_type = static_cast(vertex.type);
+ if (move_type < m_move_type_counts.size()) {
+ ++m_move_type_counts[move_type];
+ for (size_t mode = 0; mode < vertex.times.size(); ++mode)
+ m_move_type_times[move_type][mode] += vertex.times[mode];
+ if (vertex.type == libvgcode::EMoveType::Retract || vertex.type == libvgcode::EMoveType::Unretract) {
+ m_move_type_distances[move_type] += std::fabs(vertex.feedrate) * vertex.times[normal_time_mode_idx];
+ } else if (i > 0) {
+ const float dx = vertex.position[0] - data.vertices[i - 1].position[0];
+ const float dy = vertex.position[1] - data.vertices[i - 1].position[1];
+ const float dz = vertex.position[2] - data.vertices[i - 1].position[2];
+ m_move_type_distances[move_type] += std::sqrt(dx * dx + dy * dy + dz * dz);
+ }
+ }
+ }
+
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::Skirt, { 127, 255, 127 });
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::ExternalPerimeter, { 255, 255, 0 });
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::SupportMaterial, { 127, 255, 127 });
@@ -1503,6 +1569,10 @@ void GCodeViewer::reset()
m_extruders_count = 0;
m_filament_diameters = std::vector();
m_filament_densities = std::vector();
+ m_move_type_counts.fill(0);
+ for (auto& move_type_times : m_move_type_times)
+ move_type_times.fill(0.0f);
+ m_move_type_distances.fill(0.0f);
m_print_statistics.reset();
m_custom_gcode_per_print_z = std::vector();
m_left_extruder_filament.clear();
@@ -2979,39 +3049,42 @@ void GCodeViewer::render_all_plates_stats(const std::vectorfirst + 1), offsets[_u8L("Filament")]});
char buf[64];
- double unit_conver = imperial_units ? GizmoObjectManipulation::oz_to_g : 1.0;
-
float column_sum_m = 0.0f;
float column_sum_g = 0.0f;
if (displayed_columns & ColumnData::Model) {
+ const std::string weight_text = format_compact_weight(model_used_filaments_g_all_plates[i], imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", model_used_filaments_m_all_plates[i], model_used_filaments_g_all_plates[i] / unit_conver);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", model_used_filaments_m_all_plates[i], weight_text.c_str());
else
- ::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", model_used_filaments_m_all_plates[i], model_used_filaments_g_all_plates[i] / unit_conver);
+ ::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Model")] });
column_sum_m += model_used_filaments_m_all_plates[i];
column_sum_g += model_used_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::Support) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", support_used_filaments_m_all_plates[i], support_used_filaments_g_all_plates[i] / unit_conver);
+ const std::string weight_text = format_compact_weight(support_used_filaments_g_all_plates[i], imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", support_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Support")] });
column_sum_m += support_used_filaments_m_all_plates[i];
column_sum_g += support_used_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::Flushed) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", flushed_filaments_m_all_plates[i], flushed_filaments_g_all_plates[i] / unit_conver);
+ const std::string weight_text = format_compact_weight(flushed_filaments_g_all_plates[i], imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", flushed_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Flushed")] });
column_sum_m += flushed_filaments_m_all_plates[i];
column_sum_g += flushed_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::WipeTower) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", wipe_tower_used_filaments_m_all_plates[i], wipe_tower_used_filaments_g_all_plates[i] / unit_conver);
+ const std::string weight_text = format_compact_weight(wipe_tower_used_filaments_g_all_plates[i], imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", wipe_tower_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Tower")] });
column_sum_m += wipe_tower_used_filaments_m_all_plates[i];
column_sum_g += wipe_tower_used_filaments_g_all_plates[i];
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", column_sum_m, column_sum_g / unit_conver);
+ const std::string weight_text = format_compact_weight(column_sum_g, imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", column_sum_m, weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Total")] });
}
@@ -3376,7 +3449,9 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
const PrintEstimatedStatistics::Mode& time_mode = m_print_statistics.modes[static_cast(m_viewer.get_time_mode())];
const libvgcode::EViewType curr_view_type = m_viewer.get_view_type();
const int curr_view_type_i = static_cast(curr_view_type);
- bool show_estimated_time = time_mode.time > 0.0f && (curr_view_type == libvgcode::EViewType::FeatureType ||
+ const size_t current_time_mode = static_cast(m_viewer.get_time_mode());
+ const float total_estimated_time = time_mode.time > 0.0f ? time_mode.time : m_viewer.get_estimated_time();
+ bool show_estimated_time = total_estimated_time > 0.0f && (curr_view_type == libvgcode::EViewType::FeatureType ||
curr_view_type == libvgcode::EViewType::LayerTimeLinear || curr_view_type == libvgcode::EViewType::LayerTimeLogarithmic ||
(curr_view_type == libvgcode::EViewType::ColorPrint && !time_mode.custom_gcode_times.empty()));
@@ -3390,6 +3465,39 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
ImVec2 pos_rect = ImGui::GetCursorScreenPos();
float window_padding = 4.0f * m_scale;
+ auto format_compact_count = [](unsigned long long value) {
+ static constexpr const char* suffixes[] = { "", "K", "M", "B", "T", "P", "E" };
+ constexpr size_t suffix_count = sizeof(suffixes) / sizeof(suffixes[0]);
+
+ if (value < 1000)
+ return std::to_string(value);
+
+ size_t suffix_index = 0;
+ unsigned long long divisor = 1;
+ while (suffix_index + 1 < suffix_count && value / divisor >= 1000) {
+ divisor *= 1000;
+ ++suffix_index;
+ }
+
+ const unsigned long long whole = value / divisor;
+ const unsigned long long tenths = (value % divisor) * 10 / divisor;
+
+ std::string ret = std::to_string(whole);
+ if (tenths != 0)
+ ret += "." + std::to_string(tenths);
+ ret += suffixes[suffix_index];
+ return ret;
+ };
+
+ auto format_percent = [](float percent) {
+ if (percent == 0.0f)
+ return std::string("0");
+
+ char buffer[64];
+ percent > 0.001f ? ::sprintf(buffer, "%.1f", percent * 100.0f) : ::sprintf(buffer, "<0.1");
+ return std::string(buffer);
+ };
+
// ORCA dont use background on top bar to give modern look
//draw_list->AddRectFilled(ImVec2(pos_rect.x,pos_rect.y - ImGui::GetStyle().WindowPadding.y),
//ImVec2(pos_rect.x + ImGui::GetWindowWidth() + ImGui::GetFrameHeight(),pos_rect.y + ImGui::GetFrameHeight() + window_padding * 2.5),
@@ -3426,10 +3534,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
case EItemType::Line: {
draw_list->AddLine({ pos.x + 1, pos.y + icon_size + 2 }, { pos.x + icon_size - 1, pos.y + 4 }, ImGuiWrapper::to_ImU32(color), 3.0f);
break;
+ }
case EItemType::None:
break;
}
- }
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(20.0 * m_scale, 6.0 * m_scale));
@@ -3601,14 +3709,26 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm");
};
- auto role_time_and_percent = [this, time_mode](libvgcode::EGCodeExtrusionRole role) {
+ auto role_time_and_percent = [this, total_estimated_time](libvgcode::EGCodeExtrusionRole role) {
const float time = m_viewer.get_extrusion_role_estimated_time(role);
- return std::make_pair(time, time / time_mode.time);
+ return std::make_pair(time, total_estimated_time > 0.0f ? time / total_estimated_time : 0.0f);
};
- auto travel_time_and_percent = [this, time_mode]() {
+ auto travel_time_and_percent = [this, total_estimated_time]() {
const float time = m_viewer.get_travels_estimated_time();
- return std::make_pair(time, time / time_mode.time);
+ return std::make_pair(time, total_estimated_time > 0.0f ? time / total_estimated_time : 0.0f);
+ };
+
+ auto format_distance = [imperial_units](float distance_mm) {
+ char buffer[64];
+ if (imperial_units) {
+ ::sprintf(buffer, "%.2fin", distance_mm / GizmoObjectManipulation::in_to_mm);
+ } else if (std::fabs(distance_mm) < 1000.0f) {
+ ::sprintf(buffer, "%.0fmm", distance_mm);
+ } else {
+ ::sprintf(buffer, "%.2fm", distance_mm / 1000.0f);
+ }
+ return std::string(buffer);
};
auto used_filament_per_role = [this, imperial_units](ExtrusionRole role) {
@@ -3713,6 +3833,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector used_filaments_length;
std::vector used_filaments_weight;
std::string travel_percent;
+ std::string travel_distance;
+ std::string travel_moves;
std::vector model_used_filaments_m;
std::vector model_used_filaments_g;
double total_model_used_filament_m = 0, total_model_used_filament_g = 0;
@@ -3832,8 +3954,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
auto [model_used_filament_m, model_used_filament_g] = used_filament_per_role(convert(role));
::sprintf(buffer, imperial_units ? "%.2fin" : "%.2fm", model_used_filament_m); // ORCA dont use spacing between value and unit
used_filaments_length.push_back(buffer);
- ::sprintf(buffer, imperial_units ? "%.2foz" : "%.2fg", model_used_filament_g); // ORCA dont use spacing between value and unit
- used_filaments_weight.push_back(buffer);
+ used_filaments_weight.push_back(format_compact_weight(model_used_filament_g, imperial_units));
}
}
@@ -3846,10 +3967,19 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
else
percent > 0.001 ? ::sprintf(buffer, "%.1f", percent * 100) : ::sprintf(buffer, "<0.1");
travel_percent = buffer;
+ percents.push_back(travel_percent);
+
+ // Set travel distance and moves for the Travel row Usage columns
+ travel_distance = format_distance(m_print_statistics.total_travel_distance);
+ used_filaments_length.push_back(travel_distance);
+
+ travel_moves = format_compact_count(m_print_statistics.total_travel_moves);
+ used_filaments_weight.push_back(travel_moves);
}
// ORCA use % symbol for percentage and use "Usage" for "Used filaments"
offsets = calculate_offsets({ {_u8L("Line Type"), labels}, {_u8L("Time"), times}, {"%", percents}, {"", used_filaments_length}, {"", used_filaments_weight}, {_u8L("Display"), {""}}}, icon_size);
+ percents.pop_back();
append_headers({{_u8L("Line Type"), offsets[0]}, {_u8L("Time"), offsets[1]}, {"%", offsets[2]}, {_u8L("Usage"), offsets[3]}, {_u8L("Display"), offsets[5]}});
break;
}
@@ -3913,7 +4043,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
{
std::vector total_filaments;
char buffer[64];
- ::sprintf(buffer, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", ps.total_used_filament / /*1000*/koef, ps.total_weight / unit_conver);
+ const std::string total_weight_text = format_compact_weight(ps.total_weight, imperial_units);
+ ::sprintf(buffer, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", ps.total_used_filament / /*1000*/koef, total_weight_text.c_str());
total_filaments.push_back(buffer);
@@ -3948,9 +4079,54 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
default: { break; }
}
- auto append_option_item = [this, append_item](libvgcode::EOptionType type, std::vector offsets) {
- auto append_option_item_with_type = [this, offsets, append_item](libvgcode::EOptionType type, const ColorRGBA& color, const std::string& label, bool visible) {
- append_item(EItemType::Rect, color, {{ label , offsets[0] }}, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, type, visible]() {
+ auto append_option_item = [this, append_item, current_time_mode, total_estimated_time, &format_compact_count, &format_percent, &format_distance](libvgcode::EOptionType type, std::vector offsets) {
+ const bool full_layout = offsets.size() > 4;
+ auto option_stats = [this, current_time_mode, total_estimated_time, &format_compact_count, &format_percent, &format_distance, full_layout](libvgcode::EOptionType option_type) -> std::array {
+ libvgcode::EMoveType move_type;
+ bool has_move_type = true;
+ switch (option_type) {
+ case libvgcode::EOptionType::Wipes: { move_type = libvgcode::EMoveType::Wipe; break; }
+ case libvgcode::EOptionType::Retractions: { move_type = libvgcode::EMoveType::Retract; break; }
+ case libvgcode::EOptionType::Unretractions: { move_type = libvgcode::EMoveType::Unretract; break; }
+ case libvgcode::EOptionType::Seams: { move_type = libvgcode::EMoveType::Seam; break; }
+ case libvgcode::EOptionType::ToolChanges: { move_type = libvgcode::EMoveType::ToolChange; break; }
+ default: { has_move_type = false; break; }
+ }
+
+ if (!has_move_type)
+ return { "", "", "", "" };
+
+ const size_t move_type_idx = static_cast(move_type);
+ float time = m_move_type_times[move_type_idx][current_time_mode];
+ if (option_type == libvgcode::EOptionType::ToolChanges) {
+ // Toolchange delays are injected via synchronize() and are not attributed to ToolChange move vertices.
+ time = m_print_statistics.total_filament_load_time + m_print_statistics.total_filament_unload_time + m_print_statistics.total_tool_change_time;
+ }
+ const std::string time_text = full_layout && time > 0.0f ? short_time(get_time_dhms(time)) : "";
+ const std::string percent_text = full_layout && total_estimated_time > 0.0f ? format_percent(time / total_estimated_time) : "";
+ const float seam_distance = m_print_statistics.total_seam_gap_distance + m_print_statistics.total_seam_scarf_distance;
+ const float distance = (option_type == libvgcode::EOptionType::Seams && seam_distance > 0.0f) ? seam_distance : m_move_type_distances[move_type_idx];
+ const std::string distance_text = full_layout && (option_type == libvgcode::EOptionType::Wipes || option_type == libvgcode::EOptionType::Retractions || option_type == libvgcode::EOptionType::Unretractions || option_type == libvgcode::EOptionType::Seams)
+ ? format_distance(distance)
+ : "";
+ const std::string count_text = full_layout ? format_compact_count(m_move_type_counts[move_type_idx]) : "";
+
+ return { time_text, percent_text, distance_text, count_text };
+ };
+
+ auto append_option_item_with_type = [this, offsets, append_item, full_layout](libvgcode::EOptionType type, const ColorRGBA& color, const std::string& label, bool visible,
+ const std::string& time_text, const std::string& percent_text, const std::string& distance_text, const std::string& count_text) {
+ std::vector> columns_offsets;
+ columns_offsets.push_back({ label , offsets[0] });
+ if (full_layout && !time_text.empty())
+ columns_offsets.push_back({ time_text, offsets[1] });
+ if (full_layout && !percent_text.empty())
+ columns_offsets.push_back({ percent_text, offsets[2] });
+ if (full_layout && !distance_text.empty())
+ columns_offsets.push_back({ distance_text, offsets[3] });
+ if (full_layout && !count_text.empty())
+ columns_offsets.push_back({ count_text, distance_text.empty() ? offsets[3] : offsets[4] });
+ append_item(EItemType::Rect, color, columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, type, visible]() {
m_viewer.toggle_option_visibility(type);
update_moves_slider();
});
@@ -3958,18 +4134,33 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
const bool visible = m_viewer.is_option_visible(type);
if (type == libvgcode::EOptionType::Travels) {
//BBS: only display travel time in FeatureType view
- append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), _u8L("Travel"), visible);
+ append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), _u8L("Travel"), visible, "", "", "", "");
+ }
+ else if (type == libvgcode::EOptionType::Seams) {
+ const auto option_values = option_stats(type);
+ append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Seams)), _u8L("Seams"), visible,
+ option_values[0], option_values[1], option_values[2], option_values[3]);
+ }
+ else if (type == libvgcode::EOptionType::Retractions) {
+ const auto option_values = option_stats(type);
+ append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Retractions)), _u8L("Retract"), visible,
+ option_values[0], option_values[1], option_values[2], option_values[3]);
+ }
+ else if (type == libvgcode::EOptionType::Unretractions) {
+ const auto option_values = option_stats(type);
+ append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Unretractions)), _u8L("Unretract"), visible,
+ option_values[0], option_values[1], option_values[2], option_values[3]);
+ }
+ else if (type == libvgcode::EOptionType::ToolChanges) {
+ const auto option_values = option_stats(type);
+ append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::ToolChanges)), _u8L("Filament Changes"), visible,
+ option_values[0], option_values[1], option_values[2], option_values[3]);
+ }
+ else if (type == libvgcode::EOptionType::Wipes) {
+ const auto option_values = option_stats(type);
+ append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Wipes)), _u8L("Wipe"), visible,
+ option_values[0], option_values[1], option_values[2], option_values[3]);
}
- else if (type == libvgcode::EOptionType::Seams)
- append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Seams)), _u8L("Seams"), visible);
- else if (type == libvgcode::EOptionType::Retractions)
- append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Retractions)), _u8L("Retract"), visible);
- else if (type == libvgcode::EOptionType::Unretractions)
- append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Unretractions)), _u8L("Unretract"), visible);
- else if (type == libvgcode::EOptionType::ToolChanges)
- append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::ToolChanges)), _u8L("Filament Changes"), visible);
- else if (type == libvgcode::EOptionType::Wipes)
- append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Wipes)), _u8L("Wipe"), visible);
};
const libvgcode::EViewType new_view_type = curr_view_type;
@@ -4009,6 +4200,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
columns_offsets.push_back({ _u8L("Travel"), offsets[0] });
columns_offsets.push_back({ travel_time, offsets[1] });
columns_offsets.push_back({ travel_percent, offsets[2] });
+ columns_offsets.push_back({ travel_distance, offsets[3] }); // Usage column
+ columns_offsets.push_back({ travel_moves, offsets[4] }); // Usage column
append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, item, visible]() {
m_viewer.toggle_option_visibility(item);
update_moves_slider();
@@ -4100,7 +4293,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
size_t i = 0;
const std::vector& used_extruders_ids = m_viewer.get_used_extruders_ids();
for (uint8_t extruder_id : used_extruders_ids) {
- ::sprintf(buf, imperial_units ? "%.2f in %.2f g" : "%.2f m %.2f g", model_used_filaments_m[i], model_used_filaments_g[i]);
+ const std::string weight_text = format_compact_weight(model_used_filaments_g[i], imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m[i], weight_text.c_str());
append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_tool_colors()[extruder_id]), { { _u8L("Extruder") + " " + std::to_string(extruder_id + 1), offsets[0]}, {buf, offsets[1]} });
// append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_tool_colors()[extruder_id]), _u8L("Extruder") + " " + std::to_string(extruder_id + 1),
// true, "", 0.0f, 0.0f, offsets, used_filaments_m[extruder_id], used_filaments_g[extruder_id]);
@@ -4113,7 +4307,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
char buf[64];
imgui.text(_u8L("Total") + ":");
ImGui::SameLine();
- ::sprintf(buf, imperial_units ? "%.2f in / %.2f oz" : "%.2f m / %.2f g", ps.total_used_filament / koef, ps.total_weight / unit_conver);
+ const std::string total_weight_text = format_compact_weight(ps.total_weight, imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in / %s" : "%.2f m / %s", ps.total_used_filament / koef, total_weight_text.c_str());
imgui.text(buf);
ImGui::Dummy({window_padding, window_padding});
@@ -4159,34 +4354,39 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
float column_sum_m = 0.0f;
float column_sum_g = 0.0f;
if (displayed_columns & ColumnData::Model) {
+ const std::string weight_text = format_compact_weight(model_used_filaments_g[i], imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", model_used_filaments_m[i], model_used_filaments_g[i] / unit_conver);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", model_used_filaments_m[i], weight_text.c_str());
else
- ::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", model_used_filaments_m[i], model_used_filaments_g[i] / unit_conver);
+ ::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Model")] });
column_sum_m += model_used_filaments_m[i];
column_sum_g += model_used_filaments_g[i];
}
if (displayed_columns & ColumnData::Support) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", support_used_filaments_m[i], support_used_filaments_g[i] / unit_conver);
+ const std::string weight_text = format_compact_weight(support_used_filaments_g[i], imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", support_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Support")] });
column_sum_m += support_used_filaments_m[i];
column_sum_g += support_used_filaments_g[i];
}
if (displayed_columns & ColumnData::Flushed) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", flushed_filaments_m[i], flushed_filaments_g[i] / unit_conver);
+ const std::string weight_text = format_compact_weight(flushed_filaments_g[i], imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", flushed_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Flushed")]});
column_sum_m += flushed_filaments_m[i];
column_sum_g += flushed_filaments_g[i];
}
if (displayed_columns & ColumnData::WipeTower) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", wipe_tower_used_filaments_m[i], wipe_tower_used_filaments_g[i] / unit_conver);
+ const std::string weight_text = format_compact_weight(wipe_tower_used_filaments_g[i], imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", wipe_tower_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Tower")] });
column_sum_m += wipe_tower_used_filaments_m[i];
column_sum_g += wipe_tower_used_filaments_g[i];
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", column_sum_m, column_sum_g / unit_conver);
+ const std::string weight_text = format_compact_weight(column_sum_g, imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", column_sum_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Total")] });
}
@@ -4212,27 +4412,32 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector> columns_offsets;
columns_offsets.push_back({ _u8L("Total"), color_print_offsets[_u8L("Filament")]});
if (displayed_columns & ColumnData::Model) {
+ const std::string weight_text = format_compact_weight(total_model_used_filament_g, imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_model_used_filament_m, total_model_used_filament_g / unit_conver);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_model_used_filament_m, weight_text.c_str());
else
- ::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", total_model_used_filament_m, total_model_used_filament_g / unit_conver);
+ ::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", total_model_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Model")] });
}
if (displayed_columns & ColumnData::Support) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_support_used_filament_m, total_support_used_filament_g / unit_conver);
+ const std::string weight_text = format_compact_weight(total_support_used_filament_g, imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_support_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Support")] });
}
if (displayed_columns & ColumnData::Flushed) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_flushed_filament_m, total_flushed_filament_g / unit_conver);
+ const std::string weight_text = format_compact_weight(total_flushed_filament_g, imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_flushed_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Flushed")] });
}
if (displayed_columns & ColumnData::WipeTower) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_wipe_tower_used_filament_m, total_wipe_tower_used_filament_g / unit_conver);
+ const std::string weight_text = format_compact_weight(total_wipe_tower_used_filament_g, imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_wipe_tower_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Tower")] });
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
- ::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_model_used_filament_m + total_support_used_filament_m + total_flushed_filament_m + total_wipe_tower_used_filament_m,
- (total_model_used_filament_g + total_support_used_filament_g + total_flushed_filament_g + total_wipe_tower_used_filament_g) / unit_conver);
+ const std::string weight_text = format_compact_weight(total_model_used_filament_g + total_support_used_filament_g + total_flushed_filament_g + total_wipe_tower_used_filament_g, imperial_units);
+ ::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_model_used_filament_m + total_support_used_filament_m + total_flushed_filament_m + total_wipe_tower_used_filament_m,
+ weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Total")] });
}
append_item(EItemType::None, libvgcode::convert(tool_colors[0]), columns_offsets);
@@ -4243,16 +4448,14 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
ImGui::SameLine();
imgui.text(_u8L("Filament change times") + ":");
ImGui::SameLine();
- ::sprintf(buf, "%d", m_print_statistics.total_filament_changes);
- imgui.text(buf);
+ imgui.text(format_compact_count(m_print_statistics.total_filament_changes));
//display tool change times
ImGui::Dummy({window_padding, window_padding});
ImGui::SameLine();
imgui.text(_u8L("Tool changes") + ":");
ImGui::SameLine();
- ::sprintf(buf, "%d", m_print_statistics.total_extruder_changes);
- imgui.text(buf);
+ imgui.text(format_compact_count(m_print_statistics.total_extruder_changes));
//BBS display cost
ImGui::Dummy({ window_padding, window_padding });
@@ -4379,8 +4582,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
imgui.text(buffer);
ImGui::SameLine(offsets[3]);
- ::sprintf(buffer, "%.2f g", used_filament.second);
- imgui.text(buffer);
+ imgui.text(format_compact_weight(used_filament.second, imperial_units));
}
};
@@ -4705,8 +4907,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf, imperial_units ? "%.2f in" : "%.2f m", ps.total_used_filament / koef);
imgui.text(buf);
ImGui::SameLine();
- ::sprintf(buf, imperial_units ? " %.2f oz" : " %.2f g", ps.total_weight / unit_conver);
- imgui.text(buf);
+ imgui.text(" " + format_compact_weight(ps.total_weight, imperial_units));
ImGui::Dummy({ window_padding, window_padding });
ImGui::SameLine();
imgui.text(model_filament_str + ":");
@@ -4716,8 +4917,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf, imperial_units ? "%.2f in" : "%.2f m", ps.total_used_filament / koef - exlude_m);
imgui.text(buf);
ImGui::SameLine();
- ::sprintf(buf, imperial_units ? " %.2f oz" : " %.2f g", (ps.total_weight - exlude_g) / unit_conver);
- imgui.text(buf);
+ imgui.text(" " + format_compact_weight(ps.total_weight - exlude_g, imperial_units));
//BBS: display cost of filaments
ImGui::Dummy({ window_padding, window_padding });
ImGui::SameLine();
diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp
index 3300d33e0d..a73a30525d 100644
--- a/src/slic3r/GUI/GCodeViewer.hpp
+++ b/src/slic3r/GUI/GCodeViewer.hpp
@@ -14,6 +14,7 @@
// needed for tech VGCODE_ENABLE_COG_AND_TOOL_MARKERS
#include
+#include
#include
#include
#include
@@ -186,6 +187,9 @@ private:
unsigned int m_last_result_id{ 0 };
//BBS: save m_gcode_result as well
const GCodeProcessorResult* m_gcode_result;
+ std::array(EMoveType::Count)> m_move_type_counts{};
+ std::array(PrintEstimatedStatistics::ETimeMode::Count)>, static_cast(EMoveType::Count)> m_move_type_times{};
+ std::array(EMoveType::Count)> m_move_type_distances{};
//BBS: add only gcode mode
bool m_only_gcode_in_preview {false};
diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp
index 0a18cdeb09..e3b2daf282 100644
--- a/src/slic3r/GUI/GLCanvas3D.cpp
+++ b/src/slic3r/GUI/GLCanvas3D.cpp
@@ -14,6 +14,7 @@
#include "libslic3r/Technologies.hpp"
#include "libslic3r/Tesselate.hpp"
#include "libslic3r/PresetBundle.hpp"
+#include "libslic3r/AppConfig.hpp"
#include "3DScene.hpp"
#include "BackgroundSlicingProcess.hpp"
#include "GLShader.hpp"
@@ -225,7 +226,7 @@ bool GLCanvas3D::LayersEditing::is_allowed() const
float GLCanvas3D::LayersEditing::s_overlay_window_width;
-void GLCanvas3D::LayersEditing::render_variable_layer_height_dialog(const GLCanvas3D& canvas) {
+void GLCanvas3D::LayersEditing::render_variable_layer_height_dialog(GLCanvas3D& canvas) {
if (!m_enabled)
return;
@@ -335,8 +336,23 @@ void GLCanvas3D::LayersEditing::render_variable_layer_height_dialog(const GLCanv
GLGizmoUtils::render_tooltip_button(&imgui, canvas, shortcuts, x, y);
ImGui::SameLine();
- if (imgui.button(_L("Reset")))
- wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), SimpleEvent(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE));
+ imgui.disabled_begin(check_object_layers_fixed(*m_slicing_parameters, m_layer_height_profile));
+ if (imgui.button(_L("Reset"))) {
+ wxPostEvent((wxEvtHandler*) canvas.get_wxglcanvas(), SimpleEvent(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE));
+ }
+ imgui.disabled_end();
+
+ ImGui::SameLine();
+ GLGizmoUtils::begin_right_aligned_buttons({_L("Done")});
+ if (imgui.button(_L("Done"))) {
+ m_enabled = false;
+
+ GLToolbarItem* item = canvas.m_main_toolbar.get_item("layersediting");
+ item->set_state(GLToolbarItem::Normal);
+
+ canvas.set_as_dirty();
+ canvas.request_extra_frame();
+ }
GLCanvas3D::LayersEditing::s_overlay_window_width = ImGui::GetWindowSize().x;
imgui.end();
@@ -344,7 +360,7 @@ void GLCanvas3D::LayersEditing::render_variable_layer_height_dialog(const GLCanv
imgui.pop_toolbar_style();
}
-void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas)
+void GLCanvas3D::LayersEditing::render_overlay(GLCanvas3D& canvas)
{
render_variable_layer_height_dialog(canvas);
render_active_object_annotations(canvas);
@@ -1202,6 +1218,11 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
GLCanvas3D::~GLCanvas3D()
{
+ if (m_fxaa_texture_id != 0 && _set_current()) {
+ glsafe(::glDeleteTextures(1, &m_fxaa_texture_id));
+ m_fxaa_texture_id = 0;
+ }
+
reset_volumes();
m_sel_plate_toolbar.del_all_item();
@@ -2082,9 +2103,16 @@ void GLCanvas3D::render(bool only_init)
if (m_picking_enabled && m_rectangle_selection.is_dragging())
m_rectangle_selection.render(*this);
+ if (_is_fxaa_enabled())
+ _render_fxaa_pass(static_cast(cnv_size.get_width()), static_cast(cnv_size.get_height()));
+
// draw overlays
_render_overlays();
+ const int current_fps = m_render_stats.get_fps_and_reset_if_needed();
+ if (_is_fps_overlay_enabled())
+ _render_fps_overlay(current_fps);
+
if (wxGetApp().plater()->is_render_statistic_dialog_visible()) {
ImGui::ShowMetricsWindow();
@@ -2092,7 +2120,7 @@ void GLCanvas3D::render(bool only_init)
imgui.begin(std::string("Render statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
imgui.text("FPS (SwapBuffers() calls per second):");
ImGui::SameLine();
- imgui.text(std::to_string(m_render_stats.get_fps_and_reset_if_needed()));
+ imgui.text(std::to_string(current_fps));
ImGui::Separator();
imgui.text("Compressed textures:");
ImGui::SameLine();
@@ -3202,6 +3230,22 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
wxGetApp().imgui()->reset_requires_extra_frame();
#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
+ const int fps_cap = _get_effective_fps_cap();
+ if (fps_cap > 0) {
+ const auto now = std::chrono::steady_clock::now();
+ const auto min_frame_time = std::chrono::duration(1.0 / static_cast(fps_cap));
+ const auto elapsed = now - m_last_frame_start_time;
+ if (elapsed < min_frame_time) {
+ const int wait_ms = std::max(1, static_cast(std::ceil(std::chrono::duration(min_frame_time - elapsed).count())));
+ schedule_extra_frame(wait_ms);
+ evt.RequestMore();
+ return;
+ }
+
+ // Pace by frame-start interval so rendering time is part of the target budget.
+ m_last_frame_start_time = now;
+ }
+
_refresh_if_shown_on_screen();
#if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
@@ -4251,7 +4295,11 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
}
bool any_gizmo_active = m_gizmos.get_current() != nullptr;
- bool swap_mouse_buttons = wxGetApp().app_config->get_bool("swap_mouse_buttons");
+
+ std::map button_mappings;
+ button_mappings[MouseButton::Left] = static_cast(std::atoi(wxGetApp().app_config->get("left_mouse_drag_action").c_str()));
+ button_mappings[MouseButton::Middle] = static_cast(std::atoi(wxGetApp().app_config->get("middle_mouse_drag_action").c_str()));
+ button_mappings[MouseButton::Right] = static_cast(std::atoi(wxGetApp().app_config->get("right_mouse_drag_action").c_str()));
if (m_mouse.drag.move_requires_threshold && m_mouse.is_move_start_threshold_position_2D_defined() && m_mouse.is_move_threshold_met(pos)) {
m_mouse.drag.move_requires_threshold = false;
@@ -4466,7 +4514,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
m_dirty = true;
}
}
- else if (evt.Dragging() || is_camera_rotate(evt, swap_mouse_buttons) || is_camera_pan(evt, swap_mouse_buttons)) {
+ else if (evt.Dragging() || is_camera_rotate(evt, button_mappings) || is_camera_pan(evt, button_mappings)) {
m_mouse.dragging = true;
if (m_layers_editing.state != LayersEditing::Unknown && layer_editing_object_idx != -1) {
@@ -4476,10 +4524,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
}
}
// do not process the dragging if the left mouse was set down in another canvas
- else if (is_camera_rotate(evt, swap_mouse_buttons)) {
+ else if (is_camera_rotate(evt, button_mappings)) {
// Orca: Sphere rotation for painting view
- // if dragging over blank area with left button or button functions swapped then rotate
- if ((any_gizmo_active || swap_mouse_buttons || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) {
+ // if dragging over blank area with left button or other button mapped to rotate, then rotate
+ bool middle_or_right_button_used_as_rotate = (evt.MiddleIsDown() && button_mappings[MouseButton::Middle] == MouseAction::Rotation) ||
+ (evt.RightIsDown() && button_mappings[MouseButton::Right] == MouseAction::Rotation);
+ if ((any_gizmo_active || middle_or_right_button_used_as_rotate || m_hover_volume_idxs.empty()) &&
+ m_mouse.is_start_position_3D_defined()) {
Camera& camera = wxGetApp().plater()->get_camera();
auto mult_pref = wxGetApp().app_config->get("camera_orbit_mult");
const double mult = mult_pref.empty() ? 1.0 : std::stod(mult_pref);
@@ -4552,7 +4603,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
m_camera_movement = true;
m_mouse.drag.start_position_3D = Vec3d((double)pos(0), (double)pos(1), 0.0);
}
- else if (is_camera_pan(evt, swap_mouse_buttons)) {
+ else if (is_camera_pan(evt, button_mappings)) {
// if dragging with right button or if button functions swapped and dragging with left button over blank area then pan
if (m_mouse.is_start_position_2D_defined()) {
// get point in model space at Z = 0
@@ -4580,10 +4631,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
}
}
else if ((evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) ||
- (m_camera_movement && !is_camera_rotate(evt, swap_mouse_buttons) && !is_camera_pan(evt, swap_mouse_buttons))) {
+ (m_camera_movement && !is_camera_rotate(evt, button_mappings) && !is_camera_pan(evt, button_mappings))) {
m_mouse.position = pos.cast();
- if (swap_mouse_buttons ? evt.RightUp() : evt.LeftUp()) {
+ // Check if the button that was released is mapped to rotation
+ if ((evt.LeftUp() && button_mappings[MouseButton::Left] == MouseAction::Rotation) ||
+ (evt.MiddleUp() && button_mappings[MouseButton::Middle] == MouseAction::Rotation) ||
+ (evt.RightUp() && button_mappings[MouseButton::Right] == MouseAction::Rotation)) {
m_rotation_center(0) = m_rotation_center(1) = m_rotation_center(2) = 0.f;
}
@@ -4784,21 +4838,42 @@ void GLCanvas3D::on_set_focus(wxFocusEvent& evt)
m_is_touchpad_navigation = wxGetApp().app_config->get_bool("camera_navigation_style");
}
-bool GLCanvas3D::is_camera_rotate(const wxMouseEvent& evt, const bool buttonsSwapped) const
+bool GLCanvas3D::clicked_button_matches_action(const wxMouseEvent& evt, const MouseAction action, const std::map& mappings) const
+{
+ MouseButton clicked = MouseButton::None;
+ if (evt.LeftIsDown()) {
+ clicked = MouseButton::Left;
+ }
+ if (evt.MiddleIsDown()) {
+ clicked = MouseButton::Middle;
+ }
+ if (evt.RightIsDown()) {
+ clicked = MouseButton::Right;
+ }
+
+ auto it = mappings.find(clicked);
+ if (it == mappings.end()) {
+ return false;
+ }
+ return it->second == action;
+}
+
+bool GLCanvas3D::is_camera_rotate(const wxMouseEvent& evt, const std::map& mappings) const
{
if (m_is_touchpad_navigation) {
return evt.Moving() && evt.AltDown() && !evt.ShiftDown();
} else {
- return evt.Dragging() && (buttonsSwapped ? evt.RightIsDown() : evt.LeftIsDown());
+ return evt.Dragging() && clicked_button_matches_action(evt, MouseAction::Rotation, mappings);
}
}
-bool GLCanvas3D::is_camera_pan(const wxMouseEvent& evt, const bool buttonsSwapped) const
+bool GLCanvas3D::is_camera_pan(const wxMouseEvent& evt, const std::map& mappings) const
{
if (m_is_touchpad_navigation) {
return evt.Moving() && evt.ShiftDown() && !evt.AltDown();
} else {
- return evt.Dragging() && (evt.MiddleIsDown() || (buttonsSwapped ? evt.LeftIsDown() : evt.RightIsDown()));
+ return evt.Dragging() && clicked_button_matches_action(evt, MouseAction::Pan, mappings);
+ ;
}
}
@@ -7621,6 +7696,105 @@ void GLCanvas3D::_rectangular_selection_picking_pass()
_update_volumes_hover_state();
}
+bool GLCanvas3D::_is_fxaa_enabled() const
+{
+ return wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_FXAA_ENABLED);
+}
+
+int GLCanvas3D::_get_effective_fps_cap() const
+{
+ if (wxGetApp().app_config == nullptr)
+ return 0;
+
+ int fps_cap = 0;
+ try {
+ fps_cap = std::stoi(wxGetApp().app_config->get(SETTING_OPENGL_FPS_CAP));
+ }
+ catch (...) {
+ fps_cap = 0;
+ }
+
+ fps_cap = std::max(0, std::min(fps_cap, 240));
+
+ return fps_cap;
+}
+
+bool GLCanvas3D::_is_fps_overlay_enabled() const
+{
+ return wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY);
+}
+
+void GLCanvas3D::_render_fps_overlay(int fps) const
+{
+ if (fps < 0)
+ return;
+
+ ImGuiWrapper& imgui = *wxGetApp().imgui();
+ const float margin = 10.0f * get_scale();
+ const ImVec2 display_size = ImGui::GetIO().DisplaySize;
+ ImGui::SetNextWindowPos(ImVec2(display_size.x - margin, margin), ImGuiCond_Always, ImVec2(1.0f, 0.0f));
+ ImGui::SetNextWindowBgAlpha(0.35f);
+ imgui.begin(
+ std::string("###fps_overlay"),
+ ImGuiWindowFlags_AlwaysAutoResize |
+ ImGuiWindowFlags_NoResize |
+ ImGuiWindowFlags_NoCollapse |
+ ImGuiWindowFlags_NoTitleBar |
+ ImGuiWindowFlags_NoMove |
+ ImGuiWindowFlags_NoSavedSettings |
+ ImGuiWindowFlags_NoInputs);
+ imgui.text(std::string("FPS: ") + std::to_string(fps));
+ imgui.end();
+}
+
+void GLCanvas3D::_render_fxaa_pass(unsigned int width, unsigned int height)
+{
+ if (width == 0 || height == 0)
+ return;
+
+ GLShaderProgram* shader = wxGetApp().get_shader("fxaa");
+ if (shader == nullptr)
+ return;
+
+ if (m_fxaa_texture_id == 0) {
+ glsafe(::glGenTextures(1, &m_fxaa_texture_id));
+ glsafe(::glBindTexture(GL_TEXTURE_2D, m_fxaa_texture_id));
+ glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
+ glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
+ glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
+ glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
+ glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
+ }
+
+ glsafe(::glBindTexture(GL_TEXTURE_2D, m_fxaa_texture_id));
+ if (m_fxaa_texture_size[0] != width || m_fxaa_texture_size[1] != height) {
+ glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
+ m_fxaa_texture_size = { width, height };
+ }
+
+ glsafe(::glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width, height));
+
+ glsafe(::glDisable(GL_DEPTH_TEST));
+ glsafe(::glDisable(GL_BLEND));
+ glsafe(::glClear(GL_COLOR_BUFFER_BIT));
+
+ shader->start_using();
+ shader->set_uniform("view_model_matrix", Transform3d::Identity());
+ shader->set_uniform("projection_matrix", Transform3d::Identity());
+ shader->set_uniform("uniform_texture", 0);
+ shader->set_uniform("inv_tex_size", Vec2f(1.0f / static_cast(width), 1.0f / static_cast(height)));
+
+ glsafe(::glActiveTexture(GL_TEXTURE0));
+ glsafe(::glBindTexture(GL_TEXTURE_2D, m_fxaa_texture_id));
+ m_background.render();
+ glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
+ shader->stop_using();
+
+ glsafe(::glEnable(GL_DEPTH_TEST));
+ glsafe(::glEnable(GL_BLEND));
+ glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
+}
+
void GLCanvas3D::_render_background()
{
bool use_error_color = false;
diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp
index 2224c3a8aa..12d8024fc5 100644
--- a/src/slic3r/GUI/GLCanvas3D.hpp
+++ b/src/slic3r/GUI/GLCanvas3D.hpp
@@ -290,8 +290,8 @@ class GLCanvas3D
bool is_enabled() const { return m_enabled; }
void set_enabled(bool enabled) { m_enabled = is_allowed() && enabled; }
- void render_variable_layer_height_dialog(const GLCanvas3D& canvas);
- void render_overlay(const GLCanvas3D& canvas);
+ void render_variable_layer_height_dialog(GLCanvas3D& canvas);
+ void render_overlay(GLCanvas3D& canvas);
void render_volumes(const GLCanvas3D& canvas, const GLVolumeCollection& volumes);
void adjust_layer_height_profile();
@@ -610,6 +610,7 @@ private:
bool m_reload_delayed;
RenderStats m_render_stats;
+ std::chrono::time_point m_last_frame_start_time{ std::chrono::steady_clock::now() };
int m_imgui_undo_redo_hovered_pos{ -1 };
int m_mouse_wheel{ 0 };
@@ -727,6 +728,8 @@ public:
CameraTarget m_camera_target;
#endif // ENABLE_SHOW_CAMERA_TARGET
GLModel m_background;
+ unsigned int m_fxaa_texture_id{ 0 };
+ std::array m_fxaa_texture_size{ 0, 0 };
public:
explicit GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed);
~GLCanvas3D();
@@ -1038,8 +1041,11 @@ public:
void on_set_focus(wxFocusEvent& evt);
void force_set_focus();
- bool is_camera_rotate(const wxMouseEvent& evt, const bool buttonsSwapped) const;
- bool is_camera_pan(const wxMouseEvent& evt, const bool buttonsSwapped) const;
+ enum class MouseButton { None, Left, Middle, Right };
+ enum class MouseAction { None, Pan, Rotation };
+ bool clicked_button_matches_action(const wxMouseEvent& evt, MouseAction action, const std::map& mappings) const;
+ bool is_camera_rotate(const wxMouseEvent& evt, const std::map& mappings) const;
+ bool is_camera_pan(const wxMouseEvent& evt, const std::map& mappings) const;
Size get_canvas_size() const;
Vec2d get_local_mouse_position() const;
@@ -1238,6 +1244,11 @@ private:
void _rectangular_selection_picking_pass();
// IMEX ghost picking (ray vs. ghost bbox). Runs at the end of _picking_pass.
void _picking_pass_imex_ghosts();
+ bool _is_fxaa_enabled() const;
+ int _get_effective_fps_cap() const;
+ bool _is_fps_overlay_enabled() const;
+ void _render_fps_overlay(int fps) const;
+ void _render_fxaa_pass(unsigned int width, unsigned int height);
void _render_background();
void _render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes);
//BBS: add part plate related logic
diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp
index 7f28d8f777..620fc2e773 100644
--- a/src/slic3r/GUI/GLShadersManager.cpp
+++ b/src/slic3r/GUI/GLShadersManager.cpp
@@ -48,6 +48,8 @@ std::pair GLShadersManager::init()
valid &= append_shader("flat_clip", { prefix + "flat_clip.vs", prefix + "flat_clip.fs" });
// basic shader for textures, used to render textures
valid &= append_shader("flat_texture", { prefix + "flat_texture.vs", prefix + "flat_texture.fs" });
+ // used to apply post-processing antialiasing in screen space
+ valid &= append_shader("fxaa", { prefix + "fxaa.vs", prefix + "fxaa.fs" });
// used to render 3D scene background
valid &= append_shader("background", { prefix + "background.vs", prefix + "background.fs" });
#if SLIC3R_OPENGL_ES
diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp
index 7bf4d008d4..d670181b1b 100644
--- a/src/slic3r/GUI/GLTexture.cpp
+++ b/src/slic3r/GUI/GLTexture.cpp
@@ -663,7 +663,9 @@ void GLTexture::render_texture(unsigned int tex_id, float left, float right, flo
void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right, float bottom, float top, const GLTexture::Quad_UVs& uvs)
{
glsafe(::glEnable(GL_BLEND));
- glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
+ // Orca: fix washed-out toolbar icons on Wayland: keep destination alpha at 1.0 so the compositor
+ // does not treat anti-aliased icon edges as window transparency.
+ glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
glsafe(::glEnable(GL_TEXTURE_2D));
glsafe(::glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE));
diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index c12fa964f8..6b30dfa9cd 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -11,6 +11,7 @@
#include "Downloader.hpp"
#include
#include
+#include
#include
// Localization headers: include libslic3r version first so everything in this file
@@ -100,6 +101,9 @@
#include "Mouse3DController.hpp"
#include "RemovableDriveManager.hpp"
#include "InstanceCheck.hpp"
+#ifdef __APPLE__
+#include "DeepLinkHandlerMac.h"
+#endif
#include "NotificationManager.hpp"
#include "UnsavedChangesDialog.hpp"
#include "SavePresetDialog.hpp"
@@ -783,9 +787,20 @@ void GUI_App::post_init()
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
plater_->select_view_3D("3D");
//BBS init the opengl resource here
-//#ifdef __linux__
- if (plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen()&&plater_->canvas3D()->make_current_for_postinit()) {
-//#endif
+ if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
+ !plater_->canvas3D()->make_current_for_postinit()) {
+ BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": glcontext not ready, postpone init";
+ plater_->canvas3D()->enable_render(true);
+ plater_->canvas3D()->set_as_dirty();
+#ifdef __linux__
+ // Wayland/EGL may not have committed the GL surface yet; ask the
+ // idle loop to retry post_init when the canvas is actually mapped.
+ // Without this, GL function pointers stay null and the first
+ // Preview focus crashes in Camera::apply_viewport.
+ m_post_initialized = false;
+ return;
+#endif
+ } else {
Size canvas_size = plater_->canvas3D()->get_canvas_size();
wxGetApp().imgui()->set_display_size(static_cast(canvas_size.get_width()), static_cast(canvas_size.get_height()));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", start to init opengl";
@@ -805,14 +820,7 @@ void GUI_App::post_init()
plater_->canvas3D()->render(false);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished rendering a first frame for test";
}
-//#ifdef __linux__
}
- else {
- BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "Found glcontext not ready, postpone the init";
- plater_->canvas3D()->enable_render(true);
- plater_->canvas3D()->set_as_dirty();
- }
-//#endif
if (is_editor())
mainframe->select_tab(size_t(0));
if (app_config->get("default_page") == "1")
@@ -1900,6 +1908,7 @@ void GUI_App::init_networking_callbacks()
}
if (return_code == 5) {
GUI::wxGetApp().CallAfter([this, provider = event.provider] {
+ BOOST_LOG_TRIVIAL(info) << "logout: login expired";
this->request_user_logout(provider);
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
if (msg_dlg.ShowModal() == wxOK) {
@@ -2546,6 +2555,12 @@ std::string get_system_info()
bool GUI_App::on_init_inner()
{
wxLog::SetActiveTarget(new wxBoostLog());
+
+#ifdef __APPLE__
+ // Override wxWidgets' kAEGetURL handler so orcaslicer:// deep links keep
+ // working after the wxWidgets 3.3.2 upgrade on macOS (#13119).
+ register_mac_deep_link_handler();
+#endif
#if BBL_RELEASE_TO_PUBLIC
wxLog::SetLogLevel(wxLOG_Message);
#endif
@@ -4526,7 +4541,8 @@ std::string GUI_App::handle_web_request(std::string cmd)
}
else if (command_str.compare("homepage_logout") == 0) {
CallAfter([this] {
- wxGetApp().request_user_logout();
+ BOOST_LOG_TRIVIAL(info) << "logout: homepage_logout";
+ request_user_logout();
});
}
else if (command_str.compare("get_orca_login_info") == 0) {
@@ -4539,18 +4555,22 @@ std::string GUI_App::handle_web_request(std::string cmd)
CallAfter([this] { request_login(true, BBL_CLOUD_PROVIDER); });
}
else if (command_str.compare("homepage_bambu_logout") == 0) {
- CallAfter([this] { request_user_logout(BBL_CLOUD_PROVIDER); });
+ CallAfter([this] {
+ BOOST_LOG_TRIVIAL(info) << "logout: homepage_bambu_logout";
+ request_user_logout(BBL_CLOUD_PROVIDER);
+ });
}
else if (command_str.compare("homepage_orca_login_or_register") == 0) {
CallAfter([this] { request_login(true, ORCA_CLOUD_PROVIDER); });
}
else if (command_str.compare("homepage_orca_logout") == 0) {
- CallAfter([this] { request_user_logout(ORCA_CLOUD_PROVIDER); });
+ CallAfter([this] {
+ BOOST_LOG_TRIVIAL(info) << "logout: homepage_orca_logout";
+ request_user_logout(ORCA_CLOUD_PROVIDER);
+ });
}
else if (command_str.compare("homepage_modeldepot") == 0) {
- CallAfter([this] {
- wxGetApp().open_mall_page_dialog();
- });
+ CallAfter([this] { open_mall_page_dialog(); });
}
else if (command_str.compare("homepage_newproject") == 0) {
this->request_open_project("");
@@ -4783,7 +4803,7 @@ void GUI_App::handle_http_error(unsigned int status, std::string body, const std
void GUI_App::on_http_error(wxCommandEvent &evt)
{
int status = evt.GetInt();
- std::string provider = ORCA_CLOUD_PROVIDER;
+ std::string provider = "";
std::string body_str;
// Extract provider and body from event data
@@ -4825,22 +4845,63 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
// request login
if (status == 401) {
if (m_agent) {
- if (m_agent->is_user_login(provider)) {
- this->request_user_logout(provider);
+ if (!provider.empty() && m_agent->is_user_login(provider)) {
+ if (std::chrono::steady_clock::now() - m_last_401_error_time > 30s) {
+ BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
+ this->request_user_logout(provider);
- if (!m_show_http_errpr_msgdlg) {
- MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
- m_show_http_errpr_msgdlg = true;
- auto modal_result = msg_dlg.ShowModal();
- if (modal_result == wxOK || modal_result == wxCLOSE) {
- m_show_http_errpr_msgdlg = false;
- return;
+ if (!m_show_http_error_msgdlg) {
+ MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
+ m_show_http_error_msgdlg = true;
+ auto modal_result = msg_dlg.ShowModal();
+ if (modal_result == wxOK || modal_result == wxCLOSE) {
+ m_show_http_error_msgdlg = false;
+ return;
+ }
}
+
+ m_last_401_error_time = std::chrono::steady_clock::now();
+ } else {
+ BOOST_LOG_TRIVIAL(warning) << "401 encountered within grace period, suppressing logout";
}
}
}
return;
}
+
+ // No need to show dialog for 410: 410 means resource has been deleted from the server.
+ if (status == 410) {
+ BOOST_LOG_TRIVIAL(info) << "Http error 410.";
+ return;
+ }
+
+ static bool m_is_error_shown = false;
+ // Show general error notification for Orca Cloud API failures (not Bambu)
+ if (provider == ORCA_CLOUD_PROVIDER && status >= 400 && code != HttpErrorVersionLimited) {
+ wxString msg;
+ if (!error.empty()) {
+ msg = wxString::Format(_L("Failed to connect to OrcaCloud.\nPlease check your network connectivity\n(HTTP %u): %s"), status, wxString::FromUTF8(error));
+ } else {
+ msg = wxString::Format(_L("Failed to connect to OrcaCloud.\nPlease check your network connectivity\n(HTTP %u)"), status);
+ }
+
+ if (app_config->get_bool("developer_mode")) {
+ // Use notification manager if ImGui is ready; fall back to wxMessageBox on Linux
+ // where ImGui may not be initialized until the user switches to the Prepare tab.
+ if (wxGetApp().plater() != nullptr && wxGetApp().imgui()->display_initialized()) {
+ wxGetApp()
+ .plater()
+ ->get_notification_manager()
+ ->push_notification(NotificationType::PlaterError, NotificationManager::NotificationLevel::WarningNotificationLevel,
+ msg.ToUTF8().data());
+ }
+ }
+
+ if (!m_is_error_shown) {
+ m_is_error_shown = true;
+ wxMessageBox(msg, _L("Cloud Error"), wxOK | wxICON_ERROR, wxGetApp().mainframe);
+ }
+ }
}
void GUI_App::enable_user_preset_folder(bool enable)
@@ -4876,6 +4937,10 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
std::string provider = evt.GetString().ToStdString();
if (provider.empty()) provider = ORCA_CLOUD_PROVIDER;
+ // Reset 401 grace period so transient token-propagation 401s
+ // during login warmup don't trigger immediate logout.
+ m_last_401_error_time = std::chrono::steady_clock::now();
+
m_agent->connect_server();
// get machine list
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
@@ -5599,6 +5664,7 @@ void GUI_App::show_check_privacy_dlg(wxCommandEvent& evt)
privacy_dlg.Bind(EVT_PRIVACY_UPDATE_CANCEL, [this, provider](wxCommandEvent &e) {
app_config->set_bool("privacy_update_checked", false);
if (m_agent) {
+ BOOST_LOG_TRIVIAL(info) << "logout: Privacy update dialog cancelled.";
m_agent->user_logout(false, provider);
post_logout_to_webview(provider);
}
@@ -5785,18 +5851,32 @@ void GUI_App::reload_settings()
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size();
- // Check the user presets for any system vendors that need to be installed
- for (auto data : user_presets) {
- if (!check_preset_parent_available(data))
- add_pending_vendor_preset(data);
- }
- load_pending_vendors();
- preset_bundle->load_user_presets(*app_config, user_presets, ForwardCompatibilitySubstitutionRule::Enable);
- preset_bundle->save_user_presets(*app_config, get_delete_cache_presets());
- if (is_main_thread_active())
+ auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable {
+ if (is_closing() || !preset_bundle || !app_config || !mainframe)
+ return;
+
+ // Check the user presets for any system vendors that need to be installed
+ for (auto data : user_presets) {
+ if (!check_preset_parent_available(data))
+ add_pending_vendor_preset(data);
+ }
+ load_pending_vendors();
+ preset_bundle->load_user_presets(*app_config, user_presets, ForwardCompatibilitySubstitutionRule::Enable);
+ preset_bundle->save_user_presets(*app_config, get_delete_cache_presets());
+
+ // Orca: settings changed, refresh ui to reflect the new preset values
mainframe->update_side_preset_ui();
+ for (auto tab : tabs_list) {
+ tab->reload_config();
+ tab->update_changed_ui();
+ }
+ if (plater_)
+ plater_->sidebar().update_all_preset_comboboxes();
+ };
+ if (is_main_thread_active())
+ refresh_synced_ui();
else
- CallAfter([this] { mainframe->update_side_preset_ui(); });
+ CallAfter(refresh_synced_ui);
}
}
@@ -6521,8 +6601,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
dlg->Update(percent, _L("Loading user preset"));
});
};
- cancelFn = [this, dlg]() {
- return is_closing() || dlg->WasCanceled();
+ cancelFn = [this, dlg, t = std::weak_ptr(m_user_sync_token)]() {
+ return is_closing() || dlg->WasCanceled() || t.expired();
};
finishFn = [this, dlg](bool) {
CallAfter([=]{ dlg->Destroy(); });
@@ -6530,8 +6610,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
}
else {
finishFn = [](bool) {}; // reload_settings() is now triggered from the background thread
- cancelFn = [this]() {
- return is_closing();
+ cancelFn = [this, t = std::weak_ptr(m_user_sync_token)]() {
+ return is_closing() || t.expired();
};
}
@@ -6763,9 +6843,41 @@ void GUI_App::stop_sync_user_preset()
}
}
+void GUI_App::restart_sync_user_preset()
+{
+ if (!m_user_sync_token) {
+ // No sync running. If a restart helper is already in flight it will
+ // start the new sync once the old thread is joined — don't race it.
+ if (!m_restart_sync_pending)
+ start_sync_user_preset(true);
+ return;
+ }
+
+ // Resetting the token signals the old thread to stop (cancelFn checks
+ // t.expired(), so it exits after its current HTTP request completes).
+ // A helper thread joins the old thread off the UI thread — no freeze —
+ // then starts the new sync via CallAfter once the old one is fully done.
+ m_user_sync_token.reset();
+ m_restart_sync_pending = true;
+
+ auto old_thread = std::move(m_sync_update_thread);
+
+ std::thread([this, old_thread = std::move(old_thread)]() mutable {
+ if (old_thread.joinable())
+ old_thread.join();
+ m_restart_sync_pending = false;
+ if (!is_closing())
+ CallAfter([this]() {
+ if (!is_closing())
+ start_sync_user_preset(true);
+ });
+ }).detach();
+}
+
void GUI_App::on_stealth_mode_enter()
{
stop_sync_user_preset();
+ BOOST_LOG_TRIVIAL(info) << "logout: on_stealth_mode_enter";
request_user_logout(ORCA_CLOUD_PROVIDER);
request_user_logout(BBL_CLOUD_PROVIDER);
if (mainframe && mainframe->m_webview) {
@@ -7553,6 +7665,13 @@ void GUI_App::open_exportpresetbundledialog(size_t open_on_tab, const std::strin
void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_option)
{
+ static constexpr const char* opengl_fxaa_setting_key = "opengl_fxaa_enabled";
+ static constexpr const char* opengl_fps_cap_setting_key = "opengl_fps_cap";
+ static constexpr const char* opengl_show_fps_overlay_setting_key = "opengl_show_fps_overlay";
+ const std::string previous_opengl_fxaa = app_config->get(opengl_fxaa_setting_key);
+ const std::string previous_opengl_fps_cap = app_config->get(opengl_fps_cap_setting_key);
+ const std::string previous_opengl_show_fps_overlay = app_config->get(opengl_show_fps_overlay_setting_key);
+
bool need_recreate_gui = false;
std::string pending_language;
{
@@ -7591,6 +7710,14 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_
}
}
+ const bool opengl_fxaa_changed = app_config->get(opengl_fxaa_setting_key) != previous_opengl_fxaa;
+ const bool opengl_fps_cap_changed = app_config->get(opengl_fps_cap_setting_key) != previous_opengl_fps_cap;
+ const bool opengl_show_fps_overlay_changed = app_config->get(opengl_show_fps_overlay_setting_key) != previous_opengl_show_fps_overlay;
+ if ((opengl_fxaa_changed || opengl_fps_cap_changed || opengl_show_fps_overlay_changed) && !need_recreate_gui && this->plater_ != nullptr) {
+ this->plater_->set_current_canvas_as_dirty();
+ this->plater_->get_current_canvas3D()->force_set_focus();
+ }
+
if (!pending_language.empty()) {
const std::string previous_language = app_config->get("language");
app_config->set("language", pending_language);
diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp
index fac853e65c..5b6bcc6581 100644
--- a/src/slic3r/GUI/GUI_App.hpp
+++ b/src/slic3r/GUI/GUI_App.hpp
@@ -321,10 +321,12 @@ private:
boost::thread m_sync_update_thread;
std::shared_ptr m_user_sync_token;
+ std::atomic m_restart_sync_pending {false};
bool m_is_dark_mode{ false };
bool m_adding_script_handler { false };
bool m_side_popup_status{false};
- bool m_show_http_errpr_msgdlg{false};
+ bool m_show_http_error_msgdlg{false};
+ std::chrono::steady_clock::time_point m_last_401_error_time;
bool m_show_error_msgdlg{false};
wxString m_info_dialog_content;
HttpServer m_http_server;
@@ -492,7 +494,7 @@ public:
void request_open_project(std::string project_id);
void request_remove_project(std::string project_id);
- void handle_http_error(unsigned int status, std::string body, const std::string& provider = ORCA_CLOUD_PROVIDER);
+ void handle_http_error(unsigned int status, std::string body, const std::string& provider = "");
void on_http_error(wxCommandEvent &evt);
void on_update_machine_list(wxCommandEvent& evt);
void on_user_login(wxCommandEvent &evt);
@@ -530,6 +532,7 @@ public:
void sync_preset(Preset* preset);
void start_sync_user_preset(bool with_progress_dlg = false);
void stop_sync_user_preset();
+ void restart_sync_user_preset();
void on_stealth_mode_enter();
// Bundle subscription sync
diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp
index 8aa227ebdc..2a47fe1aad 100644
--- a/src/slic3r/GUI/GUI_Factories.cpp
+++ b/src/slic3r/GUI/GUI_Factories.cpp
@@ -1644,17 +1644,6 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
[]() { return true; }, m_parent);
}
- if (init) {
- append_menu_item(
- menu, wxID_ANY, _L("Delete"), _L("Delete this filament"), [](wxCommandEvent&) {
- plater()->sidebar().delete_filament(-2); }, "", nullptr,
- []() {
- return plater()->sidebar().combos_filament().size() > 1
- // Orca: only show delete filament option for SEMM machines unless is BBL
- && Sidebar::should_show_SEMM_buttons();
- }, m_parent);
- }
-
const int item_id = menu->FindItem(_L("Merge with"));
if (item_id != wxNOT_FOUND)
menu->Destroy(item_id);
@@ -1675,6 +1664,20 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
}
append_submenu(menu, sub_menu, wxID_ANY, _L("Merge with"), "", "",
[filaments_cnt]() { return filaments_cnt > 1; }, m_parent);
+
+ // ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS
+ const int delete_id = menu->FindItem(_L("Delete"));
+ if (delete_id != wxNOT_FOUND)
+ menu->Destroy(delete_id);
+
+ append_menu_item(
+ menu, wxID_ANY, _L("Delete"), _L("Delete this filament"), [](wxCommandEvent&) {
+ plater()->sidebar().delete_filament(-2); }, "", nullptr,
+ []() {
+ return plater()->sidebar().combos_filament().size() > 1
+ // Orca: only show delete filament option for SEMM machines unless is BBL
+ && Sidebar::should_show_SEMM_buttons();
+ }, m_parent);
}
//BBS: add part plate related logic
diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp
index 726ea1aca3..d0afd8e295 100644
--- a/src/slic3r/GUI/GUI_ObjectList.cpp
+++ b/src/slic3r/GUI/GUI_ObjectList.cpp
@@ -435,15 +435,25 @@ void ObjectList::create_objects_ctrl()
// column Extruder of the view control:
BitmapChoiceRenderer* bmp_choice_renderer = new BitmapChoiceRenderer();
- bmp_choice_renderer->set_can_create_editor_ctrl_function([this]() {
- return m_objects_model->GetItemType(GetSelection()) & (itVolume | itLayer | itObject);
+ const auto get_filament_context_item = [this]() {
+#ifdef __WXOSX__
+ return m_filament_editor_item.IsOk() ? m_filament_editor_item : GetSelection();
+#else
+ return GetSelection();
+#endif
+ };
+ bmp_choice_renderer->set_can_create_editor_ctrl_function([this, get_filament_context_item]() {
+ const wxDataViewItem item = get_filament_context_item();
+ return m_objects_model->GetItemType(item) & (itVolume | itLayer | itObject);
});
- bmp_choice_renderer->set_default_extruder_idx([this]() {
- return m_objects_model->GetDefaultExtruderIdx(GetSelection());
+ bmp_choice_renderer->set_default_extruder_idx([this, get_filament_context_item]() {
+ const wxDataViewItem item = get_filament_context_item();
+ return m_objects_model->GetDefaultExtruderIdx(item);
});
- bmp_choice_renderer->set_has_default_extruder([this]() {
- return m_objects_model->GetVolumeType(GetSelection()) == ModelVolumeType::PARAMETER_MODIFIER ||
- m_objects_model->GetItemType(GetSelection()) == itLayer;
+ bmp_choice_renderer->set_has_default_extruder([this, get_filament_context_item]() {
+ const wxDataViewItem item = get_filament_context_item();
+ return m_objects_model->GetVolumeType(item) == ModelVolumeType::PARAMETER_MODIFIER ||
+ m_objects_model->GetItemType(item) == itLayer;
});
AppendColumn(new wxDataViewColumn(_L("Fila."), bmp_choice_renderer,
colFilament, m_columns_width[colFilament] * em, wxALIGN_CENTER_HORIZONTAL, 0));
@@ -461,11 +471,16 @@ void ObjectList::create_objects_ctrl()
wxALIGN_CENTER_HORIZONTAL, 0);
- // Open filament editor faster
+ // On macOS, bypass wxDataViewCtrl::EditItem() for this custom renderer to avoid
+ // native text editing of wxCustomRendererObject.
this->Bind(wxEVT_DATAVIEW_ITEM_ACTIVATED, [this](wxDataViewEvent& event) {
if (event.GetColumn() == colFilament) {
+#ifdef __WXOSX__
+ start_filament_editor(event.GetItem());
+#else
// Trigger the editor opening manually
this->EditItem(event.GetItem(), GetColumn(colFilament));
+#endif
}
});
@@ -1082,10 +1097,11 @@ void ObjectList::update_filament_in_config(const wxDataViewItem& item)
if (m_prevent_update_filament_in_config)
return;
+ ModelConfig* config = nullptr;
const ItemType item_type = m_objects_model->GetItemType(item);
if (item_type & itObject) {
const int obj_idx = m_objects_model->GetIdByItem(item);
- m_config = &(*m_objects)[obj_idx]->config;
+ config = &(*m_objects)[obj_idx]->config;
}
else {
const int obj_idx = m_objects_model->GetIdByItem(m_objects_model->GetObject(item));
@@ -1094,15 +1110,17 @@ void ObjectList::update_filament_in_config(const wxDataViewItem& item)
if (obj_idx < 0 || ui_volume_idx < 0)
return;
int volume_in3d_idx = m_objects_model->get_real_volume_index_in_3d(obj_idx,ui_volume_idx);
- m_config = &(*m_objects)[obj_idx]->volumes[volume_in3d_idx]->config;
+ config = &(*m_objects)[obj_idx]->volumes[volume_in3d_idx]->config;
}
else if (item_type & itLayer)
- m_config = &get_item_config(item);
+ config = &get_item_config(item);
}
- if (!m_config)
+ if (!config)
return;
+ m_config = config;
+
take_snapshot("Change Filament");
const int extruder = m_objects_model->GetExtruderNumber(item);
@@ -2849,7 +2867,7 @@ void ObjectList::split()
take_snapshot("Split to parts");
- volume->split(filament_cnt);
+ volume->split(filament_cnt, wxGetApp().app_config->get_bool("keep_painting"));
wxBusyCursor wait;
@@ -3041,7 +3059,6 @@ void ObjectList::merge(bool to_multipart_object)
auto opt = object->config.option("extruder");
if (opt) { new_volume->config.set_key_value("extruder", new ConfigOptionInt(opt->getInt())); }
}
- new_volume->mmu_segmentation_facets.assign(std::move(volume->mmu_segmentation_facets));
}
new_object->sort_volumes(true);
@@ -3226,6 +3243,22 @@ void ObjectList::boolean()
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "boolean");
ModelObject* object = (*m_objects)[obj_idxs.front()];
+
+ const bool keep_painting = wxGetApp().app_config->get_bool("keep_painting");
+ std::vector> saved_paintings;
+ if (keep_painting) {
+ // Save painting of all the positive parts
+ saved_paintings.reserve(object->volumes.size());
+ for (const ModelVolume* vol : object->volumes) {
+ if (vol && vol->mesh_ptr() && vol->is_model_part() && vol->is_any_painted()) {
+ saved_paintings.emplace_back(vol->save_painting());
+ if (saved_paintings.back()) {
+ saved_paintings.back()->mesh.transform(vol->get_matrix(), true);
+ }
+ }
+ }
+ }
+
TriangleMesh mesh = Plater::combine_mesh_fff(*object, -1, [this](const std::string& msg) {return wxGetApp().notification_manager()->push_plater_error_notification(msg); });
// add mesh to model as a new object, keep the original object's name and config
@@ -3237,6 +3270,29 @@ void ObjectList::boolean()
new_object->add_instance();
ModelVolume* new_volume = new_object->add_volume(mesh);
+ // Remap paint
+ if (keep_painting) {
+ for (auto& saved_painting : saved_paintings) {
+ if (saved_painting) {
+ // For each original painted volume, we need to apply to each instance
+ // because we merged all instances into one in `combine_mesh_fff`
+
+ // First we save the non-instance-translated mesh
+ TriangleMesh vols_mesh(std::move(saved_painting->mesh));
+
+ for (const ModelInstance* i : object->instances) {
+ // Then for each instance, we apply the paint at the given instance place
+ saved_painting->mesh = vols_mesh;
+ saved_painting->mesh.transform(i->get_matrix());
+
+ // Then paint it
+ new_volume->restore_painting(saved_painting, true);
+ }
+
+ }
+ }
+ }
+
// BBS: ensure on bed but no need to ensure locate in the center around origin
new_object->ensure_on_bed();
new_object->center_around_origin();
@@ -6048,10 +6104,13 @@ void ObjectList::fix_through_cgal()
msg += "\n";
}
- plater->clear_before_change_mesh(obj_idx);
+ const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
+ if (!keep_painting) {
+ plater->clear_before_change_mesh(obj_idx);
+ }
const size_t volumes_before = object(obj_idx)->volumes.size();
std::string res;
- if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res))
+ if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res, keep_painting))
return false;
//wxGetApp().plater()->changed_mesh(obj_idx);
object(obj_idx)->ensure_on_bed();
@@ -6169,6 +6228,7 @@ void GUI::ObjectList::smooth_mesh()
WarningDialog dlg(static_cast(wxGetApp().mainframe), content, wxEmptyString, wxOK);
dlg.ShowModal();
};
+ const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
bool has_show_smooth_mesh_error_dlg = false;
if (vol_idxs.empty()) {
obj = object(object_idx);
@@ -6180,8 +6240,11 @@ void GUI::ObjectList::smooth_mesh()
bool ok;
auto result_mesh = TriangleMeshDeal::smooth_triangle_mesh(mv->mesh(), ok);
if (ok) {
+ const std::optional saved_painting = keep_painting ?
+ mv->save_painting() :
+ std::optional{};
mv->set_mesh(result_mesh);
- mv->reset_extra_facets(); // reset paint color
+ mv->restore_painting(saved_painting);
mv->calculate_convex_hull();
mv->invalidate_convex_hull_2d();
mv->set_new_unique_id();
@@ -6206,8 +6269,11 @@ void GUI::ObjectList::smooth_mesh()
bool ok;
auto result_mesh = TriangleMeshDeal::smooth_triangle_mesh(mv->mesh(),ok);
if (ok) {
+ const std::optional saved_painting = keep_painting ?
+ mv->save_painting() :
+ std::optional{};
mv->set_mesh(result_mesh);
- mv->reset_extra_facets(); // reset paint color
+ mv->restore_painting(saved_painting);
mv->calculate_convex_hull();
mv->invalidate_convex_hull_2d();
mv->set_new_unique_id();
@@ -6279,10 +6345,71 @@ void ObjectList::ItemValueChanged(wxDataViewEvent &event)
}
}
+#ifdef __WXOSX__
+bool ObjectList::is_live_model_item(wxDataViewItem item) const
+{
+ if (!item.IsOk() || !m_objects_model)
+ return false;
+
+ wxDataViewItemArray all_items;
+ m_objects_model->GetAllChildren(wxDataViewItem(nullptr), all_items);
+ for (const wxDataViewItem& live_item : all_items)
+ if (live_item.GetID() == item.GetID())
+ return true;
+
+ return false;
+}
+
+void ObjectList::start_filament_editor(wxDataViewItem item)
+{
+ if (m_starting_filament_editor || !is_live_model_item(item))
+ return;
+
+ const ItemType type = m_objects_model->GetItemType(item);
+ if (!(type & (itVolume | itLayer | itObject)))
+ return;
+
+ auto* column = GetColumn(colFilament);
+ if (!column)
+ return;
+
+ auto* custom_renderer = dynamic_cast(column->GetRenderer());
+ if (!custom_renderer || custom_renderer->GetEditorCtrl())
+ return;
+
+ m_starting_filament_editor = true;
+ m_filament_editor_item = item;
+ const bool started = custom_renderer->StartEditing(item, GetItemRect(item, column));
+ m_filament_editor_item = wxDataViewItem(nullptr);
+ m_starting_filament_editor = false;
+
+ if (!started)
+ return;
+
+ SetCustomRendererPtr(custom_renderer);
+ SetCustomRendererItem(item);
+}
+#endif // __WXOSX__
+
void GUI::ObjectList::OnStartEditing(wxDataViewEvent &event)
{
auto col = event.GetColumn();
auto item = event.GetItem();
+#ifdef __WXOSX__
+ if (col == colFilament) {
+ if (m_starting_filament_editor)
+ return;
+
+ event.Veto();
+ CallAfter([this, item] {
+ if (!is_live_model_item(item))
+ return;
+ start_filament_editor(item);
+ });
+ return;
+ }
+#endif // __WXOSX__
+
if (col == colName) {
ObjectDataViewModelNode* node = (ObjectDataViewModelNode*)item.GetID();
if (node->GetType() & itPlate) {
@@ -6350,6 +6477,12 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
dynamic_cast(wxGetApp().get_model_tab(vol_idx >= 0))->reset_model_config();
return;
}
+#ifdef __WXOSX__
+ if (col == colFilament) {
+ start_filament_editor(item);
+ return;
+ }
+#endif // __WXOSX__
if (col != colFilament && col != colName)
return;
auto column = GetColumn(col);
diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp
index b97eff05ae..cab78b6adc 100644
--- a/src/slic3r/GUI/GUI_ObjectList.hpp
+++ b/src/slic3r/GUI/GUI_ObjectList.hpp
@@ -192,6 +192,13 @@ private:
int m_last_selected_column = -1;
#endif /* __MSW__ */
+#ifdef __WXOSX__
+ bool m_starting_filament_editor = false;
+ wxDataViewItem m_filament_editor_item { nullptr };
+ bool is_live_model_item(wxDataViewItem item) const;
+ void start_filament_editor(wxDataViewItem item);
+#endif // __WXOSX__
+
#if 0
SettingsFactory::Bundle m_freq_settings_fff;
SettingsFactory::Bundle m_freq_settings_sla;
diff --git a/src/slic3r/GUI/GUI_ObjectTable.cpp b/src/slic3r/GUI/GUI_ObjectTable.cpp
index 71174665e1..aa3408ba29 100644
--- a/src/slic3r/GUI/GUI_ObjectTable.cpp
+++ b/src/slic3r/GUI/GUI_ObjectTable.cpp
@@ -561,14 +561,23 @@ wxString GridCellSupportEditor::ms_stringValues[2] = { wxT(""), wxT("") };
void GridCellSupportEditor::DoActivate(int row, int col, wxGrid* grid)
{
- ObjectGrid* local_table = dynamic_cast