@@ -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/PrintApply.cpp b/src/libslic3r/PrintApply.cpp
index 789076a43e..213a20ac55 100644
--- a/src/libslic3r/PrintApply.cpp
+++ b/src/libslic3r/PrintApply.cpp
@@ -1187,8 +1187,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
pem->values = effective_physical_extruder_map(pem, pei).values;
}
- m_ori_full_print_config = new_full_config;
- new_full_config.update_values_to_printer_extruders_for_multiple_filaments(new_full_config, filament_options_with_variant, "filament_self_index", "filament_extruder_variant");
auto opt_filament_map = new_full_config.option("filament_map");
std::vector filament_maps = opt_filament_map ? opt_filament_map->values : std::vector();
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/PrintObject.cpp b/src/libslic3r/PrintObject.cpp
index f572d3bbb1..aee470bab5 100644
--- a/src/libslic3r/PrintObject.cpp
+++ b/src/libslic3r/PrintObject.cpp
@@ -1703,30 +1703,46 @@ void PrintObject::detect_surfaces_type()
// Only iterate to the second-to-last layer, since we look at layer i+1.
if( (this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)){
const size_t last = (m_layers.empty() ? 0 : m_layers.size() - 1);
- tbb::parallel_for( tbb::blocked_range(0, last), [this, region_id](const tbb::blocked_range &range) {
+
+ // ORCA: Two-phase split (collect-then-apply) to eliminate a data race in the
+ // original single-phase parallel_for, where iteration `i` rewrote
+ // m_layers[i+1]->slices.surfaces via std::move while iteration `i+1` (running
+ // on an adjacent TBB block on another worker thread) was iterating that same
+ // Surfaces vector as its bot_surfs. Splitting into a read-only collect pass
+ // followed by a write-only apply pass removes the cross-iteration aliasing.
+ //
+ // Phase 1: read-only pass — collect each layer's stBottomBridge polygons into a
+ // per-layer cache. No surfaces are mutated, so concurrent reads are safe.
+ std::vector bridge_polys_per_layer(last);
+ tbb::parallel_for(tbb::blocked_range(0, last), [this, region_id, &bridge_polys_per_layer](const tbb::blocked_range &range) {
for (size_t i = range.begin(); i < range.end(); ++i) {
m_print->throw_if_canceled();
-
- // Step 1: Find bridge polygons
- // Current layer (i): Search for stBottomBridge polygons.
const Surfaces &bot_surfs = m_layers[i]->m_regions[region_id]->slices.surfaces;
- // Next layer (i+1): The layer where stInternal polygons may be re-classified.
- Surfaces &top_surfs = m_layers[i + 1]->m_regions[region_id]->slices.surfaces;
-
- // Step 2: Collect the bridge polygons in the current layer region
- Polygons polygons_bridge;
for (const Surface &sbot : bot_surfs) {
if (sbot.surface_type == stBottomBridge) {
- polygons_append(polygons_bridge, to_polygons(sbot));
+ polygons_append(bridge_polys_per_layer[i], to_polygons(sbot));
}
}
-
+ }
+ });
+
+ // Phase 2: write pass — each iteration mutates only m_layers[i+1]->slices.surfaces
+ // and reads its bridge polygons from the precomputed cache. Different iterations
+ // never share a write target, so there is no aliasing between worker threads.
+ tbb::parallel_for( tbb::blocked_range(0, last), [this, region_id, &bridge_polys_per_layer](const tbb::blocked_range &range) {
+ for (size_t i = range.begin(); i < range.end(); ++i) {
+ m_print->throw_if_canceled();
+
+ // Step 1 + 2: pull the precomputed bridge polygons for the current source layer.
+ const Polygons &polygons_bridge = bridge_polys_per_layer[i];
+
// Step 3: Early termination of loop if no meaningfull bridge found
// No bridge polygons found, continue to the next layer
if (polygons_bridge.empty())
continue;
// Step 4: Bottom bridge polygons found - scan and create layer+1 bridge polygon
+ Surfaces &top_surfs = m_layers[i + 1]->m_regions[region_id]->slices.surfaces;
Surfaces new_surfaces;
new_surfaces.reserve(top_surfs.size());
@@ -1740,7 +1756,50 @@ void PrintObject::detect_surfaces_type()
// This would also skip generation of very short dual bridge layers (that are shorter than N perimeters), but these are unecessary as the bridge distance is
// We could reduce this slightly to account for innacurcies in the clipping operation.
// TODO: Monitor GitHub issues to check whether second bridge layers are ommited where they should be generated. If yes, reduce the filtering distance
-
+
+ // ORCA: Same-layer-top guard.
+ //
+ // Collect every stTop polygon present at layer i+1 (this region) and
+ // expand it by the same `offset_distance` used by the bridge filter
+ // above. Note that `offset_distance` here is the full wall-band
+ // distance for the region (external wall width + all configured
+ // internal wall widths, i.e. external + (wall_loops - 1) × internal),
+ // not a single perimeter width. Any candidate second-bridge area that
+ // falls under this expanded mask will be subtracted out below.
+ //
+ // Why this exists: detect_surfaces_type() classifies a layer's "top"
+ // surfaces as the geometry that is not covered by the layer above. Those
+ // stTop regions often have small stInternal islands embedded in them.
+ // The pre-existing wall-band filter (shrink_ex/offset_ex by
+ // offset_distance) is supposed to throw those tiny islands away, but
+ // its result is sensitive to Clipper's floating-point order of
+ // operations: on macOS ARM the filter eats them, on Windows/Intel it
+ // doesn't. Visible bridges then show up scattered across the printed
+ // top surface.
+ //
+ // Expanding stTop by offset_distance and subtracting it from the
+ // overlap makes the decision platform-independent: an island fully
+ // surrounded by stTop disappears regardless of which Clipper happens
+ // to be doing the math, while large stInternal regions away from the
+ // top survive intact (the expansion only nibbles the wall-band depth
+ // inward).
+ //
+ // Keep ExPolygons throughout so that any holes inside an stTop surface
+ // are offset with the correct sign (positive offset shrinks holes /
+ // grows the solid region). Using Polygons + expand() would treat the
+ // contour and each hole as independent polygons and could distort the
+ // mask.
+ ExPolygons same_layer_top_expanded;
+ {
+ ExPolygons same_layer_top;
+ for (const Surface &s : top_surfs) {
+ if (s.surface_type == stTop)
+ same_layer_top.push_back(s.expolygon);
+ }
+ if (! same_layer_top.empty())
+ same_layer_top_expanded = offset_ex(same_layer_top, offset_distance);
+ }
+
// For each surface in the layer above
for (Surface &s_up : top_surfs) {
// Only reclassify stInternal polygons (i.e. what will become later solid and sparse infill)
@@ -1755,7 +1814,13 @@ void PrintObject::detect_surfaces_type()
// Filter out the resulting candidate bridges based on size. First perform a shrink operation...
// ...followed by an expand operation to bring them back to the original size (positive offset)
overlap = offset_ex(shrink_ex(overlap, offset_distance), offset_distance);
-
+
+ // ORCA: subtract the expanded same-layer stTop mask (see comment above
+ // the mask construction). Drops stInternal islands fully surrounded by
+ // stTop at i+1 without affecting bridges that lie away from the top.
+ if (! same_layer_top_expanded.empty() && ! overlap.empty())
+ overlap = diff_ex(overlap, same_layer_top_expanded, ApplySafetyOffset::Yes);
+
// Now subtract the filtered new bridge layer from the remaining internal surfaces to create the new internal surface
ExPolygons remainder = diff_ex(p_up, overlap, ApplySafetyOffset::Yes);
@@ -2500,29 +2565,76 @@ void PrintObject::bridge_over_infill()
backup_surfaces[lidx] = {};
}
- tbb::parallel_for(tbb::blocked_range(0, this->layers().size()), [po = this, &backup_surfaces,
- &surfaces_by_layer](tbb::blocked_range r) {
+ // ORCA: Two-phase split (collect-then-apply) to eliminate a data race in
+ // the original single-phase parallel_for, where iteration `lidx` read
+ // m_layers[lidx-1]->regions()->fill_surfaces (its lower_layer) to compute
+ // `lightning_fill`, while iteration `lidx-1`, on an adjacent TBB block,
+ // was concurrently std::move-ing / emplace_back-ing into that same
+ // SurfaceCollection.
+ //
+ // Semantic choice — read ORIGINAL surfaces in Phase 1:
+ // The lower_layer that iteration `lidx` looks at is the *current* layer
+ // for iteration `lidx-1`, which Phase 2 will modify. We therefore have to
+ // pick whether Phase 1 sees that layer's pre-modification or
+ // post-modification state. We deliberately use the original (pre-modification)
+ // state, for two reasons:
+ // 1. The gate is asking "does the layer below use lightning sparse
+ // infill?" — that's a property of the layer's configuration plus its
+ // original sparse-infill classification. Phase 2's edits only carve
+ // a small overhang-aligned slice of sparse into solid; they do not
+ // change whether the layer is using lightning. The realistic gate
+ // answer is the same either way.
+ // 2. Each layer's solid expansion is meant to give its OWN lower_layer
+ // something to anchor lightning lines onto. Cascading the gate
+ // across layers ("skip mine because the layer below already did
+ // some") would invert that intent and force a serial Phase 2.
+ // The original racy code didn't actually implement either choice
+ // consistently — it returned whichever bytes happened to be in the
+ // vector when the thread arrived. This split makes the behaviour
+ // defined, deterministic across runs and platforms, and equivalent to
+ // a clean sequential implementation that gathered all gates first and
+ // then applied modifications.
+ //
+ // Phase 1: read-only — for each layer, determine whether its lower_layer
+ // has any stInternal area inside a lightning-infill region. That's the
+ // sole purpose of `lightning_fill` in the original code: a gate. Capture
+ // it once into a per-layer bool, derived from the original (unmodified)
+ // surfaces, so the gate is platform-independent and order-independent.
+ std::vector needs_lightning_expansion(this->layers().size(), 0);
+ tbb::parallel_for(tbb::blocked_range(0, this->layers().size()), [po = this, &surfaces_by_layer,
+ &needs_lightning_expansion](tbb::blocked_range r) {
PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT);
for (size_t lidx = r.begin(); lidx < r.end(); lidx++) {
if (surfaces_by_layer.find(lidx) == surfaces_by_layer.end())
continue;
-
- Layer *layer = po->get_layer(lidx);
+ const Layer *layer = po->get_layer(lidx);
const Layer *lower_layer = layer->lower_layer;
if (lower_layer == nullptr)
continue;
-
- Polygons lightning_fill;
for (const LayerRegion *region : lower_layer->regions()) {
- if (region->region().config().sparse_infill_pattern == ipLightning) {
- Polygons lf = to_polygons(region->fill_surfaces.filter_by_type(stInternal));
- lightning_fill.insert(lightning_fill.end(), lf.begin(), lf.end());
+ if (region->region().config().sparse_infill_pattern == ipLightning
+ && ! region->fill_surfaces.filter_by_type(stInternal).empty()) {
+ needs_lightning_expansion[lidx] = 1;
+ break;
}
}
+ }
+ });
- if (lightning_fill.empty())
+ // Phase 2: write-only — each iteration mutates only m_layers[lidx]'s
+ // fill_surfaces and never reads any other layer's surfaces. Different
+ // iterations write to disjoint LayerRegion::fill_surfaces vectors, so
+ // there is no aliasing between worker threads.
+ tbb::parallel_for(tbb::blocked_range(0, this->layers().size()), [po = this, &backup_surfaces,
+ &surfaces_by_layer,
+ &needs_lightning_expansion](tbb::blocked_range r) {
+ PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT);
+ for (size_t lidx = r.begin(); lidx < r.end(); lidx++) {
+ if (! needs_lightning_expansion[lidx])
continue;
+ Layer *layer = po->get_layer(lidx);
+
for (LayerRegion *region : layer->regions()) {
backup_surfaces[lidx][region] = std::move(
region->fill_surfaces); // Make backup copy by move!! so that pointers in candidate surfaces stay valid
@@ -3232,53 +3344,75 @@ void PrintObject::bridge_over_infill()
// === ORCA: Create a second internal bridge layer above the first bridge layer. ========================================================
// ======================================================================================================================================
if ( this->m_config.enable_extra_bridge_layer == eblApplyToAll || this->m_config.enable_extra_bridge_layer == eblInternalBridgeOnly) {
- // Process layers in parallel up to second-to-last
- tbb::parallel_for( tbb::blocked_range(0, this->layers().size() - 1), [this](const tbb::blocked_range& r) {
- for (size_t lidx = r.begin(); lidx < r.end(); ++lidx)
- {
+ // ORCA: Two-phase to eliminate the same data race as the external-bridge
+ // pass in detect_surfaces_type().
+ //
+ // Phase 1: read-only — for each layer, collect its stInternalBridge polygons and
+ // the matching bridge angle into a per-layer cache.
+ struct LayerBridgeCache {
+ ExPolygons polys;
+ double angle = 0.0;
+ float offset_distance = 0.0f;
+ bool has_bridge = false;
+ };
+ // Guard against size_t underflow when the object has 0 or 1 layers — there is
+ // no "layer above" to receive an extra bridge, so the whole pass is a no-op.
+ const size_t last = (this->layers().size() < 2) ? 0 : this->layers().size() - 1;
+ std::vector caches(this->layers().size());
+
+ tbb::parallel_for( tbb::blocked_range(0, last), [this, &caches](const tbb::blocked_range& r) {
+ for (size_t lidx = r.begin(); lidx < r.end(); ++lidx) {
Layer* layer = this->get_layer(lidx);
-
- // (A) Gather internal bridging surfaces in the current layer
- ExPolygons bridging_current_layer;
- double bridging_angle_current = 0.0;
-
- bool found_any_bridge = false;
- float offset_distance = 0.0f;
-
- // Pick a region from which to retrieve the flow width
+ LayerBridgeCache &cache = caches[lidx];
+
if (!layer->regions().empty())
- offset_distance = layer->regions().front()->flow(frSolidInfill).scaled_width();
+ cache.offset_distance = layer->regions().front()->flow(frSolidInfill).scaled_width();
for (LayerRegion *region : layer->regions()) {
for (const Surface &surf : region->fill_surfaces.surfaces) {
if (surf.surface_type == stInternalBridge) {
- bridging_current_layer.push_back(surf.expolygon);
- bridging_angle_current = surf.bridge_angle; // Store the last bridging angle of the current print object
- found_any_bridge = true;
+ cache.polys.push_back(surf.expolygon);
+ cache.angle = surf.bridge_angle; // last bridge angle on this layer wins, matching prior behaviour
+ cache.has_bridge = true;
}
}
}
-
- // If no bridging in this layer, continue with the next
- if (!found_any_bridge || bridging_current_layer.empty())
+
+ if (!cache.has_bridge || cache.polys.empty()) {
+ cache.has_bridge = false;
continue;
-
- // (B) Shrink-expand to remove trivial bridging areas
- bridging_current_layer = offset_ex( shrink_ex(bridging_current_layer, offset_distance), offset_distance );
-
- if (bridging_current_layer.empty())
- continue; // all bridging was trivial, continue with the next layer
-
+ }
+
+ // Shrink-expand to remove trivial bridging areas
+ cache.polys = offset_ex(shrink_ex(cache.polys, cache.offset_distance), cache.offset_distance);
+ if (cache.polys.empty())
+ cache.has_bridge = false;
+ }
+ });
+
+ // Phase 2: write — each iteration mutates only m_layers[lidx+1]->fill_surfaces and
+ // pulls its bridge polygons from the precomputed cache. Different iterations never
+ // touch the same fill_surfaces vector, so there is no aliasing between workers.
+ tbb::parallel_for( tbb::blocked_range(0, last), [this, &caches](const tbb::blocked_range& r) {
+ for (size_t lidx = r.begin(); lidx < r.end(); ++lidx)
+ {
+ const LayerBridgeCache &cache = caches[lidx];
+
+ // If no bridging in this layer, continue with the next
+ if (!cache.has_bridge || cache.polys.empty())
+ continue;
+
// (C) If there is a next layer, identify overlapping stInternal & stInternalSolid areas and convert the overlap to stSecondInternalBridge
if (lidx + 1 < this->layers().size()) {
Layer* next_layer = this->get_layer(lidx + 1);
// second bridging angle is 90 degrees offset
- double bridging_angle_second = bridging_angle_current + M_PI / 2.0;
+ double bridging_angle_second = cache.angle + M_PI / 2.0;
// Union the bridging polygons
- ExPolygons bridging_union = union_safety_offset_ex(bridging_current_layer);
-
+ ExPolygons bridging_union = union_safety_offset_ex(cache.polys);
+ const float offset_distance = cache.offset_distance;
+
for (LayerRegion *next_region : next_layer->regions()) {
Surfaces next_new_surfaces;
Surfaces keep_surfaces;
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/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp
index fd3569e2ca..50d4cfcfa6 100644
--- a/src/slic3r/GUI/3DScene.cpp
+++ b/src/slic3r/GUI/3DScene.cpp
@@ -978,12 +978,51 @@ GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCo
return list;
}
-int GLVolumeCollection::get_selection_support_threshold_angle(bool &enable_support) const
+// ORCA: Compute slope.normal_z for 3D overhang highlight directly from support settings.
+// If support_threshold_angle is 0, use tree fallback angle (30 deg) for tree supports,
+// and derive an equivalent angle from threshold overlap for normal supports.
+float GLVolumeCollection::get_selection_support_normal_z() const
{
- const DynamicPrintConfig& glb_cfg = GUI::wxGetApp().preset_bundle->prints.get_edited_preset().config;
- enable_support = glb_cfg.opt_bool("enable_support");
- int support_threshold_angle = glb_cfg.opt_int("support_threshold_angle");
- return support_threshold_angle ;
+ const DynamicPrintConfig& glb_cfg = GUI::wxGetApp().preset_bundle->prints.get_edited_preset().config;
+ const auto& full_cfg = GUI::wxGetApp().preset_bundle->full_config();
+ const auto support_type = glb_cfg.opt_enum("support_type");
+ const int support_threshold_angle = glb_cfg.opt_int("support_threshold_angle");
+ double angle_rad;
+
+ if (support_threshold_angle > 0) {
+ // Match support generation: explicit threshold angles are treated as inclusive.
+ const int effective_support_threshold_angle = std::min(support_threshold_angle + 1, 89);
+ angle_rad = Geometry::deg2rad(static_cast(effective_support_threshold_angle));
+ } else if (is_tree(support_type)) {
+ angle_rad = Geometry::deg2rad(30.0); // fallback value for tree supports
+ } else { // For normal supports, if the angle is set to 0, calculate normal_z from overlap.
+ const double layer_height = full_cfg.opt_float("layer_height");
+ const auto* nozzle_diameter_opt = full_cfg.option("nozzle_diameter");
+ const int wall_filament = full_cfg.opt_int("wall_filament");
+ const size_t nozzle_count = nozzle_diameter_opt->values.size();
+ const size_t wall_extruder_idx = (wall_filament > 0 && wall_filament <= static_cast(nozzle_count))
+ ? static_cast(wall_filament - 1)
+ : 0; // Invalid extruder index falls back to extruder 1.
+
+ // Use wall extruder's nozzle diameter for better estimation of external perimeter width,
+ // which is more relevant to overhang printing than the default nozzle diameter.
+ const double nozzle_diameter = nozzle_diameter_opt->values[wall_extruder_idx];
+
+ double external_perimeter_width = full_cfg.get_abs_value("outer_wall_line_width", nozzle_diameter);
+ if (external_perimeter_width <= 0.0) {
+ external_perimeter_width = full_cfg.get_abs_value("line_width", nozzle_diameter);
+
+ if (external_perimeter_width <= 0.0)
+ external_perimeter_width = nozzle_diameter;
+ }
+
+ const double overlap_width = full_cfg.get_abs_value("support_threshold_overlap", external_perimeter_width);
+ const double lower_layer_offset = std::max(0.0, external_perimeter_width - overlap_width);
+
+ angle_rad = lower_layer_offset <= EPSILON ? Geometry::deg2rad(89.0) : std::atan(layer_height / lower_layer_offset);
+ }
+
+ return static_cast(-std::cos(std::clamp(angle_rad, 0.0, Geometry::deg2rad(89.0))));
}
//BBS: add outline drawing logic
@@ -1019,6 +1058,8 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
if (disable_cullface)
glsafe(::glDisable(GL_CULL_FACE));
+ const float support_normal_z = get_selection_support_normal_z();
+
for (GLVolumeWithIdAndZ& volume : to_render) {
#if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT
if (type == ERenderType::Transparent) {
@@ -1075,16 +1116,11 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
//use -1 ad a invalid type
shader->set_uniform("print_volume.type", -1);
}
-
- bool enable_support;
- int support_threshold_angle = get_selection_support_threshold_angle(enable_support);
-
- float normal_z = -::cos(Geometry::deg2rad((float) support_threshold_angle));
shader->set_uniform("volume_world_matrix", volume.first->world_matrix());
shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower);
shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast()));
- shader->set_uniform("slope.normal_z", normal_z);
+ shader->set_uniform("slope.normal_z", support_normal_z);
#if ENABLE_ENVIRONMENT_MAP
unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id();
diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp
index b12d048aa9..76d10620d9 100644
--- a/src/slic3r/GUI/3DScene.hpp
+++ b/src/slic3r/GUI/3DScene.hpp
@@ -488,7 +488,7 @@ public:
GLVolume* new_toolpath_volume(const ColorRGBA& rgba);
GLVolume* new_nontoolpath_volume(const ColorRGBA& rgba);
- int get_selection_support_threshold_angle(bool&) const;
+ float get_selection_support_normal_z() const;
// Render the volumes by OpenGL.
//BBS: add outline drawing logic
void render(ERenderType type,
diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp
index 19a32c2d21..4a429a4a5a 100644
--- a/src/slic3r/GUI/AMSMaterialsSetting.cpp
+++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp
@@ -863,7 +863,15 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
std::set filament_id_set;
PresetBundle * preset_bundle = wxGetApp().preset_bundle;
std::ostringstream stream;
- stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(0);
+ // Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
+ // only during the brief BBL startup window before firmware reports nozzle info. Without this,
+ // the "0.0" lookup string returns an empty set and the filament dropdown goes blank.
+ float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
+ if (machine_diameter == 0.0f && preset_bundle) {
+ const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
+ if (opt) machine_diameter = static_cast(opt)->values[0];
+ }
+ stream << std::fixed << std::setprecision(1) << machine_diameter;
std::string nozzle_diameter_str = stream.str();
std::set printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle(DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str);
@@ -1101,8 +1109,17 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
std::ostringstream stream;
- if (obj)
- stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(0);
+ if (obj) {
+ // Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
+ // only during the brief BBL startup window before firmware reports nozzle info. Without this,
+ // the "0.0" lookup string returns an empty set and filament lookup yields no results.
+ float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
+ if (machine_diameter == 0.0f) {
+ const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
+ if (opt) machine_diameter = static_cast(opt)->values[0];
+ }
+ stream << std::fixed << std::setprecision(1) << machine_diameter;
+ }
std::string nozzle_diameter_str = stream.str();
std::set printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle(DevPrinterConfigUtil::get_printer_display_name(obj->printer_type),
nozzle_diameter_str);
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/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp
index 1dee391ae1..a485aaeb52 100644
--- a/src/slic3r/GUI/CaliHistoryDialog.cpp
+++ b/src/slic3r/GUI/CaliHistoryDialog.cpp
@@ -644,7 +644,14 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject
std::set filament_id_set;
std::set printer_names;
std::ostringstream stream;
- stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(0);
+ // If the machine didn't report a nozzle diameter (0.0 = unknown), fall back to the currently
+ // selected printer preset so the filament list isn't empty.
+ float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
+ if (machine_diameter == 0.0f && preset_bundle) {
+ const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
+ if (opt) machine_diameter = static_cast(opt)->values[0];
+ }
+ stream << std::fixed << std::setprecision(1) << machine_diameter;
std::string nozzle_diameter_str = stream.str();
for (auto printer_it = preset_bundle->printers.begin(); printer_it != preset_bundle->printers.end(); printer_it++) {
diff --git a/src/slic3r/GUI/CalibrationWizard.cpp b/src/slic3r/GUI/CalibrationWizard.cpp
index c61beb3304..ab5b920fb6 100644
--- a/src/slic3r/GUI/CalibrationWizard.cpp
+++ b/src/slic3r/GUI/CalibrationWizard.cpp
@@ -761,6 +761,7 @@ void PressureAdvanceWizard::on_cali_start()
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
+ calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
calib_info.select_ams = std::to_string(selected_tray_id);
Preset *preset = selected_filaments.begin()->second;
Preset * temp_filament_preset = new Preset(preset->type, preset->name + "_temp");
@@ -1293,6 +1294,7 @@ void FlowRateWizard::on_cali_start(CaliPresetStage stage, float cali_value, Flow
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
+ calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
Preset* preset = selected_filaments.begin()->second;
temp_filament_preset = new Preset(preset->type, preset->name + "_temp");
temp_filament_preset->config = preset->config;
@@ -1708,6 +1710,7 @@ void MaxVolumetricSpeedWizard::on_cali_start()
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
+ calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
calib_info.filament_prest = selected_filaments.begin()->second;
std::map filament_list = preset_page->get_filament_ams_list();
calib_info.filament_color = filament_list[selected_filaments.begin()->first].opt_string("filament_colour", 0u);
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/FilamentGroupPopup.cpp b/src/slic3r/GUI/FilamentGroupPopup.cpp
index 2cff9c3c82..a29e4537fb 100644
--- a/src/slic3r/GUI/FilamentGroupPopup.cpp
+++ b/src/slic3r/GUI/FilamentGroupPopup.cpp
@@ -338,7 +338,21 @@ void FilamentGroupPopup::OnRadioBtn(int idx)
}
}
-void FilamentGroupPopup::OnTimer(wxTimerEvent &event) { Dismiss(); }
+void FilamentGroupPopup::OnTimer(wxTimerEvent &event)
+{
+#if __APPLE__
+ // On macOS, when moving cursor from slice button to this popup window,
+ // the popup window entering event is triggered first, then the slice button
+ // leaving event got triggered. So the timer is stopped first, then started
+ // again, causing the popup being dismissed immediately.
+ // To fix this, we check if cursor is still inside the popup window before
+ // dismissing.
+ wxPoint pos = this->ScreenToClient(wxGetMousePosition());
+ if (this->GetClientRect().Contains(pos)) return;
+#endif
+
+ Dismiss();
+}
void FilamentGroupPopup::Dismiss() {
m_active = false;
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