From b9e1db41a081f58142c5c2c4ef61c74f870aa5fc Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:41 -0300 Subject: [PATCH 01/71] fix: explicit template instantiation for clang-cl in BoundingBox clang-cl does not instantiate BoundingBoxBase::construct for Points::const_iterator through the same transitive path accepted by MSVC. Add the explicit instantiation so the template definition is emitted where the clang-cl build needs it. --- src/libslic3r/BoundingBox.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libslic3r/BoundingBox.cpp b/src/libslic3r/BoundingBox.cpp index a2a510b64c..cf5441dace 100644 --- a/src/libslic3r/BoundingBox.cpp +++ b/src/libslic3r/BoundingBox.cpp @@ -8,6 +8,8 @@ namespace Slic3r { template BoundingBoxBase::BoundingBoxBase(const Points &points); +template void BoundingBoxBase::construct<0, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator); +template void BoundingBoxBase::construct<1, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator); template BoundingBoxBase::BoundingBoxBase(const std::vector &points); template BoundingBox3Base::BoundingBox3Base(const std::vector &points); From a636243ec03f60effbf79efdae5e0aa9ed197592 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:41 -0300 Subject: [PATCH 02/71] fix: materialize Eigen cast expression before passing to distance_to_squared Eigen's .cast() returns a lazy CwiseUnaryOp expression, not a materialized Matrix. The distance_to_squared overload taking a nearest_point output parameter expects a concrete Eigen::Matrix, so template deduction fails on clang-cl. Materialize the cast into a local Vec variable before passing it. MSVC accepted the expression directly; clang-cl correctly rejects it. --- src/libslic3r/AABBTreeLines.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 97ad1bdf44..b13fe03401 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -32,7 +32,8 @@ namespace AABBTreeLines { { Vec nearest_point; const LineType& line = lines[primitive_index]; - squared_distance = line_alg::distance_to_squared(line, origin.template cast(), &nearest_point); + const Vec origin_cast = origin.template cast(); + squared_distance = line_alg::distance_to_squared(line, origin_cast, &nearest_point); return nearest_point.template cast(); } }; From ec22a58c407b6507fe6dafbaa30ed9380c4ed017 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:42 -0300 Subject: [PATCH 03/71] fix: LabelItemType underlying type to silence narrowing in clang-cl LabelItemType values are used with Marker, which is std::size_t. MSVC accepts the implicit narrowing in this path, but clang-cl diagnoses it more strictly. Give the enum the same underlying type as Marker. --- src/slic3r/GUI/PresetComboBoxes.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp index 53644cecf5..20f75f4616 100644 --- a/src/slic3r/GUI/PresetComboBoxes.hpp +++ b/src/slic3r/GUI/PresetComboBoxes.hpp @@ -39,7 +39,7 @@ public: PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size = wxDefaultSize, PresetBundle* preset_bundle = nullptr); ~PresetComboBox(); - enum LabelItemType { + enum LabelItemType : std::size_t { LABEL_ITEM_PHYSICAL_PRINTER = 0xffffff01, LABEL_ITEM_PRINTER_MODELS, LABEL_ITEM_DISABLED, From 9e635925c8d41f76e97f356a35509b738a3ba6f2 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:42 -0300 Subject: [PATCH 04/71] fix: explicit cast T2A_ to const char* for clang-cl clang-cl is stricter about converting the T2A_ helper result in this expression. Cast the conversion helper result explicitly to const char* so the intended string conversion is unambiguous across MSVC and clang-cl. --- src/dev-utils/BaseException.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev-utils/BaseException.cpp b/src/dev-utils/BaseException.cpp index d3f36fcc63..b9054ec21e 100644 --- a/src/dev-utils/BaseException.cpp +++ b/src/dev-utils/BaseException.cpp @@ -69,7 +69,7 @@ void CBaseException::OutputString(LPCTSTR lpszFormat, ...) //WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, _tcslen(szBuf), NULL, NULL); //output it to the current directory of binary - std::string output_str = textconv_helper::T2A_(szBuf); + std::string output_str = static_cast(textconv_helper::T2A_(szBuf)); *output_file << output_str; output_file->flush(); } From 7aaeea2bd2b045f90465f7a64e48b3c5540e6478 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:42 -0300 Subject: [PATCH 05/71] fix: wide string literals for url_prefix concatenation on Windows The Windows path builds url_prefix as a wide string. MSVC accepts concatenating the narrow literals here, but clang-cl rejects the mixed narrow/wide expression. Use wide literals so the concatenation type matches. --- src/slic3r/GUI/GUI_App.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index b46273d64a..d3aa486d09 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -9224,7 +9224,7 @@ bool GUI_App::check_url_association(std::wstring url_prefix, std::wstring& reg_b { reg_bin = L""; #ifdef WIN32 - wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); + wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command"); if (!key_full.Exists()) { return false; } @@ -9250,8 +9250,8 @@ void GUI_App::associate_url(std::wstring url_prefix) wxString key_string = "\"" + wbinary + "\" \"%1\""; - wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix); - wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); + wxRegKey key_first(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix); + wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command"); if (!key_first.Exists()) { key_first.Create(false); } @@ -9271,7 +9271,7 @@ void GUI_App::disassociate_url(std::wstring url_prefix) #ifdef WIN32 if (is_running_in_msix()) return; - wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); + wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command"); if (!key_full.Exists()) { return; } From d865e9e6e13e03ec0020d9529006a7ac750ca6b2 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:59 -0300 Subject: [PATCH 06/71] fix: exclude clang-cl from MSVC-only compiler guards in CMake clang-cl defines MSVC in CMake, but some guarded blocks apply flags or behavior that are specific to cl.exe and should not be passed to clang-cl. Exclude Clang from those MSVC-only branches so clang-cl follows the compatible compiler path instead of inheriting cl.exe-only settings. --- deps_src/clipper2/CMakeLists.txt | 6 +++++- deps_src/miniz/CMakeLists.txt | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/deps_src/clipper2/CMakeLists.txt b/deps_src/clipper2/CMakeLists.txt index c604002da7..86c9a9efab 100644 --- a/deps_src/clipper2/CMakeLists.txt +++ b/deps_src/clipper2/CMakeLists.txt @@ -37,7 +37,11 @@ target_include_directories(Clipper2 ) if (WIN32) - target_compile_options(Clipper2 PRIVATE /W4 /WX) + if (MSVC AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(Clipper2 PRIVATE /W4 /WX) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(Clipper2 PRIVATE /W4) + endif() else() target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror) target_link_libraries(Clipper2 PUBLIC -lm) diff --git a/deps_src/miniz/CMakeLists.txt b/deps_src/miniz/CMakeLists.txt index e02d8a4885..7e060a180f 100644 --- a/deps_src/miniz/CMakeLists.txt +++ b/deps_src/miniz/CMakeLists.txt @@ -11,6 +11,8 @@ add_library(miniz_static STATIC if(${CMAKE_C_COMPILER_ID} STREQUAL "GNU") target_compile_definitions(miniz_static PRIVATE _GNU_SOURCE) +elseif (CMAKE_C_COMPILER_ID STREQUAL "Clang" AND CMAKE_C_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(miniz_static PRIVATE /clang:-Wno-error=incompatible-pointer-types) endif() target_link_libraries(miniz INTERFACE miniz_static) From 0a7ac3f2ac7af58402447fdaea37982dd7ff282d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:59 -0300 Subject: [PATCH 07/71] fix: disable TBB LTCG to allow linking with lld-link oneTBB enables MSVC IPO/LTCG by default, which emits MSVC proprietary bitcode objects when built with cl.exe. lld-link cannot consume those /GL objects. Patch the TBB MSVC compiler settings so IPO can be disabled and the dependency produces native COFF objects that both link.exe and lld-link can consume. --- deps/TBB/MSVC.cmake | 98 +++++++++++++++++++++++++++++++++++++++++++++ deps/TBB/TBB.cmake | 6 ++- 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 deps/TBB/MSVC.cmake diff --git a/deps/TBB/MSVC.cmake b/deps/TBB/MSVC.cmake new file mode 100644 index 0000000000..d7984bff80 --- /dev/null +++ b/deps/TBB/MSVC.cmake @@ -0,0 +1,98 @@ +# Copyright (c) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(TBB_LINK_DEF_FILE_FLAG ${CMAKE_LINK_DEF_FILE_FLAG}) +set(TBB_DEF_FILE_PREFIX win${TBB_ARCH}) + +# Workaround for CMake issue https://gitlab.kitware.com/cmake/cmake/issues/18317. +# TODO: consider use of CMP0092 CMake policy. +string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + +set(TBB_WARNING_LEVEL $<$:/W4> $<$:/WX>) + +# Warning suppression C4324: structure was padded due to alignment specifier +set(TBB_WARNING_SUPPRESS /wd4324) +set(TBB_TEST_COMPILE_FLAGS /bigobj) + +if (MSVC_VERSION LESS_EQUAL 1900) + # Warning suppression C4503 for VS2015 and earlier: + # decorated name length exceeded, name was truncated. + # More info can be found at + # https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-1-c4503 + set(TBB_TEST_COMPILE_FLAGS ${TBB_TEST_COMPILE_FLAGS} /wd4503) +endif() + +set(TBB_LIB_COMPILE_FLAGS -D_CRT_SECURE_NO_WARNINGS /GS) +set(TBB_COMMON_COMPILE_FLAGS /volatile:iso /FS /EHsc) + +# Ignore /WX set through add_compile_options() or added to CMAKE_CXX_FLAGS if TBB_STRICT is disabled. +if (NOT TBB_STRICT AND COMMAND tbb_remove_compile_flag) + tbb_remove_compile_flag(/WX) +endif() + +if (WINDOWS_STORE OR TBB_WINDOWS_DRIVER) + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D_WIN32_WINNT=0x0A00) + set(TBB_COMMON_LINK_FLAGS -NODEFAULTLIB:kernel32.lib -INCREMENTAL:NO) + set(TBB_COMMON_LINK_LIBS OneCore.lib) +endif() + +if (WINDOWS_STORE) + if (NOT CMAKE_SYSTEM_VERSION EQUAL 10.0) + message(FATAL_ERROR "CMAKE_SYSTEM_VERSION must be equal to 10.0") + endif() + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /ZW /ZW:nostdlib) + # CMake define this extra lib, remove it for this build type + string(REGEX REPLACE "WindowsApp.lib" "" CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES}") + + if (TBB_NO_APPCONTAINER) + set(TBB_LIB_LINK_FLAGS ${TBB_LIB_LINK_FLAGS} -APPCONTAINER:NO) + endif() +endif() + +if (TBB_WINDOWS_DRIVER) + # Since this is universal driver disable this variable + set(CMAKE_SYSTEM_PROCESSOR "") + # CMake define list additional libs, remove it for this build type + set(CMAKE_CXX_STANDARD_LIBRARIES "") + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D _UNICODE /DUNICODE /DWINAPI_FAMILY=WINAPI_FAMILY_APP /D__WRL_NO_DEFAULT_LIB__) +endif() + +if (NOT DEFINED TBB_ENABLE_IPO) + if (DEFINED CMAKE_INTERPROCEDURAL_OPTIMIZATION) + set(TBB_ENABLE_IPO ${CMAKE_INTERPROCEDURAL_OPTIMIZATION}) + else() + set(TBB_ENABLE_IPO ON) + endif() +endif() + +if (TBB_ENABLE_IPO) + if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)") + if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)") + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg) + endif() + set(TBB_OPENMP_NO_LINK_FLAG TRUE) + set(TBB_IPO_COMPILE_FLAGS $<$>:-flto>) + else() + set(TBB_IPO_COMPILE_FLAGS $<$>:/GL>) + set(TBB_IPO_LINK_FLAGS $<$>:-LTCG> $<$>:-INCREMENTAL:NO>) + endif() +else() + if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)" AND CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)") + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg) + endif() + set(TBB_IPO_COMPILE_FLAGS "") + set(TBB_IPO_LINK_FLAGS "") +endif() + +set(TBB_OPENMP_FLAG /openmp) diff --git a/deps/TBB/TBB.cmake b/deps/TBB/TBB.cmake index 9b1452d33e..dac2ed63e6 100644 --- a/deps/TBB/TBB.cmake +++ b/deps/TBB/TBB.cmake @@ -1,4 +1,6 @@ -if (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") +if (MSVC) + set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/MSVC.cmake ./cmake/compilers/MSVC.cmake) +elseif (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/GNU.cmake ./cmake/compilers/GNU.cmake) else() set(_patch_command "") @@ -13,6 +15,8 @@ orcaslicer_add_cmake_project( -DTBB_BUILD_SHARED=OFF -DTBB_BUILD_TESTS=OFF -DTBB_TEST=OFF + -DTBB_ENABLE_IPO=OFF + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_DEBUG_POSTFIX=_debug ) From 08128911e32cc70da42045d3d0c516c468d187d3 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:59 -0300 Subject: [PATCH 08/71] fix: copy runtime DLLs for Ninja generator on Windows The runtime DLL copy was nested under CMAKE_CONFIGURATION_TYPES, so it only ran for multi-config generators. Ninja single-config leaves that variable empty, which skipped copying OCCT, GMP, MPFR, WebView2, and freetype DLLs next to the executable. Run the copy logic for both generator styles while keeping it Windows-only. --- src/CMakeLists.txt | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 73c767dad2..6ccd480e3b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -212,14 +212,6 @@ if (WIN32) VERBATIM ) endforeach () - - if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") - orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug) - elseif("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo") - orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_Release) - else() - orcaslicer_copy_dlls(COPY_DLLS "Release" "" output_dlls_Release) - endif() else () file(TO_NATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}/resources" WIN_RESOURCES_SYMLINK) add_custom_command(TARGET OrcaSlicer POST_BUILD @@ -229,6 +221,27 @@ if (WIN32) ) endif () + if (CMAKE_CONFIGURATION_TYPES) + # Multi-config generators (Visual Studio, Ninja Multi-Config): copy per config. + foreach (cfg ${CMAKE_CONFIGURATION_TYPES}) + if ("${cfg}" STREQUAL "Debug") + orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug) + elseif("${cfg}" STREQUAL "RelWithDebInfo") + orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_RelWithDebInfo) + else() + orcaslicer_copy_dlls(COPY_DLLS "${cfg}" "" output_dlls_${cfg}) + endif() + endforeach() + else() + # Single-config generators (Ninja): use CMAKE_BUILD_TYPE. + if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug) + elseif("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo") + orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_RelWithDebInfo) + else() + orcaslicer_copy_dlls(COPY_DLLS "Release" "" output_dlls_Release) + endif() + endif() else () if (APPLE AND NOT CMAKE_MACOSX_BUNDLE) From adb0ca3dc316e216d7eb5dbb60a009c78fd5f95d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:36:59 -0300 Subject: [PATCH 09/71] fix: use Ninja-compatible build target in build_release_vs.bat ALL_BUILD is a Visual Studio generator target. Ninja uses `all`. When building with -x (Ninja generator), the script fails with "ninja: error: unknown target 'ALL_BUILD'". Use the correct target name for each generator. --- build_release_vs.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_release_vs.bat b/build_release_vs.bat index 3f1a2e0af4..cc3744ae97 100644 --- a/build_release_vs.bat +++ b/build_release_vs.bat @@ -133,7 +133,7 @@ echo on set CMAKE_POLICY_VERSION_MINIMUM=3.5 if "%USE_NINJA%"=="1" ( cmake .. -G %CMAKE_GENERATOR% -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type% - cmake --build . --config %build_type% --target ALL_BUILD + cmake --build . --config %build_type% --target all ) else ( cmake .. -G %CMAKE_GENERATOR% -A x64 -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type% cmake --build . --config %build_type% --target ALL_BUILD -- -m From 5afc6ae9b2f41353d4e8a3be78aa2cc78bda75dc Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 24 Jun 2026 00:37:00 -0300 Subject: [PATCH 10/71] fix: wxWidgets target path for clang-cl on Windows wxWidgets chooses target files based on the compiler id, which makes clang-cl look under the clang_x64_lib layout. The dependencies are built with MSVC naming/layout, and clang-cl uses the MSVC frontend variant on Windows. Patch the generated wxWidgets config so clang-cl loads the vc_x64_lib targets instead. --- deps/wxWidgets/0001-Clang-CL-fix.patch | 23 +++++++++++++++++++++++ deps/wxWidgets/wxWidgets.cmake | 1 + 2 files changed, 24 insertions(+) create mode 100644 deps/wxWidgets/0001-Clang-CL-fix.patch diff --git a/deps/wxWidgets/0001-Clang-CL-fix.patch b/deps/wxWidgets/0001-Clang-CL-fix.patch new file mode 100644 index 0000000000..4765b67c36 --- /dev/null +++ b/deps/wxWidgets/0001-Clang-CL-fix.patch @@ -0,0 +1,23 @@ +--- + build/cmake/wxWidgetsConfig.cmake.in | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/build/cmake/wxWidgetsConfig.cmake.in b/build/cmake/wxWidgetsConfig.cmake.in +index 1a83f36..70ad8a4 100644 +--- a/build/cmake/wxWidgetsConfig.cmake.in ++++ b/build/cmake/wxWidgetsConfig.cmake.in +@@ -58,7 +58,11 @@ if(WIN32_MSVC_NAMING) + endif() + endif() + +-include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake") ++if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") ++ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/vc_x64_lib/@PROJECT_NAME@Targets.cmake") ++else() ++ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake") ++endif() + + macro(wx_inherit_property source dest name) + # property name without _ +-- +2.43.0 diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake index 682b28bac4..a1ed532ef2 100644 --- a/deps/wxWidgets/wxWidgets.cmake +++ b/deps/wxWidgets/wxWidgets.cmake @@ -27,6 +27,7 @@ orcaslicer_add_cmake_project( GIT_TAG v3.3.2 GIT_SHALLOW ON DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG} + PATCH_COMMAND git apply --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-Clang-CL-fix.patch CMAKE_ARGS -DwxBUILD_PRECOMP=ON ${_wx_toolkit} From 56c28fc102f399d650de2c0449058a7649695ea1 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 28 Jul 2026 19:17:05 +0800 Subject: [PATCH 11/71] feat: refactor notebook/tabs to be string based instead of fixed index based --- src/slic3r/GUI/Downloader.cpp | 2 +- src/slic3r/GUI/Field.cpp | 6 +- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/GUI_App.cpp | 20 ++-- src/slic3r/GUI/MainFrame.cpp | 137 +++++++++++++++---------- src/slic3r/GUI/MainFrame.hpp | 29 +++--- src/slic3r/GUI/Notebook.cpp | 2 + src/slic3r/GUI/Notebook.hpp | 76 ++++++++++++-- src/slic3r/GUI/NotificationManager.cpp | 12 +-- src/slic3r/GUI/Plater.cpp | 67 ++++++------ src/slic3r/GUI/PresetComboBoxes.cpp | 2 +- src/slic3r/GUI/SelectMachine.cpp | 4 +- src/slic3r/GUI/SyncAmsInfoDialog.cpp | 4 +- src/slic3r/GUI/Tab.cpp | 2 +- src/slic3r/Utils/PrintHost.cpp | 2 +- src/slic3r/Utils/SimplyPrint.cpp | 2 +- 16 files changed, 233 insertions(+), 136 deletions(-) diff --git a/src/slic3r/GUI/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp index c61b2716fc..0d37a0eca6 100644 --- a/src/slic3r/GUI/Downloader.cpp +++ b/src/slic3r/GUI/Downloader.cpp @@ -134,7 +134,7 @@ void Downloader::start_download(const std::string& full_url) Plater* plater = wxGetApp().plater(); mainframe->Freeze(); - mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + mainframe->select_tab(TAB_ID_PREPARE); plater->select_view_3D("3D"); plater->select_view("plate"); plater->get_current_canvas3D()->zoom_to_bed(); diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index bcbc381eff..532091313c 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -330,8 +330,10 @@ void Field::PostInitialize() } default: break; } - if (tab_id >= 0) - wxGetApp().mainframe->select_tab(tab_id); + if (tab_id >= 0) { + static constexpr const char* kShortcutTabIds[] = {TAB_ID_HOME, TAB_ID_PREPARE, TAB_ID_PREVIEW, TAB_ID_MONITOR}; + wxGetApp().mainframe->select_tab(kShortcutTabIds[tab_id]); + } if (tab_id > 0) // tab panel should be focused for correct navigation between tabs wxGetApp().tab_panel()->SetFocus(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index a7b063890f..58de15355d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -9132,7 +9132,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() view3d_canvas->get_gizmos_manager().reset_all_states(); // close all gizmos view3d_canvas->reload_scene(true); } - app.mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + app.mainframe->select_tab(TAB_ID_PREPARE); } } }); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 29aad072f0..1290df70e4 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -798,12 +798,12 @@ void GUI_App::post_init() m_open_method = "url"; } else { if (this->init_params->input_gcode) { - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); this->plater()->load_gcode(from_u8(this->init_params->input_files.front())); m_open_method = "gcode"; } else { - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); wxArrayString input_files; for (auto& file : this->init_params->input_files) { @@ -837,7 +837,7 @@ void GUI_App::post_init() mainframe->Freeze(); #endif plater_->canvas3D()->enable_render(false); - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); //BBS init the opengl resource here if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() || @@ -875,9 +875,9 @@ void GUI_App::post_init() } } if (is_editor()) - mainframe->select_tab(size_t(0)); + mainframe->select_tab(TAB_ID_HOME); if (app_config->get("default_page") == "1") - mainframe->select_tab(size_t(1)); + mainframe->select_tab(TAB_ID_PREPARE); #ifndef __linux__ mainframe->Thaw(); #endif @@ -1814,10 +1814,10 @@ bool GUI_App::hot_reload_network_plugin() wxWindowDisabler disabler; if (mainframe) { - int current_tab = mainframe->m_tabpanel->GetSelection(); - if (current_tab == MainFrame::TabPosition::tpMonitor) { + wxString current_tab = mainframe->m_tabpanel->GetSelectedPageName(); + if (current_tab == TAB_ID_MONITOR) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": navigating away from Monitor tab before unload"; - mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tp3DEditor); + mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREPARE); } } @@ -3323,7 +3323,7 @@ bool GUI_App::on_init_inner() mainframe = new MainFrame(); // hide settings tabs after first Layout if (is_editor()) { - mainframe->select_tab(size_t(0)); + mainframe->select_tab(TAB_ID_HOME); } sidebar().obj_list()->init(); @@ -4487,7 +4487,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name) mainframe = new MainFrame(); if (is_editor()) // hide settings tabs after first Layout - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); // Propagate model objects to object list. sidebar().obj_list()->init(); //sidebar().aux_list()->init_auxiliary(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index f55ce7cebc..e2b94d11ff 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -493,9 +493,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ }); //BBS - Bind(EVT_SELECT_TAB, [this](wxCommandEvent&evt) { - TabPosition pos = (TabPosition)evt.GetInt(); - m_tabpanel->SetSelection(pos); + Bind(EVT_SELECT_TAB, [this](wxCommandEvent& evt) { + m_tabpanel->SelectPageByName(evt.GetString()); }); Bind(EVT_SYNC_CLOUD_PRESET, &MainFrame::on_select_default_preset, this); @@ -702,7 +701,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #endif - if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; } + if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; } if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); m_print_enable = get_enable_print_status(); @@ -723,7 +722,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;} else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;} if (evt.CmdDown() && evt.GetKeyCode() == 'F') { - if (m_plater && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview)) { + if (m_plater && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW)) { m_plater->sidebar().can_search(); } } @@ -1007,8 +1006,8 @@ void MainFrame::update_layout() m_layout = layout; // From the very beginning the Print settings should be selected - //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? 0 : 1; - m_last_selected_tab = 1; + //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? TAB_ID_HOME : TAB_ID_PREPARE; + m_last_selected_tab = TAB_ID_PREPARE; // Set new settings switch (m_layout) @@ -1016,14 +1015,18 @@ void MainFrame::update_layout() case ESettingsLayout::Old: { m_plater->Reparent(m_tabpanel); - m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); - m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); + { + const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME); + const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast(home_idx) + 1; + m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); + m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); + } m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0); m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt) { // jump to 3deditor under preview_only mode - if (evt.GetId() == tp3DEditor){ + if (evt.GetId() == m_tabpanel->FindPageByName(TAB_ID_PREPARE)) { Sidebar& sidebar = GUI::wxGetApp().sidebar(); if (sidebar.need_auto_sync_after_connect_printer()) { sidebar.set_need_auto_sync_after_connect_printer(false); @@ -1252,15 +1255,14 @@ void MainFrame::init_tabpanel() { #endif //BBS wxWindow* panel = m_tabpanel->GetCurrentPage(); - int sel = m_tabpanel->GetSelection(); //wxString page_text = m_tabpanel->GetPageText(sel); - m_last_selected_tab = m_tabpanel->GetSelection(); + m_last_selected_tab = m_tabpanel->GetSelectedPageName(); if (panel == m_plater) { - if (sel == tp3DEditor) { + if (m_last_selected_tab == TAB_ID_PREPARE) { wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); m_param_panel->OnActivate(); } - else if (sel == tpPreview) { + else if (m_last_selected_tab == TAB_ID_PREVIEW) { m_plater->reset_check_status(); if (!m_plater->check_ams_status(m_slice_select == eSliceAll)) return; @@ -1275,7 +1277,7 @@ void MainFrame::init_tabpanel() { //monitor } #ifndef __APPLE__ - if (sel == tp3DEditor) { + if (m_last_selected_tab == TAB_ID_PREPARE) { m_topbar->EnableUndoRedoItems(); } else { @@ -1309,10 +1311,10 @@ void MainFrame::init_tabpanel() { m_webview = new WebViewPanel(m_tabpanel); Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) { wxString url = evt.GetString(); - select_tab(MainFrame::tpHome); + select_tab(TAB_ID_HOME); m_webview->load_url(url); }); - m_tabpanel->AddPage(m_webview, "", "tab_home_active", "tab_home_active", false); + m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active", "tab_home_active", false); m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); } @@ -1327,7 +1329,7 @@ void MainFrame::init_tabpanel() { //BBS add pages m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_monitor->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); + m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); m_printer_view = new PrinterWebView(m_tabpanel); Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { @@ -1342,16 +1344,17 @@ void MainFrame::init_tabpanel() { m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_multi_machine->SetBackgroundColour(*wxWHITE); // TODO: change the bitmap - m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false); + m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false); } m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_project->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false); + m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false); m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + if (m_plater) { // load initial config @@ -1387,7 +1390,11 @@ void MainFrame::show_device(bool bBBLPrinter) { m_monitor->SetBackgroundColour(*wxWHITE); } m_monitor->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active")); + { + const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); + const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; + m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active")); + } if (wxGetApp().is_enable_multi_machine()) { if (!m_multi_machine) { @@ -1396,17 +1403,22 @@ void MainFrame::show_device(bool bBBLPrinter) { } // TODO: change the bitmap m_multi_machine->Show(false); - m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), - std::string("tab_multi_active"), false); + { + const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); + const size_t multi_pos = (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; + m_tabpanel->InsertPage(multi_pos, TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), + std::string("tab_multi_active"), false); + } } if (!m_calibration) { m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); } m_calibration->Show(false); - // Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled, - // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position. - m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), + // Calibration is always appended last (AddPage), so it lands after whichever of Monitor/Multi-device + // actually got inserted above — no longer position-sensitive now that insertion position is computed + // from FindPageByName rather than a fixed TabPosition index. + m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); #ifdef _MSW_DARK_MODE @@ -1440,8 +1452,12 @@ void MainFrame::show_device(bool bBBLPrinter) { }); } m_printer_view->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), std::string("tab_monitor_active"), - std::string("tab_monitor_active")); + { + const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); + const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; + m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_printer_view, _L("Device"), std::string("tab_monitor_active"), + std::string("tab_monitor_active")); + } } fit_tab_labels(); // ORCA on printer change } @@ -1475,7 +1491,7 @@ void MainFrame::fit_tab_labels() bool MainFrame::preview_only_hint() { if (m_plater && (m_plater->only_gcode_mode() || (m_plater->using_exported_file()))) { - BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelection() %tp3DEditor; + BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelectedPageName() %wxString(TAB_ID_PREPARE); ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning")); confirm_dlg.Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent& e) { @@ -1793,22 +1809,22 @@ bool MainFrame::can_clone() const { bool MainFrame::can_select() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty(); } bool MainFrame::can_deselect() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty(); } bool MainFrame::can_delete() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty(); } bool MainFrame::can_delete_all() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty(); } bool MainFrame::can_reslice() const @@ -1917,7 +1933,7 @@ wxBoxSizer* MainFrame::create_side_tools() wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - this->m_tabpanel->SetSelection(tpPreview); + this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } }); @@ -3063,7 +3079,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective")); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, + this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, [this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this); viewMenu->AppendSeparator(); @@ -3072,7 +3088,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_gcode_window(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == tpPreview; }, + this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, [this]() { return wxGetApp().show_gcode_window(); }, this); append_menu_check_item( @@ -3081,7 +3097,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_3d_navigator(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, + this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, [this]() { return wxGetApp().show_3d_navigator(); }, this); append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"), @@ -3089,14 +3105,14 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_plate_gridlines(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, this, - [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, + [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, [this]() { return wxGetApp().show_plate_gridlines(); }, this); append_menu_item( viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"), [this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this, [this]() { - return (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview) && + return (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW) && m_plater->is_sidebar_enabled(); }, this); @@ -3119,7 +3135,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_outline(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor; }, + this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE; }, [this]() { return wxGetApp().show_outline(); }, this); /*viewMenu->AppendSeparator(); @@ -3920,13 +3936,18 @@ void MainFrame::select_tab(wxPanel* panel) wxGetApp().params_dialog()->Popup(); return; } + // page_name cannot be resolved via panel->GetName() — Prepare and Preview + // share the single m_plater window, so the window itself has no single correct + // name (see Global Constraints). Resolve via Notebook's per-slot m_pageNames + // instead, via the index -> id lookup, which works for any page (built-in or not). int page_idx = m_tabpanel->FindPage(panel); - if (page_idx == tp3DEditor && m_tabpanel->GetSelection() == tpPreview) + wxString page_name = (page_idx == wxNOT_FOUND) ? wxString() : m_tabpanel->GetPageName(static_cast(page_idx)); + if (page_name == TAB_ID_PREPARE && m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW) return; //BBS GUI refactor: remove unused layout new/dlg /*if (page_idx != wxNOT_FOUND && m_layout == ESettingsLayout::Dlg) page_idx++;*/ - select_tab(size_t(page_idx)); + select_tab(page_name); } //BBS @@ -3934,7 +3955,7 @@ void MainFrame::jump_to_monitor(std::string dev_id) { if(!m_monitor) return; - m_tabpanel->SetSelection(tpMonitor); + m_tabpanel->SelectPageByName(TAB_ID_MONITOR); if (!dev_id.empty()) { ((MonitorPanel*)m_monitor)->select_machine(dev_id); } @@ -3944,26 +3965,26 @@ void MainFrame::jump_to_multipage() { if(!m_multi_machine) return; - m_tabpanel->SetSelection(tpMultiDevice); + m_tabpanel->SelectPageByName(TAB_ID_MULTI_DEVICE); ((MultiMachinePage*)m_multi_machine)->jump_to_send_page(); } //BBS GUI refactor: remove unused layout new/dlg -void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) +void MainFrame::select_tab(const wxString& id/* = wxString()*/) { //bool tabpanel_was_hidden = false; // Controls on page are created on active page of active tab now. // We should select/activate tab before its showing to avoid an UI-flickering - auto select = [this, tab](bool was_hidden) { - // when tab == -1, it means we should show the last selected tab + auto select = [this, id](bool was_hidden) { + // when id is empty, it means we should show the last selected tab //BBS GUI refactor: remove unused layout new/dlg //size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : (m_layout == ESettingsLayout::Dlg && tab != 0) ? tab - 1 : tab; - size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : tab; + wxString new_selection = id.empty() ? m_last_selected_tab : id; - if (m_tabpanel->GetSelection() != (int)new_selection) - m_tabpanel->SetSelection(new_selection); + if (m_tabpanel->GetSelectedPageName() != new_selection) + m_tabpanel->SelectPageByName(new_selection); #ifdef _MSW_DARK_MODE /*if (wxGetApp().tabs_as_menu()) { if (Tab* cur_tab = dynamic_cast(m_tabpanel->GetPage(new_selection))) @@ -3972,10 +3993,14 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) m_plater->get_current_canvas3D()->render(); }*/ #endif - if (tab == MainFrame::tp3DEditor && m_layout == ESettingsLayout::Old) + // NOTE: this checks the ORIGINAL parameter (id), not the resolved new_selection — + // preserving that the fallback-to-last-tab path never triggers this render call + // even if the last selected tab happened to be Prepare. Do not "simplify" to + // new_selection == TAB_ID_PREPARE, that changes behavior. + if (id == TAB_ID_PREPARE && m_layout == ESettingsLayout::Old) m_plater->canvas3D()->render(); else if (was_hidden) { - Tab* cur_tab = dynamic_cast(m_tabpanel->GetPage(new_selection)); + Tab* cur_tab = dynamic_cast(m_tabpanel->GetPageByName(new_selection)); if (cur_tab) cur_tab->OnActivate(); } @@ -3984,10 +4009,10 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) select(false); } -void MainFrame::request_select_tab(TabPosition pos) +void MainFrame::request_select_tab(const wxString& id) { wxCommandEvent* evt = new wxCommandEvent(EVT_SELECT_TAB); - evt->SetInt(pos); + evt->SetString(id); wxQueueEvent(this, evt); } @@ -4267,7 +4292,7 @@ void MainFrame::load_printer_url() } } -bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelection() == TabPosition::tpMonitor; } +bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelectedPageName() == TAB_ID_MONITOR; } void MainFrame::refresh_plugin_tips() diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 20229a611e..9bb02b28a6 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -36,6 +36,16 @@ #include "calib_dlg.hpp" #include "MultiMachinePage.hpp" +// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are +// names rather than positional indices so optional pages cannot shift them. +#define TAB_ID_HOME "home" +#define TAB_ID_PREPARE "prepare" +#define TAB_ID_PREVIEW "preview" +#define TAB_ID_MONITOR "monitor" +#define TAB_ID_MULTI_DEVICE "multi_device" +#define TAB_ID_PROJECT "project" +#define TAB_ID_CALIBRATION "calibration" + #define ENABEL_PRINT_ALL 0 class Notebook; @@ -115,7 +125,7 @@ class MainFrame : public DPIFrame wxMenuItem* m_menu_item_reslice_now { nullptr }; wxSizer* m_main_sizer{ nullptr }; - size_t m_last_selected_tab; + wxString m_last_selected_tab; std::string get_base_name(const wxString &full_name, const char *extension = nullptr) const; std::string get_dir_name(const wxString &full_name) const; @@ -214,19 +224,6 @@ public: #ifdef __APPLE__ bool get_mac_full_screen() { return m_mac_fullscreen; } #endif - //BBS GUI refactor - enum TabPosition - { - tpHome = 0, - tp3DEditor = 1, - tpPreview = 2, - tpMonitor = 3, - tpMultiDevice = 4, - tpProject = 5, - tpCalibration = 6, - tpAuxiliary = 7, - toDebugTool = 8, - }; //BBS: add slice&&print status update logic enum SlicePrintEventType @@ -326,8 +323,8 @@ public: // When tab == -1, will be selected last selected tab //BBS: GUI refactor void select_tab(wxPanel* panel); - void select_tab(size_t tab = size_t(-1)); - void request_select_tab(TabPosition pos); + void select_tab(const wxString& id = wxString()); + void request_select_tab(const wxString& id); int get_calibration_curr_tab(); void select_view(const std::string& direction); // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index ceda3fc0d6..a6906b4550 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -253,6 +253,8 @@ void Notebook::Init() m_showTimeout = m_hideTimeout = 0; + m_pageNames.clear(); + /* On Linux, Gstreamer wxMediaCtrl does not seem to get along well with * 32-bit X11 visuals (the overlay does not work). Is this a wxWindows * bug? Is this a Gstreamer bug? No idea, but it is our problem ... diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index d333956561..a82be7be56 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -3,6 +3,7 @@ //#ifdef _WIN32 +#include #include #include @@ -42,7 +43,7 @@ private: std::vector m_pageLabels; // ORCA }; -class Notebook: public wxBookCtrlBase +class Notebook : public wxBookCtrlBase { public: Notebook(wxWindow * parent, @@ -103,7 +104,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(page, wxString(), "", ""); + return AddPage(wxString(), page, wxString(), "", ""); } @@ -136,14 +137,15 @@ public: // Implement base class pure virtual methods. // adds a new page to the control - bool AddPage(wxWindow* page, + bool AddPage(const wxString& id, + wxWindow* page, const wxString& text, const std::string& bmp_name, const std::string& inactive_bmp_name, bool bSelect = false) { DoInvalidateBestSize(); - return InsertPage(GetPageCount(), page, text, bmp_name, inactive_bmp_name, bSelect); + return InsertPage(GetPageCount(), id, page, text, bmp_name, inactive_bmp_name, bSelect); } // Page management @@ -156,6 +158,7 @@ public: if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) return false; + m_pageNames.insert(m_pageNames.begin() + n, wxString()); GetBtnsListCtrl()->InsertPage(n, text, bSelect); if (!DoSetSelectionAfterInsertion(n, bSelect)) @@ -165,6 +168,7 @@ public: } bool InsertPage(size_t n, + const wxString& id, wxWindow * page, const wxString & text, const std::string& bmp_name = "", @@ -174,10 +178,17 @@ public: if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return false; + m_pageNames.insert(m_pageNames.begin() + n, id); GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name); - if (bSelect) - SetSelection(n); + // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the + // new page to the current page's rect — it never touches visibility. A freshly + // constructed page defaults to shown, so without this it renders on top of + // whatever page is currently selected until the next SetSelection() call hides + // it. Mirrors the pure-virtual InsertPage() override above, which already does + // this correctly. + if (!DoSetSelectionAfterInsertion(n, bSelect)) + page->Hide(); return true; } @@ -251,8 +262,58 @@ public: page->SetFocus(); } + // wxBookCtrlBase::DeleteAllPages() clears its page list directly rather than + // going through DoRemovePage() per page, so it would otherwise leave + // m_pageNames desynchronized (a mutation path outside the four this class + // already keeps in sync). Not currently called on a Notebook anywhere in + // this codebase, but kept correct for the same reason the rest of this + // bookkeeping exists. + virtual bool DeleteAllPages() override + { + m_pageNames.clear(); + return wxBookCtrlBase::DeleteAllPages(); + } + ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast(m_bookctrl); } + int FindPageByName(const wxString& id) const + { + if (id.empty()) + return wxNOT_FOUND; + for (size_t i = 0; i < m_pageNames.size(); ++i) + if (m_pageNames[i] == id) + return static_cast(i); + return wxNOT_FOUND; + } + + wxWindow* GetPageByName(const wxString& id) const + { + const int idx = FindPageByName(id); + return idx == wxNOT_FOUND ? nullptr : GetPage(static_cast(idx)); + } + + bool SelectPageByName(const wxString& id) + { + const int idx = FindPageByName(id); + if (idx == wxNOT_FOUND) + return false; + SetSelection(static_cast(idx)); + return true; + } + + // Inverse of FindPageByName: index -> id. Empty string for an out-of-range + // index or a page that was never given an id (e.g. settings Tab pages). + wxString GetPageName(size_t n) const + { + return n < m_pageNames.size() ? m_pageNames[n] : wxString(); + } + + wxString GetSelectedPageName() const + { + const int sel = GetSelection(); + return sel < 0 ? wxString() : GetPageName(static_cast(sel)); + } + void UpdateMode() { GetBtnsListCtrl()->UpdateMode(); @@ -369,6 +430,7 @@ protected: wxWindow* const win = wxBookCtrlBase::DoRemovePage(page); if (win) { + m_pageNames.erase(m_pageNames.begin() + page); GetBtnsListCtrl()->RemovePage(page); DoSetSelectionAfterRemoval(page); } @@ -394,6 +456,8 @@ protected: private: void Init(); + std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages + wxShowEffect m_showEffect, m_hideEffect; diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 8e81f0654c..5e83ad845f 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1918,7 +1918,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); } else { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } return false; }; @@ -1985,7 +1985,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException wxGetApp().sidebar().jump_to_option(opt, opt_type, L""); } else { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } return false; }; @@ -2015,7 +2015,7 @@ void NotificationManager::push_slicing_error_notification(const std::string &tex if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); } } if (!ovs.empty()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items(ovs); } return false; @@ -2046,7 +2046,7 @@ void NotificationManager::push_slicing_warning_notification(const std::string& t auto& objects = wxGetApp().model().objects; auto iter = std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }); if (iter != objects.end()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items({ {*iter, nullptr} }); } return false; @@ -2693,7 +2693,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); } } if (!ovs.empty()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items(ovs); wxGetApp().obj_list()->update_selections_on_canvas(); } @@ -2777,7 +2777,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s } } - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (!sel_items.empty()) { obj_list->select_items(sel_items); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index feecdfa163..66a9fa88db 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5787,7 +5787,7 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi #endif // WIN32 m_mainframe.Raise(); - m_mainframe.select_tab(size_t(MainFrame::tp3DEditor)); + m_mainframe.select_tab(TAB_ID_PREPARE); if (wxGetApp().is_editor()) m_plater.select_view_3D("3D"); @@ -6569,9 +6569,9 @@ void Plater::priv::select_next_view_3D() { if (current_panel == view3D) - wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview)); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); else if (current_panel == preview) - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); // else if (current_panel == assemble_view) // set_current_panel(view3D); } @@ -7870,7 +7870,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ q->select_plate(first_plate_index); //set to 3d tab q->select_view_3D("Preview"); - wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); } else { //set to 3d tab @@ -7889,7 +7889,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ else { //always set to 3D after loading files q->select_view_3D("3D"); - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } if (load_model) { @@ -8797,7 +8797,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni } } - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (inst_idx != -1) { auto* model = wxGetApp().obj_list()->GetModel(); @@ -8826,7 +8826,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni } else { auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end(); if (iter != objects.end()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items({{*iter, nullptr}}); wxGetApp().obj_list()->update_selections_on_canvas(); } @@ -11208,13 +11208,20 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } const int new_sel = e.GetSelection(); - sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview; + if (new_sel == wxNOT_FOUND) { + // Guards against new_sel matching FindPageByName's own wxNOT_FOUND sentinel + // below when a TAB_ID_* isn't currently present in the tabpanel. + e.Skip(); + return; + } + sidebar_layout.show = new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_PREPARE) || + new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_PREVIEW); update_sidebar(); int old_sel = e.GetOldSelection(); const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin(); const bool use_native_device_tab = wxGetApp().preset_bundle && (wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin); - if (use_native_device_tab && new_sel == MainFrame::tpMonitor) { + if (use_native_device_tab && new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR)) { // BBL network module is only required for BBL-vendor printers. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. if (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { @@ -11226,7 +11233,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } } else { - if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { + if (new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR) && wxGetApp().preset_bundle != nullptr) { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); if (main_frame->m_printer_view && url.empty()) { @@ -12094,7 +12101,7 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); else wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - wxGetApp().mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tpPreview); + wxGetApp().mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return false; } @@ -13051,7 +13058,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ get_notification_manager()->clear_all(); if (!silent) - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); //get_partplate_list().reinit(); //get_partplate_list().update_slice_context_to_current_plate(p->background_process); @@ -13200,7 +13207,7 @@ void Plater::load_project(wxString const& filename2, if (!m_exported_file) { p->select_view("topfront"); p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } else { p->partplate_list.select_plate_view(); @@ -13314,7 +13321,7 @@ void Plater::import_model_id(wxString download_info) const int max_retries = 3; /* jump to 3D eidtor */ - wxGetApp().mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); /* prepare progress dialog */ bool cont = true; @@ -13623,7 +13630,7 @@ void Plater::calib_pa(const Calib_Params& params) { const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); new_project(false, false, calib_pa_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false)); @@ -14104,7 +14111,7 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { if (new_project(false, false, calib_name) == wxID_CANCEL) return; - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (is_linear) { if (pass == 1) @@ -14141,7 +14148,7 @@ void Plater::calib_temp(const Calib_Params& params) { const auto calib_temp_name = wxString::Format(L"Nozzle temperature test"); new_project(false, false, calib_temp_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Temp_Tower) return; if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc")) @@ -14218,7 +14225,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) { const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test"); new_project(false, false, calib_vol_speed_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Vol_speed_Tower) return; if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc")) @@ -14297,7 +14304,7 @@ void Plater::calib_retraction(const Calib_Params& params) { const auto calib_retraction_name = wxString::Format(L"Retraction"); new_project(false, false, calib_retraction_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Retraction_tower) return; @@ -14357,7 +14364,7 @@ void Plater::calib_VFA(const Calib_Params& params) { const auto calib_vfa_name = wxString::Format(L"VFA test"); new_project(false, false, calib_vfa_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_VFA_Tower) return; @@ -14403,7 +14410,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) { const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test"); new_project(false, false, calib_input_shaping_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Input_shaping_freq) return; @@ -14469,7 +14476,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) { const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test"); new_project(false, false, calib_input_shaping_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Input_shaping_damp) return; @@ -14534,7 +14541,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) { const auto Calib_Cornering = wxString::Format(L"Cornering test"); new_project(false, false, Calib_Cornering); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Cornering) return; @@ -14667,7 +14674,7 @@ void Plater::load_gcode(const wxString& filename) //p->gcode_result.reset(); //reset_gcode_toolpaths(); p->preview->reload_print(m_only_gcode); - wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); p->set_current_panel(p->preview, true); p->get_current_canvas3D()->render(); //p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); @@ -15340,7 +15347,7 @@ LoadType determine_load_type(std::string filename, std::string override_setting) wxGetApp().app_config->set("import_project_action", std::to_string(choice)); // BBS: jump to plater panel - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); return load_type; } @@ -15569,7 +15576,7 @@ void Plater::reset_with_confirm() .ShowModal() == wxID_YES) { reset(); // BBS: jump to plater panel - wxGetApp().mainframe->select_tab(size_t(0)); + wxGetApp().mainframe->select_tab(TAB_ID_HOME); } } @@ -17383,7 +17390,7 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn) //BBS void Plater::send_calibration_job_finished(wxCommandEvent & evt) { - p->main_frame->request_select_tab(MainFrame::TabPosition::tpCalibration); + p->main_frame->request_select_tab(TAB_ID_CALIBRATION); auto calibration_panel = p->main_frame->m_calibration; if (calibration_panel) { auto curr_wizard = static_cast(calibration_panel->get_tabpanel()->GetPage(evt.GetInt())); @@ -17415,7 +17422,7 @@ void Plater::print_job_finished(wxCommandEvent &evt) if (!dev) return; dev->set_selected_machine(evt.GetString().ToStdString()); - p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor); + p->main_frame->request_select_tab(TAB_ID_MONITOR); //jump to monitor and select device status panel MonitorPanel* curr_monitor = p->main_frame->m_monitor; if(curr_monitor) @@ -17430,7 +17437,7 @@ void Plater::send_job_finished(wxCommandEvent& evt) send_gcode_finish(evt.GetString()); p->hide_send_to_printer_dlg(); - //p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor); + //p->main_frame->request_select_tab(TAB_ID_MONITOR); ////jump to monitor and select device status panel //MonitorPanel* curr_monitor = p->main_frame->m_monitor; //if (curr_monitor) @@ -18324,7 +18331,7 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page")); auto result = dlg.ShowModal(); if (result == wxFORWARD) { - wxGetApp().mainframe->select_tab(size_t(MainFrame::tpMonitor)); + wxGetApp().mainframe->select_tab(TAB_ID_MONITOR); } } diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 8fdca030c6..a44a4a224e 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -1039,7 +1039,7 @@ bool PlaterPresetComboBox::switch_to_tab() //BBS Select NoteBook Tab params if (tab->GetParent() == wxGetApp().params_panel()) - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); else { wxGetApp().params_dialog()->Popup(); tab->OnActivate(); diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index d6ab50c1a4..ba3bf9f00a 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -1088,8 +1088,8 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector &res } } relayout_nozzle_cards(); - auto tab_index = (MainFrame::TabPosition) dynamic_cast(wxGetApp().tab_panel())->GetSelection(); - if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { + wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName(); + if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) { updata_thumbnail_data_after_connected_printer(); } } diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index c5c007076a..256ceecc2e 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -1218,8 +1218,8 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector &resul iter++; } } - auto tab_index = (MainFrame::TabPosition) dynamic_cast(wxGetApp().tab_panel())->GetSelection(); - if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { + wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName(); + if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) { updata_thumbnail_data_after_connected_printer(); } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6bb47059e2..55c050760c 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6415,7 +6415,7 @@ void Tab::load_current_preset() std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" : tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : "cog"; tab->Hide(); // #ys_WORKAROUND : Hide tab before inserting to avoid unwanted rendering of the tab - dynamic_cast(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(), bmp_name); + dynamic_cast(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), wxString(), tab, tab->title(), bmp_name); } else #endif diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 7f69d5e087..cc15805256 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -368,7 +368,7 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) emit_progress(100); if (the_job.switch_to_device_tab) { const auto mainframe = GUI::wxGetApp().mainframe; - mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor); + mainframe->request_select_tab(TAB_ID_MONITOR); } } } diff --git a/src/slic3r/Utils/SimplyPrint.cpp b/src/slic3r/Utils/SimplyPrint.cpp index c1e5235d98..bbfd5209c9 100644 --- a/src/slic3r/Utils/SimplyPrint.cpp +++ b/src/slic3r/Utils/SimplyPrint.cpp @@ -325,7 +325,7 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path, wxLaunchDefaultBrowser(url); } else { const auto mainframe = GUI::wxGetApp().mainframe; - mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor); + mainframe->request_select_tab(TAB_ID_MONITOR); mainframe->load_printer_url(url); } From e00906a833d588ba62eb9e51e14cae38ac496c94 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 28 Jul 2026 19:17:26 +0800 Subject: [PATCH 12/71] feat: plugin pages --- src/slic3r/CMakeLists.txt | 5 + src/slic3r/GUI/GUI_App.cpp | 14 + src/slic3r/GUI/MainFrame.cpp | 4 + src/slic3r/GUI/MainFrame.hpp | 3 + src/slic3r/GUI/Notebook.cpp | 19 +- src/slic3r/GUI/PluginWebDialog.cpp | 35 +- src/slic3r/GUI/Widgets/WebViewHostDialog.cpp | 54 +++- src/slic3r/GUI/Widgets/WebViewHostDialog.hpp | 4 + src/slic3r/plugin/PythonPluginBridge.cpp | 30 +- src/slic3r/plugin/PythonPluginInterface.hpp | 10 +- src/slic3r/plugin/host/PluginPages.cpp | 298 ++++++++++++++++++ src/slic3r/plugin/host/PluginPages.hpp | 69 ++++ .../pages/PagesPluginCapability.cpp | 59 ++++ .../pages/PagesPluginCapability.hpp | 32 ++ .../pages/PagesPluginCapabilityTrampoline.hpp | 48 +++ .../PrinterAgentPluginCapability.cpp | 4 +- .../PrinterAgentPluginCapability.hpp | 2 +- .../script/ScriptPluginCapability.cpp | 3 +- .../script/ScriptPluginCapability.hpp | 3 +- .../SlicingPipelinePluginCapability.cpp | 3 +- .../SlicingPipelinePluginCapability.hpp | 2 +- 21 files changed, 622 insertions(+), 79 deletions(-) create mode 100644 src/slic3r/plugin/host/PluginPages.cpp create mode 100644 src/slic3r/plugin/host/PluginPages.hpp create mode 100644 src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp create mode 100644 src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp create mode 100644 src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 35cf96d171..6dd17e3ce8 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -620,6 +620,8 @@ set(SLIC3R_GUI_SOURCES plugin/host/PluginHostSlicing.cpp plugin/host/PluginHostUi.cpp plugin/host/PluginHostUi.hpp + plugin/host/PluginPages.cpp + plugin/host/PluginPages.hpp plugin/CloudPluginService.cpp plugin/CloudPluginService.hpp plugin/PluginFsUtils.cpp @@ -640,6 +642,9 @@ set(SLIC3R_GUI_SOURCES plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp + plugin/pluginTypes/pages/PagesPluginCapability.hpp + plugin/pluginTypes/pages/PagesPluginCapability.cpp + plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp plugin/pluginTypes/script/ScriptPluginCapability.hpp plugin/pluginTypes/script/ScriptPluginCapability.cpp plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 1290df70e4..e7ca7cc581 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2799,6 +2799,16 @@ void GUI_App::init_plugin_gui_wiring() plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin); plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); + plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) { + if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr) + return; + wxGetApp().mainframe->plugin_pages().on_plugin_register(plugin_key); + }); + plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) { + if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr) + return; + wxGetApp().mainframe->plugin_pages().on_plugin_deregister(plugin_key); + }); plugin_mgr.subscribe_on_capability_load_callback( [refresh_plugins_dialog](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) @@ -2811,11 +2821,15 @@ void GUI_App::init_plugin_gui_wiring() if (Plater* plater = wxGetApp().plater()) plater->revalidate_current_plate_if_plugins_missing(); }); + if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe) + wxGetApp().mainframe->plugin_pages().on_cap_register(capability); }); plugin_mgr.subscribe_on_capability_unload_callback( [refresh_plugins_dialog](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); + if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe) + wxGetApp().mainframe->plugin_pages().on_cap_deregister(capability); refresh_plugins_dialog(); }); } diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index e2b94d11ff..01c698c62c 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1110,6 +1110,7 @@ void MainFrame::update_edge_panels() void MainFrame::shutdown() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; + m_plugin_pages.shutdown(); #ifdef __WXGTK__ // Edge panels are child windows — wxWidgets destroys them automatically. m_edge_bottom = nullptr; @@ -1355,6 +1356,9 @@ void MainFrame::init_tabpanel() { m_calibration->SetBackgroundColour(*wxWHITE); m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + // Plugin pages are appended after the built-in tabs; their ids are namespaced + // (plugin..) so they can't collide with the built-in TAB_ID_* constants. + m_plugin_pages.initialize(m_tabpanel); if (m_plater) { // load initial config diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 9bb02b28a6..53cc885a27 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -35,6 +35,7 @@ #include "PrinterWebView.hpp" #include "calib_dlg.hpp" #include "MultiMachinePage.hpp" +#include "slic3r/plugin/host/PluginPages.hpp" // Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are // names rather than positional indices so optional pages cannot shift them. @@ -357,6 +358,7 @@ public: //SoftFever void show_device(bool bBBLPrinter); void fit_tab_labels(); // ORCA + PluginPages& plugin_pages() { return m_plugin_pages; } PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr }; FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr }; @@ -382,6 +384,7 @@ public: CalibrationPanel* m_calibration{ nullptr }; WebViewPanel* m_webview { nullptr }; PrinterWebView* m_printer_view{nullptr}; + PluginPages m_plugin_pages; wxLogWindow* m_log_window { nullptr }; // BBS //wxBookCtrlBase* m_tabpanel { nullptr }; diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index a6906b4550..4c6430f99c 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -120,11 +120,11 @@ void ButtonsListCtrl::Rescale() void ButtonsListCtrl::SetSelection(int sel) { - if (m_selection == sel) + if (m_selection == sel && sel >= 0 && sel < static_cast(m_pageButtons.size())) return; // BBS: change button color wxColour selected_btn_bg("#009688"); // Gradient #009688 - if (m_selection >= 0) { + if (m_selection >= 0 && m_selection < static_cast(m_pageButtons.size())) { StateColor bg_color = StateColor( std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered}, std::pair{wxColour(59, 68, 70), (int) StateColor::Normal}); @@ -135,6 +135,13 @@ void ButtonsListCtrl::SetSelection(int sel) m_pageButtons[m_selection]->SetSelected(false); m_pageButtons[m_selection]->SetTextColor(text_color); } + + if (sel < 0 || sel >= static_cast(m_pageButtons.size())) { + m_selection = -1; + Refresh(); + return; + } + m_selection = sel; StateColor bg_color = StateColor( @@ -192,6 +199,14 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* void ButtonsListCtrl::RemovePage(size_t n) { + if (n >= m_pageButtons.size()) + return; + + if (m_selection == static_cast(n)) + m_selection = -1; + else if (m_selection > static_cast(n)) + --m_selection; + Button* btn = m_pageButtons[n]; m_pageButtons.erase(m_pageButtons.begin() + n); m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA diff --git a/src/slic3r/GUI/PluginWebDialog.cpp b/src/slic3r/GUI/PluginWebDialog.cpp index 1808f21ce9..d89aac7270 100644 --- a/src/slic3r/GUI/PluginWebDialog.cpp +++ b/src/slic3r/GUI/PluginWebDialog.cpp @@ -15,39 +15,6 @@ namespace Slic3r { namespace GUI { namespace { -// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare -// plugin page looks native while any CSS the plugin ships still wins. Built on the -// --orca-* variables the host injects (see WebViewHostDialog); document-start injected -// AFTER the host contract so the variables are defined (shares the base injector's -// WebView2 timing guard). -std::string plugin_defaults_user_script() -{ - std::string css; - css += ""; - return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend"); -} - // Injected into the top-level page at document start (before the plugin's own // scripts). Defines window.orca as the only host surface the page may use. It // references window.wx lazily (at call time) so it never races the backend's @@ -129,7 +96,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent, void PluginWebDialog::add_user_scripts() { if (wxWebView* wv = browser()) { - wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script())); + wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script())); wv->AddUserScript(ORCA_BRIDGE_JS); } } diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp index 5e6026d1cf..044fe33cde 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp @@ -57,18 +57,6 @@ std::string host_theme_vars_css() return s; } -// Document-start user script: injects the contract "; - return WebViewHostDialog::document_start_injector( - style, "orca-host-theme-vars", "afterbegin", - "window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";", - "if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);"); -} - // JS to re-theme an already-loaded document live (no reload): replace the injected // style's contents and update data-orca-theme. Everything downstream (theme.css // tokens, plugin element defaults, page layout) re-cascades from these values. @@ -87,6 +75,46 @@ if(document.documentElement) } // namespace +// Document-start user script: injects the contract "; + return document_start_injector( + style, "orca-host-theme-vars", "afterbegin", + "window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";", + "if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);"); +} + +std::string WebViewHostDialog::plugin_defaults_user_script() +{ + std::string css; + css += ""; + return document_start_injector(css, "orca-plugin-defaults", "beforeend"); +} + std::string WebViewHostDialog::document_start_injector(const std::string& markup, const char* dom_id, const char* position, @@ -244,7 +272,7 @@ void WebViewHostDialog::register_theme_user_scripts() // script message handler is registered separately (AddScriptMessageHandler), but on // some backends RemoveAllUserScripts() drops it too, which would break // window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live(). - m_browser->AddUserScript(wxString::FromUTF8(host_theme_user_script())); + m_browser->AddUserScript(wxString::FromUTF8(theme_user_script())); add_user_scripts(); } diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp index ed21f15f94..119e3b5955 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp @@ -49,6 +49,10 @@ public: const std::string& prelude = {}, const std::string& on_inject = {}); + // Shared by modeless Pages tabs and PluginWebDialog. + static std::string theme_user_script(); + static std::string plugin_defaults_user_script(); + protected: wxWebView* browser() const { return m_browser; } diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 328ebbace1..40f317c016 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -19,6 +19,7 @@ #include "PyPluginPackage.hpp" #include "PyPluginTrampoline.hpp" #include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp" +#include "pluginTypes/pages/PagesPluginCapability.hpp" #include "pluginTypes/script/ScriptPluginCapability.hpp" #include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" @@ -319,17 +320,17 @@ void bind_python_api(pybind11::module_& m) { m.doc() = "OrcaSlicer plugin API"; - auto pluginTypes = py::enum_(m, "PluginType", "Available plugin capability groups") - .value("PrinterConnection", PluginCapabilityType::PrinterConnection) - .value("Automation", PluginCapabilityType::Automation) - .value("Analysis", PluginCapabilityType::Analysis) - .value("Importer", PluginCapabilityType::Importer) - .value("Exporter", PluginCapabilityType::Exporter) - .value("Visualization", PluginCapabilityType::Visualization) - .value("Script", PluginCapabilityType::Script) - .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) - .value("Unknown", PluginCapabilityType::Unknown) - .export_values(); + py::enum_(m, "PluginType", "Available plugin capability groups") + .value("PrinterConnection", PluginCapabilityType::PrinterConnection) + .value("Pages", PluginCapabilityType::Pages) + .value("Analysis", PluginCapabilityType::Analysis) + .value("Importer", PluginCapabilityType::Importer) + .value("Exporter", PluginCapabilityType::Exporter) + .value("Visualization", PluginCapabilityType::Visualization) + .value("Script", PluginCapabilityType::Script) + .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) + .value("Unknown", PluginCapabilityType::Unknown) + .export_values(); py::enum_(m, "PluginResult", "Execution summary code") .value("Success", PluginResult::Success) @@ -419,9 +420,10 @@ void bind_python_api(pybind11::module_& m) BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings"; // Make sure you register your bindings here - PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes); - ScriptPluginCapability::RegisterBindings(m, pluginTypes); - SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes); + PrinterAgentPluginCapability::RegisterBindings(m); + PagesPluginCapability::RegisterBindings(m); + ScriptPluginCapability::RegisterBindings(m); + SlicingPipelinePluginCapability::RegisterBindings(m); PluginHost::RegisterBindings(m); BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; diff --git a/src/slic3r/plugin/PythonPluginInterface.hpp b/src/slic3r/plugin/PythonPluginInterface.hpp index 4a6df06441..8b7518cf1c 100644 --- a/src/slic3r/plugin/PythonPluginInterface.hpp +++ b/src/slic3r/plugin/PythonPluginInterface.hpp @@ -12,7 +12,7 @@ namespace Slic3r { -enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; +enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; struct PluginCapabilityId { @@ -39,7 +39,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type) { switch (type) { case PluginCapabilityType::PrinterConnection: return "printer-connection"; - case PluginCapabilityType::Automation: return "automation"; + case PluginCapabilityType::Pages: return "pages"; case PluginCapabilityType::Analysis: return "analysis"; case PluginCapabilityType::Importer: return "importer"; case PluginCapabilityType::Exporter: return "exporter"; @@ -54,7 +54,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type { switch (type) { case PluginCapabilityType::PrinterConnection: return "Printer connection"; - case PluginCapabilityType::Automation: return "Automation"; + case PluginCapabilityType::Pages: return "Pages"; case PluginCapabilityType::Analysis: return "Analysis"; case PluginCapabilityType::Importer: return "Importer"; case PluginCapabilityType::Exporter: return "Exporter"; @@ -76,8 +76,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view if (lowered == "printer-connection") return PluginCapabilityType::PrinterConnection; - if (lowered == "automation") - return PluginCapabilityType::Automation; + if (lowered == "pages") + return PluginCapabilityType::Pages; if (lowered == "analysis") return PluginCapabilityType::Analysis; if (lowered == "importer") diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp new file mode 100644 index 0000000000..3e7d534810 --- /dev/null +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -0,0 +1,298 @@ +#include "PluginPages.hpp" + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Notebook.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Widgets/WebView.hpp" +#include "slic3r/GUI/Widgets/WebViewHostDialog.hpp" +#include "slic3r/GUI/wxExtensions.hpp" +#include "slic3r/plugin/PluginManager.hpp" + +#include + +#include +#include +#include + +#include + +#include + +namespace Slic3r { +namespace { + +constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS( +(function () { + if (window.top !== window.self) return; + if (window.orca) return; + var handlers = []; + function deliver(payload, attempts) { + try { + if (window.wx && typeof window.wx.postMessage === 'function') { + window.wx.postMessage(payload); + return; + } + } catch (e) { /* retry while the native handler is being registered */ } + if (attempts < 100) + window.setTimeout(function () { deliver(payload, attempts + 1); }, 25); + } + function send(data) { + deliver(JSON.stringify({ + channel: 'orca', kind: 'message', data: (data === undefined ? null : data) + }), 0); + } + window.orca = { + postMessage: function (data) { send(data); }, + onMessage: function (callback) { + if (typeof callback === 'function') handlers.push(callback); + } + }; + window.__orcaDispatch = function (payload) { + var data = payload ? payload.data : null; + for (var i = 0; i < handlers.length; i++) { + try { handlers[i](data); } catch (e) {} + } + }; +})(); +)JS"; + +} // namespace + +PluginPage::PluginPage(wxWindow* parent, std::shared_ptr capability) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + , m_cap(std::move(capability)) + , m_lifetime(std::make_shared>(this)) +{ + auto* topsizer = new wxBoxSizer(wxVERTICAL); + SetSizer(topsizer); + + m_browser = WebView::CreateWebView(this, bootstrap_url()); + if (m_browser == nullptr) { + wxLogError("Could not initialize plugin page web view"); + return; + } + + topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); + m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginPage::on_bootstrap_event, this); + m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginPage::on_bootstrap_event, this); + m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this); + m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginPage::on_script_message, this); + m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::theme_user_script())); + m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::plugin_defaults_user_script())); + m_browser->AddUserScript(PLUGIN_PAGE_BRIDGE_JS); + + const std::shared_ptr> lifetime = m_lifetime; + m_cap->set_message_sender([lifetime](const std::string& message) { + if (wxTheApp == nullptr) + return; + + GUI::wxGetApp().CallAfter([lifetime, message] { + if (PluginPage* page = lifetime->load(std::memory_order_acquire)) + page->push_message(message); + }); + }); + +} + +PluginPage::~PluginPage() +{ + detach_capability(); + if (m_lifetime) + m_lifetime->store(nullptr, std::memory_order_release); +} + +void PluginPage::detach_capability() +{ + if (m_lifetime) + m_lifetime->store(nullptr, std::memory_order_release); + if (m_cap) + m_cap->clear_message_sender(); + m_cap.reset(); +} + +wxString PluginPage::web_base_url() const +{ + const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string(); + return wxString("file://") + GUI::from_u8(path) + "/"; +} + +wxString PluginPage::bootstrap_url() const +{ + const auto path = (boost::filesystem::path(resources_dir()) / "web/dialog/PluginWebDialog/blank.html").make_preferred().string(); + return wxString("file://") + GUI::from_u8(path); +} + +void PluginPage::on_bootstrap_event(wxWebViewEvent& event) +{ + load_plugin_content(); + event.Skip(); +} + +void PluginPage::load_plugin_content() +{ + if (m_content_loaded || m_browser == nullptr || m_cap == nullptr) + return; + + m_content_loaded = true; + try { + m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url()); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what(); + detach_capability(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'"; + detach_capability(); + } +} + +void PluginPage::on_new_window(wxWebViewEvent& event) +{ + const wxString url = event.GetURL(); + if (!url.empty() && m_browser != nullptr) + m_browser->LoadURL(url); + event.Veto(); +} + +void PluginPage::on_script_message(wxWebViewEvent& event) +{ + if (!m_cap) + return; + + const wxString payload = event.GetString(); + nlohmann::json root = nlohmann::json::parse(payload.utf8_string(), nullptr, false); + if (root.is_discarded() || root.value("channel", std::string()) != "orca" || + root.value("kind", std::string()) != "message") + return; + + const nlohmann::json data = root.contains("data") ? root["data"] : nlohmann::json(); + try { + m_cap->on_message(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'"; + } +} + +void PluginPage::push_message(const std::string& message) +{ + if (m_browser == nullptr) + return; + + nlohmann::json data = nlohmann::json::parse(message, nullptr, false); + if (data.is_discarded()) + data = message; + + const wxString script = wxString("(function dispatch(payload, attempts) {\n") + + wxString(" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n") + + wxString(" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n") + + wxString("})({data: ") + + wxString::FromUTF8(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)) + + wxString("}, 0);"); + WebView::RunScript(m_browser, script); +} + +PluginPages::~PluginPages() +{ + shutdown(); +} + +void PluginPages::initialize(Notebook* parent) +{ + shutdown(); + m_parent = parent; + if (m_parent == nullptr) + return; + + for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { + if (capability) + on_cap_register(capability->identity()); + } +} + +void PluginPages::shutdown() +{ + while (!m_pages.empty()) + remove_page(m_pages.begin()->first); + m_parent = nullptr; +} + +std::shared_ptr PluginPages::get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const +{ + auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false); + if (!capability || capability->is_enabled() != is_enabled || capability->type() != PluginCapabilityType::Pages) + return nullptr; + + return std::dynamic_pointer_cast(capability); +} + +void PluginPages::on_cap_register(const PluginCapabilityId& id) +{ + if (m_parent == nullptr || m_pages.find(id) != m_pages.end()) + return; + + auto capability = get_pages_cap(id, true); + if (!capability) + return; + + auto* page = new PluginPage(m_parent, std::move(capability)); + if (!page->is_valid()) { + page->Destroy(); + return; + } + + const wxString title = wxString::FromUTF8(id.name); + const wxString page_id = wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name); + if (!m_parent->AddPage(page_id, page, title, "tab_auxiliary_active", "tab_auxiliary_active", false)) { + page->Destroy(); + return; + } + + m_pages.emplace(id, page); +} + +void PluginPages::on_cap_deregister(const PluginCapabilityId& id) +{ + remove_page(id); +} + +void PluginPages::on_plugin_register(const std::string& plugin_key) +{ + for (const auto& capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::Pages)) { + if (capability) + on_cap_register(capability->identity()); + } +} + +void PluginPages::on_plugin_deregister(const std::string& plugin_key) +{ + for (auto it = m_pages.begin(); it != m_pages.end();) { + if (it->first.plugin_key != plugin_key) { + ++it; + continue; + } + + const PluginCapabilityId id = it->first; + ++it; + remove_page(id); + } +} + +void PluginPages::remove_page(const PluginCapabilityId& id) +{ + auto it = m_pages.find(id); + if (it == m_pages.end()) + return; + + PluginPage* page = it->second; + page->detach_capability(); + if (m_parent != nullptr) { + const int index = m_parent->FindPage(page); + if (index != wxNOT_FOUND) + m_parent->RemovePage(static_cast(index)); + } + page->Destroy(); + m_pages.erase(it); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp new file mode 100644 index 0000000000..bf70305201 --- /dev/null +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include +#include + +class Notebook; + +namespace Slic3r { + +class PluginPage : public wxPanel +{ +public: + PluginPage(wxWindow* parent, std::shared_ptr capability); + ~PluginPage() override; + + PluginPage() = delete; + + bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; } + void detach_capability(); + void on_bootstrap_event(wxWebViewEvent& event); + void on_new_window(wxWebViewEvent& event); + void on_script_message(wxWebViewEvent& event); + void push_message(const std::string& message); + +private: + void load_plugin_content(); + wxString bootstrap_url() const; + wxString web_base_url() const; + + wxWebView* m_browser{nullptr}; + std::shared_ptr m_cap; + std::shared_ptr> m_lifetime; + bool m_content_loaded{false}; +}; + +class PluginPages +{ +public: + PluginPages() = default; + ~PluginPages(); + + PluginPages(const PluginPages&) = delete; + PluginPages& operator=(const PluginPages&) = delete; + + void initialize(Notebook* parent); + void shutdown(); + + void on_cap_register(const PluginCapabilityId& id); + void on_cap_deregister(const PluginCapabilityId& id); + void on_plugin_register(const std::string& plugin_key); + void on_plugin_deregister(const std::string& plugin_key); + +private: + std::shared_ptr get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const; + void remove_page(const PluginCapabilityId& id); + + std::map m_pages; + Notebook* m_parent{nullptr}; +}; + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp new file mode 100644 index 0000000000..fc5d4a03b8 --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp @@ -0,0 +1,59 @@ +#include "PagesPluginCapability.hpp" +#include "PagesPluginCapabilityTrampoline.hpp" + +#include "../../PluginFsUtils.hpp" + +#include +#include + +#include + +namespace py = pybind11; + +namespace Slic3r { + +void PagesPluginCapability::RegisterBindings(pybind11::module_& module) +{ + BOOST_LOG_TRIVIAL(debug) << "Registering orca.pages bindings"; + + auto pages = module.def_submodule("pages", "Plugin page API"); + + py::class_>(pages, "PagesPluginCapabilityBase") + .def(py::init<>()) + .def("get_type", &PagesPluginCapability::get_type) + .def("get_ui", &PagesPluginCapability::get_ui) + .def("on_message", &PagesPluginCapability::on_message) + .def( + "post_message", + [](PagesPluginCapability& capability, py::object data) { + capability.post_message(py_to_json(data).dump()); + }, + py::arg("data"), "Send a JSON-compatible value to the page's window.orca.onMessage handlers."); +} + +void PagesPluginCapability::post_message(std::string message) +{ + std::function sender; + { + std::lock_guard lock(m_message_mutex); + sender = m_message_sender; + } + + if (sender) + sender(message); +} + +void PagesPluginCapability::set_message_sender(std::function sender) +{ + std::lock_guard lock(m_message_mutex); + m_message_sender = std::move(sender); +} + +void PagesPluginCapability::clear_message_sender() +{ + std::lock_guard lock(m_message_mutex); + m_message_sender = nullptr; +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp new file mode 100644 index 0000000000..e833d7dbf4 --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp @@ -0,0 +1,32 @@ +#ifndef slic3r_PagesPluginCapability_hpp_ +#define slic3r_PagesPluginCapability_hpp_ + +#include "../../PythonPluginInterface.hpp" +#include "pybind11/pybind11.h" + +#include +#include +#include + +namespace Slic3r { +class PagesPluginCapability : public PluginCapabilityInterface +{ +public: + static void RegisterBindings(pybind11::module_& module); + + PluginCapabilityType get_type() const override { return PluginCapabilityType::Pages; } + + virtual std::string get_ui() = 0; + virtual void on_message(std::string message) { (void) message; } + + void post_message(std::string message); + void set_message_sender(std::function sender); + void clear_message_sender(); + +private: + mutable std::mutex m_message_mutex; + std::function m_message_sender; +}; +} // namespace Slic3r + +#endif diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp new file mode 100644 index 0000000000..71eb5c5bcc --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "PagesPluginCapability.hpp" +#include "../../PluginFsUtils.hpp" +#include "../../PyPluginTrampoline.hpp" + +#include + +namespace Slic3r { + +class PyPagesPluginCapabilityTrampoline : public PyPluginCommonTrampoline +{ +public: + using PyPluginCommonTrampoline::PyPluginCommonTrampoline; + + std::string get_ui() override + { + ORCA_PY_OVERRIDE_AUDITED( + ::Slic3r::PluginAuditManager::AuditMode::Loading, + [] {}, + PYBIND11_OVERRIDE_PURE, + std::string, + PagesPluginCapability, + get_ui); + } + + void on_message(std::string message) override + { + PluginCapabilityInterface::RefCounter ref_counter(*this); + PythonGILState gil; + if (!gil) + throw std::runtime_error("Python interpreter is shutting down"); + + ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading); + + pybind11::function override = pybind11::get_override(static_cast(this), "on_message"); + if (!override) + return; + + nlohmann::json data = nlohmann::json::parse(message, nullptr, false); + if (data.is_discarded()) + data = message; + + ORCA_PY_LOGGED_OVERRIDE_BODY(override(::Slic3r::json_to_py(data))); + } +}; + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp index b3d3d5d44c..d428775c12 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp @@ -13,10 +13,8 @@ namespace py = pybind11; namespace Slic3r { -void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes) +void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) { - (void) pluginTypes; - auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API"); py::enum_(printer_agent_module, "FilamentSyncMode") diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp index 33ad211b9c..ede7c6a9b8 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp @@ -19,7 +19,7 @@ namespace Slic3r { class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent { public: - static void RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes); + static void RegisterBindings(pybind11::module_& module); PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; } diff --git a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp index 35a259edf6..712ba9b653 100644 --- a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp @@ -9,9 +9,8 @@ namespace py = pybind11; namespace Slic3r { -void ScriptPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes) +void ScriptPluginCapability::RegisterBindings(pybind11::module_& module) { - (void) pluginTypes; BOOST_LOG_TRIVIAL(debug) << "Registering orca.script bindings"; auto script = module.def_submodule("script", "Script Plugins API"); diff --git a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp index cb5bc45c08..fb1319e560 100644 --- a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp @@ -11,8 +11,7 @@ public: virtual ExecutionResult execute() = 0; - static void RegisterBindings(pybind11::module_ &module, - pybind11::enum_ &pluginTypes); + static void RegisterBindings(pybind11::module_ &module); }; } // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp index f4569aebba..d2e630242d 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp @@ -8,8 +8,7 @@ namespace Slic3r { bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); } -void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::enum_& pluginTypes) { - (void) pluginTypes; // unused: this capability defines its own Step enum (below) rather than extending the shared PluginCapabilityType enum. +void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module) { auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental)."); py::enum_(slicing, "Step") diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp index 639c087371..da0dbcbcbd 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp @@ -37,7 +37,7 @@ public: // Runs on the slicing worker thread. Do not call orca.host.ui.* here: the UI thread can be // blocked waiting on the slicing worker, so a marshaled UI call from this thread can deadlock. virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0; - static void RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes); + static void RegisterBindings(pybind11::module_& module); }; } // namespace Slic3r From 3145f28bb70b8605664d01fe1d47657ed37ba8a7 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 29 Jul 2026 19:37:17 +0800 Subject: [PATCH 13/71] feat: support tab icons --- src/slic3r/GUI/Auxiliary.cpp | 10 +- src/slic3r/GUI/CalibrationPanel.cpp | 1 - src/slic3r/GUI/MainFrame.cpp | 25 ++-- src/slic3r/GUI/Monitor.cpp | 10 +- src/slic3r/GUI/MultiMachinePage.cpp | 6 +- src/slic3r/GUI/Notebook.cpp | 27 +++- src/slic3r/GUI/Notebook.hpp | 128 +++++++++++++++--- src/slic3r/GUI/Tabbook.hpp | 26 +--- src/slic3r/GUI/Widgets/Button.cpp | 20 +-- src/slic3r/GUI/Widgets/Button.hpp | 7 +- src/slic3r/plugin/host/PluginPages.cpp | 71 +++++++++- src/slic3r/plugin/host/PluginPages.hpp | 20 ++- .../pages/PagesPluginCapability.cpp | 1 + .../pages/PagesPluginCapability.hpp | 1 + .../pages/PagesPluginCapabilityTrampoline.hpp | 11 ++ 15 files changed, 260 insertions(+), 104 deletions(-) diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp index 95244436a3..13ca173eb6 100644 --- a/src/slic3r/GUI/Auxiliary.cpp +++ b/src/slic3r/GUI/Auxiliary.cpp @@ -869,11 +869,11 @@ void AuxiliaryPanel::init_tabpanel() m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE); m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS); - m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), "", true); - m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), "", false); - m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), "", false); - m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), "", false); - m_tabpanel->AddPage(m_others_panel, _L("Others"), "", false); + m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true); + m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false); + m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false); + m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false); + m_tabpanel->AddPage(m_others_panel, _L("Others"), false); } wxWindow *AuxiliaryPanel::create_side_tools() diff --git a/src/slic3r/GUI/CalibrationPanel.cpp b/src/slic3r/GUI/CalibrationPanel.cpp index b006509adf..bdc79c1c8e 100644 --- a/src/slic3r/GUI/CalibrationPanel.cpp +++ b/src/slic3r/GUI/CalibrationPanel.cpp @@ -488,7 +488,6 @@ void CalibrationPanel::init_tabpanel() { selected = true; m_tabpanel->AddPage(m_cali_panels[i], get_calibration_type_name(m_cali_panels[i]->get_calibration_mode()), - "", selected); } diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 01c698c62c..d6dbd4e596 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1018,8 +1018,8 @@ void MainFrame::update_layout() { const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME); const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast(home_idx) + 1; - m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); - m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); + m_tabpanel->InsertPage(prepare_pos, m_plater, _L("Prepare"), false, Notebook::PAGE_PREPARE); + m_tabpanel->InsertPage(prepare_pos + 1, m_plater, _L("Preview"), false, Notebook::PAGE_PREVIEW); } m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0); @@ -1315,7 +1315,7 @@ void MainFrame::init_tabpanel() { select_tab(TAB_ID_HOME); m_webview->load_url(url); }); - m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active", "tab_home_active", false); + m_tabpanel->AddPage(m_webview, "", false, Notebook::PAGE_HOME); m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); } @@ -1330,7 +1330,7 @@ void MainFrame::init_tabpanel() { //BBS add pages m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_monitor->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); + m_tabpanel->AddPage(m_monitor, _L("Device"), false, Notebook::PAGE_MONITOR); m_printer_view = new PrinterWebView(m_tabpanel); Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { @@ -1345,16 +1345,16 @@ void MainFrame::init_tabpanel() { m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_multi_machine->SetBackgroundColour(*wxWHITE); // TODO: change the bitmap - m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false); + m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), false, Notebook::PAGE_MULTI_DEVICE); } m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_project->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false); + m_tabpanel->AddPage(m_project, _L("Project"), false, Notebook::PAGE_PROJECT); m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); // Plugin pages are appended after the built-in tabs; their ids are namespaced // (plugin..) so they can't collide with the built-in TAB_ID_* constants. @@ -1397,7 +1397,7 @@ void MainFrame::show_device(bool bBBLPrinter) { { const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active")); + m_tabpanel->InsertPage(monitor_pos, m_monitor, _L("Device"), false, Notebook::PAGE_MONITOR); } if (wxGetApp().is_enable_multi_machine()) { @@ -1410,8 +1410,7 @@ void MainFrame::show_device(bool bBBLPrinter) { { const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); const size_t multi_pos = (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; - m_tabpanel->InsertPage(multi_pos, TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), - std::string("tab_multi_active"), false); + m_tabpanel->InsertPage(multi_pos, m_multi_machine, _L("Multi-device"), false, Notebook::PAGE_MULTI_DEVICE); } } if (!m_calibration) { @@ -1422,8 +1421,7 @@ void MainFrame::show_device(bool bBBLPrinter) { // Calibration is always appended last (AddPage), so it lands after whichever of Monitor/Multi-device // actually got inserted above — no longer position-sensitive now that insertion position is computed // from FindPageByName rather than a fixed TabPosition index. - m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), - std::string("tab_calibration_active"), false); + m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); #ifdef _MSW_DARK_MODE wxGetApp().UpdateDarkUIWin(this); @@ -1459,8 +1457,7 @@ void MainFrame::show_device(bool bBBLPrinter) { { const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_printer_view, _L("Device"), std::string("tab_monitor_active"), - std::string("tab_monitor_active")); + m_tabpanel->InsertPage(monitor_pos, m_printer_view, _L("Device"), false, Notebook::PAGE_MONITOR); } } fit_tab_labels(); // ORCA on printer change diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 068bcf7e6d..4c26268a21 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -186,17 +186,17 @@ void MonitorPanel::init_tabpanel() //m_status_add_machine_panel = new AddMachinePanel(m_tabpanel); m_status_info_panel = new StatusPanel(m_tabpanel); - m_tabpanel->AddPage(m_status_info_panel, _L("Status"), "", true); + m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true); m_media_file_panel = new MediaFilePanel(m_tabpanel); - m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), "", false); - //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), "", false); + m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false); + //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), false); m_upgrade_panel = new UpgradePanel(m_tabpanel); - m_tabpanel->AddPage(m_upgrade_panel, _CTX(L_CONTEXT("Update", "Firmware"), "Firmware"), "", false); + m_tabpanel->AddPage(m_upgrade_panel, _CTX(L_CONTEXT("Update", "Firmware"), "Firmware"), false); m_hms_panel = new HMSPanel(m_tabpanel); - m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), "", false); + m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), false); std::string network_ver = Slic3r::NetworkAgent::get_version(); if (!network_ver.empty()) { diff --git a/src/slic3r/GUI/MultiMachinePage.cpp b/src/slic3r/GUI/MultiMachinePage.cpp index b9b71ad670..88d03007b9 100644 --- a/src/slic3r/GUI/MultiMachinePage.cpp +++ b/src/slic3r/GUI/MultiMachinePage.cpp @@ -86,9 +86,9 @@ void MultiMachinePage::init_tabpanel() m_cloud_task_manager = new CloudTaskManagerPage(m_tabpanel); m_machine_manager = new MultiMachineManagerPage(m_tabpanel); - m_tabpanel->AddPage(m_machine_manager, _L("Device"), "", true); - m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), "", false); - m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), "", false); + m_tabpanel->AddPage(m_machine_manager, _L("Device"), true); + m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), false); + m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), false); } void MultiMachinePage::init_timer() diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index 4c6430f99c..847996aba9 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -132,7 +132,6 @@ void ButtonsListCtrl::SetSelection(int sel) StateColor text_color = StateColor( std::pair{wxColour(254,254, 254), (int) StateColor::Normal} ); - m_pageButtons[m_selection]->SetSelected(false); m_pageButtons[m_selection]->SetTextColor(text_color); } @@ -152,17 +151,20 @@ void ButtonsListCtrl::SetSelection(int sel) StateColor text_color = StateColor( std::pair{wxColour(254, 254, 254), (int) StateColor::Normal} ); - m_pageButtons[m_selection]->SetSelected(true); m_pageButtons[m_selection]->SetTextColor(text_color); Refresh(); } -bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const std::string &inactive_bmp_name) +bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, int imageId /* = wxBookCtrlBase::NO_IMAGE */) { Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER); btn->SetCornerRadius(0); + if (bmp_name.empty() && m_imageList != nullptr && imageId != wxBookCtrlBase::NO_IMAGE && imageId >= 0 && + imageId < m_imageList->GetImageCount()) + btn->SetIcon(m_imageList->GetBitmap(imageId)); + int em = em_unit(this); //BBS set size for button btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10}); @@ -175,8 +177,6 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* StateColor text_color = StateColor( std::pair{wxColour(254,254, 254), (int) StateColor::Normal}); btn->SetTextColor(text_color); - btn->SetInactiveIcon(inactive_bmp_name); - btn->SetSelected(false); btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) { if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) { auto sel = it - m_pageButtons.begin(); @@ -232,6 +232,23 @@ bool ButtonsListCtrl::SetPageImage(size_t n, const std::string& bmp_name) const return true; } +bool ButtonsListCtrl::SetPageImage(size_t n, int imageId) +{ + if (n >= m_pageButtons.size()) + return false; + + if (imageId == wxBookCtrlBase::NO_IMAGE) { + m_pageButtons[n]->SetIcon(wxBitmap()); + return true; + } + + if (m_imageList == nullptr || imageId < 0 || imageId >= m_imageList->GetImageCount()) + return false; + + m_pageButtons[n]->SetIcon(m_imageList->GetBitmap(imageId)); + return true; +} + void ButtonsListCtrl::SetPageText(size_t n, const wxString& strText) { Button* btn = m_pageButtons[n]; diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index a82be7be56..859ada37a5 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -3,8 +3,11 @@ //#ifdef _WIN32 +#include #include +#include #include +#include #include class ScalableButton; @@ -24,9 +27,11 @@ public: void SetSelection(int sel); void UpdateMode(); void Rescale(); - bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const std::string &inactive_bmp_name = ""); + bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", int imageId = wxBookCtrlBase::NO_IMAGE); void RemovePage(size_t n); bool SetPageImage(size_t n, const std::string& bmp_name) const; + bool SetPageImage(size_t n, int imageId); + void SetImageList(wxImageList* imageList) { m_imageList = imageList; } void SetPageText(size_t n, const wxString& strText); void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; @@ -41,11 +46,22 @@ private: int m_btn_margin; int m_line_margin; std::vector m_pageLabels; // ORCA + wxImageList* m_imageList{nullptr}; }; class Notebook : public wxBookCtrlBase { public: + // Negative values below wxBookCtrlBase::NO_IMAGE are reserved for the built-in + // tabs. Nonnegative values are wxImageList indices supplied by plugin pages. + static constexpr int PAGE_HOME = -2; + static constexpr int PAGE_PREPARE = -3; + static constexpr int PAGE_PREVIEW = -4; + static constexpr int PAGE_MONITOR = -5; + static constexpr int PAGE_MULTI_DEVICE = -6; + static constexpr int PAGE_PROJECT = -7; + static constexpr int PAGE_CALIBRATION = -8; + Notebook(wxWindow * parent, wxWindowID winid = wxID_ANY, const wxPoint & pos = wxDefaultPosition, @@ -104,7 +120,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(wxString(), page, wxString(), "", ""); + return AddPage(page, wxString(), false, NO_IMAGE); } @@ -136,16 +152,10 @@ public: // Implement base class pure virtual methods. - // adds a new page to the control - bool AddPage(const wxString& id, - wxWindow* page, - const wxString& text, - const std::string& bmp_name, - const std::string& inactive_bmp_name, - bool bSelect = false) + bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override { DoInvalidateBestSize(); - return InsertPage(GetPageCount(), id, page, text, bmp_name, inactive_bmp_name, bSelect); + return InsertPage(GetPageCount(), page, text, bSelect, imageId); } // Page management @@ -154,12 +164,38 @@ public: const wxString & text, bool bSelect = false, int imageId = NO_IMAGE) override + { + wxString page_name; + std::string bmp_name; + const bool is_fixed_page = get_fixed_page_info(imageId, page_name, bmp_name); + const int stored_image_id = is_fixed_page ? NO_IMAGE : imageId; + + if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, stored_image_id)) + return false; + + m_pageNames.insert(m_pageNames.begin() + n, page_name); + m_pageImageIds.insert(m_pageImageIds.begin() + n, stored_image_id); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, stored_image_id); + + if (!DoSetSelectionAfterInsertion(n, bSelect)) + page->Hide(); + + return true; + } + + bool InsertPage(size_t n, + const wxString& id, + wxWindow* page, + const wxString& text, + int imageId, + bool bSelect = false) { if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) return false; - m_pageNames.insert(m_pageNames.begin() + n, wxString()); - GetBtnsListCtrl()->InsertPage(n, text, bSelect); + m_pageNames.insert(m_pageNames.begin() + n, id); + m_pageImageIds.insert(m_pageImageIds.begin() + n, imageId); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, "", imageId); if (!DoSetSelectionAfterInsertion(n, bSelect)) page->Hide(); @@ -172,14 +208,14 @@ public: wxWindow * page, const wxString & text, const std::string& bmp_name = "", - const std::string& inactive_bmp_name = "", bool bSelect = false) { if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return false; m_pageNames.insert(m_pageNames.begin() + n, id); - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name); + m_pageImageIds.insert(m_pageImageIds.begin() + n, NO_IMAGE); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name); // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the // new page to the current page's rect — it never touches visibility. A freshly @@ -222,8 +258,7 @@ public: return DoSetSelection(n); } - // Neither labels nor images are supported but we still store the labels - // just in case the user code attaches some importance to them. + // Labels are stored by the custom button list; page images use the wx image-list IDs below. virtual bool SetPageText(size_t n, const wxString & strText) override { wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page")); @@ -239,14 +274,27 @@ public: return GetBtnsListCtrl()->GetPageText(n); } - virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override + virtual bool SetPageImage(size_t n, int imageId) override { - return false; + if (n >= m_pageImageIds.size()) + return false; + + if (!GetBtnsListCtrl()->SetPageImage(n, imageId)) + return false; + + m_pageImageIds[n] = imageId; + return true; } - virtual int GetPageImage(size_t WXUNUSED(n)) const override + virtual int GetPageImage(size_t n) const override { - return NO_IMAGE; + return n < m_pageImageIds.size() ? m_pageImageIds[n] : NO_IMAGE; + } + + void SetImageList(wxImageList* imageList) + { + m_imageList = imageList; + GetBtnsListCtrl()->SetImageList(imageList); } bool SetPageImage(size_t n, const std::string& bmp_name) @@ -271,6 +319,7 @@ public: virtual bool DeleteAllPages() override { m_pageNames.clear(); + m_pageImageIds.clear(); return wxBookCtrlBase::DeleteAllPages(); } @@ -431,6 +480,7 @@ protected: if (win) { m_pageNames.erase(m_pageNames.begin() + page); + m_pageImageIds.erase(m_pageImageIds.begin() + page); GetBtnsListCtrl()->RemovePage(page); DoSetSelectionAfterRemoval(page); } @@ -454,9 +504,47 @@ protected: } private: + static bool get_fixed_page_info(int imageId, wxString& page_name, std::string& bmp_name) + { + switch (imageId) { + case PAGE_HOME: + page_name = wxS("home"); + bmp_name = "tab_home_active"; + return true; + case PAGE_PREPARE: + page_name = wxS("prepare"); + bmp_name = "tab_3d_active"; + return true; + case PAGE_PREVIEW: + page_name = wxS("preview"); + bmp_name = "tab_preview_active"; + return true; + case PAGE_MONITOR: + page_name = wxS("monitor"); + bmp_name = "tab_monitor_active"; + return true; + case PAGE_MULTI_DEVICE: + page_name = wxS("multi_device"); + bmp_name = "tab_multi_active"; + return true; + case PAGE_PROJECT: + page_name = wxS("project"); + bmp_name = "tab_auxiliary_active"; + return true; + case PAGE_CALIBRATION: + page_name = wxS("calibration"); + bmp_name = "tab_calibration_active"; + return true; + default: + return false; + } + } + void Init(); std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages + std::vector m_pageImageIds; // index-parallel to wxBookCtrlBase::m_pages + wxImageList* m_imageList{nullptr}; wxShowEffect m_showEffect, m_hideEffect; diff --git a/src/slic3r/GUI/Tabbook.hpp b/src/slic3r/GUI/Tabbook.hpp index 0cea1b8326..7f10e9dd8d 100644 --- a/src/slic3r/GUI/Tabbook.hpp +++ b/src/slic3r/GUI/Tabbook.hpp @@ -108,7 +108,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(page, wxString(), ""/*true *//* select it */); + return AddPage(page, wxString()); } // Set effect to use for showing/hiding pages. @@ -139,14 +139,13 @@ public: // Implement base class pure virtual methods. - // adds a new page to the control bool AddPage(wxWindow* page, const wxString& text, - const std::string& bmp_name, - bool bSelect = false) + bool bSelect = false, + int imageId = NO_IMAGE) override { DoInvalidateBestSize(); - return InsertNewPage(GetPageCount(), page, text, bmp_name, bSelect); + return InsertPage(GetPageCount(), page, text, bSelect, imageId); } //// Page management @@ -167,23 +166,6 @@ public: return true; } - bool InsertNewPage(size_t n, - wxWindow * page, - const wxString & text, - const std::string& bmp_name = "", - bool bSelect = false) - { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) - return false; - - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name); - - if (bSelect) - SetSelection(n); - - return true; - } - bool RemovePage(size_t n) { if (!wxBookCtrlBase::RemovePage(n)) diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index e236c84e67..1a8cbefbce 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -95,14 +95,11 @@ void Button::SetIcon(const wxString& icon) } } -void Button::SetInactiveIcon(const wxString &icon) +void Button::SetIcon(const wxBitmap& icon) { - if (!icon.IsEmpty()) { - // BBS set button icon default size to 20 - this->inactive_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); - } else { - this->inactive_icon = ScalableBitmap(); - } + this->active_icon = ScalableBitmap(); + this->active_icon.bmp() = icon; + messureSize(); Refresh(); } @@ -260,9 +257,6 @@ void Button::Rescale() if (this->active_icon.bmp().IsOk()) this->active_icon.msw_rescale(); - if (this->inactive_icon.bmp().IsOk()) - this->inactive_icon.msw_rescale(); - messureSize(); if(m_has_style) @@ -293,11 +287,7 @@ void Button::render(wxDC& dc) wxSize szIcon; wxSize textSize = this->textSize.GetSize(); - ScalableBitmap icon; - if (m_selected || ((states & (int)StateColor::State::Hovered) != 0)) - icon = active_icon; - else - icon = inactive_icon; + ScalableBitmap icon = active_icon; wxSize padding = this->paddingSize; int spacing = 5; // Wrap text diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index bc093b512a..fdc511ec7c 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -34,12 +34,10 @@ class Button : public StaticBox wxSize minSize; // set by outer wxSize paddingSize; ScalableBitmap active_icon; - ScalableBitmap inactive_icon; StateColor text_color; bool pressedDown = false; - bool m_selected = true; bool canFocus = true; bool isCenter = true; bool vertical = false; @@ -61,8 +59,7 @@ public: bool SetFont(const wxFont& font) override; void SetIcon(const wxString& icon); - - void SetInactiveIcon(const wxString& icon); + void SetIcon(const wxBitmap& icon); void SetMinSize(const wxSize& size) override; void SetMaxSize(const wxSize& size) override; @@ -75,8 +72,6 @@ public: void SetTextColorNormal(wxColor const &color); - void SetSelected(bool selected = true) { m_selected = selected; } - bool Enable(bool enable = true) override; void EnableTooltipEvenDisabled();// The tip will be shown even if the button is disabled diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 3e7d534810..3b9b1498ee 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include #include #include @@ -204,6 +206,11 @@ void PluginPages::initialize(Notebook* parent) if (m_parent == nullptr) return; + // Keep image-list indices stable for the lifetime of this notebook. Removing an image + // would shift every later index, so deregistration only removes the page. + m_image_list = std::make_unique(20, 20, true, 0); + m_parent->SetImageList(m_image_list.get()); + for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { if (capability) on_cap_register(capability->identity()); @@ -214,6 +221,9 @@ void PluginPages::shutdown() { while (!m_pages.empty()) remove_page(m_pages.begin()->first); + if (m_parent != nullptr) + m_parent->SetImageList(nullptr); + m_image_list.reset(); m_parent = nullptr; } @@ -235,6 +245,15 @@ void PluginPages::on_cap_register(const PluginCapabilityId& id) if (!capability) return; + std::string icon; + try { + icon = capability->get_icon(); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key << ": " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key; + } + auto* page = new PluginPage(m_parent, std::move(capability)); if (!page->is_valid()) { page->Destroy(); @@ -242,8 +261,29 @@ void PluginPages::on_cap_register(const PluginCapabilityId& id) } const wxString title = wxString::FromUTF8(id.name); - const wxString page_id = wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name); - if (!m_parent->AddPage(page_id, page, title, "tab_auxiliary_active", "tab_auxiliary_active", false)) { + + int image_id = wxBookCtrlBase::NO_IMAGE; + if (!icon.empty() && m_image_list) { + try { + boost::filesystem::path icon_path(icon); + const std::string extension = icon_path.extension().string(); + if (extension == ".svg" || extension == ".png") + icon_path.replace_extension(); + + const wxBitmap bitmap = create_scaled_bitmap(icon_path.string(), m_parent, 20); + if (bitmap.IsOk()) + image_id = m_image_list->Add(bitmap); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key << ": " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key; + } + } + + page->set_icon_image_id(image_id); + if (!m_parent->AddPage(page, title, false, image_id)) { + if (image_id != wxBookCtrlBase::NO_IMAGE && m_image_list && image_id == m_image_list->GetImageCount() - 1) + m_image_list->Remove(image_id); page->Destroy(); return; } @@ -285,12 +325,39 @@ void PluginPages::remove_page(const PluginCapabilityId& id) return; PluginPage* page = it->second; + const int removed_image_id = page->get_icon_image_id(); page->detach_capability(); if (m_parent != nullptr) { const int index = m_parent->FindPage(page); if (index != wxNOT_FOUND) m_parent->RemovePage(static_cast(index)); } + + if (m_image_list && removed_image_id != wxBookCtrlBase::NO_IMAGE && + removed_image_id >= 0 && removed_image_id < m_image_list->GetImageCount()) { + m_image_list->Remove(removed_image_id); + + // wxImageList IDs are positional. Removing one shifts all later images down by + // one, so update both the page state and the notebook button for those pages. + for (const auto& [other_id, other_page] : m_pages) { + if (other_id == id) + continue; + + const int other_image_id = other_page->get_icon_image_id(); + if (other_image_id <= removed_image_id) + continue; + + const int updated_image_id = other_image_id - 1; + other_page->set_icon_image_id(updated_image_id); + + if (m_parent != nullptr) { + const int other_index = m_parent->FindPage(other_page); + if (other_index != wxNOT_FOUND) + m_parent->SetPageImage(static_cast(other_index), updated_image_id); + } + } + } + page->Destroy(); m_pages.erase(it); } diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index bf70305201..d925f2eb32 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include @@ -29,16 +31,20 @@ public: void on_new_window(wxWebViewEvent& event); void on_script_message(wxWebViewEvent& event); void push_message(const std::string& message); + void set_icon_image_id(int id) { m_icon_image_id = id; } + int get_icon_image_id() const { return m_icon_image_id; } private: void load_plugin_content(); wxString bootstrap_url() const; wxString web_base_url() const; - wxWebView* m_browser{nullptr}; - std::shared_ptr m_cap; - std::shared_ptr> m_lifetime; - bool m_content_loaded{false}; + wxWebView* m_browser{nullptr}; + std::shared_ptr m_cap; + std::shared_ptr> m_lifetime; + bool m_content_loaded{false}; + + int m_icon_image_id = wxBookCtrlBase::NO_IMAGE; }; class PluginPages @@ -47,7 +53,7 @@ public: PluginPages() = default; ~PluginPages(); - PluginPages(const PluginPages&) = delete; + PluginPages(const PluginPages&) = delete; PluginPages& operator=(const PluginPages&) = delete; void initialize(Notebook* parent); @@ -63,7 +69,9 @@ private: void remove_page(const PluginCapabilityId& id); std::map m_pages; - Notebook* m_parent{nullptr}; + Notebook* m_parent{nullptr}; + + std::unique_ptr m_image_list; }; } // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp index fc5d4a03b8..e009f4426f 100644 --- a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp @@ -23,6 +23,7 @@ void PagesPluginCapability::RegisterBindings(pybind11::module_& module) .def(py::init<>()) .def("get_type", &PagesPluginCapability::get_type) .def("get_ui", &PagesPluginCapability::get_ui) + .def("get_icon", &PagesPluginCapability::get_icon) .def("on_message", &PagesPluginCapability::on_message) .def( "post_message", diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp index e833d7dbf4..978492006c 100644 --- a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp @@ -18,6 +18,7 @@ public: virtual std::string get_ui() = 0; virtual void on_message(std::string message) { (void) message; } + virtual std::string get_icon() { return {}; } void post_message(std::string message); void set_message_sender(std::function sender); diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp index 71eb5c5bcc..3fb476c228 100644 --- a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp @@ -13,6 +13,17 @@ class PyPagesPluginCapabilityTrampoline : public PyPluginCommonTrampoline::PyPluginCommonTrampoline; + std::string get_icon() override + { + ORCA_PY_OVERRIDE_AUDITED( + ::Slic3r::PluginAuditManager::AuditMode::Loading, + [] {}, + PYBIND11_OVERRIDE, + std::string, + PagesPluginCapability, + get_icon); + } + std::string get_ui() override { ORCA_PY_OVERRIDE_AUDITED( From 14b05a4d8e73982af774a2a3d74104fb89f9b9ab Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 30 Jul 2026 01:54:46 +0800 Subject: [PATCH 14/71] fix: regression after merge --- src/slic3r/GUI/Monitor.cpp | 2 +- src/slic3r/GUI/Widgets/Button.hpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 2a057fd1e3..51bd7ed878 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -193,7 +193,7 @@ void MonitorPanel::init_tabpanel() //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), false); m_upgrade_panel = new UpgradePanel(m_tabpanel); - m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), "", false); + m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), false); m_hms_panel = new HMSPanel(m_tabpanel); m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), false); diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index a0778040ca..c98d583c34 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -38,6 +38,7 @@ class Button : public StaticBox StateColor text_color; bool pressedDown = false; + bool m_selected = true; bool canFocus = true; bool isCenter = true; bool vertical = false; From 01493d4e3ae2037393c249e318dc8e56a43c9896 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 4 Aug 2026 14:30:59 +0800 Subject: [PATCH 15/71] Add developer flag for printer agents --- src/libslic3r/AppConfig.cpp | 6 ++++++ src/slic3r/GUI/Preferences.cpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 159d9bbeda..1b170bf884 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -626,6 +626,12 @@ void AppConfig::set_defaults() set_bool("window_buttons_on_left", false); #endif + if (get("use_printer_agents").empty()) + { + // false = legacy behavior using print hosts + set_bool("use_printer_agents", false); + } + // Remove legacy window positions/sizes erase("app", "main_frame_maximized"); erase("app", "main_frame_pos"); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 6bcc00848b..1a3c6fd26a 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -2101,6 +2101,12 @@ void PreferencesDialog::create_items() auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets"); g_sizer->Add(item_show_unsupported); + auto item_plugin_printer_agents = create_item_checkbox( + _L("(Experimental) Use printer agents instead of print hosts"), _L( + "Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\nWhen disabled, OrcaSlicer uses the legacy print-host behavior."), + "use_printer_agents"); + g_sizer->Add(item_plugin_printer_agents); + //// DEVELOPER > Experimental Features g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND); From 75a2460649e13554d341e4e685bfc10324728eb6 Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:21:05 +0800 Subject: [PATCH 16/71] Replace fake-enum printer agent dropdown (#121) A dedicated PrinterAgentChoice field reads rows straight from the live agent registry and stores the agent id string, replacing the fake-coEnum index mapping. The field moves to TabPrinter and registers with the searcher so UnsavedChanges renders it; the PhysicalPrinterDialog copy and its update hook are removed (#125). switch_printer_agent now resolves ids via resolve_printer_agent_id. --- src/libslic3r/Config.hpp | 2 + src/slic3r/GUI/Field.cpp | 268 +++++++++++++++-------- src/slic3r/GUI/Field.hpp | 38 ++++ src/slic3r/GUI/GUI_App.cpp | 23 +- src/slic3r/GUI/GUI_App.hpp | 7 +- src/slic3r/GUI/OptionsGroup.cpp | 26 +++ src/slic3r/GUI/PhysicalPrinterDialog.cpp | 88 +------- src/slic3r/GUI/PhysicalPrinterDialog.hpp | 1 - src/slic3r/GUI/Tab.cpp | 55 +++++ 9 files changed, 312 insertions(+), 196 deletions(-) diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 6f4117d249..509095cbfc 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2273,6 +2273,8 @@ public: plugin_picker, // Raw JSON string value, edited through a dialog behind a button rather than in the row. plugin_config, + // PrinterAgentChoice + printer_agent_select, }; // Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map. diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 1fcaef1b52..8d05de13a4 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -35,6 +35,7 @@ #include "Widgets/TextCtrl.h" #include "../Utils/ColorSpaceConvert.hpp" +#include "../Utils/NetworkAgentFactory.hpp" #ifdef __WXOSX__ #define wxOSX true #else @@ -1403,39 +1404,6 @@ using choice_ctrl = ::ComboBox; // BBS static std::map dynamic_lists; -static bool is_plugin_printer_agent_key(const std::string& value) -{ - return value.rfind("plugin:", 0) == 0; -} - -static int printer_agent_item_for_enum_index(const choice_ctrl* field, int enum_index) -{ - if (!field) - return -1; - - const unsigned int count = field->GetCount(); - for (unsigned int idx = 0; idx < count; ++idx) { - if (void* data = field->GetClientData(idx)) { - const int stored = static_cast(reinterpret_cast(data)) - 1; - if (stored == enum_index) - return static_cast(idx); - } - } - - return -1; -} - -static int printer_agent_enum_index_for_item(const choice_ctrl* field, int item_index, int fallback) -{ - if (!field || item_index < 0) - return fallback; - - if (void* data = field->GetClientData(item_index)) - return static_cast(reinterpret_cast(data)) - 1; - - return fallback; -} - void Choice::register_dynamic_list(std::string const &optname, DynamicList *list) { dynamic_lists.emplace(optname, list); } void DynamicList::update() @@ -1518,33 +1486,7 @@ void Choice::BUILD() window = dynamic_cast(temp); if (! m_opt.enum_labels.empty() || ! m_opt.enum_values.empty()) { - if (m_opt_id == "printer_agent") { - const bool has_builtin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(), - [](const std::string& value) { return !is_plugin_printer_agent_key(value); }); - const bool has_plugin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(), - [](const std::string& value) { return is_plugin_printer_agent_key(value); }); - - auto append_agent_rows = [this, temp](bool plugins) { - for (size_t i = 0; i < m_opt.enum_values.size(); ++i) { - const bool is_plugin = is_plugin_printer_agent_key(m_opt.enum_values[i]); - if (is_plugin != plugins) - continue; - - const wxString label = i < m_opt.enum_labels.size() ? _(m_opt.enum_labels[i]) : wxString(m_opt.enum_values[i]); - const int item = temp->Append(label); - temp->SetClientData(item, reinterpret_cast(static_cast(i + 1))); - } - }; - - if (has_builtin_agents) { - temp->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); - append_agent_rows(false); - } - if (has_plugin_agents) { - temp->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); - append_agent_rows(true); - } - } else if (m_opt.enum_labels.empty()) { + if (m_opt.enum_labels.empty()) { // Append non-localized enum_values for (auto el : m_opt.enum_values) temp->Append(el); @@ -1651,7 +1593,7 @@ void Choice::set_selection() switch (m_opt.type) { case coEnum:{ const int val = m_opt.default_value->getInt(); - field->SetSelection(m_opt_id == "printer_agent" ? printer_agent_item_for_enum_index(field, val) : val); + field->SetSelection(val); break; } case coFloat: @@ -1701,12 +1643,7 @@ void Choice::set_value(const std::string& value, bool change_event) //! Redunda } choice_ctrl* field = dynamic_cast(window); - if (m_opt_id == "printer_agent") { - const int enum_index = idx == m_opt.enum_values.size() ? - (m_opt.default_value ? m_opt.default_value->getInt() : 0) : - static_cast(idx); - field->SetSelection(printer_agent_item_for_enum_index(field, enum_index)); - } else if (idx == m_opt.enum_values.size()) + if (idx == m_opt.enum_values.size()) field->SetValue(value); else field->SetSelection(idx); @@ -1772,33 +1709,11 @@ void Choice::set_value(const boost::any& value, bool change_event) case coEnum: // BBS case coEnums: { - auto printer_agent_index_from_key = [this](const std::string& key) { - auto it = std::find(m_opt.enum_values.begin(), m_opt.enum_values.end(), key); - if (it != m_opt.enum_values.end()) - return static_cast(it - m_opt.enum_values.begin()); - return m_opt.default_value ? m_opt.default_value->getInt() : 0; - }; - - int val = 0; - if (m_opt_id == "printer_agent") { - if (const int* int_value = boost::any_cast(&value)) - val = *int_value; - else if (const wxString* wx_value = boost::any_cast(&value)) - val = printer_agent_index_from_key(into_u8(*wx_value)); - else if (const std::string* string_value = boost::any_cast(&value)) - val = printer_agent_index_from_key(*string_value); - else { - m_disable_change_event = false; - return; - } - } else - val = boost::any_cast(value); + int val = boost::any_cast(value); int selection = val; - if (m_opt_id == "printer_agent") { - selection = printer_agent_item_for_enum_index(field, val); - } else if (m_opt_id == "input_shaping_type") { + if (m_opt_id == "input_shaping_type") { if (field != nullptr) { const unsigned int count = field->GetCount(); int match_index = -1; @@ -1920,12 +1835,6 @@ boost::any& Choice::get_value() { if (m_opt.nullable && field->GetSelection() == -1) m_value = ConfigOptionEnumsGenericNullable::nil_value(); - else if (m_opt_id == "printer_agent") - { - const int selection = field->GetSelection(); - const int fallback = m_opt.default_value ? m_opt.default_value->getInt() : 0; - m_value = printer_agent_enum_index_for_item(field, selection, fallback); - } else if (m_opt_id == "input_shaping_type") { int selection = field->GetSelection(); @@ -2067,6 +1976,171 @@ void Choice::msw_rescale() } +// PrinterAgentChoice + +void PrinterAgentChoice::reload_rows() +{ + auto* combo = dynamic_cast(window); // wxWidgets ComboBox + if (!combo) + return; + + // clear ComboBox + combo->Clear(); + + // helpers + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + const bool has_builtin_agents = std::any_of(agents.begin(), agents.end(), + [](const PrinterAgentInfo& a) { return !a.is_plugin(); }); + const bool has_plugin_agents = std::any_of(agents.begin(), agents.end(), + [](const PrinterAgentInfo& a) { return a.is_plugin(); }); + + auto append_agent_rows = [combo](bool is_plugin) + { + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + for (size_t i = 0; i < agents.size(); ++i) + { + if (agents[i].is_plugin() != is_plugin) + continue; + const int item = combo->Append(_(agents[i].display_name)); + // why: carry the agent-id string on the row. alias is an owned wxString (auto-freed, never rendered) + combo->SetItemAlias(item, from_u8(agents[i].id)); + } + }; + + // append rows + if (has_builtin_agents) + { + combo->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); + append_agent_rows(false); // append rows for agents that are not plugins + } + if (has_plugin_agents) + { + combo->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); + append_agent_rows(true); // append rows for agents that are plugins + } +} + +void PrinterAgentChoice::BUILD() +{ + wxSize size(def_width_wider() * m_em_unit, wxDefaultCoord); + if (m_opt.height >= 0) size.SetHeight(m_opt.height * m_em_unit); + if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit); + + static Builder builder; + choice_ctrl* temp = builder.build(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, + wxCB_READONLY); + temp->Clear(); + temp->GetDropDown().SetUseContentWidth(true); + if (parent_is_custom_ctrl && m_opt.height < 0) + opt_height = (double)temp->GetTextCtrl()->GetSize().GetHeight() / m_em_unit; + temp->SetTextLabel(_L(m_opt.sidetext)); + m_combine_side_text = true; +#ifdef __WXGTK3__ + wxSize best_sz = temp->GetBestSize(); + if (best_sz.x > size.x) temp->SetSize(best_sz); +#endif + if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT); + + window = dynamic_cast(temp); + + reload_rows(); + + temp->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_change_field(); }, temp->GetId()); + temp->SetToolTip(get_tooltip_text(temp->GetValue())); +} + +// Resolve CONFIG id string to a matching row in the live REGISTRY. "" uses the vendor default. +// An unregistered id clears selection and shows " (missing)" as free text. +void PrinterAgentChoice::set_value(const std::string& value, bool change_event) +{ + m_disable_change_event = !change_event; + + auto* field = dynamic_cast(window); + + // check if any row's corresponding id matches the agent id we are attempting to set + const std::string effective_agent_id = wxGetApp().resolve_printer_agent_id(value); + const unsigned int count = field->GetCount(); + int match = wxNOT_FOUND; + for (unsigned int i = 0; i < count; ++i) + { + if (into_u8(field->GetItemAlias(i)) == effective_agent_id) // if alias == id + { + match = static_cast(i); + break; + } + } + + // based on match or not, set selection and value + // - SetSelection and SetValue are UI to manipulate the display of the ComboBox + // - SetSelection automatically calls SetValue for the same value + // - we can also SetValue separately from SetSelection + if (match == wxNOT_FOUND) + { + field->SetSelection(wxNOT_FOUND); // nothing shows as selected in the dropdown + field->SetValue(from_u8(value + " (missing)")); // set a value not in the selection (upper display field) + } + else + { + // display name of agent shows both in upper display field and appears selected in dropdown + field->SetSelection(match); + } + + m_disable_change_event = false; +} + +// Accept boost::any values from callers (usually to OptionsGroup/Field parent classes) and normalize them to an agent id. +// Then use PrinterAgentChoice::set_value(std::string& value, ...) +void PrinterAgentChoice::set_value(const boost::any& value, bool change_event) +{ + m_disable_change_event = !change_event; + + auto* field = dynamic_cast(window); + if (value.empty()) + { + field->SetValue(""); + m_value = value; + m_disable_change_event = false; + return; + } + + std::string id; + if (const std::string* s = boost::any_cast(&value)) + id = *s; + else if (const wxString* w = boost::any_cast(&value)) + id = into_u8(*w); + set_value(id, change_event); +} + +// A real row returns its alias, which is the agent id. Header rows, missing rows, +// and no selection return empty boost::any so the custom writer leaves config unchanged. +boost::any& PrinterAgentChoice::get_value() +{ + auto* field = dynamic_cast(window); + const int sel = field->GetSelection(); + const std::string id = sel < 0 ? std::string{} : into_u8(field->GetItemAlias(sel)); + if (id.empty()) + m_value = boost::any{}; + else + m_value = id; + return m_value; +} + +void PrinterAgentChoice::enable() { dynamic_cast(window)->Enable(); } +void PrinterAgentChoice::disable() { dynamic_cast(window)->Disable(); } + +void PrinterAgentChoice::msw_rescale() +{ + Field::msw_rescale(); + + auto* field = dynamic_cast(window)->GetTextCtrl(); + wxSize size(wxDefaultSize); + size.SetWidth((m_opt.width > 0 ? m_opt.width : def_width_wider()) * m_em_unit); + field->SetMinSize(wxSize(-1, int(1.5f * field->GetFont().GetPixelSize().y + 0.5f))); + field->SetSize(size); + + dynamic_cast(window)->Rescale(); +} + void PluginField::BUILD() { auto* panel = new wxPanel(m_parent, wxID_ANY); diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 4e9c65da5d..e57a569561 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -469,6 +469,44 @@ public: void suppress_scroll(); }; +// printer_agent is a coString whose choices come from the live agent registry. +// PrinterAgentChoice uses a ComboBox directly because Choice expects static config enums. +// Real rows carry the stored agent id in the row alias (SetItemAlias/GetItemAlias). +class PrinterAgentChoice : public Field +{ + using Field::Field; + +public: + PrinterAgentChoice(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) + { + } + + PrinterAgentChoice(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field( + parent, opt, id) + { + } + + ~PrinterAgentChoice() + { + } + + wxWindow* window{nullptr}; + + void BUILD() override; + // Clear and repopulate rows from the live registry (grouped System agents / Plugins). + // Does not change selection; the caller follows with set_value(stored id). + void reload_rows(); + + void set_value(const std::string& value, bool change_event = false); + void set_value(const boost::any& value, bool change_event = false) override; + boost::any& get_value() override; + + void enable() override; + void disable() override; + void msw_rescale() override; + wxWindow* getWindow() override { return window; } +}; + class PluginField : public Field { using Field::Field; public: diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 4c4bdfd62c..83d4f2abaf 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3873,6 +3873,18 @@ unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) )); } +std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) +{ + if (!stored_id.empty()) + return stored_id; + return (preset_bundle && preset_bundle->is_bbl_vendor()) ? BBL_PRINTER_AGENT_ID : ORCA_PRINTER_AGENT_ID; +} + +std::string GUI_App::canonical_printer_agent_id(const std::string& picked_id) +{ + return picked_id == resolve_printer_agent_id("") ? std::string() : picked_id; +} + void GUI_App::switch_printer_agent() { if (!m_agent) { @@ -3880,17 +3892,8 @@ void GUI_App::switch_printer_agent() return; } - // Read printer_agent from config, falling back to default - std::string effective_agent_id = ORCA_PRINTER_AGENT_ID; - if (preset_bundle->is_bbl_vendor()) - effective_agent_id = BBL_PRINTER_AGENT_ID; - const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config; - if (config.has("printer_agent")) { - const std::string& value = config.option("printer_agent")->value; - if (!value.empty()) - effective_agent_id = value; - } + const std::string effective_agent_id = resolve_printer_agent_id(config.opt_string("printer_agent")); // Check if agent is registered const PrinterAgentInfo* agent_info_ptr = NetworkAgentFactory::get_printer_agent_info(effective_agent_id); diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index bda27d40ec..6a977d37fc 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -365,9 +365,14 @@ public: HMSQuery* get_hms_query() { return hms_query; } NetworkAgent* getAgent() { return m_agent; } - // Dynamic printer agent switching + // Reconcile the live printer agent with the stored preset selection. void switch_printer_agent(); + std::string resolve_printer_agent_id(const std::string& stored_id); + // ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id + // then, all resolve and canonical would just be ORCA<->"" + std::string canonical_printer_agent_id(const std::string& picked_id); + FilamentColorCodeQuery* get_filament_color_code_query(); bool is_editor() const { return m_app_mode == EAppMode::Editor; } bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 9fb4483883..25c13c4b8d 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -54,6 +54,9 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co case ConfigOptionDef::GUIType::one_string: m_fields.emplace(id, TextCtrl::Create(this->ctrl_parent(), opt, id)); break; case ConfigOptionDef::GUIType::plugin_picker: m_fields.emplace(id, PluginField::Create(this->ctrl_parent(), opt, id)); break; case ConfigOptionDef::GUIType::plugin_config: m_fields.emplace(id, PluginConfigField::Create(this->ctrl_parent(), opt, id)); break; + case ConfigOptionDef::GUIType::printer_agent_select: m_fields.emplace( + id, PrinterAgentChoice::Create(this->ctrl_parent(), opt, id)); + break; default: switch (opt.type) { case coFloatOrPercent: @@ -654,6 +657,16 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index void ConfigOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const boost::any& value) { + if (opt_id == "printer_agent") { + // TODO: Replace this option-specific branch with a generic value adapter if + // more fields need custom field-value to config-value conversion. + if (const std::string* id = boost::any_cast(&value)) + this->change_opt_value("printer_agent", wxGetApp().canonical_printer_agent_id(*id)); + + OptionsGroup::on_change_OG(opt_id, value); + return; + } + if (!m_opt_map.empty()) { auto it = m_opt_map.find(opt_id); if (it == m_opt_map.end()) { @@ -772,6 +785,19 @@ void ConfigOptionsGroup::back_to_config_value(const DynamicPrintConfig& config, } } #endif + else if (opt_key == "printer_agent") + { + // why: printer_agent is a coString kept out of m_opt_map. The generic non-opt_map revert + // below restores the edited config from get_value(), but a deregistered/"(missing)" saved + // id has no selectable row, so the field yields no value and the edited config keeps the + // user's interim pick -> stuck dirty. Restore the SAVED id straight into the edited config + // (displayable or not; config is the saved or system baseline), then repaint and notify. + const std::string saved_id = config.opt_string("printer_agent"); + set_value(opt_key, saved_id); + this->change_opt_value(opt_key, saved_id); + OptionsGroup::on_change_OG(opt_key, saved_id); + return; + } else if (m_opt_map.find(opt_key) == m_opt_map.end() || // This option don't have corresponded field opt_key == "printable_area" || opt_key == "compatible_printers" || opt_key == "compatible_prints" || opt_key == "thumbnails" || diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index b40cd22697..4c9dd60d55 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -25,7 +25,6 @@ #include "GUI.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" -#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "format.hpp" #include "Tab.hpp" #include "wxExtensions.hpp" @@ -128,22 +127,8 @@ PhysicalPrinterDialog::~PhysicalPrinterDialog() void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgroup) { m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - // Special handling for printer_agent: convert fake enum index to string agent ID - if (opt_key == "printer_agent") { - try { - int selected_idx = boost::any_cast(value); - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - if (selected_idx >= 0 && selected_idx < static_cast(agents.size())) { - m_config->set_key_value("printer_agent", - new ConfigOptionString(agents[selected_idx].id)); - } - } catch (const boost::bad_any_cast&) { - // If value is not an int, ignore - } + if (opt_key == "host_type" || opt_key == "printhost_authorization_type") this->update(); - } else if (opt_key == "host_type" || opt_key == "printhost_authorization_type") { - this->update(); - } if (opt_key == "print_host") this->update_printhost_buttons(); if (opt_key == "printhost_port") @@ -154,47 +139,6 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr m_optgroup->append_single_option_line("host_type"); - // Build printer agent dropdown from registry (only if network agent is available) - if (wxGetApp().getAgent() != nullptr) { - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - - if (!agents.empty()) { - // Create a fake enum option to force a Choice widget instead of TextCtrl - // (printer_agent is coString in config, but we need a dropdown) - ConfigOptionDef def; - def.type = coEnum; - def.width = Field::def_width_wider(); - def.label = L("Printer Agent"); - def.tooltip = L("Select the network agent implementation for printer communication. " - "Available agents are registered at startup."); - def.mode = comAdvanced; - - // Populate enum values and labels from registered agents - for (const auto& agent : agents) { - def.enum_values.push_back(agent.id); - def.enum_labels.push_back(agent.display_name); - } - - // Resolve selected agent: use config value if valid, otherwise fall back to default - std::string selected_agent = m_config->opt_string("printer_agent"); - auto it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); - if (it == agents.end()) { - selected_agent = ORCA_PRINTER_AGENT_ID; - it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); - } - - if (it != agents.end()) { - size_t default_idx = std::distance(agents.begin(), it); - def.set_default_value(new ConfigOptionInt(static_cast(default_idx))); - } - - // Create and append the option line - auto agent_option = Option(def, "printer_agent"); - Line agent_line = m_optgroup->create_single_option_line(agent_option); - m_optgroup->append_line(agent_line); - } - } - auto create_sizer_with_btn = [](wxWindow* parent, Button** btn, const std::string& icon_name, const wxString& label) { *btn = new Button(parent, label); (*btn)->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); @@ -816,31 +760,6 @@ void PhysicalPrinterDialog::update_host_type(bool printer_change) } } -void PhysicalPrinterDialog::update_printer_agent_type() -{ - if (m_config == nullptr) - return; - - Field* agent_field = m_optgroup->get_field("printer_agent"); - if (!agent_field) - return; - - Choice* agent_choice = dynamic_cast(agent_field); - if (!agent_choice) - return; - - // Sync selection with current config value - const std::string current_agent = m_config->opt_string("printer_agent"); - - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - for (size_t i = 0; i < agents.size(); ++i) { - if (agents[i].id == current_agent) { - agent_choice->set_value(i); - return; - } - } -} - void PhysicalPrinterDialog::update_printers() { wxBusyCursor wait; @@ -894,11 +813,6 @@ void PhysicalPrinterDialog::OnOK(wxEvent& event) { wxGetApp().get_tab(Preset::TYPE_PRINTER)->save_preset("", false, false, true, m_preset_name); event.Skip(); - - // Defer printer agent switch to ensure preset save completes first - wxGetApp().CallAfter([] { - wxGetApp().switch_printer_agent(); - }); } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index 694e7aaf90..0ba2cad54f 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -60,7 +60,6 @@ public: void update(bool printer_change = false); void update_host_type(bool printer_change); - void update_printer_agent_type(); void update_preset_input(); void update_printhost_buttons(); void update_printers(); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c8fa524ca5..857abdce67 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -33,6 +33,7 @@ #include "GUI_App.hpp" #include "GUI_ObjectList.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/plugin/PluginConfig.hpp" #include "Plater.hpp" @@ -5018,6 +5019,40 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("gcode_flavor", "printer_basic_information_advanced#g-code-flavor"); optgroup->append_single_option_line("pellet_modded_printer", "printer_basic_information_advanced#pellet-modded-printer"); optgroup->append_single_option_line("bbl_use_printhost", "printer_basic_information_advanced#use-3rd-party-print-host"); + + // "Printer Agent" dropdown - printer_agent is a coString; gui_type routes it to + // PrinterAgentChoice instead of a TextCtrl. Rows and values come from the live agent + // registry, and the value is stored as the agent-id string. + if (wxGetApp().getAgent() != nullptr) + { + auto registered_printer_agents = NetworkAgentFactory::get_registered_printer_agents(); + if (!registered_printer_agents.empty()) + { + ConfigOptionDef def; + def.type = coString; + def.gui_type = ConfigOptionDef::GUIType::printer_agent_select; + def.width = 3 * Field::def_width_wider() / 2; + def.label = L("Printer Agent"); + def.tooltip = L("Select the network agent implementation for printer communication. " + "Available agents are registered at startup."); + def.mode = comAdvanced; + + // Create the field without get_option() so it is not registered in m_opt_map. + // ConfigOptionsGroup handles printer_agent before the generic mapped write path. + Line agent_line = optgroup->create_single_option_line(Option(def, "printer_agent")); + optgroup->append_line(agent_line); + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) + choice->set_value(m_config->opt_string("printer_agent"), false); + } + + // Register by hand so the UnsavedChanges dialog can render a row for it. + wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title, + optgroup->config_category()); + } + } + optgroup->append_single_option_line("use_3mf"); optgroup->append_single_option_line("scan_first_layer" , "printer_basic_information_advanced#scan-first-layer"); optgroup->append_single_option_line("enable_power_loss_recovery", "printer_basic_information_advanced#power-loss-recovery"); @@ -5884,6 +5919,16 @@ void TabPrinter::reload_config() // so update it implicitly if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); + + // m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly. + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) + { + const std::string selected_agent = m_config->opt_string("printer_agent"); + choice->set_value(selected_agent, false); + } + } } void TabPrinter::activate_selected_page(std::function throw_if_canceled) @@ -5894,6 +5939,16 @@ void TabPrinter::activate_selected_page(std::function throw_if_canceled) // so update it implicitly if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); + + // m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly. + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) + { + const std::string selected_agent = m_config->opt_string("printer_agent"); + choice->set_value(selected_agent, false); + } + } } void TabPrinter::clear_pages() From b2f08c3ff806ff3558e5c23ecb92ce9c3862a530 Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:27:53 +0800 Subject: [PATCH 17/71] Reset device selection on agent swap or unload (#124) set_live_printer_agent centralizes the swap: deselect the machine, clear stale sidebar state and the previous agent's Other Devices, then install the new agent (or null when its provider vanished). Plugin load/unload callbacks refresh the dropdown and re-run agent selection. load_last_machine no longer falls back to the first available machine. --- src/slic3r/GUI/DeviceCore/DevManager.cpp | 70 ++++++++--------- src/slic3r/GUI/DeviceCore/DevManager.h | 8 +- src/slic3r/GUI/GUI_App.cpp | 95 +++++++++++++++++++++--- src/slic3r/GUI/GUI_App.hpp | 5 ++ src/slic3r/GUI/Tab.cpp | 18 +++++ src/slic3r/GUI/Tab.hpp | 1 + 6 files changed, 150 insertions(+), 47 deletions(-) diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index 2d54b5c85f..3c664facfd 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -496,6 +496,26 @@ namespace Slic3r OnSelectedMachineChanged(previous_selected_machine, selected_machine); } + void DeviceManager::clear_other_devices() + { + // why: on agent swap, keep "My Devices" but drop the transient "Other Devices" + // Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. + const auto my = get_my_machine_list(); + for (auto it = localMachineList.begin(); it != localMachineList.end();) + { + if (my.find(it->first) == my.end()) + { + // not a "My Device" -> an "Other Device" + delete it->second; + it = localMachineList.erase(it); + } + else + { + ++it; + } + } + } + bool DeviceManager::set_selected_machine(std::string dev_id) { BOOST_LOG_TRIVIAL(info) << "set_selected_machine=" << dev_id @@ -558,7 +578,6 @@ namespace Slic3r } else { - Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning(); if (m_agent) { if (it->second->connection_type() != "lan" || it->second->connection_type().empty()) @@ -592,7 +611,6 @@ namespace Slic3r } selected_machine = dev_id; - record_user_last_machine(selected_machine); return true; } @@ -851,44 +869,26 @@ namespace Slic3r int result = m_agent->get_user_print_info(&http_code, &body, provider); if (result == 0) { - parse_user_print_info(body); + // parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map. + // on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info. + Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); }); } } - void DeviceManager::record_user_last_machine(const std::string& dev_id) - { - if (Slic3r::GUI::wxGetApp().app_config) { - Slic3r::GUI::wxGetApp().app_config->set("user_last_selected_machine", dev_id); - } - } - - std::string DeviceManager::get_user_last_machine() const - { - if (Slic3r::GUI::wxGetApp().app_config) { - const auto& user_last_machine = Slic3r::GUI::wxGetApp().app_config->get("user_last_selected_machine"); - if (!user_last_machine.empty()) { - return user_last_machine; - } else if (m_agent) { - return m_agent->get_user_selected_machine(); - } - } - - return ""; - } - void DeviceManager::load_last_machine() { - if (userMachineList.empty()) return; - else if (userMachineList.size() == 1) { - this->set_selected_machine(userMachineList.begin()->second->get_dev_id()); - } else { - const auto& last_monitor_machine = get_user_last_machine(); - if (userMachineList.find(last_monitor_machine) != userMachineList.end()) { - set_selected_machine(last_monitor_machine); - } else { - this->set_selected_machine(userMachineList.begin()->second->get_dev_id()); - } - } + // Get all available machines, include cloud machines and lan machines that have access right + auto all_machines = get_my_machine_list(); + if (all_machines.empty()) + return; + + // Reconnect the machine the user last selected, if it's still available. + // why: no first-available fallback - auto-connecting an arbitrary machine + // fights the agent-swap reset, which intentionally leaves nothing selected. + const std::string last_monitor_machine = m_agent ? m_agent->get_user_selected_machine() : ""; + const auto last_machine = all_machines.find(last_monitor_machine); + if (last_machine != all_machines.end()) + this->set_selected_machine(last_machine->second->get_dev_id()); } void DeviceManager::OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state) diff --git a/src/slic3r/GUI/DeviceCore/DevManager.h b/src/slic3r/GUI/DeviceCore/DevManager.h index 1f7f87b7fb..70bee613a8 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.h +++ b/src/slic3r/GUI/DeviceCore/DevManager.h @@ -48,8 +48,9 @@ public: MachineObject* get_selected_machine(); bool set_selected_machine(std::string dev_id); - void record_user_last_machine(const std::string& dev_id); - std::string get_user_last_machine() const; + // why: clears stale sidebar sync-status / AMS visuals. Public so the printer-agent + // swap path can reuse it instead of duplicating the two sidebar calls. + void OnSelectedMachineLost(); // local machine void set_local_selected_machine(std::string dev_id) { local_selected_machine = dev_id; }; @@ -70,6 +71,8 @@ public: void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void clean_user_info(bool keep_local_selection = false); + void clear_other_devices(); + void load_last_machine(); void update_user_machine_list_info(const std::string& provider); void parse_user_print_info(std::string body); @@ -110,7 +113,6 @@ private: void check_pushing(); void OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state); - void OnSelectedMachineLost(); void OnSelectedMachineChanged(const std::string& pre_dev_id, const std::string& new_dev_id); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 83d4f2abaf..14edeb8038 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2809,16 +2809,58 @@ void GUI_App::init_plugin_gui_wiring() }); }; + // why: a newly loaded plugin only adds a selectable agent + // refresh the dropdown and leave the live agent alone + auto refresh_printer_agent_dropdown_after_load = [](const std::string&) + { + if (!wxTheApp) + return; + + GUI_App* app = &GUI::wxGetApp(); + if (app->is_closing()) + return; + + app->CallAfter([app] + { + if (!app->is_closing()) + app->refresh_printer_agent_dropdown(); + }); + }; + + // why: the unloaded plugin may have been the provider of the live agent + // re-run selection, where a now-missing agent will be cleared + // refresh dropdown after + auto switch_printer_agent_after_unload = [](const std::string&) + { + if (!wxTheApp) + return; + + GUI_App* app = &GUI::wxGetApp(); + if (app->is_closing()) + return; + + app->CallAfter([app] { + if (app->is_closing()) + return; + + app->switch_printer_agent(); + app->refresh_printer_agent_dropdown(); + }); + }; + plugin_mgr.subscribe_on_unload_callback(PluginHostUi::close_windows_for_plugin); plugin_mgr.subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin); plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); + plugin_mgr.subscribe_on_load_callback(refresh_printer_agent_dropdown_after_load); + plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload); plugin_mgr.subscribe_on_capability_load_callback( - [refresh_plugins_dialog](const PluginCapabilityId& capability) { + [refresh_plugins_dialog, refresh_printer_agent_dropdown_after_load](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name); refresh_plugins_dialog(); + refresh_printer_agent_dropdown_after_load(capability.plugin_key); // A newly loaded capability may satisfy a missing-plugin notification; re-validate the // current plate (on the UI thread) so the notification clears once its plugin is available. if (wxTheApp && !wxGetApp().is_closing()) @@ -2828,10 +2870,11 @@ void GUI_App::init_plugin_gui_wiring() }); }); plugin_mgr.subscribe_on_capability_unload_callback( - [refresh_plugins_dialog](const PluginCapabilityId& capability) { + [refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); refresh_plugins_dialog(); + switch_printer_agent_after_unload(capability.plugin_key); }); } @@ -3873,6 +3916,36 @@ unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) )); } +void GUI_App::refresh_printer_agent_dropdown() +{ + if (Tab* tab = get_tab(Preset::TYPE_PRINTER)) + { + if (auto* printer_tab = dynamic_cast(tab)) + printer_tab->refresh_printer_agent_dropdown(); + } +} + +void GUI_App::set_live_printer_agent(std::shared_ptr agent) +{ + if (!m_agent) + return; + + // why: tearing down the old machine selection is only ever the prefix of setting the live + // agent (to a new one, or to null when the selection is missing) - so it lives here, not as + // a standalone helper. Pass nullptr to clear the selection. + if (DeviceManager* dev = getDeviceManager()) + { + dev->set_selected_machine(""); // why: empty id disconnects and deselects the current machine + m_agent->set_user_selected_machine(""); + // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) + dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS + dev->clear_other_devices(); // why: drop stale LAN discoveries; keep My Devices + } + + m_agent->set_printer_agent(agent); + sidebar().update_all_preset_comboboxes(); +} + std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) { if (!stored_id.empty()) @@ -3898,9 +3971,11 @@ void GUI_App::switch_printer_agent() // Check if agent is registered const PrinterAgentInfo* agent_info_ptr = NetworkAgentFactory::get_printer_agent_info(effective_agent_id); if (!agent_info_ptr) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": unregistered agent ID '" << effective_agent_id - << "', keeping current agent"; - // Keep current agent, don't switch + // why: the selected agent's provider is gone (e.g. plugin unloaded); leaving the old + // live agent up would keep talking to a machine the user can no longer select. + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": agent ID '" << effective_agent_id + << "' is unregistered; clearing live printer agent"; + set_live_printer_agent(nullptr); return; } const PrinterAgentInfo agent_info = *agent_info_ptr; @@ -3914,7 +3989,9 @@ void GUI_App::switch_printer_agent() NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir); if (!new_printer_agent) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent"; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id + << "'; clearing live printer agent"; + set_live_printer_agent(nullptr); return; } @@ -3937,9 +4014,9 @@ void GUI_App::switch_printer_agent() return; } - // Swap the agent - m_agent->set_printer_agent(new_printer_agent); - sidebar().update_all_preset_comboboxes(); + // Swap the agent; set_live_printer_agent resets the device selection so the new + // agent starts clean (#124). + set_live_printer_agent(new_printer_agent); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 6a977d37fc..8bf32df64c 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -803,6 +803,11 @@ private: void window_pos_center(wxTopLevelWindow *window); bool select_language(); + // Dynamic printer agent selection - internal helpers for switch_printer_agent + // and the plugin load/unload callbacks (init_plugin_gui_wiring). + void refresh_printer_agent_dropdown(); + void set_live_printer_agent(std::shared_ptr agent); // null clears the selection + bool config_wizard_startup(); void check_updates(const bool verbose); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 857abdce67..bade7fee98 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -7907,6 +7907,24 @@ bool TabPrinter::apply_extruder_cnt_from_cache() return false; } +void TabPrinter::refresh_printer_agent_dropdown() const +{ + auto* choice = dynamic_cast(get_field("printer_agent")); + if (!choice || !choice->getWindow()) + return; + + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + if (agents.empty()) + return; + + // why: rows live on PrinterAgentChoice now; rebuild them from the live registry and re-select the stored id. + const std::string selected_agent = wxGetApp().preset_bundle->printers.get_edited_preset() + .config.opt_string("printer_agent"); + choice->reload_rows(); + choice->set_value(selected_agent, false); + this->GetParent()->Layout(); +} + bool Tab::validate_custom_gcodes() { if (m_type != Preset::TYPE_FILAMENT && diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 7187aff467..19eb0b849d 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -675,6 +675,7 @@ public: wxSizer* create_bed_shape_widget(wxWindow* parent); void cache_extruder_cnt(const DynamicPrintConfig* config = nullptr); bool apply_extruder_cnt_from_cache(); + void refresh_printer_agent_dropdown() const; }; class TabSLAMaterial : public Tab From dd2cb92685b27820b059592d5c0f02856f6403bb Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:36:47 +0800 Subject: [PATCH 18/71] Gate agent mode behind use_printer_agents toggle Replace per-printer auto-activation (is_current_printer_agent_plugin) with a global experimental AppConfig toggle, default off: legacy print-host behavior is unchanged until the user opts in. The toggle drives device-tab routing, print button defaults, connect-button visibility and sidebar layout, and dedups machine-select dialog opens. --- src/slic3r/GUI/MainFrame.cpp | 9 ++-- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 54 +++++++++++++----------- src/slic3r/GUI/Preferences.cpp | 8 ++++ src/slic3r/Utils/NetworkAgentFactory.cpp | 20 --------- src/slic3r/Utils/NetworkAgentFactory.hpp | 2 - 6 files changed, 43 insertions(+), 52 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7b638e3316..39082a9dca 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -708,7 +708,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ m_print_enable = get_enable_print_status(); m_print_btn->Enable(m_print_enable); if (m_print_enable) { - if (wxGetApp().preset_bundle->use_bbl_network()) + if (wxGetApp().preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE)); else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE)); @@ -1999,7 +1999,8 @@ wxBoxSizer* MainFrame::create_side_tools() SidePopup* p = new SidePopup(this); if (wxGetApp().preset_bundle - && !wxGetApp().preset_bundle->is_bbl_vendor()) { + && !wxGetApp().preset_bundle->is_bbl_vendor() + && !wxGetApp().app_config->get_bool("use_printer_agents")) { // ThirdParty Buttons SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); export_gcode_btn->SetCornerRadius(0); @@ -2132,7 +2133,7 @@ wxBoxSizer* MainFrame::create_side_tools() const auto preset_bundle = wxGetApp().preset_bundle; if (preset_bundle) { - if (preset_bundle->use_bbl_network()) { + if (preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { // BBL network support everything } else { support_send = false; // All 3rd print hosts do not have the send options @@ -4253,7 +4254,7 @@ void MainFrame::load_printer_url(wxString url, wxString apikey) void MainFrame::load_printer_url() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin()) + if (preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents")) return; auto cfg = preset_bundle.printers.get_edited_preset().config; diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 4c9dd60d55..989cf204e1 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -669,7 +669,7 @@ void PhysicalPrinterDialog::update(bool printer_change) } // For bbl printers, show option to control the device tab - if (wxGetApp().preset_bundle->is_bbl_vendor()) { + if (wxGetApp().preset_bundle->is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) { m_optgroup->show_field("bbl_use_print_host_webui"); const bool use_print_host_webui = !current_webui.empty(); if (Field* printhost_webui_field = m_optgroup->get_field("bbl_use_print_host_webui"); printhost_webui_field) { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 106c142fea..d83ddded34 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3246,7 +3246,7 @@ void Sidebar::update_all_preset_comboboxes() auto p_mainframe = wxGetApp().mainframe; auto cfg = preset_bundle.printers.get_edited_preset().config; - const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin(); + const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents"); if (preset_bundle.use_bbl_network()) { //only show connection button for not-BBL printer @@ -3258,7 +3258,8 @@ void Sidebar::update_all_preset_comboboxes() p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); } else { //p->btn_connect_printer->Show(); - p->m_printer_connect->Show(); + // ORCA: hide the physical-printer connection button when printer agents are enabled + p->m_printer_connect->Show(!wxGetApp().app_config->get_bool("use_printer_agents")); // ORCA: show/hide sync-ams button based on filament sync mode auto agent = wxGetApp().getAgent(); @@ -3280,7 +3281,9 @@ void Sidebar::update_all_preset_comboboxes() const auto host_type = cfg.option>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) apikey = cfg.opt_string("printhost_apikey"); - print_btn_type = preset_bundle.is_bbl_vendor() ? MainFrame::PrintSelectType::ePrintPlate : MainFrame::PrintSelectType::eSendGcode; + print_btn_type = (preset_bundle.is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) + ? MainFrame::PrintSelectType::ePrintPlate + : MainFrame::PrintSelectType::eSendGcode; } if (!use_native_device_tab) @@ -3439,7 +3442,10 @@ void Sidebar::update_presets(Preset::Type preset_type) bool isBBL = preset_bundle.is_bbl_vendor(); bool is_dual_extruder = extruder_variants->size() == 2; - p->layout_printer(preset_bundle.use_bbl_network(), isBBL && is_dual_extruder); + // why: agent mode drives the native device tab, so the sidebar lays out like BBL + // (no physical-printer connect button). + p->layout_printer(preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"), + isBBL && is_dual_extruder); // Update nozzle titles from printer config (e.g. "Main Nozzle" / "Auxiliary Nozzle" for N6) // UI left = DEPUTY_EXTRUDER_ID(1), UI right = MAIN_EXTRUDER_ID(0) @@ -5625,6 +5631,7 @@ struct Plater::priv void on_action_slice_all(SimpleEvent&); void on_action_publish(wxCommandEvent &evt); void on_action_print_plate(SimpleEvent&); + void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL); void on_action_print_all(SimpleEvent&); void on_action_export_gcode(SimpleEvent&); void on_action_send_gcode(SimpleEvent&); @@ -11166,18 +11173,23 @@ void Plater::priv::on_action_print_plate(SimpleEvent&) } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_network()) { - // BBS - if (!m_select_machine_dlg) - m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL); - m_select_machine_dlg->prepare(partplate_list.get_curr_plate_index()); - m_select_machine_dlg->ShowModal(); + if (preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { + open_machine_select_dialog(partplate_list.get_curr_plate_index()); } else { q->send_gcode_legacy(PLATE_CURRENT_IDX, nullptr); } } +void Plater::priv::open_machine_select_dialog(int plate_idx, PrintFromType print_type) +{ + // BBS + if (!m_select_machine_dlg) + m_select_machine_dlg = new SelectMachineDialog(q); + m_select_machine_dlg->set_print_type(print_type); + m_select_machine_dlg->prepare(plate_idx); + m_select_machine_dlg->ShowModal(); +} + void Plater::priv::on_action_send_to_multi_machine(SimpleEvent&) { if (!m_send_multi_dlg) @@ -11193,10 +11205,7 @@ void Plater::priv::on_action_print_plate_from_sdcard(SimpleEvent&) } //BBS - if (!m_select_machine_dlg) m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_SDCARD_VIEW); - m_select_machine_dlg->prepare(0); - m_select_machine_dlg->ShowModal(); + open_machine_select_dialog(0, PrintFromType::FROM_SDCARD_VIEW); } void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) @@ -11211,13 +11220,13 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview; update_sidebar(); int old_sel = e.GetOldSelection(); - const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin(); + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_native_device_tab = wxGetApp().preset_bundle && - (wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin); + (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); if (use_native_device_tab && new_sel == MainFrame::tpMonitor) { // BBL network module is only required for BBL-vendor printers. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. - if (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { + if (!use_printer_agents && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { e.Veto(); BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel; if (q) { @@ -11273,13 +11282,8 @@ void Plater::priv::on_action_print_all(SimpleEvent&) } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_network()) { - // BBS - if (!m_select_machine_dlg) - m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL); - m_select_machine_dlg->prepare(PLATE_ALL_IDX); - m_select_machine_dlg->ShowModal(); + if (preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { + open_machine_select_dialog(PLATE_ALL_IDX); } else { q->send_gcode_legacy(PLATE_ALL_IDX, nullptr); } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 1a3c6fd26a..f802ba6ecb 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1135,6 +1135,14 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT); } + if (param == "use_printer_agents") + { + // Rebuild the Device tab so the native/web-UI choice reflects the new flag + // immediately, instead of only on the next printer-preset change or restart. + if (wxGetApp().plater()) + wxGetApp().plater()->sidebar().update_all_preset_comboboxes(); + } + if (param == "enable_high_low_temp_mixed_printing") { if (checkbox->GetValue()) { const wxString warning_title = _L("Bed Temperature Difference Warning"); diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index 3883d99f2e..ff950d0946 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -465,25 +465,5 @@ void NetworkAgentFactory::deregister_python_printer_agent(const std::string& plu << plugin_key << "' with agent ID '" << agent_id << "'"; } -bool NetworkAgentFactory::is_current_printer_agent_plugin() -{ - auto* preset_bundle = GUI::wxGetApp().preset_bundle; - if (!preset_bundle) - return false; - - std::string agent_key = ORCA_PRINTER_AGENT_ID; - if (preset_bundle->is_bbl_vendor()) - agent_key = BBL_PRINTER_AGENT_ID; - - const auto& cfg = preset_bundle->printers.get_edited_preset().config; - if (cfg.has("printer_agent")) { - const std::string& value = cfg.option("printer_agent")->value; - if (!value.empty()) - agent_key = value; - } - - const PrinterAgentInfo* info = get_printer_agent_info(agent_key); - return info && info->is_plugin(); -} } // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp index cfff6fb1c7..a055b19493 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.hpp +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -166,8 +166,6 @@ public: static void register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name); static void deregister_python_printer_agent(const std::string& plugin_key, const std::string& capability_name); - static bool is_current_printer_agent_plugin(); - private: // Factory is not instantiable NetworkAgentFactory() = delete; From 5d953f915aaeabed49316579de84dd30a077f7fb Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:37:38 +0800 Subject: [PATCH 19/71] Keep Bambu AMS dialect out of the agent waist M620 is Bambu firmware dialect, not a neutral command. Composing it in MachineObject let non-Bambu agents (Moonraker/Klipper) forward it and report success on firmware that cannot run it. Agents now own the dialect: the default refusal on IPrinterAgent returns not-supported so the UI can say so; BBLPrinterAgent keeps the byte-identical composition. --- src/slic3r/GUI/DeviceManager.cpp | 24 +++++++---- src/slic3r/Utils/BBLPrinterAgent.cpp | 61 ++++++++++++++++++++++++++++ src/slic3r/Utils/BBLPrinterAgent.hpp | 10 +++++ src/slic3r/Utils/IPrinterAgent.hpp | 10 +++++ src/slic3r/Utils/NetworkAgent.cpp | 21 ++++++++++ src/slic3r/Utils/NetworkAgent.hpp | 3 ++ 6 files changed, 120 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 5499694686..c6c8006160 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -1733,9 +1733,11 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read int MachineObject::command_ams_calibrate(int ams_id) { - std::string gcode_cmd = (boost::format("M620 C%1% \n") % ams_id).str(); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; - return this->publish_gcode(gcode_cmd); + if (!m_agent) return -1; + int rtn = m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()); + if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) + show_unsupported_dlg(rtn); + return rtn; } int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max) @@ -1773,9 +1775,11 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s int MachineObject::command_ams_refresh_rfid(std::string tray_id) { - std::string gcode_cmd = (boost::format("M620 R%1% \n") % tray_id).str(); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; - return this->publish_gcode(gcode_cmd); + if (!m_agent) return -1; + int rtn = m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()); + if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) + show_unsupported_dlg(rtn); + return rtn; } int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id) @@ -1791,9 +1795,11 @@ int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id) int MachineObject::command_ams_select_tray(std::string tray_id) { - std::string gcode_cmd = (boost::format("M620 P%1% \n") % tray_id).str(); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; - return this->publish_gcode(gcode_cmd); + if (!m_agent) return -1; + int rtn = m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()); + if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) + show_unsupported_dlg(rtn); + return rtn; } int MachineObject::command_ams_control(std::string action) diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index ef85e0a1ff..9d422552fe 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -2,7 +2,9 @@ #include "BBLNetworkPlugin.hpp" #include "NetworkAgentFactory.hpp" +#include #include +#include namespace Slic3r { @@ -20,6 +22,65 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr cloud) // Communication // ============================================================================ +std::string BBLPrinterAgent::ams_refresh_rfid_gcode(const std::string& tray_id) +{ + return (boost::format("M620 R%1% \n") % tray_id).str(); +} + +std::string BBLPrinterAgent::ams_calibrate_gcode(int ams_id) +{ + return (boost::format("M620 C%1% \n") % ams_id).str(); +} + +std::string BBLPrinterAgent::ams_select_tray_gcode(const std::string& tray_id) +{ + return (boost::format("M620 P%1% \n") % tray_id).str(); +} + +int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) +{ + const std::string gcode = ams_refresh_rfid_gcode(tray_id); + BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; + nlohmann::json j; + j["print"]["command"] = "gcode_line"; + j["print"]["param"] = gcode; + j["print"]["sequence_id"] = std::to_string(sequence_id); + return publish(dev_id, j, lan_mode); +} + +int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) +{ + const std::string gcode = ams_calibrate_gcode(ams_id); + BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; + nlohmann::json j; + j["print"]["command"] = "gcode_line"; + j["print"]["param"] = gcode; + j["print"]["sequence_id"] = std::to_string(sequence_id); + return publish(dev_id, j, lan_mode); +} + +int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) +{ + const std::string gcode = ams_select_tray_gcode(tray_id); + BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; + nlohmann::json j; + j["print"]["command"] = "gcode_line"; + j["print"]["param"] = gcode; + j["print"]["sequence_id"] = std::to_string(sequence_id); + return publish(dev_id, j, lan_mode); +} + +int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode) +{ + const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0); + if (rtn == 0) { + BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn; + } else { + BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn; + } + return rtn; +} + int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) { auto& plugin = BBLNetworkPlugin::instance(); diff --git a/src/slic3r/Utils/BBLPrinterAgent.hpp b/src/slic3r/Utils/BBLPrinterAgent.hpp index a8880bf6bf..a04cd00175 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.hpp +++ b/src/slic3r/Utils/BBLPrinterAgent.hpp @@ -5,6 +5,7 @@ #include "ICloudServiceAgent.hpp" #include #include +#include namespace Slic3r { @@ -28,6 +29,12 @@ public: // Communication int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; + static std::string ams_refresh_rfid_gcode(const std::string& tray_id); + static std::string ams_calibrate_gcode(int ams_id); + static std::string ams_select_tray_gcode(const std::string& tray_id); + int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; + int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override; + int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; int disconnect_printer() override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; @@ -85,6 +92,9 @@ public: FilamentSyncMode get_filament_sync_mode() const override; private: + // why: the lan/cloud DECISION stays machine-side; keep this mechanical branch in sync with publish_json. + int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode); + std::shared_ptr m_cloud_agent; }; diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 91d271316e..22c109946e 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -77,6 +77,16 @@ public: */ virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0; + // why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect + // gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's + // publish funnel turns into a dialog. + virtual int command_ams_refresh_rfid(std::string, std::string, int, bool) + { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } + virtual int command_ams_calibrate(std::string, int, int, bool) + { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } + virtual int command_ams_select_tray(std::string, std::string, int, bool) + { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } + /** * Establish a direct LAN connection to a printer. */ diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index 0d77e5e660..b169fca052 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -767,6 +767,27 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos return -1; } +int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_ams_refresh_rfid(dev_id, tray_id, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_ams_calibrate(dev_id, ams_id, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_ams_select_tray(dev_id, tray_id, sequence_id, lan_mode); + return -1; +} + int NetworkAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { if (m_printer_agent) diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index d7032b7a20..317a357135 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -142,6 +142,9 @@ public: int set_on_local_message_fn(OnMessageFn fn); int set_server_callback(OnServerErrFn fn); int send_message(std::string dev_id, std::string json_str, int qos, int flag); + int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode); + int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode); + int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode); int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); int disconnect_printer(); int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag); From df5a08517ab3b56136d96e079a06afcd01896843 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 4 Aug 2026 19:44:39 +0800 Subject: [PATCH 20/71] Keep printer-agent error codes with the interface --- src/slic3r/Utils/IPrinterAgent.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 22c109946e..85a1ffb8fc 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -2,6 +2,13 @@ #define __I_PRINTER_AGENT_HPP__ #include "bambu_networking.hpp" +// why: these extend the BAMBU_NETWORK_* return space rather than opening a new one - the value +// flows through the same int domain callers already compare against BAMBU_NETWORK_SUCCESS. +// They live here and not in bambu_networking.hpp because that file is a vendor header replaced +// wholesale by header-sync commits (see c09252ce11), which would silently clobber them. +// -70xx is free: the vendor occupies -1..-25 and -10xx through -60xx. +#define ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED -7010 // no translation exists for this command +#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability #include #include From 56236f56a8ff506aeaffec5aa3c58145d021dd94 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 4 Aug 2026 21:26:50 +0800 Subject: [PATCH 21/71] Add unsupported-command feedback to the device UI --- src/slic3r/GUI/DeviceManager.cpp | 34 ++++++++++++++++++++++++++++++++ src/slic3r/GUI/DeviceManager.hpp | 2 ++ 2 files changed, 36 insertions(+) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index c6c8006160..ffddf5307d 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -4653,6 +4653,40 @@ void MachineObject::set_ctt_dlg( wxString text){ } } +void MachineObject::show_unsupported_dlg(int code) +{ + // why: a dead control invites repeat clicks, and the frame is modeless - without the guard + // every click stacks another one. Same shape as set_ctt_dlg above, including the reset on + // both hide and close so a dismissed dialog can reappear on the next attempt. + if (m_unsupported_dlg_shown) { + return; + } + m_unsupported_dlg_shown = true; + + // why: two codes so the user learns which kind of dead end this is - the slicer having no + // translation for the command, or the printer's own config lacking the hardware to run it. + const wxString text = (code == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) ? + _L("This printer is not configured with the hardware this control needs.") : + _L("This control is not supported on this printer."); + + // note: constructed directly rather than through CallAfter because every publish_json caller + // is on the UI thread - clicks come from wx handlers, and the agent marshals its own push + // callbacks back to main before parse_json runs. set_ctt_dlg relies on the same property. + auto unsupported_dlg = new GUI::SecondaryCheckDialog(nullptr, wxID_ANY, _L("Warning"), + GUI::SecondaryCheckDialog::VisibleButtons::ONLY_CONFIRM); + unsupported_dlg->update_text(text); + unsupported_dlg->Bind(wxEVT_SHOW, [this](auto& e) { + if (!e.IsShown()) { + m_unsupported_dlg_shown = false; + } + }); + unsupported_dlg->Bind(wxEVT_CLOSE_WINDOW, [this](auto& e) { + e.Skip(); + m_unsupported_dlg_shown = false; + }); + unsupported_dlg->on_show(); +} + int MachineObject::publish_gcode(std::string gcode_str) { json j; diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 456901cf84..2790e37cfa 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -272,9 +272,11 @@ public: bool m_is_online; bool m_lan_mode_connection_state{false}; bool m_set_ctt_dlg{ false }; + bool m_unsupported_dlg_shown{ false }; void set_lan_mode_connection_state(bool state) {m_lan_mode_connection_state = state;}; bool get_lan_mode_connection_state() {return m_lan_mode_connection_state;}; void set_ctt_dlg( wxString text); + void show_unsupported_dlg(int code); int parse_msg_count = 0; int keep_alive_count = 0; std::chrono::system_clock::time_point last_update_time; /* last received print data from machine */ From aae83220f15af3df94a6591703be2d82a2e20c6f Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 5 Aug 2026 19:30:26 +0800 Subject: [PATCH 22/71] fix: merge duplicated access code and allow empty access code in UI --- src/slic3r/GUI/ConnectPrinter.cpp | 4 ++- src/slic3r/GUI/DeviceCore/DevManager.cpp | 19 +++++++++--- src/slic3r/GUI/DeviceManager.cpp | 36 +--------------------- src/slic3r/GUI/DeviceManager.hpp | 6 ---- src/slic3r/GUI/GUI_App.cpp | 4 +-- src/slic3r/GUI/ReleaseNote.cpp | 9 ++++-- src/slic3r/GUI/SelectMachinePop.cpp | 1 - src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 1 - 8 files changed, 26 insertions(+), 54 deletions(-) diff --git a/src/slic3r/GUI/ConnectPrinter.cpp b/src/slic3r/GUI/ConnectPrinter.cpp index b4cd7f4f2f..3e78e7fe5c 100644 --- a/src/slic3r/GUI/ConnectPrinter.cpp +++ b/src/slic3r/GUI/ConnectPrinter.cpp @@ -156,6 +156,8 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt) void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) { wxString code = m_textCtrl_code->GetTextCtrl()->GetValue(); + if (code.empty()) + code = "88888888"; for (char c : code) { if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { show_error(this, _L("Invalid input")); @@ -163,7 +165,7 @@ void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) } } if (m_obj) { - m_obj->set_user_access_code(code.ToStdString()); + m_obj->set_access_code(code.ToStdString()); } EndModal(wxID_OK); } diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index d13f8b7215..edc958ec53 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -15,6 +15,18 @@ using namespace nlohmann; +namespace { + // Orca: access_code and user_access_code used to be separate AppConfig keys before the two + // fields were merged; fall back to the legacy key so existing users' saved codes aren't lost. + std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id) + { + std::string code = config->get("access_code", dev_id); + if (code.empty()) + code = config->get("user_access_code", dev_id); + return code; + } +} + namespace Slic3r { DeviceManager::DeviceManager(NetworkAgent* agent) @@ -48,8 +60,7 @@ namespace Slic3r obj->bind_sec_link = "secure"; obj->m_is_online = true; obj->last_alive = Slic3r::Utils::get_current_time_utc(); - obj->set_access_code(config->get("access_code", m.dev_id), false); - obj->set_user_access_code(config->get("user_access_code", m.dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id), false); if (obj->has_access_right()) { localMachineList.insert(std::make_pair(m.dev_id, obj)); } else { @@ -339,8 +350,7 @@ namespace Slic3r //load access code AppConfig* config = Slic3r::GUI::wxGetApp().app_config; if (config) { - obj->set_access_code(Slic3r::GUI::wxGetApp().app_config->get("access_code", dev_id), false); - obj->set_user_access_code(Slic3r::GUI::wxGetApp().app_config->get("user_access_code", dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id), false); } localMachineList.insert(std::make_pair(dev_id, obj)); @@ -382,7 +392,6 @@ namespace Slic3r obj->m_is_online = true; obj->last_alive = Slic3r::Utils::get_current_time_utc(); obj->set_access_code(access_code, false); - obj->set_user_access_code(access_code, false); update_local_machine(*obj); diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index ef85870461..f4befe78c1 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -449,9 +449,7 @@ bool MachineObject::HasRecentLanMessage() std::string MachineObject::get_access_code() const { - if (get_user_access_code().empty()) - return access_code; - return get_user_access_code(); + return access_code; } void MachineObject::set_access_code(std::string code, bool only_refresh) @@ -470,37 +468,6 @@ void MachineObject::set_access_code(std::string code, bool only_refresh) } } -void MachineObject::erase_user_access_code() -{ - this->user_access_code = ""; - AppConfig* config = GUI::wxGetApp().app_config; - if (config) { - GUI::wxGetApp().app_config->erase("user_access_code", get_dev_id()); - //GUI::wxGetApp().app_config->save(); - } -} - -void MachineObject::set_user_access_code(std::string code, bool only_refresh) -{ - this->user_access_code = code; - if (only_refresh && !code.empty()) { - AppConfig* config = GUI::wxGetApp().app_config; - if (config && !code.empty()) { - GUI::wxGetApp().app_config->set_str("user_access_code", get_dev_id(), code); - DeviceManager::update_local_machine(*this); - } - } -} - -std::string MachineObject::get_user_access_code() const -{ - AppConfig* config = GUI::wxGetApp().app_config; - if (config) { - return GUI::wxGetApp().app_config->get("user_access_code", get_dev_id()); - } - return ""; -} - std::string MachineObject::get_show_printer_type() const { std::string printer_type = this->printer_type; @@ -2907,7 +2874,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ std::string access_code = j_pre["system"]["access_code"].get(); if (!access_code.empty()) { set_access_code(access_code); - set_user_access_code(access_code); } } } diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 2790e37cfa..33635fbe6e 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -113,7 +113,6 @@ private: std::string dev_name; std::string dev_ip; std::string access_code; - std::string user_access_code; // type, time stamp, delay std::vector> message_delay; @@ -228,11 +227,6 @@ public: std::string get_access_code() const; void set_access_code(std::string code, bool only_refresh = true); - /*user access code*/ - void set_user_access_code(std::string code, bool only_refresh = true); - void erase_user_access_code(); - std::string get_user_access_code() const; - //PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN; std::string printer_type; /* model_id */ std::string get_show_printer_type() const; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 14edeb8038..db51bd9d8e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2166,7 +2166,6 @@ void GUI_App::init_networking_callbacks() obj->is_tunnel_mqtt = tunnel; obj->command_request_push_all(true); obj->command_get_version(); - obj->erase_user_access_code(); obj->command_get_access_code(); if (m_agent) m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer()); @@ -2216,7 +2215,6 @@ void GUI_App::init_networking_callbacks() wxString text; if (msg == "5") { obj->set_access_code(""); - obj->erase_user_access_code(); text = wxString::Format(_L("Incorrect password")); wxGetApp().show_dialog(text); } else { @@ -8286,7 +8284,7 @@ bool GUI_App::show_modal_ip_address_enter_dialog(bool input_sn, wxString title) wxGetApp().app_config->save(); obj->set_dev_ip(ip_address.ToStdString()); - obj->set_user_access_code(access_code.ToStdString()); + obj->set_access_code(access_code.ToStdString()); } } }); diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 22f65f4a60..7b2d091176 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -1991,7 +1991,7 @@ void InputIpAddressDialog::workerThreadFunc(std::string str_ip, std::string str_ if (w.expired()) return; if (m_obj) { - m_obj->set_user_access_code(str_access_code); + m_obj->set_access_code(str_access_code); wxGetApp().getDeviceManager()->set_selected_machine(m_obj->get_dev_id()); } @@ -2055,6 +2055,11 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) { auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); + + if (str_access_code.empty()) { + str_access_code = "88888888"; + } + auto str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both); auto str_sn = m_input_sn->GetTextCtrl()->GetValue().Strip(wxString::both); bool invalid_access_code = true; @@ -2062,7 +2067,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) for (char c : str_access_code) { if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { invalid_access_code = false; - return; + break; } } diff --git a/src/slic3r/GUI/SelectMachinePop.cpp b/src/slic3r/GUI/SelectMachinePop.cpp index 492199569e..96324fb4d8 100644 --- a/src/slic3r/GUI/SelectMachinePop.cpp +++ b/src/slic3r/GUI/SelectMachinePop.cpp @@ -704,7 +704,6 @@ void SelectMachinePopup::update_user_devices() } mobj->set_access_code(""); - mobj->erase_user_access_code(); } if (GUI::wxGetApp().plater()) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index d21dce5070..cd3ef82b62 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -1359,7 +1359,6 @@ void MoonrakerPrinterAgent::announce_printhost_device() if (auto* app_config = GUI::wxGetApp().app_config) { const std::string access_code = device_info.api_key.empty() ? "88888888" : device_info.api_key; app_config->set_str("access_code", device_info.dev_id, access_code); - app_config->set_str("user_access_code", device_info.dev_id, access_code); } nlohmann::json payload; From ced1058b3168e9764210644ba1cfbae65e8d5755 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 5 Aug 2026 19:52:51 +0800 Subject: [PATCH 23/71] fix: naming and print host propagation --- src/slic3r/GUI/MainFrame.cpp | 24 ++++++++++++++++++------ src/slic3r/GUI/Plater.cpp | 13 ++++++++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 5ef81a32e1..5a0e70b74c 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1373,8 +1373,8 @@ void MainFrame::show_device(bool should_use_native) { const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); - // The legacy page is appended when printer agents are enabled. Remove that - // extra page before switching back to the normal native/legacy layout. + // The web page is appended when printer agents are enabled. Remove that + // extra page before switching back to the normal native/Web layout. if (!use_printer_agents) { if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) { m_printer_view->Show(false); @@ -1434,10 +1434,10 @@ void MainFrame::show_device(bool should_use_native) { if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { m_printer_view->Show(false); - m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), std::string("tab_monitor_active"), + m_tabpanel->AddPage(m_printer_view, _L("Device (Web)"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); } else { - m_tabpanel->SetPageText(idx, _L("Device (legacy)")); + m_tabpanel->SetPageText(idx, _L("Device (Web)")); } #ifdef _MSW_DARK_MODE @@ -4333,14 +4333,26 @@ void MainFrame::load_printer_url(wxString url, wxString apikey) void MainFrame::load_printer_url() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents")) + if (preset_bundle.use_bbl_device_tab() && !wxGetApp().app_config->get_bool("use_printer_agents")) return; auto cfg = preset_bundle.printers.get_edited_preset().config; + if (cfg.opt_string("print_host").empty()) { + if (auto *device_manager = wxGetApp().getDeviceManager()) { + auto *machine = device_manager->get_selected_machine(); + if (!machine) { + auto machines = device_manager->get_my_machine_list(); + if (machines.size() == 1) + machine = machines.begin()->second; + } + if (machine && !machine->get_dev_ip().empty()) + cfg.opt_string("print_host") = machine->get_dev_ip(); + } + } wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; const auto host_type = cfg.option>("host_type")->value; - if (cfg.has("printhost_apikey") && (host_type == htPrusaLink || host_type == htPrusaConnect)) + if (cfg.has("printhost_apikey") && host_type != htSimplyPrint) apikey = cfg.opt_string("printhost_apikey"); if (!url.empty()) { load_printer_url(url, apikey); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3ee09fed06..0abb2341ff 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3287,7 +3287,9 @@ void Sidebar::update_all_preset_comboboxes() : MainFrame::PrintSelectType::eSendGcode; } - if (!use_native_device_tab || use_printer_agents) + if (use_printer_agents) + p_mainframe->load_printer_url(); + else if (!use_native_device_tab) p_mainframe->load_printer_url(url, apikey); @@ -11236,9 +11238,14 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } } else { - if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { + const bool selecting_web_device_tab = main_frame->m_printer_view && + main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; + if (selecting_web_device_tab) { + // Use the selected discovered machine when the preset has no host. + main_frame->load_printer_url(); + } else if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); + wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); if (main_frame->m_printer_view && url.empty()) { // It's missing_connection page, reload so that we can replay the gif image main_frame->m_printer_view->reload(); From ef4815b26c1bd0a850ee883114445599b7e7c991 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Thu, 6 Aug 2026 06:08:02 +0800 Subject: [PATCH 24/71] fix: crash on windows --- src/slic3r/GUI/MainFrame.cpp | 2 ++ src/slic3r/GUI/Project.cpp | 61 ++++++++++++++++++++++++++++++------ src/slic3r/GUI/Project.hpp | 10 ++++-- src/slic3r/GUI/Tab.cpp | 8 +++-- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 9566426c53..5dc1f0ccfc 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1110,6 +1110,8 @@ void MainFrame::update_edge_panels() void MainFrame::shutdown() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; + if (m_project != nullptr) + m_project->shutdown(); m_plugin_pages.shutdown(); #ifdef __WXGTK__ // Edge panels are child windows — wxWidgets destroys them automatically. diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp index 57410b1202..6d1eb0e180 100644 --- a/src/slic3r/GUI/Project.cpp +++ b/src/slic3r/GUI/Project.cpp @@ -74,7 +74,18 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, Fit(); } -ProjectPanel::~ProjectPanel() {} +ProjectPanel::~ProjectPanel() +{ + shutdown(); +} + +void ProjectPanel::shutdown() +{ + m_reload_cancel_token->store(true, std::memory_order_release); + if (m_reload_task && m_reload_task->joinable()) + m_reload_task->join(); + m_reload_task.reset(); +} // Helper to convert newlines to
static std::string convert_newlines_to_br(const std::string& text) { @@ -101,7 +112,17 @@ void ProjectPanel::onWebNavigating(wxWebViewEvent& evt) void ProjectPanel::on_reload(wxCommandEvent& evt) { - boost::thread reload = boost::thread([this] { + if (wxTheApp == nullptr || wxGetApp().is_closing() || + m_reload_cancel_token->load(std::memory_order_acquire)) + return; + + if (m_reload_task && m_reload_task->joinable()) + m_reload_task->join(); + + const auto cancel_token = m_reload_cancel_token; + m_reload_task = std::make_unique([this, cancel_token] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; std::string update_type; std::string license; std::string model_name; @@ -115,6 +136,9 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) std::map> files; + if (wxGetApp().plater() == nullptr) + return; + Model model = wxGetApp().plater()->model(); auto model_info = model.model_info; @@ -156,7 +180,14 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str()); if (!file_path.empty()) { files = Reload(file_path); - wxGetApp().CallAfter([this, file_path, files] { m_auxiliary->Reload(file_path, files); }); + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; + + wxGetApp().CallAfter([this, cancel_token, file_path, files] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; + m_auxiliary->Reload(file_path, files); + }); } else { clear_model_info(); return; @@ -215,15 +246,18 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) json m_Res = json::object(); m_Res["command"] = "show_3mf_info"; - m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++); + m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed)); m_Res["model"] = j; wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); - if (m_web_init_completed) { - wxGetApp().CallAfter([this, strJS] { + if (m_web_init_completed.load(std::memory_order_acquire) && + !cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) { + wxGetApp().CallAfter([this, cancel_token, strJS] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; RunScript(strJS.ToStdString()); - }); + }); } }); } @@ -264,7 +298,7 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt) } } else if (strCmd == "request_3mf_info") { - m_web_init_completed = true; + m_web_init_completed.store(true, std::memory_order_release); } else if (strCmd == "edit_project_info") { show_info_editor(true); @@ -307,13 +341,20 @@ void ProjectPanel::update_model_data() void ProjectPanel::clear_model_info() { + if (wxTheApp == nullptr || wxGetApp().is_closing() || + m_reload_cancel_token->load(std::memory_order_acquire)) + return; + json m_Res = json::object(); m_Res["command"] = "clear_3mf_info"; - m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++); + m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed)); wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); - wxGetApp().CallAfter([this, strJS] { + const auto cancel_token = m_reload_cancel_token; + wxGetApp().CallAfter([this, cancel_token, strJS] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; RunScript(strJS.ToStdString()); }); } diff --git a/src/slic3r/GUI/Project.hpp b/src/slic3r/GUI/Project.hpp index 0071685e7d..a41f76ba7e 100644 --- a/src/slic3r/GUI/Project.hpp +++ b/src/slic3r/GUI/Project.hpp @@ -26,9 +26,11 @@ #include "nlohmann/json.hpp" #include "slic3r/Utils/json_diff.hpp" +#include #include #include #include +#include #include "Event.hpp" #include "libslic3r/ProjectTask.hpp" #include "wxExtensions.hpp" @@ -60,14 +62,17 @@ struct project_file{ class ProjectPanel : public wxPanel { private: - bool m_web_init_completed = {false}; + std::atomic m_web_init_completed{false}; bool m_reload_already = {false}; + std::shared_ptr> m_reload_cancel_token{std::make_shared>(false)}; + std::unique_ptr m_reload_task; + wxWebView* m_browser = {nullptr}; AuxiliaryPanel* m_auxiliary{nullptr}; wxString m_project_home_url; wxString m_root_dir; - static inline int m_sequence_id = 8000; + static inline std::atomic m_sequence_id{8000}; void show_info_editor(bool show); @@ -75,6 +80,7 @@ private: public: ProjectPanel(wxWindow *parent, wxWindowID id = wxID_ANY, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize, long style = wxTAB_TRAVERSAL); ~ProjectPanel(); + void shutdown(); void onWebNavigating(wxWebViewEvent& evt); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 05631c8afa..469864af46 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -8477,8 +8477,12 @@ void Page::activate(ConfigOptionMode mode, std::function throw_if_cancel #ifdef __WXMSW__ // BBS: fix field control position - wxTheApp->CallAfter([this]() { - for (auto group : m_optgroups) { + wxTheApp->CallAfter([wp = std::weak_ptr(shared_from_this())]() { + auto page = wp.lock(); + if (!page) + return; + + for (auto group : page->m_optgroups) { if (group->custom_ctrl) group->custom_ctrl->fixup_items_positions(); } From 169189498e97531fde24cc8cf73d19922a6003da Mon Sep 17 00:00:00 2001 From: peachismomo Date: Thu, 6 Aug 2026 07:56:37 +0800 Subject: [PATCH 25/71] fix regression after merge --- src/slic3r/GUI/MainFrame.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index c3b5728a53..26c03975dc 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1385,7 +1385,7 @@ void MainFrame::show_device(bool should_use_native) { // The legacy page is appended when printer agents are enabled. Remove that // extra page before switching back to the normal native/legacy layout. if (!use_printer_agents) { - if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) { + if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != m_tabpanel->FindPageByName(TAB_ID_MONITOR)) { m_printer_view->Show(false); m_tabpanel->RemovePage(idx); } @@ -1403,8 +1403,10 @@ void MainFrame::show_device(bool should_use_native) { m_tabpanel->RemovePage(idx); } m_monitor->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), - std::string("tab_monitor_active")); + const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); + const size_t monitor_pos = + (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; + m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active", false); } if (m_printer_view == nullptr) { @@ -1425,8 +1427,11 @@ void MainFrame::show_device(bool should_use_native) { // TODO: change the bitmap if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) { m_multi_machine->Show(false); - m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), - std::string("tab_multi_active"), false); + const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); + const size_t multi_pos = + (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; + m_tabpanel->InsertPage(multi_pos, TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), + "tab_multi_active", false); } } if (!m_calibration) { @@ -1437,14 +1442,12 @@ void MainFrame::show_device(bool should_use_native) { // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position. if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) { m_calibration->Show(false); - m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), - std::string("tab_calibration_active"), false); + m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); } if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { m_printer_view->Show(false); - m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), std::string("tab_monitor_active"), - std::string("tab_monitor_active"), false); + m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), false, Notebook::PAGE_MONITOR); } else { m_tabpanel->SetPageText(idx, _L("Device (legacy)")); } From 159e577543c9bd3e3a6ccc95c36b1b549827b53b Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 6 Aug 2026 16:11:44 +0800 Subject: [PATCH 26/71] feat: Isolate devices across different printer agents --- src/libslic3r/AppConfig.cpp | 6 ++ src/libslic3r/AppConfig.hpp | 11 ++- src/slic3r/GUI/DeviceCore/DevManager.cpp | 91 ++++++++++++++++++------ src/slic3r/GUI/DeviceCore/DevManager.h | 14 +++- src/slic3r/GUI/DeviceManager.cpp | 39 ++++++++-- src/slic3r/GUI/DeviceManager.hpp | 10 +++ src/slic3r/GUI/GUI_App.cpp | 8 ++- src/slic3r/GUI/SelectMachine.cpp | 3 +- src/slic3r/GUI/SelectMachinePop.cpp | 7 +- 9 files changed, 157 insertions(+), 32 deletions(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 1b170bf884..da82016a6f 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -853,6 +853,10 @@ std::string AppConfig::load() local_machine.dev_ip = p["dev_ip"].get(); if (p.contains("printer_type")) local_machine.printer_type = p["printer_type"].get(); + if (p.contains("printer_agent_id")) + local_machine.printer_agent_id = p["printer_agent_id"].get(); + if (p.contains("access_code")) + local_machine.access_code = p["access_code"].get(); m_local_machines[local_machine.dev_id] = local_machine; } } else { @@ -1065,6 +1069,8 @@ void AppConfig::save() m_json["dev_name"] = local_machine.second.dev_name; m_json["dev_ip"] = local_machine.second.dev_ip; m_json["printer_type"] = local_machine.second.printer_type; + m_json["printer_agent_id"] = local_machine.second.printer_agent_id; + m_json["access_code"] = local_machine.second.access_code; j["local_machines"][local_machine.first] = m_json; } diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 2c83ebb488..b73ff5eac4 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -61,10 +61,19 @@ struct BBLocalMachine std::string dev_ip; std::string dev_id; /* serial number */ std::string printer_type; /* model_id */ + std::string printer_agent_id; /* id of the IPrinterAgent that discovered/bound this device, e.g. "bbl"; empty for entries persisted before this field existed */ + // Access code, scoped to printer_agent_id above - so a code saved while bound under one + // printer agent isn't treated as valid for a different, independent agent talking to the + // same physical dev_id. Empty for entries persisted before this field existed; those fall + // back to the legacy flat "access_code"/"user_access_code" AppConfig sections (BBL-only, + // since BBL was the only agent when they were saved) - see + // get_access_code_with_legacy_fallback() in DevManager.cpp. + std::string access_code; bool operator==(const BBLocalMachine& other) const { - return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type; + return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type && + printer_agent_id == other.printer_agent_id && access_code == other.access_code; } bool operator!=(const BBLocalMachine& other) const { return !operator==(other); } }; diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index edc958ec53..8844303793 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -10,20 +10,36 @@ #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "libslic3r/Time.hpp" using namespace nlohmann; namespace { - // Orca: access_code and user_access_code used to be separate AppConfig keys before the two - // fields were merged; fall back to the legacy key so existing users' saved codes aren't lost. - std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id) + // Orca: access_code lives on BBLocalMachine::access_code (keyed by dev_id via + // get_local_machines(), scoped by the record's own printer_agent_id field) - so binding a + // printer under one agent doesn't silently appear as already-bound under a different, + // independent agent. This only covers LAN devices (BBLocalMachine's own scope); access_code + // and user_access_code used to be the only, flat dev_id-only AppConfig keys before + // BBLocalMachine::access_code existed, and codes saved back then are still stored flat (no + // agent association at all). Since BBL was the only agent that existed at the time, honor + // those flat legacy keys as implicitly BBL's - but only for the BBL agent, so they aren't + // leaked to other agents that never bound the device themselves. + std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id, const std::string& agent_id) { - std::string code = config->get("access_code", dev_id); - if (code.empty()) - code = config->get("user_access_code", dev_id); - return code; + const auto& machines = config->get_local_machines(); + auto it = machines.find(dev_id); + if (it != machines.end() && it->second.printer_agent_id == agent_id && !it->second.access_code.empty()) + return it->second.access_code; + + if (agent_id == Slic3r::BBL_PRINTER_AGENT_ID || agent_id.empty()) { + std::string code = config->get("access_code", dev_id); + if (code.empty()) + code = config->get("user_access_code", dev_id); + return code; + } + return ""; } } @@ -55,12 +71,13 @@ namespace Slic3r continue; MachineObject* obj = new MachineObject(this, m_agent, m.dev_name, m.dev_id, m.dev_ip); obj->printer_type = m.printer_type; + obj->printer_agent_id = m.printer_agent_id; obj->dev_connection_type = "lan"; obj->bind_state = "free"; obj->bind_sec_link = "secure"; obj->m_is_online = true; obj->last_alive = Slic3r::Utils::get_current_time_utc(); - obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id, obj->printer_agent_id), false); if (obj->has_access_right()) { localMachineList.insert(std::make_pair(m.dev_id, obj)); } else { @@ -77,10 +94,12 @@ namespace Slic3r if (m.is_lan_mode_printer()) { if (m.has_access_right()) { BBLocalMachine local_machine; - local_machine.dev_id = m.get_dev_id(); - local_machine.dev_name = m.get_dev_name(); - local_machine.dev_ip = m.get_dev_ip(); - local_machine.printer_type = m.printer_type; + local_machine.dev_id = m.get_dev_id(); + local_machine.dev_name = m.get_dev_name(); + local_machine.dev_ip = m.get_dev_ip(); + local_machine.printer_type = m.printer_type; + local_machine.printer_agent_id = m.printer_agent_id; + local_machine.access_code = m.get_access_code(); config->update_local_machine(local_machine); } } else { @@ -143,6 +162,14 @@ namespace Slic3r } } + std::string DeviceManager::get_current_printer_agent_id() const + { + if (!m_agent) + return ""; + auto printer_agent = m_agent->get_printer_agent(); + return printer_agent ? printer_agent->get_agent_info().id : ""; + } + void DeviceManager::EnableMultiMachine(bool enable) { m_agent->enable_multi_machine(enable); @@ -339,6 +366,7 @@ namespace Slic3r /* insert a new machine */ obj = new MachineObject(this, m_agent, dev_name, dev_id, dev_ip); obj->printer_type = _parse_printer_type(printer_type_str); + obj->printer_agent_id = get_current_printer_agent_id(); obj->wifi_signal = printer_signal; obj->dev_connection_type = connect_type; obj->bind_state = bind_state; @@ -350,7 +378,7 @@ namespace Slic3r //load access code AppConfig* config = Slic3r::GUI::wxGetApp().app_config; if (config) { - obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false); } localMachineList.insert(std::make_pair(dev_id, obj)); @@ -379,6 +407,7 @@ namespace Slic3r obj = it->second; } else { obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip); + obj->printer_agent_id = get_current_printer_agent_id(); localMachineList.insert(std::make_pair(machine.dev_id, obj)); } if (machine.printer_type.empty()) @@ -505,16 +534,26 @@ namespace Slic3r OnSelectedMachineChanged(previous_selected_machine, selected_machine); } - void DeviceManager::clear_other_devices() + void DeviceManager::clear_other_devices(const std::string& target_agent_id) { // why: on agent swap, keep "My Devices" but drop the transient "Other Devices" // Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. + // + // Also drop "My Devices" stamped by a different agent than the one we're swapping to + // (target_agent_id, passed by the caller since the live agent hasn't been repointed yet + // at this point): otherwise a device first discovered under agent A survives every swap + // with a stale printer_agent_id, stays hidden from every agent's filtered list, and only + // gets re-tagged if something happens to delete and re-create it (e.g. account logout). + // Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it + // like any other fresh device. const auto my = get_my_machine_list(); for (auto it = localMachineList.begin(); it != localMachineList.end();) { - if (my.find(it->first) == my.end()) + const bool is_my_device = my.find(it->first) != my.end(); + const bool agent_mismatch = !target_agent_id.empty() && it->second && + it->second->printer_agent_id != target_agent_id; + if (!is_my_device || agent_mismatch) { - // not a "My Device" -> an "Other Device" delete it->second; it = localMachineList.erase(it); } @@ -697,13 +736,16 @@ namespace Slic3r m_agent->add_subscribe(subscribe_list_cache); } - std::map DeviceManager::get_my_machine_list() + std::map DeviceManager::get_my_machine_list(const std::string& agent_id) { std::map result; for (auto it = userMachineList.begin(); it != userMachineList.end(); it++) { - if (it->second && !it->second->is_lan_mode_printer()) + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (!it->second->is_lan_mode_printer()) { result.insert(std::make_pair(it->first, it->second)); } @@ -711,7 +753,10 @@ namespace Slic3r for (auto it = localMachineList.begin(); it != localMachineList.end(); it++) { - if (it->second && it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer()) + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer()) { // remove redundant in userMachineList if (result.find(it->first) == result.end()) @@ -723,12 +768,15 @@ namespace Slic3r return result; } - std::map DeviceManager::get_my_cloud_machine_list() + std::map DeviceManager::get_my_cloud_machine_list(const std::string& agent_id) { std::map result; for (auto it = userMachineList.begin(); it != userMachineList.end(); it++) { - if (it->second && !it->second->is_lan_mode_printer()) { result.emplace(*it); } + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (!it->second->is_lan_mode_printer()) { result.emplace(*it); } } return result; } @@ -801,6 +849,7 @@ namespace Slic3r else { obj = new MachineObject(this, m_agent, "", "", ""); + obj->printer_agent_id = get_current_printer_agent_id(); if (m_agent) { obj->set_bind_status(m_agent->get_user_name(provider)); diff --git a/src/slic3r/GUI/DeviceCore/DevManager.h b/src/slic3r/GUI/DeviceCore/DevManager.h index e3ac0064b9..1f48baba98 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.h +++ b/src/slic3r/GUI/DeviceCore/DevManager.h @@ -74,7 +74,10 @@ public: void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void clean_user_info(bool keep_local_selection = false); - void clear_other_devices(); + // target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check, + // just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the + // live one - this runs before the live agent is repointed. + void clear_other_devices(const std::string& target_agent_id = ""); void load_last_machine(); void update_user_machine_list_info(const std::string& provider); @@ -90,10 +93,15 @@ public: /* my machine*/ MachineObject* get_my_machine(std::string dev_id); - std::map get_my_machine_list(); - std::map get_my_cloud_machine_list(); + std::map get_my_machine_list(const std::string& agent_id = ""); + std::map get_my_cloud_machine_list(const std::string& agent_id = ""); void modify_device_name(std::string dev_id, std::string dev_name, const std::string& provider); + // id of the currently live IPrinterAgent (IPrinterAgent::get_agent_info().id), or empty if + // m_agent has no printer agent set yet. Pass to get_my_machine_list()/get_my_cloud_machine_list() + // to scope results to the active agent. + std::string get_current_printer_agent_id() const; + /* create machine or update machine properties */ void on_machine_alive(std::string json_str); int query_bind_status(std::string& msg, const std::string& provider); diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index cb29fbe11a..48f3cb60fc 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Time.hpp" #include "libslic3r/Thread.hpp" #include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "GuiColor.hpp" #include "GUI_App.hpp" @@ -458,11 +459,41 @@ void MachineObject::set_access_code(std::string code, bool only_refresh) if (only_refresh) { AppConfig* config = GUI::wxGetApp().app_config; if (config) { - if (!code.empty()) { - GUI::wxGetApp().app_config->set_str("access_code", get_dev_id(), code); - DeviceManager::update_local_machine(*this); + if (is_lan_mode_printer()) { + // why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and + // scoped by that record's own printer_agent_id field - see the matching comment + // on get_access_code_with_legacy_fallback() in DevManager.cpp - so binding this + // device under one printer agent doesn't silently read as already-bound under a + // different, independent one. Cloud devices (the else branch below) aren't + // scoped this way: they're never recalled from a stale local cache across a + // session boundary, since parse_user_print_info() always overwrites their code + // fresh from the cloud API's current response, so there's no cross-agent leakage + // risk to guard against there. + if (!code.empty()) { + DeviceManager::update_local_machine(*this); + } else { + // Only patch an existing record's code - don't persist a brand-new + // never-bound entry just because set_access_code("") was called on it. + const auto& machines = config->get_local_machines(); + auto it = machines.find(get_dev_id()); + if (it != machines.end()) { + BBLocalMachine local_machine = it->second; + local_machine.access_code = ""; + config->update_local_machine(local_machine); + } + // Also clear the pre-scoping flat legacy key when unbinding under BBL, so an + // old BBL-era code can't silently "re-bind" this device again via + // get_access_code_with_legacy_fallback()'s legacy fallback. + if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) { + config->erase("access_code", get_dev_id()); + config->erase("user_access_code", get_dev_id()); + } + } } else { - GUI::wxGetApp().app_config->erase("access_code", get_dev_id()); + if (!code.empty()) + config->set_str("access_code", get_dev_id(), code); + else + config->erase("access_code", get_dev_id()); } } } diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 33635fbe6e..914c8f7868 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -229,6 +229,16 @@ public: //PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN; std::string printer_type; /* model_id */ + + // id of the IPrinterAgent that was used to discover or bind this device (IPrinterAgent::get_agent_info().id, + // e.g. "bbl"), stamped at creation time — not derived from get_agent(), since m_agent is a single + // process-wide NetworkAgent shared by every MachineObject and gets repointed on agent swap + // (see DeviceManager::set_agent()), so it can't tell which agent originally found this device. + // We persist this as well so that when the printer agent is swapped, we don't show unrelated devices, + // e.g. if the current printer agent is elegoo, we shouldn't show printers connected by BBL printer agent + // under local machines. + std::string printer_agent_id; + std::string get_show_printer_type() const; PrinterSeries get_printer_series() const; PrinterArch get_printer_arch() const; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index db51bd9d8e..5faf112db3 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3937,7 +3937,13 @@ void GUI_App::set_live_printer_agent(std::shared_ptr agent) m_agent->set_user_selected_machine(""); // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS - dev->clear_other_devices(); // why: drop stale LAN discoveries; keep My Devices + // why: drop stale LAN discoveries; keep My Devices, but only those belonging to the + // agent we're about to swap to, so a device stamped by the outgoing agent doesn't + // linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh. + // agent is null when clearing the live agent entirely (e.g. plugin unload); there's no + // target to filter against then, so fall back to the original "keep all My Devices" + // behavior rather than guessing. + dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string()); } m_agent->set_printer_agent(agent); diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 1ab78fcc11..23f13d1497 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -3913,7 +3913,8 @@ _collect_sorted_machines(Slic3r::DeviceManager* dev_manager, }; // collect from user machine list - const auto& user_machine_list = dev_manager->get_my_machine_list();// user machine list + const std::string agent_id = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent"); + const auto& user_machine_list = dev_manager->get_my_machine_list(agent_id);// user machine list for (const auto& elem : user_machine_list) { MachineObject* mobj = elem.second; diff --git a/src/slic3r/GUI/SelectMachinePop.cpp b/src/slic3r/GUI/SelectMachinePop.cpp index 96324fb4d8..df0a566917 100644 --- a/src/slic3r/GUI/SelectMachinePop.cpp +++ b/src/slic3r/GUI/SelectMachinePop.cpp @@ -501,6 +501,7 @@ void SelectMachinePopup::update_other_devices() DeviceManager* dev = wxGetApp().getDeviceManager(); if (!dev) return; m_free_machine_list = dev->get_local_machinelist(); + const std::string current_agent_id = dev->get_current_printer_agent_id(); BOOST_LOG_TRIVIAL(trace) << "SelectMachinePopup update_other_devices start"; this->Freeze(); @@ -512,6 +513,10 @@ void SelectMachinePopup::update_other_devices() /* do not show printer bind state is empty */ if (!mobj->is_avaliable()) continue; + /* do not show devices discovered/bound by a different printer agent */ + if (mobj->printer_agent_id != current_agent_id) + continue; + if (!wxGetApp().is_user_login(wxGetApp().get_printer_cloud_provider()) && !mobj->is_lan_mode_printer()) continue; @@ -634,7 +639,7 @@ void SelectMachinePopup::update_user_devices() } m_bind_machine_list.clear(); - m_bind_machine_list = dev->get_my_machine_list(); + m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id()); //sort list std::vector> user_machine_list; From 6345d57512eca5e86ab2bac8ad43ec2daf09a939 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 6 Aug 2026 16:26:39 +0800 Subject: [PATCH 27/71] fix: use get_current_printer_agent_id --- src/slic3r/GUI/SelectMachine.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 23f13d1497..e2b9ce78ad 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -3913,8 +3913,7 @@ _collect_sorted_machines(Slic3r::DeviceManager* dev_manager, }; // collect from user machine list - const std::string agent_id = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent"); - const auto& user_machine_list = dev_manager->get_my_machine_list(agent_id);// user machine list + const auto& user_machine_list = dev_manager->get_my_machine_list(dev_manager->get_current_printer_agent_id());// user machine list for (const auto& elem : user_machine_list) { MachineObject* mobj = elem.second; From 9421e7fa9bb69767ead7b10e9b47570d94597ca6 Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:08:54 -0500 Subject: [PATCH 28/71] Fix detached copies of system presets (#15173) * Fix detached copies of system presets * Clarify detached preset compatibility * Show unique preset state in save dialog * Update SavePresetDialog.cpp --------- Co-authored-by: yw4z --- src/slic3r/GUI/SavePresetDialog.cpp | 47 ++++++++++++++++++----------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index e24a2fc497..0b33e48ec4 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -111,18 +111,20 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string &suffix, wxBox sizer->Add(m_radio_group, 0, wxEXPAND | wxTOP | wxLEFT, BORDER_W); - std::string inherits_str = sel_preset.inherits(); - if (parent->m_mode == comDevelop && !inherits_str.empty()) { + if (parent->m_mode == comDevelop) { + // A new user copy of a system preset inherits from the selected system preset. + const std::string parent_name = sel_preset.is_system ? sel_preset.name : sel_preset.inherits(); + const bool can_detach = !parent_name.empty(); + wxBoxSizer *detach_sizer = new wxBoxSizer(wxHORIZONTAL); - auto detach_tooltip = _L("Copies all inherited values from the parent preset into this preset and removes the connection with the parent preset."); + auto detach_tooltip = _L("Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported."); auto detach_checkbox = new ::CheckBox(parent); detach_checkbox->SetToolTip(detach_tooltip); auto detach_label = new wxStaticText(parent, wxID_ANY, _L("Detach from parent")); detach_label->SetFont(::Label::Body_14); - detach_label->SetForegroundColour(wxColour("#363636")); detach_label->SetToolTip(detach_tooltip); detach_sizer->Add(detach_checkbox, 0, wxALIGN_LEFT | wxLEFT, BORDER_W); @@ -130,27 +132,36 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string &suffix, wxBox sizer->Add(detach_sizer, 0, wxEXPAND | wxTOP, BORDER_W); sizer->AddSpacer(FromDIP(5)); - auto parent_label = new wxStaticText(parent, wxID_ANY, inherits_str); + const wxString parent_text = can_detach ? from_u8(parent_name) : _L("Unique preset"); + auto parent_label = new wxStaticText(parent, wxID_ANY, parent_text); parent_label->SetFont(::Label::Body_12); parent_label->SetForegroundColour(wxColour("#6B6B6B")); - parent_label->SetToolTip(_L("Parent preset")); + parent_label->SetToolTip(can_detach ? _L("Parent preset") : _L("This preset does not inherit from another preset.")); sizer->Add(parent_label, 0, wxEXPAND | wxLEFT, BORDER_W + FromDIP(24)); sizer->AddSpacer(FromDIP(5)); - // Set initial state (unchecked by default) - detach_checkbox->SetValue(m_detach); - // Bind the checkbox event to update the detach state for this item - detach_checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, detach_checkbox](wxCommandEvent&) { m_detach = detach_checkbox->GetValue(); }); + if (!can_detach) { + detach_checkbox->Disable(); + detach_label->SetForegroundColour(wxColour("#6B6B6B")); + } + else { + // Set initial state (unchecked by default) + detach_checkbox->SetValue(m_detach); + // Bind the checkbox event to update the detach state for this item + detach_checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, detach_checkbox](wxCommandEvent&) { m_detach = detach_checkbox->GetValue(); }); - auto on_toggle = [this, detach_checkbox]() { - detach_checkbox->SetValue(!detach_checkbox->GetValue()); - wxCommandEvent ev(wxEVT_TOGGLEBUTTON, detach_checkbox->GetId()); - ev.SetEventObject(detach_checkbox); - detach_checkbox->GetEventHandler()->ProcessEvent(ev); - }; - detach_label->Bind(wxEVT_LEFT_DOWN, [on_toggle](wxMouseEvent& e) {if(!e.LeftDClick()) on_toggle();}); - detach_label->Bind(wxEVT_LEFT_DCLICK, [on_toggle](wxMouseEvent& e) {on_toggle();}); + detach_label->SetForegroundColour(wxColour("#363636")); + + auto on_toggle = [this, detach_checkbox]() { + detach_checkbox->SetValue(!detach_checkbox->GetValue()); + wxCommandEvent ev(wxEVT_TOGGLEBUTTON, detach_checkbox->GetId()); + ev.SetEventObject(detach_checkbox); + detach_checkbox->GetEventHandler()->ProcessEvent(ev); + }; + detach_label->Bind(wxEVT_LEFT_DOWN, [on_toggle](wxMouseEvent& e) {if(!e.LeftDClick()) on_toggle();}); + detach_label->Bind(wxEVT_LEFT_DCLICK, [on_toggle](wxMouseEvent& e) {on_toggle();}); + } } m_radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, [this](wxCommandEvent &e) { From 1a8f39c5f7322dc7d57343a511f9cf78a9c3a629 Mon Sep 17 00:00:00 2001 From: yw4z Date: Sun, 9 Aug 2026 08:24:04 +0300 Subject: [PATCH 29/71] Fix emboss gizmo font preview of style not rendering properly (#14612) --- .../GUI/Jobs/CreateFontStyleImagesJob.cpp | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp index f988f622ce..31bd6728b1 100644 --- a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp @@ -45,24 +45,26 @@ void CreateFontStyleImagesJob::process(Ctl &ctl) for (const ExPolygon &shape : shapes) bounding_box.merge(BoundingBox(shape.contour.points)); for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min); - - // calculate conversion from FontPoint to screen pixels by size of font - double scale = get_text_shape_scale(item.prop, *item.font.font_file) * m_input.ppm; - scales[index] = scale; - //double scale = font_prop.size_in_mm * SCALING_FACTOR; - BoundingBoxf bb2(bounding_box.min.cast(), - bounding_box.max.cast()); + if (bounding_box.size().x() < 1 || bounding_box.size().y() < 1) + continue; // or however the font job's degenerate-box case is handled + + // Normalize to fit max_size, exactly like CreateFontImageJob does against m_input.size. + // Fit by height (matches row height), then clamp width if needed. + constexpr float preview_padding_px = 2.f; // margin for AA sampling, tune to your AA kernel radius + + double scale = m_input.max_size.y() / (double) bounding_box.size().y(); + BoundingBoxf bb2(bounding_box.min.cast(), bounding_box.max.cast()); bb2.scale(scale); - image.tex_size.x = std::ceil(bb2.max.x() - bb2.min.x()); - image.tex_size.y = std::ceil(bb2.max.y() - bb2.min.y()); - // crop image width - if (image.tex_size.x > m_input.max_size.x()) + // crop width only if the (now height-normalized) text is too wide + image.tex_size.x = std::ceil(bb2.max.x() - bb2.min.x()) + 2 * preview_padding_px; + image.tex_size.y = std::ceil(bb2.max.y() - bb2.min.y()) + 2 * preview_padding_px; + + if (image.tex_size.x > m_input.max_size.x()) image.tex_size.x = m_input.max_size.x(); - // crop image height - if (image.tex_size.y > m_input.max_size.y()) - image.tex_size.y = m_input.max_size.y(); + + scales[index] = scale; } // arrange bounding boxes From c806a09c7cfaaf4c0d19aca7f6cb505487c8ecc8 Mon Sep 17 00:00:00 2001 From: Anson Liu Date: Sun, 9 Aug 2026 20:04:17 -0700 Subject: [PATCH 30/71] Show current filaments at top of AMS filament dropdown (#11293) * Move currently active filaments added to the Prepare sidebar to the top of the AMS Material Selection combo box. It is likely the user wants to set the material to the currently active filament. * Reduce logging verbosity. * Refactor current active preset filament finding to find nested preset inheritance. * Initialize pointer to null before usage. * Remove old commit code * Remove new line --------- Co-authored-by: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com> Co-authored-by: yw4z --- src/slic3r/GUI/AMSMaterialsSetting.cpp | 53 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 74165c20fe..35db1a3955 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -1075,7 +1075,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi // Sort the filaments { - static std::unordered_map sorted_names + std::unordered_map sorted_names = { {"Bambu PLA Basic", 0}, {"Bambu PLA Matte", 1}, {"Bambu PETG HF", 2}, @@ -1090,9 +1090,58 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi {"Bambu ABS-GF", 11} }; + // Helper lambda to find a filament Preset by name. We can call this multiple times to walk the inheritance chain and find the base filament. + auto find_filament_by_name = [](const std::string& wanted, const PresetCollection& filaments) -> const Preset* { + for (auto it = filaments.begin(); it != filaments.end(); ++it) { + if (it->name == wanted) { + return &(*it); + } + } + return nullptr; + }; + + // For each active filament preset, find matching Preset in bundle->filaments and add the base filament alias to sorted_names in highest rank in extruder order + auto bundle = wxGetApp().preset_bundle; + const auto& preset_names = bundle->filament_presets; + for (size_t i = preset_names.size(); i-- > 0; ) { + std::string wanted = preset_names[i]; + const int sort_rank = -((int)preset_names.size() - i); + + const Preset* match = nullptr; + + do { + auto find_result = find_filament_by_name(wanted, bundle->filaments); + if (!find_result) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " No available filament name matches " << wanted; + break; + } + + match = find_result; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found available filament matching current preset name " << wanted + << " - Name: " << match->name << " - Alias: " << match->alias + << " - Inherits: " << match->inherits(); + + if (match->inherits().length() == 0) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No more inherits so we reached the base filament"; + break; + } + + wanted = match->inherits(); + } while (1); // Or loop while (match->alias.length() == 0) because existence of alias and inherits on a Preset seem to be exclusive + + if (!match) { + continue; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Update filament rank to " + std::to_string(sort_rank) + " for preset Name: " + << match->name << " - Alias: " << match->alias; + sorted_names.insert_or_assign(match->alias, sort_rank); + } + static std::vector sorted_vendors { "Bambu Lab", "Generic" }; static std::vector sorted_types { "PLA", "PETG", "ABS", "TPU" }; - auto _filament_sorter = [&query_filament_vendors, &query_filament_types](const wxString& left, const wxString& right) -> bool + auto _filament_sorter = [&query_filament_vendors, &query_filament_types, &sorted_names](const wxString& left, const wxString& right) -> bool { { // Compare name order const auto& iter1 = sorted_names.find(left); From b422636740623f5513692e103fc8af4433acdbf6 Mon Sep 17 00:00:00 2001 From: Anson Liu Date: Mon, 10 Aug 2026 23:51:25 -0700 Subject: [PATCH 31/71] Move Generic vendor above Bambu vendor in AMS material setting. (#11306) * Move Generic vendor above Bambu vendor in AMS material setting. * Remove hardcoded sorted_names. Alphabetically sort Bambu with all vendors * Fix sorting with case insensitive comparison * Use arithmetic to get rank distance because priorities are stored in a vector. This lets us remove the include. --- src/slic3r/GUI/AMSMaterialsSetting.cpp | 83 +++++++++++++------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 35db1a3955..2e436e8462 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -4,6 +4,7 @@ #include "GUI_App.hpp" #include "libslic3r/Preset.hpp" #include "I18N.hpp" +#include #include #include #include @@ -1075,20 +1076,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi // Sort the filaments { - std::unordered_map sorted_names = - { {"Bambu PLA Basic", 0}, - {"Bambu PLA Matte", 1}, - {"Bambu PETG HF", 2}, - {"Bambu ABS", 3}, - {"Bambu PLA Silk", 4}, - {"Bambu PLA-CF" , 5}, - {"Bambu PLA Galaxy", 6}, - {"Bambu PLA Metal", 7}, - {"Bambu PLA Marble", 8}, - {"Bambu PETG-CF", 9}, - {"Bambu PETG Translucent", 10}, - {"Bambu ABS-GF", 11} - }; + std::unordered_map selected_filament_ranks; // Helper lambda to find a filament Preset by name. We can call this multiple times to walk the inheritance chain and find the base filament. auto find_filament_by_name = [](const std::string& wanted, const PresetCollection& filaments) -> const Preset* { @@ -1100,12 +1088,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi return nullptr; }; - // For each active filament preset, find matching Preset in bundle->filaments and add the base filament alias to sorted_names in highest rank in extruder order + // For each active filament preset, find its base filament alias and promote it in extruder order. auto bundle = wxGetApp().preset_bundle; const auto& preset_names = bundle->filament_presets; for (size_t i = preset_names.size(); i-- > 0; ) { std::string wanted = preset_names[i]; - const int sort_rank = -((int)preset_names.size() - i); + const int sort_rank = -static_cast(preset_names.size() - i); const Preset* match = nullptr; @@ -1136,42 +1124,57 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Update filament rank to " + std::to_string(sort_rank) + " for preset Name: " << match->name << " - Alias: " << match->alias; - sorted_names.insert_or_assign(match->alias, sort_rank); + selected_filament_ranks.insert_or_assign(match->alias, sort_rank); } - static std::vector sorted_vendors { "Bambu Lab", "Generic" }; - static std::vector sorted_types { "PLA", "PETG", "ABS", "TPU" }; - auto _filament_sorter = [&query_filament_vendors, &query_filament_types, &sorted_names](const wxString& left, const wxString& right) -> bool + static const std::vector sorted_vendors { "Generic" }; + static const std::vector sorted_types { "PLA", "PETG", "ABS", "TPU" }; + auto priority_rank = [](const std::vector& priorities, const wxString& value) { + const auto iter = std::find_if(priorities.begin(), priorities.end(), [&value](const wxString& priority) { + return priority.CmpNoCase(value) == 0; + }); + return iter - priorities.begin(); + }; + auto _filament_sorter = [&query_filament_vendors, &query_filament_types, &selected_filament_ranks, &priority_rank](const wxString& left, const wxString& right) -> bool { - { // Compare name order - const auto& iter1 = sorted_names.find(left); - int name_order1 = (iter1 != sorted_names.end()) ? iter1->second : INT_MAX; + { // Compare selected filament order + const auto& iter1 = selected_filament_ranks.find(left); + int selected_order1 = (iter1 != selected_filament_ranks.end()) ? iter1->second : INT_MAX; - const auto& iter2 = sorted_names.find(right); - int name_order2 = (iter2 != sorted_names.end()) ? iter2->second : INT_MAX; - if (name_order1 != name_order2) + const auto& iter2 = selected_filament_ranks.find(right); + int selected_order2 = (iter2 != selected_filament_ranks.end()) ? iter2->second : INT_MAX; + if (selected_order1 != selected_order2) { - return name_order1 < name_order2; + return selected_order1 < selected_order2; } } { // Compare vendor - auto iter1 = std::find(sorted_vendors.begin(), sorted_vendors.end(), query_filament_vendors[left]); - auto iter2 = std::find(sorted_vendors.begin(), sorted_vendors.end(), query_filament_vendors[right]); - if (iter1 != iter2) - { - return iter1 < iter2; - }; + const wxString& vendor1 = query_filament_vendors.at(left); + const wxString& vendor2 = query_filament_vendors.at(right); + const auto rank1 = priority_rank(sorted_vendors, vendor1); + const auto rank2 = priority_rank(sorted_vendors, vendor2); + if (rank1 != rank2) + return rank1 < rank2; + + const int vendor_compare = vendor1.CmpNoCase(vendor2); + if (vendor_compare != 0) + return vendor_compare < 0; } { // Compare type - auto iter1 = std::find(sorted_types.begin(), sorted_types.end(), query_filament_types[left]); - auto iter2 = std::find(sorted_types.begin(), sorted_types.end(), query_filament_types[right]); - if (iter1 != iter2) - { - return iter1 < iter2; - } + const wxString& type1 = query_filament_types.at(left); + const wxString& type2 = query_filament_types.at(right); + const auto rank1 = priority_rank(sorted_types, type1); + const auto rank2 = priority_rank(sorted_types, type2); + if (rank1 != rank2) + return rank1 < rank2; + + const int type_compare = type1.CmpNoCase(type2); + if (type_compare != 0) + return type_compare < 0; } - return left < right; + const int name_compare = left.CmpNoCase(right); + return name_compare != 0 ? name_compare < 0 : left < right; }; std::sort(filament_items.begin(), filament_items.end(), _filament_sorter); From 54fe28ab0885e78a6362b3b8c22738d1160c53e8 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 11 Aug 2026 17:50:20 +0800 Subject: [PATCH 32/71] feat: add max visible pages to app config under Preferences -> General -> Plugins --- src/libslic3r/AppConfig.cpp | 36 ++++ src/libslic3r/AppConfig.hpp | 10 + src/slic3r/GUI/Preferences.cpp | 17 +- src/slic3r/plugin/host/PluginPages.cpp | 257 +++++++++++++++++++++---- src/slic3r/plugin/host/PluginPages.hpp | 15 ++ 5 files changed, 294 insertions(+), 41 deletions(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index a5d0e24eac..970faeb620 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -280,6 +280,20 @@ void AppConfig::set_defaults() set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap)); } + if (get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT).empty()) + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT)); + else { + int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + try { + visible_count = std::stoi(get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT)); + } + catch (...) { + visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + } + visible_count = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(visible_count)); + } + if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty()) set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); @@ -1630,6 +1644,28 @@ void AppConfig::set_network_plugin_version(const std::string& version) set(SETTING_NETWORK_PLUGIN_VERSION, version); } +int AppConfig::get_plugin_pages_visible_count() const +{ + std::string value = get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT); + if (value.empty()) + return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + + int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + try { + visible_count = std::stoi(value); + } + catch (...) { + return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + } + return std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); +} + +void AppConfig::set_plugin_pages_visible_count(int count) +{ + count = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(count)); +} + std::vector AppConfig::get_skipped_network_versions() const { std::vector result; diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 2c83ebb488..de5c3a442f 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -41,6 +41,11 @@ using namespace nlohmann; #define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao" #define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals" +#define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count" +#define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1 +#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5 +#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10 + #if defined(_WIN32) || defined(_WIN64) #define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09" #else @@ -374,6 +379,11 @@ public: std::string get_network_plugin_version() const; void set_network_plugin_version(const std::string& version); + // Number of plugin pages shown as fixed tabs before the rest are collapsed into a + // dropdown on the last tab. + int get_plugin_pages_visible_count() const; + void set_plugin_pages_visible_count(int count); + std::vector get_skipped_network_versions() const; void add_skipped_network_version(const std::string& version); bool is_network_version_skipped(const std::string& version) const; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index f802ba6ecb..ca115b9773 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1748,11 +1748,26 @@ void PreferencesDialog::create_items() g_sizer->Add(item_pop_up_filament_map_dialog); #endif + //// GENERAL > Plugins + g_sizer->Add(create_item_title(_L("Plugins")), 1, wxEXPAND); + + auto item_plugin_pages_visible_count = create_item_spinctrl( + _L("Visible plugin pages"), + "", + _L("pages"), + _L("Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab."), + SETTING_PLUGIN_PAGES_VISIBLE_COUNT, + PLUGIN_PAGES_VISIBLE_COUNT_MIN, + PLUGIN_PAGES_VISIBLE_COUNT_MAX, + [](int value) { wxGetApp().mainframe->plugin_pages().set_visible_page_count(value); } + ); + g_sizer->Add(item_plugin_pages_visible_count); + g_sizer->AddSpacer(FromDIP(10)); sizer_page->Add(g_sizer, 0, wxEXPAND); ////////////////////////// - //// CONTROL TAB + //// CONTROL TAB ///////////////////////////////////// m_pref_tabs->AppendItem(_L("Control")); f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0)); diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 3b9b1498ee..1a40fd8984 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -1,5 +1,6 @@ #include "PluginPages.hpp" +#include "libslic3r/AppConfig.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/Notebook.hpp" #include "slic3r/GUI/GUI_App.hpp" @@ -10,12 +11,15 @@ #include +#include + #include #include #include #include #include +#include #include #include @@ -194,9 +198,119 @@ void PluginPage::push_message(const std::string& message) WebView::RunScript(m_browser, script); } +class PluginPagesOverflowPanel : public wxPanel +{ +public: + explicit PluginPagesOverflowPanel(Notebook* parent) + : wxPanel(parent, wxID_ANY) + , m_notebook(parent) + { + auto* sizer = new wxBoxSizer(wxVERTICAL); + + m_choice = new wxChoice(this, wxID_ANY); + m_choice->Bind(wxEVT_CHOICE, &PluginPagesOverflowPanel::on_choice, this); + sizer->Add(m_choice, wxSizerFlags().Expand().Border(wxALL, FromDIP(4))); + + m_content_sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(m_content_sizer, wxSizerFlags().Expand().Proportion(1)); + + SetSizer(sizer); + } + + void add_entry(const PluginCapabilityId& id, PluginPage* page, const wxString& title) + { + page->Reparent(this); + page->Hide(); + m_entries.push_back({id, page, title}); + m_choice->Append(title); + if (m_entries.size() == 1) + select_index(0); + } + + void select_entry(const PluginCapabilityId& id) + { + for (size_t i = 0; i < m_entries.size(); ++i) { + if (m_entries[i].id == id) { + select_index(i); + return; + } + } + } + + void clear() + { + if (m_shown_index != wxNOT_FOUND) + m_entries[static_cast(m_shown_index)].page->Hide(); + m_content_sizer->Clear(false); + + for (const Entry& entry : m_entries) + entry.page->Reparent(m_notebook); + + m_entries.clear(); + m_choice->Clear(); + m_shown_index = wxNOT_FOUND; + } + + wxString current_title() const { return m_shown_index == wxNOT_FOUND ? wxString() : m_entries[static_cast(m_shown_index)].title; } + int current_image_id() const + { + return m_shown_index == wxNOT_FOUND ? wxBookCtrlBase::NO_IMAGE : m_entries[static_cast(m_shown_index)].page->get_icon_image_id(); + } + +private: + struct Entry + { + PluginCapabilityId id; + PluginPage* page; + wxString title; + }; + + void on_choice(wxCommandEvent&) + { + const int selection = m_choice->GetSelection(); + if (selection != wxNOT_FOUND) + select_index(static_cast(selection)); + } + + void select_index(size_t index) + { + if (index >= m_entries.size()) + return; + + if (m_shown_index != wxNOT_FOUND) + m_entries[static_cast(m_shown_index)].page->Hide(); + + m_content_sizer->Clear(false); + m_content_sizer->Add(m_entries[index].page, wxSizerFlags().Expand().Proportion(1)); + m_entries[index].page->Show(); + Layout(); + + m_shown_index = static_cast(index); + m_choice->SetSelection(static_cast(index)); + + const int tab_index = m_notebook->FindPage(this); + if (tab_index != wxNOT_FOUND) { + m_notebook->SetPageText(static_cast(tab_index), m_entries[index].title); + m_notebook->SetPageImage(static_cast(tab_index), m_entries[index].page->get_icon_image_id()); + } + } + + Notebook* m_notebook{nullptr}; + wxChoice* m_choice{nullptr}; + wxBoxSizer* m_content_sizer{nullptr}; + std::vector m_entries; + int m_shown_index{wxNOT_FOUND}; +}; + PluginPages::~PluginPages() { shutdown(); + // try { + // } catch (const std::exception& error) { + // BOOST_LOG_TRIVIAL(error) << "PluginPages::~PluginPages: shutdown() threw: " << error.what(); + // } catch (...) { + // BOOST_LOG_TRIVIAL(error) << "PluginPages::~PluginPages: shutdown() threw a non-standard exception"; + // } } void PluginPages::initialize(Notebook* parent) @@ -206,15 +320,17 @@ void PluginPages::initialize(Notebook* parent) if (m_parent == nullptr) return; - // Keep image-list indices stable for the lifetime of this notebook. Removing an image - // would shift every later index, so deregistration only removes the page. + m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count(); + m_notebook_base_index = static_cast(m_parent->GetPageCount()); + m_image_list = std::make_unique(20, 20, true, 0); m_parent->SetImageList(m_image_list.get()); for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { if (capability) - on_cap_register(capability->identity()); + create_page(capability->identity()); } + relayout(); } void PluginPages::shutdown() @@ -225,6 +341,17 @@ void PluginPages::shutdown() m_parent->SetImageList(nullptr); m_image_list.reset(); m_parent = nullptr; + m_notebook_base_index = 0; +} + +void PluginPages::set_visible_page_count(int count) +{ + const int clamped = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); + if (clamped == m_visible_page_count) + return; + + m_visible_page_count = clamped; + relayout(); } std::shared_ptr PluginPages::get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const @@ -236,14 +363,14 @@ std::shared_ptr PluginPages::get_pages_cap(const PluginCa return std::dynamic_pointer_cast(capability); } -void PluginPages::on_cap_register(const PluginCapabilityId& id) +bool PluginPages::create_page(const PluginCapabilityId& id) { - if (m_parent == nullptr || m_pages.find(id) != m_pages.end()) - return; + if (m_pages.find(id) != m_pages.end()) + return false; auto capability = get_pages_cap(id, true); if (!capability) - return; + return false; std::string icon; try { @@ -257,11 +384,9 @@ void PluginPages::on_cap_register(const PluginCapabilityId& id) auto* page = new PluginPage(m_parent, std::move(capability)); if (!page->is_valid()) { page->Destroy(); - return; + return false; } - const wxString title = wxString::FromUTF8(id.name); - int image_id = wxBookCtrlBase::NO_IMAGE; if (!icon.empty() && m_image_list) { try { @@ -281,14 +406,18 @@ void PluginPages::on_cap_register(const PluginCapabilityId& id) } page->set_icon_image_id(image_id); - if (!m_parent->AddPage(page, title, false, image_id)) { - if (image_id != wxBookCtrlBase::NO_IMAGE && m_image_list && image_id == m_image_list->GetImageCount() - 1) - m_image_list->Remove(image_id); - page->Destroy(); - return; - } - m_pages.emplace(id, page); + m_order.push_back(id); + return true; +} + +void PluginPages::on_cap_register(const PluginCapabilityId& id) +{ + if (m_parent == nullptr) + return; + + if (create_page(id)) + relayout(); } void PluginPages::on_cap_deregister(const PluginCapabilityId& id) @@ -327,39 +456,87 @@ void PluginPages::remove_page(const PluginCapabilityId& id) PluginPage* page = it->second; const int removed_image_id = page->get_icon_image_id(); page->detach_capability(); - if (m_parent != nullptr) { - const int index = m_parent->FindPage(page); - if (index != wxNOT_FOUND) - m_parent->RemovePage(static_cast(index)); - } + + m_pages.erase(it); + m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end()); if (m_image_list && removed_image_id != wxBookCtrlBase::NO_IMAGE && removed_image_id >= 0 && removed_image_id < m_image_list->GetImageCount()) { m_image_list->Remove(removed_image_id); - // wxImageList IDs are positional. Removing one shifts all later images down by - // one, so update both the page state and the notebook button for those pages. - for (const auto& [other_id, other_page] : m_pages) { - if (other_id == id) - continue; - + // wxImageList IDs are positional. Removing one shifts all later images down by one. + for (auto& [other_id, other_page] : m_pages) { const int other_image_id = other_page->get_icon_image_id(); - if (other_image_id <= removed_image_id) - continue; - - const int updated_image_id = other_image_id - 1; - other_page->set_icon_image_id(updated_image_id); - - if (m_parent != nullptr) { - const int other_index = m_parent->FindPage(other_page); - if (other_index != wxNOT_FOUND) - m_parent->SetPageImage(static_cast(other_index), updated_image_id); - } + if (other_image_id > removed_image_id) + other_page->set_icon_image_id(other_image_id - 1); } } + relayout(); page->Destroy(); - m_pages.erase(it); +} + +wxString PluginPages::page_tab_id(const PluginCapabilityId& id) +{ + return wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name); +} + +void PluginPages::relayout() +{ + if (m_parent == nullptr) + return; + + m_order.erase(std::remove_if(m_order.begin(), m_order.end(), + [this](const PluginCapabilityId& id) { + const bool orphaned = m_pages.find(id) == m_pages.end(); + if (orphaned) + BOOST_LOG_TRIVIAL(error) << "PluginPages::relayout: '" << id.name << "' was in m_order but not m_pages, dropping"; + return orphaned; + }), + m_order.end()); + + wxString id_to_reselect = m_parent->GetSelectedPageName(); + + while (m_parent->GetPageCount() > m_notebook_base_index) + m_parent->RemovePage(m_parent->GetPageCount() - 1); + if (m_overflow_panel != nullptr) + m_overflow_panel->clear(); + + const int visible_slots = std::max(1, m_visible_page_count); + const bool need_overflow = static_cast(m_order.size()) > visible_slots; + const size_t individual_count = need_overflow ? static_cast(visible_slots - 1) : m_order.size(); + + for (size_t i = 0; i < individual_count; ++i) { + const PluginCapabilityId& id = m_order[i]; + PluginPage* page = m_pages.at(id); + m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), page->get_icon_image_id()); + } + + if (need_overflow) { + if (m_overflow_panel == nullptr) + m_overflow_panel = new PluginPagesOverflowPanel(m_parent); + + bool reselecting_overflow_entry = false; + for (size_t i = individual_count; i < m_order.size(); ++i) { + const PluginCapabilityId& id = m_order[i]; + m_overflow_panel->add_entry(id, m_pages.at(id), wxString::FromUTF8(id.name)); + if (page_tab_id(id) == id_to_reselect) { + m_overflow_panel->select_entry(id); + reselecting_overflow_entry = true; + } + } + if (reselecting_overflow_entry) + id_to_reselect = "plugin.__overflow__"; + + m_parent->InsertPage(m_parent->GetPageCount(), "plugin.__overflow__", m_overflow_panel, + m_overflow_panel->current_title(), m_overflow_panel->current_image_id()); + } else if (m_overflow_panel != nullptr) { + m_overflow_panel->Destroy(); + m_overflow_panel = nullptr; + } + + if (!id_to_reselect.empty()) + m_parent->SelectPageByName(id_to_reselect); } } // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index d925f2eb32..99213b425b 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -47,6 +48,8 @@ private: int m_icon_image_id = wxBookCtrlBase::NO_IMAGE; }; +class PluginPagesOverflowPanel; + class PluginPages { public: @@ -64,14 +67,26 @@ public: void on_plugin_register(const std::string& plugin_key); void on_plugin_deregister(const std::string& plugin_key); + int get_visible_page_count() const { return m_visible_page_count; } + void set_visible_page_count(int count); + private: std::shared_ptr get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const; + bool create_page(const PluginCapabilityId& id); void remove_page(const PluginCapabilityId& id); + void relayout(); + static wxString page_tab_id(const PluginCapabilityId& id); + std::map m_pages; + std::vector m_order; Notebook* m_parent{nullptr}; + size_t m_notebook_base_index{0}; std::unique_ptr m_image_list; + int m_visible_page_count{0}; + + PluginPagesOverflowPanel* m_overflow_panel{nullptr}; }; } // namespace Slic3r From 7be7f075516ef7fbe4b362b12399b918c86ade0a Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 11 Aug 2026 17:50:49 +0800 Subject: [PATCH 33/71] feat: UI for dropdown to select which plugin to show past max visible pages --- src/slic3r/GUI/Notebook.cpp | 18 +++ src/slic3r/GUI/Notebook.hpp | 5 + src/slic3r/plugin/host/PluginPages.cpp | 189 ++++++++----------------- src/slic3r/plugin/host/PluginPages.hpp | 7 +- 4 files changed, 87 insertions(+), 132 deletions(-) diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index 847996aba9..f9db689faa 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -272,6 +272,24 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const return btn->GetLabel(); } +// ORCA +void ButtonsListCtrl::SetOverflowButton(wxWindow* button) +{ + if (m_overflow_button == button) + return; + + if (m_overflow_button != nullptr) + m_sizer->Detach(m_overflow_button); + + m_overflow_button = button; + + if (m_overflow_button != nullptr) + // Right after the tab buttons (index 0), ahead of any stretch spacer / side_tools. + m_sizer->Insert(1, m_overflow_button, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxBOTTOM, m_btn_margin); + + m_sizer->Layout(); +} + //#endif // _WIN32 void Notebook::Init() diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index 859ada37a5..da90535481 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -36,6 +36,9 @@ public: void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA + // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g. + // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller. + void SetOverflowButton(wxWindow* button); private: wxFlexGridSizer* m_buttons_sizer; @@ -47,6 +50,7 @@ private: int m_line_margin; std::vector m_pageLabels; // ORCA wxImageList* m_imageList{nullptr}; + wxWindow* m_overflow_button{nullptr}; // ORCA }; class Notebook : public wxBookCtrlBase @@ -324,6 +328,7 @@ public: } ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast(m_bookctrl); } + void SetOverflowButton(wxWindow* button) { GetBtnsListCtrl()->SetOverflowButton(button); } int FindPageByName(const wxString& id) const { diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 1a40fd8984..551e39877d 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -4,6 +4,7 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/Notebook.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Widgets/Button.hpp" #include "slic3r/GUI/Widgets/WebView.hpp" #include "slic3r/GUI/Widgets/WebViewHostDialog.hpp" #include "slic3r/GUI/wxExtensions.hpp" @@ -19,7 +20,7 @@ #include #include -#include +#include #include #include @@ -198,110 +199,6 @@ void PluginPage::push_message(const std::string& message) WebView::RunScript(m_browser, script); } -class PluginPagesOverflowPanel : public wxPanel -{ -public: - explicit PluginPagesOverflowPanel(Notebook* parent) - : wxPanel(parent, wxID_ANY) - , m_notebook(parent) - { - auto* sizer = new wxBoxSizer(wxVERTICAL); - - m_choice = new wxChoice(this, wxID_ANY); - m_choice->Bind(wxEVT_CHOICE, &PluginPagesOverflowPanel::on_choice, this); - sizer->Add(m_choice, wxSizerFlags().Expand().Border(wxALL, FromDIP(4))); - - m_content_sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(m_content_sizer, wxSizerFlags().Expand().Proportion(1)); - - SetSizer(sizer); - } - - void add_entry(const PluginCapabilityId& id, PluginPage* page, const wxString& title) - { - page->Reparent(this); - page->Hide(); - m_entries.push_back({id, page, title}); - m_choice->Append(title); - if (m_entries.size() == 1) - select_index(0); - } - - void select_entry(const PluginCapabilityId& id) - { - for (size_t i = 0; i < m_entries.size(); ++i) { - if (m_entries[i].id == id) { - select_index(i); - return; - } - } - } - - void clear() - { - if (m_shown_index != wxNOT_FOUND) - m_entries[static_cast(m_shown_index)].page->Hide(); - m_content_sizer->Clear(false); - - for (const Entry& entry : m_entries) - entry.page->Reparent(m_notebook); - - m_entries.clear(); - m_choice->Clear(); - m_shown_index = wxNOT_FOUND; - } - - wxString current_title() const { return m_shown_index == wxNOT_FOUND ? wxString() : m_entries[static_cast(m_shown_index)].title; } - int current_image_id() const - { - return m_shown_index == wxNOT_FOUND ? wxBookCtrlBase::NO_IMAGE : m_entries[static_cast(m_shown_index)].page->get_icon_image_id(); - } - -private: - struct Entry - { - PluginCapabilityId id; - PluginPage* page; - wxString title; - }; - - void on_choice(wxCommandEvent&) - { - const int selection = m_choice->GetSelection(); - if (selection != wxNOT_FOUND) - select_index(static_cast(selection)); - } - - void select_index(size_t index) - { - if (index >= m_entries.size()) - return; - - if (m_shown_index != wxNOT_FOUND) - m_entries[static_cast(m_shown_index)].page->Hide(); - - m_content_sizer->Clear(false); - m_content_sizer->Add(m_entries[index].page, wxSizerFlags().Expand().Proportion(1)); - m_entries[index].page->Show(); - Layout(); - - m_shown_index = static_cast(index); - m_choice->SetSelection(static_cast(index)); - - const int tab_index = m_notebook->FindPage(this); - if (tab_index != wxNOT_FOUND) { - m_notebook->SetPageText(static_cast(tab_index), m_entries[index].title); - m_notebook->SetPageImage(static_cast(tab_index), m_entries[index].page->get_icon_image_id()); - } - } - - Notebook* m_notebook{nullptr}; - wxChoice* m_choice{nullptr}; - wxBoxSizer* m_content_sizer{nullptr}; - std::vector m_entries; - int m_shown_index{wxNOT_FOUND}; -}; - PluginPages::~PluginPages() { shutdown(); @@ -499,44 +396,78 @@ void PluginPages::relayout() while (m_parent->GetPageCount() > m_notebook_base_index) m_parent->RemovePage(m_parent->GetPageCount() - 1); - if (m_overflow_panel != nullptr) - m_overflow_panel->clear(); const int visible_slots = std::max(1, m_visible_page_count); const bool need_overflow = static_cast(m_order.size()) > visible_slots; - const size_t individual_count = need_overflow ? static_cast(visible_slots - 1) : m_order.size(); - for (size_t i = 0; i < individual_count; ++i) { - const PluginCapabilityId& id = m_order[i]; + // Every visible slot is a normal, individual tab hosting its own page. When there's + // overflow, the last slot's page is swappable via m_overflow_button/show_overflow_menu() + // rather than being a fixed page — m_swapped_in_id tracks which one currently sits there. + std::vector tab_ids; + if (!need_overflow) { + tab_ids = m_order; + m_swapped_in_id.reset(); + } else { + const auto overflow_begin = m_order.begin() + (visible_slots - 1); + tab_ids.assign(m_order.begin(), overflow_begin); + + if (!m_swapped_in_id || std::find(overflow_begin, m_order.end(), *m_swapped_in_id) == m_order.end()) + m_swapped_in_id = *overflow_begin; + tab_ids.push_back(*m_swapped_in_id); + } + + for (const auto& id : tab_ids) { PluginPage* page = m_pages.at(id); m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), page->get_icon_image_id()); } if (need_overflow) { - if (m_overflow_panel == nullptr) - m_overflow_panel = new PluginPagesOverflowPanel(m_parent); - - bool reselecting_overflow_entry = false; - for (size_t i = individual_count; i < m_order.size(); ++i) { - const PluginCapabilityId& id = m_order[i]; - m_overflow_panel->add_entry(id, m_pages.at(id), wxString::FromUTF8(id.name)); - if (page_tab_id(id) == id_to_reselect) { - m_overflow_panel->select_entry(id); - reselecting_overflow_entry = true; - } + if (m_overflow_button == nullptr) { + auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER); + btn->SetCornerRadius(0); + const int em = em_unit(m_parent); + btn->SetMinSize({40 * em / 10, 36 * em / 10}); + btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { show_overflow_menu(); }); + GUI::wxGetApp().UpdateDarkUI(btn); + m_overflow_button = btn; } - if (reselecting_overflow_entry) - id_to_reselect = "plugin.__overflow__"; - - m_parent->InsertPage(m_parent->GetPageCount(), "plugin.__overflow__", m_overflow_panel, - m_overflow_panel->current_title(), m_overflow_panel->current_image_id()); - } else if (m_overflow_panel != nullptr) { - m_overflow_panel->Destroy(); - m_overflow_panel = nullptr; + m_parent->SetOverflowButton(m_overflow_button); + } else if (m_overflow_button != nullptr) { + m_parent->SetOverflowButton(nullptr); + m_overflow_button->Destroy(); + m_overflow_button = nullptr; } if (!id_to_reselect.empty()) m_parent->SelectPageByName(id_to_reselect); } +void PluginPages::show_overflow_menu() +{ + const int visible_slots = std::max(1, m_visible_page_count); + if (m_overflow_button == nullptr || static_cast(m_order.size()) <= visible_slots) + return; + + const std::vector overflow_ids(m_order.begin() + (visible_slots - 1), m_order.end()); + + wxMenu menu; + for (size_t i = 0; i < overflow_ids.size(); ++i) + menu.AppendRadioItem(static_cast(wxID_HIGHEST + 1 + i), wxString::FromUTF8(overflow_ids[i].name)); + if (m_swapped_in_id) { + const auto it = std::find(overflow_ids.begin(), overflow_ids.end(), *m_swapped_in_id); + if (it != overflow_ids.end()) + menu.Check(static_cast(wxID_HIGHEST + 1 + (it - overflow_ids.begin())), true); + } + + menu.Bind(wxEVT_MENU, [this, overflow_ids](wxCommandEvent& evt) { + const size_t index = static_cast(evt.GetId() - (wxID_HIGHEST + 1)); + if (index >= overflow_ids.size()) + return; + m_swapped_in_id = overflow_ids[index]; + relayout(); + m_parent->SelectPageByName(page_tab_id(*m_swapped_in_id)); + }); + m_overflow_button->PopupMenu(&menu); +} + } // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index 99213b425b..cc54332dbe 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -48,8 +49,6 @@ private: int m_icon_image_id = wxBookCtrlBase::NO_IMAGE; }; -class PluginPagesOverflowPanel; - class PluginPages { public: @@ -76,6 +75,7 @@ private: void remove_page(const PluginCapabilityId& id); void relayout(); + void show_overflow_menu(); static wxString page_tab_id(const PluginCapabilityId& id); std::map m_pages; @@ -86,7 +86,8 @@ private: std::unique_ptr m_image_list; int m_visible_page_count{0}; - PluginPagesOverflowPanel* m_overflow_panel{nullptr}; + std::optional m_swapped_in_id; + wxWindow* m_overflow_button{nullptr}; }; } // namespace Slic3r From c5d2944ee06dab86e8f90807d7622c97f822df86 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 11 Aug 2026 09:54:37 -0300 Subject: [PATCH 34/71] Euskera update (#15215) Based in https://github.com/OrcaSlicer/OrcaSlicer/pull/14970#issuecomment-5145928650 --- localization/i18n/eu/OrcaSlicer_eu.po | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index 430e4f5a60..fa10cc387f 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -12013,11 +12013,11 @@ msgstr "Purgatze-dorreak euskarriak objektuaren geruza-altuera bera izatea eskat # AI Translated msgid "For Organic supports, two walls are supported only with the Hollow/Default base pattern." -msgstr "Euskarri organikoetan, bi horma Hollow/Default oinarri-patroiarekin soilik onartzen dira." +msgstr "Euskarri organikoetan, bi horma Hutsa/Lehenetsia oinarri-patroiarekin soilik onartzen dira." # AI Translated msgid "The Lightning base pattern is not supported by this support type; Rectilinear will be used instead." -msgstr "Lightning oinarri-patroia ez du euskarri mota honek onartzen; Rectilinear erabiliko da horren ordez." +msgstr "Tximista oinarri-patroia ez du euskarri mota honek onartzen; Lerrozuzena erabiliko da horren ordez." msgid "Organic support tree tip diameter must not be smaller than support material extrusion width." msgstr "Euskarri organikoaren zuhaitz-muturraren diametroak ezin du izan euskarri-materialaren estrusio-zabalera baino txikiagoa." @@ -12030,7 +12030,7 @@ msgstr "Euskarri organikoaren adar-diametroak ezin du izan euskarri-zuhaitzaren # AI Translated msgid "The Hollow base pattern is not supported by this support type; Rectilinear will be used instead." -msgstr "Hollow oinarri-patroia ez du euskarri mota honek onartzen; Rectilinear erabiliko da horren ordez." +msgstr "Hutsa oinarri-patroia ez du euskarri mota honek onartzen; Lerrozuzena erabiliko da horren ordez." msgid "Support enforcers are used but support is not enabled. Please enable support." msgstr "Euskarri-behartzaileak erabiltzen dira, baina euskarria ez dago gaituta. Gaitu euskarriak." @@ -13252,7 +13252,7 @@ msgstr "Moderatua" # AI Translated msgid "Top surface pattern" -msgstr "Goiko gainazalaren patroia" +msgstr "Goiko gainazaleko patroia" # AI Translated msgid "This is the line pattern for top surface infill." @@ -13265,13 +13265,13 @@ msgid "Monotonic line" msgstr "Lerro monotonikoa" msgid "Rectilinear" -msgstr "Rectilinear" +msgstr "Lerrozuzena" msgid "Aligned Rectilinear" msgstr "Lerrozuzen lerrokatua" msgid "Concentric" -msgstr "Concentric" +msgstr "Kontzentrikoa" msgid "Hilbert Curve" msgstr "Hilbert kurba" @@ -13337,7 +13337,7 @@ msgstr "Kanporantz" # AI Translated msgid "Bottom surface pattern" -msgstr "Beheko gainazalaren patroia" +msgstr "Beheko gainazaleko patroia" # AI Translated msgid "This is the line pattern of bottom surface infill, not including bridge infill." @@ -13362,7 +13362,7 @@ msgid "" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Gaineko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Konzentrikoa, Arkimedeen Akordeak, Oktograma Espirala).\n" +"Goiko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Kontzentrikoa, Arkimedesen kordak, Oktagrama-kiribila).\n" "Kanporanzkoa erdialdean hasten da, beraz, gehiegizko materiala gutxien ikusten den ertzera bultzatzen da. Barruranzkoa ertzean hasten da eta erdian kurba estuekin amaitzen da.\n" "Lehenetsiak bide laburreneko ordena erabiltzen du, zeina norabide batean zein bestean ibil daitekeen." @@ -13375,7 +13375,7 @@ msgid "" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Beheko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Konzentrikoa, Arkimedeen Akordeak, Oktograma Espirala).\n" +"Beheko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Kontzentrikoa, Arkimedesen kordak, Oktagrama-kiribila).\n" "Barruranzkoa hasten da gainazal bakoitza kanpoko kurba zabalagoekin, eta horrek lehen geruzaren atxikimendua hobetzen du erdiko kurba estuak itsatsi ez daitezkeen inprimatze-plaketan. Kanporanzkoa erdialdean hasten da, gehiegizko materiala ertzera bultzatuz.\n" "Lehenetsiak bide laburreneko ordena erabiltzen du, zeina norabide batean zein bestean ibil daitekeen." @@ -16431,9 +16431,9 @@ msgid "" msgstr "" "Euskarriaren lerro-patroia.\n" "\n" -"Zuhaitz-euskarrien aukera lehenetsia Hutsa da, hau da, ez dago oinarri-patroirik. Beste euskarri motetarako, aukera lehenetsia patroi zuzenekoa da.\n" +"Zuhaitz-euskarrien aukera lehenetsia Hutsa da, hau da, ez dago oinarri-patroirik. Beste euskarri motetarako, aukera lehenetsia patroi lerrozuzena da.\n" "\n" -"OHARRA: Euskarri organikoetan, bi hormak Hutsa/Lehenetsia oinarri-patroiarekin bakarrik onartzen dira. Tximistetan oinarritutako patroia Zuhaitz mehea/Indartsua/Hibridoa euskarriek bakarrik onartzen dute. Beste euskarri motetarako, Zuzenekoa erabiliko da Tximistenaren ordez." +"OHARRA: Euskarri organikoetan, bi hormak Hutsa/Lehenetsia oinarri-patroiarekin bakarrik onartzen dira. Tximista oinarri-patroia Zuhaitz mehea/Indartsua/Hibridoa euskarriek bakarrik onartzen dute. Beste euskarri motetarako, Lerrozuzena erabiliko da Tximistaren ordez." msgid "Rectilinear grid" msgstr "Sare lerrozuzena" @@ -16713,7 +16713,7 @@ msgid "" " - Each Model: centers the pattern on each connected body. Parts that touch or overlap share one center; parts detached from the rest each get their own.\n" " - Each Assembly: uses a single shared center for the whole object or assembly." msgstr "" -"Goiko eta beheko gainazaleko patroi zentratuen (Arkimedesen kordak, Oktagrama-espirala) zentroa non kokatzen den aukeratzen du.\n" +"Goiko eta beheko gainazaleko patroi zentratuen (Arkimedesen kordak, Oktagrama-kiribila) zentroa non kokatzen den aukeratzen du.\n" " - Gainazal bakoitza: patroia gainazal-eskualde bakoitzean zentratzen du, uharte bakoitza bere kabuz simetrikoa izan dadin.\n" " - Modelo bakoitza: patroia konektatutako gorputz bakoitzean zentratzen du. Elkar ukitzen edo gainjartzen diren piezek zentro bera partekatzen dute; gainerakoetatik bereizitako piezek beren zentroa dute.\n" " - Muntaketa bakoitza: zentro partekatu bakarra erabiltzen du objektu edo muntaketa osorako." @@ -17144,7 +17144,7 @@ msgid "Detect narrow internal solid infills" msgstr "Detektatu barruko betegarri solido estua" msgid "This option will auto-detect narrow internal solid infill areas. If enabled, the concentric pattern will be used for the area to speed up printing. Otherwise, the rectilinear pattern will be used by default." -msgstr "Aukera honek barruko betegarri solido estuko eremuak automatikoki detektatuko ditu. Gaituta badago, eremu horretan patroi zentrokidea erabiliko da inprimaketa azkartzeko. Bestela, patroi lerrozuzena erabiliko da lehenespenez." +msgstr "Aukera honek barruko betegarri solido estuko eremuak automatikoki detektatuko ditu. Gaituta badago, eremu horretan patroi kontzentrikoa erabiliko da inprimaketa azkartzeko. Bestela, patroi lerrozuzena erabiliko da lehenespenez." msgid "invalid value " msgstr "balio baliogabea " @@ -18703,7 +18703,7 @@ msgstr "YOLO (perfekzionista)" # AI Translated msgid "Top Surface Pattern" -msgstr "Goiko gainazalaren patroia" +msgstr "Goiko gainazaleko patroia" msgid "Choose a slot for the selected color" msgstr "Aukeratu zirrikitu bat hautatutako kolorearentzat" From 117ed0060d5ba6327cc993c9dcdd40de4b6c24a2 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:25:09 +0300 Subject: [PATCH 35/71] Fix Celsius symbol rendering in Preview (#15202) --- deps_src/imgui/imgui_draw.cpp | 1 + src/slic3r/GUI/ImGuiWrapper.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/deps_src/imgui/imgui_draw.cpp b/deps_src/imgui/imgui_draw.cpp index 913a551fa0..d88bc79904 100644 --- a/deps_src/imgui/imgui_draw.cpp +++ b/deps_src/imgui/imgui_draw.cpp @@ -2856,6 +2856,7 @@ const ImWchar* ImFontAtlas::GetGlyphRangesDefault() { 0x0020, 0x00FF, // Basic Latin + Latin Supplement 0x2000, 0x206F, // General Punctuation + 0x2103, 0x2103, // ℃ Celsius symbol 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana 0x31F0, 0x31FF, // Katakana Phonetic Extensions 0xFF00, 0xFFEF, // Half-width characters diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 8974a169d1..d46e8ed31b 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -2809,12 +2809,26 @@ void ImGuiWrapper::init_font(bool compress) } } + if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { + ImFontConfig fallback_cfg = cfg; + fallback_cfg.MergeMode = true; + static constexpr ImWchar celsius_range[] = { 0x2103, 0x2103, 0 }; + io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf").c_str(), m_font_size, &fallback_cfg, celsius_range); + } + bold_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_bold).c_str(), m_font_size, &cfg, ranges.Data); if (bold_font == nullptr) { bold_font = io.Fonts->AddFontDefault(); if (bold_font == nullptr) { throw Slic3r::RuntimeError("ImGui: Could not load deafult font"); } } + if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { + ImFontConfig fallback_cfg = cfg; + fallback_cfg.MergeMode = true; + static constexpr ImWchar celsius_range[] = { 0x2103, 0x2103, 0 }; + io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Bold.ttf").c_str(), m_font_size, &fallback_cfg, celsius_range); + } + if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { default_font->Scale *= 1.25f; bold_font->Scale *= 1.25f; From 6dbdb1d07e0448e0bcfc1c38451fc1d256d86c7a Mon Sep 17 00:00:00 2001 From: Terasit Juntarasombut <93132156+Icezaza2543@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:56:59 +0700 Subject: [PATCH 36/71] l10n: Fix contextual and technical translation errors in Thai (th) (#15213) * l10n: Fix contextual and technical translation errors in Thai (th) * l10n(th): standardize technical terms and sync localization glossary (#15213) * l10n(th): remove localization_glossary.tsv from PR (#15213) --- localization/i18n/th/OrcaSlicer_th.po | 368 +++++++++++++------------- 1 file changed, 184 insertions(+), 184 deletions(-) diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index 6a4499d148..a419ba320e 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -843,7 +843,7 @@ msgid "Hexagon" msgstr "หกเหลี่ยม" msgid "Keep orientation" -msgstr "รักษาปฐมนิเทศ" +msgstr "คงการวางแนว" msgid "Place on cut" msgstr "วางบนการตัด" @@ -891,7 +891,7 @@ msgid "Cut position" msgstr "ตำแหน่งตัด" msgid "Build Volume" -msgstr "ปริมาตรพื้นที่พิมพ์" +msgstr "ปริมาตรการพิมพ์ (Build Volume)" msgid "Multiple" msgstr "หลายรายการ" @@ -1200,7 +1200,7 @@ msgid "Horizontal text" msgstr "ข้อความแนวนอน" msgid "Shift+" -msgstr "กะ+" +msgstr "Shift+" msgid "Mouse move up or down" msgstr "เมาส์เลื่อนขึ้นหรือลง" @@ -2592,7 +2592,7 @@ msgid "Ironing" msgstr "รีดผิว" msgid "Fuzzy skin" -msgstr "ผิวฟัซซี" +msgstr "ผิวฟัซซี (Fuzzy Skin)" msgid "Extruders" msgstr "ชุดดันเส้น" @@ -2817,10 +2817,10 @@ msgid "current" msgstr "ปัจจุบัน" msgid "Scale to build volume" -msgstr "ปรับขนาดเพื่อสร้างปริมาณ" +msgstr "ปรับขนาดให้พอดีกับปริมาตรการพิมพ์" msgid "Scale an object to fit the build volume" -msgstr "ปรับขนาดวัตถุให้พอดีกับปริมาณงานสร้าง" +msgstr "ปรับขนาดวัตถุให้พอดีกับปริมาตรการพิมพ์" msgid "Flush Options" msgstr "ตัวเลือกการไล่เส้น" @@ -2898,10 +2898,10 @@ msgid "Change SVG source file, projection, size, ..." msgstr "เปลี่ยนไฟล์ต้นฉบับ SVG, การฉายภาพ, ขนาด, ..." msgid "Invalidate cut info" -msgstr "ข้อมูลการตัดไม่ถูกต้อง" +msgstr "ยกเลิกข้อมูลการตัด" msgid "Add Primitive" -msgstr "เพิ่มดั้งเดิม" +msgstr "เพิ่มรูปทรงพื้นฐาน" msgid "Add Handy models" msgstr "เพิ่มรุ่นแฮนดี้" @@ -3027,7 +3027,7 @@ msgid "Center" msgstr "กึ่งกลาง" msgid "Drop" -msgstr "หยด" +msgstr "วางลงฐานพิมพ์" msgid "Edit Process Settings" msgstr "แก้ไขการตั้งค่ากระบวนการ" @@ -3182,7 +3182,7 @@ msgid "Switch to per-object setting mode to edit process settings of selected ob msgstr "สลับไปที่โหมดการตั้งค่าต่ออ็อบเจ็กต์เพื่อแก้ไขการตั้งค่ากระบวนการของอ็อบเจ็กต์ที่เลือก" msgid "Remove paint-on fuzzy skin" -msgstr "ลบสีบนผิวที่คลุมเครือ" +msgstr "ลบการระบายสีผิวฟัซซี" # AI Translated msgid "Delete Settings" @@ -3242,7 +3242,7 @@ msgid "Add layers" msgstr "เพิ่มเลเยอร์" msgid "Cut Connectors information" -msgstr "ตัดข้อมูลตัวเชื่อมต่อ" +msgstr "ข้อมูลตัวเชื่อมสำหรับการตัด" msgid "Object manipulation" msgstr "การจัดการวัตถุ" @@ -3279,7 +3279,7 @@ msgid "Layer" msgstr "เลเยอร์" msgid "Selection conflicts" -msgstr "ข้อขัดแย้งในการคัดเลือก" +msgstr "การเลือกขัดแย้งกัน" msgid "If the first selected item is an object, the second should also be an object." msgstr "หากรายการแรกที่เลือกเป็นวัตถุ รายการที่สองก็ควรเป็นวัตถุด้วย" @@ -3390,7 +3390,7 @@ msgid "Plate" msgstr "ฐานพิมพ์" msgid "Brim" -msgstr "ขอบยึดชิ้นงาน" +msgstr "ขอบยึดชิ้นงาน (Brim)" msgid "Object/Part Settings" msgstr "การตั้งค่าวัตถุ/ชิ้นส่วน" @@ -4786,7 +4786,7 @@ msgid "" "YES - Keep Prime Tower\n" "NO - Keep Adaptive Layer Height and Independent Support Layer Height" msgstr "" -"ไพรม์ทาวเวอร์ไม่ทำงานเมื่อเปิดใช้งาน Adaptive Layer Height หรือ Independent ส่วนรองรับ Layer Height\n" +"Prime Tower ไม่ทำงานเมื่อเปิดใช้งาน Adaptive Layer Height หรือ Independent ส่วนรองรับ Layer Height\n" "คุณต้องการเก็บอันไหน?\n" "ใช่ - เก็บ Prime Tower ไว้\n" "ไม่ - คงความสูงของเลเยอร์แบบปรับได้และความสูงของเลเยอร์รองรับที่เป็นอิสระ" @@ -4797,7 +4797,7 @@ msgid "" "YES - Keep Prime Tower\n" "NO - Keep Adaptive Layer Height" msgstr "" -"ไพรม์ทาวเวอร์ไม่ทำงานเมื่อเปิด Adaptive Layer Height\n" +"Prime Tower ไม่ทำงานเมื่อเปิด Adaptive Layer Height\n" "คุณต้องการเก็บอันไหน?\n" "ใช่ - เก็บ Prime Tower ไว้\n" "ไม่ - คงความสูงของเลเยอร์แบบปรับได้" @@ -4808,7 +4808,7 @@ msgid "" "YES - Keep Prime Tower\n" "NO - Keep Independent Support Layer Height" msgstr "" -"ไพร์มทาวเวอร์ไม่ทำงานเมื่อเปิดความสูงของเลเยอร์รองรับอิสระ\n" +"Prime Tower ไม่ทำงานเมื่อเปิดความสูงของเลเยอร์รองรับอิสระ\n" "คุณต้องการเก็บอันไหน?\n" "ใช่ - เก็บ Prime Tower ไว้\n" "ไม่ - รักษาความสูงของชั้นรองรับที่เป็นอิสระ" @@ -5381,7 +5381,7 @@ msgid "Pressure Advance" msgstr "แรงดันล่วงหน้า (Pressure Advance)" msgid "Noop" -msgstr "นะ" +msgstr "ไม่มีการดำเนินการ" msgid "Retract" msgstr "ดึงกลับ" @@ -5405,7 +5405,7 @@ msgid "Travel" msgstr "เดินหัวเปล่า" msgid "Wipe" -msgstr "เช็ดหัวฉีด" +msgstr "เช็ดหัวฉีด (Wipe)" msgid "Extrude" msgstr "ฉีดเส้น" @@ -5441,7 +5441,7 @@ msgid "Support interface" msgstr "ผิวสัมผัสส่วนรองรับ" msgid "Prime tower" -msgstr "ทาวเวอร์ไล่เส้น" +msgstr "Prime Tower" msgid "Bottom surface" msgstr "ผิวด้านล่าง" @@ -5486,7 +5486,7 @@ msgid "Jerk: " msgstr "เจิร์ก: " msgid "PA: " -msgstr "พ่อ:" +msgstr "PA: " msgid "mm/s" msgstr "มม./วินาที" @@ -5552,7 +5552,7 @@ msgid "Tips:" msgstr "เคล็ดลับ:" msgid "Current grouping of slice result is not optimal." -msgstr "การจัดกลุ่มผลลัพธ์การแบ่งส่วนในปัจจุบันไม่เหมาะสมที่สุด" +msgstr "การจัดกลุ่มผลการสไลซ์ปัจจุบันยังไม่เหมาะสม" #, boost-format msgid "Increase %1%g filament and %2% changes compared to optimal grouping." @@ -5585,7 +5585,7 @@ msgid "Regroup filament" msgstr "จัดกลุ่มเส้นพลาสติกใหม่" msgid "up to" -msgstr "ขึ้นไป" +msgstr "สูงสุด" msgid "above" msgstr "ข้างบน" @@ -5642,7 +5642,7 @@ msgid "Filament change times" msgstr "จำนวนครั้งที่เปลี่ยนเส้น" msgid "Tool changes" -msgstr "การเปลี่ยนแปลงเครื่องมือ" +msgstr "การเปลี่ยนเครื่องมือ" msgid "Color change" msgstr "เปลี่ยนสี" @@ -5673,7 +5673,7 @@ msgid "Model printing time" msgstr "ระยะเวลาในการพิมพ์โมเดล" msgid "Show stealth mode" -msgstr "แสดงโหมดซ่อนตัว" +msgstr "แสดงโหมดเงียบ" msgid "Show normal mode" msgstr "แสดงโหมดปกติ" @@ -5690,10 +5690,10 @@ msgid "" "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume." msgstr "" "วัตถุวางอยู่เหนือขอบเขตของแผ่นหรือสูงเกินขีดจำกัดความสูง\n" -"โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรงานประกอบ" +"โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรการพิมพ์" msgid "Variable layer height" -msgstr "ความสูงของชั้นตัวแปร" +msgstr "ความสูงเลเยอร์แบบแปรผัน" msgid "Adaptive" msgstr "ปรับตัวได้" @@ -5743,7 +5743,7 @@ msgid "Following objects are laid over the boundary of plate or exceeds the heig msgstr "วัตถุต่อไปนี้วางอยู่เหนือขอบเขตของแผ่นหรือสูงเกินขีดจำกัดความสูง:\n" msgid "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume.\n" -msgstr "โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรงานประกอบ\n" +msgstr "โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรการพิมพ์\n" #, c-format, boost-format msgid "The position or size of some models exceeds the %s's printable range." @@ -5783,7 +5783,7 @@ msgid "Optimize support interface area" msgstr "ปรับพื้นที่อินเทอร์เฟซส่วนรองรับให้เหมาะสม" msgid "Orient" -msgstr "ตะวันออก" +msgstr "จัดวางแนว" msgid "Arrange options" msgstr "ตัวเลือกจัดเรียง" @@ -5929,7 +5929,7 @@ msgid "Paint Toolbar" msgstr "แถบเครื่องมือสี" msgid "Explosion Ratio" -msgstr "อัตราส่วนการระเบิด" +msgstr "ระดับการแยกชิ้นส่วน" msgid "Section View" msgstr "มุมมองส่วน" @@ -6002,7 +6002,7 @@ msgid "PLA and PETG filaments detected in the mixture. Adjust parameters accordi msgstr "ตรวจพบเส้นพลาสติก PLA และ PETG ในส่วนผสม ปรับพารามิเตอร์ตาม Wiki เพื่อรับรองคุณภาพการพิมพ์" msgid "The prime tower extends beyond the plate boundary." -msgstr "หอคอยหลักขยายออกไปเกินขอบเขตแผ่นเปลือกโลก" +msgstr "Prime Tower ยื่นออกนอกขอบเขตของเพลตพิมพ์" msgid "Partial flushing volume set to 0. Multi-color printing may cause color mixing in models. Please readjust flushing settings." msgstr "ตั้งค่าปริมาณการไล่เส้นบางส่วนเป็น 0 การพิมพ์หลายสีอาจทำให้เกิดการผสมสีในรุ่นต่างๆ โปรดปรับการตั้งค่าการไล่เส้นใหม่" @@ -7953,7 +7953,7 @@ msgid "Enabling traditional timelapse photography may cause surface imperfection msgstr "การเปิดใช้งานการถ่ายภาพไทม์แลปส์แบบดั้งเดิมอาจทำให้เกิดความไม่สมบูรณ์ของพื้นผิวได้ ขอแนะนำให้เปลี่ยนเป็นโหมดราบรื่น" msgid "Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the prime tower, re-slice and print again." -msgstr "เปิดใช้งานโหมด Smooth สำหรับไทม์แลปส์แล้ว แต่ไพรม์ทาวเวอร์ปิดอยู่ ซึ่งอาจทำให้เกิดข้อบกพร่องในการพิมพ์ โปรดเปิดใช้งานไพร์มทาวเวอร์ สไลซ์ใหม่และพิมพ์อีกครั้ง" +msgstr "เปิดใช้งานโหมด Smooth สำหรับไทม์แลปส์แล้ว แต่ Prime Tower ปิดอยู่ ซึ่งอาจทำให้เกิดข้อบกพร่องในการพิมพ์ โปรดเปิดใช้งาน Prime Tower สไลซ์ใหม่และพิมพ์อีกครั้ง" msgid "Expand sidebar" msgstr "ขยายแถบด้านข้าง" @@ -9311,7 +9311,7 @@ msgid "" "Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\n" "Highly experimental! Slow and may create artifact." msgstr "" -"พยายามคงคุณสมบัติการทาสีไว้ (สี/รอยตะเข็บ/ส่วนรองรับ/คลุมเครือ ฯลฯ) หลังจากเปลี่ยนตาข่ายวัตถุ (เช่น ตัด/โหลดซ้ำจากดิสก์/ลดความซับซ้อน/แก้ไข ฯลฯ)\n" +"พยายามคงคุณสมบัติการทาสีไว้ (สี/รอยตะเข็บ/ส่วนรองรับ/ผิวฟัซซี ฯลฯ) หลังจากเปลี่ยนตาข่ายวัตถุ (เช่น ตัด/โหลดซ้ำจากดิสก์/ลดความซับซ้อน/แก้ไข ฯลฯ)\n" "น่าทดลองมาก! ช้าและอาจสร้างสิ่งประดิษฐ์" msgid "Allow Abnormal Storage" @@ -10241,25 +10241,25 @@ msgstr "คลิกเพื่อรีเซ็ตการตั้งค่ # AI Translated msgid "Prime tower is required for nozzle changing. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?" -msgstr "จำเป็นต้องใช้ทาวเวอร์ไล่เส้นสำหรับการเปลี่ยนหัวฉีด อาจเกิดข้อบกพร่องบนโมเดลหากไม่มีทาวเวอร์ไล่เส้น คุณแน่ใจหรือไม่ว่าต้องการปิดทาวเวอร์ไล่เส้น?" +msgstr "จำเป็นต้องใช้ Prime Tower สำหรับการเปลี่ยนหัวฉีด อาจเกิดข้อบกพร่องบนโมเดลหากไม่มี Prime Tower คุณแน่ใจหรือไม่ว่าต้องการปิด Prime Tower?" msgid "A prime tower is required for smooth timelapse mode. There may be flaws on the model without a prime tower. Are you sure you want to disable the prime tower?" -msgstr "โหมดไทม์แลปส์แบบราบรื่นต้องใช้ไพรม์ทาวเวอร์ หากไม่มีไพรม์ทาวเวอร์อาจเกิดตำหนิบนโมเดลได้ คุณแน่ใจหรือไม่ว่าต้องการปิดไพรม์ทาวเวอร์?" +msgstr "จำเป็นต้องใช้ Prime Tower สำหรับโหมดไทม์แลปส์แบบราบรื่น หากไม่มี Prime Tower อาจเกิดตำหนิบนโมเดลได้ คุณแน่ใจหรือไม่ว่าต้องการปิด Prime Tower?" msgid "A prime tower is required for clumping detection. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?" -msgstr "ต้องใช้ไพรม์ทาวเวอร์ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิตรงรุ่นที่ไม่มีไพร์มทาวเวอร์ คุณแน่ใจหรือไม่ว่าต้องการปิดการใช้งานไพร์มทาวเวอร์?" +msgstr "จำเป็นต้องใช้ Prime Tower ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิบนโมเดลที่ไม่มี Prime Tower คุณแน่ใจหรือไม่ว่าต้องการปิด Prime Tower?" msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable?" -msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานหรือไม่?" +msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและ Prime Tower อาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานหรือไม่?" msgid "A prime tower is required for clumping detection. There may be flaws on the model without prime tower. Do you still want to enable clumping detection?" -msgstr "ต้องใช้ไพรม์ทาวเวอร์ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิตรงรุ่นที่ไม่มีไพร์มทาวเวอร์ คุณยังต้องการเปิดใช้งานการตรวจจับการจับกันเป็นก้อนหรือไม่" +msgstr "จำเป็นต้องใช้ Prime Tower ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิบนโมเดลที่ไม่มี Prime Tower คุณยังต้องการเปิดใช้งานการตรวจจับการจับกันเป็นก้อนหรือไม่" msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable precise Z height?" -msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานความสูง Z ที่แม่นยำหรือไม่" +msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและ Prime Tower อาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานความสูง Z ที่แม่นยำหรือไม่" msgid "A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower?" -msgstr "โหมดไทม์แลปส์แบบราบรื่นต้องใช้ไพรม์ทาวเวอร์ หากไม่มีไพรม์ทาวเวอร์อาจเกิดตำหนิบนโมเดลได้ ต้องการเปิดใช้ไพรม์ทาวเวอร์หรือไม่?" +msgstr "จำเป็นต้องใช้ Prime Tower สำหรับโหมดไทม์แลปส์แบบราบรื่น หากไม่มี Prime Tower อาจเกิดตำหนิบนโมเดลได้ ต้องการเปิดใช้ Prime Tower หรือไม่?" msgid "Still print by object?" msgstr "ยังคงพิมพ์ตามวัตถุใช่ไหม" @@ -10411,7 +10411,7 @@ msgid "Z contouring" msgstr "รูปร่าง Z" msgid "Wall generator" -msgstr "เครื่องกำเนิดไฟฟ้าติดผนัง" +msgstr "ตัวสร้างผนัง" msgid "Walls and surfaces" msgstr "ผนังและพื้นผิว" @@ -10477,7 +10477,7 @@ msgid "G-code output" msgstr "เอาต์พุตรหัส G" msgid "Change extrusion role G-code" -msgstr "เปลี่ยนบทบาทการอัดขึ้นรูป G-code" +msgstr "เปลี่ยนประเภทการพิมพ์ G-code" msgid "Post-processing Scripts" msgstr "สคริปต์หลังการประมวลผล" @@ -10616,7 +10616,7 @@ msgid "Filament end G-code" msgstr "G-code สิ้นสุดของเส้นพลาสติก" msgid "Wipe tower parameters" -msgstr "พารามิเตอร์ทาวเวอร์เช็ดหัวฉีด" +msgstr "พารามิเตอร์ Wipe Tower" msgid "Multi Filament" msgstr "เส้นพลาสติกแบบหลากหลาย" @@ -10753,7 +10753,7 @@ msgid "Nozzle diameter" msgstr "เส้นผ่านศูนย์กลางหัวฉีด" msgid "Wipe tower" -msgstr "ทาวเวอร์เช็ดหัวฉีด" +msgstr "Wipe Tower" msgid "Single extruder multi-material parameters" msgstr "พารามิเตอร์วัสดุหลายชุดดันเส้นเดี่ยว" @@ -10780,7 +10780,7 @@ msgstr "" "ต้องการตั้งค่าเป็น 100% เพื่อเปิดใช้งาน Firmware Retraction หรือไม่?" msgid "Firmware Retraction" -msgstr "การเพิกถอนเฟิร์มแวร์" +msgstr "การดึงกลับด้วยเฟิร์มแวร์ (Firmware Retraction)" msgid "Switching to a printer with different extruder types or numbers will discard or reset changes to extruder or multi-nozzle-related parameters." msgstr "การเปลี่ยนไปใช้เครื่องพิมพ์ที่มีประเภทหรือหมายเลขชุดดันเส้นที่แตกต่างกันจะยกเลิกหรือรีเซ็ตการเปลี่ยนแปลงในชุดดันเส้นหรือพารามิเตอร์ที่เกี่ยวข้องกับหัวฉีดหลายตัว" @@ -11596,7 +11596,7 @@ msgid "Gizmo mesh boolean" msgstr "Gizmo mesh บูลีน" msgid "Gizmo FDM paint-on fuzzy skin" -msgstr "Gizmo FDM เพ้นท์บนผิวที่คลุมเครือ" +msgstr "Gizmo FDM เพ้นท์ผิวฟัซซี" msgid "Gizmo SLA support points" msgstr "จุดส่วนรองรับ Gizmo SLA" @@ -11614,7 +11614,7 @@ msgid "Gizmo assemble" msgstr "กิสโมประกอบ" msgid "Gizmo brim ears" -msgstr "กิสโม่ขอบหู" +msgstr "Gizmo หูขอบยึดชิ้นงาน (Brim Ears)" msgid "Zoom in" msgstr "ซูมเข้า" @@ -11936,10 +11936,10 @@ msgid "Parts of the object at these heights may be too thin or the object may ha msgstr "บางส่วนของวัตถุที่ความสูงเหล่านี้อาจบางเกินไป หรือวัตถุอาจมี mesh ผิดปกติ" msgid "Process change extrusion role G-code" -msgstr "กระบวนการเปลี่ยนบทบาทการอัดขึ้นรูป G-code" +msgstr "กระบวนการเปลี่ยนประเภทการพิมพ์ G-code" msgid "Filament change extrusion role G-code" -msgstr "เส้นพลาสติกเปลี่ยนบทบาทการอัดขึ้นรูป G-code" +msgstr "เส้นพลาสติกเปลี่ยนประเภทการพิมพ์ G-code" msgid "No object can be printed. It may be too small." msgstr "ไม่สามารถพิมพ์วัตถุได้ อาจจะเล็กเกินไป" @@ -12100,7 +12100,7 @@ msgid " is too close to clumping detection area, there may be collisions when pr msgstr "อยู่ใกล้พื้นที่การตรวจจับการจับตัวกันมากเกินไป อาจเกิดการชนกันเมื่อพิมพ์" msgid "Prime Tower" -msgstr "ทาวเวอร์ไล่เส้น" +msgstr "Prime Tower" msgid " is too close to others, and collisions may be caused.\n" msgstr "อยู่ใกล้ผู้อื่นมากเกินไปและอาจเกิดการชนได้\n" @@ -12130,10 +12130,10 @@ msgid "Clumping detection is not supported when \"by object\" sequence is enable msgstr "ไม่รองรับการตรวจจับการจับกันเป็นก้อนเมื่อเปิดใช้งานลำดับ \"ตามวัตถุ\"" msgid "Enabling both precise Z height and the prime tower may cause slicing errors." -msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน" +msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและ Prime Tower อาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน" msgid "A prime tower is required for clumping detection; otherwise, there may be flaws on the model." -msgstr "จำเป็นต้องใช้หอคอยหลักในการตรวจจับการจับกันเป็นก้อน มิฉะนั้นอาจมีข้อบกพร่องในแบบจำลอง" +msgstr "จำเป็นต้องใช้ Prime Tower ในการตรวจจับการจับกันเป็นก้อน มิฉะนั้นอาจมีข้อบกพร่องในแบบจำลอง" msgid "Please select \"By object\" print sequence to print multiple objects in spiral vase mode." msgstr "โปรดเลือกลำดับการพิมพ์ \"ตามวัตถุ\" เพื่อพิมพ์วัตถุหลายชิ้นในโหมดแจกันเกลียว" @@ -12143,15 +12143,15 @@ msgstr "โหมดแจกันเกลียวจะไม่ทำงา #, boost-format msgid "While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation." -msgstr "แม้ว่าวัตถุ %1% จะพอดีกับปริมาตรการสร้าง แต่วัตถุนั้นเกินความสูงของปริมาตรการสร้างสูงสุดเนื่องจากการชดเชยการหดตัวของวัสดุ" +msgstr "แม้ว่าวัตถุ %1% จะพอดีกับปริมาตรการพิมพ์ แต่วัตถุนั้นเกินความสูงของปริมาตรการพิมพ์สูงสุดเนื่องจากการชดเชยการหดตัวของวัสดุ" #, boost-format msgid "The object %1% exceeds the maximum build volume height." -msgstr "วัตถุ %1% เกินความสูงของปริมาตรบิลด์สูงสุด" +msgstr "วัตถุ %1% เกินความสูงของปริมาตรการพิมพ์สูงสุด" #, boost-format msgid "While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height." -msgstr "แม้ว่าออบเจ็กต์ %1% จะพอดีกับปริมาณการสร้าง แต่เลเยอร์สุดท้ายก็เกินความสูงของปริมาตรการสร้างสูงสุด" +msgstr "แม้ว่าวัตถุ %1% จะพอดีกับปริมาตรการพิมพ์ แต่วัตถุนั้นเกินความสูงของปริมาตรการพิมพ์สูงสุด" msgid "You might want to reduce the size of your model or change current print settings and retry." msgstr "คุณอาจต้องการลดขนาดแบบจำลองของคุณหรือเปลี่ยนการตั้งค่าการพิมพ์ปัจจุบันแล้วลองอีกครั้ง" @@ -12160,40 +12160,40 @@ msgid "Variable layer height is not supported with Organic supports." msgstr "ไม่รองรับความสูงของเลเยอร์ที่แปรผันได้ด้วยการรองรับแบบออร์แกนิก" msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." -msgstr "เส้นผ่านศูนย์กลางของหัวฉีดที่แตกต่างกันและเส้นผ่านศูนย์กลางของเส้นพลาสติกที่แตกต่างกันอาจทำงานได้ไม่ดีนักเมื่อเปิดใช้งานไพรม์ทาวเวอร์ ยังเป็นการทดลองอยู่มาก ดังนั้นโปรดดำเนินการด้วยความระมัดระวัง" +msgstr "เส้นผ่านศูนย์กลางของหัวฉีดที่แตกต่างกันและเส้นผ่านศูนย์กลางของเส้นพลาสติกที่แตกต่างกันอาจทำงานได้ไม่ดีนักเมื่อเปิดใช้งาน Prime Tower ยังเป็นการทดลองอยู่มาก ดังนั้นโปรดดำเนินการด้วยความระมัดระวัง" msgid "The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1)." msgstr "ขณะนี้ Wipe Tower รองรับการกำหนดที่อยู่ของชุดดันเส้นแบบสัมพันธ์เท่านั้น (use_relative_e_distances=1)" msgid "Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off." -msgstr "รองรับการป้องกันน้ำซึมด้วยหอเช็ดเมื่อปิด 'single_extruder_multi_material' เท่านั้น" +msgstr "รองรับการป้องกันน้ำซึมด้วย Wipe Tower เมื่อปิด 'single_extruder_multi_material' เท่านั้น" msgid "The prime tower is currently only supported for the Marlin, RepRap/Sprinter, RepRapFirmware and Repetier G-code flavors." -msgstr "ขณะนี้ไพรม์ทาวเวอร์รองรับเฉพาะรสชาติ Marlin, RepRap/Sprinter, RepRapFirmware และ Repetier G-code เท่านั้น" +msgstr "ขณะนี้ Prime Tower รองรับเฉพาะรสชาติ Marlin, RepRap/Sprinter, RepRapFirmware และ Repetier G-code เท่านั้น" msgid "A prime tower is not supported in “By object” print." -msgstr "ไม่รองรับไพรม์ทาวเวอร์ในการพิมพ์ \"ตามวัตถุ\"" +msgstr "ไม่รองรับ Prime Tower ในการพิมพ์ \"ตามวัตถุ\"" msgid "A prime tower is not supported when adaptive layer height is on. It requires that all objects have the same layer height." -msgstr "ไม่รองรับไพรม์ทาวเวอร์เมื่อเปิดความสูงของเลเยอร์แบบปรับได้ กำหนดให้วัตถุทั้งหมดมีความสูงของชั้นเท่ากัน" +msgstr "ไม่รองรับ Prime Tower เมื่อเปิดความสูงของเลเยอร์แบบปรับได้ กำหนดให้วัตถุทั้งหมดมีความสูงของชั้นเท่ากัน" msgid "A prime tower requires any “support gap” to be a multiple of layer height." -msgstr "ไพรม์ทาวเวอร์ต้องการให้ “support gap” เป็นจำนวนเท่าของความสูงชั้น" +msgstr "Prime Tower ต้องการให้ “support gap” เป็นจำนวนเท่าของความสูงชั้น" msgid "A prime tower requires that all objects have the same layer height." -msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดมีความสูงชั้นเท่ากัน" +msgstr "Prime Tower ต้องการให้วัตถุทั้งหมดมีความสูงชั้นเท่ากัน" msgid "A prime tower requires that all objects are printed over the same number of raft layers." -msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดพิมพ์บนจำนวนชั้น raft เท่ากัน" +msgstr "Prime Tower ต้องการให้วัตถุทั้งหมดพิมพ์บนจำนวนชั้น raft เท่ากัน" msgid "The prime tower is only supported for multiple objects if they are printed with the same support_top_z_distance." -msgstr "ไพรม์ทาวเวอร์รองรับวัตถุหลายชิ้นเท่านั้นหากพิมพ์ด้วย support_top_z_distance เท่ากัน" +msgstr "Prime Tower รองรับวัตถุหลายชิ้นเท่านั้นหากพิมพ์ด้วย support_top_z_distance เท่ากัน" msgid "A prime tower requires that all objects are sliced with the same layer height." -msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดถูกสไลซ์ด้วยความสูงชั้นเท่ากัน" +msgstr "Prime Tower ต้องการให้วัตถุทั้งหมดถูกสไลซ์ด้วยความสูงชั้นเท่ากัน" msgid "The prime tower is only supported if all objects have the same variable layer height." -msgstr "ไพรม์ทาวเวอร์ได้รับส่วนรองรับก็ต่อเมื่อวัตถุทั้งหมดมีความสูงของเลเยอร์ที่แปรผันเท่ากัน" +msgstr "Prime Tower ได้รับส่วนรองรับก็ต่อเมื่อวัตถุทั้งหมดมีความสูงของเลเยอร์ที่แปรผันเท่ากัน" msgid "One or more object were assigned an extruder that the printer does not have." msgstr "วัตถุอย่างน้อยหนึ่งชิ้นถูกกำหนดให้เป็นชุดดันเส้นที่เครื่องพิมพ์ไม่มี" @@ -12208,7 +12208,7 @@ msgid "Printing with multiple extruders of differing nozzle diameters. If suppor msgstr "การพิมพ์ด้วยชุดดันเส้นหลายเครื่องที่มีเส้นผ่านศูนย์กลางหัวฉีดต่างกัน หากจะพิมพ์ส่วนรองรับด้วยฟิลาเมนต์ปัจจุบัน (support_filament == 0 หรือ support_interface_filament == 0) หัวฉีดทั้งหมดจะต้องมีเส้นผ่านศูนย์กลางเท่ากัน" msgid "A prime tower requires that support has the same layer height as the object." -msgstr "ไพรม์ทาวเวอร์ต้องการให้ส่วนรองรับมีความสูงชั้นเท่ากับวัตถุ" +msgstr "Prime Tower ต้องการให้ส่วนรองรับมีความสูงชั้นเท่ากับวัตถุ" msgid "For Organic supports, two walls are supported only with the Hollow/Default base pattern." msgstr "สำหรับการรองรับแบบออร์แกนิก ผนังทั้งสองได้รับการรองรับด้วยรูปแบบฐานกลวง/ค่าเริ่มต้นเท่านั้น" @@ -12845,9 +12845,9 @@ msgid "" "\n" "For the first layer, the actual flow ratio for each path role (does not affect brims and skirts) will be multiplied by this value." msgstr "" -"ปัจจัยนี้ส่งผลต่อปริมาณวัสดุในชั้นแรกสำหรับบทบาทเส้นทางการอัดขึ้นรูปที่แสดงอยู่ในส่วนนี้\n" +"ปัจจัยนี้ส่งผลต่อปริมาณวัสดุในชั้นแรกสำหรับประเภทการพิมพ์ที่แสดงอยู่ในส่วนนี้\n" "\n" -"สำหรับชั้นแรก อัตราการไหลตามจริงสำหรับแต่ละบทบาทของเส้นทาง (ไม่ส่งผลต่อขอบยึดชิ้นงานและเส้นล้อมชิ้นงาน) จะถูกคูณด้วยค่านี้" +"สำหรับชั้นแรก อัตราการไหลตามจริงสำหรับแต่ละประเภทการพิมพ์ (ไม่ส่งผลต่อขอบยึดชิ้นงานและเส้นล้อมชิ้นงาน) จะถูกคูณด้วยค่านี้" msgid "Outer wall flow ratio" msgstr "อัตราส่วนการไหลของผนังด้านนอก" @@ -13138,7 +13138,7 @@ msgstr "" "หมายเหตุ: ค่าผลลัพธ์จะไม่ได้รับผลกระทบจากอัตราส่วนการไหลของชั้นแรก" msgid "Brim follows compensated outline" -msgstr "ขอบยึดชิ้นงาน ปฏิบัติตามโครงร่างที่ได้รับการชดเชย" +msgstr "Brim ตามแนวที่ชดเชยแล้ว" msgid "" "When enabled, the brim is aligned with the first-layer perimeter geometry after Elephant Foot Compensation is applied.\n" @@ -13161,10 +13161,10 @@ msgid "Brim ears" msgstr "หู ขอบยึดชิ้นงาน" msgid "Only draw brim over the sharp edges of the model." -msgstr "วาดขอบยึดชิ้นงานไว้เหนือขอบคมของนางแบบเท่านั้น" +msgstr "วาดขอบยึดชิ้นงานไว้เหนือขอบคมของชิ้นงานเท่านั้น" msgid "Brim ear max angle" -msgstr "มุมสูงสุดของหูขอบยึดชิ้นงานนก" +msgstr "มุมสูงสุดของหูขอบยึดชิ้นงาน (Brim Ears)" msgid "" "Maximum angle to let a brim ear appear.\n" @@ -13753,7 +13753,7 @@ msgstr "" "อัตราการไหลของวัตถุขั้นสุดท้ายคือค่านี้คูณด้วยอัตราการไหลของเส้นพลาสติก" msgid "Enable pressure advance" -msgstr "เปิดใช้งานPressure Advance" +msgstr "เปิดใช้ Pressure Advance" msgid "Enable pressure advance, auto calibration result will be overwritten once enabled." msgstr "เปิดใช้งานการเลื่อนแรงดัน ผลลัพธ์การสอบเทียบอัตโนมัติจะถูกเขียนทับเมื่อเปิดใช้งาน" @@ -13762,7 +13762,7 @@ msgid "Pressure advance (Klipper) AKA Linear advance factor (Marlin)." msgstr "แรงดันล่วงหน้า (Pressure Advance) (Klipper) AKA Linear Advance Factor (Marlin)" msgid "Enable adaptive pressure advance (beta)" -msgstr "เปิดใช้งานการปรับPressure Advance (เบต้า)" +msgstr "เปิดใช้ Adaptive Pressure Advance (เบต้า)" #, no-c-format, no-boost-format msgid "" @@ -13780,7 +13780,7 @@ msgstr "" "เมื่อเปิดใช้งาน ค่าล่วงหน้าของแรงดันด้านบนจะถูกแทนที่ อย่างไรก็ตาม แนะนำให้ใช้ค่าเริ่มต้นที่สมเหตุสมผลด้านบนเพื่อเป็นทางเลือกและเมื่อมีการเปลี่ยนเครื่องมือ\n" msgid "Adaptive pressure advance measurements (beta)" -msgstr "การวัดล่วงหน้าด้วยแรงดันแบบปรับได้ (เบต้า)" +msgstr "ข้อมูลวัด Adaptive Pressure Advance (เบต้า)" #, no-c-format, no-boost-format msgid "" @@ -14021,7 +14021,7 @@ msgid "Loading speed" msgstr "ความเร็วกำลังโหลด" msgid "Speed used for loading the filament on the wipe tower." -msgstr "ความเร็วที่ใช้ในการโหลดเส้นพลาสติกบนไวด์ทาวเวอร์" +msgstr "ความเร็วที่ใช้ในการโหลดเส้นพลาสติกบน Wipe Tower" msgid "Loading speed at the start" msgstr "ความเร็วในการโหลดเมื่อเริ่มต้น" @@ -14033,7 +14033,7 @@ msgid "Unloading speed" msgstr "ความเร็วในการขนถ่าย" msgid "Speed used for unloading the filament on the wipe tower (does not affect initial part of unloading just after ramming)." -msgstr "ความเร็วที่ใช้ในการขนถ่ายเส้นพลาสติกบนไวด์ทาวเวอร์ (ไม่ส่งผลต่อส่วนเริ่มแรกของการขนถ่ายหลังจากการชน)" +msgstr "ความเร็วที่ใช้ในการขนถ่ายเส้นพลาสติกบน Wipe Tower (ไม่ส่งผลต่อส่วนเริ่มแรกของการขนถ่ายหลังจากการชน)" msgid "Unloading speed at the start" msgstr "ขนถ่ายความเร็วที่จุดเริ่มต้น" @@ -14075,10 +14075,10 @@ msgid "Minimal purge on wipe tower" msgstr "การล้างข้อมูลบน Wipe Tower น้อยที่สุด" msgid "After a tool change, the exact position of the newly loaded filament inside the nozzle may not be known, and the filament pressure is likely not yet stable. Before purging the print head into an infill or a sacrificial object, Orca Slicer will always prime this amount of material into the wipe tower to produce successive infill or sacrificial object extrusions reliably." -msgstr "หลังจากเปลี่ยนเครื่องมือ อาจไม่ทราบตำแหน่งที่แน่นอนของเส้นพลาสติกที่เพิ่งโหลดใหม่ภายในหัวฉีด และความดันเส้นพลาสติกก็มีแนวโน้มว่ายังไม่เสถียร ก่อนที่จะล้างหัวพิมพ์ลงในวัสดุไส้ในหรือวัตถุบูชายัญ Orca Slicer จะเตรียมวัสดุจำนวนนี้ลงในหอเช็ดเสมอเพื่อสร้างการอัดขึ้นรูปวัตถุแบบไส้ในหรือบูชายัญต่อเนื่องกันอย่างน่าเชื่อถือ" +msgstr "หลังจากเปลี่ยนเครื่องมือ อาจไม่ทราบตำแหน่งที่แน่นอนของเส้นพลาสติกที่เพิ่งโหลดใหม่ภายในหัวฉีด และความดันเส้นพลาสติกก็มีแนวโน้มว่ายังไม่เสถียร ก่อนที่จะล้างหัวพิมพ์ลงในวัสดุไส้ในหรือวัตถุบูชายัญ Orca Slicer จะเตรียมวัสดุจำนวนนี้ลงใน Wipe Tower เสมอเพื่อสร้างการอัดขึ้นรูปวัตถุแบบไส้ในหรือบูชายัญต่อเนื่องกันอย่างน่าเชื่อถือ" msgid "Wipe tower cooling" -msgstr "เช็ดทาวเวอร์คูลลิ่ง" +msgstr "Wipe Tower คูลลิ่ง" msgid "Temperature drop before entering filament tower" msgstr "อุณหภูมิลดลงก่อนเข้าหอใย" @@ -14087,19 +14087,19 @@ msgid "Interface layer pre-extrusion distance" msgstr "ระยะการอัดรีดชั้นอินเตอร์เฟซ" msgid "Pre-extrusion distance for prime tower interface layer (where different materials meet)." -msgstr "ระยะก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" +msgstr "ระยะก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของ Prime Tower (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" msgid "Interface layer pre-extrusion length" msgstr "ความยาวชั้นอินเตอร์เฟซก่อนการอัดขึ้นรูป" msgid "Pre-extrusion length for prime tower interface layer (where different materials meet)." -msgstr "ความยาวก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" +msgstr "ความยาวก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของ Prime Tower (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" msgid "Tower ironing area" msgstr "พื้นที่รีดผิวแบบทาวเวอร์" msgid "Ironing area for prime tower interface layer (where different materials meet)." -msgstr "พื้นที่รีดผิวสำหรับชั้นอินเทอร์เฟซของไพร์มทาวเวอร์ (บริเวณที่วัสดุต่างกันมาบรรจบกัน)" +msgstr "พื้นที่รีดผิวสำหรับชั้นอินเทอร์เฟซของ Prime Tower (บริเวณที่วัสดุต่างกันมาบรรจบกัน)" msgid "mm²" msgstr "มม.²" @@ -14108,13 +14108,13 @@ msgid "Interface layer purge length" msgstr "ความยาวการล้างเลเยอร์อินเทอร์เฟซ" msgid "Purge length for prime tower interface layer (where different materials meet)." -msgstr "ความยาวในการไล่ล้างสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (เมื่อวัสดุต่างกันมาบรรจบกัน)" +msgstr "ความยาวในการไล่ล้างสำหรับชั้นอินเทอร์เฟซของ Prime Tower (เมื่อวัสดุต่างกันมาบรรจบกัน)" msgid "Interface layer print temperature" msgstr "อุณหภูมิการพิมพ์เลเยอร์อินเทอร์เฟซ" msgid "Print temperature for prime tower interface layer (where different materials meet). If set to -1, use max recommended nozzle temperature." -msgstr "อุณหภูมิการพิมพ์สำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (เมื่อวัสดุต่างกันมาบรรจบกัน) หากตั้งค่าเป็น -1 ให้ใช้อุณหภูมิหัวฉีดสูงสุดที่แนะนำ" +msgstr "อุณหภูมิการพิมพ์สำหรับชั้นอินเทอร์เฟซของ Prime Tower (เมื่อวัสดุต่างกันมาบรรจบกัน) หากตั้งค่าเป็น -1 ให้ใช้อุณหภูมิหัวฉีดสูงสุดที่แนะนำ" msgid "Speed of the last cooling move" msgstr "ความเร็วของการทำความเย็นครั้งล่าสุด" @@ -14132,7 +14132,7 @@ msgid "Enable ramming for multi-tool setups" msgstr "เปิดใช้งานการกระแทกสำหรับการตั้งค่าหลายเครื่องมือ" msgid "Perform ramming when using multi-tool printer (i.e. when the 'Single Extruder Multimaterial' in Printer Settings is unchecked). When checked, a small amount of filament is rapidly extruded on the wipe tower just before the tool change. This option is only used when the wipe tower is enabled." -msgstr "ทำการกระแทกเมื่อใช้เครื่องพิมพ์แบบหลายเครื่องมือ (เช่น เมื่อไม่ได้เลือก 'Single ชุดดันเส้น Multimaterial' ในการตั้งค่าเครื่องพิมพ์) เมื่อตรวจสอบแล้ว เส้นพลาสติกจำนวนเล็กน้อยจะถูกอัดรีดอย่างรวดเร็วบนไวด์ทาวเวอร์ก่อนที่จะเปลี่ยนเครื่องมือ ตัวเลือกนี้ใช้เฉพาะเมื่อเปิดใช้งาน Wipe Tower เท่านั้น" +msgstr "ทำการกระแทกเมื่อใช้เครื่องพิมพ์แบบหลายเครื่องมือ (เช่น เมื่อไม่ได้เลือก 'Single ชุดดันเส้น Multimaterial' ในการตั้งค่าเครื่องพิมพ์) เมื่อตรวจสอบแล้ว เส้นพลาสติกจำนวนเล็กน้อยจะถูกอัดรีดอย่างรวดเร็วบน Wipe Tower ก่อนที่จะเปลี่ยนเครื่องมือ ตัวเลือกนี้ใช้เฉพาะเมื่อเปิดใช้งาน Wipe Tower เท่านั้น" msgid "Multi-tool ramming volume" msgstr "ปริมาณการกระแทกหลายเครื่องมือ" @@ -14517,13 +14517,13 @@ msgid "Filament-specific override for ironing flow. This allows you to customize msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับกระแสการรีดผิว ซึ่งช่วยให้คุณปรับแต่งกระแสการรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้ ค่าที่สูงเกินไปส่งผลให้เกิดการอัดขึ้นรูปมากเกินไปบนพื้นผิว" msgid "Ironing line spacing" -msgstr "ระยะห่างระหว่างสายรีดผิว" +msgstr "ระยะห่างระหว่างเส้นรีดผิว" msgid "Filament-specific override for ironing line spacing. This allows you to customize the spacing between ironing lines for each filament type." msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับระยะห่างระหว่างรีดผิว ซึ่งช่วยให้คุณปรับแต่งระยะห่างระหว่างเส้นรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" msgid "Ironing inset" -msgstr "อุปกรณ์รีดผิว" +msgstr "ระยะเว้นขอบการรีดผิว" msgid "Filament-specific override for ironing inset. This allows you to customize the distance to keep from the edges when ironing for each filament type." msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับส่วนเสริมการรีดผิว ซึ่งช่วยให้คุณปรับแต่งระยะห่างจากขอบเมื่อรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" @@ -14565,10 +14565,10 @@ msgid "The average distance between the random points introduced on each line se msgstr "ระยะห่างเฉลี่ยระหว่างจุดสุ่มที่แนะนำในแต่ละส่วนของเส้น" msgid "Apply fuzzy skin to first layer" -msgstr "ทาผิวที่คลุมเครือเป็นชั้นแรก" +msgstr "ใช้ Fuzzy Skin กับชั้นแรก" msgid "Whether to apply fuzzy skin on the first layer." -msgstr "ไม่ว่าจะทาผิวฟุ้งๆในชั้นแรกหรือไม่" +msgstr "กำหนดว่าจะใช้ Fuzzy Skin กับชั้นแรกหรือไม่" msgid "Fuzzy skin generator mode" msgstr "โหมดสร้างผิวฟัซซี" @@ -14599,7 +14599,7 @@ msgid "Combined" msgstr "รวม" msgid "Fuzzy skin noise type" -msgstr "ประเภทเสียงผิวเลือน" +msgstr "ประเภท Noise ของ Fuzzy Skin" msgid "" "Noise type to use for fuzzy skin generation:\n" @@ -14610,12 +14610,12 @@ msgid "" "Voronoi: Divides the surface into voronoi cells, and displaces each one by a random amount. Creates a patchwork texture.\n" "Ripple: Uniform ripple pattern that ripples left and right of the original path. Repeating pattern, woven appearance." msgstr "" -"ประเภทเสียงรบกวนที่ใช้สำหรับการสร้างผิวที่คลุมเครือ:\n" +"ประเภท Noise ที่ใช้สำหรับการสร้างผิวฟัซซี:\n" "คลาสสิก: เสียงสุ่มเครื่องแบบคลาสสิก\n" "Perlin: เสียง Perlin ซึ่งให้เนื้อสัมผัสที่สม่ำเสมอยิ่งขึ้น\n" "Billow: คล้ายกับเสียงเพอร์ลิน แต่เป็นกลุ่มมากกว่า\n" "Ridged Multifractal: สัญญาณรบกวนที่คมชัดพร้อมคุณสมบัติหยัก สร้างพื้นผิวเหมือนหินอ่อน\n" -"โวโรนอย: แบ่งพื้นผิวออกเป็นเซลล์โวโรนอย และแทนที่แต่ละเซลล์ด้วยจำนวนสุ่ม สร้างพื้นผิวแบบเย็บปะติดปะต่อกัน\n" +"Voronoi: แบ่งพื้นผิวออกเป็นเซลล์ Voronoi และแทนที่แต่ละเซลล์ด้วยจำนวนสุ่ม สร้างพื้นผิวแบบเย็บปะติดปะต่อกัน\n" "ระลอกคลื่น: รูปแบบระลอกคลื่นสม่ำเสมอที่กระเพื่อมไปทางซ้ายและขวาของเส้นทางเดิม ลายซ้ำ ลักษณะการทอ." msgid "Classic" @@ -14631,7 +14631,7 @@ msgid "Ridged Multifractal" msgstr "Multifractal แบบสัน" msgid "Voronoi" -msgstr "โวโรน้อย" +msgstr "Voronoi" msgid "Ripple" msgstr "ระลอกคลื่น" @@ -14643,13 +14643,13 @@ msgid "The base size of the coherent noise features, in mm. Higher values will r msgstr "ขนาดฐานของคุณสมบัติเสียงที่สอดคล้องกัน หน่วยเป็น มม. ค่าที่สูงกว่าจะส่งผลให้มีคุณลักษณะที่ใหญ่ขึ้น" msgid "Fuzzy Skin Noise Octaves" -msgstr "อ็อกเทฟเสียงผิวฟัซซี" +msgstr "จำนวน Octave ของ Noise ใน Fuzzy Skin" msgid "The number of octaves of coherent noise to use. Higher values increase the detail of the noise, but also increase computation time." msgstr "จำนวนอ็อกเทฟของสัญญาณรบกวนที่สอดคล้องกันที่จะใช้ ค่าที่สูงกว่าจะเพิ่มรายละเอียดของสัญญาณรบกวน แต่ยังเพิ่มเวลาในการคำนวณด้วย" msgid "Fuzzy skin noise persistence" -msgstr "ความคงอยู่ของเสียงผิวเลือน" +msgstr "ค่า Persistence ของ Noise ใน Fuzzy Skin" msgid "The decay rate for higher octaves of the coherent noise. Lower values will result in smoother noise." msgstr "อัตราการสลายตัวของอ็อกเทฟที่สูงขึ้นของสัญญาณรบกวนที่สอดคล้องกัน ค่าที่ต่ำกว่าจะส่งผลให้มีสัญญาณรบกวนที่นุ่มนวลขึ้น" @@ -15737,7 +15737,7 @@ msgid "The start and end points which are from the cutter area to the excess chu msgstr "จุดเริ่มต้นและจุดสิ้นสุดตั้งแต่บริเวณเครื่องตัดถึงถังขยะ" msgid "Reduce infill retraction" -msgstr "ลดการหดตัวของ ไส้ใน" +msgstr "ลดการดึงกลับในไส้ใน" msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." msgstr "อย่าถอยกลับเมื่อการเดินทางอยู่ภายในพื้นที่ที่ไส้ในเข้าไปทั้งหมด นั่นหมายความว่าไม่สามารถมองเห็นการรั่วไหลได้ วิธีนี้จะช่วยลดเวลาในการดึงกลับสำหรับโมเดลที่ซับซ้อนและประหยัดเวลาในการพิมพ์ แต่จะทำให้การแบ่งส่วนและการสร้าง G-code ช้าลง โปรดทราบว่า z-hop จะไม่ดำเนินการในพื้นที่ที่มีการข้ามการถอนกลับ" @@ -15825,10 +15825,10 @@ msgid "If you want to process the output G-code through custom scripts, just lis msgstr "หากคุณต้องการประมวลผลเอาต์พุต G-code ผ่านสคริปต์ที่กำหนดเอง เพียงระบุเส้นทางสัมบูรณ์ของสคริปต์ไว้ที่นี่ แยกสคริปต์หลายรายการด้วยเครื่องหมายอัฒภาค สคริปต์จะถูกส่งผ่านเส้นทางสัมบูรณ์ไปยังไฟล์ G-code เป็นอาร์กิวเมนต์แรก และสคริปต์เหล่านี้สามารถเข้าถึงการตั้งค่าการกำหนดค่า Orca Slicer ได้โดยการอ่านตัวแปรสภาพแวดล้อม" msgid "Change extrusion role G-code (process)" -msgstr "เปลี่ยนบทบาทการอัดขึ้นรูป G-code (กระบวนการ)" +msgstr "เปลี่ยนประเภทการพิมพ์ G-code (กระบวนการ)" msgid "This G-code is inserted when the extrusion role is changed. It runs after the machine and filament extrusion role G-code." -msgstr "G-code นี้จะถูกแทรกเมื่อบทบาทการอัดขึ้นรูปมีการเปลี่ยนแปลง มันทำงานหลังจากบทบาทการอัดขึ้นรูปของเครื่องจักรและการอัดขึ้นรูปเส้นพลาสติก G-code" +msgstr "G-code นี้จะถูกแทรกเมื่อประเภทการพิมพ์มีการเปลี่ยนแปลง มันทำงานหลังจากประเภทการพิมพ์ของเครื่องจักรและเส้นพลาสติก G-code" # AI Translated msgid "Plugins Used" @@ -15897,7 +15897,7 @@ msgid "Only trigger retraction when the travel distance is longer than this thre msgstr "ทริกเกอร์การถอนกลับเมื่อระยะการเดินทางยาวกว่าเกณฑ์นี้เท่านั้น" msgid "Retract amount before wipe" -msgstr "ถอนจำนวนก่อนเช็ด" +msgstr "สัดส่วนการดึงกลับก่อน Wipe" msgid "This is the length of fast retraction before a wipe, relative to retraction length." msgstr "ความยาวของการดึงกลับอย่างรวดเร็วก่อนเช็ด สัมพันธ์กับความยาวการดึงกลับ" @@ -15916,7 +15916,7 @@ msgstr "" "ค่าจะถูกจำกัดด้วย 100% ลบด้วยปริมาณการดึงกลับก่อนค่าเช็ดหัว" msgid "Retract on layer change" -msgstr "ถอนออกเมื่อเปลี่ยนเลเยอร์" +msgstr "ดึงเส้นกลับเมื่อเปลี่ยนเลเยอร์" msgid "This forces a retraction on layer changes." msgstr "บังคับให้ถอนกลับเมื่อเปลี่ยนเลเยอร์" @@ -15925,7 +15925,7 @@ msgid "Retraction Length" msgstr "ระยะดึงกลับ" msgid "Some amount of material in extruder is pulled back to avoid ooze during long travel. Set zero to disable retraction." -msgstr "วัสดุบางส่วนในชุดดันเส้นถูกดึงกลับเพื่อหลีกเลี่ยงไม่ให้ซึ่มในระหว่างการเดินทางระยะไกล ตั้งค่าเป็นศูนย์เพื่อปิดใช้งานการเพิกถอน" +msgstr "วัสดุบางส่วนในชุดดันเส้นถูกดึงกลับเพื่อหลีกเลี่ยงไม่ให้ซึ่มในระหว่างการเดินทางระยะไกล ตั้งเป็น 0 เพื่อปิดการดึงกลับ" msgid "Long retraction when cut (beta)" msgstr "การถอยกลับยาวเมื่อตัด (เบต้า)" @@ -16049,7 +16049,7 @@ msgid "Speed for retracting filament from the nozzle." msgstr "ความเร็วในการดึงเส้นพลาสติกออกจากหัวฉีด" msgid "Deretraction speed" -msgstr "ความเร็วในการถอนกลับ" +msgstr "ความเร็วคืนเส้นหลังดึงกลับ" msgid "Speed for reloading filament into the nozzle. Zero means same speed of retraction." msgstr "ความเร็วในการบรรจุเส้นพลาสติกลงในหัวฉีด ศูนย์หมายถึงความเร็วการถอยกลับเท่ากัน" @@ -16063,7 +16063,7 @@ msgid "Speed for reloading filament into the nozzle when switching extruder." msgstr "ความเร็วในการโหลดเส้นพลาสติกกลับเข้าหัวฉีดเมื่อเปลี่ยนชุดดันเส้น" msgid "Use firmware retraction" -msgstr "ใช้การเพิกถอนเฟิร์มแวร์" +msgstr "ใช้การดึงกลับด้วยเฟิร์มแวร์" msgid "This experimental setting uses G10 and G11 commands to have the firmware handle the retraction. This is only supported in recent Marlin." msgstr "การตั้งค่าทดลองนี้ใช้คำสั่ง G10 และ G11 เพื่อให้เฟิร์มแวร์จัดการกับการเพิกถอน สิ่งนี้รองรับใน Marlin ล่าสุดเท่านั้น" @@ -16115,16 +16115,16 @@ msgstr "" "ปริมาณนี้สามารถระบุได้ในหน่วยมิลลิเมตรหรือเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของชุดดันเส้นในปัจจุบัน ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 10%" msgid "Scarf joint seam (beta)" -msgstr "รอยต่อเฉียง (เบต้า)" +msgstr "รอยต่อ Scarf (เบต้า)" msgid "Use scarf joint to minimize seam visibility and increase seam strength." -msgstr "ใช้ข้อต่อเฉียงเพื่อลดการมองเห็นรอยตะเข็บและเพิ่มความแข็งแรงของรอยตะเข็บ" +msgstr "ใช้รอยต่อ Scarf เพื่อลดการมองเห็นรอยตะเข็บและเพิ่มความแข็งแรง" msgid "Conditional scarf joint" -msgstr "ข้อต่อเฉียงแบบมีเงื่อนไข" +msgstr "รอยต่อ Scarf แบบมีเงื่อนไข" msgid "Apply scarf joints only to smooth perimeters where traditional seams do not conceal the seams at sharp corners effectively." -msgstr "ใช้ข้อต่อเฉียงกับขอบเรียบเท่านั้น โดยที่รอยตะเข็บแบบเดิมไม่สามารถปกปิดรอยตะเข็บที่มุมแหลมคมได้อย่างมีประสิทธิภาพ" +msgstr "ใช้รอยต่อ Scarf กับขอบเรียบเท่านั้น โดยที่รอยตะเข็บแบบเดิมไม่สามารถปกปิดรอยตะเข็บที่มุมแหลมคมได้อย่างมีประสิทธิภาพ" msgid "Conditional angle threshold" msgstr "เกณฑ์มุมแบบมีเงื่อนไข" @@ -16133,67 +16133,67 @@ msgid "" "This option sets the threshold angle for applying a conditional scarf joint seam.\n" "If the maximum angle within the perimeter loop exceeds this value (indicating the absence of sharp corners), a scarf joint seam will be used. The default value is 155°." msgstr "" -"ตัวเลือกนี้จะกำหนดมุมเกณฑ์สำหรับการใช้รอยตะเข็บข้อต่อเฉียงแบบมีเงื่อนไข\n" -"หากมุมสูงสุดภายในวงรอบปริมณฑลเกินค่านี้ (แสดงว่าไม่มีมุมแหลมคม) จะใช้รอยตะเข็บข้อต่อเฉียง ค่าเริ่มต้นคือ 155°" +"ตัวเลือกนี้จะกำหนดมุมเกณฑ์สำหรับการใช้รอยต่อ Scarf แบบมีเงื่อนไข\n" +"หากมุมสูงสุดภายในวงรอบปริมณฑลเกินค่านี้ (แสดงว่าไม่มีมุมแหลมคม) จะใช้รอยต่อ Scarf ค่าเริ่มต้นคือ 155°" msgid "Conditional overhang threshold" msgstr "เกณฑ์ระยะยื่นแบบมีเงื่อนไข" #, no-c-format, no-boost-format msgid "This option determines the overhang threshold for the application of scarf joint seams. If the unsupported portion of the perimeter is less than this threshold, scarf joint seams will be applied. The default threshold is set at 40% of the external wall's width. Due to performance considerations, the degree of overhang is estimated." -msgstr "ตัวเลือกนี้จะกำหนดเกณฑ์ส่วนยื่นสำหรับการใช้รอยตะเข็บข้อต่อเฉียง หากส่วนที่ไม่ได้รับส่วนรองรับของเส้นรอบวงน้อยกว่าเกณฑ์นี้ จะมีการเย็บรอยตะเข็บเฉียง เกณฑ์เริ่มต้นตั้งไว้ที่ 40% ของความกว้างของผนังภายนอก เมื่อพิจารณาถึงประสิทธิภาพแล้ว ระดับของระยะยื่นจึงถูกประมาณไว้" +msgstr "ตัวเลือกนี้จะกำหนดเกณฑ์ส่วนยื่นสำหรับการใช้รอยต่อ Scarf หากส่วนที่ไม่ได้รับส่วนรองรับของเส้นรอบวงน้อยกว่าเกณฑ์นี้ จะใช้รอยต่อ Scarf เกณฑ์เริ่มต้นตั้งไว้ที่ 40% ของความกว้างของผนังภายนอก เมื่อพิจารณาถึงประสิทธิภาพแล้ว ระดับของระยะยื่นจึงถูกประมาณไว้" msgid "Scarf joint speed" -msgstr "ความเร็วของข้อต่อเฉียง" +msgstr "ความเร็วรอยต่อ Scarf" msgid "This option sets the printing speed for scarf joints. It is recommended to print scarf joints at a slow speed (less than 100 mm/s). It's also advisable to enable 'Extrusion rate smoothing' if the set speed varies significantly from the speed of the outer or inner walls. If the speed specified here is higher than the speed of the outer or inner walls, the printer will default to the slower of the two speeds. When specified as a percentage (e.g., 80%), the speed is calculated based on the respective outer or inner wall speed. The default value is set to 100%." -msgstr "ตัวเลือกนี้จะตั้งค่าความเร็วในการพิมพ์สำหรับข้อต่อเฉียง ขอแนะนำให้พิมพ์ข้อต่อเฉียงด้วยความเร็วต่ำ (น้อยกว่า 100 มม./วินาที) ขอแนะนำให้เปิดใช้งาน 'การปรับอัตราการอัดรีดให้เรียบ' หากความเร็วที่ตั้งไว้แตกต่างอย่างมากจากความเร็วของผนังด้านนอกหรือด้านใน หากความเร็วที่ระบุที่นี่สูงกว่าความเร็วของผนังด้านนอกหรือด้านใน เครื่องพิมพ์จะตั้งค่าเริ่มต้นไว้ที่ความเร็วที่ช้ากว่าทั้งสอง เมื่อระบุเป็นเปอร์เซ็นต์ (เช่น 80%) ความเร็วจะคำนวณตามความเร็วผนังด้านนอกหรือด้านในตามลำดับ ค่าเริ่มต้นตั้งไว้ที่ 100%" +msgstr "ตัวเลือกนี้จะตั้งค่าความเร็วในการพิมพ์สำหรับรอยต่อ Scarf ขอแนะนำให้พิมพ์รอยต่อ Scarfด้วยความเร็วต่ำ (น้อยกว่า 100 มม./วินาที) ขอแนะนำให้เปิดใช้งาน 'การปรับอัตราการอัดรีดให้เรียบ' หากความเร็วที่ตั้งไว้แตกต่างอย่างมากจากความเร็วของผนังด้านนอกหรือด้านใน หากความเร็วที่ระบุที่นี่สูงกว่าความเร็วของผนังด้านนอกหรือด้านใน เครื่องพิมพ์จะตั้งค่าเริ่มต้นไว้ที่ความเร็วที่ช้ากว่าทั้งสอง เมื่อระบุเป็นเปอร์เซ็นต์ (เช่น 80%) ความเร็วจะคำนวณตามความเร็วผนังด้านนอกหรือด้านในตามลำดับ ค่าเริ่มต้นตั้งไว้ที่ 100%" msgid "Scarf joint flow ratio" -msgstr "อัตราการไหลของข้อต่อเฉียง" +msgstr "อัตราส่วนการไหลของรอยต่อ Scarf" msgid "This factor affects the amount of material for scarf joints." -msgstr "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับข้อต่อเฉียง" +msgstr "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับรอยต่อ Scarf" msgid "Scarf start height" -msgstr "ความสูงเริ่มต้นของเฉียง" +msgstr "ความสูงเริ่มต้นของรอยต่อ Scarf" msgid "" "Start height of the scarf.\n" "This amount can be specified in millimeters or as a percentage of the current layer height. The default value for this parameter is 0." msgstr "" -"เริ่มต้นความสูงของเฉียง\n" +"ความสูงเริ่มต้นของรอยต่อ Scarf\n" "จำนวนนี้สามารถระบุได้ในหน่วยมิลลิเมตรหรือเป็นเปอร์เซ็นต์ของความสูงของเลเยอร์ปัจจุบัน ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 0" msgid "Scarf around entire wall" -msgstr "เฉียงพันรอบผนังทั้งหมด" +msgstr "ใช้รอยต่อ Scarf ตลอดทั้งผนัง" msgid "The scarf extends to the entire length of the wall." -msgstr "เฉียงยาวตลอดความยาวของผนัง" +msgstr "รอยต่อ Scarf ครอบคลุมตลอดความยาวผนัง" msgid "Scarf length" -msgstr "ความยาวเฉียง" +msgstr "ความยาวรอยต่อ Scarf" msgid "Length of the scarf. Setting this parameter to zero effectively disables the scarf." -msgstr "ความยาวของเฉียง. การตั้งค่าพารามิเตอร์นี้เป็นศูนย์จะปิดใช้เฉียงอย่างมีประสิทธิภาพ" +msgstr "ความยาวของรอยต่อ Scarf ตั้งเป็น 0 เพื่อปิดการใช้ Scarf" msgid "Scarf steps" -msgstr "ขั้นตอนเฉียง" +msgstr "จำนวนขั้นของรอยต่อ Scarf" msgid "Minimum number of segments of each scarf." -msgstr "จำนวนขั้นต่ำของส่วนเฉียงแต่ละอัน" +msgstr "จำนวนเซกเมนต์ขั้นต่ำของรอยต่อ Scarf" msgid "Scarf joint for inner walls" -msgstr "ข้อต่อเฉียงสำหรับผนังด้านใน" +msgstr "รอยต่อ Scarf สำหรับผนังด้านใน" msgid "Use scarf joint for inner walls as well." -msgstr "ใช้ข้อต่อเฉียงสำหรับผนังด้านในด้วย" +msgstr "ใช้รอยต่อ Scarf กับผนังด้านในด้วย" msgid "Role base wipe speed" -msgstr "ความเร็วในการล้างฐานบทบาท" +msgstr "ความเร็ว Wipe ตามประเภทการพิมพ์" msgid "The wipe speed is determined by the speed of the current extrusion role. e.g. if a wipe action is executed immediately following an outer wall extrusion, the speed of the outer wall extrusion will be utilized for the wipe action." -msgstr "ความเร็วในการเช็ดถูกกำหนดโดยความเร็วของบทบาทการอัดขึ้นรูปในปัจจุบัน เช่น หากการดำเนินการเช็ดถูกดำเนินการทันทีหลังจากการอัดขึ้นรูปผนังด้านนอก ความเร็วของการอัดขึ้นรูปผนังด้านนอกจะถูกใช้สำหรับการดำเนินการเช็ด" +msgstr "ความเร็ว Wipe จะอิงจากความเร็วของประเภทการพิมพ์ปัจจุบัน เช่น หากการ Wipe เกิดขึ้นทันทีหลังจากพิมพ์ผนังด้านนอก ความเร็วของผนังด้านนอกจะถูกใช้สำหรับการ Wipe" msgid "Wipe on loops" msgstr "เช็ดบนลูป" @@ -16241,10 +16241,10 @@ msgid "Single loop after first layer" msgstr "วนรอบเดียวหลังจากชั้นแรก" msgid "Limits the skirt/draft shield loops to one wall after the first layer. This is useful, on occasion, to conserve filament but may cause the draft shield/skirt to warp / crack." -msgstr "จำกัดห่วงสเกิร์ต/โล่ครอบไว้ที่ผนังด้านหนึ่งหลังจากชั้นแรก สิ่งนี้มีประโยชน์ในบางครั้งเพื่ออนุรักษ์เส้นพลาสติก แต่อาจทำให้โครง/เส้นล้อมชิ้นงานบิดเบี้ยว/แตกร้าวได้" +msgstr "จำกัดห่วงสเกิร์ต/แนวป้องกันลม (Draft Shield) ไว้ที่ผนังเดียวหลังจากชั้นแรก สิ่งนี้มีประโยชน์ในบางครั้งเพื่อประหยัดเส้นพลาสติก แต่อาจทำให้แนวป้องกันลม/เส้นล้อมชิ้นงานบิดเบี้ยว/แตกร้าวได้" msgid "Draft shield" -msgstr "โล่ร่าง" +msgstr "แนวป้องกันลม (Draft Shield)" msgid "" "A draft shield is useful to protect an ABS or ASA print from warping and detaching from print bed due to wind draft. It is usually needed only with open frame printers, i.e. without an enclosure.\n" @@ -16252,10 +16252,10 @@ msgid "" "Enabled = skirt is as tall as the highest printed object. Otherwise 'Skirt height' is used.\n" "Note: With the draft shield active, the skirt will be printed at skirt distance from the object. Therefore, if brims are active it may intersect with them. To avoid this, increase the skirt distance value.\n" msgstr "" -"แผงครอบมีประโยชน์ในการปกป้องงานพิมพ์ ABS หรือ ASA จากการบิดงอและการหลุดออกจากฐานพิมพ์เนื่องจากกระแสลม โดยทั่วไปจำเป็นต้องใช้กับเครื่องพิมพ์แบบเปิดเฟรมเท่านั้น กล่าวคือ ไม่มีกล่องหุ้ม\n" +"แนวป้องกันลม (Draft Shield) มีประโยชน์ในการปกป้องงานพิมพ์ ABS หรือ ASA จากการบิดงอและการหลุดออกจากฐานพิมพ์เนื่องจากกระแสลม โดยทั่วไปจำเป็นต้องใช้กับเครื่องพิมพ์แบบเปิดเฟรมเท่านั้น กล่าวคือ ไม่มีกล่องหุ้ม\n" "\n" "Enabled = เส้นล้อมชิ้นงานสูงเท่ากับวัตถุที่พิมพ์สูงสุด มิฉะนั้น จะใช้ 'ความสูงของเส้นล้อมชิ้นงาน'\n" -"หมายเหตุ: เมื่อใช้งานดราฟชีลด์ เส้นล้อมชิ้นงานจะถูกพิมพ์ที่ระยะห่างจากเส้นล้อมชิ้นงานจากวัตถุ ดังนั้นหากขอบยึดชิ้นงานยังทำงานอยู่ ขอบยึดชิ้นงานอาจตัดกัน เพื่อหลีกเลี่ยงปัญหานี้ ให้เพิ่มค่าระยะห่างของเส้นล้อมชิ้นงาน\n" +"หมายเหตุ: เมื่อใช้งานแนวป้องกันลม (Draft Shield) เส้นล้อมชิ้นงานจะถูกพิมพ์ที่ระยะห่างจากวัตถุ ดังนั้นหากขอบยึดชิ้นงานยังทำงานอยู่ ขอบยึดชิ้นงานอาจตัดกัน เพื่อหลีกเลี่ยงปัญหานี้ ให้เพิ่มค่าระยะห่างของเส้นล้อมชิ้นงาน\n" msgid "Enabled" msgstr "เปิดใช้" @@ -16362,7 +16362,7 @@ msgid "Sets the finishing flow ratio while ending the spiral. Normally the spira msgstr "ตั้งค่าอัตราส่วนการไหลขั้นสุดท้ายขณะสิ้นสุดเกลียว โดยปกติการเปลี่ยนผ่านของเกลียวจะปรับขนาดอัตราส่วนการไหลจาก 100% เป็น 0% ในระหว่างลูปสุดท้าย ซึ่งในบางกรณีอาจนำไปสู่การรีดขึ้นรูปที่ปลายเกลียว" msgid "If smooth or traditional mode is selected, a timelapse video will be generated for each print. After each layer is printed, a snapshot is taken with the chamber camera. All of these snapshots are composed into a timelapse video when printing completes. If smooth mode is selected, the toolhead will move to the excess chute after each layer is printed and then take a snapshot. Since the melt filament may leak from the nozzle during the process of taking a snapshot, a prime tower is required for smooth mode to wipe the nozzle." -msgstr "หากเลือกโหมดเรียบหรือโหมดดั้งเดิม วิดีโอไทม์แลปส์จะถูกสร้างขึ้นสำหรับการพิมพ์แต่ละครั้ง หลังจากพิมพ์แต่ละชั้นแล้ว กล้องจะถ่ายภาพสแนปช็อตด้วยกล้องแชมเบอร์ สแน็ปช็อตทั้งหมดนี้จะถูกประกอบเป็นวิดีโอไทม์แลปส์เมื่อการพิมพ์เสร็จสิ้น หากเลือกโหมดเรียบ หัวเครื่องมือจะย้ายไปยังรางส่วนเกินหลังจากพิมพ์แต่ละเลเยอร์แล้วจึงถ่ายภาพสแน็ปช็อต เนื่องจากเส้นพลาสติกที่หลอมละลายอาจรั่วไหลออกจากหัวฉีดในระหว่างขั้นตอนการถ่ายภาพ จึงจำเป็นต้องมีไพรม์ทาวเวอร์เพื่อให้โหมดราบรื่นในการเช็ดหัวฉีด" +msgstr "หากเลือกโหมดเรียบหรือโหมดดั้งเดิม วิดีโอไทม์แลปส์จะถูกสร้างขึ้นสำหรับการพิมพ์แต่ละครั้ง หลังจากพิมพ์แต่ละชั้นแล้ว กล้องจะถ่ายภาพสแนปช็อตด้วยกล้องแชมเบอร์ สแน็ปช็อตทั้งหมดนี้จะถูกประกอบเป็นวิดีโอไทม์แลปส์เมื่อการพิมพ์เสร็จสิ้น หากเลือกโหมดเรียบ หัวเครื่องมือจะย้ายไปยังรางส่วนเกินหลังจากพิมพ์แต่ละเลเยอร์แล้วจึงถ่ายภาพสแน็ปช็อต เนื่องจากเส้นพลาสติกที่หลอมละลายอาจรั่วไหลออกจากหัวฉีดในระหว่างขั้นตอนการถ่ายภาพ จึงจำเป็นต้องมี Prime Tower เพื่อให้โหมดราบรื่นในการเช็ดหัวฉีด" msgid "Traditional" msgstr "แบบดั้งเดิม" @@ -16376,7 +16376,7 @@ msgstr "ไทม์แลปส์จุดไกลสุด" # AI Translated msgid "When enabled, the timelapse snapshot is taken at the farthest point from camera instead of traveling to the wipe tower or excess chute. Only effective in traditional timelapse mode on non-I3 printers." -msgstr "เมื่อเปิดใช้งาน ภาพไทม์แลปส์จะถูกถ่ายที่จุดไกลสุดจากกล้องแทนที่จะเดินหัวไปยังทาวเวอร์เช็ดหัวฉีดหรือช่องทิ้งส่วนเกิน มีผลเฉพาะในโหมดไทม์แลปส์แบบดั้งเดิมบนเครื่องพิมพ์ที่ไม่ใช่ I3" +msgstr "เมื่อเปิดใช้งาน ภาพไทม์แลปส์จะถูกถ่ายที่จุดไกลสุดจากกล้องแทนที่จะเดินหัวไปยัง Wipe Tower หรือช่องทิ้งส่วนเกิน มีผลเฉพาะในโหมดไทม์แลปส์แบบดั้งเดิมบนเครื่องพิมพ์ที่ไม่ใช่ I3" msgid "Temperature variation" msgstr "การเปลี่ยนแปลงของอุณหภูมิ" @@ -16425,10 +16425,10 @@ msgid "Enable this option to omit the custom Change filament G-code only at the msgstr "เปิดใช้งานตัวเลือกนี้เพื่อละเว้น G-code เปลี่ยนฟิลาเมนต์แบบกำหนดเองเฉพาะตอนเริ่มต้นการพิมพ์เท่านั้น คำสั่งเปลี่ยนเครื่องมือ (เช่น T0) จะถูกข้ามไปตลอดการพิมพ์ทั้งหมด สิ่งนี้มีประโยชน์สำหรับการพิมพ์หลายวัสดุด้วยตนเอง โดยที่เราใช้ M600/PAUSE เพื่อกระตุ้นการดำเนินการเปลี่ยนเส้นพลาสติกด้วยตนเอง" msgid "Wipe tower type" -msgstr "ชนิดทาวเวอร์เช็ด" +msgstr "ชนิด Wipe Tower" msgid "Choose the wipe tower implementation for multi-material prints. Type 1 is recommended for Bambu and Qidi printers with a filament cutter. Type 2 offers better compatibility with multi-tool and MMU printers and provide overall better compatibility." -msgstr "เลือกการใช้งานไวด์ทาวเวอร์สำหรับการพิมพ์แบบหลายวัสดุ แนะนำให้ใช้ประเภท 1 สำหรับเครื่องพิมพ์ Bambu และ Qidi ที่มีเครื่องตัดเส้นพลาสติก Type 2 ให้ความเข้ากันได้ที่ดีกว่ากับเครื่องพิมพ์หลายเครื่องมือและ MMU และให้ความเข้ากันได้โดยรวมดีขึ้น" +msgstr "เลือกการใช้งาน Wipe Tower สำหรับการพิมพ์แบบหลายวัสดุ แนะนำให้ใช้ประเภท 1 สำหรับเครื่องพิมพ์ Bambu และ Qidi ที่มีเครื่องตัดเส้นพลาสติก Type 2 ให้ความเข้ากันได้ที่ดีกว่ากับเครื่องพิมพ์หลายเครื่องมือและ MMU และให้ความเข้ากันได้โดยรวมดีขึ้น" msgid "Type 1" msgstr "ประเภทที่ 1" @@ -16437,25 +16437,25 @@ msgid "Type 2" msgstr "ประเภทที่ 2" msgid "Purge in prime tower" -msgstr "ระยะเว้นในไพร์มทาวเวอร์" +msgstr "ระยะเว้นใน Prime Tower" msgid "Purge remaining filament into prime tower." -msgstr "ล้างเส้นพลาสติกที่เหลือลงในไพร์มทาวเวอร์" +msgstr "ล้างเส้นพลาสติกที่เหลือลงใน Prime Tower" msgid "Enable filament ramming" msgstr "เปิดใช้งานการอัดกระแทกเส้นเส้นพลาสติก" msgid "Tool change on wipe tower" -msgstr "การเปลี่ยนเครื่องมือบนไวด์ทาวเวอร์" +msgstr "การเปลี่ยนเครื่องมือบน Wipe Tower" msgid "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." -msgstr "บังคับให้หัวเครื่องมือเคลื่อนที่ไปที่หอเช็ดก่อนที่จะออกคำสั่งเปลี่ยนเครื่องมือ (Tx) เกี่ยวข้องเฉพาะกับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่อง (หลายหัวเครื่องมือ) ที่ใช้แผ่นเช็ดแบบ Type 2 ตามค่าเริ่มต้น Orca จะข้ามการเดินทางบนเครื่องที่มีหัวเครื่องมือหลายหัวเนื่องจากเฟิร์มแวร์จัดการการสลับหัว ซึ่งอาจส่งผลให้มีการออกคำสั่ง Tx เหนือส่วนที่พิมพ์ เปิดใช้งานตัวเลือกนี้หากคุณต้องการให้ทำการเปลี่ยนแปลงเครื่องมือเหนือหอเช็ดแทนเสมอ" +msgstr "บังคับให้หัวเครื่องมือเคลื่อนที่ไปที่ Wipe Tower ก่อนที่จะออกคำสั่งเปลี่ยนเครื่องมือ (Tx) เกี่ยวข้องเฉพาะกับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่อง (หลายหัวเครื่องมือ) ที่ใช้แผ่นเช็ดแบบ Type 2 ตามค่าเริ่มต้น Orca จะข้ามการเดินทางบนเครื่องที่มีหัวเครื่องมือหลายหัวเนื่องจากเฟิร์มแวร์จัดการการสลับหัว ซึ่งอาจส่งผลให้มีการออกคำสั่ง Tx เหนือส่วนที่พิมพ์ เปิดใช้งานตัวเลือกนี้หากคุณต้องการให้ทำการเปลี่ยนแปลงเครื่องมือเหนือ Wipe Tower แทนเสมอ" msgid "No sparse layers (beta)" msgstr "ไม่มีชั้นกระจัดกระจาย (เบต้า)" msgid "If enabled, the wipe tower will not be printed on layers with no tool changes. On layers with a tool change, extruder will travel downward to print the wipe tower. User is responsible for ensuring there is no collision with the print." -msgstr "หากเปิดใช้งาน หอเช็ดจะไม่ถูกพิมพ์บนเลเยอร์โดยไม่มีการเปลี่ยนแปลงเครื่องมือ บนเลเยอร์ที่มีการเปลี่ยนเครื่องมือ ชุดดันเส้นจะเคลื่อนลงด้านล่างเพื่อพิมพ์ไวด์ทาวเวอร์ ผู้ใช้มีหน้าที่รับผิดชอบในการตรวจสอบให้แน่ใจว่าไม่มีการชนกันกับงานพิมพ์" +msgstr "หากเปิดใช้งาน Wipe Tower จะไม่ถูกพิมพ์บนเลเยอร์โดยไม่มีการเปลี่ยนแปลงเครื่องมือ บนเลเยอร์ที่มีการเปลี่ยนเครื่องมือ ชุดดันเส้นจะเคลื่อนลงด้านล่างเพื่อพิมพ์ Wipe Tower ผู้ใช้มีหน้าที่รับผิดชอบในการตรวจสอบให้แน่ใจว่าไม่มีการชนกันกับงานพิมพ์" msgid "Prime all printing extruders" msgstr "ใช้ชุดดันเส้นการพิมพ์ทั้งหมด" @@ -16720,7 +16720,7 @@ msgid "Independent support layer height" msgstr "ความสูงของชั้นรองรับอิสระ" msgid "Support layer uses layer height independent with object layer. This is to support customizing Z-gap and save print time. This option will be invalid when the prime tower is enabled." -msgstr "เลเยอร์ส่วนรองรับใช้ความสูงของเลเยอร์ที่เป็นอิสระจากเลเยอร์วัตถุ เพื่อรองรับการปรับแต่ง Z-gap และประหยัดเวลาในการพิมพ์ ตัวเลือกนี้จะไม่ถูกต้องเมื่อเปิดใช้งานไพรม์ทาวเวอร์" +msgstr "เลเยอร์ส่วนรองรับใช้ความสูงของเลเยอร์ที่เป็นอิสระจากเลเยอร์วัตถุ เพื่อรองรับการปรับแต่ง Z-gap และประหยัดเวลาในการพิมพ์ ตัวเลือกนี้จะไม่ถูกต้องเมื่อเปิดใช้งาน Prime Tower" msgid "Threshold angle" msgstr "มุมเกณฑ์" @@ -16885,13 +16885,13 @@ msgid "This G-code is inserted when filament is changed, including T commands to msgstr "รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนเส้นพลาสติก รวมถึงคำสั่ง T เพื่อกระตุ้นการเปลี่ยนเครื่องมือ" msgid "This G-code is inserted when the extrusion role is changed." -msgstr "G-code นี้จะถูกแทรกเมื่อบทบาทการอัดขึ้นรูปมีการเปลี่ยนแปลง" +msgstr "G-code นี้จะถูกแทรกเมื่อประเภทการพิมพ์มีการเปลี่ยนแปลง" msgid "Change extrusion role G-code (filament)" -msgstr "เปลี่ยนบทบาทการอัดขึ้นรูป G-code (เส้นพลาสติก)" +msgstr "เปลี่ยนประเภทการพิมพ์ G-code (เส้นพลาสติก)" msgid "This G-code is inserted when the extrusion role is changed for the active filament." -msgstr "รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนบทบาทการอัดขึ้นรูปสำหรับเส้นพลาสติกที่ใช้งานอยู่" +msgstr "รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนประเภทการพิมพ์สำหรับเส้นพลาสติกที่ใช้งานอยู่" msgid "Line width for top surfaces. If expressed as a %, it will be computed over the nozzle diameter." msgstr "ความกว้างของเส้นสำหรับพื้นผิวด้านบน หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" @@ -16981,13 +16981,13 @@ msgstr "" "การตั้งค่าในจำนวนการถอนก่อนการล้างการตั้งค่าด้านล่างจะทำการถอนส่วนที่เกินก่อนการล้าง มิฉะนั้นจะดำเนินการหลังจากนั้น" msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." -msgstr "หอเช็ดสามารถใช้เพื่อทำความสะอาดสิ่งตกค้างบนหัวฉีด และทำให้แรงดันในห้องภายในหัวฉีดคงที่ เพื่อหลีกเลี่ยงข้อบกพร่องในลักษณะที่ปรากฏเมื่อพิมพ์วัตถุ" +msgstr "Wipe Tower สามารถใช้เพื่อทำความสะอาดสิ่งตกค้างบนหัวฉีด และทำให้แรงดันในห้องภายในหัวฉีดคงที่ เพื่อหลีกเลี่ยงข้อบกพร่องในลักษณะที่ปรากฏเมื่อพิมพ์วัตถุ" msgid "Internal ribs" -msgstr "ซี่โครงภายใน" +msgstr "ครีบเสริมภายใน" msgid "Enable internal ribs to increase the stability of the prime tower." -msgstr "เปิดใช้งานซี่โครงภายในเพื่อเพิ่มความมั่นคงของหอคอยหลัก" +msgstr "เปิดใช้ครีบเสริมภายในเพื่อเพิ่มความมั่นคงของ Prime Tower" msgid "Purging volumes" msgstr "ปริมาตรการไล่เส้น" @@ -17007,10 +17007,10 @@ msgid "The flush multiplier used in fast purge mode." msgstr "ตัวคูณการไล่เส้นที่ใช้ในโหมดไล่เส้นเร็ว" msgid "Prime volume" -msgstr "ปริมาณเฉพาะ" +msgstr "ปริมาตร Prime" msgid "This is the volume of material to prime the extruder with on the tower." -msgstr "ปริมาตรวัสดุสำหรับเตรียมหัวฉีดบนไพรม์ทาวเวอร์" +msgstr "ปริมาตรวัสดุสำหรับเตรียมหัวฉีดบน Prime Tower" # AI Translated msgid "Prime volume mode" @@ -17018,7 +17018,7 @@ msgstr "โหมดปริมาณการไพรม์" # AI Translated msgid "Selects how the wipe-tower prime and flush volumes are computed on multi-extruder printers." -msgstr "เลือกวิธีการคำนวณปริมาณการไพรม์และการไล่เส้นของทาวเวอร์เช็ดหัวฉีดบนเครื่องพิมพ์แบบหลายชุดดันเส้น" +msgstr "เลือกวิธีการคำนวณปริมาณการไพรม์และการไล่เส้นของ Wipe Tower บนเครื่องพิมพ์แบบหลายชุดดันเส้น" # AI Translated msgid "Saving" @@ -17029,25 +17029,25 @@ msgid "Fast" msgstr "เร็ว" msgid "This is the width of prime towers." -msgstr "ความกว้างของไพรม์ทาวเวอร์" +msgstr "ความกว้างของ Prime Tower" msgid "Wipe tower rotation angle" -msgstr "เช็ดมุมการหมุนของทาวเวอร์" +msgstr "มุมหมุนของ Wipe Tower" msgid "Wipe tower rotation angle with respect to X axis." -msgstr "เช็ดมุมการหมุนของทาวเวอร์ตามแกน X" +msgstr "มุมหมุนของ Wipe Tower เทียบกับแกน X" msgid "Brim width of prime tower, negative number means auto calculated width based on the height of prime tower." -msgstr "ความกว้างขอบของหอคอยหลัก ตัวเลขติดลบหมายถึงความกว้างที่คำนวณโดยอัตโนมัติตามความสูงของหอคอยหลัก" +msgstr "ความกว้างขอบของ Prime Tower ตัวเลขติดลบหมายถึงความกว้างที่คำนวณโดยอัตโนมัติตามความสูงของ Prime Tower" msgid "Stabilization cone apex angle" msgstr "มุมเอเพ็กซ์ของกรวยป้องกันการสั่นไหว" msgid "Angle at the apex of the cone that is used to stabilize the wipe tower. Larger angle means wider base." -msgstr "มุมที่ปลายกรวยที่ใช้เพื่อรักษาเสถียรภาพของหอเช็ด มุมที่ใหญ่ขึ้นหมายถึงฐานที่กว้างขึ้น" +msgstr "มุมที่ปลายกรวยที่ใช้เพื่อรักษาเสถียรภาพของ Wipe Tower มุมที่ใหญ่ขึ้นหมายถึงฐานที่กว้างขึ้น" msgid "Maximum wipe tower print speed" -msgstr "ความเร็วการพิมพ์ไวต์ทาวเวอร์สูงสุด" +msgstr "ความเร็วการพิมพ์ Wipe Tower สูงสุด" msgid "" "The maximum print speed when purging in the wipe tower and printing the wipe tower sparse layers. When purging, if the sparse infill speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n" @@ -17064,11 +17064,11 @@ msgstr "" "\n" "เมื่อพิมพ์ชั้นเบาบาง หากความเร็วเส้นรอบวงภายในหรือความเร็วที่คำนวณจากความเร็วปริมาตรสูงสุดของเส้นพลาสติกต่ำกว่า ความเร็วต่ำสุดจะถูกนำมาใช้แทน\n" "\n" -"การเพิ่มความเร็วนี้อาจส่งผลต่อเสถียรภาพของทาวเวอร์ รวมทั้งเพิ่มแรงที่หัวฉีดชนกับหยดใดๆ ที่อาจก่อตัวบนทาวเวอร์เช็ด\n" +"การเพิ่มความเร็วนี้อาจส่งผลต่อเสถียรภาพของทาวเวอร์ รวมทั้งเพิ่มแรงที่หัวฉีดชนกับหยดใดๆ ที่อาจก่อตัวบน Wipe Tower\n" "\n" "ก่อนที่จะเพิ่มพารามิเตอร์นี้เกินกว่าค่าเริ่มต้นที่ 90 มม./วินาที ตรวจสอบให้แน่ใจว่าเครื่องพิมพ์ของคุณสามารถเชื่อมต่อที่ความเร็วที่เพิ่มขึ้นได้อย่างน่าเชื่อถือ และจะมีการควบคุมอย่างดีเมื่อเปลี่ยนเครื่องมือ\n" "\n" -"สำหรับปริมณฑลภายนอกของไวต์ทาวเวอร์ ความเร็วของปริมณฑลภายในจะถูกใช้โดยไม่คำนึงถึงการตั้งค่านี้" +"สำหรับปริมณฑลภายนอกของ Wipe Tower ความเร็วของปริมณฑลภายในจะถูกใช้โดยไม่คำนึงถึงการตั้งค่านี้" msgid "Wall type" msgstr "ชนิดติดผนัง" @@ -17079,10 +17079,10 @@ msgid "" "2. Cone: A cone with a fillet at the bottom to help stabilize the wipe tower.\n" "3. Rib: Adds four ribs to the tower wall for enhanced stability." msgstr "" -"เช็ดทาวเวอร์ชนิดผนังด้านนอก\n" +"Wipe Tower ชนิดผนังด้านนอก\n" "1. สี่เหลี่ยมผืนผ้า: ประเภทผนังเริ่มต้น ซึ่งเป็นสี่เหลี่ยมผืนผ้าที่มีความกว้างและความสูงคงที่\n" -"2. กรวย: กรวยที่มีเนื้ออยู่ด้านล่างเพื่อช่วยรักษาเสถียรภาพของหอเช็ด\n" -"3. ซี่โครง: เพิ่มสี่ซี่โครงเข้ากับผนังหอคอยเพื่อเพิ่มความมั่นคง" +"2. กรวย: กรวยที่มีเนื้ออยู่ด้านล่างเพื่อช่วยรักษาเสถียรภาพของ Wipe Tower\n" +"3. ซี่โครง: เพิ่มสี่ซี่โครงเข้ากับผนัง Wipe Tower เพื่อเพิ่มความมั่นคง" msgid "Rectangle" msgstr "สี่เหลี่ยมผืนผ้า" @@ -17100,40 +17100,40 @@ msgid "Rib width" msgstr "ความกว้างของซี่โครง" msgid "Rib width is always less than half the prime tower side length." -msgstr "ความกว้างของซี่โครงจะน้อยกว่าครึ่งหนึ่งของความยาวด้านของไพรม์ทาวเวอร์เสมอ" +msgstr "ความกว้างของซี่โครงจะน้อยกว่าครึ่งหนึ่งของความยาวด้านของ Prime Tower เสมอ" msgid "Fillet wall" msgstr "ผนังเนื้อ" msgid "The wall of prime tower will fillet." -msgstr "ผนังของไพร์มทาวเวอร์จะแล่เป็นเนื้อเดียวกัน" +msgstr "ผนังของ Prime Tower จะแล่เป็นเนื้อเดียวกัน" msgid "The extruder to use when printing perimeter of the wipe tower. Set to 0 to use the one that is available (non-soluble would be preferred)." -msgstr "ชุดดันเส้นที่จะใช้ในการพิมพ์ปริมณฑลของหอเช็ด ตั้งค่าเป็น 0 เพื่อใช้อันที่มีอยู่ (แนะนำให้ใช้แบบไม่ละลายน้ำ)" +msgstr "ชุดดันเส้นที่จะใช้ในการพิมพ์ปริมณฑลของ Wipe Tower ตั้งค่าเป็น 0 เพื่อใช้อันที่มีอยู่ (แนะนำให้ใช้แบบไม่ละลายน้ำ)" msgid "Purging volumes - load/unload volumes" msgstr "การล้างไดรฟ์ข้อมูล - โหลด/ยกเลิกการโหลดไดรฟ์ข้อมูล" msgid "This vector saves required volumes to change from/to each tool used on the wipe tower. These values are used to simplify creation of the full purging volumes below." -msgstr "เวกเตอร์นี้จะบันทึกปริมาณที่ต้องการเพื่อเปลี่ยนจาก/ไปยังแต่ละเครื่องมือที่ใช้บนไวด์ทาวเวอร์ ค่าเหล่านี้ใช้เพื่อทำให้การสร้างวอลุ่มการล้างข้อมูลทั้งหมดด้านล่างง่ายขึ้น" +msgstr "เวกเตอร์นี้จะบันทึกปริมาณที่ต้องการเพื่อเปลี่ยนจาก/ไปยังแต่ละเครื่องมือที่ใช้บน Wipe Tower ค่าเหล่านี้ใช้เพื่อทำให้การสร้างวอลุ่มการล้างข้อมูลทั้งหมดด้านล่างง่ายขึ้น" msgid "Skip points" msgstr "ข้ามจุด" msgid "The wall of prime tower will skip the start points of wipe path." -msgstr "ผนังของไพร์มทาวเวอร์จะข้ามจุดเริ่มต้นของเส้นทางการเช็ด" +msgstr "ผนังของ Prime Tower จะข้ามจุดเริ่มต้นของเส้นทางการเช็ด" msgid "Enable tower interface features" msgstr "เปิดใช้งานคุณสมบัติอินเทอร์เฟซแบบทาวเวอร์" msgid "Enable optimized prime tower interface behavior when different materials meet." -msgstr "เปิดใช้งานพฤติกรรมอินเทอร์เฟซของไพรม์ทาวเวอร์ที่ได้รับการปรับให้เหมาะสมเมื่อวัสดุที่แตกต่างกันมาบรรจบกัน" +msgstr "เปิดใช้งานพฤติกรรมอินเทอร์เฟซของ Prime Tower ที่ได้รับการปรับให้เหมาะสมเมื่อวัสดุที่แตกต่างกันมาบรรจบกัน" msgid "Cool down from interface boost during prime tower" -msgstr "เย็นลงจากการเพิ่มอินเทอร์เฟซระหว่างหอคอยหลัก" +msgstr "การระบายความร้อนที่เลเยอร์อินเทอร์เฟซของ Prime Tower" msgid "When interface-layer temperature boost is active, set the nozzle back to print temperature at the start of the prime tower so it cools down during the tower." -msgstr "เมื่อเปิดใช้งานการเพิ่มอุณหภูมิของชั้นอินเทอร์เฟซ ให้ตั้งค่าหัวฉีดกลับไปเป็นอุณหภูมิการพิมพ์ที่จุดเริ่มต้นของไพรม์ทาวเวอร์ เพื่อให้เย็นลงระหว่างทาวเวอร์" +msgstr "เมื่อเปิดใช้งานการเพิ่มอุณหภูมิของชั้นอินเทอร์เฟซ ให้ตั้งค่าหัวฉีดกลับไปเป็นอุณหภูมิการพิมพ์ที่จุดเริ่มต้นของ Prime Tower เพื่อให้เย็นลงระหว่าง Prime Tower" msgid "Infill gap" msgstr "การเติมช่องว่าง" @@ -17142,13 +17142,13 @@ msgid "Infill gap." msgstr "การเติมช่องว่าง." msgid "Purging after filament change will be done inside objects' infills. This may lower the amount of waste and decrease the print time. If the walls are printed with transparent filament, the mixed color infill will be visible. It will not take effect unless the prime tower is enabled." -msgstr "การล้างหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนไส้ในของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ หากผนังพิมพ์ด้วยเส้นพลาสติกโปร่งใส จะเห็นไส้ในสีผสมไว้ด้านนอก มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgstr "การล้างหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนไส้ในของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ หากผนังพิมพ์ด้วยเส้นพลาสติกโปร่งใส จะเห็นไส้ในสีผสมไว้ด้านนอก มันจะไม่มีผลเว้นแต่จะเปิดใช้งาน Prime Tower" msgid "Purging after filament change will be done inside objects' support. This may lower the amount of waste and decrease the print time. It will not take effect unless a prime tower is enabled." -msgstr "การล้างข้อมูลหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนรองรับของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgstr "การล้างข้อมูลหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนรองรับของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ มันจะไม่มีผลเว้นแต่จะเปิดใช้งาน Prime Tower" msgid "This object will be used to purge the nozzle after a filament change to save filament and decrease the print time. Colors of the objects will be mixed as a result. It will not take effect unless the prime tower is enabled." -msgstr "วัตถุนี้จะใช้ในการล้างหัวฉีดหลังจากเปลี่ยนเส้นพลาสติกเพื่อประหยัดเส้นพลาสติกและลดเวลาในการพิมพ์ สีของวัตถุจะผสมกัน มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgstr "วัตถุนี้จะใช้ในการล้างหัวฉีดหลังจากเปลี่ยนเส้นพลาสติกเพื่อประหยัดเส้นพลาสติกและลดเวลาในการพิมพ์ สีของวัตถุจะผสมกัน มันจะไม่มีผลเว้นแต่จะเปิดใช้งาน Prime Tower" msgid "Maximal bridging distance" msgstr "ระยะเชื่อมต่อสูงสุด" @@ -17157,16 +17157,16 @@ msgid "Maximal distance between supports on sparse infill sections." msgstr "ระยะห่างสูงสุดระหว่างส่วนรองรับในส่วน ไส้ใน แบบกระจัดกระจาย" msgid "Wipe tower purge lines spacing" -msgstr "เช็ดระยะห่างบรรทัดล้างทาวเวอร์" +msgstr "ระยะห่างเส้นไล่พลาสติกของ Wipe Tower" msgid "Spacing of purge lines on the wipe tower." -msgstr "ระยะห่างของเส้นไล่ล้างบนหอเช็ด" +msgstr "ระยะห่างของเส้นไล่ล้างบน Wipe Tower" msgid "Extra flow for purging" msgstr "กระแสพิเศษสำหรับการล้าง" msgid "Extra flow used for the purging lines on the wipe tower. This makes the purging lines thicker or narrower than they normally would be. The spacing is adjusted automatically." -msgstr "การไหลพิเศษที่ใช้สำหรับท่อไล่ล้างบนหอเช็ด ซึ่งจะทำให้เส้นการล้างหนาหรือแคบกว่าปกติ ระยะห่างจะถูกปรับโดยอัตโนมัติ" +msgstr "การไหลพิเศษที่ใช้สำหรับท่อไล่ล้างบน Wipe Tower ซึ่งจะทำให้เส้นการล้างหนาหรือแคบกว่าปกติ ระยะห่างจะถูกปรับโดยอัตโนมัติ" msgid "Idle temperature" msgstr "อุณหภูมิว่าง" @@ -17748,10 +17748,10 @@ msgid "Specific for sequential printing. Zero-based index of currently printed o msgstr "เฉพาะสำหรับการพิมพ์ตามลำดับ ดัชนีแบบศูนย์ของวัตถุที่พิมพ์ในปัจจุบัน" msgid "Has wipe tower" -msgstr "มีหอเช็ด" +msgstr "มี Wipe Tower" msgid "Whether or not wipe tower is being generated in the print." -msgstr "มีการสร้างเช็ดทาวเวอร์ในการพิมพ์หรือไม่" +msgstr "มีการสร้าง Wipe Tower ในการพิมพ์หรือไม่" msgid "Initial extruder" msgstr "ชุดดันเส้นเริ่มต้น" @@ -17838,16 +17838,16 @@ msgid "Total cost of all material used in the print. Calculated from filament_co msgstr "ต้นทุนรวมของวัสดุทั้งหมดที่ใช้ในการพิมพ์ คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" msgid "Total wipe tower cost" -msgstr "ต้นทุนเช็ดทาวเวอร์ทั้งหมด" +msgstr "ต้นทุน Wipe Tower ทั้งหมด" msgid "Total cost of the material wasted on the wipe tower. Calculated from filament_cost value in Filament Settings." -msgstr "ต้นทุนรวมของวัสดุที่เสียไปบนไวด์ทาวเวอร์ คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" +msgstr "ต้นทุนรวมของวัสดุที่เสียไปบน Wipe Tower คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" msgid "Wipe tower volume" msgstr "เช็ดปริมาตรทาวเวอร์" msgid "Total filament volume extruded on the wipe tower." -msgstr "ปริมาตรเส้นพลาสติกทั้งหมดที่อัดบนไวด์ทาวเวอร์" +msgstr "ปริมาตรเส้นพลาสติกทั้งหมดที่อัดบน Wipe Tower" msgid "Used filament" msgstr "เส้นพลาสติกที่ใช้แล้ว" @@ -18045,8 +18045,8 @@ msgid "" "An object has enabled XY Size compensation which will not be used because it is also fuzzy skin painted.\n" "XY Size compensation cannot be combined with fuzzy skin painting." msgstr "" -"วัตถุได้เปิดใช้งานการชดเชยขนาด XY ซึ่งจะไม่ถูกใช้เนื่องจากเป็นสีที่ไม่ชัดเจนเช่นกัน\n" -"การชดเชยขนาด XY ไม่สามารถใช้ร่วมกับการลงสีผิวแบบคลุมเครือได้" +"วัตถุได้เปิดใช้งานการชดเชยขนาด XY ซึ่งจะไม่ถูกใช้เนื่องจากถูกระบายสีผิวฟัซซีไว้เช่นกัน\n" +"การชดเชยขนาด XY ไม่สามารถใช้ร่วมกับการระบายสีผิวฟัซซีได้" msgid "Object name" msgstr "ชื่อออบเจ็กต์" @@ -20591,7 +20591,7 @@ msgid "Auto-generate" msgstr "สร้างอัตโนมัติ" msgid "Generate brim ears using Max angle and Detection radius" -msgstr "สร้างหูขอบยึดชิ้นงานนกโดยใช้มุมสูงสุดและรัศมีการตรวจจับ" +msgstr "สร้างหูขอบยึดชิ้นงาน (Brim Ears) โดยใช้มุมสูงสุดและรัศมีการตรวจจับ" msgid "Add or Select" msgstr "เพิ่มหรือเลือก" @@ -20606,7 +20606,7 @@ msgid "invalid brim ears" msgstr "หูขอบยึดชิ้นงานไม่ถูกต้อง" msgid "Brim Ears" -msgstr "หูขอบยึดชิ้นงาน" +msgstr "หูขอบยึดชิ้นงาน (Brim Ears)" msgid "Please select single object." msgstr "กรุณาเลือกวัตถุเดียว" @@ -21572,7 +21572,7 @@ msgstr "" #~ msgstr "เนื้อหาที่ตั้งไว้ล่วงหน้ามีขนาดใหญ่เกินกว่าจะซิงค์กับระบบคลาวด์ (เกิน 1MB) โปรดลดขนาดที่กำหนดไว้ล่วงหน้าโดยการลบการกำหนดค่าที่กำหนดเองออกหรือใช้เฉพาะในเครื่องเท่านั้น" #~ msgid "Enable adaptive pressure advance for overhangs (beta)" -#~ msgstr "เปิดใช้งานการปรับPressure Advanceสำหรับระยะยื่น (เบต้า)" +#~ msgstr "เปิดใช้ Adaptive Pressure Advance สำหรับส่วนยื่น (เบต้า)" #~ msgid "" #~ "Enable adaptive PA for overhangs as well as when flow changes within the same feature. This is an experimental option, as if the PA profile is not set accurately, it will cause uniformity issues on the external surfaces before and after overhangs.\n" @@ -21582,7 +21582,7 @@ msgstr "" #~ "ไม่รองรับเครื่องพิมพ์ Prusa เพราะจะหยุดชั่วคราวเพื่อประมวลผลการเปลี่ยน PA ทำให้เกิดความล่าช้าและข้อบกพร่อง" #~ msgid "Pressure advance for bridges" -#~ msgstr "แรงดันล่วงหน้า (Pressure Advance)สำหรับสะพาน" +#~ msgstr "Pressure Advance สำหรับสะพาน" #~ msgid "" #~ "Pressure advance value for bridges. Set to 0 to disable.\n" From e9d421050e0eff618c0c2c5abb5869d91bfa4081 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 12 Aug 2026 18:50:49 +0800 Subject: [PATCH 37/71] refactor: access codes and device tab (#15134) --- src/slic3r/GUI/ConnectPrinter.cpp | 4 ++- src/slic3r/GUI/DeviceCore/DevManager.cpp | 19 +++++++++--- src/slic3r/GUI/DeviceManager.cpp | 36 +--------------------- src/slic3r/GUI/DeviceManager.hpp | 6 ---- src/slic3r/GUI/GUI_App.cpp | 4 +-- src/slic3r/GUI/MainFrame.cpp | 24 +++++++++++---- src/slic3r/GUI/Plater.cpp | 13 ++++++-- src/slic3r/GUI/ReleaseNote.cpp | 9 ++++-- src/slic3r/GUI/SelectMachinePop.cpp | 1 - src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 1 - 10 files changed, 54 insertions(+), 63 deletions(-) diff --git a/src/slic3r/GUI/ConnectPrinter.cpp b/src/slic3r/GUI/ConnectPrinter.cpp index b4cd7f4f2f..3e78e7fe5c 100644 --- a/src/slic3r/GUI/ConnectPrinter.cpp +++ b/src/slic3r/GUI/ConnectPrinter.cpp @@ -156,6 +156,8 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt) void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) { wxString code = m_textCtrl_code->GetTextCtrl()->GetValue(); + if (code.empty()) + code = "88888888"; for (char c : code) { if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { show_error(this, _L("Invalid input")); @@ -163,7 +165,7 @@ void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) } } if (m_obj) { - m_obj->set_user_access_code(code.ToStdString()); + m_obj->set_access_code(code.ToStdString()); } EndModal(wxID_OK); } diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index d13f8b7215..edc958ec53 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -15,6 +15,18 @@ using namespace nlohmann; +namespace { + // Orca: access_code and user_access_code used to be separate AppConfig keys before the two + // fields were merged; fall back to the legacy key so existing users' saved codes aren't lost. + std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id) + { + std::string code = config->get("access_code", dev_id); + if (code.empty()) + code = config->get("user_access_code", dev_id); + return code; + } +} + namespace Slic3r { DeviceManager::DeviceManager(NetworkAgent* agent) @@ -48,8 +60,7 @@ namespace Slic3r obj->bind_sec_link = "secure"; obj->m_is_online = true; obj->last_alive = Slic3r::Utils::get_current_time_utc(); - obj->set_access_code(config->get("access_code", m.dev_id), false); - obj->set_user_access_code(config->get("user_access_code", m.dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id), false); if (obj->has_access_right()) { localMachineList.insert(std::make_pair(m.dev_id, obj)); } else { @@ -339,8 +350,7 @@ namespace Slic3r //load access code AppConfig* config = Slic3r::GUI::wxGetApp().app_config; if (config) { - obj->set_access_code(Slic3r::GUI::wxGetApp().app_config->get("access_code", dev_id), false); - obj->set_user_access_code(Slic3r::GUI::wxGetApp().app_config->get("user_access_code", dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id), false); } localMachineList.insert(std::make_pair(dev_id, obj)); @@ -382,7 +392,6 @@ namespace Slic3r obj->m_is_online = true; obj->last_alive = Slic3r::Utils::get_current_time_utc(); obj->set_access_code(access_code, false); - obj->set_user_access_code(access_code, false); update_local_machine(*obj); diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index ef85870461..f4befe78c1 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -449,9 +449,7 @@ bool MachineObject::HasRecentLanMessage() std::string MachineObject::get_access_code() const { - if (get_user_access_code().empty()) - return access_code; - return get_user_access_code(); + return access_code; } void MachineObject::set_access_code(std::string code, bool only_refresh) @@ -470,37 +468,6 @@ void MachineObject::set_access_code(std::string code, bool only_refresh) } } -void MachineObject::erase_user_access_code() -{ - this->user_access_code = ""; - AppConfig* config = GUI::wxGetApp().app_config; - if (config) { - GUI::wxGetApp().app_config->erase("user_access_code", get_dev_id()); - //GUI::wxGetApp().app_config->save(); - } -} - -void MachineObject::set_user_access_code(std::string code, bool only_refresh) -{ - this->user_access_code = code; - if (only_refresh && !code.empty()) { - AppConfig* config = GUI::wxGetApp().app_config; - if (config && !code.empty()) { - GUI::wxGetApp().app_config->set_str("user_access_code", get_dev_id(), code); - DeviceManager::update_local_machine(*this); - } - } -} - -std::string MachineObject::get_user_access_code() const -{ - AppConfig* config = GUI::wxGetApp().app_config; - if (config) { - return GUI::wxGetApp().app_config->get("user_access_code", get_dev_id()); - } - return ""; -} - std::string MachineObject::get_show_printer_type() const { std::string printer_type = this->printer_type; @@ -2907,7 +2874,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ std::string access_code = j_pre["system"]["access_code"].get(); if (!access_code.empty()) { set_access_code(access_code); - set_user_access_code(access_code); } } } diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 2790e37cfa..33635fbe6e 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -113,7 +113,6 @@ private: std::string dev_name; std::string dev_ip; std::string access_code; - std::string user_access_code; // type, time stamp, delay std::vector> message_delay; @@ -228,11 +227,6 @@ public: std::string get_access_code() const; void set_access_code(std::string code, bool only_refresh = true); - /*user access code*/ - void set_user_access_code(std::string code, bool only_refresh = true); - void erase_user_access_code(); - std::string get_user_access_code() const; - //PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN; std::string printer_type; /* model_id */ std::string get_show_printer_type() const; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 14edeb8038..db51bd9d8e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2166,7 +2166,6 @@ void GUI_App::init_networking_callbacks() obj->is_tunnel_mqtt = tunnel; obj->command_request_push_all(true); obj->command_get_version(); - obj->erase_user_access_code(); obj->command_get_access_code(); if (m_agent) m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer()); @@ -2216,7 +2215,6 @@ void GUI_App::init_networking_callbacks() wxString text; if (msg == "5") { obj->set_access_code(""); - obj->erase_user_access_code(); text = wxString::Format(_L("Incorrect password")); wxGetApp().show_dialog(text); } else { @@ -8286,7 +8284,7 @@ bool GUI_App::show_modal_ip_address_enter_dialog(bool input_sn, wxString title) wxGetApp().app_config->save(); obj->set_dev_ip(ip_address.ToStdString()); - obj->set_user_access_code(access_code.ToStdString()); + obj->set_access_code(access_code.ToStdString()); } } }); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 5ef81a32e1..5a0e70b74c 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1373,8 +1373,8 @@ void MainFrame::show_device(bool should_use_native) { const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); - // The legacy page is appended when printer agents are enabled. Remove that - // extra page before switching back to the normal native/legacy layout. + // The web page is appended when printer agents are enabled. Remove that + // extra page before switching back to the normal native/Web layout. if (!use_printer_agents) { if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) { m_printer_view->Show(false); @@ -1434,10 +1434,10 @@ void MainFrame::show_device(bool should_use_native) { if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { m_printer_view->Show(false); - m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), std::string("tab_monitor_active"), + m_tabpanel->AddPage(m_printer_view, _L("Device (Web)"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); } else { - m_tabpanel->SetPageText(idx, _L("Device (legacy)")); + m_tabpanel->SetPageText(idx, _L("Device (Web)")); } #ifdef _MSW_DARK_MODE @@ -4333,14 +4333,26 @@ void MainFrame::load_printer_url(wxString url, wxString apikey) void MainFrame::load_printer_url() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents")) + if (preset_bundle.use_bbl_device_tab() && !wxGetApp().app_config->get_bool("use_printer_agents")) return; auto cfg = preset_bundle.printers.get_edited_preset().config; + if (cfg.opt_string("print_host").empty()) { + if (auto *device_manager = wxGetApp().getDeviceManager()) { + auto *machine = device_manager->get_selected_machine(); + if (!machine) { + auto machines = device_manager->get_my_machine_list(); + if (machines.size() == 1) + machine = machines.begin()->second; + } + if (machine && !machine->get_dev_ip().empty()) + cfg.opt_string("print_host") = machine->get_dev_ip(); + } + } wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; const auto host_type = cfg.option>("host_type")->value; - if (cfg.has("printhost_apikey") && (host_type == htPrusaLink || host_type == htPrusaConnect)) + if (cfg.has("printhost_apikey") && host_type != htSimplyPrint) apikey = cfg.opt_string("printhost_apikey"); if (!url.empty()) { load_printer_url(url, apikey); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 8299125353..5b06db9d3e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3287,7 +3287,9 @@ void Sidebar::update_all_preset_comboboxes() : MainFrame::PrintSelectType::eSendGcode; } - if (!use_native_device_tab || use_printer_agents) + if (use_printer_agents) + p_mainframe->load_printer_url(); + else if (!use_native_device_tab) p_mainframe->load_printer_url(url, apikey); @@ -11236,9 +11238,14 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } } else { - if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { + const bool selecting_web_device_tab = main_frame->m_printer_view && + main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; + if (selecting_web_device_tab) { + // Use the selected discovered machine when the preset has no host. + main_frame->load_printer_url(); + } else if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); + wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); if (main_frame->m_printer_view && url.empty()) { // It's missing_connection page, reload so that we can replay the gif image main_frame->m_printer_view->reload(); diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 22f65f4a60..7b2d091176 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -1991,7 +1991,7 @@ void InputIpAddressDialog::workerThreadFunc(std::string str_ip, std::string str_ if (w.expired()) return; if (m_obj) { - m_obj->set_user_access_code(str_access_code); + m_obj->set_access_code(str_access_code); wxGetApp().getDeviceManager()->set_selected_machine(m_obj->get_dev_id()); } @@ -2055,6 +2055,11 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) { auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); + + if (str_access_code.empty()) { + str_access_code = "88888888"; + } + auto str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both); auto str_sn = m_input_sn->GetTextCtrl()->GetValue().Strip(wxString::both); bool invalid_access_code = true; @@ -2062,7 +2067,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) for (char c : str_access_code) { if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { invalid_access_code = false; - return; + break; } } diff --git a/src/slic3r/GUI/SelectMachinePop.cpp b/src/slic3r/GUI/SelectMachinePop.cpp index 492199569e..96324fb4d8 100644 --- a/src/slic3r/GUI/SelectMachinePop.cpp +++ b/src/slic3r/GUI/SelectMachinePop.cpp @@ -704,7 +704,6 @@ void SelectMachinePopup::update_user_devices() } mobj->set_access_code(""); - mobj->erase_user_access_code(); } if (GUI::wxGetApp().plater()) diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index d21dce5070..cd3ef82b62 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -1359,7 +1359,6 @@ void MoonrakerPrinterAgent::announce_printhost_device() if (auto* app_config = GUI::wxGetApp().app_config) { const std::string access_code = device_info.api_key.empty() ? "88888888" : device_info.api_key; app_config->set_str("access_code", device_info.dev_id, access_code); - app_config->set_str("user_access_code", device_info.dev_id, access_code); } nlohmann::json payload; From ee6613a4b8b0720723518c823ef815c4be9d64d4 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:18:00 +0300 Subject: [PATCH 38/71] Fix stale flush matrix after enabling SEMM (#15223) --- src/libslic3r/PresetBundle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 01cbc43bc2..5557d36891 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5378,7 +5378,7 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam f_multiplier.resize(nozzle_nums, 1.f); } - if ( (num_filaments * num_filaments) != size_t(old_matrix.size() / old_nozzle_nums) ) { + if (old_matrix.size() != num_filaments * num_filaments * nozzle_nums) { // First verify if purging volumes presets for each extruder matches number of extruders std::vector& filaments = this->project_config.option("flush_volumes_vector")->values; while (filaments.size() < 2* num_filaments) { From d322b1a156b9afb6ef412665594e374baf316a88 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:42:21 +0300 Subject: [PATCH 39/71] Fix assembly parts omitted by height range modifiers (#15225) --- src/libslic3r/PrintApply.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index e2e9bc737d..6d5dbb05f5 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -559,9 +559,11 @@ static inline bool model_volume_solid_or_modifier(const ModelVolume &mv) static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo) { - Transform3d m = object_trafo * volume_trafo; - m.translation().x() = 0.; - m.translation().y() = 0.; + // Orca: Keep the volume's local XY offset for multipart overlap checks, but remove the object's bed placement. + Transform3d object_trafo_local = object_trafo; + object_trafo_local.translation().x() = 0.; + object_trafo_local.translation().y() = 0.; + Transform3d m = object_trafo_local * volume_trafo; return m.cast(); } From 50dfcff0314154c635bf973f8888e18a533e4646 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 13 Aug 2026 14:54:46 +0800 Subject: [PATCH 40/71] fix: plugin pages removing calibration tab --- src/slic3r/GUI/MainFrame.cpp | 2 ++ src/slic3r/plugin/host/PluginPages.cpp | 13 +++++++++---- src/slic3r/plugin/host/PluginPages.hpp | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 26c03975dc..52da60a4a8 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1457,6 +1457,7 @@ void MainFrame::show_device(bool should_use_native) { #endif // _MSW_DARK_MODE fit_tab_labels(); // ORCA on printer change + m_plugin_pages.relayout(); // keep plugin tabs after the native tabs just mutated above return; } @@ -1545,6 +1546,7 @@ void MainFrame::show_device(bool should_use_native) { } } fit_tab_labels(); // ORCA on printer change + m_plugin_pages.relayout(); // keep plugin tabs after the native tabs just mutated above } void MainFrame::fit_tab_labels() diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 551e39877d..90e1282dd9 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -218,7 +218,6 @@ void PluginPages::initialize(Notebook* parent) return; m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count(); - m_notebook_base_index = static_cast(m_parent->GetPageCount()); m_image_list = std::make_unique(20, 20, true, 0); m_parent->SetImageList(m_image_list.get()); @@ -238,7 +237,6 @@ void PluginPages::shutdown() m_parent->SetImageList(nullptr); m_image_list.reset(); m_parent = nullptr; - m_notebook_base_index = 0; } void PluginPages::set_visible_page_count(int count) @@ -369,6 +367,10 @@ void PluginPages::remove_page(const PluginCapabilityId& id) } } + const int idx = m_parent != nullptr ? m_parent->FindPage(page) : wxNOT_FOUND; + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + relayout(); page->Destroy(); } @@ -394,8 +396,11 @@ void PluginPages::relayout() wxString id_to_reselect = m_parent->GetSelectedPageName(); - while (m_parent->GetPageCount() > m_notebook_base_index) - m_parent->RemovePage(m_parent->GetPageCount() - 1); + for (const auto& [id, page] : m_pages) { + const int idx = m_parent->FindPage(page); + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + } const int visible_slots = std::max(1, m_visible_page_count); const bool need_overflow = static_cast(m_order.size()) > visible_slots; diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index cc54332dbe..3dc86b8aee 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -69,19 +69,19 @@ public: int get_visible_page_count() const { return m_visible_page_count; } void set_visible_page_count(int count); + void relayout(); + private: std::shared_ptr get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const; bool create_page(const PluginCapabilityId& id); void remove_page(const PluginCapabilityId& id); - void relayout(); void show_overflow_menu(); static wxString page_tab_id(const PluginCapabilityId& id); std::map m_pages; std::vector m_order; Notebook* m_parent{nullptr}; - size_t m_notebook_base_index{0}; std::unique_ptr m_image_list; int m_visible_page_count{0}; From fd23b74b99d95ef25e5b524f90a308538632afd9 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:09:10 +0300 Subject: [PATCH 41/71] Fix single-value object overrides on multi-nozzle printers (#15221) --- src/libslic3r/PrintConfig.cpp | 12 +++++++++++- src/libslic3r/PrintConfig.hpp | 3 +++ src/libslic3r/PrintObject.cpp | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index fdb20253d6..f9d895332a 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10426,6 +10426,16 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector &variant_index, int stride) +{ + // A single-value object or region override applies to every nozzle variant. + std::vector indices = variant_index; + if (source.size() == 1 && !source.is_nil(0)) + std::fill(indices.begin(), indices.end(), 0); + target.set_to_index(&source, indices, stride); +} + //used for object/region config //use the smallest of multiple to single @@ -11503,7 +11513,7 @@ void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPr else { ConfigOptionVectorBase* opt_vec_src = static_cast(opt_src); const ConfigOptionVectorBase* opt_vec_dest = static_cast(opt_dest); - opt_vec_src->set_to_index(opt_vec_dest, variant_index, stride); + set_variant_override(*opt_vec_src, *opt_vec_dest, variant_index, stride); } } } diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 6029d5bd88..b6364c32c2 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -842,6 +842,9 @@ extern std::set printer_options_with_variant_1; extern std::set printer_options_with_variant_2; extern std::set empty_options; +void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source, + const std::vector &variant_index, int stride = 1); + extern std::set filament_dev_options; extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector variant_index, std::set& key_set1, int stride = 1); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index b2a92f11a6..8368de1a4f 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3812,7 +3812,7 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr else { ConfigOptionVectorBase* opt_vec_src = static_cast(my_opt); const ConfigOptionVectorBase* opt_vec_dest = static_cast(it->second.get()); - opt_vec_src->set_to_index(opt_vec_dest, variant_index, 1); + set_variant_override(*opt_vec_src, *opt_vec_dest, variant_index); } } } From 56d2c527cbb0fe50afbb09c75bc74cf2cdeddda7 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:28:31 +0300 Subject: [PATCH 42/71] Fix crashes from object-level small perimeter speed overrides (#15232) --- src/libslic3r/Config.hpp | 2 ++ src/libslic3r/Model.cpp | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 509095cbfc..ef93f0d509 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2982,6 +2982,8 @@ public: const double & opt_float(const t_config_option_key &opt_key, unsigned int idx) const; double & opt_float_nullable(const t_config_option_key &opt_key, unsigned int idx) { return this->option(opt_key)->get_at(idx); } const double & opt_float_nullable(const t_config_option_key &opt_key, unsigned int idx) const { return dynamic_cast(this->option(opt_key))->get_at(idx); } + FloatOrPercent & opt_float_or_percent_nullable(const t_config_option_key &opt_key, unsigned int idx) { return this->option(opt_key)->get_at(idx); } + const FloatOrPercent & opt_float_or_percent_nullable(const t_config_option_key &opt_key, unsigned int idx) const { return dynamic_cast(this->option(opt_key))->get_at(idx); } int& opt_int(const t_config_option_key &opt_key) { return this->option(opt_key)->value; } int opt_int(const t_config_option_key &opt_key) const { return dynamic_cast(this->option(opt_key))->value; } diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 1177a5227d..c689c7ce78 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -3243,9 +3243,9 @@ double Model::findMaxSpeed(const ModelObject* object) { if (objectKey == "outer_wall_speed") externalPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0); if (objectKey == "small_perimeter_speed") - smallPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0); + smallPerimeterSpeedObj = object->config.get().opt_float_or_percent_nullable(objectKey, 0).get_abs_value(externalPerimeterSpeedObj); if (objectKey == "small_support_perimeter_speed") - smallSupportPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0); + smallSupportPerimeterSpeedObj = object->config.get().opt_float_or_percent_nullable(objectKey, 0).get_abs_value(supportSpeedObj); } objMaxSpeed = std::max(perimeterSpeedObj, std::max(externalPerimeterSpeedObj, std::max(infillSpeedObj, std::max(solidInfillSpeedObj, std::max(topSolidInfillSpeedObj, std::max(supportSpeedObj, std::max(smallPerimeterSpeedObj, std::max(smallSupportPerimeterSpeedObj, objMaxSpeed)))))))); if (objMaxSpeed <= 0) objMaxSpeed = 250.; From 78eef79ffea599653c305b2461afa51b174ef72b Mon Sep 17 00:00:00 2001 From: Robert J Audas Date: Thu, 13 Aug 2026 14:50:55 -0600 Subject: [PATCH 43/71] Fix flushing-volume warning for single-filament plates (#14704) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- src/libslic3r/PrintConfig.hpp | 49 ++++++++++++++++++++++++++++++++ src/slic3r/GUI/GLCanvas3D.cpp | 20 ++++--------- tests/libslic3r/test_config.cpp | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 15 deletions(-) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index b6364c32c2..f51c1c6411 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -2397,6 +2397,55 @@ static void set_flush_volumes_matrix(std::vector &out_matrix, const std::vect } } +template +static bool has_zero_flush_volume_for_used_filaments(const std::vector &fv_matrix, + const std::vector &flush_multipliers, + const std::vector &used_filaments) +{ + if (used_filaments.size() < 2 || flush_multipliers.empty()) + return false; + + if (fv_matrix.size() % flush_multipliers.size() != 0) + return false; + + const size_t matrix_len = fv_matrix.size() / flush_multipliers.size(); + const size_t row_len = size_t(std::sqrt(double(matrix_len))); + if (row_len < 2 || row_len * row_len != matrix_len) + return false; + + std::vector filtered_filaments; + filtered_filaments.reserve(used_filaments.size()); + for (int filament_id : used_filaments) { + if (filament_id <= 0 || filament_id > int(row_len)) + continue; + if (std::find(filtered_filaments.begin(), filtered_filaments.end(), filament_id) == filtered_filaments.end()) + filtered_filaments.push_back(filament_id); + } + if (filtered_filaments.size() < 2) + return false; + + for (T multiplier : flush_multipliers) { + if (multiplier == 0) + return true; + } + + for (size_t nozzle_idx = 0; nozzle_idx < flush_multipliers.size(); nozzle_idx++) { + const size_t block_offset = nozzle_idx * matrix_len; + for (int from_id : filtered_filaments) { + for (int to_id : filtered_filaments) { + if (from_id == to_id) + continue; + + const size_t matrix_idx = block_offset + size_t(from_id - 1) * row_len + size_t(to_id - 1); + if (matrix_idx < fv_matrix.size() && fv_matrix[matrix_idx] == 0) + return true; + } + } + } + + return false; +} + size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id); } // namespace Slic3r diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 303f9a2b76..d0eb79881b 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -10738,24 +10738,14 @@ bool GLCanvas3D::is_flushing_matrix_error() { if (!Sidebar::should_show_SEMM_buttons()) return false; + std::vector plate_extruders = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_extruders(true); + if (plate_extruders.size() < 2) + return false; + const auto &project_config = wxGetApp().preset_bundle->project_config; const std::vector &config_matrix = (project_config.option("flush_volumes_matrix"))->values; const std::vector &config_multiplier = (project_config.option("flush_multiplier"))->values; - - for (auto multiplier : config_multiplier) { - if (multiplier == 0) return true; - } - - int matrix_len = config_matrix.size() / config_multiplier.size(); - int row_len = std::sqrt(matrix_len); - for (int i = 0; i < config_matrix.size(); i++) - { - int relative_id = i % matrix_len; - int row_id = relative_id / row_len; - int col_id = relative_id % row_len; - if (row_id != col_id && config_matrix[i] == 0) return true; - } - return false; + return has_zero_flush_volume_for_used_filaments(config_matrix, config_multiplier, plate_extruders); } bool GLCanvas3D::_is_any_volume_outside() const diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 5bc825c3b2..12b161322d 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -235,6 +235,56 @@ SCENARIO("Config ini load/save interface", "[Config]") { } } +TEST_CASE("Flush-volume warning predicate respects used filament transitions", "[Config][Regression]") +{ + const std::vector multipliers = {1.0}; + + SECTION("Single used filament does not trigger warning with zero transition entries") + { + const std::vector matrix = { + 0.0, 0.0, + 0.0, 0.0 + }; + const std::vector used_filaments = {1}; + + REQUIRE_FALSE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments)); + } + + SECTION("Two used filaments trigger warning when transition flush entry is zero") + { + const std::vector matrix = { + 0.0, 0.0, + 0.0, 0.0 + }; + const std::vector used_filaments = {1, 2}; + + REQUIRE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments)); + } + + SECTION("Two used filaments do not trigger warning when transitions are non-zero") + { + const std::vector matrix = { + 0.0, 280.0, + 280.0, 0.0 + }; + const std::vector used_filaments = {1, 2}; + + REQUIRE_FALSE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments)); + } + + SECTION("Zero multiplier still triggers warning when multiple filaments are used") + { + const std::vector matrix = { + 0.0, 280.0, + 280.0, 0.0 + }; + const std::vector zero_multiplier = {0.0}; + const std::vector used_filaments = {1, 2}; + + REQUIRE(has_zero_flush_volume_for_used_filaments(matrix, zero_multiplier, used_filaments)); + } +} + // TODO: https://github.com/SoftFever/OrcaSlicer/issues/11269 - Is this test still relevant? Delete if not. // It was failing so at least "nozzle_type" and "extruder_printable_area" could not be serialized // and an exception was thrown, but "nozzle_type" has been around for at least 3 months now. From c5aedd1cea4b00b30b67749ad69781a563a07e8b Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:25:52 -0500 Subject: [PATCH 44/71] Enable Snapmaker U1 bed type selector (#15174) --- resources/profiles/Snapmaker.json | 2 +- .../profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json | 1 - .../profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json | 1 - .../profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json | 1 - .../profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json | 1 - resources/profiles/Snapmaker/machine/fdm_U1.json | 3 ++- 6 files changed, 3 insertions(+), 6 deletions(-) diff --git a/resources/profiles/Snapmaker.json b/resources/profiles/Snapmaker.json index ab2433242e..9a6fab7942 100644 --- a/resources/profiles/Snapmaker.json +++ b/resources/profiles/Snapmaker.json @@ -1,6 +1,6 @@ { "name": "Snapmaker", - "version": "02.04.00.08", + "version": "02.04.00.09", "force_update": "0", "description": "Snapmaker configurations", "machine_model_list": [ diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index aebc032855..183e125c73 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -186,7 +186,6 @@ "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\nTIMELAPSE_TAKE_FRAME\nDEFECT_DETECTION_DETECT", "machine_pause_gcode": "M600", "nozzle_volume": "143", - "support_multi_bed_types": "0", "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER={layer_num+1}", "default_print_profile": "0.10 Standard @Snapmaker U1 (0.2 nozzle)" } diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 28ccfd0a29..6d2ec2cfe6 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -186,7 +186,6 @@ "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\nTIMELAPSE_TAKE_FRAME\nDEFECT_DETECTION_DETECT", "default_print_profile": "0.20 Standard @Snapmaker U1 (0.4 nozzle)", "machine_pause_gcode": "M600", - "default_bed_type": "Textured PEI Plate", "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER={layer_num+1}", "nozzle_volume": "143", "resonance_avoidance": "1", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index f4dff2f357..a6cb0d0bd3 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -187,6 +187,5 @@ "machine_pause_gcode": "M600", "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER={layer_num+1}", "nozzle_volume": "143", - "support_multi_bed_types": "0", "default_print_profile": "0.30 Standard @Snapmaker U1 (0.6 nozzle)" } diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index e356f4264b..ef4da1a516 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -187,6 +187,5 @@ "machine_pause_gcode": "M600", "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER={layer_num+1}", "nozzle_volume": "143", - "support_multi_bed_types": "0", "default_print_profile": "0.40 Standard @Snapmaker U1 (0.8 nozzle)" } diff --git a/resources/profiles/Snapmaker/machine/fdm_U1.json b/resources/profiles/Snapmaker/machine/fdm_U1.json index 7ee65878e6..717215b507 100644 --- a/resources/profiles/Snapmaker/machine/fdm_U1.json +++ b/resources/profiles/Snapmaker/machine/fdm_U1.json @@ -183,7 +183,8 @@ "scan_first_layer": "0", "nozzle_type": "undefine", "auxiliary_fan": "0", - "default_bed_type": "Textured PEI Plate", + "support_multi_bed_types": "1", + "default_bed_type": "4", "printable_area": [ "0.5x1", "270.5x1", From 0225cadff03e6750cfb505ca00aae37ce67b9a54 Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:57:08 -0500 Subject: [PATCH 45/71] Fix PLA/PETG warning wiki link (#15172) --- src/slic3r/GUI/GLCanvas3D.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index d0eb79881b..a51a10296c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -10602,9 +10602,8 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) wxString region = L"en"; if (language.find("zh") == 0) region = L"zh"; - // Use the generic dual-nozzle PLA+PETG guide rather than the H2D-specific page - // so the link is relevant for all dual-extrusion printers, not just Bambu H2D. (#12073) - wxGetApp().open_browser_with_warning_dialog(wxString::Format(L"https://wiki.bambulab.com/%s/filament-acc/filament/pla-and-petg-dual-extrusion", region)); + // Although this link looks like it's only for the H2D, its guidance is generic. + wxGetApp().open_browser_with_warning_dialog(wxString::Format(L"https://wiki.bambulab.com/%s/filament-acc/filament/h2d-pla-and-petg-mutual-support", region)); return false; }); } From d5dbd96dd64b830076c81053ed5fda26d5a1771b Mon Sep 17 00:00:00 2001 From: Manzari <22736528+manzari@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:52:52 +0200 Subject: [PATCH 46/71] Skip filament_colour_type in G-code config block to fix Anycubic Kobra 3 parse crash (#13507) Co-authored-by: manzari Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- src/libslic3r/GCode.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8e2e9f713c..18d805936e 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6726,6 +6726,7 @@ void GCode::append_full_config(const Print &print, std::string &str) "farthest_point_timelapse"sv, "compatible_printers"sv, "compatible_prints"sv, + "filament_colour_type"sv, "print_host"sv, "print_host_webui"sv, "printhost_apikey"sv, From 9d37ee4709fdb60bcbfe2b98778b73e604a4d47b Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 14 Aug 2026 15:12:18 +0800 Subject: [PATCH 47/71] removed changes that are out of scope --- src/slic3r/GUI/DeviceManager.cpp | 24 ++++------- src/slic3r/Utils/BBLPrinterAgent.cpp | 61 ---------------------------- src/slic3r/Utils/BBLPrinterAgent.hpp | 10 ----- src/slic3r/Utils/IPrinterAgent.hpp | 10 ----- src/slic3r/Utils/NetworkAgent.cpp | 21 ---------- src/slic3r/Utils/NetworkAgent.hpp | 3 -- 6 files changed, 9 insertions(+), 120 deletions(-) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 48f3cb60fc..d8487f4660 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -1731,11 +1731,9 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read int MachineObject::command_ams_calibrate(int ams_id) { - if (!m_agent) return -1; - int rtn = m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()); - if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) - show_unsupported_dlg(rtn); - return rtn; + std::string gcode_cmd = (boost::format("M620 C%1% \n") % ams_id).str(); + BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; + return this->publish_gcode(gcode_cmd); } int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max) @@ -1773,11 +1771,9 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s int MachineObject::command_ams_refresh_rfid(std::string tray_id) { - if (!m_agent) return -1; - int rtn = m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()); - if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) - show_unsupported_dlg(rtn); - return rtn; + std::string gcode_cmd = (boost::format("M620 R%1% \n") % tray_id).str(); + BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; + return this->publish_gcode(gcode_cmd); } int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id) @@ -1793,11 +1789,9 @@ int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id) int MachineObject::command_ams_select_tray(std::string tray_id) { - if (!m_agent) return -1; - int rtn = m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()); - if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) - show_unsupported_dlg(rtn); - return rtn; + std::string gcode_cmd = (boost::format("M620 P%1% \n") % tray_id).str(); + BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; + return this->publish_gcode(gcode_cmd); } int MachineObject::command_ams_control(std::string action) diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index 9d422552fe..ef85e0a1ff 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -2,9 +2,7 @@ #include "BBLNetworkPlugin.hpp" #include "NetworkAgentFactory.hpp" -#include #include -#include namespace Slic3r { @@ -22,65 +20,6 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr cloud) // Communication // ============================================================================ -std::string BBLPrinterAgent::ams_refresh_rfid_gcode(const std::string& tray_id) -{ - return (boost::format("M620 R%1% \n") % tray_id).str(); -} - -std::string BBLPrinterAgent::ams_calibrate_gcode(int ams_id) -{ - return (boost::format("M620 C%1% \n") % ams_id).str(); -} - -std::string BBLPrinterAgent::ams_select_tray_gcode(const std::string& tray_id) -{ - return (boost::format("M620 P%1% \n") % tray_id).str(); -} - -int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) -{ - const std::string gcode = ams_refresh_rfid_gcode(tray_id); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = gcode; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) -{ - const std::string gcode = ams_calibrate_gcode(ams_id); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = gcode; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) -{ - const std::string gcode = ams_select_tray_gcode(tray_id); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode; - nlohmann::json j; - j["print"]["command"] = "gcode_line"; - j["print"]["param"] = gcode; - j["print"]["sequence_id"] = std::to_string(sequence_id); - return publish(dev_id, j, lan_mode); -} - -int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode) -{ - const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0); - if (rtn == 0) { - BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn; - } else { - BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn; - } - return rtn; -} - int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) { auto& plugin = BBLNetworkPlugin::instance(); diff --git a/src/slic3r/Utils/BBLPrinterAgent.hpp b/src/slic3r/Utils/BBLPrinterAgent.hpp index a04cd00175..a8880bf6bf 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.hpp +++ b/src/slic3r/Utils/BBLPrinterAgent.hpp @@ -5,7 +5,6 @@ #include "ICloudServiceAgent.hpp" #include #include -#include namespace Slic3r { @@ -29,12 +28,6 @@ public: // Communication int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; - static std::string ams_refresh_rfid_gcode(const std::string& tray_id); - static std::string ams_calibrate_gcode(int ams_id); - static std::string ams_select_tray_gcode(const std::string& tray_id); - int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; - int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override; - int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; int disconnect_printer() override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; @@ -92,9 +85,6 @@ public: FilamentSyncMode get_filament_sync_mode() const override; private: - // why: the lan/cloud DECISION stays machine-side; keep this mechanical branch in sync with publish_json. - int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode); - std::shared_ptr m_cloud_agent; }; diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 85a1ffb8fc..0fa3616344 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -84,16 +84,6 @@ public: */ virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0; - // why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect - // gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's - // publish funnel turns into a dialog. - virtual int command_ams_refresh_rfid(std::string, std::string, int, bool) - { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } - virtual int command_ams_calibrate(std::string, int, int, bool) - { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } - virtual int command_ams_select_tray(std::string, std::string, int, bool) - { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } - /** * Establish a direct LAN connection to a printer. */ diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index b169fca052..0d77e5e660 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -767,27 +767,6 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos return -1; } -int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) -{ - if (m_printer_agent) - return m_printer_agent->command_ams_refresh_rfid(dev_id, tray_id, sequence_id, lan_mode); - return -1; -} - -int NetworkAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) -{ - if (m_printer_agent) - return m_printer_agent->command_ams_calibrate(dev_id, ams_id, sequence_id, lan_mode); - return -1; -} - -int NetworkAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) -{ - if (m_printer_agent) - return m_printer_agent->command_ams_select_tray(dev_id, tray_id, sequence_id, lan_mode); - return -1; -} - int NetworkAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { if (m_printer_agent) diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index 317a357135..d7032b7a20 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -142,9 +142,6 @@ public: int set_on_local_message_fn(OnMessageFn fn); int set_server_callback(OnServerErrFn fn); int send_message(std::string dev_id, std::string json_str, int qos, int flag); - int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode); - int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode); - int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode); int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); int disconnect_printer(); int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag); From 728cf63c3d0b3a59be0a70cdc158515f251c9181 Mon Sep 17 00:00:00 2001 From: Alexandre Folle de Menezes Date: Sat, 15 Aug 2026 12:54:00 -0300 Subject: [PATCH 48/71] Verify and improve AI pt_BR translations (#15261) --- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 220 +++++++------------- 1 file changed, 81 insertions(+), 139 deletions(-) diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 0d777ec32e..595e46b8cf 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -217,7 +217,6 @@ msgstr "Os filamentos %s são duros e quebradiços, podendo se romper no AMS. E msgid "%s has a risk of nozzle clogging when using 0.4, 0.6, 0.8mm high-flow nozzles. Use with caution." msgstr "%s apresenta risco de entupimento do bico ao utilizar bicos de alto fluxo de 0,4, 0,6 ou 0,8 mm. Use com cautela." -# AI Translated #, c-format, boost-format msgid "%s may fail to load or unload due to the Filament Track Switch. If you wish to continue." msgstr "%s pode falhar ao carregar ou descarregar devido ao Filament Track Switch. Se você deseja continuar." @@ -347,7 +346,6 @@ msgstr "Leitura " msgid "Please wait" msgstr "Por favor, aguarde" -# AI Translated msgid "Reading" msgstr "Lendo" @@ -700,7 +698,6 @@ msgstr "Redefinir posição" msgid "Reset rotation" msgstr "Redefinir rotação" -# AI Translated msgid "World" msgstr "Mundo" @@ -988,7 +985,6 @@ msgstr "Plano de corte com cavidade é inválido" msgid "Connector" msgstr "Conector" -# AI Translated #, boost-format msgid "" "Objects(%1%) have duplicated connectors. Some connectors may be missing in slicing result.\n" @@ -2032,7 +2028,6 @@ msgstr "" "\n" "Se você não usava o Bambu Cloud para sincronizar perfis, esta mudança não afeta você e você pode ignorar esta mensagem com segurança." -# AI Translated msgid "Profile syncing change" msgstr "Alteração de sincronização de perfil" @@ -3429,7 +3424,7 @@ msgid "AMS has not been initialized. Please initialize it before use." msgstr "O AMS não foi inicializado. Por favor, inicialize-o antes de usar." msgid "Changing fan speed during printing may affect print quality, please choose carefully." -msgstr "Mudar a velocidade do ventilador durante a impressão pode afetar a qualidade da impressão. Escolha com cuidado." +msgstr "Mudar a velocidade da ventoinha durante a impressão pode afetar a qualidade da impressão. Escolha com cuidado." msgid "Change Anyway" msgstr "Mudar Mesmo Assim" @@ -3441,7 +3436,7 @@ msgid "Filter" msgstr "Filtrar" msgid "Enabling filtration redirects the right fan to filter gas, which may reduce cooling performance." -msgstr "Ativar a filtragem redireciona o ventilador direito para filtrar o gás, o que pode reduzir o desempenho de resfriamento." +msgstr "Ativar a filtragem redireciona a ventoinha direito para filtrar o gás, o que pode reduzir o desempenho de resfriamento." msgid "Enabling filtration during printing may reduce cooling and affect print quality. Please choose carefully." msgstr "Habilitar a filtragem durante a impressão pode reduzir o resfriamento e afetar a qualidade da impressão. Escolha com cuidado." @@ -3474,7 +3469,7 @@ msgid "Top" msgstr "Topo" msgid "The fan controls the temperature during printing to improve print quality. The system automatically adjusts the fan's switch and speed according to different printing materials." -msgstr "O ventilador controla a temperatura durante a impressão para melhorar a qualidade da impressão. O sistema ajusta automaticamente a ativação e a velocidade do ventilador de acordo com os diferentes materiais de impressão." +msgstr "A ventoinha controla a temperatura durante a impressão para melhorar a qualidade da impressão. O sistema ajusta automaticamente a ativação e a velocidade da ventoinha de acordo com os diferentes materiais de impressão." msgid "Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air." msgstr "O modo de resfriamento é adequado para impressão com materiais PLA/PETG/TPU e filtra o ar da câmara." @@ -4798,7 +4793,7 @@ msgid "Pause (AMS offline)" msgstr "Pausa (AMS offline)" msgid "Pause (low speed of the heatbreak fan)" -msgstr "Pausa (baixa velocidade do ventilador do heatbreak)" +msgstr "Pausa (baixa velocidade da ventoinha do heatbreak)" msgid "Pause (chamber temperature control problem)" msgstr "Pausa (problema no controle de temperatura da câmara)" @@ -4922,7 +4917,7 @@ msgstr "Para garantir sua segurança, certas tarefas de processamento (como o la #, c-format, boost-format msgid "The chamber temperature is too high, which may cause the filament to soften. Please wait until the chamber temperature drops below %d℃. You may open the front door or enable fans to cool down." -msgstr "A temperatura da câmara está muito alta, o que pode causar o amolecimento do filamento. Aguarde até que a temperatura da câmara caia abaixo de %d℃. Você pode abrir a porta frontal ou ligar os ventiladores para resfriar." +msgstr "A temperatura da câmara está muito alta, o que pode causar o amolecimento do filamento. Aguarde até que a temperatura da câmara caia abaixo de %d℃. Você pode abrir a porta frontal ou ligar as ventoinhas para resfriar." #, c-format, boost-format msgid "AMS temperature is too high, which may cause the filament to soften. Please wait until the AMS temperature drops below %d℃." @@ -5208,7 +5203,7 @@ msgid "Jerk" msgstr "Jerk" msgid "Fan Speed" -msgstr "Velocidade do Ventilador" +msgstr "Velocidade da Ventoinha" msgid "Flow" msgstr "Fluxo" @@ -5314,7 +5309,7 @@ msgid "Flow: " msgstr "Fluxo: " msgid "Fan: " -msgstr "Ventilador: " +msgstr "Ventoinha: " msgid "Temperature: " msgstr "Temperatura: " @@ -5350,7 +5345,7 @@ msgid "Flow rate" msgstr "Taxa de fluxo" msgid "Fan speed" -msgstr "Velocidade do ventilador" +msgstr "Velocidade da ventoinha" msgid "Time" msgstr "Tempo" @@ -5464,7 +5459,7 @@ msgid "Jerk (mm/s)" msgstr "Jerk (mm/s)" msgid "Fan speed (%)" -msgstr "Velocidade do ventilador (%)" +msgstr "Velocidade da ventoinha (%)" msgid "Temperature (℃)" msgstr "Temperatura (℃)" @@ -7368,12 +7363,11 @@ msgstr "Inferior" msgid "Plugin Selection" msgstr "Seleção de plugins" -# AI Translated msgid "" "No plugins capabilities available for this type.\n" "Enable or install some to use." msgstr "" -"Nenhum recurso de plugins disponível para este tipo.\n" +"Nenhuma capacidade de plugin disponível para este tipo.\n" "Ative ou instale algum para usar." msgid "There is stringing-prone filament in the current print job. Enabling nozzle clumping detection now may degrade print quality. Are you sure you want to enable it?" @@ -9758,7 +9752,7 @@ msgid "Unable to automatically match to suitable filament. Please click to manua msgstr "Não foi possível encontrar automaticamente um filamento adequado. Clique para selecionar manualmente." msgid "Install toolhead enhanced cooling fan to prevent filament softening." -msgstr "Instale um ventilador de resfriamento aprimorado no cabeçote de impressão para evitar o amolecimento do filamento." +msgstr "Instale uma ventoinha de resfriamento aprimorado no cabeçote de impressão para evitar o amolecimento do filamento." msgid "Smooth Cool Plate" msgstr "Placa Fria Lisa" @@ -10382,25 +10376,25 @@ msgid "Cooling for specific layer" msgstr "Resfriamento para camada específica" msgid "Part cooling fan" -msgstr "Ventilador de resfriamento de peças" +msgstr "Ventoinha de resfriamento de peças" msgid "Min fan speed threshold" -msgstr "Limiar de velocidade mínima do ventilador" +msgstr "Limiar de velocidade mínima da ventoinha" msgid "The part cooling fan will run at the minimum fan speed when the estimated layer time is longer than the threshold value. When the layer time is shorter than the threshold, the fan speed will be interpolated between the minimum and maximum fan speed according to layer printing time." -msgstr "O ventilador de resfriamento de peças irá girar na velocidade mínima quando o tempo estimado da camada for mais longo do que o valor de limiar. Quando o tempo da camada for mais curto que o limiar, a velocidade do ventilador é interpolada entre a velocidade mínima e máxima de acordo com o tempo de impressão da camada." +msgstr "A ventoinha de resfriamento de peças irá girar na velocidade mínima quando o tempo estimado da camada for mais longo do que o valor de limiar. Quando o tempo da camada for mais curto que o limiar, a velocidade da ventoinha é interpolada entre a velocidade mínima e máxima de acordo com o tempo de impressão da camada." msgid "Max fan speed threshold" -msgstr "Limiar de velocidade máxima do ventilador" +msgstr "Limiar de velocidade máxima da ventoinha" msgid "The part cooling fan will run at maximum speed when the estimated layer time is shorter than the threshold value." -msgstr "O ventilador de resfriamento de peças irá girar na velocidade máxima quando o tempo estimado da camada for mais curto que o limiar." +msgstr "A ventoinha de resfriamento de peças irá girar na velocidade máxima quando o tempo estimado da camada for mais curto que o limiar." msgid "Auxiliary part cooling fan" -msgstr "Ventilador auxiliar de resfriamento de peças" +msgstr "Ventoinha auxiliar de resfriamento de peças" msgid "Exhaust fan" -msgstr "Ventilador de exaustão" +msgstr "Ventoinha de exaustão" msgid "During print" msgstr "Durante a impressão" @@ -10450,10 +10444,10 @@ msgid "G-code flavor is switched" msgstr "Tipo de G-code está trocado" msgid "Cooling Fan" -msgstr "Ventilador de resfriamento" +msgstr "Ventoinha de resfriamento" msgid "Fan speed-up time" -msgstr "Tempo de aceleração do ventilador" +msgstr "Tempo de aceleração da ventoinha" msgid "Extruder Clearance" msgstr "Folga da extrusora" @@ -11770,7 +11764,6 @@ msgstr "Erro de agrupamento: " msgid " can not be placed in the " msgstr " não pode ser colocado na " -# AI Translated msgid "Group error in manual mode. Please check nozzle count or regroup." msgstr "Erro de agrupamento no modo manual. Por favor, verifique o número de bicos ou reagrupe." @@ -12096,7 +12089,6 @@ msgstr "A contração de filamento não será usada porque a contração dos fil msgid "Generating skirt & brim" msgstr "Gerando saia e borda" -# AI Translated msgid "" "Per-object skirts cannot fit between the objects in By object print sequence.\n" "\n" @@ -12277,9 +12269,8 @@ msgstr "API Key" msgid "HTTP digest" msgstr "Digest HTTP" -# AI Translated msgid "Configuration for the plugin capabilities this preset uses, overriding the global Capabilities configuration. Stored as a raw JSON array and edited through the dialog behind the button, never typed in directly." -msgstr "Configuração dos recursos de plugin que esta predefinição usa, substituindo a configuração global de Recursos. Armazenada como um array JSON bruto e editada por meio da caixa de diálogo atrás do botão, nunca digitada diretamente." +msgstr "Configuração das capacidades de plugin que esta predefinição usa, substituindo a configuração global de Capacidades. Armazenada como um array JSON bruto e editada por meio da caixa de diálogo atrás do botão, nunca digitada diretamente." msgid "Avoid crossing walls" msgstr "Evitar atravessar paredes" @@ -12420,26 +12411,26 @@ msgid "Force cooling for overhangs and bridges" msgstr "Resfriamento forçado para saliências e pontes" msgid "Enable this option to allow adjustment of the part cooling fan speed for specifically for overhangs, internal and external bridges. Setting the fan speed specifically for these features can improve overall print quality and reduce warping." -msgstr "Habilite esta opção para permitir o ajuste da velocidade do ventilador de resfriamento de peças especificamente para saliências, pontes internas e externas. Definir a velocidade do ventilador especificamente para esses recursos pode melhorar a qualidade geral da impressão e reduzir a deformação." +msgstr "Habilite esta opção para permitir o ajuste da velocidade da ventoinha de resfriamento de peças especificamente para saliências, pontes internas e externas. Definir a velocidade da ventoinha especificamente para esses recursos pode melhorar a qualidade geral da impressão e reduzir a deformação." msgid "Overhangs and external bridges fan speed" -msgstr "Velocidade do ventilador para saliências e pontes externas" +msgstr "Velocidade da ventoinha para saliências e pontes externas" msgid "" "Use this part cooling fan speed when printing bridges or overhang walls with an overhang threshold that exceeds the value set in the 'Overhangs cooling threshold' parameter above. Increasing the cooling specifically for overhangs and bridges can improve the overall print quality of these features.\n" "\n" "Please note, this fan speed is clamped on the lower end by the minimum fan speed threshold set above. It is also adjusted upwards up to the maximum fan speed threshold when the minimum layer time threshold is not met." msgstr "" -"Use esta parte da velocidade do ventilador de resfriamento ao imprimir pontes ou paredes salientes com um limite de saliência que exceda o valor definido no parâmetro 'Limiar de resfriamento de saliências' acima. Aumentar o resfriamento especificamente para saliências e pontes pode melhorar a qualidade geral de impressão desses recursos.\n" +"Use esta parte da velocidade da ventoinha de resfriamento ao imprimir pontes ou paredes salientes com um limite de saliência que exceda o valor definido no parâmetro 'Limiar de resfriamento de saliências' acima. Aumentar o resfriamento especificamente para saliências e pontes pode melhorar a qualidade geral de impressão desses recursos.\n" "\n" -"Observe que esta velocidade do ventilador é fixada na extremidade inferior pelo limiar mínimo de velocidade do ventilador definido acima. Ela também é ajustada para cima até o limiar máximo de velocidade do ventilador quando o limiar mínimo de tempo da camada não é atingido." +"Observe que esta velocidade da ventoinha é fixada na extremidade inferior pelo limiar mínimo de velocidade da ventoinha definido acima. Ela também é ajustada para cima até o limiar máximo de velocidade da ventoinha quando o limiar mínimo de tempo da camada não é atingido." msgid "Overhang cooling activation threshold" msgstr "Limiar de ativação de resfriamento de saliência" #, no-c-format, no-boost-format msgid "When the overhang exceeds this specified threshold, force the cooling fan to run at the 'Overhang Fan Speed' set below. This threshold is expressed as a percentage, indicating the portion of each line's width that is unsupported by the layer beneath it. Setting this value to 0% forces the cooling fan to run for all outer walls, regardless of the overhang degree." -msgstr "Quando a saliência excede esse limiar especificado, força o ventilador de resfriamento a funcionar na 'Velocidade da ventoinha de saliência' definida abaixo. Esse limiar é expresso como uma porcentagem, indicando a parte da largura de cada linha que não é suportada pela camada abaixo dela. Definir esse valor como 0% força o ventilador de resfriamento a funcionar para todas as paredes externas, independentemente do grau de saliência." +msgstr "Quando a saliência excede esse limiar especificado, força a ventoinha de resfriamento a funcionar na 'Velocidade da ventoinha de saliência' definida abaixo. Esse limiar é expresso como uma porcentagem, indicando a parte da largura de cada linha que não é suportada pela camada abaixo dela. Definir esse valor como 0% força a ventoinha de resfriamento a funcionar para todas as paredes externas, independentemente do grau de saliência." msgid "External bridge infill direction" msgstr "Direção de preenchimento de ponte externa" @@ -13026,11 +13017,9 @@ msgstr "" msgid "As object list" msgstr "Como lista de objetos" -# AI Translated msgid "Best of all (shortest path)" msgstr "Melhor de todas (caminho mais curto)" -# AI Translated msgid "Snake" msgstr "Serpentina" @@ -13038,7 +13027,7 @@ msgid "Slow printing down for better layer cooling" msgstr "Diminuir a velocidade de impressão para melhor resfriamento de camada" msgid "Enable this option to slow printing speed down to ensure that the final layer time is not shorter than the layer time threshold in \"Max fan speed threshold\", so that the layer can be cooled for a longer time. This can improve the quality for small details." -msgstr "Ative esta opção para diminuir a velocidade de impressão para que o tempo da camada final não seja menor do que o limiar de tempo da camada em \"Limiar de velocidade máxima do ventilador\", para que a camada possa ser resfriada um tempo mais longo. Isso pode melhorar a qualidade para detalhes pequenos." +msgstr "Ative esta opção para diminuir a velocidade de impressão para que o tempo da camada final não seja menor do que o limiar de tempo da camada em \"Limiar de velocidade máxima da ventoinha\", para que a camada possa ser resfriada um tempo mais longo. Isso pode melhorar a qualidade para detalhes pequenos." msgid "Normal printing" msgstr "Impressão normal" @@ -13093,16 +13082,16 @@ msgid "Enable this to override the fan speed set in custom G-code after print co msgstr "Habilite para substituir a velocidade da ventoinha definida no G-code personalizado após a conclusão da impressão." msgid "Speed of exhaust fan during printing. This speed will override the speed in filament custom G-code." -msgstr "Velocidade do ventilador de exaustão durante a impressão. Esta velocidade substituirá a velocidade no G-code personalizado do filamento." +msgstr "Velocidade da ventoinha de exaustão durante a impressão. Esta velocidade substituirá a velocidade no G-code personalizado do filamento." msgid "Speed of exhaust fan after printing completes." -msgstr "Velocidade do ventilador de exaustão após a conclusão da impressão." +msgstr "Velocidade da ventoinha de exaustão após a conclusão da impressão." msgid "No cooling for the first" msgstr "Sem resfriamento para as primeiras" msgid "Turn off all cooling fans for the first few layers. This can be used to improve build plate adhesion." -msgstr "Desligar todos os ventiladores de resfriamento para as primeiras camadas. Isso pode ser usado para obter uma melhor adesão à placa de impressão." +msgstr "Desligar todos as ventoinhas de resfriamento para as primeiras camadas. Isso pode ser usado para obter uma melhor adesão à placa de impressão." msgid "Don't support bridges" msgstr "Não suportar pontes" @@ -13278,11 +13267,9 @@ msgstr "Densidade da superfície superior" msgid "Density of top surface layer. A value of 100% creates a fully solid, smooth top layer. Reducing this value results in a textured top surface, according to the chosen top surface pattern. A value of 0% will result in only the walls on the top layer being created. Intended for aesthetic or functional purposes, not to fix issues such as over-extrusion." msgstr "Densidade da camada superior. Um valor de 100% cria uma camada superior totalmente sólida e lisa. Reduzir esse valor resulta em uma superfície superior texturizada, de acordo com o padrão de superfície superior escolhido. Um valor de 0% resultará na criação apenas das paredes da camada superior. Destinado a fins estéticos ou funcionais, não para corrigir problemas como extrusão excessiva." -# AI Translated msgid "Top surface expansion" msgstr "Expansão da superfície superior" -# AI Translated msgid "" "Expands the top surfaces by this distance to connect distinct top surfaces and fill gaps.\n" "Useful for cases where the top surface is interrupted by a raised feature, such as text on a plane. Expanding it removes the holes beneath these features and creates a continuous path with a better finish for printing on top. The expansion is applied to the original top surface, before any other processing such as bridging or overhang detection." @@ -13290,11 +13277,9 @@ msgstr "" "Expande as superfícies superiores por esta distância para conectar superfícies superiores distintas e preencher lacunas.\n" "Útil para casos em que a superfície superior é interrompida por um recurso elevado, como um texto sobre um plano. Expandi-la remove os buracos sob esses recursos e cria um caminho contínuo com melhor acabamento para imprimir por cima. A expansão é aplicada à superfície superior original, antes de qualquer outro processamento, como detecção de ponte ou de saliência." -# AI Translated msgid "Top expansion wall margin" msgstr "Margem de parede da expansão superior" -# AI Translated msgid "" "Using “Top surface expansion” may cause a surface that did not previously touch the model's outer walls to now do so.\n" "This can cause contraction marks (such as the hull line) on the outer walls.\n" @@ -13304,11 +13289,9 @@ msgstr "" "Isso pode causar marcas de contração (como a linha do casco) nas paredes externas.\n" "Ao adicionar uma pequena margem, essa contração não ocorrerá diretamente nas paredes, evitando assim uma marca visível." -# AI Translated msgid "Top expansion direction" msgstr "Direção da expansão superior" -# AI Translated msgid "" "Direction in which the top surface expansion grows.\n" " - Inward grows into the holes and gaps left by features rising from the middle of a top surface.\n" @@ -13335,11 +13318,9 @@ msgstr "Padrão de superfície inferior" msgid "This is the line pattern of bottom surface infill, not including bridge infill." msgstr "Este é o padrão de linha do preenchimento da superfície inferior, não incluindo o preenchimento de ponte." -# AI Translated msgid "Bottom surface density" msgstr "Densidade da superfície inferior" -# AI Translated msgid "" "Density of the bottom surface layer. Intended for aesthetic or functional purposes, not to fix issues such as over-extrusion.\n" "WARNING: Lowering this value may negatively affect bed adhesion." @@ -13347,31 +13328,27 @@ msgstr "" "Densidade da camada da superfície inferior. Destinada a fins estéticos ou funcionais, não a corrigir problemas como sobre-extrusão.\n" "AVISO: reduzir este valor pode afetar negativamente a aderência à mesa." -# AI Translated msgid "Top surface fill order" msgstr "Ordem de preenchimento da superfície superior" -# AI Translated msgid "" "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direção em que as superfícies superiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral Octograma).\n" +"Direção em que as superfícies superiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" "Para fora começa no centro da superfície, de modo que qualquer excesso de material seja empurrado em direção à borda, onde é menos visível. Para dentro começa na borda e termina com as curvas fechadas no centro.\n" "O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." -# AI Translated msgid "Bottom surface fill order" msgstr "Ordem de preenchimento da superfície inferior" -# AI Translated msgid "" "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direção em que as superfícies inferiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral Octograma).\n" +"Direção em que as superfícies inferiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" "Para dentro começa cada superfície com as curvas externas mais largas, o que melhora a aderência da primeira camada em mesas onde as curvas fechadas no centro podem não aderir. Para fora começa no centro, empurrando qualquer excesso de material em direção à borda.\n" "O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." @@ -13399,19 +13376,15 @@ msgstr "Limiar de pequenos perímetros" msgid "This sets the threshold for small perimeter length. Default threshold is 0mm." msgstr "Isso define o limiar para o comprimento do perímetro pequeno. O limiar padrão é 0 mm." -# AI Translated msgid "Small support perimeters" msgstr "Pequenos perímetros de suporte" -# AI Translated msgid "Same as \"Small perimeters\", but for supports. This separate setting will affect the speed of support for areas <= `small_support_perimeter_threshold`. If expressed as a percentage (for example: 80%), it will be calculated on the support or support interface speed setting above. Set to zero for auto." msgstr "Igual a \"Pequenos perímetros\", mas para suportes. Esta configuração separada afetará a velocidade do suporte para áreas <= `small_support_perimeter_threshold`. Se expressa como uma porcentagem (por exemplo: 80%), será calculada com base na configuração de velocidade de suporte ou de interface de suporte acima. Defina como zero para automático." -# AI Translated msgid "Small support perimeters threshold" -msgstr "Limite de pequenos perímetros de suporte" +msgstr "Limiar de pequenos perímetros de suporte" -# AI Translated msgid "This sets the threshold for small support perimeter length. The default threshold is 0mm." msgstr "Isto define o limite para o comprimento de pequenos perímetros de suporte. O limite padrão é 0mm." @@ -13603,7 +13576,6 @@ msgstr "" msgid "Enable adaptive pressure advance within features (beta)" msgstr "Habilitar pressure advance adaptativo nos recursos (beta)" -# AI Translated msgid "" "Enable adaptive PA whenever there are flow changes in a feature, such as line width changes in a corner or overhangs.\n" "\n" @@ -13635,10 +13607,10 @@ msgid "Default line width if other line widths are set to 0. If expressed as a % msgstr "Largura de linha padrão se outras larguras de linha estiverem definidas como 0. Se expresso como %, será calculado sobre o diâmetro do bico." msgid "Keep fan always on" -msgstr "Manter o ventilador sempre ligado" +msgstr "Manter a ventoinha sempre ligado" msgid "Enabling this setting means that part cooling fan will never stop entirely and will instead run at least at minimum speed to reduce the frequency of starting and stopping." -msgstr "Habilitar esta configuração significa que o ventilador de resfriamento da peça nunca será desligado completamente e funcionará pelo menos na velocidade mínima para reduzir a frequência de inícios e paradas." +msgstr "Habilitar esta configuração significa que a ventoinha de resfriamento da peça nunca será desligado completamente e funcionará pelo menos na velocidade mínima para reduzir a frequência de inícios e paradas." msgid "Don't slow down outer walls" msgstr "Não desacelerar as paredes externas" @@ -13658,7 +13630,7 @@ msgid "Layer time" msgstr "Tempo da camada" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." -msgstr "O ventilador de resfriamento de peças será ativado para camadas cujo tempo estimado seja mais curto que esse valor. A velocidade do ventilador é interpolada entre as velocidades mínima e máxima do ventilador de acordo com o tempo de impressão da camada." +msgstr "A ventoinha de resfriamento de peças será ativado para camadas cujo tempo estimado seja mais curto que esse valor. A velocidade da ventoinha é interpolada entre as velocidades mínima e máxima da ventoinha de acordo com o tempo de impressão da camada." msgid "s" msgstr "s" @@ -13706,7 +13678,6 @@ msgstr "Temperatura de purga" msgid "Temperature when flushing filament. 0 indicates the upper bound of the recommended nozzle temperature range." msgstr "Temperatura ao purgar filamento. 0 indica o limite superior da faixa de temperatura recomendada para o bico." -# AI Translated msgid "Flush temperature used in fast purge mode." msgstr "Temperatura de purga usada no modo de purga rápida." @@ -13972,11 +13943,9 @@ msgstr "Filamento imprimível" msgid "The filament is printable in extruder." msgstr "O filamento é imprimível na extrusora." -# AI Translated msgid "Filament-extruder compatibility" msgstr "Compatibilidade filamento-extrusora" -# AI Translated msgid "A single 32-bit int encoding the compatibility level of a filament across all extruders (up to 10). Every 3 bits represent one extruder (bits [3*i, 3*i+2] for extruder i). 0: printable, 1: error, 2: critical warning, 3: warning, 4-7: reserved." msgstr "Um único inteiro de 32 bits que codifica o nível de compatibilidade de um filamento em todas as extrusoras (até 10). Cada 3 bits representam uma extrusora (bits [3*i, 3*i+2] para a extrusora i). 0: imprimível, 1: erro, 2: aviso crítico, 3: aviso, 4-7: reservado." @@ -14016,11 +13985,9 @@ msgstr "Direção do preenchimento sólido" msgid "Angle for solid infill pattern, which controls the start or main direction of line." msgstr "Ângulo para padrão de preenchimento sólido, que controla a direção inicial ou principal da linha." -# AI Translated msgid "Top layer direction" msgstr "Direção da camada superior" -# AI Translated msgid "" "Fixed angle for the top solid infill and ironing lines.\n" "Set to -1 to follow the default solid infill direction." @@ -14028,11 +13995,9 @@ msgstr "" "Ângulo fixo para o preenchimento sólido superior e as linhas de alisamento.\n" "Defina como -1 para seguir a direção padrão do preenchimento sólido." -# AI Translated msgid "Bottom layer direction" msgstr "Direção da camada inferior" -# AI Translated msgid "" "Fixed angle for the bottom solid infill lines.\n" "Set to -1 to follow the default solid infill direction." @@ -14047,11 +14012,9 @@ msgstr "Densidade do preenchimento esparso" msgid "Density of internal sparse infill, 100% turns all sparse infill into solid infill and internal solid infill pattern will be used." msgstr "Densidade do preenchimento esparso interno, 100% transforma todo o preenchimento esparso em preenchimento sólido e será usado o padrão de preenchimento sólido interno." -# AI Translated msgid "Align directions to model" msgstr "Alinhar direções ao modelo" -# AI Translated msgid "" "Aligns infill, bridge, ironing, and top/bottom surface directions to follow the model's orientation on the build plate.\n" "When enabled, these directions rotate together with the model so the printed features keep their intended orientation relative to the part, preserving optimal strength and surface characteristics regardless of how the model is placed." @@ -14071,11 +14034,9 @@ msgstr "Multilinhas de Preenchimento" msgid "Using multiple lines for the infill pattern, if supported by infill pattern." msgstr "Usar múltiplas linhas para o padrão de preenchimento, se suportado pelo padrão de preenchimento." -# AI Translated msgid "Z-buckling bias optimization (experimental)" msgstr "Otimização de tendência à flambagem em Z (experimental)" -# AI Translated #, no-c-format, no-boost-format msgid "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." msgstr "Aperta a onda giroide ao longo do eixo Z (vertical) em baixa densidade de preenchimento para encurtar o comprimento efetivo da coluna vertical e melhorar a resistência à flambagem por compressão no eixo Z. O uso de filamento é preservado. Sem efeito em densidade de preenchimento esparso de ~30% ou mais. Aplica-se apenas quando o padrão de Preenchimento esparso está definido como Giroide." @@ -14198,13 +14159,12 @@ msgstr "Jerk para primeira camada." msgid "Jerk for travel." msgstr "Jerk para deslocamento." -# AI Translated msgid "" "Travel jerk of first layer.\n" "The percentage value is relative to Travel Jerk." msgstr "" "Jerk de deslocamento da primeira camada.\n" -"O valor percentual é relativo ao Jerk de deslocamento." +"O valor percentual é relativo ao Jerk de Deslocamento." msgid "Line width of the first layer. If expressed as a %, it will be computed over the nozzle diameter." msgstr "Largura da linha da primeira camada. Se expresso como uma %, será calculado sobre o diâmetro do bico." @@ -14243,10 +14203,10 @@ msgid "Nozzle temperature for printing the first layer with this filament" msgstr "Temperatura do bico para imprimir a primeira camada com este filamento" msgid "Full fan speed at layer" -msgstr "Velocidade total do ventilador na camada" +msgstr "Velocidade total da ventoinha na camada" msgid "Fan speed will be ramped up linearly from zero at layer \"close_fan_the_first_x_layers\" to maximum at layer \"full_fan_speed_layer\". \"full_fan_speed_layer\" will be ignored if lower than \"close_fan_the_first_x_layers\", in which case the fan will be running at maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." -msgstr "A velocidade do ventilador aumentará linearmente de zero na camada \"close_fan_the_first_x_layers\" para o máximo na camada \"full_fan_speed_layer\". \"full_fan_speed_layer\" será ignorado se for menor que \"close_fan_the_first_x_layers\", caso em que o ventilador funcionará na velocidade máxima permitida na camada \"close_fan_the_first_x_layers\" + 1." +msgstr "A velocidade da ventoinha aumentará linearmente de zero na camada \"close_fan_the_first_x_layers\" para o máximo na camada \"full_fan_speed_layer\". \"full_fan_speed_layer\" será ignorado se for menor que \"close_fan_the_first_x_layers\", caso em que a ventoinha funcionará na velocidade máxima permitida na camada \"close_fan_the_first_x_layers\" + 1." msgid "layer" msgstr "camada" @@ -14254,7 +14214,6 @@ msgstr "camada" msgid "First layer fan speed" msgstr "Velocidade da ventoinha na primeira camada" -# AI Translated msgid "" "Sets an exact fan speed for the first layer, overriding all other cooling settings. Useful for protecting 3D-printed toolhead parts (e.g. Voron-style ABS/ASA ducts) from a hot bed. A small amount of airflow cools the ducts down, without using full cooling that may in certain conditions hurt first-layer adhesion.\n" "From the second layer onwards, normal cooling resumes.\n" @@ -14262,44 +14221,44 @@ msgid "" "Only available when \"No cooling for the first\" is 0.\n" "Set to -1 to disable it." msgstr "" -"Define uma velocidade exata do ventilador para a primeira camada, substituindo todas as outras configurações de resfriamento. Útil para proteger peças impressas em 3D da cabeça da ferramenta (por exemplo, dutos ABS/ASA no estilo Voron) de uma mesa quente. Uma pequena quantidade de fluxo de ar resfria os dutos, sem usar o resfriamento total que pode, em certas condições, prejudicar a aderência da primeira camada.\n" +"Define uma velocidade exata da ventoinha para a primeira camada, substituindo todas as outras configurações de resfriamento. Útil para proteger peças impressas em 3D da cabeça da ferramenta (por exemplo, dutos ABS/ASA no estilo Voron) de uma mesa aquecida. Uma pequena quantidade de fluxo de ar resfria os dutos, sem usar o resfriamento total que pode, em certas condições, prejudicar a aderência da primeira camada.\n" "A partir da segunda camada, o resfriamento normal é retomado.\n" -"Se \"Velocidade total do ventilador na camada\" também estiver definida, o ventilador aumenta suavemente deste valor na primeira camada até o seu alvo na camada escolhida.\n" +"Se \"Velocidade total da ventoinha na camada\" também estiver definida, a ventoinha aumenta suavemente deste valor na primeira camada até o seu alvo na camada escolhida.\n" "Disponível apenas quando \"Sem resfriamento nas primeiras\" é 0.\n" "Defina como -1 para desativá-la." msgid "Support interface fan speed" -msgstr "Velocidade do ventilador para interface de suporte" +msgstr "Velocidade da ventoinha para interface de suporte" msgid "" "This part cooling fan speed is applied when printing support interfaces. Setting this parameter to a higher than regular speed reduces the layer binding strength between supports and the supported part, making them easier to separate.\n" "Set to -1 to disable it.\n" "This setting is overridden by disable_fan_first_layers." msgstr "" -"Esta velocidade do ventilador de resfriamento de peças é aplicada ao imprimir interfaces de suporte. Definir este parâmetro para uma velocidade maior que a normal reduz a força de adesão de camada entre os suportes e a peça suportada, tornando-os mais fáceis de separar.\n" +"Esta velocidade da ventoinha de resfriamento de peças é aplicada ao imprimir interfaces de suporte. Definir este parâmetro para uma velocidade maior que a normal reduz a força de adesão de camada entre os suportes e a peça suportada, tornando-os mais fáceis de separar.\n" "Defina como -1 para desabilitá-lo.\n" "Esta configuração é substituída por disable_fan_first_layers." msgid "Internal bridges fan speed" -msgstr "Velocidade do ventilador para pontes internas" +msgstr "Velocidade da ventoinha para pontes internas" msgid "" "The part cooling fan speed used for all internal bridges. Set to -1 to use the overhang fan speed settings instead.\n" "\n" "Reducing the internal bridges fan speed, compared to your regular fan speed, can help reduce part warping due to excessive cooling applied over a large surface for a prolonged period of time." msgstr "" -"A velocidade do ventilador de resfriamento de peças usada para todas as pontes internas. Defina como -1 para usar as configurações de velocidade do ventilador de sobreposição.\n" +"A velocidade da ventoinha de resfriamento de peças usada para todas as pontes internas. Defina como -1 para usar as configurações de velocidade da ventoinha de sobreposição.\n" "\n" -"Reduzir a velocidade do ventilador das pontes internas, em comparação com a velocidade normal do ventilador, pode ajudar a reduzir a deformação das peças devido ao resfriamento excessivo aplicado sobre uma grande superfície por um período prolongado de tempo." +"Reduzir a velocidade da ventoinha das pontes internas, em comparação com a velocidade normal da ventoinha, pode ajudar a reduzir a deformação das peças devido ao resfriamento excessivo aplicado sobre uma grande superfície por um período prolongado de tempo." msgid "Ironing fan speed" -msgstr "Velocidade do ventilador para alisamento" +msgstr "Velocidade da ventoinha para alisamento" msgid "" "This part cooling fan speed is applied when ironing. Setting this parameter to a lower than regular speed reduces possible nozzle clogging due to the low volumetric flow rate, making the interface smoother.\n" "Set to -1 to disable it." msgstr "" -"Esta velocidade do ventilador de resfriamento de peças é aplicada durante o alisamento. Definir este parâmetro para uma velocidade menor que a normal reduz a possibilidade de entupimento do bico devido ao baixa taxa de fluxo volumétrico, tornando a interface mais suave.\n" +"Esta velocidade da ventoinha de resfriamento de peças é aplicada durante o alisamento. Definir este parâmetro para uma velocidade menor que a normal reduz a possibilidade de entupimento do bico devido ao baixa taxa de fluxo volumétrico, tornando a interface mais suave.\n" "Defina como -1 para desabilitá-lo." msgid "Ironing flow" @@ -14584,7 +14543,7 @@ msgid "Best auto arranging position in range [0,1] w.r.t. bed shape." msgstr "Melhor posição de arranjo automático na faixa [0,1] em relação ao formato da mesa." msgid "Enable this option if machine has auxiliary part cooling fan. G-code command: M106 P2 S(0-255)." -msgstr "Habilitar esta opção se a máquina tiver ventilador auxiliar de resfriamento de peças. Comando G-code: M106 P2 S(0-255)." +msgstr "Habilitar esta opção se a máquina tiver ventoinha auxiliar de resfriamento de peças. Comando G-code: M106 P2 S(0-255)." msgid "Fan direction" msgstr "Direção da ventoinha" @@ -14592,7 +14551,6 @@ msgstr "Direção da ventoinha" msgid "Cooling fan direction of the printer" msgstr "Direção da ventoinha de resfriamento da impressora" -# AI Translated msgid "Both" msgstr "Ambos" @@ -14602,9 +14560,9 @@ msgid "" "It won't move fan commands into the start G-code if the 'only custom start G-code' is activated.\n" "Use 0 to deactivate." msgstr "" -"Ativar o ventilador este número de segundos antes do seu tempo de início alvo (você pode usar frações de segundos). Ele assume aceleração infinita para esta estimativa de tempo e levará em conta apenas os movimentos G1 e G0 (o ajuste de arco não é suportado).\n" -"Não moverá G-code de comandos do ventilador personalizados (eles funcionam como uma espécie de 'barreira').\n" -"Não moverá comandos do ventilador para o início do G-code se 'apenas G-code de início personalizado' estiver ativo.\n" +"Ativar a ventoinha este número de segundos antes do seu tempo de início alvo (você pode usar frações de segundos). Ele assume aceleração infinita para esta estimativa de tempo e levará em conta apenas os movimentos G1 e G0 (o ajuste de arco não é suportado).\n" +"Não moverá G-code de comandos da ventoinha personalizados (eles funcionam como uma espécie de 'barreira').\n" +"Não moverá comandos da ventoinha para o início do G-code se 'apenas G-code de início personalizado' estiver ativo.\n" "Use 0 para desativar." msgid "Only overhangs" @@ -14614,15 +14572,15 @@ msgid "Will only take into account the delay for the cooling of overhangs." msgstr "Levará em conta apenas o atraso para o resfriamento das saliências." msgid "Fan kick-start time" -msgstr "Tempo de inicialização do ventilador" +msgstr "Tempo de inicialização da ventoinha" msgid "" "Emit a max fan speed command for this amount of seconds before reducing to target speed to kick-start the cooling fan.\n" "This is useful for fans where a low PWM/power may be insufficient to get the fan started spinning from a stop, or to get the fan up to speed faster.\n" "Set to 0 to deactivate." msgstr "" -"Emita um comando de velocidade máxima do ventilador por esta quantidade de segundos antes de reduzir para a velocidade alvo para iniciar o ventilador de resfriamento.\n" -"Isto é útil para ventiladores onde um baixo PWM/potência pode ser insuficiente para fazer o ventilador começar a girar a partir de uma parada, ou para fazer o ventilador alcançar a velocidade mais rapidamente.\n" +"Emita um comando de velocidade máxima da ventoinha por esta quantidade de segundos antes de reduzir para a velocidade alvo para iniciar a ventoinha de resfriamento.\n" +"Isto é útil para ventoinhas onde um baixo PWM/potência pode ser insuficiente para fazer a ventoinha começar a girar a partir de uma parada, ou para fazer a ventoinha alcançar a velocidade mais rapidamente.\n" "Defina como 0 para desativar." msgid "Minimum non-zero part cooling fan speed" @@ -14830,19 +14788,15 @@ msgstr "Ângulo de saliência do preenchimento" msgid "The angle of the infill angled lines. 60° will result in a pure honeycomb." msgstr "O ângulo das linhas de preenchimento. 60° resultará em um favo de mel puro." -# AI Translated msgid "Lightning overhang angle" -msgstr "Ângulo de saliência Relâmpago" +msgstr "Ângulo de saliência de Relâmpago" -# AI Translated msgid "Maximum overhang angle for Lightning infill support propagation." msgstr "Ângulo máximo de saliência para a propagação de suporte do preenchimento Relâmpago." -# AI Translated msgid "Prune angle" msgstr "Ângulo de poda" -# AI Translated msgid "" "Controls how aggressively short or unsupported Lightning branches are pruned.\n" "This angle is converted internally to a per-layer distance." @@ -14850,11 +14804,9 @@ msgstr "" "Controla a agressividade com que os ramos Relâmpago curtos ou sem suporte são podados.\n" "Este ângulo é convertido internamente em uma distância por camada." -# AI Translated msgid "Straightening angle" msgstr "Ângulo de retificação" -# AI Translated msgid "Maximum straightening angle used to simplify Lightning branches." msgstr "Ângulo máximo de retificação usado para simplificar os ramos Relâmpago." @@ -15205,7 +15157,7 @@ msgstr "Força máxima do eixo Y" msgid "The allowed maximum output force of Y axis" msgstr "A força máxima de saída permitida do eixo Y" -# AI Translated +#, fuzzy msgid "N" msgstr "N" @@ -15215,6 +15167,7 @@ msgstr "Massa da mesa do eixo Y" msgid "The machine bed mass load of Y axis" msgstr "A carga de massa da mesa do equipamento no eixo Y" +#, fuzzy msgid "g" msgstr "G" @@ -15369,7 +15322,7 @@ msgstr "" "Para desativar o modelador de entrada, use o tipo Desativar." msgid "The part cooling fan speed may be increased when auto cooling is enabled. This is the maximum speed for the part cooling fan." -msgstr "A velocidade do ventilador de resfriamento de peças pode ser aumentada quando o resfriamento automático está habilitado. Este é o limite máximo de velocidade do ventilador de resfriamento de peças." +msgstr "A velocidade da ventoinha de resfriamento de peças pode ser aumentada quando o resfriamento automático está habilitado. Este é o limite máximo de velocidade da ventoinha de resfriamento de peças." msgid "The highest printable layer height for the extruder. Used to limit the maximum layer height when enable adaptive layer height." msgstr "A maior altura de camada imprimível para a extrusora. Usada para limitar a altura máxima da camada quando a altura da camada adaptativa está ativada." @@ -15432,31 +15385,31 @@ msgid "Applies extrusion rate smoothing only on external perimeters and overhang msgstr "Aplica suavização de taxa de extrusão somente em perímetros externos e saliências. Isso pode ajudar a reduzir artefatos devido a transições de velocidade bruscas em saliências visíveis externamente sem impactar a velocidade de impressão de recursos que não serão visíveis ao usuário." msgid "Minimum speed for part cooling fan." -msgstr "Velocidade mínima para o ventilador de resfriamento de peças." +msgstr "Velocidade mínima para a ventoinha de resfriamento de peças." msgid "" "Speed of auxiliary part cooling fan. Auxiliary fan will run at this speed during printing except the first several layers which is defined by no cooling layers.\n" "Please enable auxiliary_fan in printer settings to use this feature. G-code command: M106 P2 S(0-255)" msgstr "" -"Velocidade do ventilador auxiliar de resfriamento de peças. O ventilador auxiliar funcionará nesta velocidade durante a impressão, exceto nas primeiras camadas, que são definidas por camadas sem resfriamento.\n" +"Velocidade da ventoinha auxiliar de resfriamento de peças. A ventoinha auxiliar funcionará nesta velocidade durante a impressão, exceto nas primeiras camadas, que são definidas por camadas sem resfriamento.\n" "\n" -"Por favor, habilite o ventilador auxiliar nas configurações da impressora para usar esta função. Comando G-code: M106 P2 S(0-255)" +"Por favor, habilite a ventoinha auxiliar nas configurações da impressora para usar esta função. Comando G-code: M106 P2 S(0-255)" msgid "For the first" msgstr "Para as primeiras" msgid "Set special auxiliary cooling fan for the first certain layers." -msgstr "Definir um ventilador auxiliar de resfriamento específico para as primeiras camadas." +msgstr "Definir uma ventoinha auxiliar de resfriamento específico para as primeiras camadas." msgid "" "Auxiliary fan speed will be ramped up linearly from layer \"For the first\" to maximum at layer \"Full fan speed at layer\".\n" "\"Full fan speed at layer\" will be ignored if lower than \"For the first\", in which case the fan will run at maximum allowed speed at layer \"For the first\" + 1." msgstr "" -"A velocidade do ventilador auxiliar aumentará linearmente da camada \"Para as primeiras\" até o máximo na camada \"Velocidade total do ventilador na camada\".\n" -"A \"Velocidade total do ventilador na camada\" será ignorada se for menor que \"Para as primeiras\", caso em que o ventilador funcionará na velocidade máxima permitida na camada \"Para as primeiras\" + 1." +"A velocidade da ventoinha auxiliar aumentará linearmente da camada \"Para as primeiras\" até o máximo na camada \"Velocidade total da ventoinha na camada\".\n" +"A \"Velocidade total da ventoinha na camada\" será ignorada se for menor que \"Para as primeiras\", caso em que a ventoinha funcionará na velocidade máxima permitida na camada \"Para as primeiras\" + 1." msgid "Special auxiliary cooling fan speed, effective only for the first x layers." -msgstr "Velocidade especial do ventilador de resfriamento auxiliar, efetiva apenas para as primeiras x camadas." +msgstr "Velocidade especial da ventoinha de resfriamento auxiliar, efetiva apenas para as primeiras x camadas." msgid "The lowest printable layer height for the extruder. Used to limit the minimum layer height when enable adaptive layer height." msgstr "A menor altura de camada imprimível para a extrusora. Usada para limitar a altura mínima da camada ao habilitar a altura de camada adaptativa." @@ -15621,11 +15574,9 @@ msgstr "Este G-code é inserido quando a função de extrusão é trocada. Ele msgid "Plugins Used" msgstr "Plugins Utilizados" -# AI Translated msgid "Plugin capabilities referenced by this preset, stored as name;uuid;capability." -msgstr "Recursos de plugin referenciados por esta predefinição, armazenados como name;uuid;capability." +msgstr "Capacidades de plugin referenciados por esta predefinição, armazenados como name;uuid;capability." -# AI Translated msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental." msgstr "Plugin(s) Python invocado(s) em cada etapa do pipeline de fatiamento para ler e modificar dados intermediários de fatiamento, incluindo uma etapa final de pós-processamento do G-code. Pesquisa/experimental." @@ -16243,11 +16194,9 @@ msgstr "Preparar todas as extrusoras de impressão" msgid "If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print." msgstr "Se ativado, todos as extrusoras de impressão serão preparados na borda frontal da mesa de impressão no início da impressão." -# AI Translated msgid "Toolchange ordering" msgstr "Ordenação de troca de ferramenta" -# AI Translated msgid "" "Determines the order of tool changes on each layer.\n" "- Default: Starts with the last used extruder to minimize tool changes.\n" @@ -16257,7 +16206,6 @@ msgstr "" "- Padrão: começa com a última extrusora usada para minimizar as trocas de ferramenta.\n" "- Cíclico: usa uma sequência fixa de ferramentas em cada camada. Isso sacrifica a velocidade em prol de uma melhor qualidade de superfície, pois as trocas de ferramenta extras dão mais tempo para as camadas resfriarem." -# AI Translated msgid "Cyclic" msgstr "Cíclico" @@ -16638,7 +16586,6 @@ msgstr "" "\n" "Se habilitado, este parâmetro também define uma variável G-code chamada chamber_temperature, que pode ser usada para passar a temperatura desejada da câmara para sua macro de início de impressão ou uma macro de absorção de calor como esta: PRINT_START (outras variáveis) CHAMBER_TEMP=[chamber_temperature]. Isso pode ser útil se sua impressora não suportar comandos M141/M191 ou se você desejar lidar com a absorção de calor na macro de início de impressão se nenhum aquecedor de câmara ativo estiver instalado." -# AI Translated msgid "" "This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n" "\n" @@ -16646,11 +16593,11 @@ msgid "" "\n" "Unlike the \"Target\" chamber temperature, this option does not emit any M141/M191 commands; it only exposes the value to your custom G-code. It should not exceed the \"Target\" chamber temperature." msgstr "" -"Esta é a temperatura da câmara na qual a impressão deve começar, enquanto a câmara continua aquecendo em direção à temperatura da câmara \"Alvo\". Por exemplo, defina o Alvo como 60 e o Mínimo como 50 para começar a imprimir assim que a câmara atingir 50℃, sem esperar pelos 60℃ completos.\n" +"Esta é a temperatura da câmara na qual a impressão deve começar, enquanto a câmara continua aquecendo em direção à temperatura \"Alvo\" da câmara. Por exemplo, defina o Alvo como 60 e o Mínimo como 50 para começar a imprimir assim que a câmara atingir 50℃, sem esperar pelos 60℃ completos.\n" "\n" "Isso define uma variável de G-code chamada chamber_minimal_temperature, que pode ser passada para a sua macro de início de impressão ou uma macro de aquecimento prolongado, assim: PRINT_START (outras variáveis) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n" "\n" -"Ao contrário da temperatura da câmara \"Alvo\", esta opção não emite nenhum comando M141/M191; ela apenas expõe o valor ao seu G-code personalizado. Não deve exceder a temperatura da câmara \"Alvo\"." +"Ao contrário da temperatura \"Alvo\" da câmara, esta opção não emite nenhum comando M141/M191; ela apenas expõe o valor ao seu G-code personalizado. Não deve exceder a temperatura \"Alvo\" da câmara." msgid "Chamber minimal temperature" msgstr "Temperatura mínima da câmara" @@ -16694,20 +16641,18 @@ msgstr "Espessura da casca do topo" msgid "The number of top solid layers is increased when slicing if the thickness calculated by top shell layers is thinner than this value. This can avoid having too thin a shell when layer height is small. 0 means that this setting is disabled and thickness of top shell is determined simply by the number of top shell layers." msgstr "O número de camadas sólidas superiores é aumentado ao fatiar se a espessura calculada pelas camadas da casca do topo for menor do que este valor. Isso pode evitar que a casca seja muito fina quando a altura da camada é pequena. 0 significa que esta configuração está desativada e a espessura da casca do topo é determinada apenas pelo número de camadas da casca do topo." -# AI Translated msgid "Separated infills" msgstr "Preenchimentos separados" -# AI Translated msgid "" "Centers the internal infill of each part on itself, as if it were sliced on its own, instead of on the whole assembly. Parts that touch or overlap are treated as one body and share a center; separate parts (or distinct 3D objects) each get their own.\n" "Useful when an assembly groups several objects that should each keep a consistent, self-centered infill.\n" "Affects line and grid patterns and rotation-template infills.\n" "Patterns locked to global coordinates (Gyroid, Honeycomb, TPMS, ...) are unaffected." msgstr "" -"Centraliza o preenchimento interno de cada peça em si mesma, como se fosse fatiada isoladamente, em vez de no conjunto inteiro. Peças que se tocam ou se sobrepõem são tratadas como um único corpo e compartilham um centro; peças separadas (ou objetos 3D distintos) recebem cada uma o seu próprio.\n" +"Centraliza o preenchimento interno de cada peça em si mesmo, como se fosse fatiada isoladamente, em vez de no conjunto inteiro. Peças que se tocam ou se sobrepõem são tratadas como um único corpo e compartilham um centro; peças separadas (ou objetos 3D distintos) recebem cada uma o seu próprio.\n" "Útil quando um conjunto agrupa vários objetos que devem manter, cada um, um preenchimento consistente e autocentrado.\n" -"Afeta os padrões de linha e grade e os preenchimentos com modelo de rotação.\n" +"Afeta os padrões de linha e grade e os preenchimentos com gabarito de rotação.\n" "Os padrões fixados em coordenadas globais (Giroide, Favo de mel, TPMS, ...) não são afetados." msgid "Center surface pattern on" @@ -16776,11 +16721,9 @@ msgstr "Multiplicador de purga" msgid "The actual flushing volumes is equal to the flush multiplier value multiplied by the flushing volumes in the table." msgstr "Os volumes de purga reais são iguais ao multiplicador de purga multiplicado pelos volumes de purga na tabela." -# AI Translated msgid "Flush multiplier (Fast mode)" msgstr "Multiplicador de purga (Modo rápido)" -# AI Translated msgid "The flush multiplier used in fast purge mode." msgstr "O multiplicador de purga usado no modo de purga rápida." @@ -16790,13 +16733,12 @@ msgstr "Volume de preparo" msgid "This is the volume of material to prime the extruder with on the tower." msgstr "Este é o volume de material para preparar a extrusora na torre." -# AI Translated +#,fuzzy msgid "Prime volume mode" msgstr "Modo de volume de preparação" -# AI Translated msgid "Selects how the wipe-tower prime and flush volumes are computed on multi-extruder printers." -msgstr "Seleciona como os volumes de preparação e de purga da torre de purga são calculados em impressoras com várias extrusoras." +msgstr "Seleciona como os volumes de preparação e de purga da torre de purga são computados em impressoras com múltiplas extrusoras." msgid "Saving" msgstr "Salvando" @@ -17116,7 +17058,7 @@ msgid "The maximum volumetric speed for ramming before extruder change, where -1 msgstr "A velocidade volumétrica máxima para compactação antes da troca de extrusor, onde -1 significa usar a velocidade volumétrica máxima." msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. Note: only a cooldown command and fan activation are triggered, reaching the target temperature is not guaranteed. 0 means disabled." -msgstr "Para evitar o gotejamento, a temperatura do bico será reduzida durante a compactação. Nota: somente um comando de resfriamento e ativação do ventilador são acionados, não sendo garantido o alcance da temperatura alvo. 0 significa desativado." +msgstr "Para evitar o gotejamento, a temperatura do bico será reduzida durante a compactação. Nota: somente um comando de resfriamento e ativação da ventoinha são acionados, não sendo garantido o alcance da temperatura alvo. 0 significa desativado." msgid "The maximum volumetric speed for ramming before a hotend change, where -1 means using the maximum volumetric speed." msgstr "A velocidade volumétrica máxima para compactação antes de uma troca de hotend, em que -1 significa usar a velocidade volumétrica máxima." @@ -20744,8 +20686,8 @@ msgid "" "Auxiliary fan\n" "Did you know that OrcaSlicer supports Auxiliary part cooling fan?" msgstr "" -"Ventilador auxiliar\n" -"Você sabia que o OrcaSlicer suporta ventilador auxiliar de resfriamento de peças?" +"Ventoinha auxiliar\n" +"Você sabia que o OrcaSlicer suporta ventoinha auxiliar de resfriamento de peças?" #: resources/data/hints.ini: [hint:Air filtration] msgid "" @@ -21939,7 +21881,7 @@ msgstr "" #~ msgstr "Pausado devido à perda do AMS" #~ msgid "Paused due to low speed of the heat break fan" -#~ msgstr "Pausado devido a baixa velocidade do ventilador do bloco de aquecimento" +#~ msgstr "Pausado devido a baixa velocidade da ventoinha do bloco de aquecimento" #~ msgid "Paused due to chamber temperature control error" #~ msgstr "Pausado devido a erro no controle de temperatura da câmara" @@ -22468,20 +22410,20 @@ msgstr "" #~ msgstr "Forçar resfriamento para saliências e pontes" #~ msgid "Enable this option to optimize part cooling fan speed for overhang and bridge to get better cooling" -#~ msgstr "Ative esta opção para otimizar a velocidade do ventilador de resfriamento de peças para saliência e ponte para obter melhor resfriamento" +#~ msgstr "Ative esta opção para otimizar a velocidade da ventoinha de resfriamento de peças para saliência e ponte para obter melhor resfriamento" #~ msgid "Fan speed for overhang" -#~ msgstr "Velocidade do ventilador para saliência" +#~ msgstr "Velocidade da ventoinha para saliência" #~ msgid "Force part cooling fan to be this speed when printing bridge or overhang wall which has large overhang degree. Forcing cooling for overhang and bridge can get better quality for these part" -#~ msgstr "Forçar o ventilador de resfriamento de peças a ser nesta velocidade ao imprimir ponte ou parede saliente que tenha um grande grau de saliência. Forçar o resfriamento para saliência e ponte pode obter melhor qualidade para estas partes" +#~ msgstr "Forçar a ventoinha de resfriamento de peças a ser nesta velocidade ao imprimir ponte ou parede saliente que tenha um grande grau de saliência. Forçar o resfriamento para saliência e ponte pode obter melhor qualidade para estas partes" #~ msgid "Cooling overhang threshold" #~ msgstr "Limiar de resfriamento de saliência" #, c-format #~ msgid "Force cooling fan to be specific speed when overhang degree of printed part exceeds this value. Expressed as percentage which indicates how much width of the line without support from lower layer. 0% means forcing cooling for all outer wall no matter how much overhang degree" -#~ msgstr "Forçar o ventilador de resfriamento a ser uma velocidade específica quando o grau de saliência das peças impressa excede este valor. Expresso como porcentagem, que indica quanto da largura da linha sem suporte da camada inferior. Zero significa forçar o resfriamento para toda a parede externa, não importa quanto seja o grau de saliência" +#~ msgstr "Forçar a ventoinha de resfriamento a ser uma velocidade específica quando o grau de saliência das peças impressa excede este valor. Expresso como porcentagem, que indica quanto da largura da linha sem suporte da camada inferior. Zero significa forçar o resfriamento para toda a parede externa, não importa quanto seja o grau de saliência" #~ msgid "Density of external bridges. 100% means solid bridge. Default is 100%." #~ msgstr "Densidade de pontes externas. 100% significa ponte sólida. O padrão é 100%." From f529692ac0eb0f224f13b1e5f21e27744028db18 Mon Sep 17 00:00:00 2001 From: Robert J Audas Date: Sun, 16 Aug 2026 20:40:50 -0600 Subject: [PATCH 49/71] Fix slowdown for caged external overhangs (#14735) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Co-authored-by: Ian Bassi --- src/libslic3r/GCode/ExtrusionProcessor.hpp | 148 ++++++- tests/fff_print/CMakeLists.txt | 1 + tests/fff_print/test_extrusion_processor.cpp | 441 +++++++++++++++++++ 3 files changed, 569 insertions(+), 21 deletions(-) create mode 100644 tests/fff_print/test_extrusion_processor.cpp diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index b282af8f4e..1d65e83f3e 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,11 @@ std::vector> estimate_points_properties(const POINTS& const AABBTreeLines::LinesDistancer& unscaled_prev_layer, float flow_width, float max_line_length = -1.0f, - float min_distance = -1.0f) + float min_distance = -1.0f, + // Maps an overhang distance onto the speed it will be printed at. Interior sampling + // needs it to tell which of the points it could add would change the G-code, and is + // skipped without it. + const std::function& distance_to_speed = {}) { bool looped = input_points.front() == input_points.back(); std::function get_prev_index = [](size_t idx, size_t count) { @@ -120,6 +125,107 @@ std::vector> estimate_points_properties(const POINTS& points.push_back(next_point); } + // ORCA: Interior sampling + // The passes below infer the support under a span from its endpoints alone, so an interior that is supported + // differently from both ends is invisible to them: the outer perimeter of an overhang whose ends are caged by + // full height walls reads as supported along its whole length. Probe the interior, keep the samples the + // endpoint interpolation fails to predict, and bisect either side of each one, so a span that is only partly + // unsupported gets points where its support actually changes instead of one reading spread across all of it. + if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && min_distance > 0 && distance_to_speed) { + // Probe at least this densely before treating matching samples as evidence that a span is uniform. The + // segmentation pass below only splits lines of 2mm or more, and every pass here drops points closer + // together than min_spacing, so finer discovery would not produce a more precise speed transition. + const double max_probe_spacing = std::max(2., 4. * min_spacing); + // A backstop for that length test, which on a non-finite length would never be met. + constexpr int max_bisection_depth = 10; + // Whether two readings are interchangeable. A segment is printed at the lower of the speeds its ends + // read, so a sample that agrees on speed with what is already known cannot change the G-code, whatever + // its distance says. The distances themselves are far too coarse a stand-in for this: the speed sections + // interpolate, so readings a small fraction of min_distance apart can still be tens of mm/s apart. + // The tolerance matches the one GCode.cpp applies when it decides a path has a variable speed at all. + auto same_speed = [&distance_to_speed](float a, float b) { + return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f; + }; + // Whether the first reading is printed slower than the second, once they are known to differ. + auto prints_slower = [&distance_to_speed](float a, float b) { return distance_to_speed(a) < distance_to_speed(b); }; + + // Part of a segment still to bisect: its positions along the segment and bisections left. + struct Subspan { double t0, t1; int depth; }; + + std::vector> sampled_points; // Populated lazily, on the first insertion + std::vector> interior; // Samples of one segment, keyed by position along it + std::vector pending; + + for (size_t point_idx = 0; point_idx + 1 < points.size(); ++point_idx) { + const ExtendedPoint& curr = points[point_idx]; + const ExtendedPoint& next = points[point_idx + 1]; + const Vec step = next.position - curr.position; + const double line_len = step.norm(); + + interior.clear(); + if (line_len >= max_probe_spacing) + pending.push_back({0., 1., max_bisection_depth}); + + while (!pending.empty()) { + const Subspan subspan = pending.back(); + pending.pop_back(); + if (subspan.depth <= 0 || (subspan.t1 - subspan.t0) * line_len < max_probe_spacing) + continue; + + const double t = 0.5 * (subspan.t0 + subspan.t1); + auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra( + (curr.position + t * step).template cast()); + const float sampled = float(distance + boundary_offset); + + interior.emplace_back(t, sampled); + pending.push_back({subspan.t0, t, subspan.depth - 1}); + pending.push_back({t, subspan.t1, subspan.depth - 1}); + } + + if (!interior.empty()) { + std::sort(interior.begin(), interior.end(), + [](const std::pair& l, const std::pair& r) { return l.first < r.first; }); + // Coarse probing keeps every sample it took until this pass can see which ones bracket a speed + // transition. Matching samples cannot be discarded during discovery: one may be the last + // supported point before a narrow unsupported pocket found by a later probe. + size_t kept = 0; + for (size_t i = 0; i < interior.size(); ++i) { + const float sample = interior[i].second; + const bool at_start = kept == 0; // Nothing kept yet, so the segment's own start precedes it + const bool at_end = i + 1 == interior.size(); // And nothing follows the last sample but the segment's end + const float before = at_start ? curr.distance : interior[kept - 1].second; + const float after = at_end ? next.distance : interior[i + 1].second; + // A sample is worth a point in the path only where it prints at a different speed from the + // readings either side of it. Differing from one of the segment's own ends is not enough on + // its own where the sample is the faster of the two: the segmentation pass below already + // ends the slowdown an end reads, at a distance taken from how far out that end is rather + // than from wherever bisection happened to stop, and a point here would leave the span + // beside the end too short for that pass to run at all. Support an end cannot account for, + // where the interior is the slower reading, is exactly what this pass is here to find. + const bool worth_before = !same_speed(sample, before) && (!at_start || prints_slower(sample, before)); + const bool worth_after = !same_speed(sample, after) && (!at_end || prints_slower(sample, after)); + if (worth_before || worth_after) + interior[kept++] = interior[i]; + } + interior.resize(kept); + } + + if (!interior.empty() && sampled_points.empty()) { + sampled_points.reserve(points.size() + 8); + sampled_points.assign(points.begin(), points.begin() + point_idx + 1); + } + if (!sampled_points.empty()) { + // Only a sub-span of max_probe_spacing or more is ever bisected, so these sit at least + // 2 * min_spacing apart, and need none of the filtering the passes either side of this one do. + for (const auto& [t, distance] : interior) + sampled_points.push_back({curr.position + t * step, distance}); + sampled_points.push_back(next); + } + } + if (!sampled_points.empty()) + points = std::move(sampled_points); + } + // Segmentation handling if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS) { std::vector> new_points; @@ -362,9 +468,28 @@ public: smallest_distance_with_lower_speed=-1.f; // Orca: Pass to the point properties estimator the smallest ovehang distance that triggers a slowdown (smallest_distance_with_lower_speed) + auto calculate_speed = [&speed_sections, &original_speed](float distance) { + float final_speed; + if (distance <= speed_sections.front().first) { + final_speed = original_speed; + } else if (distance >= speed_sections.back().first) { + final_speed = speed_sections.back().second; + } else { + size_t section_idx = 0; + while (distance > speed_sections[section_idx + 1].first) { + section_idx++; + } + float t = (distance - speed_sections[section_idx].first) / + (speed_sections[section_idx + 1].first - speed_sections[section_idx].first); + t = std::clamp(t, 0.0f, 1.0f); + final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second; + } + return round(final_speed); + }; + std::vector> extended_points = estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1, - smallest_distance_with_lower_speed); + smallest_distance_with_lower_speed, calculate_speed); const auto width_inv = 1.0f / path.width; std::vector processed_points; processed_points.reserve(extended_points.size()); @@ -423,25 +548,6 @@ public: } } - auto calculate_speed = [&speed_sections, &original_speed](float distance) { - float final_speed; - if (distance <= speed_sections.front().first) { - final_speed = original_speed; - } else if (distance >= speed_sections.back().first) { - final_speed = speed_sections.back().second; - } else { - size_t section_idx = 0; - while (distance > speed_sections[section_idx + 1].first) { - section_idx++; - } - float t = (distance - speed_sections[section_idx].first) / - (speed_sections[section_idx + 1].first - speed_sections[section_idx].first); - t = std::clamp(t, 0.0f, 1.0f); - final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second; - } - return round(final_speed); - }; - float extrusion_speed = std::min(calculate_speed(curr.distance), calculate_speed(next.distance)); // ORCA: Clamp resulting speed to lowest of calculated speed based on the overhang values and the current speed // Fixes bug where resulting overhang speed is higher than the current speed due to (for example) volumetric flow limits. diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 08f86de8a7..43afd4281d 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(${_TEST_NAME}_tests test_helpers.hpp test_cooling.cpp test_extrusion_entity.cpp + test_extrusion_processor.cpp test_fill.cpp test_flow.cpp test_gcode_timing.cpp diff --git a/tests/fff_print/test_extrusion_processor.cpp b/tests/fff_print/test_extrusion_processor.cpp new file mode 100644 index 0000000000..76e331d66a --- /dev/null +++ b/tests/fff_print/test_extrusion_processor.cpp @@ -0,0 +1,441 @@ +#include + +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/GCode/ExtrusionProcessor.hpp" +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/TriangleMesh.hpp" + +#include "test_helpers.hpp" + +#include +#include +#include +#include + +using namespace Slic3r; +using namespace Slic3r::Test; + +namespace { + +// Print settings the assertions below are derived from. +constexpr double caged_layer_height = 0.2; // mm +constexpr double caged_wall_width = 0.42; // mm, outer wall line width +constexpr double caged_outer_wall_speed = 200.; // mm/s +constexpr double caged_slow_speed = 100.; // mm/s, between every configured overhang speed (<= 50) and the wall speed + +// A wall running 0.2mm out over a previous layer whose edge dishes 0.03mm away from it in the middle, +// standing in for the endpoint readings a caged overhang perimeter takes: enough of a difference to +// print at another speed, but only a fraction of the distance at which slowdown begins. +constexpr double dished_wall_gap = 0.2; // mm, how far the wall runs out past the previous layer's edge +constexpr double dished_layer_depth = 0.03; // mm, how much further out the middle of it reads +constexpr double dished_min_distance = 0.042; // mm, the reading at which the configured speeds begin to slow down +// Every reading here is past that, so the whole wall is slowed and only the amount is in question. +constexpr float dished_end_reading = float(dished_wall_gap + 0.5 * caged_wall_width); +constexpr float dished_mid_reading = float(dished_end_reading + dished_layer_depth); +// The two readings are dished_layer_depth apart, so half of that tells them apart while still allowing +// for the points the passes after sampling add, which read a little further out than the ends do. +constexpr double dished_reading_tolerance = 0.5 * dished_layer_depth; + +// A 40 x 20 x 20 mm box with a 45 degree overhang cut into the y = 0 side. The sloped face spans +// x = 5.086 .. 34.914 only, so the full-height walls of the box cage both ends of every overhang +// perimeter: the endpoints look supported even though the span between them is not. +TriangleMesh caged_overhang_mesh() +{ + return TriangleMesh( + { + {5.0859987f, 10.167065f, 5.711731f}, {34.914257f, 10.167065f, 5.711731f}, + {34.914257f, 0.f, 15.878796f}, {5.0859995f, 0.f, 15.878796f}, + {0.f, 0.f, 0.f}, {0.f, 0.f, 20.f}, + {0.f, 20.f, 20.f}, {0.f, 20.f, 0.f}, + {40.f, 20.f, 20.f}, {40.f, 20.f, 0.f}, + {40.f, 0.f, 20.f}, {40.f, 0.f, 0.f}, + {34.914257f, 0.f, 0.f}, {5.0859995f, 0.f, 0.f}, + {34.914257f, 10.167065f, 0.f}, {5.0859995f, 10.167065f, 0.f}, + }, + { + {0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7}, {7, 6, 8}, {7, 8, 9}, + {9, 8, 10}, {9, 10, 11}, {12, 11, 10}, {5, 4, 13}, {5, 13, 3}, {2, 12, 10}, + {5, 3, 2}, {10, 5, 2}, {9, 11, 12}, {9, 12, 14}, {13, 4, 7}, {9, 14, 15}, + {15, 13, 7}, {7, 9, 15}, {8, 6, 5}, {8, 5, 10}, {14, 1, 0}, {14, 0, 15}, + {2, 1, 14}, {2, 14, 12}, {15, 0, 3}, {15, 3, 13}, + }); +} + +// Mesh geometry the wall filters below are derived from. +constexpr double caged_box_depth = 20.; // mm, the box spans y = 0 .. 20 +constexpr double caged_slope_face_sum = 15.878796; // mm, y + z of the sloped face, from its corners +// The sloped face spans this x range; outside it the box walls run full height. +constexpr double caged_slope_x_min = 5.0859995; +constexpr double caged_slope_x_max = 34.914257; +constexpr double caged_slope_span = caged_slope_x_max - caged_slope_x_min; // ~29.8 mm +// The z range the sloped face occupies, from the same fixture vertices. +constexpr double caged_slope_z_min = 5.711731; +constexpr double caged_slope_z_max = 15.878796; +// The lowest slope layer still sits on the solid body below the notch, so it is fully supported and +// runs at the outer wall speed by design. The caged span proper begins one layer above it. +constexpr double caged_span_z_min = caged_slope_z_min + caged_layer_height; + +// A layer printed at z is sliced at z - layer_height / 2, and the outer wall centreline sits half a +// line width inside the contour, so the wall on the slope satisfies y + z = 16.189. +constexpr double caged_slope_wall_sum = caged_slope_face_sum + 0.5 * caged_layer_height + 0.5 * caged_wall_width; +// Same inset on the fully supported y = 20 face, vertical over the whole height. +constexpr double caged_back_wall_y = caged_box_depth - 0.5 * caged_wall_width; +// And on the y = 0 face, which runs full height only outside the slope's x range. +constexpr double caged_front_wall_y = 0.5 * caged_wall_width; +// Arachne varies the wall width along a face, and the centreline inset is half that width, so a +// wall sits within about half a line width of where the nominal inset alone would put it. The +// faces being selected are millimetres apart, so this stays far from ambiguous. +constexpr double caged_wall_tolerance = 0.5 * caged_wall_width; + +// Feed rates in mm/min of the long outer wall extrusions `keep_line` selects. +template std::vector outer_wall_feed_rates(const std::string& gcode, KeepLine keep_line) +{ + std::vector feed_rates; + bool outer_wall = false; + GCodeReader parser; + parser.parse_buffer(gcode, [&feed_rates, &outer_wall, &keep_line](GCodeReader& self, const GCodeReader::GCodeLine& line) { + const std::string_view comment = line.comment(); + if (comment.find("FEATURE:") != std::string_view::npos || comment.find("TYPE:") != std::string_view::npos) + outer_wall = comment.find("Outer wall") != std::string_view::npos || + comment.find("External perimeter") != std::string_view::npos; + + if (outer_wall && line.extruding(self) && line.dist_XY(self) > 1.0 && keep_line(self, line)) + feed_rates.push_back(line.new_F(self)); + }); + + return feed_rates; +} + +// The caged 45 degree overhang: outer walls crossing the sloped face for most of its width, on the +// layers where the face genuinely overhangs. +// Both ends are tested against the slope plane rather than requiring a constant Y. Arachne's +// variable-width walls drift slightly in Y along the same slope (Y6.186 -> Y6.189 on one move), so +// a constant-Y filter matches almost nothing under Arachne and silently reduces its coverage. +// The length test excludes the cage walls: they are only as wide as the box is either side of the +// slope, but being vertical their y + z sweeps through the slope plane as z rises, so a couple of +// their fully supported moves would otherwise be counted as part of the span. +std::vector caged_slope_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + const double z = line.new_Z(self); + return z > caged_span_z_min && z < caged_slope_z_max && + line.dist_XY(self) > 0.5 * caged_slope_span && + std::abs(self.y() + z - caged_slope_wall_sum) < caged_wall_tolerance && + std::abs(line.new_Y(self) + z - caged_slope_wall_sum) < caged_wall_tolerance; + }); +} + +// The opposite, fully supported face, skipping the initial layer and its own speed settings. +std::vector back_wall_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + return line.new_Z(self) > 1.5 * caged_layer_height && + std::abs(self.y() - caged_back_wall_y) < caged_wall_tolerance && + std::abs(line.new_Y(self) - caged_back_wall_y) < caged_wall_tolerance; + }); +} + +// The first layer printed entirely above the slope. Its y = 0 wall runs the full width of the box. +const double caged_layer_above_slope_z = std::ceil(caged_slope_z_max / caged_layer_height) * caged_layer_height; + +// The parts of that wall standing on the cage rather than the slope, so on a contour identical to their own. +// Where the support changes is found by bisection, which stops at spans of 2mm, so the move spanning each end of +// the slope reaches a little way into the cage. Taking only the moves lying wholly outside the slope's x range +// leaves the wall that is unambiguously supported, without asserting how closely the bisection converged. +std::vector cage_shoulder_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + return std::abs(line.new_Z(self) - caged_layer_above_slope_z) < 0.5 * caged_layer_height && + std::abs(self.y() - caged_front_wall_y) < caged_wall_tolerance && + std::abs(line.new_Y(self) - caged_front_wall_y) < caged_wall_tolerance && + (std::max(self.x(), line.new_X(self)) <= caged_slope_x_min || + std::min(self.x(), line.new_X(self)) >= caged_slope_x_max); + }); +} + +// The readings a 40mm wall takes over a previous layer whose edge falls away by 0.03mm towards the +// middle: both ends read the same, and the middle reads slightly further out over air. Whether that +// middle reading survives is what decides the speed the wall is printed at. +std::vector> sampled_wall_over_dished_layer(const std::function& distance_to_speed) +{ + const AABBTreeLines::LinesDistancer prev_layer(std::vector{ + {{0., 0.}, {20., -dished_layer_depth}}, + {{20., -dished_layer_depth}, {40., 0.}}, + {{40., 0.}, {40., -10.}}, + {{40., -10.}, {0., -10.}}, + {{0., -10.}, {0., 0.}}, + }); + const Points wall{Point::new_scale(0., dished_wall_gap), Point::new_scale(40., dished_wall_gap)}; + + return estimate_points_properties(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// A straight, otherwise supported wall over a previous-layer boundary with a 2mm-wide pocket. Moving the +// pocket between x = 10 and x = 20 covers both discovery away from the wall's midpoint and refinement around +// a midpoint that has already been discovered. The current wall is inset half its width from the flat boundary, +// so its supported readings are zero after the estimator applies its boundary offset. +constexpr double narrow_pocket_wall_length = 40.; +constexpr double narrow_pocket_width = 2.; +constexpr double narrow_pocket_depth = 0.3; + +std::vector> sampled_wall_over_narrow_pocket( + double pocket_center, const std::function& distance_to_speed) +{ + const double pocket_left = pocket_center - 0.5 * narrow_pocket_width; + const double pocket_right = pocket_center + 0.5 * narrow_pocket_width; + const AABBTreeLines::LinesDistancer prev_layer(std::vector{ + {{0., 0.}, {pocket_left, 0.}}, + {{pocket_left, 0.}, {pocket_left, -narrow_pocket_depth}}, + {{pocket_left, -narrow_pocket_depth}, {pocket_right, -narrow_pocket_depth}}, + {{pocket_right, -narrow_pocket_depth}, {pocket_right, 0.}}, + {{pocket_right, 0.}, {narrow_pocket_wall_length, 0.}}, + {{narrow_pocket_wall_length, 0.}, {narrow_pocket_wall_length, -10.}}, + {{narrow_pocket_wall_length, -10.}, {0., -10.}}, + {{0., -10.}, {0., 0.}}, + }); + const double wall_y = -0.5 * caged_wall_width; + const Points wall{Point::new_scale(0., wall_y), Point::new_scale(narrow_pocket_wall_length, wall_y)}; + + return estimate_points_properties(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// A cross section that grows a layer's worth on the two faces meeting at either end of a wall, as any +// 45 degree overhang does. The wall itself stands on a contour identical to its own, but its ends sit +// where the growing faces cut the corners off, and the previous layer's edge there is nearer than the +// half line width the centreline is inset by. Both ends therefore read an overhang while everything +// between them reads supported: the reverse of the caged span, and the case the sampling above must +// leave to the passes after it. +constexpr double stepped_wall_inset = 0.5 * caged_wall_width; // mm, centreline inset from the contour +constexpr double stepped_end_gap = stepped_wall_inset - caged_layer_height; // mm, how far inside the corner ends up +constexpr double stepped_wall_span = 30.; // mm, the length of the wall + +std::vector> sampled_wall_between_growing_corners(const std::function& distance_to_speed) +{ + const AABBTreeLines::LinesDistancer prev_layer(std::vector{ + {{0., 0.}, {32., 0.}}, + {{32., 0.}, {32., -stepped_wall_span}}, + {{32., -stepped_wall_span}, {0., -stepped_wall_span}}, + {{0., -stepped_wall_span}, {0., 0.}}, + }); + const Points wall{Point::new_scale(stepped_wall_inset, -stepped_end_gap), + Point::new_scale(stepped_wall_inset, stepped_end_gap - stepped_wall_span)}; + + return estimate_points_properties(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// How much of a path is printed below the speed a fully supported reading gives. A segment is printed +// at the lower of the speeds its ends read. +double slowed_length(const std::vector>& points, const std::function& distance_to_speed) +{ + double length = 0.; + for (size_t i = 0; i + 1 < points.size(); ++i) + if (std::min(distance_to_speed(points[i].distance), distance_to_speed(points[i + 1].distance)) < distance_to_speed(0.f)) + length += (points[i + 1].position - points[i].position).norm(); + return length; +} + +float furthest_reading(const std::vector>& points) +{ + return std::max_element(points.begin(), points.end(), [](const ExtendedPoint<2>& l, const ExtendedPoint<2>& r) { + return l.distance < r.distance; + })->distance; +} + +DynamicPrintConfig caged_overhang_config(const char* wall_generator){ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + {"nozzle_diameter", "0.4"}, + {"initial_layer_print_height", caged_layer_height}, + {"layer_height", caged_layer_height}, + {"line_width", caged_wall_width}, + {"outer_wall_line_width", caged_wall_width}, + {"inner_wall_line_width", "0.45"}, + {"wall_loops", "2"}, + {"wall_generator", wall_generator}, + {"wall_sequence", "inner wall/outer wall"}, + {"sparse_infill_density", "15%"}, + {"detect_overhang_wall", "1"}, + {"enable_overhang_speed", "1"}, + {"slowdown_for_curled_perimeters", "0"}, + {"zaa_enabled", "0"}, + {"outer_wall_speed", caged_outer_wall_speed}, + {"inner_wall_speed", "300"}, + {"overhang_1_4_speed", "0"}, + {"overhang_2_4_speed", "50"}, + {"overhang_3_4_speed", "30"}, + {"overhang_4_4_speed", "10"}, + {"bridge_speed", "50"}, + {"filament_max_volumetric_speed", "22"}, + {"slow_down_for_layer_cooling", "0"}, + {"slow_down_layers", "0"}, // Nothing but the overhang settings may lower a wall speed + }); + return config; +} + +std::string caged_overhang_gcode(const char* wall_generator) +{ + Print print; + Model model; + init_print(std::vector{caged_overhang_mesh()}, print, model, caged_overhang_config(wall_generator), nullptr, + false); + return gcode(print); +} + +// Reports the matched move count alongside the extremes, so a filter that selected nothing is +// distinguishable from a span that simply was not slowed. +void info_feed_rates(const char* span, const std::vector& feed_rates) +{ + UNSCOPED_INFO("matched " << feed_rates.size() << " " << span << " moves"); + if (!feed_rates.empty()) { + const auto extremes = std::minmax_element(feed_rates.begin(), feed_rates.end()); + UNSCOPED_INFO("slowest " << *extremes.first / MM_PER_MIN << " mm/s, fastest " << *extremes.second / MM_PER_MIN << " mm/s"); + } +} + +} // namespace + +// Classic reproduces the endpoint-sampling bug: it emits the span as one long move whose endpoints +// both read as supported, so endpoint-only sampling never slows it. Arachne's endpoints already read +// as overhanging, but their placement near the cage makes the inferred support vary by layer. Arachne +// parity is therefore part of this regression's scope: both generators must classify the unsupported +// interior of the same 45-degree span consistently. +TEST_CASE("Caged external overhangs are slowed along their span", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector feed_rates = caged_slope_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("caged slope", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + // The endpoint bug left Classic at the full wall speed, while Arachne's cage-adjacent endpoint + // samples selected much faster bands on some layers. The whole span must stay in the slowed range + // for both generators, without requiring their different path segmentations to match. + const double fastest = *std::max_element(feed_rates.begin(), feed_rates.end()); + REQUIRE(fastest < caged_slow_speed * MM_PER_MIN); +} + +// The other side of the fix: the midpoint probe fires on every long external perimeter, so a +// regression that over-slows would leave the test above green. A fully supported wall must keep the +// speed it was configured with. +TEST_CASE("Supported vertical walls keep their normal speed", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector feed_rates = back_wall_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("back wall", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + const double slowest = *std::min_element(feed_rates.begin(), feed_rates.end()); + REQUIRE(slowest >= caged_slow_speed * MM_PER_MIN); +} + +// The slope's top edge falls mid layer, so the first layer above it still stands 0.179mm proud of the layer +// below wherever that layer was still on the slope. That is a real overhang and is slowed, but it ends with the +// slope: outside the slope's x range the box runs full height, so the same wall stands on a contour identical to +// its own. Sampling the interior of that wall at a single point reported one support reading for all of it and +// slowed these fully supported ends along with the rest. +TEST_CASE("Wall sections beside a caged overhang keep their normal speed", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector feed_rates = cage_shoulder_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("cage shoulder", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + const double slowest = *std::min_element(feed_rates.begin(), feed_rates.end()); + REQUIRE_THAT(slowest / MM_PER_MIN, Catch::Matchers::WithinRel(caged_outer_wall_speed, 0.01)); +} + +// A wall is printed at the lower of the speeds its ends read, so a reading only earns a point in the +// path where it prints at a different speed from the readings around it. Judging that on the readings +// themselves rather than the speeds they produce was too coarse: the configured speeds interpolate +// between their sections, so readings a fraction of the slowdown threshold apart still print more than +// 10% apart, and a real 45 degree overhang had its true reading dropped as if it agreed with its ends. +// The ends then chose the speed on their own, and being next to the walls either side of the overhang +// they read differently from layer to layer, banding an overhang that should have been uniform. +TEST_CASE("An overhang reading is kept whenever it changes the speed", "[ExtrusionProcessor][Regression]") +{ + // A steep speed curve, of the kind the configured overhang speeds interpolate across. + const std::vector> points = + sampled_wall_over_dished_layer([](float distance) { return std::round(200.f - 400.f * distance); }); + + REQUIRE_THAT(furthest_reading(points), Catch::Matchers::WithinAbs(dished_mid_reading, dished_reading_tolerance)); +} + +// The complement, and why the readings alone were tempting: a reading that prints at the same speed as +// its neighbours cannot change the G-code, so sampling must leave the path alone however far out it is. +TEST_CASE("An overhang reading is dropped when the speed is unchanged", "[ExtrusionProcessor]") +{ + // A flat speed curve, of the kind a single configured overhang speed produces. + const std::vector> points = sampled_wall_over_dished_layer([](float) { return 50.f; }); + + REQUIRE_THAT(furthest_reading(points), Catch::Matchers::WithinAbs(dished_end_reading, dished_reading_tolerance)); +} + +TEST_CASE("Coarse probing detects an unsupported pocket away from the wall midpoint", + "[ExtrusionProcessor][Regression]") +{ + const std::function distance_to_speed = [](float distance) { return distance <= 0.2f ? 100.f : 50.f; }; + const std::vector> points = + sampled_wall_over_narrow_pocket(0.25 * narrow_pocket_wall_length, distance_to_speed); + const double slowed = slowed_length(points, distance_to_speed); + + REQUIRE(slowed > 0.); + REQUIRE(slowed < 5.); +} + +TEST_CASE("Coarse probing brackets a narrow slowdown at the wall midpoint", + "[ExtrusionProcessor][Regression]") +{ + // Half of the pocket reading still maps to full speed. A matching probe in either half therefore must not + // prune that half before a supported point has been found close enough to bracket the slow midpoint. + const std::function distance_to_speed = [](float distance) { return distance <= 0.2f ? 100.f : 50.f; }; + const std::vector> points = + sampled_wall_over_narrow_pocket(0.5 * narrow_pocket_wall_length, distance_to_speed); + const double slowed = slowed_length(points, distance_to_speed); + + REQUIRE(slowed > 0.); + REQUIRE(slowed < 5.); +} + +// Sampling probes the interior, so it must not answer for the ends. On a supported wall between two +// corners that read an overhang, the reading that differs is the end's own, and the pass that ends a +// slowdown an end reads places its point from how far out that end is. Sampling took the difference as +// its own to report and put a point at the nearest position bisection had reached instead, which both +// sits further along the wall and leaves too little of it for that pass to run on, so the corner +// slowdown ran millimetres up an otherwise supported wall. Its length grows with the wall, so on a +// model whose cross section keeps growing it reads as a stair stepped band up the corner. +TEST_CASE("A supported wall between overhanging corners is slowed no further than its ends require", + "[ExtrusionProcessor][Regression]") +{ + // A steep speed curve, so the ends and the interior between them print at clearly different speeds. + const std::function distance_to_speed = [](float distance) { + return std::round(float(caged_outer_wall_speed) - 400.f * distance); + }; + + const double sampled = slowed_length(sampled_wall_between_growing_corners(distance_to_speed), distance_to_speed); + // The same wall with sampling switched off: what the endpoint driven passes alone make of the corners. + const double unsampled = slowed_length(sampled_wall_between_growing_corners({}), distance_to_speed); + + // The corners do read an overhang, so there is a slowdown for sampling to have lengthened. + REQUIRE(unsampled > 0.); + REQUIRE(sampled <= unsampled); +} + +TEST_CASE("Benchmark caged overhang interior sampling", "[ExtrusionProcessor][!benchmark]"){ + const char* wall_generator = GENERATE("classic", "arachne"); + + BENCHMARK(wall_generator) + { + return caged_overhang_gcode(wall_generator); + }; +} From 57092d5abd55daf2f3e2e44b06507646d77c3420 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 17 Aug 2026 13:31:30 +0800 Subject: [PATCH 50/71] Reorder network initialization calls --- src/slic3r/GUI/GUI_App.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index db51bd9d8e..0d4e3f35cf 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3273,15 +3273,12 @@ bool GUI_App::on_init_inner() } } */ - copy_network_if_available(); if (scrn) { scrn->SetText(_L("Loading Plugins") + dots, 20); wxYield(); } - on_init_network(); - // Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically. // initialize() also installs the libslic3r hooks (capability resolver, // slicing-pipeline dispatcher) via plugin_hooks::install() -- no @@ -3310,6 +3307,9 @@ bool GUI_App::on_init_inner() } } + copy_network_if_available(); + on_init_network(); + if (m_agent) plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast(m_agent->get_cloud_agent())); From 542cd18d19887d23bbf454de363ca7cf807f70b3 Mon Sep 17 00:00:00 2001 From: jimmy-brightz Date: Mon, 17 Aug 2026 02:34:41 -0400 Subject: [PATCH 51/71] Fix prime tower rotation angle setting not working (#15227) --- src/libslic3r/PresetBundle.cpp | 1 - src/slic3r/GUI/GLCanvas3D.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 5557d36891..e66ae064f0 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -49,7 +49,6 @@ static std::vector s_project_options { "filament_multi_colour", "wipe_tower_x", "wipe_tower_y", - "wipe_tower_rotation_angle", "curr_bed_type", "flush_multiplier", // Fast-purge mode: project-level purge control, inert at Default. diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index a51a10296c..f6ffb75405 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2887,7 +2887,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re float x = dynamic_cast(proj_cfg.option("wipe_tower_x"))->get_at(plate_id); float y = dynamic_cast(proj_cfg.option("wipe_tower_y"))->get_at(plate_id); float w = dynamic_cast(m_config->option("prime_tower_width"))->value; - float a = dynamic_cast(proj_cfg.option("wipe_tower_rotation_angle"))->value; + float a = dynamic_cast(m_config->option("wipe_tower_rotation_angle"))->value; // BBS float v = dynamic_cast(m_config->option("prime_volume"))->value; Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5b06db9d3e..2225812ff6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12674,7 +12674,7 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : view3D->get_canvas3d()->get_gizmos_manager(); @@ -12784,7 +12784,7 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx; From ba22a87a0b873c09260c771e29a31b6059fc879c Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 18 Aug 2026 00:42:03 +0800 Subject: [PATCH 52/71] Add /bot merge for delegated vendor profile maintainers (#15279) * Add /bot merge for delegated vendor profile maintainers Vendor profile PRs no longer need a maintainer with repository write access: an account listed in the FOLDER_MERGERS variable can squash-merge a PR confined to the folders it owns by commenting /bot merge on it. Anything reaching outside that grant, targeting a branch other than main or release/*, or missing a green Check profiles run is declined with a comment naming the offending files. Grants live in the merge-delegation environment, so only an admin can change who may merge, and MERGE_BOT_DRY_RUN stops all merging without a code change. Check profiles now also runs on release/* pull requests; nothing else changes for existing contributors. * Add profile version bump to the code review checklist Without the bump in resources/profiles/.json, a preset change never reaches existing installs over the air. --- .github/workflows/check_profiles.yml | 6 + .github/workflows/pr-merge-bot.yml | 510 +++++++++++++++++++++++++++ AGENTS.md | 1 + 3 files changed, 517 insertions(+) create mode 100644 .github/workflows/pr-merge-bot.yml diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml index 59c92e3ec0..db3ae6c4e8 100644 --- a/.github/workflows/check_profiles.yml +++ b/.github/workflows/check_profiles.yml @@ -1,8 +1,12 @@ name: Check profiles on: pull_request: + # release/* is included because pr-merge-bot.yml lets delegates merge into + # it, and it gates on this workflow's result. Without it a delegated merge + # into a release branch would run no profile validation at all. branches: - main + - release/* paths: - 'resources/profiles/**' - ".github/workflows/check_profiles.yml" @@ -20,6 +24,8 @@ permissions: jobs: check_profiles: + # This job name is the check-run name pr-merge-bot.yml requires before a + # delegated merge. Renaming it silently disables that gate. name: Check profiles runs-on: ubuntu-24.04 steps: diff --git a/.github/workflows/pr-merge-bot.yml b/.github/workflows/pr-merge-bot.yml new file mode 100644 index 0000000000..9b5ff2dfaf --- /dev/null +++ b/.github/workflows/pr-merge-bot.yml @@ -0,0 +1,510 @@ +name: PR Merge Bot + +# Merges a pull request on request from a delegated vendor profile maintainer. +# The merge is performed by this workflow's GITHUB_TOKEN, so a delegate needs no +# repository access. +# +# Commands, posted as a comment on the PR: +# /bot merge squash-merge the PR +# /bot merge --dry-run report the verdict without merging +# +# Merges only when the commenter holds a grant covering every changed path, the +# PR targets main or release/*, and CI is green on the head commit. Otherwise it +# comments naming the files that fell outside the grant. +# +# Grants come from the FOLDER_MERGERS variable in the `merge-delegation` +# environment: one per line, `account: path`, `#` comments and blank lines +# allowed. Paths may contain spaces. A vendor takes two grants, the folder and +# its sibling bundle JSON: +# +# # Acme profiles +# vendor-maintainer: resources/profiles/Acme/ +# vendor-maintainer: resources/profiles/Acme.json +# +# Edit the grant list (environment scope, so admin only): +# gh variable set FOLDER_MERGERS --env merge-delegation --body "$(cat folder-mergers.txt)" +# gh variable get FOLDER_MERGERS --env merge-delegation +# +# Stop all merging without touching this file: +# gh variable set MERGE_BOT_DRY_RUN --body true + +on: + issue_comment: + types: + - created + +# One merge attempt per PR at a time, so two quick comments cannot race. +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + merge: + # Skips the job unless a PR comment mentions the command. + if: >- + github.repository == 'OrcaSlicer/OrcaSlicer' + && github.event.issue.pull_request != null + && contains(github.event.comment.body, '/bot merge') + permissions: + contents: write # pulls.merge + pull-requests: write # pulls.merge + issues: write # feedback comment + reactions + actions: write # re-dispatch build_all.yml after the merge + runs-on: ubuntu-latest + timeout-minutes: 10 + # Supplies FOLDER_MERGERS. Must carry no protection rules, or every + # delegated merge would wait for a human reviewer. + environment: merge-delegation + steps: + - name: Merge PR on behalf of a folder delegate + uses: actions/github-script@v9 + env: + # Read as env vars, never interpolated into the script body. + FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }} + MERGE_BOT_DRY_RUN: ${{ vars.MERGE_BOT_DRY_RUN }} + with: + script: | + function isPermissionDenied(error) { + return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || ''); + } + + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + const MARKER = ''; + // No grant may reach outside this root. + const DELEGATABLE_ROOT = 'resources/profiles/'; + const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/; + const MERGE_METHOD = 'squash'; + const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml + const MAX_CHANGED_FILES = 500; // policy cap, well under listFiles' 3000 + const LISTFILES_CAP = 3000; + const MAX_REPORTED_FILES = 12; + const MERGEABLE_ATTEMPTS = 5; + const MERGEABLE_DELAY_MS = 2000; + const OK_CONCLUSIONS = new Set(['success', 'neutral', 'skipped']); + const REGULAR_FILE_MODES = new Set(['100644', '100755']); + + // Paths refused whatever the grants say. Checked before grants, so + // delegating a new root means removing it from this list too. + const DENIED_PATTERNS = [ + /^\.github\//, + /(^|\/)\.git(attributes|modules|ignore|config)$/, + /^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//, + /(^|\/)cmakelists\.txt$/, + /\.cmake$/, + /^build_[^/]*\.(?:sh|bat)$/, + /^version\.inc$/, + // Executables, including those inside the delegatable root. + /\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/ + ]; + + function parseGrants(raw) { + // GitHub login: 1-39 chars, alphanumerics with single interior hyphens. + const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/; + const grantsByLogin = new Map(); + const problems = []; + + (raw || '').split(/\r?\n/).forEach((rawLine, index) => { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) { + return; + } + + // Splits on the first colon only, so paths may contain ':' and spaces. + const separator = line.indexOf(':'); + if (separator === -1) { + problems.push(`line ${index + 1}: expected \`account: path\``); + return; + } + + const login = line.slice(0, separator).trim().replace(/^@/, ''); + const path = line.slice(separator + 1).trim().replace(/\/+$/, ''); + + if (!loginPattern.test(login)) { + problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`); + return; + } + if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) { + problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`); + return; + } + // Rejects anything outside the root, and the bare root itself. + if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) { + problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``); + return; + } + + const key = login.toLowerCase(); + grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path)); + }); + + return { grantsByLogin, problems }; + } + + function isDenied(path) { + if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) { + return true; + } + + const normalized = path.normalize('NFKC').toLowerCase(); + return DENIED_PATTERNS.some((pattern) => pattern.test(normalized)); + } + + // Byte-exact match on directory boundaries, so a grant of + // `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`. + function isGranted(path, grants) { + return grants.some((grant) => path === grant || path.startsWith(`${grant}/`)); + } + + // Both endpoints of a rename; both must satisfy the grant. + function pathsFor(file) { + return [file.filename, file.previous_filename].filter(Boolean); + } + + function formatList(items) { + const unique = [...new Set(items)]; + const shown = unique.slice(0, MAX_REPORTED_FILES).map((item) => `- \`${item}\``); + if (unique.length > MAX_REPORTED_FILES) { + shown.push(`- …and ${unique.length - MAX_REPORTED_FILES} more`); + } + return shown.join('\n'); + } + + const { owner, repo } = context.repo; + const issue = context.payload.issue; + const comment = context.payload.comment; + + if (!issue.pull_request) { + core.info('Ignoring comment that is not on a pull request.'); + return; + } + // Ignores a comment whose sender is not its author. + if (context.payload.action !== 'created' || context.payload.sender.login !== comment.user.login) { + core.warning('Ignoring comment whose sender does not match its author.'); + return; + } + if (comment.user.type !== 'User') { + core.info('Ignoring bot-authored command.'); + return; + } + + const commandLine = (comment.body || '') + .split('\n') + .map((line) => line.trim()) + .find((line) => /^\/bot\s+merge\b/i.test(line)); + + if (!commandLine) { + core.info('No /bot merge command found.'); + return; + } + + const commenter = comment.user.login; + const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS); + const grants = grantsByLogin.get(commenter.toLowerCase()) || []; + + for (const problem of problems) { + core.warning(`FOLDER_MERGERS ${problem}`); + } + + // Says nothing to accounts with no grant, so it cannot be used to spam. + if (!grants.length) { + core.info(`Ignoring /bot merge from @${commenter}: not listed in FOLDER_MERGERS.`); + return; + } + + // Warns instead of failing when the token cannot post feedback. + async function bestEffort(call, warning) { + try { + await call(); + } catch (error) { + if (isPermissionDenied(error)) { + core.warning(warning); + return; + } + + throw error; + } + } + + const react = (content) => bestEffort( + () => github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content }), + `Cannot add the "${content}" reaction because the token cannot write.`); + + const say = (body) => bestEffort( + () => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n${body}` }), + 'Cannot post a comment because the token cannot write comments.'); + + // Declines the command: warns in the log, reacts, explains on the PR. + async function refuse(reason) { + const configNote = problems.length + ? `\n\n\`FOLDER_MERGERS\` also has problems a maintainer needs to fix:\n${problems.map((problem) => `- ${problem}`).join('\n')}` + : ''; + const grantsNote = `\n\n
Your current grants\n\n${formatList(grants)}\n\n
`; + + core.warning(`Refused /bot merge from @${commenter}: ${reason}`); + await react('-1'); + await say(`@${commenter} I can't merge this PR: ${reason}${configNote}${grantsNote}`); + } + + await react('eyes'); + + const args = (commandLine.match(/^\/bot\s+merge\s*(.*)$/i)[1] || '').trim().split(/\s+/).filter(Boolean); + const unknownArgs = args.filter((arg) => arg.toLowerCase() !== '--dry-run'); + const dryRun = String(process.env.MERGE_BOT_DRY_RUN || '').toLowerCase() === 'true' + || unknownArgs.length !== args.length; + + if (unknownArgs.length) { + return refuse( + `I don't understand ${unknownArgs.map((arg) => `\`${arg}\``).join(', ')}. ` + + 'Usage: `/bot merge` or `/bot merge --dry-run`.' + ); + } + + // Refuses everything while the grant list is malformed. + if (problems.length) { + return refuse( + 'the `FOLDER_MERGERS` grant list has malformed lines, so I refuse every merge until it is fixed.' + ); + } + + let { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: issue.number + }); + + if (pr.merged) { + return refuse('it is already merged.'); + } + if (pr.state !== 'open') { + return refuse(`its state is \`${pr.state}\`, not \`open\`.`); + } + if (pr.draft) { + return refuse('it is still a draft. Mark it ready for review first.'); + } + if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) { + return refuse(`it targets \`${pr.base.ref}\`. Delegated merges are only allowed into \`main\` and \`release/*\`.`); + } + + // ---- folder scope ---- + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: pr.number, + per_page: 100 + }); + + if (!files.length) { + return refuse('it changes no files, so there is nothing to verify or merge.'); + } + // Refuses when the file list is truncated or disagrees with the PR. + if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) { + return refuse( + `it reports ${pr.changed_files} changed files but the API listed ${files.length}, ` + + 'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.' + ); + } + if (pr.changed_files > MAX_CHANGED_FILES) { + return refuse(`it changes ${pr.changed_files} files; delegated merges are capped at ${MAX_CHANGED_FILES}.`); + } + + const deniedFiles = []; + const outsideFiles = []; + + for (const file of files) { + for (const path of pathsFor(file)) { + if (isDenied(path)) { + deniedFiles.push(path); + } else if (!isGranted(path, grants)) { + outsideFiles.push(path); + } + } + } + + if (deniedFiles.length) { + core.error(`@${commenter} attempted a delegated merge touching protected paths: ${deniedFiles.join(', ')}`); + return refuse( + 'it touches paths that are never delegatable, whatever the grants say ' + + `(CI, build, scripts or executable files):\n\n${formatList(deniedFiles)}\n\nA maintainer should look at this before it goes any further.` + ); + } + if (outsideFiles.length) { + return refuse( + `${outsideFiles.length} changed path(s) fall outside your grants:\n\n${formatList(outsideFiles)}\n\n` + + 'A vendor needs both grants: `resources/profiles//` **and** `resources/profiles/.json`.' + ); + } + + // ---- file modes: rejects symlinks and submodules ---- + // Fetches the delegatable subtree only; listFiles does not report modes. + const headSha = pr.head.sha; + const { data: tree } = await github.rest.git.getTree({ + owner, + repo, + tree_sha: `${headSha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`, + recursive: 'true' + }); + + if (tree.truncated) { + return refuse('the git tree is too large to verify file modes. A maintainer must merge this one.'); + } + + // Entry paths are subtree-relative. + const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode])); + const irregularFiles = files + .filter((file) => file.status !== 'removed') + .map((file) => [file.filename, modesByPath.get(file.filename)]) + .filter(([, mode]) => !REGULAR_FILE_MODES.has(mode)) + .map(([path, mode]) => `${path} (mode ${mode || 'missing'})`); + + if (irregularFiles.length) { + core.error(`@${commenter} attempted a delegated merge with non-regular files: ${irregularFiles.join(', ')}`); + return refuse( + `it adds symlinks, submodules or files I cannot verify:\n\n${formatList(irregularFiles)}\n\nA maintainer should look at this before it goes any further.` + ); + } + + // ---- mergeability: waits for GitHub to compute it ---- + for (let attempt = 0; pr.mergeable === null && attempt < MERGEABLE_ATTEMPTS; attempt += 1) { + core.info(`Mergeability not computed yet; retrying in ${MERGEABLE_DELAY_MS}ms.`); + await sleep(MERGEABLE_DELAY_MS); + ({ data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pr.number + })); + } + + if (pr.mergeable === null) { + return refuse('GitHub is still working out whether it can be merged. Try `/bot merge` again in a minute.'); + } + if (!pr.mergeable) { + return refuse(`it is not mergeable (\`${pr.mergeable_state}\`) - most likely a conflict with \`${pr.base.ref}\`.`); + } + + // ---- CI on the head commit ---- + const checkRuns = await github.paginate(github.rest.checks.listForRef, { + owner, + repo, + ref: headSha, + filter: 'latest', + per_page: 100 + }); + const pendingChecks = checkRuns.filter((run) => run.status !== 'completed'); + const failedChecks = checkRuns.filter((run) => run.status === 'completed' && !OK_CONCLUSIONS.has(run.conclusion)); + + if (pendingChecks.length) { + return refuse( + `${pendingChecks.length} check(s) are still running on \`${headSha.slice(0, 7)}\`:\n\n` + + `${formatList(pendingChecks.map((run) => run.name))}\n\nRe-run \`/bot merge\` once they finish.` + ); + } + if (failedChecks.length) { + return refuse( + `${failedChecks.length} check(s) are not green on \`${headSha.slice(0, 7)}\`:\n\n` + + formatList(failedChecks.map((run) => `${run.name} (${run.conclusion})`)) + ); + } + + const { data: combined } = await github.rest.repos.getCombinedStatusForRef({ + owner, + repo, + ref: headSha + }); + // total_count 0 only means there are no legacy statuses. + if (combined.total_count > 0 && combined.state !== 'success') { + return refuse( + `the combined commit status on \`${headSha.slice(0, 7)}\` is \`${combined.state}\`:\n\n` + + formatList(combined.statuses.filter((status) => status.state !== 'success') + .map((status) => `${status.context} (${status.state})`)) + ); + } + + // Requires the check to have actually run, not merely to have not failed. + const requiredCheck = checkRuns.find((run) => + run.name === REQUIRED_CHECK && + run.app && run.app.slug === 'github-actions' && + run.status === 'completed' && OK_CONCLUSIONS.has(run.conclusion)); + + if (!requiredCheck) { + return refuse( + `the \`${REQUIRED_CHECK}\` check has not succeeded on \`${headSha.slice(0, 7)}\`. ` + + 'If it never ran, a maintainer needs to approve the workflow run first.' + ); + } + + const scopeSummary = `${files.length} file(s), all within:\n${formatList(grants)}`; + + if (dryRun) { + core.info('Dry run: every gate passed, not merging.'); + await react('+1'); + await say( + `@${commenter} **dry run** - this PR passes every gate and I *would* squash-merge it ` + + `at \`${headSha.slice(0, 7)}\`.\n\nVerified scope: ${scopeSummary}` + ); + return; + } + + // ---- re-validate, then merge ---- + // An unchanged head SHA means the verified file list still holds. + const { data: fresh } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pr.number + }); + + if (fresh.head.sha !== headSha || fresh.base.ref !== pr.base.ref || fresh.state !== 'open' || fresh.merged || fresh.draft) { + return refuse('it changed while I was checking it. Nothing was merged - re-run `/bot merge`.'); + } + + let merged; + try { + // Pinned to the verified head: a moved head fails with 409. + ({ data: merged } = await github.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + sha: headSha, + merge_method: MERGE_METHOD, + commit_title: `${pr.title} (#${pr.number})`, + commit_message: + `Merged by /bot merge on behalf of @${commenter} (id ${comment.user.id}).\n` + + `Grants: ${grants.join(', ')}\nHead: ${headSha}\n` + })); + } catch (error) { + const hint = { + 403: 'the workflow token cannot write to the repository.', + 405: 'GitHub refused the merge - branch protection, a required review or check, a newly added CODEOWNERS file, or squash merging being disabled.', + 409: `the head commit moved after I verified it (was \`${headSha.slice(0, 7)}\`).`, + 422: 'GitHub rejected the merge as invalid.' + }[error.status]; + + if (!hint) { + throw error; + } + + await refuse(`${hint}\n\n> ${error.message}\n\nNothing was merged.`); + core.setFailed(`Delegated merge failed: ${error.status} ${error.message}`); + return; + } + + core.info(`Merged #${pr.number} as ${merged.sha}.`); + await react('rocket'); + await say( + `@${commenter} squash-merged into \`${pr.base.ref}\` as ${merged.sha}.\n\nVerified scope: ${scopeSummary}` + ); + + // ---- re-kick the build ---- + // A GITHUB_TOKEN merge fires no push event, so build_all.yml would + // otherwise never see these files. + try { + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: 'build_all.yml', + ref: pr.base.ref + }); + core.info(`Dispatched build_all.yml on ${pr.base.ref}.`); + } catch (error) { + core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`); + } diff --git a/AGENTS.md b/AGENTS.md index fbc624b958..236aa54c05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,7 @@ ctest --test-dir ./tests/fff_print - Add helper functions or utilities only when existing code cannot reasonably be reused. Avoid duplication. - Keep code concise and clear. Manually simplify AI generated bloated codes before review. - Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults. +- For profile changes (`resources/profiles//**`), check that `version` in the sibling `resources/profiles/.json` was bumped. - For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) for that language. ## Localization & translations From b6e4f52c05350f08f4f38483aa87541566047eb6 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Tue, 18 Aug 2026 19:00:53 +0800 Subject: [PATCH 53/71] fix: recursive include between HMS.hpp and GUI_App.hpp --- src/slic3r/GUI/HMS.cpp | 1 + src/slic3r/GUI/HMS.hpp | 29 ++++++++++++++++------------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/slic3r/GUI/HMS.cpp b/src/slic3r/GUI/HMS.cpp index 4d67a398f7..5471c16064 100644 --- a/src/slic3r/GUI/HMS.cpp +++ b/src/slic3r/GUI/HMS.cpp @@ -1,6 +1,7 @@ #include "HMS.hpp" #include "GUI.hpp" +#include "GUI_App.hpp" #include "DeviceManager.hpp" #include "DeviceCore/DevManager.h" #include "DeviceCore/DevUtil.h" diff --git a/src/slic3r/GUI/HMS.hpp b/src/slic3r/GUI/HMS.hpp index d2a87ebf42..c494539b36 100644 --- a/src/slic3r/GUI/HMS.hpp +++ b/src/slic3r/GUI/HMS.hpp @@ -1,7 +1,6 @@ #ifndef slic3r_HMS_hpp_ #define slic3r_HMS_hpp_ -#include "GUI_App.hpp" #include "GUI.hpp" #include "I18N.hpp" #include "Widgets/Label.hpp" @@ -11,7 +10,11 @@ #include "slic3r/Utils/Http.hpp" #include "libslic3r/Thread.hpp" #include "nlohmann/json.hpp" +#include #include +#include +#include +#include namespace Slic3r { @@ -26,12 +29,12 @@ namespace GUI { class HMSQuery { protected: - std::unordered_map m_hms_info_jsons; // key-> device id type, the first three digits of SN number - std::unordered_map m_hms_action_jsons;// key-> device id type + std::unordered_map m_hms_info_jsons; // key-> device id type, the first three digits of SN number + std::unordered_map m_hms_action_jsons;// key-> device id type std::unordered_map m_hms_local_images; // key-> image name mutable std::mutex m_hms_mutex; - std::unordered_map m_cloud_hms_last_update_time; + std::unordered_map m_cloud_hms_last_update_time; public: HMSQuery() { } @@ -61,18 +64,18 @@ private: // load hms void init_hms_info(const std::string& dev_type_id); void copy_from_data_dir_to_local(); - int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, json* receive_json); - int load_from_local(const std::string& hms_type, const std::string& dev_id_type, json* receive_json, std::string& version_info); - int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, json save_json); + int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json); + int load_from_local(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json, std::string& version_info); + int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, nlohmann::json save_json); std::string get_hms_file(std::string hms_type, std::string lang = std::string("en"), std::string dev_id_type = ""); // internal query - string get_dev_id_type(const MachineObject* obj) const; - wxString _query_hms_msg(const string& dev_id_type, const string& long_error_code, const string& lang_code = std::string("en")); + std::string get_dev_id_type(const MachineObject* obj) const; + wxString _query_hms_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en")); - bool _is_internal_error(const string &dev_id_type, const string &long_error_code, const string &lang_code = std::string("en")); - wxString _query_error_msg(const string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en")); - wxString _query_error_image_action(const string& dev_id_type, const std::string& long_error_code, std::vector& button_action); + bool _is_internal_error(const std::string &dev_id_type, const std::string &long_error_code, const std::string &lang_code = std::string("en")); + wxString _query_error_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en")); + wxString _query_error_image_action(const std::string& dev_id_type, const std::string& long_error_code, std::vector& button_action); }; int get_hms_info_version(std::string &version); @@ -85,4 +88,4 @@ std::string get_error_message(int error_code); } -#endif \ No newline at end of file +#endif From 6a52ea1818936f4663e21d7bd57cca4020d2c8ba Mon Sep 17 00:00:00 2001 From: GlauTech <33813227+GlauTechCo@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:51:40 +0300 Subject: [PATCH 54/71] Update OrcaSlicer_tr.po (#15119) * Update OrcaSlicer_tr.po * Update OrcaSlicer_tr.po Fixed inaccurate AI-generated text and updated missing translations. * REmoive # AI Translated --------- Co-authored-by: Ian Bassi --- localization/i18n/tr/OrcaSlicer_tr.po | 157 ++++++++++++-------------- 1 file changed, 71 insertions(+), 86 deletions(-) diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 467b3c355b..63d1e5bc75 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -4,7 +4,7 @@ msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-07-29 17:40-0300\n" -"PO-Revision-Date: 2026-08-01 20:32+0300\n" +"PO-Revision-Date: 2026-08-04 19:36+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" "Language: tr\n" @@ -738,9 +738,8 @@ msgstr "Sabit adım sürükleme" msgid "Context Menu" msgstr "Bağlam Menüsü" -# AI Translated msgid "Toggle Auto-Drop" -msgstr "Otomatik Bırakmayı Aç/Kapat" +msgstr "Otomatik düşürmeyi aç / kapat" msgid "Single sided scaling" msgstr "Tek taraflı ölçekleme" @@ -791,9 +790,8 @@ msgstr "Nesne" msgid "Part" msgstr "Parça" -# AI Translated msgid "Relative" -msgstr "Göreli" +msgstr "Göreceli" # AI Translated msgid "Coordinate system used for transform actions." @@ -2306,7 +2304,7 @@ msgid "new or open project file is not allowed during the slicing process!" msgstr "dilimleme işlemi sırasında yeni veya açık proje dosyasına izin verilmez!" msgid "Open Project" -msgstr "Projeyi Aç" +msgstr "Projeyi aç" msgid "The version of Orca Slicer is too low and needs to be updated to the latest version before it can be used normally." msgstr "Orca Slicer'ın sürümü çok düşük ve normal şekilde kullanılabilmesi için en son sürüme güncellenmesi gerekiyor." @@ -2734,16 +2732,15 @@ msgstr "Simit" msgid "Orca Cube" msgstr "Orca Küpü" -# AI Translated msgid "OrcaSliced Combo" -msgstr "OrcaSliced Combo" +msgstr "Orca Dilimleme Paketi" # AI Translated msgid "Orca Badge" msgstr "Orca Rozeti" msgid "Orca Tolerance Test" -msgstr "Orca tolerans testi" +msgstr "Orca Tolerans Testi" msgid "3DBenchy" msgstr "3DBenchy" @@ -2807,7 +2804,7 @@ msgid "Set as Individual Objects" msgstr "Bireysel nesneler olarak ayarla" msgid "Fill bed with copies" -msgstr "Yatağı kopyalarla doldurun" +msgstr "Tablayı kopyalarla doldur" msgid "Fill the remaining area of bed with copies of the selected object" msgstr "Yatağın kalan alanını seçilen nesnenin kopyalarıyla doldurun" @@ -2815,9 +2812,8 @@ msgstr "Yatağın kalan alanını seçilen nesnenin kopyalarıyla doldurun" msgid "Printable" msgstr "Yazdırılabilir" -# AI Translated msgid "Auto Drop" -msgstr "Otomatik Bırakma" +msgstr "Otomatik düşür" # AI Translated msgid "Automatically drops the selected object to the build plate." @@ -2967,7 +2963,7 @@ msgid "Add Models" msgstr "Model ekle" msgid "Show Labels" -msgstr "Etiketleri Göster" +msgstr "Etiketleri göster" msgid "To Objects" msgstr "Nesnelere" @@ -3009,7 +3005,7 @@ msgid "Select all objects on the current plate" msgstr "Mevcut plakadaki tüm nesneleri seç" msgid "Select All Plates" -msgstr "Tüm Plakaları Seç" +msgstr "Tüm plakaları seç" msgid "Select all objects on all plates" msgstr "Tüm plakalardaki tüm nesneleri seç" @@ -3045,13 +3041,13 @@ msgid "Remove the selected plate" msgstr "Seçilen plakayı kaldır" msgid "Add instance" -msgstr "Örnek ekle" +msgstr "Kopya ekle" msgid "Add one more instance of the selected object" msgstr "Seçilen nesnenin bir örneğini daha ekle" msgid "Remove instance" -msgstr "Örneği kaldır" +msgstr "Kopyayı kaldır" msgid "Remove one instance of the selected object" msgstr "Seçilen nesnenin bir örneğini kaldır" @@ -3060,10 +3056,10 @@ msgid "Set number of instances" msgstr "Örnek sayısını ayarlayın" msgid "Change the number of instances of the selected object" -msgstr "Seçilen nesnenin örnek sayısını değiştirme" +msgstr "Seçilen nesnenin kopya sayısını değiştirme" msgid "Fill bed with instances" -msgstr "Yatağı örneklerle doldurun" +msgstr "Tablayı kopyalarla doldur" msgid "Fill the remaining area of bed with instances of the selected object" msgstr "Yatağın kalan alanını seçilen nesnenin örnekleriyle doldurun" @@ -3075,7 +3071,7 @@ msgid "Simplify Model" msgstr "Modeli basitleştir" msgid "Subdivision mesh" -msgstr "Alt bölüm ağı" +msgstr "Poligon artırma" msgid "(Lost color)" msgstr "(Renk kaybı)" @@ -3090,10 +3086,10 @@ msgid "Edit Process Settings" msgstr "İşlem ayarlarını düzenle" msgid "Copy Process Settings" -msgstr "İşlem Ayarlarını Kopyala" +msgstr "İşlem ayarlarını kopyala" msgid "Paste Process Settings" -msgstr "İşlem Ayarlarını Yapıştır" +msgstr "İşlem ayarlarını yapıştır" msgid "Edit print parameters for a single object" msgstr "Tek bir nesne için yazdırma parametrelerini düzenleme" @@ -3478,7 +3474,7 @@ msgid "More" msgstr "Daha" msgid "Open Preferences" -msgstr "Tercihleri Aç" +msgstr "Tercihleri aç" msgid "Open next tip" msgstr "Sonraki ipucunu aç" @@ -5450,10 +5446,10 @@ msgid "Acceleration" msgstr "Hızlanma" msgid "Jerk" -msgstr "Jerk" +msgstr "Sarsıntı" msgid "Fan Speed" -msgstr "Fan hızı" +msgstr "Fan Hızı" msgid "Flow" msgstr "Akış" @@ -5468,7 +5464,7 @@ msgid "Layer Time" msgstr "Katman Süresi" msgid "Layer Time (log)" -msgstr "Katman Süresi (günlük)" +msgstr "Katman Süresi (log)" msgid "Pressure Advance" msgstr "Basınç İlerlemesi" @@ -5477,10 +5473,10 @@ msgid "Noop" msgstr "Hayır" msgid "Retract" -msgstr "Geri Çekme" +msgstr "Geri çekme" msgid "Unretract" -msgstr "İleri İtme" +msgstr "İleri itme" msgid "Seam" msgstr "Dikiş" @@ -5578,7 +5574,7 @@ msgid "Acceleration: " msgstr "İvme: " msgid "Jerk: " -msgstr "Jerk: " +msgstr "Sarsıntı: " msgid "PA: " msgstr "PA: " @@ -5608,7 +5604,7 @@ msgid "Actual speed profile" msgstr "Gerçek hız profili" msgid "Statistics of All Plates" -msgstr "Tüm Plakaların İstatistikleri" +msgstr "Tüm plakaların istatistikleri" msgid "Display" msgstr "Ekran" @@ -5708,7 +5704,7 @@ msgid "Acceleration (mm/s²)" msgstr "İvme (mm/s²)" msgid "Jerk (mm/s)" -msgstr "Jerk (mm/s)" +msgstr "Sarsıntı (mm/s)" msgid "Fan speed (%)" msgstr "Fan hızı (%)" @@ -5759,9 +5755,8 @@ msgstr "Normal mod" msgid "Total Filament" msgstr "Toplam filament" -# AI Translated msgid "Model Filament" -msgstr "Model Filamenti" +msgstr "Model filamenti" msgid "Prepare time" msgstr "Hazırlık süresi" @@ -6282,20 +6277,19 @@ msgid "Setup Wizard" msgstr "Kurulum sihirbazı" msgid "Show Configuration Folder" -msgstr "Yapılandırma Klasörünü Göster" +msgstr "Yapılandırma klasörünü göster" -# AI Translated msgid "Troubleshoot Center" -msgstr "Sorun Giderme Merkezi" +msgstr "Sorun giderme merkezi" msgid "Open Network Test" -msgstr "Ağ Testini Aç" +msgstr "Ağ testini aç" msgid "Show Tip of the Day" -msgstr "Günün İpucunu Göster" +msgstr "Günün ipucunu göster" msgid "Check for Updates" -msgstr "Güncellemeleri Kontrol Et" +msgstr "Güncellemeleri kontrol et" #, c-format, boost-format msgid "&About %s" @@ -6349,7 +6343,7 @@ msgid "Recent files" msgstr "Son dosyalar" msgid "Save Project" -msgstr "Projeyi Kaydet" +msgstr "Projeyi kaydet" msgid "Save current project to file" msgstr "Mevcut projeyi dosyaya kaydet" @@ -6415,13 +6409,13 @@ msgid "Export toolpaths as OBJ" msgstr "Takımyollarını OBJ olarak dışa aktar" msgid "Export Preset Bundle" -msgstr "Ön Ayar Paketini Dışa Aktar" +msgstr "Ön ayar paketini dışa aktar" msgid "Export current configuration to files" msgstr "Geçerli yapılandırmayı dosyalara aktar" msgid "Export" -msgstr "Dışa Aktar" +msgstr "Dışa aktar" msgid "Quit" msgstr "Çıkış" @@ -6478,13 +6472,13 @@ msgid "Deselects all objects" msgstr "Tüm nesnelerin seçimini kaldırır" msgid "Use Perspective View" -msgstr "Perspektif Görünüm" +msgstr "Perspektif görünüm" msgid "Use Orthogonal View" -msgstr "Ortogonal Görünüm" +msgstr "Ortogonal görünüm" msgid "Auto Perspective" -msgstr "Otomatik Perspektif" +msgstr "Otomatik perspektif" msgid "Automatically switch between orthographic and perspective when changing from top/bottom/side views." msgstr "Üst/Alt/Yan görünümler arasında geçiş yaparken ortografik ve perspektif arasında otomatik olarak geçiş yapın." @@ -6496,37 +6490,37 @@ msgid "Show G-code window in Preview scene." msgstr "Previce sahnesinde G-kodu penceresini göster." msgid "Show 3D Navigator" -msgstr "3D Gezgini Göster" +msgstr "3D gezgini göster" msgid "Show 3D navigator in Prepare and Preview scene." msgstr "Hazırlama ve Önizleme sahnesinde 3D gezgini göster." msgid "Show Gridlines" -msgstr "Kılavuz Çizgilerini Göster" +msgstr "Kılavuz çizgilerini göster" msgid "Show Gridlines on plate" msgstr "Kılavuz Çizgilerini plaka üzerinde göster" msgid "Reset Window Layout" -msgstr "Pencere Düzenini Sıfırla" +msgstr "Pencere düzenini sıfırla" msgid "Reset to default window layout" msgstr "Varsayılan pencere düzenine sıfırla" msgid "Show &Labels" -msgstr "Etiketleri Göster" +msgstr "Etiketleri göster" msgid "Show object labels in 3D scene." msgstr "3B sahnede nesne etiketlerini göster." msgid "Show &Overhang" -msgstr "Çıkıntıyı Göster" +msgstr "Çıkıntıyı göster" msgid "Show object overhang highlight in 3D scene." msgstr "3B sahnede nesne çıkıntısı vurgusunu göster." msgid "Show Selected Outline (beta)" -msgstr "Seçilen Taslağı Göster (Deneysel)" +msgstr "Seçilen taslağı göster (deneysel)" msgid "Show outline around selected object in 3D scene." msgstr "3D sahnede seçilen nesnenin etrafındaki ana hatları göster." @@ -6542,13 +6536,11 @@ msgstr "Düzen" msgid "View" msgstr "Görünüm" -# AI Translated msgid "Preset Bundle" -msgstr "Ön Ayar Paketi" +msgstr "Ön ayar paketi" -# AI Translated msgid "Sync Presets" -msgstr "Ön Ayarları Eşitle" +msgstr "Ön ayarları eşitle" # AI Translated msgid "Pull and apply the latest presets from OrcaCloud" @@ -6590,10 +6582,10 @@ msgid "Cornering calibration" msgstr "Viraj kalibrasyonu" msgid "Input Shaping Frequency" -msgstr "Input shaping Frekansı" +msgstr "Input shaping frekansı" msgid "Input Shaping Damping/zeta factor" -msgstr "Input shaping Sönümleme/zeta faktörü" +msgstr "Input shaping sönümleme/zeta faktörü" msgid "Input Shaping" msgstr "Input shaping" @@ -6601,9 +6593,8 @@ msgstr "Input shaping" msgid "VFA" msgstr "VFA" -# AI Translated msgid "Calibration Guide" -msgstr "Kalibrasyon Kılavuzu" +msgstr "Kalibrasyon kılavuzu" msgid "&Open G-code" msgstr "&G kodunu aç" @@ -8219,9 +8210,8 @@ msgstr "Bu dosyalar birden fazla parçadan oluşan tek bir nesne olarak mı yük msgid "An object with multiple parts was detected" msgstr "Birden fazla parçaya sahip nesne algılandı" -# AI Translated msgid "Auto-Drop" -msgstr "Otomatik Bırakma" +msgstr "Otomatik düşür" #, c-format, boost-format msgid "Connected printer is %s. It must match the project preset for printing.\n" @@ -8296,9 +8286,8 @@ msgstr "Seçilen nesne bölünemedi." msgid "Split to Objects" msgstr "Nesnelere Ayır" -# AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" -msgstr "Z konumunu korumak için Otomatik Bırakma devre dışı bırakılsın mı?\n" +msgstr "Z konumunu korumak için Otomatik düşürme devre dışı bırakılsın mı?\n" # AI Translated msgid "Object with floating parts was detected" @@ -8433,7 +8422,7 @@ msgid "Creating a new project" msgstr "Yeni bir proje oluşturma" msgid "Load project" -msgstr "Projeyi Aç" +msgstr "Projeyi aç" msgid "" "Failed to save the project.\n" @@ -8869,10 +8858,10 @@ msgid "Current Association: " msgstr "Mevcut Bağlantı: " msgid "Current Instance" -msgstr "Mevcut Örnek" +msgstr "Mevcut Kopya" msgid "Current Instance Path: " -msgstr "Mevcut Örnek Yolu: " +msgstr "Mevcut Kopya Yolu: " msgid "General" msgstr "Genel" @@ -10708,7 +10697,7 @@ msgid "Reserved keywords found" msgstr "Ayrılmış anahtar kelimeler bulundu" msgid "Setting Overrides" -msgstr "Ayarların Üzerine Yazma" +msgstr "Ayarların Üzerine Yaz" msgid "Basic information" msgstr "Temel Bilgiler" @@ -12227,7 +12216,7 @@ msgid "Group error in manual mode. Please check nozzle count or regroup." msgstr "Elle modda gruplama hatası. Lütfen nozul sayısını denetleyin veya yeniden gruplayın." msgid "Internal Bridge" -msgstr "İç Köprü" +msgstr "İç köprü" msgid "undefined error" msgstr "bilinmeyen hata" @@ -12920,7 +12909,6 @@ msgstr "Çıkıntı bu belirtilen eşiği aştığında, soğutma fanını aşa msgid "External bridge infill direction" msgstr "Dış köprü dolgu yönü" -# AI Translated #, no-c-format, no-boost-format msgid "" "External Bridging angle override.\n" @@ -12937,14 +12925,13 @@ msgstr "" "Aksi hâlde verilen açı şuna göre kullanılır:\n" " - Mutlak koordinatlar\n" " - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\n" -" - En uygun otomatik açı + bu değer: 'Göreli Köprü Açısı' etkinse\n" +" - En uygun otomatik açı + bu değer: ‘Göreceli Köprü Açısı' etkinse\n" "\n" "Sıfır mutlak açı için 180° kullanın." msgid "Internal bridge infill direction" msgstr "İç köprü dolgu yönü" -# AI Translated msgid "" "Internal Bridging angle override.\n" "If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n" @@ -12960,13 +12947,12 @@ msgstr "" "Aksi hâlde verilen açı şuna göre kullanılır:\n" " - Mutlak koordinatlar\n" " - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\n" -" - En uygun otomatik açı + bu değer: 'Göreli Köprü Açısı' etkinse\n" +" - En uygun otomatik açı + bu değer: 'Göreceli Köprü Açısı' etkinse\n" "\n" "Sıfır mutlak açı için 180° kullanın." -# AI Translated msgid "Relative bridge angle" -msgstr "Göreli köprü açısı" +msgstr "Göreceli köprü açısı" # AI Translated msgid "When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it." @@ -13413,7 +13399,7 @@ msgstr "" "Not: Elde edilen değer ilk katman akış oranından etkilenmez." msgid "Brim follows compensated outline" -msgstr "Kenar telafi edilen taslağı takip ediyor" +msgstr "Kenar toleranslı dış sınırı takip etsin" # AI Translated msgid "" @@ -13724,13 +13710,13 @@ msgid "" msgstr "" "Dikey kabuk kalınlığını garanti etmek için eğimli yüzeylerin yakınına katı dolgu ekleyin (üst + alt katı katmanlar)\n" "Yok: Hiçbir yere katı dolgu eklenmez. Dikkat: Modelinizin eğimli yüzeyleri varsa bu seçeneği dikkatli kullanın.\n" -"Yalnızca kritik: Duvarlar için katı dolgu eklemekten kaçının\n" +"Kritik: Duvarlar için katı dolgu eklemekten kaçının\n" "Orta: Yalnızca çok eğimli yüzeyler için katı dolgu ekleyin\n" "Hepsi: Tüm uygun eğimli yüzeyler için katı dolgu ekleyin\n" "Varsayılan değer Tümü'dür." msgid "Critical Only" -msgstr "Yalnızca kritik" +msgstr "Kritik" msgid "Moderate" msgstr "Orta" @@ -14247,7 +14233,7 @@ msgid "By First filament" msgstr "İlk filamente göre" msgid "By Highest Temp" -msgstr "En Yüksek Sıcaklığa Göre" +msgstr "En yüksek sıcaklığa göre" msgid "Filament diameter is used to calculate extrusion variables in G-code, so it is important that this is accurate and precise." msgstr "Filament çapı, gcode'da ekstrüzyonu hesaplamak için kullanılır; bu nedenle önemlidir ve doğru olmalıdır." @@ -14685,10 +14671,10 @@ msgid "Marlin Firmware Junction Deviation (replaces the traditional XY Jerk sett msgstr "Marlin Firmware Köşe Sapması (geleneksel XY Sarsıntı ayarının yerini alır)" msgid "Jerk of outer walls." -msgstr "Dış duvar JERK değeri." +msgstr "Dış duvar sarsıntı değeri." msgid "Jerk of inner walls." -msgstr "İç duvarlar JERK değeri." +msgstr "İç duvarlar sarsıntı değeri." msgid "Jerk for top surface." msgstr "Üst yüzey için JERK değeri." @@ -15721,7 +15707,7 @@ msgid "" "If your Marlin 2 printer uses Classic Jerk set this value to 0.)" msgstr "" "Maksimum bağlantı sapması (M205 J, yalnızca Marlin Aygıt Yazılımı için JD > 0 ise geçerlidir)\n" -"Marlin 2 yazıcınız Classic Jerk kullanıyorsa bu değeri 0 olarak ayarlayın.)" +"Marlin 2 yazıcınız Classic sarsıntı kullanıyorsa bu değeri 0 olarak ayarlayın.)" msgid "Minimum speed for extruding" msgstr "Ekstrüzyon için minimum hız" @@ -16914,7 +16900,7 @@ msgid "This setting only generates supports that begin on the build plate." msgstr "Model yüzeyinde destek oluşturmayın, yalnızca baskı plakasında." msgid "Support critical regions only" -msgstr "Yalnızca kritik bölgeleri destekleyin" +msgstr "Kritik bölgeleri destekleyin" msgid "Only create support for critical regions including sharp tail, cantilever, etc." msgstr "Yalnızca keskin kuyruk, konsol vb. gibi kritik bölgeler için destek oluşturun." @@ -19288,13 +19274,13 @@ msgid "" "To test Classic Jerk, set 'Maximum Junction Deviation' in Motion ability to 0." msgstr "" "Marlin 2 Kavşak Sapması tespit edildi:\n" -"Classic Jerk'i test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı 0'a ayarlayın." +"Classic sarsıntıyı test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı 0'a ayarlayın." msgid "" "Marlin 2 Classic Jerk detected:\n" "To test Junction Deviation, set 'Maximum Junction Deviation' in Motion ability to a value > 0." msgstr "" -"Marlin 2 Classic Jerk tespit edildi:\n" +"Marlin 2 Classic sarsıntı tespit edildi:\n" "Kavşak Sapmasını test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı > 0 değerine ayarlayın." msgid "" @@ -19538,7 +19524,7 @@ msgid "Start Test Single-Thread" msgstr "Tek İş Parçacığı Testini Başlat" msgid "Export Log" -msgstr "Logu Dışa Aktar" +msgstr "Logu dışa aktar" msgid "OrcaSlicer Version:" msgstr "OrcaSlicer Sürümü:" @@ -20281,9 +20267,8 @@ msgstr "Sistem klasörü silinemedi..." msgid "Failed to determine executable path." msgstr "Yürütülebilir dosya yolu belirlenemedi." -# AI Translated msgid "Failed to launch a new instance." -msgstr "Yeni bir örnek başlatılamadı." +msgstr "Yeni bir kopya başlatılamadı." # AI Translated msgid "log(s)" From 6c0f5eee55bb83212a3f99668b79fc8e4e7d52cc Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 18 Aug 2026 22:17:59 +0800 Subject: [PATCH 55/71] Clarify icon rescaling condition in Button::Rescale method --- src/slic3r/GUI/Widgets/Button.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 1a8cbefbce..94be6ce301 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -254,7 +254,8 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type) void Button::Rescale() { - if (this->active_icon.bmp().IsOk()) + // Only a named icon can be re-rasterized; one set from a wxBitmap has no source file, + if (!this->active_icon.name().empty()) this->active_icon.msw_rescale(); messureSize(); From 322dc9b6a69932198db0f97fe872102d8827bc7f Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 18 Aug 2026 12:19:27 -0300 Subject: [PATCH 56/71] Smooth more patterns (#15205) --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Fill/Fill.cpp | 6 +- src/libslic3r/Fill/Fill3DHoneycomb.cpp | 4 + src/libslic3r/Fill/FillConcentric.cpp | 23 +- src/libslic3r/Fill/FillCornerSmoothing.cpp | 226 ++++++++++++++ src/libslic3r/Fill/FillCornerSmoothing.hpp | 108 +++++++ src/libslic3r/Fill/FillCrossHatch.cpp | 4 + src/libslic3r/Fill/FillHoneycomb.cpp | 4 + src/libslic3r/Fill/FillLightning.cpp | 14 + src/libslic3r/Fill/FillPlanePath.cpp | 206 ++++--------- src/libslic3r/Fill/FillPlanePath.hpp | 6 +- src/libslic3r/Fill/FillRectilinear.cpp | 5 + src/libslic3r/PrintConfig.cpp | 5 +- src/libslic3r/PrintConfig.hpp | 23 ++ src/slic3r/GUI/ConfigManipulation.cpp | 2 +- tests/fff_print/test_fill.cpp | 287 ++++++++++++++++++ tests/libslic3r/CMakeLists.txt | 1 + .../libslic3r/test_fill_corner_smoothing.cpp | 173 +++++++++++ tests/libslic3r/test_fill_plane_path.cpp | 54 ++++ 19 files changed, 996 insertions(+), 157 deletions(-) create mode 100644 src/libslic3r/Fill/FillCornerSmoothing.cpp create mode 100644 src/libslic3r/Fill/FillCornerSmoothing.hpp create mode 100644 tests/libslic3r/test_fill_corner_smoothing.cpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 812d28e088..f7b4de6e25 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -149,6 +149,8 @@ set(lisbslic3r_sources Fill/FillConcentric.hpp Fill/FillConcentricInternal.cpp Fill/FillConcentricInternal.hpp + Fill/FillCornerSmoothing.cpp + Fill/FillCornerSmoothing.hpp Fill/Fill.cpp Fill/FillCrossHatch.cpp Fill/FillCrossHatch.hpp diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 88d87ddb26..0888e1bb55 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -970,9 +970,9 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p region_config.sparse_infill_rotate_template.value); params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty(); - // Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill. - // FillHilbertCurve::generate clamps and validates the value itself. - if (params.pattern == ipHilbertCurve) + // Orca: the smoothing factor only applies to the sparse infill patterns that + // implement it. The fills clamp and validate the value themselves. + if (is_smoothable_infill_pattern(params.pattern, params.multiline)) params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value; } else { const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.; diff --git a/src/libslic3r/Fill/Fill3DHoneycomb.cpp b/src/libslic3r/Fill/Fill3DHoneycomb.cpp index 5908f854de..ad5f8918fd 100644 --- a/src/libslic3r/Fill/Fill3DHoneycomb.cpp +++ b/src/libslic3r/Fill/Fill3DHoneycomb.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "Fill3DHoneycomb.hpp" namespace Slic3r { @@ -271,6 +272,9 @@ void Fill3DHoneycomb::_fill_surface_single( for (Polyline &pl : polylines){ pl.translate(bb.min); pl.simplify(5 * spacing); // simplify to 5x line width + // Orca: round the corners of the octahedral wave. The layers where the wave degenerates to a + // straight line have no corner to round. + smooth_polyline_corners(pl, params.smooth_factor, scaled(params.resolution)); } // Apply multiline offset if needed diff --git a/src/libslic3r/Fill/FillConcentric.cpp b/src/libslic3r/Fill/FillConcentric.cpp index a75d2ed7d3..1882f7a656 100644 --- a/src/libslic3r/Fill/FillConcentric.cpp +++ b/src/libslic3r/Fill/FillConcentric.cpp @@ -5,6 +5,7 @@ #include "Arachne/WallToolPaths.hpp" #include "FillConcentric.hpp" +#include "FillCornerSmoothing.hpp" #include namespace Slic3r { @@ -32,12 +33,32 @@ void FillConcentric::_fill_surface_single( Polygons loops = to_polygons(contracted); - ExPolygons last { std::move(contracted) }; + ExPolygons last { contracted }; while (! last.empty()) { last = offset2_ex(last, -(distance + min_spacing/2), +min_spacing/2); append(loops, to_polygons(last)); } + // Orca: round the corners of the loops. Unlike the other patterns these are never clipped to the + // fill region - they are its offsets - so a corner may only be rounded where the curve replacing it + // stays inside. Rounding cuts toward the inside of the turn, which around a hole, at a concave + // feature or across a thin region is outside the fill and would put the extrusion over a wall. + // The reach is capped at half the distance between two loops as well: a loop is as long as the + // object, and a corner cut by half of its side would swallow the neighbouring loops. + auto corner_stays_inside = [&contracted](const Vec2d &from, const Vec2d &to) { + // The straight chord between the ends of the curve is the deepest the curve can cut. + for (const double t : { 0.25, 0.5, 0.75 }) { + const Vec2d sample = from + t * (to - from); + const Point point(coord_t(sample.x()), coord_t(sample.y())); + if (std::none_of(contracted.begin(), contracted.end(), + [&point](const ExPolygon ®ion) { return region.contains(point); })) + return false; + } + return true; + }; + smooth_polygons_corners(loops, params.smooth_factor, scaled(params.resolution), 0.5 * distance, + corner_stays_inside); + // generate paths from the outermost to the innermost, to avoid // adhesion problems of the first central tiny loops loops = union_pt_chained_outside_in(loops); diff --git a/src/libslic3r/Fill/FillCornerSmoothing.cpp b/src/libslic3r/Fill/FillCornerSmoothing.cpp new file mode 100644 index 0000000000..2af9f6bb9c --- /dev/null +++ b/src/libslic3r/Fill/FillCornerSmoothing.cpp @@ -0,0 +1,226 @@ +#include + +#include "FillCornerSmoothing.hpp" + +namespace Slic3r { + +// Turns sharper than this are left untouched: both ends of the curve replacing such a corner nearly +// coincide, so the corner would be rounded into a degenerate loop instead of a hairpin. +static constexpr const double min_smoothed_turn_cosine = -0.9; + +// The control points are expressed in the (incoming, outgoing) basis of the corner, which is not +// orthonormal for turns other than a right angle. +using QuinticBezier = std::array; + +static bool is_bezier_flat(const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation) +{ + // A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every + // control point within a deviation-wide strip around the endpoint chord conservatively bounds the + // flattening error. The cross product is the perpendicular distance scaled by the chord length; + // comparing squared values avoids a square root. + auto in_plane = [&incoming, &outgoing](const Vec2d &c) { return c.x() * incoming + c.y() * outgoing; }; + const Vec2d chord = in_plane(curve.back() - curve.front()); + const double chord_length_sq = chord.squaredNorm(); + const double max_cross_sq = deviation * deviation * chord_length_sq; + + for (size_t i = 1; i + 1 < curve.size(); ++i) { + const Vec2d offset = in_plane(curve[i] - curve.front()); + const double cross = chord.x() * offset.y() - chord.y() * offset.x(); + if (cross * cross > max_cross_sq) + return false; + } + return true; +} + +static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right) +{ + // Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one + // control point to the left half and one to the right half; the latter is filled backwards to keep + // both resulting control polygons in their original parameter direction. + QuinticBezier subdivision = curve; + left.front() = subdivision.front(); + right.back() = subdivision.back(); + for (size_t level = 1; level < curve.size(); ++level) { + for (size_t i = 0; i + level < curve.size(); ++i) + subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]); + left[level] = subdivision.front(); + right[curve.size() - level - 1] = subdivision[curve.size() - level - 1]; + } +} + +static void flatten_bezier( + const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation, std::vector &output) +{ + // Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord. + // A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth, + // avoiding abrupt segment-length jumps at adaptive-depth boundaries. + static constexpr size_t max_depth = 16; + + std::vector subcurves(2); + subdivide_bezier(curve, subcurves[0], subcurves[1]); + + for (size_t depth = 1; depth < max_depth; ++depth) { + bool all_flat = true; + for (const QuinticBezier &c : subcurves) + if (!is_bezier_flat(c, incoming, outgoing, deviation)) { + all_flat = false; + break; + } + if (all_flat) + break; + std::vector finer(subcurves.size() * 2); + for (size_t i = 0; i < subcurves.size(); ++i) + subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]); + subcurves = std::move(finer); + } + + // The curve start is deliberately omitted so it can be shared with the straight leg feeding into it. + output.clear(); + output.reserve(subcurves.size()); + for (const QuinticBezier &c : subcurves) + output.emplace_back(c.back()); +} + +const std::vector& CornerSmoother::curve_coefficients( + const double corner_distance, const Vec2d &incoming, const Vec2d &outgoing) +{ + const double cosine = incoming.dot(outgoing); + // Corners of the same size and turn angle are congruent, so they flatten identically. An infill + // path walks over the very same corner over and over again, the Hilbert curve over a single one. + if (m_has_cached_coefficients && corner_distance == m_cached_distance && cosine == m_cached_cosine) + return m_cached_coefficients; + + // One canonical corner running from -corner_distance along the incoming leg to corner_distance + // along the outgoing one. At each end, the first three control points are collinear and equally + // spaced: the tangent follows the adjoining straight leg and the second derivative is zero. The + // endpoint curvature is therefore zero, giving G2 joins to both legs. + const double d = corner_distance; + const QuinticBezier corner_curve {{ + {-d, 0.}, {-0.7 * d, 0.}, {-0.4 * d, 0.}, {0., 0.4 * d}, {0., 0.7 * d}, {0., d} + }}; + // Retain a finite positive tolerance if the smoother was set up with an invalid one. + const double deviation = m_tolerance > 0. && std::isfinite(m_tolerance) ? m_tolerance : EPSILON; + flatten_bezier(corner_curve, incoming, outgoing, deviation, m_cached_coefficients); + + m_cached_distance = corner_distance; + m_cached_cosine = cosine; + m_has_cached_coefficients = true; + return m_cached_coefficients; +} + +void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next) +{ + m_corner_points.clear(); + + const Vec2d incoming_leg = corner - previous; + const Vec2d outgoing_leg = next - corner; + const double incoming_length = incoming_leg.norm(); + const double outgoing_length = outgoing_leg.norm(); + if (incoming_length < EPSILON || outgoing_length < EPSILON) { + m_corner_points.emplace_back(corner); + return; + } + + const Vec2d incoming = incoming_leg / incoming_length; + const Vec2d outgoing = outgoing_leg / outgoing_length; + const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x(); + // A collinear vertex is no corner at all, and a hairpin cannot be rounded, see above. + if (std::abs(cross) < EPSILON || incoming.dot(outgoing) < min_smoothed_turn_cosine) { + m_corner_points.emplace_back(corner); + return; + } + + // Consuming at most half of the shorter leg keeps the curves of two adjacent corners apart. + double corner_distance = m_corner_distance_ratio * std::min(incoming_length, outgoing_length); + if (m_max_corner_distance > 0.) + corner_distance = std::min(corner_distance, m_max_corner_distance); + + const Vec2d curve_start = corner - corner_distance * incoming; + const Vec2d curve_end = corner + corner_distance * outgoing; + if (m_corner_filter && !m_corner_filter(curve_start, curve_end)) { + m_corner_points.emplace_back(corner); + return; + } + + const std::vector &coefficients = curve_coefficients(corner_distance, incoming, outgoing); + m_corner_points.reserve(coefficients.size() + 1); + m_corner_points.emplace_back(curve_start); + for (const Vec2d &coefficient : coefficients) + m_corner_points.emplace_back(corner + coefficient.x() * incoming + coefficient.y() * outgoing); +} + +// Rounds the corners of a scaled point sequence. A polygon closes implicitly, so all of its vertices +// are corners; a polyline is an open path that keeps both of its ends, even where they coincide - a +// path returning to where it started retraces its way back and is not a loop. +static Points smooth_corners(const Points &points, const bool polygon, CornerSmoother &smoother) +{ + // A polygon has no free ends, so its first vertex is a corner like any other. Rounding it takes + // feeding the smoother the last vertex first, whose own output point is then dropped again. + size_t skip = polygon ? 1 : 0; + + Points smoothed; + smoothed.reserve(2 * points.size()); + auto emit = [&smoothed, &skip](const Vec2d &point) { + if (skip > 0) { + --skip; + return; + } + smoothed.emplace_back(coord_t(std::floor(point.x() + 0.5)), coord_t(std::floor(point.y() + 0.5))); + }; + + if (polygon) + smoother.push(points.back().cast(), emit); + for (const Point &point : points) + smoother.push(point.cast(), emit); + if (polygon) + // Wrap the first vertex around, so that the last one is a corner as well. + smoother.push(points.front().cast(), emit); + smoother.flush(emit); + + if (polygon) + // The flushed point is the wrapped first vertex, which a polygon does not store. + smoothed.pop_back(); + return smoothed; +} + +void smooth_polyline_corners(Polyline &polyline, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter); + if (!smoother.enabled() || polyline.size() < 3) + return; + + polyline.points = smooth_corners(polyline.points, false, smoother); + // Rounding back to the integer grid may collapse neighbouring samples of a curve. + polyline.remove_duplicate_points(); +} + +void smooth_polylines_corners(Polylines &polylines, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + if (sanitize_smooth_factor(smooth_factor) == 0.) + return; + for (Polyline &polyline : polylines) + smooth_polyline_corners(polyline, smooth_factor, tolerance, max_corner_distance, corner_filter); +} + +void smooth_polygons_corners(Polygons &polygons, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter); + if (!smoother.enabled()) + return; + + for (Polygon &polygon : polygons) { + if (polygon.size() < 3) + continue; + polygon.points = smooth_corners(polygon.points, true, smoother); + polygon.remove_duplicate_points(); + // The curves of the first and of the last corner may have met on the segment they share. A + // polygon closes implicitly, so it must not repeat its first vertex at the end. + if (polygon.points.size() > 1 && polygon.points.front() == polygon.points.back()) + polygon.points.pop_back(); + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillCornerSmoothing.hpp b/src/libslic3r/Fill/FillCornerSmoothing.hpp new file mode 100644 index 0000000000..1852fc4c67 --- /dev/null +++ b/src/libslic3r/Fill/FillCornerSmoothing.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Polyline.hpp" + +namespace Slic3r { + +// Orca: NaN or infinite factors disable the smoothing, everything else is clamped to <0, 1>. +inline double sanitize_smooth_factor(double smooth_factor) +{ + return std::isfinite(smooth_factor) ? std::clamp(smooth_factor, 0., 1.) : 0.; +} + +// Decides whether a corner may be replaced by the curve that leaves the path at `from` and rejoins it +// at `to`, both in the coordinate system of the pushed points. Rounding cuts toward the inside of the +// turn, so a path that is not clipped to the fill region afterwards needs this to stay inside it. +using CornerFilter = std::function; + +// Orca: Replaces the sharp vertices of an infill path with curves that join the adjoining straight +// legs with a continuous curvature, so the toolhead does not have to stop in every corner. +// Points are pushed one by one, because the plane path fills produce their path on the fly, and +// every point of the smoothed path is handed over to the caller supplied emit callback. +// Fully smoothed adjacent corners meet at the midpoint of the segment they share, so the emitted +// points may collapse onto each other once rounded to the integer grid of the caller. Dropping such +// duplicates is left to the caller, which is the only one knowing that grid. +class CornerSmoother +{ +public: + // tolerance is the maximum chordal deviation of the flattened curves, in the units of the pushed + // points. max_corner_distance caps how far a curve may reach along a leg, in the same units; it + // bounds how far a rounded corner moves away from the original path, which matters where the legs + // are much longer than the spacing of the pattern. Zero leaves the reach uncapped. + CornerSmoother(double smooth_factor, double tolerance, double max_corner_distance = 0., + CornerFilter corner_filter = {}) + : m_corner_distance_ratio(0.5 * sanitize_smooth_factor(smooth_factor)), m_tolerance(tolerance), + m_max_corner_distance(max_corner_distance), m_corner_filter(std::move(corner_filter)) + {} + + bool enabled() const { return m_corner_distance_ratio > 0.; } + + template void push(const Vec2d &point, Emit &emit) + { + if (m_pending == 0) { + emit(point); + m_previous = point; + } else if (m_pending > 1) { + round_corner(m_previous, m_corner, point); + for (const Vec2d &corner_point : m_corner_points) + emit(corner_point); + m_previous = m_corner; + } + m_corner = point; + m_pending = std::min(m_pending + 1, 2); + } + + // Emits the last point of the path and prepares the smoother for a new one. + template void flush(Emit &emit) + { + if (m_pending > 1) + emit(m_corner); + m_pending = 0; + } + +private: + // Fills m_corner_points with the points replacing the corner vertex. + void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next); + // Flattens the canonical corner curve of the given size and turn into coordinates of the + // (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner. + const std::vector& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing); + + // Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment + // is the maximum, otherwise the curves of two adjacent corners would overlap. + const double m_corner_distance_ratio; + const double m_tolerance; + const double m_max_corner_distance; + const CornerFilter m_corner_filter; + std::vector m_corner_points; + // Cached flattening of the last corner, valid for corners of the same size and turn angle. + std::vector m_cached_coefficients; + double m_cached_distance { 0. }; + double m_cached_cosine { 0. }; + bool m_has_cached_coefficients { false }; + + Vec2d m_previous { Vec2d::Zero() }; + Vec2d m_corner { Vec2d::Zero() }; + // Number of points held back: none, the first point of a path, or a corner candidate. + int m_pending { 0 }; +}; + +// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone. +// Both ends of a polyline are kept where they are, even when they coincide: such a path retraces its +// way back and joining its ends would turn it into a loop. See CornerSmoother for max_corner_distance. +void smooth_polyline_corners(Polyline &polyline, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); +void smooth_polylines_corners(Polylines &polylines, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); +// Polygons close implicitly, so every one of their vertices is a corner. +void smooth_polygons_corners(Polygons &polygons, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillCrossHatch.cpp b/src/libslic3r/Fill/FillCrossHatch.cpp index 571095eca4..98be5ef46b 100644 --- a/src/libslic3r/Fill/FillCrossHatch.cpp +++ b/src/libslic3r/Fill/FillCrossHatch.cpp @@ -3,6 +3,7 @@ #include "../Surface.hpp" #include #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "FillCrossHatch.hpp" namespace Slic3r { @@ -205,6 +206,9 @@ void FillCrossHatch ::_fill_surface_single( // shift the pattern to the actual space for (Polyline &pl : polylines) { pl.translate(bb.min); } + // Orca: round the corners of the transition layers. The repeat layers are straight lines and stay as they are. + smooth_polylines_corners(polylines, params.smooth_factor, scaled(params.resolution)); + // Apply multiline offset if needed multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/Fill/FillHoneycomb.cpp b/src/libslic3r/Fill/FillHoneycomb.cpp index a595cdb664..82679541da 100644 --- a/src/libslic3r/Fill/FillHoneycomb.cpp +++ b/src/libslic3r/Fill/FillHoneycomb.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" +#include "FillCornerSmoothing.hpp" #include "FillHoneycomb.hpp" namespace Slic3r { @@ -70,6 +71,9 @@ void FillHoneycomb::_fill_surface_single( } p.rotate(-direction.first, m.hex_center); p.simplify(5 * spacing); // simplify to 5x line width + // Orca: round the corners of the honeycomb cells. Done before the clipping, so that the + // curves are cut by the region boundary just like the sharp path would be. + smooth_polyline_corners(p, params.smooth_factor, scaled(params.resolution)); all_polylines.push_back(p); } } diff --git a/src/libslic3r/Fill/FillLightning.cpp b/src/libslic3r/Fill/FillLightning.cpp index 7937b9d129..77031b42e0 100644 --- a/src/libslic3r/Fill/FillLightning.cpp +++ b/src/libslic3r/Fill/FillLightning.cpp @@ -2,6 +2,7 @@ #include "../Print.hpp" #include "../ShortestPath.hpp" #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "FillLightning.hpp" #include "Lightning/Generator.hpp" @@ -17,6 +18,19 @@ void Filler::_fill_surface_single( const Layer &layer = generator->getTreesForLayer(this->layer_id); Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled(0.5 * this->spacing - this->overlap)); + // Orca: round the turns of the branches. Hairpins are left sharp, as they cannot be rounded, and + // the reach is capped: cutting a corner moves the branch, and a branch is as long as the object + // rather than as long as one cell of a pattern, so half of a leg would merge it with its neighbour + // instead of rounding the turn between them. Half the distance between two branches keeps them + // apart. With more than one line per infill wall the branches are printed as outlines drawn around + // them, and the outlines of branches that run into each other merge into a single one; moving a + // branch by more than a fraction of its printed width breaks such an outline up into separate + // loops, so that width bounds the reach as well. + const double branch_width = scaled(this->spacing) * params.multiline; + const double branch_spacing = branch_width / std::max(double(params.density), EPSILON); + const double max_reach = 0.5 * (params.multiline > 1 ? branch_width : branch_spacing); + smooth_polylines_corners(fill_lines, params.smooth_factor, scaled(params.resolution), max_reach); + // Apply multiline offset if needed multiline_fill(fill_lines, params, spacing); diff --git a/src/libslic3r/Fill/FillPlanePath.cpp b/src/libslic3r/Fill/FillPlanePath.cpp index 7c4f285ac6..577aef0600 100644 --- a/src/libslic3r/Fill/FillPlanePath.cpp +++ b/src/libslic3r/Fill/FillPlanePath.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" +#include "FillCornerSmoothing.hpp" #include "FillPlanePath.hpp" namespace Slic3r { @@ -288,145 +289,60 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x, } } -using QuinticBezier = std::array; - -static bool is_bezier_flat(const QuinticBezier &curve, const double deviation) -{ - // A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every - // control point within a deviation-wide strip around the endpoint chord conservatively bounds the - // flattening error. The cross product is the perpendicular distance scaled by the chord length; - // comparing squared values avoids a square root. - const Vec2d chord = curve.back() - curve.front(); - const double chord_length_sq = chord.squaredNorm(); - const double max_cross_sq = deviation * deviation * chord_length_sq; - - for (size_t i = 1; i + 1 < curve.size(); ++i) { - const Vec2d offset = curve[i] - curve.front(); - const double cross = chord.x() * offset.y() - chord.y() * offset.x(); - if (cross * cross > max_cross_sq) - return false; - } - return true; -} - -static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right) -{ - // Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one - // control point to the left half and one to the right half; the latter is filled backwards to keep - // both resulting control polygons in their original parameter direction. - QuinticBezier subdivision = curve; - left.front() = subdivision.front(); - right.back() = subdivision.back(); - for (size_t level = 1; level < curve.size(); ++level) { - for (size_t i = 0; i + level < curve.size(); ++i) - subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]); - left[level] = subdivision.front(); - right[curve.size() - level - 1] = subdivision[curve.size() - level - 1]; - } -} - -static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector &output) -{ - // Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord. - // A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth, - // avoiding abrupt segment-length jumps at adaptive-depth boundaries. - static constexpr size_t max_depth = 16; - - std::vector subcurves(2); - subdivide_bezier(curve, subcurves[0], subcurves[1]); - - for (size_t depth = 1; depth < max_depth; ++depth) { - bool all_flat = true; - for (const QuinticBezier &c : subcurves) - if (!is_bezier_flat(c, deviation)) { - all_flat = false; - break; - } - if (all_flat) - break; - std::vector finer(subcurves.size() * 2); - for (size_t i = 0; i < subcurves.size(); ++i) - subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]); - subcurves = std::move(finer); - } - - // The curve start is deliberately omitted so consecutive curve pieces can share it without duplication. - output.reserve(output.size() + subcurves.size()); - for (const QuinticBezier &c : subcurves) - output.emplace_back(c.back()); -} - +// Rounds the corners of the generated path on its way to the infill output. template -static void generate_smooth_hilbert_curve( - coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, - const double corner_distance, Output &output) +class SmoothingPolylineOutput { - // A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed - // generator, expand the larger requested dimension to the next valid Hilbert grid size. The output - // clipper or the later region intersection removes the padded part of the traversal. - size_t sz = 2; - const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y); - while (sz < sz0) - sz <<= 1; +public: + SmoothingPolylineOutput(Output &output, const double smooth_factor, const double tolerance) + : m_output(output), m_smoother(smooth_factor, tolerance) {} - const size_t point_count = sz * sz; - output.reserve(point_count); + void reserve(size_t n) { m_output.reserve(n); } + void add_point(const Vec2d &pt) { auto emit = emitter(); m_smoother.push(pt, emit); } + // The smoother holds back the last point of the path until it knows there is no corner left to round. + void finish() { auto emit = emitter(); m_smoother.flush(emit); } - // The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance - // if this helper is invoked with an invalid resolution. - const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON; - // Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance). - // At each end, the first three control points are collinear and equally spaced: the tangent follows - // the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore - // zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten - // it only once to the requested chordal-deviation tolerance. - const QuinticBezier corner_curve {{ - {-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.}, - {0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance} - }}; - std::vector curve_coefficients; - flatten_bezier(corner_curve, deviation, curve_coefficients); - - auto translated_point = [min_x, min_y](size_t idx) { - Point p = hilbert_n_to_xy(idx); - return Point(p.x() + min_x, p.y() + min_y); - }; - auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); }; - bool has_last_output = false; - Vec2d last_output; - // Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates - // to avoid emitting zero-length extrusion segments. - auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) { - if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) { - output.add_point(point); - last_output = point; - has_last_output = true; - } - }; - - Vec2d previous = to_vec2d(translated_point(0)); - Vec2d corner = to_vec2d(translated_point(1)); - add_point(previous); - // Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of - // its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline. - for (size_t i = 1; i + 1 < point_count; ++i) { - const Vec2d next = to_vec2d(translated_point(i + 1)); - const Vec2d incoming = (corner - previous).normalized(); - const Vec2d outgoing = (next - corner).normalized(); - const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x(); - - if (std::abs(cross) < EPSILON) { - add_point(corner); - } else { - add_point(corner - corner_distance * incoming); - for (const Vec2d &coefficient : curve_coefficients) - add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing); - } - - previous = corner; - corner = next; +private: + // The curves of two adjacent corners meet at the midpoint of the segment they share, where they + // may round to the very same output point. Drop those, they would be zero length extrusions. + auto emitter() + { + return [this](const Vec2d &pt) { + const Point snapped = m_output.scaled(pt); + if (m_has_last_snapped && snapped == m_last_snapped) + return; + m_last_snapped = snapped; + m_has_last_snapped = true; + m_output.add_point(pt); + }; } - add_point(corner); + + Output &m_output; + CornerSmoother m_smoother; + Point m_last_snapped { Point::Zero() }; + bool m_has_last_snapped { false }; +}; + +// Runs the path generator against the concrete output type, optionally through the corner smoother. +// The outputs do not share a virtual add_point(), so the type has to be resolved here. +template +static void generate_path(InfillPolylineOutput &output, const FillParams ¶ms, const double resolution, GenerateFn generate) +{ + const double smooth_factor = sanitize_smooth_factor(params.smooth_factor); + auto run = [smooth_factor, resolution, &generate](auto &out) { + if (smooth_factor == 0.) { + generate(out); + } else { + SmoothingPolylineOutput> smoothing(out, smooth_factor, resolution); + generate(smoothing); + smoothing.finish(); + } + }; + + if (output.clips()) + run(static_cast(output)); + else + run(output); } void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output) @@ -440,19 +356,8 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, const FillParams ¶ms, InfillPolylineOutput &output) { - const double smooth_factor = std::isfinite(params.smooth_factor) ? - std::clamp(params.smooth_factor, 0., 1.) : 0.; - if (smooth_factor == 0.) { - this->generate(min_x, min_y, max_x, max_y, resolution, output); - return; - } - - const double corner_distance = 0.5 * smooth_factor; - if (output.clips()) - generate_smooth_hilbert_curve( - min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast(output)); - else - generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output); + generate_path(output, params, resolution, + [min_x, min_y, max_x, max_y](auto &out) { generate_hilbert_curve(min_x, min_y, max_x, max_y, out); }); } template @@ -495,4 +400,11 @@ void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, c generate_octagram_spiral(min_x, min_y, max_x, max_y, output); } +void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, + const FillParams ¶ms, InfillPolylineOutput &output) +{ + generate_path(output, params, resolution, + [min_x, min_y, max_x, max_y](auto &out) { generate_octagram_spiral(min_x, min_y, max_x, max_y, out); }); +} + } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillPlanePath.hpp b/src/libslic3r/Fill/FillPlanePath.hpp index b4b25b73ae..a1e9068ca9 100644 --- a/src/libslic3r/Fill/FillPlanePath.hpp +++ b/src/libslic3r/Fill/FillPlanePath.hpp @@ -21,10 +21,10 @@ public: void add_point(const Vec2d& pt) { m_out.emplace_back(this->scaled(pt)); } Points&& result() { return std::move(m_out); } virtual bool clips() const { return false; } - -protected: + // The output grid the generated points are snapped to. const Point scaled(const Vec2d& fpt) const { return { coord_t(floor(fpt.x() * m_scale_out + 0.5)), coord_t(floor(fpt.y() * m_scale_out + 0.5)) }; } +protected: // Output polyline. Points m_out; @@ -93,6 +93,8 @@ public: protected: bool centered() const override { return true; } void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override; + void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, + const FillParams ¶ms, InfillPolylineOutput &output) override; }; } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 8b40b8753c..0c82354b38 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -18,6 +18,7 @@ #include "../ShortestPath.hpp" #include "../VariableWidth.hpp" +#include "FillCornerSmoothing.hpp" #include "FillRectilinear.hpp" // #define SLIC3R_DEBUG @@ -3364,6 +3365,10 @@ bool FillRectilinear::fill_surface_trapezoidal( for (Polyline &pl : polylines) pl.translate(rotate_vector.second); + // Orca: round the corners of the trapezoids. The straight base lines of the triangular family + // have no corner to round. + smooth_polylines_corners(polylines, params.smooth_factor, scaled(params.resolution)); + // Apply multiline fill multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f9d895332a..8083da954e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3469,9 +3469,8 @@ void PrintConfigDef::init_fff_params() def = this->add("sparse_infill_smooth_factor", coPercent); def->label = L("Sparse infill smooth factor"); def->category = L("Strength"); - def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, " - "while 100% produces the largest possible curves between adjacent infill lines. " - "Currently applies only to the Hilbert Curve."); + def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, " + "while 100% produces the largest possible curves between adjacent infill lines."); def->sidetext = "%"; def->min = 0; def->max = 100; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f51c1c6411..26a708b78d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -146,6 +146,29 @@ inline bool is_separable_infill_pattern(InfillPattern pattern) } } +// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option. +// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more +// than one line per infill wall; a single line makes them plain crossing lines with nothing to round. +inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline = 1) +{ + switch (pattern) { + case ipHilbertCurve: + case ipOctagramSpiral: + case ipLightning: + case ipHoneycomb: + case ip3DHoneycomb: + case ipConcentric: + case ipCrossHatch: + return true; + case ipGrid: + case ipTriangles: + case ipStars: + return multiline > 1; + default: + return false; + } +} + enum class IroningType { NoIroning, TopSurfaces, diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 3885a391b8..de94bb6b4b 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -752,7 +752,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool has_top_shell = has_top_shell_layers && config->option("top_surface_density")->value > 0; bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0; bool has_solid_infill = has_top_shell_layers || has_bottom_shell; - toggle_line("sparse_infill_smooth_factor", pattern == ipHilbertCurve); + toggle_line("sparse_infill_smooth_factor", is_smoothable_infill_pattern(pattern, config->opt_int("fill_multiline"))); toggle_field("top_surface_pattern", has_top_shell); toggle_field("bottom_surface_pattern", has_bottom_shell); toggle_field("top_surface_density", has_top_shell_layers); diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 5fbce5a342..07460d3990 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -698,3 +698,290 @@ TEST_CASE("Solid infill direction offsets every layer when no template is set", CHECK(delta == 30); } } + +TEST_CASE("Honeycomb infill rounds its cell corners with the smooth factor", "[Fill]") +{ + // A cell whose sides are several times the line width, so that the corners have room to be rounded. + const double spacing = 0.45; + const double density = 0.1; + auto fill = [spacing, density](double smooth_factor) { + std::unique_ptr filler(Slic3r::Fill::new_from_type("honeycomb")); + filler->spacing = spacing; + + FillParams params; + params.density = float(density); + params.dont_adjust = true; + // Keep the fragments apart, so that only the turns of the pattern itself are measured. + params.anchor_length_max = 0.f; + params.smooth_factor = smooth_factor; + + Slic3r::ExPolygon square{ Slic3r::Points{ + Point::new_scale(0., 0.), Point::new_scale(50., 0.), Point::new_scale(50., 50.), Point::new_scale(0., 50.) } }; + Slic3r::Surface surface(stInternal, square); + return filler->fill_surface(&surface, params); + }; + + // Cosine of the sharpest turn of any of the paths, 1 meaning none of them turns at all. + auto sharpest_turn_cosine = [](const Slic3r::Polylines &polylines) { + double sharpest = 1.; + for (const Polyline &polyline : polylines) + for (size_t i = 1; i + 1 < polyline.size(); ++i) { + const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast().normalized(); + const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; + }; + auto point_count = [](const Slic3r::Polylines &polylines) { + return std::accumulate(polylines.begin(), polylines.end(), size_t(0), + [](size_t count, const Polyline &polyline) { return count + polyline.size(); }); + }; + + const Slic3r::Polylines sharp = fill(0.); + const Slic3r::Polylines smooth = fill(1.); + + REQUIRE(!sharp.empty()); + REQUIRE(smooth.size() == sharp.size()); + REQUIRE(point_count(smooth) > point_count(sharp)); + // The cell corners turn by 60 degrees; smoothing replaces them by gentle curves. + REQUIRE(sharpest_turn_cosine(sharp) < 0.6); + REQUIRE(sharpest_turn_cosine(smooth) > 0.9); +} + +// Point count, number of turns sharper than 25 degrees and length of the sparse infill of a print. +// A rounded corner is a run of much gentler turns, so smoothing shows up as fewer sharp ones. +struct SparseInfillShape { + size_t point_count { 0 }; + size_t sharp_turns { 0 }; + size_t path_count { 0 }; + double length { 0. }; +}; + +static SparseInfillShape sparse_infill_shape(const Print &print) +{ + SparseInfillShape shape; + + auto account = [&shape](const ExtrusionPath &path) { + if (!sparse_role(path.role())) + return; + const Points3 &pts = path.polyline.points; + ++shape.path_count; + shape.point_count += pts.size(); + for (size_t i = 1; i < pts.size(); ++i) + shape.length += (pts[i] - pts[i - 1]).head<2>().cast().norm(); + for (size_t i = 1; i + 1 < pts.size(); ++i) { + const Vec2d incoming = (pts[i] - pts[i - 1]).head<2>().cast(); + const Vec2d outgoing = (pts[i + 1] - pts[i]).head<2>().cast(); + if (incoming.squaredNorm() > 0. && outgoing.squaredNorm() > 0. && + incoming.normalized().dot(outgoing.normalized()) < 0.9) + ++shape.sharp_turns; + } + }; + + for (const Layer *layer : print.objects().front()->layers()) + for (const LayerRegion *region : layer->regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) { + if (auto *path = dynamic_cast(entity)) + account(*path); + else if (auto *multi = dynamic_cast(entity)) + for (const ExtrusionPath &p : multi->paths) + account(p); + else if (auto *loop = dynamic_cast(entity)) + for (const ExtrusionPath &p : loop->paths) + account(p); + } + return shape; +} + +TEST_CASE("Lightning infill rounds the turns of its branches with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "15%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + // The branch turns are replaced by curves, which cut the corners off and take more points to + // describe. The turns where two branches are joined into one path stay sharp. + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Concentric infill rounds its loops with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "concentric"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Cross hatch infill rounds its transition layers with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "crosshatch"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one line", "[Fill]") +{ + auto shape_for = [](int multiline, const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "grid"}, + {"sparse_infill_density", "20%"}, + {"fill_multiline", multiline}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for(2, "0%"); + const SparseInfillShape smooth = shape_for(2, "100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); + + // A single line per infill wall is the plain crossing line grid, which has no corner of its own. + const SparseInfillShape single_sharp = shape_for(1, "0%"); + const SparseInfillShape single_smooth = shape_for(1, "100%"); + REQUIRE(single_sharp.point_count > 0); + REQUIRE(single_smooth.point_count == single_sharp.point_count); + REQUIRE(single_smooth.length == single_sharp.length); +} + +TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "3dhoneycomb"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Smoothed concentric infill stays inside the fill region", "[Fill][Regression]") +{ + // The concentric loops are offsets of the fill region and are never clipped to it, so a corner + // rounded across its boundary ends up in a hole or over a wall. Rounding cuts toward the inside of + // the turn, which leaves the region at every corner of a hole, and in a region thinner than the + // curve even at a corner turning inwards. + const bool thin_region = GENERATE(false, true); + ExPolygon region; + if (thin_region) { + // An L of two 1.2mm wide arms: cutting the corner they meet at crosses both of them. + region = ExPolygon{ Slic3r::Points{ + Point::new_scale(0., 0.), Point::new_scale(20., 0.), Point::new_scale(20., 1.2), + Point::new_scale(1.2, 1.2), Point::new_scale(1.2, 20.), Point::new_scale(0., 20.) } }; + } else { + region = ExPolygon{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(50., 0.), + Point::new_scale(50., 50.), Point::new_scale(0., 50.) }, + Slic3r::Points{ Point::new_scale(30., 20.), Point::new_scale(30., 30.), + Point::new_scale(20., 30.), Point::new_scale(20., 20.) } }; + } + CAPTURE(thin_region); + + auto fill = [®ion](double smooth_factor) { + std::unique_ptr filler(Slic3r::Fill::new_from_type("concentric")); + filler->spacing = 0.45; + + FillParams params; + params.density = 0.1f; + params.dont_adjust = true; + params.smooth_factor = smooth_factor; + + Slic3r::Surface surface(stInternal, region); + return filler->fill_surface(&surface, params); + }; + auto point_count = [](const Slic3r::Polylines &polylines) { + return std::accumulate(polylines.begin(), polylines.end(), size_t(0), + [](size_t count, const Polyline &polyline) { return count + polyline.size(); }); + }; + + const Slic3r::Polylines sharp = fill(0.); + const Slic3r::Polylines smooth = fill(1.); + REQUIRE(!sharp.empty()); + + // Nothing leaves the fill region, which the unrounded loops already touch from the inside. + const ExPolygons bounds = offset_ex(region, float(SCALED_EPSILON)); + REQUIRE(diff_pl(sharp, bounds).empty()); + REQUIRE(diff_pl(smooth, bounds).empty()); + // The corners that the region has room for are still rounded. + if (!thin_region) + REQUIRE(point_count(smooth) > point_count(sharp)); +} + +TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "[Fill][Regression]") +{ + // With more than one line per infill wall, the branches are printed as outlines drawn around them, + // and the outlines of branches that run close to each other merge into one. Rounding the branches + // before those outlines are built moves them apart, which breaks the merged outlines up into + // separate loops - many more of them, each needing its own travel move. + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "50%"}, + {"fill_multiline", 2}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.path_count > 0); + REQUIRE(smooth.path_count <= sharp.path_count); + // The outlines are still rounded. + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 1ad299473c..bc10bb4f73 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests test_preset_setting_id.cpp test_preset_diff.cpp test_elephant_foot_compensation.cpp + test_fill_corner_smoothing.cpp test_fill_plane_path.cpp test_geometry.cpp test_multimaterial_segmentation.cpp diff --git a/tests/libslic3r/test_fill_corner_smoothing.cpp b/tests/libslic3r/test_fill_corner_smoothing.cpp new file mode 100644 index 0000000000..f2c25e816d --- /dev/null +++ b/tests/libslic3r/test_fill_corner_smoothing.cpp @@ -0,0 +1,173 @@ +#include + +#include +#include +#include + +#include "libslic3r/Fill/FillCornerSmoothing.hpp" +#include "libslic3r/Polyline.hpp" +#include "libslic3r/libslic3r.h" + +using namespace Slic3r; + +namespace { + +// A right angle turn, with the outgoing leg ten times longer than the incoming one. +Polyline asymmetric_corner() +{ + return Polyline{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 100.) }; +} + +double max_turn_cosine(const Polyline &polyline) +{ + double sharpest = 1.; + for (size_t i = 1; i + 1 < polyline.size(); ++i) { + const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast().normalized(); + const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; +} + +bool contains(const Polyline &polyline, const Point &point) +{ + return std::find(polyline.points.begin(), polyline.points.end(), point) != polyline.points.end(); +} + +const double tolerance = scaled(0.0125); + +} // namespace + +TEST_CASE("Corner smoothing replaces a sharp vertex by a curve", "[FillCornerSmoothing]") +{ + const Polyline sharp = asymmetric_corner(); + Polyline smooth = sharp; + smooth_polyline_corners(smooth, 1., tolerance); + + REQUIRE(smooth.size() > sharp.size()); + REQUIRE(smooth.front() == sharp.front()); + REQUIRE(smooth.back() == sharp.back()); + // The right angle is gone, every remaining turn is a gentle one. + REQUIRE(max_turn_cosine(sharp) < 0.1); + REQUIRE(max_turn_cosine(smooth) > 0.9); + REQUIRE(smooth.length() < sharp.length()); +} + +TEST_CASE("Corner smoothing keeps the path untouched at a zero factor", "[FillCornerSmoothing]") +{ + const Polyline sharp = asymmetric_corner(); + + Polyline none = sharp; + smooth_polyline_corners(none, 0., tolerance); + REQUIRE(none.points == sharp.points); + + Polyline invalid = sharp; + smooth_polyline_corners(invalid, std::numeric_limits::quiet_NaN(), tolerance); + REQUIRE(invalid.points == sharp.points); +} + +TEST_CASE("Corner smoothing consumes at most half of the shorter leg", "[FillCornerSmoothing]") +{ + // The curve must not reach beyond the middle of either adjoining segment, otherwise the curves of + // two adjacent corners would overlap. The shorter leg is 10mm long, so the corner at (10, 0) is + // left 5mm before it and rejoined 5mm past it, even though the other leg is 100mm long. + Polyline smooth = asymmetric_corner(); + smooth_polyline_corners(smooth, 1., tolerance); + + REQUIRE(contains(smooth, Point::new_scale(5., 0.))); + REQUIRE(contains(smooth, Point::new_scale(10., 5.))); + // A Bezier curve stays within the convex hull of its control points, so the rounded path stays + // inside the box spanned by the two legs. + for (const Point &point : smooth.points) { + REQUIRE(point.x() >= 0); + REQUIRE(point.y() >= 0); + REQUIRE(point.x() <= Point::new_scale(10., 0.).x()); + REQUIRE(point.y() <= Point::new_scale(0., 100.).y()); + } +} + +TEST_CASE("Corner smoothing scales the curve with the factor", "[FillCornerSmoothing]") +{ + Polyline half = asymmetric_corner(); + smooth_polyline_corners(half, 0.5, tolerance); + Polyline full = asymmetric_corner(); + smooth_polyline_corners(full, 1., tolerance); + + // Half of the factor leaves the 10mm leg half as far from the corner. + REQUIRE(contains(half, Point::new_scale(7.5, 0.))); + REQUIRE(contains(full, Point::new_scale(5., 0.))); + // A larger factor rounds a wider portion of the legs, cutting more of the corner off. + REQUIRE(full.length() < half.length()); +} + +TEST_CASE("Corner smoothing leaves hairpins sharp", "[FillCornerSmoothing]") +{ + // Both ends of a curve replacing a nearly reversing turn coincide, which would round the hairpin + // into a degenerate loop instead of a tip. + Polyline hairpin{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(0., 0.5) }; + const Polyline sharp = hairpin; + smooth_polyline_corners(hairpin, 1., tolerance); + REQUIRE(hairpin == sharp); +} + +TEST_CASE("Corner smoothing follows the flattening tolerance", "[FillCornerSmoothing]") +{ + Polyline coarse = asymmetric_corner(); + smooth_polyline_corners(coarse, 1., scaled(0.2)); + Polyline fine = asymmetric_corner(); + smooth_polyline_corners(fine, 1., scaled(0.001)); + + REQUIRE(fine.size() > coarse.size()); + REQUIRE(fine.front() == coarse.front()); + REQUIRE(fine.back() == coarse.back()); +} + +TEST_CASE("Corner smoothing emits no zero length segments", "[FillCornerSmoothing]") +{ + // Fully smoothed adjacent corners meet at the midpoint of the segment they share. + Polyline zigzag; + for (int i = 0; i < 8; ++i) + zigzag.points.emplace_back(Point::new_scale(i, i % 2 ? 1. : 0.)); + smooth_polyline_corners(zigzag, 1., tolerance); + + for (size_t i = 1; i < zigzag.size(); ++i) + REQUIRE((zigzag[i] - zigzag[i - 1]).cast().squaredNorm() > 0.); +} + +TEST_CASE("Corner smoothing rounds every vertex of a polygon", "[FillCornerSmoothing]") +{ + // A polygon closes implicitly, so none of its corners may stay sharp, not even the first one. + const Polygon square{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.), + Point::new_scale(0., 10.) }; + Polygons smooth{ square }; + smooth_polygons_corners(smooth, 1., tolerance); + const Polyline rounded = smooth.front().split_at_first_point(); + + REQUIRE(smooth.front().size() > square.size()); + REQUIRE(max_turn_cosine(rounded) > 0.9); + // The turn from the closing segment back into the first one must be gentle as well. + const Vec2d incoming = (rounded[rounded.size() - 1] - rounded[rounded.size() - 2]).cast().normalized(); + const Vec2d outgoing = (rounded[1] - rounded[0]).cast().normalized(); + REQUIRE(incoming.dot(outgoing) > 0.9); + // None of the corners is cut by more than half of a 10mm side. + for (const Point &point : smooth.front().points) { + REQUIRE(point.x() >= 0); + REQUIRE(point.y() >= 0); + REQUIRE(point.x() <= Point::new_scale(10., 0.).x()); + REQUIRE(point.y() <= Point::new_scale(0., 10.).y()); + } +} + +TEST_CASE("Corner smoothing keeps the ends of a path that returns to its start", "[FillCornerSmoothing][Regression]") +{ + // A branch of a lightning tree walks out and retraces its way back, ending where it started. Its + // ends are two free ends that happen to coincide, and joining them would close it into a loop. + Polyline retrace{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.), + Point::new_scale(5., 10.), Point::new_scale(0., 0.) }; + const Polyline sharp = retrace; + smooth_polyline_corners(retrace, 1., tolerance); + + REQUIRE(retrace.size() > sharp.size()); + REQUIRE(retrace.front() == sharp.front()); + REQUIRE(retrace.back() == sharp.back()); +} diff --git a/tests/libslic3r/test_fill_plane_path.cpp b/tests/libslic3r/test_fill_plane_path.cpp index bbb75dce58..7fc7f4a6b0 100644 --- a/tests/libslic3r/test_fill_plane_path.cpp +++ b/tests/libslic3r/test_fill_plane_path.cpp @@ -27,6 +27,31 @@ public: } }; +class TestableOctagramSpiral : public FillOctagramSpiral +{ +public: + Points generate_points(double resolution, double smooth_factor = 0., coord_t max_coordinate = 7) + { + InfillPolylineOutput output(output_scale); + FillParams params; + params.smooth_factor = smooth_factor; + FillOctagramSpiral::generate(-max_coordinate, -max_coordinate, max_coordinate, max_coordinate, resolution, params, output); + return std::move(output.result()); + } +}; + +// Cosine of the sharpest turn of a path, 1 meaning it has no turn at all. +double sharpest_turn_cosine(const Points &points) +{ + double sharpest = 1.; + for (size_t i = 1; i + 1 < points.size(); ++i) { + const Vec2d incoming = (points[i] - points[i - 1]).cast().normalized(); + const Vec2d outgoing = (points[i + 1] - points[i]).cast().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; +} + double path_length(const Points &points) { double length = 0.; @@ -146,6 +171,35 @@ TEST_CASE("Hilbert smoothing joins straight segments with continuous curvature", REQUIRE(fine_entry_curvature < 0.25 * coarse_entry_curvature); } +TEST_CASE("Octagram spiral smoothing rounds the turns of the spiral", "[FillPlanePath]") +{ + const Points sharp = TestableOctagramSpiral().generate_points(0.005); + const Points smooth = TestableOctagramSpiral().generate_points(0.005, 1.); + + REQUIRE(smooth.size() > sharp.size()); + REQUIRE(smooth.front() == sharp.front()); + REQUIRE(smooth.back() == sharp.back()); + // The spiral alternates between 90 and 135 degree turns; both are rounded into gentle ones. + REQUIRE(sharpest_turn_cosine(sharp) < -0.7); + REQUIRE(sharpest_turn_cosine(smooth) > 0.9); + + for (size_t i = 1; i < smooth.size(); ++i) + REQUIRE((smooth[i] - smooth[i - 1]).cast().squaredNorm() > 0.); +} + +TEST_CASE("Octagram spiral smooth factor controls corner curvature", "[FillPlanePath]") +{ + const Points sharp = TestableOctagramSpiral().generate_points(0.005); + const Points half_smooth = TestableOctagramSpiral().generate_points(0.005, 0.5); + const Points full_smooth = TestableOctagramSpiral().generate_points(0.005, 1.); + const Points invalid_factor = TestableOctagramSpiral().generate_points( + 0.005, std::numeric_limits::quiet_NaN()); + + REQUIRE(path_length(full_smooth) < path_length(half_smooth)); + REQUIRE(path_length(half_smooth) < path_length(sharp)); + REQUIRE(invalid_factor == sharp); +} + TEST_CASE("Hilbert curve smooth factor controls corner curvature", "[FillPlanePath]") { const Points sharp = TestableHilbertCurve().generate_points(0.005); From 4fd7fdb3faa24bdb903d5c89bb5c83801c63345c Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 18 Aug 2026 12:25:59 -0300 Subject: [PATCH 57/71] Move smooth factor wiki link (#15287) --- src/slic3r/GUI/Tab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1a31355d0e..0ea685a02c 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2790,7 +2790,7 @@ void TabPrint::build() optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline"); optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern"); optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized"); - optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_patterns#sparse-infill-smooth-factor"); + optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor"); optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction"); optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage"); optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag"); From ffee4024941006da85c10109826607d8bb44d40a Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 19 Aug 2026 00:27:48 +0800 Subject: [PATCH 58/71] Give the printer-agents web Device tab its own page id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In printer-agents mode the legacy web page was appended under Notebook::PAGE_MONITOR, which resolves to the same "monitor" id as the native Device tab. FindPageByName returns the first match, so PluginPages::relayout() — which saves the selection by name and restores it after rebuilding the tab strip — moved the user off the web tab onto the native one. The tab also disagreed with its own label, being created as "Device (legacy)" and renamed to "Device (Web)" on the next show_device() call. --- src/slic3r/GUI/MainFrame.cpp | 5 +++-- src/slic3r/GUI/MainFrame.hpp | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 334bd9f3d4..9e3d40994b 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1385,7 +1385,7 @@ void MainFrame::show_device(bool should_use_native) { // The web page is appended when printer agents are enabled. Remove that // extra page before switching back to the normal native/Web layout. if (!use_printer_agents) { - if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != m_tabpanel->FindPageByName(TAB_ID_MONITOR)) { + if ((idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR_WEB)) != wxNOT_FOUND) { m_printer_view->Show(false); m_tabpanel->RemovePage(idx); } @@ -1447,7 +1447,8 @@ void MainFrame::show_device(bool should_use_native) { if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { m_printer_view->Show(false); - m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), false, Notebook::PAGE_MONITOR); + m_tabpanel->InsertPage(m_tabpanel->GetPageCount(), TAB_ID_MONITOR_WEB, m_printer_view, + _L("Device (Web)"), "tab_monitor_active", false); } else { m_tabpanel->SetPageText(idx, _L("Device (Web)")); } diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 6d8e548ce8..d6e8173288 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -43,6 +43,10 @@ #define TAB_ID_PREPARE "prepare" #define TAB_ID_PREVIEW "preview" #define TAB_ID_MONITOR "monitor" +// Printer-agents mode shows the legacy web page alongside the native Device tab, so it needs an +// id of its own: sharing TAB_ID_MONITOR makes every name lookup resolve to whichever of the two +// comes first, which silently defeats PluginPages' selection round-trip across a tab relayout. +#define TAB_ID_MONITOR_WEB "monitor_web" #define TAB_ID_MULTI_DEVICE "multi_device" #define TAB_ID_PROJECT "project" #define TAB_ID_CALIBRATION "calibration" From 02736fee163b09639e9c5780777ccf892152dc22 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 19 Aug 2026 00:27:48 +0800 Subject: [PATCH 59/71] Restore the web Device tab URL load on tab selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting the web Device tab loaded the printer's web UI from the selected discovered machine when the preset carried no host. That arm was lost merging main into this branch — two of the three Plater.cpp hunks from #15134 survived, this one did not — leaving the tab blank, since PrinterWebView starts on an empty URL and nothing else navigates it. --- src/slic3r/GUI/Plater.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9fad2574ab..bf9d697a7a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -11245,7 +11245,15 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } } else { - if (new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR) && wxGetApp().preset_bundle != nullptr) { + // Pointer test, not a name lookup: in printer-agents mode this page is TAB_ID_MONITOR_WEB + // while the native Device tab holds TAB_ID_MONITOR, and in legacy-web mode it holds + // TAB_ID_MONITOR itself. + const bool selecting_web_device_tab = main_frame->m_printer_view && + main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; + if (selecting_web_device_tab) { + // Use the selected discovered machine when the preset has no host. + main_frame->load_printer_url(); + } else if (new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR) && wxGetApp().preset_bundle != nullptr) { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); if (main_frame->m_printer_view && url.empty()) { From 5be1f8f20910970c31db9bb131d312960c941f13 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 19 Aug 2026 01:37:23 +0800 Subject: [PATCH 60/71] fix crash on Mac --- src/slic3r/GUI/Monitor.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 51bd7ed878..1a6d969988 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -413,7 +413,10 @@ void MonitorPanel::update_hms_tag() bool MonitorPanel::Show(bool show) { #ifdef __APPLE__ - wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize()); + // Notebook::InsertPage() hides every page it appends, so this also runs while MainFrame is + // still constructing, before GUI_App::mainframe is assigned. Same guard as Plater::Show(). + if (wxGetApp().mainframe) + wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize()); #endif NetworkAgent* m_agent = wxGetApp().getAgent(); From e840187edc83aee65cf447780edb32749c9b7be1 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Wed, 19 Aug 2026 03:12:56 +0800 Subject: [PATCH 61/71] fix: clang-cl arm64 wxWidgets path and plater desctructor before merging main --- deps/wxWidgets/0001-Clang-CL-fix.patch | 13 +++++++++---- src/slic3r/GUI/Plater.cpp | 2 ++ src/slic3r/GUI/Plater.hpp | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/deps/wxWidgets/0001-Clang-CL-fix.patch b/deps/wxWidgets/0001-Clang-CL-fix.patch index 4765b67c36..23bf23b3f4 100644 --- a/deps/wxWidgets/0001-Clang-CL-fix.patch +++ b/deps/wxWidgets/0001-Clang-CL-fix.patch @@ -1,18 +1,23 @@ --- - build/cmake/wxWidgetsConfig.cmake.in | 6 +++++- - 1 file changed, 5 insertions(+), 1 deletion(-) + build/cmake/wxWidgetsConfig.cmake.in | 10 +++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/build/cmake/wxWidgetsConfig.cmake.in b/build/cmake/wxWidgetsConfig.cmake.in index 1a83f36..70ad8a4 100644 --- a/build/cmake/wxWidgetsConfig.cmake.in +++ b/build/cmake/wxWidgetsConfig.cmake.in -@@ -58,7 +58,11 @@ if(WIN32_MSVC_NAMING) +@@ -58,7 +58,16 @@ if(WIN32_MSVC_NAMING) endif() endif() -include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake") +if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") -+ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/vc_x64_lib/@PROJECT_NAME@Targets.cmake") ++ if (CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR CMAKE_VS_PLATFORM_NAME STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") ++ set(_wx_clang_msvc_lib_dir "vc_arm64_lib") ++ else() ++ set(_wx_clang_msvc_lib_dir "vc_x64_lib") ++ endif() ++ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/${_wx_clang_msvc_lib_dir}/@PROJECT_NAME@Targets.cmake") +else() + include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake") +endif() diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index aecea8f3b8..99c28ed52c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4958,6 +4958,8 @@ private: bool show_warning_dialog { false }; }; +Plater::~Plater() = default; + const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf)", std::regex::icase); const std::regex Plater::priv::pattern_3mf(".*3mf", std::regex::icase); const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index c70c5ca7c1..d8e95bd7d5 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -283,7 +283,7 @@ public: Plater(const Plater &) = delete; Plater &operator=(Plater &&) = delete; Plater &operator=(const Plater &) = delete; - ~Plater() = default; + ~Plater(); bool Show(bool show = true); @@ -978,4 +978,4 @@ wxArrayString get_all_camera_view_type(); } // namespace GUI } // namespace Slic3r -#endif \ No newline at end of file +#endif From 1e87d56482f819a412b897860d24305259d24405 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 19 Aug 2026 14:08:19 +0800 Subject: [PATCH 62/71] Micro-refactor --- src/libslic3r/AppConfig.cpp | 23 +-- src/libslic3r/AppConfig.hpp | 1 - src/slic3r/GUI/MainFrame.cpp | 148 +++++++++----------- src/slic3r/GUI/MainFrame.hpp | 2 + src/slic3r/GUI/Notebook.cpp | 24 +--- src/slic3r/GUI/Notebook.hpp | 186 +++++++------------------ src/slic3r/GUI/Plater.cpp | 11 +- src/slic3r/GUI/Widgets/Button.cpp | 2 +- src/slic3r/plugin/host/PluginPages.cpp | 107 +++++++------- src/slic3r/plugin/host/PluginPages.hpp | 12 +- 10 files changed, 181 insertions(+), 335 deletions(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 970faeb620..4dc838172c 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -280,19 +280,8 @@ void AppConfig::set_defaults() set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap)); } - if (get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT).empty()) - set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT)); - else { - int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; - try { - visible_count = std::stoi(get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT)); - } - catch (...) { - visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; - } - visible_count = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); - set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(visible_count)); - } + // The getter already defaults, parses and clamps; write back what it resolves to. + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count())); if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty()) set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); @@ -1657,13 +1646,7 @@ int AppConfig::get_plugin_pages_visible_count() const catch (...) { return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; } - return std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); -} - -void AppConfig::set_plugin_pages_visible_count(int count) -{ - count = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); - set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(count)); + return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); } std::vector AppConfig::get_skipped_network_versions() const diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index de5c3a442f..65c57cdb30 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -382,7 +382,6 @@ public: // Number of plugin pages shown as fixed tabs before the rest are collapsed into a // dropdown on the last tab. int get_plugin_pages_visible_count() const; - void set_plugin_pages_visible_count(int count); std::vector get_skipped_network_versions() const; void add_skipped_network_version(const std::string& version); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 9e3d40994b..3b1cf1dffd 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -722,7 +722,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;} else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;} if (evt.CmdDown() && evt.GetKeyCode() == 'F') { - if (m_plater && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW)) { + if (m_plater && is_prepare_or_preview_tab()) { m_plater->sidebar().can_search(); } } @@ -1015,12 +1015,12 @@ void MainFrame::update_layout() case ESettingsLayout::Old: { m_plater->Reparent(m_tabpanel); - { - const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME); - const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast(home_idx) + 1; - m_tabpanel->InsertPage(prepare_pos, m_plater, _L("Prepare"), false, Notebook::PAGE_PREPARE); - m_tabpanel->InsertPage(prepare_pos + 1, m_plater, _L("Preview"), false, Notebook::PAGE_PREVIEW); - } + // Right after Home — or first, when there is no Home tab (PositionAfter() would + // append instead, and by now the other built-in tabs are already in place). + const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME); + const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast(home_idx) + 1; + m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active"); + m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active"); m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0); m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt) @@ -1290,24 +1290,6 @@ void MainFrame::init_tabpanel() { if (panel) panel->SetFocus(); - - /*switch (sel) { - case TabPosition::tpHome: - show_option(false); - break; - case TabPosition::tp3DEditor: - show_option(true); - break; - case TabPosition::tpPreview: - show_option(true); - break; - case TabPosition::tpMonitor: - show_option(false); - break; - default: - show_option(false); - break; - }*/ }); if (wxGetApp().is_editor()) { @@ -1317,7 +1299,7 @@ void MainFrame::init_tabpanel() { select_tab(TAB_ID_HOME); m_webview->load_url(url); }); - m_tabpanel->AddPage(m_webview, "", false, Notebook::PAGE_HOME); + m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active"); m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); } @@ -1332,7 +1314,7 @@ void MainFrame::init_tabpanel() { //BBS add pages m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_monitor->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_monitor, _L("Device"), false, Notebook::PAGE_MONITOR); + m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active"); m_printer_view = new PrinterWebView(m_tabpanel); Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { @@ -1347,16 +1329,16 @@ void MainFrame::init_tabpanel() { m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_multi_machine->SetBackgroundColour(*wxWHITE); // TODO: change the bitmap - m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), false, Notebook::PAGE_MULTI_DEVICE); + m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active"); } m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_project->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_project, _L("Project"), false, Notebook::PAGE_PROJECT); + m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), "tab_auxiliary_active"); m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); + m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), "tab_calibration_active"); // Plugin pages are appended after the built-in tabs; their ids are namespaced // (plugin..) so they can't collide with the built-in TAB_ID_* constants. @@ -1382,9 +1364,14 @@ void MainFrame::show_device(bool should_use_native) { const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); - // The web page is appended when printer agents are enabled. Remove that - // extra page before switching back to the normal native/Web layout. - if (!use_printer_agents) { + // The web Device page is the extra tab printer-agents mode shows alongside the native one. + // Printers that drive the native Bambu device tab have nothing to put in it, so they don't + // get it — otherwise a Bambu user sees two Device tabs, one of them permanently empty. + const bool want_web_device_tab = use_printer_agents && wxGetApp().preset_bundle != nullptr && + !wxGetApp().preset_bundle->use_bbl_device_tab(); + + // Remove the extra page before switching to any layout that shouldn't have it. + if (!want_web_device_tab) { if ((idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR_WEB)) != wxNOT_FOUND) { m_printer_view->Show(false); m_tabpanel->RemovePage(idx); @@ -1403,10 +1390,8 @@ void MainFrame::show_device(bool should_use_native) { m_tabpanel->RemovePage(idx); } m_monitor->Show(false); - const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); - const size_t monitor_pos = - (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active", false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + _L("Device"), "tab_monitor_active"); } if (m_printer_view == nullptr) { @@ -1427,30 +1412,31 @@ void MainFrame::show_device(bool should_use_native) { // TODO: change the bitmap if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) { m_multi_machine->Show(false); - const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); - const size_t multi_pos = - (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; - m_tabpanel->InsertPage(multi_pos, TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), - "tab_multi_active", false); + // Past the web Device tab when it is already there, so enabling multi-machine + // later can't wedge this page between the two Device tabs. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR_WEB, TAB_ID_MONITOR}), + TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active"); } } if (!m_calibration) { m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); } - // Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled, - // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position. if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) { m_calibration->Show(false); - m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration, + _L("Calibration"), "tab_calibration_active"); } - if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { - m_printer_view->Show(false); - m_tabpanel->InsertPage(m_tabpanel->GetPageCount(), TAB_ID_MONITOR_WEB, m_printer_view, - _L("Device (Web)"), "tab_monitor_active", false); - } else { - m_tabpanel->SetPageText(idx, _L("Device (Web)")); + if (want_web_device_tab) { + if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { + m_printer_view->Show(false); + // Immediately right of the native Device tab, not at the end of the tab bar. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MONITOR_WEB, + m_printer_view, _L("Device (Web)"), "tab_monitor_active"); + } else { + m_tabpanel->SetPageText(idx, _L("Device (Web)")); + } } #ifdef _MSW_DARK_MODE @@ -1458,7 +1444,7 @@ void MainFrame::show_device(bool should_use_native) { #endif // _MSW_DARK_MODE fit_tab_labels(); // ORCA on printer change - m_plugin_pages.relayout(); // keep plugin tabs after the native tabs just mutated above + m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above return; } @@ -1480,11 +1466,8 @@ void MainFrame::show_device(bool should_use_native) { m_monitor->SetBackgroundColour(*wxWHITE); } m_monitor->Show(false); - { - const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); - const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, m_monitor, _L("Device"), false, Notebook::PAGE_MONITOR); - } + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + _L("Device"), "tab_monitor_active"); if (wxGetApp().is_enable_multi_machine()) { if (!m_multi_machine) { @@ -1493,21 +1476,18 @@ void MainFrame::show_device(bool should_use_native) { } // TODO: change the bitmap m_multi_machine->Show(false); - { - const int monitor_idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR); - const size_t multi_pos = (monitor_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(monitor_idx) + 1; - m_tabpanel->InsertPage(multi_pos, m_multi_machine, _L("Multi-device"), false, Notebook::PAGE_MULTI_DEVICE); - } + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine, + _L("Multi-device"), "tab_multi_active"); } if (!m_calibration) { m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); } m_calibration->Show(false); - // Calibration is always appended last (AddPage), so it lands after whichever of Monitor/Multi-device - // actually got inserted above — no longer position-sensitive now that insertion position is computed - // from FindPageByName rather than a fixed TabPosition index. - m_tabpanel->AddPage(m_calibration, _L("Calibration"), false, Notebook::PAGE_CALIBRATION); + // Last of the built-in tabs, but plugin tabs already sit past it — anchor rather than + // append, so its position doesn't depend on the relayout() below running afterwards. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration, + _L("Calibration"), "tab_calibration_active"); #ifdef _MSW_DARK_MODE wxGetApp().UpdateDarkUIWin(this); @@ -1540,14 +1520,17 @@ void MainFrame::show_device(bool should_use_native) { }); } m_printer_view->Show(false); - { - const int preview_idx = m_tabpanel->FindPageByName(TAB_ID_PREVIEW); - const size_t monitor_pos = (preview_idx == wxNOT_FOUND) ? m_tabpanel->GetPageCount() : static_cast(preview_idx) + 1; - m_tabpanel->InsertPage(monitor_pos, m_printer_view, _L("Device"), false, Notebook::PAGE_MONITOR); - } + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view, + _L("Device"), "tab_monitor_active"); } fit_tab_labels(); // ORCA on printer change - m_plugin_pages.relayout(); // keep plugin tabs after the native tabs just mutated above + m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above +} + +bool MainFrame::is_prepare_or_preview_tab() const +{ + const wxString tab = m_tabpanel->GetSelectedPageName(); + return tab == TAB_ID_PREPARE || tab == TAB_ID_PREVIEW; } void MainFrame::fit_tab_labels() @@ -3168,7 +3151,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective")); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, + this, [this]() { return is_prepare_or_preview_tab(); }, [this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this); viewMenu->AppendSeparator(); @@ -3186,7 +3169,7 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_3d_navigator(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, + this, [this]() { return is_prepare_or_preview_tab(); }, [this]() { return wxGetApp().show_3d_navigator(); }, this); append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"), @@ -3194,15 +3177,14 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_plate_gridlines(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, this, - [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, + [this]() { return is_prepare_or_preview_tab(); }, [this]() { return wxGetApp().show_plate_gridlines(); }, this); append_menu_item( viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"), [this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this, [this]() { - return (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE || m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW) && - m_plater->is_sidebar_enabled(); + return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled(); }, this); @@ -4025,10 +4007,8 @@ void MainFrame::select_tab(wxPanel* panel) wxGetApp().params_dialog()->Popup(); return; } - // page_name cannot be resolved via panel->GetName() — Prepare and Preview - // share the single m_plater window, so the window itself has no single correct - // name (see Global Constraints). Resolve via Notebook's per-slot m_pageNames - // instead, via the index -> id lookup, which works for any page (built-in or not). + // Not panel->GetName(): Prepare and Preview share the single m_plater window, so the + // window has no one correct name. The slot -> id lookup is the only correct resolution. int page_idx = m_tabpanel->FindPage(panel); wxString page_name = (page_idx == wxNOT_FOUND) ? wxString() : m_tabpanel->GetPageName(static_cast(page_idx)); if (page_name == TAB_ID_PREPARE && m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW) @@ -4082,10 +4062,8 @@ void MainFrame::select_tab(const wxString& id/* = wxString()*/) m_plater->get_current_canvas3D()->render(); }*/ #endif - // NOTE: this checks the ORIGINAL parameter (id), not the resolved new_selection — - // preserving that the fallback-to-last-tab path never triggers this render call - // even if the last selected tab happened to be Prepare. Do not "simplify" to - // new_selection == TAB_ID_PREPARE, that changes behavior. + // Intentionally `id`, not `new_selection`: the fallback-to-last-tab path must not + // trigger this render even when the last selected tab was Prepare. if (id == TAB_ID_PREPARE && m_layout == ESettingsLayout::Old) m_plater->canvas3D()->render(); else if (was_hidden) { diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index d6e8173288..2052860d80 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -362,6 +362,8 @@ public: //SoftFever void show_device(bool should_use_native); void fit_tab_labels(); // ORCA + // True while either of the two tabs backed by m_plater is selected. + bool is_prepare_or_preview_tab() const; PluginPages& plugin_pages() { return m_plugin_pages; } PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr }; diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index f9db689faa..673508454a 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -156,14 +156,13 @@ void ButtonsListCtrl::SetSelection(int sel) Refresh(); } -bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, int imageId /* = wxBookCtrlBase::NO_IMAGE */) +bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */) { Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER); btn->SetCornerRadius(0); - if (bmp_name.empty() && m_imageList != nullptr && imageId != wxBookCtrlBase::NO_IMAGE && imageId >= 0 && - imageId < m_imageList->GetImageCount()) - btn->SetIcon(m_imageList->GetBitmap(imageId)); + if (bmp_name.empty() && bmp.IsOk()) + btn->SetIcon(bmp); int em = em_unit(this); //BBS set size for button @@ -232,23 +231,6 @@ bool ButtonsListCtrl::SetPageImage(size_t n, const std::string& bmp_name) const return true; } -bool ButtonsListCtrl::SetPageImage(size_t n, int imageId) -{ - if (n >= m_pageButtons.size()) - return false; - - if (imageId == wxBookCtrlBase::NO_IMAGE) { - m_pageButtons[n]->SetIcon(wxBitmap()); - return true; - } - - if (m_imageList == nullptr || imageId < 0 || imageId >= m_imageList->GetImageCount()) - return false; - - m_pageButtons[n]->SetIcon(m_imageList->GetBitmap(imageId)); - return true; -} - void ButtonsListCtrl::SetPageText(size_t n, const wxString& strText) { Button* btn = m_pageButtons[n]; diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index da90535481..4734122b18 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -3,11 +3,11 @@ //#ifdef _WIN32 +#include #include #include -#include #include -#include +#include #include class ScalableButton; @@ -27,11 +27,9 @@ public: void SetSelection(int sel); void UpdateMode(); void Rescale(); - bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", int imageId = wxBookCtrlBase::NO_IMAGE); + bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const wxBitmap &bmp = wxNullBitmap); void RemovePage(size_t n); bool SetPageImage(size_t n, const std::string& bmp_name) const; - bool SetPageImage(size_t n, int imageId); - void SetImageList(wxImageList* imageList) { m_imageList = imageList; } void SetPageText(size_t n, const wxString& strText); void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; @@ -49,23 +47,12 @@ private: int m_btn_margin; int m_line_margin; std::vector m_pageLabels; // ORCA - wxImageList* m_imageList{nullptr}; wxWindow* m_overflow_button{nullptr}; // ORCA }; class Notebook : public wxBookCtrlBase { public: - // Negative values below wxBookCtrlBase::NO_IMAGE are reserved for the built-in - // tabs. Nonnegative values are wxImageList indices supplied by plugin pages. - static constexpr int PAGE_HOME = -2; - static constexpr int PAGE_PREPARE = -3; - static constexpr int PAGE_PREVIEW = -4; - static constexpr int PAGE_MONITOR = -5; - static constexpr int PAGE_MULTI_DEVICE = -6; - static constexpr int PAGE_PROJECT = -7; - static constexpr int PAGE_CALIBRATION = -8; - Notebook(wxWindow * parent, wxWindowID winid = wxID_ANY, const wxPoint & pos = wxDefaultPosition, @@ -156,83 +143,58 @@ public: // Implement base class pure virtual methods. + // Page management. Every insertion funnels through the InsertPage() below; `id` is the + // stable page name FindPageByName() resolves. Built-in tabs name a resource bitmap, + // plugin pages hand over a ready wxBitmap; wx's own imageId overloads carry neither. + bool AddPage(const wxString& id, + wxWindow* page, + const wxString& text, + const std::string& bmp_name = "", + bool bSelect = false) + { + DoInvalidateBestSize(); + return InsertPage(GetPageCount(), id, page, text, bmp_name, bSelect); + } + bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override { DoInvalidateBestSize(); return InsertPage(GetPageCount(), page, text, bSelect, imageId); } - // Page management - virtual bool InsertPage(size_t n, - wxWindow * page, - const wxString & text, - bool bSelect = false, - int imageId = NO_IMAGE) override - { - wxString page_name; - std::string bmp_name; - const bool is_fixed_page = get_fixed_page_info(imageId, page_name, bmp_name); - const int stored_image_id = is_fixed_page ? NO_IMAGE : imageId; - - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, stored_image_id)) - return false; - - m_pageNames.insert(m_pageNames.begin() + n, page_name); - m_pageImageIds.insert(m_pageImageIds.begin() + n, stored_image_id); - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, stored_image_id); - - if (!DoSetSelectionAfterInsertion(n, bSelect)) - page->Hide(); - - return true; - } - - bool InsertPage(size_t n, - const wxString& id, - wxWindow* page, - const wxString& text, - int imageId, - bool bSelect = false) - { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) - return false; - - m_pageNames.insert(m_pageNames.begin() + n, id); - m_pageImageIds.insert(m_pageImageIds.begin() + n, imageId); - GetBtnsListCtrl()->InsertPage(n, text, bSelect, "", imageId); - - if (!DoSetSelectionAfterInsertion(n, bSelect)) - page->Hide(); - - return true; - } - bool InsertPage(size_t n, const wxString& id, wxWindow * page, const wxString & text, const std::string& bmp_name = "", - bool bSelect = false) + bool bSelect = false, + const wxBitmap& bmp = wxNullBitmap) { if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return false; m_pageNames.insert(m_pageNames.begin() + n, id); - m_pageImageIds.insert(m_pageImageIds.begin() + n, NO_IMAGE); - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, bmp); - // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the - // new page to the current page's rect — it never touches visibility. A freshly - // constructed page defaults to shown, so without this it renders on top of - // whatever page is currently selected until the next SetSelection() call hides - // it. Mirrors the pure-virtual InsertPage() override above, which already does - // this correctly. + // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the new + // page to the current page's rect — it never touches visibility, and a freshly + // constructed page defaults to shown. Without this it renders on top of whatever + // page is currently selected until the next SetSelection() call hides it. if (!DoSetSelectionAfterInsertion(n, bSelect)) page->Hide(); return true; } + virtual bool InsertPage(size_t n, + wxWindow * page, + const wxString & text, + bool bSelect = false, + int WXUNUSED(imageId) = NO_IMAGE) override + { + return InsertPage(n, wxString(), page, text, "", bSelect); + } + virtual int SetSelection(size_t n) override { int ret = DoSetSelection(n, SetSelection_SendEvent); @@ -262,7 +224,8 @@ public: return DoSetSelection(n); } - // Labels are stored by the custom button list; page images use the wx image-list IDs below. + // Labels are stored by the custom button list; wx's image-list API is unused — tab icons + // are set directly on the buttons, either from a resource name or a ready wxBitmap. virtual bool SetPageText(size_t n, const wxString & strText) override { wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page")); @@ -278,27 +241,14 @@ public: return GetBtnsListCtrl()->GetPageText(n); } - virtual bool SetPageImage(size_t n, int imageId) override + virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override { - if (n >= m_pageImageIds.size()) - return false; - - if (!GetBtnsListCtrl()->SetPageImage(n, imageId)) - return false; - - m_pageImageIds[n] = imageId; - return true; + return false; } - virtual int GetPageImage(size_t n) const override + virtual int GetPageImage(size_t WXUNUSED(n)) const override { - return n < m_pageImageIds.size() ? m_pageImageIds[n] : NO_IMAGE; - } - - void SetImageList(wxImageList* imageList) - { - m_imageList = imageList; - GetBtnsListCtrl()->SetImageList(imageList); + return NO_IMAGE; } bool SetPageImage(size_t n, const std::string& bmp_name) @@ -314,22 +264,27 @@ public: page->SetFocus(); } - // wxBookCtrlBase::DeleteAllPages() clears its page list directly rather than - // going through DoRemovePage() per page, so it would otherwise leave - // m_pageNames desynchronized (a mutation path outside the four this class - // already keeps in sync). Not currently called on a Notebook anywhere in - // this codebase, but kept correct for the same reason the rest of this - // bookkeeping exists. + // The base clears its page list directly instead of calling DoRemovePage() per page, + // which would leave m_pageNames behind. No caller today; kept in sync regardless. virtual bool DeleteAllPages() override { m_pageNames.clear(); - m_pageImageIds.clear(); return wxBookCtrlBase::DeleteAllPages(); } ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast(m_bookctrl); } void SetOverflowButton(wxWindow* button) { GetBtnsListCtrl()->SetOverflowButton(button); } + // Insertion index just past the first of `ids` that is present, or the end of the bar + // if none is — lets call sites state tab order as "after X" instead of re-deriving it. + size_t PositionAfter(std::initializer_list ids) const + { + for (const char* id : ids) + if (const int idx = FindPageByName(id); idx != wxNOT_FOUND) + return static_cast(idx) + 1; + return GetPageCount(); + } + int FindPageByName(const wxString& id) const { if (id.empty()) @@ -485,7 +440,6 @@ protected: if (win) { m_pageNames.erase(m_pageNames.begin() + page); - m_pageImageIds.erase(m_pageImageIds.begin() + page); GetBtnsListCtrl()->RemovePage(page); DoSetSelectionAfterRemoval(page); } @@ -509,47 +463,9 @@ protected: } private: - static bool get_fixed_page_info(int imageId, wxString& page_name, std::string& bmp_name) - { - switch (imageId) { - case PAGE_HOME: - page_name = wxS("home"); - bmp_name = "tab_home_active"; - return true; - case PAGE_PREPARE: - page_name = wxS("prepare"); - bmp_name = "tab_3d_active"; - return true; - case PAGE_PREVIEW: - page_name = wxS("preview"); - bmp_name = "tab_preview_active"; - return true; - case PAGE_MONITOR: - page_name = wxS("monitor"); - bmp_name = "tab_monitor_active"; - return true; - case PAGE_MULTI_DEVICE: - page_name = wxS("multi_device"); - bmp_name = "tab_multi_active"; - return true; - case PAGE_PROJECT: - page_name = wxS("project"); - bmp_name = "tab_auxiliary_active"; - return true; - case PAGE_CALIBRATION: - page_name = wxS("calibration"); - bmp_name = "tab_calibration_active"; - return true; - default: - return false; - } - } - void Init(); - std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages - std::vector m_pageImageIds; // index-parallel to wxBookCtrlBase::m_pages - wxImageList* m_imageList{nullptr}; + std::vector m_pageNames; // index-parallel to wxBookCtrlBase::m_pages wxShowEffect m_showEffect, m_hideEffect; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5dd77f4389..aba0910630 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -11221,19 +11221,18 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) const int new_sel = e.GetSelection(); if (new_sel == wxNOT_FOUND) { - // Guards against new_sel matching FindPageByName's own wxNOT_FOUND sentinel - // below when a TAB_ID_* isn't currently present in the tabpanel. + // GetPage(new_sel) below needs a valid index. e.Skip(); return; } - sidebar_layout.show = new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_PREPARE) || - new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_PREVIEW); + const wxString new_name = main_frame->m_tabpanel->GetPageName(new_sel); + sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW; update_sidebar(); int old_sel = e.GetOldSelection(); const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_native_device_tab = wxGetApp().preset_bundle && (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); - if (use_native_device_tab && new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR)) { + if (use_native_device_tab && new_name == TAB_ID_MONITOR) { // BBL network module is only required for BBL-vendor printers. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. if (!use_printer_agents && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { @@ -11253,7 +11252,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) if (selecting_web_device_tab) { // Use the selected discovered machine when the preset has no host. main_frame->load_printer_url(); - } else if (new_sel == main_frame->m_tabpanel->FindPageByName(TAB_ID_MONITOR) && wxGetApp().preset_bundle != nullptr) { + } else if (new_name == TAB_ID_MONITOR && wxGetApp().preset_bundle != nullptr) { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); if (main_frame->m_printer_view && url.empty()) { diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 94be6ce301..74ed2cbadd 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -288,7 +288,7 @@ void Button::render(wxDC& dc) wxSize szIcon; wxSize textSize = this->textSize.GetSize(); - ScalableBitmap icon = active_icon; + const ScalableBitmap& icon = active_icon; wxSize padding = this->paddingSize; int spacing = 5; // Wrap text diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index 90e1282dd9..fed16ee416 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -171,9 +171,11 @@ void PluginPage::on_script_message(wxWebViewEvent& event) root.value("kind", std::string()) != "message") return; - const nlohmann::json data = root.contains("data") ? root["data"] : nlohmann::json(); + const auto data = root.find("data"); try { - m_cap->on_message(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)); + m_cap->on_message(data == root.end() + ? "null" + : data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)); } catch (const std::exception& error) { BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what(); } catch (...) { @@ -186,28 +188,23 @@ void PluginPage::push_message(const std::string& message) if (m_browser == nullptr) return; - nlohmann::json data = nlohmann::json::parse(message, nullptr, false); - if (data.is_discarded()) - data = message; + // PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a + // non-JSON payload needs wrapping as a string literal. + const std::string payload = nlohmann::json::accept(message) + ? message + : nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); - const wxString script = wxString("(function dispatch(payload, attempts) {\n") + - wxString(" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n") + - wxString(" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n") + - wxString("})({data: ") + - wxString::FromUTF8(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)) + - wxString("}, 0);"); - WebView::RunScript(m_browser, script); + WebView::RunScript(m_browser, wxString::Format( + "(function dispatch(payload, attempts) {\n" + " if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n" + " if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n" + "})({data: %s}, 0);", + wxString::FromUTF8(payload))); } PluginPages::~PluginPages() { shutdown(); - // try { - // } catch (const std::exception& error) { - // BOOST_LOG_TRIVIAL(error) << "PluginPages::~PluginPages: shutdown() threw: " << error.what(); - // } catch (...) { - // BOOST_LOG_TRIVIAL(error) << "PluginPages::~PluginPages: shutdown() threw a non-standard exception"; - // } } void PluginPages::initialize(Notebook* parent) @@ -219,9 +216,6 @@ void PluginPages::initialize(Notebook* parent) m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count(); - m_image_list = std::make_unique(20, 20, true, 0); - m_parent->SetImageList(m_image_list.get()); - for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { if (capability) create_page(capability->identity()); @@ -233,15 +227,12 @@ void PluginPages::shutdown() { while (!m_pages.empty()) remove_page(m_pages.begin()->first); - if (m_parent != nullptr) - m_parent->SetImageList(nullptr); - m_image_list.reset(); m_parent = nullptr; } void PluginPages::set_visible_page_count(int count) { - const int clamped = std::max(PLUGIN_PAGES_VISIBLE_COUNT_MIN, std::min(count, PLUGIN_PAGES_VISIBLE_COUNT_MAX)); + const int clamped = std::clamp(count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); if (clamped == m_visible_page_count) return; @@ -282,17 +273,14 @@ bool PluginPages::create_page(const PluginCapabilityId& id) return false; } - int image_id = wxBookCtrlBase::NO_IMAGE; - if (!icon.empty() && m_image_list) { + if (!icon.empty()) { try { boost::filesystem::path icon_path(icon); const std::string extension = icon_path.extension().string(); if (extension == ".svg" || extension == ".png") icon_path.replace_extension(); - const wxBitmap bitmap = create_scaled_bitmap(icon_path.string(), m_parent, 20); - if (bitmap.IsOk()) - image_id = m_image_list->Add(bitmap); + page->set_icon(create_scaled_bitmap(icon_path.string(), m_parent, 20)); } catch (const std::exception& error) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key << ": " << error.what(); } catch (...) { @@ -300,7 +288,6 @@ bool PluginPages::create_page(const PluginCapabilityId& id) } } - page->set_icon_image_id(image_id); m_pages.emplace(id, page); m_order.push_back(id); return true; @@ -349,24 +336,11 @@ void PluginPages::remove_page(const PluginCapabilityId& id) return; PluginPage* page = it->second; - const int removed_image_id = page->get_icon_image_id(); page->detach_capability(); m_pages.erase(it); m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end()); - if (m_image_list && removed_image_id != wxBookCtrlBase::NO_IMAGE && - removed_image_id >= 0 && removed_image_id < m_image_list->GetImageCount()) { - m_image_list->Remove(removed_image_id); - - // wxImageList IDs are positional. Removing one shifts all later images down by one. - for (auto& [other_id, other_page] : m_pages) { - const int other_image_id = other_page->get_icon_image_id(); - if (other_image_id > removed_image_id) - other_page->set_icon_image_id(other_image_id - 1); - } - } - const int idx = m_parent != nullptr ? m_parent->FindPage(page) : wxNOT_FOUND; if (idx != wxNOT_FOUND) m_parent->RemovePage(idx); @@ -394,14 +368,6 @@ void PluginPages::relayout() }), m_order.end()); - wxString id_to_reselect = m_parent->GetSelectedPageName(); - - for (const auto& [id, page] : m_pages) { - const int idx = m_parent->FindPage(page); - if (idx != wxNOT_FOUND) - m_parent->RemovePage(idx); - } - const int visible_slots = std::max(1, m_visible_page_count); const bool need_overflow = static_cast(m_order.size()) > visible_slots; @@ -421,9 +387,37 @@ void PluginPages::relayout() tab_ids.push_back(*m_swapped_in_id); } - for (const auto& id : tab_ids) { - PluginPage* page = m_pages.at(id); - m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), page->get_icon_image_id()); + // MainFrame::show_device() relayouts on every printer change and most of those change + // nothing, so only touch the notebook when the trailing slots don't already spell out + // tab_ids — a rebuild destroys and recreates every tab button and rasterizes every icon. + const size_t page_count = m_parent->GetPageCount(); + bool up_to_date = page_count >= tab_ids.size(); + for (size_t i = 0; up_to_date && i < tab_ids.size(); ++i) + up_to_date = m_parent->GetPageName(page_count - tab_ids.size() + i) == page_tab_id(tab_ids[i]); + for (const auto& [id, page] : m_pages) { + if (!up_to_date) + break; + const bool wanted = std::find(tab_ids.begin(), tab_ids.end(), id) != tab_ids.end(); + up_to_date = (m_parent->FindPage(page) != wxNOT_FOUND) == wanted; + } + + if (!up_to_date) { + const wxString id_to_reselect = m_parent->GetSelectedPageName(); + + for (const auto& [id, page] : m_pages) { + const int idx = m_parent->FindPage(page); + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + } + + for (const auto& id : tab_ids) { + PluginPage* page = m_pages.at(id); + m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), "", + false, page->icon()); + } + + if (!id_to_reselect.empty()) + m_parent->SelectPageByName(id_to_reselect); } if (need_overflow) { @@ -442,9 +436,6 @@ void PluginPages::relayout() m_overflow_button->Destroy(); m_overflow_button = nullptr; } - - if (!id_to_reselect.empty()) - m_parent->SelectPageByName(id_to_reselect); } void PluginPages::show_overflow_menu() diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp index 3dc86b8aee..4d00de6df9 100644 --- a/src/slic3r/plugin/host/PluginPages.hpp +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -10,8 +10,7 @@ #include #include -#include -#include +#include #include #include @@ -33,8 +32,8 @@ public: void on_new_window(wxWebViewEvent& event); void on_script_message(wxWebViewEvent& event); void push_message(const std::string& message); - void set_icon_image_id(int id) { m_icon_image_id = id; } - int get_icon_image_id() const { return m_icon_image_id; } + void set_icon(const wxBitmap& icon) { m_icon = icon; } + const wxBitmap& icon() const { return m_icon; } private: void load_plugin_content(); @@ -45,8 +44,7 @@ private: std::shared_ptr m_cap; std::shared_ptr> m_lifetime; bool m_content_loaded{false}; - - int m_icon_image_id = wxBookCtrlBase::NO_IMAGE; + wxBitmap m_icon; }; class PluginPages @@ -66,7 +64,6 @@ public: void on_plugin_register(const std::string& plugin_key); void on_plugin_deregister(const std::string& plugin_key); - int get_visible_page_count() const { return m_visible_page_count; } void set_visible_page_count(int count); void relayout(); @@ -83,7 +80,6 @@ private: std::vector m_order; Notebook* m_parent{nullptr}; - std::unique_ptr m_image_list; int m_visible_page_count{0}; std::optional m_swapped_in_id; From 80471419813a0f4d7289da1365d27855d7945a91 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Wed, 19 Aug 2026 09:28:58 -0500 Subject: [PATCH 63/71] test: fix the flaky multiline lightning smoothing assertion (#15294) --- tests/fff_print/test_fill.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 07460d3990..21a5000401 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -980,7 +980,11 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", " const SparseInfillShape smooth = shape_for("100%"); REQUIRE(sharp.path_count > 0); - REQUIRE(smooth.path_count <= sharp.path_count); + // The loop count varies by a loop or two between platforms and between runs, so this is not an + // exact comparison. Smoothing should leave it about where it was; uncapping the smoothing + // reach, the regression this guards against, adds about 10%. + const size_t allowed_extra = sharp.path_count / 50; // 2% + REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra); // The outlines are still rounded. REQUIRE(smooth.point_count > sharp.point_count); REQUIRE(smooth.sharp_turns < sharp.sharp_turns); From f5f3d2221dd929360407aa2ae6759302a8d2c575 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Wed, 19 Aug 2026 14:47:34 -0300 Subject: [PATCH 64/71] AI Translation update (#15300) --- localization/i18n/OrcaSlicer.pot | 144 ++++++++--- localization/i18n/ca/OrcaSlicer_ca.po | 192 +++++++++++--- localization/i18n/cs/OrcaSlicer_cs.po | 194 ++++++++++++--- localization/i18n/de/OrcaSlicer_de.po | 192 +++++++++++--- localization/i18n/en/OrcaSlicer_en.po | 144 ++++++++--- localization/i18n/es/OrcaSlicer_es.po | 192 +++++++++++--- localization/i18n/eu/OrcaSlicer_eu.po | 192 +++++++++++--- localization/i18n/fr/OrcaSlicer_fr.po | 262 ++++++++++++++------ localization/i18n/hu/OrcaSlicer_hu.po | 192 +++++++++++--- localization/i18n/it/OrcaSlicer_it.po | 192 +++++++++++--- localization/i18n/ja/OrcaSlicer_ja.po | 192 +++++++++++--- localization/i18n/ko/OrcaSlicer_ko.po | 196 ++++++++++++--- localization/i18n/lt/OrcaSlicer_lt.po | 196 ++++++++++++--- localization/i18n/nl/OrcaSlicer_nl.po | 196 ++++++++++++--- localization/i18n/pl/OrcaSlicer_pl.po | 196 ++++++++++++--- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 202 +++++++++++---- localization/i18n/ru/OrcaSlicer_ru.po | 238 ++++++++++++++---- localization/i18n/sv/OrcaSlicer_sv.po | 200 ++++++++++++--- localization/i18n/th/OrcaSlicer_th.po | 192 +++++++++++--- localization/i18n/tr/OrcaSlicer_tr.po | 198 ++++++++++++--- localization/i18n/uk/OrcaSlicer_uk.po | 194 ++++++++++++--- localization/i18n/vi/OrcaSlicer_vi.po | 196 ++++++++++++--- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 196 ++++++++++++--- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 192 +++++++++++--- 24 files changed, 3722 insertions(+), 958 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 88e49a85fc..6ec0ecd9cf 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -4452,6 +4452,20 @@ msgstr "" msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "" +#, possible-c-format, possible-boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4533,6 +4547,12 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" @@ -4784,6 +4804,12 @@ msgstr "" msgid "Calibration error" msgstr "" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "" @@ -5615,7 +5641,7 @@ msgstr "" msgid "Size:" msgstr "" -#, possible-c-format, possible-boost-format +#, possible-boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "" @@ -5790,6 +5816,9 @@ msgstr "" msgid "Project" msgstr "" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "" @@ -7780,19 +7809,19 @@ msgstr "" msgid "Replaced with 3D files from directory:\n" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "✖ Skipped %s: same file.\n" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "✔ Replaced %s.\n" msgstr "" @@ -8472,6 +8501,15 @@ msgstr "" msgid "Pop up to select filament grouping mode" msgstr "" +msgid "Visible plugin pages" +msgstr "" + +msgid "pages" +msgstr "" + +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "" + msgid "Behaviour" msgstr "" @@ -8797,6 +8835,14 @@ msgstr "" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "" @@ -9052,9 +9098,21 @@ msgstr "" msgid "Preset Inside Project" msgstr "" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "" @@ -9732,20 +9790,6 @@ msgstr "" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "" - -msgid "Adjust to the set range automatically?\n" -msgstr "" - -msgid "Adjust" -msgstr "" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "" @@ -9931,6 +9975,9 @@ msgstr "" msgid "Setting Overrides" msgstr "" +msgid "Retraction when switching material" +msgstr "" + msgid "Basic information" msgstr "" @@ -10057,6 +10104,12 @@ msgstr "" msgid "Printable space" msgstr "" +msgid "Printer Agent" +msgstr "" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, possible-boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10179,9 +10232,6 @@ msgstr "" msgid "Z-Hop" msgstr "" -msgid "Retraction when switching material" -msgstr "" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11445,6 +11495,9 @@ msgstr "" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "" @@ -11740,9 +11793,6 @@ msgstr "" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "" -msgid "Printer Agent" -msgstr "" - msgid "Select the network agent implementation for printer communication." msgstr "" @@ -12279,9 +12329,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "" -msgid "Brim width" -msgstr "" - msgid "This is the distance from the model to the outermost brim line." msgstr "" @@ -12347,6 +12394,12 @@ msgid "" "0 to deactivate." msgstr "" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "" @@ -13359,6 +13412,12 @@ msgstr "" msgid "Gyroid" msgstr "" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "" @@ -13839,6 +13898,12 @@ msgstr "" msgid "Klipper" msgstr "" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "" @@ -14800,6 +14865,12 @@ msgstr "" msgid "Retraction distance when extruder change" msgstr "" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "" @@ -14893,6 +14964,9 @@ msgstr "" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "" @@ -15278,6 +15352,12 @@ msgstr "" msgid "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." msgstr "" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "" @@ -18253,9 +18333,6 @@ msgstr "" msgid "Print Host upload" msgstr "" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "" - msgid "Select a Flashforge printer" msgstr "" @@ -19087,9 +19164,6 @@ msgstr "" msgid "User canceled." msgstr "" -msgid "Head diameter" -msgstr "" - msgid "Max angle" msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index 79a9d82df4..b7eb022c0d 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -4828,6 +4828,23 @@ msgstr "La temperatura actual de la cambra és superior a la temperatura segura msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura mínima de la cambra (%d℃) és superior a la temperatura objectiu de la cambra (%d℃). El valor mínim és el llindar a partir del qual comença la impressió mentre la cambra continua escalfant-se cap a l'objectiu, de manera que no l'hauria de superar. Es limitarà al valor objectiu." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "L'alçada de capa és massa petita. S'establirà al mínim (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "L'alçada de capa està fora dels límits establerts a Configuració de la Impressora -> Extrusora -> Límits d'alçada de la capa, això pot causar problemes de qualitat d'impressió." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Voleu ajustar-la automàticament al límit (%g mm)?" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4948,6 +4965,13 @@ msgstr "" "Sí - Activa el generador de parets Arachne\n" "No - Desactiva el generador de parets Arachne i estableix el mode [Desplaçament] de la pell difusa" +# AI Translated +msgid "Brim ear radius" +msgstr "Radi de l'orella de la Vora d'Adherència" + +msgid "Brim width" +msgstr "Ample de la Vora d'Adherència" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "El mode espiral només funciona quan els bucles de paret són 1, el suport està desactivat, la detecció d'acumulació per sondeig està desactivada, les capes de la coberta superior són 0, la densitat de farciment dispers és 0 i el tipus de timelapse és tradicional." @@ -5202,6 +5226,14 @@ msgstr "No s'ha pogut generar el gcode cali" msgid "Calibration error" msgstr "Error de calibratge" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Aquesta impressora no està configurada amb el maquinari que necessita aquest control." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Aquest control no és compatible amb aquesta impressora." + # AI Translated msgid "Network unavailable" msgstr "Xarxa no disponible" @@ -6067,7 +6099,7 @@ msgstr "Volum:" msgid "Size:" msgstr "Mida:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "S'han trobat conflictes de rutes gcode a la capa %d, Z = %.2lfmm. Si us plau, separeu els objectes conflictius més lluny ( %s <-> %s )." @@ -6248,6 +6280,10 @@ msgstr "Multidispositiu" msgid "Project" msgstr "Projecte" +# AI Translated +msgid "Device (Web)" +msgstr "Dispositiu (Web)" + msgid "Yes" msgstr "Sí" @@ -8361,19 +8397,19 @@ msgstr "No s'ha seleccionat el directori per a la substitució" msgid "Replaced with 3D files from directory:\n" msgstr "Substituït amb fitxers 3D del directori:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Omès %s: mateix fitxer.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Omès %s: el fitxer no existeix.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Omès %s: la substitució ha fallat.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Substituït %s.\n" @@ -9116,6 +9152,18 @@ msgstr "Amb aquesta opció habilitada, podeu enviar una tasca a diversos disposi msgid "Pop up to select filament grouping mode" msgstr "Finestra emergent per seleccionar el mode d'agrupació de filaments" +# AI Translated +msgid "Visible plugin pages" +msgstr "Pàgines de connectors visibles" + +# AI Translated +msgid "pages" +msgstr "pàgines" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Nombre de pàgines de connectors que es mostren com a pestanyes fixes abans que la resta de pàgines es replegui en un desplegable a l'última pestanya." + msgid "Behaviour" msgstr "Comportament" @@ -9506,6 +9554,18 @@ msgstr "Mostrar els perfils no compatibles" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostra els perfils incompatibles o no compatibles a les llistes desplegables d'impressora i de filament. Aquests perfils no es poden seleccionar." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimental) Utilitza agents d'impressora en lloc d'amfitrions d'impressió" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Envia els treballs d'impressió de les impressores que no són Bambu a través dels agents de connector d'impressora en lloc del flux clàssic de pujada a l'amfitrió d'impressió.\n" +"Quan està desactivat, OrcaSlicer utilitza el comportament antic de l'amfitrió d'impressió." + # AI Translated msgid "Experimental Features" msgstr "Funcions experimentals" @@ -9776,9 +9836,25 @@ msgstr "Perfil d'usuari" msgid "Preset Inside Project" msgstr "Perfil intern del Projecte" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copia en aquest perfil tots els valors heretats del perfil pare i elimina la relació d'herència. Els perfils compatibles només amb el perfil pare poden deixar de ser compatibles." + msgid "Detach from parent" msgstr "Desvincula del pare" +# AI Translated +msgid "Unique preset" +msgstr "Perfil únic" + +# AI Translated +msgid "Parent preset" +msgstr "Perfil pare" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Aquest perfil no hereta de cap altre perfil." + msgid "Name is unavailable." msgstr "El nom no està disponible." @@ -10521,22 +10597,6 @@ msgstr "Estàs segur que vols activar aquesta opció?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Els patrons de farciment estan dissenyats normalment per gestionar la rotació automàticament per garantir una impressió correcta i aconseguir els efectes desitjats (p. ex., Gyroid, Cúbic). Rotar el patró de farciment dispers actual pot portar a un suport insuficient. Procediu amb precaució i comproveu minuciosament qualsevol problema d'impressió potencial. Esteu segur que voleu activar aquesta opció?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"L'alçada de la capa és massa petita.\n" -"Es posarà a min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "L'alçada de la capa supera el límit a Configuració de la Impressora -> Extrusora -> Límits d'alçada de la capa, això pot causar problemes de qualitat d'impressió." - -msgid "Adjust to the set range automatically?\n" -msgstr "Voleu ajustar el rang automàticament?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Característica experimental: Retreure i tallar el filament a major distància durant els canvis de filaments per minimitzar el flux. Tot i que pot reduir notablement el flux, també pot elevar el risc d'esclops de broquets o altres complicacions d'impressió." @@ -10735,6 +10795,9 @@ msgstr "Trobades paraules clau reservades" msgid "Setting Overrides" msgstr "Anul·lacions de configuració" +msgid "Retraction when switching material" +msgstr "Retracció en canviar de material" + msgid "Basic information" msgstr "Informació bàsica" @@ -10867,6 +10930,12 @@ msgstr "Perfils de processos compatibles" msgid "Printable space" msgstr "Espai imprimible" +msgid "Printer Agent" +msgstr "Agent de la impressora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora. Els agents disponibles es registren a l'inici." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10997,9 +11066,6 @@ msgstr "Límits d'alçada de capa" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retracció en canviar de material" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12380,6 +12446,10 @@ msgstr " està massa a prop de la zona d'exclusió, i es provocaran col·lisions msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " és massa a prop de l'àrea de detecció d'acumulació i es causaran col·lisions.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " està parcialment fora de l'àrea imprimible, i no es pot imprimir.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Les temperatures de broquet seleccionades són incompatibles. La temperatura de broquet de cada filament ha d'estar dins del rang de temperatura de broquet recomanat dels altres filaments. Altrament, es pot produir una obturació del broquet o danys a la impressora." @@ -12714,9 +12784,6 @@ msgstr "Utilitzar 3MF en lloc de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activeu-ho si la impressora accepta un fitxer 3MF com a treball d'impressió. Quan està activat, Orca Slicer envia el fitxer laminat com a .gcode.3mf, en lloc d'un fitxer .gcode simple." -msgid "Printer Agent" -msgstr "Agent de la impressora" - msgid "Select the network agent implementation for printer communication." msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora." @@ -13402,9 +13469,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocitat dels ponts interns. Si el valor s'expressa com un percentatge, es calcularà en funció de la velocitat del pont (bridge_speed). El valor per defecte és del 150%." -msgid "Brim width" -msgstr "Ample de la Vora d'Adherència" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distància del model a la línia de la Vora d'Adherència més exterior" @@ -13488,6 +13552,14 @@ msgstr "" "La geometria es simplificarà abans de detectar angles pronunciats. Aquest paràmetre indica la longitud mínima de la desviació per a la simplificació.\n" "0 per desactivar" +# AI Translated +msgid "Brim ears outer only" +msgstr "Orelles de la Vora d'Adherència només a l'exterior" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genera orelles de ratolí només al contorn exterior del model, excloent-ne els forats i les seccions tancades." + msgid "upward compatible machine" msgstr "màquina compatible ascendent" @@ -14679,6 +14751,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Factor de suavitzat del farciment poc dens" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Controla com s'arrodoneixen les cantonades del farciment poc dens. 0% manté el traçat original amb cantonades vives, mentre que 100% produeix les corbes més amples possibles entre línies de farciment adjacents." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Acceleració del farciment superficial superior. L'ús d'un valor inferior pot millorar la qualitat de la superfície superior" @@ -15232,6 +15312,14 @@ msgstr "Amb quin tipus de Codi-G és compatible la impressora." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Omet el bloc de configuració del G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "No escriu el CONFIG_BLOCK (els parells clau/valor de la configuració del laminador) al fitxer G-code. Això pot ajudar amb impressores el microprogramari de les quals falla en analitzar aquestes línies de comentari (p. ex. Anycubic go-klipper). Nota: el fitxer G-code ja no contindrà la configuració del laminador, de manera que en tornar-lo a importar a OrcaSlicer no es restaurarà la configuració." + msgid "Pellet Modded Printer" msgstr "Impressora modificada de pellets" @@ -16321,6 +16409,14 @@ msgstr "Retracció llarga al canviar d'extrusor" msgid "Retraction distance when extruder change" msgstr "Distància de retracció al canviar d'extrusor" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Longitud de retracció (Canvi d'eina)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Quan s'activa la retracció abans d'un canvi d'eina, el filament es retira la quantitat especificada (la longitud es mesura sobre el filament en brut, abans d'entrar a l'extrusor)." + msgid "Z-hop height" msgstr "Alçada Z-hop" @@ -16419,6 +16515,10 @@ msgstr "Longitud addicional en reiniciar" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quan la retracció es compensa després d'un desplaçament, l'extrusor introduirà una quantitat addicional de filament. Aquest ajustament rarament es necessita." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Longitud addicional en reiniciar (Canvi d'eina)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quan la retracció es compensa després d'un canvi d'eina, l'extrusor introduirà una quantitat addicional de filament." @@ -16835,6 +16935,14 @@ msgstr "Canvi d'eina a la Torre de Purga" msgid "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." msgstr "Força el capçal a desplaçar-se a la Torre de Purga abans d'emetre l'ordre de canvi d'eina (Tx). Només és rellevant per a impressores multiextrusor (multicapçal) que utilitzen una Torre de Purga de tipus 2. Per defecte, Orca omet aquest desplaçament en màquines multicapçal perquè el firmware gestiona el canvi de capçal, cosa que pot fer que l'ordre Tx s'emeti sobre la peça impresa. Activeu aquesta opció si voleu que el canvi d'eina s'emeti sempre sobre la Torre de Purga." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Espera la temperatura a la Torre de Purga" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Recull la nova eina sense esperar que arribi a la temperatura d'impressió, es desplaça a la Torre de Purga i hi espera la temperatura, just abans de purgar. El degoteig de l'escalfament cau sobre la torre en lloc del model, i el desplaçament se solapa amb l'escalfament. Només és rellevant per a impressores multiextrusor (multicapçal) que utilitzen una Torre de Purga de tipus 2. El microprogramari o la macro de canvi d'eina no han d'esperar la temperatura pel seu compte. Quan està desactivat, l'espera de temperatura s'emet just després de l'ordre de canvi d'eina." + msgid "No sparse layers (beta)" msgstr "Sense capes poc denses( beta )" @@ -20121,9 +20229,6 @@ msgstr "Impressora Física" msgid "Print Host upload" msgstr "Pujada al amfitrió( host ) d'impressió" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora. Els agents disponibles es registren a l'inici." - # AI Translated msgid "Select a Flashforge printer" msgstr "Seleccioneu una impressora Flashforge" @@ -21066,9 +21171,6 @@ msgstr "Alguna cosa inesperada ha passat en intentar iniciar sessió, torneu-ho msgid "User canceled." msgstr "Usuari cancel·lat." -msgid "Head diameter" -msgstr "Diàmetre del cap" - msgid "Max angle" msgstr "Angle màxim" @@ -21887,6 +21989,22 @@ msgstr "" "Evitar la deformació( warping )\n" "Sabíeu que quan imprimiu materials propensos a deformar-se, com ara l'ABS, augmentar adequadament la temperatura del llit pot reduir la probabilitat de deformació?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "L'alçada de la capa és massa petita.\n" +#~ "Es posarà a min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "L'alçada de la capa supera el límit a Configuració de la Impressora -> Extrusora -> Límits d'alçada de la capa, això pot causar problemes de qualitat d'impressió." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Voleu ajustar el rang automàticament?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diàmetre del cap" + #~ msgid "Print order within a single layer." #~ msgstr "Ordre d'impressió dins d'una sola capa" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index e21fd3086c..b521a8073b 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -4786,6 +4786,23 @@ msgstr "Aktuální teplota komory je vyšší než bezpečná teplota materiálu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimální teplota komory (%d℃) je vyšší než cílová teplota komory (%d℃). Minimální hodnota je práh, při kterém tisk začíná, zatímco se komora dále ohřívá k cílové teplotě, takže by ji neměla překročit. Bude omezena na cílovou hodnotu." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Výška vrstvy je příliš malá. Bude nastavena na minimum (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Výška vrstvy je mimo limity nastavené v Nastavení tiskárny -> Extruder -> Omezení výšky vrstvy, což může způsobit problémy s kvalitou tisku." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Upravit ji automaticky na limit (%g mm)?" + +msgid "Adjust" +msgstr "Upravit" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4906,6 +4923,13 @@ msgstr "" "Ano – povolit Arachne Wall Generator\n" "Ne – zakázat Arachne Wall Generator a nastavit režim [Displacement] pro Fuzzy Skin" +# AI Translated +msgid "Brim ear radius" +msgstr "Poloměr ouška límce" + +msgid "Brim width" +msgstr "Šířka límce" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spirálový režim funguje pouze tehdy, když je počet smyček stěny 1, podpěry jsou vypnuté, detekce usazenin sondováním je vypnutá, počet horních plných vrstev je 0, hustota řídké výplně je 0 a typ časosběru je tradiční." @@ -5160,6 +5184,14 @@ msgstr "Nepodařilo se vygenerovat kalibrační G-code." msgid "Calibration error" msgstr "Chyba kalibrace" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Tato tiskárna nemá nakonfigurovaný hardware, který tento ovládací prvek vyžaduje." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Tento ovládací prvek není na této tiskárně podporován." + # AI Translated msgid "Network unavailable" msgstr "Síť není dostupná" @@ -6029,7 +6061,7 @@ msgstr "Objem:" msgid "Size:" msgstr "Velikost:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Byly nalezeny konflikty drah G-kódu ve vrstvě %d, Z = %.2lf mm. Oddělte prosím konfliktní objekty více od sebe (%s <-> %s)." @@ -6210,6 +6242,10 @@ msgstr "Více zařízení" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Zařízení (Web)" + msgid "Yes" msgstr "Ano" @@ -8320,19 +8356,19 @@ msgstr "Nebyla vybrána složka pro nahrazení" msgid "Replaced with 3D files from directory:\n" msgstr "Nahrazeno 3D soubory ze složky:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Přeskočeno %s: stejný soubor.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Přeskočeno %s: soubor neexistuje.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Přeskočeno %s: nahrazení se nezdařilo.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Nahrazeno %s.\n" @@ -9070,6 +9106,18 @@ msgstr "Pokud je tato volba povolena, můžete odeslat úlohu na více zařízen msgid "Pop up to select filament grouping mode" msgstr "Zobrazit dialog pro výběr režimu seskupení filamentů" +# AI Translated +msgid "Visible plugin pages" +msgstr "Viditelné stránky pluginů" + +# AI Translated +msgid "pages" +msgstr "stránek" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Počet stránek pluginů zobrazených jako pevné karty, než se zbývající stránky sbalí do rozbalovací nabídky na poslední kartě." + msgid "Behaviour" msgstr "Chování" @@ -9457,6 +9505,18 @@ msgstr "Zobrazit nepodporované předvolby" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Zobrazovat nekompatibilní/nepodporované předvolby v rozevíracích seznamech tiskáren a filamentů. Tyto předvolby nelze vybrat." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimentální) Používat agenty tiskárny místo tiskových hostů" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Směruje tiskové úlohy pro tiskárny jiné než Bambu přes agenty pluginů tiskárny místo klasického nahrávání na tiskový host.\n" +"Pokud je vypnuto, OrcaSlicer používá původní chování tiskového hosta." + # AI Translated msgid "Experimental Features" msgstr "Experimentální funkce" @@ -9724,10 +9784,26 @@ msgstr "Uživatelská předvolba" msgid "Preset Inside Project" msgstr "Předvolba v projektu" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Zkopíruje do této předvolby všechny hodnoty zděděné z nadřazené předvolby a odstraní vztah dědičnosti. Předvolby kompatibilní pouze s nadřazenou předvolbou mohou přestat být podporovány." + # AI Translated msgid "Detach from parent" msgstr "Oddělit od nadřazeného" +# AI Translated +msgid "Unique preset" +msgstr "Samostatná předvolba" + +# AI Translated +msgid "Parent preset" +msgstr "Nadřazená předvolba" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Tato předvolba nedědí z jiné předvolby." + msgid "Name is unavailable." msgstr "Název není k dispozici." @@ -10469,22 +10545,6 @@ msgstr "Opravdu chcete tuto možnost povolit?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Vzory výplně jsou obvykle navrženy tak, aby automaticky pracovaly s rotací a zajistily správný tisk i zamýšlený efekt (např. Gyroid, Cubic). Otočení aktuální řídké výplně může vést k nedostatečné opoře. Postupujte opatrně a pečlivě zkontrolujte možné problémy při tisku. Opravdu chcete tuto možnost povolit?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Výška vrstvy je příliš malá.\n" -"Bude nastavena na min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Výška vrstvy přesahuje limit v Nastavení tiskárny -> Extruder -> Omezení výšky vrstvy, což může způsobit problémy s kvalitou tisku." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automaticky upravit do nastaveného rozsahu?\n" - -msgid "Adjust" -msgstr "Upravit" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentální funkce: Stažení a odstřižení filamentu na větší vzdálenost během výměny filamentu pro minimalizaci purge. Ačkoliv to může výrazně snížit purge, může to také zvýšit riziko ucpání trysky nebo jiných komplikací při tisku." @@ -10684,6 +10744,9 @@ msgstr "Byla nalezena rezervovaná klíčová slova" msgid "Setting Overrides" msgstr "Přepisování nastavení" +msgid "Retraction when switching material" +msgstr "Retrakce při změně materiálu" + msgid "Basic information" msgstr "Základní informace" @@ -10816,6 +10879,13 @@ msgstr "Kompatibilní procesní profily" msgid "Printable space" msgstr "Tisknutelný prostor" +# AI Translated +msgid "Printer Agent" +msgstr "Agent tiskárny" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou. Dostupní agenti jsou registrováni při spuštění." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10943,9 +11013,6 @@ msgstr "Omezení výšky vrstvy" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retrakce při změně materiálu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12363,6 +12430,10 @@ msgstr " je příliš blízko oblasti vyloučení a může způsobit kolize.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " je příliš blízko oblasti detekce shlukování a dojde ke kolizi.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " je částečně mimo tisknutelnou oblast a nelze jej vytisknout.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Vybrané teploty trysky nejsou kompatibilní. Teplota trysky každého filamentu musí spadat do doporučeného rozsahu teplot ostatních filamentů. Jinak může dojít k ucpání trysky nebo poškození tiskárny." @@ -12696,10 +12767,6 @@ msgstr "Použít 3MF místo G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Zapněte, pokud tiskárna přijímá jako tiskovou úlohu soubor 3MF. Je-li zapnuto, odešle Orca Slicer slicovaný soubor jako .gcode.3mf místo prostého souboru .gcode." -# AI Translated -msgid "Printer Agent" -msgstr "Agent tiskárny" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou." @@ -13387,9 +13454,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Rychlost vnitřních mostů. Pokud je hodnota zadána v procentech, vypočítá se podle bridge_speed. Výchozí hodnota je 150 %." -msgid "Brim width" -msgstr "Šířka límce" - msgid "This is the distance from the model to the outermost brim line." msgstr "Vzdálenost od modelu k nejvzdálenější brim linii." @@ -13470,6 +13534,14 @@ msgstr "" "Geometrie bude decimována před detekcí ostrých úhlů. Tento parametr určuje minimální délku odchylky pro decimaci.\n" "0 pro deaktivaci." +# AI Translated +msgid "Brim ears outer only" +msgstr "Ouška límce pouze na vnějším obrysu" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Vytvoří myší ouška pouze na vnějším obrysu modelu, bez otvorů a uzavřených částí." + msgid "upward compatible machine" msgstr "stroj zpětně kompatibilní" @@ -14646,6 +14718,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Faktor vyhlazení řídké výplně" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Určuje, jak silně se zaoblují rohy řídké výplně. 0% zachová původní ostrou dráhu, zatímco 100% vytvoří největší možné křivky mezi sousedními liniemi výplně." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Akcelerace výplně horní plochy. Použití nižší hodnoty může zlepšit kvalitu horní plochy." @@ -15198,6 +15278,14 @@ msgstr "Jaký typ G-code je s tiskárnou kompatibilní." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Vynechat konfigurační blok G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Nezapisuje CONFIG_BLOCK (dvojice klíč/hodnota s konfigurací sliceru) do souboru G-code. Může to pomoci u tiskáren, jejichž firmware při zpracování těchto řádků s komentáři havaruje (např. Anycubic go-klipper). Poznámka: soubor G-code již nebude obsahovat nastavení sliceru, takže jeho opětovný import do OrcaSlicer konfiguraci neobnoví." + msgid "Pellet Modded Printer" msgstr "Tiskárna na pelety" @@ -16265,6 +16353,14 @@ msgstr "Dlouhá retrakce při změně extruderu" msgid "Retraction distance when extruder change" msgstr "Délka retrakce při změně extruderu" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Délka retrakce (Změna nástroje)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Když je retrakce spuštěna před změnou nástroje, filament se zatáhne o zadanou hodnotu (délka se měří na nezpracovaném filamentu, než vstoupí do extruderu)." + msgid "Z-hop height" msgstr "Výška Z-hopu" @@ -16362,6 +16458,10 @@ msgstr "Dodatečná délka při restartu" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Při kompenzaci retrakce po pohybu přesunu extruder posune toto přídavné množství filamentu. Toto nastavení je potřeba jen zřídka." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Dodatečná délka při restartu (Změna nástroje)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Při kompenzaci retrakce po výměně nástroje extruder posune toto přídavné množství filamentu." @@ -16780,6 +16880,14 @@ msgstr "Výměna nástroje na věži na očištění trysky" msgid "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." msgstr "Vynutí přejezd tiskové hlavy k věži na očištění trysky před vydáním příkazu k výměně nástroje (Tx). Týká se pouze tiskáren s více extrudery (více tiskovými hlavami), které používají věž na očištění trysky typu 2. Ve výchozím nastavení Orca na strojích s více tiskovými hlavami tento přejezd vynechává, protože výměnu hlavy řeší firmware, což může vést k vydání příkazu Tx nad tištěným dílem. Zapněte tuto volbu, chcete-li, aby byla výměna nástroje vždy vydána nad věží na očištění trysky." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Čekat na teplotu na věži na očištění trysky" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Vyzvedne nový nástroj, aniž by čekal na dosažení tiskové teploty, přejede na věž na očištění trysky a počká na teplotu tam, těsně před čištěním. Materiál vytékající při ohřevu skončí na věži místo na modelu a přejezd se překrývá s ohřevem. Relevantní pouze pro tiskárny s více extrudery (více tiskovými hlavami) používající věž na očištění trysky typu 2. Firmware ani makro pro změnu nástroje nesmí na teplotu čekat samo. Pokud je vypnuto, čekání na teplotu se vloží hned po příkazu ke změně nástroje." + msgid "No sparse layers (beta)" msgstr "Žádné řídké vrstvy (beta)" @@ -20043,9 +20151,6 @@ msgstr "Fyzická tiskárna" msgid "Print Host upload" msgstr "Nahrání na tiskový server" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou. Dostupní agenti jsou registrováni při spuštění." - # AI Translated msgid "Select a Flashforge printer" msgstr "Vyberte tiskárnu Flashforge" @@ -21002,9 +21107,6 @@ msgstr "Při pokusu o přihlášení došlo k neočekávané chybě, zkuste to p msgid "User canceled." msgstr "Zrušeno uživatelem." -msgid "Head diameter" -msgstr "Průměr hlavy" - msgid "Max angle" msgstr "Maximální úhel" @@ -21873,6 +21975,22 @@ msgstr "" "Zamezte kroucení\n" "Víte, že při tisku materiálů náchylných ke kroucení, jako je ABS, může vhodné zvýšení teploty vyhřívané desky snížit pravděpodobnost kroucení?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Výška vrstvy je příliš malá.\n" +#~ "Bude nastavena na min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Výška vrstvy přesahuje limit v Nastavení tiskárny -> Extruder -> Omezení výšky vrstvy, což může způsobit problémy s kvalitou tisku." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automaticky upravit do nastaveného rozsahu?\n" + +#~ msgid "Head diameter" +#~ msgstr "Průměr hlavy" + #~ msgid "Print order within a single layer." #~ msgstr "Pořadí tisku v rámci jedné vrstvy." diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 50384598e3..436966457a 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher \n" "Language-Team: \n" @@ -4692,6 +4692,23 @@ msgstr "Die aktuelle Kammer-Temperatur ist höher als die sichere Temperatur des msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Die minimale Druckraumtemperatur (%d℃) ist höher als die Ziel-Druckraumtemperatur (%d℃). Der Minimalwert ist der Schwellenwert, bei dem der Druck beginnt, während der Druckraum weiter auf die Zieltemperatur heizt; er sollte diese daher nicht überschreiten. Er wird auf die Zieltemperatur begrenzt." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Die Schichthöhe ist zu klein. Sie wird auf den Mindestwert (%g mm) gesetzt." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Die Schichthöhe liegt außerhalb der in Druckereinstellungen -> Extruder -> Schichthöhenlimits festgelegten Grenzen. Dies kann zu Problemen mit der Druckqualität führen." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Automatisch an den Grenzwert (%g mm) anpassen?" + +msgid "Adjust" +msgstr "Anpassen" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4812,6 +4829,13 @@ msgstr "" "Ja - Arachne Wall Generator aktivieren\n" "Nein - Arachne Wall Generator deaktivieren und den Modus [Verschiebung] des Fuzzy Skin setzen" +# AI Translated +msgid "Brim ear radius" +msgstr "Radius der Brim-Ohren" + +msgid "Brim width" +msgstr "Randbreite" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Der Spiralmodus funktioniert nur, wenn die Wandschleifen 1 sind, die Stütze deaktiviert ist, die Klumpenerkennung durch Abtasten deaktiviert ist, die oberen Schichtlagen 0 sind, die Dichte der spärlichen Füllung 0 ist und der Zeitraffertyp traditionell ist." @@ -5066,6 +5090,14 @@ msgstr "Fehler beim Generieren des Kalibrierungs-G-Codes" msgid "Calibration error" msgstr "Kalibrierungsfehler" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Dieser Drucker ist nicht mit der Hardware ausgestattet, die dieses Bedienelement benötigt." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Dieses Bedienelement wird von diesem Drucker nicht unterstützt." + # AI Translated msgid "Network unavailable" msgstr "Netzwerk nicht verfügbar" @@ -5923,7 +5955,7 @@ msgstr "Volumen:" msgid "Size:" msgstr "Größe:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Konflikte von G-Code-Pfaden wurden bei Layer %d, Z = %.2lf mm gefunden.Bitte trennen Sie die konfliktbehafteten Objekte weiter voneinander (%s <-> %s)." @@ -6103,6 +6135,10 @@ msgstr "Multi-Gerät" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Gerät (Web)" + msgid "Yes" msgstr "Ja" @@ -8191,19 +8227,19 @@ msgstr "Verzeichnis um daraus zu ersetzen wurde nicht ausgewählt" msgid "Replaced with 3D files from directory:\n" msgstr "Ersetzt durch 3D-Dateien aus Verzeichnis:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Übersprungen %s: gleiche Datei.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Übersprungen %s: Datei existiert nicht.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Übersprungen %s: Ersetzen fehlgeschlagen.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Ersetzt %s.\n" @@ -8941,6 +8977,18 @@ msgstr "Wenn diese Option aktiviert ist, können Sie eine Aufgabe gleichzeitig a msgid "Pop up to select filament grouping mode" msgstr "Popup zum Auswählen des Filament-Gruppierungsmodus" +# AI Translated +msgid "Visible plugin pages" +msgstr "Sichtbare Plugin-Seiten" + +# AI Translated +msgid "pages" +msgstr "Seiten" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Anzahl der Plugin-Seiten, die als feste Tabs angezeigt werden, bevor die übrigen Seiten im letzten Tab zu einem Dropdown zusammengefasst werden." + msgid "Behaviour" msgstr "Verhalten" @@ -9296,6 +9344,18 @@ msgstr "Nicht unterstützte Profile anzeigen" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Zeigt inkompatible/nicht unterstützte Profile in den Dropdown-Listen für Drucker und Filament an. Diese Profile können nicht ausgewählt werden." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimentell) Drucker-Agenten anstelle von Druck-Hosts verwenden" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Leitet Druckaufträge für Nicht-Bambu-Drucker über Drucker-Plugin-Agenten statt über den klassischen Druck-Host-Upload.\n" +"Wenn deaktiviert, verwendet OrcaSlicer das bisherige Druck-Host-Verhalten." + msgid "Experimental Features" msgstr "Experimentelle Funktionen" @@ -9558,9 +9618,25 @@ msgstr "Benutzerprofil" msgid "Preset Inside Project" msgstr "Projektbasiertes Profil" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Kopiert alle vom übergeordneten Profil geerbten Werte in dieses Profil und entfernt die Vererbungsbeziehung. Profile, die nur mit dem übergeordneten Profil kompatibel sind, können dadurch nicht mehr unterstützt werden." + msgid "Detach from parent" msgstr "Vom übergeordneten Element trennen" +# AI Translated +msgid "Unique preset" +msgstr "Eigenständiges Profil" + +# AI Translated +msgid "Parent preset" +msgstr "Übergeordnetes Profil" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Dieses Profil erbt nicht von einem anderen Profil." + msgid "Name is unavailable." msgstr "Der Name ist nicht verfügbar." @@ -10296,22 +10372,6 @@ msgstr "Sind Sie sicher, dass Sie diese Option aktivieren möchten?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Infill-Muster sind in der Regel so konzipiert, dass sie eine automatische Drehung ermöglichen, um einen ordnungsgemäßen Druck zu gewährleisten und die beabsichtigten Effekte zu erzielen (z. B. Gyroid, Cubic). Das Drehen des aktuellen spärlichen Infill-Musters kann zu unzureichender Unterstützung führen. Bitte gehen Sie vorsichtig vor und überprüfen Sie gründlich auf mögliche Druckprobleme. Sind Sie sicher, dass Sie diese Option aktivieren möchten?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Die Schichthöhe ist zu klein.\n" -"Sie wird auf min_layer_height gesetzt\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Die Schichthöhe überschreitet das Limit in Druckereinstellungen -> Extruder -> Schichthöhenlimits. Dies kann zu Problemen mit der Druckqualität führen." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automatisch an den eingestellten Bereich anpassen?\n" - -msgid "Adjust" -msgstr "Anpassen" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentelle Funktion: Filament beim Filamentwechsel weiter zurückziehen und abschneiden, um den Flush zu minimieren. Obwohl dies den Flush deutlich reduzieren kann, kann es auch das Risiko von Düsenverstopfungen oder anderen Druckkomplikationen erhöhen." @@ -10505,6 +10565,9 @@ msgstr "Reservierte Schlüsselwörter gefunden" msgid "Setting Overrides" msgstr "Überschreiben der Einstellungen" +msgid "Retraction when switching material" +msgstr "Rückzug bei Materialwechsel" + msgid "Basic information" msgstr "Grundlegende Informationen" @@ -10634,6 +10697,12 @@ msgstr "Kompatible Prozessprofile" msgid "Printable space" msgstr "Druckbarer Raum" +msgid "Printer Agent" +msgstr "Drucker-Agent" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Wählen Sie die Implementierung des Netzwerkagenten für die Druckerkommunikation. Verfügbare Agenten werden beim Start registriert." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10759,9 +10828,6 @@ msgstr "Höhenbegrenzungen für Schichten" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Rückzug bei Materialwechsel" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12103,6 +12169,10 @@ msgstr " ist zu nahe am Sperrbereich und es werden Kollisionen verursacht.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ist zu nahe am Klumpenerkennungsbereich und es werden Kollisionen verursacht.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " liegt teilweise außerhalb des druckbaren Bereichs und kann nicht gedruckt werden.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Die ausgewählten Düsentemperaturen sind nicht kompatibel. Die Düsentemperatur jedes Filaments muss innerhalb des empfohlenen Düsentemperaturbereichs der anderen Filamente liegen. Andernfalls kann es zu Düsenverstopfungen oder Druckerschäden kommen." @@ -12418,9 +12488,6 @@ msgstr "Benutze 3MF statt G-Code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Aktivieren Sie diese Option, wenn der Drucker eine 3MF-Datei als Druckauftrag akzeptiert. Wenn aktiviert, sendet Orca Slicer die geslicete Datei als .gcode.3mf, anstatt als einfache .gcode-Datei." -msgid "Printer Agent" -msgstr "Drucker-Agent" - msgid "Select the network agent implementation for printer communication." msgstr "Wählen Sie die Netzwerk-Agent-Implementierung für die Druckerkommunikation aus." @@ -13091,9 +13158,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Geschwindigkeit der internen Brücken. Wenn der Wert als Prozentsatz angegeben wird, wird er auf der Grundlage der Brückengeschwindigkeit berechnet. Der Standardwert beträgt 150 %." -msgid "Brim width" -msgstr "Randbreite" - msgid "This is the distance from the model to the outermost brim line." msgstr "Abstand vom Modell zur äußersten Randlinie" @@ -13174,6 +13238,14 @@ msgstr "" "Die Geometrie wird vor der Erkennung scharfer Winkel reduziert. Dieser Parameter ist ein Indikator für die minimale Länge der Abweichung für die Reduzierung.\n" "0 zum Deaktivieren." +# AI Translated +msgid "Brim ears outer only" +msgstr "Brim-Ohren nur außen" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Erzeugt Mausohren nur an der Außenkontur des Modells, ohne Löcher und geschlossene Bereiche." + msgid "upward compatible machine" msgstr "Aufwärtskompatible Maschine" @@ -14341,6 +14413,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Glättungsfaktor der Füllung" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Legt fest, wie stark die Ecken der Füllung abgerundet werden. 0% behält den ursprünglichen scharfkantigen Pfad bei, während 100% die größtmöglichen Kurven zwischen benachbarten Fülllinien erzeugt." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Dies ist die Beschleunigung der Füllung von der obersten Schicht. Die Verwendung eines niedrigeren Werts kann die Qualität der Oberfläche verbessern." @@ -14874,6 +14954,14 @@ msgstr "Mit welcher Art von G-Code ist der Drucker kompatibel." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code-Konfigurationsblock auslassen" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Schreibt den CONFIG_BLOCK (die Schlüssel-Wert-Paare der Slicer-Konfiguration) nicht in die G-code-Datei. Das kann bei Druckern helfen, deren Firmware beim Verarbeiten dieser Kommentarzeilen abstürzt (z. B. Anycubic go-klipper). Hinweis: Die G-code-Datei enthält dann keine Slicer-Einstellungen mehr, sodass beim erneuten Importieren in OrcaSlicer die Konfiguration nicht wiederhergestellt wird." + msgid "Pellet Modded Printer" msgstr "Pellet-Modifizierter Drucker" @@ -15920,6 +16008,14 @@ msgstr "Langer Rückzug beim Extruderwechsel" msgid "Retraction distance when extruder change" msgstr "Rückzugslänge beim Extruderwechsel" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Rückzugslänge (Werkzeugwechsel)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Wenn vor einem Werkzeugwechsel ein Rückzug ausgelöst wird, wird das Filament um den angegebenen Betrag zurückgezogen (die Länge wird am rohen Filament gemessen, bevor es in den Extruder gelangt)." + msgid "Z-hop height" msgstr "Z-Hub-Höhe" @@ -16014,6 +16110,10 @@ msgstr "Zusätzliche Länge beim Neustart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Wenn die Rückzugskompensation nach dem Reisemove durchgeführt wird, wird der Extruder diese zusätzliche Menge an Filament schieben. Diese Einstellung wird nur selten benötigt." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Zusätzliche Länge beim Neustart (Werkzeugwechsel)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Wenn die Rückzugskompensation nach dem Wechsel des Werkzeugs durchgeführt wird, wird der Extruder diese zusätzliche Menge an Filament schieben." @@ -16431,6 +16531,14 @@ msgstr "Werkzeugwechsel auf dem Reinigungsturm" msgid "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." msgstr "Erzwinge, dass der Werkzeugkopf zum Reinigungsturm fährt, bevor der Werkzeugwechselbefehl (Tx) ausgegeben wird. Nur relevant für Mehrfach-Extruder (Mehrfach-Werkzeugkopf) Drucker, die einen Typ-2-Reinigungsturm verwenden. Standardmäßig überspringt Orca die Fahrt auf Mehrfach-Werkzeugkopf-Maschinen, da die Firmware den Kopfwechsel übernimmt, was dazu führen kann, dass der Tx-Befehl über dem gedruckten Teil ausgegeben wird. Aktivieren Sie diese Option, wenn Sie möchten, dass der Werkzeugwechsel immer über dem Reinigungsturm ausgegeben wird." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Auf Temperatur am Reinigungsturm warten" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Nimmt das neue Werkzeug auf, ohne auf das Erreichen der Drucktemperatur zu warten, fährt zum Reinigungsturm und wartet dort unmittelbar vor dem Spülen auf die Temperatur. Das beim Aufheizen austretende Material landet auf dem Turm statt auf dem Modell, und die Fahrt überlappt sich mit dem Aufheizen. Nur relevant für Multi-Extruder-Drucker (mehrere Werkzeugköpfe) mit einem Reinigungsturm vom Typ 2. Die Firmware bzw. das Werkzeugwechsel-Makro darf nicht selbst auf die Temperatur warten. Wenn deaktiviert, wird das Warten auf die Temperatur direkt nach dem Werkzeugwechselbefehl ausgegeben." + msgid "No sparse layers (beta)" msgstr "Keine dünnen Schichten (Beta)" @@ -19650,9 +19758,6 @@ msgstr "Drucker" msgid "Print Host upload" msgstr "Hochladen zum Druck-Host" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Wählen Sie die Implementierung des Netzwerkagenten für die Druckerkommunikation. Verfügbare Agenten werden beim Start registriert." - msgid "Select a Flashforge printer" msgstr "Wählen Sie einen Flashforge-Drucker aus" @@ -20500,9 +20605,6 @@ msgstr "Es ist etwas Unerwartetes passiert, als Sie versucht haben, sich anzumel msgid "User canceled." msgstr "Benutzer abgebrochen." -msgid "Head diameter" -msgstr "Kopfdurchmesser" - msgid "Max angle" msgstr "Maximaler Winkel" @@ -21286,6 +21388,22 @@ msgstr "" "Verwerfungen vermeiden\n" "Wussten Sie, dass beim Drucken von Materialien, die zu Verwerfungen neigen, wie z.B. ABS, durch eine entsprechende Erhöhung der Heizbetttemperatur die Wahrscheinlichkeit von Verwerfungen verringert werden kann?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Die Schichthöhe ist zu klein.\n" +#~ "Sie wird auf min_layer_height gesetzt\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Die Schichthöhe überschreitet das Limit in Druckereinstellungen -> Extruder -> Schichthöhenlimits. Dies kann zu Problemen mit der Druckqualität führen." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automatisch an den eingestellten Bereich anpassen?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kopfdurchmesser" + #~ msgid "Print order within a single layer." #~ msgstr "Druckreihenfolge innerhalb einer einzelnen Schicht" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 232820f681..88fb455959 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -4448,6 +4448,20 @@ msgstr "" msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "" +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4529,6 +4543,12 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" @@ -4780,6 +4800,12 @@ msgstr "" msgid "Calibration error" msgstr "" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "" @@ -5611,7 +5637,7 @@ msgstr "" msgid "Size:" msgstr "" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "" @@ -5786,6 +5812,9 @@ msgstr "" msgid "Project" msgstr "" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "" @@ -7776,19 +7805,19 @@ msgstr "" msgid "Replaced with 3D files from directory:\n" msgstr "" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "" @@ -8468,6 +8497,15 @@ msgstr "" msgid "Pop up to select filament grouping mode" msgstr "" +msgid "Visible plugin pages" +msgstr "" + +msgid "pages" +msgstr "" + +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "" + msgid "Behaviour" msgstr "" @@ -8793,6 +8831,14 @@ msgstr "" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "" @@ -9048,9 +9094,21 @@ msgstr "" msgid "Preset Inside Project" msgstr "" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "" @@ -9728,20 +9786,6 @@ msgstr "" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "" - -msgid "Adjust to the set range automatically?\n" -msgstr "" - -msgid "Adjust" -msgstr "" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "" @@ -9927,6 +9971,9 @@ msgstr "" msgid "Setting Overrides" msgstr "" +msgid "Retraction when switching material" +msgstr "" + msgid "Basic information" msgstr "" @@ -10053,6 +10100,12 @@ msgstr "" msgid "Printable space" msgstr "" +msgid "Printer Agent" +msgstr "" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10175,9 +10228,6 @@ msgstr "" msgid "Z-Hop" msgstr "" -msgid "Retraction when switching material" -msgstr "" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11441,6 +11491,9 @@ msgstr "" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "" @@ -11736,9 +11789,6 @@ msgstr "" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "" -msgid "Printer Agent" -msgstr "" - msgid "Select the network agent implementation for printer communication." msgstr "" @@ -12275,9 +12325,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "" -msgid "Brim width" -msgstr "" - msgid "This is the distance from the model to the outermost brim line." msgstr "" @@ -12343,6 +12390,12 @@ msgid "" "0 to deactivate." msgstr "" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "" @@ -13355,6 +13408,12 @@ msgstr "" msgid "Gyroid" msgstr "" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "" @@ -13835,6 +13894,12 @@ msgstr "" msgid "Klipper" msgstr "" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "" @@ -14796,6 +14861,12 @@ msgstr "" msgid "Retraction distance when extruder change" msgstr "" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "" @@ -14889,6 +14960,9 @@ msgstr "" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "" @@ -15274,6 +15348,12 @@ msgstr "" msgid "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." msgstr "" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "" @@ -18249,9 +18329,6 @@ msgstr "" msgid "Print Host upload" msgstr "" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "" - msgid "Select a Flashforge printer" msgstr "" @@ -19083,9 +19160,6 @@ msgstr "" msgid "User canceled." msgstr "" -msgid "Head diameter" -msgstr "" - msgid "Max angle" msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 1913c4512a..9c5127e50a 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -4564,6 +4564,23 @@ msgstr "La temperatura actual de la recámara es superior a la temperatura de se msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura mínima de la recámara (%d℃) es superior a la temperatura objetivo de la recámara (%d℃). El valor mínimo es el umbral en el que comienza la impresión mientras la recámara continúa calentándose hacia el objetivo, por lo que no debería superarlo. Se ajustará al valor objetivo." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "La altura de capa es demasiado pequeña. Se establecerá en el mínimo (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "La altura de capa está fuera de los límites establecidos en Ajustes de la Impresora -> Extrusor -> Limite de Altura de Capa, esto puede causar problemas de calidad de impresión." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "¿Ajustarla automáticamente al límite (%g mm)?" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4684,6 +4701,13 @@ msgstr "" "Sí: habilitar el generador de muros Arachne\n" "No: deshabilitar el generador de paredes Arachne y establecer el modo [Desplazamiento] de la piel rugosa" +# AI Translated +msgid "Brim ear radius" +msgstr "Radio de las orejas de borde" + +msgid "Brim width" +msgstr "Ancho del borde de adherencia" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "El modo espiral solo funciona cuando los bucles de perímetro son 1, el soporte está desactivado, la detección de agrupamientos mediante sondeo está desactivada, las capas superiores de la carcasa son 0, la densidad de relleno es 0 y el tipo de lapso de tiempo es tradicional." @@ -4938,6 +4962,14 @@ msgstr "Fallo al generar el G-Code de calibración" msgid "Calibration error" msgstr "Error de calibración" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Esta impresora no está configurada con el hardware que necesita este control." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Este control no es compatible con esta impresora." + msgid "Network unavailable" msgstr "Red no disponible" @@ -5779,7 +5811,7 @@ msgstr "Volumen:" msgid "Size:" msgstr "Tamaño:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Se han encontrado conflictos de rutas G-Code en la capa %d, Z = %.2lfmm. Por favor, separe más los objetos en conflicto (%s <-> %s)." @@ -5960,6 +5992,10 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Proyecto" +# AI Translated +msgid "Device (Web)" +msgstr "Dispositivo (Web)" + msgid "Yes" msgstr "Sí" @@ -7997,19 +8033,19 @@ msgstr "No se seleccionó el directorio para el reemplazo" msgid "Replaced with 3D files from directory:\n" msgstr "Reemplazado con archivos 3D desde el directorio:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Omitido %s: mismo archivo.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Omitido %s: el archivo no existe.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Omitido %s: fallo al reemplazar.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Reemplazado %s.\n" @@ -8725,6 +8761,18 @@ msgstr "Con esta opción activada, puede enviar una tarea a varios dispositivos msgid "Pop up to select filament grouping mode" msgstr "Ventana emergente para seleccionar el modo de agrupación de filamentos" +# AI Translated +msgid "Visible plugin pages" +msgstr "Páginas de plugins visibles" + +# AI Translated +msgid "pages" +msgstr "páginas" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Número de páginas de plugins que se muestran como pestañas fijas antes de que el resto de páginas se agrupe en un desplegable en la última pestaña." + msgid "Behaviour" msgstr "Comportamiento" @@ -9074,6 +9122,18 @@ msgstr "Mostrar ajustes preestablecidos no compatibles" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostrar los ajustes preestablecidos incompatibles o no compatibles en los menús desplegables de impresoras y filamentos. Estos ajustes preestablecidos no se pueden seleccionar." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimental) Usar agentes de impresora en lugar de hosts de impresión" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Envía los trabajos de impresión de impresoras que no son Bambu a través de los agentes de plugin de impresora en lugar del flujo clásico de subida al host de impresión.\n" +"Cuando está desactivado, OrcaSlicer utiliza el comportamiento heredado del host de impresión." + msgid "Experimental Features" msgstr "Funciones experimentales" @@ -9333,9 +9393,25 @@ msgstr "Perfil de usuario" msgid "Preset Inside Project" msgstr "Perfil interno del proyecto" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copia en este perfil todos los valores heredados del perfil padre y elimina la relación de herencia. Los perfiles compatibles solo con el perfil padre pueden dejar de ser compatibles." + msgid "Detach from parent" msgstr "Separar del elemento padre" +# AI Translated +msgid "Unique preset" +msgstr "Perfil único" + +# AI Translated +msgid "Parent preset" +msgstr "Perfil padre" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Este perfil no hereda de otro perfil." + msgid "Name is unavailable." msgstr "El nombre no está disponible." @@ -10031,22 +10107,6 @@ msgstr "¿Está seguro de que desea activar esta opción?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Los patrones de relleno suelen diseñarse para gestionar la rotación automáticamente y asegurar una impresión adecuada y lograr sus efectos previstos (p. ej., Giroide, Cúbico). Rotar el patrón de relleno actual puede provocar soporte insuficiente. Proceda con precaución y compruebe detenidamente posibles problemas de impresión. ¿Está seguro de que desea activar esta opción?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"La altura de la capa es demasiado pequeña.\n" -"Se establecerá en min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "La altura de la capa excede el límite en Ajustes de la Impresora -> Extrusor -> Limite de Altura de Capa, esto puede causar problemas de calidad de impresión." - -msgid "Adjust to the set range automatically?\n" -msgstr "¿Desea ajustar el rango automáticamente?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Función experimental: retraer y cortar el filamento a una mayor distancia durante los cambios de filamento para minimizar el purgado. Aunque puede reducir notablemente el purgado, también puede aumentar el riesgo de atascos de boquilla u otras complicaciones de impresión.Característica experimental: Retraer y cortar el filamento a mayor distancia durante los cambios de filamento para minimizar el descarte. Aunque puede reducir notablemente el descarte, también puede elevar el riesgo de atascos de boquillas u otros problemas en la impresión." @@ -10238,6 +10298,9 @@ msgstr "Palabras clave utilizadas y encontradas" msgid "Setting Overrides" msgstr "Sobreescribir Ajustes de impresora" +msgid "Retraction when switching material" +msgstr "Retracción al cambiar de material" + msgid "Basic information" msgstr "Información básica" @@ -10364,6 +10427,12 @@ msgstr "Perfiles de proceso compatibles" msgid "Printable space" msgstr "Espacio imprimible" +msgid "Printer Agent" +msgstr "Agente de impresora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora. Los agentes disponibles se registran al iniciar el sistema." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10489,9 +10558,6 @@ msgstr "Límites de altura de la capa" msgid "Z-Hop" msgstr "Salto en Z" -msgid "Retraction when switching material" -msgstr "Retracción al cambiar de material" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11809,6 +11875,10 @@ msgstr " está demasiado cerca de una zona de exclusión, lo que provocará coli msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " está demasiado cerca del área de detección de aglomeraciones, y se producirán colisiones.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " está parcialmente fuera del área imprimible, y no se puede imprimir.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Las temperaturas de boquilla seleccionadas son incompatibles. La temperatura de boquilla de cada filamento debe estar dentro del rango de temperaturas recomendado para los demás filamentos. De lo contrario, podrían producirse atascos en la boquilla o daños en la impresora." @@ -12116,9 +12186,6 @@ msgstr "Utiliza 3MF en lugar de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activa esta opción si la impresora admite un archivo 3MF como trabajo de impresión. Cuando está activada, Orca Slicer envía el archivo cortado como un archivo .gcode.3mf, en lugar de como un archivo .gcode convencional." -msgid "Printer Agent" -msgstr "Agente de impresora" - msgid "Select the network agent implementation for printer communication." msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora." @@ -12794,9 +12861,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocidad de los puntes internos. Si se expresa como un porcentaje, será Calculado en base a la velocidad de puente. El valor por defecto es 150%." -msgid "Brim width" -msgstr "Ancho del borde de adherencia" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distancia del modelo a la línea más externa del borde de adherencia." @@ -12876,6 +12940,14 @@ msgstr "" "La geometría se verá diezmada antes de detectar angulos agudos. Este parámetro indica la longitud mínima de desviación para el diezmado\n" "0 para desactivar." +# AI Translated +msgid "Brim ears outer only" +msgstr "Orejas de borde solo en el exterior" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genera orejas de ratón únicamente en el contorno exterior del modelo, excluyendo agujeros y secciones cerradas." + msgid "upward compatible machine" msgstr "máquina compatible ascendente" @@ -14011,6 +14083,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Factor de suavizado del relleno poco denso" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Controla cuánto se redondean las esquinas del relleno poco denso. 0% mantiene el trazado original con esquinas vivas, mientras que 100% produce las curvas más amplias posibles entre líneas de relleno adyacentes." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Aceleración del relleno de la superficie superior. El uso de un valor más bajo puede mejorar la calidad de la superficie superior." @@ -14544,6 +14624,14 @@ msgstr "Con qué tipo de G-Code es compatible la impresora." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Omitir el bloque de configuración del G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "No escribe el CONFIG_BLOCK (los pares clave/valor de la configuración del laminador) en el archivo G-code. Esto puede ayudar con impresoras cuyo firmware falla al analizar esas líneas de comentario (p. ej. Anycubic go-klipper). Nota: el archivo G-code ya no contendrá los ajustes del laminador, por lo que al importarlo de nuevo en OrcaSlicer no se restaurará la configuración." + msgid "Pellet Modded Printer" msgstr "Impresora Modificada para Pellets" @@ -15583,6 +15671,14 @@ msgstr "Retracción larga al cambiar de extrusor" msgid "Retraction distance when extruder change" msgstr "Distancia de retracción al cambiar de extrusor" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Longitud de retracción (Cambio de herramienta)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Cuando se activa la retracción antes de un cambio de herramienta, el filamento se retrae la cantidad especificada (la longitud se mide sobre el filamento en bruto, antes de entrar en el extrusor)." + msgid "Z-hop height" msgstr "Altura de Salto en Z" @@ -15676,6 +15772,10 @@ msgstr "Longitud extra de reinicio" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Cuando la retracción se compensa después de un desplazamiento, el extrusor expulsará esta cantidad adicional de filamento. Esta función no suele ser necesaria." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Longitud extra de reinicio (Cambio de herramienta)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Cuando se compensa la retracción después de cambiar de cabezal, el extrusor expulsará esta cantidad adicional de filamento." @@ -16082,6 +16182,14 @@ msgstr "Cambio de herramienta en la torre de purga" msgid "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." msgstr "Obliga al cabezal a desplazarse hasta la torre de purga antes de emitir el comando de cambio de herramienta (Tx). Solo es relevante para impresoras con múltiples extrusores (múltiples cabezales) que utilicen una torre de limpieza de tipo 2. Por defecto, Orca omite el desplazamiento en máquinas con múltiples cabezales porque el firmware se encarga del cambio de cabezal, lo que puede provocar que el comando Tx se emita por encima de la pieza impresa. Habilita esta opción si deseas que el cambio de herramienta se emita siempre por encima de la torre de purga." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Esperar la temperatura en la torre de purga" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Recoge la nueva herramienta sin esperar a que alcance la temperatura de impresión, se desplaza a la torre de purga y espera allí la temperatura, justo antes de purgar. El rezumado del calentamiento cae sobre la torre en lugar de sobre el modelo, y el desplazamiento se solapa con el calentamiento. Solo es relevante para impresoras multiextrusor (multicabezal) que usan una torre de purga de tipo 2. El firmware o la macro de cambio de herramienta no deben esperar la temperatura por su cuenta. Cuando está desactivado, la espera de temperatura se emite justo después del comando de cambio de herramienta." + msgid "No sparse layers (beta)" msgstr "Sin capas de baja densidad (beta)" @@ -19281,9 +19389,6 @@ msgstr "Impresora física" msgid "Print Host upload" msgstr "Mandar al servidor de impresión" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora. Los agentes disponibles se registran al iniciar el sistema." - msgid "Select a Flashforge printer" msgstr "Selecciona una impresora Flashforge" @@ -20125,9 +20230,6 @@ msgstr "Ha ocurrido algo inesperado al intentar iniciar sesión, inténtelo de n msgid "User canceled." msgstr "Cancelado por el usuario." -msgid "Head diameter" -msgstr "Diámetro de la cabeza" - msgid "Max angle" msgstr "Ángulo máximo" @@ -20861,6 +20963,22 @@ msgstr "" "Evita la deformación\n" "¿Sabías que al imprimir materiales propensos a la deformación como el ABS, aumentar adecuadamente la temperatura de la cama térmica puede reducir la probabilidad de deformaciones?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "La altura de la capa es demasiado pequeña.\n" +#~ "Se establecerá en min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "La altura de la capa excede el límite en Ajustes de la Impresora -> Extrusor -> Limite de Altura de Capa, esto puede causar problemas de calidad de impresión." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "¿Desea ajustar el rango automáticamente?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diámetro de la cabeza" + #~ msgid "Print order within a single layer." #~ msgstr "Orden de impresión dentro de cada capa." diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index fa10cc387f..03e6d6685d 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana \n" "Language-Team: \n" @@ -4606,6 +4606,23 @@ msgstr "Uneko ganberako tenperatura materialaren tenperatura segurua baino handi msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Ganberako gutxieneko tenperatura (%d ℃) helburuko ganbera-tenperatura (%d ℃) baino altuagoa da. Gutxieneko balioa inprimaketa hasten den atalasea da, ganberak helbururantz berotzen jarraitzen duen bitartean; beraz, ez luke helburua gainditu behar. Helburuko baliora mugatuko da." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Geruza-altuera txikiegia da. Gutxienekora ezarriko da (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Geruza-altuera Inprimagailuaren ezarpenak -> Estrusorea -> Geruza-altueraren mugak atalean ezarritako mugetatik kanpo dago; horrek inprimatze-kalitateko arazoak sor ditzake." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Automatikoki mugara (%g mm) doitu nahi duzu?" + +msgid "Adjust" +msgstr "Doitu" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4725,6 +4742,13 @@ msgstr "" "Bai - Gaitu Arachne horma-sorgailua\n" "Ez - Desgaitu Arachne horma-sorgailua eta ezarri gainazal zimurraren [Desplazamendua] modua" +# AI Translated +msgid "Brim ear radius" +msgstr "Ertz-belarriaren erradioa" + +msgid "Brim width" +msgstr "Itsaspen ertzaren zabalera" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Espiral moduak baldintza hauetan bakarrik funtzionatzen du: horma-begiztak 1 izatea, euskarriak desgaituta egotea, haztatze bidezko material-metaketa detektatzea desgaituta egotea, goiko estalki-geruzak 0 izatea, dentsitate baxuko betegarriaren dentsitatea 0 izatea eta timelapse mota tradizionala izatea." @@ -4979,6 +5003,14 @@ msgstr "Hutsegitea gertatu da kalibrazioko G-Code-a sortzean" msgid "Calibration error" msgstr "Kalibrazio akatsa" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Inprimagailu honek ez dauka kontrol honek behar duen hardwarea konfiguratuta." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Kontrol hau ez da bateragarria inprimagailu honekin." + # AI Translated msgid "Network unavailable" msgstr "Sarea ez dago erabilgarri" @@ -5828,7 +5860,7 @@ msgstr "Bolumena:" msgid "Size:" msgstr "Tamaina:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "G-code ibilbideen gatazkak aurkitu dira %d geruzan, Z = %.2lf mm. Urrundu gehiago gatazkan dauden objektuak (%s <-> %s)." @@ -6005,6 +6037,10 @@ msgstr "Gailu anitz" msgid "Project" msgstr "Proiektua" +# AI Translated +msgid "Device (Web)" +msgstr "Gailua (Web)" + msgid "Yes" msgstr "Bai" @@ -8064,19 +8100,19 @@ msgstr "Ez da ordezkatzeko direktoriorik hautatu" msgid "Replaced with 3D files from directory:\n" msgstr "Direktorio honetako 3D fitxategiekin ordeztuta:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ %s saltatu da: fitxategi bera.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ %s saltatu da: fitxategia ez da existitzen.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ %s saltatu da: ezin izan da ordeztu.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ %s ordezkatu da.\n" @@ -8790,6 +8826,18 @@ msgstr "Aukera hau gaituta, zeregin bat hainbat gailutara bidali eta hainbat gai msgid "Pop up to select filament grouping mode" msgstr "Erakutsi filamentuak taldekatzeko modua hautatzeko leihoa" +# AI Translated +msgid "Visible plugin pages" +msgstr "Ikusgai dauden plugin-orriak" + +# AI Translated +msgid "pages" +msgstr "orri" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Fitxa finko gisa erakusten diren plugin-orrien kopurua; gainerako orriak azken fitxako goitibeherako zerrendan bilduko dira." + msgid "Behaviour" msgstr "Jokabidea" @@ -9142,6 +9190,18 @@ msgstr "Erakutsi onartzen ez diren aurrezarpenak" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Erakutsi bateraezinak edo onartu gabeak diren aurrezarpenak inprimagailuaren eta filamentuaren goitibeherako zerrendetan. Aurrezarpen hauek ezin dira hautatu." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Esperimentala) Erabili inprimagailu-agenteak inprimatze-hostenen ordez" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Bideratu Bambu ez diren inprimagailuen inprimatze-lanak inprimagailuaren plugin-agenteen bidez, inprimatze-hostera igotzeko fluxu klasikoaren ordez.\n" +"Desgaituta dagoenean, OrcaSlicer-ek inprimatze-hostaren aurreko portaera erabiltzen du." + msgid "Experimental Features" msgstr "Ezaugarri esperimentalak" @@ -9402,9 +9462,25 @@ msgstr "Erabiltzailearen aurrezarpena" msgid "Preset Inside Project" msgstr "Proiektu barruko aurrezarpena" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Aurrezarpen honetara gurasoaren balio heredatu guztiak kopiatzen ditu eta gurasoarekiko lotura kentzen du. Gurasoarekin soilik bateragarriak diren aurrezarpenak bateraezin gera daitezke." + msgid "Detach from parent" msgstr "Bereizi gurasotik" +# AI Translated +msgid "Unique preset" +msgstr "Aurrezarpen bakarra" + +# AI Translated +msgid "Parent preset" +msgstr "Guraso-aurrezarpena" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Aurrezarpen honek ez du beste aurrezarpen batetik heredatzen." + msgid "Name is unavailable." msgstr "Izena ez dago erabilgarri." @@ -10124,22 +10200,6 @@ msgstr "Ziur aukera hau gaitu nahi duzula?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Betegarri-patroiak normalean biraketa automatikoki kudeatzeko diseinatuta daude, behar bezala inprimatzeko eta nahi den efektua lortzeko (adibidez, Giroidea edo Kubikoa). Uneko dentsitate baxuko betegarri-patroia biratzeak euskarri eskasa eragin dezake. Kontuz jarraitu eta egiaztatu arretaz inprimatze-arazorik sor daitekeen. Ziur zaude aukera hau gaitu nahi duzula?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Geruza-altuera txikiegia da.\n" -"min_layer_height baliora ezarriko da\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Geruza-altuerak Inprimagailuaren ezarpenak -> Estrusorea -> Geruza-altueraren mugak ataleko muga gainditzen du; horrek inprimatze-kalitateko arazoak sor ditzake." - -msgid "Adjust to the set range automatically?\n" -msgstr "Doitu automatikoki ezarritako barrutira?\n" - -msgid "Adjust" -msgstr "Doitu" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funtzio esperimentala: filamentu aldaketetan distantzia handiagoan atzera egitea eta moztea, purgatzea minimizatzeko. Purgatzea nabarmen murriztu dezakeen arren, pitaren buxadurak edo bestelako inprimatze-arazoak izateko arriskua ere handitu dezake." @@ -10333,6 +10393,9 @@ msgstr "Erreserbatutako gako-hitzak aurkitu dira" msgid "Setting Overrides" msgstr "Ezarpenen gainidazketak" +msgid "Retraction when switching material" +msgstr "Atzera-egitea materiala aldatzean" + msgid "Basic information" msgstr "Oinarrizko informazioa" @@ -10459,6 +10522,12 @@ msgstr "Prozesu-profil bateragarriak" msgid "Printable space" msgstr "Inprimatzeko espazioa" +msgid "Printer Agent" +msgstr "Inprimagailu-agentea" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Hautatu sare-agentearen inplementazioa inprimagailuarekin komunikatzeko. Erabilgarri dauden agenteak abioan erregistratzen dira." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10584,9 +10653,6 @@ msgstr "Geruza-altueraren mugak" msgid "Z-Hop" msgstr "Z jauzia" -msgid "Retraction when switching material" -msgstr "Atzera-egitea materiala aldatzean" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11912,6 +11978,10 @@ msgstr " bazterketa-eremu batetik gertuegi dago, eta talkak eragingo ditu.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " material-metaketa detektatzeko eremutik gertuegi dago, eta talkak eragingo ditu.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " inprimagarri den eremutik kanpo dago partzialki, eta ezin da inprimatu.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Hautatutako pita-tenperaturak ez dira bateragarriak. Filamentu bakoitzaren pita-tenperaturak gainerako filamentuen gomendatutako pita-tenperatura tartean egon behar du. Bestela, pita buxatu edo inprimagailua kaltetu daiteke." @@ -12228,9 +12298,6 @@ msgstr "Erabili 3MF G-codearen ordez" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Gaitu aukera hau inprimagailuak 3MF fitxategi bat inprimatze-lan gisa onartzen badu. Gaituta dagoenean, OrcaSlicerrek xerratutako fitxategia .gcode.3mf gisa bidaltzen du, .gcode fitxategi arrunt baten ordez." -msgid "Printer Agent" -msgstr "Inprimagailu-agentea" - msgid "Select the network agent implementation for printer communication." msgstr "Hautatu inprimagailuarekin komunikatzeko sare-agentearen inplementazioa." @@ -12905,9 +12972,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Barru-zubien abiadura. Balioa ehuneko gisa adierazten bada, Zubien abiadura-ren arabera kalkulatuko da. Lehenetsitako balioa % 150ekoa da." -msgid "Brim width" -msgstr "Itsaspen ertzaren zabalera" - msgid "This is the distance from the model to the outermost brim line." msgstr "Hau da modelotik itsaspen ertzaren kanporen lerrora dagoen distantzia." @@ -12987,6 +13051,14 @@ msgstr "" "Geometria sinplifikatu egingo da angelu zorrotzak detektatu aurretik. Parametro honek sinplifikaziorako desbideratzearen gutxieneko luzera adierazten du.\n" "0, desaktibatzeko." +# AI Translated +msgid "Brim ears outer only" +msgstr "Ertz-belarriak kanpoaldean soilik" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Sortu saguaren belarriak modeloaren kanpoko ingeradan soilik, zuloak eta itxitako atalak baztertuta." + msgid "upward compatible machine" msgstr "gorantz bateragarria den makina" @@ -14137,6 +14209,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroidea" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Dentsitate baxuko betegarriaren leuntze-faktorea" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Dentsitate baxuko betegarriaren izkinak zenbateraino biribiltzen diren kontrolatzen du. 0% balioak jatorrizko ibilbide zorrotza mantentzen du, eta 100% balioak ondoz ondoko betegarri-lerroen arteko kurbarik zabalenak sortzen ditu." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Hau da goiko gainazaleko betegarriaren azelerazioa. Balio txikiago batek goiko gainazalaren kalitatea hobetu dezake." @@ -14676,6 +14756,14 @@ msgstr "Inprimagailua zer G-code motarekin den bateragarria." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Saltatu G-code-aren konfigurazio-blokea" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Ez idatzi CONFIG_BLOCK (xerragailuaren konfigurazioko gako/balio bikoteak) G-code fitxategian. Lagungarria izan daiteke firmwareak iruzkin-lerro horiek prozesatzean huts egiten duen inprimagailuetan (adib. Anycubic go-klipper). Oharra: G-code fitxategiak ez ditu jada xerragailuaren ezarpenak edukiko; beraz, OrcaSlicer-era berriro inportatzeak ez du konfigurazioa berreskuratuko." + msgid "Pellet Modded Printer" msgstr "Pelletekin moldatutako inprimagailua" @@ -15719,6 +15807,14 @@ msgstr "Atzera-egite luzea estrusorea aldatzean" msgid "Retraction distance when extruder change" msgstr "Atzera-egite distantzia estrusorea aldatzean" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Atzera-egitearen luzera (Erreminta aldaketa)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Erreminta aldatu aurretik atzera-egitea abiarazten denean, filamentua zehaztutako kopurua atzeratzen da (luzera filamentu gordinean neurtzen da, estrusorean sartu aurretik)." + msgid "Z-hop height" msgstr "Z jauziaren altuera" @@ -15812,6 +15908,10 @@ msgstr "Berrabiaraztean luzera gehigarria" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Mugimenduaren ondoren atzera-egitea konpentsatzen denean, estrusoreak filamentu kantitate gehigarri hau bultzatuko du. Ezarpen hau gutxitan behar da." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Berrabiaraztean luzera gehigarria (Erreminta aldaketa)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Tresna aldatu ondoren atzera-egitea konpentsatzen denean, estrusoreak filamentu kantitate gehigarri hau bultzatuko du." @@ -16220,6 +16320,14 @@ msgstr "Tresna-aldaketa purgatze-dorrean" msgid "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." msgstr "Behartu inprimatze-burua purgatze-dorrera joatera tresna aldatzeko agindua (Tx) eman aurretik. 2. motako purgatze-dorrea erabiltzen duten estrusore anitzeko (inprimatze-buru anitzeko) inprimagailuetarako bakarrik da garrantzitsua. Lehenespenez, Orcak ez du joan-etorria egiten inprimatze-buru anitzeko makinetan, firmwareak buruaren aldaketa kudeatzen duelako; horren ondorioz, Tx agindua inprimatutako piezaren gainean eman daiteke. Gaitu aukera hau tresna-aldaketa beti purgatze-dorrearen gainean egin dadin." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Itxaron tenperatura purgatze-dorrean" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Hartu erreminta berria inprimatze-tenperaturara iritsi arte itxaron gabe, joan purgatze-dorrera eta itxaron han tenperatura, purgatu aurretik. Berotzeak eragindako jarioa dorrean erortzen da modeloan beharrean, eta desplazamendua berotzearekin gainjartzen da. Estrusore anitzeko (inprimatze-buru anitzeko) inprimagailuetan soilik da baliagarria, 2. motako purgatze-dorrea erabiltzen dutenean. Firmwareak edo erreminta aldaketaren makroak ez du tenperaturaren zain egon behar. Desgaituta dagoenean, tenperaturaren zain egoteko agindua erreminta aldaketaren komandoaren ondoren bidaltzen da." + msgid "No sparse layers (beta)" msgstr "Geruza bakandurik ez (beta)" @@ -19429,9 +19537,6 @@ msgstr "Inprimagailu fisikoa" msgid "Print Host upload" msgstr "Inprimatze-ostalariaren karga" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Hautatu sare-agentearen inplementazioa inprimagailuarekin komunikatzeko. Erabilgarri dauden agenteak abioan erregistratzen dira." - msgid "Select a Flashforge printer" msgstr "Hautatu Flashforge inprimagailu bat" @@ -20278,9 +20383,6 @@ msgstr "Ustekabeko zerbait gertatu da saioa hasten saiatzean; saiatu berriro." msgid "User canceled." msgstr "Erabiltzaileak bertan behera utzi du." -msgid "Head diameter" -msgstr "Buruaren diametroa" - msgid "Max angle" msgstr "Gehieneko angelua" @@ -21016,6 +21118,22 @@ msgstr "" "Saihestu okertzea\n" "Ba al zenekien ABS bezalako okertzeko joera duten materialak inprimatzean ohe beroaren tenperatura egoki igotzeak okertzeko probabilitatea murriztu dezakeela?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Geruza-altuera txikiegia da.\n" +#~ "min_layer_height baliora ezarriko da\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Geruza-altuerak Inprimagailuaren ezarpenak -> Estrusorea -> Geruza-altueraren mugak ataleko muga gainditzen du; horrek inprimatze-kalitateko arazoak sor ditzake." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Doitu automatikoki ezarritako barrutira?\n" + +#~ msgid "Head diameter" +#~ msgstr "Buruaren diametroa" + #~ msgid "Print order within a single layer." #~ msgstr "Geruza bakarreko inprimatze-ordena." diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 1257994f16..fc58381106 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -4643,6 +4643,23 @@ msgstr "La température actuelle du caisson est supérieure à la température d msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La température minimale du caisson (%d℃) est supérieure à la température cible du caisson (%d℃). La valeur minimale est le seuil à partir duquel l’impression démarre tandis que le caisson continue de chauffer vers la cible ; elle ne doit donc pas la dépasser. Elle sera limitée à la cible." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "La hauteur de couche est trop faible. Elle sera définie au minimum (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "La hauteur de couche est en dehors des limites définies dans Paramètres de l’imprimante -> Extrudeur -> Limites de la hauteur de la couche, ce qui peut entraîner des problèmes de qualité d’impression." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "L’ajuster automatiquement à la limite (%g mm) ?" + +msgid "Adjust" +msgstr "Ajuster" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4762,6 +4779,13 @@ msgstr "" "Oui - Activer le générateur de parois Arachne\n" "Non - Désactiver le générateur de parois Arachne et définir le mode [Déplacement] de la surface irrégulière" +# AI Translated +msgid "Brim ear radius" +msgstr "Rayon de la bordure à oreilles" + +msgid "Brim width" +msgstr "Largeur de la bordure" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Le mode spirale ne fonctionne que lorsque le nombre de parois est 1, le support est désactivé, la détection d'agglomération par sondage est désactivée, les couches supérieures sont à 0, la densité de remplissage clairsemé est à 0 et le type de timelapse est traditionnel." @@ -4835,7 +4859,7 @@ msgid "Calibrating the micro lidar" msgstr "Calibrage du micro-Lidar" msgid "Calibrating flow ratio" -msgstr "Calibration du ratio de débit" +msgstr "Calibration du rapport de débit" msgid "Pause (nozzle temperature malfunction)" msgstr "Pause (dysfonctionnement de la température de la buse)" @@ -5016,6 +5040,14 @@ msgstr "Échec de la génération du G-code de calibration" msgid "Calibration error" msgstr "Erreur de la calibration" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Cette imprimante ne dispose pas du matériel requis par ce contrôle." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Ce contrôle n’est pas pris en charge sur cette imprimante." + # AI Translated msgid "Network unavailable" msgstr "Réseau indisponible" @@ -5871,7 +5903,7 @@ msgstr "Volume :" msgid "Size:" msgstr "Taille :" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Des conflits de chemins G-code ont été trouvés au niveau de la couche %d, z = %.2lfmm. Veuillez séparer davantage les objets en conflit (%s <-> %s)." @@ -6052,6 +6084,10 @@ msgstr "Multi-appareils" msgid "Project" msgstr "Projet" +# AI Translated +msgid "Device (Web)" +msgstr "Appareil (Web)" + msgid "Yes" msgstr "Oui" @@ -7434,11 +7470,11 @@ msgstr "Erreur lors du chargement des shaders" msgctxt "Layers" msgid "Top" -msgstr "Du haut" +msgstr "Supérieur" msgctxt "Layers" msgid "Bottom" -msgstr "Du bas" +msgstr "Inférieur" # AI Translated msgid "Plugin Selection" @@ -8120,19 +8156,19 @@ msgstr "Le répertoire pour le remplacement n'a pas été sélectionné" msgid "Replaced with 3D files from directory:\n" msgstr "Remplacé par des fichiers 3D depuis le répertoire :\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Ignoré %s : même fichier.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Ignoré %s : le fichier n'existe pas.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Ignoré %s : échec du remplacement.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Remplacé %s.\n" @@ -8857,6 +8893,18 @@ msgstr "Si cette option est activée, vous pouvez envoyer une tâche à plusieur msgid "Pop up to select filament grouping mode" msgstr "Fenêtre contextuelle pour sélectionner le mode de regroupement des filaments" +# AI Translated +msgid "Visible plugin pages" +msgstr "Pages de plugins visibles" + +# AI Translated +msgid "pages" +msgstr "pages" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Nombre de pages de plugins affichées sous forme d’onglets fixes avant que les pages restantes ne soient regroupées dans un menu déroulant sur le dernier onglet." + msgid "Behaviour" msgstr "Comportement" @@ -9211,6 +9259,18 @@ msgstr "Afficher les préréglages non pris en charge" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Affiche les préréglages incompatibles ou non pris en charge dans les listes déroulantes d’imprimantes et de filaments. Ces préréglages ne peuvent pas être sélectionnés." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Expérimental) Utiliser les agents d’imprimante au lieu des hôtes d’impression" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Achemine les tâches d’impression des imprimantes non Bambu via les agents de plugin d’imprimante au lieu du flux classique d’envoi vers l’hôte d’impression.\n" +"Lorsque cette option est désactivée, OrcaSlicer utilise l’ancien comportement de l’hôte d’impression." + msgid "Experimental Features" msgstr "Fonctionnalités expérimentales" @@ -9472,9 +9532,25 @@ msgstr "Préréglage utilisateur" msgid "Preset Inside Project" msgstr "Préréglage intégré au projet" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copie dans ce préréglage toutes les valeurs héritées du préréglage parent et supprime le lien d’héritage. Les préréglages compatibles uniquement avec le parent peuvent devenir incompatibles." + msgid "Detach from parent" msgstr "Détacher du parent" +# AI Translated +msgid "Unique preset" +msgstr "Préréglage unique" + +# AI Translated +msgid "Parent preset" +msgstr "Préréglage parent" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Ce préréglage n’hérite d’aucun autre préréglage." + msgid "Name is unavailable." msgstr "Le nom n'est pas disponible." @@ -10211,27 +10287,11 @@ msgstr "Voulez-vous vraiment activer cette option ?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Les motifs de remplissage sont généralement conçus pour gérer la rotation automatiquement afin d'assurer une impression correcte et d'atteindre les effets souhaités (ex. : Gyroïde, Cubique). La rotation du motif de remplissage clairsemé actuel peut entraîner un support insuffisant. Veuillez procéder avec précaution et vérifier soigneusement tout problème d'impression potentiel. Voulez-vous vraiment activer cette option ?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"La hauteur de couche est trop faible.\n" -"Elle sera définie à min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "La hauteur de la couche dépasse la limite fixée dans Paramètres de l’imprimante -> Extrudeur -> Limites de la hauteur de la couche, ce qui peut entraîner des problèmes de qualité d’impression." - -msgid "Adjust to the set range automatically?\n" -msgstr "S’ajuster automatiquement à la plage définie ?\n" - -msgid "Adjust" -msgstr "Ajuster" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." -msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser le rinçage. Bien que cela puisse réduire considérablement le rinçage, cela peut également augmenter le risque de bouchage des buses ou d’autres complications d’impression." +msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser la purge. Bien que cela puisse réduire considérablement la purge, cela peut également augmenter le risque de bouchage des buses ou d’autres complications d’impression." msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications. Please use with the latest printer firmware." -msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser l’affleurement. Bien que cela puisse réduire sensiblement l’affleurement, cela peut également augmenter le risque d’obstruction des buses ou d’autres complications d’impression. Veuillez utiliser le dernier micrologiciel de l’imprimante." +msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser la purge. Bien que cela puisse réduire sensiblement la purge, cela peut également augmenter le risque d’obstruction des buses ou d’autres complications d’impression. Veuillez utiliser le dernier micrologiciel de l’imprimante." msgid "" "When recording timelapse without toolhead, it is recommended to add a \"Timelapse Wipe Tower\" \n" @@ -10422,6 +10482,9 @@ msgstr "Mots clés réservés trouvés" msgid "Setting Overrides" msgstr "Forçage des réglages" +msgid "Retraction when switching material" +msgstr "Rétraction lors du changement de matériau" + msgid "Basic information" msgstr "Informations de base" @@ -10548,6 +10611,12 @@ msgstr "Profils de traitement compatibles" msgid "Printable space" msgstr "Espace imprimable" +msgid "Printer Agent" +msgstr "Agent d'imprimante" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante. Les agents disponibles sont enregistrés au démarrage." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10673,9 +10742,6 @@ msgstr "Limites de hauteur de couche" msgid "Z-Hop" msgstr "Saut en Z" -msgid "Retraction when switching material" -msgstr "Rétraction lors du changement de matériau" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12010,6 +12076,10 @@ msgstr " est trop proche d'une zone d'exclusion. Cela va entraîner des collisio msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " est trop proche de la zone de détection d'agglomération, et des collisions seront causées.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " est partiellement en dehors de la zone imprimable et ne peut pas être imprimé.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Les températures de buse sélectionnées sont incompatibles. La température de buse de chaque filament doit se situer dans la plage de température de buse recommandée des autres filaments. Sinon, un bouchage de la buse ou des dommages à l’imprimante peuvent survenir." @@ -12323,9 +12393,6 @@ msgstr "Utiliser le 3MF au lieu du G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activez ceci si l’imprimante accepte un fichier 3MF comme tâche d’impression. Lorsque cette option est activée, Orca Slicer envoie le fichier découpé au format .gcode.3mf au lieu d’un simple fichier .gcode." -msgid "Printer Agent" -msgstr "Agent d'imprimante" - msgid "Select the network agent implementation for printer communication." msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante." @@ -12689,7 +12756,7 @@ msgstr "" "Si réglée à 0, la largeur de ligne correspond à celle du remplissage plein interne." msgid "Internal bridge flow ratio" -msgstr "Ratio de débit du pont interne" +msgstr "Rapport de débit du pont interne" msgid "" "This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill so increasing it may increase strength and upper layer quality.\n" @@ -12729,13 +12796,13 @@ msgstr "" "Le débit réel du remplissage solide inférieur utilisé est calculé en multipliant cette valeur par le rapport de débit du filament et, s’il est défini, par le rapport de débit de l’objet." msgid "Set other flow ratios" -msgstr "Définir d'autres ratios de débit" +msgstr "Définir d'autres rapports de débit" msgid "Change flow ratios for other extrusion path types." -msgstr "Modifier les ratios de débit pour d'autres types de chemin d'extrusion." +msgstr "Modifier les rapports de débit pour d'autres types de chemin d'extrusion." msgid "First layer flow ratio" -msgstr "Ratio de débit de la première couche" +msgstr "Rapport de débit de la première couche" msgid "" "This factor affects the amount of material on the first layer for the extrusion path roles listed in this section.\n" @@ -12744,10 +12811,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau sur la première couche pour les rôles de chemin d'extrusion listés dans cette section.\n" "\n" -"Pour la première couche, le ratio de débit réel pour chaque rôle de chemin (n'affecte pas les bordures et les jupes) sera multiplié par cette valeur." +"Pour la première couche, le rapport de débit réel pour chaque rôle de chemin (n'affecte pas les bordures et les jupes) sera multiplié par cette valeur." msgid "Outer wall flow ratio" -msgstr "Ratio de débit de la paroi extérieure" +msgstr "Rapport de débit de la paroi extérieure" msgid "" "This factor affects the amount of material for outer walls.\n" @@ -12756,10 +12823,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour les parois extérieures.\n" "\n" -"Le débit réel de la paroi extérieure est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel de la paroi extérieure est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Inner wall flow ratio" -msgstr "Ratio de débit de la paroi intérieure" +msgstr "Rapport de débit de la paroi intérieure" msgid "" "This factor affects the amount of material for inner walls.\n" @@ -12768,10 +12835,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour les parois intérieures.\n" "\n" -"Le débit réel de la paroi intérieure est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel de la paroi intérieure est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Overhang flow ratio" -msgstr "Ratio de débit de surplomb" +msgstr "Rapport de débit de surplomb" msgid "" "This factor affects the amount of material for overhangs.\n" @@ -12780,10 +12847,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour les surplombs.\n" "\n" -"Le débit réel de surplomb est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel de surplomb est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Sparse infill flow ratio" -msgstr "Ratio de débit du remplissage clairsemé" +msgstr "Rapport de débit du remplissage clairsemé" msgid "" "This factor affects the amount of material for sparse infill.\n" @@ -12792,10 +12859,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour le remplissage clairsemé.\n" "\n" -"Le débit réel du remplissage clairsemé est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel du remplissage clairsemé est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Internal solid infill flow ratio" -msgstr "Ratio de débit du remplissage solide interne" +msgstr "Rapport de débit du remplissage solide interne" msgid "" "This factor affects the amount of material for internal solid infill.\n" @@ -12804,10 +12871,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour le remplissage solide interne.\n" "\n" -"Le débit réel du remplissage solide interne est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel du remplissage solide interne est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Gap fill flow ratio" -msgstr "Ratio de débit du remplissage des espaces" +msgstr "Rapport de débit du remplissage des espaces" msgid "" "This factor affects the amount of material for filling the gaps.\n" @@ -12816,10 +12883,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour le remplissage des espaces.\n" "\n" -"Le débit réel du remplissage des espaces est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel du remplissage des espaces est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Support flow ratio" -msgstr "Ratio de débit des supports" +msgstr "Rapport de débit des supports" msgid "" "This factor affects the amount of material for support.\n" @@ -12828,10 +12895,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour les supports.\n" "\n" -"Le débit réel des supports est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel des supports est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Support interface flow ratio" -msgstr "Ratio de débit de l'interface de support" +msgstr "Rapport de débit de l'interface de support" msgid "" "This factor affects the amount of material for the support interface.\n" @@ -12840,7 +12907,7 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour l'interface de support.\n" "\n" -"Le débit réel de l'interface de support est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel de l'interface de support est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Precise wall" msgstr "Parois précises" @@ -13000,9 +13067,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Vitesse des ponts internes. Si la valeur est exprimée en pourcentage, elle sera calculée sur la base de la vitesse du pont. La valeur par défaut est 150%." -msgid "Brim width" -msgstr "Largeur de la bordure" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distance du modèle à la ligne de bord la plus externe" @@ -13043,10 +13107,10 @@ msgid "" "\n" "If your current setup already works well, enabling it may be unnecessary and can cause the brim to fuse with upper layers." msgstr "" -"Lorsqu'il est activé, le bordure est aligné avec la géométrie du périmètre de la première couche après l'application de la compensation du pied d'éléphant.\n" -"Cette option est destinée aux cas où la compensation du pied d'éléphant modifie considérablement l’empreinte de la première couche.\n" +"Lorsqu'il est activé, la bordure est alignée avec la géométrie du périmètre de la première couche après l'application de la compensation de la patte d'éléphant.\n" +"Cette option est destinée aux cas où la compensation de la patte d'éléphant modifie considérablement l’empreinte de la première couche.\n" "\n" -"Si votre configuration actuelle fonctionne déjà bien, son activation peut être inutile et peut provoquer la fusion du bordure avec les couches supérieures." +"Si votre configuration actuelle fonctionne déjà bien, son activation peut être inutile et peut provoquer la fusion de la bordure avec les couches supérieures." msgid "Combine brims" msgstr "Combiner les bordures" @@ -13082,6 +13146,14 @@ msgstr "" "La géométrie sera décimée avant de détecter les angles vifs. Ce paramètre indique la longueur minimale de l’écart pour la décimation.\n" "0 pour désactiver" +# AI Translated +msgid "Brim ears outer only" +msgstr "Bordure à oreilles sur le contour extérieur uniquement" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Génère des oreilles de souris uniquement sur le contour extérieur du modèle, en excluant les trous et les sections fermées." + msgid "upward compatible machine" msgstr "machine à compatibilité ascendante" @@ -13643,7 +13715,7 @@ msgid "" msgstr "" "Le matériau peut présenter un changement volumétrique après le passage de l’état fondu à l’état cristallin. Ce paramètre modifie proportionnellement tous les débits d’extrusion de ce filament dans le G-code. La valeur recommandée est comprise entre 0,95 et 1,05. Vous pouvez peut-être ajuster cette valeur pour obtenir une belle surface plate lorsqu’il y a un léger débordement ou un sous-débordement.\n" "\n" -"Le ratio de débit de l’objet final est cette valeur multipliée par le ratio de débit du filament." +"Le rapport de débit de l’objet final est cette valeur multipliée par le rapport de débit du filament." msgid "Enable pressure advance" msgstr "Activer la Pressure Advance" @@ -14236,6 +14308,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroïde" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Facteur de lissage du remplissage" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Contrôle le degré d’arrondi des angles du remplissage. 0% conserve le tracé anguleux d’origine, tandis que 100% produit les courbes les plus amples possibles entre les lignes de remplissage adjacentes." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Il s'agit de l'accélération de la surface supérieure du remplissage. Utiliser une valeur plus petite pourrait améliorer la qualité de la surface supérieure" @@ -14774,6 +14854,14 @@ msgstr "Avec quel type de G-code l'imprimante est-elle compatible." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Omettre le bloc de configuration du G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "N’écrit pas le CONFIG_BLOCK (les paires clé/valeur de la configuration du logiciel de découpe) dans le fichier G-code. Cela peut aider avec les imprimantes dont le firmware plante lors de l’analyse de ces lignes de commentaire (par ex. Anycubic go-klipper). Remarque : le fichier G-code ne contiendra plus les réglages du logiciel de découpe, sa réimportation dans OrcaSlicer ne restaurera donc pas la configuration." + msgid "Pellet Modded Printer" msgstr "Imprimante à pellets" @@ -15821,6 +15909,14 @@ msgstr "Rétraction longue lors du changement d'extrudeur" msgid "Retraction distance when extruder change" msgstr "Distance de rétraction lors du changement d'extrudeur" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Longueur de rétraction (Changement d’outil)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Lorsque la rétraction est déclenchée avant un changement d’outil, le filament est rétracté de la quantité spécifiée (la longueur est mesurée sur le filament brut, avant son entrée dans l’extrudeur)." + msgid "Z-hop height" msgstr "Hauteur du saut en Z" @@ -15914,6 +16010,10 @@ msgstr "Longueur supplémentaire" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Lorsque la rétraction est compensée après le mouvement de déplacement, l’extrudeuse poussera cette quantité supplémentaire de filament. Ce paramètre est rarement nécessaire." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Longueur supplémentaire à la reprise (Changement d’outil)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Lorsque la rétraction est compensée après le changement d’outil, l’extrudeur poussera cette quantité supplémentaire de filament." @@ -16012,11 +16112,11 @@ msgstr "" "Si l’angle maximal à l’intérieur de la boucle périmétrique dépasse cette valeur (indiquant l’absence d’angles vifs), une couture en biseau sera utilisée. La valeur par défaut est de 155°." msgid "Conditional overhang threshold" -msgstr "Seuil de dépassement conditionnel" +msgstr "Seuil de surplomb conditionnel" #, no-c-format, no-boost-format msgid "This option determines the overhang threshold for the application of scarf joint seams. If the unsupported portion of the perimeter is less than this threshold, scarf joint seams will be applied. The default threshold is set at 40% of the external wall's width. Due to performance considerations, the degree of overhang is estimated." -msgstr "Cette option détermine le seuil de surplomb pour l’application des coutures en écharpe. Si la partie non soutenue du périmètre est inférieure à ce seuil, des coutures en biseau seront appliquées. Le seuil par défaut est fixé à 40 % de la largeur de la paroi extérieure. Pour des raisons de performance, le degré de surplomb est estimé." +msgstr "Cette option détermine le seuil de surplomb pour l’application des coutures en biseau. Si la partie non soutenue du périmètre est inférieure à ce seuil, des coutures en biseau seront appliquées. Le seuil par défaut est fixé à 40 % de la largeur de la paroi extérieure. Pour des raisons de performance, le degré de surplomb est estimé." msgid "Scarf joint speed" msgstr "Vitesse de la couture en biseau" @@ -16025,7 +16125,7 @@ msgid "This option sets the printing speed for scarf joints. It is recommended t msgstr "Cette option définit la vitesse d’impression des coutures en biseau. Il est recommandé d’imprimer les coutures en biseau à une vitesse lente (moins de 100 mm/s). Il est également conseillé d’activer l’option « Lissage de la vitesse d’extrusion » si la vitesse définie varie de manière significative par rapport à la vitesse des parois extérieures ou intérieures. Si la vitesse spécifiée ici est supérieure à la vitesse des parois extérieures ou intérieures, l’imprimante prendra par défaut la plus lente des deux vitesses. Lorsqu’elle est spécifiée sous forme de pourcentage (par exemple, 80 %), la vitesse est calculée sur la base de la vitesse de la paroi extérieure ou intérieure. La valeur par défaut est fixée à 100 %." msgid "Scarf joint flow ratio" -msgstr "Ratio de débit de la couture en biseau" +msgstr "Rapport de débit de la couture en biseau" msgid "This factor affects the amount of material for scarf joints." msgstr "Ce facteur influe sur la quantité de matériau pour les coutures en biseau." @@ -16234,7 +16334,7 @@ msgstr "Taux de débit de la finition en spirale" #, no-c-format, no-boost-format msgid "Sets the finishing flow ratio while ending the spiral. Normally the spiral transition scales the flow ratio from 100% to 0% during the last loop which can in some cases lead to under extrusion at the end of the spiral." -msgstr "Définit le ratio de débit de finition lors de la fin de la spirale. Normalement, la transition de la spirale fait passer le taux de débit de 100% à 0% au cours de la dernière boucle, ce qui peut dans certains cas entraîner une sous-extrusion à la fin de la spirale." +msgstr "Définit le rapport de débit de finition lors de la fin de la spirale. Normalement, la transition de la spirale fait passer le taux de débit de 100% à 0% au cours de la dernière boucle, ce qui peut dans certains cas entraîner une sous-extrusion à la fin de la spirale." msgid "If smooth or traditional mode is selected, a timelapse video will be generated for each print. After each layer is printed, a snapshot is taken with the chamber camera. All of these snapshots are composed into a timelapse video when printing completes. If smooth mode is selected, the toolhead will move to the excess chute after each layer is printed and then take a snapshot. Since the melt filament may leak from the nozzle during the process of taking a snapshot, a prime tower is required for smooth mode to wipe the nozzle." msgstr "Si le mode fluide ou traditionnel est sélectionné, une vidéo en timelapse sera générée pour chaque impression. À chaque couche imprimée, un instantané est pris avec la caméra intégrée. Tous ces instantanés seront assemblés dans une vidéo timelapse une fois l'impression terminée. Si le mode lisse est sélectionné, l'extrudeur se déplace vers la goulotte d'évacuation à chaque couche imprimée, puis prend un cliché. Étant donné que le filament fondu peut s'échapper de la buse pendant la prise de vue, une tour d’amorçage est requise en mode lisse pour essuyer la buse." @@ -16326,6 +16426,14 @@ msgstr "Changement d’outil sur la tour d’essuyage" msgid "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." msgstr "Force la tête d’outil à se déplacer vers la tour d’essuyage avant d’émettre la commande de changement d’outil (Tx). Pertinent uniquement pour les imprimantes multi-extrudeurs (à têtes d’outil multiples) utilisant une tour d’essuyage de type 2. Par défaut, Orca omet ce déplacement sur les machines à têtes d’outil multiples car le firmware gère le changement de tête, ce qui peut entraîner l’émission de la commande Tx au-dessus de la pièce imprimée. Activez cette option si vous préférez que le changement d’outil soit toujours émis au-dessus de la tour d’essuyage." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Attendre la température sur la tour d’essuyage" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Prend le nouvel outil sans attendre qu’il atteigne la température d’impression, se déplace vers la tour d’essuyage et y attend la température, juste avant la purge. Le suintement dû à la chauffe se dépose sur la tour plutôt que sur le modèle, et le déplacement se superpose à la chauffe. Uniquement pertinent pour les imprimantes multi-extrudeurs (multi-têtes) utilisant une tour d’essuyage de type 2. Le firmware ou la macro de changement d’outil ne doivent pas attendre la température eux-mêmes. Lorsque cette option est désactivée, l’attente de température est émise juste après la commande de changement d’outil." + msgid "No sparse layers (beta)" msgstr "Pas de couches éparses (beta)" @@ -18217,7 +18325,7 @@ msgid "Record Factor" msgstr "Enregistrer le facteur" msgid "We found the best flow ratio for you" -msgstr "Nous avons trouvé le meilleur ratio de débit pour vous" +msgstr "Nous avons trouvé le meilleur rapport de débit pour vous" msgid "Flow Ratio" msgstr "Rapport de débit" @@ -19542,9 +19650,6 @@ msgstr "Imprimante Physique" msgid "Print Host upload" msgstr "Envoi vers l’imprimante hôte" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante. Les agents disponibles sont enregistrés au démarrage." - msgid "Select a Flashforge printer" msgstr "Sélectionner une imprimante Flashforge" @@ -20392,9 +20497,6 @@ msgstr "Un événement inattendu s’est produit lors de la connexion, veuillez msgid "User canceled." msgstr "L’utilisateur a annulé." -msgid "Head diameter" -msgstr "Diamètre de la tête" - msgid "Max angle" msgstr "Angle maximal" @@ -21176,6 +21278,22 @@ msgstr "" "Éviter la déformation\n" "Saviez-vous que lors de l’impression de matériaux susceptibles de se déformer, tels que l’ABS, une augmentation appropriée de la température du plateau chauffant peut réduire la probabilité de déformation?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "La hauteur de couche est trop faible.\n" +#~ "Elle sera définie à min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "La hauteur de la couche dépasse la limite fixée dans Paramètres de l’imprimante -> Extrudeur -> Limites de la hauteur de la couche, ce qui peut entraîner des problèmes de qualité d’impression." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "S’ajuster automatiquement à la plage définie ?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diamètre de la tête" + #~ msgid "Print order within a single layer." #~ msgstr "Ordre d’impression au sein d’une même couche" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 98cd987512..9f8a849884 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -4739,6 +4739,23 @@ msgstr "A kamra aktuális hőmérséklete magasabb az anyag biztonságos hőmér msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "A minimális kamrahőmérséklet (%d℃) magasabb a cél kamrahőmérsékletnél (%d℃). A minimális érték az a küszöb, amelynél a nyomtatás elindul, miközben a kamra tovább melegszik a célérték felé, ezért nem haladhatja meg azt. Az érték a célértékre lesz korlátozva." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "A rétegmagasság túl kicsi. A minimumra lesz állítva (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "A rétegmagasság a Nyomtatóbeállítások -> Extruder -> Rétegmagasság limitek menüpontban megadott határértékeken kívül esik, ez minőségbeli problémákat okozhat a nyomtatás során." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Szeretnéd automatikusan a határértékre (%g mm) igazítani?" + +msgid "Adjust" +msgstr "Módosítás" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4858,6 +4875,13 @@ msgstr "" "Igen - Engedélyezd az Arachne falgenerátort\n" "Nem - Tiltsd le az Arachne falgenerátort, majd állítsd a barázdált felületet [Eltolás] módra" +# AI Translated +msgid "Brim ear radius" +msgstr "Peremfül sugara" + +msgid "Brim width" +msgstr "Perem szélessége" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "A spirál mód csak akkor működik, ha a falhurkok száma 1, a támasz és a szondázásos csomósodásészlelés ki van kapcsolva, a felső héjrétegek száma 0, a kitöltés sűrűsége 0, a Timelapse típusa pedig hagyományos." @@ -5112,6 +5136,14 @@ msgstr "Nem sikerült létrehozni a kalibrációs G-kódot" msgid "Calibration error" msgstr "Kalibrációs hiba" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Ez a nyomtató nincs felszerelve a vezérlőelemhez szükséges hardverrel." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Ez a vezérlőelem nem támogatott ezen a nyomtatón." + # AI Translated msgid "Network unavailable" msgstr "A hálózat nem érhető el" @@ -5971,7 +6003,7 @@ msgstr "Térfogat:" msgid "Size:" msgstr "Méret:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "G-kód útvonalütközés található a(z) %d. rétegen, Z = %.2lfmm. Helyezd távolabb egymástól az ütköző objektumokat (%s <-> %s)." @@ -6153,6 +6185,10 @@ msgstr "Több eszköz" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Nyomtató (Web)" + msgid "Yes" msgstr "Igen" @@ -8244,19 +8280,19 @@ msgstr "A cseréhez nem lett mappa kiválasztva" msgid "Replaced with 3D files from directory:\n" msgstr "Cserélve a mappából származó 3D fájlokra:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ %s kihagyva: azonos fájl.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ %s kihagyva: a fájl nem létezik.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ %s kihagyva: a csere sikertelen.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔%s lecserélve.\n" @@ -8993,6 +9029,18 @@ msgstr "Ezzel az opcióval egyszerre több eszközre küldhetsz feladatot és t msgid "Pop up to select filament grouping mode" msgstr "Felugró ablak a filamentcsoportosítási mód kiválasztásához" +# AI Translated +msgid "Visible plugin pages" +msgstr "Látható bővítményoldalak" + +# AI Translated +msgid "pages" +msgstr "oldal" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "A rögzített fülként megjelenő bővítményoldalak száma; a fennmaradó oldalak az utolsó fülön lenyíló listába kerülnek." + msgid "Behaviour" msgstr "Viselkedés" @@ -9362,6 +9410,18 @@ msgstr "Nem támogatott beállítások megjelenítése" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Megjeleníti a nem kompatibilis vagy nem támogatott beállításokat a nyomtató- és filamentlegördülő listákban. Ezek a beállítások nem választhatók ki." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Kísérleti) Nyomtatóügynökök használata nyomtatókiszolgálók helyett" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"A nem Bambu nyomtatók nyomtatási feladatait a nyomtató bővítményügynökein keresztül továbbítja a klasszikus nyomtatókiszolgálóra való feltöltés helyett.\n" +"Ha ki van kapcsolva, az OrcaSlicer a régi nyomtatókiszolgáló-viselkedést használja." + # AI Translated msgid "Experimental Features" msgstr "Kísérleti funkciók" @@ -9632,9 +9692,25 @@ msgstr "Felhasználói beállítás" msgid "Preset Inside Project" msgstr "Projekt a beállításon belül" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Az összes örökölt értéket átmásolja a szülő előbeállításból ebbe az előbeállításba, és megszünteti az öröklési kapcsolatot. A csak a szülővel kompatibilis előbeállítások támogatása megszűnhet." + msgid "Detach from parent" msgstr "Leválasztás a szülőről" +# AI Translated +msgid "Unique preset" +msgstr "Önálló előbeállítás" + +# AI Translated +msgid "Parent preset" +msgstr "Szülő előbeállítás" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Ez az előbeállítás nem örököl másik előbeállításból." + msgid "Name is unavailable." msgstr "A név nem elérhető." @@ -10376,22 +10452,6 @@ msgstr "Biztos, hogy engedélyezed ezt az opciót?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "A kitöltési minták általában maguk kezelik a forgatást a megfelelő nyomtatás és a kívánt hatás elérése érdekében (pl. Gyroid, Cubic). A jelenlegi kitöltési minta elforgatása elégtelen alátámasztáshoz vezethet. Kérlek, járj el körültekintően, és alaposan ellenőrizd a lehetséges nyomtatási problémákat. Biztos, hogy engedélyezed ezt a beállítást?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"A rétegmagasság túl kicsi.\n" -"A rendszer a min_layer_height értékre állítja.\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "A rétegmagasság meghaladja a Nyomtatóbeállítások -> Extruder -> Rétegmagasság limitek menüpontban megadott értéket, ez minőségbeli problémákat okozhat a nyomtatás során." - -msgid "Adjust to the set range automatically?\n" -msgstr "Szeretnéd az értéket automatikusan a beállított tartományhoz igazítani?\n" - -msgid "Adjust" -msgstr "Módosítás" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Kísérleti funkció: Filamentcsere közben nagyobb távolságon történő visszahúzás és elvágás az öblítés minimalizálása érdekében. Bár ez jelentősen csökkentheti az öblítés mértékét, növelheti a fúvóka eltömődésének vagy más nyomtatási problémák kockázatát." @@ -10587,6 +10647,9 @@ msgstr "Foglalt kulcsszavakat találtunk" msgid "Setting Overrides" msgstr "Beállítások felülbírálása" +msgid "Retraction when switching material" +msgstr "Visszahúzás anyagváltáskor" + msgid "Basic information" msgstr "Alapinformációk" @@ -10720,6 +10783,12 @@ msgstr "Kompatibilis folyamatprofilok" msgid "Printable space" msgstr "Nyomtatási terület" +msgid "Printer Agent" +msgstr "Nyomtatóügynök" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Válaszd ki a nyomtatóval való kommunikációhoz használt hálózati ügynököt. Az elérhető ügynököket indításkor regisztrálja a rendszer." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10845,9 +10914,6 @@ msgstr "Rétegmagasság limitek" msgid "Z-Hop" msgstr "Z-emelés" -msgid "Retraction when switching material" -msgstr "Visszahúzás anyagváltáskor" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12200,6 +12266,10 @@ msgstr " túl közel van a tiltott területhez, a nyomtatás során előfordulha msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " túl közel van a csomósodásészlelési területhez, és ez ütközést fog okozni.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " részben a nyomtatható területen kívül esik, ezért nem nyomtatható ki.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "A kiválasztott fúvóka hőmérsékletek nem kompatibilisek. Mindegyik filament fúvóka hőmérsékletének a többi filament ajánlott fúvóka hőmérsékleti tartományába kell esnie. Ellenkező esetben a fúvóka eltömődhet vagy a nyomtató megsérülhet." @@ -12530,9 +12600,6 @@ msgstr "3MF használata G-kód helyett" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Kapcsold be, ha a nyomtató 3MF fájlt fogad el nyomtatási feladatként. Bekapcsolva az Orca Slicer a szeletelt fájlt .gcode.3mf formátumban küldi el egyszerű .gcode fájl helyett." -msgid "Printer Agent" -msgstr "Nyomtatóügynök" - msgid "Select the network agent implementation for printer communication." msgstr "Válaszd ki a nyomtató kommunikációjához használt hálózati ügynök implementációját." @@ -13220,9 +13287,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "A belső hidak sebessége. Ha az érték százalékban van megadva, a bridge_speed alapján lesz kiszámítva. Az alapértelmezett érték 150%." -msgid "Brim width" -msgstr "Perem szélessége" - msgid "This is the distance from the model to the outermost brim line." msgstr "A modell és a legkülső peremvonal közötti távolság" @@ -13302,6 +13366,14 @@ msgstr "" "Az éles szögek észlelése előtt a geometria egyszerűsítve lesz. Ez a paraméter a leegyszerűsítésnél figyelembe vett eltérés minimális hosszát adja meg.\n" "0 értékkel kikapcsolható." +# AI Translated +msgid "Brim ears outer only" +msgstr "Peremfülek csak kívül" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Egérfüleket csak a modell külső kontúrján hoz létre, a furatokat és a zárt szakaszokat kihagyva." + msgid "upward compatible machine" msgstr "felfelé kompatibilis gép" @@ -14475,6 +14547,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Kitöltés simítási tényezője" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Azt szabályozza, hogy a kitöltés sarkai mennyire legyenek lekerekítve. A 0% megtartja az eredeti éles útvonalat, a 100% pedig a lehető legnagyobb íveket hozza létre a szomszédos kitöltővonalak között." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "A felső felületi kitöltés gyorsulása. Alacsonyabb érték használata javíthatja a felső felület minőségét" @@ -15017,6 +15097,14 @@ msgstr "Milyen G-kóddal kompatibilis a nyomtató." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code konfigurációs blokk kihagyása" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Nem írja a CONFIG_BLOCK blokkot (a szeletelő beállításainak kulcs/érték párjait) a G-code fájlba. Ez segíthet azoknál a nyomtatóknál, amelyek firmware-e összeomlik ezeknek a megjegyzéssoroknak a feldolgozásakor (pl. Anycubic go-klipper). Megjegyzés: a G-code fájl így már nem tartalmazza a szeletelő beállításait, ezért az OrcaSlicerbe való visszaimportálás nem állítja vissza a konfigurációt." + msgid "Pellet Modded Printer" msgstr "Granulátumos módosított nyomtató" @@ -16079,6 +16167,14 @@ msgstr "Hosszú visszahúzás extruderváltáskor" msgid "Retraction distance when extruder change" msgstr "Visszahúzási távolság extruderváltáskor" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Visszahúzás hossza (Eszközváltás)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Amikor a visszahúzás eszközváltás előtt aktiválódik, a filament a megadott értékkel húzódik vissza (a hossz a nyers filamenten mérve, mielőtt az az extruderbe kerülne)." + msgid "Z-hop height" msgstr "Z-emelés magassága" @@ -16172,6 +16268,10 @@ msgstr "Extra hossz újraindításkor" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Amikor a visszahúzás kompenzálásra kerül utazási mozgás után, az extruder ezt a további szálmennyiséget nyomja előre. Erre a beállításra ritkán van szükség." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Extra hossz újraindításkor (Eszközváltás)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Amikor a visszahúzás kompenzálásra kerül szerszámváltás után, az extruder ezt a további szálmennyiséget nyomja előre." @@ -16588,6 +16688,14 @@ msgstr "Szerszámcsere a törlőtoronyban" msgid "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." msgstr "A szerszámcsere parancs (Tx) kiadása előtt a törlőtoronyhoz mozgatja a szerszámfejet. Csak a 2-es típusú törlőtornyot használó többextruderes (több szerszámfejes) nyomtatóknál van jelentősége. Az Orca alapértelmezés szerint kihagyja ezt a mozgást a több szerszámfejes gépeknél, mert a fejcserét a firmware kezeli. Emiatt azonban előfordulhat, hogy a Tx parancsot a nyomtatott tárgy felett adja ki. Kapcsold be ezt a beállítást, ha azt szeretnéd, hogy a szerszámcsere mindig a törlőtorony felett történjen." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Várakozás a hőmérsékletre a törlőtornyon" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Felveszi az új szerszámot anélkül, hogy megvárná a nyomtatási hőmérséklet elérését, a törlőtoronyhoz áll, és ott várja meg a hőmérsékletet, közvetlenül az öblítés előtt. A felfűtés közben kiszivárgó anyag a toronyra kerül a modell helyett, a mozgás pedig átfedésben van a fűtéssel. Csak több extruderes (több szerszámfejes) nyomtatóknál releváns, amelyek 2-es típusú törlőtornyot használnak. A firmware vagy a szerszámváltó makró nem várhat magától a hőmérsékletre. Ha ki van kapcsolva, a hőmérsékletre várakozás közvetlenül a szerszámváltó parancs után kerül kiadásra." + msgid "No sparse layers (beta)" msgstr "Nincsenek ritka rétegek (béta)" @@ -19847,9 +19955,6 @@ msgstr "Fizikai nyomtató" msgid "Print Host upload" msgstr "Feltöltés a nyomtatóra" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Válaszd ki a nyomtatóval való kommunikációhoz használt hálózati ügynököt. Az elérhető ügynököket indításkor regisztrálja a rendszer." - # AI Translated msgid "Select a Flashforge printer" msgstr "Válassz egy Flashforge nyomtatót" @@ -20791,9 +20896,6 @@ msgstr "Bejelentkezés közben váratlan hiba történt, próbáld újra." msgid "User canceled." msgstr "Felhasználó által megszakítva." -msgid "Head diameter" -msgstr "Fej átmérő" - msgid "Max angle" msgstr "Maximális szög" @@ -21607,6 +21709,22 @@ msgstr "" "Kunkorodás elkerülése\n" "Tudtad, hogy a kunkorodásra hajlamos anyagok (például ABS) nyomtatásakor az asztal hőmérsékletének növelése csökkentheti a kunkorodás valószínűségét?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "A rétegmagasság túl kicsi.\n" +#~ "A rendszer a min_layer_height értékre állítja.\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "A rétegmagasság meghaladja a Nyomtatóbeállítások -> Extruder -> Rétegmagasság limitek menüpontban megadott értéket, ez minőségbeli problémákat okozhat a nyomtatás során." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Szeretnéd az értéket automatikusan a beállított tartományhoz igazítani?\n" + +#~ msgid "Head diameter" +#~ msgstr "Fej átmérő" + #~ msgid "Print order within a single layer." #~ msgstr "Nyomtatási sorrend egyetlen rétegen belül." diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index 3c43178102..bc36e08d25 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4741,6 +4741,23 @@ msgstr "L'attuale temperatura della camera è superiore alla temperatura di sicu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura minima della camera (%d℃) è superiore alla temperatura target della camera (%d℃). Il valore minimo è la soglia alla quale inizia la stampa mentre la camera continua a riscaldarsi verso il target, quindi non dovrebbe superarlo. Verrà limitato al valore target." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "L'altezza dello strato è troppo piccola. Sarà impostata al valore minimo (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "L'altezza dello strato è fuori dai limiti impostati in Impostazioni stampante -> Estrusore -> Limiti Altezza Strato, ciò potrebbe causare problemi di qualità di stampa." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Regolarla automaticamente al limite (%g mm)?" + +msgid "Adjust" +msgstr "Regola" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4860,6 +4877,13 @@ msgstr "" "Sì - Abilita generatore di pareti Arachne\n" "No - Disabilita generatore di pareti Arachne e imposta la modalità [Spostamento] della Superficie ruvida" +# AI Translated +msgid "Brim ear radius" +msgstr "Raggio della tesa ad orecchio" + +msgid "Brim width" +msgstr "Larghezza tesa" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "La modalità spirale funziona solo quando i perimetri sono 1, il supporto è disabilitato, il rilevamento degli ammassi tramite sondaggio è disabilitato, gli strati superiori della shell sono 0, la densità del riempimento sparso è 0 e il tipo di timelapse è tradizionale." @@ -5114,6 +5138,14 @@ msgstr "Impossibile generare G-code di calibrazione" msgid "Calibration error" msgstr "Errore di calibrazione" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Questa stampante non dispone dell'hardware richiesto da questo controllo." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Questo controllo non è supportato su questa stampante." + # AI Translated msgid "Network unavailable" msgstr "Rete non disponibile" @@ -5973,7 +6005,7 @@ msgstr "Volume:" msgid "Size:" msgstr "Dimensione:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Sono stati trovati conflitti di percorsi nel G-code sullo strato %d, Z = %.2lfmm. Si prega di separare gli oggetti in conflitto (%s <-> %s)." @@ -6154,6 +6186,10 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Progetto" +# AI Translated +msgid "Device (Web)" +msgstr "Dispositivo (Web)" + msgid "Yes" msgstr "Sì" @@ -8244,19 +8280,19 @@ msgstr "La directory per la sostituzione non è stata selezionata" msgid "Replaced with 3D files from directory:\n" msgstr "Sostituito con file 3D dalla directory:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Saltato %s: stesso file.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Saltato %s: il file non esiste.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Saltato %s: sostituzione fallita.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Sostituito %s.\n" @@ -8995,6 +9031,18 @@ msgstr "Abilitando questa opzione, puoi inviare un'attività a più dispositivi msgid "Pop up to select filament grouping mode" msgstr "Popup per selezionare la modalità di raggruppamento filamenti" +# AI Translated +msgid "Visible plugin pages" +msgstr "Pagine dei plugin visibili" + +# AI Translated +msgid "pages" +msgstr "pagine" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Numero di pagine dei plugin mostrate come schede fisse prima che le pagine rimanenti vengano raccolte in un menu a discesa nell'ultima scheda." + msgid "Behaviour" msgstr "Comportamento" @@ -9381,6 +9429,18 @@ msgstr "Mostra i profili non supportati" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostra i profili incompatibili/non supportati negli elenchi a discesa di stampante e filamento. Questi profili non possono essere selezionati." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Sperimentale) Usa gli agenti stampante invece degli host di stampa" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Instrada i lavori di stampa delle stampanti non Bambu attraverso gli agenti plugin della stampante invece del classico flusso di caricamento sull'host di stampa.\n" +"Quando è disattivato, OrcaSlicer usa il comportamento legacy dell'host di stampa." + # AI Translated msgid "Experimental Features" msgstr "Funzionalità sperimentali" @@ -9650,9 +9710,25 @@ msgstr "Profilo utente" msgid "Preset Inside Project" msgstr "Profilo interno al progetto" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copia in questo profilo tutti i valori ereditati dal profilo padre e rimuove la relazione di ereditarietà. I profili compatibili solo con il profilo padre potrebbero non essere più supportati." + msgid "Detach from parent" msgstr "Scollega dal genitore" +# AI Translated +msgid "Unique preset" +msgstr "Profilo unico" + +# AI Translated +msgid "Parent preset" +msgstr "Profilo padre" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Questo profilo non eredita da un altro profilo." + msgid "Name is unavailable." msgstr "Nome non disponibile." @@ -10392,22 +10468,6 @@ msgstr "Sei sicuro di voler abilitare questa opzione?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "I pattern di riempimento sono generalmente progettati per gestire automaticamente la rotazione per garantire una stampa corretta e ottenere gli effetti desiderati (ad es. Gyroid, Cubico). La rotazione del pattern di riempimento sparso corrente potrebbe portare a un supporto insufficiente. Procedere con cautela e verificare accuratamente eventuali problemi di stampa. Sei sicuro di voler abilitare questa opzione?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"L'altezza dello strato è troppo piccola.\n" -"Sarà impostato su min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "L'altezza dello strato supera il limite in Impostazioni stampante -> Estrusore -> Limiti Altezza Strato. Ciò potrebbe causare problemi di qualità di stampa." - -msgid "Adjust to the set range automatically?\n" -msgstr "Regolare automaticamente l'intervallo impostato?\n" - -msgid "Adjust" -msgstr "Regola" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funzionalità sperimentale: ritrazione e taglio del filamento a una distanza maggiore durante i cambi di filamento per ridurre al minimo lo spurgo. Sebbene possa ridurre notevolmente lo spurgo, può anche aumentare il rischio di intasamento degli ugelli o di altre complicazioni di stampa." @@ -10603,6 +10663,9 @@ msgstr "Parole chiave riservate trovate" msgid "Setting Overrides" msgstr "Sovrascrivi impostazioni" +msgid "Retraction when switching material" +msgstr "Retrazione quando si cambia materiale" + msgid "Basic information" msgstr "Informazioni di base" @@ -10734,6 +10797,12 @@ msgstr "Profili di processo compatibili" msgid "Printable space" msgstr "Spazio di stampa" +msgid "Printer Agent" +msgstr "Agente stampante" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante. Gli agenti disponibili vengono registrati all'avvio." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10859,9 +10928,6 @@ msgstr "Limiti altezza strati" msgid "Z-Hop" msgstr "Sollevamento Z" -msgid "Retraction when switching material" -msgstr "Retrazione quando si cambia materiale" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12221,6 +12287,10 @@ msgstr " è troppo vicino all'area di esclusione e si verificheranno collisioni. msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " è troppo vicino all'area di rilevamento ammassi e verranno causate collisioni.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " è parzialmente fuori dall'area stampabile e non può essere stampato.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Le temperature degli ugelli selezionate sono incompatibili. La temperatura dell'ugello per ciascun filamento deve rientrare nell'intervallo di temperatura consigliato per gli altri filamenti. In caso contrario, potrebbero verificarsi ostruzioni degli ugelli o danni alla stampante." @@ -12550,9 +12620,6 @@ msgstr "Usa 3MF invece di G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Abilita questa opzione se la stampante accetta un file 3MF come processo di stampa. Quando è abilitata, Orca Slicer invia il file elaborato come .gcode.3mf, invece di un semplice file .gcode." -msgid "Printer Agent" -msgstr "Agente stampante" - msgid "Select the network agent implementation for printer communication." msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante." @@ -13239,9 +13306,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocità dei ponti interni. Se il valore è espresso in percentuale, verrà calcolato in base a bridge_speed. Il valore predefinito è 150%." -msgid "Brim width" -msgstr "Larghezza tesa" - msgid "This is the distance from the model to the outermost brim line." msgstr "Questa è la distanza tra il modello e la linea più esterna della tesa." @@ -13321,6 +13385,14 @@ msgstr "" "La geometria verrà decimata prima di rilevare gli spigoli vivi. Questo parametro indica la lunghezza minima dello scostamento per la decimazione.\n" "0 per disattivare." +# AI Translated +msgid "Brim ears outer only" +msgstr "Tesa ad orecchio solo sul contorno esterno" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genera gli orecchi di topo solo sul contorno esterno del modello, escludendo fori e sezioni chiuse." + msgid "upward compatible machine" msgstr "macchina compatibile con versioni successive" @@ -14495,6 +14567,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Fattore di arrotondamento del riempimento sparso" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Controlla quanto vengono arrotondati gli angoli del riempimento sparso. 0% mantiene il percorso originale con angoli vivi, mentre 100% produce le curve più ampie possibili tra linee di riempimento adiacenti." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Accelerazione del riempimento della superficie superiore. L'utilizzo di un valore inferiore può migliorare la qualità della superficie superiore." @@ -15039,6 +15119,14 @@ msgstr "Con quale tipo di G-code la stampante è compatibile." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Ometti il blocco di configurazione del G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Non scrive il CONFIG_BLOCK (le coppie chiave/valore della configurazione dello slicer) nel file G-code. Può essere utile con stampanti il cui firmware va in crash durante l'analisi di queste righe di commento (ad es. Anycubic go-klipper). Nota: il file G-code non conterrà più le impostazioni dello slicer, quindi reimportandolo in OrcaSlicer la configurazione non verrà ripristinata." + msgid "Pellet Modded Printer" msgstr "Stampante modificata per granuli" @@ -16098,6 +16186,14 @@ msgstr "Retrazione lunga al cambio estrusore" msgid "Retraction distance when extruder change" msgstr "Distanza di retrazione al cambio estrusore" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Lunghezza di retrazione (Cambio testina)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Quando la retrazione viene attivata prima di un cambio testina, il filamento viene ritirato della quantità specificata (la lunghezza è misurata sul filamento grezzo, prima che entri nell'estrusore)." + msgid "Z-hop height" msgstr "Altezza sollevamento Z" @@ -16195,6 +16291,10 @@ msgstr "Lunghezza aggiuntiva in ripresa" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quando la retrazione è compensata dopo uno spostamento, l'estrusore espelle questa quantità aggiuntiva di filamento. Questa impostazione è raramente necessaria." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Lunghezza aggiuntiva in ripresa (Cambio testina)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quando la retrazione è compensata dopo un cambio di testina, l'estrusore espelle questa quantità aggiuntiva di filamento." @@ -16612,6 +16712,14 @@ msgstr "Cambio utensile sulla torre di spurgo" msgid "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." msgstr "Forza la testa di stampa a spostarsi sulla torre di spurgo prima di emettere il comando di cambio utensile (Tx). Rilevante solo per le stampanti multi-estrusore (multi-testa) che utilizzano una torre di spurgo di Tipo 2. Per impostazione predefinita Orca salta lo spostamento sulle macchine multi-testa perché il firmware gestisce il cambio della testa, il che può far sì che il comando Tx venga emesso sopra la parte stampata. Abilita questa opzione se desideri che il cambio utensile venga sempre emesso sopra la torre di spurgo." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Attendi la temperatura sulla torre di spurgo" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Preleva la nuova testina senza attendere che raggiunga la temperatura di stampa, si sposta sulla torre di spurgo e attende lì la temperatura, subito prima dello spurgo. Il trasudo dovuto al riscaldamento finisce sulla torre invece che sul modello, e lo spostamento si sovrappone al riscaldamento. Rilevante solo per stampanti multi-estrusore (multi-testina) che usano una torre di spurgo di tipo 2. Il firmware o la macro di cambio testina non devono attendere la temperatura autonomamente. Quando è disattivato, l'attesa della temperatura viene emessa subito dopo il comando di cambio testina." + msgid "No sparse layers (beta)" msgstr "Nessuno strato sparso (beta)" @@ -19865,9 +19973,6 @@ msgstr "Stampante fisica" msgid "Print Host upload" msgstr "Caricamento host di stampa" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante. Gli agenti disponibili vengono registrati all'avvio." - # AI Translated msgid "Select a Flashforge printer" msgstr "Seleziona una stampante Flashforge" @@ -20810,9 +20915,6 @@ msgstr "Si è verificato un problema imprevisto durante il tentativo di accesso. msgid "User canceled." msgstr "Utente rimosso." -msgid "Head diameter" -msgstr "Diametro testa" - msgid "Max angle" msgstr "Angolo massimo" @@ -21631,6 +21733,22 @@ msgstr "" "Evita le deformazioni\n" "Sapevi che quando si stampano materiali soggetti a deformazioni come l'ABS, aumentare in modo appropriato la temperatura del piano riscaldato può ridurre la probabilità di deformazione?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "L'altezza dello strato è troppo piccola.\n" +#~ "Sarà impostato su min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "L'altezza dello strato supera il limite in Impostazioni stampante -> Estrusore -> Limiti Altezza Strato. Ciò potrebbe causare problemi di qualità di stampa." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Regolare automaticamente l'intervallo impostato?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diametro testa" + #~ msgid "Print order within a single layer." #~ msgstr "Ordine di stampa all'interno di un singolo strato." diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 0d9f044060..1b70ddbf67 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4750,6 +4750,23 @@ msgstr "現在のチャンバー温度が材料の安全温度を超えていま msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低庫内温度 (%d℃) が目標庫内温度 (%d℃) を上回っています。最低値は、チャンバーが目標に向けて加熱を続けながら印刷を開始するしきい値であるため、目標値を超えてはいけません。値は目標値に制限されます。" +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "積層ピッチが小さすぎます。最小値 (%g mm) に設定されます。" + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "積層ピッチが、プリンター設定 -> 押出機 -> 積層ピッチの制限 で設定された範囲を外れています。印刷品質の問題が発生する可能性があります。" + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "自動的に制限値 (%g mm) に調整しますか?" + +msgid "Adjust" +msgstr "調整" + # AI Translated msgid "" "Layer height too small\n" @@ -4873,6 +4890,13 @@ msgstr "" "はい - Arachneウォールジェネレーターを有効にする\n" "いいえ - Arachneウォールジェネレーターを無効にし、ファジースキンを[変位]モードに設定する" +# AI Translated +msgid "Brim ear radius" +msgstr "ブリムイヤー半径" + +msgid "Brim width" +msgstr "ブリム幅" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "スパイラルモードは壁ループが1、サポートが無効、プロービングによるクランピング検出が無効、上部シェルレイヤーが0、スパースインフィル密度が0、タイムラプスタイプがトラディショナルの場合のみ機能します。" @@ -5127,6 +5151,14 @@ msgstr "キャリブレーションG-codeの生成に失敗しました" msgid "Calibration error" msgstr "キャリブレーションエラー" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "このプリンターには、このコントロールに必要なハードウェアが設定されていません。" + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "このコントロールはこのプリンターではサポートされていません。" + # AI Translated msgid "Network unavailable" msgstr "ネットワークが利用できません" @@ -5988,7 +6020,7 @@ msgstr "ボリューム" msgid "Size:" msgstr "サイズ:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "レイヤー%d、Z = %.2lfmmでG-codeパスの衝突が検出されました。衝突するオブジェクトをもっと離してください(%s <-> %s)。" @@ -6164,6 +6196,10 @@ msgstr "マルチデバイス" msgid "Project" msgstr "プロジェクト" +# AI Translated +msgid "Device (Web)" +msgstr "デバイス (Web)" + msgid "Yes" msgstr "はい" @@ -8262,19 +8298,19 @@ msgstr "置換用のディレクトリが選択されていません" msgid "Replaced with 3D files from directory:\n" msgstr "ディレクトリの3Dファイルで置換しました:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ スキップ %s: 同一ファイル。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ スキップ %s: ファイルが存在しません。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ スキップ %s: 置換に失敗しました。\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ 置換しました %s。\n" @@ -9015,6 +9051,18 @@ msgstr "このオプションを有効にすると、複数のデバイスに同 msgid "Pop up to select filament grouping mode" msgstr "フィラメントグルーピングモード選択のポップアップ" +# AI Translated +msgid "Visible plugin pages" +msgstr "表示するプラグインページ数" + +# AI Translated +msgid "pages" +msgstr "ページ" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "固定タブとして表示するプラグインページの数です。残りのページは最後のタブのドロップダウンにまとめられます。" + msgid "Behaviour" msgstr "動作" @@ -9404,6 +9452,18 @@ msgstr "非対応のプリセットを表示" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "プリンターとフィラメントのドロップダウンリストに、互換性のない/非対応のプリセットを表示します。これらのプリセットは選択できません。" +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(実験的) プリントホストの代わりにプリンターエージェントを使用" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Bambu 以外のプリンターの印刷ジョブを、従来のプリントホストへのアップロードではなく、プリンターのプラグインエージェント経由で送信します。\n" +"無効の場合、OrcaSlicer は従来のプリントホストの動作を使用します。" + # AI Translated msgid "Experimental Features" msgstr "実験的機能" @@ -9672,9 +9732,25 @@ msgstr "ユーザープリセット" msgid "Preset Inside Project" msgstr "プロジェクト プリセット" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "親プリセットから継承したすべての値をこのプリセットにコピーし、親との継承関係を解除します。親プリセットとのみ互換性のあるプリセットは、サポートされなくなる場合があります。" + msgid "Detach from parent" msgstr "親から分離" +# AI Translated +msgid "Unique preset" +msgstr "独立したプリセット" + +# AI Translated +msgid "Parent preset" +msgstr "親プリセット" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "このプリセットは他のプリセットを継承していません。" + msgid "Name is unavailable." msgstr "名称は使用できません" @@ -10416,22 +10492,6 @@ msgstr "このオプションを有効にしてもよろしいですか?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "インフィルパターンは通常、適切な印刷と意図した効果を確保するために回転を自動的に処理するように設計されています(例: ジャイロイド、キュービック)。現在のスパースインフィルパターンを回転させると、サポートが不十分になる可能性があります。慎重に進め、潜在的な印刷問題を十分に確認してください。このオプションを有効にしてもよろしいですか?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"レイヤー高さが小さすぎます。\n" -"min_layer_heightに設定されます\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "レイヤー高さがプリンター設定 -> エクストルーダー -> レイヤー高さ制限の上限を超えています。印刷品質の問題が発生する可能性があります。" - -msgid "Adjust to the set range automatically?\n" -msgstr "設定範囲に自動調整しますか?\n" - -msgid "Adjust" -msgstr "調整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "実験的機能: フィラメント交換時により長い距離でフィラメントをリトラクト・カットしてフラッシュを最小化します。フラッシュを大幅に削減できますが、ノズル詰まりやその他の印刷問題のリスクが高まる可能性もあります。" @@ -10621,6 +10681,9 @@ msgstr "保留キーワードが見つかりました" msgid "Setting Overrides" msgstr "上書き設定" +msgid "Retraction when switching material" +msgstr "素材変更時のリトラクション" + msgid "Basic information" msgstr "基本情報" @@ -10751,6 +10814,12 @@ msgstr "互換性のあるプロセスプロファイル" msgid "Printable space" msgstr "造形可能領域" +msgid "Printer Agent" +msgstr "プリンターエージェント" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "プリンター通信用のネットワークエージェント実装を選択します。使用可能なエージェントは起動時に登録されます。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10877,9 +10946,6 @@ msgstr "積層ピッチの制限" msgid "Z-Hop" msgstr "Z-ホップ" -msgid "Retraction when switching material" -msgstr "素材変更時のリトラクション" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12258,6 +12324,10 @@ msgstr " は除外エリアに近すぎるため、衝突が発生します。\n msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " がクランピング検出エリアに近すぎ、衝突が発生します。\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " は造形可能領域から一部はみ出しているため、印刷できません。\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "選択したノズル温度に互換性がありません。各フィラメントのノズル温度は、他のフィラメントの推奨ノズル温度範囲内に収まる必要があります。そうでない場合、ノズル詰まりやプリンターの損傷が発生する可能性があります。" @@ -12599,9 +12669,6 @@ msgstr "G-codeの代わりに3MFを使用" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "プリンターが印刷ジョブとして3MFファイルを受け付ける場合に有効にします。有効にすると、Orca Slicerはスライス済みファイルを通常の.gcodeファイルではなく.gcode.3mfとして送信します。" -msgid "Printer Agent" -msgstr "プリンターエージェント" - msgid "Select the network agent implementation for printer communication." msgstr "プリンター通信用のネットワークエージェント実装を選択します。" @@ -13320,9 +13387,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "内部ブリッジの速度です。値を%で指定した場合、bridge_speedを基準に計算されます。デフォルト値は150%です。" -msgid "Brim width" -msgstr "ブリム幅" - msgid "This is the distance from the model to the outermost brim line." msgstr "一番外側のブリム線がモデルと距離です。" @@ -13411,6 +13475,14 @@ msgstr "" "鋭角を検出する前にジオメトリが間引かれます。このパラメータは、間引きにおける偏差の最小長さを指定します。\n" "0で無効になります。" +# AI Translated +msgid "Brim ears outer only" +msgstr "ブリムイヤーを外側の輪郭のみ" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "穴や閉じた部分を除き、モデルの外側の輪郭にのみマウスイヤーを生成します。" + msgid "upward compatible machine" msgstr "互換性のあるデバイス" @@ -14634,6 +14706,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "ジャイロイド" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "スパース インフィルの平滑化係数" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "スパース インフィルの角をどの程度丸めるかを設定します。0% では元の鋭い経路のまま、100% では隣接するインフィル線の間で可能な限り大きな曲線になります。" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "トップ面のインフィル加速度です。遅くすると表面の仕上がりが向上させることができます" @@ -15233,6 +15313,14 @@ msgstr "プリンターが対応するG-code" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code の設定ブロックを省略" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "CONFIG_BLOCK (スライサー設定のキーと値のペア) を G-code ファイルに書き込みません。これらのコメント行の解析でファームウェアがクラッシュするプリンター (例: Anycubic go-klipper) で役立ちます。注意: G-code ファイルにスライサー設定が含まれなくなるため、OrcaSlicer に読み込み直しても設定は復元されません。" + # AI Translated msgid "Pellet Modded Printer" msgstr "ペレット改造プリンター" @@ -16374,6 +16462,14 @@ msgstr "押出機切り替え時のロングリトラクション" msgid "Retraction distance when extruder change" msgstr "押出機切替時のリトラクション距離" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "リトラクション量 (ツール交換)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "ツール交換の前にリトラクションが行われるとき、指定した量だけフィラメントが引き戻されます (長さは押出機に入る前の未加工のフィラメントで測定されます)。" + # AI Translated msgid "Z-hop height" msgstr "Zホップの高さ" @@ -16488,6 +16584,10 @@ msgstr "再開時の追加長さ" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "移動後に引込みが補償されると、エクストルーダーはこの追加量のフィラメントを押し出します。 この設定はほとんど必要ありません。" +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "再開時の追加長さ (ツール交換)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "ツールの交換後に吸込み分が補正されると、エクストルーダーはこの追加量のフィラメントを押し出します。" @@ -16963,6 +17063,14 @@ msgstr "ワイプタワー上でツール交換" msgid "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." msgstr "ツール交換コマンド (Tx) を発行する前に、ツールヘッドを強制的にワイプタワーへ移動させます。タイプ2のワイプタワーを使用するマルチ押出機 (マルチツールヘッド) プリンターにのみ関係します。デフォルトでは、マルチツールヘッド機ではファームウェアがヘッドの交換を処理するためOrcaは移動をスキップしますが、その結果Txコマンドが造形物の上で発行される場合があります。ツール交換を常にワイプタワーの上で発行したい場合は、このオプションを有効にしてください。" +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "ワイプタワーで温度待機" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "印刷温度に達するのを待たずに新しいツールを取り付け、ワイプタワーへ移動し、パージ直前にそこで温度を待ちます。加熱中の垂れ出しはモデルではなくタワーに落ち、移動時間が加熱と重なります。タイプ 2 のワイプタワーを使用するマルチ押出機 (マルチツールヘッド) プリンターでのみ有効です。ファームウェアやツール交換マクロ側で温度待機を行わないようにしてください。無効の場合、温度待機はツール交換コマンドの直後に出力されます。" + # AI Translated msgid "No sparse layers (beta)" msgstr "スパース層なし (ベータ)" @@ -20389,9 +20497,6 @@ msgstr "実物プリンター" msgid "Print Host upload" msgstr "プリントホストのアップロード" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "プリンター通信用のネットワークエージェント実装を選択します。使用可能なエージェントは起動時に登録されます。" - # AI Translated msgid "Select a Flashforge printer" msgstr "Flashforgeプリンターを選択" @@ -21363,9 +21468,6 @@ msgstr "ログイン中に予期しない問題が発生しました。再試行 msgid "User canceled." msgstr "ユーザーがキャンセルしました。" -msgid "Head diameter" -msgstr "直径" - msgid "Max angle" msgstr "最大角度" @@ -22194,6 +22296,22 @@ msgstr "" "反りを避ける\n" "ABSのような反りやすい素材を印刷する場合、ヒートベッドの温度を適切に上げることで、反りが発生する確率を下げることができることをご存知ですか?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "レイヤー高さが小さすぎます。\n" +#~ "min_layer_heightに設定されます\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "レイヤー高さがプリンター設定 -> エクストルーダー -> レイヤー高さ制限の上限を超えています。印刷品質の問題が発生する可能性があります。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "設定範囲に自動調整しますか?\n" + +#~ msgid "Head diameter" +#~ msgstr "直径" + #~ msgid "Print order within a single layer." #~ msgstr "単一レイヤー内の印刷順序。" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 5a6ac0438b..767674e525 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz \n" "Language-Team: \n" @@ -4763,6 +4763,23 @@ msgstr "현재 챔버 온도가 재료의 안전 온도보다 높으므로 재 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "최소 챔버 온도(%d℃)가 목표 챔버 온도(%d℃)보다 높습니다. 최소값은 챔버가 목표 온도까지 계속 가열되는 동안 출력을 시작하는 기준값이므로 목표값을 초과해서는 안 됩니다. 이 값은 목표값으로 제한됩니다." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "레이어 높이가 너무 작습니다. 최솟값(%g mm)으로 설정됩니다." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "레이어 높이가 프린터 설정 -> 압출기 -> 레이어 높이 한도에서 설정한 범위를 벗어났습니다. 출력 품질 문제가 발생할 수 있습니다." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "한도(%g mm)에 맞게 자동으로 조정할까요?" + +msgid "Adjust" +msgstr "조정" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4884,6 +4901,13 @@ msgstr "" "예 - 아라크네 벽 생성기 활성화\n" "아니오 - 아라크네 벽 생성기 비활성화 및 퍼지 스킨 [변위] 모드 설정" +# AI Translated +msgid "Brim ear radius" +msgstr "브림 귀 반경" + +msgid "Brim width" +msgstr "브림 너비" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "나선형 모드는 벽 루프가 1이고, 서포트가 비활성화되고, 프로빙에 의한 클럼핑 감지가 비활성화되고, 상단 셸 레이어가 0이고, 희소 인필 밀도가 0이고 타임랩스 유형이 전통적인 경우에만 작동합니다." @@ -5138,6 +5162,14 @@ msgstr "교정 Gcode를 생성하지 못했습니다" msgid "Calibration error" msgstr "교정 오류" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "이 프린터에는 이 컨트롤에 필요한 하드웨어가 구성되어 있지 않습니다." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "이 컨트롤은 이 프린터에서 지원되지 않습니다." + # AI Translated msgid "Network unavailable" msgstr "네트워크를 사용할 수 없음" @@ -6001,7 +6033,7 @@ msgstr "용량:" msgid "Size:" msgstr "크기:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "레이어 %d, Z = %.2lf mm에서 Gcode 경로 충돌이 발견되었습니다. 충돌하는 객체를 더 멀리 분리하세요 (%s <-> %s)." @@ -6178,6 +6210,10 @@ msgstr "멀티 디바이스" msgid "Project" msgstr "프로젝트" +# AI Translated +msgid "Device (Web)" +msgstr "장치 (웹)" + msgid "Yes" msgstr "예" @@ -8288,22 +8324,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "다음 디렉터리의 3D 파일로 교체했습니다:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ 건너뜀 %s: 동일한 파일입니다.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ 건너뜀 %s: 파일이 존재하지 않습니다.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ 건너뜀 %s: 교체하지 못했습니다.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ %s을(를) 교체했습니다.\n" @@ -9077,6 +9113,18 @@ msgstr "활성화하면 여러 장치에 동시에 작업을 보내고 여러 msgid "Pop up to select filament grouping mode" msgstr "필라멘트 그룹화 모드를 선택하기 위한 팝업" +# AI Translated +msgid "Visible plugin pages" +msgstr "표시할 플러그인 페이지" + +# AI Translated +msgid "pages" +msgstr "페이지" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "고정 탭으로 표시되는 플러그인 페이지 수입니다. 나머지 페이지는 마지막 탭의 드롭다운으로 묶입니다." + # AI Translated msgid "Behaviour" msgstr "동작" @@ -9491,6 +9539,18 @@ msgstr "지원되지 않는 사전 설정 표시" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "프린터 및 필라멘트 드롭다운 목록에 호환되지 않거나 지원되지 않는 사전 설정을 표시합니다. 이러한 사전 설정은 선택할 수 없습니다." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(실험적) 출력 호스트 대신 프린터 에이전트 사용" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Bambu 이외의 프린터 출력 작업을 기존 출력 호스트 업로드 방식 대신 프린터 플러그인 에이전트를 통해 전달합니다.\n" +"비활성화하면 OrcaSlicer는 기존 출력 호스트 동작을 사용합니다." + # AI Translated msgid "Experimental Features" msgstr "실험적 기능" @@ -9762,10 +9822,26 @@ msgstr "사용자 사전 설정" msgid "Preset Inside Project" msgstr "프로젝트 내부 사전 설정" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "상위 사전 설정에서 상속한 모든 값을 이 사전 설정으로 복사하고 상속 관계를 제거합니다. 상위 사전 설정에서만 호환되는 사전 설정은 지원되지 않을 수 있습니다." + # AI Translated msgid "Detach from parent" msgstr "상위 항목에서 분리" +# AI Translated +msgid "Unique preset" +msgstr "독립 사전 설정" + +# AI Translated +msgid "Parent preset" +msgstr "상위 사전 설정" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "이 사전 설정은 다른 사전 설정을 상속하지 않습니다." + msgid "Name is unavailable." msgstr "이름을 사용할 수 없습니다." @@ -10519,22 +10595,6 @@ msgstr "이 옵션을 사용하시겠습니까?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "채우기 패턴은 일반적으로 올바른 출력과 의도한 효과를 위해 회전을 자동으로 처리하도록 설계되어 있습니다(예: 자이로이드, 큐빅). 현재 드문 채우기 패턴을 회전시키면 지지력이 부족해질 수 있습니다. 신중하게 진행하고 출력 문제가 발생하지 않는지 충분히 확인하십시오. 이 옵션을 활성화하시겠습니까?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"레이어 높이가 너무 작습니다.\n" -"min_layer_height로 설정됩니다.\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "레이어 높이가 프린터 설정 -> 압출기 -> 레이어의 제한을 초과합니다.높이 제한으로 인해 출력 품질 문제가 발생할 수 있습니다." - -msgid "Adjust to the set range automatically?\n" -msgstr "설정 범위에 자동으로 맞춰지나요?\n" - -msgid "Adjust" -msgstr "조정" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "실험적 기능: 플러시를 최소화하기 위해 필라멘트 교체 중에 더 먼 거리에서 필라멘트를 집어넣고 절단합니다. 플러시를 눈에 띄게 줄일 수 있지만 노즐 막힘이나 기타 출력 문제의 위험이 높아질 수도 있습니다." @@ -10728,6 +10788,9 @@ msgstr "예약어를 찾았습니다" msgid "Setting Overrides" msgstr "설정 덮어쓰기" +msgid "Retraction when switching material" +msgstr "재료 전환 시 후퇴" + msgid "Basic information" msgstr "기본 정보" @@ -10861,6 +10924,14 @@ msgstr "호환 프로세스 사전설정" msgid "Printable space" msgstr "출력 가능 공간" +# AI Translated +msgid "Printer Agent" +msgstr "프린터 에이전트" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다. 사용 가능한 에이전트는 시작 시 등록됩니다." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10993,9 +11064,6 @@ msgstr "레이어 높이 한도" msgid "Z-Hop" msgstr "Z올리기" -msgid "Retraction when switching material" -msgstr "재료 전환 시 후퇴" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12388,6 +12456,10 @@ msgstr " 이(가) 제외 영역에 너무 가깝습니다. 출력 시 충돌이 msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " 뭉침 감지 영역에 너무 가까워 충돌이 발생할 수 있습니다.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " 이(가) 출력 가능 영역을 일부 벗어나 출력할 수 없습니다.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "선택한 노즐 온도가 서로 호환되지 않습니다. 각 필라멘트의 노즐 온도는 다른 필라멘트의 권장 노즐 온도 범위 안에 있어야 합니다. 그렇지 않으면 노즐 막힘이나 프린터 손상이 발생할 수 있습니다." @@ -12732,10 +12804,6 @@ msgstr "G-code 대신 3MF 사용" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "프린터가 출력 작업으로 3MF 파일을 허용하는 경우 이 옵션을 활성화하십시오. 활성화하면 Orca Slicer가 슬라이스된 파일을 일반 .gcode 파일 대신 .gcode.3mf로 전송합니다." -# AI Translated -msgid "Printer Agent" -msgstr "프린터 에이전트" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다." @@ -13446,9 +13514,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "내부 브릿지의 속도. 값을 백분율로 표현하면 bridge_speed를 기준으로 계산됩니다. 기본값은 150%입니다." -msgid "Brim width" -msgstr "브림 너비" - msgid "This is the distance from the model to the outermost brim line." msgstr "모델과 가장 바깥쪽 브림 선까지의 거리" @@ -13533,6 +13598,14 @@ msgstr "" "날카로운 각도를 감지하기 전에 형상이 무시됩니다. 이 매개변수는 무시하는 형상의 최소 길이를 나타냅니다.\n" "0으로 비활성화합니다" +# AI Translated +msgid "Brim ears outer only" +msgstr "브림 귀를 바깥쪽에만" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "구멍과 닫힌 영역을 제외하고 모델의 바깥쪽 윤곽에만 생쥐 귀를 생성합니다." + msgid "upward compatible machine" msgstr "상향 호환 장치" @@ -14729,6 +14802,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "자이로이드" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "드문 채우기 부드러움 계수" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "드문 채우기의 모서리를 얼마나 둥글게 할지 조절합니다. 0%는 원래의 날카로운 경로를 유지하고, 100%는 인접한 채우기 선 사이에 가능한 가장 큰 곡선을 만듭니다." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "상단 표면 가속도. 낮은 값을 사용하면 상단 표면 품질이 향상될 수 있습니다" @@ -15291,6 +15372,14 @@ msgstr "프린터와 호환되는 Gcode 종류" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code 설정 블록 생략" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "CONFIG_BLOCK(슬라이서 설정의 키/값 쌍)을 G-code 파일에 기록하지 않습니다. 이 주석 줄을 해석할 때 펌웨어가 중단되는 프린터(예: Anycubic go-klipper)에 도움이 될 수 있습니다. 참고: G-code 파일에 슬라이서 설정이 더 이상 포함되지 않으므로, 이 파일을 OrcaSlicer로 다시 가져와도 설정이 복원되지 않습니다." + msgid "Pellet Modded Printer" msgstr "펠릿 프린터" @@ -16400,6 +16489,14 @@ msgstr "압출기 교체 시 긴 수축" msgid "Retraction distance when extruder change" msgstr "압출기 교체 시 수축 거리" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "후퇴 길이 (툴 체인지)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "툴 체인지 전에 후퇴가 실행되면 지정한 양만큼 필라멘트가 뒤로 당겨집니다 (길이는 압출기에 들어가기 전의 원래 필라멘트를 기준으로 측정됩니다)." + msgid "Z-hop height" msgstr "Z올리기 높이" @@ -16498,6 +16595,10 @@ msgstr "재 시작 시 추가 길이" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "이동 후 후퇴가 보상되면 압출기는 이 추가 양의 필라멘트를 밀어냅니다. 이 설정은 거의 필요하지 않습니다." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "재 시작 시 추가 길이 (툴 체인지)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "툴 체인지 후 후퇴가 보상되면 압출기는 이 추가 양의 필라멘트를 밀어냅니다." @@ -16922,6 +17023,14 @@ msgstr "프라임 타워에서 툴 체인지" msgid "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." msgstr "툴 체인지 명령(Tx)을 실행하기 전에 툴헤드가 반드시 프라임 타워로 이동하도록 합니다. 유형 2 프라임 타워를 사용하는 다중 압출기(멀티 툴헤드) 프린터에만 해당됩니다. 기본적으로 Orca는 멀티 툴헤드 장비에서 펌웨어가 헤드 교체를 처리하므로 이동을 생략하는데, 이 때문에 Tx 명령이 출력물 위에서 실행될 수 있습니다. 툴 체인지가 항상 프라임 타워 위에서 실행되도록 하려면 이 옵션을 활성화하십시오." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "프라임 타워에서 온도 대기" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "출력 온도에 도달할 때까지 기다리지 않고 새 툴을 집은 뒤 프라임 타워로 이동하여, 퍼지 직전에 그곳에서 온도를 기다립니다. 가열 중 흘러나온 재료는 모델이 아닌 타워에 떨어지고, 이동 시간이 가열 시간과 겹칩니다. 타입 2 프라임 타워를 사용하는 다중 압출기(다중 툴헤드) 프린터에만 해당합니다. 펌웨어나 툴 체인지 매크로가 직접 온도를 기다려서는 안 됩니다. 비활성화하면 툴 체인지 명령 직후에 온도 대기가 실행됩니다." + msgid "No sparse layers (beta)" msgstr "희소 레이어 없음(베타)" @@ -20261,10 +20370,6 @@ msgstr "물리 프린터" msgid "Print Host upload" msgstr "출력 호스트 업로드" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다. 사용 가능한 에이전트는 시작 시 등록됩니다." - # AI Translated msgid "Select a Flashforge printer" msgstr "Flashforge 프린터 선택" @@ -21217,9 +21322,6 @@ msgstr "로그인을 시도하는 동안 예기치 않은 문제가 발생했습 msgid "User canceled." msgstr "사용자가 취소했습니다." -msgid "Head diameter" -msgstr "헤드 직경" - msgid "Max angle" msgstr "최대 각도" @@ -22057,6 +22159,22 @@ msgstr "" "뒤틀림 방지\n" "ABS와 같이 뒤틀림이 발생하기 쉬운 소재를 출력할 때, 히트베드 온도를 적절하게 높이면 뒤틀림 가능성을 줄일 수 있다는 사실을 알고 계셨나요?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "레이어 높이가 너무 작습니다.\n" +#~ "min_layer_height로 설정됩니다.\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "레이어 높이가 프린터 설정 -> 압출기 -> 레이어의 제한을 초과합니다.높이 제한으로 인해 출력 품질 문제가 발생할 수 있습니다." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "설정 범위에 자동으로 맞춰지나요?\n" + +#~ msgid "Head diameter" +#~ msgstr "헤드 직경" + #~ msgid "Print order within a single layer." #~ msgstr "단일 레이어 내의 출력 순서" diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 9a6b7ae590..ad5edffc9a 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas \n" "Language-Team: \n" @@ -4728,6 +4728,23 @@ msgstr "Dabartinė kameros temperatūra yra aukštesnė už saugią medžiagos t msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimali kameros temperatūra (%d℃) yra aukštesnė nei tikslinė kameros temperatūra (%d℃). Minimali vertė yra slenkstis, kurį pasiekus pradedamas spausdinimas, kol kamera vis dar kaitinama iki tikslinės temperatūros, todėl ji neturėtų viršyti tikslinės. Vertė bus apribota iki tikslinės temperatūros." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Sluoksnio aukštis per mažas. Jis bus nustatytas į mažiausią reikšmę (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Sluoksnio aukštis yra už ribų, nurodytų Spausdintuvo nustatymai -> Ekstruderis -> Sluoksnio aukščio ribos, tai gali sukelti spausdinimo kokybės problemų." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Automatiškai sureguliuoti iki ribos (%g mm)?" + +msgid "Adjust" +msgstr "Sureguliuoti" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4847,6 +4864,13 @@ msgstr "" "Taip – įjungti „Arachne“ sienelių generatorių\n" "Ne – išjungti „Arachne“ sienelių generatorių ir nustatyti „Šiurkštaus paviršius“ režimą [Slinktis]" +# AI Translated +msgid "Brim ear radius" +msgstr "Apvado „ausies“ spindulys" + +msgid "Brim width" +msgstr "Pado apvado plotis" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiralinis režimas veikia tik tada, kai sienelės kilpų skaičius yra 1, atramos išjungtos, sulipimo aptikimas zonduojant išjungtas, viršutinių apvalkalo sluoksnių yra 0, reto užpildo tankis yra 0 %, o laiko intervalų vaizdo įrašo tipas – tradicinis." @@ -5101,6 +5125,14 @@ msgstr "Nepavyko sugeneruoti kalibravimo G-kodo" msgid "Calibration error" msgstr "Kalibravimo klaida" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Šiame spausdintuve nėra sukonfigūruotos įrangos, kurios reikia šiam valdikliui." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Šis valdiklis šiame spausdintuve nepalaikomas." + # AI Translated msgid "Network unavailable" msgstr "Tinklas neprieinamas" @@ -5961,7 +5993,7 @@ msgstr "Tūris:" msgid "Size:" msgstr "Dydis:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Rasta G-kodo trajektorijų konfliktų %d sluoksnyje, Z = %.2lfmm. Prašome labiau atskirti konfliktuojančius objektus (%s <-> %s)." @@ -6142,6 +6174,10 @@ msgstr "Kelių įrenginių valdymas (Multi-device)" msgid "Project" msgstr "Projektas" +# AI Translated +msgid "Device (Web)" +msgstr "Įrenginys (Web)" + msgid "Yes" msgstr "Taip" @@ -8239,19 +8275,19 @@ msgstr "" "Pakeista 3D failais iš katalogo:\n" "\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Praleistas %s: tas pats failas.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Praleistas %s: failas neegzistuoja.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Praleistas %s: nepavyko pakeisti.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Pakeistas %s.\n" @@ -8977,6 +9013,18 @@ msgstr "Kai įjungta ši funkcija, jūs galite siųsti užduotį keliems įrengi msgid "Pop up to select filament grouping mode" msgstr "Iššokantis langas gijų grupavimo režimui pasirinkti" +# AI Translated +msgid "Visible plugin pages" +msgstr "Matomi papildinių puslapiai" + +# AI Translated +msgid "pages" +msgstr "puslapiai" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Papildinių puslapių, rodomų kaip fiksuotos kortelės, skaičius; likę puslapiai sutraukiami į išskleidžiamąjį sąrašą paskutinėje kortelėje." + msgid "Behaviour" msgstr "Elgsena" @@ -9329,6 +9377,18 @@ msgstr "Rodyti nepalaikomus profilius" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Rodyti nesuderinamus / nepalaikomus profilius spausdintuvų ir gijų išskleidžiamuosiuose sąrašuose. Šių profilių pasirinkti negalima." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Eksperimentinė) Naudoti spausdintuvo agentus vietoj spausdinimo serverių" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Nukreipia ne Bambu spausdintuvų spausdinimo užduotis per spausdintuvo papildinių agentus, o ne per klasikinį įkėlimo į spausdinimo serverį srautą.\n" +"Kai išjungta, OrcaSlicer naudoja senąjį spausdinimo serverio veikimą." + msgid "Experimental Features" msgstr "Eksperimentinis" @@ -9590,9 +9650,25 @@ msgstr "Naudotojo profilis" msgid "Preset Inside Project" msgstr "Profilis projekto viduje" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Nukopijuoja į šį profilį visas iš pirminio profilio paveldėtas reikšmes ir pašalina paveldėjimo ryšį. Profiliai, suderinami tik su pirminiu profiliu, gali tapti nepalaikomi." + msgid "Detach from parent" msgstr "Atskirti nuo tėvinio profilio" +# AI Translated +msgid "Unique preset" +msgstr "Savarankiškas profilis" + +# AI Translated +msgid "Parent preset" +msgstr "Pirminis profilis" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Šis profilis nepaveldi iš kito profilio." + msgid "Name is unavailable." msgstr "Nėra pavadinimo." @@ -10330,24 +10406,6 @@ msgstr "Ar tikrai norite įjungti šią parinktį?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Užpildymo modeliai paprastai yra suprojektuoti taip, kad automatiškai tvarkytų sukimąsi, siekiant užtikrinti tinkamą spausdinimą ir pasiekti numatytus efektus (pvz., Gyroid, Cubic). Sukant esamą retą užpildymo modelį, gali atsirasti nepakankamas atraminis paviršius. Prašome elgtis atsargiai ir atidžiai patikrinti, ar nėra galimų spausdinimo problemų. Ar tikrai norite įjungti šią parinktį?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Per mažas sluoksnio aukštis.\n" -"Jis bus nustatytas į min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Sluoksnio aukštis viršija ribą, nurodytą Spausdintuvo nustatymai -> Ekstruderis -> Sluoksnio aukščio ribos, tai gali sukelti spausdinimo kokybės problemų." - -msgid "Adjust to the set range automatically?\n" -msgstr "" -"Sureguliuoti pagal nustatytą diapazoną automatiškai?\n" -"\n" - -msgid "Adjust" -msgstr "Sureguliuoti" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Eksperimentinė funkcija: gijos įtraukimas ir nukirpimas didesniu atstumu keičiant giją, siekiant sumažinti išvalymą (flush). Nors tai gali pastebimai sumažinti išvalymą, taip pat gali padidėti purkštuko užsikimšimo ar kitų spausdinimo komplikacijų rizika." @@ -10547,6 +10605,9 @@ msgstr "Rasti rezervuoti raktažodžiai" msgid "Setting Overrides" msgstr "Nustatymų perrašymas" +msgid "Retraction when switching material" +msgstr "Įtraukimas keičiant medžiagą" + msgid "Basic information" msgstr "Pagrindinė informacija" @@ -10673,6 +10734,12 @@ msgstr "Suderinami apdorojimo profiliai" msgid "Printable space" msgstr "Erdvė spausdinimui" +msgid "Printer Agent" +msgstr "Spausdintuvo agentas" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti. Prieinami agentai užregistruojami paleidimo metu." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10798,9 +10865,6 @@ msgstr "Sluoksnio aukščio ribos" msgid "Z-Hop" msgstr "Z šuolis" -msgid "Retraction when switching material" -msgstr "Įtraukimas keičiant medžiagą" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12146,6 +12210,10 @@ msgstr "" " yra per arti sulipimo aptikimo zonos, todėl įvyks susidūrimai.\n" "\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " yra iš dalies už spausdinimo srities ribų ir negali būti atspausdintas.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Pasirinktos purkštuko temperatūros yra nesuderinamos. Kiekvienos gijos purkštuko temperatūra turi patekti į kitų gijų rekomenduojamos temperatūros diapazoną. Priešingu atveju gali užsikimšti purkštukas arba sugesti spausdintuvas." @@ -12459,9 +12527,6 @@ msgstr "Vietoj G-kodo naudoti 3MF" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Įjunkite, jei spausdintuvas spausdinimo užduotims priima 3MF failus. Kai įjungta, „Orca Slicer“ sugeneruotą failą siunčia kaip „.gcode.3mf“, o ne kaip paprastą „.gcode“ failą." -msgid "Printer Agent" -msgstr "Spausdintuvo agentas" - msgid "Select the network agent implementation for printer communication." msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti." @@ -13134,9 +13199,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Vidinių tiltelių spausdinimo greitis. Jei reikšmė nurodoma procentais, ji apskaičiuojama pagal „bridge_speed“ (tiltelių greitį). Numatytoji reikšmė – 150 %." -msgid "Brim width" -msgstr "Pado apvado plotis" - msgid "This is the distance from the model to the outermost brim line." msgstr "Atstumas nuo modelio iki išorinės krašto linijos" @@ -13217,6 +13279,14 @@ msgstr "" "Prieš aptinkant aštrius kampus, geometrija yra supaprastinama (decimuojama). Šis parametras nurodo minimalų nuokrypio ilgį supaprastinimui atlikti.\n" "Įrašykite 0, kad išjungtumėte." +# AI Translated +msgid "Brim ears outer only" +msgstr "Apvado „ausys“ tik išorėje" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Kuria peliukų ausis tik ant išorinio modelio kontūro, praleidžiant skyles ir uždaras sritis." + msgid "upward compatible machine" msgstr "atgaliniu būdu suderinamas įrenginys" @@ -14370,6 +14440,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroidas" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Reto užpildo glotninimo koeficientas" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Nustato, kaip stipriai suapvalinami reto užpildo kampai. 0% palieka pradinę aštrią trajektoriją, o 100% sukuria didžiausias įmanomas kreives tarp gretimų užpildo linijų." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Viršutinio paviršiaus užpildo pagreitis. Naudojant mažesnę vertę gali pagerėti viršutinio paviršiaus kokybė." @@ -14914,6 +14992,14 @@ msgstr "Su kokiu G kodu suderinamas spausdintuvas." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Praleisti G-code konfigūracijos bloką" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Neįrašo CONFIG_BLOCK (pjaustyklės konfigūracijos raktų ir reikšmių porų) į G-code failą. Tai gali padėti su spausdintuvais, kurių programinė įranga stringa apdorodama šias komentarų eilutes (pvz., Anycubic go-klipper). Pastaba: G-code faile nebeliks pjaustyklės nustatymų, todėl importavus jį atgal į OrcaSlicer konfigūracija nebus atkurta." + msgid "Pellet Modded Printer" msgstr "Modifikuotas granulinis spausdintuvas" @@ -15955,6 +16041,14 @@ msgstr "Ilgas įtraukimas keičiant ekstruderį" msgid "Retraction distance when extruder change" msgstr "Įtraukimo atstumas keičiant ekstruderį" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Atitraukimo ilgis (Įrankio keitimas)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Kai atitraukimas suaktyvinamas prieš įrankio keitimą, gija atitraukiama nurodytu atstumu (ilgis matuojamas ant neapdorotos gijos, prieš jai patenkant į ekstruderį)." + msgid "Z-hop height" msgstr "„Z-hop“ (pakėlimo) aukštis" @@ -16049,6 +16143,10 @@ msgstr "Papildomas ilgis po sugrąžinimo" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Kai po judėjimo kompensuojamas gijos įtraukimas, ekstruderis papildomai išstums šį gijos kiekį. Šis nustatymas reikalingas retai." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Papildomas ilgis po sugrąžinimo (Įrankio keitimas)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Kai po įrankio pakeitimo kompensuojamas gijos įtraukimas, ekstruderis papildomai išstums šį gijos kiekį." @@ -16461,6 +16559,14 @@ msgstr "Įrankio keitimas virš valymo bokšto" msgid "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." msgstr "Priverstinai nukreipti spausdinimo galvutę prie valymo bokšto prieš vykdant įrankio keitimo komandą (Tx). Aktualu tik spausdintuvams su keliais ekstruderiais (keliomis galvutėmis), naudojantiems 2 tipo valymo bokštą. Pagal numatytuosius nustatymus „OrcaSlicer“ praleidžia šį judesį kelių galvučių įrenginiuose, nes galvučių sukeitimą valdo aparatinė programinė įranga, todėl Tx komanda gali būti įvykdyta virš spausdinamos detalės. Įjunkite šią parinktį, jei norite, kad įrankio keitimas visada vyktų virš valymo bokšto." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Laukti temperatūros ant valymo bokšto" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Paima naują įrankį nelaukdamas, kol jis pasieks spausdinimo temperatūrą, nuvažiuoja prie valymo bokšto ir ten laukia temperatūros, prieš pat pravalymą. Kaitinant ištekėjusi medžiaga patenka ant bokšto, o ne ant modelio, o pervažiavimas persidengia su kaitinimu. Aktualu tik daugiaekstruderiams (kelių spausdinimo galvučių) spausdintuvams, naudojantiems 2 tipo valymo bokštą. Programinė įranga ar įrankio keitimo makrokomanda neturi pati laukti temperatūros. Kai išjungta, laukimo temperatūros komanda pateikiama iškart po įrankio keitimo komandos." + msgid "No sparse layers (beta)" msgstr "Nėra retų sluoksnių (beta)" @@ -19702,9 +19808,6 @@ msgstr "Fizinis spausdintuvas" msgid "Print Host upload" msgstr "Įkėlimas spausdinimui tinkle" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti. Prieinami agentai užregistruojami paleidimo metu." - msgid "Select a Flashforge printer" msgstr "Pasirinkite „Flashforge“ spausdintuvą" @@ -20552,9 +20655,6 @@ msgstr "Bandant prisijungti įvyko kažkas netikėto. Bandykite dar kartą." msgid "User canceled." msgstr "Vartotojas atšaukė." -msgid "Head diameter" -msgstr "Galvutės skersmuo" - msgid "Max angle" msgstr "Maksimalus kampas" @@ -21336,6 +21436,24 @@ msgstr "" "Venkite deformacijų (warping)\n" "Ar žinojote, kad spausdinant medžiagas, kurios yra linkusios trauktis ir riestis (pvz., ABS), tinkamas kaitinamojo pagrindo temperatūros padidinimas gali sumažinti deformacijų (warping) tikimybę?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Per mažas sluoksnio aukštis.\n" +#~ "Jis bus nustatytas į min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Sluoksnio aukštis viršija ribą, nurodytą Spausdintuvo nustatymai -> Ekstruderis -> Sluoksnio aukščio ribos, tai gali sukelti spausdinimo kokybės problemų." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "" +#~ "Sureguliuoti pagal nustatytą diapazoną automatiškai?\n" +#~ "\n" + +#~ msgid "Head diameter" +#~ msgstr "Galvutės skersmuo" + #~ msgid "Print order within a single layer." #~ msgstr "Elementų spausdinimo eiliškumas vieno sluoksnio ribose." diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 1ae53b2888..28ad63d455 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -5150,6 +5150,23 @@ msgstr "De huidige kamertemperatuur is hoger dan de veilige temperatuur van het msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "De minimale kamertemperatuur (%d℃) is hoger dan de doelkamertemperatuur (%d℃). De minimale waarde is de drempel waarbij het printen start terwijl de kamer verder opwarmt naar de doelwaarde; deze mag die dus niet overschrijden. De waarde wordt begrensd tot de doelwaarde." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "De laaghoogte is te klein. Deze wordt ingesteld op het minimum (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "De laaghoogte valt buiten de limieten die zijn ingesteld in Printerinstellingen -> Extruder -> Laaghoogtelimieten, dit kan problemen met de afdrukkwaliteit veroorzaken." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Automatisch aanpassen naar de limiet (%g mm)?" + +msgid "Adjust" +msgstr "Aanpassen" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5277,6 +5294,13 @@ msgstr "" "Ja - Arachne-wandgenerator inschakelen\n" "Nee - Arachne-wandgenerator uitschakelen en de modus [Displacement] van Vage buitenkant instellen" +# AI Translated +msgid "Brim ear radius" +msgstr "Straal van randoren" + +msgid "Brim width" +msgstr "Rand breedte" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "De spiraalmodus werkt alleen wanneer Wanden 1 is, ondersteuning is uitgeschakeld, klontdetectie via aftasten is uitgeschakeld, het aantal bovenste buitenlagen 0 is, de dichtheid van de dunne vulling (infill) 0 is en het timelapse-type traditioneel is." @@ -5582,6 +5606,14 @@ msgstr "Cali G-code niet gegenereerd" msgid "Calibration error" msgstr "Kalibratiefout" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Deze printer beschikt niet over de hardware die dit besturingselement nodig heeft." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Dit besturingselement wordt niet ondersteund op deze printer." + # AI Translated msgid "Network unavailable" msgstr "Netwerk niet beschikbaar" @@ -6513,7 +6545,7 @@ msgid "Size:" msgstr "Maat:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Er zijn conflicten tussen G-code-paden gevonden op laag %d, Z = %.2lfmm. Plaats de conflicterende objecten verder uit elkaar (%s <-> %s)." @@ -6714,6 +6746,10 @@ msgstr "Meerdere apparaten" msgid "Project" msgstr "Project" +# AI Translated +msgid "Device (Web)" +msgstr "Apparaat (Web)" + msgid "Yes" msgstr "Ja" @@ -8999,22 +9035,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Vervangen door 3D-bestanden uit de map:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Overgeslagen %s: hetzelfde bestand.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Overgeslagen %s: bestand bestaat niet.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Overgeslagen %s: vervangen is mislukt.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Vervangen %s.\n" @@ -9827,6 +9863,18 @@ msgstr "Met deze optie ingeschakeld kunt u een taak tegelijkertijd naar meerdere msgid "Pop up to select filament grouping mode" msgstr "Pop-up om de filamentgroeperingsmodus te kiezen" +# AI Translated +msgid "Visible plugin pages" +msgstr "Zichtbare plug-inpagina's" + +# AI Translated +msgid "pages" +msgstr "pagina's" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Aantal plug-inpagina's dat als vaste tabbladen wordt getoond voordat de overige pagina's worden samengevouwen in een vervolgkeuzelijst op het laatste tabblad." + msgid "Behaviour" msgstr "Gedrag" @@ -10243,6 +10291,18 @@ msgstr "Niet-ondersteunde voorinstellingen tonen" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Toon incompatibele/niet-ondersteunde voorinstellingen in de keuzelijsten voor printer en filament. Deze voorinstellingen kunnen niet worden geselecteerd." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimenteel) Printeragents gebruiken in plaats van printhosts" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Stuurt printtaken voor niet-Bambu-printers via printer-plug-inagents in plaats van via de klassieke uploadstroom naar de printhost.\n" +"Wanneer dit is uitgeschakeld, gebruikt OrcaSlicer het oude printhostgedrag." + # AI Translated msgid "Experimental Features" msgstr "Experimentele functies" @@ -10523,10 +10583,26 @@ msgstr "Gebruikersvoorinstelling" msgid "Preset Inside Project" msgstr "Voorinstelling binnen project" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Kopieert alle overgeërfde waarden van de bovenliggende voorinstelling naar deze voorinstelling en verwijdert de overervingsrelatie. Voorinstellingen die alleen met de bovenliggende voorinstelling compatibel zijn, kunnen daardoor niet meer worden ondersteund." + # AI Translated msgid "Detach from parent" msgstr "Losmaken van bovenliggend element" +# AI Translated +msgid "Unique preset" +msgstr "Unieke voorinstelling" + +# AI Translated +msgid "Parent preset" +msgstr "Bovenliggende voorinstelling" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Deze voorinstelling erft niet van een andere voorinstelling." + msgid "Name is unavailable." msgstr "Naam is niet beschikbaar." @@ -11336,22 +11412,6 @@ msgstr "Weet u zeker dat u deze optie wilt inschakelen?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Vulpatronen zijn doorgaans ontworpen om rotatie automatisch af te handelen, zodat ze goed printen en hun beoogde effect bereiken (bijv. Gyroide, Kubisch). Het roteren van het huidige patroon voor de dunne vulling (infill) kan tot onvoldoende ondersteuning leiden. Ga voorzichtig te werk en controleer grondig op mogelijke printproblemen. Weet u zeker dat u deze optie wilt inschakelen?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Laaghoogte is te klein.\n" -"Het zal worden ingesteld op min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "De laaghoogte overschrijdt de limiet in Printerinstellingen -> Extruder -> Laaghoogtelimieten, dit kan problemen met de afdrukkwaliteit veroorzaken." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automatisch aanpassen aan het ingestelde bereik?\n" - -msgid "Adjust" -msgstr "Aanpassen" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentele functie: Het filament op grotere afstand terugtrekken en afsnijden tijdens filamentwisselingen om flush te minimaliseren. Hoewel het het doorspoelen aanzienlijk kan verminderen, kan het ook het risico op een verstopt mondstuk of andere printcomplicaties vergroten." @@ -11551,6 +11611,9 @@ msgstr "Gereserveerde zoekworden gevonden" msgid "Setting Overrides" msgstr "Overschrijvingen instellen" +msgid "Retraction when switching material" +msgstr "Terugtrekken (retraction) bij het wisselen van filament" + msgid "Basic information" msgstr "Basisinformatie" @@ -11689,6 +11752,14 @@ msgstr "Geschikte proces profielen" msgid "Printable space" msgstr "Ruimte waarbinnen geprint kan worden" +# AI Translated +msgid "Printer Agent" +msgstr "Printeragent" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer. Beschikbare agenten worden bij het opstarten geregistreerd." + # AI Translated #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format @@ -11829,9 +11900,6 @@ msgstr "Limieten voor laaghoogte" msgid "Z-Hop" msgstr "Z-hop" -msgid "Retraction when switching material" -msgstr "Terugtrekken (retraction) bij het wisselen van filament" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -13323,6 +13391,10 @@ msgstr " bevindt zich te dicht bij het uitsluitingsgebied en er zullen botsingen msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ligt te dicht bij het gebied voor klontdetectie, waardoor er botsingen zullen ontstaan.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " ligt gedeeltelijk buiten het printbare gebied en kan niet worden geprint.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "De geselecteerde mondstuktemperaturen zijn niet compatibel. De mondstuktemperatuur van elk filament moet binnen het aanbevolen mondstuktemperatuurbereik van de andere filamenten vallen. Anders kan het mondstuk verstopt raken of kan de printer beschadigd raken." @@ -13686,10 +13758,6 @@ msgstr "3MF gebruiken in plaats van G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Schakel dit in als de printer een 3MF-bestand als printopdracht accepteert. Indien ingeschakeld verzendt Orca Slicer het geslicede bestand als een .gcode.3mf in plaats van als een gewoon .gcode-bestand." -# AI Translated -msgid "Printer Agent" -msgstr "Printeragent" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer." @@ -14443,9 +14511,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Snelheid van interne bruggen. Als de waarde als percentage wordt uitgedrukt, wordt deze berekend op basis van bridge_speed. De standaardwaarde is 150%." -msgid "Brim width" -msgstr "Rand breedte" - msgid "This is the distance from the model to the outermost brim line." msgstr "Dit is de afstand van het model tot de buitenste randlijn." @@ -14537,6 +14602,14 @@ msgstr "" "De geometrie wordt vereenvoudigd voordat scherpe hoeken worden gedetecteerd. Deze parameter geeft de minimale lengte van de afwijking voor die vereenvoudiging aan.\n" "0 om uit te schakelen." +# AI Translated +msgid "Brim ears outer only" +msgstr "Randoren alleen aan de buitenzijde" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genereert alleen muisoren op de buitencontour van het model, met uitsluiting van gaten en gesloten secties." + msgid "upward compatible machine" msgstr "opwaarts compatibele machine" @@ -15846,6 +15919,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Afvlakkingsfactor voor dunne vulling" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Bepaalt hoe sterk de hoeken van de dunne vulling worden afgerond. 0% behoudt het oorspronkelijke scherpe pad, terwijl 100% de grootst mogelijke bochten tussen aangrenzende vullijnen oplevert." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Versnelling van de topoppervlakte-invulling. Gebruik van een lagere waarde kan de kwaliteit van de bovenlaag verbeteren." @@ -16456,6 +16537,14 @@ msgstr "Het type G-code waarmee de printer compatibel is." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code-configuratieblok overslaan" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Schrijft het CONFIG_BLOCK (de sleutel/waarde-paren van de slicerconfiguratie) niet naar het G-code-bestand. Dit kan helpen bij printers waarvan de firmware vastloopt bij het verwerken van deze commentaarregels (bijv. Anycubic go-klipper). Let op: het G-code-bestand bevat dan geen slicerinstellingen meer, dus door het weer in OrcaSlicer te importeren wordt de configuratie niet hersteld." + # AI Translated msgid "Pellet Modded Printer" msgstr "Printer omgebouwd voor pellets" @@ -17653,6 +17742,14 @@ msgstr "Lange terugtrekking bij extruderwissel" msgid "Retraction distance when extruder change" msgstr "Terugtrekafstand bij extruderwissel" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Terugtreklengte (Gereedschapswissel)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Wanneer het terugtrekken vóór een gereedschapswissel wordt geactiveerd, wordt het filament met de opgegeven hoeveelheid teruggetrokken (de lengte wordt gemeten op het onbewerkte filament, voordat het de extruder ingaat)." + # AI Translated msgid "Z-hop height" msgstr "Z-hop-hoogte" @@ -17763,6 +17860,10 @@ msgstr "Extra lengte bij herstart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Als retracten wordt gecompenseerd na een beweging, wordt deze extra hoeveelheid filament geëxtrudeerd. Deze instelling is zelden van toepassing." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Extra lengte bij herstart (Gereedschapswissel)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Als retracten wordt gecompenseerd na een toolwisseling, wordt deze extra hoeveelheid filament geëxtrudeerd." @@ -18255,6 +18356,14 @@ msgstr "Toolwissel op het afveegblok" msgid "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." msgstr "Dwing de printkop naar het afveegblok te bewegen voordat de opdracht voor de toolwissel (Tx) wordt gegeven. Alleen relevant voor printers met meerdere extruders (meerdere printkoppen) die een afveegblok van type 2 gebruiken. Standaard slaat Orca deze verplaatsing op machines met meerdere printkoppen over, omdat de firmware de kopwissel afhandelt, waardoor de Tx-opdracht boven het geprinte onderdeel kan worden gegeven. Schakel deze optie in als u wilt dat de toolwissel altijd boven het afveegblok wordt uitgevoerd." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Wachten op temperatuur bij het afveegblok" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Pakt het nieuwe gereedschap op zonder te wachten tot het de printtemperatuur bereikt, verplaatst zich naar het afveegblok en wacht daar op de temperatuur, vlak voor het spoelen. Het materiaal dat tijdens het opwarmen uitloopt komt op het blok terecht in plaats van op het model, en de verplaatsing overlapt met het opwarmen. Alleen relevant voor printers met meerdere extruders (meerdere printkoppen) die een afveegblok van type 2 gebruiken. De firmware of de gereedschapswisselmacro mag niet zelf op de temperatuur wachten. Wanneer dit is uitgeschakeld, wordt het wachten op de temperatuur direct na het gereedschapswisselcommando uitgevoerd." + # AI Translated msgid "No sparse layers (beta)" msgstr "Geen dunne lagen (bèta)" @@ -21860,10 +21969,6 @@ msgstr "Fysieke printer" msgid "Print Host upload" msgstr "Host-upload afdrukken" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer. Beschikbare agenten worden bij het opstarten geregistreerd." - # AI Translated msgid "Select a Flashforge printer" msgstr "Selecteer een Flashforge-printer" @@ -22918,9 +23023,6 @@ msgstr "Er is iets onverwachts gebeurd bij het inloggen. Probeer het opnieuw." msgid "User canceled." msgstr "Gebruiker geannuleerd." -msgid "Head diameter" -msgstr "Kopdiameter" - # AI Translated msgid "Max angle" msgstr "Maximale hoek" @@ -23781,6 +23883,22 @@ msgstr "" "Kromtrekken voorkomen\n" "Wist je dat bij het printen van materialen die gevoelig zijn voor kromtrekken, zoals ABS, een juiste verhoging van de temperatuur van het warmtebed de kans op kromtrekken kan verkleinen?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Laaghoogte is te klein.\n" +#~ "Het zal worden ingesteld op min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "De laaghoogte overschrijdt de limiet in Printerinstellingen -> Extruder -> Laaghoogtelimieten, dit kan problemen met de afdrukkwaliteit veroorzaken." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automatisch aanpassen aan het ingestelde bereik?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kopdiameter" + # AI Translated #~ msgid "Print order within a single layer." #~ msgstr "Printvolgorde binnen één laag." diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index a0701dca09..e8acc62a9b 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <>\n" "Language-Team: \n" @@ -4843,6 +4843,23 @@ msgstr "Obecna temperatura komory jest wyższa niż bezpieczna temperatura dla f msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimalna temperatura komory (%d℃) jest wyższa niż docelowa temperatura komory (%d℃). Wartość minimalna to próg, przy którym rozpoczyna się druk, podczas gdy komora nadal nagrzewa się do wartości docelowej, więc nie powinna jej przekraczać. Zostanie ograniczona do wartości docelowej." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Wysokość warstwy jest zbyt mała. Zostanie ustawiona na minimum (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Wysokość warstwy wykracza poza limity ustawione w Ustawieniach Drukarki -> Ekstruder -> Limity wysokości warstwy, co może powodować problemy z jakością druku." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Dostosować ją automatycznie do limitu (%g mm)?" + +msgid "Adjust" +msgstr "Dostosuj" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4965,6 +4982,13 @@ msgstr "" "Tak — włącz generator ścian Arachne\n" "Nie — wyłącz generator ścian Arachne i ustaw tryb [Przesunięcie] skóry fuzzy" +# AI Translated +msgid "Brim ear radius" +msgstr "Promień ucha brim" + +msgid "Brim width" +msgstr "Szerokość brimu" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Tryb spiralny działa tylko wtedy, gdy liczba pętli ściany wynosi 1, podpory są wyłączone, wykrywanie zlepiania przez sondowanie jest wyłączone, liczba warstw górnej powłoki wynosi 0, gęstość wypełnienia wynosi 0, a typ timelapse jest tradycyjny." @@ -5226,6 +5250,14 @@ msgstr "Nie udało się wygenerować kodu kalibracji" msgid "Calibration error" msgstr "Błąd kalibracji" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Ta drukarka nie ma skonfigurowanego sprzętu wymaganego przez ten element sterujący." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Ten element sterujący nie jest obsługiwany przez tę drukarkę." + # AI Translated msgid "Network unavailable" msgstr "Sieć niedostępna" @@ -6109,7 +6141,7 @@ msgstr "Objętość:" msgid "Size:" msgstr "Rozmiar:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Wykryto konflikty ścieżek G-code na warstwie %d, Z = %.2lfmm. Proszę oddalić od siebie obiekty będące w konflikcie (%s <-> %s)." @@ -6295,6 +6327,10 @@ msgstr "Wiele urządzeń" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Urządzenie (Web)" + msgid "Yes" msgstr "Tak" @@ -8444,22 +8480,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Zastąpiono plikami 3D z katalogu:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Pominięto %s: ten sam plik.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Pominięto %s: plik nie istnieje.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Pominięto %s: nie udało się zastąpić.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Zastąpiono %s.\n" @@ -9232,6 +9268,18 @@ msgstr "Umożliwia wysyłanie zadania do wielu urządzeń jednocześnie i zarzą msgid "Pop up to select filament grouping mode" msgstr "Okno dialogowe do wyboru trybu grupowania filamentów" +# AI Translated +msgid "Visible plugin pages" +msgstr "Widoczne strony wtyczek" + +# AI Translated +msgid "pages" +msgstr "stron" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Liczba stron wtyczek wyświetlanych jako stałe karty, zanim pozostałe strony zostaną zwinięte do listy rozwijanej na ostatniej karcie." + # AI Translated msgid "Behaviour" msgstr "Zachowanie" @@ -9647,6 +9695,18 @@ msgstr "Pokaż nieobsługiwane profile" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Pokazuj niekompatybilne/nieobsługiwane profile na listach rozwijanych drukarek i filamentów. Tych profili nie można wybrać." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Eksperymentalne) Używaj agentów drukarki zamiast serwerów druku" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Kieruje zadania druku dla drukarek innych niż Bambu przez agentów wtyczek drukarki zamiast klasycznego przesyłania do serwera druku.\n" +"Gdy opcja jest wyłączona, OrcaSlicer korzysta z dotychczasowego działania serwera druku." + # AI Translated msgid "Experimental Features" msgstr "Funkcje eksperymentalne" @@ -9918,10 +9978,26 @@ msgstr "Profil użytkownika" msgid "Preset Inside Project" msgstr "Profil wewnątrz projektu" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Kopiuje do tego profilu wszystkie wartości odziedziczone z profilu nadrzędnego i usuwa relację dziedziczenia. Profile zgodne wyłącznie z profilem nadrzędnym mogą przestać być obsługiwane." + # AI Translated msgid "Detach from parent" msgstr "Odłącz od elementu nadrzędnego" +# AI Translated +msgid "Unique preset" +msgstr "Profil niezależny" + +# AI Translated +msgid "Parent preset" +msgstr "Profil nadrzędny" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Ten profil nie dziedziczy z innego profilu." + msgid "Name is unavailable." msgstr "Nazwa jest niedostępna." @@ -10684,22 +10760,6 @@ msgstr "Czy na pewno włączyć tę opcję?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Wzory wypełnienia są zwykle projektowane tak, aby samodzielnie obsługiwać obrót, co zapewnia prawidłowy druk i zamierzony efekt (np. Gyroidalny, Sześcienny). Obracanie bieżącego wzoru wypełnienia może prowadzić do niewystarczającego podparcia. Zachowaj ostrożność i dokładnie sprawdź, czy nie występują problemy z drukiem. Czy na pewno chcesz włączyć tę opcję?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Wysokość warstwy jest zbyt mała.\n" -"Ustawione zostanie na min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Wysokość warstwy przekracza limit w Ustawieniach Drukarki -> Extruder -> Limity wysokości warstwy, co może powodować problemy z jakością druku." - -msgid "Adjust to the set range automatically?\n" -msgstr "Dostosować automatycznie do ustawionego zakresu?\n" - -msgid "Adjust" -msgstr "Dostosuj" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funkcja eksperymentalna: Polega na wycofywaniu filamentu na większą odległość w celu zminimalizowania płukania, a następne jego odcięcie. Choć może to znacząco zmniejszyć ilość zużytego filamentu, może również zwiększyć ryzyko zatknięcia dyszy lub innych problemów z drukowaniem." @@ -10899,6 +10959,9 @@ msgstr "Znaleziono zarezerwowane słowa kluczowe" msgid "Setting Overrides" msgstr "Nadpisywane Ustawień" +msgid "Retraction when switching material" +msgstr "Retrakcja podczas zmiany filamentu" + msgid "Basic information" msgstr "Podstawowe informacje" @@ -11033,6 +11096,14 @@ msgstr "Kompatybilne profile procesów" msgid "Printable space" msgstr "Przestrzeń do druku" +# AI Translated +msgid "Printer Agent" +msgstr "Agent drukarki" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką. Dostępni agenci są rejestrowani przy uruchamianiu." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -11165,9 +11236,6 @@ msgstr "Ograniczenia wysokości warstwy" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retrakcja podczas zmiany filamentu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12559,6 +12627,10 @@ msgstr " jest zbyt blisko obszaru wykluczenia, mogą wystąpić kolizje.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " jest zbyt blisko obszaru wykrywania zalepienia dyszy, co doprowadzi do kolizji.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " znajduje się częściowo poza obszarem druku i nie może zostać wydrukowany.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Wybrane temperatury dyszy są niezgodne. Temperatura dyszy każdego filamentu musi mieścić się w zalecanym zakresie temperatur dyszy pozostałych filamentów. W przeciwnym razie może dojść do zatkania dyszy lub uszkodzenia drukarki." @@ -12901,10 +12973,6 @@ msgstr "Użyj 3MF zamiast G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Włącz tę opcję, jeśli drukarka przyjmuje plik 3MF jako zadanie druku. Po włączeniu Orca Slicer wysyła plik po cięciu jako .gcode.3mf zamiast zwykłego pliku .gcode." -# AI Translated -msgid "Printer Agent" -msgstr "Agent drukarki" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką." @@ -13617,9 +13685,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Prędkość wewnętrznych mostów. Jeśli wartość jest wyrażona w procentach, będzie obliczana na podstawie prędkości mostu. Wartość domyślna wynosi 150%." -msgid "Brim width" -msgstr "Szerokość brimu" - msgid "This is the distance from the model to the outermost brim line." msgstr "Odległość od modelu do najbardziej zewnętrznej linii brimu" @@ -13703,6 +13768,14 @@ msgstr "" "Kształt zostanie zredukowany przed wykryciem ostrych kątów. Ten parametr wskazuje minimalną długość odchylenia dla redukcji.\n" "0, aby dezaktywować" +# AI Translated +msgid "Brim ears outer only" +msgstr "Uszy brim tylko na zewnątrz" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Generuje uszy myszy tylko na zewnętrznym obrysie modelu, z pominięciem otworów i zamkniętych sekcji." + msgid "upward compatible machine" msgstr "drukarka kompatybilna i wzwyż" @@ -14896,6 +14969,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroidalny" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Współczynnik wygładzania wypełnienia" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Określa, jak mocno zaokrąglane są narożniki wypełnienia. 0% zachowuje oryginalną ostrą ścieżkę, a 100% tworzy największe możliwe łuki pomiędzy sąsiednimi liniami wypełnienia." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Przyspieszenie dla wypełnienia górnej powierzchni. Użycie niższej wartości może poprawić jakość górnej powierzchni" @@ -15459,6 +15540,14 @@ msgstr "Z jakim rodzajem G-code drukarka jest kompatybilna." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Pomiń blok konfiguracyjny G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Nie zapisuje bloku CONFIG_BLOCK (par klucz/wartość z konfiguracją slicera) do pliku G-code. Może to pomóc w przypadku drukarek, których firmware ulega awarii podczas przetwarzania tych linii komentarza (np. Anycubic go-klipper). Uwaga: plik G-code nie będzie już zawierał ustawień slicera, więc ponowne zaimportowanie go do OrcaSlicer nie przywróci konfiguracji." + msgid "Pellet Modded Printer" msgstr "Drukarka do druku granulatem" @@ -16571,6 +16660,14 @@ msgstr "Długa retrakcja podczas zmian ekstruderów" msgid "Retraction distance when extruder change" msgstr "Długość retrakcji podczas zmian ekstruderów" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Długość retrakcji (Zmiana narzędzia)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Gdy retrakcja jest wyzwalana przed zmianą narzędzia, filament zostaje wycofany o określoną wartość (długość mierzona jest na surowym filamencie, przed wejściem do ekstrudera)." + msgid "Z-hop height" msgstr "Wysokość Z-hop" @@ -16669,6 +16766,10 @@ msgstr "Dodatkowa ilość dla powrotu" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Gdy retrakcja jest kompensowana po przemieszczeniu, ekstruder przepycha tę dodatkową ilość filamentu. To opcja jest rzadko potrzebna." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Dodatkowa ilość dla powrotu (Zmiana narzędzia)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Jeśli retrakcja jest korygowana po zmianie narzędzia, extruder przepchnie taką dodatkową ilość filamentu." @@ -17099,6 +17200,14 @@ msgstr "Zmiana narzędzia na wieży czyszczącej" msgid "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." msgstr "Wymusza przemieszczenie głowicy do wieży czyszczącej przed wydaniem polecenia zmiany narzędzia (Tx). Dotyczy tylko drukarek wieloekstruderowych (wielogłowicowych) korzystających z wieży czyszczącej typu 2. Domyślnie Orca pomija to przemieszczenie na maszynach wielogłowicowych, ponieważ zamianą głowic zajmuje się oprogramowanie sprzętowe, przez co polecenie Tx może zostać wydane nad drukowaną częścią. Włącz tę opcję, jeśli chcesz, aby zmiana narzędzia zawsze następowała nad wieżą czyszczącą." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Czekaj na temperaturę na wieży czyszczącej" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Pobiera nowe narzędzie bez czekania, aż osiągnie temperaturę druku, przejeżdża do wieży czyszczącej i tam czeka na temperaturę, tuż przed płukaniem. Materiał wyciekający podczas nagrzewania trafia na wieżę zamiast na model, a przejazd nakłada się na nagrzewanie. Dotyczy wyłącznie drukarek z wieloma ekstruderami (wieloma głowicami) używających wieży czyszczącej typu 2. Firmware ani makro zmiany narzędzia nie mogą samodzielnie czekać na temperaturę. Gdy opcja jest wyłączona, oczekiwanie na temperaturę jest wysyłane bezpośrednio po poleceniu zmiany narzędzia." + msgid "No sparse layers (beta)" msgstr "Warstwy bez czyszczenia (beta)" @@ -20445,10 +20554,6 @@ msgstr "Fizyczna drukarka" msgid "Print Host upload" msgstr "Przesyłanie do hosta drukowania" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką. Dostępni agenci są rejestrowani przy uruchamianiu." - # AI Translated msgid "Select a Flashforge printer" msgstr "Wybierz drukarkę Flashforge" @@ -21401,9 +21506,6 @@ msgstr "Wystąpił problem podczas próby logowania, proszę spróbować ponowni msgid "User canceled." msgstr "Anulowane przez użytkownika." -msgid "Head diameter" -msgstr "Średnica łącznika" - msgid "Max angle" msgstr "Maksymalny kąt" @@ -22234,6 +22336,22 @@ msgstr "" "Unikaj odkształceń\n" "Czy wiesz, że podczas drukowania filamentami podatnymi na odkształcenia, takimi jak ABS, odpowiednie zwiększenie temperatury podgrzewanej płyty może zmniejszyć prawdopodobieństwo odkształceń?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Wysokość warstwy jest zbyt mała.\n" +#~ "Ustawione zostanie na min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Wysokość warstwy przekracza limit w Ustawieniach Drukarki -> Extruder -> Limity wysokości warstwy, co może powodować problemy z jakością druku." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Dostosować automatycznie do ustawionego zakresu?\n" + +#~ msgid "Head diameter" +#~ msgstr "Średnica łącznika" + #~ msgid "Print order within a single layer." #~ msgstr "Kolejność druku obiektów w obrębie jednej warstwy. Domyślnie lub według listy obiektów" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 595e46b8cf..1b39d4a159 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -4577,6 +4577,23 @@ msgstr "A temperatura da câmara atual está mais alta do que a temperatura segu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "A temperatura mínima da câmara (%d℃) é superior à temperatura alvo da câmara (%d℃). O valor mínimo é o limite no qual a impressão começa enquanto a câmara continua aquecendo em direção ao alvo; portanto, não deve execedê-lo. O valor será limitado ao alvo." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "A altura da camada é muito pequena. Ela será definida para o mínimo (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "A altura da camada está fora dos limites definidos em Configurações da Impressora -> Extrusora -> Limites de altura da camada, isso pode causar problemas de qualidade de impressão." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Ajustar automaticamente para o limite (%g mm)?" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4696,6 +4713,13 @@ msgstr "" "Sim - Habilitar Gerador de Parede Arachne\n" "Não - Desabilitar Gerador de Parede Arachne e setar o modo [Deslocamento] da Textura Difusa" +# AI Translated +msgid "Brim ear radius" +msgstr "Raio da orelha da borda" + +msgid "Brim width" +msgstr "Largura da borda" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "O modo espiral só funciona quando as voltas da parede são 1, o suporte está desativado, a detecção de aglomeração por sondagem está desativada, as camadas da casca de topo são 0, a densidade de preenchimento esparso é 0 e o tipo de timelapse é tradicional." @@ -4950,6 +4974,14 @@ msgstr "Falha ao gerar o G-code de calibração" msgid "Calibration error" msgstr "Erro de calibração" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Esta impressora não está configurada com o hardware que este controle requer." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Este controle não é suportado nesta impressora." + msgid "Network unavailable" msgstr "Rede indisponível" @@ -5793,7 +5825,7 @@ msgstr "Volume:" msgid "Size:" msgstr "Tamanho:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Foram encontrados conflitos de caminhos de G-code na camada %d, Z = %.2lfmm. Por favor, separe mais os objetos em conflito (%s <-> %s)." @@ -5974,6 +6006,10 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Projeto" +# AI Translated +msgid "Device (Web)" +msgstr "Dispositivo (Web)" + msgid "Yes" msgstr "Sim" @@ -8020,19 +8056,19 @@ msgstr "Diretório para substituição não foi selecionado" msgid "Replaced with 3D files from directory:\n" msgstr "Substituído por arquivos 3D do diretório:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ %s Ignorados: mesmo arquivo.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ %s Ignorados: arquivo não existe.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ %s Ignorados: falha ao substituir.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ %s Substituídos.\n" @@ -8759,6 +8795,18 @@ msgstr "Com esta opção habilitada, você pode enviar uma tarefa para vários d msgid "Pop up to select filament grouping mode" msgstr "Abrir seleção do modo de agrupamento de filamento" +# AI Translated +msgid "Visible plugin pages" +msgstr "Páginas de plugin visíveis" + +# AI Translated +msgid "pages" +msgstr "páginas" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Número de páginas de plugin exibidas como abas fixas antes que as páginas restantes sejam agrupadas em um menu suspenso na última aba." + msgid "Behaviour" msgstr "Comportamento" @@ -9113,6 +9161,18 @@ msgstr "Mostrar predefinições não suportadas" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Exibir predefinições incompatíveis e não suportadas nas listas de impressora e filamento. Essas predefinições não podem ser selecionadas." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimental) Usar agentes de impressora em vez de hosts de impressão" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Encaminha os trabalhos de impressão de impressoras que não são Bambu pelos agentes de plugin de impressora em vez do fluxo clássico de envio ao host de impressão.\n" +"Quando desativado, o OrcaSlicer usa o comportamento antigo do host de impressão." + msgid "Experimental Features" msgstr "Recursos Experimentais" @@ -9374,9 +9434,25 @@ msgstr "Predefinição do Usuário" msgid "Preset Inside Project" msgstr "Predefinição Dentro do Projeto" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copia para esta predefinição todos os valores herdados da predefinição pai e remove a relação de herança. Predefinições compatíveis apenas com a predefinição pai podem deixar de ser suportadas." + msgid "Detach from parent" msgstr "Separar do pai" +# AI Translated +msgid "Unique preset" +msgstr "Predefinição única" + +# AI Translated +msgid "Parent preset" +msgstr "Predefinição pai" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Esta predefinição não herda de outra predefinição." + msgid "Name is unavailable." msgstr "O nome não está disponível." @@ -10096,24 +10172,6 @@ msgstr "Tem certeza de que deseja habilitar esta opção?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Padrões de preenchimento são projetados para lidar com a rotação automaticamente para garantir a impressão adequada e atingir os efeitos pretendidos (Ex. Giroide, Cúbico). Girar o padrão de preenchimento esparso atual pode causar suporte insuficiente. Prossiga com cautela e verifique cuidadosamente se há possíveis problemas de impressão. Tem certeza de que deseja habilitar esta opção?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"A altura da camada é muito pequena.\n" -"Ela será definida como altura mínima da camada\n" -"A altura da camada é muito pequena.\n" -"Ela será definida como altura mínima da camada\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "A altura da camada excede o limite em Configurações da Impressora -> Extrusora -> Limites de altura da camada, isso pode causar problemas de qualidade de impressão." - -msgid "Adjust to the set range automatically?\n" -msgstr "Ajustar automaticamente à faixa definida?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funcionalidade experimental: Retrair e cortar o filamento a uma distância maior durante mudanças de filamento para minimizar a purga. Embora possa reduzir notavelmente a purga, ele também pode elevar o risco de bolhas no bico ou outras complicações de impressão." @@ -10308,6 +10366,9 @@ msgstr "Palavras-chave reservadas encontradas" msgid "Setting Overrides" msgstr "Sobrescrever configurações" +msgid "Retraction when switching material" +msgstr "Retração ao trocar material" + msgid "Basic information" msgstr "Informações básicas" @@ -10435,6 +10496,12 @@ msgstr "Perfis de processo compatíveis" msgid "Printable space" msgstr "Espaço de impressão" +msgid "Printer Agent" +msgstr "Agente de Impressora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selecione a implementação do agente de rede para comunicação com a impressora. Os agentes disponíveis são registrados na inicialização." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10560,9 +10627,6 @@ msgstr "Limites de altura da camada" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retração ao trocar material" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11893,6 +11957,10 @@ msgstr " está muito perto de uma área de exclusão, e colisões vão ocorrer.\ msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " está muito perto da área de detecção de aglomeração, e ocorrerão colisões.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " está parcialmente fora da área imprimível, e não pode ser impresso.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "As temperaturas dos bicos selecionadas são incompatíveis. A temperatura do bico de cada filamento deve estar dentro da faixa de temperatura recomendada para os demais filamentos. Caso contrário, pode ocorrer entupimento do bico ou danos à impressora." @@ -12206,9 +12274,6 @@ msgstr "Usar 3MF em vez de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Ative esta opção se a impressora aceitar um arquivo 3MF como trabalho de impressão. Quando ativada, o OrcaSlicer envia o arquivo fatiado como .gcode.3mf, em vez de um arquivo .gcode comum." -msgid "Printer Agent" -msgstr "Agente de Impressora" - msgid "Select the network agent implementation for printer communication." msgstr "Selecione a implementação do agente de rede para comunicação com a impressora." @@ -12888,9 +12953,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocidade de pontes internas. Se o valor for expresso como uma porcentagem, ele será calculado com base na bridge_speed. O valor padrão é 150%." -msgid "Brim width" -msgstr "Largura da borda" - msgid "This is the distance from the model to the outermost brim line." msgstr "Essa é a distância do modelo até a linha da borda mais externa." @@ -12970,6 +13032,14 @@ msgstr "" "A geometria será decimada antes de detectar ângulos agudos. Este parâmetro indica o comprimento mínimo da divergência para a decimação.\n" "0 para desativar." +# AI Translated +msgid "Brim ears outer only" +msgstr "Orelhas da borda apenas externas" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Gera orelhas de rato apenas no contorno externo do modelo, excluindo furos e seções fechadas." + msgid "upward compatible machine" msgstr "uáquina compatível ascendente" @@ -14104,6 +14174,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Fator de suavização do preenchimento esparso" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Controla o quanto os cantos do preenchimento esparso são arredondados. 0% mantém o trajeto original com cantos vivos, enquanto 100% produz as maiores curvas possíveis entre linhas de preenchimento adjacentes." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Esta é a aceleração do preenchimento da superfície superior. Usar um valor menor pode melhorar a qualidade da superfície superior." @@ -14639,6 +14717,14 @@ msgstr "Com que tipo de G-code a impressora é compatível." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Omitir o bloco de configuração do G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Não grava o CONFIG_BLOCK (os pares chave/valor da configuração do fatiador) no arquivo G-code. Isso pode ajudar com impressoras cujo firmware trava ao interpretar essas linhas de comentário (por exemplo, Anycubic go-klipper). Observação: o arquivo G-code não conterá mais as configurações do fatiador, então importá-lo de volta no OrcaSlicer não restaurará a configuração." + msgid "Pellet Modded Printer" msgstr "Impressora Modificada para Pellets" @@ -15157,7 +15243,6 @@ msgstr "Força máxima do eixo Y" msgid "The allowed maximum output force of Y axis" msgstr "A força máxima de saída permitida do eixo Y" -#, fuzzy msgid "N" msgstr "N" @@ -15167,9 +15252,9 @@ msgstr "Massa da mesa do eixo Y" msgid "The machine bed mass load of Y axis" msgstr "A carga de massa da mesa do equipamento no eixo Y" -#, fuzzy +# AI Translated msgid "g" -msgstr "G" +msgstr "g" msgid "The allowed max printed mass" msgstr "Massa máxima de impressão permitida" @@ -15681,6 +15766,14 @@ msgstr "Retração longa na troca de extrusora" msgid "Retraction distance when extruder change" msgstr "Distância de retração na troca de extrusora" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Comprimento da retração (Troca de ferramenta)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Quando a retração é acionada antes da troca de ferramenta, o filamento é puxado de volta na quantidade especificada (o comprimento é medido no filamento bruto, antes de entrar na extrusora)." + msgid "Z-hop height" msgstr "Altura de Z-hop" @@ -15774,6 +15867,10 @@ msgstr "Comprimento extra na retração" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quando a retração é compensada após o movimento de deslocamento, a extrusora empurrará essa quantidade adicional de filamento. Esta configuração é raramente necessária." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Comprimento extra na retração (Troca de ferramenta)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quando a retração é compensada após a troca de ferramenta, a extrusora empurrará essa quantidade adicional de filamento." @@ -16182,6 +16279,14 @@ msgstr "Troca de ferramenta na torre de purga" msgid "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." msgstr "Força o cabeçote de impressão a se deslocar até a torre de purga antes de emitir o comando de troca de ferramenta (Tx). Relevante apenas para impressoras com múltiplas extrusoras (múltiplos cabeçotes de impressão) que utilizam uma torre de purga Tipo 2. Por padrão, o Orca ignora o deslocamento em máquinas com múltiplos cabeçotes de impressão, pois o firmware gerencia a troca do cabeçote, o que pode resultar na emissão do comando Tx acima da peça impressa. Habilite esta opção se desejar que a troca de ferramenta seja sempre emitida acima da torre de purga." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Aguardar a temperatura na torre de purga" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Pega a nova ferramenta sem esperar que ela atinja a temperatura de impressão, desloca-se até a torre de purga e aguarda a temperatura ali, logo antes de purgar. O vazamento causado pelo aquecimento cai na torre em vez do modelo, e o deslocamento acontece durante o aquecimento. Relevante apenas para impressoras multiextrusora (multicabeça) que usam uma torre de purga do tipo 2. O firmware ou a macro de troca de ferramenta não devem aguardar a temperatura por conta própria. Quando desativado, a espera de temperatura é emitida logo após o comando de troca de ferramenta." + msgid "No sparse layers (beta)" msgstr "Sem camadas esparsas (beta)" @@ -16733,7 +16838,6 @@ msgstr "Volume de preparo" msgid "This is the volume of material to prime the extruder with on the tower." msgstr "Este é o volume de material para preparar a extrusora na torre." -#,fuzzy msgid "Prime volume mode" msgstr "Modo de volume de preparação" @@ -19369,9 +19473,6 @@ msgstr "Impressora Física" msgid "Print Host upload" msgstr "Upload do Host de Impressão" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selecione a implementação do agente de rede para comunicação com a impressora. Os agentes disponíveis são registrados na inicialização." - msgid "Select a Flashforge printer" msgstr "Selecione uma impressora Flashforge" @@ -20213,9 +20314,6 @@ msgstr "Algo inesperado aconteceu ao tentar conectar, por favor tente novamente. msgid "User canceled." msgstr "Cancelado pelo usuário." -msgid "Head diameter" -msgstr "Diâmetro da cabeça" - msgid "Max angle" msgstr "Ângulo máx" @@ -20949,6 +21047,24 @@ msgstr "" "Evitar empenamento\n" "Você sabia que ao imprimir materiais propensos ao empenamento como ABS, aumentar adequadamente a temperatura da mesa aquecida pode reduzir a probabilidade de empenamento?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "A altura da camada é muito pequena.\n" +#~ "Ela será definida como altura mínima da camada\n" +#~ "A altura da camada é muito pequena.\n" +#~ "Ela será definida como altura mínima da camada\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "A altura da camada excede o limite em Configurações da Impressora -> Extrusora -> Limites de altura da camada, isso pode causar problemas de qualidade de impressão." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Ajustar automaticamente à faixa definida?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diâmetro da cabeça" + #~ msgid "Print order within a single layer." #~ msgstr "Ordem de impressão dentro de uma única camada." diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index c2fbcb54be..2372471707 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg \n" @@ -4715,6 +4715,23 @@ msgstr "Текущая температура внутри термокамер msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Стартовая температура внутри термокамеры (%d℃) превышает целевую (%d℃). Подразумевается, что печать начинается заранее, поэтому стартовая температура не должна превышать её. Значение будет уменьшено." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Высота слоя слишком мала. Будет установлено минимальное значение (%g мм)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Высота слоя выходит за пределы, заданные в настройках принтера → Экструдер → Ограничение высоты слоя. Это может вызвать проблемы с качеством печати." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Автоматически подстроить под предел (%g мм)?" + +msgid "Adjust" +msgstr "Подстроиться" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4839,6 +4856,13 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "Использовать нечёткую оболочку с движком Arachne?" +# AI Translated +msgid "Brim ear radius" +msgstr "Радиус ушек каймы" + +msgid "Brim width" +msgstr "Ширина каймы" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" "Для печати в режиме вазы необходимы следующие настройки:\n" @@ -5107,6 +5131,14 @@ msgstr "Не удалось сгенерировать калибровочны msgid "Calibration error" msgstr "Ошибка калибровки" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "На этом принтере не настроено оборудование, необходимое для этого элемента управления." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Этот элемент управления не поддерживается на этом принтере." + msgid "Network unavailable" msgstr "Сеть недоступна" @@ -5991,7 +6023,7 @@ msgstr "Объём:" msgid "Size:" msgstr "Размер:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "В G-коде на %d слое (z = %.2lf мм) обнаружен конфликт путей. Пожалуйста, разместите конфликтующие модели дальше друг от друга (%s <-> %s)." @@ -6198,6 +6230,10 @@ msgstr "Принтеры" msgid "Project" msgstr "Проект" +# AI Translated +msgid "Device (Web)" +msgstr "Принтер (веб)" + msgid "Yes" msgstr "Да" @@ -8299,19 +8335,19 @@ msgstr "Расположение для замены не указано" msgid "Replaced with 3D files from directory:\n" msgstr "Заменено файлами из расположения:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Пропущен %s: идентичный файл.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Пропущен %s: файл не существует.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Пропущен %s: не удалось заменить.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Заменён %s.\n" @@ -9040,6 +9076,18 @@ msgstr "Если включено, вы сможете управлять нес msgid "Pop up to select filament grouping mode" msgstr "Всплывающее окно для выбора режима группировки материалов" +# AI Translated +msgid "Visible plugin pages" +msgstr "Видимые страницы плагинов" + +# AI Translated +msgid "pages" +msgstr "стр." + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Количество страниц плагинов, отображаемых как закреплённые вкладки, прежде чем остальные страницы будут свёрнуты в выпадающий список на последней вкладке." + msgid "Behaviour" msgstr "Автоматизация" @@ -9400,6 +9448,18 @@ msgstr "" "\n" "Примечание: профили остаются недоступными для выбора." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Экспериментально) Использовать агентов принтера вместо хостов печати" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Отправлять задания печати для принтеров, отличных от Bambu, через агентов плагинов принтера вместо классической загрузки на хост печати.\n" +"Если отключено, OrcaSlicer использует прежнее поведение хоста печати." + msgid "Experimental Features" msgstr "Экспериментальные настройки" @@ -9666,9 +9726,25 @@ msgstr "Пользовательский профиль" msgid "Preset Inside Project" msgstr "Профиль внутри проекта" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Копирует в этот профиль все значения, унаследованные от родительского профиля, и удаляет связь наследования. Профили, совместимые только с родительским, могут стать неподдерживаемыми." + msgid "Detach from parent" msgstr "Сделать независимым" +# AI Translated +msgid "Unique preset" +msgstr "Независимый профиль" + +# AI Translated +msgid "Parent preset" +msgstr "Родительский профиль" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Этот профиль не наследуется от другого профиля." + msgid "Name is unavailable." msgstr "Имя недоступно." @@ -9686,7 +9762,9 @@ msgstr "" "несовместим с текущим принтером." msgid "Please note that saving will overwrite the current preset." -msgstr "Обратите внимание: при сохранении произойдёт\nперезапись текущего профиля." +msgstr "" +"Обратите внимание: при сохранении произойдёт\n" +"перезапись текущего профиля." msgid "The name cannot be the same as a preset alias name." msgstr "Имя не должно совпадать с именем предустановленного профиля." @@ -10389,22 +10467,6 @@ msgstr "Вы действительно хотите задействовать msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Многие шаблоны заполнения разработаны на основе автоматического поворота по определённым правилам для поддержания правильной печати и желаемого эффекта (например, «Гироид» или «Куб»). Изменение правила поворота текущего шаблона может привести к его провисанию. Будьте осторожны и внимательно проверяйте результат на наличие потенциальных проблем с печатью." -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Высота слоя слишком мала.\n" -"Будет установлено значение min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Высота слоя не может превышать ограничения, установленные в настройках принтера → Экструдер → Ограничение высоты слоя. Это может вызвать проблемы с качеством печати." - -msgid "Adjust to the set range automatically?\n" -msgstr "Автоматически подстроиться под заданный в настройках диапазон?\n" - -msgid "Adjust" -msgstr "Подстроиться" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "[Экспериментальная функция] Втягивание и обрезка прутка на большем расстоянии во время его замены для минимизации очистки. Хотя это значительно сокращает величину очистки, это может повысить риск возникновения затора или вызвать другие проблемы при печати." @@ -10604,6 +10666,9 @@ msgstr "Найдены зарезервированные ключевые сл msgid "Setting Overrides" msgstr "Замещение настроек" +msgid "Retraction when switching material" +msgstr "Откат при смене материала" + msgid "Basic information" msgstr "Основные" @@ -10751,6 +10816,12 @@ msgstr "Совместимые настройки" msgid "Printable space" msgstr "Область печати" +msgid "Printer Agent" +msgstr "Сетевой агент" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Реализация сетевого агента для обмена информацией с принтером. Доступные реализации определяются при запуске." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10879,9 +10950,6 @@ msgstr "Ограничение высоты слоя" msgid "Z-Hop" msgstr "Подъём головы при откате" -msgid "Retraction when switching material" -msgstr "Откат при смене материала" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12218,6 +12286,10 @@ msgstr " находится слишком близко к области иск msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " находится слишком близко к зоне обнаружения налипаний, столкновения неизбежны.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " частично находится за пределами области печати и не может быть напечатан.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Обнаружен недопустимый перепад температур. Каждый из используемых материалов должен иметь в профиле температуру печати в пределах допустимого диапазона других материалов. В противном случае сопло может забиться и повредить принтер." @@ -12539,9 +12611,6 @@ msgstr "Сжатие G-кода перед отправкой" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Рекомендуется для принтеров, поддерживающих печать из архивов 3MF. Файлы печати будут отправляться с расширением \".gcode.3mf\"." -msgid "Printer Agent" -msgstr "Сетевой агент" - msgid "Select the network agent implementation for printer communication." msgstr "Реализация сетевого агента для обмена информацией с принтером." @@ -13232,9 +13301,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Скорость печати внутреннего моста. Можно указать процент от скорости внешнего моста (bridge_speed). По умолчанию – 150%." -msgid "Brim width" -msgstr "Ширина каймы" - msgid "This is the distance from the model to the outermost brim line." msgstr "Расстояние от модели до внешней линии каймы." @@ -13316,6 +13382,14 @@ msgstr "" "Геометрия модели будет упрощена перед обнаружением острых углов. Этот параметр задаёт минимальную длину отклонения для её упрощения.\n" "Установите 0 для отключения." +# AI Translated +msgid "Brim ears outer only" +msgstr "Ушки каймы только снаружи" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Создавать мышиные ушки только на внешнем контуре модели, исключая отверстия и замкнутые участки." + msgid "upward compatible machine" msgstr "условия для совместимых принтеров" @@ -14308,13 +14382,19 @@ msgid "Interface layer pre-extrusion distance" msgstr "Дистанция избыточной подачи при смене" msgid "Pre-extrusion distance for prime tower interface layer (where different materials meet)." -msgstr "Протяжённость первичного движения прочистки после смены материала. Позволяет быстро набрать давление в сопле и сбросить перегретый материал.\n\nПримечание: фактическая длина может быть ограничена шириной башни." +msgstr "" +"Протяжённость первичного движения прочистки после смены материала. Позволяет быстро набрать давление в сопле и сбросить перегретый материал.\n" +"\n" +"Примечание: фактическая длина может быть ограничена шириной башни." msgid "Interface layer pre-extrusion length" msgstr "Длина прутка для избыточной подачи" msgid "Pre-extrusion length for prime tower interface layer (where different materials meet)." -msgstr "Длина прутка, которую необходимо продавить на этапе избыточной подачи.\n\n0 – отключить этот этап." +msgstr "" +"Длина прутка, которую необходимо продавить на этапе избыточной подачи.\n" +"\n" +"0 – отключить этот этап." msgid "Tower ironing area" msgstr "Разглаживание кончиков" @@ -14626,6 +14706,14 @@ msgstr "ТПМП Фишера-Коха S" msgid "Gyroid" msgstr "Гироид" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Коэффициент сглаживания заполнения" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Определяет, насколько сильно скругляются углы заполнения. 0% сохраняет исходную траекторию с острыми углами, а 100% создаёт максимально возможные скругления между соседними линиями заполнения." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Ускорение на верхней поверхности. Использование меньшего значения может улучшить качество верхней поверхности." @@ -15213,6 +15301,14 @@ msgstr "Выбор типа G-кода для совместимости с пр msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Пропустить блок конфигурации в G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Не записывать CONFIG_BLOCK (пары ключ/значение с настройками слайсера) в файл G-code. Это может помочь с принтерами, прошивка которых аварийно завершается при разборе этих строк комментариев (например, Anycubic go-klipper). Примечание: файл G-code больше не будет содержать настройки слайсера, поэтому при обратном импорте в OrcaSlicer конфигурация не восстановится." + msgid "Pellet Modded Printer" msgstr "Гранульная модификация принтера" @@ -15396,8 +15492,7 @@ msgstr "Наклон опор" msgid "" "Controls how aggressively short or unsupported Lightning branches are pruned.\n" "This angle is converted internally to a per-layer distance." -msgstr "" -"Допустимый наклон опор молнии. Чем выше, тем быстрее и экономичнее распространяются её ветви." +msgstr "Допустимый наклон опор молнии. Чем выше, тем быстрее и экономичнее распространяются её ветви." # "Выпрямление" здесь, вопреки первой мысли – это как раз-таки наоборот искажение шаблона по ходу печати для сокращения количества ветвей. Короче, опять путаница из-за того, что генерация ветвей происходит сверху вниз. При печати снизу вверх шаблон именно что искажается. msgid "Straightening angle" @@ -16309,8 +16404,7 @@ msgid "" "The length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" -"Быстрый откат после очистки, выраженный в процентах от общей длины отката. В некоторых случаях позволяет значительно снизить количество «паутины»." -"\n" +"Быстрый откат после очистки, выраженный в процентах от общей длины отката. В некоторых случаях позволяет значительно снизить количество «паутины».\n" "Примечание: суммарное значение не должно превышать 100% и будет скорректировано автоматически." msgid "Retract on layer change" @@ -16344,6 +16438,14 @@ msgstr "Длинный откат перед сменой экструдера" msgid "Retraction distance when extruder change" msgstr "Длина отката перед сменой экструдера" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Длина отката (смена инструмента)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "При срабатывании отката перед сменой инструмента материал втягивается на указанную величину (длина измеряется по прутку материала до его входа в экструдер)." + msgid "Z-hop height" msgstr "Высота подъёма" @@ -16461,6 +16563,10 @@ msgstr "Доп. подача после отката" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Дополнительная длина подачи при возврате прутка после отката. Требуется крайне редко (например, для компенсации багов прошивки принтера)." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Доп. подача после отката (смена инструмента)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Дополнительная длина подачи после смены насадки." @@ -16474,7 +16580,9 @@ msgid "Deretraction speed" msgstr "Скорость возврата" msgid "Speed for reloading filament into the nozzle. Zero means same speed of retraction." -msgstr "Скорость возврата материала в сопло после отката.\n0 – использовать скорость отката." +msgstr "" +"Скорость возврата материала в сопло после отката.\n" +"0 – использовать скорость отката." msgid "Deretraction speed (extruder change)" msgstr "Скорость возврата (смена экструдера)" @@ -16945,6 +17053,14 @@ msgstr "" "\n" "Внимание: применимо только к многоэкструдерным принтерам с черновой башней 2 типа." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Ожидание температуры на черновой башне" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Забирает новый инструмент, не дожидаясь достижения температуры печати, перемещается к черновой башне и ждёт нагрева там, непосредственно перед прочисткой. Подтёки при нагреве попадают на башню, а не на модель, а перемещение совмещается с нагревом. Актуально только для принтеров с несколькими экструдерами (несколькими печатающими головами), использующих черновую башню типа 2. Прошивка или макрос смены инструмента не должны сами ждать нагрева. Если отключено, команда ожидания температуры выдаётся сразу после команды смены инструмента." + msgid "No sparse layers (beta)" msgstr "Без разреженных слоёв (beta)" @@ -17942,13 +18058,21 @@ msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. msgstr "Во избежание подтёков температура сопла будет снижена на время рэмминга. Поэтому время рэмминга должно быть больше времени охлаждения. 0 значит отключено." msgid "The maximum volumetric speed for ramming before extruder change, where -1 means using the maximum volumetric speed." -msgstr "Максимальный объёмный расход для рэмминга перед сменой экструдера.\n-1 – использовать максимальный расход." +msgstr "" +"Максимальный объёмный расход для рэмминга перед сменой экструдера.\n" +"-1 – использовать максимальный расход." msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. Note: only a cooldown command and fan activation are triggered, reaching the target temperature is not guaranteed. 0 means disabled." -msgstr "Во избежание подтёков температура сопла будет снижена на время рэмминга.\n0 – не менять температуру.\n\nПримечание: срабатывают только команда охлаждения и включение вентилятора; достижение целевой температуры не гарантируется." +msgstr "" +"Во избежание подтёков температура сопла будет снижена на время рэмминга.\n" +"0 – не менять температуру.\n" +"\n" +"Примечание: срабатывают только команда охлаждения и включение вентилятора; достижение целевой температуры не гарантируется." msgid "The maximum volumetric speed for ramming before a hotend change, where -1 means using the maximum volumetric speed." -msgstr "Максимальный объёмный расход для рэмминга перед сменой хотэнда.\n-1 – использовать максимальный расход." +msgstr "" +"Максимальный объёмный расход для рэмминга перед сменой хотэнда.\n" +"-1 – использовать максимальный расход." msgid "length when change hotend" msgstr "Откат при смене хотэнда" @@ -19414,10 +19538,14 @@ msgid "Continue anyway?" msgstr "Всё равно продолжить?" msgid "Enable \"Auto-adjust\" to fix this automatically, or continue anyway?" -msgstr "Включить адаптацию к расходу для автоматического исправления?\nНет – игнорировать предупреждение." +msgstr "" +"Включить адаптацию к расходу для автоматического исправления?\n" +"Нет – игнорировать предупреждение." msgid "Enable \"Auto-scale for nozzle\" and \"Auto-adjust\" to fix this automatically, or continue anyway?" -msgstr "Включить адаптацию к соплу и расходу для автоматического исправления?\nНет – игнорировать предупреждение." +msgstr "" +"Включить адаптацию к соплу и расходу для автоматического исправления?\n" +"Нет – игнорировать предупреждение." msgid "Start retraction length: " msgstr "Начальная длина отката: " @@ -20341,9 +20469,6 @@ msgstr "Физический принтер" msgid "Print Host upload" msgstr "Загрузка на хост печати" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Реализация сетевого агента для обмена информацией с принтером. Доступные реализации определяются при запуске." - msgid "Select a Flashforge printer" msgstr "Выберите принтер Flashforge" @@ -21202,9 +21327,6 @@ msgstr "При попытке войти произошла какая-то ош msgid "User canceled." msgstr "Отменено пользователем." -msgid "Head diameter" -msgstr "Диаметр уха" - msgid "Max angle" msgstr "Макс. угол" @@ -21959,6 +22081,22 @@ msgstr "" "Предотвращение коробления материала\n" "Знаете ли вы, что при печати материалами, склонными к короблению, таких как ABS, повышение температуры подогреваемого стола может снизить эту вероятность?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Высота слоя слишком мала.\n" +#~ "Будет установлено значение min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Высота слоя не может превышать ограничения, установленные в настройках принтера → Экструдер → Ограничение высоты слоя. Это может вызвать проблемы с качеством печати." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Автоматически подстроиться под заданный в настройках диапазон?\n" + +#~ msgid "Head diameter" +#~ msgstr "Диаметр уха" + #~ msgid "Print order within a single layer." #~ msgstr "Последовательность печати моделей в пределах одного слоя." diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 5686fb7d8f..432000f96d 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -5213,6 +5213,23 @@ msgstr "Kammarens aktuella temperatur är högre än materialets säkra temperat msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Kammarens minimitemperatur (%d℃) är högre än kammarens måltemperatur (%d℃). Minimivärdet är tröskeln där utskriften startar medan kammaren fortsätter värmas mot målet, så det bör inte överstiga målet. Värdet begränsas till måltemperaturen." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Lagerhöjden är för liten. Den kommer att sättas till minimivärdet (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Lagerhöjden ligger utanför gränserna som anges i Skrivarinställningar -> Extruder -> Lagerhöjds gränser, detta kan orsaka problem med utskriftskvaliteten." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Justera automatiskt till gränsvärdet (%g mm)?" + +msgid "Adjust" +msgstr "Justera" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5339,6 +5356,13 @@ msgstr "" "Ja – Aktivera Arachne-väggeneratorn\n" "Nej – Inaktivera Arachne-väggeneratorn och ställ in läget [Förskjutning] för ojämn yta" +# AI Translated +msgid "Brim ear radius" +msgstr "Radie för brim-öra" + +msgid "Brim width" +msgstr "Brim bredd" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiralläget fungerar bara när antal väggar är 1, support är avstängt, detektering av klumpbildning med sondering är avstängd, antal översta skallager är 0, sparsam ifyllnadsdensitet är 0 och timelapse-typen är traditionell." @@ -5645,6 +5669,14 @@ msgstr "Misslyckades med att generera cali G kod" msgid "Calibration error" msgstr "Fel vid kalibrering" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Den här skrivaren är inte konfigurerad med den maskinvara som den här kontrollen kräver." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Den här kontrollen stöds inte på den här skrivaren." + # AI Translated msgid "Network unavailable" msgstr "Nätverket är inte tillgängligt" @@ -6596,7 +6628,7 @@ msgid "Size:" msgstr "Storlek:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Konflikter mellan G-code-banor hittades på lager %d, Z = %.2lfmm. Placera de objekt som krockar längre ifrån varandra (%s <-> %s)." @@ -6798,6 +6830,10 @@ msgstr "Flera enheter" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Enhet (Webb)" + msgid "Yes" msgstr "Ja" @@ -9088,22 +9124,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Ersatt med 3D-filer från mappen:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Hoppade över %s: samma fil.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Hoppade över %s: filen finns inte.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Hoppade över %s: det gick inte att ersätta.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Ersatte %s.\n" @@ -9933,6 +9969,18 @@ msgstr "Med det här alternativet aktiverat kan du skicka en uppgift till flera msgid "Pop up to select filament grouping mode" msgstr "Visa dialogruta för val av filamentgrupperingsläge" +# AI Translated +msgid "Visible plugin pages" +msgstr "Synliga insticksmodulsidor" + +# AI Translated +msgid "pages" +msgstr "sidor" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Antal insticksmodulsidor som visas som fasta flikar innan de återstående sidorna fälls ihop i en rullgardinsmeny på den sista fliken." + # AI Translated msgid "Behaviour" msgstr "Beteende" @@ -10357,6 +10405,18 @@ msgstr "Visa förinställningar som inte stöds" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Visa inkompatibla förinställningar och förinställningar som inte stöds i rullgardinslistorna för skrivare och filament. Dessa förinställningar kan inte väljas." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimentellt) Använd skrivaragenter i stället för utskriftsvärdar" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Skickar utskriftsjobb för icke-Bambu-skrivare via skrivarens insticksmodulagenter i stället för det klassiska uppladdningsflödet till utskriftsvärden.\n" +"När detta är avaktiverat använder OrcaSlicer det äldre beteendet för utskriftsvärdar." + # AI Translated msgid "Experimental Features" msgstr "Experimentella funktioner" @@ -10637,10 +10697,26 @@ msgstr "Användar förinställning" msgid "Preset Inside Project" msgstr "Projekt förinställning" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Kopierar alla ärvda värden från den överordnade förinställningen till den här förinställningen och tar bort arvsrelationen. Förinställningar som endast är kompatibla med den överordnade förinställningen kan sluta stödjas." + # AI Translated msgid "Detach from parent" msgstr "Koppla loss från överordnad" +# AI Translated +msgid "Unique preset" +msgstr "Unik förinställning" + +# AI Translated +msgid "Parent preset" +msgstr "Överordnad förinställning" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Den här förinställningen ärver inte från någon annan förinställning." + msgid "Name is unavailable." msgstr "Namnet ej tillgängligt." @@ -11459,23 +11535,6 @@ msgstr "Är du säker på att du vill aktivera det här alternativet?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Ifyllnadsmönster är oftast konstruerade för att hantera rotation automatiskt så att de skrivs ut korrekt och ger avsedd effekt (t.ex. Gyroid, Kubisk). Att rotera det aktuella sparsamma ifyllnadsmönstret kan ge otillräckligt stöd. Var försiktig och kontrollera noga om det uppstår utskriftsproblem. Är du säker på att du vill aktivera det här alternativet?" -# AI Translated -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Lagerhöjden är för liten.\n" -"Den ställs in på min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Lagerhöjden överskrider gränsen i Skrivarinställningar -> Extruder -> Lagerhöjds gränser, detta kan orsaka problem med utskriftskvaliteten." - -msgid "Adjust to the set range automatically?\n" -msgstr "Justera automatiskt till det inställda området?\n" - -msgid "Adjust" -msgstr "Justera" - # AI Translated msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentell funktion: Filamentet dras tillbaka och kapas på ett längre avstånd vid filamentbyten för att minimera rensningen. Det kan minska rensningen avsevärt, men kan också öka risken för igensatt nozzel eller andra utskriftsproblem." @@ -11707,6 +11766,9 @@ msgstr "Hittade reserverade nyckelord" msgid "Setting Overrides" msgstr "Åsidosätter inställningar" +msgid "Retraction when switching material" +msgstr "Reduktion vid material byte" + msgid "Basic information" msgstr "Allmän information" @@ -11848,6 +11910,14 @@ msgstr "Kompatibla process profiler" msgid "Printable space" msgstr "Utskriftsbar yta" +# AI Translated +msgid "Printer Agent" +msgstr "Skrivaragent" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren. Tillgängliga agenter registreras vid start." + # AI Translated #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format @@ -11992,9 +12062,6 @@ msgstr "Lagerhöjds begränsning" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Reduktion vid material byte" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -13486,6 +13553,10 @@ msgstr " är för nära uteslutningsområdet, och kollisioner kommer att orsakas msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ligger för nära området för klumpdetektering, vilket kommer att orsaka kollisioner.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " är delvis utanför det utskrivbara området och kan inte skrivas ut.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "De valda nozzeltemperaturerna är inkompatibla. Varje filaments nozzeltemperatur måste ligga inom de andra filamentens rekommenderade nozzeltemperaturintervall. Annars kan nozzeln sättas igen eller skrivaren skadas." @@ -13856,10 +13927,6 @@ msgstr "Använd 3MF i stället för G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Aktivera detta om skrivaren tar emot en 3MF-fil som utskriftsjobb. När det är aktiverat skickar Orca Slicer den beredda filen som en .gcode.3mf i stället för en vanlig .gcode-fil." -# AI Translated -msgid "Printer Agent" -msgstr "Skrivaragent" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren." @@ -14616,9 +14683,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Hastighet för inre bridges. Om värdet anges i procent beräknas det utifrån bridge_speed. Standardvärdet är 150 %." -msgid "Brim width" -msgstr "Brim bredd" - msgid "This is the distance from the model to the outermost brim line." msgstr "Avståndet från modellen till yttersta brim linjen" @@ -14707,6 +14771,14 @@ msgstr "" "Geometrin decimeras innan skarpa vinklar detekteras. Den här parametern anger avvikelsens minsta längd för decimeringen.\n" "0 för att avaktivera." +# AI Translated +msgid "Brim ears outer only" +msgstr "Brim-öron endast utvändigt" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genererar musöron endast på modellens yttre kontur, exklusive hål och slutna sektioner." + msgid "upward compatible machine" msgstr "uppåt kompatibel maskin" @@ -16039,6 +16111,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Utjämningsfaktor för sparsam ifyllnad" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Styr hur kraftigt hörnen i den sparsamma ifyllnaden rundas av. 0% behåller den ursprungliga skarpa banan, medan 100% ger största möjliga kurvor mellan intilliggande ifyllnadslinjer." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Acceleration av fyllning av toppytan. Att använda ett lägre värde kan förbättra ytkvaliteten" @@ -16651,6 +16731,14 @@ msgstr "Vilken typ av G-kod är skrivaren kompatibel med" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Hoppa över G-code-konfigurationsblocket" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Skriver inte CONFIG_BLOCK (nyckel/värde-paren för slicerkonfigurationen) till G-code-filen. Detta kan hjälpa med skrivare vars firmware kraschar när dessa kommentarrader tolkas (t.ex. Anycubic go-klipper). Obs: G-code-filen kommer inte längre att innehålla slicerinställningarna, så att importera den tillbaka till OrcaSlicer återställer inte konfigurationen." + # AI Translated msgid "Pellet Modded Printer" msgstr "Skrivare ombyggd för pellets" @@ -17868,6 +17956,14 @@ msgstr "Lång reduktion vid extruderbyte" msgid "Retraction distance when extruder change" msgstr "Reduktionssträcka vid extruderbyte" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Reduktionslängd (Verktygsbyte)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "När reduktionen utlöses före ett verktygsbyte dras filamentet tillbaka med den angivna mängden (längden mäts på det obearbetade filamentet, innan det når extrudern)." + # AI Translated msgid "Z-hop height" msgstr "Z-hop-höjd" @@ -17983,6 +18079,10 @@ msgstr "Extra längd vid omstart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "När reduktionen kompenseras efter flyttrörelsen trycker extrudern fram den här extra mängden filament. Den här inställningen behövs sällan." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Extra längd vid omstart (Verktygsbyte)" + # AI Translated msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "När reduktionen kompenseras efter verktygsbyte trycker extrudern fram den här extra mängden filament." @@ -18477,6 +18577,14 @@ msgstr "Verktygsbyte vid prime tornet" msgid "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." msgstr "Tvinga verktygshuvudet att flytta till prime tornet innan verktygsbyteskommandot (Tx) skickas. Endast relevant för skrivare med flera extrudrar (flera verktygshuvuden) som använder ett prime torn av typ 2. Som standard hoppar Orca över flytten på maskiner med flera verktygshuvuden, eftersom den fasta programvaran hanterar huvudbytet, vilket kan leda till att Tx-kommandot skickas ovanför den utskrivna delen. Aktivera det här alternativet om du vill att verktygsbytet alltid ska ske ovanför prime tornet i stället." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Vänta på temperatur vid prime tornet" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Hämtar det nya verktyget utan att vänta på att det ska nå utskriftstemperatur, förflyttar sig till prime tornet och väntar på temperaturen där, precis före rensningen. Materialet som droppar under uppvärmningen hamnar på tornet i stället för på modellen, och förflyttningen sker samtidigt som uppvärmningen. Endast relevant för skrivare med flera extrudrar (flera verktygshuvuden) som använder ett prime torn av typ 2. Firmware eller verktygsbytesmakrot får inte vänta på temperaturen själv. När detta är avaktiverat utfärdas temperaturväntan direkt efter verktygsbyteskommandot." + # AI Translated msgid "No sparse layers (beta)" msgstr "Inga glesa lager (beta)" @@ -22101,10 +22209,6 @@ msgstr "Fysisk printer" msgid "Print Host upload" msgstr "Uppladdning utskriftsvärd" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren. Tillgängliga agenter registreras vid start." - # AI Translated msgid "Select a Flashforge printer" msgstr "Välj en Flashforge-skrivare" @@ -23181,10 +23285,6 @@ msgstr "Något oväntat hände vid inloggningen, försök igen." msgid "User canceled." msgstr "Användaren avbröt." -# AI Translated -msgid "Head diameter" -msgstr "Huvuddiameter" - # AI Translated msgid "Max angle" msgstr "Maxvinkel" @@ -24071,6 +24171,24 @@ msgstr "" "Undvik vridning\n" "Visste du att när du skriver ut material som är benägna att vrida, såsom ABS, kan en lämplig ökning av värmebäddens temperatur minska sannolikheten för vridning?" +# AI Translated +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Lagerhöjden är för liten.\n" +#~ "Den ställs in på min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Lagerhöjden överskrider gränsen i Skrivarinställningar -> Extruder -> Lagerhöjds gränser, detta kan orsaka problem med utskriftskvaliteten." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Justera automatiskt till det inställda området?\n" + +# AI Translated +#~ msgid "Head diameter" +#~ msgstr "Huvuddiameter" + # AI Translated #~ msgid "Print order within a single layer." #~ msgstr "Utskriftsordning inom ett enskilt lager." diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index a419ba320e..a0c0079125 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -4720,6 +4720,23 @@ msgstr "อุณหภูมิห้องพิมพ์ปัจจุบั msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "อุณหภูมิห้องพิมพ์ต่ำสุด (%d℃) สูงกว่าอุณหภูมิห้องพิมพ์เป้าหมาย (%d℃) ค่าต่ำสุดคือเกณฑ์ที่การพิมพ์จะเริ่มต้นในขณะที่ห้องพิมพ์ยังคงร้อนขึ้นไปสู่เป้าหมาย จึงไม่ควรเกินค่าเป้าหมาย ระบบจะจำกัดค่าให้เท่ากับเป้าหมาย" +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "ความสูงเลเยอร์น้อยเกินไป จะถูกตั้งค่าเป็นค่าต่ำสุด (%g mm)" + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "ความสูงเลเยอร์อยู่นอกขีดจำกัดที่ตั้งไว้ใน การตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> การจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "ปรับเป็นค่าขีดจำกัด (%g mm) โดยอัตโนมัติหรือไม่?" + +msgid "Adjust" +msgstr "ปรับ" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4840,6 +4857,13 @@ msgstr "" "ใช่ - เปิดใช้งาน Arachne Wall Generator\n" "ไม่ - ปิดการใช้งาน Arachne Wall Generator และตั้งค่าโหมด [Displacement] ของ Fuzzy Skin" +# AI Translated +msgid "Brim ear radius" +msgstr "รัศมีของหูขอบยึดชิ้นงาน" + +msgid "Brim width" +msgstr "ความกว้าง ขอบยึดชิ้นงาน" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "โหมดเกลียวจะทำงานเฉพาะเมื่อลูปติดผนังเป็น 1, ปิดใช้งานส่วนรองรับ, การตรวจจับการจับตัวเป็นก้อนโดยการตรวจวัดถูกปิดใช้งาน, ชั้นเปลือกด้านบนเป็น 0, ความหนาแน่นของไส้ในแบบกระจายเป็น 0 และประเภทไทม์แลปส์เป็นแบบดั้งเดิม" @@ -5094,6 +5118,14 @@ msgstr "ไม่สามารถสร้าง cali G-code" msgid "Calibration error" msgstr "ข้อผิดพลาดในการสอบเทียบ" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "เครื่องพิมพ์นี้ไม่ได้ตั้งค่าฮาร์ดแวร์ที่ตัวควบคุมนี้ต้องการ" + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "ตัวควบคุมนี้ไม่รองรับบนเครื่องพิมพ์นี้" + # AI Translated msgid "Network unavailable" msgstr "เครือข่ายไม่พร้อมใช้งาน" @@ -5952,7 +5984,7 @@ msgstr "ปริมาณ:" msgid "Size:" msgstr "ขนาด:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "พบความขัดแย้งของเส้นทางรหัส G ที่เลเยอร์ %d, Z = %.2lfmm โปรดแยกวัตถุที่ขัดแย้งกันให้ไกลออกไป (%s <-> %s)" @@ -6133,6 +6165,10 @@ msgstr "หลายอุปกรณ์" msgid "Project" msgstr "โปรเจกต์" +# AI Translated +msgid "Device (Web)" +msgstr "อุปกรณ์ (เว็บ)" + msgid "Yes" msgstr "ใช่" @@ -8199,19 +8235,19 @@ msgstr "ไม่ได้เลือกไดเรกทอรีสำหร msgid "Replaced with 3D files from directory:\n" msgstr "แทนที่ด้วยไฟล์ 3D จากไดเรกทอรี:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ ข้าม %s: ไฟล์เดียวกัน\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ ข้าม %s: ไม่มีไฟล์อยู่\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ ข้าม %s: ไม่สามารถแทนที่ได้\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔แทนที่ %s\n" @@ -8945,6 +8981,18 @@ msgstr "เมื่อเปิดใช้งานตัวเลือกน msgid "Pop up to select filament grouping mode" msgstr "ปรากฏขึ้นเพื่อเลือกโหมดการจัดกลุ่มเส้นพลาสติก" +# AI Translated +msgid "Visible plugin pages" +msgstr "หน้าปลั๊กอินที่แสดง" + +# AI Translated +msgid "pages" +msgstr "หน้า" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "จำนวนหน้าปลั๊กอินที่แสดงเป็นแท็บถาวร ก่อนที่หน้าที่เหลือจะถูกยุบรวมเป็นเมนูแบบเลื่อนลงในแท็บสุดท้าย" + msgid "Behaviour" msgstr "พฤติกรรม" @@ -9299,6 +9347,18 @@ msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้ msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้าที่ไม่เข้ากันหรือไม่รองรับในรายการเลือกเครื่องพิมพ์และเส้นพลาสติก ไม่สามารถเลือกค่าที่ตั้งไว้ล่วงหน้าเหล่านี้ได้" +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(ทดลอง) ใช้เอเจนต์เครื่องพิมพ์แทนโฮสต์การพิมพ์" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"ส่งงานพิมพ์ของเครื่องพิมพ์ที่ไม่ใช่ Bambu ผ่านเอเจนต์ปลั๊กอินของเครื่องพิมพ์ แทนการอัพโหลดไปยังโฮสต์การพิมพ์แบบเดิม\n" +"เมื่อปิดใช้ OrcaSlicer จะใช้พฤติกรรมโฮสต์การพิมพ์แบบเดิม" + # AI Translated msgid "Experimental Features" msgstr "ฟีเจอร์ทดลอง" @@ -9563,9 +9623,25 @@ msgstr "พรีเซ็ตผู้ใช้" msgid "Preset Inside Project" msgstr "พรีเซ็ตภายในโปรเจ็กต์" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "คัดลอกค่าที่สืบทอดมาจากพรีเซ็ตแม่ทั้งหมดมาไว้ในพรีเซ็ตนี้ และตัดความสัมพันธ์กับพรีเซ็ตแม่ พรีเซ็ตที่เข้ากันได้กับพรีเซ็ตแม่เท่านั้นอาจไม่ได้รับการรองรับอีกต่อไป" + msgid "Detach from parent" msgstr "แยกออกจากพรีเซ็ตแม่" +# AI Translated +msgid "Unique preset" +msgstr "พรีเซ็ตอิสระ" + +# AI Translated +msgid "Parent preset" +msgstr "พรีเซ็ตแม่" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "พรีเซ็ตนี้ไม่ได้สืบทอดมาจากพรีเซ็ตอื่น" + msgid "Name is unavailable." msgstr "ชื่อไม่พร้อมใช้งาน" @@ -10305,22 +10381,6 @@ msgstr "คุณแน่ใจหรือไม่ว่าต้องกา msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "โดยทั่วไปรูปแบบไส้ในได้รับการออกแบบให้รองรับการหมุนโดยอัตโนมัติเพื่อให้แน่ใจว่าการพิมพ์ถูกต้องและบรรลุผลตามที่ต้องการ (เช่น Gyroid, ลูกบาศก์) การหมุนรูปแบบ ไส้ใน แบบกระจัดกระจายในปัจจุบันอาจทำให้ส่วนรองรับไม่เพียงพอ โปรดดำเนินการด้วยความระมัดระวังและตรวจสอบปัญหาการพิมพ์ที่อาจเกิดขึ้นอย่างละเอียด คุณแน่ใจหรือไม่ว่าต้องการเปิดใช้งานตัวเลือกนี้" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"ความสูงของเลเยอร์น้อยเกินไป\n" -"มันจะตั้งค่าเป็น min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "ความสูงของเลเยอร์เกินขีดจำกัดในการตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> ขีดจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" - -msgid "Adjust to the set range automatically?\n" -msgstr "ปรับเป็นช่วงที่ตั้งไว้อัตโนมัติ?\n" - -msgid "Adjust" -msgstr "ปรับ" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "คุณลักษณะการทดลอง: การดึงกลับและตัดเส้นพลาสติกออกในระยะห่างที่มากขึ้นระหว่างการเปลี่ยนเส้นพลาสติกเพื่อลดการไล่เส้น แม้ว่าจะสามารถลดการไล่เส้นได้อย่างเห็นได้ชัด แต่ก็อาจเพิ่มความเสี่ยงของการอุดตันของหัวฉีดหรือภาวะแทรกซ้อนในการพิมพ์อื่นๆ อีกด้วย" @@ -10513,6 +10573,9 @@ msgstr "พบคีย์เวิร์ดที่สงวนไว้" msgid "Setting Overrides" msgstr "การตั้งค่าการแทนที่" +msgid "Retraction when switching material" +msgstr "การร่นกลับเมื่อเปลี่ยนวัสดุ" + msgid "Basic information" msgstr "ข้อมูลพื้นฐาน" @@ -10642,6 +10705,12 @@ msgstr "โปรไฟล์กระบวนการที่เข้าก msgid "Printable space" msgstr "พื้นที่ที่สามารถพิมพ์ได้" +msgid "Printer Agent" +msgstr "ตัวแทนเครื่องพิมพ์" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10767,9 +10836,6 @@ msgstr "การจำกัดความสูงของเลเยอร msgid "Z-Hop" msgstr "ยกแกน Z" -msgid "Retraction when switching material" -msgstr "การร่นกลับเมื่อเปลี่ยนวัสดุ" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12111,6 +12177,10 @@ msgstr "อยู่ใกล้เขตหวงห้ามมากเกิ msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "อยู่ใกล้พื้นที่การตรวจจับการจับตัวกันมากเกินไป และจะเกิดการชนกัน\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "อยู่นอกพื้นที่การพิมพ์บางส่วน จึงไม่สามารถพิมพ์ได้\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "อุณหภูมิหัวฉีดที่เลือกเข้ากันไม่ได้ อุณหภูมิหัวฉีดของเส้นพลาสติกแต่ละเส้นต้องอยู่ในช่วงอุณหภูมิหัวฉีดที่แนะนำของเส้นพลาสติกอื่นๆ มิฉะนั้นอาจเกิดการอุดตันของหัวฉีดหรือเครื่องพิมพ์เสียหายได้" @@ -12426,9 +12496,6 @@ msgstr "ใช้ 3MF แทน G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "เปิดใช้งานหากเครื่องพิมพ์รับไฟล์ 3MF เป็นงานพิมพ์ เมื่อเปิดใช้งาน OrcaSlicer จะส่งไฟล์ที่สไลซ์แล้วเป็น .gcode.3mf แทนไฟล์ .gcode ธรรมดา" -msgid "Printer Agent" -msgstr "ตัวแทนเครื่องพิมพ์" - msgid "Select the network agent implementation for printer communication." msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์" @@ -13103,9 +13170,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "ความเร็วของสะพานภายใน หากค่าแสดงเป็นเปอร์เซ็นต์ ค่าดังกล่าวจะถูกคำนวณตาม bridge_speed ค่าเริ่มต้นคือ 150%" -msgid "Brim width" -msgstr "ความกว้าง ขอบยึดชิ้นงาน" - msgid "This is the distance from the model to the outermost brim line." msgstr "ระยะห่างจากแบบจำลองถึงเส้นขอบยึดชิ้นงานด้านนอกสุด" @@ -13185,6 +13249,14 @@ msgstr "" "รูปทรงจะถูกทำลายก่อนที่จะตรวจจับมุมแหลม พารามิเตอร์นี้ระบุความยาวขั้นต่ำของการเบี่ยงเบนสำหรับการทำลาย\n" "0 เพื่อปิดการใช้งาน" +# AI Translated +msgid "Brim ears outer only" +msgstr "หูขอบยึดชิ้นงานเฉพาะด้านนอก" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "สร้างหูหนูเฉพาะบนคอนทัวร์ด้านนอกของโมเดล โดยไม่รวมรูและส่วนที่ปิดล้อม" + msgid "upward compatible machine" msgstr "เครื่องที่รองรับขึ้นไป" @@ -14351,6 +14423,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "ไจรอยด์" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "ค่าความเรียบของไส้ในแบบโปร่ง" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "ควบคุมระดับความมนของมุมไส้ในแบบโปร่ง ค่า 0% จะคงเส้นทางเดิมที่เป็นมุมแหลม ส่วน 100% จะสร้างส่วนโค้งที่ใหญ่ที่สุดเท่าที่เป็นไปได้ระหว่างเส้นไส้ในที่อยู่ติดกัน" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "ความเร่งของไส้ในพื้นผิวด้านบน การใช้ค่าที่ต่ำกว่าอาจปรับปรุงคุณภาพพื้นผิวด้านบนได้" @@ -14893,6 +14973,14 @@ msgstr "เครื่องพิมพ์ G-code ชนิดใดที่ msgid "Klipper" msgstr "คลิปเปอร์" +# AI Translated +msgid "Skip G-code config block" +msgstr "ข้ามบล็อกการตั้งค่าใน G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "ไม่เขียน CONFIG_BLOCK (คู่คีย์/ค่าของการตั้งค่าโปรแกรมสไลซ์) ลงในไฟล์ G-code ซึ่งอาจช่วยได้กับเครื่องพิมพ์ที่เฟิร์มแวร์ขัดข้องเมื่ออ่านบรรทัดคอมเมนต์เหล่านี้ (เช่น Anycubic go-klipper) หมายเหตุ: ไฟล์ G-code จะไม่มีการตั้งค่าโปรแกรมสไลซ์อีกต่อไป ดังนั้นการนำเข้ากลับมาใน OrcaSlicer จะไม่คืนค่าการตั้งค่า" + msgid "Pellet Modded Printer" msgstr "เครื่องพิมพ์ Modded เม็ด" @@ -15945,6 +16033,14 @@ msgstr "การถอยกลับนานเมื่อเปลี่ย msgid "Retraction distance when extruder change" msgstr "ระยะการดึงกลับเมื่อชุดดันเส้นเปลี่ยน" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "ความยาวการดึงกลับ (การเปลี่ยนเครื่องมือ)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "เมื่อการดึงกลับทำงานก่อนการเปลี่ยนเครื่องมือ เส้นพลาสติกจะถูกดึงกลับตามระยะที่กำหนด (วัดความยาวบนเส้นพลาสติกดิบ ก่อนเข้าสู่ชุดดันเส้น)" + msgid "Z-hop height" msgstr "ความสูงยกแกน Z" @@ -16039,6 +16135,10 @@ msgstr "ความยาวพิเศษเมื่อรีสตาร์ msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "เมื่อชดเชยการดึงกลับหลังการเคลื่อนที่เดินทาง ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้ การตั้งค่านี้ไม่ค่อยจำเป็น" +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "ความยาวพิเศษเมื่อรีสตาร์ท (การเปลี่ยนเครื่องมือ)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "เมื่อชดเชยการดึงกลับหลังเปลี่ยนเครื่องมือ ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้" @@ -16451,6 +16551,14 @@ msgstr "การเปลี่ยนเครื่องมือบน Wipe msgid "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." msgstr "บังคับให้หัวเครื่องมือเคลื่อนที่ไปที่ Wipe Tower ก่อนที่จะออกคำสั่งเปลี่ยนเครื่องมือ (Tx) เกี่ยวข้องเฉพาะกับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่อง (หลายหัวเครื่องมือ) ที่ใช้แผ่นเช็ดแบบ Type 2 ตามค่าเริ่มต้น Orca จะข้ามการเดินทางบนเครื่องที่มีหัวเครื่องมือหลายหัวเนื่องจากเฟิร์มแวร์จัดการการสลับหัว ซึ่งอาจส่งผลให้มีการออกคำสั่ง Tx เหนือส่วนที่พิมพ์ เปิดใช้งานตัวเลือกนี้หากคุณต้องการให้ทำการเปลี่ยนแปลงเครื่องมือเหนือ Wipe Tower แทนเสมอ" +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "รอให้ถึงอุณหภูมิที่ Wipe Tower" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "รับเครื่องมือใหม่โดยไม่รอให้ถึงอุณหภูมิการพิมพ์ แล้วเคลื่อนที่ไปยัง Wipe Tower และรออุณหภูมิที่นั่นก่อนไล่เส้นทันที เส้นพลาสติกที่ซึมออกมาระหว่างการอุ่นจะตกลงบน Wipe Tower แทนที่จะตกบนโมเดล และการเคลื่อนที่จะเกิดขึ้นพร้อมกับการอุ่น ใช้ได้เฉพาะกับเครื่องพิมพ์แบบหลายชุดดันเส้น (หลายหัวพิมพ์) ที่ใช้ Wipe Tower ชนิดที่ 2 เฟิร์มแวร์หรือแมโครการเปลี่ยนเครื่องมือต้องไม่รออุณหภูมิเอง เมื่อปิดใช้ คำสั่งรออุณหภูมิจะถูกส่งทันทีหลังคำสั่งเปลี่ยนเครื่องมือ" + msgid "No sparse layers (beta)" msgstr "ไม่มีชั้นกระจัดกระจาย (เบต้า)" @@ -19681,9 +19789,6 @@ msgstr "เครื่องพิมพ์ทางกายภาพ" msgid "Print Host upload" msgstr "อัพโหลดโฮสต์การพิมพ์" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" - msgid "Select a Flashforge printer" msgstr "เลือกเครื่องพิมพ์ Flashforge" @@ -20575,9 +20680,6 @@ msgstr "เกิดสิ่งที่ไม่คาดคิดขณะพ msgid "User canceled." msgstr "ผู้ใช้ยกเลิก" -msgid "Head diameter" -msgstr "เส้นผ่านศูนย์กลางหัว" - msgid "Max angle" msgstr "มุมสูงสุด" @@ -21361,6 +21463,22 @@ msgstr "" "หลีกเลี่ยงการบิดเบี้ยว\n" "คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "ความสูงของเลเยอร์น้อยเกินไป\n" +#~ "มันจะตั้งค่าเป็น min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "ความสูงของเลเยอร์เกินขีดจำกัดในการตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> ขีดจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "ปรับเป็นช่วงที่ตั้งไว้อัตโนมัติ?\n" + +#~ msgid "Head diameter" +#~ msgstr "เส้นผ่านศูนย์กลางหัว" + #~ msgid "Print order within a single layer." #~ msgstr "สั่งพิมพ์ภายในชั้นเดียว" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 63d1e5bc75..a31d2216f4 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-08-04 19:36+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -4808,6 +4808,23 @@ msgstr "Mevcut hazne sıcaklığı malzemenin güvenli sıcaklığından yüksek msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimum oda sıcaklığı (%d℃), hedef oda sıcaklığından (%d℃) yüksek. Minimum değer, oda hedefe doğru ısınmaya devam ederken baskının başladığı eşiktir; bu nedenle hedefi aşmamalıdır. Değer hedefe sınırlandırılacak." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Katman yüksekliği çok küçük. Minimum değere (%g mm) ayarlanacak." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Katman yüksekliği, Yazıcı Ayarları -> Ekstruder -> Katman yüksekliği sınırları bölümünde ayarlanan sınırların dışında, bu durum baskı kalitesi sorunlarına neden olabilir." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Otomatik olarak sınır değerine (%g mm) ayarlansın mı?" + +msgid "Adjust" +msgstr "Ayarla" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4928,6 +4945,13 @@ msgstr "" "Evet - Arachne Duvarı Oluşturucusunu Etkinleştir\n" "Hayır - Arachne Duvarı Oluşturucusunu Devre Dışı Bırak ve Pütürlü Yüzey [Yer Değiştirme] modunu ayarla" +# AI Translated +msgid "Brim ear radius" +msgstr "Kenar kulak yarıçapı" + +msgid "Brim width" +msgstr "Kenar genişliği" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiral mod yalnızca duvar döngüleri 1 olduğunda, destek devre dışı bırakıldığında, problama yoluyla topaklanma tespiti devre dışı bırakıldığında, üst kabuk katmanları 0 olduğunda, seyrek dolgu yoğunluğu 0 olduğunda ve hızlandırılmış tip geleneksel olduğunda çalışır." @@ -5182,6 +5206,14 @@ msgstr "Cali G-code oluşturma başarısız oldu" msgid "Calibration error" msgstr "Kalibrasyon hatası" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Bu yazıcı, bu denetimin ihtiyaç duyduğu donanımla yapılandırılmamış." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Bu denetim bu yazıcıda desteklenmiyor." + # AI Translated msgid "Network unavailable" msgstr "Ağ kullanılamıyor" @@ -6045,7 +6077,7 @@ msgstr "Hacim:" msgid "Size:" msgstr "Boyut:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "%d katmanında gcode yollarında çakışmalar bulundu, Z = %.2lfmm. Lütfen çakışan nesneleri daha uzağa ayırın (%s <-> %s)." @@ -6227,6 +6259,10 @@ msgstr "Çoklu cihaz" msgid "Project" msgstr "Proje" +# AI Translated +msgid "Device (Web)" +msgstr "Cihaz (Web)" + msgid "Yes" msgstr "Evet" @@ -8324,19 +8360,19 @@ msgstr "Değiştirme için dizin seçilmedi" msgid "Replaced with 3D files from directory:\n" msgstr "Dizindeki 3D dosyalarla değiştirildi:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ %s atlandı: aynı dosya.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ %s atlandı: dosya mevcut değil.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ %s atlandı: değiştirilemedi.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ %s değiştirildi.\n" @@ -9076,6 +9112,18 @@ msgstr "Bu seçenek etkinleştirildiğinde, aynı anda birden fazla cihaza bir g msgid "Pop up to select filament grouping mode" msgstr "Filament gruplama modunu seçmek için açılır pencere" +# AI Translated +msgid "Visible plugin pages" +msgstr "Görünür eklenti sayfaları" + +# AI Translated +msgid "pages" +msgstr "sayfa" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Kalan sayfalar son sekmedeki açılır listeye toplanmadan önce sabit sekme olarak gösterilen eklenti sayfalarının sayısı." + msgid "Behaviour" msgstr "Davranış" @@ -9466,6 +9514,18 @@ msgstr "Desteklenmeyen ön ayarları göster" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Yazıcı ve filament açılır listelerinde uyumsuz/desteklenmeyen ön ayarları gösterir. Bu ön ayarlar seçilemez." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Deneysel) Baskı sunucuları yerine yazıcı aracılarını kullan" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Bambu olmayan yazıcıların baskı işlerini, klasik baskı sunucusuna yükleme akışı yerine yazıcı eklenti aracıları üzerinden yönlendirir.\n" +"Devre dışı bırakıldığında OrcaSlicer eski baskı sunucusu davranışını kullanır." + # AI Translated msgid "Experimental Features" msgstr "Deneysel Özellikler" @@ -9733,9 +9793,25 @@ msgstr "Kullanıcı Ön Ayarı" msgid "Preset Inside Project" msgstr "Ön ayar içerisinde proje" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Üst ön ayardan devralınan tüm değerleri bu ön ayara kopyalar ve üst ön ayarla olan ilişkiyi kaldırır. Yalnızca üst ön ayarla uyumlu olan ön ayarlar desteklenmeyebilir." + msgid "Detach from parent" msgstr "Ebeveynden ayrıl" +# AI Translated +msgid "Unique preset" +msgstr "Bağımsız ön ayar" + +# AI Translated +msgid "Parent preset" +msgstr "Üst ön ayar" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Bu ön ayar başka bir ön ayardan devralmıyor." + msgid "Name is unavailable." msgstr "Ad kullanılamıyor." @@ -10485,22 +10561,6 @@ msgstr "Bu seçeneği etkinleştirmek istediğinizden emin misiniz?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Dolgu desenleri genellikle, doğru baskı alınmasını ve istenen etkilerin (ör. Gyroid, Kübik) elde edilmesini sağlamak için döndürme işlemini otomatik olarak yapacak şekilde tasarlanmıştır. Mevcut seyrek dolgu desenini döndürmek, yetersiz destekle sonuçlanabilir. Lütfen dikkatli ilerleyin ve olası baskı sorunlarını iyice kontrol edin. Bu seçeneği etkinleştirmek istediğinizden emin misiniz?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Katman yüksekliği çok küçük.\n" -"min_layer_height olarak ayarlanacak\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Katman yüksekliği, Yazıcı Ayarları -> Ekstruder -> Katman yüksekliği sınırları bölümündeki sınırı aşıyor bu durum baskı kalitesi sorunlarına neden olabilir." - -msgid "Adjust to the set range automatically?\n" -msgstr "Ayarlanan aralığa otomatik olarak ayarlansın mı?\n" - -msgid "Adjust" -msgstr "Ayarla" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Deneysel özellik: Filament değişiklikleri sırasında, floşu en aza indirmek için filamenti daha büyük bir mesafeden geri çekmek ve kesmek. Flush’u önemli ölçüde azaltabilmesine rağmen, aynı zamanda nozul tıkanmaları veya diğer yazdırma komplikasyonları riskini de artırabilir." @@ -10699,6 +10759,9 @@ msgstr "Ayrılmış anahtar kelimeler bulundu" msgid "Setting Overrides" msgstr "Ayarların Üzerine Yaz" +msgid "Retraction when switching material" +msgstr "Malzemeyi Değiştirirken Geri Çekme" + msgid "Basic information" msgstr "Temel Bilgiler" @@ -10832,6 +10895,12 @@ msgstr "Uyumlu süreç profilleri" msgid "Printable space" msgstr "Plaka Ayarı" +msgid "Printer Agent" +msgstr "Yazıcı Aracısı" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin. Kullanılabilir aracılar başlangıçta kaydedilir." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10962,9 +11031,6 @@ msgstr "Katman Yüksekliği Sınırları" msgid "Z-Hop" msgstr "Z Sıçraması" -msgid "Retraction when switching material" -msgstr "Malzemeyi Değiştirirken Geri Çekme" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12342,6 +12408,10 @@ msgstr " dışlama alanına çok yakın ve çarpışmalara neden olacak.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " topaklanma algılama alanına çok yakın, çarpışmalar meydana gelecektir.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " yazdırılabilir alanın kısmen dışında ve yazdırılamaz.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Seçilen nozul sıcaklıkları uyumsuz. Her filamentin nozul sıcaklığı, diğer filamentlerin önerilen nozul sıcaklığı aralığında olmalıdır. Aksi hâlde nozul tıkanması veya yazıcıda hasar oluşabilir." @@ -12677,9 +12747,6 @@ msgstr "G-code yerine 3MF kullan" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Yazıcı, baskı işi olarak 3MF dosyası kabul ediyorsa bunu etkinleştirin. Etkinleştirildiğinde Orca Slicer, dilimlenmiş dosyayı düz bir .gcode dosyası yerine .gcode.3mf olarak gönderir." -msgid "Printer Agent" -msgstr "Yazıcı Aracısı" - msgid "Select the network agent implementation for printer communication." msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin." @@ -13362,9 +13429,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "İç köprülerin hızı. Değer yüzde olarak ifade edilirse köprü hızına göre hesaplanacaktır. Varsayılan değer %150’dir." -msgid "Brim width" -msgstr "Kenar genişliği" - msgid "This is the distance from the model to the outermost brim line." msgstr "Modelden en dış kenar çizgisine kadar olan mesafe." @@ -13449,6 +13513,14 @@ msgstr "" "Keskin açılar algılanmadan önce geometri azaltılacaktır. Bu parametre, azaltma için minimum sapma uzunluğunu belirtir.\n" "Devre dışı bırakmak için 0." +# AI Translated +msgid "Brim ears outer only" +msgstr "Kenar kulakları yalnızca dışta" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Fare kulaklarını yalnızca modelin dış konturunda oluşturur, delikleri ve kapalı bölümleri hariç tutar." + msgid "upward compatible machine" msgstr "yukarı doğru uyumlu makine" @@ -14633,6 +14705,14 @@ msgstr "Tpms-fk" msgid "Gyroid" msgstr "Jiroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Dolgu yumuşatma faktörü" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Dolgu köşelerinin ne kadar yuvarlatılacağını belirler. 0% özgün keskin yolu korur, 100% ise komşu dolgu çizgileri arasında mümkün olan en büyük eğrileri üretir." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Üst yüzey dolgusunun hızlandırılması. Daha düşük bir değerin kullanılması üst yüzey kalitesini iyileştirebilir." @@ -15191,6 +15271,14 @@ msgstr "Yazıcının ne tür bir gcode ile uyumlu olduğu." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code yapılandırma bloğunu atla" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "CONFIG_BLOCK bloğunu (dilimleyici yapılandırmasının anahtar/değer çiftlerini) G-code dosyasına yazmaz. Bu, bu yorum satırlarını ayrıştırırken donanım yazılımı çöken yazıcılarda yardımcı olabilir (ör. Anycubic go-klipper). Not: G-code dosyası artık dilimleyici ayarlarını içermeyeceğinden, dosyayı OrcaSlicer'a geri aktarmak yapılandırmayı geri yüklemez." + msgid "Pellet Modded Printer" msgstr "Pelet modlu yazıcı" @@ -16278,6 +16366,14 @@ msgstr "Ekstruder değiştiğinde uzun geri çekilme" msgid "Retraction distance when extruder change" msgstr "Ekstruder değiştiğinde geri çekilme mesafesi" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Geri çekme uzunluğu (Takım değişimi)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Takım değişiminden önce geri çekme tetiklendiğinde, filament belirtilen miktarda geri çekilir (uzunluk, ekstrudere girmeden önce ham filament üzerinde ölçülür)." + msgid "Z-hop height" msgstr "Z-Sıçrama yüksekliği" @@ -16377,6 +16473,10 @@ msgstr "Yeniden başlatma sırasında ekstra uzunluk" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "İlerleme hareketinden sonra geri çekilme telafi edildiğinde, ekstruder bu ek filament miktarını itecektir. Bu ayara nadiren ihtiyaç duyulur." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Yeniden başlatma sırasında ekstra uzunluk (Takım değişimi)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Takım değiştirildikten sonra geri çekilme telafi edildiğinde, ekstruder bu ilave filament miktarını itecektir." @@ -16794,6 +16894,14 @@ msgstr "Silme kulesinde takım değişimi" msgid "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." msgstr "Takım değişimi komutu (Tx) verilmeden önce baskı kafasını silme kulesine gitmeye zorlar. Yalnızca Tip 2 silme kulesi kullanan çok ekstruderli (çok baskı kafalı) yazıcılar için geçerlidir. Orca, çok baskı kafalı makinelerde bu seyahati varsayılan olarak atlar çünkü kafa değişimini ürün yazılımı yönetir; bu da Tx komutunun yazdırılan parçanın üzerinde verilmesine yol açabilir. Takım değişiminin her zaman silme kulesinin üzerinde verilmesini istiyorsanız bu seçeneği etkinleştirin." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Silme kulesinde sıcaklığı bekle" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Yeni takımı baskı sıcaklığına ulaşmasını beklemeden alır, silme kulesine gider ve sıcaklığı orada, yıkamadan hemen önce bekler. Isınma sırasında sızan malzeme modele değil kuleye düşer ve hareket ısınmayla çakışır. Yalnızca 2. tip silme kulesi kullanan çok ekstruderli (çok baskı kafalı) yazıcılar için geçerlidir. Donanım yazılımı veya takım değişimi makrosu sıcaklığı kendisi beklememelidir. Devre dışı bırakıldığında, sıcaklık bekleme komutu takım değişimi komutundan hemen sonra verilir." + msgid "No sparse layers (beta)" msgstr "Seyrek katman yok (beta)" @@ -20078,9 +20186,6 @@ msgstr "Fiziksel Yazıcı" msgid "Print Host upload" msgstr "Yazıcı Bağlantı Ayarları" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin. Kullanılabilir aracılar başlangıçta kaydedilir." - # AI Translated msgid "Select a Flashforge printer" msgstr "Bir Flashforge yazıcısı seçin" @@ -21022,9 +21127,6 @@ msgstr "Giriş yapmaya çalışırken beklenmeyen bir şey oldu, lütfen tekrar msgid "User canceled." msgstr "Kullanıcı iptal edildi." -msgid "Head diameter" -msgstr "Kafa çapı" - msgid "Max angle" msgstr "Maksimum açı" @@ -21753,7 +21855,8 @@ msgstr "" "Baskılarınızı plakalara ayırın\n" "Çok sayıda parçası olan bir modeli baskıya hazır ayrı kalıplara bölebileceğinizi biliyor muydunuz? Bu, tüm parçaları takip etme sürecini basitleştirecektir." -#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer Height] +#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer +#: Height] msgid "" "Speed up your print with Adaptive Layer Height\n" "Did you know that you can print a model even faster by using the Adaptive Layer Height option? Check it out!" @@ -21826,7 +21929,8 @@ msgstr "" "Gücü artırın\n" "Modelin gücünü artırmak için daha fazla duvar halkası ve daha yüksek seyrek dolgu yoğunluğu kullanabileceğinizi biliyor muydunuz?" -#: resources/data/hints.ini: [hint:When do you need to print with the printer door opened] +#: resources/data/hints.ini: [hint:When do you need to print with the printer +#: door opened] msgid "" "When do you need to print with the printer door opened?\n" "Did you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? There is more info about this in the Wiki." @@ -21842,6 +21946,22 @@ msgstr "" "Eğilmeyi önleyin\n" "ABS gibi bükülmeye yatkın malzemelere baskı yaparken, ısıtma yatağı sıcaklığının uygun şekilde arttırılmasının bükülme olasılığını azaltabileceğini biliyor muydunuz?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Katman yüksekliği çok küçük.\n" +#~ "min_layer_height olarak ayarlanacak\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Katman yüksekliği, Yazıcı Ayarları -> Ekstruder -> Katman yüksekliği sınırları bölümündeki sınırı aşıyor bu durum baskı kalitesi sorunlarına neden olabilir." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Ayarlanan aralığa otomatik olarak ayarlansın mı?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kafa çapı" + #~ msgid "Print order within a single layer." #~ msgstr "Tek bir katmanda yazdırma sırası." diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 9204a67ec3..4c3cc1d56c 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk \n" "Language-Team: Ukrainian\n" @@ -4716,6 +4716,23 @@ msgstr "Поточна температура камери вища, ніж бе msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Мінімальна температура камери (%d℃) вища за цільову температуру камери (%d℃). Мінімальне значення — це поріг, за якого починається друк, поки камера продовжує нагріватися до цільової температури, тому воно не повинно її перевищувати. Значення буде обмежено цільовим." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Висота шару занадто мала. Буде встановлено мінімальне значення (%g мм)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Висота шару виходить за межі, задані в Налаштуваннях принтера -> Екструдер -> Ліміти висоти шару, це може призвести до проблем з якістю друку." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Автоматично налаштувати до межі (%g мм)?" + +msgid "Adjust" +msgstr "Налаштувати" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4839,6 +4856,13 @@ msgstr "" "Так - Увімкнути генератор стінок Arachne\n" "Ні - Вимкнути генератор стінок Arachne і встановити режим [Зміщення] для шорсткої поверхні" +# AI Translated +msgid "Brim ear radius" +msgstr "Радіус вушка кайми" + +msgid "Brim width" +msgstr "Ширина кайми" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Спіральний режим працює лише тоді, коли кількість стінок дорівнює 1, підтримки вимкнено, виявлення налипання зондуванням вимкнено, кількість верхніх шарів оболонки дорівнює 0, щільність часткового заповнення дорівнює 0, а тип таймлапсу — традиційний." @@ -5104,6 +5128,14 @@ msgstr "Не вдалося згенерувати калібрувальний msgid "Calibration error" msgstr "Помилка калібрування" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "На цьому принтері не налаштовано обладнання, потрібне для цього елемента керування." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Цей елемент керування не підтримується на цьому принтері." + # AI Translated msgid "Network unavailable" msgstr "Мережа недоступна" @@ -5978,7 +6010,7 @@ msgid "Size:" msgstr "Розмір:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Виявлено конфлікти шляхів G-коду на шарі %d, Z = %.2lf мм. Будь ласка, рознесіть конфліктуючі обʼєкти далі один від одного (%s <-> %s)." @@ -6170,6 +6202,10 @@ msgstr "Багато пристроїв" msgid "Project" msgstr "Проєкт" +# AI Translated +msgid "Device (Web)" +msgstr "Пристрій (Веб)" + msgid "Yes" msgstr "Так" @@ -8306,19 +8342,19 @@ msgstr "Каталог для заміни не вибрано" msgid "Replaced with 3D files from directory:\n" msgstr "Замінено 3D-файлами з каталогу:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Пропущено %s: той самий файл.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Пропущено %s: файл не існує.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Пропущено %s: не вдалося замінити.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Замінено %s.\n" @@ -9069,6 +9105,18 @@ msgstr "З цією опцією ввімкненою, ви можете від msgid "Pop up to select filament grouping mode" msgstr "Показувати вікно вибору режиму групування філаментів" +# AI Translated +msgid "Visible plugin pages" +msgstr "Видимі сторінки плагінів" + +# AI Translated +msgid "pages" +msgstr "стор." + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Кількість сторінок плагінів, що показуються як закріплені вкладки, перш ніж решта сторінок згорнеться у випадний список на останній вкладці." + msgid "Behaviour" msgstr "Поведінка" @@ -9446,6 +9494,18 @@ msgstr "Показати непідтримувані пресети" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Показати несумісні/непідтримувані пресети у випадаючому списку принтера і філаменту. Ці пресети не можна вибрати." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Експериментально) Використовувати агентів принтера замість хостів друку" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Спрямовує завдання друку для принтерів, відмінних від Bambu, через агентів плагінів принтера замість класичного завантаження на хост друку.\n" +"Коли вимкнено, OrcaSlicer використовує попередню поведінку хоста друку." + msgid "Experimental Features" msgstr "Експериментальні функції" @@ -9710,10 +9770,26 @@ msgstr "Пресети користувача" msgid "Preset Inside Project" msgstr "Налаштування проекту всередині" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Копіює в цей пресет усі значення, успадковані від батьківського пресета, і видаляє звʼязок успадкування. Пресети, сумісні лише з батьківським, можуть стати непідтримуваними." + # AI Translated msgid "Detach from parent" msgstr "Відʼєднати від батьківського" +# AI Translated +msgid "Unique preset" +msgstr "Незалежний пресет" + +# AI Translated +msgid "Parent preset" +msgstr "Батьківський пресет" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Цей пресет не успадковується від іншого пресета." + msgid "Name is unavailable." msgstr "Назва недоступна." @@ -10492,22 +10568,6 @@ msgstr "Ви впевнені, що хочете ввімкнути цю опц msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Шаблони заповнення зазвичай розроблені так, щоб автоматично враховувати обертання, забезпечувати належний друк і досягати задуманого ефекту (наприклад, Гіроїд, Кубічний). Обертання поточного шаблону часткового заповнення може призвести до недостатньої підтримки. Дійте обережно та ретельно перевіряйте можливі проблеми друку. Ви впевнені, що хочете увімкнути цю опцію?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Висота шару занадто мала.\n" -"Буде встановлено значення min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Висота шару перевищує ліміт у Налаштуваннях принтера -> Екструдер -> Ліміти висоти шару, це може призвести до проблем з якістю друку." - -msgid "Adjust to the set range automatically?\n" -msgstr "Автоматично налаштувати на встановлений діапазон?\n" - -msgid "Adjust" -msgstr "Налаштувати" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Експериментальна функція: Втягування та відрізання філаменту на більшій відстані під час зміни філаменту для мінімізації промивання. Хоча це може помітно зменшити промивання, це також може підвищити ризик засмічення сопла або інших ускладнень друку." @@ -10711,6 +10771,9 @@ msgstr "Знайдено зарезервовані ключові слова" msgid "Setting Overrides" msgstr "Налаштування перевизначень" +msgid "Retraction when switching material" +msgstr "Втягування під час зміни матеріалу" + msgid "Basic information" msgstr "Базова інформація" @@ -10848,6 +10911,13 @@ msgstr "Сумісні профілі процесів" msgid "Printable space" msgstr "Місце для друку" +msgid "Printer Agent" +msgstr "Агент принтера" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером. Доступні агенти реєструються під час запуску." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10978,9 +11048,6 @@ msgstr "Обмеження висоти шару" msgid "Z-Hop" msgstr "Стрибок-Z" -msgid "Retraction when switching material" -msgstr "Втягування під час зміни матеріалу" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12376,6 +12443,10 @@ msgstr " знаходиться надто близько до зони відч msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " розташовано занадто близько до зони виявлення налипання, і це спричинить зіткнення.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " частково знаходиться за межами області друку, і його неможливо надрукувати.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Вибрані температури сопла несумісні. Температура сопла кожного філаменту має входити в рекомендований діапазон температур сопла інших філаментів. Інакше можливе засмічення сопла або пошкодження принтера." @@ -12722,9 +12793,6 @@ msgstr "Використовувати 3MF замість G-коду" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Увімкніть, якщо принтер приймає файл 3MF як завдання друку. Якщо увімкнено, Orca Slicer надсилає нарізаний файл як .gcode.3mf замість звичайного файлу .gcode." -msgid "Printer Agent" -msgstr "Агент принтера" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером." @@ -13438,9 +13506,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Швидкість внутрішніх мостів. Якщо значення вказано у відсотках, воно буде розраховане на основі bridge_speed. Значення за замовчуванням: 150%." -msgid "Brim width" -msgstr "Ширина кайми" - msgid "This is the distance from the model to the outermost brim line." msgstr "Відстань від моделі до останньої зовнішньої лінії кайми" @@ -13525,6 +13590,14 @@ msgstr "" "Геометрія буде оброблена перед детектуванням гострих кутів. Цей параметр вказує мінімальну довжину відхилення для обробки.\n" "0 для вимкнення" +# AI Translated +msgid "Brim ears outer only" +msgstr "Вушка кайми лише ззовні" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Створювати мишачі вушка лише на зовнішньому контурі моделі, за винятком отворів і замкнених ділянок." + msgid "upward compatible machine" msgstr "висхідна сумісна машина" @@ -14734,6 +14807,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Гіроїд" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Коефіцієнт згладжування часткового заповнення" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Визначає, наскільки сильно заокруглюються кути часткового заповнення. 0% зберігає початкову траєкторію з гострими кутами, а 100% створює максимально можливі заокруглення між сусідніми лініями заповнення." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Прискорення заповнення верхньої поверхні. Використання меншого значенняможе покращити якість верхньої поверхні" @@ -15300,6 +15381,14 @@ msgstr "З яким gcode сумісний принтер" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Пропустити блок конфігурації G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Не записувати CONFIG_BLOCK (пари ключ/значення з конфігурацією слайсера) у файл G-code. Це може допомогти з принтерами, прошивка яких аварійно завершується під час розбору цих рядків коментарів (напр. Anycubic go-klipper). Примітка: файл G-code більше не міститиме налаштувань слайсера, тож зворотний імпорт до OrcaSlicer не відновить конфігурацію." + msgid "Pellet Modded Printer" msgstr "Принтер модифікований гранулами" @@ -16438,6 +16527,14 @@ msgstr "Довге втягування при зміні екструдера" msgid "Retraction distance when extruder change" msgstr "Відстань втягування при зміні екструдера" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Довжина втягування (Зміна інструменту)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Коли втягування спрацьовує перед зміною інструменту, філамент відтягується на вказану величину (довжина вимірюється на необробленому філаменті, до його входу в екструдер)." + msgid "Z-hop height" msgstr "Висота Z-підйому" @@ -16534,6 +16631,10 @@ msgstr "Додаткова довжина під час перезавантаж msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Коли втягування компенсується після переміщення, екструдер проштовхуєЦе додаткова кількість нитки. Ця установка рідко потрібна." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Додаткова довжина під час перезавантаження (Зміна інструменту)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Коли втягування компенсується після заміни інструменту, екструдерпроштовхує цю додаткову кількість нитки." @@ -16960,6 +17061,14 @@ msgstr "Зміна інструмента на вежі протирання" msgid "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." msgstr "Примусово переміщати головку до вежі протирання перед видачею команди зміни інструмента (Tx). Стосується лише багатоекструдерних (багатоінструментальних) принтерів з вежею протирання типу 2. Типово Orca пропускає це переміщення на багатоінструментальних машинах, оскільки заміну головки виконує прошивка, через що команда Tx може бути видана над надрукованою деталлю. Увімкніть цю опцію, якщо хочете, щоб зміна інструмента завжди відбувалася над вежею протирання." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Очікувати температуру на вежі протирання" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Бере новий інструмент, не чекаючи, доки він досягне температури друку, переміщується до вежі протирання й чекає на температуру там, безпосередньо перед промивкою. Матеріал, що витікає під час нагрівання, потрапляє на вежу, а не на модель, а переміщення збігається з нагріванням. Актуально лише для принтерів із кількома екструдерами (кількома головками), які використовують вежу протирання типу 2. Прошивка або макрос зміни інструменту не повинні самі чекати на температуру. Коли вимкнено, команда очікування температури видається одразу після команди зміни інструменту." + msgid "No sparse layers (beta)" msgstr "Без розріджених шарів (бета)" @@ -20304,10 +20413,6 @@ msgstr "Фізичний принтер" msgid "Print Host upload" msgstr "Завантаження хоста друку" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером. Доступні агенти реєструються під час запуску." - msgid "Select a Flashforge printer" msgstr "Вибрати принтер Flashforge" @@ -21181,9 +21286,6 @@ msgstr "Під час спроби входу трапилося щось нес msgid "User canceled." msgstr "Користувача скасовано." -msgid "Head diameter" -msgstr "Діаметр голови" - msgid "Max angle" msgstr "Максимальний кут" @@ -21979,6 +22081,22 @@ msgstr "" "Уникнення деформації\n" "Чи знаєте ви, що при друку матеріалами, схильними до деформації, такими як ABS, відповідне підвищення температури столу може зменшити ймовірність деформації?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Висота шару занадто мала.\n" +#~ "Буде встановлено значення min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Висота шару перевищує ліміт у Налаштуваннях принтера -> Екструдер -> Ліміти висоти шару, це може призвести до проблем з якістю друку." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Автоматично налаштувати на встановлений діапазон?\n" + +#~ msgid "Head diameter" +#~ msgstr "Діаметр голови" + #~ msgid "Print order within a single layer." #~ msgstr "Друк замовлення в один шар" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index e6e7adf43d..00a9a558ba 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -4975,6 +4975,23 @@ msgstr "Nhiệt độ buồng hiện tại cao hơn nhiệt độ an toàn của msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Nhiệt độ buồng tối thiểu (%d℃) cao hơn nhiệt độ buồng mục tiêu (%d℃). Giá trị tối thiểu là ngưỡng để bắt đầu in trong khi buồng vẫn tiếp tục gia nhiệt tới mục tiêu, nên nó không được vượt quá giá trị mục tiêu. Nó sẽ được giới hạn về mức mục tiêu." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Chiều cao lớp quá nhỏ. Nó sẽ được đặt về giá trị tối thiểu (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Chiều cao lớp nằm ngoài giới hạn được đặt trong Cài đặt máy in -> Extruder -> Giới hạn chiều cao lớp, điều này có thể gây ra vấn đề chất lượng in." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Tự động điều chỉnh về giới hạn (%g mm)?" + +msgid "Adjust" +msgstr "Điều chỉnh" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5095,6 +5112,13 @@ msgstr "" "Yes - Bật trình tạo wall Arachne\n" "No - Tắt trình tạo wall Arachne và đặt chế độ [Displacement] của Fuzzy Skin" +# AI Translated +msgid "Brim ear radius" +msgstr "Bán kính tai brim" + +msgid "Brim width" +msgstr "Độ rộng brim" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Chế độ xoắn ốc chỉ hoạt động khi vòng wall bằng 1, support bị tắt, phát hiện vón cục bằng dò bị tắt, số lớp vỏ trên bằng 0, mật độ infill thưa bằng 0 và loại timelapse là truyền thống." @@ -5399,6 +5423,14 @@ msgstr "Không thể tạo G-code hiệu chỉnh" msgid "Calibration error" msgstr "Lỗi hiệu chỉnh" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Máy in này không được cấu hình phần cứng mà điều khiển này cần." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Điều khiển này không được hỗ trợ trên máy in này." + # AI Translated msgid "Network unavailable" msgstr "Mạng không khả dụng" @@ -6317,7 +6349,7 @@ msgstr "Thể tích:" msgid "Size:" msgstr "Kích thước:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Đã tìm thấy xung đột đường đi G-code tại lớp %d, Z = %.2lfmm. Vui lòng tách các vật thể xung đột ra xa hơn (%s <-> %s)." @@ -6516,6 +6548,10 @@ msgstr "Nhiều thiết bị" msgid "Project" msgstr "Dự án" +# AI Translated +msgid "Device (Web)" +msgstr "Thiết bị (Web)" + msgid "Yes" msgstr "Có" @@ -8721,22 +8757,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Đã thay thế bằng file 3D từ thư mục:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Đã bỏ qua %s: cùng một file.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Đã bỏ qua %s: file không tồn tại.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Đã bỏ qua %s: thay thế thất bại.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Đã thay thế %s.\n" @@ -9532,6 +9568,18 @@ msgstr "Với tùy chọn này được bật, bạn có thể gửi tác vụ msgid "Pop up to select filament grouping mode" msgstr "Hiện cửa sổ để chọn chế độ nhóm filament" +# AI Translated +msgid "Visible plugin pages" +msgstr "Số trang plugin hiển thị" + +# AI Translated +msgid "pages" +msgstr "trang" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Số trang plugin được hiển thị dưới dạng tab cố định trước khi các trang còn lại được gom vào danh sách thả xuống ở tab cuối cùng." + # AI Translated msgid "Behaviour" msgstr "Hành vi" @@ -9947,6 +9995,18 @@ msgstr "Hiện cài đặt sẵn không được hỗ trợ" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Hiện các cài đặt sẵn không tương thích/không được hỗ trợ trong danh sách thả xuống máy in và filament. Không thể chọn các cài đặt sẵn này." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Thử nghiệm) Dùng tác nhân máy in thay cho máy chủ in" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Định tuyến các tác vụ in của máy in không phải Bambu qua các tác nhân plugin máy in thay vì luồng tải lên máy chủ in cổ điển.\n" +"Khi tắt, OrcaSlicer sẽ dùng hành vi máy chủ in cũ." + # AI Translated msgid "Experimental Features" msgstr "Tính năng thử nghiệm" @@ -10223,10 +10283,26 @@ msgstr "Preset người dùng" msgid "Preset Inside Project" msgstr "Preset bên trong dự án" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Sao chép tất cả các giá trị kế thừa từ preset cha vào preset này và gỡ bỏ quan hệ kế thừa. Các preset chỉ tương thích với preset cha có thể không còn được hỗ trợ." + # AI Translated msgid "Detach from parent" msgstr "Tách khỏi vật thể cha" +# AI Translated +msgid "Unique preset" +msgstr "Preset độc lập" + +# AI Translated +msgid "Parent preset" +msgstr "Preset cha" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Preset này không kế thừa từ preset khác." + msgid "Name is unavailable." msgstr "Tên không khả dụng." @@ -11026,22 +11102,6 @@ msgstr "Bạn có chắc chắn muốn bật tùy chọn này?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Mẫu infill thường được thiết kế để xử lý xoay tự động nhằm đảm bảo in đúng cách và đạt được hiệu quả dự kiến (ví dụ: Gyroid, Cubic). Xoay mẫu infill thưa hiện tại có thể dẫn đến support không đủ . Vui lòng tiến hành thận trọng và kiểm tra kỹ bất kỳ vấn đề in tiềm ẩn nào. Bạn có chắc chắn muốn bật tùy chọn này?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Chiều cao lớp quá nhỏ.\n" -"Nó sẽ được đặt thành min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Chiều cao lớp vượt quá giới hạn trong Cài đặt máy in -> Extruder -> Giới hạn chiều cao lớp, điều này có thể gây ra vấn đề chất lượng in." - -msgid "Adjust to the set range automatically?\n" -msgstr "Điều chỉnh về phạm vi đặt tự động?\n" - -msgid "Adjust" -msgstr "Điều chỉnh" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Tính năng thử nghiệm: Rút và cắt filament ở khoảng cách lớn hơn trong quá trình thay filament để giảm thiểu xả. Mặc dù có thể giảm đáng kể lượng xả, nó cũng có thể làm tăng nguy cơ tắc đầu phun hoặc các vấn đề in khác." @@ -11235,6 +11295,9 @@ msgstr "Tìm thấy từ khóa dành riêng" msgid "Setting Overrides" msgstr "Ghi đè cài đặt" +msgid "Retraction when switching material" +msgstr "Rút khi chuyển vật liệu" + msgid "Basic information" msgstr "Thông tin cơ bản" @@ -11366,6 +11429,14 @@ msgstr "Hồ sơ quy trình tương thích" msgid "Printable space" msgstr "Không gian in" +# AI Translated +msgid "Printer Agent" +msgstr "Tác nhân máy in" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in. Các tác nhân khả dụng được đăng ký khi khởi động." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -11498,9 +11569,6 @@ msgstr "Giới hạn chiều cao lớp" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Rút khi chuyển vật liệu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12950,6 +13018,10 @@ msgstr " quá gần vùng loại trừ, và sẽ gây va chạm.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ở quá gần vùng phát hiện vón cục, và sẽ gây ra va chạm.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " nằm một phần ngoài vùng in được, và không thể in.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Nhiệt độ đầu phun đã chọn không tương thích. Nhiệt độ đầu phun của mỗi filament phải nằm trong dải nhiệt độ đầu phun được khuyến nghị của các filament còn lại. Nếu không, có thể xảy ra tắc đầu phun hoặc hư hỏng máy in." @@ -13291,10 +13363,6 @@ msgstr "Dùng 3MF thay cho G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Bật tùy chọn này nếu máy in nhận file 3MF làm tác vụ in. Khi bật, Orca Slicer sẽ gửi file đã slice dưới dạng .gcode.3mf thay vì file .gcode thuần." -# AI Translated -msgid "Printer Agent" -msgstr "Tác nhân máy in" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in." @@ -14002,9 +14070,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Tốc độ của cầu bên trong. Nếu giá trị được biểu thị dưới dạng phần trăm, nó sẽ được tính dựa trên bridge_speed. Giá trị mặc định là 150%." -msgid "Brim width" -msgstr "Độ rộng brim" - msgid "This is the distance from the model to the outermost brim line." msgstr "Khoảng cách từ model đến đường brim ngoài cùng." @@ -14088,6 +14153,14 @@ msgstr "" "Hình học sẽ được giảm trước khi phát hiện góc sắc. Tham số này chỉ ra độ dài tối thiểu của độ lệch cho việc giảm.\n" "0 để vô hiệu hóa." +# AI Translated +msgid "Brim ears outer only" +msgstr "Tai brim chỉ ở mặt ngoài" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Chỉ tạo tai chuột trên đường viền ngoài của mô hình, không tính các lỗ và phần khép kín." + msgid "upward compatible machine" msgstr "máy tương thích ngược" @@ -15305,6 +15378,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Hệ số làm mượt infill thưa" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Điều chỉnh mức độ bo tròn các góc của infill thưa. 0% giữ nguyên đường đi sắc cạnh ban đầu, còn 100% tạo ra các đường cong lớn nhất có thể giữa các đường infill liền kề." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Gia tốc của infill bề mặt trên. Sử dụng giá trị thấp hơn có thể cải thiện chất lượng bề mặt trên." @@ -15868,6 +15949,14 @@ msgstr "Loại G-code mà máy in tương thích." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Bỏ qua khối cấu hình G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Không ghi CONFIG_BLOCK (các cặp khóa/giá trị cấu hình của phần mềm slice) vào tệp G-code. Điều này có thể hữu ích với các máy in có firmware bị treo khi phân tích những dòng chú thích này (ví dụ Anycubic go-klipper). Lưu ý: tệp G-code sẽ không còn chứa các thiết lập slice, nên việc nhập lại tệp vào OrcaSlicer sẽ không khôi phục được cấu hình." + msgid "Pellet Modded Printer" msgstr "Máy in Pellet đã chỉnh sửa" @@ -16971,6 +17060,14 @@ msgstr "Rút dài khi đổi extruder" msgid "Retraction distance when extruder change" msgstr "Khoảng cách rút khi đổi extruder" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Độ dài rút (Đổi công cụ)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Khi rút được kích hoạt trước khi đổi công cụ, filament sẽ bị kéo lùi lại theo lượng đã chỉ định (độ dài được đo trên filament thô, trước khi nó đi vào extruder)." + msgid "Z-hop height" msgstr "Chiều cao Z-hop" @@ -17069,6 +17166,10 @@ msgstr "Độ dài bổ sung khi khởi động lại" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Khi rút được bù sau khi di chuyển, extruder sẽ đẩy lượng filament bổ sung này. Cài đặt này hiếm khi cần thiết." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Độ dài bổ sung khi khởi động lại (Đổi công cụ)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Khi rút được bù sau khi thay công cụ, extruder sẽ đẩy lượng filament bổ sung này." @@ -17489,6 +17590,14 @@ msgstr "Đổi công cụ trên wipe tower" msgid "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." msgstr "Buộc đầu công cụ di chuyển đến wipe tower trước khi phát lệnh đổi công cụ (Tx). Chỉ liên quan đến máy in nhiều extruder (nhiều đầu công cụ) dùng wipe tower Loại 2. Theo mặc định, Orca bỏ qua bước di chuyển này trên máy nhiều đầu công cụ vì firmware tự xử lý việc đổi đầu, điều này có thể khiến lệnh Tx được phát ra ngay phía trên phần đang in. Hãy bật tùy chọn này nếu bạn muốn việc đổi công cụ luôn diễn ra phía trên wipe tower." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Chờ nhiệt độ tại wipe tower" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Lấy công cụ mới mà không chờ nó đạt nhiệt độ in, di chuyển đến wipe tower và chờ nhiệt độ tại đó, ngay trước khi xả. Nhựa chảy ra trong lúc gia nhiệt sẽ rơi lên wipe tower thay vì lên mô hình, và quãng di chuyển diễn ra đồng thời với quá trình gia nhiệt. Chỉ áp dụng cho máy in nhiều extruder (nhiều đầu công cụ) dùng wipe tower loại 2. Firmware hoặc macro đổi công cụ không được tự chờ nhiệt độ. Khi tắt, lệnh chờ nhiệt độ sẽ được phát ngay sau lệnh đổi công cụ." + msgid "No sparse layers (beta)" msgstr "Không có lớp thưa (beta)" @@ -20849,10 +20958,6 @@ msgstr "Máy in vật lý" msgid "Print Host upload" msgstr "Tải lên máy chủ in" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in. Các tác nhân khả dụng được đăng ký khi khởi động." - # AI Translated msgid "Select a Flashforge printer" msgstr "Chọn một máy in Flashforge" @@ -21832,9 +21937,6 @@ msgstr "Đã xảy ra điều gì đó không mong đợi khi cố gắng đăng msgid "User canceled." msgstr "Người dùng đã hủy." -msgid "Head diameter" -msgstr "Đường kính đầu" - msgid "Max angle" msgstr "Góc tối đa" @@ -22702,6 +22804,22 @@ msgstr "" "Tránh cong vênh\n" "Bạn có biết rằng khi in vật liệu dễ cong vênh như ABS, tăng nhiệt độ bàn nóng một cách thích hợp có thể giảm xác suất cong vênh không?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Chiều cao lớp quá nhỏ.\n" +#~ "Nó sẽ được đặt thành min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Chiều cao lớp vượt quá giới hạn trong Cài đặt máy in -> Extruder -> Giới hạn chiều cao lớp, điều này có thể gây ra vấn đề chất lượng in." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Điều chỉnh về phạm vi đặt tự động?\n" + +#~ msgid "Head diameter" +#~ msgstr "Đường kính đầu" + #~ msgid "Print order within a single layer." #~ msgstr "Thứ tự in trong một lớp đơn." diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index 345891f250..133e0815a4 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle \n" "Language-Team: \n" @@ -4574,6 +4574,23 @@ msgstr "当前腔体温度高于材料的安全温度,这可能导致材料软 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低机箱温度(%d℃)高于目标机箱温度(%d℃)。最低值是开始打印的阈值,此时机箱会持续朝目标温度加热,因此它不应超过目标值。该值将被限制到目标值。" +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "层高太小,将设置为最小值(%g mm)。" + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "层高超出了打印机设置 -> 挤出机 -> 层高限制中设置的范围,这可能导致打印质量问题。" + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "是否自动调整到限制值(%g mm)?" + +msgid "Adjust" +msgstr "调整" + # AI Translated msgid "" "Layer height too small\n" @@ -4696,6 +4713,13 @@ msgstr "" "是 - 启用Arachne墙生成器\n" "否 - 禁用Arachne墙生成器并将绒毛表面设置为[位移]模式" +# AI Translated +msgid "Brim ear radius" +msgstr "圆盘半径" + +msgid "Brim width" +msgstr "Brim宽度" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "螺旋模式仅在壁环为 1、支撑被禁用、探测结块检测被禁用、顶部壳层为 0、稀疏填充密度为 0 且延时类型为传统时才起作用。" @@ -4950,6 +4974,14 @@ msgstr "生成校准gcode失败" msgid "Calibration error" msgstr "校准错误" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "此打印机未配置该控件所需的硬件。" + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "此打印机不支持该控件。" + # AI Translated msgid "Network unavailable" msgstr "网络不可用" @@ -5807,7 +5839,7 @@ msgstr "体积:" msgid "Size:" msgstr "尺寸:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。" @@ -5988,6 +6020,10 @@ msgstr "多设备" msgid "Project" msgstr "项目" +# AI Translated +msgid "Device (Web)" +msgstr "设备(网页)" + msgid "Yes" msgstr "是" @@ -8028,19 +8064,19 @@ msgstr "未选择替换目录" msgid "Replaced with 3D files from directory:\n" msgstr "替换为目录中的 3D 文件:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ 跳过 %s:同一文件。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ 跳过%s:文件不存在。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ 跳过%s:替换失败。\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ 替换了 %s。\n" @@ -8767,6 +8803,18 @@ msgstr "启用此选项后,您可以同时向多个设备发送任务并管理 msgid "Pop up to select filament grouping mode" msgstr "弹出选择耗材丝分组模式" +# AI Translated +msgid "Visible plugin pages" +msgstr "可见插件页数" + +# AI Translated +msgid "pages" +msgstr "页" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "作为固定标签显示的插件页数量,其余页面将折叠到最后一个标签的下拉菜单中。" + msgid "Behaviour" msgstr "行为" @@ -9121,6 +9169,18 @@ msgstr "显示不受支持的预设" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "在打印机和耗材下拉列表中显示不兼容/不受支持的预设。这些预设无法被选择。" +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(实验性)使用打印机代理替代打印主机" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"非 Bambu 打印机的打印任务将通过打印机插件代理发送,而不是经典的打印主机上传流程。\n" +"禁用时,OrcaSlicer 使用旧的打印主机行为。" + # AI Translated msgid "Experimental Features" msgstr "实验性功能" @@ -9385,9 +9445,25 @@ msgstr "用户预设" msgid "Preset Inside Project" msgstr "项目预设" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "将父预设继承的所有数值复制到当前预设,并解除继承关系。仅与父预设兼容的预设可能会变为不受支持。" + msgid "Detach from parent" msgstr "与父级分离" +# AI Translated +msgid "Unique preset" +msgstr "独立预设" + +# AI Translated +msgid "Parent preset" +msgstr "父预设" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "此预设未继承自其它预设。" + msgid "Name is unavailable." msgstr "名称不可用。" @@ -10093,24 +10169,6 @@ msgstr "您确定要启用此选项吗?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "填充图案通常设计为自动处理旋转,以确保正确打印并实现其预期效果(例如,Gyroid、Cubic)。旋转当前的稀疏填充图案可能会导致支撑不足。请谨慎操作并彻底检查是否存在任何潜在的打印问题。您确定要启用此选项吗?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"层高太小。\n" -"将设置为min_layer_height\n" -"层高太小。\n" -"将自动设置为min_layer_height的值\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "层高超出了打印机设置->挤出机->层高限制中的范围,这可能导致打印质量问题。" - -msgid "Adjust to the set range automatically?\n" -msgstr "是否自动调整到范围内?\n" - -msgid "Adjust" -msgstr "调整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "实验性选项。在更换耗材丝时,将耗材丝回抽一段距离后再切断以最小化冲刷。虽然这可以显著减少冲刷,但也可能增加喷嘴堵塞或其他打印问题的风险。" @@ -10303,6 +10361,9 @@ msgstr "检测到保留的关键字" msgid "Setting Overrides" msgstr "参数覆盖" +msgid "Retraction when switching material" +msgstr "切换材料时的回抽量" + msgid "Basic information" msgstr "基础信息" @@ -10433,6 +10494,12 @@ msgstr "兼容的切片配置" msgid "Printable space" msgstr "可打印区域" +msgid "Printer Agent" +msgstr "打印机代理" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "为打印机通信选择网络代理。可用的代理将在启动时列出。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10558,9 +10625,6 @@ msgstr "层高限制" msgid "Z-Hop" msgstr "Z轴抬升" -msgid "Retraction when switching material" -msgstr "切换材料时的回抽量" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11911,6 +11975,10 @@ msgstr "离不可打印区域太近,会发生碰撞。\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "距离聚集检测区域太近,会引起碰撞。\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "有部分超出可打印区域,无法打印。\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "所选的喷嘴温度不兼容。每种耗材的喷嘴温度都必须落在其他耗材的推荐喷嘴温度范围内。否则可能会发生喷嘴堵塞或打印机损坏。" @@ -12224,9 +12292,6 @@ msgstr "使用 3MF 代替 G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "如果打印机接受 3MF 文件作为打印任务,请启用此选项。启用后,Orca Slicer 将以 .gcode.3mf 格式发送切片文件,而不是普通的 .gcode 文件。" -msgid "Printer Agent" -msgstr "打印机代理" - msgid "Select the network agent implementation for printer communication." msgstr "选择打印机通信的网络代理实施。" @@ -12861,9 +12926,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "内部桥接的速度。如果该值以百分比表示,将基于桥接速度计算。默认值为150%。" -msgid "Brim width" -msgstr "Brim宽度" - msgid "This is the distance from the model to the outermost brim line." msgstr "从模型到最外圈brim走线的距离" @@ -12944,6 +13006,14 @@ msgstr "" "在检测尖锐角度之前,几何形状将被简化。此参数表示简化的最小偏差长度。\n" "设为0以停用" +# AI Translated +msgid "Brim ears outer only" +msgstr "仅外轮廓生成圆盘" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "仅在模型的外轮廓上生成圆盘,不包括孔洞和封闭区域。" + msgid "upward compatible machine" msgstr "向上兼容的机器" @@ -14119,6 +14189,14 @@ msgstr "TPMS-FK结构" msgid "Gyroid" msgstr "螺旋体" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "稀疏填充平滑系数" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "控制稀疏填充拐角的圆滑程度。0% 保持原有的尖锐路径,100% 则在相邻填充线之间生成尽可能大的圆弧。" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "顶面填充的加速度。使用较低值可能会改善顶面质量" @@ -14659,6 +14737,14 @@ msgstr "打印机兼容的G-code风格'" msgid "Klipper" msgstr "Klipper固件" +# AI Translated +msgid "Skip G-code config block" +msgstr "跳过 G-code 配置块" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "不将 CONFIG_BLOCK(切片软件配置的键值对)写入 G-code 文件。这对固件在解析这些注释行时会崩溃的打印机(例如 Anycubic go-klipper)有帮助。注意:G-code 文件将不再包含切片设置,因此重新导入到 OrcaSlicer 时无法恢复配置。" + msgid "Pellet Modded Printer" msgstr "颗粒改装打印机" @@ -15704,6 +15790,14 @@ msgstr "更换挤出机时长回缩" msgid "Retraction distance when extruder change" msgstr "更换挤出机时的回缩距离" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "回抽长度(换工具头)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "在换工具头之前触发回抽时,耗材丝会按指定的长度回抽(长度是在耗材丝进入挤出机之前,以原始耗材丝测量的)。" + msgid "Z-hop height" msgstr "Z抬升高度" @@ -15797,6 +15891,10 @@ msgstr "额外回填长度" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "每当空驶后回抽被补偿时,挤出机将推入额外数量的耗材丝。很少需要此设置。" +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "额外回填长度(换工具头)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "当换色后回抽被补偿时,挤出机将推入额外数量的耗材丝。" @@ -16211,6 +16309,14 @@ msgstr "在擦拭塔上换头" msgid "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." msgstr "在发出换头命令 (Tx) 之前,强制打印头先移动到擦拭塔。仅与使用第 2 类擦拭塔的多挤出机(多打印头)打印机相关。默认情况下,Orca 会在多打印头机器上跳过此移动,因为固件会处理换头,这可能导致 Tx 命令在打印件上方发出。如果您希望换头命令始终在擦拭塔上方发出,请启用此选项。" +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "在擦拭塔上等待温度" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "拾取新工具头后不等待其达到打印温度,直接移动到擦拭塔,并在冲刷前于擦拭塔上等待温度。升温过程中渗出的耗材丝会落在擦拭塔上而不是模型上,且移动时间与加热过程重叠。仅适用于使用 2 型擦拭塔的多挤出机(多工具头)打印机。固件或换工具头宏本身不得等待温度。禁用时,等待温度的指令将在换工具头命令之后立即发出。" + msgid "No sparse layers (beta)" msgstr "无稀疏层 (实验功能)" @@ -19433,9 +19539,6 @@ msgstr "物理打印机" msgid "Print Host upload" msgstr "打印主机上传" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "为打印机通信选择网络代理。可用的代理将在启动时列出。" - msgid "Select a Flashforge printer" msgstr "选择一台 Flashforge 打印机" @@ -20325,9 +20428,6 @@ msgstr "在尝试登录时发生了异常,请重试。" msgid "User canceled." msgstr "用户已取消。" -msgid "Head diameter" -msgstr "Brim 直径" - msgid "Max angle" msgstr "最大角度" @@ -21111,6 +21211,24 @@ msgstr "" "避免翘曲\n" "您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "层高太小。\n" +#~ "将设置为min_layer_height\n" +#~ "层高太小。\n" +#~ "将自动设置为min_layer_height的值\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "层高超出了打印机设置->挤出机->层高限制中的范围,这可能导致打印质量问题。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "是否自动调整到范围内?\n" + +#~ msgid "Head diameter" +#~ msgstr "Brim 直径" + #~ msgid "Print order within a single layer." #~ msgstr "同一层内的打印顺序" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 9b37009978..cf6a2519c3 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-19 14:07-0300\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -4691,6 +4691,23 @@ msgstr "目前列印裝置內部溫度高於線材的安全溫度,可能會導 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低倉室溫度(%d℃)高於目標倉室溫度(%d℃)。最低值是列印開始的門檻,此時倉室會持續朝目標溫度加熱,因此不應超過目標值。系統會將其限制在目標值。" +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "層高過小,將設定為最小值(%g mm)。" + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "層高超出了印表裝置設定 -> 擠出機 -> 層高限制中設定的範圍,這可能會導致列印品質問題。" + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "是否自動調整至限制值(%g mm)?" + +msgid "Adjust" +msgstr "調整" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4825,6 +4842,13 @@ msgstr "" "是 - 啟用 Arachne Wall 產生器\n" "否 - 停用 Arachne Wall 產生器,並將 Fuzzy Skin 設定為 [位移] 模式" +# AI Translated +msgid "Brim ear radius" +msgstr "耳狀 Brim 半徑" + +msgid "Brim width" +msgstr "Brim 寬度" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "花瓶模式僅適用於牆體圈數為 1、停用支撐、停用偵測堵塞、頂部外殼層數為 0、稀疏填充密度為 0,且延時攝影類型為傳統模式時。" @@ -5079,6 +5103,14 @@ msgstr "產生校正代碼失敗" msgid "Calibration error" msgstr "校正錯誤" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "此列印裝置未配置此控制項所需的硬體。" + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "此列印裝置不支援此控制項。" + # AI Translated msgid "Network unavailable" msgstr "網路無法使用" @@ -5936,7 +5968,7 @@ msgstr "體積:" msgid "Size:" msgstr "尺寸:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "發現 G-code 路徑在 %d 層,Z = %.2lf mm 處的衝突。請將有衝突的物件分離得更遠(%s <-> %s)。" @@ -6118,6 +6150,10 @@ msgstr "多臺裝置" msgid "Project" msgstr "專案" +# AI Translated +msgid "Device (Web)" +msgstr "裝置(網頁)" + msgid "Yes" msgstr "是" @@ -8193,19 +8229,19 @@ msgstr "未選擇替換的目錄" msgid "Replaced with 3D files from directory:\n" msgstr "已從目錄替換為 3D 檔案:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ 已跳過 %s:相同檔案。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ 已跳過 %s:檔案不存在。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ 已跳過 %s:無法替換。\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ 已替換 %s。\n" @@ -8940,6 +8976,18 @@ msgstr "啟用時可以同時傳送到並管理多個機臺。" msgid "Pop up to select filament grouping mode" msgstr "彈出視窗選擇線材分組模式" +# AI Translated +msgid "Visible plugin pages" +msgstr "可見的外掛頁面數" + +# AI Translated +msgid "pages" +msgstr "頁" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "以固定分頁顯示的外掛頁面數量,其餘頁面會收合至最後一個分頁的下拉選單中。" + msgid "Behaviour" msgstr "行為" @@ -9294,6 +9342,18 @@ msgstr "顯示不支援的預設" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "在列印裝置和線材下拉選單中顯示不相容/不支援的預設。這些預設無法選取。" +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(實驗性)使用列印裝置代理程式取代列印主機" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"將非 Bambu 列印裝置的列印工作透過列印裝置外掛代理程式傳送,而非傳統的列印主機上傳流程。\n" +"停用時,OrcaSlicer 會使用舊有的列印主機行為。" + # AI Translated msgid "Experimental Features" msgstr "實驗性功能" @@ -9558,9 +9618,25 @@ msgstr "使用者預設" msgid "Preset Inside Project" msgstr "項目預設" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "將父配置繼承的所有數值複製到目前的配置,並解除繼承關係。僅與父配置相容的配置可能會變成不受支援。" + msgid "Detach from parent" msgstr "從父預設分離" +# AI Translated +msgid "Unique preset" +msgstr "獨立配置" + +# AI Translated +msgid "Parent preset" +msgstr "父配置" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "此配置未繼承自其他配置。" + msgid "Name is unavailable." msgstr "名稱不可用。" @@ -10299,22 +10375,6 @@ msgstr "您確認要啟用此選項嗎?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "填充模式通常設計為自動處理旋轉,以確保正確列印並實現其預期效果(例如:Gyroid、Cubic)。旋轉目前的稀疏填充模式可能會導致支撐不足。請謹慎操作,並仔細檢查任何潛在的列印問題。您確定要啟用此選項嗎?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"層高過薄\n" -"將改為 min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "層高超過了印表裝置設定 -> 擠出機 -> 層高限制,這可能會導致列印品質問題。" - -msgid "Adjust to the set range automatically?\n" -msgstr "是否自動調整至設定範圍?\n" - -msgid "Adjust" -msgstr "調整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "實驗性功能:在換線過程中以更大的距離收回並切斷線材,以減少沖洗量。儘管這可以顯著減少沖洗,但也可能增加噴嘴堵塞或其他列印問題的風險。" @@ -10507,6 +10567,9 @@ msgstr "偵測到保留的關鍵字" msgid "Setting Overrides" msgstr "參數覆蓋" +msgid "Retraction when switching material" +msgstr "切換線材時的回抽量" + msgid "Basic information" msgstr "基本資訊" @@ -10637,6 +10700,12 @@ msgstr "相容的切片設定" msgid "Printable space" msgstr "可列印區域" +msgid "Printer Agent" +msgstr "列印裝置代理" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "選擇列印裝置通訊的網路代理實施。可用代理在啟動時註冊。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10762,9 +10831,6 @@ msgstr "層高限制" msgid "Z-Hop" msgstr "Z 軸抬升" -msgid "Retraction when switching material" -msgstr "切換線材時的回抽量" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12113,6 +12179,10 @@ msgstr "離淨空區域太近,會發生碰撞。\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "離堵塞偵測區域太近,會發生碰撞。\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "有部分超出可列印區域,無法列印。\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "所選的噴嘴溫度不相容。每種線材的噴嘴溫度都必須落在其他線材的建議噴嘴溫度範圍內。否則可能會發生噴嘴堵塞或列印裝置損壞。" @@ -12426,9 +12496,6 @@ msgstr "使用 3MF 取代 G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "若列印裝置接受 3MF 檔案作為列印作業,請啟用此選項。啟用後,Orca Slicer 會將切片後的檔案以 .gcode.3mf 形式傳送,而非單純的 .gcode 檔案。" -msgid "Printer Agent" -msgstr "列印裝置代理" - msgid "Select the network agent implementation for printer communication." msgstr "選擇用於列印裝置通訊的網路代理實作。" @@ -13074,9 +13141,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "內部橋接速度。如果該值以百分比表示,將基於 bridge_speed 進行計算。預設值為 150%。" -msgid "Brim width" -msgstr "Brim 寬度" - msgid "This is the distance from the model to the outermost brim line." msgstr "從模型到 Brim 最外圈的距離" @@ -13157,6 +13221,14 @@ msgstr "" "在偵測尖銳角度之前,幾何形狀將被簡化。此參數表示簡化的最小偏差長度。\n" "設為 0 以停用" +# AI Translated +msgid "Brim ears outer only" +msgstr "僅外輪廓產生耳狀 Brim" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "僅在模型的外輪廓上產生耳狀 Brim,不包含孔洞與封閉區域。" + msgid "upward compatible machine" msgstr "向上相容的裝置" @@ -14316,6 +14388,14 @@ msgstr "TPMS-FK結構" msgid "Gyroid" msgstr "螺旋體" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "稀疏填充平滑係數" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "控制稀疏填充轉角的圓滑程度。0% 保持原有的銳利路徑,100% 則在相鄰填充線之間產生盡可能大的圓弧。" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "頂面填充的加速度。使用較低值可能會改善頂面列印品質" @@ -14856,6 +14936,14 @@ msgstr "列印裝置相容的 G-code 樣式" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "略過 G-code 設定區塊" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "不將 CONFIG_BLOCK(切片軟體設定的鍵值對)寫入 G-code 檔案。這對於韌體在解析這些註解行時會當機的列印裝置(例如 Anycubic go-klipper)有幫助。注意:G-code 檔案將不再包含切片設定,因此重新匯入 OrcaSlicer 時無法還原設定。" + msgid "Pellet Modded Printer" msgstr "顆粒改裝列印裝置" @@ -15909,6 +15997,14 @@ msgstr "更換擠出機時長回抽" msgid "Retraction distance when extruder change" msgstr "更換擠出機時的回抽距離" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "回抽長度(換工具)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "在換工具之前觸發回抽時,線材會依指定的長度回抽(長度是在線材進入擠出機之前,以原始線材測量)。" + msgid "Z-hop height" msgstr "Z 抬升高度" @@ -16002,6 +16098,10 @@ msgstr "額外回填長度" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "每當空駛後回抽被補償時,擠出機將推入額外長度的線材。很少需要此設定。" +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "額外回填長度(換工具)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "當換色後回抽被補償時,擠出機將推入額外長度的線材。" @@ -16405,6 +16505,14 @@ msgstr "在換料塔上換刀" msgid "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." msgstr "強制工具頭在發出換刀指令 (Tx) 之前先移動到換料塔。僅適用於使用 Type 2 換料塔的多擠出機(多工具頭)列印裝置。預設情況下,Orca 會在多工具頭機器上略過此空駛,因為韌體會處理工具頭交換,這可能導致 Tx 指令在已列印零件上方發出。若您希望換刀一律改在換料塔上方發出,請啟用此選項。" +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "在換料塔上等待溫度" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "取用新工具時不等待其達到列印溫度,先移動到換料塔,並在清理前於換料塔上等待溫度。升溫過程中滲出的線材會落在換料塔上而非模型上,且移動時間與加熱過程重疊。僅適用於使用第 2 型換料塔的多擠出機(多工具頭)列印裝置。韌體或換工具巨集本身不得等待溫度。停用時,等待溫度的指令會在換工具命令之後立即發出。" + msgid "No sparse layers (beta)" msgstr "取消稀疏層(Beta)" @@ -19622,9 +19730,6 @@ msgstr "實體列印裝置" msgid "Print Host upload" msgstr "列印主機上傳" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "選擇列印裝置通訊的網路代理實施。可用代理在啟動時註冊。" - msgid "Select a Flashforge printer" msgstr "選取 Flashforge 列印裝置" @@ -20516,9 +20621,6 @@ msgstr "嘗試登入時發生了意外錯誤,請再試一次。" msgid "User canceled." msgstr "使用者取消。" -msgid "Head diameter" -msgstr "頭直徑" - msgid "Max angle" msgstr "最大角度" @@ -21323,6 +21425,22 @@ msgstr "" "避免翹曲\n" "您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "層高過薄\n" +#~ "將改為 min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "層高超過了印表裝置設定 -> 擠出機 -> 層高限制,這可能會導致列印品質問題。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "是否自動調整至設定範圍?\n" + +#~ msgid "Head diameter" +#~ msgstr "頭直徑" + #~ msgid "Print order within a single layer." #~ msgstr "每一層的列印順序" From ba229739198dfbb2b4982a57e1f6c4195ea47f11 Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:34:12 -0300 Subject: [PATCH 65/71] Revert "Fix assembly parts omitted by height range modifiers" (#15301) --- src/libslic3r/PrintApply.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 6d5dbb05f5..e2e9bc737d 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -559,11 +559,9 @@ static inline bool model_volume_solid_or_modifier(const ModelVolume &mv) static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo) { - // Orca: Keep the volume's local XY offset for multipart overlap checks, but remove the object's bed placement. - Transform3d object_trafo_local = object_trafo; - object_trafo_local.translation().x() = 0.; - object_trafo_local.translation().y() = 0.; - Transform3d m = object_trafo_local * volume_trafo; + Transform3d m = object_trafo * volume_trafo; + m.translation().x() = 0.; + m.translation().y() = 0.; return m.cast(); } From aaa8e98bb0ead79d5edc9c368dd8b80201ff14ea Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Thu, 20 Aug 2026 09:16:02 -0300 Subject: [PATCH 66/71] Time estimator fixes (#15304) * Plan corners with junction deviation where the firmware uses it The time estimator only ever had the classic per-axis jerk model, which limits a corner by the largest single-axis component of the velocity change. That is anisotropic: the same corner is allowed sqrt(2) more speed on a diagonal than on an axis, which paints a four-lobed ripple around every circular wall in the actual speed and actual flow views, worst on small parts whose walls are made of short segments. Klipper has no classic jerk at all and Marlin 2 has none while M205 J is in use; both plan corners with junction deviation, which sees only the corner angle. Add that model and use it for those machines: - Klipper: derived from the square corner velocity, as the firmware does (jd = scv^2 * (sqrt(2) - 1) / max_accel), reading the scv from machine_max_jerk_x, where process_SET_VELOCITY_LIMIT() already stores SQUARE_CORNER_VELOCITY. - Marlin 2: machine_max_junction_deviation, which was already loaded into the machine limits but never reached the planner. - Every other flavor keeps the classic jerk path unchanged. The model has no per-axis jerk floor, so this also drops the hard slow spot the estimator drew at the start of every loop from machine_max_jerk_e. Toolpaths are unaffected: on a full export the only lines that change are M73. The junction deviation maths, including Marlin's JD_HANDLE_SMALL_SEGMENTS arc approximation, is ported from PrusaSlicer's src/libslic3r/GCode/GCodeProcessor.cpp. The Klipper mapping is not in PrusaSlicer, which ignores SET_VELOCITY_LIMIT. * Add tests for junction deviation corner planning Cover the three properties the change rests on: - a right angle on Klipper is planned at exactly the square corner velocity, the identity that makes the scv to junction deviation mapping correct, and a shallow corner is planned far faster than per-axis jerk allows; - junction deviation gives the same speed whatever the corner's orientation, while classic jerk keeps its sqrt(2) spread, which is the four-lobed ripple; - machines that do not plan with junction deviation are provably untouched, including a Marlin 2 printer that has it disabled. --- src/libslic3r/GCode/GCodeProcessor.cpp | 149 +++++++++++++++++++--- src/libslic3r/GCode/GCodeProcessor.hpp | 14 +++ tests/fff_print/test_gcode_timing.cpp | 164 +++++++++++++++++++++++++ 3 files changed, 307 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index cebfe486cb..93621648ff 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -298,6 +298,7 @@ void GCodeProcessor::TimeMachine::State::reset() //BBS enter_direction = { 0.0f, 0.0f, 0.0f }; exit_direction = { 0.0f, 0.0f, 0.0f }; + jd_unit_vec = { 0.0f, 0.0f, 0.0f, 0.0f }; } void GCodeProcessor::TimeMachine::CustomGCodeTime::reset() @@ -5036,6 +5037,10 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; + curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]) * inv_distance, + static_cast(delta_pos[Y]) * inv_distance, + static_cast(delta_pos[Z]) * inv_distance, + static_cast(delta_pos[E]) * inv_distance); TimeBlock block; block.move_type = type; @@ -5118,22 +5123,32 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes block.acceleration = acceleration; - // calculates block exit feedrate - curr.safe_feedrate = block.feedrate_profile.cruise; + static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; + const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD; - for (unsigned char a = X; a <= E; ++a) { - float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); - if (curr.abs_axis_feedrate[a] > axis_max_jerk) - curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + // Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J). + // Negative leaves the classic jerk path below unchanged. + const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move, + static_cast(i)); + const bool use_junction_deviation = vmax_junction_jd >= 0.0f; + + // calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is + // free to start from rest. + curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise; + + if (!use_junction_deviation) { + for (unsigned char a = X; a <= E; ++a) { + float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); + if (curr.abs_axis_feedrate[a] > axis_max_jerk) + curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + } } block.feedrate_profile.exit = curr.safe_feedrate; - static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; - // calculates block entry feedrate - float vmax_junction = curr.safe_feedrate; - if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { + float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate; + if (!use_junction_deviation && has_prev_move) { bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. @@ -5400,6 +5415,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; + curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]) * inv_distance, + static_cast(delta_pos[Y]) * inv_distance, + static_cast(delta_pos[Z]) * inv_distance, + static_cast(delta_pos[E]) * inv_distance); TimeBlock block; block.move_type = type; @@ -5480,22 +5499,32 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) block.acceleration = acceleration; - // calculates block exit feedrate - curr.safe_feedrate = block.feedrate_profile.cruise; + static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; + const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD; - for (unsigned char a = X; a <= E; ++a) { - float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); - if (curr.abs_axis_feedrate[a] > axis_max_jerk) - curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + // Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J). + // Negative leaves the classic jerk path below unchanged. + const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move, + static_cast(i)); + const bool use_junction_deviation = vmax_junction_jd >= 0.0f; + + // calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is + // free to start from rest. + curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise; + + if (!use_junction_deviation) { + for (unsigned char a = X; a <= E; ++a) { + float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); + if (curr.abs_axis_feedrate[a] > axis_max_jerk) + curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + } } block.feedrate_profile.exit = curr.safe_feedrate; - static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; - // calculates block entry feedrate - float vmax_junction = curr.safe_feedrate; - if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { + float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate; + if (!use_junction_deviation && has_prev_move) { bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. @@ -7168,6 +7197,86 @@ float GCodeProcessor::get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeM return get_axis_max_jerk_with_jd(mode, axis, get_acceleration(mode)); } +float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const +{ + const size_t id = static_cast(mode); + + // Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel + // (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel + // in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner. + if (m_flavor == gcfKlipper) { + // machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it. + const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id); + if (scv <= 0.0f || acceleration <= 0.0f) + return 0.0f; + return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration; + } + + // Marlin 2 plans with junction deviation only when M205 J > 0; classic jerk leaves it at 0. + if (m_flavor == gcfMarlinFirmware) + return get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, id); + + return 0.0f; +} + +float GCodeProcessor::calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec, + PrintEstimatedStatistics::ETimeMode mode) const +{ + float junction_acceleration = block.acceleration; + for (unsigned char a = X; a <= E; ++a) { + if (junction_unit_vec[a] == 0.0f) + continue; + const float axis_max_acceleration = get_axis_max_acceleration(mode, static_cast(a), m_machine_config_idx); + if (axis_max_acceleration > 0.0f) + junction_acceleration = std::min(junction_acceleration, std::abs(axis_max_acceleration / junction_unit_vec[a])); + } + return junction_acceleration; +} + +// Ported from PrusaSlicer (src/libslic3r/GCode/GCodeProcessor.cpp). +float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev, + const TimeMachine::State& curr, bool has_prev_move, + PrintEstimatedStatistics::ETimeMode mode) const +{ + const float junction_deviation = get_junction_deviation(mode, block.acceleration); + if (junction_deviation <= 0.0f) + return -1.0f; // classic jerk machine, the caller keeps its own computation + if (!has_prev_move) + return 0.0f; // starts from rest, the planner raises this on the reverse pass + + // -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin(). + float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec); + if (junction_cos_theta > 0.999999f) + return 0.0f; // the path doubles back, the machine has to stop + junction_cos_theta = std::max(junction_cos_theta, -0.999999f); // guards the division below + + const float sin_theta_d2 = std::sqrt(0.5f * (1.0f - junction_cos_theta)); // always positive + const Vec4f junction_vec = curr.jd_unit_vec - prev.jd_unit_vec; + const float junction_vec_norm = junction_vec.norm(); + const Vec4f junction_unit_vec = (junction_vec_norm > 0.0f) ? Vec4f(junction_vec / junction_vec_norm) + : Vec4f(0.0f, 0.0f, 0.0f, 0.0f); + const float junction_acceleration = calc_junction_acceleration(block, junction_unit_vec, mode); + + float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2); + + // Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and + // capped by the centripetal acceleration it needs. Klipper has no equivalent. + if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) { + // Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin: + // https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html + const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f; + const float t = neg * junction_cos_theta; + const float asinx = 0.032843707f + t * (-1.451838349f + t * (29.66153956f + t * (-131.1123477f + + t * (262.8130562f + t * (-242.7199627f + t * (84.31466202f)))))); + const float junction_theta = float(0.5 * M_PI) + neg * asinx; // acos(-t), bottoms out at 0.033 + vmax_junction_sqr = std::min(vmax_junction_sqr, (block.distance * junction_acceleration) / junction_theta); + } + + // Never faster than either of the two moves the junction joins. + vmax_junction_sqr = std::min(vmax_junction_sqr, std::min(sqr(block.feedrate_profile.cruise), sqr(prev.feedrate))); + return std::sqrt(vmax_junction_sqr); +} + float GCodeProcessor::get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const { const size_t id = static_cast(mode); diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f5bec9e826..e968986695 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -637,6 +637,10 @@ class Print; //For line move, there are same. For arc move, there are different. Vec3f enter_direction; Vec3f exit_direction; + // Orca: move direction over all four axes, scaled by 1 / block.distance. Used by + // calc_vmax_junction_deviation(), which needs E to see extrusion-rate changes + // between collinear moves the way Marlin and Klipper do. + Vec4f jd_unit_vec; void reset(); }; @@ -1488,6 +1492,16 @@ class Print; float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const; float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const; float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const; + // Orca: junction deviation for a block at the given acceleration, 0 for a classic jerk machine. + float get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const; + // Orca: acceleration along the junction direction, clamped by the per axis limits. + float calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec, + PrintEstimatedStatistics::ETimeMode mode) const; + // Orca: entry speed from the junction deviation model, which limits a corner by its angle alone + // and is therefore isotropic, unlike per axis jerk. Negative means classic jerk applies instead. + float calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev, + const TimeMachine::State& curr, bool has_prev_move, + PrintEstimatedStatistics::ETimeMode mode) const; float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const; Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const; float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const; diff --git a/tests/fff_print/test_gcode_timing.cpp b/tests/fff_print/test_gcode_timing.cpp index 4570f253bb..9802bcc8f7 100644 --- a/tests/fff_print/test_gcode_timing.cpp +++ b/tests/fff_print/test_gcode_timing.cpp @@ -7,9 +7,14 @@ #include "test_utils.hpp" +#include #include +#include #include #include +#include +#include +#include using namespace Slic3r; using Catch::Matchers::WithinAbs; @@ -418,3 +423,162 @@ TEST_CASE("Per-slot machine limits follow the active nozzle", "[GCodeTiming][Mul REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 200.0, 0.10)); } } + +// Junction planning decides the speeds the "actual speed" / "actual flow" preview shows. Per-axis +// jerk limits a corner by the largest single-axis component of the velocity change, allowing sqrt(2) +// more speed on a diagonal than on an axis -- a four-lobed ripple around every circle. Klipper and +// Marlin 2 with M205 J plan with junction deviation instead, which sees only the corner angle. +namespace { + +// One acceleration everywhere and axis limits far above it, so only the junction model under test +// can slow a corner down. +FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity, double junction_deviation) +{ + FullPrintConfig config; + config.gcode_flavor.value = flavor; + config.filament_diameter.values = {1.75}; + config.filament_map.values = {1}; + + const std::vector accel = {1000.0, 1000.0}; + const std::vector axis = {20000.0, 20000.0}; + const std::vector speed = {500.0, 500.0}; + config.machine_max_acceleration_extruding.values = accel; + config.machine_max_acceleration_travel.values = accel; + config.machine_max_acceleration_retracting.values = accel; + config.machine_max_acceleration_x.values = axis; + config.machine_max_acceleration_y.values = axis; + config.machine_max_acceleration_z.values = axis; + config.machine_max_acceleration_e.values = axis; + config.machine_max_speed_x.values = speed; + config.machine_max_speed_y.values = speed; + config.machine_max_speed_z.values = speed; + config.machine_max_speed_e.values = speed; + // Klipper reads this as the square corner velocity, Marlin as classic jerk. + config.machine_max_jerk_x.values = {corner_velocity, corner_velocity}; + config.machine_max_jerk_y.values = {corner_velocity, corner_velocity}; + config.machine_max_jerk_z.values = {corner_velocity, corner_velocity}; + // Kept out of the way so it never binds in the classic-jerk comparisons. + config.machine_max_jerk_e.values = {100.0, 100.0}; + config.machine_max_junction_deviation.values = {junction_deviation, junction_deviation}; + config.machine_min_extruding_rate.values = {0.0, 0.0}; + config.machine_min_travel_rate.values = {0.0, 0.0}; + return config; +} + +constexpr double junction_x = 60.0; +constexpr double junction_y = 60.0; + +// Two 40mm travels meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`. +// 40mm is long enough to reach the commanded 150mm/s and brake back to any corner speed these tests +// produce. Travels (no E) keep the junction vector purely geometric, as the formulas below assume. +std::string corner_gcode(double turn_deg, double orientation_deg) +{ + const double len = 40.0; + const double a_in = orientation_deg * M_PI / 180.0; + const double a_out = (orientation_deg + turn_deg) * M_PI / 180.0; + + std::ostringstream os; + os << std::fixed << std::setprecision(4) + << "M83\n" + << "G1 Z0.2 F1200\n" + << "G1 X" << junction_x - len * std::cos(a_in) << " Y" << junction_y - len * std::sin(a_in) << " F6000\n" + << "G1 X" << junction_x << " Y" << junction_y << " F9000\n" + << "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) << " F9000\n"; + return os.str(); +} + +// Speed allowed through the corner: the vertex ending the incoming move carries that block's exit +// speed, and the vertices the actual-speed pass inserts are all strictly interior. +double corner_speed(const GCodeProcessorResult& r) +{ + for (const auto& mv : r.moves) + if (mv.type == EMoveType::Travel && + std::abs(mv.position.x() - junction_x) < 1e-3 && + std::abs(mv.position.y() - junction_y) < 1e-3) + return mv.actual_feedrate; + return -1.0; +} + +double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation, + double turn_deg, double orientation_deg = 0.0) +{ + GCodeProcessor proc; + run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation), + corner_gcode(turn_deg, orientation_deg).c_str()); + return corner_speed(proc.get_result()); +} + +} // namespace + +TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity", + "[GCodeTiming][JunctionDeviation]") +{ + // jd = scv^2 * (sqrt(2) - 1) / max_accel, then v^2 = jd * accel * sin(t/2) / (1 - sin(t/2)). + // The acceleration cancels: the corner speed depends only on the scv and the angle. + const double scv = 5.0; + + SECTION("a right angle is taken at exactly the square corner velocity") { + // sin(t/2) = sqrt(0.5) at 90 degrees, so v == scv -- the definition of the square corner + // velocity, and what makes the mapping above the right one. + REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, 90.0), Catch::Matchers::WithinRel(scv, 0.02)); + } + + SECTION("a shallow corner is taken far faster than the per-axis jerk model allows") { + // 6 degrees: sin(t/2) = cos(3 deg), so v = 5 * sqrt((sqrt(2) - 1) * 728.68) = 86.9mm/s. Per-axis + // jerk ignores the angle and caps the velocity *change* (2v*sin(3 deg)), giving 47.8mm/s. + const double jd_speed = planned_corner_speed(gcfKlipper, scv, 0.0, 6.0); + const double jerk_speed = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, 6.0); + REQUIRE_THAT(jd_speed, Catch::Matchers::WithinRel(86.87, 0.02)); + REQUIRE_THAT(jerk_speed, Catch::Matchers::WithinRel(47.75, 0.02)); + } +} + +TEST_CASE("Junction deviation limits a corner by its angle alone, not by its orientation", + "[GCodeTiming][JunctionDeviation]") +{ + // The four-lobed ripple on circular walls is per-axis jerk being anisotropic: a velocity change + // lying on an axis gets sqrt(2) less headroom than the same change on the diagonal. + const double scv = 5.0; + const double turn = 6.0; + + SECTION("Klipper plans both orientations identically") { + const double on_axis = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0); + const double diagonal = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 45.0); + REQUIRE(on_axis > 0.0); + REQUIRE_THAT(diagonal, Catch::Matchers::WithinRel(on_axis, 0.02)); + } + + SECTION("the classic jerk model keeps its orientation dependence") { + const double on_axis = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 0.0); + const double diagonal = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 45.0); + REQUIRE(on_axis > 0.0); + REQUIRE(diagonal / on_axis > 1.2); + } +} + +TEST_CASE("Junction deviation is only used where the firmware actually plans with it", + "[GCodeTiming][JunctionDeviation]") +{ + const double jerk = 5.0; + + SECTION("Marlin 2 with M205 J disabled keeps the classic jerk planning") { + // machine_max_junction_deviation == 0 is how a Marlin 2 printer says it runs classic jerk. + const double classic = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0); + REQUIRE(classic > 0.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.0, 90.0), + Catch::Matchers::WithinRel(classic, 1e-4)); + } + + SECTION("Marlin 2 with M205 J enabled switches to junction deviation") { + // sqrt(1000 * 0.05 * 2.4142136) = 11.0mm/s, independent of the jerk values it no longer reads. + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.05, 90.0), + Catch::Matchers::WithinRel(10.99, 0.02)); + } + + SECTION("machines without junction deviation are untouched by the jerk values it would ignore") { + // A flavor that never enters the junction deviation path must ignore the setting entirely. + const double without = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinLegacy, jerk, 0.05, 90.0), + Catch::Matchers::WithinRel(without, 1e-4)); + } +} From aa233a82a5a5defa32d790588428a2456bbcfa89 Mon Sep 17 00:00:00 2001 From: pbannykh Date: Fri, 21 Aug 2026 01:47:38 +0500 Subject: [PATCH 67/71] fix: pass douglas_peucker tolerance in scaled units so the cancel-object outline is actually simplified (#15291) Co-authored-by: bannykh --- src/libslic3r/Print.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1af28255ee..509744abe2 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -5827,7 +5827,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const { Polygon PrintInstance::get_convex_hull_2d() { Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); - poly.douglas_peucker(0.1); + poly.douglas_peucker(scale_(0.1)); return poly; } From 87ca2bc42ed15772677f310f87d536a52eee3e92 Mon Sep 17 00:00:00 2001 From: Valerii Bokhan <80919135+valerii-bokhan@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:03:38 +0200 Subject: [PATCH 68/71] Fix unstable contours from triangulated planar faces (#15313) --- src/libslic3r/TriangleMeshSlicer.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 2c1c0da23f..738965d75b 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -1461,6 +1461,13 @@ static Polygons make_loops( chain_open_polylines_close_gaps(open_polylines, loops, max_gap, true); #endif + // Orca: A planar quad represented by two triangles contributes a point where the + // slicing plane crosses the shared diagonal. After rounding to coord_t this + // point may be very slightly off the otherwise straight contour edge. Apart + // from being redundant, such points make the subsequent contour + // simplification depend on the slice height (and may move seam candidates). + remove_collinear(loops); + #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { static int iRun = 0; From ca65f0fd8e657cf99c9cf80a035244a4f0f12efe Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Thu, 20 Aug 2026 18:50:23 -0300 Subject: [PATCH 69/71] Normalize the junction direction vector over XYZE (#15308) * Normalize the junction direction vector over XYZE calc_vmax_junction_deviation() treats the dot product of two jd_unit_vec as a cosine, but the vectors were scaled by 1 / block.distance, which is the XYZ length. On an extruding move the E component then pushes the 4D norm above 1 and the dot product below -1, so the corner reads as straighter than it is and is planned too fast -- the more so the higher the flow. Measured on a 6 degree corner at scv 5: 86.9mm/s with no extrusion, 94.4mm/s at 0.029mm/mm, 150.0mm/s at 0.1mm/mm. Neither firmware does that. Marlin normalizes over XYZE for any extruding move (planner.cpp: `if (... || esteps > 0) normalize_junction_vector(unit_vec)`) and Klipper leaves E out of the cosine entirely, dotting only axes_r[0..2] (toolhead.py::Move.calc_junction). Normalizing satisfies both: with E normalized in, the cosine differs from the XYZ-only one by ~1e-5 at printing flow rates. This is a deliberate divergence from PrusaSlicer, which still scales by 1 / distance -- it carries an older Marlin's behaviour. Travel moves are unaffected, their vector was already unit length. Reported by Copilot in review of #15304. * Test that extrusion rate does not change corner planning The junction deviation tests were all travel-only, which is exactly why the E component of the junction vector went unchecked. Cover it: the same corner has to be planned the same whether nothing, an ordinary 0.42 x 0.2 line, or a fat large-nozzle line is extruded through it, on both Klipper and Marlin 2. Reported by Copilot in review of #15304. --- src/libslic3r/GCode/GCodeProcessor.cpp | 21 ++++++----- src/libslic3r/GCode/GCodeProcessor.hpp | 5 ++- tests/fff_print/test_gcode_timing.cpp | 48 +++++++++++++++++++++----- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 93621648ff..b13273d696 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -5037,10 +5037,10 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; - curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]) * inv_distance, - static_cast(delta_pos[Y]) * inv_distance, - static_cast(delta_pos[Z]) * inv_distance, - static_cast(delta_pos[E]) * inv_distance); + curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]), + static_cast(delta_pos[Y]), + static_cast(delta_pos[Z]), + static_cast(delta_pos[E])).normalized(); TimeBlock block; block.move_type = type; @@ -5415,10 +5415,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; - curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]) * inv_distance, - static_cast(delta_pos[Y]) * inv_distance, - static_cast(delta_pos[Z]) * inv_distance, - static_cast(delta_pos[E]) * inv_distance); + curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]), + static_cast(delta_pos[Y]), + static_cast(delta_pos[Z]), + static_cast(delta_pos[E])).normalized(); TimeBlock block; block.move_type = type; @@ -7245,6 +7245,11 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const return 0.0f; // starts from rest, the planner raises this on the reverse pass // -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin(). + // Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance + // instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter + // than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0) + // and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree + // that the corner is planned by its geometry, and normalizing matches them to within 1e-5. float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec); if (junction_cos_theta > 0.999999f) return 0.0f; // the path doubles back, the machine has to stop diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e968986695..505f7c06a0 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -637,9 +637,8 @@ class Print; //For line move, there are same. For arc move, there are different. Vec3f enter_direction; Vec3f exit_direction; - // Orca: move direction over all four axes, scaled by 1 / block.distance. Used by - // calc_vmax_junction_deviation(), which needs E to see extrusion-rate changes - // between collinear moves the way Marlin and Klipper do. + // Orca: move direction over all four axes, unit length. Used by + // calc_vmax_junction_deviation(); see there for why E is normalized in. Vec4f jd_unit_vec; void reset(); diff --git a/tests/fff_print/test_gcode_timing.cpp b/tests/fff_print/test_gcode_timing.cpp index 9802bcc8f7..8c08fc4f03 100644 --- a/tests/fff_print/test_gcode_timing.cpp +++ b/tests/fff_print/test_gcode_timing.cpp @@ -468,22 +468,27 @@ FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity, constexpr double junction_x = 60.0; constexpr double junction_y = 60.0; -// Two 40mm travels meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`. +// Two 40mm moves meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`. // 40mm is long enough to reach the commanded 150mm/s and brake back to any corner speed these tests -// produce. Travels (no E) keep the junction vector purely geometric, as the formulas below assume. -std::string corner_gcode(double turn_deg, double orientation_deg) +// produce. `e_per_mm` of zero makes them travels, which keeps the junction vector purely geometric +// as the formulas below assume. +std::string corner_gcode(double turn_deg, double orientation_deg, double e_per_mm = 0.0) { const double len = 40.0; const double a_in = orientation_deg * M_PI / 180.0; const double a_out = (orientation_deg + turn_deg) * M_PI / 180.0; + std::ostringstream extrude; + if (e_per_mm > 0.0) + extrude << std::fixed << std::setprecision(4) << " E" << len * e_per_mm; std::ostringstream os; os << std::fixed << std::setprecision(4) << "M83\n" << "G1 Z0.2 F1200\n" << "G1 X" << junction_x - len * std::cos(a_in) << " Y" << junction_y - len * std::sin(a_in) << " F6000\n" - << "G1 X" << junction_x << " Y" << junction_y << " F9000\n" - << "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) << " F9000\n"; + << "G1 X" << junction_x << " Y" << junction_y << extrude.str() << " F9000\n" + << "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) + << extrude.str() << " F9000\n"; return os.str(); } @@ -492,7 +497,7 @@ std::string corner_gcode(double turn_deg, double orientation_deg) double corner_speed(const GCodeProcessorResult& r) { for (const auto& mv : r.moves) - if (mv.type == EMoveType::Travel && + if ((mv.type == EMoveType::Travel || mv.type == EMoveType::Extrude) && std::abs(mv.position.x() - junction_x) < 1e-3 && std::abs(mv.position.y() - junction_y) < 1e-3) return mv.actual_feedrate; @@ -500,11 +505,11 @@ double corner_speed(const GCodeProcessorResult& r) } double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation, - double turn_deg, double orientation_deg = 0.0) + double turn_deg, double orientation_deg = 0.0, double e_per_mm = 0.0) { GCodeProcessor proc; run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation), - corner_gcode(turn_deg, orientation_deg).c_str()); + corner_gcode(turn_deg, orientation_deg, e_per_mm).c_str()); return corner_speed(proc.get_result()); } @@ -582,3 +587,30 @@ TEST_CASE("Junction deviation is only used where the firmware actually plans wit Catch::Matchers::WithinRel(without, 1e-4)); } } + +TEST_CASE("How fast a corner is taken does not depend on how much is extruded through it", + "[GCodeTiming][JunctionDeviation]") +{ + // The junction cosine is taken over XYZE, so the direction vectors have to be unit length or the + // E term makes the two paths look more parallel than they are and the corner comes out too fast, + // the more so the higher the flow. Marlin normalizes over XYZE on any extruding move + // (planner.cpp, esteps > 0) and Klipper leaves E out of the cosine altogether + // (toolhead.py::Move.calc_junction); on both, this corner is planned by its geometry alone. + const double scv = 5.0; + const double turn = 6.0; + const double geometric = planned_corner_speed(gcfKlipper, scv, 0.0, turn); + REQUIRE(geometric > 0.0); + + // 0.029mm/mm is an ordinary 0.42 x 0.2 line on 1.75mm filament; 0.1 is a fat large-nozzle one. + // Unnormalized these came out at 94.4 and 150.0mm/s against a geometric 86.9. + for (double e_per_mm : {0.029, 0.1}) + REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0, e_per_mm), + Catch::Matchers::WithinRel(geometric, 0.02)); + + SECTION("and the same holds on Marlin 2") { + const double marlin = planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn); + REQUIRE(marlin > 0.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn, 0.0, 0.029), + Catch::Matchers::WithinRel(marlin, 0.02)); + } +} From 6ef02a67dbb22ae1a019d9f485f46bfc3e1b44aa Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:35:28 -0300 Subject: [PATCH 70/71] Revert "Fix unstable contours from triangulated planar faces" (#15315) --- src/libslic3r/TriangleMeshSlicer.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 738965d75b..2c1c0da23f 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -1461,13 +1461,6 @@ static Polygons make_loops( chain_open_polylines_close_gaps(open_polylines, loops, max_gap, true); #endif - // Orca: A planar quad represented by two triangles contributes a point where the - // slicing plane crosses the shared diagonal. After rounding to coord_t this - // point may be very slightly off the otherwise straight contour edge. Apart - // from being redundant, such points make the subsequent contour - // simplification depend on the slice height (and may move seam candidates). - remove_collinear(loops); - #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { static int iRun = 0; From 5ed56eb876ab8112e2d58961a9005f022905d1d8 Mon Sep 17 00:00:00 2001 From: ExPikaPaka <112851715+ExPikaPaka@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:56:52 +0200 Subject: [PATCH 71/71] Cache system presets to eliminate startup and wizard load times (#14217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add caching system for presets * Removing user\bundle serialization and keeping it only for system presets * Integrate caching into WebGuideDialog which speeds up time of SetupWizzard and PrinterSelection dialog * Add CI\CD step to prepare cache file in ahead of time so user does not need to wait * Add partial cache generation when only one of the vendros is changed to speed up recalculation time * Handle corrupted files * Add cache to GuideDialog as previos version didn't work as expected * Add inspecting tool and fix CI cache generation * Generate cache per vendor * Simplify code by mergin it in PresetBundle * Simplify code a bit more * Add cereal serialize() to VendorProfile, PrinterModel, Preset, and Semver * Remove CachedPrinterModel/VendorProfile/Preset mirror structs from VendorCache * Fix use-after-free in CallAfter lambda; replace raw thread pointer with unique_ptr * Use get_vendor_cache_key() to match cache keys written by the app * Remove BOM added by VSC * Skip invalid vendors * Remove leftover cache file * Fix build for windows arm64 * Revert json cache back * Update check for stale cache * Serealize all value fields for Preset class to minimize regression later * Minimize field duplication by moving Cache thing into PresetBundle * Add tests for Cache system * Add a bit more tests * Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed * Rvert from per-verndor to single cache file Replace N per-vendor .cache files with a single system_presets.cache that holds all vendors and presets in one serialized blob. Cache load is now all-or-nothing: on hit all vendors are applied from the bundle (sub-second); on miss all vendors are parsed from JSON and a fresh bundle is written to the user cache dir. Invalidation is driven by bundle_key - a sorted concatenation of all vendor JSON version strings. Any vendor update invalidates the whole cache and triggers re-parse on next launch. Guide wizard (WebGuideDialog) loads the bundled cache into a plain PresetBundle instead of a separate VendorGuideData struct, removing the duplicate data model. generate_system_cache simplified from a per-vendor loop to a single save_system_presets_cache() call producing one output file. * Transfer all Preset fields from cache via move assignmet apply_vendor_preset_group was copying fields manually and missed bundle_id, user_id, base_id, sync_info, updated_time, key_values, ini_str. Replace field-by-field copy with move assignment of the fully-deserialized Preset, then restore the vendor pointer which is excluded from serialization. * Ignore cache for future * Remove not used files * Ship one preset cache per vendor in place of the profile JSONs Each vendor's system presets serialize into a single .opc built at package time, and a shipped build carries that file alone — the profile JSON and its sub-file tree are pruned. The vendor loader, the setup wizard's profile list and the resource installer all read a vendor through its cache, falling back to parsing whenever one is absent, stale or unreadable, so the cache stays an optimization and never a source of truth. Caches hold presets in source form and resolve inheritance at load, through the same code the JSON path uses. * Make the preset cache self-describing and load each vendor from the system folder alone The cached DynamicPrintConfig is keyed by name, through a per-file dictionary of the distinct opt_keys, the type each was written as, and the distinct enum value names, instead of by serialization_key_ordinal — a position assigned by declaration order at static init, where inserting one option shifts every later ordinal and the lookup then succeeds on the wrong option. Because a name-keyed payload drops the options this build cannot place rather than being rejected wholesale, the schema fingerprint goes, and with it the two fallbacks that existed only because an installed cache died on every app upgrade: the second lookup tier into resources/profiles and the parse fallback to the same place. A vendor is loaded from /system/ and nowhere else, as on main — which is what makes the app write its .opc files there again. * Simplify the preset cache internals after review * Use the shared temp-dir helper in the preset bundle loading test * Bound stamp string reads in the preset cache * Speed up the setup wizard with a profile-data cache The wizard's per-vendor fast path threw on vendors present only in resources, falling back to a ~29 s raw JSON scan on every open. Each vendor now loads from the directory it was found in, and the derived model/machine/filament/process catalog is cached whole in /cache/wizard_profile_data.json, stamped by each vendor's name and version - a fresh cache makes an open one file read, with no bundle built and no presets installed (~0.2 s vs ~2 s). * Remove debug SVG dump from a geometry test * Move the per-vendor cache file format into PresetCacheFormat * Move the vendor install helpers from PresetBundle into Utils * rename * fix flatpak * change cache version to 1 --------- Co-authored-by: SoftFever --- .gitattributes | 5 + .github/workflows/build_all.yml | 2 + .github/workflows/build_orca.yml | 29 + .gitignore | 1 + build_linux.sh | 2 + docs/HLSD/preset-cache.md | 402 ++++ scripts/build_preset_cache.bat | 141 ++ scripts/build_preset_cache.sh | 161 ++ scripts/flatpak/com.orcaslicer.OrcaSlicer.yml | 11 + src/dev-utils/CMakeLists.txt | 10 + src/dev-utils/generate_system_cache.cpp | 84 + src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Config.hpp | 3 + src/libslic3r/Preset.cpp | 35 +- src/libslic3r/Preset.hpp | 29 +- src/libslic3r/PresetBundle.cpp | 871 +++++---- src/libslic3r/PresetBundle.hpp | 73 +- src/libslic3r/PresetCacheFormat.cpp | 588 ++++++ src/libslic3r/PresetCacheFormat.hpp | 192 ++ src/libslic3r/PrintConfig.hpp | 3 +- src/libslic3r/Semver.hpp | 13 + src/libslic3r/Utils.hpp | 43 +- src/libslic3r/utils.cpp | 154 +- src/slic3r/Config/Snapshot.cpp | 11 +- src/slic3r/GUI/ConfigWizard.cpp | 68 +- src/slic3r/GUI/ConfigWizard_private.hpp | 4 +- src/slic3r/GUI/CreatePresetsDialog.cpp | 19 +- src/slic3r/GUI/GUI_App.cpp | 6 + src/slic3r/GUI/WebGuideDialog.cpp | 460 ++++- src/slic3r/GUI/WebGuideDialog.hpp | 18 +- src/slic3r/Utils/PresetUpdater.cpp | 61 +- tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_geometry.cpp | 5 - .../libslic3r/test_preset_bundle_loading.cpp | 28 +- tests/libslic3r/test_vendor_cache.cpp | 1620 +++++++++++++++++ 35 files changed, 4585 insertions(+), 570 deletions(-) create mode 100644 docs/HLSD/preset-cache.md create mode 100644 scripts/build_preset_cache.bat create mode 100755 scripts/build_preset_cache.sh create mode 100644 src/dev-utils/generate_system_cache.cpp create mode 100644 src/libslic3r/PresetCacheFormat.cpp create mode 100644 src/libslic3r/PresetCacheFormat.hpp create mode 100644 tests/libslic3r/test_vendor_cache.cpp diff --git a/.gitattributes b/.gitattributes index 4cab1f4d26..441bdfe1eb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,7 @@ # Set the default behavior, in case people don't have core.autocrlf set. * text=auto + +# Shell scripts are run by Git Bash on Windows CI, which cannot read a script +# with CRLF line endings: it fails on the first line. Windows checkouts default +# to core.autocrlf=true, so keep these LF whatever the platform. +*.sh text eol=lf diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 1c392e4bc6..3de2a9184b 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -14,6 +14,7 @@ on: - 'localization/**' - 'resources/**' - ".github/workflows/build_*.yml" + - 'scripts/build_preset_cache.*' - 'scripts/flatpak/**' - 'scripts/msix/**' - 'tests/**' @@ -33,6 +34,7 @@ on: - 'build_release_vs.bat' - 'build_release_vs2022.bat' - 'build_release_macos.sh' + - 'scripts/build_preset_cache.*' - 'scripts/flatpak/**' - 'scripts/msix/**' - 'tests/**' diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index a7652c3bd6..112c0b279b 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -162,6 +162,14 @@ jobs: retention-days: 5 if-no-files-found: error + - name: Build system preset cache (macOS) + if: runner.os == 'macOS' && !inputs.macos-combine-only + working-directory: ${{ github.workspace }} + shell: bash + # The bundle was already packed from resources/, so the caches have to be + # installed into it here; the source tree keeps its JSONs for later jobs. + run: ./scripts/build_preset_cache.sh -b build/${{ inputs.arch }} build/${{ inputs.arch }}/OrcaSlicer/OrcaSlicer.app/Contents/Resources/profiles + - name: Pack macOS app bundle ${{ inputs.arch }} if: runner.os == 'macOS' && !inputs.macos-combine-only working-directory: ${{ github.workspace }} @@ -390,6 +398,13 @@ jobs: if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 tests } else { .\build_release_vs.bat slicer tests } shell: pwsh + - name: Build system preset cache (Windows) + if: runner.os == 'Windows' + shell: cmd + # Shipped into both the already-installed tree (portable zip, MSIX) and + # the checkout cpack re-installs from when it builds the NSIS installer. + run: scripts\build_preset_cache.bat --prune-source "%BUILD_DIR%" "resources\profiles" "%BUILD_DIR%\OrcaSlicer\resources\profiles" + - name: Pack unit tests Win if: runner.os == 'Windows' working-directory: ${{ github.workspace }} @@ -539,6 +554,20 @@ jobs: retention-days: 5 if-no-files-found: error + - name: Build system preset cache (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + # Both were packed from resources/ before the caches existed, so the + # AppImage is unpacked first and the caches shipped into it and into + # the package tree; the source tree keeps its JSONs for later steps. + appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1) + chmod +x "$appimage" + "$appimage" --appimage-extract + ./scripts/build_preset_cache.sh -b build build/package/resources/profiles squashfs-root/resources/profiles + appimagetool=$(find build -name "appimagetool.AppImage" | head -1) + ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage" + rm -rf squashfs-root # Ship the freshly-built validator so slice_check_linux (build_all.yml) # can slice-sweep the shipped profiles with this PR's engine. Taken from # the aarch64 leg so the sweep also exercises the arm build; x86_64 on diff --git a/.gitignore b/.gitignore index 916c7207b7..4d3ccb5c7b 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ internal_docs/ # Python bytecode __pycache__/ *.pyc +*.opc diff --git a/build_linux.sh b/build_linux.sh index 72ea742f1a..6d65a10e41 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -567,6 +567,8 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer echo "Building OrcaSlicer_profile_validator .." print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer_profile_validator + echo "Building generate_system_cache ..." + print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target generate_system_cache ./scripts/run_gettext.sh fi if [[ -n "${BUILD_TESTS}" ]] ; then diff --git a/docs/HLSD/preset-cache.md b/docs/HLSD/preset-cache.md new file mode 100644 index 0000000000..6e693dbd6f --- /dev/null +++ b/docs/HLSD/preset-cache.md @@ -0,0 +1,402 @@ +# System Preset Cache — High Level Design + +## Why it exists + +OrcaSlicer ships tens of thousands of system preset JSON files. Every launch used to +parse all of them: read each vendor profile, walk its machine, process and filament +sub-files, resolve inheritance, and build the preset collections from scratch. That +parse dominated startup, and it produced the same result every time, because system +presets only change when the app is updated or a profile update is installed. + +The preset cache replaces that parse with a read. Each vendor's presets are serialized +once — at build time, in CI — into a single binary file the app reads in one pass. The +read replaces the file walk and the JSON parsing, which is where the time went; +resolving inheritance and registering the presets still runs at load, through the same +code the JSON path uses, so the result is the parse's result without the parse. + +The cache is **only ever an optimization**. Every rule below exists to guarantee that a +cache is either provably equivalent to parsing the JSONs, or rejected. There is no +"mostly right" cache. + +## The unit is one vendor + +A cache covers exactly one vendor. `BBL.opc` sits beside `BBL.json` and holds +everything `BBL.json` and the `BBL/` sub-file tree would have produced. + +Per-vendor granularity is what makes the system practical: + +- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd + vendors keep theirs — even when the bumped vendor is the shared Orca filament + library everyone else inherits from. +- The setup wizard, which loads vendors one at a time, gets the same speedup as + startup without a second code path. +- A vendor with no cache, or a broken one, costs only that vendor a parse. + +A cache holds *system* presets only. User presets, project settings and modified +presets are never serialized — they have their own storage and their own lifecycle. + +## Where the files live + +| Location | Contents on a shipped build | Role | +|---|---|---| +| `resources/profiles/` | `.opc` alone — the profile and its preset JSONs both pruned | What the app ships with; what installing copies from, and the only thing it is read for | +| `/system/` | `.opc` alone, or `.json` + `/` after an update | What the user has installed | +| `/system/` (dev build) | `.json` + `/` + `.opc` written at runtime | A developer tree caches as it parses | +| `/cache/wizard_profile_data.json` | The wizard's derived vendor catalog plus the stamps it was built from | Written and read by the setup wizard only; never shipped (see "The wizard's profile-data cache") | + +Two forms of the same vendor therefore exist, and the system's central rule is that +**a vendor's cache is the whole of it**. Where a cache ships or is installed, no profile +and no preset JSONs sit beside it: the cache carries the presets, the vendor profile, +and the version stamp that says which release it came from. A vendor is "installed" if +either form is present *and usable*, and its installed version is read from whichever +form a load would serve. + +What stays beside the caches in `resources/profiles/` is everything that is not a +preset: each vendor's directory of printer thumbnails, cover images, bed models and +hotend meshes, which are read from disk by path and were never part of the cache. Files +that are not vendors at all, `blacklist.json` chief among them, are untouched. + +The alternative — shipping both and treating the cache as a sidecar — was rejected. It +doubles the installed size, and it creates a class of bug where the two disagree and +the app's behavior depends on which one a given code path happened to read. + +## What a cache file is + +A fixed-size header followed by one binary stream. + +The header carries a magic number, the cache format version, the payload size and a +CRC32 of the payload. It exists so that a truncated download, a half-written file or a +file from an entirely different program is rejected in microseconds, before anything +tries to interpret it. + +The payload opens with the stamps that decide whether the cache may be used at all — +format version, vendor name, vendor version — then a dictionary, and then the vendor's +data: its vendor profile, three lists of preset entries (process, filament, machine), +and the count of errors the original parse hit. + +Each entry is one preset **in source form**: what its JSON sub-file states and nothing +that resolving it derives — the preset's own config diff, the name of the preset it +inherits, and the parse metadata (name, sub-path, description, instantiation, setting +and filament ids, renames). Non-instantiated base presets are stored too; the children +that inherit from them cannot resolve without them. + +**The payload names its own keys.** The dictionary holds the distinct `opt_key`s the +file uses, the `ConfigOptionType` each was written as, and the distinct enum *value +names*; an option in an entry's config is then a `uint16` index into that dictionary +plus its value. Names are written once per file rather than once per occurrence, and a +reader resolves the dictionary against this build's `print_config_def` once, after +which reading an option is a vector index. + +This is what makes the cache survive config-schema drift. The alternative — keying an +option by its `serialization_key_ordinal`, the position `ConfigDef::add` assigns by +declaration order at static init — cannot: inserting one option into the middle of +`PrintConfig.cpp` shifts every later ordinal, and the lookup on the way back in then +*succeeds on the wrong option*, silently, wherever the two share a type. Because a +name-keyed payload instead drops the individual options this build cannot place, the +file as a whole stays readable, and there is no schema fingerprint — no checksum over +the option schema that would reject every cache on every release. An option this build +no longer defines, or now defines with a different type, gets exactly what it gets from +a JSON profile: read, dropped, and the rest of the preset loads. + +The ordinal-keyed cereal hooks in `PrintConfig.hpp` are untouched — they are also the +undo/redo wire format, where the process cannot change underneath them. The cache has +its own serialization in `PresetCacheFormat.{hpp,cpp}`. + +Three deliberate choices in the layout: + +- **Stamps come first**, so the question "what version is this vendor installed at?" + can be answered by reading the first kilobyte. The updater asks that question for + every vendor on every launch; reading tens of megabytes to answer it would give back + the startup time the cache saved. The dictionary sits behind them, ahead of the + entries, so a reader that does go on resolves it once and then indexes. +- **Nothing inherited is baked in.** A filament preset that inherits from the shared + library is stored as its own diff plus its parent's name, and the parent is looked up + when the entry is installed, against whatever library is loaded then. A cache + therefore carries no other vendor's values, and no other vendor's update — the + library's included — can make it stale. +- **Nothing derived is stored.** Default presets, flattened configs, aliases and + lookup maps are all reconstructed at load by the same code the JSON path runs, and + state that path never fills (obsolete-preset lists) is not stored either. This keeps + the cache a record of the vendor's data, not a memory image of the program's state. + +## When a cache may be used + +A cache is accepted only if every gate below passes. Any failure means "parse the +JSONs instead" — never a hard error, never a partial load. + +**1. Integrity.** Magic number, a declared body size that is exactly the rest of the +file, CRC32 over the payload. The size is checked against the file's real length before +anything is allocated on the strength of it, so an eight-byte field in an unauthenticated +file cannot ask for a gigabyte. + +**2. Cache format version.** A single integer bumped by hand whenever the binary layout +changes in a way nothing else would catch: reordering or retyping a hand-written +serialized field, or changing what the cache's own stamps mean. Config-schema drift is +explicitly *not* such a change — the dictionary handles it — so this no longer moves +every release. + +**3. Vendor identity and version.** The cache names the vendor it holds and the profile +version it was built from. It is accepted only if that version is at least as new as +the profile now on disk. Where no profile sits beside the cache — the shipped, +cache-only form — the comparison is skipped, because nothing on disk can be newer than +a cache that is the installation. + +**4. Every entry installs.** Entries are installed as they are read, and an entry that +cannot be — typically one that inherits a parent the currently loaded filament library +no longer provides — rejects the whole cache, never just the entry. A partial vendor is +not a vendor. + +There is deliberately no stamp for the shared filament library. A cache stores its +filaments' inheritance by name and resolves it at load, so a library update changes +what a cache load *produces*, never whether the cache is *valid* — the same file yields +the updated result. This matters most on a shipped build, where a vendor is its cache +and nothing else: a profile update that delivered only the library would otherwise have +stranded every other vendor with a cache it invalidated and no JSONs to fall back on. + +A vendor profile with no parsable version is never cached and never served from a +cache. There would be no way to tell later whether the cache had gone stale, and a +cache nothing can invalidate is worse than no cache. + +## How a vendor is loaded + +Vendors load in a fixed order, because filament inheritance crosses exactly one +boundary: any vendor's filament may inherit from the shared Orca filament library, +and nothing else reaches across vendors. The library therefore goes first, alone; +every other vendor follows in parallel, resolving against it; and the results are +merged in a stable order: + +```mermaid +flowchart LR + lib["1 · OrcaFilamentLibrary
loaded first, synchronously"] --> par["2 · every other vendor in parallel,
each into its own bundle, filaments
resolving against the loaded library"] --> merge["3 · bundles merged into one,
sequentially, in stable vendor order"] +``` + +Whether a vendor comes from its cache or from a parse changes nothing in that +order — both produce the same bundle, so cached and parsed vendors mix freely in +one startup. + +**A vendor is loaded from where it is installed and nowhere else.** For startup that +is `/system/`; resources reaches the app by being *installed* into that +directory first, never by being loaded from. (The setup wizard is the one caller with +a different notion of "where": it also shows vendors the user has not installed, and +loads those from `resources/profiles` — see "The wizard's profile-data cache".) There +is one lookup tier and one parse source: + +``` +load vendor V from /system: + system/V.opc passes CACHE_VERSION + size + CRC + vendor name + version gate? + yes -> serve from it + no -> parse system/V.json, then write system/V.opc back +``` + +The same decision drawn out — "the gates" are the four acceptance checks above: + +```mermaid +flowchart TB + start["load vendor V from a directory dir
— normally <data_dir>/system/"] + start --> stamp["installed version = version of dir/V.json
— or ∞ with no profile there,
the cache then being the installation"] + stamp --> g1{"dir/V.opc
passes all four gates?"} + g1 -- "yes" --> hit(["served from the
installed cache"]) + g1 -- "no" --> pd["parse the JSONs in dir"] + pd --> ver{"profile version
parsable?"} + ver -- "yes" --> save(["loaded; dir/V.opc written back —
the next load takes the top path"]) + ver -- "no" --> raw(["loaded, never cached"]) +``` + +A second tier into `resources/profiles/` used to sit between those two, and a parse +fallback to the same place behind them. Both existed only because an installed cache +died on every app upgrade, when the schema fingerprint rejected it; with the fingerprint +gone there is nothing for them to rescue. They also had a cost: on a developer tree the +shipped cache answered first, so the profile in `/system/` was never parsed +and its cache was never written back. + +Serving from a cache is not a memory-image restore. The entries are deserialized and +then installed one by one — inheritance resolved against the presets installed before +them and the currently loaded filament library, configs flattened onto the collection +defaults, validated and registered — by the same function the JSON path calls straight +after parsing a sub-file. The two paths share everything below the parse, which is what +makes a cache-loaded bundle indistinguishable from a JSON-loaded one by construction +rather than by test coverage. Installation also rebuilds each preset's file path from +the local data directory, so a shipped cache never carries the generating machine's +paths. + +App upgrades work because a cache normally survives one. Only a deliberate +`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at +install time rather than at load: a vendor whose cache this build cannot read counts +as **not installed**, so the updater lays down a working copy on the next launch (see +below). A vendor that still has its profile JSONs beside the cache is simply parsed +and re-cached. + +If a parse does happen and the vendor's profile carries a version, the app writes the +cache back beside where it looked for the vendor. That is how a developer build warms +itself up on second launch, and how a vendor delivered by a profile update becomes +cached without waiting for the next release. + +## The wizard's profile-data cache + +The setup wizard's printer and filament pages want every vendor in one bundle — the +installed ones *and* the shipped ones the user has not installed yet, because the +wizard is where installing is chosen. Its set therefore spans two directories: +`/system/` for installed vendors (shadowing resources on a name collision), +`resources/profiles` for the rest, each vendor loaded from its own directory. + +What the wizard actually consumes from that bundle is one derived JSON — the model / +machine / filament / process catalog its web pages render — and that JSON is a pure +function of the vendor set: each vendor's name and version, in load order. A profile +change requires a version bump, so name and version determine a vendor's content +wherever its copy sits; which directory served it is deliberately **not** stamped, +and installing or removing a copy at an unchanged version leaves the cache valid. So +the wizard caches the *derived JSON*, not another form of the inputs: +`/cache/wizard_profile_data.json` holds the stamp list and the catalog. On +open, the wizard computes the current stamps (one version peek per vendor) and, when +they match, serves the catalog from the file — no bundle built, no preset installed. +Caching bundle inputs instead was tried and measured: rebuilding the bundle from +per-vendor caches costs ~2 s of preset installation whatever feeds it, so only +skipping the rebuild entirely wins. + +Any change to the set — a vendor added, removed or updated, or its cache-only +`.opc` replaced by a newer one — changes the stamps and retires the whole file; +the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where +they cover) and writes the catalog back. Selections, region and per-open decorations +are applied downstream of the cache either way, so a served catalog is +indistinguishable from a rebuilt one. Nothing ships this file and the updater never +touches it; it is a locally written artifact, re-derived whenever stale, written +through a temp file and rename so half a cache is never readable. + +The cache lives under `/cache/`, not beside the vendors: everything that +scans `/system/` treats any `.opc` there as a vendor, so a non-vendor +cache file must not sit in that directory. Relatedly, the stamp reader is hardened: +`read_cache_stamps` validates the cache version before reading anything +variable-length and bounds the stamp strings' lengths, so a reader pointed at a +foreign or damaged `.opc` rejects it cleanly instead of aborting on a garbage +64-bit allocation. + +## How a vendor is installed + +Installing copies from `resources/profiles/` into `/system/`. A shipped build +offers only a cache and a source tree only JSONs, but a partially-generated tree can +have both, at different versions, so the installer picks the form that ships at the +**newer version** and installs only that one: + +- Cache newer or equal, and readable → copy the `.opc`, verify the *copy* is one this + build can read, and only then delete any profile and vendor directory a previous + install left behind, so nothing can shadow it. +- Profile newer, or the cache unreadable or absent → copy the profile and the vendor's + preset JSONs exactly as the app did before caches existed, and delete any stale `.opc` + once the profile is safely in place. + +One vendor that cannot be installed is one vendor missing, not a reason to leave the +rest uninstalled: the installer skips it, records the failure, and carries on with the +batch. A vendor whose cache arrives unreadable falls back to installing its profile, +which is decided by reading the copy rather than by the kilobyte peek that chose the +form. + +**"Installed" means present and usable.** Where the cache is the whole of a vendor's +installation, a `.opc` this build cannot read is not an installation — counted as one, +the vendor would be stranded with nothing to load and the updater would never repair +it. The installed version is likewise whichever form a load would actually serve: the +cache's stamp while it covers the profile beside it, the profile's own version once it +does not. + +The result is that only one form of a vendor is ever present, and it is the newest one +the build has. This matters most for the update check, which compares what is installed +against what installing *would* lay down: if those two disagreed about which form +counts, a vendor could reinstall on every launch forever, or silently never update. + +Profile updates delivered over the air always arrive as JSONs, and they win — an +updated vendor's real profile lands in the data directory, the installed cache beside it +is older and gets rejected, and the vendor is parsed and re-cached. An update that touches only +the filament library needs nothing more: every other vendor's cache stays valid and +simply resolves against the new library on its next load. + +## How the caches are produced + +Cache generation is a build step, not something a user ever runs. + +One script per platform does the whole job, and CI calls it once on each. It builds a +small dev-utility that loads a profiles directory exactly as the app would, with cache +writing enabled, dropping a `.opc` beside every vendor profile it parses; then +it copies those caches into each packaged application it was pointed at and deletes +every preset JSON they replace — the vendor's own profile included. Only a vendor that +actually has a cache is pruned, so a vendor the generator skipped keeps its JSONs and is +simply parsed at startup. + +Caches are generated into the checkout's own `resources/profiles`, because that is what +cpack re-installs from when it builds the NSIS installer — so that directory is also a +prune target in CI. Pruning it deletes the checkout's preset JSONs, which is a packaging +step, not something a build should do to a working tree by surprise: the Windows script +refuses that target unless given `--prune-source`, and CI passes it. + +Generation runs after the build, in the same job, so the caches ship with a build that +can read them. + +The flatpak differs only in where the script is called from. Nothing outside +flatpak-builder ever builds it, so there is no packaged tree for the workflow to point +the script at afterwards: the manifest runs it as a build step instead, against the +profiles the install has already copied into `/app`. + +## Behavior when things go wrong + +The system is designed so that no cache problem is fatal: + +- **Corrupt, truncated or foreign file** — rejected at the header, vendor parsed. A + cache is written to a temp file beside its target and moved into place, so a write + that dies partway leaves the previous cache intact rather than a truncated one. +- **An option this build no longer has, or now types differently** — that option alone + is dropped, exactly as a JSON profile's would be. The preset and the file load. +- **Cache from a build with a different cache layout** — rejected on `CACHE_VERSION`. + A vendor with JSONs beside it is parsed and re-cached; a cache-only vendor reads as + not installed and the updater reinstalls it. +- **Stale cache** — rejected on the vendor version stamp, vendor parsed and re-cached. +- **Failure part-way through loading** — a deserialization error, or any entry that + fails to install — rejects the whole cache, and the bundle is reset to a clean state + before falling back, so a half-loaded cache can never leak into the parsed result. +- **A vendor that can be neither read nor parsed** — logged, and left out. The setup + wizard drops that vendor from its list and opens with the rest; startup records the + error alongside the vendors that did load. One broken vendor never takes the app down. + +The one genuine limit: on a shipped build a vendor is its cache and nothing else, so a +rejected cache has nothing to fall back to for that vendor. This is by design — the +alternative is shipping every preset twice — and it is why the acceptance gates are +conservative and why CI generates the caches with the same build that ships them. The +recovery path is a profile update, which delivers real JSONs. + +It also means nothing may quietly assume a `.json` exists. Discovery, version +checks and the update decision all read whichever form is present, and a code path that +enumerates only `*.json` will find no vendors at all in a packaged build. + +## Maintenance rules + +- **Adding, removing, retyping or reordering a config option** needs nothing. The + payload names its keys and its enum values, so an option a cache carries and this + build does not is dropped; one this build has and the cache does not is simply + absent, as it would be from a JSON that predates it. +- **Changing a hand-written `serialize()`** — `VendorProfile` or its nested types — or + the `CachedPreset` field list — written and read by `visit_entry` in + `PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or + the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand. +- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most + 65535 options and one cache at most 65535 distinct enum value names. + `CacheDictionary::save` throws past that, which surfaces when CI generates the + caches rather than on a user's machine. +- **Bumping `CACHE_VERSION` is safe without a resources fallback** because + `is_vendor_installed` means *present and usable*: cache-only vendors read as not + installed after a bump, and the updater reinstalls them from resources. +- **Bumping a vendor profile's version** invalidates that vendor's cache and nothing + else — the filament library's included. Other vendors' caches resolve against the + new library the next time they load. +- **Caches are never committed.** They are build artifacts, generated per build, + ignored by git. + +## Where this lives in the tree + +| Area | Files | +|---|---| +| Everything about the bytes on disk — the dictionary, one config's wire format, the file framing and stamps, entry serialization, `VendorCacheFile` save/load/peeks | `src/libslic3r/PresetCacheFormat.{hpp,cpp}` | +| Serve-or-parse decision, installing cache entries into a bundle, cache write-back | `src/libslic3r/PresetBundle.{hpp,cpp}` | +| Vendor profile serialization | `src/libslic3r/Preset.hpp` | +| Vendor discovery, installed/shipped versions, installation | `src/libslic3r/utils.cpp` (declared in `Utils.hpp`) | +| Update and reinstall decisions | `src/slic3r/Utils/PresetUpdater.cpp` | +| Setup wizard and printer-selection dialog | `src/slic3r/GUI/ConfigWizard.cpp`, `src/slic3r/GUI/WebGuideDialog.cpp` | +| Generator tool | `src/dev-utils/generate_system_cache.cpp` | +| Build and packaging script | `scripts/build_preset_cache.{sh,bat}` | +| Tests | `tests/libslic3r/test_vendor_cache.cpp` | diff --git a/scripts/build_preset_cache.bat b/scripts/build_preset_cache.bat new file mode 100644 index 0000000000..82f3e02723 --- /dev/null +++ b/scripts/build_preset_cache.bat @@ -0,0 +1,141 @@ +@echo off +rem Build the per-vendor system preset caches (one .opc per vendor) by +rem running the generate_system_cache.exe dev tool against a profiles directory, +rem and make every profiles directory named on the command line ship-ready: +rem install the caches into it and delete the preset JSONs they replace, so a +rem build ships one copy of its presets instead of two. +rem +rem scripts\build_preset_cache.bat [build_dir] [target_dir ...] +rem +rem build_dir defaults to "build" +rem target_dir profiles directories to ship into. Caches are generated into +rem the source tree's resources\profiles, which is what every +rem packaging step copies from; a target may be that same +rem directory, which then only gets pruned. +rem --prune-source +rem allow a target that is the directory the caches were generated +rem into (resources\profiles). Pruning it deletes the checkout's +rem own preset JSONs, which is a packaging step - not something a +rem build should do to a working tree by surprise. CI passes it. +rem +rem Shipping deletes, so it is a CI packaging step. A vendor's own .json +rem goes along with its preset JSONs: the cache carries the vendor profile and +rem the version it was built at, so discovery, version checks and installing all +rem read it there. Only a vendor that has a cache is pruned, so non-vendor JSONs +rem (blacklist.json) are left alone, as are the vendor directories themselves - +rem thumbnails, covers and bed models still live there. +rem +rem set CONFIG= to pin the build config for multi-config generators +rem (default: the config of the tool already in the build tree, else Release) +setlocal enabledelayedexpansion + +set "REPO_ROOT=%~dp0.." + +set "PRUNE_SOURCE=" +:parse_flags +if /i "%~1"=="--prune-source" ( + set "PRUNE_SOURCE=1" + shift + goto :parse_flags +) + +set "BUILD_DIR=%~1" +if "%BUILD_DIR%"=="" set "BUILD_DIR=build" +if not exist "%BUILD_DIR%\" ( + echo ERROR: build tree not found: %BUILD_DIR% 1>&2 + exit /b 1 +) +if not "%~1"=="" shift + +rem Newest match wins: a stale binary silently produces a stale cache layout. +call :find_tool +if not defined CONFIG ( + for %%c in (Debug Release RelWithDebInfo MinSizeRel) do ( + echo !TOOL! | findstr /i "\\%%c\\" >nul && set "CONFIG=%%c" + ) +) +if not defined CONFIG set "CONFIG=Release" + +echo Building generate_system_cache in %BUILD_DIR% (%CONFIG%) +cmake --build "%BUILD_DIR%" --config %CONFIG% --target generate_system_cache +if errorlevel 1 ( + echo ERROR: could not build generate_system_cache - configure the build tree with -DORCA_TOOLS=ON: 1>&2 + echo cmake -S "%REPO_ROOT%" -B "%BUILD_DIR%" -DORCA_TOOLS=ON 1>&2 + exit /b 1 +) +call :find_tool +if not defined TOOL ( + echo ERROR: generate_system_cache.exe not found under %BUILD_DIR% - build with -DORCA_TOOLS=ON 1>&2 + exit /b 1 +) + +set "PROFILES=%REPO_ROOT%\resources\profiles" +if not exist "%PROFILES%\" ( + echo ERROR: profiles directory not found: %PROFILES% 1>&2 + exit /b 1 +) +for %%d in ("%PROFILES%") do set "PROFILES=%%~fd" + +rem Add the slicer's runtime DLL directory to PATH so generate_system_cache.exe +rem can resolve its dependencies (TKernel.dll etc.) without a full install step. +set "DLL_DIR=" +for /f "delims=" %%f in ('dir /s /b "%BUILD_DIR%\TKernel.dll" 2^>nul') do ( + if not defined DLL_DIR set "DLL_DIR=%%~dpf" +) +if defined DLL_DIR set "PATH=%DLL_DIR%;%PATH%" + +echo Generating per-vendor preset caches in %PROFILES% +rem Start clean so vendors that went away - and caches written by older tool +rem versions - don't linger next to the freshly generated ones. +del /q "%PROFILES%\*.opc" 2>nul +del /q "%PROFILES%\*.cache" 2>nul +"%TOOL%" --path "%PROFILES%" --log_level 2 +if errorlevel 1 exit /b %errorlevel% + +:next_target +if "%~1"=="" exit /b 0 +call :ship "%~1" +if errorlevel 1 exit /b 1 +shift +goto :next_target + +:ship +set "TARGET=%~1" +if not exist "%TARGET%\" ( + echo ERROR: profiles directory not found: %TARGET% 1>&2 + exit /b 1 +) +for %%d in ("%TARGET%") do set "TARGET=%%~fd" +if /i "%TARGET%"=="%PROFILES%" if not defined PRUNE_SOURCE ( + echo %TARGET%: skipped - this is where the caches were generated. + echo Pass --prune-source to prune it; that deletes this checkout's preset JSONs. + exit /b 0 +) +if /i not "%TARGET%"=="%PROFILES%" copy /y "%PROFILES%\*.opc" "%TARGET%\" >nul + +set /a SHIPPED=0 +set /a PRUNED=0 +for %%c in ("%PROFILES%\*.opc") do ( + set /a SHIPPED+=1 + set "VENDOR=%%~nc" + if exist "%TARGET%\!VENDOR!.json" ( + del /q "%TARGET%\!VENDOR!.json" + set /a PRUNED+=1 + ) + if exist "%TARGET%\!VENDOR!\" ( + for /f %%n in ('dir /s /b "%TARGET%\!VENDOR!\*.json" 2^>nul ^| find /c /v ""') do set /a PRUNED+=%%n + del /s /q "%TARGET%\!VENDOR!\*.json" >nul 2>&1 + rem Deepest first, so a directory the delete above emptied goes too; rd + rem refuses the ones still holding covers or meshes. + for /f "delims=" %%d in ('dir /s /b /ad "%TARGET%\!VENDOR!" 2^>nul ^| sort /r') do rd "%%d" 2>nul + ) +) +echo %TARGET%: !SHIPPED! caches, dropped !PRUNED! preset JSONs +exit /b 0 + +:find_tool +set "TOOL=" +for /f "delims=" %%f in ('dir /s /b /o-d "%BUILD_DIR%\generate_system_cache.exe" 2^>nul') do ( + if not defined TOOL set "TOOL=%%f" +) +exit /b 0 diff --git a/scripts/build_preset_cache.sh b/scripts/build_preset_cache.sh new file mode 100755 index 0000000000..7a874f7e06 --- /dev/null +++ b/scripts/build_preset_cache.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Build the per-vendor system preset caches (one .opc per vendor) by +# running the generate_system_cache dev tool against a profiles directory, and +# make every profiles directory named on the command line ship-ready: install +# the caches into it and delete the preset JSONs they replace, so a build ships +# one copy of its presets instead of two. +# +# ./scripts/build_preset_cache.sh # caches into resources/profiles +# ./scripts/build_preset_cache.sh -b build/arm64 # search this build tree for the tool +# ./scripts/build_preset_cache.sh [ ...] # and ship into these profiles dirs +# +# Caches are generated into the source tree's resources/profiles, which is what +# every packaging step copies from. Shipping deletes, so it is a CI packaging +# step: pass packaged output directories, or the checkout of a build that is +# about to be packaged from it. +# +# A vendor's own .json goes along with its preset JSONs: the cache +# carries the vendor profile and the version it was built at, so discovery, +# version checks and installing all read it there. A shipped vendor is its cache +# and nothing else. Only a vendor that has a cache is pruned, so an ungenerated +# vendor keeps its JSONs and is simply parsed at startup; non-vendor JSONs +# (blacklist.json) are left alone, as are the vendor directories themselves — +# thumbnails, covers and bed models still live there. +# +# -b build tree holding the tool +# (default: build/arm64, build/x86_64, or build — first that exists) +# -p profiles directory to generate caches into +# (default: /resources/profiles) +# -c build config for multi-config generators +# (default: the config of the tool already in the build tree, else +# the build tree's CMAKE_BUILD_TYPE) +# -n skip the rebuild and run the tool already in the build tree +# -l tool log level (default: 2) +# --prune-source +# allow a target that is the directory the caches were generated +# into (resources/profiles). Pruning it deletes the checkout's own +# preset JSONs, which is a packaging step - not something a build +# should do to a working tree by surprise. +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd -P)" +build_dir="" +profiles_dir="" +config="" +build_tool=1 +log_level=2 +prune_source=0 + +# getopts does not do long options; pull this one out first. +args=() +for arg in "$@"; do + if [ "$arg" = "--prune-source" ]; then prune_source=1; else args+=("$arg"); fi +done +set -- ${args+"${args[@]}"} + +while getopts "b:p:c:l:nh" opt; do + case $opt in + b) build_dir="$OPTARG" ;; + p) profiles_dir="$OPTARG" ;; + c) config="$OPTARG" ;; + n) build_tool=0 ;; + l) log_level="$OPTARG" ;; + h) sed -n '2,${/^#/!q;p;}' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) exit 1 ;; + esac +done +shift $((OPTIND - 1)) + +if [ -z "$build_dir" ]; then + for candidate in "$repo_root/build/arm64" "$repo_root/build/x86_64" "$repo_root/build"; do + if [ -d "$candidate" ]; then build_dir="$candidate"; break; fi + done +fi +if [ -z "$build_dir" ] || [ ! -d "$build_dir" ]; then + echo "ERROR: build tree not found (pass -b )" >&2 + exit 1 +fi + +# Newest match wins: multi-config trees keep one binary per config, and a stale +# one silently produces a stale cache layout. +find_tool() { + local best="" f + while IFS= read -r f; do + [ -n "$f" ] || continue + if [ -z "$best" ] || [ "$f" -nt "$best" ]; then best="$f"; fi + done < <(find "$build_dir" -name generate_system_cache -type f 2>/dev/null) + printf '%s' "$best" +} + +tool=$(find_tool) +if [ -z "$config" ]; then + case "$tool" in + */Debug/*) config=Debug ;; + */Release/*) config=Release ;; + */RelWithDebInfo/*) config=RelWithDebInfo ;; + */MinSizeRel/*) config=MinSizeRel ;; + *) config=$(sed -n 's/^CMAKE_BUILD_TYPE:[A-Z]*=\(.\+\)$/\1/p' "$build_dir/CMakeCache.txt" 2>/dev/null | head -1 || true) ;; + esac +fi + +if [ "$build_tool" = 1 ]; then + echo "Building generate_system_cache in $build_dir${config:+ ($config)}" + build_args=(--build "$build_dir" --target generate_system_cache) + if [ -n "$config" ]; then build_args+=(--config "$config"); fi + if ! cmake "${build_args[@]}"; then + echo "ERROR: could not build generate_system_cache — configure the build tree with -DORCA_TOOLS=ON:" >&2 + echo " cmake -S \"$repo_root\" -B \"$build_dir\" -DORCA_TOOLS=ON" >&2 + exit 1 + fi + tool=$(find_tool) +fi + +if [ -z "$tool" ]; then + echo "ERROR: generate_system_cache not found under $build_dir — build with -DORCA_TOOLS=ON" >&2 + exit 1 +fi + +if [ -z "$profiles_dir" ]; then profiles_dir="$repo_root/resources/profiles"; fi +if [ ! -d "$profiles_dir" ]; then + echo "ERROR: profiles directory not found: $profiles_dir" >&2 + exit 1 +fi +profiles_dir=$(cd "$profiles_dir" && pwd -P) + +# Start clean so vendors that went away — and caches written by older tool +# versions — don't linger next to the freshly generated ones. +echo "Generating per-vendor preset caches in $profiles_dir" +rm -f "$profiles_dir"/*.opc "$profiles_dir"/*.cache +"$tool" --path "$profiles_dir" --log_level "$log_level" + +for target in "$@"; do + resolved=$(cd "$target" 2>/dev/null && pwd -P) || { + echo "ERROR: profiles directory not found: $target" >&2 + exit 1 + } + if [ "$resolved" = "$profiles_dir" ] && [ "$prune_source" -eq 0 ]; then + echo "$resolved: skipped - this is where the caches were generated." + echo " Pass --prune-source to prune it; that deletes this checkout's preset JSONs." + continue + fi + if [ "$resolved" != "$profiles_dir" ]; then + cp "$profiles_dir"/*.opc "$resolved"/ + fi + + pruned=0 + shipped=0 + for cache in "$profiles_dir"/*.opc; do + vendor=$(basename "$cache" .opc) + shipped=$(( shipped + 1 )) + if [ -f "$resolved/$vendor.json" ]; then + rm -f "$resolved/$vendor.json" + pruned=$(( pruned + 1 )) + fi + [ -d "$resolved/$vendor" ] || continue + n=$(find "$resolved/$vendor" -name '*.json' | wc -l) + find "$resolved/$vendor" -name '*.json' -delete + find "$resolved/$vendor" -type d -empty -delete + pruned=$(( pruned + n )) + done + echo "$resolved: $shipped caches, dropped $pruned preset JSONs" +done diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml index c33425f23f..94c98121ec 100644 --- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml +++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml @@ -347,6 +347,7 @@ modules: - | cmake . -B build_flatpak \ -DFLATPAK=ON \ + -DORCA_TOOLS=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/app \ -DCMAKE_INSTALL_PREFIX=/app \ @@ -357,6 +358,13 @@ modules: - ./scripts/run_gettext.sh - cmake --build build_flatpak --target install -j$FLATPAK_BUILDER_N_JOBS + # Per-vendor preset caches. On the other platforms CI runs this script + # itself; the flatpak is built inside flatpak-builder and the generator + # only exists in here, so the swap is a build step instead, against the + # profiles the install above copied into /app. + - cmake --build build_flatpak --target generate_system_cache -j$FLATPAK_BUILDER_N_JOBS + - ./scripts/build_preset_cache.sh -n -b build_flatpak /app/share/OrcaSlicer/profiles + cleanup: - /include @@ -403,6 +411,9 @@ modules: - type: file path: ../run_gettext.sh dest: scripts + - type: file + path: ../build_preset_cache.sh + dest: scripts # AppData metainfo for GNOME Software & Co. - type: file diff --git a/src/dev-utils/CMakeLists.txt b/src/dev-utils/CMakeLists.txt index e3534a024a..2cfce6a7c5 100644 --- a/src/dev-utils/CMakeLists.txt +++ b/src/dev-utils/CMakeLists.txt @@ -20,6 +20,16 @@ if (SLIC3R_ENC_CHECK) ) endif() +if (ORCA_TOOLS) + set(_DEV_DEFS -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8) + + # generate_system_cache: pre-generates per-vendor .opc files under resources/profiles for CI bundling. + add_executable(generate_system_cache generate_system_cache.cpp) + target_link_libraries(generate_system_cache libslic3r boost_headeronly) + target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS}) + +endif() + # Function that adds source file encoding check to a target # using the above encoding-check binary diff --git a/src/dev-utils/generate_system_cache.cpp b/src/dev-utils/generate_system_cache.cpp new file mode 100644 index 0000000000..426ccee997 --- /dev/null +++ b/src/dev-utils/generate_system_cache.cpp @@ -0,0 +1,84 @@ +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/Utils.hpp" + +#include +#include +#include +#include +#include + +using namespace Slic3r; +namespace fs = boost::filesystem; +namespace po = boost::program_options; + +int main(int argc, char* argv[]) +{ + po::options_description desc("OrcaSlicer System Cache Generator\nUsage"); + // clang-format off + desc.add_options() + ("help,h", "Show help") +#ifdef __APPLE__ + ("path,p", po::value()->default_value("../../../../../../../resources/profiles"), "Path to profiles directory") +#else + ("path,p", po::value()->default_value("../../../resources/profiles"), "Path to profiles directory") +#endif + ("log_level,l", po::value()->default_value(2), "Log level (0=trace, 2=info, 4=error)"); + // clang-format on + + po::variables_map vm; + try { + po::store(po::parse_command_line(argc, argv, desc), vm); + if (vm.count("help")) { std::cout << desc << "\n"; return 0; } + po::notify(vm); + } catch (const po::error& e) { + std::cerr << "Error: " << e.what() << "\n" << desc << "\n"; + return 1; + } + + const std::string profiles_path = vm["path"].as(); + const int log_level = vm["log_level"].as(); + + if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) { + std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n"; + return 1; + } + + set_logging_level(log_level); + set_data_dir(profiles_path); + set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string()); + + const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR; + if (!fs::exists(user_dir)) + fs::create_directories(user_dir); + + AppConfig app_config; + app_config.set("preset_folder", "default"); + + auto preset_bundle = std::make_unique(); + preset_bundle->set_is_validation_mode(true); + preset_bundle->set_default_suppressed(true); + preset_bundle->set_generate_vendor_caches(true); + + std::cout << "Loading system presets from: " << profiles_path << "\n"; + + try { + // In validation mode data_dir() is the profiles directory set above, so the + // loader writes each .opc next to its .json as it parses it. + preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); + } catch (const std::exception& ex) { + std::cerr << "Failed to load presets: " << ex.what() << "\n"; + return 1; + } + + size_t cache_count = 0; + for (auto& entry : fs::directory_iterator(profiles_path)) + if (boost::iends_with(entry.path().string(), ".opc")) + ++ cache_count; + if (cache_count == 0) { + std::cerr << "No vendor cache files were generated under " << profiles_path << "\n"; + return 1; + } + std::cout << "Generated " << cache_count << " vendor cache file(s) under " << profiles_path << "\n"; + return 0; +} diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index f7b4de6e25..333f43a68c 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -348,6 +348,8 @@ set(lisbslic3r_sources Polyline.hpp PresetBundle.cpp PresetBundle.hpp + PresetCacheFormat.cpp + PresetCacheFormat.hpp Preset.cpp Preset.hpp PrincipalComponents2D.cpp diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index ef93f0d509..9e4344820d 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -28,6 +28,9 @@ #include #include +// The serialize() members below archive ConfigOption hierarchies through +// cereal::base_class, whose registration machinery lives in polymorphic.hpp. +#include namespace Slic3r { struct FloatOrPercent diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9ae8b86fd8..1334bd4e7a 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -147,6 +147,9 @@ Semver get_version_from_json(std::string file_path) return Semver(); //throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file_path, err.what())); } + catch(...) { + return Semver(); + } } //BBS: add a function to load the key-values from xxx.json @@ -261,18 +264,28 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil } }; + // The four variant sets are immutable after static init and probed for every + // key of every preset loaded; one merged map makes that a single lookup. + // emplace keeps the first insertion, preserving the first-set-wins priority + // of the else-if chain this replaces. + static const std::unordered_map variant_class = [] { + std::unordered_map m; + for (const std::string& k : print_options_with_variant) m.emplace(k, 0); + for (const std::string& k : filament_options_with_variant) m.emplace(k, 1); + for (const std::string& k : printer_options_with_variant_1) m.emplace(k, 2); + for (const std::string& k : printer_options_with_variant_2) m.emplace(k, 3); + return m; + }(); + for(auto& key :config.keys()){ - if(auto iter = print_options_with_variant.find(key); iter != print_options_with_variant.end()){ - replace_nil_and_resize(key, process_variant_length); - } - else if(auto iter = filament_options_with_variant.find(key); iter != filament_options_with_variant.end()){ - replace_nil_and_resize(key, filament_variant_length); - } - else if(auto iter = printer_options_with_variant_1.find(key); iter != printer_options_with_variant_1.end()){ - replace_nil_and_resize(key, machine_variant_length); - } - else if(auto iter = printer_options_with_variant_2.find(key); iter != printer_options_with_variant_2.end()){ - replace_nil_and_resize(key, machine_variant_length * 2); + auto iter = variant_class.find(key); + if (iter == variant_class.end()) + continue; + switch (iter->second) { + case 0: replace_nil_and_resize(key, process_variant_length); break; + case 1: replace_nil_and_resize(key, filament_variant_length); break; + case 2: replace_nil_and_resize(key, machine_variant_length); break; + case 3: replace_nil_and_resize(key, machine_variant_length * 2); break; } } } diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index b88b5a5ed5..c9b3197a6f 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -131,6 +131,10 @@ public: PrinterVariant() {} PrinterVariant(const std::string &name) : name(name) {} std::string name; + + // All fields, declaration order — keep in sync; bump CACHE_VERSION on change. + template + void serialize(Archive& ar) { ar(name); } // PrinterVariant }; struct PrinterModel { @@ -139,7 +143,7 @@ public: std::string name; //BBS: this is internal id for the printer. Currently only used for searching in database std::string model_id; - PrinterTechnology technology; + PrinterTechnology technology = ptFFF; std::string family; std::vector variants; std::vector default_materials; @@ -162,6 +166,17 @@ public: } const PrinterVariant* variant(const std::string &name) const { return const_cast(this)->variant(name); } + + // All fields, declaration order — keep in sync; bump CACHE_VERSION on change. + template + void serialize(Archive& ar) // PrinterModel + { + ar(id, name, model_id, technology, family, variants, default_materials, + not_support_bed_types, bed_model, bed_texture, image_bed_type, + bottom_texture_end_name, use_double_extruder_default_texture, + bottom_texture_rect, bottom_texture_rect_longer, middle_texture_rect, + hotend_model); + } }; std::vector models; @@ -173,6 +188,14 @@ public: bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); } + // All fields, declaration order — keep in sync; bump CACHE_VERSION on change. + template + void serialize(Archive& ar) // VendorProfile + { + ar(name, id, config_version, config_update_url, changelog_url, + models, default_filaments, default_sla_materials); + } + // Load VendorProfile from an ini file. // If `load_all` is false, only the header with basic info (name, version, URLs) is loaded. static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true); @@ -427,10 +450,10 @@ public: Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {} protected: - Preset() = default; - friend class PresetCollection; friend class PresetBundle; + + Preset() = default; }; bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index e66ae064f0..6fcc6e05c1 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1,7 +1,11 @@ #include +#include #include +#include #include "PresetBundle.hpp" + +#include "PresetCacheFormat.hpp" #include "PrintConfig.hpp" #include "libslic3r.h" #include "I18N.hpp" @@ -307,16 +311,20 @@ std::string PresetBundle::find_preset_vendor(const std::string &preset_name, Pre return ""; } - // Iterate through vendor JSON files in the system directory - for (auto& dir_entry : fs::directory_iterator(system_dir)) { - std::string vendor_file = dir_entry.path().string(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Checking vendor: " << vendor_file; - if (!Slic3r::is_json_file(vendor_file)) + // A vendor is named by its profile or, where the build ships preset caches + // instead of the raw profile JSONs, by its cache alone. + for (const std::string& vendor_name : vendor_names_in(system_dir)) { + const fs::path vendor_json = system_dir / (vendor_name + ".json"); + if (! fs::exists(vendor_json)) { + if (VendorCacheFile::carries_preset((system_dir / (vendor_name + ".opc")).string(), vendor_name, type, preset_name)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << preset_name + << " in vendor cache " << vendor_name; + return vendor_name; + } continue; - - // Get vendor name (filename without .json extension) - std::string vendor_name = dir_entry.path().filename().string(); - vendor_name.erase(vendor_name.size() - 5); // Remove ".json" + } + const std::string vendor_file = vendor_json.string(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Checking vendor: " << vendor_file; try { // Load and parse the vendor JSON file @@ -563,6 +571,8 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward //BBS: add config related logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%")%substitution_rule%preferred_selection.printer_model_id; + const auto startup_t0 = std::chrono::steady_clock::now(); + //BBS: change system config to json std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule); @@ -588,6 +598,12 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward set_calibrate_printer(""); + { + const auto total_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startup_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: all presets loaded in " << total_ms << " ms"; + } + //BBS: add config related logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); return substitutions; @@ -1000,6 +1016,8 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For bundles.m_bundles.clear(); bundles.WriteUnlock(); + const auto user_load_t0 = std::chrono::steady_clock::now(); + // Load bundle metadata from _local directory first fs::path local_dir(folder / PRESET_LOCAL_DIR); if (fs::exists(local_dir)) { @@ -1018,7 +1036,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For metadata.filament_presets.clear(); metadata.printer_presets.clear(); - // Add the profiles this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.print_presets.push_back(preset.name); }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); @@ -1055,7 +1072,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For metadata.printer_presets.clear(); metadata.is_subscribed = true; - // Load presets from bundle (same logic as __local__) this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.print_presets.push_back(preset.name); }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); @@ -1076,34 +1092,41 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For } } - // BBS do not load sla_print - // BBS: change directoties by design - try { - std::string print_selected_preset_name = prints.get_selected_preset().name; - this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); - prints.select_preset_by_name(print_selected_preset_name, false); - } catch (const std::runtime_error &err) { - errors_cummulative += err.what(); + // BBS: change directories by design + + { + const auto json_t0 = std::chrono::steady_clock::now(); + try { + std::string sel = prints.get_selected_preset().name; + this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); + prints.select_preset_by_name(sel, false); + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + try { + std::string sel = filaments.get_selected_preset().name; + this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); + filaments.select_preset_by_name(sel, false); + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + try { + std::string sel = printers.get_selected_preset().name; + this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); + printers.select_preset_by_name(sel, false); + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); + + const auto json_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - json_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: user presets loaded from JSON in " << json_ms << " ms"; } - try { - std::string filament_selected_preset_name = filaments.get_selected_preset().name; - this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); - filaments.select_preset_by_name(filament_selected_preset_name, false); - } catch (const std::runtime_error &err) { - errors_cummulative += err.what(); + + { + const auto ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - user_load_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: user + bundle presets loaded in " << ms << " ms"; } - try { - std::string printer_selected_preset_name = printers.get_selected_preset().name; - this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); - printers.select_preset_by_name(printer_selected_preset_name, false); - } catch (const std::runtime_error &err) { - errors_cummulative += err.what(); - } - if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); + this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); - set_calibrate_printer(""); return PresetsConfigSubstitutions(); @@ -1209,13 +1232,10 @@ bool PresetBundle::apply_vendor_config( : std::map(); // Find vendors that need installation - const auto vendor_dir = (fs::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); - std::vector install_bundles; for (const auto &it : new_vendors) { if (it.second.size() > 0) { - auto vendor_file = vendor_dir / (it.first + ".json"); - if (!fs::exists(vendor_file)) { + if (!is_vendor_installed(it.first)) { install_bundles.emplace_back(it.first); } } @@ -2223,6 +2243,16 @@ void PresetBundle::remove_users_preset(AppConfig &config, std::mapprints.m_printer_hold_alias.clear(); + this->sla_prints.m_printer_hold_alias.clear(); + this->filaments.m_printer_hold_alias.clear(); + this->sla_materials.m_printer_hold_alias.clear(); + this->printers.m_printer_hold_alias.clear(); +} //BBS: add json related logic, load system presets from json std::pair PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) @@ -2242,22 +2272,19 @@ std::pair PresetBundle::load_system_pre if (validation_mode) dir = (boost::filesystem::path(data_dir())).make_preferred(); + const auto load_t0 = std::chrono::steady_clock::now(); + + // The vendors below are loaded whole and against each other — the filament + // library first, then every other vendor with it as the base — so each parse + // is complete enough to be worth caching. + m_generate_vendor_caches = m_generate_vendor_caches || ! validation_mode; + PresetsConfigSubstitutions substitutions; std::string errors_cummulative; - bool first = true; - std::vector vendor_names; - // store all vendor names in vendor_names - for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { - std::string vendor_file = dir_entry.path().string(); - if (!Slic3r::is_json_file(vendor_file)) - continue; - - std::string vendor_name = dir_entry.path().filename().string(); - - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - vendor_names.push_back(vendor_name); - } + bool first = true; + // Sorted, so any duplicate-preset warning below comes out in the same order on + // every run. + const std::set vendor_names = vendor_names_in(dir); // Separate ORCA_FILAMENT_LIBRARY from other vendors. It must be loaded // first because other vendors' filaments may inherit from it via the // `base_bundle` lookup in parse_subfile. The remaining vendors are @@ -2273,8 +2300,13 @@ std::pair PresetBundle::load_system_pre } // Step 1: Load ORCA_FILAMENT_LIBRARY into `this` synchronously. - if (!orca_lib_vendor.empty()) { + if (! orca_lib_vendor.empty()) { try { + // Match a fresh launch before parsing: hold aliases and the error + // counter survive reset(), and would otherwise carry prior-cycle + // state into this load. + this->clear_printer_hold_aliases(); + this->m_errors = 0; append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); first = false; } catch (const std::runtime_error &err) { @@ -2297,10 +2329,10 @@ std::pair PresetBundle::load_system_pre for (size_t i = range.begin(); i < range.end(); ++i) { auto bundle = std::make_unique(); bundle->set_is_validation_mode(validation_mode); + bundle->set_generate_vendor_caches(m_generate_vendor_caches); try { auto result = bundle->load_vendor_configs_from_json( - dir.string(), other_vendors[i], PresetBundle::LoadSystem, - compatibility_rule, this); + dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this); parallel_substitutions[i] = std::move(result.first); parallel_bundles[i] = std::move(bundle); } catch (const std::runtime_error &err) { @@ -2345,6 +2377,11 @@ std::pair PresetBundle::load_system_pre } this->update_system_maps(); + + const auto load_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - load_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << vendor_names.size() << " vendor(s) loaded in " << load_ms << " ms"; + //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative; return std::make_pair(std::move(substitutions), errors_cummulative); @@ -4759,30 +4796,287 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": finished"); } +// Orca: load one source-form preset entry — parsed from its JSON subfile just +// now, or deserialized from the vendor's cache; the code is shared so a +// cache-loaded bundle cannot come out different from a JSON-loaded one. +// Resolves `inherits` against the presets loaded before this one +// (config_maps) or against base_bundle's filament library, flattens, validates +// and registers the preset. Returns the reason loading failed, empty on +// success. +std::string PresetBundle::load_vendor_preset( + const CachedPreset& entry, + const std::string& path, const std::string& vendor_name, + const PresetBundle* base_bundle, + LoadConfigBundleAttributes flags, + ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, + std::map& config_maps, std::map& filament_id_maps, + PresetCollection* presets_collection, size_t& count, bool is_from_lib, + const std::set* retain_configs) +{ + const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); + const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; + const std::string& preset_name = entry.name; + std::string alias_name, filament_id = entry.filament_id; + std::vector renamed_from = entry.renamed_from; + DynamicPrintConfig config; + const DynamicPrintConfig* default_config = nullptr; + std::string reason; + + //check whether it inherits other preset or not + if (! entry.inherits.empty()) { + auto it2 = config_maps.find(entry.inherits); + if (it2 != config_maps.end()) + default_config = &(it2->second); + if (default_config == nullptr && base_bundle != nullptr) { + auto base_it2 = base_bundle->m_config_maps.find(entry.inherits); + if (base_it2 != base_bundle->m_config_maps.end()) + default_config = &(base_it2->second); + } + if (default_config != nullptr) { + if (filament_id.empty() && (presets_collection->type() == Preset::TYPE_FILAMENT)) { + auto filament_id_map_iter = filament_id_maps.find(entry.inherits); + if (filament_id_map_iter != filament_id_maps.end()) { + filament_id = filament_id_map_iter->second; + } + if (filament_id.empty() && base_bundle != nullptr) { + auto base_filament_id_map_iter = base_bundle->m_filament_id_maps.find(entry.inherits); + if (base_filament_id_map_iter != base_bundle->m_filament_id_maps.end()) { + filament_id = base_filament_id_map_iter->second; + } + } + } + } + else { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << entry.inherits << " for " << preset_name; + // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); + reason = "Can not find inherits: " + entry.inherits; + return reason; + } + } + else { + if (presets_collection->type() == Preset::TYPE_PRINTER) + default_config = &presets_collection->default_preset_for(entry.config_src).config; + else + default_config = &presets_collection->default_preset().config; + } + config = *default_config; + config.apply(entry.config_src); + extend_default_config_length(config, true, *default_config); + if (entry.instantiation == "false" && "Template" != vendor_name) { + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys + << ", which were removed"; + } + + if (retain_configs == nullptr || retain_configs->count(preset_name) != 0) + config_maps.emplace(preset_name, std::move(config)); + if ((presets_collection->type() == Preset::TYPE_FILAMENT) && (!filament_id.empty())) + filament_id_maps.emplace(preset_name, filament_id); + return reason; + } + if (config.has("alias")) + alias_name = (dynamic_cast(config.option("alias")))->value; + Preset::normalize(config); + + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys + << ", which were removed"; + } + + if (presets_collection->type() == Preset::TYPE_PRINTER) { + // Filter out printer presets, which are not mentioned in the vendor profile. + // These presets are considered not installed. + auto printer_model = config.opt_string("printer_model"); + if (printer_model.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines no printer model, it will be ignored."; + reason = std::string("can not find printer_model"); + return reason; + } + auto printer_variant = config.opt_string("printer_variant"); + if (printer_variant.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines no printer variant, it will be ignored."; + reason = std::string("can not find printer_variant"); + return reason; + } + auto it_model = std::find_if(current_vendor_profile->models.cbegin(), current_vendor_profile->models.cend(), + [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } + ); + if (it_model == current_vendor_profile->models.end()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; + reason = std::string("can not find printer model in vendor profile"); + return reason; + } + auto it_variant = it_model->variant(printer_variant); + if (it_variant == nullptr) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; + reason = std::string("can not find printer_variant in vendor profile"); + return reason; + } + // An instantiation printer profile's nozzle_diameter must match the numeric (diameter) + // prefix of its printer_variant: "0.4" -> {0.4}, "0.8HF" -> {0.8} (a trailing + // non-numeric suffix such as "HF"/"HS" distinguishes a hardware sub-variant and is + // ignored here), and for multi-nozzle printers "0.4+0.6" -> {0.4, 0.6}. + // Note: a variant may legitimately repeat across presets of the same model (e.g. speed + // modes, IDEX copy/mirror, or different control boards), so only the diameter is + // validated, not variant uniqueness. Validation-only so the app keeps loading existing + // profiles unchanged. + if (validation_mode && entry.instantiation == "true") { + const auto *nd = config.option("nozzle_diameter"); + std::set nozzles, variant_nozzles; + if (nd != nullptr) + nozzles.insert(nd->values.begin(), nd->values.end()); + std::vector variant_tokens; + boost::algorithm::split(variant_tokens, printer_variant, boost::algorithm::is_any_of("+")); + bool variant_ok = true; // printer_variant is already guaranteed non-empty above + for (const std::string &tok : variant_tokens) { + size_t consumed = 0; + double d = string_to_double_decimal_point(tok, &consumed); + // Require a leading numeric diameter; a trailing suffix (e.g. "HF") is allowed. + if (consumed == 0) { variant_ok = false; break; } + variant_nozzles.insert(d); + } + if (!variant_ok || variant_nozzles != nozzles) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" has printer_variant \"" << printer_variant << + "\" that does not match its nozzle_diameter \"" << (nd ? nd->serialize() : std::string()) << "\". " + "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " + "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " + "nozzle order (e.g. \"0.4+0.6\")."; + } + } + } + const Preset *preset_existing = presets_collection->find_preset(preset_name, false); + if (preset_existing != nullptr) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" has already been loaded from another Config Bundle."; + reason = std::string("duplicated defines"); + return reason; + } + + auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / entry.sub_path).make_preferred(); + if(validation_mode) + file_path = (boost::filesystem::path(data_dir()) / vendor_name / entry.sub_path).make_preferred(); + + // Load the preset into the list of presets, save it to disk. + Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); + if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { + loaded.is_system = true; + loaded.vendor = current_vendor_profile; + loaded.version = current_vendor_profile->config_version; + loaded.description = entry.description; + loaded.setting_id = entry.setting_id; + // Derive the preset setting_id on the fly when a profile ships without one, + // matching scripts/assign_vendor_setting_ids.py. Only instantiated presets + // carry an id; non-instantiated base profiles return earlier above. This never + // touches the per-user cloud-sync setting_id written into user .info files. + if (loaded.setting_id.empty() && entry.instantiation == "true") + loaded.setting_id = generate_preset_setting_id( + vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); + loaded.filament_id = filament_id; + loaded.m_from_orca_filament_lib = is_from_lib; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; + if (presets_collection->type() == Preset::TYPE_FILAMENT) { + if (filament_id.empty() && "Template" != vendor_name) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find filament_id for " << preset_name; + //throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); + reason = "Can not find filament_id for " + preset_name; + return reason; + } + else { + filament_id_maps.emplace(preset_name, filament_id); + } + } + } + + // Derive the profile logical name aka alias from the preset name if the alias was not stated explicitely. + 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); + if (renamed_from.empty()) + // Add the preset name with the '@' character removed into the "renamed_from" list. + renamed_from.emplace_back(alias_name + preset_name.substr(end_pos + 1)); + boost::trim_right(alias_name); + } + } + if (alias_name.empty()) + loaded.alias = preset_name; + else { + loaded.alias = std::move(alias_name); + filaments.set_printer_hold_alias(loaded.alias, loaded); + } + loaded.renamed_from = std::move(renamed_from); + if (! substitution_context.empty()) + substitutions.push_back({ + preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); + if (retain_configs == nullptr || retain_configs->count(preset_name) != 0) + config_maps.emplace(preset_name, loaded.config); + ++count; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %subfile; + return reason; +} + //BBS: Load a config bundle file from json std::pair PresetBundle::load_vendor_configs_from_json( - const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) + const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) { // Enable substitutions for user config bundle, throw an exception when loading a system profile. ConfigSubstitutionContext substitution_context { compatibility_rule }; PresetsConfigSubstitutions substitutions; + // Errors already on this bundle when the load began; the cache stamp below + // counts only what this parse adds. + const int errors_at_entry = m_errors; //BBS: add config related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%")%path.c_str()%compatibility_rule; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%")%dir.c_str()%compatibility_rule; if (flags.has(LoadConfigBundleAttribute::ResetUserProfile) || flags.has(LoadConfigBundleAttribute::LoadSystem)) // Reset this bundle, delete user profile files if SaveImported. this->reset(flags.has(LoadConfigBundleAttribute::SaveImported)); + // Orca: only a whole-vendor load has a cache — the vendor-only and filament-only + // scans want a slice of one. Validation reads the JSONs whatever is cached. + const boost::filesystem::path dir_path(dir); + const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); + if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) { + size_t presets_loaded = 0; + for (const PresetCollection* coll : std::initializer_list{ + &this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, &this->printers }) + presets_loaded += coll->m_presets.size() - coll->m_num_default_presets; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", %1% served from its preset cache, %2% presets")%vendor_name%presets_loaded; + return std::make_pair(std::move(substitutions), presets_loaded); + } + // 1) load the vroot json and construct the vendor profile VendorProfile vendor_profile(vendor_name); - std::string root_file = path + "/" + vendor_name + ".json"; + std::string root_file = dir + "/" + vendor_name + ".json"; std::vector> machine_model_subfiles; std::vector> process_subfiles; std::vector> filament_subfiles; std::vector> machine_subfiles; auto get_name_and_subpath = [this](json::iterator& it, std::vector>& subfile_map) { if (it.value().is_array()) { - for (auto iter1 = it.value().begin(); iter1 != it.value().end(); iter1++) { + size_t index = 0; + for (auto iter1 = it.value().begin(); iter1 != it.value().end(); iter1++, index++) { if (iter1.value().is_object()) { std::string name, subpath; for (auto iter2 = iter1.value().begin(); iter2 != iter1.value().end(); iter2++) { @@ -4802,7 +5096,10 @@ std::pair PresetBundle::load_vendor_configs_ subfile_map.push_back(std::make_pair(name, subpath)); } else { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid type for " << iter1.key(); + // An array element has no key, and asking one for it throws + // nlohmann's invalid_iterator — not a parse_error, so it would + // escape the catch around this parse. Say where it is instead. + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid type for " << it.key() << "[" << index << "]"; } } } else { @@ -4823,7 +5120,7 @@ std::pair PresetBundle::load_vendor_configs_ if (! config_version) { ++m_errors; throw ConfigurationError((boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly") - % vendor_name % version_str % path).str()); + % vendor_name % version_str % dir).str()); } else { vendor_profile.config_version = std::move(*config_version); } @@ -4861,7 +5158,7 @@ std::pair PresetBundle::load_vendor_configs_ catch(nlohmann::detail::parse_error &err) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "< PresetBundle::load_vendor_configs_ //2) paste the machine model for (auto& machine_model : machine_model_subfiles) { - std::string subfile = path + "/" + vendor_name + "/" + machine_model.second; + std::string subfile = dir + "/" + vendor_name + "/" + machine_model.second; VendorProfile::PrinterModel model; model.id = machine_model.first; try { @@ -4976,7 +5273,7 @@ std::pair PresetBundle::load_vendor_configs_ catch(nlohmann::detail::parse_error &err) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly") - %subfile %err.what() % path).str()); + %subfile %err.what() % dir).str()); } if (! model.id.empty() && ! model.variants.empty()) @@ -4985,7 +5282,6 @@ std::pair PresetBundle::load_vendor_configs_ //insert the vendor profile this->vendors.emplace(vendor_name, vendor_profile); - const VendorProfile* current_vendor_profile = &this->vendors[vendor_name]; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", loaded vendor profile, name %1%, id %2%, version %3%")%vendor_profile.name%vendor_profile.id%vendor_profile.config_version.to_string(); @@ -4996,123 +5292,65 @@ std::pair PresetBundle::load_vendor_configs_ PresetCollection *presets = nullptr; size_t presets_loaded = 0; - auto parse_subfile = [this, path, vendor_name, presets_loaded, current_vendor_profile, base_bundle]( + // Parse one subfile into a source-form entry — everything the JSON states, + // nothing resolved. Loading the entry (load_vendor_preset) is the + // same code whether the entry was parsed just now or deserialized from the + // vendor's cache. + auto parse_subfile = [this, dir, vendor_name]( ConfigSubstitutionContext& substitution_context, - PresetsConfigSubstitutions& substitutions, - LoadConfigBundleAttributes& flags, - std::pair& subfile_iter, - std::map& config_maps, - std::map& filament_id_maps, - PresetCollection* presets_collection, - size_t& count, bool is_from_lib = false) -> std::string { + const std::pair& subfile_iter, + CachedPreset& entry) -> std::string { - std::string subfile = path + "/" + vendor_name + "/" + subfile_iter.second; - // Load the print, filament or printer preset. - std::string preset_name; - DynamicPrintConfig config; - std::string alias_name, inherits, description, instantiation, setting_id, filament_id; - std::vector renamed_from; - const DynamicPrintConfig* default_config = nullptr; - std::string reason; + std::string subfile = dir + "/" + vendor_name + "/" + subfile_iter.second; + std::string reason; try { std::map key_values; substitution_context.substitutions.clear(); //parse the json elements - DynamicPrintConfig config_src; - std::string _renamed_from_str; - config_src.load_from_json(subfile, substitution_context, false, key_values, reason); + entry.sub_path = subfile_iter.second; + entry.config_src.load_from_json(subfile, substitution_context, false, key_values, reason); if (!reason.empty()) { ++m_errors; BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": load config file "<second; + entry.setting_id = setting_it->second; auto filament_it = key_values.find(BBL_JSON_KEY_FILAMENT_ID); if (filament_it != key_values.end()) - filament_id = filament_it->second; - //check whether it inherits other preset or not + entry.filament_id = filament_it->second; auto it1 = key_values.find(BBL_JSON_KEY_INHERITS); if (it1 != key_values.end()) { - inherits = it1->second; - auto it2 = config_maps.find(inherits); - default_config = nullptr; - if (it2 != config_maps.end()) - default_config = &(it2->second); - if(default_config == nullptr && base_bundle != nullptr) { - auto base_it2 = base_bundle->m_config_maps.find(inherits); - if (base_it2 != base_bundle->m_config_maps.end()) - default_config = &(base_it2->second); - } - if (default_config != nullptr) { - if (filament_id.empty() && (presets_collection->type() == Preset::TYPE_FILAMENT)) { - auto filament_id_map_iter = filament_id_maps.find(inherits); - if (filament_id_map_iter != filament_id_maps.end()) { - filament_id = filament_id_map_iter->second; - } - if (filament_id.empty() && base_bundle != nullptr) { - auto filament_id_map_iter = base_bundle->m_filament_id_maps.find(inherits); - if (filament_id_map_iter != base_bundle->m_filament_id_maps.end()) { - filament_id = filament_id_map_iter->second; - } - } - } - } - else { + entry.inherits = it1->second; + // An `inherits` key naming nothing can never resolve; fail it + // here so install can key off the empty string as "no inherits". + if (entry.inherits.empty()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << inherits << " for " << preset_name; - // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); - reason = "Can not find inherits: " + inherits; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << entry.inherits << " for " << entry.name; + reason = "Can not find inherits: " + entry.inherits; return reason; } } - else { - if (presets_collection->type() == Preset::TYPE_PRINTER) - default_config = &presets_collection->default_preset_for(config_src).config; - else - default_config = &presets_collection->default_preset().config; - } - config = *default_config; - config.apply(config_src); - extend_default_config_length(config, true, *default_config); - if (instantiation == "false" && "Template" != vendor_name) { - // Report configuration fields, which are misplaced into a wrong group. - std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); - if (!incorrect_keys.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys - << ", which were removed"; - } - - config_maps.emplace(preset_name, std::move(config)); - if ((presets_collection->type() == Preset::TYPE_FILAMENT) && (!filament_id.empty())) - filament_id_maps.emplace(preset_name, filament_id); - return reason; - } - if (config.has("alias")) - alias_name = (dynamic_cast(config.option("alias")))->value; - if (key_values.find(ORCA_JSON_KEY_RENAMED_FROM) != key_values.end()) { - if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], renamed_from)) { - BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << path << "\": The preset \"" << preset_name + if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], entry.renamed_from)) { + BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << dir << "\": The preset \"" << entry.name << "\" contains invalid \"renamed_from\" key, which is being ignored."; } } - Preset::normalize(config); } catch(nlohmann::detail::parse_error &err) { ++m_errors; @@ -5120,195 +5358,60 @@ std::pair PresetBundle::load_vendor_configs_ reason = std::string("json parse error") + err.what(); return reason; } - - // Report configuration fields, which are misplaced into a wrong group. - std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); - if (!incorrect_keys.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys - << ", which were removed"; - } - - if (presets_collection->type() == Preset::TYPE_PRINTER) { - // Filter out printer presets, which are not mentioned in the vendor profile. - // These presets are considered not installed. - auto printer_model = config.opt_string("printer_model"); - if (printer_model.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines no printer model, it will be ignored."; - reason = std::string("can not find printer_model"); - return reason; - } - auto printer_variant = config.opt_string("printer_variant"); - if (printer_variant.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines no printer variant, it will be ignored."; - reason = std::string("can not find printer_variant"); - return reason; - } - auto it_model = std::find_if(current_vendor_profile->models.cbegin(), current_vendor_profile->models.cend(), - [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } - ); - if (it_model == current_vendor_profile->models.end()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; - reason = std::string("can not find printer model in vendor profile"); - return reason; - } - auto it_variant = it_model->variant(printer_variant); - if (it_variant == nullptr) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; - reason = std::string("can not find printer_variant in vendor profile"); - return reason; - } - // An instantiation printer profile's nozzle_diameter must match the numeric (diameter) - // prefix of its printer_variant: "0.4" -> {0.4}, "0.8HF" -> {0.8} (a trailing - // non-numeric suffix such as "HF"/"HS" distinguishes a hardware sub-variant and is - // ignored here), and for multi-nozzle printers "0.4+0.6" -> {0.4, 0.6}. - // Note: a variant may legitimately repeat across presets of the same model (e.g. speed - // modes, IDEX copy/mirror, or different control boards), so only the diameter is - // validated, not variant uniqueness. Validation-only so the app keeps loading existing - // profiles unchanged. - if (validation_mode && instantiation == "true") { - const auto *nd = config.option("nozzle_diameter"); - std::set nozzles, variant_nozzles; - if (nd != nullptr) - nozzles.insert(nd->values.begin(), nd->values.end()); - std::vector variant_tokens; - boost::algorithm::split(variant_tokens, printer_variant, boost::algorithm::is_any_of("+")); - bool variant_ok = true; // printer_variant is already guaranteed non-empty above - for (const std::string &tok : variant_tokens) { - size_t consumed = 0; - double d = string_to_double_decimal_point(tok, &consumed); - // Require a leading numeric diameter; a trailing suffix (e.g. "HF") is allowed. - if (consumed == 0) { variant_ok = false; break; } - variant_nozzles.insert(d); - } - if (!variant_ok || variant_nozzles != nozzles) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" has printer_variant \"" << printer_variant << - "\" that does not match its nozzle_diameter \"" << (nd ? nd->serialize() : std::string()) << "\". " - "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " - "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " - "nozzle order (e.g. \"0.4+0.6\")."; - } - } - } - const Preset *preset_existing = presets_collection->find_preset(preset_name, false); - if (preset_existing != nullptr) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" has already been loaded from another Config Bundle."; - reason = std::string("duplicated defines"); - return reason; - } - - auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / subfile_iter.second).make_preferred(); - if(validation_mode) - file_path = (boost::filesystem::path(data_dir()) / vendor_name / subfile_iter.second).make_preferred(); - - // Load the preset into the list of presets, save it to disk. - Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); - if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { - loaded.is_system = true; - loaded.vendor = current_vendor_profile; - loaded.version = current_vendor_profile->config_version; - loaded.description = description; - loaded.setting_id = setting_id; - // Derive the preset setting_id on the fly when a profile ships without one, - // matching scripts/assign_vendor_setting_ids.py. Only instantiated presets - // carry an id; non-instantiated base profiles return earlier above. This never - // touches the per-user cloud-sync setting_id written into user .info files. - if (loaded.setting_id.empty() && instantiation == "true") - loaded.setting_id = generate_preset_setting_id( - vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); - loaded.filament_id = filament_id; - loaded.m_from_orca_filament_lib = is_from_lib; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; - if (presets_collection->type() == Preset::TYPE_FILAMENT) { - if (filament_id.empty() && "Template" != vendor_name) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find filament_id for " << preset_name; - //throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); - reason = "Can not find filament_id for " + preset_name; - return reason; - } - else { - filament_id_maps.emplace(preset_name, filament_id); - } - } - } - - // Derive the profile logical name aka alias from the preset name if the alias was not stated explicitely. - 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); - if (renamed_from.empty()) - // Add the preset name with the '@' character removed into the "renamed_from" list. - renamed_from.emplace_back(alias_name + preset_name.substr(end_pos + 1)); - boost::trim_right(alias_name); - } - } - if (alias_name.empty()) - loaded.alias = preset_name; - else { - loaded.alias = std::move(alias_name); - filaments.set_printer_hold_alias(loaded.alias, loaded); - } - loaded.renamed_from = std::move(renamed_from); - if (! substitution_context.empty()) - substitutions.push_back({ - preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, - std::string(), std::move(substitution_context.substitutions) }); - config_maps.emplace(preset_name, loaded.config); - ++count; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %subfile; return reason; }; std::map configs; std::map filament_id_maps; + // Orca: whether to (re)write the vendor's cache after this parse, leaving it + // in step with the profile so the next run reads it instead. It is written + // where the vendor was looked for, even when the profile came from resources, + // and stamped with the version that profile claims — a profile without one + // cannot be judged for staleness later, and a cache nothing can invalidate is + // worse than none. + const bool will_cache = cacheable && m_generate_vendor_caches && vendor_profile.config_version.valid(); + VendorCacheData cache_data; + // Errors added by install are counted apart: a cache load runs install again, + // so the parse_errors stamped into the cache must hold only what a cache load + // will not recount. + int install_errors = 0; + auto load_subfiles = [&](std::vector>& subfiles, + std::vector& entries, const char* kind, bool is_from_lib = false) { + configs.clear(); + filament_id_maps.clear(); + for (auto& subfile : subfiles) { + CachedPreset entry; + std::string reason = parse_subfile(substitution_context, subfile, entry); + if (reason.empty()) { + const int errors_before_install = m_errors; + reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags, + substitution_context, substitutions, configs, filament_id_maps, presets, + presets_loaded, is_from_lib); + install_errors += m_errors - errors_before_install; + } + if (!reason.empty()) { + ++m_errors; + //parse error + std::string subfile_path = dir + "/" + vendor_name + "/" + subfile.second; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse %1% setting from %2%") % kind % subfile_path; + throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % dir).str()); + } + if (will_cache) + entries.emplace_back(std::move(entry)); + } + }; + + // The section order below — process, filaments (with the ORCA-lib map copy), + // printers — is mirrored by load_vendor_cache's install loops; keep the two + // in lockstep. //3.1) paste the process presets = &this->prints; - configs.clear(); - filament_id_maps.clear(); - for (auto& subfile : process_subfiles) - { - std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); - if (!reason.empty()) { - ++m_errors; - //parse error - std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse process setting from %1%") % subfile_path; - throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); - } - } + load_subfiles(process_subfiles, cache_data.process_entries, "process"); //3.2) paste the filaments presets = &this->filaments; - configs.clear(); - filament_id_maps.clear(); const auto is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; - for (auto& subfile : filament_subfiles) - { - std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, - presets_loaded, is_orca_lib); - if (!reason.empty()) { - ++m_errors; - //parse error - std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse filament setting from %1%") % subfile_path; - throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); - } - } + load_subfiles(filament_subfiles, cache_data.filament_entries, "filament", is_orca_lib); if (is_orca_lib) { m_config_maps = configs; m_filament_id_maps = filament_id_maps; @@ -5316,18 +5419,16 @@ std::pair PresetBundle::load_vendor_configs_ //3.3) paste the printers presets = &this->printers; - configs.clear(); - filament_id_maps.clear(); - for (auto& subfile : machine_subfiles) - { - std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); - if (!reason.empty()) { - ++m_errors; - //parse error - std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse printer setting from %1%") % subfile_path; - throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); - } + load_subfiles(machine_subfiles, cache_data.machine_entries, "printer"); + + if (will_cache) { + // Clamped: the count is a difference of three tallies, and a stamp that + // wrapped would be added to every future load of this vendor. + cache_data.parse_errors = uint64_t(std::max(0, m_errors - errors_at_entry - install_errors)); + cache_data.vendors = this->vendors; + if (! VendorCacheFile::save((dir_path / (vendor_name + ".opc")).string(), vendor_name, + vendor_profile.config_version.to_string(), cache_data)) + BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache for " << vendor_name; } //BBS: add config related logs @@ -5952,4 +6053,94 @@ bool BundleMetadata::save_to_json(const std::string& path) const return false; } } +// ---- Per-vendor preset cache: install into this bundle ------------------- +// The file format itself lives in PresetCacheFormat.cpp (VendorCacheFile). + +bool PresetBundle::load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle) +{ + // A vendor is loaded from where it is installed and nowhere else; resources + // reaches the app by being installed into `dir` first. The cache there is + // judged against the profile beside it — or, where the cache is the whole + // of the installation, against nothing, since nothing on disk can then be + // newer than it. That state is Semver::inf(), which no real profile carries. + const boost::filesystem::path profile = dir / (vendor_name + ".json"); + const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) + : Semver::inf(); + return this->load_vendor_cache((dir / (vendor_name + ".opc")).string(), vendor_name, version, base_bundle); +} + +bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, const PresetBundle* base_bundle) +{ + // What this bundle had counted before the cache was tried. The caller + // measures its own parse against this same baseline, so a rejection must + // put it back rather than reset it to zero. + const int errors_at_entry = this->m_errors; + // Read and validated before this bundle is touched: a rejected file leaves + // no state to roll back. + VendorCacheData data; + if (! VendorCacheFile::load(cache_path, expected_vendor_name, expected_vendor_version, data)) + return false; + try { + const std::string& vendor_name = expected_vendor_name; // VendorCacheFile::load checked they match + this->vendors = std::move(data.vendors); + + // What the parse counted before install took over; install recounts its + // own below, so m_errors comes out as a JSON parse would leave it. + m_errors += int(data.parse_errors); + + // Install the entries exactly as load_vendor_configs_from_json installs + // them straight after parsing — same code, same order. The substitution + // context stays empty (the entries were substituted when they were + // parsed), so no substitutions are reported, as before. + ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent }; + PresetsConfigSubstitutions substitutions; + std::map configs; + std::map filament_id_maps; + const std::string path = boost::filesystem::path(cache_path).parent_path().string(); + size_t count = 0; + auto install_entries = [&](const std::vector& entries, PresetCollection* presets, bool is_from_lib) { + configs.clear(); + filament_id_maps.clear(); + // Only configs of presets that other entries inherit are ever looked + // up again; registering just those skips one full config copy for + // every leaf preset. The library's filaments are all retained — they + // become the m_config_maps other vendors resolve against. + std::set inherited; + for (const CachedPreset& entry : entries) + if (! entry.inherits.empty()) + inherited.insert(entry.inherits); + const std::set* retain_configs = is_from_lib ? nullptr : &inherited; + for (const CachedPreset& entry : entries) { + const std::string reason = load_vendor_preset(entry, path, vendor_name, + base_bundle, LoadConfigBundleAttribute::LoadSystem, substitution_context, substitutions, + configs, filament_id_maps, presets, count, is_from_lib, retain_configs); + if (! reason.empty()) + throw std::runtime_error("entry " + entry.name + " failed to install: " + reason); + } + }; + install_entries(data.process_entries, &this->prints, false); + const bool is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; + install_entries(data.filament_entries, &this->filaments, is_orca_lib); + if (is_orca_lib) { + m_config_maps = configs; + m_filament_id_maps = filament_id_maps; + } + install_entries(data.machine_entries, &this->printers, false); + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "PresetBundle: rejecting vendor cache " << cache_path << ": " << e.what(); + // Restore a clean state so the caller can fall back to the JSON parse. + this->reset(false); + this->vendors.clear(); + this->m_config_maps.clear(); + this->m_filament_id_maps.clear(); + this->m_errors = errors_at_entry; + // A failure partway through installing may have left presets in some + // collections with hold aliases already registered. + this->clear_printer_hold_aliases(); + return false; + } +} + } // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 685687975b..9da8fb4251 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -2,10 +2,12 @@ #define slic3r_PresetBundle_hpp_ #include "Preset.hpp" +#include "PresetCacheFormat.hpp" #include "AppConfig.hpp" #include "enum_bitmask.hpp" #include +#include #include #include #include @@ -170,6 +172,31 @@ struct PresetBundleMetadata class PresetBundle { public: + // ---- Per-vendor preset cache -------------------------------------------- + // One cache file per vendor (plus the Orca filament library), stamped with + // the vendor's own profile version rather than a directory scan. The bytes + // on disk are VendorCacheFile's business (PresetCacheFormat.hpp); what + // lives here is how a cache's contents install into a bundle. + + // The cache is not something a caller loads from: a vendor is loaded with + // load_vendor_configs_from_json, which comes from the cache whenever one covers + // it. What is public here is what the cache's own tests drive directly. + + // Load a per-vendor cache into this bundle by installing its entries, with + // base_bundle's filament library as the inheritance base. Rejects (returns + // false, with this bundle left clean) unless VendorCacheFile::load accepts + // the file — see its contract for the version and identity checks — and + // every entry installs. Options this build no longer defines are dropped, + // not fatal — the payload names its own keys. + bool load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, const PresetBundle* base_bundle = nullptr); + + // Enable writing a per-vendor cache after a JSON parse (off by default). Cache + // content is pure parse output, so the guard is policy, not correctness: only + // the deliberate generators (load_system_presets_from_json, the cache build + // tool) write files, not every incidental load a dialog performs. + void set_generate_vendor_caches(bool enable) { m_generate_vendor_caches = enable; } + static DynamicPrintConfig construct_full_config(Preset &in_printer_preset, Preset &in_print_preset, const DynamicPrintConfig &project_config, @@ -444,8 +471,12 @@ public: /*std::pair load_configbundle( const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/ //Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance + // Orca: `dir` is where the vendor is looked for — its own directory, whether or + // not the profile JSONs are still there. A whole-vendor load comes from the + // vendor's preset cache whenever one covers the profile on disk, and is parsed + // from the JSONs in `dir` only when none does. Nothing here reads resources. std::pair load_vendor_configs_from_json( - const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr); + const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr); // Export a config bundle file containing all the presets and the names of the active presets. //void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); @@ -517,11 +548,49 @@ public: // Orca: for validation only. bool has_errors(bool check_duplicate_filament_subtypes = false) const; + // Errors the last load recorded. What the cache's error accounting promises — + // a cache-served vendor reports what its parse would — is pinned against this. + int error_count() const { return m_errors; } + // Orca: for validation only. Flag any system preset whose inherits / compatible_printers / // compatible_prints references a deleted (unknown) or renamed (old) preset name. bool check_preset_references() const; + // Merge one vendor's presets with the other vendor's presets, report duplicates. + // Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a + // bundle out of several per-vendor caches loaded into separate PresetBundle instances. + std::vector merge_presets(PresetBundle &&other); + private: + // Load one vendor from the preset cache installed in `dir`, judged against + // the vendor profile there. False, with this bundle left clean, when there + // is no usable cache and the vendor has to be parsed. This is how + // load_vendor_configs_from_json reads a cache. + bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle); + + // Load one source-form preset entry into this bundle: resolve `inherits`, + // flatten, validate and register the preset. Returns the reason loading + // failed, empty on success. See the definition for the sharing contract + // between the JSON parse and the cache load. + // retain_configs, when non-null, names the only presets registered into + // config_maps (a full config copy each). The cache load passes the names its + // entries inherit — the only ones ever looked up again; the JSON parse + // retains all, not knowing what later subfiles inherit. + std::string load_vendor_preset(const CachedPreset& entry, + const std::string& path, const std::string& vendor_name, + const PresetBundle* base_bundle, + LoadConfigBundleAttributes flags, + ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, + std::map& config_maps, std::map& filament_id_maps, + PresetCollection* presets_collection, size_t& count, bool is_from_lib, + const std::set* retain_configs = nullptr); + + // Clear every collection's m_printer_hold_alias, which reset() leaves alone. + void clear_printer_hold_aliases(); + + // Whether to (re)write a per-vendor cache after a JSON parse. + bool m_generate_vendor_caches { false }; + // Orca: validation only - flag any printer with two or more compatible // filament presets sharing one filament_id (ambiguous AMS subtype match). bool check_duplicate_filament_subtypes() const; @@ -529,8 +598,6 @@ private: //std::pair load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule); //BBS: add json related logic std::pair load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule); - // Merge one vendor's presets with the other vendor's presets, report duplicates. - std::vector merge_presets(PresetBundle &&other); // Update the multicolor information for filaments. void update_filament_multi_color(); // Update renamed_from and alias maps of system profiles. diff --git a/src/libslic3r/PresetCacheFormat.cpp b/src/libslic3r/PresetCacheFormat.cpp new file mode 100644 index 0000000000..accebba7b4 --- /dev/null +++ b/src/libslic3r/PresetCacheFormat.cpp @@ -0,0 +1,588 @@ +#include "libslic3r/PresetCacheFormat.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" + +namespace Slic3r { + +CacheDictionary::CacheDictionary() +{ + // ENUM_UNNAMED is index 0 and always the empty name. + m_enum_values.emplace_back(); +} + +// The ints an enum option holds — one for a coEnum, the whole vector for coEnums. +static std::vector enum_ints(const ConfigOptionDef& def, const ConfigOption* opt) +{ + if (def.type == coEnum) + return { opt->getInt() }; + return static_cast(opt)->values; +} + +// The name this build gives one of those ints, empty where it has none — a +// nullable option's nil, or a definition carrying no enum_keys_map. Enums are +// written by name so a build that reorders an enum's values still reads it right. +static std::string enum_name_of(const ConfigOptionDef& def, int value) +{ + if (def.enum_keys_map != nullptr) + for (const auto& kvp : *def.enum_keys_map) + if (kvp.second == value) + return kvp.first; + return {}; +} + +void CacheDictionary::collect(const DynamicPrintConfig& config) +{ + for (auto it = config.cbegin(); it != config.cend(); ++ it) { + const ConfigOptionDef* def = print_config_def.get(it->first); + if (def == nullptr) + continue; // save_config does not write it either + if (m_key_index.try_emplace(it->first, uint16_t(m_keys.size())).second) { + m_keys.push_back(it->first); + m_types.push_back(uint16_t(def->type)); + } + if (def->type != coEnum && def->type != coEnums) + continue; + for (int value : enum_ints(*def, it->second.get())) { + std::string name = enum_name_of(*def, value); + if (! name.empty() && m_enum_index.try_emplace(name, uint16_t(m_enum_values.size())).second) + m_enum_values.push_back(std::move(name)); + } + } +} + +uint16_t CacheDictionary::key_index(const t_config_option_key& key) const +{ + auto it = m_key_index.find(key); + if (it == m_key_index.end()) + throw std::runtime_error("preset cache: option " + key + " was never collected into the dictionary"); + return it->second; +} + +uint16_t CacheDictionary::enum_index(const std::string& name) const +{ + if (name.empty()) + return ENUM_UNNAMED; + auto it = m_enum_index.find(name); + return it == m_enum_index.end() ? ENUM_UNNAMED : it->second; +} + +void CacheDictionary::save(cereal::BinaryOutputArchive& ar) const +{ + // Checked here rather than left to the caller: an index that wrapped would + // be written silently, and nothing downstream could tell. + if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES) + throw std::runtime_error("preset cache: the option dictionary outgrew the uint16 it is indexed with"); + ar(m_keys, m_types, m_enum_values); +} + +void CacheDictionary::load(cereal::BinaryInputArchive& ar) +{ + ar(m_keys, m_types, m_enum_values); + if (m_keys.size() != m_types.size()) + throw std::runtime_error("preset cache: dictionary key and type tables differ in length"); + if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES) + throw std::runtime_error("preset cache: dictionary is larger than the uint16 it is indexed with"); + if (m_enum_values.empty() || ! m_enum_values.front().empty()) + throw std::runtime_error("preset cache: dictionary is missing its unnamed-enum slot"); + // Resolved once per file: every option read after this is a vector index. + m_defs.resize(m_keys.size()); + for (size_t i = 0; i < m_keys.size(); ++ i) { + const ConfigOptionDef* def = print_config_def.get(m_keys[i]); + m_defs[i] = (def != nullptr && uint16_t(def->type) == m_types[i]) ? def : nullptr; + } +} + +// ---- one config ----------------------------------------------------------- + +static void save_enum_option(cereal::BinaryOutputArchive& ar, const ConfigOptionDef& def, + const ConfigOption* opt, const CacheDictionary& dict) +{ + const std::vector values = enum_ints(def, opt); + ar(uint32_t(values.size())); + for (int value : values) { + const uint16_t idx = dict.enum_index(enum_name_of(def, value)); + ar(idx); + if (idx == CacheDictionary::ENUM_UNNAMED) + ar(int32_t(value)); + } +} + +// `config` may be null, in which case the option is read and dropped. +static void load_enum_option(cereal::BinaryInputArchive& ar, ConfigOptionType type, + const ConfigOptionDef* def, DynamicPrintConfig* config, + const CacheDictionary& dict) +{ + uint32_t cnt = 0; + ar(cnt); + if (type == coEnum && cnt != 1) + throw std::runtime_error("preset cache: a scalar enum carrying more than one value"); + // Every element is read whatever happens, so the stream stays in sync and + // whatever follows this option still loads. + bool usable = def != nullptr && config != nullptr; + std::vector values; + values.reserve(cnt); + for (uint32_t i = 0; i < cnt; ++ i) { + uint16_t idx = 0; + ar(idx); + if (! dict.valid_enum_index(idx)) + throw std::runtime_error("preset cache: enum value index past the end of the dictionary"); + if (idx == CacheDictionary::ENUM_UNNAMED) { + // An int the writer could not name — a nil, or an option whose + // definition carried no enum_keys_map. It travels verbatim. + int32_t raw = 0; + ar(raw); + values.push_back(int(raw)); + continue; + } + if (! usable) + continue; // the index above was this element's whole payload + if (def->enum_keys_map == nullptr) { + usable = false; // this build no longer maps this option's names + continue; + } + const auto it = def->enum_keys_map->find(dict.enum_name_at(idx)); + if (it == def->enum_keys_map->end()) { + usable = false; // a value this build dropped: the option goes with it + continue; + } + values.push_back(it->second); + } + if (! usable) + return; + if (type == coEnum) { + config->set_key_value(def->opt_key, new ConfigOptionEnumGeneric(def->enum_keys_map, values.front())); + } else { + auto* opt = def->nullable ? static_cast(new ConfigOptionEnumsGenericNullable(def->enum_keys_map)) + : static_cast(new ConfigOptionEnumsGeneric(def->enum_keys_map)); + opt->values = std::move(values); + config->set_key_value(def->opt_key, opt); + } +} + +void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict) +{ + struct Written { uint16_t idx; const ConfigOptionDef* def; const ConfigOption* opt; }; + std::vector written; + written.reserve(config.size()); + for (auto it = config.cbegin(); it != config.cend(); ++ it) + if (const ConfigOptionDef* def = print_config_def.get(it->first)) + written.push_back({ dict.key_index(it->first), def, it->second.get() }); + + ar(uint32_t(written.size())); + for (const Written& w : written) { + ar(w.idx); + if (w.def->type == coEnum || w.def->type == coEnums) + save_enum_option(ar, *w.def, w.opt, dict); + else + w.def->save_option_to_archive(ar, w.opt); + } +} + +// `config` null means: read everything, keep nothing. +static void read_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig* config, const CacheDictionary& dict) +{ + uint32_t cnt = 0; + ar(cnt); + if (config != nullptr) + config->clear(); + // Reused across the loop: constructing a ConfigOptionDef per dropped option + // would allocate its strings and vectors for nothing. + ConfigOptionDef scratch; + for (uint32_t i = 0; i < cnt; ++ i) { + uint16_t idx = 0; + ar(idx); + if (! dict.valid_key_index(idx)) + throw std::runtime_error("preset cache: option index past the end of the dictionary"); + const ConfigOptionType type = dict.type_at(idx); + const ConfigOptionDef* def = dict.def_at(idx); + if (type == coEnum || type == coEnums) { + load_enum_option(ar, type, def, config, dict); + } else if (def != nullptr && config != nullptr) { + config->set_key_value(def->opt_key, def->load_option_from_archive(ar)); + } else { + // Read by the type the writer recorded, then drop: the same outcome + // a JSON profile gets for an option this build no longer has. + scratch.type = type; + std::unique_ptr discard(scratch.load_option_from_archive(ar)); + } + } +} + +void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict) +{ + read_config(ar, &config, dict); +} + +void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict) +{ + read_config(ar, nullptr, dict); +} + +// ---- The per-vendor cache file (.opc) ----------------------------- + +namespace { + +#pragma pack(push, 1) +struct CacheFileHeader { + uint32_t magic; + uint32_t version; + uint64_t data_size; + uint32_t crc32; +}; +#pragma pack(pop) +static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes"); + +constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ" +// Bump when the wire format changes in a way the payload cannot describe +// itself out of: reordering, removing or retyping a field of a hand-written +// serialize() (VendorProfile and its nested types, CachedPreset via +// save_entries below), or a change to the cache's own layout or the +// meaning of its stamps. Option-schema drift is NOT such a change — the +// dictionary handles it, which is why this no longer moves every release. +constexpr uint32_t CACHE_VERSION = 1; + +// A stamp-string read that refuses an absurd length before allocating anything. +// The stamps are read from files named from the outside (peek_version is +// pointed at whatever .opc a directory holds), so the length word may +// be arbitrary bytes — and a resize to a garbage 64-bit length does not fail as +// a catchable bad_alloc here, it takes the app down through the out-of-memory +// handler. A vendor name or profile version is a short token; anything longer +// is not a cache this build wrote. +std::string read_bounded_string(cereal::BinaryInputArchive& ar) +{ + constexpr uint64_t MAX_STAMP_LEN = 1024; + cereal::size_type len = 0; + ar(cereal::make_size_tag(len)); + if (uint64_t(len) > MAX_STAMP_LEN) + throw std::runtime_error("preset cache: string length out of bounds"); + std::string s(size_t(len), '\0'); + ar(cereal::binary_data(s.data(), size_t(len))); + return s; +} + +// The prologue every cache reader starts with: the format version, then the +// vendor's identity. Returns the vendor version stamped on a body this build can +// read, empty on anything else — which is the same answer as "not this vendor". +std::string read_cache_stamps(cereal::BinaryInputArchive& ar, const std::string& expected_vendor_name) +{ + // The version is judged before anything variable-length is read: on a body + // that is not a per-vendor cache of this version, the bytes where a string + // length would sit may be arbitrary framing. + uint32_t cache_version = 0; + ar(cache_version); + if (cache_version != CACHE_VERSION) + return {}; + const std::string vendor_name = read_bounded_string(ar); + const std::string vendor_version = read_bounded_string(ar); + if (vendor_name != expected_vendor_name) + return {}; + return vendor_version; +} + +// A cache stays usable as long as it was built from a vendor profile at least +// as new as the one now on disk. Profiles whose version is invalid cannot be +// judged this way and are never served from cache; where no profile sits +// beside the cache at all, nothing can be newer than it — that state is passed +// as Semver::inf(), which no real profile can carry (an invalid version could +// not say it apart from "profile there but unjudgeable", and zero would +// collide with a genuine "0.0.0"). This is the serve rule; the install rule +// (cache_covers in PresetBundle.cpp) deliberately reads an unjudgeable profile +// the other way, so the two are not one function. +bool cache_covers_version(const std::string& cached, const Semver& on_disk) +{ + if (on_disk == Semver::inf()) + return true; // before parsing `cached`: nothing exists that the stamp must cover + if (! on_disk.valid()) + return false; + const auto cached_ver = Semver::parse(cached); + return cached_ver && *cached_ver >= on_disk; +} + +// CachedPreset on the wire: all fields, declaration order, in one place. +// `config` writes, reads or skips the config sitting in the middle of that +// order — the three things a reader can want to do with it — so save, load and +// the name peek below cannot drift apart. Keep in sync with the struct in +// PresetCacheFormat.hpp and bump CACHE_VERSION on change. Written here rather +// than as a serialize() member because the config needs the file's dictionary, +// which cereal cannot thread through one. +template +void visit_entry(Archive& ar, Entry& e, ConfigFn&& config) +{ + ar(e.name, e.sub_path); + config(); + ar(e.inherits, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from); +} + +// The count comes from a file that has already passed magic and CRC, but a +// reserve is a promise to allocate: cap it and let push_back grow the rest. +constexpr uint32_t MAX_RESERVED_ENTRIES = 4096; + +void save_entries(cereal::BinaryOutputArchive& ar, + const std::vector& entries, + const CacheDictionary& dict) +{ + ar(uint32_t(entries.size())); + for (const CachedPreset& e : entries) + visit_entry(ar, e, [&] { save_config(ar, e.config_src, dict); }); +} + +void load_entries(cereal::BinaryInputArchive& ar, + std::vector& entries, + const CacheDictionary& dict) +{ + uint32_t cnt = 0; + ar(cnt); + entries.clear(); + entries.reserve(std::min(cnt, MAX_RESERVED_ENTRIES)); + for (uint32_t i = 0; i < cnt; ++ i) { + CachedPreset e; + visit_entry(ar, e, [&] { load_config(ar, e.config_src, dict); }); + entries.push_back(std::move(e)); + } +} + +// Read a raw cache body: verify magic, size, CRC. +bool read_cache_blob(const std::string& path, std::string& out_blob) +{ + try { + boost::nowide::ifstream ifs(path, std::ios::binary); + if (!ifs.is_open()) + return false; + CacheFileHeader fhdr; + if (!ifs.read(reinterpret_cast(&fhdr), sizeof(fhdr))) + return false; + if (fhdr.magic != CACHE_MAGIC) + return false; + // data_size is 8 bytes from a file nothing has authenticated yet, and + // it is about to size an allocation. The body is the whole of the file + // behind the header — anything else is not a cache this build wrote. + ifs.seekg(0, std::ios::end); + const std::streamoff file_size = ifs.tellg(); + if (file_size < std::streamoff(sizeof(fhdr)) || + fhdr.data_size == 0 || + fhdr.data_size != uint64_t(file_size) - sizeof(fhdr)) + return false; + ifs.seekg(sizeof(fhdr), std::ios::beg); + out_blob.assign(fhdr.data_size, '\0'); + if (!ifs.read(&out_blob[0], static_cast(fhdr.data_size))) + return false; + boost::crc_32_type crc; + crc.process_bytes(out_blob.data(), out_blob.size()); + if (crc.checksum() != fhdr.crc32) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: CRC mismatch: " << path; + return false; + } + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: read failed (" << path << "): " << e.what(); + return false; + } +} + +// Write a cache body behind the standard 20-byte file header. False when the +// file could not be opened or written whole. +bool write_cache_blob(const std::string& path, const std::string& blob) +{ + boost::crc_32_type crc; + crc.process_bytes(blob.data(), blob.size()); + // Written beside the target and moved into place, as AppConfig::save does: + // a cache is truncated and rewritten in full, so a write that dies partway + // would otherwise leave a header claiming more body than the file holds. + // The PID suffix also keeps two instances writing the same vendor from + // interleaving. + const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp"; + try { + boost::filesystem::create_directories(boost::filesystem::path(path).parent_path()); + { + boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc); + if (!ofs.is_open()) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path; + return false; + } + CacheFileHeader fhdr; + fhdr.magic = CACHE_MAGIC; + fhdr.version = CACHE_VERSION; + fhdr.data_size = static_cast(blob.size()); + fhdr.crc32 = crc.checksum(); + ofs.write(reinterpret_cast(&fhdr), sizeof(fhdr)); + ofs.write(blob.data(), static_cast(blob.size())); + ofs.close(); // flush; close() raises failbit on error + if (! ofs.good()) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")"; + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + return false; + } + } + if (const std::error_code ec = rename_file(tmp_path, path)) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message(); + boost::system::error_code rm; + boost::filesystem::remove(tmp_path, rm); + return false; + } + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what(); + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + return false; + } +} + +} // anonymous namespace + +// static +bool VendorCacheFile::save(const std::string& path, const std::string& vendor_name, + const std::string& vendor_version, const VendorCacheData& data) +{ + try { + // Collected before anything is written: the dictionary sits ahead of the + // entries so a reader resolves it once and then indexes. + CacheDictionary dict; + for (const std::vector* entries : { &data.process_entries, &data.filament_entries, &data.machine_entries }) + for (const CachedPreset& e : *entries) + dict.collect(e.config_src); + + std::ostringstream body(std::ios::binary); + { + cereal::BinaryOutputArchive ar(body); + ar(CACHE_VERSION); + ar(vendor_name, vendor_version); + dict.save(ar); + ar(data.vendors); + save_entries(ar, data.process_entries, dict); + save_entries(ar, data.filament_entries, dict); + save_entries(ar, data.machine_entries, dict); + ar(data.parse_errors); + } + return write_cache_blob(path, body.str()); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: failed to save vendor cache " << path << ": " << e.what(); + return false; + } +} + +// static +bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, VendorCacheData& data) +{ + std::string blob; + if (! read_cache_blob(path, blob)) + return false; + try { + // Read in place: an istringstream would copy the blob once more just to + // stream over it. + boost::iostreams::stream body(blob.data(), blob.size()); + cereal::BinaryInputArchive ar(body); + const std::string vendor_version = read_cache_stamps(ar, expected_vendor_name); + if (vendor_version.empty() || ! cache_covers_version(vendor_version, expected_vendor_version)) + return false; + CacheDictionary dict; + dict.load(ar); + ar(data.vendors); + load_entries(ar, data.process_entries, dict); + load_entries(ar, data.filament_entries, dict); + load_entries(ar, data.machine_entries, dict); + ar(data.parse_errors); + if (data.vendors.find(expected_vendor_name) == data.vendors.end()) + throw std::runtime_error("vendor cache does not carry its own vendor profile"); + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: rejecting vendor cache " << path << ": " << e.what(); + return false; + } +} + +// static +std::string VendorCacheFile::peek_version(const std::string& path, const std::string& expected_vendor_name) +{ + try { + boost::nowide::ifstream ifs(path, std::ios::binary); + CacheFileHeader fhdr; + if (! ifs.read(reinterpret_cast(&fhdr), sizeof(fhdr)) || fhdr.magic != CACHE_MAGIC) + return {}; + // Only the head of the body is read, and its CRC left unverified: the + // stamps sit at the front, and this answers "what version is this?" + // without paying for tens of megabytes. Callers that need to know the + // file is whole use usable_version instead. + std::string head(static_cast(std::min(fhdr.data_size, 1024)), '\0'); + if (! ifs.read(&head[0], static_cast(head.size()))) + return {}; + std::istringstream body(head, std::ios::binary); + cereal::BinaryInputArchive ar(body); + return read_cache_stamps(ar, expected_vendor_name); + } catch (const std::exception&) { + return {}; + } +} + +// static +Semver VendorCacheFile::usable_version(const std::string& path, const std::string& expected_vendor_name) +{ + std::string blob; + if (! read_cache_blob(path, blob)) + return Semver::invalid(); + try { + boost::iostreams::stream body(blob.data(), blob.size()); + cereal::BinaryInputArchive ar(body); + const auto ver = Semver::parse(read_cache_stamps(ar, expected_vendor_name)); + return ver ? *ver : Semver::invalid(); + } catch (const std::exception&) { + return Semver::invalid(); + } +} + +// static +bool VendorCacheFile::carries_preset(const std::string& path, const std::string& vendor_name, + Preset::Type type, const std::string& preset_name) +{ + std::string blob; + if (! read_cache_blob(path, blob)) + return false; + try { + boost::iostreams::stream body(blob.data(), blob.size()); + cereal::BinaryInputArchive ar(body); + if (read_cache_stamps(ar, vendor_name).empty()) + return false; + CacheDictionary dict; + dict.load(ar); + VendorMap vendors; + ar(vendors); + // Reused: every entry overwrites it, and only its name is ever looked at. + CachedPreset entry; + // Written in this order by save. The list that could carry the preset + // is the last one worth reading. + for (Preset::Type kind : { Preset::TYPE_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_PRINTER }) { + uint32_t cnt = 0; + ar(cnt); + for (uint32_t i = 0; i < cnt; ++ i) { + visit_entry(ar, entry, [&] { skip_config(ar, dict); }); + if (kind == type && entry.name == preset_name) + return true; + } + if (kind == type) + return false; + } + return false; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not read preset names from " << path << ": " << e.what(); + return false; + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/PresetCacheFormat.hpp b/src/libslic3r/PresetCacheFormat.hpp new file mode 100644 index 0000000000..b200ec9911 --- /dev/null +++ b/src/libslic3r/PresetCacheFormat.hpp @@ -0,0 +1,192 @@ +#ifndef slic3r_PresetCacheFormat_hpp_ +#define slic3r_PresetCacheFormat_hpp_ + +#include +#include +#include +#include + +#include +#include +#include + +#include "libslic3r/Config.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/Semver.hpp" + +namespace Slic3r { + +// How the preset cache writes a DynamicPrintConfig. +// +// Not through the global cereal hooks in PrintConfig.hpp: those key an option by +// its serialization_key_ordinal, which ConfigDef::add assigns by declaration +// order at static-init time. Inserting one option into the middle of +// PrintConfig.cpp shifts every later ordinal, and the lookup on the way back in +// then SUCCEEDS on the wrong option — where the two share a type, and hundreds +// of coFloat/coBool/coInt options do, the bytes deserialize cleanly into the +// wrong key. Silently wrong print settings, no error. Those hooks are also the +// undo/redo wire format, where the process cannot change underneath them, so +// they stay as they are and the cache keys by name instead. +// +// Names are not repeated per preset. Each cache file carries one dictionary of +// the distinct opt_keys it uses, the type each was written as, and the distinct +// enum value names; an option on the wire is then a uint16 index into it plus +// its value. The dictionary is resolved to this build's option definitions once +// per file, after which reading an option is a vector index. +class CacheDictionary +{ +public: + CacheDictionary(); + + // Index reserved in the enum table for an int the writing build could not + // name — a nullable option's nil, or a definition carrying no + // enum_keys_map. The raw int32 follows it on the wire and is loaded + // verbatim, so those values survive too. + static constexpr uint16_t ENUM_UNNAMED = 0; + + // ---- writing ---- + + // Record every key and enum value `config` uses. Call for every config that + // will be written, before writing the dictionary. + void collect(const DynamicPrintConfig& config); + + uint16_t key_index(const t_config_option_key& key) const; + // ENUM_UNNAMED for an empty name or one that was never collected. + uint16_t enum_index(const std::string& name) const; + + // ---- reading ---- + + // The definition an index resolves to in THIS build, or nullptr where the + // key is unknown here or is now defined with a different type. A nullptr + // entry's value is still read — using type_at(idx), the type the writer + // recorded — and then dropped, which is what a JSON profile gets for an + // option this build no longer has. + const ConfigOptionDef* def_at(uint16_t idx) const { return m_defs[idx]; } + ConfigOptionType type_at(uint16_t idx) const { return ConfigOptionType(m_types[idx]); } + const std::string& enum_name_at(uint16_t idx) const { return m_enum_values[idx]; } + // m_defs, not m_keys: only load() sizes it, so this is false for every index + // on a dictionary that was collected rather than read. + bool valid_key_index(uint16_t idx) const { return size_t(idx) < m_defs.size(); } + bool valid_enum_index(uint16_t idx) const { return size_t(idx) < m_enum_values.size(); } + + // The layout these two agree on is covered by CACHE_VERSION (PresetCacheFormat.cpp); + // bump it when they change. + // Throws when either table outgrew the uint16 the wire format indexes it + // with. Both are bounded by the option count (912 at the time of writing), so + // that is a build-time failure in CI, not a runtime one. + void save(cereal::BinaryOutputArchive& ar) const; + // Throws on a dictionary that cannot be indexed as written. + void load(cereal::BinaryInputArchive& ar); + +private: + // Indices are uint16, so a table may hold at most this many entries. + static constexpr size_t MAX_ENTRIES = 0xFFFF; + + std::vector m_keys; + // ConfigOptionType, as written. Sixteen bits, not eight: coVectorType is + // 0x4000, so every vector type — coFloats, coEnums, coStrings — is above + // 255, and a byte would fold each one onto its scalar counterpart. + std::vector m_types; + std::vector m_enum_values; // [ENUM_UNNAMED] is always empty + + // Writing. + std::unordered_map m_key_index; + std::unordered_map m_enum_index; + // Reading, resolved once by load(). + std::vector m_defs; +}; + +// One config, keyed through `dict`. Options print_config_def does not know are +// not written: nothing could give them a type on the way back in. +void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict); +// Throws only on a payload that cannot be indexed; an option this build cannot +// place is dropped, not fatal. +void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict); +// Consume one config without building it, for a reader that only wants what +// comes after. +void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict); + +// One preset as its JSON subfile states it: the config diff, the name of the +// preset it inherits, and the parse metadata — everything the parse phase of +// load_vendor_configs_from_json extracts and nothing it derives. Inheritance +// is resolved when the entry is installed, against whatever filament library +// is loaded then, so a cache carries no other vendor's values and no other +// vendor's update can make it stale. +// Written and read by visit_entry in PresetCacheFormat.cpp, which lists every +// field below in this order — once, for the save, the load and the name peek alike. +struct CachedPreset +{ + std::string name; + std::string sub_path; // path under the vendor's directory + DynamicPrintConfig config_src; // the preset's own diff, nothing inherited + std::string inherits; + std::string description; + std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error + std::string setting_id; + std::string filament_id; + std::vector renamed_from; +}; + +// What one per-vendor cache file carries besides its stamps: the vendor profile +// map, the presets in source form, and how many errors their parse counted. +struct VendorCacheData +{ + VendorMap vendors; + std::vector process_entries; + std::vector filament_entries; + std::vector machine_entries; + uint64_t parse_errors = 0; +}; + +// A per-vendor preset cache file (.opc): a 20-byte header (magic, format +// version, body size, CRC) framing one cereal body — stamps (format version, +// vendor name, vendor profile version), the option dictionary, then the +// VendorCacheData. Everything about those bytes lives here; when a vendor is +// served from its cache, and how entries install into a bundle, is +// PresetBundle's business. +class VendorCacheFile +{ +public: + // Save one vendor (vendor_name at vendor_version). False when the file + // could not be written whole. + static bool save(const std::string& path, const std::string& vendor_name, + const std::string& vendor_version, const VendorCacheData& data); + + // Read a whole cache into `data`. False — with `data` in an unspecified + // state — unless the file is a cache this build wrote, its CRC holds, it + // names this vendor, it was built from a vendor profile at least as new as + // `expected_vendor_version`, and it carries its own vendor profile. An + // invalid expected version (a profile whose version + // cannot be judged) is never served from cache; Semver::inf() (no profile + // beside the cache at all) accepts whatever is cached. + static bool load(const std::string& path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, VendorCacheData& data); + + // Read the profile version a cache was stamped with, without deserializing + // its presets. Empty if the file is unreadable, not a cache this build + // understands, or not this vendor's. This is how an installed vendor's + // version is known when only its cache is installed. + static std::string peek_version(const std::string& path, const std::string& expected_vendor_name); + + // The profile version an installed cache can actually be served at, or an + // invalid Semver when the file is not a cache this build can read. Unlike + // peek_version this verifies the body's CRC, at the cost of reading the + // whole file: where the cache is the vendor's whole installation, "a file + // is there" is not enough to call it installed, and a vendor wrongly + // believed installed is never repaired. + static Semver usable_version(const std::string& path, const std::string& expected_vendor_name); + + // Whether a cache carries a preset of `type` under `preset_name`, without + // installing any of them. False when the file is not a cache this build can + // read. The three kinds are written in one stream, so reaching the machines + // means reading past the processes and filaments — their configs are consumed + // and dropped rather than built. This is how a build that ships caches instead + // of preset JSONs answers "which vendor carries this preset?". + static bool carries_preset(const std::string& path, const std::string& vendor_name, + Preset::Type type, const std::string& preset_name); +}; + +} // namespace Slic3r + +#endif // slic3r_PresetCacheFormat_hpp_ diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 26a708b78d..255c8721b9 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -2488,7 +2488,8 @@ namespace cereal { archive(serialization_key_ordinal); assert(serialization_key_ordinal > 0); auto it = Slic3r::print_config_def.by_serialization_key_ordinal.find(serialization_key_ordinal); - assert(it != Slic3r::print_config_def.by_serialization_key_ordinal.end()); + if (it == Slic3r::print_config_def.by_serialization_key_ordinal.end()) + throw std::runtime_error("VendorCache: unknown serialization_key_ordinal " + std::to_string(serialization_key_ordinal) + " - cache is stale"); config.set_key_value(it->second->opt_key, it->second->load_option_from_archive(archive)); } } diff --git a/src/libslic3r/Semver.hpp b/src/libslic3r/Semver.hpp index 4d64b1c7db..d3683b4eb8 100644 --- a/src/libslic3r/Semver.hpp +++ b/src/libslic3r/Semver.hpp @@ -190,6 +190,19 @@ public: os << self.to_string(); return os; } + + // cereal: round-trip through the standard 3-part string (major.minor.patch). + // to_string() uses a BBS 4-part format that semver_parse() cannot read back. + template + std::string save_minimal(const Archive&) const { return to_string_sf(); } + template + void load_minimal(const Archive&, const std::string& s) { + auto v = Semver::parse(s); + if (! v) + throw std::runtime_error("Semver: cannot parse serialized version: " + s); + *this = std::move(*v); + } + private: semver_t ver; diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 62b2eeb78e..55d9b716cf 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include "libslic3r.h" +#include "Semver.hpp" //define CLI errors @@ -722,11 +724,42 @@ void copy_directory_recursively(const boost::filesystem::path& source, std::function filter = nullptr, bool merge_mode = false); -// Install vendor bundles from resources directory to data directory -// bundle_names: vector of vendor bundle names (without .json extension) -// resource_subdir: subdirectory under resources_dir() (default: "profiles") -// data_subdir: subdirectory under data_dir() (default: "system") -// Returns: true if all bundles installed successfully, false otherwise +// ---- Vendor installation on disk ------------------------------------------ +// How a vendor bundle is installed from resources into data_dir()/system: as +// its profile and preset JSONs or, in a build that ships preset caches, as its +// .opc preset cache alone. Loading what is installed is PresetBundle's business; +// the cache file format itself is VendorCacheFile's (PresetCacheFormat.hpp). + +// True if `vendor` is installed in data_dir()/system. A build that ships preset +// caches installs the cache alone, so it — not the profile — marks a vendor +// installed; a cache this build cannot read marks nothing. +bool is_vendor_installed(const std::string& vendor); + +// The version the installed vendor would be loaded at: its cache's stamp while +// that covers the profile beside it, the profile's own version once it does not. +// Invalid Semver if neither form is installed. +Semver installed_vendor_version(const std::string& vendor); + +// Remove every form `vendor` can be installed as from data_dir()/system: its +// profile, its preset cache, and its preset directory. +void remove_installed_vendor(const std::string& vendor); + +// The vendors `dir` holds, sorted: one is named by its profile or, in a build that +// ships preset caches instead of the raw profile JSONs, by its cache alone. +std::set vendor_names_in(const boost::filesystem::path& dir); + +// The version a build ships `vendor` at: whichever of its preset cache and its +// profile is newer, that being the one installing lays down. Invalid Semver if the +// build ships neither. +Semver resource_vendor_version(const std::string& vendor); + +// Install vendors from the resources directory into the data directory, each as +// its preset cache or as its profile and preset JSONs — whichever of the two the +// build ships at the newer version. Anything the previous install of that vendor +// left behind goes, so only the form just installed is there to be loaded. +// bundle_names: vendor names, without extension. +// Every bundle that can be installed is, whatever the others do. Returns false +// if any named bundle could not be installed. bool install_vendor_bundles_from_resources(const std::vector& bundle_names, const std::string& resource_subdir = "profiles", const std::string& data_subdir = "system"); diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 5f429f076a..58ec8318a6 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -17,6 +17,10 @@ #include "Platform.hpp" #include "Time.hpp" #include "libslic3r.h" +// For the vendor-installation helpers: the vendor profile version +// (get_version_from_json) and the preset cache stamp (VendorCacheFile). +#include "Preset.hpp" +#include "PresetCacheFormat.hpp" #ifdef __APPLE__ #include "MacUtils.hpp" @@ -1724,6 +1728,85 @@ void copy_directory_recursively(const boost::filesystem::path& source, return; } +// ---- Vendor installation on disk ------------------------------------------ + +// Whether a cache stamped `cache_ver` still speaks for a vendor whose profile on +// disk claims `profile_ver`: it does unless the profile has moved ahead of it. A +// profile that is missing or carries no judgeable version cannot be ahead of +// anything. The one rule behind both "which form gets installed" and "which form +// is installed"; they must not drift apart. Deliberately NOT the serve rule +// (VendorCacheFile::load), which refuses an unjudgeable profile instead. +static bool cache_covers(const Semver& cache_ver, const Semver& profile_ver) +{ + return cache_ver.valid() && (! profile_ver.valid() || cache_ver >= profile_ver); +} + +bool is_vendor_installed(const std::string& vendor) +{ + const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR; + // A cache is the whole of a cache-only installation, so a file this build + // cannot serve the vendor from is not an installation. Left counted as one, + // the updater would never lay a working copy down. + return boost::filesystem::exists(dir / (vendor + ".json")) + || VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor).valid(); +} + +Semver installed_vendor_version(const std::string& vendor) +{ + const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR; + const boost::filesystem::path json = dir / (vendor + ".json"); + // Guarded: get_version_from_json logs an error and throws-and-catches its way + // to an invalid version on a file that is not there, and a cache-only vendor + // never has one. + const Semver from_json = boost::filesystem::exists(json) ? get_version_from_json(json.string()) : Semver(); + const Semver from_cache = VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor); + // Whichever form a load would serve. + return cache_covers(from_cache, from_json) ? from_cache : from_json; +} + +void remove_installed_vendor(const std::string& vendor) +{ + const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR; + boost::filesystem::remove(dir / (vendor + ".json")); + boost::filesystem::remove(dir / (vendor + ".opc")); + if (boost::filesystem::exists(dir / vendor)) + boost::filesystem::remove_all(dir / vendor); +} + +std::set vendor_names_in(const boost::filesystem::path& dir) +{ + std::set names; + for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { + const auto& path = dir_entry.path(); + if (Slic3r::is_json_file(path.string()) || path.extension() == ".opc") + names.insert(path.stem().string()); + } + return names; +} + +// A vendor's preset cache is the whole of its installation: it carries the presets, +// the vendor profile and the version they were built at, so where one ships nothing +// else needs copying. Unless the profile beside it claims a newer version — a cache +// generated before that profile was bumped is out of date, and a cache that cannot +// be read is no installation at all — and the vendor is installed the way it was +// before caches existed, as its profile and the preset JSONs it points at. Returns +// the version the cache is stamped with, invalid when it is not the form to install. +static Semver installable_cache_version(const boost::filesystem::path& dir, const std::string& vendor) +{ + const auto cache_ver = Semver::parse(VendorCacheFile::peek_version((dir / (vendor + ".opc")).string(), vendor)); + if (! cache_ver) + return Semver::invalid(); + const Semver profile_ver = get_version_from_json((dir / (vendor + ".json")).string()); + return cache_covers(*cache_ver, profile_ver) ? *cache_ver : Semver::invalid(); +} + +Semver resource_vendor_version(const std::string& vendor) +{ + const boost::filesystem::path dir = boost::filesystem::path(resources_dir()) / "profiles"; + const Semver ver = installable_cache_version(dir, vendor); + return ver.valid() ? ver : get_version_from_json((dir / (vendor + ".json")).string()); +} + bool install_vendor_bundles_from_resources( const std::vector& bundle_names, const std::string& resource_subdir, @@ -1736,37 +1819,82 @@ bool install_vendor_bundles_from_resources( BOOST_LOG_TRIVIAL(info) << "Installing " << bundle_names.size() << " bundles from resources..."; + // One vendor that cannot be installed is one vendor missing, not a reason to + // leave the rest uninstalled. The caller is told, and every bundle that can + // be laid down is. + bool all_installed = true; + for (const auto &bundle : bundle_names) { try { + if (bundle.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Refusing to install a bundle with no name"; + all_installed = false; + continue; + } + // Install the JSON file auto path_in_rsrc = (rsrc_path / bundle).replace_extension(".json"); auto path_in_vendors = (vendor_path / bundle).replace_extension(".json"); + auto cache_in_rsrc = (rsrc_path / bundle).replace_extension(".opc"); + auto cache_in_vendors = (vendor_path / bundle).replace_extension(".opc"); - if (!fs::exists(path_in_rsrc)) { + // Either form of the vendor will do: a build may ship it as a cache alone. + if (!fs::exists(path_in_rsrc) && !fs::exists(cache_in_rsrc)) { BOOST_LOG_TRIVIAL(warning) << "Bundle not found in resources: " << bundle; - return false; + all_installed = false; + continue; } // Create target directory if needed if (!fs::exists(vendor_path)) fs::create_directories(vendor_path); - // Copy JSON file std::string error_message; - CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false); - if (cfr != CopyFileResult::SUCCESS) { - BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message; - return false; + bool installed_cache = false; + if (installable_cache_version(rsrc_path, bundle).valid()) { + installed_cache = copy_file(cache_in_rsrc.string(), cache_in_vendors.string(), error_message, false) == CopyFileResult::SUCCESS; + if (! installed_cache) { + BOOST_LOG_TRIVIAL(warning) << "Failed to copy " << bundle << ".opc: " << error_message; + } else if (! VendorCacheFile::usable_version(cache_in_vendors.string(), bundle).valid()) { + // The copy is what will be loaded, so it — not the kilobyte + // peek that chose this form — decides whether the profile + // beside it can go. + BOOST_LOG_TRIVIAL(warning) << "Installed cache for " << bundle << " cannot be read; installing its profile instead"; + boost::system::error_code ec; + fs::remove(cache_in_vendors, ec); + installed_cache = false; + } + } + + if (! installed_cache) { + CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false); + if (cfr != CopyFileResult::SUCCESS) { + BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message; + all_installed = false; + continue; + } + // Only now: an earlier install's cache would shadow this profile, + // but removing it before the profile lands would leave neither. + boost::system::error_code ec; + fs::remove(cache_in_vendors, ec); + } else { + // Left in place, an earlier install's profile would shadow the cache. + boost::system::error_code ec; + fs::remove(path_in_vendors, ec); + if (ec) + BOOST_LOG_TRIVIAL(warning) << "Could not remove the superseded profile " << path_in_vendors.string() << ": " << ec.message(); } // Copy the vendor directory (if it exists) auto dir_in_rsrc = rsrc_path / bundle; auto dir_in_vendors = vendor_path / bundle; - if (fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) { - // Remove existing directory - if (fs::exists(dir_in_vendors)) - fs::remove_all(dir_in_vendors); + // Whatever is installed came from an earlier version of this vendor and + // would be parsed in place of the one being installed now. + if (fs::exists(dir_in_vendors)) + fs::remove_all(dir_in_vendors); + + if (! installed_cache && fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) { fs::create_directories(dir_in_vendors); // Copy with file filter (same as PresetUpdater::install_bundles_rsrc) @@ -1787,11 +1915,11 @@ bool install_vendor_bundles_from_resources( } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Exception installing bundle " << bundle << ": " << e.what(); - return false; + all_installed = false; } } - return true; + return all_installed; } void save_string_file(const boost::filesystem::path& p, const std::string& str) diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index 4b071994fc..a7135eac6f 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -432,14 +432,9 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: cfg.models_variants_installed.erase(it ++); else ++ it; - // Read the active config bundle, parse the config version. - PresetBundle bundle; - //BBS: change directoties by design - //bundle.load_configbundle((data_dir / PRESET_SYSTEM_DIR / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent); - bundle.load_vendor_configs_from_json((data_dir/PRESET_SYSTEM_DIR).string(), cfg.name, PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent); - for (const auto &vp : bundle.vendors) - if (vp.second.id == cfg.name) - cfg.version.config_version = vp.second.config_version; + // Orca: the version the vendor is installed at, read from its profile or — + // where the cache is the whole installation — from the cache's own stamp. + cfg.version.config_version = installed_vendor_version(cfg.name); snapshot.vendor_configs.emplace_back(std::move(cfg)); } diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 0bbbc15f87..dba8699105 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -66,41 +66,41 @@ using Config::SnapshotDB; // Configuration data structures extensions needed for the wizard //BBS: set BBL as default -bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_bbl_bundle) +bool Bundle::load(fs::path dir, const std::string &vendor_name, bool ais_in_resources, bool ais_bbl_bundle) { this->preset_bundle = std::make_unique(); this->is_in_resources = ais_in_resources; this->is_bbl_bundle = ais_bbl_bundle; - std::string path_string = source_path.string(); - std::string parent_path = source_path.parent_path().string(); //BBS: add json logic for vendor bundles - std::string vendor_name = source_path.filename().string(); - if (Slic3r::is_json_file(path_string)) { - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - } - else + // Orca: served from the vendor's preset cache where one covers it — which is + // how a shipped build carries its vendors — and parsed from the JSONs otherwise. + // A vendor that can be neither read nor parsed — a cache the build cannot use + // with the preset JSONs behind it pruned, say — is one the wizard cannot offer. + // Every other vendor still can be, so it is left out rather than thrown over. + size_t presets_loaded = 0; + try { + auto [config_substitutions, loaded] = preset_bundle->load_vendor_configs_from_json( + dir.string(), vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); + UNUSED(config_substitutions); + // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. + assert(config_substitutions.empty()); + presets_loaded = loaded; + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(fatal) << boost::format("Vendor bundle: `%1%`: cannot be loaded, leaving it out: %2%") % vendor_name % e.what(); return false; - - // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. - //BBS: add json logic for vendor bundles - auto [config_substitutions, presets_loaded] = preset_bundle->load_vendor_configs_from_json( - parent_path, vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); - UNUSED(config_substitutions); - // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. - assert(config_substitutions.empty()); + } auto first_vendor = preset_bundle->vendors.begin(); if (first_vendor == preset_bundle->vendors.end()) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % vendor_name; return false; } if (presets_loaded == 0) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % vendor_name; return false; - } + } - BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; + BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % vendor_name % presets_loaded; this->vendor_profile = &first_vendor->second; return true; } @@ -125,15 +125,10 @@ BundleMap BundleMap::load() //Orca: add custom as default //Orca: add json logic for vendor bundle - auto orca_bundle_path = (vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json"); - auto orca_bundle_rsrc = false; - if (!boost::filesystem::exists(orca_bundle_path)) { - orca_bundle_path = (rsrc_vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json"); - orca_bundle_rsrc = true; - } { + const bool from_rsrc = ! is_vendor_installed(PresetBundle::ORCA_DEFAULT_BUNDLE); Bundle bbl_bundle; - if (bbl_bundle.load(std::move(orca_bundle_path), orca_bundle_rsrc, true)) + if (bbl_bundle.load(from_rsrc ? rsrc_vendor_dir : vendor_dir, PresetBundle::ORCA_DEFAULT_BUNDLE, from_rsrc, true)) res.emplace(PresetBundle::ORCA_DEFAULT_BUNDLE, std::move(bbl_bundle)); } @@ -141,18 +136,13 @@ BundleMap BundleMap::load() // and then additionally from resources/profiles. bool is_in_resources = false; for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { - for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { - //BBS: add json logic for vendor bundle - if (Slic3r::is_json_file(dir_entry.path().string())) { - std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".json" part + for (const std::string &id : vendor_names_in(*dir)) { + // Don't load this bundle if we've already loaded it. + if (res.find(id) != res.end()) { continue; } - // Don't load this bundle if we've already loaded it. - if (res.find(id) != res.end()) { continue; } - - Bundle bundle; - if (bundle.load(dir_entry.path(), is_in_resources)) - res.emplace(std::move(id), std::move(bundle)); - } + Bundle bundle; + if (bundle.load(*dir, id, is_in_resources)) + res.emplace(id, std::move(bundle)); } is_in_resources = true; diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index 364d378b42..7b9674b216 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -71,9 +71,11 @@ struct Bundle Bundle() = default; Bundle(Bundle&& other); + // Load the vendor `vendor_name` as it is installed in `dir`, from its preset + // cache or its profile JSONs, whichever is usable. // Returns false if not loaded. Reason for that is logged as boost::log error. //BBS: set BBL as default - bool load(fs::path source_path, bool is_in_resources, bool is_bbl_bundle = false); + bool load(fs::path dir, const std::string &vendor_name, bool is_in_resources, bool is_bbl_bundle = false); const std::string& vendor_id() const { return vendor_profile->id; } }; diff --git a/src/slic3r/GUI/CreatePresetsDialog.cpp b/src/slic3r/GUI/CreatePresetsDialog.cpp index 1bd80d5f00..33f49c2a38 100644 --- a/src/slic3r/GUI/CreatePresetsDialog.cpp +++ b/src/slic3r/GUI/CreatePresetsDialog.cpp @@ -2201,25 +2201,14 @@ bool CreatePrinterPresetDialog::load_system_and_user_presets_with_curr_model(Pre } else { selected_vendor_id = m_printer_preset_vendor_selected.id; - if (boost::filesystem::exists(boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR / selected_vendor_id)) { - preset_path = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(); - } else if (boost::filesystem::exists(boost::filesystem::path(Slic3r::resources_dir()) / "profiles" / selected_vendor_id)) { - preset_path = (boost::filesystem::path(Slic3r::resources_dir()) / "profiles").string(); - } - - if (preset_path.empty()) { - BOOST_LOG_TRIVIAL(info) << "Preset path was not found"; - MessageDialog dlg(this, _L("Preset path was not found; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), - wxYES_NO | wxYES_DEFAULT | wxCENTRE); - dlg.ShowModal(); - return false; - } - try { // Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base // bundle so vendor filaments that inherit OFL bases resolve via the existing // cross-vendor inheritance path. - temp_preset_bundle.load_vendor_configs_from_json(preset_path, selected_vendor_id, + // Orca: served from the vendor's preset cache where one covers it — a shipped + // build carries that instead of the raw preset JSONs — and parsed otherwise. + temp_preset_bundle.load_vendor_configs_from_json((boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(), + selected_vendor_id, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent, wxGetApp().preset_bundle); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 3a85dc1928..6028ada640 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -6817,6 +6817,12 @@ void GUI_App::add_pending_vendor_preset(const std::pair>(); diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 0d2f6c724b..6b58fffead 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -1,7 +1,9 @@ #include "WebGuideDialog.hpp" #include "ConfigWizard.hpp" +#include #include +#include #include #include #include @@ -9,7 +11,9 @@ #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/Config.hpp" +#include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PresetCacheFormat.hpp" #include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "libslic3r_version.h" @@ -41,8 +45,6 @@ using namespace nlohmann; namespace Slic3r { namespace GUI { -json m_ProfileJson; - static wxString update_custom_filaments() { json m_Res = json::object(); @@ -190,12 +192,10 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style) GuideFrame::~GuideFrame() { - m_destroy = true; - if (m_load_task && m_load_task->joinable()) { + *m_cancel_token = true; // stop the loading thread and any queued CallAfter lambdas before join + if (m_load_task && m_load_task->joinable()) m_load_task->join(); - delete m_load_task; - m_load_task = nullptr; - } + m_load_task.reset(); if (m_browser) { delete m_browser; m_browser = nullptr; @@ -301,15 +301,71 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt) /** * Callback invoked when a navigation request was accepted */ +// The empty shape every profile-loading path starts from or falls back to. +void GuideFrame::reset_profile_json() +{ + m_ProfileJson["model"] = json::array(); + m_ProfileJson["machine"] = json::object(); + m_ProfileJson["filament"] = json::object(); + m_ProfileJson["process"] = json::array(); +} + +void GuideFrame::init_guide_paths() +{ + m_ProfileJson = json::parse("{}"); + reset_profile_json(); + + vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); + rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + orca_bundle_rsrc = true; + + if (boost::filesystem::exists(vendor_dir)) { + for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) { + if (!boost::filesystem::is_directory(entry) && + boost::iequals(entry.path().extension().string(), ".json") && + !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) { + orca_bundle_rsrc = false; + break; + } + } + } + + auto lib_json = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); + m_OrcaFilaLibPath = boost::filesystem::exists(vendor_dir / lib_json) + ? (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string() + : (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string(); +} + +void GuideFrame::on_profile_loaded() +{ + // Must be called on the main thread. + SaveProfileData(); + const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll; + json res; + res["command"] = "userguide_profile_load_finish"; + res["sequence_id"] = "10001"; + RunScript(wxString::Format("HandleStudio(%s)", res.dump(-1, ' ', true))); +} + void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt) { //wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'"); if (!bFirstComplete) { - m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this)); - // boost::thread LoadProfileThread(boost::bind(&GuideFrame::LoadProfileData, this)); - //LoadProfileThread.detach(); - bFirstComplete = true; + try { + init_guide_paths(); + if (BuildProfileDataFromPresetBundle()) { + if (!*m_cancel_token) + on_profile_loaded(); + } else { + // Presets not yet in memory — delegate to background thread. + m_load_task = std::make_unique(boost::bind(&GuideFrame::LoadProfileData, this)); + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", init error: " << e.what(); + m_load_task = std::make_unique(boost::bind(&GuideFrame::LoadProfileData, this)); + } } m_browser->Show(); @@ -762,11 +818,9 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle bool check_unsaved_preset_changes = false; std::vector install_bundles; std::vector remove_bundles; - const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); for (const auto &it : enabled_vendors) { if (it.second.size() > 0) { - auto vendor_file = vendor_dir/(it.first + ".json"); - if (!fs::exists(vendor_file)) { + if (!is_vendor_installed(it.first)) { install_bundles.emplace_back(it.first); } } @@ -777,8 +831,7 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle if (it.second.size() > 0) { if (enabled_vendors.find(it.first) != enabled_vendors.end()) continue; - auto vendor_file = vendor_dir/(it.first + ".json"); - if (fs::exists(vendor_file)) { + if (is_vendor_installed(it.first)) { remove_bundles.emplace_back(it.first); } } @@ -1127,99 +1180,324 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList, return status; } -int GuideFrame::LoadProfileData() +bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors) { try { - m_ProfileJson = json::parse("{}"); - m_ProfileJson["model"] = json::array(); - m_ProfileJson["machine"] = json::object(); - m_ProfileJson["filament"] = json::object(); - m_ProfileJson["process"] = json::array(); + // Models from vendor profiles + for (const auto& [vendor_id, vp] : bundle.vendors) { + for (const auto& model : vp.models) { + std::string nozzle_str; + for (const auto& v : model.variants) { + if (!nozzle_str.empty()) nozzle_str += ";"; + nozzle_str += v.name; + } + const std::string materials_str = boost::algorithm::join(model.default_materials, ";"); + boost::filesystem::path cover_path = + (boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (model.id + "_cover.png")) + .make_preferred(); + if (!boost::filesystem::exists(cover_path)) + cover_path = + (boost::filesystem::path(resources_dir()) / "web/image/printer" / (model.id + "_cover.png")) + .make_preferred(); - vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); - rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); - - // Orca: add custom as default - // Orca: add json logic for vendor bundle - orca_bundle_rsrc = true; - - // search if there exists a .json file in vendor_dir folder, if exists, set orca_bundle_rsrc to false - for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) { - if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json") && !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) { - orca_bundle_rsrc = false; - break; + json entry; + entry["model"] = model.id; + entry["name"] = model.name; + entry["vendor"] = vp.id; + entry["nozzle_diameter"] = nozzle_str; + entry["materials"] = materials_str; + entry["cover"] = cover_path.string(); + entry["nozzle_selected"] = ""; + entry["sub_path"] = ""; + m_ProfileJson["model"].push_back(entry); } } - // load the default filament library first - std::set loaded_vendors; - auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); - if (boost::filesystem::exists(vendor_dir / filament_library_name)) { - m_OrcaFilaLibPath = (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string(); - LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string()); - } else { - m_OrcaFilaLibPath = (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string(); - LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string()); - } - loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY); + // Machine map: preset name -> {model, nozzle variant} + for (const Preset& p : bundle.printers()) { + if (!p.is_system || !p.vendor) continue; + const auto* printer_model = p.config.option("printer_model"); + const auto* printer_variant = p.config.option("printer_variant"); + if (!printer_model || printer_model->value.empty() || !printer_variant) continue; - //load custom bundle from user data path - boost::filesystem::directory_iterator endIter; - for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) { - if (!boost::filesystem::is_directory(*iter)) { - wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); - strVendor = strVendor.AfterLast('\\'); - strVendor = strVendor.AfterLast('/'); - - wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); - if(strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) - continue; - - LoadProfileFamily(w2s(strVendor), iter->path().string()); - loaded_vendors.insert(w2s(strVendor)); - } - if (m_destroy) - return 0; + json mach; + mach["model"] = printer_model->value; + mach["nozzle"] = printer_variant->value; + m_ProfileJson["machine"][p.name] = mach; } - boost::filesystem::directory_iterator others_endIter; - for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) { - if (!boost::filesystem::is_directory(*iter)) { - wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); - strVendor = strVendor.AfterLast('\\'); - strVendor = strVendor.AfterLast('/'); - wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); - if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) - continue; + // Filament map from system filament presets (vendor/type already resolved in config) + const json& machines = m_ProfileJson["machine"]; + for (const Preset& p : bundle.filaments()) { + if (!p.is_system || !p.vendor) continue; + const auto* fila_vendor = p.config.option("filament_vendor"); + const auto* fila_type = p.config.option("filament_type"); + const auto* compat_printers = p.config.option("compatible_printers"); - LoadProfileFamily(w2s(strVendor), iter->path().string()); - loaded_vendors.insert(w2s(strVendor)); + std::string vendor = (fila_vendor && !fila_vendor->values.empty()) ? fila_vendor->values[0] : ""; + std::string type = (fila_type && !fila_type->values.empty()) ? fila_type->values[0] : ""; + + std::string model_list; + if (compat_printers) { + for (const std::string& pname : compat_printers->values) { + auto it = machines.find(pname); + if (it != machines.end()) { + const std::string m = (*it)["model"]; + const std::string n = (*it)["nozzle"]; + model_list += "[" + m + "++" + n + "]"; + } + } } - if (m_destroy) - return 0; + + json ff; + ff["name"] = p.name; + ff["sub_path"] = p.file; + ff["vendor"] = vendor; + ff["type"] = type; + ff["models"] = model_list; + ff["selected"] = 0; + m_ProfileJson["filament"][p.name] = ff; } - wxGetApp().CallAfter([this] { - if (!m_destroy) { - //sync to appconfig first to populate current selections - SaveProfileData(); + // Process list from visible system print presets + for (const Preset& p : bundle.prints()) { + if (!p.is_system || !p.vendor || !p.is_visible) continue; + json entry; + entry["name"] = p.name; + entry["sub_path"] = p.file; + m_ProfileJson["process"].push_back(entry); + } - //sync to web after selections are populated - std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore); + if (require_all_resource_vendors) { + // If rsrc_vendor_dir has vendors (profile JSONs, or the preset caches a + // packaged build ships instead) not covered by the current bundle, the + // bundle is incomplete (e.g. dev env where data_dir/system only has + // OrcaFilamentLibrary+Custom). Fall back so the slow path reads both dirs. + try { + for (const std::string& name : vendor_names_in(rsrc_vendor_dir)) { + if (bundle.vendors.find(name) == bundle.vendors.end()) { + BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << name + << "' in resources but not in preset_bundle — falling back to JSON loading"; + reset_profile_json(); + return false; + } + } + } catch (const std::exception&) {} + } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents: " << std::endl << strAll; - json m_Res = json::object(); - m_Res["command"] = "userguide_profile_load_finish"; - m_Res["sequence_id"] = "10001"; - wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', true)); + BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data (" + << m_ProfileJson["model"].size() << " models, " + << m_ProfileJson["machine"].size() << " machines, " + << m_ProfileJson["filament"].size() << " filaments)"; + return !m_ProfileJson["machine"].empty(); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileJson failed: " << e.what() + << " — falling back to JSON loading"; + reset_profile_json(); + return false; + } +} - RunScript(strJS); +bool GuideFrame::BuildProfileDataFromPresetBundle() +{ + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb || pb->vendors.empty()) + return false; + return BuildProfileJson(*pb, /*require_all_resource_vendors=*/true); +} + +bool GuideFrame::BuildProfileDataFromVendors() +{ + try { + // Same vendor set and precedence as the JSON scan in LoadProfileData: a + // vendor in the user's system dir shadows the bundled one of that name. + // vendor_names_in names a vendor by its profile or, where a build ships + // preset caches instead, by its cache alone. + std::map vendor_sources; + for (const boost::filesystem::path& dir : { vendor_dir, rsrc_vendor_dir }) { + boost::system::error_code ec; + if (boost::filesystem::exists(dir, ec)) + for (const std::string& name : vendor_names_in(dir)) + vendor_sources.emplace(name, dir); // first dir wins + } + + // The load order: the filament library first, because the others' + // filaments inherit from it, then every versioned vendor — each loaded + // from the directory it was found in, so a vendor that is not installed + // is served from the shipped profiles. Each is stamped by name and + // version alone: a profile change requires a version bump, so those two + // determine content wherever the vendor's copy sits. + struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; }; + std::vector ordered; + auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) { + // The version a load from `dir` would serve: the profile's where one + // exists (a cache is only served while it covers the profile beside + // it), the cache's own stamp where the cache is the whole vendor. + // A profile without a version (blacklist.json) carries no presets + // and is passed over. + const boost::filesystem::path profile = dir / (name + ".json"); + if (boost::filesystem::exists(profile)) { + const Semver v = get_version_from_json(profile.string()); + if (v.valid()) + ordered.push_back({name, dir, v.to_string()}); + } else { + ordered.push_back({name, dir, + VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)}); } + }; + const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY); + if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end()) + add_vendor(filament_library, it->second); + for (const auto& [name, dir] : vendor_sources) + if (name != filament_library) + add_vendor(name, dir); + if (ordered.empty()) + return false; + json stamps = json::array(); + for (const VendorSource& v : ordered) + stamps.push_back({v.name, v.version}); + + // What this function derives is a pure function of that stamped set, so + // the derived JSON is cached whole: a fresh cache makes an open one + // file read, with no bundle built and no preset installed. Stale or + // absent, the bundle is rebuilt below and the result written back. + const boost::filesystem::path cache_file = + boost::filesystem::path(Slic3r::data_dir()) / "cache" / "wizard_profile_data.json"; + try { + // Slurped whole and parsed from the buffer — nlohmann's fastest + // input path; a stream adapter costs real time on a multi-MB file. + boost::nowide::ifstream ifs(cache_file.string(), std::ios::binary); + if (ifs.is_open()) { + const std::string text{std::istreambuf_iterator(ifs), std::istreambuf_iterator()}; + json cached = json::parse(text); + if (cached.value("format", 0) == 1 && cached["vendors"] == stamps && + ! cached["profile"]["machine"].empty()) { + for (const char* key : { "model", "machine", "filament", "process" }) + m_ProfileJson[key] = std::move(cached["profile"][key]); + BOOST_LOG_TRIVIAL(info) << "GuideFrame: profile data served from " << cache_file; + return true; + } + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(info) << "GuideFrame: rejecting cached profile data: " << e.what(); + } + + // Each vendor comes from its preset cache where one covers it, which is + // what makes this worth doing instead of the scan below; loading into a + // bundle per vendor keeps the install order the startup path has. + PresetBundle bundle; + auto load_vendor = [](PresetBundle& into, const std::string& vendor, + const boost::filesystem::path& dir, const PresetBundle* base) { + into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, base); + }; + for (const VendorSource& v : ordered) { + if (*m_cancel_token) + return false; // as in the scan below: a vendor without a cache is parsed, and that takes time + if (v.name == filament_library) { + load_vendor(bundle, v.name, v.dir, nullptr); + } else { + PresetBundle tmp; + load_vendor(tmp, v.name, v.dir, &bundle); + bundle.merge_presets(std::move(tmp)); + } + } + if (bundle.vendors.empty()) + return false; + if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false)) + return false; + + // Written through a temp file and moved into place, as the preset caches + // are: half a cache must never be readable, and the PID suffix keeps two + // instances from interleaving on one temp file. + const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp"; + try { + json out; + out["format"] = 1; + out["vendors"] = std::move(stamps); + json& profile = out["profile"]; + for (const char* key : { "model", "machine", "filament", "process" }) + profile[key] = m_ProfileJson[key]; + boost::filesystem::create_directories(cache_file.parent_path()); + { + boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc); + ofs << out.dump(-1, ' ', false, json::error_handler_t::ignore); + ofs.close(); + if (! ofs.good()) + throw std::runtime_error("write failed"); + } + if (const std::error_code ec = rename_file(tmp_path, cache_file.string())) + throw std::runtime_error(ec.message()); + } catch (const std::exception& e) { + boost::system::error_code rm; + boost::filesystem::remove(tmp_path, rm); + BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what(); + } + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed: " << e.what(); + reset_profile_json(); + return false; + } +} + +int GuideFrame::LoadProfileData() +{ + // Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded). + // Loading order (fastest to slowest): + // 1. Load every vendor, from its preset cache wherever one covers it + // 2. Read all vendor JSONs by hand + try { + if (!BuildProfileDataFromVendors()) { + // Last resort — read all vendor JSONs + std::set loaded_vendors; + auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); + if (boost::filesystem::exists(vendor_dir / filament_library_name)) + LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string()); + else + LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string()); + loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY); + + boost::filesystem::directory_iterator endIter; + for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) { + if (!boost::filesystem::is_directory(*iter)) { + wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); + strVendor = strVendor.AfterLast('\\'); + strVendor = strVendor.AfterLast('/'); + wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); + if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) + continue; + LoadProfileFamily(w2s(strVendor), iter->path().string()); + loaded_vendors.insert(w2s(strVendor)); + } + if (*m_cancel_token) return 0; + } + + boost::filesystem::directory_iterator others_endIter; + for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) { + if (!boost::filesystem::is_directory(*iter)) { + wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); + strVendor = strVendor.AfterLast('\\'); + strVendor = strVendor.AfterLast('/'); + wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); + if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) + continue; + LoadProfileFamily(w2s(strVendor), iter->path().string()); + loaded_vendors.insert(w2s(strVendor)); + } + if (*m_cancel_token) return 0; + } + } + + // Capture the cancel token by value (shared_ptr) so the lambda doesn't + // touch `this` if GuideFrame is destroyed before the event fires. + auto tok = m_cancel_token; + wxGetApp().CallAfter([this, tok] { + if (!*tok) + on_profile_loaded(); }); - } catch (std::exception& e) { - // wxLogMessage("GUIDE: load_profile_error %s ", e.what()); - // wxMessageBox(e.what(), "", MB_OK); - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what() << std::endl; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what(); } filament_info_cache.clear(); diff --git a/src/slic3r/GUI/WebGuideDialog.hpp b/src/slic3r/GUI/WebGuideDialog.hpp index fcdb0841db..b9592d03fe 100644 --- a/src/slic3r/GUI/WebGuideDialog.hpp +++ b/src/slic3r/GUI/WebGuideDialog.hpp @@ -30,10 +30,14 @@ #include "libslic3r/PresetBundle.hpp" #include "slic3r/Utils/PresetUpdater.hpp" +#include +#include #include #include +#include + namespace Slic3r { namespace GUI { class GuideFrame : public DPIDialog @@ -78,6 +82,12 @@ public: int LoadProfileData(); int SaveProfileData(); int LoadProfileFamily(std::string strVendor, std::string strFilePath); + void init_guide_paths(); + void on_profile_loaded(); + bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors); + bool BuildProfileDataFromPresetBundle(); + bool BuildProfileDataFromVendors(); + void reset_profile_json(); int SaveProfile(); int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType); @@ -112,8 +122,11 @@ private: //First Load bool bFirstComplete{false}; - bool m_destroy{false}; - boost::thread* m_load_task{ nullptr }; + // Set once in the destructor. Read through `this` by the loading thread + // (joined before `this` dies) and captured as the shared_ptr by CallAfter + // lambdas so they don't touch `this` after the object is freed. + std::shared_ptr> m_cancel_token{std::make_shared>(false)}; + std::unique_ptr m_load_task; // User Config bool PrivacyUse; @@ -123,6 +136,7 @@ private: bool InstallNetplugin; bool network_plugin_ready {false}; + json m_ProfileJson; json m_OrcaFilaList; std::string m_OrcaFilaLibPath; diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 18a9db4e26..06808e253d 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -1044,46 +1044,42 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const std::set bundles; // Orca: always install filament library bundles.insert(PresetBundle::ORCA_FILAMENT_LIBRARY); - for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_path)) { - const auto &path = dir_entry.path(); - std::string file_path = path.string(); - if (is_json_file(file_path)) { - const auto path_in_vendor = vendor_path / path.filename(); - std::string vendor_name = path.filename().string(); - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - if (bundles.find(vendor_name) != bundles.end())continue; + // A vendor is named by its profile or, where the build ships preset caches + // instead of the raw profile JSONs, by its cache alone. + for (const std::string &vendor_name : vendor_names_in(rsrc_path)) { + if (bundles.find(vendor_name) != bundles.end())continue; - const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE - || (enabled_vendors.find(vendor_name) != enabled_vendors.end()); - if (enabled_config_update) { - if ( fs::exists(path_in_vendor)) { - if (is_vendor_enabled) { - Semver resource_ver = get_version_from_json(file_path); - Semver vendor_ver = get_version_from_json(path_in_vendor.string()); + const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE + || (enabled_vendors.find(vendor_name) != enabled_vendors.end()); + if (enabled_config_update) { + if (is_vendor_installed(vendor_name)) { + if (is_vendor_enabled) { + // Orca: whichever form of the vendor resources ships at the newer + // version is the one installing lays down, and the one to judge + // what is installed against. + Semver resource_ver = resource_vendor_version(vendor_name); + // Orca: a vendor installed as a preset cache has no profile + // beside it; the version it was installed at is in the cache. + Semver vendor_ver = installed_vendor_version(vendor_name); - if (vendor_ver < resource_ver) { - BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version " - << resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string(); - bundles.insert(vendor_name); - } - } - else { - //need to be removed because not installed - fs::remove(path_in_vendor); - const auto path_of_vendor = vendor_path / vendor_name; - if (fs::exists(path_of_vendor)) - fs::remove_all(path_of_vendor); + if (vendor_ver < resource_ver) { + BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version " + << resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string(); + bundles.insert(vendor_name); } } - else if (is_vendor_enabled) { - bundles.insert(vendor_name); + else { + //need to be removed because not installed + remove_installed_vendor(vendor_name); } } else if (is_vendor_enabled) { bundles.insert(vendor_name); } } + else if (is_vendor_enabled) { + bundles.insert(vendor_name); + } } if (bundles.size() > 0) { @@ -1163,11 +1159,12 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME); auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME); - if (( fs::exists(path_in_vendor)) + if (is_vendor_installed(vendor_name) || fs::exists(print_in_cache) || fs::exists(filament_in_cache) || fs::exists(machine_in_cache)) { - Semver vendor_ver = get_version_from_json(path_in_vendor.string()); + // Orca: a vendor installed as a preset cache carries its version there. + Semver vendor_ver = installed_vendor_version(vendor_name); std::map key_values; std::vector keys(3); diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index bc10bb4f73..28c39c2d6a 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable(${_TEST_NAME}_tests test_preset_bundle_loading.cpp test_preset_setting_id.cpp test_preset_diff.cpp + test_vendor_cache.cpp test_elephant_foot_compensation.cpp test_fill_corner_smoothing.cpp test_fill_plane_path.cpp diff --git a/tests/libslic3r/test_geometry.cpp b/tests/libslic3r/test_geometry.cpp index b5f7b7ef98..b4bbe86cc6 100644 --- a/tests/libslic3r/test_geometry.cpp +++ b/tests/libslic3r/test_geometry.cpp @@ -574,11 +574,6 @@ TEST_CASE("Convex polygon intersection on two squares touching one vertex", "[Ge Polygon B = A; B.translate(10 / SCALING_FACTOR, 10 / SCALING_FACTOR); - SVG svg{std::string("one_vertex_touch") + ".svg"}; - svg.draw(A, "blue"); - svg.draw(B, "green"); - svg.Close(); - bool is_inters = Geometry::convex_polygons_intersect(A, B); REQUIRE(is_inters == false); diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 844ccb6a8b..037a76a805 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1,6 +1,7 @@ #include #include +#include #include "libslic3r/PresetBundle.hpp" #include "libslic3r/AppConfig.hpp" @@ -132,7 +133,7 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl { PresetBundle bundle; - VendorProfile orca_vendor("ORCA"); + VendorProfile orca_vendor; orca_vendor.id = "ORCA"; VendorProfile::PrinterModel model; model.name = "Orca Test"; orca_vendor.models.emplace_back(model); @@ -143,6 +144,31 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl CHECK(bundle.get_current_vendor_type() == VendorType::Unknown); } +TEST_CASE("A malformed entry in a vendor's preset list is counted, not thrown", "[Preset][Bundle]") +{ + ScopedTemporaryDir dir; + + // A bare number where the list wants an object. An array element has no key, + // so reporting one as if it did throws nlohmann's invalid_iterator - which is + // not a parse_error, and escapes the catch around the vendor profile parse. + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[123,)" + << R"({"name":"0.20mm Standard @Acme","sub_path":"process/standard.json"}]})"; + fs::create_directories(dir.path() / "Acme" / "process"); + std::ofstream((dir.path() / "Acme" / "process" / "standard.json").string()) + << R"({"type":"process","name":"0.20mm Standard @Acme","from":"system",)" + << R"("instantiation":"true","layer_height":"0.2"})"; + + PresetBundle bundle; + size_t loaded = 0; + REQUIRE_NOTHROW(loaded = bundle.load_vendor_configs_from_json( + dir.path().string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent).second); + + CHECK(bundle.error_count() > 0); // the malformed element was counted + CHECK(loaded == 1); // the well-formed one beside it still loaded +} + TEST_CASE("Printer extruder count tolerates missing nozzle diameter", "[Preset][Bundle]") { PresetBundle bundle; diff --git a/tests/libslic3r/test_vendor_cache.cpp b/tests/libslic3r/test_vendor_cache.cpp new file mode 100644 index 0000000000..85e100f4d4 --- /dev/null +++ b/tests/libslic3r/test_vendor_cache.cpp @@ -0,0 +1,1620 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PresetCacheFormat.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/Utils.hpp" + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; +namespace fs = boost::filesystem; + +namespace { + +struct TempDir { + fs::path path; + TempDir() { + path = fs::temp_directory_path() / fs::unique_path("orca-cache-test-%%%%-%%%%"); + fs::create_directories(path); + } + ~TempDir() { boost::system::error_code ec; fs::remove_all(path, ec); } +}; + +std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id, + const std::string& version = "1.0.0") +{ + const fs::path p = dir / (vendor_id + ".json"); + std::ofstream f(p.string()); + f << R"({"version":")" << version << R"(","name":")" << vendor_id << R"("})"; + return p.string(); +} + +// One vendor profile with a single process preset beside it, as an install or an +// update lays it down: /.json plus //process/standard.json. +void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version) +{ + fs::create_directories(dir / vendor / "process"); + std::ofstream((dir / (vendor + ".json")).string()) + << R"({"version":")" << version << R"(","name":")" << vendor + << R"(","process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}]})"; + std::ofstream((dir / vendor / "process" / "standard.json").string()) + << R"({"type":"process","name":"0.20mm Standard @)" << vendor + << R"(","from":"system","instantiation":"true","layer_height":"0.2"})"; +} + +// A small but complete vendor: one machine model, one process, a non-instantiated +// base filament with an instantiated child inheriting it, a second standalone +// filament carrying explicit metadata, and one machine preset with a rename — so +// the equivalence test below sees every CachedPreset field populated. +void write_full_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version) +{ + fs::create_directories(dir / vendor / "process"); + fs::create_directories(dir / vendor / "filament"); + fs::create_directories(dir / vendor / "machine"); + std::ofstream((dir / (vendor + ".json")).string()) + << R"({"version":")" << version << R"(","name":")" << vendor << R"(",)" + << R"("machine_model_list":[{"name":"Test Model","sub_path":"machine/model.json"}],)" + << R"("process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}],)" + << R"("filament_list":[)" + << R"({"name":")" << vendor << R"( Base PLA","sub_path":"filament/base.json"},)" + << R"({"name":")" << vendor << R"( PLA @0.4","sub_path":"filament/pla.json"},)" + << R"({"name":")" << vendor << R"( Silk PLA @0.4","sub_path":"filament/silk.json"}],)" + << R"("machine_list":[{"name":")" << vendor << R"( 0.4 nozzle","sub_path":"machine/printer.json"}]})"; + std::ofstream((dir / vendor / "machine" / "model.json").string()) + << R"({"type":"machine_model","name":"Test Model","nozzle_diameter":"0.4"})"; + std::ofstream((dir / vendor / "process" / "standard.json").string()) + << R"({"type":"process","name":"0.20mm Standard @)" << vendor + << R"(","from":"system","instantiation":"true","layer_height":"0.2"})"; + std::ofstream((dir / vendor / "filament" / "base.json").string()) + << R"({"type":"filament","name":")" << vendor + << R"( Base PLA","from":"system","instantiation":"false","filament_id":"GFA_base","filament_cost":"42"})"; + std::ofstream((dir / vendor / "filament" / "pla.json").string()) + << R"({"type":"filament","name":")" << vendor + << R"( PLA @0.4","from":"system","instantiation":"true","filament_id":"GFA00","filament_cost":"20",)" + << R"("setting_id":"GFSA04","description":"Test PLA description"})"; + std::ofstream((dir / vendor / "filament" / "silk.json").string()) + << R"({"type":"filament","name":")" << vendor + << R"( Silk PLA @0.4","from":"system","instantiation":"true","inherits":")" << vendor << R"( Base PLA"})"; + std::ofstream((dir / vendor / "machine" / "printer.json").string()) + << R"({"type":"machine","name":")" << vendor + << R"( 0.4 nozzle","from":"system","instantiation":"true","printer_model":"Test Model","printer_variant":"0.4",)" + << R"("renamed_from":")" << vendor << R"( old 0.4 nozzle"})"; +} + +// The filament library: one non-instantiated base filament other vendors inherit +// from. `cost` lets a test bump the library and watch the change flow through. +void write_lib_tree(const fs::path& dir, const std::string& version, const std::string& cost) +{ + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + fs::create_directories(dir / lib / "filament"); + std::ofstream((dir / (lib + ".json")).string()) + << R"({"version":")" << version << R"(","name":")" << lib << R"(",)" + << R"("filament_list":[{"name":"Generic PLA","sub_path":"filament/generic_pla.json"}]})"; + std::ofstream((dir / lib / "filament" / "generic_pla.json").string()) + << R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false",)" + << R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})"; +} + +// A vendor whose one filament inherits the library's base and states nothing of +// its own — everything it shows comes from the library it is resolved against. +void write_vendor_with_lib_filament(const fs::path& dir, const std::string& vendor, const std::string& version) +{ + fs::create_directories(dir / vendor / "filament"); + std::ofstream((dir / (vendor + ".json")).string()) + << R"({"version":")" << version << R"(","name":")" << vendor << R"(",)" + << R"("filament_list":[{"name":")" << vendor << R"( PLA @0.4","sub_path":"filament/pla.json"}]})"; + std::ofstream((dir / vendor / "filament" / "pla.json").string()) + << R"({"type":"filament","name":")" << vendor + << R"( PLA @0.4","from":"system","instantiation":"true","inherits":"Generic PLA"})"; +} + +std::string write_versionless_vendor_json(const fs::path& dir, const std::string& vendor_id) +{ + const fs::path p = dir / (vendor_id + ".json"); + std::ofstream f(p.string()); + f << R"({"name":")" << vendor_id << R"("})"; + return p.string(); +} + +// Whole file as bytes, for the byte-identity comparisons below. +std::string slurp(const fs::path& p) +{ + std::string s; + load_string_file(p, s); + return s; +} + +// Flip one byte of the body. The default lands in the stamps at the front, which +// every reader checks; pass an offset past them to corrupt a file that still +// answers VendorCacheFile::peek_version but cannot survive its CRC. +void corrupt_blob_byte(const std::string& path, std::streamoff at = 30) +{ + std::fstream f(path, std::ios::in | std::ios::out | std::ios::binary); + f.seekp(at); + char b = 0; f.read(&b, 1); + f.seekp(at); + b ^= 0xFF; + f.write(&b, 1); +} + +// Overwrite `n` bytes at `payload_off` into the cache's payload (which starts at +// file offset 20, behind the header) and recompute the header CRC, so the file +// stays authentic and only the deserializer can object to its contents. +void patch_payload_bytes(const std::string& path, size_t payload_off, const void* bytes, size_t n) +{ + constexpr size_t header_size = 20; // magic(4) + version(4) + data_size(8) + crc32(4) + std::ifstream in(path, std::ios::binary); + std::vector data(std::istreambuf_iterator(in), {}); + in.close(); + REQUIRE(data.size() >= header_size + payload_off + n); + std::memcpy(&data[header_size + payload_off], bytes, n); + boost::crc_32_type crc; + crc.process_bytes(&data[header_size], data.size() - header_size); + const uint32_t new_crc = crc.checksum(); + std::memcpy(&data[16], &new_crc, 4); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(data.data(), static_cast(data.size())); +} + +// Patch cache_version (the payload's first word) so the file passes the CRC +// check but fails the cache_version check in VendorCacheFile::load. +void patch_cache_version(const std::string& path, uint32_t wrong_version) +{ + patch_payload_bytes(path, 0, &wrong_version, sizeof(wrong_version)); +} + +// Truncates the cache's PAYLOAD (everything after the 20-byte header) by +// `truncate_by` bytes and recomputes data_size/crc32 in the header, exactly +// as the cache writer computes them, so the framing's size and CRC checks +// still pass but cereal runs out of bytes partway through deserializing the +// body — exercising VendorCacheFile::load's catch block instead of its early +// (pre-body) rejection paths. +void truncate_payload_and_fix_header(const std::string& path, size_t truncate_by) +{ + constexpr size_t header_size = 20; // magic(4) + version(4) + data_size(8) + crc32(4) + std::ifstream in(path, std::ios::binary); + std::vector data(std::istreambuf_iterator(in), {}); + in.close(); + REQUIRE(data.size() > header_size + truncate_by); + const size_t new_payload_size = data.size() - header_size - truncate_by; + const uint64_t data_size_field = static_cast(new_payload_size); + boost::crc_32_type crc; + crc.process_bytes(&data[header_size], new_payload_size); + const uint32_t crc_field = crc.checksum(); + std::memcpy(&data[8], &data_size_field, sizeof(data_size_field)); // data_size offset + std::memcpy(&data[16], &crc_field, sizeof(crc_field)); // crc32 offset + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(data.data(), static_cast(header_size + new_payload_size)); +} + +// One vendor as a cache's VendorMap. It carries one printer model ("Test Model", +// variant "0.4") so machine entries can pass install's model/variant validation. +VendorMap one_vendor(const std::string& vendor_id, const std::string& name = "", + Semver ver = Semver(1, 0, 0)) +{ + VendorMap vendors; + VendorProfile vp(vendor_id); + vp.name = name.empty() ? vendor_id + " Corp" : name; + vp.config_version = ver; + VendorProfile::PrinterModel model; + model.id = "Test Model"; + model.variants.emplace_back(VendorProfile::PrinterVariant("0.4")); + vp.models.push_back(model); + vendors.emplace(vendor_id, vp); + return vendors; +} + +// Source-form entries as parse_subfile would emit them. The alias is derived by +// install from the '@' in the name, exactly as it is for the JSON parse. +CachedPreset filament_entry(const std::string& name, const std::string& filament_id = "GFA00", + const std::string& inherits = "") +{ + CachedPreset e; + e.name = name; + e.sub_path = "filament/" + name + ".json"; + e.instantiation = "true"; + e.filament_id = filament_id; + e.inherits = inherits; + return e; +} + +CachedPreset printer_entry(const std::string& name) +{ + CachedPreset e; + e.name = name; + e.sub_path = "machine/" + name + ".json"; + e.instantiation = "true"; + e.config_src.set_key_value("printer_model", new ConfigOptionString("Test Model")); + e.config_src.set_key_value("printer_variant", new ConfigOptionString("0.4")); + return e; +} + +static bool save_one_vendor(const std::string& path, const VendorMap& vendors, + const std::string& vendor, const std::string& vendor_version, + const std::vector& filament_entries = {}, + const std::vector& machine_entries = {}, + const std::vector& process_entries = {}) +{ + VendorCacheData data; + data.vendors = vendors; + data.process_entries = process_entries; + data.filament_entries = filament_entries; + data.machine_entries = machine_entries; + return VendorCacheFile::save(path, vendor, vendor_version, data); +} + +// resources_dir()/data_dir() are process-wide, so restore them however the test +// leaves — including through a failed REQUIRE — to stay green under --order rand. +struct ScopedDirs { + std::string prev_data{data_dir()}, prev_rsrc{resources_dir()}; + ScopedDirs(const fs::path& data, const fs::path& rsrc) + { + set_data_dir(data.string()); + set_resources_dir(rsrc.string()); + } + ~ScopedDirs() { set_data_dir(prev_data); set_resources_dir(prev_rsrc); } +}; + +// A data dir and a resources dir, both pointed at by the process-wide accessors, +// with the two directories a vendor is installed into and shipped from already +// created. What every install- and load-order test needs before it starts. +struct InstallDirs { + TempDir data, rsrc; + fs::path system = data.path / PRESET_SYSTEM_DIR; + fs::path profiles = rsrc.path / "profiles"; + ScopedDirs scoped { data.path, rsrc.path }; + + InstallDirs() + { + fs::create_directories(system); + fs::create_directories(profiles); + } +}; + +// Helper: filter a collection by vendor_id. +std::vector presets_for(const PresetCollection& coll, const std::string& vendor_id) +{ + std::vector out; + for (const Preset& p : coll()) + if (p.is_system && p.vendor && p.vendor->id == vendor_id) + out.push_back(&p); + return out; +} + +} // namespace + +namespace Slic3r { +inline bool operator==(const VendorProfile::PrinterVariant& a, const VendorProfile::PrinterVariant& b) { return a.name == b.name; } +inline bool operator==(const VendorProfile::PrinterModel& a, const VendorProfile::PrinterModel& b) +{ + return a.id == b.id && a.name == b.name && a.model_id == b.model_id && a.technology == b.technology + && a.family == b.family && a.variants == b.variants && a.default_materials == b.default_materials + && a.not_support_bed_types == b.not_support_bed_types && a.bed_model == b.bed_model + && a.bed_texture == b.bed_texture && a.image_bed_type == b.image_bed_type + && a.bottom_texture_end_name == b.bottom_texture_end_name + && a.use_double_extruder_default_texture == b.use_double_extruder_default_texture + && a.bottom_texture_rect == b.bottom_texture_rect + && a.bottom_texture_rect_longer == b.bottom_texture_rect_longer + && a.middle_texture_rect == b.middle_texture_rect && a.hotend_model == b.hotend_model; +} +} // namespace Slic3r + +static bool vendor_deep_equal(const VendorProfile& a, const VendorProfile& b) +{ + return a.name == b.name && a.id == b.id && a.config_version == b.config_version + && a.config_update_url == b.config_update_url && a.changelog_url == b.changelog_url + && a.models == b.models && a.default_filaments == b.default_filaments + && a.default_sla_materials == b.default_sla_materials; +} + +static bool preset_deep_equal(const Preset& a, const Preset& b) +{ + return a.type == b.type && a.is_default == b.is_default && a.is_external == b.is_external + && a.is_system == b.is_system && a.is_visible == b.is_visible && a.is_dirty == b.is_dirty + && a.is_compatible == b.is_compatible && a.is_project_embedded == b.is_project_embedded + && a.name == b.name && a.file == b.file && a.loaded == b.loaded + && a.config.equals(b.config) + && a.alias == b.alias && a.renamed_from == b.renamed_from + && a.m_excluded_from == b.m_excluded_from && a.m_from_orca_filament_lib == b.m_from_orca_filament_lib + && a.bundle_id == b.bundle_id && a.version == b.version && a.ini_str == b.ini_str + && a.setting_id == b.setting_id && a.filament_id == b.filament_id && a.user_id == b.user_id + && a.base_id == b.base_id && a.sync_info == b.sync_info && a.description == b.description + && a.updated_time == b.updated_time && a.key_values == b.key_values; +} + +TEST_CASE("a saved cache loads back with names, aliases and filament ids intact", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA @0.4", "GFL_acme_pla")}, + {printer_entry(vid + " Printer 0.4")})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + REQUIRE(out.vendors.count(vid) == 1); + + auto fi = presets_for(out.filaments, vid); + auto pr = presets_for(out.printers, vid); + REQUIRE(fi.size() == 1); + CHECK(fi[0]->name == vid + " PLA @0.4"); + CHECK(fi[0]->alias == "Acme PLA"); + CHECK(fi[0]->filament_id == "GFL_acme_pla"); + REQUIRE(pr.size() == 1); + CHECK(pr[0]->name == vid + " Printer 0.4"); +} + +TEST_CASE("loading a missing cache file returns false", "[VendorCache]") +{ + TempDir tmp; + PresetBundle out; + REQUIRE(!out.load_vendor_cache((tmp.path / "nonexistent.opc").string(), "Acme", Semver("1.0.0"))); +} + +TEST_CASE("a cache with a corrupted byte is rejected by the CRC check", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA")})); + corrupt_blob_byte(cache.string()); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); +} + +TEST_CASE("two vendors produce two independent cache files", "[VendorCache]") +{ + TempDir tmp; + const fs::path cacheA = tmp.path / "vendorA.opc"; + const fs::path cacheB = tmp.path / "vendorB.opc"; + + REQUIRE(save_one_vendor(cacheA.string(), one_vendor("VendorA"), "VendorA", "1.0.0", + {filament_entry("VendorA PLA")})); + REQUIRE(save_one_vendor(cacheB.string(), one_vendor("VendorB"), "VendorB", "1.0.0", + {filament_entry("VendorB PLA")})); + + // Corrupt only vendor B's file; vendor A's must be unaffected. + corrupt_blob_byte(cacheB.string()); + + PresetBundle outA; + REQUIRE(outA.load_vendor_cache(cacheA.string(), "VendorA", Semver("1.0.0"))); + REQUIRE(outA.vendors.count("VendorA") == 1); + REQUIRE(presets_for(outA.filaments, "VendorA").size() == 1); + + PresetBundle outB; + REQUIRE(!outB.load_vendor_cache(cacheB.string(), "VendorB", Semver("1.0.0"))); + REQUIRE(outB.vendors.empty()); +} + +TEST_CASE("vendor profile fields survive a cache round-trip", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + VendorMap vendors; + VendorProfile vp(vid); + vp.name = "Acme Corporation"; + vp.config_version = Semver(2, 5, 1); + VendorProfile::PrinterModel model; + model.id = "AcmePro"; + model.name = "Acme Pro"; + VendorProfile::PrinterVariant v0_4; v0_4.name = "0.4"; + model.variants.push_back(v0_4); + vp.models.push_back(model); + vendors.emplace(vid, vp); + REQUIRE(save_one_vendor(cache.string(), vendors, vid, "2.5.1")); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("2.5.1"))); + REQUIRE(out.vendors.count(vid) == 1); + const VendorProfile& gvp = out.vendors.at(vid); + REQUIRE(vendor_deep_equal(gvp, vendors.at(vid))); + // Spot-check the fields the old test asserted directly, so a + // vendor_deep_equal regression still points at what actually broke. + CHECK(gvp.id == vid); + CHECK(gvp.name == "Acme Corporation"); + REQUIRE(gvp.models.size() == 1); + CHECK(gvp.models[0].id == "AcmePro"); + CHECK(gvp.models[0].name == "Acme Pro"); + REQUIRE(gvp.models[0].variants.size() == 1); + CHECK(gvp.models[0].variants[0].name == "0.4"); +} + +TEST_CASE("config option values survive a cache round-trip", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + auto entry = filament_entry(vid + " PETG @0.4"); + entry.config_src.set_key_value("filament_type", new ConfigOptionStrings({"PETG"})); + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", {entry})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + + auto fi = presets_for(out.filaments, vid); + REQUIRE(fi.size() == 1); + const auto* ft = fi[0]->config.option("filament_type"); + REQUIRE(ft != nullptr); + REQUIRE(ft->values.size() >= 1); + CHECK(ft->values[0] == "PETG"); +} + +TEST_CASE("multiple presets in one collection all round-trip", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + const std::vector fi_names = {vid + " PLA", vid + " PETG", vid + " ABS"}; + const std::vector pr_names = {vid + " Printer 0.4", vid + " Printer 0.6"}; + std::vector filament_entries, machine_entries; + for (const auto& n : fi_names) filament_entries.push_back(filament_entry(n)); + for (const auto& n : pr_names) machine_entries.push_back(printer_entry(n)); + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + filament_entries, machine_entries)); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + + auto fi = presets_for(out.filaments, vid); + auto pr = presets_for(out.printers, vid); + REQUIRE(fi.size() == 3); + REQUIRE(pr.size() == 2); + + std::set fi_got, pr_got; + for (const auto* p : fi) fi_got.insert(p->name); + for (const auto* p : pr) pr_got.insert(p->name); + for (const auto& n : fi_names) CHECK(fi_got.count(n) == 1); + for (const auto& n : pr_names) CHECK(pr_got.count(n) == 1); +} + +TEST_CASE("a truncated cache file is rejected", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "truncated.opc"; + { + std::ofstream f(cache.string(), std::ios::binary); + const char data[] = {0x4F, 0x52, 0x43}; + f.write(data, sizeof(data)); + } + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); +} + +TEST_CASE("a cache with the wrong magic number is rejected", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA")})); + + { + std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary); + const uint32_t bad = 0xDEADBEEFu; + f.write(reinterpret_cast(&bad), sizeof(bad)); + } + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); +} + +TEST_CASE("a vendor with no presets saves and loads cleanly", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid, "Acme Corporation"), vid, "1.0.0")); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + REQUIRE(out.vendors.count(vid) == 1); + CHECK(out.vendors.at(vid).id == vid); + CHECK(out.vendors.at(vid).name == "Acme Corporation"); + CHECK(presets_for(out.filaments, vid).empty()); + CHECK(presets_for(out.printers, vid).empty()); + CHECK(presets_for(out.prints, vid).empty()); +} + +TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + write_full_vendor_tree(user, "Acme", "1.0.0"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle from_json; + from_json.set_generate_vendor_caches(true); + from_json.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(user / "Acme.opc")); + + // Take the preset JSONs away: were the cache rejected, the load below would + // have nothing to parse — so its success proves the cache answered. + fs::remove_all(user / "Acme"); + PresetBundle from_cache; + from_cache.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + // Both paths run the same install code over the same entries, so everything + // observable must come out identical — the vendor profile and every preset, + // field by field. + REQUIRE(from_cache.vendors.count("Acme") == 1); + REQUIRE(vendor_deep_equal(from_cache.vendors.at("Acme"), from_json.vendors.at("Acme"))); + const std::pair colls[] = { + {&from_json.prints, &from_cache.prints}, + {&from_json.filaments, &from_cache.filaments}, + {&from_json.printers, &from_cache.printers}, + }; + for (const auto& [jc, cc] : colls) { + auto a = presets_for(*jc, "Acme"); + auto b = presets_for(*cc, "Acme"); + REQUIRE(a.size() == b.size()); + REQUIRE(!a.empty()); + for (size_t i = 0; i < a.size(); ++i) { + CHECK(a[i]->name == b[i]->name); + CHECK(preset_deep_equal(*a[i], *b[i])); + } + } + + // Pin the explicit metadata against symmetric loss: dropping a field from + // visit_entry (PresetCacheFormat.cpp) keeps the two bundles equal to each + // other, but not to the fixture. + const Preset* pla = from_cache.filaments.find_preset("Acme PLA @0.4", false); + REQUIRE(pla != nullptr); + CHECK(pla->setting_id == "GFSA04"); + CHECK(pla->description == "Test PLA description"); + const Preset* silk = from_cache.filaments.find_preset("Acme Silk PLA @0.4", false); + REQUIRE(silk != nullptr); + CHECK(silk->filament_id == "GFA_base"); // inherited from the non-instantiated base + const auto* cost = silk->config.option("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); + const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false); + REQUIRE(pr != nullptr); + CHECK(pr->renamed_from == std::vector{"Acme old 0.4 nozzle"}); +} + +TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + // One process preset without the required "instantiation" key — a parse-phase + // error the load survives, so it must reach the cache's parse_errors stamp. + fs::create_directories(user / "Acme" / "process"); + std::ofstream((user / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[{"name":"0.20mm Standard @Acme","sub_path":"process/standard.json"}]})"; + std::ofstream((user / "Acme" / "process" / "standard.json").string()) + << R"({"type":"process","name":"0.20mm Standard @Acme","from":"system","layer_height":"0.2"})"; + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle from_json; + from_json.set_generate_vendor_caches(true); + from_json.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(user / "Acme.opc")); + CHECK(from_json.error_count() > 0); + + fs::remove_all(user / "Acme"); + PresetBundle from_cache; + from_cache.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(from_cache.error_count() == from_json.error_count()); + CHECK(presets_for(from_cache.prints, "Acme").size() == 1); +} + +TEST_CASE("a non-instantiated base in a regular vendor's cache resolves its children and stays out of the library maps", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + + // Entry order is the resolution order: the base must install (into the local + // config maps) before the child that inherits it. + auto base = filament_entry("Acme Base PLA", "GFA_base"); + base.instantiation = "false"; + base.config_src.set_key_value("filament_cost", new ConfigOptionFloats({42.})); + auto child = filament_entry("Acme Silk PLA @0.4", "", "Acme Base PLA"); + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0", {base, child})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); + auto fi = presets_for(out.filaments, "Acme"); + REQUIRE(fi.size() == 1); // the base never becomes a preset + CHECK(fi[0]->name == "Acme Silk PLA @0.4"); + CHECK(fi[0]->filament_id == "GFA_base"); + const auto* cost = fi[0]->config.option("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); + // Only the filament library's bases persist as the cross-vendor inheritance + // maps; a regular vendor's stay local to its own load. + CHECK(out.m_config_maps.empty()); + CHECK(out.m_filament_id_maps.empty()); +} + +TEST_CASE("a cache with the wrong cache version is rejected", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA")})); + patch_cache_version(cache.string(), 0xFFFFFFFFu); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); +} + +TEST_CASE("a cache truncated mid-blob is rejected", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA")})); + + { + std::ifstream in(cache.string(), std::ios::binary); + std::vector buf(30); // 20-byte header + 10 bytes of blob + in.read(buf.data(), 30); + in.close(); + std::ofstream out(cache.string(), std::ios::binary | std::ios::trunc); + out.write(buf.data(), 30); + } + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); +} + +TEST_CASE("printer model bed texture fields survive a cache round-trip", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + VendorMap vendors = one_vendor(vid); + VendorProfile::PrinterModel model; + model.id = "N1"; + model.name = "Neat One"; + model.bottom_texture_rect_longer = "5,5,50,10"; + vendors.at(vid).models.push_back(model); + REQUIRE(save_one_vendor(cache.string(), vendors, vid, "1.0.0")); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + REQUIRE(out.vendors.at(vid).models.size() == 2); + REQUIRE(vendor_deep_equal(out.vendors.at(vid), vendors.at(vid))); + CHECK(out.vendors.at(vid).models[1].bottom_texture_rect_longer == "5,5,50,10"); +} + +TEST_CASE("a cache older than the vendor profile on disk is rejected", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.1"))); +} + +TEST_CASE("a cache newer than the vendor profile on disk is used", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.2.0", + {filament_entry("Acme PLA")})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); + CHECK(presets_for(out.filaments, "Acme").size() == 1); +} + +TEST_CASE("a vendor cache outlives a filament library update and resolves against the new library", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + write_lib_tree(user, "1.0.0", "20"); + write_vendor_with_lib_filament(user, "Acme", "1.0.0"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + // First launch: the library parses first, then the vendor against it, and + // both caches are written. + PresetBundle base1; + base1.set_generate_vendor_caches(true); + base1.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + PresetBundle acme1; + acme1.set_generate_vendor_caches(true); + acme1.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &base1); + REQUIRE(fs::exists(user / "Acme.opc")); + { + auto fi = presets_for(acme1.filaments, "Acme"); + REQUIRE(fi.size() == 1); + const auto* cost = fi[0]->config.option("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(20., 1e-9)); + } + + // An update delivers a new library only; the vendor stays as it was. + write_lib_tree(user, "2.0.0", "30"); + PresetBundle base2; + base2.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + // Take the vendor's preset JSONs away: were its cache rejected, the load + // below would have nothing to parse — so its success proves the cache + // survived the library bump. + fs::remove_all(user / "Acme"); + PresetBundle acme2; + auto [substitutions, presets_loaded] = acme2.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &base2); + CHECK(presets_loaded == 1); + auto fi = presets_for(acme2.filaments, "Acme"); + REQUIRE(fi.size() == 1); + // The cache holds only the vendor's own diff; the library values come from + // the library loaded now, not the one in effect when the cache was written. + const auto* cost = fi[0]->config.option("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9)); + CHECK(fi[0]->filament_id == "GFL99"); +} + +TEST_CASE("a vendor installed as its cache alone still loads after a library update", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + write_lib_tree(user, "1.0.0", "20"); + write_vendor_with_lib_filament(user, "Acme", "1.0.0"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + // Generate the vendor's cache, then strip the vendor to the cache alone — + // the shape of a packaged install, which ships each vendor as its .opc and + // nothing else. + PresetBundle base1; + base1.set_generate_vendor_caches(true); + base1.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + PresetBundle acme1; + acme1.set_generate_vendor_caches(true); + acme1.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &base1); + fs::remove(user / "Acme.json"); + fs::remove_all(user / "Acme"); + + // An OTA update then delivers a new library only. With no JSONs anywhere to + // fall back on, the vendor must keep loading from its cache. + write_lib_tree(user, "2.0.0", "30"); + PresetBundle base2; + base2.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + PresetBundle acme2; + auto [substitutions, presets_loaded] = acme2.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &base2); + CHECK(presets_loaded == 1); + REQUIRE(acme2.vendors.count("Acme") == 1); + auto fi = presets_for(acme2.filaments, "Acme"); + REQUIRE(fi.size() == 1); + const auto* cost = fi[0]->config.option("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9)); +} + +TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSONs", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + write_vendor_tree(user, "Acme", "1.0.0"); + + // A cache claiming the installed version, but whose entry inherits a preset + // no loaded library provides. + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", + {filament_entry("Acme PLA @0.4", "GFA00", "No Such Base")})); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + // Directly: the load fails and leaves the bundle clean. + PresetBundle direct; + REQUIRE(!direct.load_vendor_cache((user / "Acme.opc").string(), "Acme", Semver("1.0.0"))); + CHECK(direct.vendors.empty()); + + // Through the vendor load: the JSONs answer instead, as if no cache existed. + PresetBundle out; + auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(presets_loaded == 1); + CHECK(out.vendors.at("Acme").name == "Acme"); // the profile's name, not the cache's +} + +TEST_CASE("a profile with no usable version is never served from cache", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); + + PresetBundle out; + // An unversioned vendor profile has no version to compare against. + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver::invalid())); + // And a cache carrying no version of its own cannot cover a profile that has one. + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "")); + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); + REQUIRE(out.vendors.empty()); +} + +TEST_CASE("a versionless profile beside a cache keeps the cache from being served", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", + {filament_entry("Acme PLA @0.4")})); + // The profile beside the cache parses to no usable version, which can no + // more judge the cache's staleness than it could be cached itself. + write_versionless_vendor_json(user, "Acme"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle out; + auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); + // Nothing came from the cache: the versionless profile was parsed instead, + // and it carries no presets. + CHECK(presets_loaded == 0); +} + +TEST_CASE("a vendor's cache is its whole installation", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources"; + const fs::path data = tmp.path / "data"; + fs::create_directories(rsrc / "profiles" / "Acme" / "machine"); + write_vendor_json(rsrc / "profiles", "Acme"); + std::ofstream((rsrc / "profiles" / "Acme" / "machine" / "printer.json").string()) << "{}"; + + REQUIRE(save_one_vendor((rsrc / "profiles" / "Acme.opc").string(), one_vendor("Acme"), "Acme", "1.0.0")); + + ScopedDirs dirs(data, rsrc); + REQUIRE(install_vendor_bundles_from_resources({"Acme"})); + // The cache carries the presets, the vendor profile and the version they were + // built at, so it is installed on its own. + CHECK(fs::exists(data / "system" / "Acme.opc")); + CHECK(!fs::exists(data / "system" / "Acme.json")); + CHECK(!fs::exists(data / "system" / "Acme")); + CHECK(is_vendor_installed("Acme")); + CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); + + // A vendor with no cache is installed as its profile and preset JSONs instead, + // parsing them being the only way left to load it — and the cache the previous + // install left behind has to go, or it would shadow the profile just installed. + fs::remove(rsrc / "profiles" / "Acme.opc"); + REQUIRE(install_vendor_bundles_from_resources({"Acme"})); + CHECK(!fs::exists(data / "system" / "Acme.opc")); + CHECK(fs::exists(data / "system" / "Acme" / "machine" / "printer.json")); + CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); + + // Installing the cache again takes the profile and its preset JSONs back out. + REQUIRE(save_one_vendor((rsrc / "profiles" / "Acme.opc").string(), one_vendor("Acme"), "Acme", "1.0.0")); + REQUIRE(install_vendor_bundles_from_resources({"Acme"})); + CHECK(fs::exists(data / "system" / "Acme.opc")); + CHECK(!fs::exists(data / "system" / "Acme.json")); + CHECK(!fs::exists(data / "system" / "Acme")); +} + +TEST_CASE("a vendor shipped as a cache alone is installed and loaded from it", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + + // A packaged build: every vendor is its cache, with no profile of any kind + // beside it — not even the filament library's. + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + REQUIRE(save_one_vendor((rsrc / (lib + ".opc")).string(), one_vendor(lib, "Shipped Library"), lib, "1.0.0")); + REQUIRE(save_one_vendor((rsrc / "Acme.opc").string(), one_vendor("Acme", "Shipped Acme"), "Acme", "1.0.0")); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + // The version the build ships the vendor at comes from the cache, there being + // no profile to read it from. + CHECK(resource_vendor_version("Acme") == Semver(1, 0, 0)); + + // Resources reaches the app by being installed, never by being loaded from. + REQUIRE(install_vendor_bundles_from_resources({lib, "Acme"})); + CHECK(fs::exists(user / "Acme.opc")); + CHECK(!fs::exists(user / "Acme.json")); + CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); + + PresetBundle after; + after.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(after.vendors.at("Acme").name == "Shipped Acme"); +} + +TEST_CASE("a vendor with a profile in the data dir is parsed there and cached there, whatever resources ships", "[VendorCache]") +{ + // The reported regression: a valid resources/profiles/.opc answered + // first, so the JSON in system/ was never parsed and system/.opc was + // never written. Main reads system/ and nothing else. + InstallDirs dirs; + + write_vendor_tree(dirs.system, "Shadow", "1.0.0"); + // A cache in resources at the very same version — under the old two-tier + // lookup this was accepted and the parse skipped. + REQUIRE(save_one_vendor((dirs.profiles / "Shadow.opc").string(), one_vendor("Shadow"), "Shadow", "1.0.0")); + + PresetBundle bundle; + bundle.set_generate_vendor_caches(true); + REQUIRE(bundle.load_vendor_configs_from_json(dirs.system.string(), "Shadow", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent).second > 0); + + // Parsed from system/, and its cache written back beside the profile. + CHECK(fs::exists(dirs.system / "Shadow.opc")); + CHECK(presets_for(bundle.prints, "Shadow").size() == 1); +} + +TEST_CASE("a vendor with nothing installed is not loaded from resources", "[VendorCache]") +{ + // Resources reaches the app by being installed into system/ first. A vendor + // that is not installed is not loaded, however completely resources ships it. + InstallDirs dirs; + + write_vendor_tree(dirs.profiles, "Absent", "1.0.0"); + REQUIRE(save_one_vendor((dirs.profiles / "Absent.opc").string(), one_vendor("Absent"), "Absent", "1.0.0")); + + PresetBundle bundle; + REQUIRE_THROWS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Absent", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent)); + CHECK(presets_for(bundle.prints, "Absent").empty()); +} + +TEST_CASE("a cache installed with no profile beside it is used whatever its version", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + write_vendor_json(rsrc, "Acme"); + + // Installed at an older version than the one now shipped in resources. Nothing + // sits beside it claiming to be newer, so the cache is what the vendor is. + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Installed Acme"), "Acme", "0.9.0")); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") == "0.9.0"); + CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Other").empty()); + CHECK(installed_vendor_version("Acme") == Semver(0, 9, 0)); + + // Loading the vendor takes the installed cache, not the newer shipped profile. + PresetBundle out; + out.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(out.vendors.at("Acme").name == "Installed Acme"); +} + +TEST_CASE("a vendor whose cache covers it is loaded without parsing any JSON", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", + {filament_entry("Acme PLA @0.4")}, + {printer_entry("Acme Printer 0.4")})); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + // The cache is the whole installation — no profile, no preset JSONs — and the + // caller asks for the vendor exactly as it would for a JSON install. + PresetBundle out; + auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); + CHECK(substitutions.empty()); + CHECK(presets_loaded == 2); + CHECK(out.vendors.at("Acme").name == "Cached Acme"); + + // Nothing was written back: the presets never came from a parse. + CHECK(!fs::exists(user / "Acme.json")); +} + +TEST_CASE("a vendor whose cache is stale falls back to parsing its JSONs", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + + // An update installed the vendor at 2.0.0; the cache next to it was built from + // the profile before that, so it no longer covers what is on disk. + write_vendor_tree(user, "Acme", "2.0.0"); + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0")); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle out; + auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(presets_loaded == 1); + CHECK(out.vendors.at("Acme").config_version == Semver(2, 0, 0)); + + // A one-off parse like this one leaves the stale cache alone: only a bundle + // told its parses are complete writes one. + CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") == "1.0.0"); + + PresetBundle caching; + caching.set_generate_vendor_caches(true); + caching.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") + == get_version_from_json((user / "Acme.json").string()).to_string()); +} + +TEST_CASE("a cache with a mismatched vendor name is rejected", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("VendorA"), "VendorA", "1.0.0")); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), "VendorB", Semver("1.0.0"))); +} + +TEST_CASE("a cache is rejected against an unparsable version", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); + PresetBundle out; + // A profile version that does not parse comes out of get_version_from_json + // as zero, which cannot be judged any more than Semver::invalid() can. + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver())); + REQUIRE(out.vendors.empty()); // rejection happens before the body is touched +} + +TEST_CASE("the filament library's inheritance maps are rebuilt on cache load", "[VendorCache]") +{ + // m_config_maps/m_filament_id_maps are the inheritance base other vendors + // resolve against. The cache no longer stores them: they are rebuilt by + // installing the library's entries — including the non-instantiated bases, + // which exist for exactly this and never become presets. + TempDir tmp; + const fs::path cache = tmp.path / "lib.opc"; + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + + auto base = filament_entry("Generic PLA", "GFL99"); + base.instantiation = "false"; + base.config_src.set_key_value("filament_cost", new ConfigOptionFloats({20.})); + REQUIRE(save_one_vendor(cache.string(), one_vendor(lib), lib, "1.0.0", {base})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), lib, Semver("1.0.0"))); + REQUIRE(out.m_config_maps.count("Generic PLA") == 1); + const auto* cost = out.m_config_maps.at("Generic PLA").option("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(20., 1e-9)); + CHECK(out.m_filament_id_maps.at("Generic PLA") == "GFL99"); + CHECK(presets_for(out.filaments, lib).empty()); // not instantiated, not a preset +} + +TEST_CASE("the same fixture parsed twice serializes byte-identically", "[VendorCache]") +{ + // Shipped caches must be reproducible: the same profiles must produce the + // same bytes on every machine that generates them. + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + write_full_vendor_tree(user, "Acme", "1.0.0"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle first; + first.set_generate_vendor_caches(true); + first.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(user / "Acme.opc")); + const std::string bytes1 = slurp(user / "Acme.opc"); + fs::remove(user / "Acme.opc"); + + PresetBundle second; + second.set_generate_vendor_caches(true); + second.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(slurp(user / "Acme.opc") == bytes1); +} + +TEST_CASE("a cache that fails mid-body deserialization is rejected and leaves the bundle clean", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path valid_cache = tmp.path / "valid.opc"; + const fs::path corrupt_cache = tmp.path / "corrupt.opc"; + + REQUIRE(save_one_vendor(valid_cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA @0.4")}, + {printer_entry(vid + " Printer 0.4")})); + // Truncate the tail (machine entries + parse_errors, per VendorCacheFile::save's + // field order) so the header's size/CRC still validate but cereal runs out of + // bytes partway through the body. Grow the cut if a given size ever stops + // throwing (e.g. after an unrelated field-order change to the cache format). + size_t truncate_by = 40; + bool throws = false; + for (; truncate_by <= 200; truncate_by += 8) { + fs::copy_file(valid_cache, corrupt_cache, fs::copy_option::overwrite_if_exists); + truncate_payload_and_fix_header(corrupt_cache.string(), truncate_by); + PresetBundle probe_bundle; + if (!probe_bundle.load_vendor_cache(corrupt_cache.string(), vid, Semver("1.0.0"))) { + throws = true; + break; + } + } + REQUIRE(throws); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(corrupt_cache.string(), vid, Semver("1.0.0"))); + // The catch block put the bundle back the way a failed parse would leave it. + CHECK(out.vendors.empty()); + CHECK(out.m_config_maps.empty()); + CHECK(presets_for(out.filaments, vid).empty()); + + // The recovery must leave a bundle a caller can still load a good cache into. + REQUIRE(out.load_vendor_cache(valid_cache.string(), vid, Semver("1.0.0"))); + CHECK(out.vendors.count(vid) == 1); + CHECK(presets_for(out.filaments, vid).size() == 1); +} + +TEST_CASE("a cache rejected mid-body leaves the error count where it found it", "[VendorCache]") +{ + InstallDirs dirs; + + // A vendor whose root profile counts a parse error, so the bundle carries a + // non-zero tally into the load below. Without one there is nothing for a + // rejected cache to zero, and nothing to underflow. + std::ofstream((dirs.system / "Noisy.json").string()) + << R"({"version":"1.0.0","name":"Noisy","process_list":"not a list"})"; + + write_vendor_tree(dirs.system, "Counted", "1.0.0"); + // A cache that passes every stamp and then dies in the entries. + REQUIRE(save_one_vendor((dirs.system / "Counted.opc").string(), one_vendor("Counted"), "Counted", "1.0.0", + {filament_entry("Counted PLA @0.4")})); + truncate_payload_and_fix_header((dirs.system / "Counted.opc").string(), 8); + + PresetBundle bundle; + bundle.set_generate_vendor_caches(true); + bundle.load_vendor_configs_from_json(dirs.system.string(), "Noisy", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(bundle.error_count() > 0); + + // The same bundle: the cache is tried, fails mid-body, and the parse that + // follows must be measured against the tally the cache found rather than + // against zero. + REQUIRE(bundle.load_vendor_configs_from_json(dirs.system.string(), "Counted", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent).second > 0); + + // The rewritten cache must carry the parse's own error count, not an + // underflowed one. Reload it and check the bundle does not inherit a + // nonsensical tally. + PresetBundle reloaded; + REQUIRE(reloaded.load_vendor_cache((dirs.system / "Counted.opc").string(), "Counted", Semver(1, 0, 0))); + CHECK(reloaded.error_count() == 0); +} + +TEST_CASE("a preset is traced to its vendor in a build that ships caches alone", "[VendorCache]") +{ + InstallDirs dirs; + + std::vector filaments { filament_entry("Cached PLA @0.4") }; + std::vector printers { printer_entry("Cached 0.4 nozzle") }; + REQUIRE(save_one_vendor((dirs.profiles / "Cached.opc").string(), one_vendor("Cached"), "Cached", "1.0.0", + filaments, printers)); + + CHECK(PresetBundle::find_preset_vendor("Cached PLA @0.4", Preset::TYPE_FILAMENT) == "Cached"); + CHECK(PresetBundle::find_preset_vendor("Cached 0.4 nozzle", Preset::TYPE_PRINTER) == "Cached"); + CHECK(PresetBundle::find_preset_vendor("Nobody's PLA", Preset::TYPE_FILAMENT).empty()); +} + +TEST_CASE("a bundle that cannot be installed does not drop the others", "[VendorCache]") +{ + InstallDirs dirs; + + write_vendor_tree(dirs.profiles, "Good", "1.0.0"); + + // An empty name sorts first out of a std::map, and a name resources does not + // carry can appear anywhere. Neither may cost the batch the vendors it can + // install. + CHECK_FALSE(install_vendor_bundles_from_resources({"", "Absent", "Good"})); + CHECK(fs::exists(dirs.system / "Good.json")); +} + +TEST_CASE("a cache that arrives unusable leaves the profile fallback in place", "[VendorCache]") +{ + InstallDirs dirs; + + write_vendor_tree(dirs.profiles, "Torn", "1.0.0"); + const std::string cache = (dirs.profiles / "Torn.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Torn"), "Torn", "1.0.0")); + // Past the stamps at the front, so the 1 KB peek that chooses the cache form + // still succeeds — only the CRC, which decides whether it can be served, + // catches this. + corrupt_blob_byte(cache, std::streamoff(fs::file_size(cache)) - 4); + REQUIRE(VendorCacheFile::peek_version(cache, "Torn") == "1.0.0"); + + CHECK(install_vendor_bundles_from_resources({"Torn"})); + CHECK(fs::exists(dirs.system / "Torn.json")); + CHECK_FALSE(fs::exists(dirs.system / "Torn.opc")); +} + +TEST_CASE("a vendor installed as an unreadable cache alone counts as not installed", "[VendorCache]") +{ + InstallDirs dirs; + + const std::string cache = (dirs.system / "Broken.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Broken"), "Broken", "1.0.0")); + REQUIRE(is_vendor_installed("Broken")); + + // A cache this build cannot serve is not an installation: there is no + // profile beside it and, since the single-tier load, nowhere else to load + // the vendor from. + corrupt_blob_byte(cache); + CHECK_FALSE(is_vendor_installed("Broken")); + CHECK_FALSE(installed_vendor_version("Broken").valid()); +} + +TEST_CASE("a stale profile beside a newer cache does not hide the cache's version", "[VendorCache]") +{ + InstallDirs dirs; + + write_vendor_json(dirs.system, "Both", "1.0.0"); + REQUIRE(save_one_vendor((dirs.system / "Both.opc").string(), one_vendor("Both"), "Both", "2.0.0")); + + // The cache covers the profile, so the cache is what a load serves — and + // 2.0.0 is the version installed, not the 1.0.0 the profile still claims. + CHECK(installed_vendor_version("Both") == Semver(2, 0, 0)); +} + +TEST_CASE("a profile newer than the cache beside it is the installed version", "[VendorCache]") +{ + InstallDirs dirs; + + write_vendor_json(dirs.system, "Both", "3.0.0"); + REQUIRE(save_one_vendor((dirs.system / "Both.opc").string(), one_vendor("Both"), "Both", "2.0.0")); + + // The cache no longer covers the profile, so the profile is parsed — and + // its version is the one in force. + CHECK(installed_vendor_version("Both") == Semver(3, 0, 0)); +} + +TEST_CASE("a header claiming more body than the file holds is rejected", "[VendorCache]") +{ + TempDir tmp; + const std::string cache = (tmp.path / "Bounded.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Bounded"), "Bounded", "1.0.0")); + + // Claim a body far larger than the file. Nothing may be allocated on the + // strength of that number. + { + std::fstream f(cache, std::ios::in | std::ios::out | std::ios::binary); + const uint64_t huge = 400ull * 1024ull * 1024ull; + f.seekp(8); + f.write(reinterpret_cast(&huge), sizeof(huge)); + } + + PresetBundle bundle; + REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0))); +} + +TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]") +{ + TempDir tmp; + const std::string cache = (tmp.path / "Durable.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0")); + const std::string before = slurp(cache); + + // A directory where the temp file wants to go: the write cannot complete, + // and must not have destroyed what was already there to find that out. + const fs::path blocker = fs::path(cache + "." + std::to_string(get_current_pid()) + ".tmp"); + fs::create_directories(blocker); + + REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0")); + CHECK(slurp(cache) == before); + + fs::remove_all(blocker); +} + +TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]") +{ + // The regression the fingerprint used to prevent by refusing the file + // outright: nothing in the payload depends on serialization_key_ordinal, so + // a build that inserted an option ahead of these reads them back correctly. + TempDir tmp; + const std::string cache = (tmp.path / "Ordinal.opc").string(); + + auto e = filament_entry("Ordinal PLA @0.4"); + e.config_src.set_key_value("filament_cost", new ConfigOptionFloats({42.})); + e.config_src.set_key_value("filament_type", new ConfigOptionStrings({"PLA"})); + REQUIRE(save_one_vendor(cache, one_vendor("Ordinal"), "Ordinal", "1.0.0", {e})); + + PresetBundle bundle; + REQUIRE(bundle.load_vendor_cache(cache, "Ordinal", Semver(1, 0, 0))); + const auto filaments = presets_for(bundle.filaments, "Ordinal"); + REQUIRE(filaments.size() == 1); + const auto* cost = filaments.front()->config.option("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); + CHECK(filaments.front()->config.option("filament_type")->values.front() == "PLA"); +} + +// ---- CacheDictionary and the name-keyed config payload ------------------- + +namespace { + +// Round-trip one config through the dictionary payload, optionally mutating the +// dictionary between write and read to stand in for another build's schema. +DynamicPrintConfig roundtrip_config(const DynamicPrintConfig& in, + const std::function& mutate_blob = {}) +{ + CacheDictionary wdict; + wdict.collect(in); + std::ostringstream os(std::ios::binary); + { + cereal::BinaryOutputArchive ar(os); + wdict.save(ar); + save_config(ar, in, wdict); + } + std::string blob = os.str(); + if (mutate_blob) + mutate_blob(blob); + std::istringstream is(blob, std::ios::binary); + cereal::BinaryInputArchive ar(is); + CacheDictionary rdict; + rdict.load(ar); + DynamicPrintConfig out; + load_config(ar, out, rdict); + return out; +} + +} // namespace + +TEST_CASE("a config round-trips through the cache dictionary", "[VendorCache]") +{ + DynamicPrintConfig in; + in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); + in.set_key_value("printer_model", new ConfigOptionString("Test Model")); + in.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.6})); + in.set_key_value("spiral_mode", new ConfigOptionBool(true)); + + const DynamicPrintConfig out = roundtrip_config(in); + + CHECK_THAT(out.opt_float("layer_height"), WithinAbs(0.28, 1e-9)); + CHECK(out.opt_string("printer_model") == "Test Model"); + REQUIRE(out.option("nozzle_diameter") != nullptr); + CHECK(out.option("nozzle_diameter")->values.size() == 2); + CHECK(out.opt_bool("spiral_mode") == true); +} + +TEST_CASE("an enum option round-trips by name, not by index", "[VendorCache]") +{ + // top_surface_pattern is a coEnum; its stored int is an index into an enum + // whose order is not a wire contract. Assert on the NAME, so a reordering + // of the enum in PrintConfig.cpp cannot make this test pass by accident. + const ConfigOptionDef* def = print_config_def.get("top_surface_pattern"); + REQUIRE(def != nullptr); + REQUIRE(def->type == coEnum); + REQUIRE(def->enum_keys_map != nullptr); + const int monotonic = def->enum_keys_map->at("monotonic"); + + DynamicPrintConfig in; + in.set_key_value("top_surface_pattern", new ConfigOptionEnumGeneric(def->enum_keys_map, monotonic)); + + const DynamicPrintConfig out = roundtrip_config(in); + REQUIRE(out.option("top_surface_pattern") != nullptr); + CHECK(out.opt_enum("top_surface_pattern") == InfillPattern(monotonic)); + CHECK(out.option("top_surface_pattern")->serialize() == "monotonic"); +} + +TEST_CASE("a nullable vector enum round-trips by name, nil included", "[VendorCache]") +{ + // coEnums carries a vector of ints and, unlike coEnum, its ConfigOptionType + // does not fit in a byte - a truncated type in the dictionary would make a + // reader take this for a scalar enum and run off the end of the stream. + // nozzle_type is also nullable, and nil is an int no enum_keys_map names, + // so this covers the dictionary's unnamed-value escape hatch too. + const ConfigOptionDef* def = print_config_def.get("nozzle_type"); + REQUIRE(def != nullptr); + REQUIRE(def->type == coEnums); + REQUIRE(def->nullable); + REQUIRE(def->enum_keys_map != nullptr); + const int brass = def->enum_keys_map->at("brass"); + const int nil = ConfigOptionInts::nil_value(); + + DynamicPrintConfig in; + auto* opt = new ConfigOptionEnumsGenericNullable(def->enum_keys_map); + opt->values = { brass, nil }; + in.set_key_value("nozzle_type", opt); + in.set_key_value("printer_model", new ConfigOptionString("Test Model")); + + const DynamicPrintConfig out = roundtrip_config(in); + const auto* got = out.option("nozzle_type"); + REQUIRE(got != nullptr); + CHECK(got->values == std::vector{brass, nil}); + CHECK(out.opt_string("printer_model") == "Test Model"); +} + +TEST_CASE("an option the build no longer knows is dropped, and the rest still load", "[VendorCache]") +{ + DynamicPrintConfig in; + in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); + in.set_key_value("printer_model", new ConfigOptionString("Test Model")); + + // Rename the key in the dictionary the reader sees: "layer_height" becomes + // "layer_heighX", a key no build defines. Same length, so the blob's + // offsets are untouched - this is exactly what a removed or renamed option + // looks like to a reader. + const DynamicPrintConfig out = roundtrip_config(in, [](std::string& blob) { + const size_t at = blob.find("layer_height"); + REQUIRE(at != std::string::npos); + blob[at + 11] = 'X'; + }); + + CHECK(out.option("layer_height") == nullptr); + CHECK(out.opt_string("printer_model") == "Test Model"); +} + +TEST_CASE("an option whose type changed is dropped, and the rest still load", "[VendorCache]") +{ + // A payload from a build where layer_height was a coString. This one has it + // as a coFloat, so nothing can be done with the value - but the dictionary + // says how it was written, so its bytes are still consumed and printer_model + // behind it still lands. Hand-written rather than round-tripped: only a + // dictionary this build did not produce can disagree with it. + std::ostringstream os(std::ios::binary); + { + cereal::BinaryOutputArchive ar(os); + const std::vector keys { "layer_height", "printer_model" }; + const std::vector types { uint16_t(coString), uint16_t(coString) }; + const std::vector enums { std::string() }; // the ENUM_UNNAMED slot + ar(keys, types, enums); + ar(uint32_t(2)); + ar(uint16_t(0)); ar(ConfigOptionString("0.28")); + ar(uint16_t(1)); ar(ConfigOptionString("Test Model")); + } + + std::istringstream is(os.str(), std::ios::binary); + cereal::BinaryInputArchive ar(is); + CacheDictionary rdict; + rdict.load(ar); + DynamicPrintConfig out; + REQUIRE_NOTHROW(load_config(ar, out, rdict)); + CHECK(out.option("layer_height") == nullptr); + CHECK(out.opt_string("printer_model") == "Test Model"); +} + +TEST_CASE("skip_config consumes a config without building one", "[VendorCache]") +{ + DynamicPrintConfig in; + in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); + in.set_key_value("printer_model", new ConfigOptionString("Test Model")); + + CacheDictionary wdict; + wdict.collect(in); + std::ostringstream os(std::ios::binary); + { + cereal::BinaryOutputArchive ar(os); + wdict.save(ar); + save_config(ar, in, wdict); + ar(std::string("sentinel")); // must still be reachable after the skip + } + + std::istringstream is(os.str(), std::ios::binary); + cereal::BinaryInputArchive ar(is); + CacheDictionary rdict; + rdict.load(ar); + skip_config(ar, rdict); + std::string sentinel; + ar(sentinel); + CHECK(sentinel == "sentinel"); +} + +TEST_CASE("a dictionary index past the end of the table is refused", "[VendorCache]") +{ + DynamicPrintConfig in; + in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); + + CacheDictionary wdict; + wdict.collect(in); + std::ostringstream os(std::ios::binary); + { + cereal::BinaryOutputArchive ar(os); + wdict.save(ar); + save_config(ar, in, wdict); + } + std::string blob = os.str(); + // The payload's tail is the option count (uint32), the key index (uint16) + // and the double. Point the key index somewhere the table does not go. + const uint16_t bad = 0xFFFE; + std::memcpy(&blob[blob.size() - sizeof(double) - sizeof(uint16_t)], &bad, sizeof(bad)); + + std::istringstream is(blob, std::ios::binary); + cereal::BinaryInputArchive ar(is); + CacheDictionary rdict; + rdict.load(ar); + DynamicPrintConfig out; + REQUIRE_THROWS(load_config(ar, out, rdict)); +} + +TEST_CASE("a stamp string with an absurd length is rejected, not allocated", "[VendorCache]") +{ + // The stamps are read from whatever .opc a directory holds, and a + // string resize to a garbage 64-bit length does not fail as a catchable + // bad_alloc — it takes the app down through the out-of-memory handler. A + // CRC-valid body opening with the right cache version but foreign framing + // where the name's length word sits must be refused before anything is + // allocated. + TempDir tmp; + const std::string cache = (tmp.path / "Evil.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Evil"), "Evil", "1.0.0")); + + // The vendor name's length word sits right behind the payload's version + // word; make it claim a ~9-exabyte name. + const uint64_t huge = 0x7FFFFFFFFFFFFFFFull; + patch_payload_bytes(cache, sizeof(uint32_t), &huge, sizeof(huge)); + + PresetBundle out; + REQUIRE(! out.load_vendor_cache(cache, "Evil", Semver::inf())); + CHECK(out.vendors.empty()); + CHECK(VendorCacheFile::peek_version(cache, "Evil").empty()); +} +