diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml
index ab4cd57f5f..b849979de0 100644
--- a/.github/workflows/build_all.yml
+++ b/.github/workflows/build_all.yml
@@ -5,7 +5,6 @@ on:
branches:
- main
- release/*
- - belt-printer
paths:
- 'deps/**'
- 'src/**'
@@ -262,9 +261,6 @@ jobs:
date:
ver:
ver_pure:
- # Belt-printer nightlies share the main nightly release but carry a `_belt`
- # suffix so they never overwrite the main assets.
- nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: "Remove unneeded stuff to free disk space"
run:
@@ -439,13 +435,13 @@ jobs:
name: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
- name: Deploy Flatpak to nightly release
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer')
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
- asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
+ asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
# The asset is /app (the exes link it at runtime) plus the build tree
diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml
index 7b001a8c48..7115efab09 100644
--- a/.github/workflows/build_orca.yml
+++ b/.github/workflows/build_orca.yml
@@ -33,11 +33,6 @@ jobs:
ubuntu-ver: '2404'
ubuntu-ver-str: '_Ubuntu2404'
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
- # Branches whose builds are published to the nightly release. The
- # belt-printer branch ships alongside main but its assets carry a `_belt`
- # suffix (nightly_suffix) so they never overwrite the main nightly assets.
- deploy_nightly: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' }}
- nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: Checkout
@@ -260,7 +255,7 @@ jobs:
# Thanks to RaySajuuk, it's working now
- name: Sign app and notary
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
timeout-minutes: 30
working-directory: ${{ github.workspace }}
env:
@@ -348,7 +343,7 @@ jobs:
fi
- name: Create DMG without notary
- if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/belt-printer' && runner.os == 'macOS' && inputs.macos-combine-only
+ if: github.ref != 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
@@ -394,13 +389,13 @@ jobs:
if-no-files-found: ignore
- name: Deploy Mac release
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
- asset_name: OrcaSlicer_Mac_universal_nightly${{ env.nightly_suffix }}.dmg
+ asset_name: OrcaSlicer_Mac_universal_nightly.dmg
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
@@ -543,24 +538,24 @@ jobs:
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Deploy Windows release portable
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip
- asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}_portable.zip
+ asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly_portable.zip
asset_content_type: application/x-zip-compressed
max_releases: 1
- name: Deploy Windows release installer
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
- asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}.exe
+ asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly.exe
asset_content_type: application/x-msdownload
max_releases: 1
@@ -701,13 +696,13 @@ jobs:
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release
- if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
+ if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
- asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly${{ env.nightly_suffix }}.AppImage
+ asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly.AppImage
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
- name: Deploy Ubuntu release
diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml
index 2a6ed8f0a4..422513442e 100644
--- a/.github/workflows/check_profiles.yml
+++ b/.github/workflows/check_profiles.yml
@@ -74,12 +74,26 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]}
+ # The validator above is the nightly build of main, so it cannot slice profiles that use
+ # settings a PR adds to the engine: it reports their placeholders as undefined. A PR that
+ # changes src/ also runs Build all, whose Slice check runs this same sweep with the
+ # validator built from the PR, so the sweep below only runs for the other PRs.
+ - name: Detect engine changes
+ id: engine_changes
+ if: ${{ github.event_name == 'pull_request' }}
+ run: |
+ base=${{ github.event.pull_request.base.sha }}
+ if git fetch --no-tags --depth=1 origin "$base" && ! git diff --quiet "$base" HEAD -- src/; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ echo "::notice::This PR changes src/, so Build all's Slice check slices the profiles with the PR-built validator."
+ fi
# Slice a two-colour cube through every printer, and through every system process/filament whose
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
# invalid-flow bugs the static checks above cannot see.
- name: validate slice (expand custom g-code)
id: validate_slice
+ if: ${{ steps.engine_changes.outputs.changed != 'true' }}
continue-on-error: true
run: |
set +e
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2bfb01a657..a208c5e7a2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1084,10 +1084,10 @@ endif ()
find_path(SPNAV_INCLUDE_DIR spnav.h)
if (SPNAV_INCLUDE_DIR)
- find_library(SPNAV_LIB NAMES libspnav.a) # Force linking libspnav statically
+ find_library(SPNAV_LIB NAMES libspnav.a spnav)
if (SPNAV_LIB)
add_definitions(-DHAVE_SPNAV)
- message(STATUS "SPNAV library found")
+ message(STATUS "SPNAV library found: ${SPNAV_LIB}")
else()
message(STATUS "SPNAV library NOT found, Spacenavd not supported")
endif()
diff --git a/README.md b/README.md
index 40ba7637b9..408efb57b7 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,8 @@ If you come across any of these in search results, please report them as
Regular updates fueled by continuous community contributions.
- **Wide Printer Compatibility**
Supports a broad range of printers: Bambu Lab, Prusa, Creality, Voron, and more.
+- **[Belt Printer Support](https://www.orcaslicer.com/wiki/belt_printing)**
+ Slice for belt / conveyor (infinite-Z) printers, with belt-aware supports and a tilted-bed preview. Contributed by [Joseph Robertson (@HarrierPigeon)](https://github.com/HarrierPigeon).
- Additional features can be found in the [change notes](https://github.com/OrcaSlicer/OrcaSlicer/releases/).
# Wiki
@@ -89,17 +91,6 @@ Visit our GitHub Releases page for the latest stable version of OrcaSlicer, reco
🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)**
Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated.
-### Belt Printer Builds
-
-The [nightly release](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds) ships **two parallel builds**: the standard build and a belt-printer build. Both are attached to the same release — tell them apart by the filename suffix:
-
-- **Standard** — no suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly.exe`)
-- **Belt** — `_belt` suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly_belt.exe`)
-
-The `_belt` builds add **experimental support for belt / conveyor (infinite-Z) printers**, where the model is sliced against a tilted belt surface instead of a flat horizontal bed. They include ready-to-use belt printer profiles, the full belt slicing pipeline (mesh rotation and G-code transforms), belt-aware support generation, and a tilted-bed preview.
-
-> ⚠️ Belt printer support is under active development and is **not yet merged into `main`** — it currently ships only in these parallel `_belt` builds, produced from the [`belt-printer`](https://github.com/OrcaSlicer/OrcaSlicer/tree/belt-printer) branch. See tracking PR [#14394](https://github.com/OrcaSlicer/OrcaSlicer/pull/14394) and the original documentation in [#12998](https://github.com/OrcaSlicer/OrcaSlicer/pull/12998).
-
# How to install
## Windows
diff --git a/build_release_macos.sh b/build_release_macos.sh
index db03cb4ecd..728cb8e757 100755
--- a/build_release_macos.sh
+++ b/build_release_macos.sh
@@ -104,8 +104,8 @@ fi
CMAKE_VERSION=$(cmake --version | head -1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
if [ "$CMAKE_VERSION" -ge 4 ] 2>/dev/null; then
- export CMAKE_POLICY_VERSION_MINIMUM=3.5
- export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
+ export CMAKE_POLICY_VERSION_MINIMUM=3.10
+ export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.10"
echo "Detected CMake 4.x, adding compatibility flag (env + cmake arg)"
else
export CMAKE_POLICY_COMPAT=""
diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt
index aa64ad4dc9..eda0c67e94 100644
--- a/deps/CMakeLists.txt
+++ b/deps/CMakeLists.txt
@@ -1,5 +1,5 @@
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0")
- set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
+ set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "" FORCE)
endif()
#
@@ -24,7 +24,7 @@ endif()
# therefore, unfortunately, the installation cannot be copied/moved elsewhere without re-installing wxWidgets.
#
-cmake_minimum_required(VERSION 3.2)
+cmake_minimum_required(VERSION 3.10)
if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
@@ -224,7 +224,7 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
${_source_dir_arg}
${_gen}
CMAKE_ARGS
- -DCMAKE_POLICY_VERSION_MINIMUM=3.5
+ -DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_MODULE_PATH:STRING=${PROJECT_SOURCE_DIR}/../cmake/modules
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
@@ -279,7 +279,7 @@ else()
${_source_dir_arg}
${_gen}
CMAKE_ARGS
- -DCMAKE_POLICY_VERSION_MINIMUM=3.5
+ -DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
diff --git a/deps/OCCT/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch b/deps/OCCT/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch
new file mode 100644
index 0000000000..84c599a5f3
--- /dev/null
+++ b/deps/OCCT/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch
@@ -0,0 +1,28 @@
+diff --git a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
+index 6f63781..9b1c08e 100644
+--- a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
++++ b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
+@@ -210,10 +210,10 @@ private:
+
+ // Define two pcurves of the seam-edge.
+ occ::handle aPC1, aPC2;
+- double af, al;
++ double af, al, af1, al1;
+
+ aE.Orientation(TopAbs_FORWARD);
+- aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
++ aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af1, al1);
+
+ aE.Orientation(TopAbs_REVERSED);
+ aPC2 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
+@@ -224,7 +224,9 @@ private:
+ }
+
+ // Select the correct pcurve of the seam-edge.
+- const gp_Pnt2d& aFPntOfPC1 = aPC1->Value(aPC1->FirstParameter());
++ // Use the edge's first parameter. A Geom2d_Line's FirstParameter() is -Precision::Infinite(),
++ // where a direction of (2e-16, -1) from rounding error gives an X far outside the U range.
++ const gp_Pnt2d aFPntOfPC1 = aPC1->Value(af1);
+
+ if (std::abs(aLPntOfIPC1.X() - aFPntOfPC1.X()) > Precision::Confusion())
+ {
diff --git a/deps/OCCT/OCCT.cmake b/deps/OCCT/OCCT.cmake
index 4cab60ec6a..0466d8e868 100644
--- a/deps/OCCT/OCCT.cmake
+++ b/deps/OCCT/OCCT.cmake
@@ -25,9 +25,16 @@ endif()
# shipped bytes. Windows ships only the DLLs libslic3r links, so the tab adds the TKFillet,
# TKOffset and TKBool DLLs. See docs/HLSD/design-tab.md.
+if (IN_GIT_REPO)
+ set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
+endif ()
+
orcaslicer_add_cmake_project(OCCT
URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V8_0_1.zip
URL_HASH SHA256=7c033d917ee8f040c0512d289dcc5f02c148889d5bac17c3e25639accb44f0da
+ # Makes BRepMesh triangulate cone faces whose seam pcurve is slightly tilted
+ # (Open-Cascade-SAS/OCCT#572); remove the patch once an OCCT release includes the fix.
+ PATCH_COMMAND git apply ${OCCT_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch
#DEPENDS dep_Boost
DEPENDS ${FREETYPE_PKG}
CMAKE_ARGS
diff --git a/deps/OpenSSL/OpenSSL.cmake b/deps/OpenSSL/OpenSSL.cmake
index 2bbf2b81b7..35b17ed50e 100644
--- a/deps/OpenSSL/OpenSSL.cmake
+++ b/deps/OpenSSL/OpenSSL.cmake
@@ -44,6 +44,18 @@ else()
if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
else()
+ # A static library that is embedded into a shared object must not export
+ # its symbols. On Linux the running process also loads the system OpenSSL
+ # 3.x (WebKitGTK/gnutls pull in libcrypto.so.3), and CPython's _ssl and
+ # _hashlib are dlopened (RTLD_LOCAL) DSOs that each embed this OpenSSL.
+ # With default visibility their unversioned OpenSSL references are
+ # preempted by that global 3.x copy, mixing the 1.1.1 and 3.x ABIs and
+ # corrupting the heap (ssl.create_default_context() aborts). Hidden
+ # visibility makes each embedded copy self-contained. Linux-only: macOS
+ # binds dylibs with a two-level namespace (no interposition) and ships no
+ # OpenSSL, and Windows has no equivalent flag and no system OpenSSL to
+ # collide with.
+ set(_openssl_extra_cflags -fvisibility=hidden)
set(_conf_cmd env "CC=${CMAKE_C_COMPILER}" "LDFLAGS=${CMAKE_EXE_LINKER_FLAGS}" "./config")
endif()
set(_cross_comp_prefix_line "")
@@ -102,3 +114,20 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
COMMAND ${CMAKE_COMMAND} -E copy_directory openssl "${DESTDIR}${CMAKE_INSTALL_LIBDIR}/cmake/openssl"
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
)
+
+if (NOT WIN32 AND NOT APPLE)
+ # OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for
+ # example to add -fvisibility=hidden) relinks the archives from stale
+ # objects instead of recompiling them, and the change silently has no
+ # effect. Drop the objects whenever this recipe changes so the next build
+ # actually recompiles them.
+ ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
+ ExternalProject_Add_Step(dep_OpenSSL clean_objects
+ DEPENDEES configure
+ DEPENDERS build
+ COMMAND make clean
+ WORKING_DIRECTORY "${SOURCE_DIR}"
+ DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
+ COMMENT "OpenSSL: cleaning objects after a recipe change"
+ )
+endif ()
diff --git a/deps/python3/python3.cmake b/deps/python3/python3.cmake
index 6b4443ef3a..b0415f2bb5 100644
--- a/deps/python3/python3.cmake
+++ b/deps/python3/python3.cmake
@@ -299,3 +299,27 @@ endif()
if(TARGET dep_ZLIB)
add_dependencies(dep_python3 dep_ZLIB)
endif()
+
+if (NOT WIN32 AND NOT APPLE)
+ # CPython's Makefile rules for _ssl and _hashlib depend only on their own
+ # sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make
+ # them relink and they keep the previous symbols. On an incremental tree,
+ # drop the built modules and relink them against the current OpenSSL; a
+ # fresh build is left alone (its PGO target builds them). "make" alone is a
+ # no-op once PGO has run, so sharedmods is invoked explicitly.
+ ExternalProject_Get_Property(dep_python3 SOURCE_DIR)
+ file(GLOB _python_ssl_modules
+ "${SOURCE_DIR}/Modules/_ssl*.so"
+ "${SOURCE_DIR}/Modules/_hashlib*.so")
+ if (_python_ssl_modules)
+ ExternalProject_Add_Step(dep_python3 relink_ssl_extensions
+ DEPENDEES configure
+ DEPENDERS build
+ COMMAND sh -c "rm -f '${SOURCE_DIR}'/Modules/_ssl*.so '${SOURCE_DIR}'/Modules/_hashlib*.so && make -j${NPROC} sharedmods"
+ WORKING_DIRECTORY "${SOURCE_DIR}"
+ COMMENT "CPython: relinking _ssl/_hashlib against the current OpenSSL"
+ DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
+ "${CMAKE_CURRENT_LIST_DIR}/../OpenSSL/OpenSSL.cmake"
+ )
+ endif ()
+endif ()
diff --git a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp
index dc7733ebd6..62d231e375 100644
--- a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp
+++ b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp
@@ -88,6 +88,18 @@ struct NfpPConfig {
*/
bool explore_holes = false;
+ /**
+ * @brief Keep the final pile on the bin.
+ *
+ * The final alignment centres the pile on the alignment target. A target
+ * near an edge (a belt printer starts its parts at the leading end of the
+ * belt) would push part of a pile that is larger than the room around that
+ * point off the bed; with this set the pile stops at the edge instead, and a
+ * pile that does not fit along an axis is centred on it. Off by default, so
+ * the alignment of every other printer is unchanged.
+ */
+ bool clamp_to_bin = false;
+
/**
* @brief If true, use all CPUs available. Run on a single core otherwise.
*/
@@ -1111,7 +1123,24 @@ private:
default: ; // DONT_ALIGN
}
- auto d = cb - ci;
+ auto d = cb - ci;
+
+ // Keep the pile on the bin (see Config::clamp_to_bin). The items' boxes carry
+ // their inflation, which is the margin left at the edge.
+ if (config_.clamp_to_bin) {
+ auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) {
+ if (hi - lo >= bin_hi - bin_lo)
+ return (bin_lo + bin_hi) / 2 - (lo + hi) / 2;
+ if (lo + shift < bin_lo)
+ shift = bin_lo - lo;
+ if (hi + shift > bin_hi)
+ shift = bin_hi - hi;
+ return shift;
+ };
+ setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d)));
+ setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d)));
+ cb = ci + d;
+ }
// BBS make sure the item won't clash with excluded regions
// do we have wipe tower after arranging?
diff --git a/docs/HLSD/adaptive-tpms-infill.md b/docs/HLSD/adaptive-tpms-infill.md
new file mode 100644
index 0000000000..5fc578f7e2
--- /dev/null
+++ b/docs/HLSD/adaptive-tpms-infill.md
@@ -0,0 +1,221 @@
+# Adaptive TPMS infill — High Level Design
+
+## Purpose and scope
+
+`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
+patterns inside the object: the cells grow continuously from the surface
+towards the center of the object. `distance_warp`, `smooth_blend` and
+`stepped_shells` follow the distance to the nearest surface, including the top
+and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
+the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
+follow each section of the object normal to that axis, so the grading does not
+change along the axis, as suits a profile extruded along it.
+`sparse_infill_density` is the density at the surface, `tpms_interior_density`
+the density at the center, and `tpms_adaptive_gradient` picks how the density
+goes from one to the other. Only internal sparse infill is graded; the Gyroid
+Z-buckling optimization does not apply to it.
+
+The design has two parts: a field built once per object, and a pattern made
+from it, warped around the center of each lobe of a body so that its cell size
+follows the field, or, in the modes following the distance to the surface,
+split into shells or blended between densities.
+
+## Radial field
+
+`TpmsRadialField` gives every point of an object the center of its lobe and a
+radial coordinate: 0 at the center, 1 at the surface along the ray from the
+center. `PrintObject::prepare_tpms_radial_fields()` builds it in
+`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
+infill generated there has to match the printed infill. A field is built for
+every mode a region uses, and is shared by the regions using that mode: the
+field depends on the geometry only, the densities are applied per region in the
+fill. An object thinner than the grid cells has no body in the field; no field
+is kept then, and the infill falls back to the regular pattern. In the modes
+following the distance to the surface, the field also gives the depth of every
+point (see below).
+
+A regular 3D grid of cubic cells is rasterized from the `lslices` of the layers,
+so the field follows what is printed: negative volumes, the union of
+overlapping parts and holes are taken into account, and the mesh does not need
+to be closed. A padding node around the grid is always outside. The grid is
+capped at about a million nodes, with cells no smaller than 0.5 mm.
+
+- Bodies are the connected inside nodes. Each is graded on its own, so separate
+ parts of one object each get their own sparse center.
+- A body is split into lobes around the local maxima of the depth, by an exact
+ Euclidean distance transform (Felzenszwalb and Huttenlocher, one pass per
+ axis). Two maxima are in separate lobes when the depth along the segment
+ between them drops below 0.8 of the shallower one, like at the neck between
+ two united spheres; maxima shallower than 0.3 of the deepest one are ignored.
+ A maximum joins the first lobe whose first maximum it sees without a neck.
+ The lobes are made one at a time, the remaining maxima tested against the
+ first one in parallel, as a plate has a whole plane of them.
+ Where the depth ties along a line or a plane, as in a tall box, the lobe's
+ center is the node nearest to the middle of the tied nodes, so the center is
+ in the middle of the height and not a column.
+- A point belongs to the lobe it is nearest to relative to their depths, so the
+ side between two lobes is nearer to the smaller one. Near that side, within a
+ tenth of that relative distance, the patterns of the lobes morph into each
+ other, so the lines stay continuous. Every lobe in that range takes part, up
+ to four, so the morph is also continuous where three or four lobes meet.
+- The reach of a lobe is the distance from its center to the first exit along
+ 24 x 48 latitude-longitude directions, smoothed twice over neighbouring
+ directions in log space. Towards a neighbouring lobe it stops at twice the
+ distance to the side between them, so that side is graded half way, as deep
+ as a neck is, rather than as sparse as the center or as dense as the surface.
+ Only the lobes whose centers are near enough to be nearer at the current
+ distance are compared along a ray, so many lobes, as in a perforated plate,
+ stay cheap.
+ The radial coordinate of a point is its distance to the center over the reach
+ in its direction. Behind a gap, as across the hole
+ of a ring, the radial coordinate is above 1 and the infill keeps the surface
+ density.
+- Every outside node belongs to its nearest body, so points near a surface find
+ their body without a search. With a single body, all nodes belong to it.
+
+In the 2D modes, every plane of nodes normal to the axis is a field of its own:
+the distance transform skips the axis, bodies, lobes and the nearest body are
+found within the plane, and the reach is sampled on a circle of 48 directions.
+A point is looked up in the two planes around it, the weights of their lobes
+interpolated along the axis, so the grading does not step between planes; a
+plane without a body uses the nearest one that has one. The planes are a cell
+apart, not a layer: where the sections change abruptly, as at a step, the
+patterns of the two planes morph into each other over that cell.
+
+A distance to the nearest surface would be the obvious field, but no smooth map
+follows it. By the divergence theorem, the mean scale of a map over a body is
+fixed by its values on the surface: a map that keeps the full density along the
+whole surface, as the distance would ask under the top and bottom, has the mean
+density of the uniform infill, the sparser core being paid for by lines crowding
+along the walls. The layers of a plate at different depths would also need
+different line spacings in the same directions, which no continuous map allows
+without shearing across the plate. Following the distance needs changes of the
+topology of the lattice (see below). The radial coordinate instead grades what a
+single map can: towards one point.
+
+## Warped pattern
+
+The pattern is evaluated on warped coordinates:
+
+ TPMS(f_surface * m(t) * (p - center))
+
+where `m` scales the pattern around the center of the lobe: its frequency is
+`m + t * m'` along the ray and `m` across it. `m(t)` is the mean of the target
+scale over the ball of radius `t`, `3 / t^3 * integral of s^2 * target(s) ds`, so
+the mean of the three, and with it the density, follows the gradient. The cells
+are round at the center; near the surface they are flattened, with the lines
+running parallel to it. Beyond the surface the target is the surface scale, so
+the warp extends continuously outside.
+
+In the 2D modes only the coordinates within the plane are warped, and `m(t)` is
+the mean over the disc, `2 / t^2 * integral of s * target(s) ds`. Along the axis
+the pattern keeps the interior frequency: scaling it with `m` would shear the
+pattern by the distance along the axis times the gradient of `m`, without bound
+on a long object. The cells are round at the center and stretched along the
+axis near the surface. With Normal Z the layers are graded exactly, since
+the lines of a layer follow its in-plane frequencies; normal to X or Y, the
+layers near the sides are as dense as the larger of the two frequencies in the
+layer, which is the surface one.
+
+Evaluating a TPMS at a frequency that varies with the position without such a
+map distorts it wherever the frequency changes, because the phase also changes
+with the gradient of the frequency times the distance from the origin. Fitting a
+smooth map to a varying isotropic scale in the least-squares sense (a Poisson
+problem per axis) cannot grade strongly: its divergence is the target scale plus
+a harmonic function pinned by the surface, which keeps the scale in the core
+near two thirds of the surface one. Following a distance exactly needs the
+lattice to change its topology, by blending lattices of different densities or
+filling shells of equal distance with them, as the modes following the distance
+to the surface do.
+
+The target scale at depth `d = 1 - t`, with `S` the surface and `I` the interior
+frequency, both from each pattern's own density calibration:
+
+| Gradient | Scale |
+|-------------|------------------------|
+| Linear | `1 + (I / S - 1) * d` |
+| Quadratic | `1 + (I / S - 1) * d^2`|
+| Exponential | `(I / S)^d` |
+
+With a denser surface, quadratic keeps the surface density deepest and
+exponential drops fastest. A denser interior works the same way.
+
+The zero level is extracted with marching squares like the regular TPMS-FK, on
+a sampling grid fixed in the fill frame like the optimized Gyroid, so that every
+region of a layer connects its lines the same way at the saddles of the pattern.
+Loops narrower than two lines (shorter than `2 * PI * spacing`) are dropped, as
+they would print as blobs. The fill works in a frame rotated by the infill
+angle, so the radial field is looked up at the point rotated back into the
+object frame, and the center rotated into the fill frame. Both use the middle of
+the layer.
+
+## Modes following the distance to the surface
+
+`distance_warp`, `smooth_blend` and `stepped_shells` grade by the distance to
+the nearest surface, including the top and bottom, as concentric shells do. The
+depth of a point is that distance over the distance of the deepest point of its
+body, from 0 at the surface to 1; the field keeps it for every node and
+interpolates it between them. A tall box so keeps its whole axis as sparse as
+its center, and a plate is graded through its thickness.
+
+No single smooth pattern follows that depth without distortion (see above), so
+the three modes trade differently:
+
+- Distance warp keeps the lobes and the warp of Lobes, but its radial profile
+ comes from the depth. For every direction of a lobe, the mean depth over the
+ ball along the ray is sampled at 33 radii up to the reach, then smoothed over
+ the neighbouring directions like the reach. The radial coordinate is the one
+ of the linear profile with the same mean depth, `t = 4/3 * (1 - mean depth)`,
+ so with a linear gradient the mean cell size follows the depth exactly, and
+ with the others approximately. In a sphere or a cube, where the depth falls
+ linearly along every ray, it is Lobes. Elsewhere the profile changes with the
+ direction, and the warp shears where neighbouring directions differ, as in
+ plates and long bodies; right under the top of a long body the cells are
+ sparser within the layer, the warp moving their density into the height.
+ The profiles are smoothed over the directions like the reach, as sharper
+ ones shear the pattern across the layer, which adds lines. A long body is so
+ graded partly along its length, between Lobes and the distance.
+- Smooth blend evaluates the regular patterns of the two levels around the
+ target of every point and blends them by a smoothstep over the whole gap
+ between the levels, here at most 2.5 times apart. The density follows the
+ depth without steps, but where two lattices blend, part of their lines run
+ along the blend, so fewer levels print fewer extra lines. From 25% to 5%,
+ three levels print about 0.45 of the uniform infill in a deep core whose
+ levels alone would print 0.3; levels 1.5 times apart print 0.6 to 0.7, and a
+ single blend from the surface to the interior 0.6.
+- Stepped shells split each region of a layer into shells and fill every shell
+ with the regular pattern at its density. The densities are levels from the
+ surface to the interior density at most 1.5 times apart, five from 25% to 5%,
+ and a point takes the level nearest to its target on that geometric scale.
+ The shells are traced by marching squares of the continuous level over the
+ layer on a 0.5 mm grid fixed in the object, so every region of a layer gets
+ the same shells, and clipped to the region. Each shell is shrunk by half a
+ line, like a filled region, and its regular filler connects its lines along
+ that boundary, so the connections of two neighbouring shells lie side by side
+ instead of on top of each other. The pattern is never
+ distorted, but its lines end at every shell, and thin parts get thin shells.
+ The connections add lines: in the core of a 60 mm cube, about a third more
+ than the target.
+
+Stepped shells and Smooth blend need neither lobes nor reaches, which are not
+built for them.
+
+## Constraints
+
+- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
+ to their defaults, so they neither change the infill nor split fill batches.
+- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
+ densities, as the voids at the center are that large.
+- At a sparse infill density of 100% the sparse infill is turned into solid
+ infill, so there is nothing to grade: the options are hidden and no field is
+ built.
+- The adaptive options invalidate `posPrepareInfill`, which rebuilds the field
+ and the anchoring infill.
+- An elongated body without a neck has one center, so its far ends are graded
+ as the outer part of the body, and the warp shears where the reach changes
+ quickly with the direction. A concave body, like an L, may be split into lobes
+ where its maxima cannot see each other in a straight line.
+- Across a ray, the scale is the mean of the gradient from the center, so the
+ layers right under the top and above the bottom are sparser than the surface
+ density in their middle, and a plate is graded from its middle outwards rather
+ than through its thickness.
diff --git a/docs/HLSD/separated-infills.md b/docs/HLSD/separated-infills.md
new file mode 100644
index 0000000000..10742eb042
--- /dev/null
+++ b/docs/HLSD/separated-infills.md
@@ -0,0 +1,97 @@
+# Separated infills — High Level Design
+
+## Purpose and scope
+
+An object's infill patterns are laid out from one reference point, the center
+of the object. When an object groups several parts that do not touch, every
+part cuts the same object-wide pattern at a different place, so equal parts get
+different infill. `separated_infills` lays the infill of every connected body
+out from the center of that body instead, as if the body were sliced on its own.
+
+The option covers sparse infill, internal solid infill and bridges. Top and
+bottom surfaces are left to `center_of_surface_pattern`, which centers the
+Archimedean Chords and Octagram Spiral surface patterns. The option is off by
+default; with it off, or for an object made of a single body, no fill changes.
+Adaptive Cubic and Support Cubic do not depend on the option: they always fill
+each body on its own (see Octree infill).
+
+## Bodies
+
+`PrintObject::prepare_infill()` groups the islands of every layer (`lslices`)
+into 3D connected bodies before bridges are detected, so bridge anchors and
+printed infill share one origin. Islands on adjacent layers belong to one body
+when their slices overlap. Parts that touch or overlap form one body. Separate
+parts, disconnected islands of one mesh, and interleaved parts that never touch,
+such as chain links, each form their own. Every island stores the index of its
+body in `Layer::lslices_separated_component_ids`, and
+`PrintObject::separated_body_bboxes()` holds the bounding box of each body over
+all its layers.
+
+The pass runs when a region uses separated infills, per-model surface centering
+or an octree infill pattern. It is skipped when the object has one model part
+that cannot be split, since a single body already shares the object center.
+
+## Centering a fill
+
+`infill_body()` matches each fill region to the island it overlaps most, among
+the islands whose bounding boxes overlap it, and the filler takes the bounding
+box of that island's body instead of the object's. The box covers every layer
+of the body, which is the box the body gets when sliced alone, so patterns that
+depend on its extent as well as its center come out the same too. Bridge
+anchoring (`Layer::generate_sparse_infill_polylines_for_anchoring()`) makes the
+same choice, so the anchors match the printed infill.
+
+The patterns follow the body's box in one of two ways:
+
+- Rectilinear and its variants, Line, Grid, Triangles, Tri-hexagon, Cubic,
+ Quarter Cubic, Lateral Lattice, Lateral Honeycomb and the plane-path patterns
+ (Hilbert Curve, Archimedean Chords, Octagram Spiral) are laid out from the
+ box: they phase their lines through its center, and Hilbert Curve and the Zig
+ Zag links start from its corner. `Fill::extended_object_bounding_box()`
+ extends the box about its center, so it also serves a box that is not
+ centered on the origin.
+- Honeycomb, 3D Honeycomb, Cross Hatch, Gyroid, TPMS-D and TPMS-FK are laid out
+ from the coordinate origin, which is the object center. They return true from
+ `Fill::aligned_to_origin()`, and `Fill::fill_surface()` moves each region so
+ that the box center lands on the origin, fills it, and moves the paths back.
+ With the default box the center is the origin, so nothing moves.
+
+`is_separable_infill_pattern()` lists these patterns. The settings show the
+option only when the sparse infill pattern is one of them.
+
+## Octree infill
+
+Adaptive Cubic and Support Cubic take their lines from an octree, laid out from
+the center of the mesh it is built from and refined near its surfaces. An
+octree of the whole object would lay every part out from the object's center
+and refine it near the other parts, so these patterns
+(`is_octree_infill_pattern()`) always fill each body on its own, and the
+settings hide the option for them.
+
+For an object of several bodies, `PrintObject::prepare_adaptive_infill_data()`
+builds one octree per body (`FillAdaptive::Octrees`) from the triangles of that
+body only, which is the octree the body gets when sliced alone. Each connected
+component of the mesh goes to the body that most of a few sampled triangles lie
+on. A sample is taken a layer height inside the solid, behind the triangle, and
+looked up in the islands of the nearest layer. Each internal bridge surface goes
+to the body of its island. The fill takes the octree of the region's body, from
+the same `infill_body()`. The octree of the whole object is built only for an
+object of a single body, or when some body received no triangles, which then
+uses it.
+
+The line spacing of an octree comes from the density, line width and multiline
+count of a region, so a modifier or a part with its own density needs octrees of
+its own. `adaptive_fill_line_spacing()` gives the spacing of each region, and
+`FillAdaptive::RegionOctrees` holds one set of octrees per distinct spacing,
+shared by the regions that have it. A set is built only for the bodies its
+regions fill. The fill takes the set of its region, then the octree of its body.
+
+## Patterns left out
+
+Lightning grows its trees over the whole object, so moving a reference point
+cannot center it on one body. Concentric and Spiral Inset follow the outline of
+each region and need no centering.
+
+Solid infill at full density spaces its lines over the extent of each region,
+so it is already independent of the other bodies. Only bridges, which keep
+their line spacing, and the plane-path solid patterns depend on the center.
diff --git a/resources/calib/temperature_tower/belt_temp_provino_unit.stl b/resources/calib/temperature_tower/belt_temp_provino_unit.stl
new file mode 100644
index 0000000000..556cf313d2
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_provino_unit.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_230_190.stl b/resources/calib/temperature_tower/belt_temp_tower_230_190.stl
new file mode 100644
index 0000000000..0496fd1522
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_230_190.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_240_210.stl b/resources/calib/temperature_tower/belt_temp_tower_240_210.stl
new file mode 100644
index 0000000000..31801c1744
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_240_210.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_250_230.stl b/resources/calib/temperature_tower/belt_temp_tower_250_230.stl
new file mode 100644
index 0000000000..b03f057884
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_250_230.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_270_230.stl b/resources/calib/temperature_tower/belt_temp_tower_270_230.stl
new file mode 100644
index 0000000000..7a0bfbd1da
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_270_230.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_280_240.stl b/resources/calib/temperature_tower/belt_temp_tower_280_240.stl
new file mode 100644
index 0000000000..1276d5ce04
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_280_240.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_320_280.stl b/resources/calib/temperature_tower/belt_temp_tower_320_280.stl
new file mode 100644
index 0000000000..957e038067
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_320_280.stl differ
diff --git a/resources/calib/temperature_tower/gen_belt_temp_tower.py b/resources/calib/temperature_tower/gen_belt_temp_tower.py
new file mode 100644
index 0000000000..9182647acb
--- /dev/null
+++ b/resources/calib/temperature_tower/gen_belt_temp_tower.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""Belt temperature-tower asset generator (discrete-provini design).
+
+A vertical temperature tower cannot be sliced on a belt printer, so lay a row of
+DISCRETE provini (one per temperature) along the belt (designed Y) with a fixed
+surface gap. Each provino is the chevron+arc unit (belt_temp_provino_unit.stl,
+keel-first); its temperature is ENGRAVED upright into the 50 mm face — a raised
+number would be an unsupported overhang on the belt. The C++ calib_temp belt branch
+(Plater.cpp) injects one M104 per zone 70 layers INTO provino i:
+ print_z[i] = i * PITCH * cos(theta) + 70 * layer_height (theta = 45)
+inside the body, not in the empty inter-provino gap (which has no sliced layers for
+the event to attach to). PITCH below is the shared geometry contract with that code —
+keep them in sync.
+
+Generates one STL per filament temp range used by Temp_Calibration_Dlg.
+"""
+import numpy as np, trimesh, os
+from matplotlib.textpath import TextPath
+from matplotlib.font_manager import FontProperties
+from shapely.geometry import Polygon as ShPoly
+from shapely.ops import unary_union
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+UNIT = os.path.join(HERE, 'belt_temp_provino_unit.stl') # single provino, keel-first
+SURF_GAP = 25.0 # surface-to-surface gap between provini (mm) — user spec
+TEXT_H = 9.0
+TEXT_DEPTH = 0.8 # engraving depth (numbers are CUT into the face, not raised:
+ # a raised number is an unsupported Y-overhang on the belt)
+TEXT_OVERSHOOT = 0.6 # extra height poking out of the face for a clean boolean cut
+
+# Temperature ranges (start, end) per filament family, 5 C step. File name encodes them.
+RANGES = [(230,190),(270,230),(250,230),(280,240),(240,210),(320,280)]
+
+unit = trimesh.load(UNIT)
+dY = unit.bounds[1,1] - unit.bounds[0,1]
+PITCH = dY + SURF_GAP # designed-Y pitch == C++ contract constant
+print(f"unit dY={dY:.2f} PITCH={PITCH:.3f} (C++ contract: print_z[i]=i*{PITCH:.3f}*cos45)")
+
+# 50 mm face normal (0,-1,1)/sqrt2 ; UPRIGHT basis u=+X det(+1) (verified non-mirrored)
+n = np.array([0,-1,1.])/np.sqrt(2)
+u = np.array([1,0,0.]); v = np.array([0,1,1.])/np.sqrt(2)
+R = np.column_stack([u,v,n])
+fn = unit.face_normals; fc = unit.triangles_center; fa = unit.area_faces
+sel = (fn@n) > 0.9
+face_c = (fc[sel]*fa[sel,None]).sum(0)/fa[sel].sum()
+
+def text_mesh(s):
+ tp = TextPath((0,0), s, size=TEXT_H, prop=FontProperties(family='DejaVu Sans'))
+ rings = [ShPoly(p) for p in tp.to_polygons() if len(p)>=3]
+ rings.sort(key=lambda r:r.area, reverse=True)
+ used=[False]*len(rings); parts=[]
+ for i,o in enumerate(rings):
+ if used[i]: continue
+ holes=[]
+ for j in range(i+1,len(rings)):
+ if not used[j] and o.contains(rings[j]): holes.append(rings[j].exterior.coords); used[j]=True
+ parts.append(ShPoly(o.exterior.coords,holes)); used[i]=True
+ poly = unary_union(parts)
+ geoms = list(poly.geoms) if poly.geom_type=='MultiPolygon' else [poly]
+ m = trimesh.util.concatenate([trimesh.creation.extrude_polygon(g,height=TEXT_DEPTH+TEXT_OVERSHOOT) for g in geoms])
+ c = m.bounds.mean(axis=0); m.apply_translation([-c[0],-c[1],0]); return m
+
+for t_start, t_end in RANGES:
+ temps = list(range(t_start, t_end-1, -5))
+ parts=[]
+ for i,T in enumerate(temps):
+ c = unit.copy(); c.apply_translation([0, i*PITCH, 0])
+ t = text_mesh(str(T)); M=np.eye(4); M[:3,:3]=R; t.apply_transform(M)
+ # place the text spanning from TEXT_DEPTH inside the face to TEXT_OVERSHOOT outside,
+ # then CUT it out of the provino (engrave) — no raised material, no Y-overhang.
+ t.apply_translation(face_c - n*TEXT_DEPTH + np.array([0,i*PITCH,0]))
+ c = trimesh.boolean.difference([c, t], engine='manifold')
+ parts.append(c)
+ asset = trimesh.util.concatenate(parts)
+ out = os.path.join(HERE, f"belt_temp_tower_{t_start}_{t_end}.stl")
+ asset.export(out)
+ dims = np.round(asset.bounds[1]-asset.bounds[0],1)
+ wt = all(p.is_watertight for p in parts)
+ print(f" {t_start}->{t_end}: {len(temps)} zones bbox={dims} watertight={wt} -> {os.path.basename(out)}")
diff --git a/resources/images/delete_all_filaments.svg b/resources/images/delete_all_filaments.svg
new file mode 100644
index 0000000000..e2f3641300
--- /dev/null
+++ b/resources/images/delete_all_filaments.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/toolbar_tooltip.svg b/resources/images/toolbar_tooltip.svg
index 96c5684cb0..49279b8ed9 100644
--- a/resources/images/toolbar_tooltip.svg
+++ b/resources/images/toolbar_tooltip.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/resources/images/toolbar_tooltip_hover.svg b/resources/images/toolbar_tooltip_hover.svg
deleted file mode 100644
index c8d606829e..0000000000
--- a/resources/images/toolbar_tooltip_hover.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/resources/images/toolbar_video_guide.svg b/resources/images/toolbar_video_guide.svg
new file mode 100644
index 0000000000..b39e485af2
--- /dev/null
+++ b/resources/images/toolbar_video_guide.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/images/toolbar_wiki.svg b/resources/images/toolbar_wiki.svg
new file mode 100644
index 0000000000..b10b1dcb17
--- /dev/null
+++ b/resources/images/toolbar_wiki.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/profiles/BBL/machine/Bambu Lab P2S 0.4 nozzle template machine_start_gcode.json b/resources/profiles/BBL/machine/Bambu Lab P2S 0.4 nozzle template machine_start_gcode.json
index a7dee998fb..5abab59c71 100644
--- a/resources/profiles/BBL/machine/Bambu Lab P2S 0.4 nozzle template machine_start_gcode.json
+++ b/resources/profiles/BBL/machine/Bambu Lab P2S 0.4 nozzle template machine_start_gcode.json
@@ -2,5 +2,5 @@
"type": "machine",
"name": "Bambu Lab P2S 0.4 nozzle template machine_start_gcode",
"instantiation": "false",
- "machine_start_gcode": ";M1002 set_flag extrude_cali_flag=1\n;M1002 set_flag g29_before_print_flag=1\n;M1002 set_flag auto_cali_toolhead_offset_flag=1\n;M1002 set_flag build_plate_detect_flag=1\n\n;======== P2S start gcode==========\n;===== 2026/05/18 =====\n\n M140 S[bed_temperature_initial_layer_single] ; heat heatbed first\n M993 A0 B0 C0 ; nozzle cam detection not allowed.\n M400\n\n;=====printer start sound ===================\nM17\nM400 S1\nM1006 S1\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B9 L50 C61 D9 M50 E61 F9 N50\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B18 L50 C61 D18 M50 E61 F18 N50\nM1006 W\n;=====printer start sound ===================\n\n M620 M ;enable remap\n G389\n\n;===== avoid end stop =================\n G91\n G380 S2 Z22 F1200\n G380 S2 Z-12 F1200\n G90\n;===== avoid end stop =================\n\n;===== reset machine status =================\n M204 S10000\n M630 S0 P1\n G90\n M17 D ; reset motor current to default\n M960 S5 P1 ; turn on logo lamp\n G90\n M220 S100 ;Reset Feedrate\n M1002 set_gcode_claim_speed_level: 5\n M221 S100 ;Reset Flowrate\n M73.2 R1.0 ;Reset left time magnitude\n G29.1 Z{+0.0} ; clear z-trim value first\n M983.1 M1\n M982.2 S1 ; turn on cog noise reduction\n M983.4 S0\n;===== reset machine status =================\n\n;==== set airduct mode ====\n;==== if Chamber Cooling is necessary ====\n{if (overall_chamber_temperature >= 40)}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S255 ; turn on filter fan\nM622.1 S0\nM1002 judge_flag ventobox_replace_aux1_fan_flag\nM622 J0\nM106 P10 S0 ; turn off left aux fan\nM623\n{else}\n{if (min_vitrification_temperature <= 50)}\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on chamber cooling fan\nM622.1 S0\nM1002 judge_flag ventobox_replace_aux1_fan_flag\nM622 J0\nM106 P10 S0 ; turn off left aux fan\nM623\nM142 P6 R30 S40 U0.3 V0.8 ; set PETG exhaust chamber autocooling\n{else}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S127 ; turn on 50% filter fan\nM142 P6 R30 S40 U0.3 V0.8 ; set PLA/TPU exhaust chamber autocooling\n{endif}\n{endif}\n;==== set airduct mode ====\n\n;===== start to heat heatbed & hotend==========\n M1002 gcode_claim_action : 2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M104 S140 A\n\n G29.2 S0 ; avoid invalid abl data\n\n;===== first homing start =====\n M1002 gcode_claim_action : 13\n G28 X T300\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G150.3\n M972 S24 P0\n M972 S26 P0 C0\n M972 S42 P0 T5000\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G90\n G1 X128 Y128 F30000\n G28 Z P0 T400\n M400\n;===== first homign end =====\n\n;===== detection start =====\n M1002 gcode_claim_action : 11\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-80} A ; rise temp in advance\n M972 S19 P0 T5000 ;plate type detection\n\n {if max_print_z >= 145}\n M1002 gcode_claim_action : 75 ; Detect obstacles at the botton of the heated bed\n G150.3\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} ; rise temp in advance\n G3811 Z{max_print_z} ; Detect obstacles at the bottom of the heated bed\n {endif}\n;===== detection end =====\n\n;===== prepare print temperature and material ==========\n M400\n M211 X0 Y0 Z0 ;turn off soft endstop\n M975 S1 ; turn on input shaping\n\n G29.2 S0 ; avoid invalid abl data\n G150.3\n{if ((filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{endif}\n\n M620.11 P0 L0 I[initial_no_support_filament_id] E0\n M620.11 K0 I[initial_no_support_filament_id] R0\n\n M620 S[initial_no_support_filament_id]A ; switch material if AMS exist\n M1002 gcode_claim_action : 4\n M1002 set_filament_type:UNKNOWN\n M400\n T[initial_no_support_filament_id]\n M400\n M628 S0\n M629\n M400\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M621 S[initial_no_support_filament_id]A\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n M106 P1 S0\n M400\n G29.2 S1\n;===== prepare print temperature and material ==========\n\n\n;===== auto extrude cali start =========================\n M975 S1\n M1002 judge_flag extrude_cali_flag\n M622 J0\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M623\n\n M622 J1\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n\n M622 J2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n;===== auto extrude cali end =========================\n\n {if hold_chamber_temp_for_flat_print}\n M1002 gcode_claim_action : 58\n M104 S{first_layer_temperature[initial_no_support_filament_id]}\n {if bed_temperature_initial_layer_single > 89}\n M1030 S1800\n SYNC R0 T1800\n {else}\n M1030 S300\n SYNC R0 T300\n {endif}\n M1030 C\n {endif}\n\n {if filament_type[initial_filament_id] == \"TPU\" || filament_type[initial_filament_id] == \"PVA\"}\n {else}\n M83\n G1 E-3 F1800\n M400 P500\n {endif}\n G150.2\n G150.1 F8000\n G150.2\n G150.1 F8000\n\n G91\n G1 Y-16 F12000 ; move away from the trash bin\n G90\n M400\n\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-80} A\n\n;===== wipe right nozzle start =====\n M1002 gcode_claim_action : 14\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n\n{if filament_type[initial_filament_id] == \"PC\"}\n M109 S170 A\n{else}\n M109 S140 A\n{endif}\n G91\n G1 Z5 F1200\n G90\n M400\n G150.1\n;===== wipe left nozzle end =====\n\n\n;===== mech mode sweep start =====\n M1002 gcode_claim_action : 3\n G90\n G1 X128 Y128 F20000\n G1 Z5 F1200\n M400 P200\n M970.3 Q1 A5 K0 O1\n M970.2 Q1 K1 W74 Z0.01\n M974 Q1 S2 P0\n M970.3 Q0 A7 K0 O1\n M970.2 Q0 K1 W74 Z0.01\n M974 Q0 S2 P0\n M975 S1\n M400\n;===== mech mode sweep end =====\n\n;===== bed leveling ==================================\n M1002 gcode_claim_action : 54\n M190 S[bed_temperature_initial_layer_single]; ensure bed temp\n M109 S140 A\n M106 S0 ; turn off fan , too noisy\n M1002 judge_flag g29_before_print_flag\n M622 J1\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H\n {else}\n G29 A1 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]}\n {endif}\n M400\n M623\n\n M622 J2\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H\n {else}\n G29 A2 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]}\n {endif}\n M400\n M623\n\n M622 J0\n G28\n M623\n G29.2 S1\n G28\n;===== bed leveling end ================================\n\n M985.1 U0 E2\n M985.1 U1 E2\n\n M104 S{nozzle_temperature_initial_layer[initial_filament_id]} A\n G150.3 ; move to garbage can to wait for temp\n\n;===== wait temperature reaching the reference value =======\n M190 S[bed_temperature_initial_layer_single]\n\n ;========turn off light and fans =============\n M960 S1 P0 ; turn off laser\n M960 S2 P0 ; turn off laser\n M106 S0 ; turn off cooling fan\n\n;===== wait temperature reaching the reference value =======\n\n M1002 gcode_claim_action : 255\n M400\n M975 S1 ; turn on mech mode supression\n\n;============switch again==================\n M211 X0 Y0 Z0 ;turn off soft endstop\n G91\n G1 Z6 F1200\n G90\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M620 S[initial_no_support_filament_id]A\n M400\n T[initial_no_support_filament_id]\n M400\n M628 S0\n M629\n M400\n M621 S[initial_no_support_filament_id]A\n;============switch again==================\n\n;===== for Textured PEI Plate , lower the nozzle as the nozzle was touching topmost of the texture when homing ==\n {if bed_temperature_initial_layer_single > 89}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{-0.02} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.0}\n {endif}\n {else}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{0.01} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.03}\n {endif}\n {endif}\n\n\n;===== nozzle load line ===============================\nM1002 gcode_claim_action : 51\n G29.2 S1 ; ensure z comp turn on\n G90\n M83\n M400 P50\n M500 D1\n M400 S3\n M109 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n G0 X100 Y0 F24000\n M400\n ;G130 O0 X100 Y-0.4 Z0.8 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E20 D5\n G130 O0 X100 Y-0.2 Z0.6 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E12 D4\n G90\n M83\n G1 Z1\n M400\n;===== noozle load line end ===========================\nM1002 gcode_claim_action : 0\n G29.99\n\n{if (filament_type[initial_no_support_filament_id] == \"TPU\") ||\n(filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")}\nM1015.3 S1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]};enable tpu, pla and petg clog detect\n{else}\nM1015.3 S0;disable clog detect\n{endif}\n\n{if (filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")\n || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;disable E air printing detect\n{endif}\n\nM620.6 I[initial_no_support_filament_id] W1 ;enable ams air printing detect\n\nM1010 Q0 B0.023 S0.01\nM1010 Q1 B0.005 S0.01\nM1010.1 S1\n"
+ "machine_start_gcode": ";M1002 set_flag extrude_cali_flag=1\n;M1002 set_flag g29_before_print_flag=1\n;M1002 set_flag auto_cali_toolhead_offset_flag=1\n;M1002 set_flag build_plate_detect_flag=1\n\n;======== P2S start gcode==========\n;===== Based on official 2026/05/18 start gcode =====\n\n M140 S[bed_temperature_initial_layer_single] ; heat heatbed first\n M993 A0 B0 C0 ; nozzle cam detection not allowed.\n M400\n\n;=====printer start sound ===================\nM17\nM400 S1\nM1006 S1\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B9 L50 C61 D9 M50 E61 F9 N50\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B18 L50 C61 D18 M50 E61 F18 N50\nM1006 W\n;=====printer start sound ===================\n\n M620 M ;enable remap\n G389\n\n;===== avoid end stop =================\n G91\n G380 S2 Z22 F1200\n G380 S2 Z-12 F1200\n G90\n;===== avoid end stop =================\n\n;===== reset machine status =================\n M204 S10000\n M630 S0 P1\n G90\n M17 D ; reset motor current to default\n M960 S5 P1 ; turn on logo lamp\n G90\n M220 S100 ;Reset Feedrate\n M1002 set_gcode_claim_speed_level: 5\n M221 S100 ;Reset Flowrate\n M73.2 R1.0 ;Reset left time magnitude\n G29.1 Z{+0.0} ; clear z-trim value first\n M983.1 M1\n M975 S1 ; turn on input shaping\n M982.2 S1 ; turn on cog noise reduction\n M983.4 S0\n;===== reset machine status =================\n\n;==== set airduct mode ====\n;==== if Chamber Cooling is necessary ====\n{if (overall_chamber_temperature >= 40)}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S255 ; turn on filter fan\nM622.1 S0\nM1002 judge_flag ventobox_replace_aux1_fan_flag\nM622 J0\nM106 P10 S0 ; turn off left aux fan\nM623\n{else}\n{if (min_vitrification_temperature <= 50)}\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on chamber cooling fan\nM622.1 S0\nM1002 judge_flag ventobox_replace_aux1_fan_flag\nM622 J0\nM106 P10 S0 ; turn off left aux fan\nM623\nM142 P6 R30 S40 U0.3 V0.8 ; set PETG exhaust chamber autocooling\n{else}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S127 ; turn on 50% filter fan\nM142 P6 R30 S40 U0.3 V0.8 ; set PLA/TPU exhaust chamber autocooling\n{endif}\n{endif}\n;==== set airduct mode ====\n\n;===== start to heat heatbed & hotend==========\n M1002 gcode_claim_action : 2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M104 S140 A\n\n G29.2 S0 ; avoid invalid abl data\n\n;===== first homing start =====\n M1002 gcode_claim_action : 13\n G28 X T300\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G150.3\n M972 S24 P0\n M972 S26 P0 C0\n M972 S42 P0 T5000\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G90\n G1 X128 Y128 F30000\n G28 Z P0 T400\n M400\n;===== first homing end =====\n\n;===== detection start =====\n M1002 gcode_claim_action : 11\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-80} A ; rise temp in advance\n M972 S19 P0 T5000 ;plate type detection\n\n {if max_print_z >= 145}\n M1002 gcode_claim_action : 75 ; Detect obstacles at the botton of the heated bed\n G150.3\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} ; rise temp in advance\n G3811 Z{max_print_z} ; Detect obstacles at the bottom of the heated bed\n {endif}\n;===== detection end =====\n\n;===== prepare print temperature and material ==========\n M400\n M211 X0 Y0 Z0 ;turn off soft endstop\n G29.2 S0 ; avoid invalid abl data\n G150.3\n{if ((filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{endif}\n\n M620.11 P0 L0 I[initial_no_support_filament_id] E0\n M620.11 K0 I[initial_no_support_filament_id] R0\n\n M620 S[initial_no_support_filament_id]A ; switch material if AMS exist\n M1002 gcode_claim_action : 4\n M1002 set_filament_type:UNKNOWN\n M400\n T[initial_no_support_filament_id]\n M400\n M628 S0\n M629\n M400\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M621 S[initial_no_support_filament_id]A\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n M106 P1 S0\n M400\n G29.2 S1\n;===== prepare print temperature and material ==========\n\n\n;===== auto extrude cali start =========================\n M975 S1\n M1002 judge_flag extrude_cali_flag\n M622 J0\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M623\n\n M622 J1\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n\n M622 J2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n;===== auto extrude cali end =========================\n\n {if hold_chamber_temp_for_flat_print}\n M1002 gcode_claim_action : 58\n M104 S{first_layer_temperature[initial_no_support_filament_id]}\n {if bed_temperature_initial_layer_single > 89}\n M1030 S1800\n SYNC R0 T1800\n {else}\n M1030 S300\n SYNC R0 T300\n {endif}\n M1030 C\n {endif}\n\n {if filament_type[initial_filament_id] == \"TPU\" || filament_type[initial_filament_id] == \"PVA\"}\n {else}\n M83\n G1 E-3 F1800\n M400 P500\n {endif}\n\n ; series of wipes to ensure the nozzle is as clean as possible to avoid potential z-drift issues.\n M1002 gcode_claim_action : 14\n M104 S180\n G150.2\n G150.1 F8000\n G150.2\n G150.1 F8000\n M106 P1 S255\n M109 S180 A\n G150.1 F8000 \n M109 S140 A\n M106 P1 S0\n G150.1 F8000\n G91\n G1 Y-16 F12000\n G1 X100 F6000\n G90\n M400\n\n;===== wipe right nozzle start =====\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n\n{if filament_type[initial_filament_id] == \"PC\"}\n M109 S170 A\n{else}\n M109 S140 A\n{endif}\n G91\n G1 Z5 F1200\n G90\n M400\n G150.1\n;===== wipe left nozzle end =====\n\n\n;===== mech mode sweep start =====\n M1002 gcode_claim_action : 3\n G90\n G1 X128 Y128 F20000\n G1 Z5 F1200\n M400 P200\n M970.3 Q1 A5 K0 O1\n M970.2 Q1 K1 W74 Z0.01\n M974 Q1 S2 P0\n M970.3 Q0 A7 K0 O1\n M970.2 Q0 K1 W74 Z0.01\n M974 Q0 S2 P0\n M975 S1\n M400\n;===== mech mode sweep end =====\n\n;===== bed leveling ==================================\n M1002 gcode_claim_action : 54\n M190 S[bed_temperature_initial_layer_single]; ensure bed temp\n M109 S140 A\n M106 S0 ; turn off fan , too noisy\n M1002 judge_flag g29_before_print_flag\n M622 J1\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H\n {else}\n G29 A1 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]}\n {endif}\n M400\n M623\n\n M622 J2\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H\n {else}\n G29 A2 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]}\n {endif}\n M400\n M623\n\n M622 J0\n ;skip to homing\n M623\n G29.2 S1\n M1002 gcode_claim_action : 13\n G28\n;===== bed leveling end ================================\n\n M985.1 U0 E2\n M985.1 U1 E2\n\n M104 S{nozzle_temperature_initial_layer[initial_filament_id]} A\n G150.3 ; move to garbage can to wait for temp\n\n;===== wait temperature reaching the reference value =======\n M190 S[bed_temperature_initial_layer_single]\n\n ;========turn off light and fans =============\n M960 S1 P0 ; turn off laser\n M960 S2 P0 ; turn off laser\n M106 S0 ; turn off cooling fan\n\n;===== wait temperature reaching the reference value =======\n\n M1002 gcode_claim_action : 255\n M400\n M975 S1 ; turn on mech mode supression\n\n;============switch again==================\n M211 X0 Y0 Z0 ;turn off soft endstop\n G91\n G1 Z6 F1200\n G90\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M620 S[initial_no_support_filament_id]A\n M400\n T[initial_no_support_filament_id]\n M400\n M628 S0\n M629\n M400\n M621 S[initial_no_support_filament_id]A\n;============switch again==================\n\n;===== for Textured PEI Plate , lower the nozzle as the nozzle was touching topmost of the texture when homing ==\n {if bed_temperature_initial_layer_single > 89}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{-0.02} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.0}\n {endif}\n {else}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{0.01} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.03}\n {endif}\n {endif}\n\n\n;===== nozzle load line ===============================\n M1002 gcode_claim_action : 7\n G29.2 S1 ; ensure z comp turn on\n G90\n M83\n M400 P50\n M500 D1\n M400 S3\n M109 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 51\n G0 X100 Y0 F24000\n M400\n ;G130 O0 X100 Y-0.4 Z0.8 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E20 D5\n G130 O0 X100 Y-0.2 Z0.6 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E12 D4\n G90\n M83\n G1 Z1\n M400\n;===== nozzle load line end ===========================\nM1002 gcode_claim_action : 0\n G29.99\n\n{if (filament_type[initial_no_support_filament_id] == \"TPU\") ||\n(filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")}\nM1015.3 S1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]};enable tpu, pla and petg clog detect\n{else}\nM1015.3 S0;disable clog detect\n{endif}\n\n{if (filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")\n || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;disable E air printing detect\n{endif}\n\nM620.6 I[initial_no_support_filament_id] W1 ;enable ams air printing detect\n\nM1010 Q0 B0.023 S0.01\nM1010 Q1 B0.005 S0.01\nM1010.1 S1\n"
}
diff --git a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json
index 8ca40003f3..44bd17e125 100644
--- a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json
+++ b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json
@@ -10,6 +10,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000"
diff --git a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json
index bd399e65a1..1867783ab5 100644
--- a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json
@@ -10,6 +10,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000",
diff --git a/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json
index 02ec0007ec..2d00b3d04e 100644
--- a/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json
+++ b/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json
@@ -9,6 +9,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000"
diff --git a/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json
index e3471e88b0..37194e5627 100644
--- a/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json
+++ b/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json
@@ -9,6 +9,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000"
diff --git a/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json
index a9c9eb7bdf..2847f1cae2 100644
--- a/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json
@@ -10,6 +10,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000",
diff --git a/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json
index 4d6e809633..cdd946b32f 100644
--- a/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json
@@ -11,6 +11,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000",
diff --git a/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json
index 657b0b7c38..41bc2ba0f0 100644
--- a/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json
@@ -11,6 +11,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json
index 4d2722df04..2103b9cc04 100644
--- a/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json
+++ b/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json
@@ -11,6 +11,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json
index 2a76fcba00..46a148408c 100644
--- a/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json
@@ -11,6 +11,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000",
diff --git a/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json
index 13c9f1e722..54ec7a734a 100644
--- a/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json
@@ -11,6 +11,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json
index 24f35fc875..6ca9a556a9 100644
--- a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json
+++ b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json
@@ -11,6 +11,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json
index 64c46e6414..47bcce8c51 100644
--- a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json
+++ b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json
@@ -10,6 +10,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000"
diff --git a/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json
index bec3147a16..90ef61fe8c 100644
--- a/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json
@@ -10,6 +10,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json
index d745625988..22171f7b4f 100644
--- a/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json
+++ b/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json
@@ -11,6 +11,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json
index 979edcca6e..b6f59a2ce1 100644
--- a/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json
+++ b/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json
@@ -10,6 +10,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000"
diff --git a/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json
index d3aaa84887..843a06b9a9 100644
--- a/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json
+++ b/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json
@@ -10,6 +10,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000"
diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json
index afbcdacbe8..ab2084d898 100644
--- a/resources/profiles/Custom.json
+++ b/resources/profiles/Custom.json
@@ -1,9 +1,13 @@
{
"name": "Custom Printer",
- "version": "02.04.00.07",
+ "version": "02.04.00.08",
"force_update": "0",
"description": "My configurations",
"machine_model_list": [
+ {
+ "name": "Generic Belt Printer",
+ "sub_path": "machine/MyBeltPrinter.json"
+ },
{
"name": "Generic Klipper Printer",
"sub_path": "machine/MyKlipper.json"
@@ -50,6 +54,10 @@
"name": "0.08mm Extra Fine @MyKlipper",
"sub_path": "process/0.08mm Extra Fine @MyKlipper.json"
},
+ {
+ "name": "0.12mm Fine @MyBeltPrinter",
+ "sub_path": "process/0.12mm Fine @MyBeltPrinter.json"
+ },
{
"name": "0.12mm Fine @MyKlipper",
"sub_path": "process/0.12mm Fine @MyKlipper.json"
@@ -62,6 +70,10 @@
"name": "0.16mm Optimal @MyKlipper",
"sub_path": "process/0.16mm Optimal @MyKlipper.json"
},
+ {
+ "name": "0.20mm Standard @MyBeltPrinter",
+ "sub_path": "process/0.20mm Standard @MyBeltPrinter.json"
+ },
{
"name": "0.20mm Standard @MyKlipper",
"sub_path": "process/0.20mm Standard @MyKlipper.json"
@@ -262,6 +274,10 @@
"name": "MyKlipper 0.8 nozzle",
"sub_path": "machine/MyKlipper 0.8 nozzle.json"
},
+ {
+ "name": "fdm_belt_common",
+ "sub_path": "machine/fdm_belt_common.json"
+ },
{
"name": "fdm_toolchanger_common",
"sub_path": "machine/fdm_toolchanger_common.json"
@@ -274,6 +290,22 @@
"name": "MyRRF 0.4 nozzle",
"sub_path": "machine/MyRRF 0.4 nozzle.json"
},
+ {
+ "name": "MyBeltPrinter 0.2 nozzle",
+ "sub_path": "machine/MyBeltPrinter 0.2 nozzle.json"
+ },
+ {
+ "name": "MyBeltPrinter 0.4 nozzle",
+ "sub_path": "machine/MyBeltPrinter 0.4 nozzle.json"
+ },
+ {
+ "name": "MyBeltPrinter 0.6 nozzle",
+ "sub_path": "machine/MyBeltPrinter 0.6 nozzle.json"
+ },
+ {
+ "name": "MyBeltPrinter 0.8 nozzle",
+ "sub_path": "machine/MyBeltPrinter 0.8 nozzle.json"
+ },
{
"name": "MyToolChanger 0.2 nozzle",
"sub_path": "machine/MyToolChanger 0.2 nozzle.json"
diff --git a/resources/profiles/Custom/Generic Belt Printer_cover.png b/resources/profiles/Custom/Generic Belt Printer_cover.png
new file mode 100644
index 0000000000..d42f2ef228
Binary files /dev/null and b/resources/profiles/Custom/Generic Belt Printer_cover.png differ
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json
new file mode 100644
index 0000000000..1226f4dd68
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json
@@ -0,0 +1,27 @@
+{
+ "type": "machine",
+ "name": "MyBeltPrinter 0.2 nozzle",
+ "inherits": "fdm_belt_common",
+ "from": "system",
+ "setting_id": "3w1uyJdmm14QhDnH",
+ "instantiation": "true",
+ "printer_model": "Generic Belt Printer",
+ "default_print_profile": "0.12mm Fine @MyBeltPrinter",
+ "nozzle_diameter": [
+ "0.2"
+ ],
+ "max_layer_height": [
+ "0.16"
+ ],
+ "min_layer_height": [
+ "0.04"
+ ],
+ "printer_variant": "0.2",
+ "printable_area": [
+ "0x0",
+ "350x0",
+ "350x350",
+ "0x350"
+ ],
+ "printable_height": "300"
+}
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json
new file mode 100644
index 0000000000..e8149509ca
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json
@@ -0,0 +1,20 @@
+{
+ "type": "machine",
+ "name": "MyBeltPrinter 0.4 nozzle",
+ "inherits": "fdm_belt_common",
+ "from": "system",
+ "setting_id": "6nRHUtvJOUffocbu",
+ "instantiation": "true",
+ "printer_model": "Generic Belt Printer",
+ "nozzle_diameter": [
+ "0.4"
+ ],
+ "printer_variant": "0.4",
+ "printable_area": [
+ "0x0",
+ "350x0",
+ "350x350",
+ "0x350"
+ ],
+ "printable_height": "300"
+}
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json
new file mode 100644
index 0000000000..b8ca999dca
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json
@@ -0,0 +1,26 @@
+{
+ "type": "machine",
+ "name": "MyBeltPrinter 0.6 nozzle",
+ "inherits": "fdm_belt_common",
+ "from": "system",
+ "setting_id": "K0m9HbUNwKT4UCJV",
+ "instantiation": "true",
+ "printer_model": "Generic Belt Printer",
+ "nozzle_diameter": [
+ "0.6"
+ ],
+ "max_layer_height": [
+ "0.4"
+ ],
+ "min_layer_height": [
+ "0.12"
+ ],
+ "printer_variant": "0.6",
+ "printable_area": [
+ "0x0",
+ "350x0",
+ "350x350",
+ "0x350"
+ ],
+ "printable_height": "300"
+}
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json
new file mode 100644
index 0000000000..b546b5b288
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json
@@ -0,0 +1,26 @@
+{
+ "type": "machine",
+ "name": "MyBeltPrinter 0.8 nozzle",
+ "inherits": "fdm_belt_common",
+ "from": "system",
+ "setting_id": "rHAweDz4eNwttPNA",
+ "instantiation": "true",
+ "printer_model": "Generic Belt Printer",
+ "nozzle_diameter": [
+ "0.8"
+ ],
+ "max_layer_height": [
+ "0.6"
+ ],
+ "min_layer_height": [
+ "0.2"
+ ],
+ "printer_variant": "0.8",
+ "printable_area": [
+ "0x0",
+ "350x0",
+ "350x350",
+ "0x350"
+ ],
+ "printable_height": "300"
+}
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter.json b/resources/profiles/Custom/machine/MyBeltPrinter.json
new file mode 100644
index 0000000000..0f6e0c5891
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter.json
@@ -0,0 +1,12 @@
+{
+ "type": "machine_model",
+ "name": "Generic Belt Printer",
+ "model_id": "my_belt_01",
+ "nozzle_diameter": "0.4;0.2;0.6;0.8",
+ "machine_tech": "FFF",
+ "family": "MyPrinter",
+ "bed_model": "Custom_350_bed.stl",
+ "bed_texture": "orcaslicer_bed_texture.svg",
+ "hotend_model": "",
+ "default_materials": "Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System"
+}
diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json
new file mode 100644
index 0000000000..96b3138832
--- /dev/null
+++ b/resources/profiles/Custom/machine/fdm_belt_common.json
@@ -0,0 +1,95 @@
+{
+ "type": "machine",
+ "name": "fdm_belt_common",
+ "inherits": "fdm_klipper_common",
+ "from": "system",
+ "instantiation": "false",
+ "gcode_flavor": "klipper",
+ "single_extruder_multi_material": "0",
+ "default_filament_profile": [
+ "Generic PLA @System"
+ ],
+ "default_print_profile": "0.20mm Standard @MyBeltPrinter",
+ "max_layer_height": [
+ "0.32"
+ ],
+ "min_layer_height": [
+ "0.08"
+ ],
+ "deretraction_speed": [
+ "30"
+ ],
+ "extruder_colour": [
+ "#FCE94F"
+ ],
+ "extruder_offset": [
+ "0x0"
+ ],
+ "long_retractions_when_cut": [
+ "0"
+ ],
+ "nozzle_diameter": [
+ "0.4"
+ ],
+ "retract_before_wipe": [
+ "70%"
+ ],
+ "retract_length_toolchange": [
+ "2"
+ ],
+ "retract_lift_above": [
+ "0"
+ ],
+ "retract_lift_below": [
+ "0"
+ ],
+ "retract_lift_enforce": [
+ "All Surfaces"
+ ],
+ "retract_restart_extra": [
+ "0"
+ ],
+ "retract_restart_extra_toolchange": [
+ "0"
+ ],
+ "retract_when_changing_layer": [
+ "1"
+ ],
+ "retraction_distances_when_cut": [
+ "18"
+ ],
+ "retraction_length": [
+ "0.8"
+ ],
+ "retraction_minimum_travel": [
+ "1"
+ ],
+ "retraction_speed": [
+ "30"
+ ],
+ "travel_slope": [
+ "3"
+ ],
+ "wipe": [
+ "1"
+ ],
+ "wipe_distance": [
+ "1"
+ ],
+ "z_hop": [
+ "0"
+ ],
+ "z_hop_types": [
+ "Normal Lift"
+ ],
+ "gcode_remap_x": "rev_x",
+ "gcode_remap_y": "pos_z",
+ "gcode_remap_z": "pos_y",
+ "belt_printer": "1",
+ "belt_slice_rotation": "x",
+ "belt_slice_rotation_angle": "45",
+ "build_plate_tilt_x": "45",
+ "purge_in_prime_tower": "0",
+ "scan_first_layer": "0",
+ "auxiliary_fan": "0"
+}
diff --git a/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json b/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json
new file mode 100644
index 0000000000..15814e6725
--- /dev/null
+++ b/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json
@@ -0,0 +1,20 @@
+{
+ "type": "process",
+ "name": "0.12mm Fine @MyBeltPrinter",
+ "inherits": "fdm_process_klipper_common",
+ "from": "system",
+ "setting_id": "EugqqdLJ423bgEwN",
+ "instantiation": "true",
+ "layer_height": "0.12",
+ "initial_layer_print_height": "0.12",
+ "bottom_shell_layers": "5",
+ "top_shell_layers": "6",
+ "support_top_z_distance": "0.08",
+ "support_bottom_z_distance": "0.08",
+ "skirt_loops": "0",
+ "skirt_distance": "0",
+ "compatible_printers": [
+ "MyBeltPrinter 0.2 nozzle",
+ "MyBeltPrinter 0.4 nozzle"
+ ]
+}
diff --git a/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json b/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json
new file mode 100644
index 0000000000..6c10b250d6
--- /dev/null
+++ b/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json
@@ -0,0 +1,17 @@
+{
+ "type": "process",
+ "name": "0.20mm Standard @MyBeltPrinter",
+ "inherits": "fdm_process_klipper_common",
+ "from": "system",
+ "setting_id": "YzCDAgH3uLOM53pF",
+ "instantiation": "true",
+ "layer_height": "0.2",
+ "initial_layer_print_height": "0.2",
+ "skirt_loops": "0",
+ "skirt_distance": "0",
+ "compatible_printers": [
+ "MyBeltPrinter 0.4 nozzle",
+ "MyBeltPrinter 0.6 nozzle",
+ "MyBeltPrinter 0.8 nozzle"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json
new file mode 100644
index 0000000000..ebed0bc9e0
--- /dev/null
+++ b/resources/profiles/IdeaFormer.json
@@ -0,0 +1,54 @@
+{
+ "name": "IdeaFormer",
+ "version": "02.00.00.06",
+ "force_update": "0",
+ "description": "IdeaFormer belt printer configurations",
+ "machine_model_list": [
+ {
+ "name": "IdeaFormer IR3 V2",
+ "sub_path": "machine/IdeaFormer IR3 V2.json"
+ }
+ ],
+ "process_list": [
+ {
+ "name": "fdm_process_common",
+ "sub_path": "process/fdm_process_common.json"
+ },
+ {
+ "name": "0.20mm Standard @IdeaFormer IR3 V2",
+ "sub_path": "process/0.20mm Standard @IdeaFormer IR3 V2.json"
+ }
+ ],
+ "filament_list": [
+ {
+ "name": "Generic PLA @IdeaFormer IR3 V2",
+ "sub_path": "filament/Generic PLA @IdeaFormer IR3 V2.json"
+ },
+ {
+ "name": "eSUN PLA @IdeaFormer IR3 V2",
+ "sub_path": "filament/eSUN PLA @IdeaFormer IR3 V2.json"
+ },
+ {
+ "name": "Generic PETG @IdeaFormer IR3 V2",
+ "sub_path": "filament/Generic PETG @IdeaFormer IR3 V2.json"
+ }
+ ],
+ "machine_list": [
+ {
+ "name": "fdm_machine_common",
+ "sub_path": "machine/fdm_machine_common.json"
+ },
+ {
+ "name": "fdm_klipper_common",
+ "sub_path": "machine/fdm_klipper_common.json"
+ },
+ {
+ "name": "fdm_belt_common",
+ "sub_path": "machine/fdm_belt_common.json"
+ },
+ {
+ "name": "IdeaFormer IR3 V2 0.4 nozzle",
+ "sub_path": "machine/IdeaFormer IR3 V2 0.4 nozzle.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png b/resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png
new file mode 100644
index 0000000000..71485f54f5
Binary files /dev/null and b/resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png differ
diff --git a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..28da3959f4
--- /dev/null
+++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json
@@ -0,0 +1,77 @@
+{
+ "type": "filament",
+ "name": "Generic PETG @IdeaFormer IR3 V2",
+ "inherits": "Generic PETG @System",
+ "from": "system",
+ "setting_id": "n4zaXcUUzTqAxq5f",
+ "instantiation": "true",
+ "filament_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "compatible_printers": [
+ "IdeaFormer IR3 V2 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PETG"
+ ],
+ "filament_vendor": [
+ "Generic"
+ ],
+ "filament_settings_id": [
+ "Generic PETG @IdeaFormer IR3 V2"
+ ],
+ "filament_flow_ratio": [
+ "0.95"
+ ],
+ "filament_cost": [
+ "25"
+ ],
+ "nozzle_temperature": [
+ "240"
+ ],
+ "nozzle_temperature_initial_layer": [
+ "245"
+ ],
+ "cool_plate_temp": [
+ "80"
+ ],
+ "cool_plate_temp_initial_layer": [
+ "80"
+ ],
+ "fan_min_speed": [
+ "40"
+ ],
+ "fan_max_speed": [
+ "60"
+ ],
+ "overhang_fan_threshold": [
+ "25%"
+ ],
+ "overhang_fan_speed": [
+ "80"
+ ],
+ "full_fan_speed_layer": [
+ "8"
+ ],
+ "slow_down_min_speed": [
+ "20"
+ ],
+ "slow_down_layer_time": [
+ "4"
+ ],
+ "fan_cooling_layer_time": [
+ "100"
+ ],
+ "filament_retraction_length": [
+ "2"
+ ],
+ "filament_retraction_speed": [
+ "40"
+ ],
+ "filament_deretraction_speed": [
+ "40"
+ ],
+ "filament_start_gcode": [
+ "; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..da5d4af50a
--- /dev/null
+++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json
@@ -0,0 +1,65 @@
+{
+ "type": "filament",
+ "name": "Generic PLA @IdeaFormer IR3 V2",
+ "inherits": "Generic PLA @System",
+ "from": "system",
+ "setting_id": "1xjycsEAFh6KQIhp",
+ "instantiation": "true",
+ "filament_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "compatible_printers": [
+ "IdeaFormer IR3 V2 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PLA"
+ ],
+ "filament_vendor": [
+ "Generic"
+ ],
+ "filament_settings_id": [
+ "Generic PLA @IdeaFormer IR3 V2"
+ ],
+ "nozzle_temperature": [
+ "215"
+ ],
+ "hot_plate_temp": [
+ "75"
+ ],
+ "hot_plate_temp_initial_layer": [
+ "75"
+ ],
+ "cool_plate_temp": [
+ "75"
+ ],
+ "cool_plate_temp_initial_layer": [
+ "75"
+ ],
+ "textured_plate_temp": [
+ "75"
+ ],
+ "textured_plate_temp_initial_layer": [
+ "75"
+ ],
+ "close_fan_the_first_x_layers": [
+ "3"
+ ],
+ "full_fan_speed_layer": [
+ "8"
+ ],
+ "slow_down_min_speed": [
+ "20"
+ ],
+ "filament_retraction_length": [
+ "1.5"
+ ],
+ "filament_retraction_speed": [
+ "35"
+ ],
+ "filament_deretraction_speed": [
+ "30"
+ ],
+ "filament_start_gcode": [
+ "; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..b39a4c22fc
--- /dev/null
+++ b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json
@@ -0,0 +1,36 @@
+{
+ "type": "filament",
+ "name": "eSUN PLA @IdeaFormer IR3 V2",
+ "inherits": "Generic PLA @IdeaFormer IR3 V2",
+ "from": "system",
+ "setting_id": "XqkviBmFHEglXueX",
+ "filament_id": "OFkrxQC4",
+ "instantiation": "true",
+ "compatible_printers": [
+ "IdeaFormer IR3 V2 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PLA"
+ ],
+ "filament_vendor": [
+ "eSUN"
+ ],
+ "filament_settings_id": [
+ "eSUN PLA @IdeaFormer IR3 V2"
+ ],
+ "nozzle_temperature_initial_layer": [
+ "200"
+ ],
+ "nozzle_temperature": [
+ "200"
+ ],
+ "enable_pressure_advance": [
+ "1"
+ ],
+ "pressure_advance": [
+ "0.12"
+ ],
+ "filament_max_volumetric_speed": [
+ "20"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json
new file mode 100644
index 0000000000..f993631921
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json
@@ -0,0 +1,98 @@
+{
+ "type": "machine",
+ "name": "IdeaFormer IR3 V2 0.4 nozzle",
+ "inherits": "fdm_belt_common",
+ "from": "system",
+ "setting_id": "MDQZgwRgg72lmjtu",
+ "instantiation": "true",
+ "printer_model": "IdeaFormer IR3 V2",
+ "printer_variant": "0.4",
+ "nozzle_diameter": [
+ "0.4"
+ ],
+ "printable_area": [
+ "0x0",
+ "250x0",
+ "250x2000",
+ "0x2000"
+ ],
+ "printable_height": "250",
+ "belt_printer_infinite_y": "1",
+ "thumbnails": [
+ "48x48/PNG",
+ "300x300/PNG"
+ ],
+ "default_filament_profile": [
+ "Generic PLA @IdeaFormer IR3 V2"
+ ],
+ "default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2",
+ "use_relative_e_distances": "1",
+ "machine_max_acceleration_extruding": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_retracting": [
+ "1000",
+ "1000"
+ ],
+ "machine_max_acceleration_travel": [
+ "9000",
+ "9000"
+ ],
+ "machine_max_acceleration_x": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_y": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_z": [
+ "100",
+ "100"
+ ],
+ "machine_max_jerk_x": [
+ "10",
+ "10"
+ ],
+ "machine_max_jerk_y": [
+ "10",
+ "10"
+ ],
+ "machine_max_jerk_z": [
+ "0.4",
+ "0.4"
+ ],
+ "machine_max_speed_e": [
+ "60",
+ "60"
+ ],
+ "machine_max_speed_x": [
+ "500",
+ "500"
+ ],
+ "machine_max_speed_y": [
+ "500",
+ "500"
+ ],
+ "machine_max_speed_z": [
+ "20",
+ "20"
+ ],
+ "retraction_length": [
+ "2"
+ ],
+ "retraction_speed": [
+ "40"
+ ],
+ "deretraction_speed": [
+ "40"
+ ],
+ "retract_lift_below": [
+ "300"
+ ],
+ "machine_start_gcode": "; === IdeaFormer IR3 V2 Belt Printer Start ===\n; Axes: X=lateral, Y=gantry height (probe), Z=belt\nG90 ; absolute positioning\nM82 ; absolute extruder\nG21 ; millimeters\nG28 ; home all axes\nG1 Y20 F500 ; lift nozzle 20mm from belt\n; Bed + hotend temps come from the active filament profile. Belt PLA requires 75 C bed — use Generic/eSun PLA @IdeaFormer IR3 V2 filament presets to get it automatically.\nM140 S[hot_plate_temp_initial_layer] ; set bed temp\nM104 S[nozzle_temperature_initial_layer] ; hotend temp\nM109 S[nozzle_temperature_initial_layer] ; wait hotend\nM190 S[hot_plate_temp_initial_layer] ; wait bed\n; --- Purge blob ---\nG92 E0 ; zero extruder\nG1 Y.1 ; nozzle 0.1mm above belt\nG1 E15 F1000 ; purge 15mm blob\nG1 Z20 E25 F800 ; belt advance 20mm + extrude\nG1 E23 ; retract 2mm\nG28 Y ; re-probe belt surface\nG1 E25 ; de-retract\n; --- Prime lines (full 250mm bed width) ---\nFMS_on ; filament motion sensor\nG1 X250 E50 F2000 ; prime line 1\nG92 Z0 ; reset belt origin\nG1 Z.4 ; belt advance 0.4mm\nG1 X0 E75 ; prime line 2\nG1 F1000 ; default feedrate\nG92 E0 Z0 ; zero extruder + belt = print origin\n",
+ "machine_end_gcode": "; === IdeaFormer IR3 V2 Belt Printer End ===\nM400 ; wait for moves to finish\nM104 S0 ; heater off\nM140 S0 ; bed off\nG92 E0 ; zero extruder\nG1 E-5 F300 ; retract 5mm\nG4 P5000 ; wait for ooze\nG91 ; relative mode - keep every end move relative on a belt\nG1 Y20 F1000 ; raise gantry 20mm for clearance over the part\nG1 Z676 F3000 ; advance belt one full machine-depth to eject the part and clean the belt\nG90 ; back to absolute\nG28 X ; home X only - NEVER 'G28' all: that homes Z/belt and reverses the whole print back into the gantry\nFMS_off ; filament motion sensor off\nBED_MESH_CLEAR\nM84 ; disable motors\n",
+ "machine_pause_gcode": "PAUSE",
+ "layer_change_gcode": "G92 E0 ; belt: reset extruder at layer change (relative E)"
+}
diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..bc6d1a6e77
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json
@@ -0,0 +1,12 @@
+{
+ "type": "machine_model",
+ "name": "IdeaFormer IR3 V2",
+ "model_id": "IdeaFormer_IR3_V2",
+ "nozzle_diameter": "0.4",
+ "machine_tech": "FFF",
+ "family": "IdeaFormer",
+ "bed_model": "",
+ "bed_texture": "",
+ "hotend_model": "",
+ "default_materials": "Generic PLA @IdeaFormer IR3 V2;Generic PETG @IdeaFormer IR3 V2"
+}
diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json
new file mode 100644
index 0000000000..aacd36e118
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json
@@ -0,0 +1,98 @@
+{
+ "type": "machine",
+ "name": "fdm_belt_common",
+ "inherits": "fdm_klipper_common",
+ "from": "system",
+ "instantiation": "false",
+ "gcode_flavor": "klipper",
+ "single_extruder_multi_material": "0",
+ "default_filament_profile": [
+ "Generic PLA @System"
+ ],
+ "default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2",
+ "max_layer_height": [
+ "0.32"
+ ],
+ "min_layer_height": [
+ "0.08"
+ ],
+ "deretraction_speed": [
+ "30"
+ ],
+ "extruder_colour": [
+ "#FCE94F"
+ ],
+ "extruder_offset": [
+ "0x0"
+ ],
+ "long_retractions_when_cut": [
+ "0"
+ ],
+ "nozzle_diameter": [
+ "0.4"
+ ],
+ "retract_before_wipe": [
+ "70%"
+ ],
+ "retract_length_toolchange": [
+ "2"
+ ],
+ "retract_lift_above": [
+ "0"
+ ],
+ "retract_lift_below": [
+ "0"
+ ],
+ "retract_lift_enforce": [
+ "All Surfaces"
+ ],
+ "retract_restart_extra": [
+ "0"
+ ],
+ "retract_restart_extra_toolchange": [
+ "0"
+ ],
+ "retract_when_changing_layer": [
+ "1"
+ ],
+ "retraction_distances_when_cut": [
+ "18"
+ ],
+ "retraction_length": [
+ "0.8"
+ ],
+ "retraction_minimum_travel": [
+ "1"
+ ],
+ "retraction_speed": [
+ "30"
+ ],
+ "travel_slope": [
+ "3"
+ ],
+ "wipe": [
+ "1"
+ ],
+ "wipe_distance": [
+ "1"
+ ],
+ "z_hop": [
+ "0"
+ ],
+ "z_hop_types": [
+ "Normal Lift"
+ ],
+ "gcode_remap_x": "rev_x",
+ "gcode_remap_y": "pos_z",
+ "gcode_remap_z": "pos_y",
+ "printer_extruder_id": [
+ "1"
+ ],
+ "belt_printer": "1",
+ "belt_slice_rotation": "x",
+ "belt_slice_rotation_angle": "45",
+ "build_plate_tilt_x": "45",
+ "purge_in_prime_tower": "0",
+ "scan_first_layer": "0",
+ "auxiliary_fan": "0"
+}
diff --git a/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json
new file mode 100644
index 0000000000..5297f646ff
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json
@@ -0,0 +1,140 @@
+{
+ "type": "machine",
+ "name": "fdm_klipper_common",
+ "inherits": "fdm_machine_common",
+ "from": "system",
+ "instantiation": "false",
+ "gcode_flavor": "klipper",
+ "machine_max_acceleration_e": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_extruding": [
+ "20000",
+ "20000"
+ ],
+ "machine_max_acceleration_retracting": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_travel": [
+ "20000",
+ "20000"
+ ],
+ "machine_max_acceleration_x": [
+ "20000",
+ "20000"
+ ],
+ "machine_max_acceleration_y": [
+ "20000",
+ "20000"
+ ],
+ "machine_max_acceleration_z": [
+ "500",
+ "200"
+ ],
+ "machine_max_speed_e": [
+ "25",
+ "25"
+ ],
+ "machine_max_speed_x": [
+ "500",
+ "200"
+ ],
+ "machine_max_speed_y": [
+ "500",
+ "200"
+ ],
+ "machine_max_speed_z": [
+ "12",
+ "12"
+ ],
+ "machine_max_jerk_e": [
+ "2.5",
+ "2.5"
+ ],
+ "machine_max_jerk_x": [
+ "9",
+ "9"
+ ],
+ "machine_max_jerk_y": [
+ "9",
+ "9"
+ ],
+ "machine_max_jerk_z": [
+ "0.2",
+ "0.4"
+ ],
+ "machine_min_extruding_rate": [
+ "0",
+ "0"
+ ],
+ "machine_min_travel_rate": [
+ "0",
+ "0"
+ ],
+ "max_layer_height": [
+ "0.32"
+ ],
+ "min_layer_height": [
+ "0.08"
+ ],
+ "printable_height": "250",
+ "extruder_clearance_radius": "65",
+ "extruder_clearance_height_to_rod": "36",
+ "extruder_clearance_height_to_lid": "140",
+ "printer_settings_id": "",
+ "printer_technology": "FFF",
+ "printer_variant": "0.4",
+ "retraction_minimum_travel": [
+ "1"
+ ],
+ "retract_before_wipe": [
+ "70%"
+ ],
+ "retract_when_changing_layer": [
+ "1"
+ ],
+ "retraction_length": [
+ "0.8"
+ ],
+ "retract_length_toolchange": [
+ "2"
+ ],
+ "z_hop": [
+ "0.4"
+ ],
+ "retract_restart_extra": [
+ "0"
+ ],
+ "retract_restart_extra_toolchange": [
+ "0"
+ ],
+ "retraction_speed": [
+ "30"
+ ],
+ "deretraction_speed": [
+ "30"
+ ],
+ "z_hop_types": "Normal Lift",
+ "single_extruder_multi_material": "1",
+ "change_filament_gcode": "",
+ "wipe": [
+ "1"
+ ],
+ "default_filament_profile": [
+ "Generic PLA @System"
+ ],
+ "default_print_profile": "0.20mm Standard @MyKlipper",
+ "bed_exclude_area": [
+ "0x0"
+ ],
+ "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n",
+ "machine_end_gcode": "PRINT_END",
+ "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
+ "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
+ "machine_pause_gcode": "PAUSE",
+ "scan_first_layer": "0",
+ "nozzle_type": "undefine",
+ "auxiliary_fan": "0"
+}
diff --git a/resources/profiles/IdeaFormer/machine/fdm_machine_common.json b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json
new file mode 100644
index 0000000000..bf8f2249cf
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json
@@ -0,0 +1,118 @@
+{
+ "type": "machine",
+ "name": "fdm_machine_common",
+ "from": "system",
+ "instantiation": "false",
+ "printer_technology": "FFF",
+ "deretraction_speed": [
+ "40"
+ ],
+ "extruder_colour": [
+ "#FCE94F"
+ ],
+ "extruder_offset": [
+ "0x0"
+ ],
+ "gcode_flavor": "marlin",
+ "machine_max_acceleration_e": [
+ "5000"
+ ],
+ "machine_max_acceleration_extruding": [
+ "10000"
+ ],
+ "machine_max_acceleration_retracting": [
+ "1000"
+ ],
+ "machine_max_acceleration_x": [
+ "10000"
+ ],
+ "machine_max_acceleration_y": [
+ "10000"
+ ],
+ "machine_max_acceleration_z": [
+ "500"
+ ],
+ "machine_max_speed_e": [
+ "60"
+ ],
+ "machine_max_speed_x": [
+ "500"
+ ],
+ "machine_max_speed_y": [
+ "500"
+ ],
+ "machine_max_speed_z": [
+ "10"
+ ],
+ "machine_max_jerk_e": [
+ "5"
+ ],
+ "machine_max_jerk_x": [
+ "8"
+ ],
+ "machine_max_jerk_y": [
+ "8"
+ ],
+ "machine_max_jerk_z": [
+ "0.4"
+ ],
+ "machine_min_extruding_rate": [
+ "0"
+ ],
+ "machine_min_travel_rate": [
+ "0"
+ ],
+ "max_layer_height": [
+ "0.32"
+ ],
+ "min_layer_height": [
+ "0.08"
+ ],
+ "printable_height": "250",
+ "extruder_clearance_radius": "65",
+ "extruder_clearance_height_to_rod": "36",
+ "extruder_clearance_height_to_lid": "140",
+ "nozzle_diameter": [
+ "0.4"
+ ],
+ "printer_settings_id": "",
+ "printer_variant": "0.4",
+ "retraction_minimum_travel": [
+ "2"
+ ],
+ "retract_before_wipe": [
+ "70%"
+ ],
+ "retract_when_changing_layer": [
+ "1"
+ ],
+ "retraction_length": [
+ "1"
+ ],
+ "retract_length_toolchange": [
+ "1"
+ ],
+ "z_hop": [
+ "0"
+ ],
+ "retract_restart_extra": [
+ "0"
+ ],
+ "retract_restart_extra_toolchange": [
+ "0"
+ ],
+ "retraction_speed": [
+ "60"
+ ],
+ "single_extruder_multi_material": "1",
+ "change_filament_gcode": "",
+ "wipe": [
+ "1"
+ ],
+ "default_print_profile": "",
+ "machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up",
+ "machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end",
+ "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
+ "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
+ "machine_pause_gcode": "M601"
+}
diff --git a/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..c2e46e4154
--- /dev/null
+++ b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json
@@ -0,0 +1,23 @@
+{
+ "type": "process",
+ "name": "0.20mm Standard @IdeaFormer IR3 V2",
+ "inherits": "fdm_process_common",
+ "from": "system",
+ "setting_id": "91atcIwv5728phqX",
+ "instantiation": "true",
+ "layer_height": "0.2",
+ "initial_layer_print_height": "0.2",
+ "initial_layer_line_width": "0.42",
+ "wall_loops": "2",
+ "reduce_infill_retraction": "1",
+ "detect_overhang_wall": "1",
+ "skirt_loops": "0",
+ "skirt_distance": "0",
+ "sparse_infill_pattern": "grid",
+ "sparse_infill_speed": "200",
+ "support_base_pattern": "rectilinear",
+ "support_interface_pattern": "rectilinear",
+ "compatible_printers": [
+ "IdeaFormer IR3 V2 0.4 nozzle"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer/process/fdm_process_common.json b/resources/profiles/IdeaFormer/process/fdm_process_common.json
new file mode 100644
index 0000000000..0d558129af
--- /dev/null
+++ b/resources/profiles/IdeaFormer/process/fdm_process_common.json
@@ -0,0 +1,106 @@
+{
+ "type": "process",
+ "name": "fdm_process_common",
+ "from": "system",
+ "instantiation": "false",
+ "reduce_crossing_wall": "0",
+ "max_travel_detour_distance": "0",
+ "bottom_surface_pattern": "monotonic",
+ "bottom_shell_thickness": "0",
+ "bridge_speed": "50",
+ "brim_width": "5",
+ "brim_object_gap": "0.1",
+ "compatible_printers": [],
+ "compatible_printers_condition": "",
+ "print_sequence": "by layer",
+ "default_acceleration": "1000",
+ "initial_layer_acceleration": "500",
+ "top_surface_acceleration": "1000",
+ "travel_acceleration": "1000",
+ "inner_wall_acceleration": "1000",
+ "outer_wall_acceleration": "700",
+ "bridge_no_support": "0",
+ "draft_shield": "disabled",
+ "elefant_foot_compensation": "0",
+ "enable_arc_fitting": "0",
+ "wall_infill_order": "inner wall/outer wall/infill",
+ "infill_direction": "45",
+ "sparse_infill_density": "15%",
+ "sparse_infill_pattern": "crosshatch",
+ "initial_layer_print_height": "0.2",
+ "infill_combination": "0",
+ "infill_wall_overlap": "25%",
+ "interface_shells": "0",
+ "ironing_flow": "10%",
+ "ironing_spacing": "0.15",
+ "ironing_speed": "30",
+ "ironing_type": "no ironing",
+ "reduce_infill_retraction": "1",
+ "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode",
+ "detect_overhang_wall": "1",
+ "slowdown_for_curled_perimeters": "1",
+ "overhang_1_4_speed": "0",
+ "overhang_2_4_speed": "50",
+ "overhang_3_4_speed": "30",
+ "overhang_4_4_speed": "10",
+ "line_width": "110%",
+ "inner_wall_line_width": "110%",
+ "outer_wall_line_width": "100%",
+ "top_surface_line_width": "93.75%",
+ "sparse_infill_line_width": "110%",
+ "initial_layer_line_width": "120%",
+ "internal_solid_infill_line_width": "120%",
+ "support_line_width": "96%",
+ "wall_loops": "3",
+ "print_settings_id": "",
+ "raft_layers": "0",
+ "seam_position": "aligned",
+ "skirt_distance": "2",
+ "skirt_height": "3",
+ "min_skirt_length": "4",
+ "skirt_loops": "0",
+ "minimum_sparse_infill_area": "15",
+ "spiral_mode": "0",
+ "standby_temperature_delta": "-5",
+ "enable_support": "0",
+ "resolution": "0.012",
+ "support_type": "normal(auto)",
+ "support_on_build_plate_only": "0",
+ "support_top_z_distance": "0.2",
+ "support_bottom_z_distance": "0.2",
+ "support_filament": "0",
+ "support_interface_loop_pattern": "0",
+ "support_interface_filament": "0",
+ "support_interface_top_layers": "2",
+ "support_interface_bottom_layers": "2",
+ "support_interface_spacing": "0.5",
+ "support_interface_speed": "80",
+ "support_base_pattern": "default",
+ "support_base_pattern_spacing": "2.5",
+ "support_speed": "150",
+ "support_threshold_angle": "30",
+ "support_object_xy_distance": "0.35",
+ "tree_support_branch_angle": "30",
+ "tree_support_wall_count": "0",
+ "detect_thin_wall": "0",
+ "top_surface_pattern": "monotonicline",
+ "top_shell_thickness": "0.8",
+ "enable_prime_tower": "1",
+ "wipe_tower_no_sparse_layers": "0",
+ "prime_tower_width": "60",
+ "xy_hole_compensation": "0",
+ "xy_contour_compensation": "0",
+ "layer_height": "0.2",
+ "bottom_shell_layers": "3",
+ "top_shell_layers": "4",
+ "bridge_flow": "1",
+ "initial_layer_speed": "45",
+ "initial_layer_infill_speed": "45",
+ "outer_wall_speed": "45",
+ "inner_wall_speed": "80",
+ "sparse_infill_speed": "150",
+ "internal_solid_infill_speed": "150",
+ "top_surface_speed": "50",
+ "gap_infill_speed": "30",
+ "travel_speed": "200"
+}
diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json
new file mode 100644
index 0000000000..70338174d6
--- /dev/null
+++ b/resources/profiles/Printcepts.json
@@ -0,0 +1,54 @@
+{
+ "name": "Printcepts",
+ "version": "01.00.00.04",
+ "force_update": "0",
+ "description": "Printcepts belt printer configurations",
+ "machine_model_list": [
+ {
+ "name": "BabyBelt Pro",
+ "sub_path": "machine/BabyBelt Pro.json"
+ }
+ ],
+ "process_list": [
+ {
+ "name": "fdm_process_common",
+ "sub_path": "process/fdm_process_common.json"
+ },
+ {
+ "name": "0.20mm Standard @BabyBelt Pro",
+ "sub_path": "process/0.20mm Standard @BabyBelt Pro.json"
+ }
+ ],
+ "filament_list": [
+ {
+ "name": "Generic PLA @BabyBelt Pro",
+ "sub_path": "filament/Generic PLA @BabyBelt Pro.json"
+ },
+ {
+ "name": "eSUN PLA @BabyBelt Pro",
+ "sub_path": "filament/eSUN PLA @BabyBelt Pro.json"
+ },
+ {
+ "name": "Generic PETG @BabyBelt Pro",
+ "sub_path": "filament/Generic PETG @BabyBelt Pro.json"
+ }
+ ],
+ "machine_list": [
+ {
+ "name": "fdm_machine_common",
+ "sub_path": "machine/fdm_machine_common.json"
+ },
+ {
+ "name": "fdm_klipper_common",
+ "sub_path": "machine/fdm_klipper_common.json"
+ },
+ {
+ "name": "fdm_belt_common",
+ "sub_path": "machine/fdm_belt_common.json"
+ },
+ {
+ "name": "BabyBelt Pro 0.4 nozzle",
+ "sub_path": "machine/BabyBelt Pro 0.4 nozzle.json"
+ }
+ ]
+}
diff --git a/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg b/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg
new file mode 100644
index 0000000000..3193c6a284
--- /dev/null
+++ b/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg
@@ -0,0 +1,70 @@
+
+
diff --git a/resources/profiles/Printcepts/BabyBelt Pro_cover.png b/resources/profiles/Printcepts/BabyBelt Pro_cover.png
new file mode 100644
index 0000000000..aaf5dd266f
Binary files /dev/null and b/resources/profiles/Printcepts/BabyBelt Pro_cover.png differ
diff --git a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json
new file mode 100644
index 0000000000..9b71a420b6
--- /dev/null
+++ b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json
@@ -0,0 +1,77 @@
+{
+ "type": "filament",
+ "name": "Generic PETG @BabyBelt Pro",
+ "inherits": "Generic PETG @System",
+ "from": "system",
+ "setting_id": "gCzHpDNgVwQR6tgk",
+ "instantiation": "true",
+ "filament_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "compatible_printers": [
+ "BabyBelt Pro 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PETG"
+ ],
+ "filament_vendor": [
+ "Generic"
+ ],
+ "filament_settings_id": [
+ "Generic PETG @BabyBelt Pro"
+ ],
+ "filament_flow_ratio": [
+ "0.95"
+ ],
+ "filament_cost": [
+ "25"
+ ],
+ "nozzle_temperature": [
+ "240"
+ ],
+ "nozzle_temperature_initial_layer": [
+ "245"
+ ],
+ "cool_plate_temp": [
+ "80"
+ ],
+ "cool_plate_temp_initial_layer": [
+ "80"
+ ],
+ "fan_min_speed": [
+ "40"
+ ],
+ "fan_max_speed": [
+ "60"
+ ],
+ "overhang_fan_threshold": [
+ "25%"
+ ],
+ "overhang_fan_speed": [
+ "80"
+ ],
+ "full_fan_speed_layer": [
+ "8"
+ ],
+ "slow_down_min_speed": [
+ "20"
+ ],
+ "slow_down_layer_time": [
+ "4"
+ ],
+ "fan_cooling_layer_time": [
+ "100"
+ ],
+ "filament_retraction_length": [
+ "2"
+ ],
+ "filament_retraction_speed": [
+ "40"
+ ],
+ "filament_deretraction_speed": [
+ "40"
+ ],
+ "filament_start_gcode": [
+ "; Generic PETG @BabyBelt Pro — belt PETG, bed 80C"
+ ]
+}
diff --git a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json
new file mode 100644
index 0000000000..3345b5f509
--- /dev/null
+++ b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json
@@ -0,0 +1,65 @@
+{
+ "type": "filament",
+ "name": "Generic PLA @BabyBelt Pro",
+ "inherits": "Generic PLA @System",
+ "from": "system",
+ "setting_id": "24PpcnhVx9v5f4fD",
+ "instantiation": "true",
+ "filament_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "compatible_printers": [
+ "BabyBelt Pro 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PLA"
+ ],
+ "filament_vendor": [
+ "Generic"
+ ],
+ "filament_settings_id": [
+ "Generic PLA @BabyBelt Pro"
+ ],
+ "nozzle_temperature": [
+ "215"
+ ],
+ "hot_plate_temp": [
+ "75"
+ ],
+ "hot_plate_temp_initial_layer": [
+ "75"
+ ],
+ "cool_plate_temp": [
+ "75"
+ ],
+ "cool_plate_temp_initial_layer": [
+ "75"
+ ],
+ "textured_plate_temp": [
+ "75"
+ ],
+ "textured_plate_temp_initial_layer": [
+ "75"
+ ],
+ "close_fan_the_first_x_layers": [
+ "3"
+ ],
+ "full_fan_speed_layer": [
+ "8"
+ ],
+ "slow_down_min_speed": [
+ "20"
+ ],
+ "filament_retraction_length": [
+ "1.5"
+ ],
+ "filament_retraction_speed": [
+ "35"
+ ],
+ "filament_deretraction_speed": [
+ "30"
+ ],
+ "filament_start_gcode": [
+ "; Generic PLA @BabyBelt Pro — belt PLA, bed 75C"
+ ]
+}
diff --git a/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json
new file mode 100644
index 0000000000..1f93703e1c
--- /dev/null
+++ b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json
@@ -0,0 +1,36 @@
+{
+ "type": "filament",
+ "name": "eSUN PLA @BabyBelt Pro",
+ "inherits": "Generic PLA @BabyBelt Pro",
+ "from": "system",
+ "setting_id": "EH3X7oE0DU5tSpjW",
+ "filament_id": "OFkrxQC4",
+ "instantiation": "true",
+ "compatible_printers": [
+ "BabyBelt Pro 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PLA"
+ ],
+ "filament_vendor": [
+ "eSUN"
+ ],
+ "filament_settings_id": [
+ "eSUN PLA @BabyBelt Pro"
+ ],
+ "nozzle_temperature_initial_layer": [
+ "200"
+ ],
+ "nozzle_temperature": [
+ "200"
+ ],
+ "enable_pressure_advance": [
+ "1"
+ ],
+ "pressure_advance": [
+ "0.12"
+ ],
+ "filament_max_volumetric_speed": [
+ "20"
+ ]
+}
diff --git a/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json
new file mode 100644
index 0000000000..84218476cb
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json
@@ -0,0 +1,88 @@
+{
+ "type": "machine",
+ "name": "BabyBelt Pro 0.4 nozzle",
+ "inherits": "fdm_belt_common",
+ "from": "system",
+ "setting_id": "34OWINlJpJgA9DwQ",
+ "instantiation": "true",
+ "printer_model": "BabyBelt Pro",
+ "printer_variant": "0.4",
+ "nozzle_diameter": [
+ "0.4"
+ ],
+ "default_filament_profile": [
+ "Generic PLA @BabyBelt Pro"
+ ],
+ "default_print_profile": "0.20mm Standard @BabyBelt Pro",
+ "printable_area": [
+ "0x0",
+ "95x0",
+ "95x500",
+ "0x500"
+ ],
+ "printable_height": "100",
+ "best_object_pos": "0.5,0.05",
+ "nozzle_type": [
+ "hardened_steel"
+ ],
+ "printer_extruder_id": [
+ "1"
+ ],
+ "printer_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "thumbnails": [
+ "48x48/PNG",
+ "300x300/PNG"
+ ],
+ "machine_max_acceleration_e": [
+ "500",
+ "5000"
+ ],
+ "machine_max_acceleration_extruding": [
+ "500",
+ "20000"
+ ],
+ "machine_max_acceleration_retracting": [
+ "500",
+ "5000"
+ ],
+ "machine_max_acceleration_x": [
+ "500",
+ "20000"
+ ],
+ "machine_max_acceleration_y": [
+ "500",
+ "20000"
+ ],
+ "machine_max_junction_deviation": [
+ "0.01",
+ "0.01"
+ ],
+ "machine_max_speed_x": [
+ "50",
+ "200"
+ ],
+ "machine_max_speed_y": [
+ "50",
+ "200"
+ ],
+ "machine_max_speed_z": [
+ "5",
+ "12"
+ ],
+ "retraction_length": [
+ "1.5"
+ ],
+ "retraction_speed": [
+ "20"
+ ],
+ "deretraction_speed": [
+ "25"
+ ],
+ "retract_lift_enforce": [
+ "Top and Bottom"
+ ],
+ "support_chamber_temp_control": "0",
+ "machine_start_gcode": ";Start GCode\nPRINT_START ANGLE=[belt_slice_rotation_angle] EXTRUDER=[nozzle_temperature_initial_layer] BED=[hot_plate_temp_initial_layer] MATERIAL=[filament_type]\n"
+}
diff --git a/resources/profiles/Printcepts/machine/BabyBelt Pro.json b/resources/profiles/Printcepts/machine/BabyBelt Pro.json
new file mode 100644
index 0000000000..eb711541bd
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/BabyBelt Pro.json
@@ -0,0 +1,12 @@
+{
+ "type": "machine_model",
+ "name": "BabyBelt Pro",
+ "model_id": "Printcepts_BabyBelt_Pro",
+ "nozzle_diameter": "0.4",
+ "machine_tech": "FFF",
+ "family": "Printcepts",
+ "bed_model": "",
+ "bed_texture": "BabyBelt Pro_bed_texture.svg",
+ "hotend_model": "",
+ "default_materials": "Generic PLA @BabyBelt Pro;Generic PETG @BabyBelt Pro"
+}
diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json
new file mode 100644
index 0000000000..96d38ac31a
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json
@@ -0,0 +1,98 @@
+{
+ "type": "machine",
+ "name": "fdm_belt_common",
+ "inherits": "fdm_klipper_common",
+ "from": "system",
+ "instantiation": "false",
+ "gcode_flavor": "klipper",
+ "single_extruder_multi_material": "0",
+ "default_filament_profile": [
+ "Generic PLA @System"
+ ],
+ "default_print_profile": "0.20mm Standard @BabyBelt Pro",
+ "max_layer_height": [
+ "0.32"
+ ],
+ "min_layer_height": [
+ "0.08"
+ ],
+ "deretraction_speed": [
+ "30"
+ ],
+ "extruder_colour": [
+ "#FCE94F"
+ ],
+ "extruder_offset": [
+ "0x0"
+ ],
+ "long_retractions_when_cut": [
+ "0"
+ ],
+ "nozzle_diameter": [
+ "0.4"
+ ],
+ "retract_before_wipe": [
+ "70%"
+ ],
+ "retract_length_toolchange": [
+ "2"
+ ],
+ "retract_lift_above": [
+ "0"
+ ],
+ "retract_lift_below": [
+ "0"
+ ],
+ "retract_lift_enforce": [
+ "All Surfaces"
+ ],
+ "retract_restart_extra": [
+ "0"
+ ],
+ "retract_restart_extra_toolchange": [
+ "0"
+ ],
+ "retract_when_changing_layer": [
+ "1"
+ ],
+ "retraction_distances_when_cut": [
+ "18"
+ ],
+ "retraction_length": [
+ "0.8"
+ ],
+ "retraction_minimum_travel": [
+ "1"
+ ],
+ "retraction_speed": [
+ "30"
+ ],
+ "travel_slope": [
+ "3"
+ ],
+ "wipe": [
+ "1"
+ ],
+ "wipe_distance": [
+ "1"
+ ],
+ "z_hop": [
+ "0"
+ ],
+ "z_hop_types": [
+ "Normal Lift"
+ ],
+ "gcode_remap_x": "rev_x",
+ "gcode_remap_y": "pos_z",
+ "gcode_remap_z": "pos_y",
+ "printer_extruder_id": [
+ "1"
+ ],
+ "belt_printer": "1",
+ "belt_slice_rotation": "x",
+ "belt_slice_rotation_angle": "45",
+ "build_plate_tilt_x": "45",
+ "purge_in_prime_tower": "0",
+ "scan_first_layer": "0",
+ "auxiliary_fan": "0"
+}
diff --git a/resources/profiles/Printcepts/machine/fdm_klipper_common.json b/resources/profiles/Printcepts/machine/fdm_klipper_common.json
new file mode 100644
index 0000000000..5297f646ff
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/fdm_klipper_common.json
@@ -0,0 +1,140 @@
+{
+ "type": "machine",
+ "name": "fdm_klipper_common",
+ "inherits": "fdm_machine_common",
+ "from": "system",
+ "instantiation": "false",
+ "gcode_flavor": "klipper",
+ "machine_max_acceleration_e": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_extruding": [
+ "20000",
+ "20000"
+ ],
+ "machine_max_acceleration_retracting": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_travel": [
+ "20000",
+ "20000"
+ ],
+ "machine_max_acceleration_x": [
+ "20000",
+ "20000"
+ ],
+ "machine_max_acceleration_y": [
+ "20000",
+ "20000"
+ ],
+ "machine_max_acceleration_z": [
+ "500",
+ "200"
+ ],
+ "machine_max_speed_e": [
+ "25",
+ "25"
+ ],
+ "machine_max_speed_x": [
+ "500",
+ "200"
+ ],
+ "machine_max_speed_y": [
+ "500",
+ "200"
+ ],
+ "machine_max_speed_z": [
+ "12",
+ "12"
+ ],
+ "machine_max_jerk_e": [
+ "2.5",
+ "2.5"
+ ],
+ "machine_max_jerk_x": [
+ "9",
+ "9"
+ ],
+ "machine_max_jerk_y": [
+ "9",
+ "9"
+ ],
+ "machine_max_jerk_z": [
+ "0.2",
+ "0.4"
+ ],
+ "machine_min_extruding_rate": [
+ "0",
+ "0"
+ ],
+ "machine_min_travel_rate": [
+ "0",
+ "0"
+ ],
+ "max_layer_height": [
+ "0.32"
+ ],
+ "min_layer_height": [
+ "0.08"
+ ],
+ "printable_height": "250",
+ "extruder_clearance_radius": "65",
+ "extruder_clearance_height_to_rod": "36",
+ "extruder_clearance_height_to_lid": "140",
+ "printer_settings_id": "",
+ "printer_technology": "FFF",
+ "printer_variant": "0.4",
+ "retraction_minimum_travel": [
+ "1"
+ ],
+ "retract_before_wipe": [
+ "70%"
+ ],
+ "retract_when_changing_layer": [
+ "1"
+ ],
+ "retraction_length": [
+ "0.8"
+ ],
+ "retract_length_toolchange": [
+ "2"
+ ],
+ "z_hop": [
+ "0.4"
+ ],
+ "retract_restart_extra": [
+ "0"
+ ],
+ "retract_restart_extra_toolchange": [
+ "0"
+ ],
+ "retraction_speed": [
+ "30"
+ ],
+ "deretraction_speed": [
+ "30"
+ ],
+ "z_hop_types": "Normal Lift",
+ "single_extruder_multi_material": "1",
+ "change_filament_gcode": "",
+ "wipe": [
+ "1"
+ ],
+ "default_filament_profile": [
+ "Generic PLA @System"
+ ],
+ "default_print_profile": "0.20mm Standard @MyKlipper",
+ "bed_exclude_area": [
+ "0x0"
+ ],
+ "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n",
+ "machine_end_gcode": "PRINT_END",
+ "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
+ "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
+ "machine_pause_gcode": "PAUSE",
+ "scan_first_layer": "0",
+ "nozzle_type": "undefine",
+ "auxiliary_fan": "0"
+}
diff --git a/resources/profiles/Printcepts/machine/fdm_machine_common.json b/resources/profiles/Printcepts/machine/fdm_machine_common.json
new file mode 100644
index 0000000000..bf8f2249cf
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/fdm_machine_common.json
@@ -0,0 +1,118 @@
+{
+ "type": "machine",
+ "name": "fdm_machine_common",
+ "from": "system",
+ "instantiation": "false",
+ "printer_technology": "FFF",
+ "deretraction_speed": [
+ "40"
+ ],
+ "extruder_colour": [
+ "#FCE94F"
+ ],
+ "extruder_offset": [
+ "0x0"
+ ],
+ "gcode_flavor": "marlin",
+ "machine_max_acceleration_e": [
+ "5000"
+ ],
+ "machine_max_acceleration_extruding": [
+ "10000"
+ ],
+ "machine_max_acceleration_retracting": [
+ "1000"
+ ],
+ "machine_max_acceleration_x": [
+ "10000"
+ ],
+ "machine_max_acceleration_y": [
+ "10000"
+ ],
+ "machine_max_acceleration_z": [
+ "500"
+ ],
+ "machine_max_speed_e": [
+ "60"
+ ],
+ "machine_max_speed_x": [
+ "500"
+ ],
+ "machine_max_speed_y": [
+ "500"
+ ],
+ "machine_max_speed_z": [
+ "10"
+ ],
+ "machine_max_jerk_e": [
+ "5"
+ ],
+ "machine_max_jerk_x": [
+ "8"
+ ],
+ "machine_max_jerk_y": [
+ "8"
+ ],
+ "machine_max_jerk_z": [
+ "0.4"
+ ],
+ "machine_min_extruding_rate": [
+ "0"
+ ],
+ "machine_min_travel_rate": [
+ "0"
+ ],
+ "max_layer_height": [
+ "0.32"
+ ],
+ "min_layer_height": [
+ "0.08"
+ ],
+ "printable_height": "250",
+ "extruder_clearance_radius": "65",
+ "extruder_clearance_height_to_rod": "36",
+ "extruder_clearance_height_to_lid": "140",
+ "nozzle_diameter": [
+ "0.4"
+ ],
+ "printer_settings_id": "",
+ "printer_variant": "0.4",
+ "retraction_minimum_travel": [
+ "2"
+ ],
+ "retract_before_wipe": [
+ "70%"
+ ],
+ "retract_when_changing_layer": [
+ "1"
+ ],
+ "retraction_length": [
+ "1"
+ ],
+ "retract_length_toolchange": [
+ "1"
+ ],
+ "z_hop": [
+ "0"
+ ],
+ "retract_restart_extra": [
+ "0"
+ ],
+ "retract_restart_extra_toolchange": [
+ "0"
+ ],
+ "retraction_speed": [
+ "60"
+ ],
+ "single_extruder_multi_material": "1",
+ "change_filament_gcode": "",
+ "wipe": [
+ "1"
+ ],
+ "default_print_profile": "",
+ "machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up",
+ "machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end",
+ "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
+ "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
+ "machine_pause_gcode": "M601"
+}
diff --git a/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json b/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json
new file mode 100644
index 0000000000..f592cfe87e
--- /dev/null
+++ b/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json
@@ -0,0 +1,23 @@
+{
+ "type": "process",
+ "name": "0.20mm Standard @BabyBelt Pro",
+ "inherits": "fdm_process_common",
+ "from": "system",
+ "setting_id": "JGfGtqX6CWjCt437",
+ "instantiation": "true",
+ "layer_height": "0.2",
+ "initial_layer_print_height": "0.2",
+ "initial_layer_line_width": "0.42",
+ "wall_loops": "2",
+ "reduce_infill_retraction": "1",
+ "detect_overhang_wall": "1",
+ "skirt_loops": "0",
+ "skirt_distance": "0",
+ "sparse_infill_pattern": "grid",
+ "sparse_infill_speed": "200",
+ "support_base_pattern": "rectilinear",
+ "support_interface_pattern": "rectilinear",
+ "compatible_printers": [
+ "BabyBelt Pro 0.4 nozzle"
+ ]
+}
diff --git a/resources/profiles/Printcepts/process/fdm_process_common.json b/resources/profiles/Printcepts/process/fdm_process_common.json
new file mode 100644
index 0000000000..0d558129af
--- /dev/null
+++ b/resources/profiles/Printcepts/process/fdm_process_common.json
@@ -0,0 +1,106 @@
+{
+ "type": "process",
+ "name": "fdm_process_common",
+ "from": "system",
+ "instantiation": "false",
+ "reduce_crossing_wall": "0",
+ "max_travel_detour_distance": "0",
+ "bottom_surface_pattern": "monotonic",
+ "bottom_shell_thickness": "0",
+ "bridge_speed": "50",
+ "brim_width": "5",
+ "brim_object_gap": "0.1",
+ "compatible_printers": [],
+ "compatible_printers_condition": "",
+ "print_sequence": "by layer",
+ "default_acceleration": "1000",
+ "initial_layer_acceleration": "500",
+ "top_surface_acceleration": "1000",
+ "travel_acceleration": "1000",
+ "inner_wall_acceleration": "1000",
+ "outer_wall_acceleration": "700",
+ "bridge_no_support": "0",
+ "draft_shield": "disabled",
+ "elefant_foot_compensation": "0",
+ "enable_arc_fitting": "0",
+ "wall_infill_order": "inner wall/outer wall/infill",
+ "infill_direction": "45",
+ "sparse_infill_density": "15%",
+ "sparse_infill_pattern": "crosshatch",
+ "initial_layer_print_height": "0.2",
+ "infill_combination": "0",
+ "infill_wall_overlap": "25%",
+ "interface_shells": "0",
+ "ironing_flow": "10%",
+ "ironing_spacing": "0.15",
+ "ironing_speed": "30",
+ "ironing_type": "no ironing",
+ "reduce_infill_retraction": "1",
+ "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode",
+ "detect_overhang_wall": "1",
+ "slowdown_for_curled_perimeters": "1",
+ "overhang_1_4_speed": "0",
+ "overhang_2_4_speed": "50",
+ "overhang_3_4_speed": "30",
+ "overhang_4_4_speed": "10",
+ "line_width": "110%",
+ "inner_wall_line_width": "110%",
+ "outer_wall_line_width": "100%",
+ "top_surface_line_width": "93.75%",
+ "sparse_infill_line_width": "110%",
+ "initial_layer_line_width": "120%",
+ "internal_solid_infill_line_width": "120%",
+ "support_line_width": "96%",
+ "wall_loops": "3",
+ "print_settings_id": "",
+ "raft_layers": "0",
+ "seam_position": "aligned",
+ "skirt_distance": "2",
+ "skirt_height": "3",
+ "min_skirt_length": "4",
+ "skirt_loops": "0",
+ "minimum_sparse_infill_area": "15",
+ "spiral_mode": "0",
+ "standby_temperature_delta": "-5",
+ "enable_support": "0",
+ "resolution": "0.012",
+ "support_type": "normal(auto)",
+ "support_on_build_plate_only": "0",
+ "support_top_z_distance": "0.2",
+ "support_bottom_z_distance": "0.2",
+ "support_filament": "0",
+ "support_interface_loop_pattern": "0",
+ "support_interface_filament": "0",
+ "support_interface_top_layers": "2",
+ "support_interface_bottom_layers": "2",
+ "support_interface_spacing": "0.5",
+ "support_interface_speed": "80",
+ "support_base_pattern": "default",
+ "support_base_pattern_spacing": "2.5",
+ "support_speed": "150",
+ "support_threshold_angle": "30",
+ "support_object_xy_distance": "0.35",
+ "tree_support_branch_angle": "30",
+ "tree_support_wall_count": "0",
+ "detect_thin_wall": "0",
+ "top_surface_pattern": "monotonicline",
+ "top_shell_thickness": "0.8",
+ "enable_prime_tower": "1",
+ "wipe_tower_no_sparse_layers": "0",
+ "prime_tower_width": "60",
+ "xy_hole_compensation": "0",
+ "xy_contour_compensation": "0",
+ "layer_height": "0.2",
+ "bottom_shell_layers": "3",
+ "top_shell_layers": "4",
+ "bridge_flow": "1",
+ "initial_layer_speed": "45",
+ "initial_layer_infill_speed": "45",
+ "outer_wall_speed": "45",
+ "inner_wall_speed": "80",
+ "sparse_infill_speed": "150",
+ "internal_solid_infill_speed": "150",
+ "top_surface_speed": "50",
+ "gap_infill_speed": "30",
+ "travel_speed": "200"
+}
diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs
index c5efeef277..50789327c5 100644
--- a/resources/shaders/110/gouraud.fs
+++ b/resources/shaders/110/gouraud.fs
@@ -26,6 +26,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform vec4 uniform_color;
diff --git a/resources/shaders/110/gouraud.vs b/resources/shaders/110/gouraud.vs
index 37be529b09..5ee685c070 100644
--- a/resources/shaders/110/gouraud.vs
+++ b/resources/shaders/110/gouraud.vs
@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -77,8 +78,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
- // z component of normal vector in world coordinate used for slope shading
- world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
+ // dot product of world normal with up direction, used for slope shading
+ world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {
diff --git a/resources/shaders/110/mm_gouraud.fs b/resources/shaders/110/mm_gouraud.fs
index 821af13f03..431c936e9c 100644
--- a/resources/shaders/110/mm_gouraud.fs
+++ b/resources/shaders/110/mm_gouraud.fs
@@ -37,6 +37,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform SlopeDetection slope;
@@ -85,7 +86,7 @@ void main()
color = LightBlue;
alpha = 1.0;
}
- else if( transformed_normal.z < slope.normal_z - EPSILON)
+ else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
{
color = color * 0.5 + LightRed * 0.5;
alpha = 1.0;
diff --git a/resources/shaders/110/mm_gouraud.vs b/resources/shaders/110/mm_gouraud.vs
index b0cea9cfd6..c54ae4bff5 100644
--- a/resources/shaders/110/mm_gouraud.vs
+++ b/resources/shaders/110/mm_gouraud.vs
@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform SlopeDetection slope;
void main()
diff --git a/resources/shaders/110/phong.fs b/resources/shaders/110/phong.fs
index a47a24fdea..7d1e7bfe8c 100644
--- a/resources/shaders/110/phong.fs
+++ b/resources/shaders/110/phong.fs
@@ -41,6 +41,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform vec4 uniform_color;
diff --git a/resources/shaders/110/phong.vs b/resources/shaders/110/phong.vs
index 10d36e233f..d857147c28 100644
--- a/resources/shaders/110/phong.vs
+++ b/resources/shaders/110/phong.vs
@@ -7,6 +7,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -46,8 +47,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
- // z component of normal vector in world coordinate used for slope shading
- world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
+ // dot product of world normal with up direction, used for slope shading
+ world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {
diff --git a/resources/shaders/110/texture_displacement_shaded.fs b/resources/shaders/110/texture_displacement_shaded.fs
index 4a2980cd64..44ea89b844 100644
--- a/resources/shaders/110/texture_displacement_shaded.fs
+++ b/resources/shaders/110/texture_displacement_shaded.fs
@@ -29,19 +29,13 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
-// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b,
-// num parts of a in every den, and the print shows that interleave rather than the entry's average
-// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does
-// per triangle on the CPU, so the preview shows the pattern the bake will print.
+// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
+// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
+// colour up.
uniform int palette_a[64];
uniform int palette_b[64];
-uniform int palette_num[64];
-uniform int palette_den[64];
uniform vec3 filament_rgb[16];
uniform int filament_count;
-uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto
-uniform float layer_height; // mm; one Z band per print layer
-uniform float dither_cell; // mm; one XY dither cell
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -229,63 +223,16 @@ int nearest_palette_entry(vec3 rgb)
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
-float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; }
-// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or
-// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the
-// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with
-// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no
-// integer % (not available on every GLSL 1.10 target).
-vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint)
+// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
+// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
+// per print layer, so there is nothing left to interleave here.
+vec3 printed_color(int index)
{
int a = palette_a[index];
- int b = palette_b[index];
- if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
+ if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
- if (a == b)
- return filament_rgb[a];
- float num = float(palette_num[index]);
- float den = float(palette_den[index]);
- // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere.
- if (mix_mode == 2 && abs(normal.z) >= 0.7)
- return filament_rgb[(num * 2.0 >= den) ? a : b];
-
- // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither
- // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned
- // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a
- // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a
- // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel,
- // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own
- // pattern is finer than this view can resolve, show what the print looks like from here, which is
- // the entry's perceptual average. The Normal view remains where the per-facet truth lives.
- float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01);
- float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z;
- float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0);
- if (sharp <= 0.0)
- return palette_rgb[index];
-
- vec3 picked;
- if (mix_mode == 1) {
- // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table
- // 0 8 2 10
- // 12 4 14 6
- // 3 11 1 9
- // 15 7 13 5
- // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has
- // no constant arrays).
- float cell = max(dither_cell, 0.01);
- float gx = mod(floor(pos.x / cell), 4.0);
- float gy = mod(floor(pos.y / cell), 4.0);
- float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0));
- picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b];
- } else {
- // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both
- // operands are integer-valued, so the half keeps "phase < num" exact under float rounding.
- float slot = floor(pos.z / max(layer_height, 0.01));
- float phase = mod(slot, den);
- picked = filament_rgb[(phase < num - 0.5) ? a : b];
- }
- return mix(palette_rgb[index], picked, sharp);
+ return filament_rgb[a];
}
void main()
@@ -296,9 +243,6 @@ void main()
// World millimetres throughout, like the bake - see the 140 variant.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
- // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the
- // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined.
- vec3 pos_fwidth = fwidth(world_pos.xyz);
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -420,6 +364,6 @@ void main()
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
- albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth);
+ albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb));
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs
index 7b315f90c0..f609495597 100644
--- a/resources/shaders/140/gouraud.fs
+++ b/resources/shaders/140/gouraud.fs
@@ -29,6 +29,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform vec4 uniform_color;
diff --git a/resources/shaders/140/gouraud.vs b/resources/shaders/140/gouraud.vs
index 11fc4b70c8..7f753a3059 100644
--- a/resources/shaders/140/gouraud.vs
+++ b/resources/shaders/140/gouraud.vs
@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -77,8 +78,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
- // z component of normal vector in world coordinate used for slope shading
- world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
+ // dot product of world normal with up direction, used for slope shading
+ world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {
diff --git a/resources/shaders/140/mm_gouraud.fs b/resources/shaders/140/mm_gouraud.fs
index c5fe86efc5..7c77d5565e 100644
--- a/resources/shaders/140/mm_gouraud.fs
+++ b/resources/shaders/140/mm_gouraud.fs
@@ -37,6 +37,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform SlopeDetection slope;
@@ -87,7 +88,7 @@ void main()
color = LightBlue;
alpha = 1.0;
}
- else if( transformed_normal.z < slope.normal_z - EPSILON)
+ else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
{
color = color * 0.5 + LightRed * 0.5;
alpha = 1.0;
diff --git a/resources/shaders/140/mm_gouraud.vs b/resources/shaders/140/mm_gouraud.vs
index b191e35fa8..191613f957 100644
--- a/resources/shaders/140/mm_gouraud.vs
+++ b/resources/shaders/140/mm_gouraud.vs
@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform SlopeDetection slope;
void main()
diff --git a/resources/shaders/140/phong.fs b/resources/shaders/140/phong.fs
index 809621b76d..4cfae1e03e 100644
--- a/resources/shaders/140/phong.fs
+++ b/resources/shaders/140/phong.fs
@@ -44,6 +44,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform vec4 uniform_color;
diff --git a/resources/shaders/140/phong.vs b/resources/shaders/140/phong.vs
index c7570edb95..7120b62c66 100644
--- a/resources/shaders/140/phong.vs
+++ b/resources/shaders/140/phong.vs
@@ -7,6 +7,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -46,8 +47,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
- // z component of normal vector in world coordinate used for slope shading
- world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
+ // dot product of world normal with up direction, used for slope shading
+ world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {
diff --git a/resources/shaders/140/texture_displacement_shaded.fs b/resources/shaders/140/texture_displacement_shaded.fs
index 1d8bee10fa..b2936598cd 100644
--- a/resources/shaders/140/texture_displacement_shaded.fs
+++ b/resources/shaders/140/texture_displacement_shaded.fs
@@ -88,19 +88,13 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
-// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b,
-// num parts of a in every den, and the print shows that interleave rather than the entry's average
-// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does
-// per triangle on the CPU, so the preview shows the pattern the bake will print.
+// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
+// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
+// colour up.
uniform int palette_a[64];
uniform int palette_b[64];
-uniform int palette_num[64];
-uniform int palette_den[64];
uniform vec3 filament_rgb[16];
uniform int filament_count;
-uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto
-uniform float layer_height; // mm; one Z band per print layer
-uniform float dither_cell; // mm; one XY dither cell
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -295,63 +289,16 @@ int nearest_palette_entry(vec3 rgb)
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
-float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; }
-// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or
-// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the
-// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with
-// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no
-// integer % (not available on every GLSL 1.10 target).
-vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint)
+// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
+// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
+// per print layer, so there is nothing left to interleave here.
+vec3 printed_color(int index)
{
int a = palette_a[index];
- int b = palette_b[index];
- if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
+ if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
- if (a == b)
- return filament_rgb[a];
- float num = float(palette_num[index]);
- float den = float(palette_den[index]);
- // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere.
- if (mix_mode == 2 && abs(normal.z) >= 0.7)
- return filament_rgb[(num * 2.0 >= den) ? a : b];
-
- // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither
- // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned
- // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a
- // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a
- // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel,
- // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own
- // pattern is finer than this view can resolve, show what the print looks like from here, which is
- // the entry's perceptual average. The Normal view remains where the per-facet truth lives.
- float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01);
- float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z;
- float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0);
- if (sharp <= 0.0)
- return palette_rgb[index];
-
- vec3 picked;
- if (mix_mode == 1) {
- // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table
- // 0 8 2 10
- // 12 4 14 6
- // 3 11 1 9
- // 15 7 13 5
- // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has
- // no constant arrays).
- float cell = max(dither_cell, 0.01);
- float gx = mod(floor(pos.x / cell), 4.0);
- float gy = mod(floor(pos.y / cell), 4.0);
- float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0));
- picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b];
- } else {
- // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both
- // operands are integer-valued, so the half keeps "phase < num" exact under float rounding.
- float slot = floor(pos.z / max(layer_height, 0.01));
- float phase = mod(slot, den);
- picked = filament_rgb[(phase < num - 0.5) ? a : b];
- }
- return mix(palette_rgb[index], picked, sharp);
+ return filament_rgb[a];
}
void main()
@@ -364,9 +311,6 @@ void main()
// world position and perturb the world normal.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
- // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the
- // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined.
- vec3 pos_fwidth = fwidth(world_pos.xyz);
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -508,6 +452,6 @@ void main()
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
- albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth);
+ albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb));
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
diff --git a/scripts/Dockerfile.deps-assimp b/scripts/Dockerfile.deps-assimp
index 40566885c0..505eb3e4f5 100644
--- a/scripts/Dockerfile.deps-assimp
+++ b/scripts/Dockerfile.deps-assimp
@@ -46,7 +46,7 @@ RUN set -eux; \
tar -xzf /tmp/assimp.tar.gz -C /tmp/assimp-src --strip-components=1; \
DESTDIR=/OrcaSlicer/deps/build/destdir/usr/local; \
cmake -S /tmp/assimp-src -B /tmp/assimp-build -G Ninja \
- -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
+ -DCMAKE_POLICY_VERSION_MINIMUM=3.10 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$DESTDIR" \
-DCMAKE_PREFIX_PATH="$DESTDIR" \
diff --git a/scripts/orca_profile_tool.py b/scripts/orca_profile_tool.py
index f7f622c8c0..ec94eb7653 100755
--- a/scripts/orca_profile_tool.py
+++ b/scripts/orca_profile_tool.py
@@ -167,6 +167,11 @@ OBSOLETE_KEYS = {
"filament_load_time", "filament_unload_time", "smooth_coefficient",
"overhang_totally_speed", "silent_mode", "overhang_speed_classic",
"anisotropic_surfaces",
+ # Belt printer options retired before the feature shipped (#16236).
+ "belt_slice_rotation_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z",
+ "preslice_remap_global", "belt_support_z_offset_mode", "first_layer_plane",
+ "first_layer_plane_offset", "belt_preslice_global", "gcode_back_transform",
+ "belt_support_floor_mode", "first_layer_plane_thickness",
}
# Keys renamed at some point, whose old and new spellings must never co-exist:
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f0de57f12c..0acbd610f8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -91,6 +91,12 @@ if (SLIC3R_GUI)
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
find_package(wxInspector REQUIRED)
+ # wxInspector 1.0.0 installs its headers but accidentally declares the
+ # INSTALL_INTERFACE include directory PRIVATE, so its imported target does
+ # not expose them to consumers. Restore the package prefix include path until
+ # the upstream export is fixed.
+ get_filename_component(WXINSPECTOR_PREFIX "${wxInspector_DIR}/../../.." ABSOLUTE)
+ target_include_directories(wxInspector::wxInspector INTERFACE "${WXINSPECTOR_PREFIX}/include")
# wxInspector's exported interface names the release wxWidgets import
# libraries, which a Debug build cannot link. wx is linked above instead.
@@ -186,7 +192,7 @@ endif ()
# Add the Slic3r GUI library, libcurl, OpenGL and GLU libraries.
if (SLIC3R_GUI)
# target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES})
-target_link_libraries(OrcaSlicer libslic3r_gui)
+target_link_libraries(OrcaSlicer libslic3r_gui wxInspector::wxInspector)
if (MSVC)
# Generate debug symbols even in release mode.
target_link_options(OrcaSlicer PUBLIC "$<$:/DEBUG>")
diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp
index b895c25e89..1b764fe468 100644
--- a/src/OrcaSlicer.cpp
+++ b/src/OrcaSlicer.cpp
@@ -1820,6 +1820,8 @@ int CLI::run(int argc, char **argv)
old_printable_width = static_cast(old_printable_bbox.size().x());
old_printable_depth = static_cast(old_printable_bbox.size().y());
}
+ // A 3mf can carry an empty project_settings.config - the models in
+ // resources/handy_models do - and opt_float() dereferences without checking.
if (config.option("printable_height"))
old_printable_height = (int)(config.opt_float("printable_height"));
@@ -2505,7 +2507,8 @@ int CLI::run(int argc, char **argv)
orig_printable_width = static_cast(orig_printable_bbox.size().x());
orig_printable_depth = static_cast(orig_printable_bbox.size().y());
}
- orig_printable_height = (int)(config.opt_float("printable_height"));
+ if (config.option("printable_height"))
+ orig_printable_height = (int)(config.opt_float("printable_height"));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
%__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height;
if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
@@ -3432,9 +3435,14 @@ int CLI::run(int argc, char **argv)
max_self_index = std::max(max_self_index, v);
min_self_index = std::min(min_self_index, v);
}
- if (max_self_index > filament_count || min_self_index < 1) {
- BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating")
- % min_self_index % max_self_index % filament_count;
+ // And a project saved with FEWER filaments than are now loaded (a
+ // one-filament project sliced with two --load-filaments) leaves the tables half filled:
+ // the variant matching below then reads past filament_extruder_variant and
+ // set_with_restore_2 throws an uncaught size error. Regenerate in that case too.
+ if (max_self_index > filament_count || min_self_index < 1 || max_self_index < filament_count
+ || (int) filament_self_index_opt->values.size() < filament_count) {
+ BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] (size %4%) is invalid for filament_count %3%, regenerating")
+ % min_self_index % max_self_index % filament_count % filament_self_index_opt->values.size();
need_regenerate_self_index = true;
}
}
@@ -3525,6 +3533,10 @@ int CLI::run(int argc, char **argv)
std::vector& filament_variants = curr_variant_opt->values;
filament_variants.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
}
+ // See the filament_self_index note above: one variant per filament for
+ // the filaments the project did not know about.
+ if ((int) curr_variant_opt->values.size() < filament_count)
+ curr_variant_opt->values.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
const ConfigOptionStrings *new_variant_opt = dynamic_cast(config.option("filament_extruder_variant", true));
std::vector new_variant_indice;
@@ -3533,7 +3545,7 @@ int CLI::run(int argc, char **argv)
for (int i = 0; i < new_variant_count; i++)
{
- for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count; j++)
+ for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count && j < (int) curr_variant_opt->values.size(); j++)
{
if (curr_variant_opt->values[j] == new_variant_opt->values[i]) {
new_variant_indice[i] = j;
@@ -3585,7 +3597,18 @@ int CLI::run(int argc, char **argv)
ConfigOptionVectorBase* opt_vec_dst = static_cast(opt);
const ConfigOptionVectorBase* opt_vec_src = static_cast(source_opt);
//set with index
- opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
+ try {
+ // A project with fewer filaments than are loaded: grow the
+ // destination to the filament's slot first (set_with_restore_2 only restores).
+ if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count))
+ opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
+ opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
+ } catch (const std::exception &ex) { // Was an uncaught abort
+ BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
+ boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
+ record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
+ flush_and_exit(CLI_CONFIG_FILE_ERROR);
+ }
}
continue;
@@ -3634,7 +3657,16 @@ int CLI::run(int argc, char **argv)
if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) {
std::vector temp_variant_indice;
temp_variant_indice.resize(new_variant_count, -1);
- opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
+ try {
+ if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) // See above
+ opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
+ opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
+ } catch (const std::exception &ex) { // Was an uncaught abort
+ BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
+ boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
+ record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
+ flush_and_exit(CLI_CONFIG_FILE_ERROR);
+ }
if (opt_key == "filament_extruder_variant")
new_variant_counts[filament_index - 1] = opt_vec_src->size();
@@ -4150,6 +4182,10 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to wrapping detect")%__LINE__;
}
+ // Belt printers never get the classic wipe tower (see Print::has_wipe_tower()), so reserve no space for it.
+ const ConfigOptionBool* belt_printer_opt = m_print_config.option("belt_printer");
+ const bool is_belt_printer = belt_printer_opt && belt_printer_opt->value;
+
auto timelapse_type_opt = m_print_config.option("timelapse_type");
bool is_smooth_timelapse = false;
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
@@ -4387,11 +4423,11 @@ int CLI::run(int argc, char **argv)
}
};
- auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
+ auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, is_belt_printer](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
- if (!print_config.has("wipe_tower_x")) {
+ if (is_belt_printer || !print_config.has("wipe_tower_x")) {
plate_obj_size_info.has_wipe_tower = false;
BOOST_LOG_TRIVIAL(info) << boost::format("can not found wipe_tower_x in config, set to no wipe tower");
return;
@@ -4617,7 +4653,8 @@ int CLI::run(int argc, char **argv)
BoundingBoxf temp_printable_bbox(temp_printable_area);
printer_plate.printable_width = static_cast(temp_printable_bbox.size().x());
printer_plate.printable_depth = static_cast(temp_printable_bbox.size().y());
- printer_plate.printable_height = (int)(config.opt_float("printable_height"));
+ if (config.option("printable_height"))
+ printer_plate.printable_height = (int)(config.opt_float("printable_height"));
}
if (temp_exclude_area.size() >= 4) {
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
@@ -5261,7 +5298,7 @@ int CLI::run(int argc, char **argv)
}
}
- if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
+ if (!is_belt_printer && ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)) || (enable_wrapping_detect && !current_wrapping_exclude_area.empty())))
{
//prepare the wipe tower
int plate_count = partplate_list.get_plate_count();
@@ -5413,7 +5450,7 @@ int CLI::run(int argc, char **argv)
bool is_seq_print = false;
get_print_sequence(cur_plate, m_print_config, is_seq_print);
- if (!is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
+ if (!is_belt_printer && !is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
{
//prepare the wipe tower
auto printer_structure_opt = m_print_config.option>("printer_structure");
@@ -5581,7 +5618,7 @@ int CLI::run(int argc, char **argv)
};
const int max_filament_count = plate_count > 0 ? *std::max_element(plate_filament_counts.begin(), plate_filament_counts.end()) : 0;
- if (plate_needs_wipe_tower(max_filament_count))
+ if (!is_belt_printer && plate_needs_wipe_tower(max_filament_count))
{
//prepare the wipe tower
auto printer_structure_opt = m_print_config.option>("printer_structure");
@@ -5688,7 +5725,7 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found single object mode");
}
- if (m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
+ if (!is_belt_printer && m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
float x;
float y;
if (duplicate_count > 0) {
@@ -6313,7 +6350,7 @@ int CLI::run(int argc, char **argv)
// The stored (or default) tower position may not fit the tower these plates
// need, and no CLI placement site runs on a plain slice - mirror the GUI's
// reload clamp and fit every plate's tower into the printable area first.
- if (m_print_config.option("enable_prime_tower", true)->value) {
+ if (!is_belt_printer && m_print_config.option("enable_prime_tower", true)->value) {
for (int index = 0; index < partplate_list.get_plate_count(); index++) {
if ((plate_to_slice != 0) && (plate_to_slice != (index + 1)))
continue;
diff --git a/src/dev-utils/CMakeLists.txt b/src/dev-utils/CMakeLists.txt
index e718da9e83..19fa1c3ce3 100644
--- a/src/dev-utils/CMakeLists.txt
+++ b/src/dev-utils/CMakeLists.txt
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
+ # texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers,
+ # chart by chart, so a defect can be reproduced from the project file instead of from a screenshot.
+ add_executable(texture_unwrap_dump texture_unwrap_dump.cpp)
+ target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg)
+ target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS})
+
# profile_include_dump: prints what included templates contribute to a vendor's presets,
# to diff against the same tool built in BambuStudio. Built only on request.
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
diff --git a/src/dev-utils/OrcaSlicer_profile_validator.cpp b/src/dev-utils/OrcaSlicer_profile_validator.cpp
index 687292b8c7..208b950cfb 100644
--- a/src/dev-utils/OrcaSlicer_profile_validator.cpp
+++ b/src/dev-utils/OrcaSlicer_profile_validator.cpp
@@ -202,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
{
const Vec2d center = printable_area_center(cfg);
+ const bool belt = cfg.opt_bool("belt_printer");
std::vector cube_mins;
if (by_object) {
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
@@ -212,6 +213,12 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
cfg.set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional));
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
+ } else if (belt) {
+ // A belt object's slicing Z starts at the belt below its leading end, well below its first
+ // printed layer, so a height range in slicing Z does not map onto the part. Two cubes one
+ // behind the other along the belt, the second on filament 2, give the one filament change
+ // instead (the purge prism is an object the GUI adds, so there is no tower to place).
+ cube_mins = {center - Vec2d(5., 15.), center + Vec2d(-5., 5.)};
} else {
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
if (!cfg.opt_string("wrapping_detection_gcode").empty())
@@ -229,14 +236,20 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
obj->add_volume(m);
obj->add_instance();
- // Filament 2 is used only above z=4, so the upper layers carry a single filament change.
- DynamicPrintConfig range_config;
- range_config.set_key_value("extruder", new ConfigOptionInt(2));
- // Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
- // printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
- // trips Flow::with_spacing.
- range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
- obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
+ if (belt && !by_object) {
+ // The second cube along the belt is on filament 2 (see cube_mins above).
+ if (&cube_min == &cube_mins.back())
+ obj->config.set_key_value("extruder", new ConfigOptionInt(2));
+ } else {
+ // Filament 2 is used only above z=4, so the upper layers carry a single filament change.
+ DynamicPrintConfig range_config;
+ range_config.set_key_value("extruder", new ConfigOptionInt(2));
+ // Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
+ // printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
+ // trips Flow::with_spacing.
+ range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
+ obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
+ }
obj->ensure_on_bed();
print.auto_assign_extruders(obj);
}
@@ -521,11 +534,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
const std::string filament_name = bundle.filaments.get_selected_preset_name();
const std::string what = "Printer \"" + printer + "\"";
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
+ // A belt printer has no wipe tower (it purges into a prism object on the belt), so its
+ // filament change is the plain tool change set_extruder() emits rather than the tower's block.
+ const bool belt = bundle.printers.get_selected_preset().config.opt_bool("belt_printer");
+ const std::string marker = belt ? "\nT1\n" : "CP TOOLCHANGE START";
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
++failures;
- else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
- // The filament change never rode the tower, so change_filament_gcode was not exercised.
- BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
+ else if (out.find(marker) == std::string::npos) {
+ // The filament change never fired, so change_filament_gcode was not exercised.
+ BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no "
+ << (belt ? "T1" : "CP TOOLCHANGE START") << ")";
++failures;
}
cover(bundle.prints.get_selected_preset());
diff --git a/src/dev-utils/platform/unix/build_linux_image.sh.in b/src/dev-utils/platform/unix/build_linux_image.sh.in
index 8466597d34..03dc483b7d 100755
--- a/src/dev-utils/platform/unix/build_linux_image.sh.in
+++ b/src/dev-utils/platform/unix/build_linux_image.sh.in
@@ -303,10 +303,11 @@ else
fi
has_host_runtime_library() {
- local lib_name path
+ local lib_name="\$1"
+ local path
if command -v ldconfig >/dev/null 2>&1; then
- if ldconfig -p 2>/dev/null | grep -Fq " \$lib_name"; then
+ if ldconfig -p 2>/dev/null | grep -Fq "\$lib_name ("; then
return 0
fi
fi
diff --git a/src/dev-utils/texture_unwrap_dump.cpp b/src/dev-utils/texture_unwrap_dump.cpp
new file mode 100644
index 0000000000..5c94f4b12b
--- /dev/null
+++ b/src/dev-utils/texture_unwrap_dump.cpp
@@ -0,0 +1,292 @@
+// Diagnostic for the LSCM unwrap of a texture displacement layer.
+//
+// It exists because the defect it hunts only shows up on a real painted patch: the paint mask is built
+// by TriangleSelector splitting base triangles, so the patch topology cannot be written down by hand,
+// and reasoning about it from a screenshot of the 3D view had already produced three wrong diagnoses.
+// This loads a saved project, rebuilds exactly the patch the bake would act on, runs the same unwrap,
+// and reports what came out - per chart, so a bad one can be pointed at rather than guessed at.
+//
+// texture_unwrap_dump
+
+// nanosvg is header-only and libslic3r's 3mf import references it without carrying the implementation,
+// so every executable that links libslic3r has to supply it. Must precede any include that pulls the
+// header in, or its include guard suppresses the implementation. Same pattern as the other dev tools.
+#define NANOSVG_IMPLEMENTATION
+#include "nanosvg/nanosvg.h"
+#define NANOSVGRAST_IMPLEMENTATION
+#include "nanosvg/nanosvgrast.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "libslic3r/Model.hpp"
+#include "libslic3r/TextureDisplacement.hpp"
+#include "libslic3r/Format/bbs_3mf.hpp"
+#include "libslic3r/Utils.hpp"
+
+#include
+
+using namespace Slic3r;
+
+namespace {
+
+uint64_t edge_key(int a, int b)
+{
+ if (a > b)
+ std::swap(a, b);
+ return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
+}
+
+// Boundary loops and the Euler characteristic of a face set, which together say whether a chart is the
+// topological disk LSCM needs (one loop, V - E + F == 1).
+void chart_topology(const indexed_triangle_set &mesh, const std::vector &faces, int &loops, int &euler)
+{
+ std::unordered_map edge_use;
+ std::unordered_map local;
+ for (const int f : faces) {
+ const stl_triangle_vertex_indices &t = mesh.indices[size_t(f)];
+ for (int i = 0; i < 3; ++i) {
+ ++edge_use[edge_key(t[i], t[(i + 1) % 3])];
+ local.emplace(t[i], int(local.size()));
+ }
+ }
+ euler = int(local.size()) - int(edge_use.size()) + int(faces.size());
+
+ std::unordered_map parent;
+ const std::function find = [&](int x) {
+ while (parent[x] != x)
+ x = parent[x] = parent[parent[x]];
+ return x;
+ };
+ for (const auto &[key, uses] : edge_use)
+ if (uses == 1)
+ for (const int v : { int(key >> 32), int(uint32_t(key)) })
+ parent.emplace(v, v);
+ for (const auto &[key, uses] : edge_use)
+ if (uses == 1) {
+ const int a = find(int(key >> 32)), b = find(int(uint32_t(key)));
+ if (a != b)
+ parent[b] = a;
+ }
+ std::unordered_map roots;
+ for (const auto &[v, p] : parent)
+ roots[find(v)] = 1;
+ loops = int(roots.size());
+}
+
+float signed_area_2d(const Vec2f &a, const Vec2f &b, const Vec2f &c)
+{
+ return 0.5f * ((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
+}
+
+} // namespace
+
+int main(int argc, char **argv)
+{
+ if (argc < 2) {
+ std::printf("usage: texture_unwrap_dump \n");
+ return 2;
+ }
+
+ Model model;
+ DynamicPrintConfig config;
+ ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable);
+ PlateDataPtrs plate_data;
+ std::vector project_presets;
+ bool is_bbl_3mf = false, is_orca_3mf = false;
+ Semver file_version;
+ // The importer writes a backup copy under the data dir and silently loses objects without one.
+ const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() / "texture_unwrap_dump";
+ boost::filesystem::create_directories(tmp);
+ set_data_dir(tmp.string());
+
+ // LoadModel so the meshes come through; AddDefaultInstances because an object with no instance is
+ // dropped by the plate mapping, which is what "skip this object" in the log means.
+ if (!load_bbs_3mf(argv[1], &config, &ctx, &model, &plate_data, &project_presets, &is_bbl_3mf, &is_orca_3mf,
+ &file_version, nullptr,
+ LoadStrategy::LoadModel | LoadStrategy::LoadConfig | LoadStrategy::AddDefaultInstances |
+ LoadStrategy::Silence)) {
+ std::printf("failed to load %s\n", argv[1]);
+ return 1;
+ }
+
+ std::printf("loaded: %zu object(s)\n", model.objects.size());
+
+ for (const ModelObject *object : model.objects)
+ for (const ModelVolume *volume : object->volumes) {
+ if (volume->texture_displacement_layers.empty()) {
+ std::printf("volume \"%s\": no texture displacement layers; paint masks per slot:",
+ volume->name.c_str());
+ for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
+ std::printf(" %zu", volume->texture_displacement_facet(i).get_data().triangles_to_split.size());
+ std::printf("\n");
+ continue;
+ }
+ std::printf("volume \"%s\": %zu base triangles, %zu layer(s)\n", volume->name.c_str(),
+ volume->mesh().its.indices.size(), volume->texture_displacement_layers.size());
+
+ for (const TextureDisplacementLayer &layer : volume->texture_displacement_layers) {
+ std::printf("\n layer %d \"%s\" mapping=%d seam_angle=%.1f connect=%d islands_stored=%zu\n",
+ layer.slot, layer.name.c_str(), int(layer.projection_method),
+ layer.lscm_seam_angle_deg, int(layer.auto_connect_islands), layer.islands.size());
+ if (layer.projection_method != TextureProjectionMethod::LSCM)
+ continue;
+
+ const indexed_triangle_set patch =
+ extract_painted_patch(volume->mesh().its, volume->texture_displacement_facet(layer.slot).get_data());
+ std::printf(" patch: %zu vertices, %zu triangles\n", patch.vertices.size(), patch.indices.size());
+ if (patch.indices.empty())
+ continue;
+
+ const auto t0 = std::chrono::steady_clock::now();
+ const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f,
+ layer.lscm_seam_edges);
+ const auto t1 = std::chrono::steady_clock::now();
+ std::printf(" TIMING compute_patch_unwrap: %.0f ms\n",
+ std::chrono::duration(t1 - t0).count());
+ std::printf(" unwrap: %d charts, %zu unwrapped triangles\n", unwrap.chart_count,
+ unwrap.indices.size());
+
+ // Group the patch's faces by chart so each can be examined on its own.
+ std::vector> chart_faces(size_t(std::max(unwrap.chart_count, 0)));
+ for (size_t i = 0; i < unwrap.indices.size(); ++i) {
+ const int chart = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
+ if (chart >= 0 && size_t(chart) < chart_faces.size())
+ chart_faces[size_t(chart)].push_back(unwrap.source_face[i]);
+ }
+
+ int bad_charts = 0;
+ for (size_t c = 0; c < chart_faces.size(); ++c) {
+ int loops = 0, euler = 0;
+ chart_topology(patch, chart_faces[c], loops, euler);
+
+ // Flipped triangles: the unwrap folded over itself, which is what a planar fallback
+ // does to a chart that is not flat. Measured on the unwrap's own triangles.
+ int pos = 0, neg = 0;
+ for (size_t i = 0; i < unwrap.indices.size(); ++i) {
+ const stl_triangle_vertex_indices &t = unwrap.indices[i];
+ if (unwrap.vertex_chart[size_t(t[0])] != int(c))
+ continue;
+ const float a = signed_area_2d(unwrap.uvs[size_t(t[0])], unwrap.uvs[size_t(t[1])],
+ unwrap.uvs[size_t(t[2])]);
+ if (a > 0.f) ++pos; else if (a < 0.f) ++neg;
+ }
+ const int flipped = std::min(pos, neg);
+ const bool disk = loops == 1 && euler == 1;
+ if (!disk || flipped > 0) {
+ ++bad_charts;
+ std::printf(" chart %2zu: %4zu faces loops=%d euler=%d%s flipped=%d/%d%s\n", c,
+ chart_faces[c].size(), loops, euler, disk ? "" : " NOT A DISK", flipped,
+ pos + neg, flipped ? " FOLDED" : "");
+ }
+ }
+ std::printf(" charts with a defect: %d / %d\n", bad_charts, unwrap.chart_count);
+
+ // What the eye actually sees. Every patch edge shared by two charts should carry the same
+ // UV on both sides once the islands are laid out as a connected net; where it does not,
+ // the texture jumps across that seam. Measured through compute_lscm_uvs(), i.e. the exact
+ // coordinates the bake and the checker overlay sample.
+ {
+ const auto n0 = std::chrono::steady_clock::now();
+ const std::vector net = compute_connected_net(unwrap);
+ const auto n1 = std::chrono::steady_clock::now();
+ std::printf(" TIMING compute_connected_net: %.0f ms (%zu islands)\n",
+ std::chrono::duration(n1 - n0).count(), net.size());
+ }
+ const auto t2 = std::chrono::steady_clock::now();
+ const std::vector uv = compute_lscm_uvs(patch, layer);
+ const auto t3 = std::chrono::steady_clock::now();
+ std::printf(" TIMING compute_lscm_uvs: %.0f ms (called on every preview, overlay and bake)\n",
+ std::chrono::duration(t3 - t2).count());
+ if (uv.size() != patch.vertices.size()) {
+ std::printf(" compute_lscm_uvs returned %zu uvs for %zu vertices\n", uv.size(),
+ patch.vertices.size());
+ continue;
+ }
+ // Per-corner UVs carry each chart's own placement, so an edge shared by two charts shows
+ // the jump directly: the same mesh vertex lands at two different UVs. That is exactly what
+ // the eye reads as the texture breaking.
+ const auto t4 = std::chrono::steady_clock::now();
+ const std::vector corner = compute_lscm_corner_uvs(patch, layer);
+ const auto t5 = std::chrono::steady_clock::now();
+ std::printf(" TIMING compute_lscm_corner_uvs: %.0f ms\n",
+ std::chrono::duration(t5 - t4).count());
+ // Keyed by edge, holding the UV each incident face gives to the edge's *lower-numbered*
+ // endpoint. Comparing that same vertex on both sides is the point: indexing by corner
+ // position instead compares opposite ends of the edge, because the two faces wind it in
+ // opposite directions.
+ std::unordered_map> edge_seen;
+ if (corner.size() == patch.indices.size() * 3)
+ for (size_t f = 0; f < patch.indices.size(); ++f) {
+ const stl_triangle_vertex_indices &t = patch.indices[f];
+ for (int k = 0; k < 3; ++k) {
+ const int a = t[k], b = t[(k + 1) % 3];
+ const int probe = std::min(a, b);
+ const int local = (a == probe) ? k : (k + 1) % 3;
+ edge_seen[edge_key(a, b)].push_back(corner[f * 3 + size_t(local)]);
+ }
+ }
+ // Which chart each patch face belongs to, so a broken edge can be attributed to a pair.
+ std::vector chart_of_face(patch.indices.size(), -1);
+ for (size_t i = 0; i < unwrap.indices.size(); ++i)
+ chart_of_face[size_t(unwrap.source_face[i])] = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
+
+ std::unordered_map> edge_faces;
+ for (size_t f = 0; f < patch.indices.size(); ++f) {
+ const stl_triangle_vertex_indices &t = patch.indices[f];
+ for (int k = 0; k < 3; ++k)
+ edge_faces[edge_key(t[k], t[(k + 1) % 3])].push_back(int(f));
+ }
+
+ int adjacent = 0, broken = 0, broken_same_chart = 0;
+ float worst = 0.f;
+ std::map, std::pair> by_pair;
+ for (const auto &[key, seen] : edge_seen) {
+ if (seen.size() != 2)
+ continue;
+ ++adjacent;
+ const float d = (seen[0] - seen[1]).norm();
+ if (d <= 1e-4f)
+ continue;
+ ++broken;
+ worst = std::max(worst, d);
+ const auto &faces_here = edge_faces[key];
+ int c1 = -1, c2 = -1;
+ if (faces_here.size() == 2) {
+ c1 = chart_of_face[size_t(faces_here[0])];
+ c2 = chart_of_face[size_t(faces_here[1])];
+ }
+ if (c1 == c2)
+ ++broken_same_chart;
+ auto &slot = by_pair[{ std::min(c1, c2), std::max(c1, c2) }];
+ ++slot.first;
+ slot.second = std::max(slot.second, d);
+ }
+ std::printf(" broken edges inside a single chart: %d\n", broken_same_chart);
+ std::printf(" broken by chart pair:");
+ for (const auto &[pk, v] : by_pair)
+ std::printf(" (%d,%d)x%d/%.1f", pk.first, pk.second, v.first, v.second);
+ std::printf("\n");
+ // Total length of the seams left broken, in mm: how much visibly torn edge the layout has,
+ // which is what the eye adds up. A count alone hides whether the breaks are hairlines or
+ // whole sides of an island.
+ float seam_mm = 0.f;
+ for (const auto &[key, seen] : edge_seen) {
+ if (seen.size() != 2 || (seen[0] - seen[1]).norm() <= 1e-4f)
+ continue;
+ seam_mm += (patch.vertices[size_t(key >> 32)] - patch.vertices[size_t(uint32_t(key))]).norm();
+ }
+ std::printf(" interior edges: %d, discontinuous: %d, total torn seam: %.2f mm (worst jump %.3f)\n",
+ adjacent, broken, seam_mm, worst);
+ std::printf(" stored islands %zu vs charts %d -> %s\n", layer.islands.size(),
+ unwrap.chart_count,
+ layer.islands.size() == size_t(unwrap.chart_count) ? "stored placements used"
+ : "net rebuilt");
+ }
+ }
+ return 0;
+}
diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp
index b79450a535..eacb122d05 100644
--- a/src/libslic3r/Arrange.cpp
+++ b/src/libslic3r/Arrange.cpp
@@ -299,7 +299,15 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
template
void fill_config(PConf& pcfg, const ArrangeParams ¶ms) {
- if (params.is_seq_print) {
+ if (params.is_belt) {
+ // Pack from the end of the belt that prints first, and keep the pile on the
+ // bed when it is larger than the room around that end.
+ pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
+ params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
+ PConf::Alignment::BOTTOM_RIGHT;
+ pcfg.clamp_to_bin = true;
+ }
+ else if (params.is_seq_print) {
// Start placing the items from the center of the print bed
pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT;
}
@@ -444,6 +452,50 @@ protected:
return bindist;
}
+ // Belt printers pack from the end of the belt that prints first, and a corner
+ // packer's checks (pile inside the bin, pack origin) apply to them as well.
+ bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; }
+
+ static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); }
+
+ // Position along the belt in print order: increasing from the end that prints first.
+ double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); }
+ double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); }
+ double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); }
+
+ // The corner of the bin the belt pile grows from.
+ Box::PointType belt_origin() const
+ {
+ const Box bb = sl::boundingBox(m_bin);
+ auto o = bb.minCorner();
+ if (params.belt_reversed) {
+ if (params.belt_axis == 0) setX(o, getX(bb.maxCorner()));
+ else setY(o, getY(bb.maxCorner()));
+ }
+ return o;
+ }
+
+ // An item's far edge in print order is what it costs (so a row fills across the
+ // belt before the pile advances), with a slight pull toward the near lateral
+ // edge and the same penalty as the bottom-left heuristic for sitting outside
+ // the corner.
+ double dist_along_belt(const Box &ibb)
+ {
+ const Box bin = sl::boundingBox(m_bin);
+ const int l = 1 - params.belt_axis;
+ const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l);
+ double d = belt_end(ibb) - belt_start(bin);
+ d += lat < 0 ? 10 * -lat : 0.1 * lat;
+ if (double behind = belt_start(ibb) - belt_start(bin); behind < 0)
+ d += 10 * -behind;
+ return norm(d);
+ }
+
+ double corner_bindist(const Box &ibb, const Slic3r::Point &origin_pack)
+ {
+ return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack);
+ }
+
double dist_to_bin(const Box& ibb, const Slic3r::Point& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
{
double bindist = 0;
@@ -533,8 +585,8 @@ protected:
// The smalles distance from the arranged pile center:
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
- if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
- double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack);
+ if (corner_packing()) {
+ double bindist = corner_bindist(ibb, origin_pack);
score = 0.2 * dist + 0.8 * bindist;
}
else {
@@ -591,8 +643,8 @@ protected:
break;
}
case LAST_BIG_ITEM: {
- if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
- score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
+ if (corner_packing()) {
+ score = corner_bindist(ibb, origin_pack);
}
else {
if (m_pilebb.defined)
@@ -607,8 +659,8 @@ protected:
// already processed bigger items.
// No need to play around with the anchor points, the center will be
// just fine for small items
- if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
- score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
+ if (corner_packing())
+ score = corner_bindist(ibb, origin_pack);
else {
// Align mainly around existing items
score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack));
@@ -709,6 +761,28 @@ protected:
score += 1 * (new_extruder_cnt-last_extruder_cnt);
}
+ // On a belt the parts print in belt order, so every colour change between
+ // parts is a filament change. Items arrive sorted by extruder and the pile
+ // grows from the leading end; keep each colour's run contiguous by charging
+ // an item for every packed item of another colour it does not fully follow,
+ // counting the tilted layers that reach belt_tilt_slope * height past that
+ // item's far edge.
+ if (params.is_belt && !params.is_seq_print) {
+ const std::set item_colours(item.extrude_ids.begin(), item.extrude_ids.end());
+ const double item_start = belt_start(ibb);
+ for (Item &p : m_items) {
+ if (p.is_virt_object)
+ continue;
+ const std::set p_colours(p.extrude_ids.begin(), p.extrude_ids.end());
+ const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end())
+ || std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end());
+ if (same_colour)
+ continue;
+ if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope))
+ score += 10.;
+ }
+ }
+
return std::make_tuple(score, fullbb);
}
@@ -785,7 +859,8 @@ public:
auto binbb = sl::boundingBox(m_bin);
- auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
+ auto starting_point = this->params.is_belt ? belt_origin() :
+ cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
// if we have wipe tower, items should be arranged around wipe tower
for (Item itm : items) {
if (itm.is_wipe_tower) {
@@ -936,7 +1011,7 @@ std::function AutoArranger::g
auto mp = m_merged_pile;
mp.emplace_back(itm.transformedShape());
auto chull = sl::convexHull(mp);
- if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
+ if (corner_packing())
{
if (!sl::isInside(chull, m_bin))
score += LARGE_COST_TO_REJECT;
diff --git a/src/libslic3r/Arrange.hpp b/src/libslic3r/Arrange.hpp
index ea89640a7b..b63b8c938b 100644
--- a/src/libslic3r/Arrange.hpp
+++ b/src/libslic3r/Arrange.hpp
@@ -146,6 +146,13 @@ struct ArrangeParams {
float nozzle_height = 0;
float printable_height = 256.0;
Vec2d align_center{ 0.5,0.5 };
+ // Belt printer: items print in the order they lie along the belt axis, from
+ // its low end unless belt_reversed, and a part's top prints
+ // belt_tilt_slope * height further along it than its base.
+ bool is_belt = false;
+ int belt_axis = 1; // 0 = X, 1 = Y
+ bool belt_reversed = false;
+ float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted
ArrangePolygons excluded_regions; // regions cant't be used
ArrangePolygons nonprefered_regions; // regions can be used but not prefered
diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp
new file mode 100644
index 0000000000..d51f1248de
--- /dev/null
+++ b/src/libslic3r/BeltBrim.cpp
@@ -0,0 +1,562 @@
+#include
+#include "BeltBrim.hpp"
+
+#include "ClipperUtils.hpp"
+#include "Flow.hpp"
+#include "Layer.hpp"
+#include "Polygon.hpp"
+#include "Print.hpp"
+#include "ShortestPath.hpp"
+#include "Support/BeltFloorContext.hpp"
+#include "BoundingBox.hpp"
+#include "ExPolygon.hpp"
+#include "ExtrusionEntity.hpp"
+#include "ExtrusionEntityCollection.hpp"
+#include "Point.hpp"
+#include "Polyline.hpp"
+#include "PrintConfig.hpp"
+#include "libslic3r.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Slic3r {
+
+// ---------------------------------------------------------------- scaling
+
+static inline Point scale_u_point(const Point &p, int from_axis, double factor)
+{
+ // llround, not a cast: casting truncates toward zero, so a round trip would
+ // walk every vertex toward the origin by up to one unit per pass.
+ return from_axis == 0 ?
+ Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) :
+ Point(p.x(), coord_t(std::llround(double(p.y()) * factor)));
+}
+
+static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor)
+{
+ for (Point &p : poly.points)
+ p = scale_u_point(p, from_axis, factor);
+}
+
+ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor)
+{
+ ExPolygons out = src;
+ for (ExPolygon &ex : out) {
+ scale_u_polygon(ex.contour, frame.from_axis, factor);
+ for (Polygon &hole : ex.holes)
+ scale_u_polygon(hole, frame.from_axis, factor);
+ }
+ return out;
+}
+
+Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor)
+{
+ Polylines out = src;
+ for (Polyline &pl : out)
+ for (Point &p : pl.points)
+ p = scale_u_point(p, frame.from_axis, factor);
+ return out;
+}
+
+// ---------------------------------------------------------------- sweep
+
+ExPolygons sweep_ex(const ExPolygons &src, const Point &t)
+{
+ if (src.empty())
+ return {};
+ if (t == Point(0, 0))
+ return src;
+
+ // One parallelogram per boundary edge. Together with P and P + t these
+ // cover the Minkowski sum exactly: for any q = p + s*t with p in P and
+ // s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P.
+ // Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e,
+ // putting q in that edge's parallelogram. Hole edges must be included, or
+ // holes narrower than t along t would wrongly survive the sweep.
+ Polygons quads;
+ for (const ExPolygon &ex : src)
+ for (size_t c = 0; c < ex.num_contours(); ++ c)
+ for (const Line &e : ex.contour_or_hole(c).lines()) {
+ if (e.a == e.b)
+ continue;
+ Polygon q;
+ q.points = { e.a, e.b, e.b + t, e.a + t };
+ // The non-zero fill rule counts a clockwise ring as -1, which
+ // would punch a hole instead of adding material. Edges parallel
+ // to t give a zero-area quad; Clipper discards those harmlessly.
+ if (q.is_clockwise())
+ q.reverse();
+ quads.emplace_back(std::move(q));
+ }
+
+ ExPolygons shifted = src;
+ for (ExPolygon &ex : shifted)
+ ex.translate(t);
+
+ // union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the
+ // argument above relies on.
+ return union_ex(union_ex(src, shifted), quads);
+}
+
+// ---------------------------------------------------------------- brim region
+
+ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
+ bool has_outer,
+ bool has_inner,
+ coord_t brim_width,
+ coord_t object_gap,
+ coord_t leading,
+ coord_t lateral,
+ const BeltBrimFrame &frame)
+{
+ if (footprint_flat.empty() || (! has_outer && ! has_inner))
+ return {};
+
+ ExPolygons out;
+
+ if (has_outer) {
+ // Offset the outer ring from the contours only, so a hole cannot punch
+ // through it. Same reasoning as the plate brim in Brim.cpp.
+ Polygons contours;
+ contours.reserve(footprint_flat.size());
+ for (const ExPolygon &ex : footprint_flat)
+ contours.emplace_back(ex.contour);
+
+ // Inner and outer boundary offset from the same polygon, to avoid
+ // round-off mismatch between them.
+ ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION);
+
+ // Close the interior before offsetting outwards. A belt contact patch is often a
+ // narrow, broken-up strip, and the offset rings of two islands less than
+ // 2 x brim_width apart merge and fill the space between them - space that lies
+ // UNDER the part, which is not what "outer brim" means. Closing also swallows
+ // holes in the patch for the same reason. Concavity-filling only, so an apron or
+ // any other outward protrusion is untouched.
+ ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner;
+
+ ExPolygons base = envelope;
+ if (leading > 0) {
+ // Sweep downhill from the gapped keep-out, so the apron is contiguous with
+ // the ring instead of starting inside the gap.
+ const Point t = frame.from_axis == 0 ?
+ Point(frame.downhill_sign() * leading, 0) :
+ Point(0, frame.downhill_sign() * leading);
+ base = union_ex(base, sweep_ex(envelope, t));
+ }
+ if (lateral > 0) {
+ // Across the belt, both ways. Swept from `base` so the apron is widened
+ // too, and in the flattened frame the cross-belt axis is unscaled, so this
+ // distance is already a true on-belt distance.
+ const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0);
+ ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y())));
+ base = union_ex(base, to_polygons(widened));
+ }
+ ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION);
+ expolygons_append(out, diff_ex(outer, envelope));
+ }
+
+ if (has_inner) {
+ // Holes reversed so a negative offset grows inward, mirroring Brim.cpp.
+ // No apron here: an apron growing into a hole interior is never useful.
+ Polygons holes;
+ for (const ExPolygon &ex : footprint_flat)
+ polygons_append(holes, ex.holes);
+ polygons_reverse(holes);
+ if (! holes.empty()) {
+ ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap));
+ ExPolygons hole_outer = offset_ex(holes, - float(object_gap));
+ expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes));
+ }
+ }
+
+ return union_ex(out);
+}
+
+// ---------------------------------------------------------------- line lattice
+
+std::vector belt_brim_line_positions(coord_t u_lo,
+ coord_t u_hi,
+ coord_t pitch_u,
+ coord_t u_anchor)
+{
+ std::vector out;
+ if (pitch_u <= 0 || u_hi <= u_lo)
+ return out;
+
+ // Walk the lattice from just below u_lo. Integer arithmetic throughout, so
+ // the half-open interval needs no epsilon: a point landing exactly on u_hi
+ // belongs to the next band.
+ int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1;
+ while (u_anchor + coord_t(k) * pitch_u < u_lo)
+ ++ k;
+ for (;; ++ k) {
+ const coord_t u = u_anchor + coord_t(k) * pitch_u;
+ if (u >= u_hi)
+ break;
+ out.emplace_back(u);
+ }
+ return out;
+}
+
+// ---------------------------------------------------------------- pipeline
+
+// A band of the belt surface as an explicit box, clamped to `bounds` along the
+// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those
+// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to
+// feed through the flattening scale.
+static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi)
+{
+ coord_t lo = scale_(u_lo);
+ coord_t hi = scale_(u_hi);
+ const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y();
+ const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y();
+ lo = std::max(lo, bmin);
+ hi = std::min(hi, bmax);
+ Polygon poly;
+ if (hi <= lo)
+ return poly;
+ if (from_axis == 0)
+ poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()),
+ Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) };
+ else
+ poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo),
+ Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) };
+ return poly;
+}
+
+ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut)
+{
+ if (region.empty())
+ return region;
+ BoundingBox keep_bb = get_extents(region);
+ keep_bb.offset(scale_(1.));
+ const bool low_side = frame.downhill_sign() < 0; // downhill is -u
+ const Polygon keep = band_box(keep_bb, frame.from_axis,
+ low_side ? unscale(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
+ low_side ? u_cut : unscale(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y()));
+ return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep });
+}
+
+// Everything the per-band line generator needs, gathered once per object.
+struct BeltBrimContext
+{
+ BeltFloorContext ctx;
+ BeltBrimFrame frame;
+ ExPolygons region; // brim region, object-local slicing XY
+ BoundingBox region_bbox;
+ Flow brim_flow;
+ coord_t pitch_u = 0;
+ coord_t u_anchor = 0;
+ double in_plane_pitch = 0.; // mm
+};
+
+// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z].
+static void belt_brim_band_paths(const BeltBrimContext &bc,
+ coordf_t print_z,
+ coordf_t height,
+ const Polygons &obstacles,
+ ExtrusionEntityCollection &out,
+ ExPolygons &areas_out)
+{
+ coordf_t u_lo = bc.ctx.cutoff_u(print_z - height);
+ coordf_t u_hi = bc.ctx.cutoff_u(print_z);
+ if (u_lo > u_hi)
+ std::swap(u_lo, u_hi);
+
+ // How wide this band is measured ON the belt, versus one nominal bead.
+ const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch();
+
+ // Fraction of the layer height at which a line sits above the belt. Toward the
+ // downhill edge, so the sheet is reasonably thick while the nozzle stays clear of
+ // the belt itself.
+ static constexpr double BAND_CLEARANCE_FRACTION = 0.75;
+
+ std::vector us;
+ double uniform_clearance = 0.; // 0 => derive per line from its own position
+ double line_pitch = bc.in_plane_pitch;
+ // One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
+ // first layer at 45 degrees): its flow is matched to the band, so the bead is that much
+ // wider. Two lattice lines in such a band would land almost on top of each other.
+ if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
+ // Steep belt, which is the normal case: the band is narrower than one bead, so
+ // exactly one line fits. Place it at a FIXED fraction of the band rather than
+ // on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
+ // point in its band, the clearance sweeps [0, height] from band to band, and the
+ // bead width therefore varies by 2x - visible as ragged, uneven brim lines.
+ // Anchoring to the band makes the clearance identical everywhere, so every bead
+ // is the same width.
+ //
+ // The spacing is then whatever the bands give (height / sin(tilt) on the belt)
+ // rather than the nominal bead spacing, so the flow below is matched to THAT
+ // pitch. Matched flow at the real pitch is what keeps the sheet uniform and
+ // gap-free; using nominal flow at band spacing would over-feed it.
+ us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height)));
+ uniform_clearance = BAND_CLEARANCE_FRACTION * height;
+ line_pitch = band_in_plane;
+ } else {
+ // Shallow belt: the band is wider than a bead, so it takes several lines and they
+ // have to sit on the nominal lattice. Their clearances then differ, and so do
+ // their widths - unavoidable here, but shallow belts are the rare case.
+ us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor);
+ }
+ if (us.empty())
+ return;
+
+ const Polygons region_polys = to_polygons(bc.region);
+
+ // One lattice line at a time: the clearance - and therefore the extrusion
+ // volume - is a property of the line's u, so the pieces of different lines
+ // must not be pooled before the flow is resolved.
+ // Overshoot the region so the clip, not the line's ends, decides the extent.
+ const coord_t margin = coord_t(SCALED_EPSILON) + 1;
+ coord_t u_prev = std::numeric_limits::min();
+ for (coord_t u : us) {
+ // Nozzle-to-belt clearance for this line. Constant along the line, because the
+ // belt height depends only on the shear-axis coordinate. Band-anchored lines
+ // share one clearance by construction; lattice lines (shallow belts, or a first
+ // layer thick enough that the band is wider than a bead) each get their own.
+ //
+ // A lattice line can fall where the belt is only a hair below the band's print_z.
+ // The bead there would be laid scraping the belt while its flow is sized for a
+ // taller cell, so it is moved uphill to the same fraction of the band the
+ // single-line case uses. (The clearance is along slice Z; the real gap under the
+ // nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
+ double clearance = uniform_clearance;
+ if (clearance <= 0.) {
+ const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
+ clearance = print_z - bc.ctx.floor_print_z(probe);
+ if (clearance < BAND_CLEARANCE_FRACTION * height) {
+ clearance = BAND_CLEARANCE_FRACTION * height;
+ u = scale_(bc.ctx.cutoff_u(print_z - clearance));
+ }
+ clearance = std::min(clearance, height);
+ }
+ // A line moved uphill can land on, or almost on, its neighbour; two beads closer
+ // than half a pitch would be laid into the same cell.
+ if (u_prev != std::numeric_limits::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
+ continue;
+ u_prev = u;
+
+ Polyline line;
+ if (bc.frame.from_axis == 0)
+ line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
+ Point(u, coord_t(bc.region_bbox.max.y() + margin)) };
+ else
+ line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u),
+ Point(coord_t(bc.region_bbox.max.x() + margin), u) };
+
+ Polylines pieces = intersection_pl(Polylines{ line }, region_polys);
+ if (! obstacles.empty())
+ pieces = diff_pl(pieces, obstacles);
+ if (pieces.empty())
+ continue;
+
+ // with_cross_section, not with_height: it reaches the prescribed volume while
+ // KEEPING the extrusion spacing, so the bead is sized to fill exactly one
+ // pitch x clearance cell of the sheet.
+ const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance));
+
+ // Footprint of these beads, for the first-layer convex hull and bbox.
+ for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width())))
+ areas_out.emplace_back(ExPolygon(p));
+
+ extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)),
+ erBrim, f.mm3_per_mm(), f.width(), float(clearance));
+ }
+}
+
+// Union of everything extruded at `print_z` that the brim must keep clear of, expressed
+// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can
+// overhang outside the belt footprint and land in the brim ring, which the flattened
+// brim_object_gap - a belt-plane separation - does not cover.
+//
+// SEQUENCING: this reads every object's layers and this object's own support layers.
+// Another object's support step shifts that object's layer Z into the object frame for
+// the duration of the run (PrintObject::_generate_support_material()), so the brims must
+// not overlap with the parallel support step: Print::process() generates them one object
+// after the other once that step is over (PrintObject::generate_belt_brim()), and an
+// object that arrives on or leaves the plate invalidates the other brim owners' support
+// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this
+// object's supports are dodged; another object's support at the same Z is not.
+// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole
+// objects are skipped without materialising their polygons. On a typical plate the
+// objects do not overlap and every foreign object drops out here, which matters because
+// this runs once per band - hundreds of times per object.
+static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self,
+ const BoundingBox ®ion_bbox, coordf_t print_z, coordf_t tol)
+{
+ const Point shift_self = self.instances().empty() ? Point(0, 0)
+ : self.instances().front().shift_without_plate_offset();
+ Polygons out;
+ for (const PrintObject *o : print.objects()) {
+ const bool is_self = (o == &self);
+ for (const PrintInstance &inst : o->instances()) {
+ const Point delta = inst.shift_without_plate_offset() - shift_self;
+ if (const Layer *l = o->get_layer_at_printz(print_z, tol)) {
+ BoundingBox lb = get_extents(l->lslices);
+ lb.translate(delta.x(), delta.y());
+ if (lb.overlap(region_bbox)) {
+ Polygons ps = to_polygons(l->lslices);
+ for (Polygon &p : ps)
+ p.translate(delta);
+ polygons_append(out, std::move(ps));
+ }
+ }
+ if (! is_self)
+ continue;
+ if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) {
+ Polygons ps = sl->support_fills.polygons_covered_by_spacing();
+ for (Polygon &p : ps)
+ p.translate(delta);
+ polygons_append(out, std::move(ps));
+ }
+ }
+ }
+ if (out.size() < 2)
+ return out; // union_() of 0 or 1 polygons is pure overhead
+ return union_(out);
+}
+
+void make_belt_brim(PrintObject &object)
+{
+ object.clear_belt_brim();
+ if (! object.has_belt_brim())
+ return;
+
+ const Print &print = *object.print();
+ BeltBrimContext bc;
+ if (! bc.ctx.init(object.slicing_parameters(), print.config()))
+ return;
+ bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() };
+
+ const size_t nlayers = object.layers().size();
+ if (nlayers == 0)
+ return;
+
+ // 1. Belt footprint: the union of each layer's slice clipped to that layer's
+ // own contact band. This is the object's bottom face, which on a belt is
+ // spread over every layer instead of sitting in layer 0.
+ ExPolygons footprint_acc;
+ // The first layer that touches the belt: where the leading-edge brim is cut.
+ const Layer *first_contact = nullptr;
+ for (size_t i = 0; i < nlayers; ++ i) {
+ const Layer &layer = *object.layers()[i];
+ if (layer.lslices.empty())
+ continue;
+ // print_z - height, not the previous layer's print_z: variable layer
+ // heights make the latter wrong.
+ coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height);
+ coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z);
+ if (u_lo > u_hi)
+ std::swap(u_lo, u_hi);
+ BoundingBox bb = get_extents(layer.lslices);
+ bb.offset(scale_(1.));
+ const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi);
+ if (band.empty())
+ continue;
+ ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band });
+ if (contact.empty())
+ continue;
+ if (first_contact == nullptr)
+ first_contact = &layer;
+ expolygons_append(footprint_acc, std::move(contact));
+ }
+ const ExPolygons footprint = union_ex(footprint_acc);
+ if (footprint.empty())
+ return;
+
+ // 2. Brim region, offset in the flattened (true on-belt) metric.
+ const PrintObjectConfig &cfg = object.config();
+ bc.brim_flow = print.brim_flow();
+ const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR;
+ // Quantize to an even number of lines, as the plate brim does.
+ const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2);
+ const coord_t leading = scale_(cfg.leading_brim_length.value);
+ const coord_t lateral = scale_(cfg.extra_brim_width.value);
+ const coord_t gap = scale_(cfg.brim_object_gap.value);
+
+ // Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width
+ // heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only
+ // is an outer brim too; it is narrowed down to the first contact below.
+ const BrimType bt = cfg.brim_type.value;
+ const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner
+ || bt == btAutoBrim || bt == btEar || bt == btPainted
+ || bt == btLeadingEdgeOnly;
+ const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner;
+
+ bc.region = belt_unflatten(
+ belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner,
+ width, gap, leading, lateral, bc.frame),
+ bc.frame);
+
+ if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty())
+ // The cut is the uphill edge of the first contact's band: everything past it
+ // belongs to later contacts. The first contact is the first layer that touches
+ // the belt (step 1), neither layers().front(), an empty lead-in layer, nor the
+ // first layer with geometry, which is an overhang's tip when the part overhangs
+ // its leading end: both lie ahead of the part.
+ bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z));
+
+ if (bc.region.empty())
+ return;
+ bc.region_bbox = get_extents(bc.region);
+
+ // 3. Line lattice. Fixed pitch in the flattened metric, anchored at the
+ // footprint's leading-most edge so lines stay collinear across
+ // disconnected islands and across the apron prologue.
+ bc.pitch_u = std::max(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt()));
+ bc.in_plane_pitch = unscale(bc.pitch_u) * bc.frame.u_stretch();
+ {
+ const BoundingBox fbb = get_extents(footprint);
+ const bool low_side = bc.frame.shear > 0.;
+ bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x())
+ : (low_side ? fbb.min.y() : fbb.max.y());
+ }
+
+ // 4. Bands coincident with an object layer.
+ std::vector by_layer(nlayers);
+ std::vector areas_by_layer(nlayers);
+ for (size_t i = 0; i < nlayers; ++ i) {
+ const Layer &layer = *object.layers()[i];
+ const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height);
+ belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]);
+ }
+
+ // 5. Apron prologue: the part of the region downhill of the object's first
+ // layer, which has no object layer to ride on.
+ std::vector prologue;
+ {
+ const Layer &first = *object.layers().front();
+ const coordf_t h = first.height;
+ const bool low_side = bc.frame.shear > 0.;
+ const coord_t u_lead_s = bc.frame.from_axis == 0
+ ? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x())
+ : (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y());
+ const coordf_t u_lead = unscale(u_lead_s);
+ // print_z at which the belt surface crosses the region's leading edge.
+ const coordf_t z_lead = bc.ctx.shear_factor() * u_lead
+ + bc.ctx.floor_offset() + bc.ctx.z_shift();
+ if (h > EPSILON)
+ for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) {
+ const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h);
+ BeltBrimBand band;
+ band.print_z = z;
+ band.height = h;
+ belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas);
+ if (! band.fills.empty())
+ prologue.emplace_back(std::move(band));
+ }
+ // Lowest Z first, so collect_layers_to_print sees them in print order.
+ std::reverse(prologue.begin(), prologue.end());
+ }
+
+ object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue));
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltBrim.hpp b/src/libslic3r/BeltBrim.hpp
new file mode 100644
index 0000000000..3f4fcc97de
--- /dev/null
+++ b/src/libslic3r/BeltBrim.hpp
@@ -0,0 +1,178 @@
+#ifndef slic3r_BeltBrim_hpp_
+#define slic3r_BeltBrim_hpp_
+
+#include "ExPolygon.hpp"
+#include "ExtrusionEntityCollection.hpp"
+#include "Point.hpp"
+#include "Polyline.hpp"
+#include "libslic3r.h"
+
+#include
+#include
+
+// Belt-printer brim geometry.
+//
+// A belt printer slices in a ROTATED frame, so the belt surface is not the
+// Z=0 bed plane but a tilted plane in slicing space:
+//
+// z_slicing(u) = shear * u + floor_offset + z_shift, u = X or Y
+//
+// where `shear == tan(tilt)` (SlicingParameters::belt_floor_shear_factor) and
+// the axis is selected by SlicingParameters::belt_floor_from_axis. See
+// Support/BeltFloorContext.hpp for the canonical accessors.
+//
+// Consequences that drive everything in this file:
+//
+// * A horizontal slicing layer touches the belt only along a narrow strip at
+// its leading edge, `layer_height / shear` wide (~0.2 mm at 45 degrees).
+// The object's belt footprint - its bottom face - is therefore spread over
+// every layer, not contained in layer 0.
+// * Distances measured in slicing XY are NOT on-belt distances: moving `du`
+// along the shear axis travels `du / cos(tilt)` across the belt. So brim
+// offsets have to be taken in a "flattened" space where the shear axis is
+// stretched by `1 / cos(tilt)`, then mapped back.
+// * Brim ahead of the part (downhill) lies at slicing Z BELOW the object's
+// first layer, because the object's layer 0 is precisely its leading
+// contact with the belt.
+//
+// Everything here is pure geometry on ExPolygons/Polylines so it can be unit
+// tested without a Print. Keep user-visible strings out of this file: it is
+// not listed in localization/i18n/list.txt.
+
+namespace Slic3r {
+
+// Tilt window within which the BELT plane, not the bed plane, is the adhesion
+// surface. Below ~1 degree a belt is a flat bed as far as adhesion goes, and the
+// contact band would be layer_height/sin(tilt) - tens of millimetres - so the
+// ordinary plate brim is both correct and cheaper. Above ~85 degrees the whole
+// brim compresses into a sliver and is not worth generating.
+inline constexpr double BELT_BRIM_MIN_TILT_DEG = 1.;
+inline constexpr double BELT_BRIM_MAX_TILT_DEG = 85.;
+
+// Description of the tilted belt plane, reduced to what the brim geometry needs.
+struct BeltBrimFrame
+{
+ // tan(tilt). Sign selects which way is downhill.
+ double shear = 0.;
+ // 0 = X, 1 = Y. Matches BeltFloorContext::from_axis().
+ int from_axis = 1;
+
+ // 1 / cos(tilt). Stretch factor that turns a projected distance along
+ // `from_axis` into the true distance travelled across the belt.
+ double u_stretch() const { return std::sqrt(1. + shear * shear); }
+ // cos(tilt). The inverse mapping.
+ double cos_tilt() const { return 1. / this->u_stretch(); }
+ // Downhill is where the belt surface is lower, i.e. printed earlier, i.e.
+ // the leading edge of the part. For shear > 0 that is -u.
+ int downhill_sign() const { return shear > 0. ? -1 : +1; }
+};
+
+// Scale only the `from_axis` component by `factor`, rounding to nearest.
+//
+// Deliberately not MultiPoint::scale(fx, fy) / ExPolygon::scale(fx, fy): those
+// truncate toward zero, which is asymmetric about the origin and loses up to a
+// full coordinate unit per vertex on every round trip.
+ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor);
+Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor);
+
+// Into / out of the space where Euclidean offsets equal true on-belt distances.
+inline ExPolygons belt_flatten(const ExPolygons &src, const BeltBrimFrame &frame)
+ { return belt_scale_u(src, frame, frame.u_stretch()); }
+inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &frame)
+ { return belt_scale_u(src, frame, frame.cos_tilt()); }
+
+// Minkowski sum of `src` with the segment [0, t]: the region swept by sliding
+// `src` along t. Used to grow the brim downhill for "extra brim width".
+//
+// Implemented as union_(P, P + t, {parallelogram per boundary edge}) over ALL
+// contours including holes, with every parallelogram forced counter-clockwise
+// so the non-zero fill rule closes holes narrower than t along the sweep
+// direction. A hole survives exactly when it is wider than |t| measured along
+// t - not when it is wider in its narrowest Euclidean direction.
+ExPolygons sweep_ex(const ExPolygons &src, const Point &t);
+
+// "Leading edge only": keep the part of a brim region (unflattened, slicing XY)
+// at or downhill of the object's first contact with the belt, so the part is
+// supported as it lands and nothing is printed alongside it afterwards. `u_cut`
+// is the uphill edge of the first layer's contact band along `frame.from_axis`,
+// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side
+// `frame.downhill_sign()` points to.
+ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut);
+
+// Brim region for one already-flattened belt footprint. All lengths are scaled
+// and measured in the flattened (true on-belt) metric.
+//
+// `has_outer` / `has_inner` are the resolved BrimType: belt printers collapse
+// Auto / Mouse ear / Painted to outer-only, so the caller does that mapping and
+// this function never needs PrintConfig.
+//
+// Two directional extras are applied to the footprint before the outer offset, so
+// each one buys reach in one direction only:
+//
+// `leading` (leading_brim_length) sweeps the footprint DOWNHILL along the belt,
+// so every leading-facing edge gains an apron ahead of it.
+// `lateral` (extra_brim_width) sweeps it BOTH WAYS across the belt, widening
+// the brim sideways without pushing it further ahead or behind.
+//
+// Neither is applied to the inner (hole) ring.
+ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
+ bool has_outer,
+ bool has_inner,
+ coord_t brim_width,
+ coord_t object_gap,
+ coord_t leading,
+ coord_t lateral,
+ const BeltBrimFrame &frame);
+
+// Brim line positions for one layer band.
+//
+// Lines sit on a fixed lattice `u_anchor + k * pitch_u` so the on-belt spacing
+// between neighbouring brim lines is constant regardless of how the lattice
+// falls across layer bands. Snapping to band centres instead would quantise
+// the spacing to whole bands and under-deposit by ~35% at 45 degrees.
+//
+// The band is half-open, [u_lo, u_hi), so every lattice point belongs to
+// exactly one band: none duplicated at a boundary, none dropped. A band
+// narrower than the pitch simply yields nothing; a band much wider (shallow
+// tilt) yields several lines.
+std::vector belt_brim_line_positions(coord_t u_lo,
+ coord_t u_hi,
+ coord_t pitch_u,
+ coord_t u_anchor);
+
+// ---------------------------------------------------------------- pipeline
+
+// One brim-only layer printed BEFORE the object's first layer, carrying the
+// apron that has to be stuck to the belt ahead of the part.
+//
+// Deliberately not a Layer subclass. A synthetic Layer would inherit id()
+// semantics that leak into initial-layer temperature selection, the spiral vase
+// probe, gradual interpolation, avoid-crossing-perimeters and cooling, all of
+// which key off Layer::id() == 0 or off a layer's regions. A plain record
+// carries only what the emitter needs.
+//
+// `height` is the LAYER height, used for the Z move and ordering metadata only.
+// Each extrusion path inside `fills` carries its own height, equal to that
+// line's nozzle-to-belt clearance, which varies across the band.
+struct BeltBrimBand
+{
+ coordf_t print_z = 0.;
+ coordf_t height = 0.;
+ // erBrim paths in the object's local slicing frame, untranslated.
+ ExtrusionEntityCollection fills;
+ // Footprint of those paths, for the first-layer convex hull / bbox.
+ ExPolygons areas;
+};
+
+class PrintObject;
+
+// Generate the belt brim for one object: fills its per-object-layer bands and
+// its apron prologue. No-op unless PrintObject::has_belt_brim().
+//
+// Runs inside posSupportMaterial rather than the brim step, because the prologue
+// print_z values must exist before ToolOrdering is built at psWipeTower.
+void make_belt_brim(PrintObject &object);
+
+} // namespace Slic3r
+
+#endif // slic3r_BeltBrim_hpp_
diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp
new file mode 100644
index 0000000000..e76e76b84f
--- /dev/null
+++ b/src/libslic3r/BeltGCode.cpp
@@ -0,0 +1,50 @@
+#include "BeltGCode.hpp"
+#include "GCodeWriter.hpp"
+#include "GCode/BeltKinematics.hpp"
+#include "BeltTransform.hpp"
+#include "Print.hpp"
+#include "Point.hpp"
+#include "PrintConfig.hpp"
+#include "libslic3r.h"
+#include
+
+namespace Slic3r {
+
+void BeltGCode::init_belt_writer(Print &print)
+{
+ // Axis remap and build volume max are set by base GCode after init_belt_writer
+ // returns; set_kinematics() replays them, so install order does not matter.
+ install_belt_kinematics(m_writer, print.config());
+ m_writer.set_force_normal_lift(true);
+}
+
+void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
+{
+ const auto &full_cfg = print.full_print_config();
+ // Slicing rotation: the belt tilt (axis + angle) and the single source of truth
+ // for the physical tilt the G-code viewer uses to enable belt view.
+ file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
+ file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
+ // Machine-frame transform: shear (cot) + scale (1/|sin|) derived from the belt
+ // tilt angle (or belt_frame_tilt_angle when decoupled).
+ file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
+ file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value);
+}
+
+void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
+{
+ // Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
+ // the origin through the belt pipeline so that back_transform(T * origin) =
+ // origin (correct machine position). The back_transform applied during
+ // G-code emission is the inverse of the forward transform.
+
+ // Adjust origin: transform through belt forward pipeline so that
+ // the back-transform correctly recovers model-space positions.
+ Transform3d T = BeltTransformPipeline::build_forward_transform(m_config);
+ Vec2d cur_origin = this->origin();
+ Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.);
+ Vec3d adjusted = T.linear() * origin3d;
+ this->set_origin(Vec2d(adjusted.x(), adjusted.y()));
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp
new file mode 100644
index 0000000000..1631a7dc9c
--- /dev/null
+++ b/src/libslic3r/BeltGCode.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "GCode.hpp"
+#include "Point.hpp"
+#include "Print.hpp"
+
+namespace Slic3r {
+
+// Belt-printer-specific GCode export.
+//
+// Inherits from GCode and overrides virtual hooks to:
+// - Install a BeltKinematics on the GCodeWriter
+// - Write belt configuration to the G-code header
+// - Adjust the origin for global pre-slice transforms when switching instances
+// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(),
+// which the base GCode::should_disable_arc_fitting() consults -- no override needed.)
+class BeltGCode : public GCode
+{
+protected:
+ void init_belt_writer(Print &print) override;
+ void write_belt_header(GCodeOutputStream &file, const Print &print) override;
+ void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
+};
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp
new file mode 100644
index 0000000000..5fb925f9aa
--- /dev/null
+++ b/src/libslic3r/BeltPurge.cpp
@@ -0,0 +1,433 @@
+// ORCA-Belt: backend of the belt purge tower (the belt replacement for the
+// classic wipe/prime tower).
+//
+// Kept in its own translation unit so the belt-purge logic stays out of the way
+// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries
+// no regression risk for normal printers: none of these methods do anything
+// unless the print is a belt printer with the belt purge tower enabled.
+//
+// Print::has_belt_purge_tower() - is the belt purge tower active?
+// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the
+// printed objects' grid
+// Print::_plan_belt_purge() - route filament-change purging into the
+// prism (flush-into-objects), no wipe tower
+// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid
+// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap
+//
+// (Declarations live in Print.hpp alongside the rest of the Print interface.)
+
+#include "Print.hpp"
+#include "PrintConfig.hpp"
+#include "Exception.hpp"
+#include "GCode/ToolOrdering.hpp"
+#include "Layer.hpp"
+#include "ExtrusionEntity.hpp"
+#include "ExtrusionEntityCollection.hpp"
+#include "I18N.hpp"
+#include "format.hpp"
+#include "LocalesUtils.hpp"
+#include "libslic3r.h"
+#include "BeltBrim.hpp"
+#include "PrintBase.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Slic3r {
+
+// Belt purge prism: purging after filament changes is routed into a sliced
+// prism object via the flush-into-objects machinery instead of a wipe tower.
+bool Print::has_belt_purge_tower() const
+{
+ // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower
+ // option (not the classic enable_prime_tower).
+ if (!(m_config.belt_printer.value
+ && m_config.enable_belt_purge_tower.value
+ && !m_config.spiral_mode.value
+ && m_config.filament_diameter.values.size() > 1))
+ return false;
+
+ return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
+ return object->config().belt_purge_tower_object.value;
+ });
+}
+
+// Belt mode: align ALL objects on the plate (the printed objects AND the purge
+// prism) onto one common layer grid, so the prism can absorb every toolchange.
+//
+// After belt slicing each object's layer print_z carries a per-object global z
+// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so
+// objects at different belt-Y positions get layer grids with DIFFERENT residues
+// (mod layer height). Purge marking looks absorbers up with
+// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only
+// absorbs into the prism if the prism has a layer at B's print_z. Snapping only
+// the prism to one object therefore worked for a single (assembled) multi-color
+// object but failed with multiple separate objects — the prism could follow only
+// one grid, and toolchanges on the others went unabsorbed ("multiple layer
+// grids" warning).
+//
+// Fix: pick one reference grid (the tallest object) and shift every object onto
+// it. Each shift is at most half a layer height — a sub-100µm move along the
+// belt, the very same mechanism the per-object global_z_offset already uses, and
+// it keeps each object internally consistent (belt_shift_layer_grid moves the
+// object's layers, its support layers, and its belt floor together). Equal layer
+// height across objects is enforced by Print::validate(), so once residues match
+// every object steps on the same lattice {ref_offset + k*h} and every toolchange
+// layer coincides with a prism layer.
+void Print::_align_belt_purge_layers()
+{
+ PrintObject *prism = nullptr;
+ for (PrintObject *po : m_objects)
+ if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
+ prism = po;
+ break;
+ }
+ if (prism == nullptr || prism->layers().empty())
+ return;
+
+ const double h = prism->config().layer_height.value;
+ if (h <= EPSILON)
+ return;
+
+ // Grid residue of an object's layer grid: identical for all of an object's
+ // layers above the first since they step by h.
+ auto grid_offset = [h](const PrintObject *po) -> double {
+ if (po->layers().empty())
+ return 0.;
+ const double z = po->layers().front()->print_z;
+ return z - std::floor(z / h) * h; // in [0, h)
+ };
+
+ // Reference grid: the tallest non-prism object (proxy for the object with
+ // the most toolchange layers — minimizes how far the rest must move).
+ const PrintObject *ref = nullptr;
+ double ref_top = -std::numeric_limits::max();
+ for (const PrintObject *po : m_objects) {
+ if (po->config().belt_purge_tower_object.value || po->layers().empty())
+ continue;
+ const double top = po->layers().back()->print_z;
+ if (top > ref_top) {
+ ref_top = top;
+ ref = po;
+ }
+ }
+ if (ref == nullptr)
+ return;
+
+ const double ref_offset = grid_offset(ref);
+
+ // Snap every object (printed objects AND the prism) onto the reference grid.
+ for (PrintObject *po : m_objects) {
+ if (po->layers().empty())
+ continue;
+ double delta = ref_offset - grid_offset(po);
+ if (delta > 0.5 * h)
+ delta -= h;
+ else if (delta <= -0.5 * h)
+ delta += h;
+ po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
+ }
+}
+
+// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
+// into the belt purge prism (and any other flush_into_* object) using the
+// flush-into-objects machinery, without generating classic wipe tower G-code.
+// The toolchange itself is emitted by GCode::set_extruder() via the
+// change_filament_gcode macro; the overrides marked here make the new
+// filament's first extrusions land in the purge prism.
+void Print::_plan_belt_purge()
+{
+ m_wipe_tower_data.clear();
+
+ // psWipeTower may be invalidated without posSlice (for example after a
+ // filament-map or tool-ordering change). Restore a prism shortened by the
+ // previous plan so a newly higher toolchange can use its original layers.
+ for (PrintObject *po : m_objects)
+ if (po->config().belt_purge_tower_object.value)
+ po->belt_undo_purge_plan();
+
+ // Must run before ToolOrdering is built: LayerTools merge per-object layer
+ // print_z values, and the prism only absorbs purge where its (snapped)
+ // layers coincide with the toolchange layers.
+ this->_align_belt_purge_layers();
+
+ const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size();
+
+ // No initial priming extrusions: there is no tower to prime on.
+ m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false);
+ m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
+
+ if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
+ throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
+
+ // Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
+ // FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
+ // neither object nor support and so never gets the flag even when the print changes filament.
+ {
+ bool any_change = false;
+ unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder();
+ for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools())
+ for (const unsigned int e : lt.extruders)
+ if (e != cur) { any_change = true; cur = e; }
+ if (! any_change)
+ return;
+ }
+
+ this->throw_if_canceled();
+
+ // Flush volumes per filament pair, mirroring the generic wipe tower path:
+ // full flush matrix for single extruder multi material with purging enabled,
+ // plain prime volume otherwise.
+ std::vector flush_matrix(cast(
+ get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size())));
+ std::vector> wipe_volumes;
+ for (unsigned int i = 0; i < number_of_extruders; ++i)
+ wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * number_of_extruders,
+ flush_matrix.begin() + (i + 1) * number_of_extruders));
+ const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
+ const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0);
+
+ // Cancel the purge prism early: pre-scan the tool ordering for the highest
+ // print_z that actually has a toolchange, then drop the prism's layers above
+ // it so the tower stops at the last color swap (saves filament/time). This
+ // MUST happen before the marking loop below: ensure_perimeters_infills_order
+ // force-overrides the prism's extrusions on every layer (it is a dedicated
+ // flush object), so truncating afterwards would leave dangling overrides
+ // pointing into deleted layers.
+ {
+ // The tool ordering covers the WHOLE print, and the prism is a printed
+ // object in it. Left unbounded, the scan below sees the prism's own
+ // toolchanges on layers above every model object -- the prism runs past
+ // them by design (ramp/height compensation at the tilted ends) -- so
+ // last_tc_z lands at the prism's own top and the truncation cancels
+ // nothing. The tower ends up justifying its own existence.
+ //
+ // Nothing above the tallest printed object can require a color change,
+ // so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
+ // z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
+ // 39.4 mm of tower that no swap ever needed.
+ // Support layers count too: on a belt they can extend above the object's
+ // own top, and a toolchange there is a real one.
+ double obj_top_z = -1.;
+ for (const PrintObject *po : m_objects) {
+ if (po->config().belt_purge_tower_object.value)
+ continue;
+ if (!po->layers().empty())
+ obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
+ if (!po->support_layers().empty())
+ obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
+ }
+
+ double last_tc_z = -1.;
+ unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
+ for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) {
+ // layer_tools() is ordered by print_z ascending.
+ if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
+ break;
+ for (const unsigned int e : lt.extruders)
+ if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
+ }
+ // Deliberately NOT cancelling the prism outright when no object toolchange
+ // exists: belt_truncate_layers_above(0.) empties m_layers, and an object
+ // with zero layers is not something the rest of the pipeline expects. The
+ // GUI already declines to create a prism unless more than one filament is
+ // in use, so this case is a stale prism, not a hot path -- leave it whole
+ // rather than risk a zero-layer object.
+ if (last_tc_z >= 0.)
+ for (PrintObject *po : m_objects)
+ if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
+ po->belt_truncate_layers_above(last_tc_z);
+ break;
+ }
+ }
+
+ // The prism only absorbs purge at toolchange layers whose print_z coincides
+ // with one of its own layers.
+ PrintObject *prism_po = nullptr;
+ for (PrintObject *po : m_objects)
+ if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
+
+ float total_leftover = 0.f;
+ float worst_layer_leftover = 0.f;
+ double worst_layer_z = 0.;
+
+ unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
+ for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
+ float layer_leftover = 0.f;
+ for (const unsigned int extruder_id : layer_tools.extruders) {
+ if (extruder_id == current_extruder_id)
+ continue;
+ float volume_to_wipe = use_flush_matrix ?
+ wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
+ (float) m_config.prime_volume;
+ float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
+ volume_to_wipe);
+ layer_leftover += leftover;
+ current_extruder_id = extruder_id;
+ }
+
+ // Plastic saving: drop the prism's fills that no toolchange on this layer
+ // claimed. At this point the prism's OVERRIDDEN fills are exactly the
+ // purge; the rest would print as solid infill in the prism's own filament
+ // for nothing -- which is the whole prism on a layer with no toolchange
+ // (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
+ // left alone so the bar keeps a continuous wall along the belt.
+ //
+ // Non-destructive: the entities are stashed with their positions and put
+ // back by belt_restore_dropped_fills() at the top of the next plan. An
+ // earlier version deleted them outright, which broke replanning when a
+ // later tool ordering needed what this one had not claimed -- that is why
+ // it was removed rather than kept.
+ if (prism_po != nullptr) {
+ const auto &we = layer_tools.wiping_extrusions();
+ prism_po->belt_drop_unclaimed_fills(
+ prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
+ [&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
+ }
+
+ layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
+ if (layer_leftover > 0.f) {
+ total_leftover += layer_leftover;
+ if (layer_leftover > worst_layer_leftover) {
+ worst_layer_leftover = layer_leftover;
+ worst_layer_z = layer_tools.print_z;
+ }
+ }
+ this->throw_if_canceled();
+ }
+
+ if (total_leftover > 1.f) {
+ this->active_step_add_warning(
+ PrintStateBase::WarningLevel::CRITICAL,
+ Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not "
+ "be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. "
+ "Increase the belt purge tower width, or reduce flushing volumes."),
+ int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
+ Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
+ }
+}
+
+// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset
+// application in slice() — layer print_z and belt_floor_z_shift move together
+// so belt floor clipping stays consistent with the shifted grid. Used by
+// Print::_align_belt_purge_layers() to snap the purge prism onto the printed
+// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift
+// of the prism along the belt.
+void PrintObject::belt_shift_layer_grid(double delta)
+{
+ if (std::abs(delta) < EPSILON)
+ return;
+ for (Layer *layer : m_layers)
+ layer->print_z += delta;
+ for (SupportLayer *layer : m_support_layers)
+ layer->print_z += delta;
+ // The brim's apron bands below the first layer carry their own print_z.
+ for (BeltBrimBand &band : m_belt_brim_prologue)
+ band.print_z += delta;
+ m_slicing_params.belt_floor_z_shift += delta;
+ // The grid stays shifted across a support-only or brim-only change (posSlice does
+ // not rerun), so everything slice() derived from it has to follow: the cached floor
+ // that update_slicing_parameters() restores, and the global offset the organic
+ // support layers and the adaptive infill octree are placed with. Left alone, the
+ // next alignment finds a delta of 0 and the floor and the supports sit up to half
+ // a layer off the grid, unlike a fresh slice.
+ if (m_belt_floor_z_shift_cache_valid)
+ m_belt_floor_z_shift_cached += delta;
+ m_belt_global_z_offset += delta;
+}
+
+// Belt mode: drop layers strictly above z (used to cancel the purge prism early
+// once there are no more toolchanges above z, so the tower stops at the last
+// color swap instead of wasting filament up the rest of the belt). Each layer's
+// cross-section is already sliced, so removing upper layers does not affect the
+// last toolchange's coverage. Deletes the Layer objects and clears the new top
+// layer's upper-layer link. Returns the number of layers removed.
+size_t PrintObject::belt_truncate_layers_above(coordf_t z)
+{
+ // A repeated plan always starts from the restored full layer set.
+ assert(m_belt_truncated_layers.empty());
+ size_t keep = m_layers.size();
+ while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON)
+ --keep;
+ if (keep >= m_layers.size())
+ return 0;
+ const size_t removed = m_layers.size() - keep;
+ m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end());
+ m_layers.resize(keep);
+ if (!m_layers.empty())
+ m_layers.back()->upper_layer = nullptr;
+ return removed;
+}
+
+// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
+//
+// Called per layer from _plan_belt_purge(), after the real-purge marking and
+// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
+// remaining fill on the prism (it is a dedicated flush object), so afterwards
+// everything looks claimed and nothing could be distinguished.
+size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function &claimed)
+{
+ if (layer == nullptr)
+ return 0;
+ size_t dropped = 0;
+ for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
+ LayerRegion *lr = layer->get_region(ri);
+ auto &ents = lr->fills.entities;
+ ExtrusionEntitiesPtr keep;
+ keep.reserve(ents.size());
+ for (size_t i = 0; i < ents.size(); ++i) {
+ if (claimed(ents[i])) {
+ keep.emplace_back(ents[i]);
+ } else {
+ // Stash with its original index so the restore is exact.
+ m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
+ ++dropped;
+ }
+ }
+ ents = std::move(keep);
+ }
+ return dropped;
+}
+
+void PrintObject::belt_restore_dropped_fills()
+{
+ if (m_belt_dropped_fills.empty())
+ return;
+ // Ascending index per (layer, region): inserting in that order lands every
+ // entity back at its original position, because each insertion shifts only
+ // the entries after it, which are themselves still to be inserted.
+ std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
+ [](const BeltDroppedFill &a, const BeltDroppedFill &b) {
+ if (a.layer != b.layer) return a.layer < b.layer;
+ if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
+ return a.index < b.index;
+ });
+ for (const BeltDroppedFill &d : m_belt_dropped_fills) {
+ auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
+ ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
+ }
+ m_belt_dropped_fills.clear();
+}
+
+void PrintObject::belt_restore_truncated_layers()
+{
+ if (m_belt_truncated_layers.empty())
+ return;
+
+ m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end());
+ m_belt_truncated_layers.clear();
+ for (size_t i = 0; i < m_layers.size(); ++i) {
+ m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1];
+ m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr;
+ }
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp
new file mode 100644
index 0000000000..7b25ac53ec
--- /dev/null
+++ b/src/libslic3r/BeltSliceStrategy.cpp
@@ -0,0 +1,82 @@
+#include "BeltSliceStrategy.hpp"
+#include "Model.hpp"
+#include "BeltTransform.hpp"
+#include "Point.hpp"
+#include "PrintConfig.hpp"
+
+#include
+#include
+
+namespace Slic3r {
+
+void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
+ const PrintConfig &config,
+ const ModelVolumePtrs &model_volumes,
+ double *out_belt_min_z)
+{
+ // 1. Belt rotation — the sole mesh-side belt transform (matching
+ // BeltTransformPipeline::build_forward_transform). Only active in
+ // belt-printer mode.
+ bool has_rotation = false;
+ if (config.belt_printer.value) {
+ const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation);
+ if (has_rotation) {
+ Transform3d belt_xform = Transform3d::Identity();
+ belt_xform.linear() = rot;
+ trafo = belt_xform * trafo;
+ }
+ }
+
+ if (!has_rotation)
+ return;
+
+ // 2. Z-shift — detect if the mesh clips below the build plate after the
+ // transforms and lift it. Each mesh vertex must be brought into object space
+ // via mv->get_matrix() before applying the full trafo (which is in object
+ // space). Missing this on assemblies (where per-volume get_matrix() positions
+ // each volume within the object) would compute min_z against mesh-local vertex
+ // coordinates rather than object-space coordinates, so volumes translated along
+ // the slicer's Z axis would be silently excluded from the bound check.
+
+ //
+ // The lift is measured to the lowest point of the SUPPORT region, not of the
+ // mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
+ // coordinate) runs below every vertex, and under the leading end of an
+ // overhang it lies below the lowest vertex by up to the overhang's length
+ // times the shear. Supports have to reach that floor, and every support
+ // generator works in layers at z >= 0, so z = 0 has to be the lowest floor
+ // point under the footprint. The layers between it and the first vertex
+ // come out empty, which belt slicing already tolerates (the bottom corner
+ // of a tilted part is a point). Vertices on the belt have z == floor, so
+ // for a part resting on the belt this is simply the floor at its leading
+ // extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
+ BeltTransformPipeline::BeltFloorParams floor;
+ const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
+ double min_z = std::numeric_limits::max();
+ for (const ModelVolume *mv : model_volumes) {
+ if (!mv->is_model_part()) continue;
+ Transform3d vol_trafo = trafo * mv->get_matrix();
+ const auto &its = mv->mesh().its;
+ for (const stl_vertex &v : its.vertices) {
+ Vec3d vm = v.cast();
+ Vec3d pt = vol_trafo * vm;
+ min_z = std::min(min_z, pt.z());
+ if (has_floor)
+ min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
+ }
+ }
+ if (has_floor && min_z != std::numeric_limits::max())
+ min_z -= BeltTransformPipeline::frame_margin(floor);
+ const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.;
+ if (z_shift_val > 0.) {
+ Transform3d z_shift = Transform3d::Identity();
+ z_shift.matrix()(2, 3) = z_shift_val;
+ trafo = z_shift * trafo;
+ }
+ // out_belt_min_z is only meaningful in belt mode.
+ if (out_belt_min_z && config.belt_printer.value) {
+ *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.;
+ }
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp
new file mode 100644
index 0000000000..1385818b5c
--- /dev/null
+++ b/src/libslic3r/BeltSliceStrategy.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "libslic3r.h"
+#include "Point.hpp"
+#include "BeltTransform.hpp"
+#include "PrintConfig.hpp"
+#include "Model.hpp"
+
+namespace Slic3r {
+
+// Belt printer pre-slice transform strategy.
+//
+// Composes, in order, the mesh transforms applied before slicing on a belt printer:
+// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a
+// g-code-side stage, see MachineFrameTransform)
+// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the
+// belt below its footprint
+//
+// Isolates this belt-specific logic from the generic slicing pipeline in
+// PrintObjectSlice.cpp.
+class BeltSliceStrategy
+{
+public:
+ // Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt
+ // rotation is configured.
+ //
+ // out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms.
+ static void apply_preslice_transforms(Transform3d &trafo,
+ const PrintConfig &config,
+ const ModelVolumePtrs &model_volumes,
+ double *out_belt_min_z = nullptr);
+};
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp
new file mode 100644
index 0000000000..9863e80831
--- /dev/null
+++ b/src/libslic3r/BeltTransform.cpp
@@ -0,0 +1,153 @@
+#include "BeltTransform.hpp"
+#include "Model.hpp"
+#include "BoundingBox.hpp"
+#include "Config.hpp"
+#include "Geometry.hpp"
+#include "Point.hpp"
+#include "PrintConfig.hpp"
+#include "libslic3r.h"
+
+#include
+#include
+#include
+#include
+
+namespace Slic3r {
+
+// ---- Matrix builders ------------------------------------------------------
+
+Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
+{
+ BeltRotationAxis axis = config.belt_slice_rotation.value;
+ double angle_deg = config.belt_slice_rotation_angle.value;
+ bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON;
+ if (has_rot_out) *has_rot_out = active;
+ if (!active)
+ return Matrix3d::Identity();
+ double angle_rad = Geometry::deg2rad(angle_deg);
+ Vec3d unit_axis;
+ switch (axis) {
+ case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
+ case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
+ case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
+ default: return Matrix3d::Identity();
+ }
+ return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
+}
+
+Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
+{
+ // Mesh-side belt transform: the rotation. (Shear & scale are a g-code-side
+ // stage, not part of the mesh transform.)
+ Transform3d combined = Transform3d::Identity();
+ combined.linear() = build_rotation_matrix(config);
+ return combined;
+}
+
+// ---- Belt floor parameters ------------------------------------------------
+
+namespace {
+
+// Belt floor in the rotated slicer frame: the image of z_machine = 0 under R.
+// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
+// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
+// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
+void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
+{
+ double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
+ switch (rot_axis) {
+ case BeltRotationAxis::X:
+ out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
+ out.from_axis = 1; // Y
+ break;
+ case BeltRotationAxis::Y:
+ out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
+ out.from_axis = 0; // X
+ break;
+ case BeltRotationAxis::Z:
+ default:
+ out.shear_factor = 0.0;
+ out.from_axis = 1;
+ break;
+ }
+}
+
+// Z of the belt floor directly under a point of the rotated (unshifted) frame.
+inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
+{
+ return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
+}
+
+
+BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
+ const PrintConfig &config, const BoundingBoxf3 &bb, double original_height)
+{
+ BeltTransformPipeline::BeltHeightResult result;
+ result.object_height = original_height;
+
+ // The mesh rotation (the sole mesh-side belt transform).
+ const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
+ const double rot_angle = config.belt_slice_rotation_angle.value;
+
+ bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON;
+ if (!has_rotation)
+ return result;
+
+ // Rotation path: sweep the 8 bbox corners through R to get the rotated height,
+ // then derive the belt floor (the image of machine-Z = 0 under R).
+ double angle_rad = Geometry::deg2rad(rot_angle);
+ Vec3d unit_axis;
+ switch (rot_axis) {
+ case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
+ case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
+ case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
+ default: unit_axis = Vec3d::UnitX(); break;
+ }
+ Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
+ belt_floor_shear(rot_axis, angle_rad, result.floor_params);
+ // The slicing frame starts at the lowest point of the support region: the
+ // lowest belt-floor point under the footprint, not the lowest vertex. The
+ // belt under the leading end of an overhang lies below every vertex of the
+ // part, and supports have to be able to reach it (see
+ // BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
+ // counterpart of this bbox estimate).
+ double min_rz = std::numeric_limits::max();
+ double max_rz = std::numeric_limits::lowest();
+ for (int i = 0; i < 8; ++i) {
+ Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
+ (i & 2) ? bb.max.y() : bb.min.y(),
+ (i & 4) ? bb.max.z() : bb.min.z());
+ Vec3d rc = R * c;
+ double z = rc.z();
+ min_rz = std::min(min_rz, z);
+ max_rz = std::max(max_rz, z);
+ min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
+ }
+ min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
+ result.object_height = max_rz - min_rz;
+
+ result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
+
+ return result;
+}
+
+} // anonymous namespace
+
+BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
+ const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height)
+{
+ return compute_belt_height_and_floor_impl(config, bbox, original_height);
+}
+
+bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
+{
+ out = BeltFloorParams{};
+ const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
+ const double rot_angle = config.belt_slice_rotation_angle.value;
+ if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
+ return false;
+ belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
+ return std::abs(out.shear_factor) > EPSILON;
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp
new file mode 100644
index 0000000000..1aae8ba39b
--- /dev/null
+++ b/src/libslic3r/BeltTransform.hpp
@@ -0,0 +1,132 @@
+#pragma once
+
+#include "libslic3r.h"
+#include "Point.hpp"
+#include "BoundingBox.hpp"
+#include "PrintConfig.hpp"
+#include "Geometry.hpp"
+#include "Config.hpp"
+
+#include
+
+namespace Slic3r {
+
+class ModelObject;
+
+// Shared belt-printer transform math.
+//
+// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
+// trafo_out = z_shift * rotation * trafo_in
+//
+// Rotation is the sole mesh-side belt transform; shear & scale are applied
+// to the g-code instead (see MachineFrameTransform). This class provides the
+// building blocks so every call site uses the same implementation. z_shift is
+// object-dependent (computed from mesh vertex bounds) and is NOT included in
+// build_forward_transform(). The machine-frame shear/scale is derived directly
+// from the tilt angle in MachineFrameTransform and no longer lives here.
+//
+// Design note: this mesh-rotation approach replaced an earlier pre-shear
+// method (now removed). While that initial pre-shear method was instrumental
+// in getting belt printer slicing off the ground in the first place, its place is
+// in the past. A big thank you goes to the Unlayered3D team, who recommended
+// switching to a pre-slice rotation stage instead. Doing so keeps the slicing
+// operation isometric — no distortion of the sliced geometry — while the
+// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale
+// derived from the same tilt angle.
+//
+// This fixed a number of issues, including several issues noticed by hotcubcar
+// regarding adaptive infills not working, gyroid becoming anisotropic, and more
+// that were all mostly resolved as a result of the switch.
+//
+// This also means that the pre-slice rotation transform methodology can be used
+// more cleanly on non-belt printers.
+// - HarrierPigeon (Joseph Robertson)
+
+class BeltTransformPipeline
+{
+public:
+ // ---- Identity checks --------------------------------------------------
+
+ // Whether the G-code axis remap applies at all. The remap fields are only
+ // offered in the belt printer group, so a value left in a profile must not
+ // change a non-belt print: with belt mode off every belt-only key is a no-op.
+ // This is the one place to widen if a non-belt use ever needs them.
+ static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; }
+
+ static bool has_rotation(const PrintConfig &config)
+ {
+ return config.belt_slice_rotation.value != BeltRotationAxis::None &&
+ std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
+ }
+
+ // Physical belt tilt derived from the slicing rotation — the single source of
+ // truth for bed rendering, support gravity tilt and the bed-exclusion
+ // projection. Returns the tilt magnitude in degrees split onto the X and Y
+ // build-plate tilt axes according to the rotation axis:
+ // rotation about X → tilt_x = angle (gantry tilts in the YZ plane)
+ // rotation about Y → tilt_y = angle (gantry tilts in the XZ plane)
+ // rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt)
+ // The magnitude uses abs(angle) so a negative rotation still reports a positive
+ // physical tilt.
+ struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; };
+
+ static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg)
+ {
+ PhysicalTilt t;
+ double mag = std::abs(angle_deg);
+ switch (axis) {
+ case BeltRotationAxis::X: t.tilt_x_deg = mag; break;
+ case BeltRotationAxis::Y: t.tilt_y_deg = mag; break;
+ default: break; // Z / None: no physical tilt
+ }
+ return t;
+ }
+
+ // ---- Matrix builders --------------------------------------------------
+
+ // Build the 3x3 rotation matrix from belt_slice_rotation* config.
+ // Returns Identity if rotation axis is None or angle is ~0.
+ // Also sets has_rot_out if non-null.
+ static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
+
+ // Forward transform (the rotation) — the mesh-side belt transform that
+ // BeltSliceStrategy applies and BeltBackTransform inverts.
+ // Does NOT include the per-object Z-shift.
+ static Transform3d build_forward_transform(const PrintConfig &config);
+
+ // ---- Belt floor parameters --------------------------------------------
+
+ struct BeltFloorParams {
+ double shear_factor = 0.0;
+ int from_axis = 1;
+ double z_shift = 0.0;
+ };
+
+ // Shear factor and from-axis of the belt floor in the rotated slicer frame
+ // (z_floor = shear_factor * u, u = the from-axis coordinate), for the
+ // rotation the config selects. z_shift is left at 0. Returns false (and
+ // zero shear) when the config has no tilt.
+ static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
+
+ // How far below the lowest belt-floor point under the footprint the slicing
+ // frame starts, in slicing Z. A support column meeting the belt is wider at
+ // its base than at its tip, so under a leading overhang the base reaches ahead
+ // of the part along the belt, and the layers that trim it to the belt plane
+ // lie below that lowest point: 10 mm along the belt.
+ static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
+
+ // Result of computing belt height + floor params.
+ struct BeltHeightResult {
+ double object_height; // Effective object height after shear/scale
+ BeltFloorParams floor_params;
+ };
+
+ // Compute effective object height and belt floor parameters from config
+ // and the object's bounding box. original_height is the input height
+ // (bb.size().z() or model_object.max_z()).
+ static BeltHeightResult compute_belt_height_and_floor(
+ const PrintConfig &config, const BoundingBoxf3 &bbox,
+ double original_height);
+};
+
+} // namespace Slic3r
diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp
index d5707ca0de..23b515647b 100644
--- a/src/libslic3r/Brim.cpp
+++ b/src/libslic3r/Brim.cpp
@@ -474,7 +474,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
const bool use_brim_ears = object->config().brim_type == btPainted;
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
- const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
+ // btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading
+ // edge, so it degrades to an ordinary outer brim rather than silently to none.
+ const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || use_auto_brim_ears || use_brim_ears;
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
//ORCA: Select brim base slices from EFC-compensated outline when enabled.
@@ -889,6 +891,17 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
std::vector& printExtruders,
std::map* objectBrimAreasByInstanceOut)
{
+ // Belt printers never use the flat plate brim.
+ //
+ // With a tilted belt the brim has to be laid onto the belt plane over many layers,
+ // which BeltBrim.cpp does during posSupportMaterial. With an untilted belt this
+ // could in principle fall through and produce an ordinary brim, but it would never
+ // reach the G-code: the plate brim is emitted out of skirt_brim_groups(), which
+ // _make_skirt() builds, and that returns early for every belt printer. Running the
+ // generator anyway would just burn time on geometry nobody prints.
+ if (print.config().belt_printer.value)
+ return;
+
std::map brimAreaMap;
Flow flow = print.brim_flow();
ExPolygons islands_area_ex = outer_inner_brim_area(print,
diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp
index 8c53242fe8..9c48363cf8 100644
--- a/src/libslic3r/BuildVolume.hpp
+++ b/src/libslic3r/BuildVolume.hpp
@@ -84,7 +84,7 @@ public:
indexed_triangle_set bounding_mesh(bool scale=true) const;
// Center of the print bed, unscaled.
- Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
+ Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
// Convex hull of polygon(), scaled.
const Polygon& convex_hull() const { return m_convex_hull; }
// Smallest enclosing circle of polygon(), scaled.
diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index 781fd374d4..ca95cfb364 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -85,6 +85,15 @@ set(lisbslic3r_sources
BoundingBox.hpp
BridgeDetector.cpp
BridgeDetector.hpp
+ BeltBrim.cpp
+ BeltBrim.hpp
+ BeltGCode.cpp
+ BeltGCode.hpp
+ BeltPurge.cpp
+ BeltSliceStrategy.cpp
+ BeltSliceStrategy.hpp
+ BeltTransform.cpp
+ BeltTransform.hpp
Brim.cpp
BrimEarsPoint.hpp
Brim.hpp
@@ -171,6 +180,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp
+ Fill/FillTpmsAdaptive.cpp
+ Fill/FillTpmsAdaptive.hpp
Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp
@@ -232,6 +243,14 @@ set(lisbslic3r_sources
GCode/AdaptivePAProcessor.hpp
GCode/AvoidCrossingPerimeters.cpp
GCode/AvoidCrossingPerimeters.hpp
+ GCode/BeltBackTransform.cpp
+ GCode/BeltBackTransform.hpp
+ GCode/MachineFrameTransform.cpp
+ GCode/MachineFrameTransform.hpp
+ GCode/BeltKinematics.cpp
+ GCode/BeltKinematics.hpp
+ GCode/MachineKinematics.cpp
+ GCode/MachineKinematics.hpp
GCode/ConflictChecker.cpp
GCode/ConflictChecker.hpp
GCode/CoolingBuffer.cpp
@@ -453,6 +472,8 @@ set(lisbslic3r_sources
SlicingAdaptive.hpp
Slicing.cpp
Slicing.hpp
+ Support/BeltFloorContext.cpp
+ Support/BeltFloorContext.hpp
Support/SupportCommon.cpp
Support/SupportCommon.hpp
Support/SupportLayer.hpp
diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp
index d695171d1e..5df348281a 100644
--- a/src/libslic3r/ExPolygon.hpp
+++ b/src/libslic3r/ExPolygon.hpp
@@ -403,6 +403,11 @@ inline void translate(ExPolygons &expolys, const Point &p) {
expoly.translate(p);
}
+inline void translate(Polygons &polys, const Point &p) {
+ for (Polygon &poly : polys)
+ poly.translate(p);
+}
+
inline void polygons_append(Polygons &dst, const ExPolygon &src)
{
dst.reserve(dst.size() + src.holes.size() + 1);
diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp
index b1d8ff2b13..146d3ee7b4 100644
--- a/src/libslic3r/Fill/Fill.cpp
+++ b/src/libslic3r/Fill/Fill.cpp
@@ -29,6 +29,7 @@
#include "ExtrusionEntity.hpp"
#include "Fill.hpp"
#include "libslic3r/Fill/FillBase.hpp"
+#include "FillAdaptive.hpp"
#include "FillRectilinear.hpp"
#include "FillLightning.hpp"
#include "FillConcentricInternal.hpp"
@@ -310,6 +311,11 @@ struct SurfaceFillParams
// For Gyroid: when true, use the parameterized "optimized" wave.
bool gyroid_optimized = false;
+ // For TPMS: grade the density from the surface to the interior of the object.
+ TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
+ float tpms_interior_density = 0.f;
+ TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
+
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -352,6 +358,9 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
+ RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
+ RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
+ RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -385,6 +394,9 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized &&
+ this->tpms_adaptive == rhs.tpms_adaptive &&
+ this->tpms_interior_density == rhs.tpms_interior_density &&
+ this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order;
}
@@ -926,7 +938,6 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p
params.lateral_lattice_angle_2 = region_config.lateral_lattice_angle_2;
params.infill_overhang_angle = region_config.infill_overhang_angle;
params.center_of_surface_pattern = region_config.center_of_surface_pattern;
- params.separated_infills = region_config.separated_infills;
if (params.pattern == ipLockedZag) {
params.infill_lock_depth = scale_(region_config.infill_lock_depth);
params.skin_infill_depth = scale_(region_config.skin_infill_depth);
@@ -994,11 +1005,22 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p
// Orca: apply fill multiline only for sparse infill
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
+ // Orca: Pass through separated_infills only where it can move the pattern.
+ params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) &&
+ params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
+
+ // Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
+ params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
+ region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
+ const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
+ params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
+ params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
+
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
- // (which would unnecessarily split fill batching).
+ // (which would unnecessarily split fill batching). Adaptive density replaces it.
// Stored on SurfaceFillParams; copied to FillParams during conversion.
- params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized;
+ params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
@@ -1271,29 +1293,28 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p
// Orca: Anchors and printed infill must share the same body origin. Keep the choice
// here so per-model surface centering and separated sparse infill cannot drift apart.
-static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox)
+// Returns the connected body the fill region is laid out on, or -1 to keep the object's origin.
+static int infill_body(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly)
{
const auto ¶ms = fill.params;
- const auto &config = layer.regions()[fill.region_id]->region().config();
- const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
- const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
+ const bool per_model = (params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface) &&
+ params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
(params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral);
- const bool separate = !external && params.separated_infills &&
- (is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() ||
- !config.sparse_infill_rotate_template.value.empty());
- if (per_model || separate) {
- double best_overlap = 0.;
- for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) {
+ int body = -1;
+ if (per_model || params.separated_infills || is_octree_infill_pattern(params.pattern)) {
+ const BoundingBox box = get_extents(expoly);
+ double best_overlap = 0.;
+ for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_ids.size(); ++i) {
+ if (! layer.lslices_bboxes[i].overlap(box))
+ continue;
const double overlap = area(intersection_ex(layer.lslices[i], expoly));
if (overlap > best_overlap) {
best_overlap = overlap;
- const Point center = layer.lslices_separated_component_bboxes[i].center();
- bbox = layer.object()->bounding_box();
- bbox.translate(center.x(), center.y());
+ body = int(layer.lslices_separated_component_ids[i]);
}
}
}
- return bbox;
+ return body;
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
@@ -1318,7 +1339,7 @@ void export_group_fills_to_svg(const char *path, const std::vector
#endif
// friend to Layer
-void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator)
+void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator)
{
for (LayerRegion *layerm : m_regions)
layerm->fills.clear();
@@ -1351,7 +1372,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
f->z = this->print_z;
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
- f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree;
+ const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
+ f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1401,6 +1423,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
+ params.tpms_adaptive = surface_fill.params.tpms_adaptive;
+ params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
+ params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// BBS
@@ -1443,8 +1468,10 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.can_reverse = false;
for (ExPolygon& expoly : surface_fill.expolygons) {
- // Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface.
- f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
+ // Orca: Reuse the body box and octree used for bridge anchoring, resetting them for each surface.
+ const int body = infill_body(*this, surface_fill, expoly);
+ f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
+ f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
@@ -1512,7 +1539,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
* - For lightning/adaptive patterns, the respective generators are wired so their
* polylines match the final infill layout.
*/
-Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator) const
+Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator) const
{
LockRegionParam skin_inner_param;
std::vector surface_fills = group_fills(*this, skin_inner_param);
@@ -1570,7 +1597,8 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
f->z = this->print_z;
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
- f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree;
+ const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
+ f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
@@ -1610,6 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
+ params.tpms_adaptive = surface_fill.params.tpms_adaptive;
+ params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
+ params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds
@@ -1617,8 +1648,10 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
params.extrusion_role = surface_fill.params.extrusion_role;
for (ExPolygon &expoly : surface_fill.expolygons) {
- // Orca: Match the per-body origin of make_fills() before generating physical anchors.
- f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
+ // Orca: Match the per-body box and octree of make_fills() before generating physical anchors.
+ const int body = infill_body(*this, surface_fill, expoly);
+ f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
+ f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
f->spacing = surface_fill.params.spacing;
surface_fill.surface.expolygon = std::move(expoly);
diff --git a/src/libslic3r/Fill/Fill3DHoneycomb.hpp b/src/libslic3r/Fill/Fill3DHoneycomb.hpp
index a31608310d..66e16510da 100644
--- a/src/libslic3r/Fill/Fill3DHoneycomb.hpp
+++ b/src/libslic3r/Fill/Fill3DHoneycomb.hpp
@@ -25,6 +25,7 @@ public:
// pattern is placed on top of previous layers
bool use_bridge_flow() const override { return false; }
bool is_self_crossing() override { return false; }
+ bool aligned_to_origin() const override { return true; }
protected:
void _fill_surface_single(
diff --git a/src/libslic3r/Fill/FillAdaptive.cpp b/src/libslic3r/Fill/FillAdaptive.cpp
index 210394ea51..b693ae7cfa 100644
--- a/src/libslic3r/Fill/FillAdaptive.cpp
+++ b/src/libslic3r/Fill/FillAdaptive.cpp
@@ -301,88 +301,25 @@ void OctreeDeleter::operator()(Octree *p) {
delete p;
}
-std::pair adaptive_fill_line_spacing(const PrintObject &print_object)
+std::vector adaptive_fill_line_spacing(const PrintObject &print_object)
{
- // Output, spacing for icAdaptiveCubic and icSupportCubic
- double adaptive_line_spacing = 0.;
- double support_line_spacing = 0.;
-
- enum class Tristate {
- Yes,
- No,
- Maybe
- };
- struct RegionFillData {
- Tristate has_adaptive_infill;
- Tristate has_support_infill;
- double density;
- double extrusion_width;
- };
- std::vector region_fill_data;
- region_fill_data.reserve(print_object.num_printing_regions());
- bool build_octree = false;
+ std::vector line_spacing(print_object.num_printing_regions(), 0.);
const std::vector &nozzle_diameters = print_object.print()->config().nozzle_diameter.values;
double max_nozzle_diameter = *std::max_element(nozzle_diameters.begin(), nozzle_diameters.end());
double default_infill_extrusion_width = Flow::auto_extrusion_width(FlowRole::frInfill, float(max_nozzle_diameter));
- for (size_t region_id = 0; region_id < print_object.num_printing_regions(); ++ region_id) {
- const PrintRegionConfig &config = print_object.printing_region(region_id).config();
- bool nonempty = config.sparse_infill_density > 0;
- bool has_adaptive_infill = nonempty && config.sparse_infill_pattern == ipAdaptiveCubic;
- bool has_support_infill = nonempty && config.sparse_infill_pattern == ipSupportCubic;
- double sparse_infill_line_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
- region_fill_data.push_back(RegionFillData({
- has_adaptive_infill ? Tristate::Maybe : Tristate::No,
- has_support_infill ? Tristate::Maybe : Tristate::No,
- config.sparse_infill_density,
- sparse_infill_line_width != 0. ? sparse_infill_line_width : default_infill_extrusion_width
- }));
- build_octree |= has_adaptive_infill || has_support_infill;
+ for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id) {
+ const PrintRegionConfig &config = print_object.printing_region(region_id).config();
+ if (config.sparse_infill_density <= 0 || ! is_octree_infill_pattern(config.sparse_infill_pattern) ||
+ std::none_of(print_object.layers().begin(), print_object.layers().end(), [region_id](const Layer *layer) {
+ return region_id < layer->regions().size() && ! layer->regions()[region_id]->fill_surfaces.empty();
+ }))
+ continue;
+ double extrusion_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
+ if (extrusion_width == 0.)
+ extrusion_width = default_infill_extrusion_width;
+ line_spacing[region_id] = extrusion_width / ((config.sparse_infill_density / 100.0f) * 0.333333333f) * config.fill_multiline.value;
}
-
- if (build_octree) {
- // Compute the average of above parameters over all layers
- for (const Layer *layer : print_object.layers())
- for (size_t region_id = 0; region_id < layer->regions().size(); ++ region_id) {
- RegionFillData &rd = region_fill_data[region_id];
- if (rd.has_adaptive_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
- rd.has_adaptive_infill = Tristate::Yes;
- if (rd.has_support_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
- rd.has_support_infill = Tristate::Yes;
- }
-
- double adaptive_fill_density = 0.;
- double adaptive_infill_extrusion_width = 0.;
- int adaptive_cnt = 0;
- double support_fill_density = 0.;
- double support_infill_extrusion_width = 0.;
- int support_cnt = 0;
-
- for (const RegionFillData &rd : region_fill_data) {
- if (rd.has_adaptive_infill == Tristate::Yes) {
- adaptive_fill_density += rd.density;
- adaptive_infill_extrusion_width += rd.extrusion_width;
- ++ adaptive_cnt;
- } else if (rd.has_support_infill == Tristate::Yes) {
- support_fill_density += rd.density;
- support_infill_extrusion_width += rd.extrusion_width;
- ++ support_cnt;
- }
- }
-
- auto to_line_spacing = [](int cnt, double density, double extrusion_width) {
- if (cnt) {
- density /= double(cnt);
- extrusion_width /= double(cnt);
- return extrusion_width / ((density / 100.0f) * 0.333333333f);
- } else
- return 0.;
- };
- const int n_multiline = print_object.printing_region(0).config().fill_multiline.value;
- adaptive_line_spacing = to_line_spacing(adaptive_cnt, adaptive_fill_density, adaptive_infill_extrusion_width) * n_multiline;
- support_line_spacing = to_line_spacing(support_cnt, support_fill_density, support_infill_extrusion_width) * n_multiline;
- }
-
- return std::make_pair(adaptive_line_spacing, support_line_spacing);
+ return line_spacing;
}
// Context used by generate_infill_lines() when recursively traversing an octree in a DDA fashion
diff --git a/src/libslic3r/Fill/FillAdaptive.hpp b/src/libslic3r/Fill/FillAdaptive.hpp
index 9b424133a2..47b6f62e28 100644
--- a/src/libslic3r/Fill/FillAdaptive.hpp
+++ b/src/libslic3r/Fill/FillAdaptive.hpp
@@ -14,6 +14,7 @@
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "FillBase.hpp"
+#include
#include
#include
#include
@@ -37,11 +38,43 @@ struct Octree;
struct OctreeDeleter { void operator()(Octree *p); };
using OctreePtr = std::unique_ptr;
-// Calculate line spacing for
-// 1) adaptive cubic infill
-// 2) adaptive internal support cubic infill
-// Returns zero for a particular infill type if no such infill is to be generated.
-std::pair adaptive_fill_line_spacing(const PrintObject &print_object);
+// Orca: One octree per body (see Layer::lslices_separated_component_ids), and one of the whole object
+// for objects of a single body or with a body that has none of its own.
+struct Octrees
+{
+ OctreePtr object;
+ std::vector bodies;
+
+ // A body without an octree, or body -1, uses the object's, or any body's when the object has none.
+ Octree *get(int body) const
+ {
+ if (body >= 0 && size_t(body) < bodies.size() && bodies[body])
+ return bodies[body].get();
+ if (object)
+ return object.get();
+ for (const OctreePtr &octree : bodies)
+ if (octree)
+ return octree.get();
+ return nullptr;
+ }
+};
+
+// Orca: The octrees of each line spacing the regions of an object fill with.
+struct RegionOctrees
+{
+ std::vector sets;
+ // Index into sets for each region, -1 for a region without adaptive or support cubic infill.
+ std::vector region_set;
+
+ const Octrees *region(size_t region_id) const
+ {
+ return region_id < region_set.size() && region_set[region_id] >= 0 ? &sets[region_set[region_id]] : nullptr;
+ }
+};
+
+// Line spacing of the adaptive or support cubic infill of each region of the object,
+// zero for a region that generates no such infill.
+std::vector adaptive_fill_line_spacing(const PrintObject &print_object);
// Rotation of the octree to stand on one of its corners.
Eigen::Quaterniond transform_to_world();
diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp
index 95b7dc28db..a852f2c42b 100644
--- a/src/libslic3r/Fill/FillBase.cpp
+++ b/src/libslic3r/Fill/FillBase.cpp
@@ -128,6 +128,9 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms)
{
// Perform offset.
Slic3r::ExPolygons expp = offset_ex(surface->expolygon, float(scale_(this->overlap - 0.5 * this->spacing)));
+ // Orca: Separated infills move the box center onto each body; origin-aligned patterns follow it.
+ const Point shift = this->aligned_to_origin() && ! empty(this->bounding_box) ? this->bounding_box.center() : Point::Zero();
+ translate(expp, -shift);
// Create the infills for each of the regions.
Polylines polylines_out;
for (size_t i = 0; i < expp.size(); ++ i)
@@ -137,6 +140,8 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms)
_infill_direction(surface),
std::move(expp[i]),
polylines_out);
+ for (Polyline &pl : polylines_out)
+ pl.translate(shift);
return polylines_out;
}
@@ -1591,13 +1596,18 @@ BoundaryInfillGraph create_boundary_infill_graph(const Polylines &infill_ordered
// The extended bounding box of the whole object that covers any rotation of every layer.
BoundingBox Fill::extended_object_bounding_box() const
{
- BoundingBox out = bounding_box;
+ // Orca: Extend about the box center, which separated infills move off the origin.
+ const Point c = this->bounding_box.center();
+ BoundingBox out = this->bounding_box;
+ out.translate(-c.x(), -c.y());
out.merge(Point(out.min.y(), out.min.x()));
out.merge(Point(out.max.y(), out.max.x()));
// The bounding box is scaled by sqrt(2.) to ensure that the bounding box
// covers any possible rotations.
- return out.scaled(sqrt(2.));
+ out = out.scaled(sqrt(2.));
+ out.translate(c.x(), c.y());
+ return out;
}
void Fill::connect_infill(Polylines &&infill_ordered, const std::vector &boundary_src, const BoundingBox &bbox, Polylines &polylines_out, const double spacing, const FillParams ¶ms)
diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp
index 7599b2a0d2..e0308c9450 100644
--- a/src/libslic3r/Fill/FillBase.hpp
+++ b/src/libslic3r/Fill/FillBase.hpp
@@ -33,6 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r {
class Surface;
+class TpmsRadialField;
enum InfillPattern : int;
namespace FillAdaptive {
@@ -91,6 +92,11 @@ struct FillParams
// For Gyroid: when true, use the parameterized "optimized" variant.
bool gyroid_optimized { false };
+ // For TPMS: grade the density from the surface to the interior of the object. Density fraction.
+ TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
+ float tpms_interior_density { 0.f };
+ TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
+
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -155,6 +161,9 @@ public:
// Octree builds on mesh for usage in the adaptive cubic infill
FillAdaptive::Octree* adapt_fill_octree = nullptr;
+ // Radial coordinate inside the object for the adaptive TPMS infill
+ const TpmsRadialField* tpms_radial_field = nullptr;
+
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function.
const PrintConfig *print_config = nullptr;
@@ -188,6 +197,9 @@ public:
// Return true if infill has a consistent pattern between layers.
virtual bool has_consistent_pattern() const { return false; }
+ // Orca: Is the pattern laid out from the origin instead of the bounding box center?
+ virtual bool aligned_to_origin() const { return false; }
+
// Perform the fill.
virtual Polylines fill_surface(const Surface *surface, const FillParams ¶ms);
virtual ThickPolylines fill_surface_arachne(const Surface* surface, const FillParams& params);
diff --git a/src/libslic3r/Fill/FillCrossHatch.hpp b/src/libslic3r/Fill/FillCrossHatch.hpp
index 4b18443aae..14eb7fcbf4 100644
--- a/src/libslic3r/Fill/FillCrossHatch.hpp
+++ b/src/libslic3r/Fill/FillCrossHatch.hpp
@@ -19,6 +19,7 @@ public:
Fill *clone() const override { return new FillCrossHatch(*this); };
~FillCrossHatch() override {}
bool is_self_crossing() override { return false; }
+ bool aligned_to_origin() const override { return true; }
protected:
void _fill_surface_single(
diff --git a/src/libslic3r/Fill/FillGyroid.cpp b/src/libslic3r/Fill/FillGyroid.cpp
index 3b2803738c..06cb55f70e 100644
--- a/src/libslic3r/Fill/FillGyroid.cpp
+++ b/src/libslic3r/Fill/FillGyroid.cpp
@@ -17,6 +17,17 @@
#include "libslic3r/Polyline.hpp"
#include "FillGyroid.hpp"
#include "libslic3r/Polygon.hpp"
+#include "libslic3r/PrintConfig.hpp"
+#include "FillTpmsAdaptive.hpp"
+
+namespace Slic3r {
+
+static float gyroid(float x, float y, float z)
+{
+ return std::sin(x) * std::cos(y) + std::sin(y) * std::cos(z) + std::sin(z) * std::cos(x);
+}
+
+} // namespace Slic3r
// ---------------------------------------------------------------------------
// Marching-squares scalar field for the optimized gyroid branch.
@@ -62,10 +73,7 @@ struct GyroidField
float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const
{
- const float a = fx * float(x);
- const float b = fy * float(y);
- const float c = fz * float(z_arg);
- return std::sin(a) * std::cos(b) + std::sin(b) * std::cos(c) + std::sin(c) * std::cos(a);
+ return gyroid(fx * float(x), fy * float(y), fz * float(z_arg));
}
float get_scalar(Coord p) const
@@ -307,6 +315,14 @@ void FillGyroid::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
+ if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
+ fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
+ [&](const FillParams &shell_params, const ExPolygon &shell) {
+ this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
+ });
+ return;
+ }
+
auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
if(std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle);
@@ -326,7 +342,12 @@ void FillGyroid::_fill_surface_single(
// generate pattern
Polylines polylines;
- if (params.gyroid_optimized) {
+ if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
+ // Radians per mm of the regular pattern at a density.
+ auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
+ polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
+ *this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
+ } else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
// omega >= 1 always, so fz >= baseline -> shorter vertical wavelength ->
diff --git a/src/libslic3r/Fill/FillGyroid.hpp b/src/libslic3r/Fill/FillGyroid.hpp
index ad032d5904..a60216824b 100644
--- a/src/libslic3r/Fill/FillGyroid.hpp
+++ b/src/libslic3r/Fill/FillGyroid.hpp
@@ -20,6 +20,7 @@ public:
// require bridge flow since most of this pattern hangs in air
bool use_bridge_flow() const override { return false; }
bool is_self_crossing() override { return false; }
+ bool aligned_to_origin() const override { return true; }
// Correction applied to regular infill angle to maximize printing
// speed in default configuration (degrees)
diff --git a/src/libslic3r/Fill/FillHoneycomb.hpp b/src/libslic3r/Fill/FillHoneycomb.hpp
index fd1fe039de..d682e69b2c 100644
--- a/src/libslic3r/Fill/FillHoneycomb.hpp
+++ b/src/libslic3r/Fill/FillHoneycomb.hpp
@@ -20,6 +20,7 @@ class FillHoneycomb : public Fill
public:
~FillHoneycomb() override {}
bool is_self_crossing() override { return false; }
+ bool aligned_to_origin() const override { return true; }
protected:
Fill* clone() const override { return new FillHoneycomb(*this); };
diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp
index 85de82e27c..f7954ef7fc 100644
--- a/src/libslic3r/Fill/FillRectilinear.cpp
+++ b/src/libslic3r/Fill/FillRectilinear.cpp
@@ -2750,23 +2750,6 @@ static void polylines_from_paths(const std::vector &path, c
}
}
-// The extended bounding box of the whole object that covers any rotation of every layer.
-BoundingBox FillRectilinear::extended_object_bounding_box() const {
- // Build the extension around the box center. The transpose merge and the sqrt(2.) scaling
- // (which covers any possible rotation) are both defined about the origin, so a box that is not
- // origin-centered — e.g. a separated-infill box re-centered on a single assembly part — would be
- // distorted. Shift to the origin first and back afterwards; for the default origin-centered box
- // the two translations cancel and this is identical to the original behavior.
- const Point c = this->bounding_box.center();
- BoundingBox out = this->bounding_box;
- out.translate(-c.x(), -c.y());
- out.merge(Point(out.min.y(), out.min.x()));
- out.merge(Point(out.max.y(), out.max.x()));
- out = out.scaled(sqrt(2.));
- out.translate(c.x(), c.y());
- return out;
-}
-
bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillParams ¶ms, float angleBase, float pattern_shift, Polylines &polylines_out)
{
// At the end, only the new polylines will be rotated back.
@@ -2801,7 +2784,13 @@ bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillPa
// For infill that needs to be consistent between layers (like Zig Zag),
// we use bounding box of whole object to match vertical lines between layers.
BoundingBox bounding_box_src = poly_with_offset.bounding_box_src();
- BoundingBox bounding_box = this->has_consistent_pattern() ? this->extended_object_bounding_box() : bounding_box_src;
+ BoundingBox bounding_box = bounding_box_src;
+ if (this->has_consistent_pattern()) {
+ // Orca: The polygons are rotated about the origin, so follow the box center to where it was rotated.
+ const Point c = this->bounding_box.center();
+ bounding_box = this->extended_object_bounding_box();
+ bounding_box.translate(c.rotated(- rotate_vector.first) - c);
+ }
// define flow spacing according to requested density
if (params.full_infill() && !params.dont_adjust) {
diff --git a/src/libslic3r/Fill/FillRectilinear.hpp b/src/libslic3r/Fill/FillRectilinear.hpp
index 59bb56a5c8..56f4505bd2 100644
--- a/src/libslic3r/Fill/FillRectilinear.hpp
+++ b/src/libslic3r/Fill/FillRectilinear.hpp
@@ -42,9 +42,6 @@ protected:
};
bool fill_surface_by_multilines(const Surface *surface, FillParams params, const std::initializer_list &sweep_params, Polylines &polylines_out);
bool fill_surface_trapezoidal(const Surface *surface, FillParams params, Polylines &polylines_out, int Pattern_type);
-
- // The extended bounding box of the whole object that covers any rotation of every layer.
- BoundingBox extended_object_bounding_box() const;
};
class FillAlignedRectilinear : public FillRectilinear
diff --git a/src/libslic3r/Fill/FillTpmsAdaptive.cpp b/src/libslic3r/Fill/FillTpmsAdaptive.cpp
new file mode 100644
index 0000000000..2dcd2930b9
--- /dev/null
+++ b/src/libslic3r/Fill/FillTpmsAdaptive.cpp
@@ -0,0 +1,823 @@
+#include "FillTpmsAdaptive.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "../BoundingBox.hpp"
+#include "../ClipperUtils.hpp"
+#include "../ExPolygon.hpp"
+#include "FillBase.hpp"
+#include "../Execution/ExecutionTBB.hpp"
+#include "../MarchingSquares.hpp"
+#include "../Point.hpp"
+#include "../Polygon.hpp"
+#include "../Polyline.hpp"
+#include "../PrintConfig.hpp"
+#include "../libslic3r.h"
+
+namespace Slic3r {
+
+namespace {
+
+// At most 4 MB of body indices; finer cells would not change the grading.
+constexpr double MaxNodes = double(1 << 20);
+constexpr double MinCellSize = 0.5;
+
+// Two deepest points are in separate lobes when the depth between them drops below this ratio of the shallower one.
+constexpr double NeckRatio = 0.8;
+// Lobes shallower than this ratio of the deepest one of their body are graded as part of it.
+constexpr double MinLobeRatio = 0.3;
+// A lobe reaches twice as far as the side towards its neighbour, so that the side is half way to the surface.
+constexpr double LobeReach = 2.;
+// Width of the morph between the patterns of two lobes, in their distance to the center over its depth.
+constexpr double LobeMorph = 0.1;
+// Densities of the levels of Stepped shells, and of Smooth blend, are at most these ratios apart. Fewer levels blend
+// with fewer lines running along the blends.
+constexpr double ShellRatio = 1.5;
+constexpr double BlendRatio = 2.5;
+// Distance warp: samples of the mean depth along a ray.
+constexpr int Samples = 32;
+
+constexpr float InfF = std::numeric_limits::infinity();
+constexpr double InfD = std::numeric_limits::infinity();
+
+// Squared distance transform of a line (Felzenszwalb & Huttenlocher); infinite samples are no sites.
+void distance_transform_line(const float *f, float *d, int n, int *v, double *s)
+{
+ int k = -1;
+ for (int q = 0; q < n; ++q) {
+ if (f[q] == InfF)
+ continue;
+ double x = -InfD;
+ while (k >= 0) {
+ x = (f[q] + double(q) * q - f[v[k]] - double(v[k]) * v[k]) / (2. * (q - v[k]));
+ if (x > s[k])
+ break;
+ --k;
+ }
+ if (k < 0)
+ x = -InfD;
+ v[++k] = q;
+ s[k] = x;
+ s[k + 1] = InfD;
+ }
+ if (k < 0) {
+ std::fill(d, d + n, InfF);
+ return;
+ }
+ for (int q = 0, j = 0; q < n; ++q) {
+ while (s[j + 1] < q)
+ ++j;
+ d[q] = float(sqr(double(q - v[j])) + f[v[j]]);
+ }
+}
+
+void distance_transform_axis(std::vector &grid, const Vec3i32 &size, int axis, const std::function &throw_if_canceled)
+{
+ const int n = size[axis];
+ const int a1 = (axis + 1) % 3;
+ const int a2 = (axis + 2) % 3;
+ const size_t stride = axis == 0 ? 1 : axis == 1 ? size_t(size.x()) : size_t(size.x()) * size.y();
+ tbb::parallel_for(tbb::blocked_range(0, size_t(size[a1]) * size[a2]), [&](const tbb::blocked_range &range) {
+ std::vector f(n), d(n);
+ std::vector v(n);
+ std::vector s(n + 1);
+ for (size_t line = range.begin(); line < range.end(); ++line) {
+ Vec3i32 idx;
+ idx[axis] = 0;
+ idx[a1] = int(line % size[a1]);
+ idx[a2] = int(line / size[a1]);
+ const size_t first = (size_t(idx.z()) * size.y() + idx.y()) * size.x() + idx.x();
+ for (int i = 0; i < n; ++i)
+ f[i] = grid[first + i * stride];
+ distance_transform_line(f.data(), d.data(), n, v.data(), s.data());
+ for (int i = 0; i < n; ++i)
+ grid[first + i * stride] = d[i];
+ }
+ throw_if_canceled();
+ });
+}
+
+// Marks the nodes inside the expolygons with infinity, by even-odd scanlines.
+void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, int nx, int ny, float *nodes)
+{
+ std::vector> crossings(ny);
+ auto add_crossings = [&](const Polygon &polygon) {
+ const Points &pts = polygon.points;
+ for (size_t i = 0; i < pts.size(); ++i) {
+ const Vec2d a = unscaled(pts[i]);
+ const Vec2d b = unscaled(pts[i + 1 == pts.size() ? 0 : i + 1]);
+ if (a.y() == b.y())
+ continue;
+ const auto [lo, hi] = std::minmax(a.y(), b.y());
+ const int j0 = std::max(0, int(std::ceil((lo - origin.y()) / cell)));
+ const int j1 = std::min(ny, int(std::ceil((hi - origin.y()) / cell)));
+ for (int j = j0; j < j1; ++j) {
+ const double y = origin.y() + j * cell;
+ crossings[j].push_back(a.x() + (b.x() - a.x()) * (y - a.y()) / (b.y() - a.y()));
+ }
+ }
+ };
+ for (const ExPolygon &expolygon : expolygons) {
+ add_crossings(expolygon.contour);
+ for (const Polygon &hole : expolygon.holes)
+ add_crossings(hole);
+ }
+ for (int j = 0; j < ny; ++j) {
+ std::vector &xs = crossings[j];
+ std::sort(xs.begin(), xs.end());
+ for (size_t k = 0; k + 1 < xs.size(); k += 2) {
+ const int i0 = std::max(0, int(std::ceil((xs[k] - origin.x()) / cell)));
+ const int i1 = std::min(nx, int(std::ceil((xs[k + 1] - origin.x()) / cell)));
+ std::fill(nodes + size_t(j) * nx + std::min(i0, i1), nodes + size_t(j) * nx + i1, InfF);
+ }
+ }
+}
+
+// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
+double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
+{
+ switch (gradient) {
+ case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
+ case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
+ default: return 1. + (ratio - 1.) * depth;
+ }
+}
+
+// Levels of Stepped shells and Smooth blend, geometric from the surface at 0 to the interior at count, and the
+// continuous level of the target of a depth.
+struct DensityLevels
+{
+ DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
+ : ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
+ {}
+
+ double scale(int level) const { return count == 0 ? 1. : std::pow(ratio, double(level) / count); }
+ double level(double depth) const
+ {
+ return count == 0 ? 0. : count * std::log(target_scale(ratio, gradient, depth)) / std::log(ratio);
+ }
+
+ double ratio;
+ TpmsAdaptiveGradient gradient;
+ int count;
+};
+
+// Scale of the pattern around the center at a radial coordinate t. The mean cell scale over the ball of radius t,
+// t^-3 * integral of 3 t'^2 * target(t'), or over the disc in 2D, follows the gradient; beyond the surface the target
+// is the surface scale.
+class RadialScale
+{
+public:
+ RadialScale(const AdaptiveTpms &tpms, int dimensions) : m_dimensions(dimensions)
+ {
+ const double ratio = std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3);
+ auto target = [&tpms, ratio](double depth) { return target_scale(ratio, tpms.gradient, depth); };
+ m_scale[0] = target(1.);
+ double volume = 0.;
+ for (size_t i = 1; i < m_scale.size(); ++i) {
+ const double t0 = double(i - 1) / double(m_scale.size() - 1);
+ const double t1 = double(i) / double(m_scale.size() - 1);
+ volume += (std::pow(t1, m_dimensions) - std::pow(t0, m_dimensions)) * target(1. - 0.5 * (t0 + t1));
+ m_scale[i] = volume / std::pow(t1, m_dimensions);
+ }
+ }
+
+ double operator()(double t) const
+ {
+ if (t >= 1.) {
+ const double volume = std::pow(t, m_dimensions);
+ return (m_scale.back() + volume - 1.) / volume;
+ }
+ const double x = t * double(m_scale.size() - 1);
+ const size_t i = std::min(size_t(x), m_scale.size() - 2);
+ return m_scale[i] + (m_scale[i + 1] - m_scale[i]) * (x - double(i));
+ }
+
+private:
+ int m_dimensions;
+ std::array m_scale;
+};
+
+} // namespace
+
+TpmsRadialField::TpmsRadialField(const std::vector &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
+ const std::function &throw_if_canceled)
+ : m_mode(mode)
+ , m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
+{
+ assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled);
+ const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
+ const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
+
+ // Padded by a node on each side, so that the border of the grid is outside.
+ m_cell = std::max(MinCellSize, std::cbrt(extent.prod() / MaxNodes));
+ auto nodes = [this](double length) { return int(std::ceil(length / m_cell)) + 3; };
+ while (double(nodes(extent.x())) * nodes(extent.y()) * nodes(extent.z()) > MaxNodes)
+ m_cell *= 1.1;
+ m_size = Vec3i32(nodes(extent.x()), nodes(extent.y()), nodes(extent.z()));
+ m_origin = min - Vec3d::Constant(m_cell);
+
+ const size_t sy = size_t(m_size.x());
+ const size_t sz = sy * m_size.y();
+ std::vector depth(sz * m_size.z(), 0.f);
+ tbb::parallel_for(tbb::blocked_range(0, m_size.z()), [&](const tbb::blocked_range &range) {
+ for (int k = range.begin(); k < range.end(); ++k) {
+ const double z = m_origin.z() + k * m_cell;
+ auto it = std::lower_bound(slices.begin(), slices.end(), z, [](const Slice &s, double z) { return s.top_z < z; });
+ if (it != slices.end() && z > it->bottom_z)
+ rasterize(*it->expolygons, m_origin.head<2>(), m_cell, m_size.x(), m_size.y(), depth.data() + k * sz);
+ }
+ throw_if_canceled();
+ });
+ for (int axis = 0; axis < 3; ++axis)
+ if (axis != m_axis)
+ distance_transform_axis(depth, m_size, axis, throw_if_canceled);
+
+ auto position = [this, sy, sz](size_t i) {
+ return Vec3d(m_origin + m_cell * Vec3d(double(i % sy), double(i / sy % m_size.y()), double(i / sz)));
+ };
+ const std::array steps{1, -1, std::ptrdiff_t(sy), -std::ptrdiff_t(sy), std::ptrdiff_t(sz), -std::ptrdiff_t(sz)};
+
+ auto node_of = [this, sy, sz](const Vec3d &pt) -> std::ptrdiff_t {
+ const Vec3d f = (pt - m_origin) / m_cell;
+ const long x = std::lround(f.x()), y = std::lround(f.y()), z = std::lround(f.z());
+ if (x < 0 || y < 0 || z < 0 || x >= m_size.x() || y >= m_size.y() || z >= m_size.z())
+ return -1;
+ return std::ptrdiff_t(size_t(z) * sz + size_t(y) * sy + size_t(x));
+ };
+ // In the 2D modes, the steps within a section.
+ auto in_section = [this](size_t step) { return int(step / 2) != m_axis; };
+ std::vector neighbours;
+ for (int dz = -1; dz <= 1; ++dz)
+ for (int dy = -1; dy <= 1; ++dy)
+ for (int dx = -1; dx <= 1; ++dx)
+ if ((dx != 0 || dy != 0 || dz != 0) && (m_axis < 0 || Vec3i32(dx, dy, dz)[m_axis] == 0))
+ neighbours.push_back(std::ptrdiff_t(dz) * std::ptrdiff_t(sz) + std::ptrdiff_t(dy) * std::ptrdiff_t(sy) + dx);
+
+ // Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
+ // centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
+ // is the node nearest to the middle of the tied nodes.
+ const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend;
+ m_body.assign(depth.size(), -1);
+ std::vector body_nodes;
+ for (size_t seed = 0; seed < depth.size(); ++seed) {
+ if (depth[seed] == 0.f || m_body[seed] >= 0)
+ continue;
+ const int id = int(m_bodies.size());
+ body_nodes.assign(1, seed);
+ m_body[seed] = id;
+ float max_depth = 0.f;
+ for (size_t k = 0; k < body_nodes.size(); ++k) {
+ max_depth = std::max(max_depth, depth[body_nodes[k]]);
+ for (size_t s = 0; s < steps.size(); ++s)
+ if (const size_t j = body_nodes[k] + steps[s]; in_section(s) && depth[j] > 0.f && m_body[j] < 0) {
+ m_body[j] = id;
+ body_nodes.push_back(j);
+ }
+ }
+ m_bodies.push_back({m_lobes.size(), 0, (std::sqrt(double(max_depth)) - 0.5) * m_cell});
+ if (!lobes)
+ continue;
+
+ std::vector peaks;
+ for (size_t i : body_nodes)
+ if (depth[i] >= sqr(MinLobeRatio) * max_depth &&
+ std::all_of(neighbours.begin(), neighbours.end(), [&](std::ptrdiff_t n) { return depth[i + n] <= depth[i]; }))
+ peaks.push_back(i);
+ std::sort(peaks.begin(), peaks.end(), [&depth](size_t a, size_t b) { return depth[a] > depth[b] || (depth[a] == depth[b] && a < b); });
+ auto necked = [&](size_t a, size_t b) {
+ const Vec3d pa = position(a), pb = position(b);
+ const double limit = sqr(NeckRatio) * std::min(depth[a], depth[b]);
+ const int samples = int(std::ceil((pb - pa).norm() / (0.5 * m_cell)));
+ for (int s = 1; s < samples; ++s)
+ if (depth[node_of(pa + (pb - pa) * (double(s) / samples))] < limit)
+ return true;
+ return false;
+ };
+ // A peak joins the first lobe it sees without a neck, if it is as deep. The lobes are made one at a time,
+ // from the first remaining peak, testing the others in parallel.
+ std::vector> ties;
+ while (!peaks.empty()) {
+ const size_t front = peaks.front();
+ std::vector joins(peaks.size(), 0);
+ tbb::parallel_for(tbb::blocked_range(1, peaks.size()), [&](const tbb::blocked_range &range) {
+ for (size_t k = range.begin(); k < range.end(); ++k)
+ if (!necked(front, peaks[k]))
+ joins[k] = std::sqrt(depth[peaks[k]]) >= std::sqrt(depth[front]) - 1.f ? 1 : 2;
+ });
+ std::vector &tied = ties.emplace_back(1, front);
+ std::vector remaining;
+ for (size_t k = 1; k < peaks.size(); ++k)
+ if (joins[k] == 0)
+ remaining.push_back(peaks[k]);
+ else if (joins[k] == 1)
+ tied.push_back(peaks[k]);
+ peaks = std::move(remaining);
+ }
+ m_bodies.back().lobes = ties.size();
+ for (const std::vector &tied : ties) {
+ Vec3d middle(0., 0., 0.);
+ for (size_t i : tied)
+ middle += position(i);
+ middle /= double(tied.size());
+ Vec3d center = position(tied.front());
+ for (size_t i : tied)
+ if ((position(i) - middle).squaredNorm() < (center - middle).squaredNorm())
+ center = position(i);
+ m_lobes.push_back({center, (std::sqrt(double(depth[tied.front()])) - 0.5) * m_cell, {}});
+ }
+ }
+ throw_if_canceled();
+
+ if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
+ m_depth.assign(depth.size(), 0.f);
+ for (size_t i = 0; i < depth.size(); ++i)
+ if (m_body[i] >= 0)
+ m_depth[i] = float(std::min(1., std::max(0., std::sqrt(double(depth[i])) - 0.5) * m_cell / m_bodies[m_body[i]].depth));
+ }
+
+ // The reach of a lobe is the first exit along each direction from its center, smoothed over the directions.
+ // It is shortened towards a neighbouring lobe, from where the point is nearer to the other lobe relative to their depths.
+ std::vector lobe_body(m_lobes.size());
+ for (size_t id = 0; id < m_bodies.size(); ++id)
+ std::fill_n(lobe_body.begin() + m_bodies[id].first_lobe, m_bodies[id].lobes, int(id));
+ const int rows = this->directions() / Azimuth;
+ auto direction = [this](int i, int j) {
+ const double azimuth = j * 2. * PI / Azimuth;
+ Vec3d dir = Vec3d::Zero();
+ if (m_axis < 0) {
+ const double polar = (i + 0.5) * PI / Polar;
+ dir = Vec3d(std::sin(polar) * std::cos(azimuth), std::sin(polar) * std::sin(azimuth), std::cos(polar));
+ } else {
+ dir[(m_axis + 1) % 3] = std::cos(azimuth);
+ dir[(m_axis + 2) % 3] = std::sin(azimuth);
+ }
+ return dir;
+ };
+ // Two passes of a box filter over the neighbouring directions, for each of the values of a direction.
+ auto smooth = [rows](std::vector &values, size_t count) {
+ for (int pass = 0; pass < 2; ++pass) {
+ std::vector smoothed(values.size(), 0.);
+ for (int i = 0; i < rows; ++i)
+ for (int j = 0; j < Azimuth; ++j)
+ for (size_t k = 0; k < count; ++k) {
+ double &sum = smoothed[size_t(i * Azimuth + j) * count + k];
+ for (int di = -1; di <= 1; ++di)
+ for (int dj = -1; dj <= 1; ++dj)
+ sum += values[size_t(std::clamp(i + di, 0, rows - 1) * Azimuth + (j + dj + Azimuth) % Azimuth) * count + k];
+ sum /= 9.;
+ }
+ values = std::move(smoothed);
+ }
+ };
+ tbb::parallel_for(tbb::blocked_range(0, m_lobes.size()), [&](const tbb::blocked_range &range) {
+ for (size_t l = range.begin(); l < range.end(); ++l) {
+ const int id = lobe_body[l];
+ const Body &body = m_bodies[id];
+ Lobe &lobe = m_lobes[l];
+ // The other lobes of the body, by the distance from the center beyond which they may be nearer.
+ std::vector> others;
+ for (size_t k = body.first_lobe; k < body.first_lobe + body.lobes; ++k)
+ if (k != l)
+ others.emplace_back((m_lobes[k].center - lobe.center).norm() / (1. + m_lobes[k].depth / lobe.depth), k);
+ std::sort(others.begin(), others.end());
+ auto nearer_lobe = [&](const Vec3d &pt, double r) {
+ for (auto it = others.begin(); it != others.end() && it->first < r; ++it)
+ if ((pt - m_lobes[it->second].center).norm() / m_lobes[it->second].depth < r / lobe.depth)
+ return true;
+ return false;
+ };
+ const double step = 0.5 * m_cell;
+ std::vector log_reach(this->directions());
+ for (int i = 0; i < rows; ++i)
+ for (int j = 0; j < Azimuth; ++j) {
+ const Vec3d dir = direction(i, j);
+ double r = 0.;
+ double limit = InfD;
+ for (;;) {
+ const Vec3d pt = lobe.center + (r + step) * dir;
+ const std::ptrdiff_t n = node_of(pt);
+ if (n < 0 || depth[n] == 0.f || m_body[n] != id || r + step >= limit)
+ break;
+ if (limit == InfD && nearer_lobe(pt, r + step))
+ limit = LobeReach * (r + step);
+ r += step;
+ }
+ log_reach[i * Azimuth + j] = std::log(std::min(r + 0.5 * step, limit));
+ }
+ smooth(log_reach, 1);
+ lobe.reach.resize(log_reach.size());
+ std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
+
+ if (m_mode == TpmsAdaptiveMode::DistanceWarp) {
+ std::vector mean(log_reach.size() * (Samples + 1));
+ for (size_t d = 0; d < log_reach.size(); ++d) {
+ const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
+ double integral = 0.;
+ double previous = this->depth(lobe.center);
+ mean[d * (Samples + 1)] = previous;
+ for (int k = 1; k <= Samples; ++k) {
+ // Exact for a depth linear between the samples, a + b * tau, weighted by tau^2.
+ const double t0 = double(k - 1) / Samples;
+ const double t1 = double(k) / Samples;
+ const double d1 = this->depth(lobe.center + t1 * lobe.reach[d] * dir);
+ const double b = (d1 - previous) * Samples;
+ const double a = previous - b * t0;
+ integral += a * (std::pow(t1, 3) - std::pow(t0, 3)) / 3. + b * (std::pow(t1, 4) - std::pow(t0, 4)) / 4.;
+ previous = d1;
+ mean[d * (Samples + 1) + k] = 3. * integral / std::pow(t1, 3);
+ }
+ }
+ // Smoothed like the reach: sharper profiles shear the pattern across the layer, adding lines.
+ smooth(mean, Samples + 1);
+ lobe.mean_depth.assign(mean.begin(), mean.end());
+ }
+ }
+ throw_if_canceled();
+ });
+
+ // Every other node belongs to its nearest body, within its section in the 2D modes.
+ if (m_axis >= 0) {
+ std::vector has_body(m_size[m_axis], false);
+ for (size_t i = 0; i < m_body.size(); ++i)
+ if (m_body[i] >= 0)
+ has_body[m_axis == 0 ? i % sy : m_axis == 1 ? i / sy % m_size.y() : i / sz] = true;
+ m_section.assign(m_size[m_axis], -1);
+ for (int k = 0; k < m_size[m_axis]; ++k)
+ for (int d = 0; d < m_size[m_axis] && m_section[k] < 0; ++d)
+ if (k - d >= 0 && has_body[k - d])
+ m_section[k] = k - d;
+ else if (k + d < m_size[m_axis] && has_body[k + d])
+ m_section[k] = k + d;
+ }
+ // Stepped shells and Smooth blend only look up the depth.
+ if (!lobes) {
+ m_body = {};
+ return;
+ }
+ if (m_bodies.size() == 1) {
+ std::fill(m_body.begin(), m_body.end(), 0);
+ return;
+ }
+ std::deque queue;
+ for (size_t i = 0; i < m_body.size(); ++i)
+ if (m_body[i] >= 0)
+ queue.push_back(i);
+ while (!queue.empty()) {
+ const size_t i = queue.front();
+ queue.pop_front();
+ const size_t x = i % sy, y = i / sy % m_size.y(), z = i / sz;
+ const std::array valid{x + 1 < sy, x > 0, y + 1 < size_t(m_size.y()), y > 0, z + 1 < size_t(m_size.z()), z > 0};
+ for (size_t k = 0; k < steps.size(); ++k)
+ if (valid[k] && in_section(k) && m_body[i + steps[k]] < 0) {
+ m_body[i + steps[k]] = m_body[i];
+ queue.push_back(i + steps[k]);
+ }
+ }
+}
+
+double TpmsRadialField::depth(const Vec3d &pt) const
+{
+ assert(!m_depth.empty());
+ const Vec3d f = (pt - m_origin) / m_cell;
+ std::array n0;
+ std::array w;
+ for (int a = 0; a < 3; ++a) {
+ n0[a] = std::clamp(int(std::floor(f[a])), 0, m_size[a] - 2);
+ w[a] = std::clamp(f[a] - n0[a], 0., 1.);
+ }
+ const size_t sy = size_t(m_size.x());
+ const size_t sz = sy * size_t(m_size.y());
+ double value = 0.;
+ for (int c = 0; c < 8; ++c) {
+ const size_t n = size_t(n0[2] + (c >> 2)) * sz + size_t(n0[1] + (c >> 1 & 1)) * sy + size_t(n0[0] + (c & 1));
+ value += (c & 1 ? w[0] : 1. - w[0]) * (c >> 1 & 1 ? w[1] : 1. - w[1]) * (c >> 2 ? w[2] : 1. - w[2]) * m_depth[n];
+ }
+ return value;
+}
+
+Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d ¢er) const
+{
+ Vec3d d = pt - center;
+ if (m_axis >= 0)
+ d[m_axis] = 0.;
+ return d;
+}
+
+double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
+{
+ const Vec3d d = this->offset(pt, lobe.center);
+ const double r = d.norm();
+ // Distance warp: the radial coordinate of the linear profile with the same mean depth, which is 1 - 3/4 of it.
+ auto warp = [](double mean_depth) { return std::max(0., 4. / 3. * (1. - mean_depth)); };
+ if (r < EPSILON)
+ return lobe.mean_depth.empty() ? 0. : warp(lobe.mean_depth.front());
+ int i = 0;
+ double fi = 0.;
+ double azimuth;
+ if (m_axis < 0) {
+ const double polar = std::clamp(std::acos(std::clamp(d.z() / r, -1., 1.)) / PI * Polar - 0.5, 0., double(Polar - 1));
+ i = std::min(int(polar), Polar - 2);
+ fi = polar - i;
+ azimuth = std::atan2(d.y(), d.x());
+ } else
+ azimuth = std::atan2(d[(m_axis + 2) % 3], d[(m_axis + 1) % 3]);
+ azimuth *= Azimuth / (2. * PI);
+ if (azimuth < 0.)
+ azimuth += Azimuth;
+ const int j0 = int(azimuth) % Azimuth;
+ const int j1 = (j0 + 1) % Azimuth;
+ const double fj = azimuth - std::floor(azimuth);
+ auto at = [&lobe](int i, int j) { return double(lobe.reach[i * Azimuth + j]); };
+ double reach = at(i, j0) * (1. - fj) + at(i, j1) * fj;
+ if (fi > 0.)
+ reach = reach * (1. - fi) + (at(i + 1, j0) * (1. - fj) + at(i + 1, j1) * fj) * fi;
+ const double tau = r / reach;
+ if (lobe.mean_depth.empty())
+ return tau;
+ // Beyond the surface, the depth is zero.
+ auto mean_at = [&lobe, tau](int i, int j) {
+ const float *mean = lobe.mean_depth.data() + size_t(i * Azimuth + j) * (Samples + 1);
+ if (tau >= 1.)
+ return double(mean[Samples]) / (tau * tau * tau);
+ const double x = tau * Samples;
+ const int k = std::min(int(x), Samples - 1);
+ return mean[k] + (mean[k + 1] - mean[k]) * (x - k);
+ };
+ double mean = mean_at(i, j0) * (1. - fj) + mean_at(i, j1) * fj;
+ if (fi > 0.)
+ mean = mean * (1. - fi) + (mean_at(i + 1, j0) * (1. - fj) + mean_at(i + 1, j1) * fj) * fi;
+ return warp(mean);
+}
+
+size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
+{
+ assert(!this->empty());
+ Vec3i32 idx;
+ for (int axis = 0; axis < 3; ++axis)
+ idx[axis] = std::clamp(int(std::lround((pt[axis] - m_origin[axis]) / m_cell)), 0, m_size[axis] - 1);
+ auto node = [this](const Vec3i32 &idx) { return (size_t(idx.z()) * m_size.y() + idx.y()) * m_size.x() + idx.x(); };
+ if (m_axis < 0)
+ return this->body_radial(node(idx), pt, 1.f, out.data());
+
+ // The sections around pt, or the nearest ones with a body.
+ const double f = std::clamp((pt[m_axis] - m_origin[m_axis]) / m_cell, 0., double(m_size[m_axis] - 1));
+ const int k = std::min(int(f), m_size[m_axis] - 2);
+ const float w = float(f - k);
+ size_t count = 0;
+ if (w < 1.f) {
+ idx[m_axis] = m_section[k];
+ count += this->body_radial(node(idx), pt, 1.f - w, out.data());
+ }
+ if (w > 0.f) {
+ idx[m_axis] = m_section[k + 1];
+ count += this->body_radial(node(idx), pt, w, out.data() + count);
+ }
+ return count;
+}
+
+size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
+{
+ const Body &body = m_bodies[m_body[node]];
+ if (body.lobes == 1) {
+ const Lobe &lobe = m_lobes[body.first_lobe];
+ out[0] = {lobe.center, radial(lobe, pt), weight};
+ return 1;
+ }
+
+ // The lobes nearest relative to their depth; they morph into each other near the sides where they are as near.
+ std::array, MaxMorph> nearest;
+ size_t count = 0;
+ for (size_t l = body.first_lobe; l < body.first_lobe + body.lobes; ++l) {
+ const double d = this->offset(pt, m_lobes[l].center).norm() / m_lobes[l].depth;
+ if (count < MaxMorph)
+ nearest[count++] = {d, l};
+ else if (d < nearest.back().first)
+ nearest.back() = {d, l};
+ else
+ continue;
+ for (size_t k = count - 1; k > 0 && nearest[k].first < nearest[k - 1].first; --k)
+ std::swap(nearest[k], nearest[k - 1]);
+ }
+ std::array blend;
+ double total = 0.;
+ size_t morphs = 0;
+ for (; morphs < count; ++morphs) {
+ const double u = 0.5 - (nearest[morphs].first - nearest[0].first) / LobeMorph;
+ if (u <= 0.)
+ break;
+ blend[morphs] = u * u * (3. - 2. * u);
+ total += blend[morphs];
+ }
+ for (size_t k = 0; k < morphs; ++k) {
+ const Lobe &lobe = m_lobes[nearest[k].second];
+ out[k] = {lobe.center, radial(lobe, pt), float(weight * blend[k] / total)};
+ }
+ return morphs;
+}
+
+} // namespace Slic3r
+
+namespace marchsq {
+using namespace Slic3r;
+
+struct AdaptiveTpmsField
+{
+ static constexpr float gsizef = 0.40f; // grid cell size in mm (roughly line segment length).
+ static constexpr float rsizef = 0.004f; // raster pixel size in mm (roughly point accuracy).
+ const coord_t rsize = scaled(rsizef);
+ const long gsize = std::lround(gsizef / rsizef);
+
+ const AdaptiveTpms &tpms;
+ const TpmsRadialField &radial_field;
+ RadialScale scale;
+ DensityLevels levels;
+ Point size;
+ Point offs;
+ double z;
+ double cos_angle;
+ double sin_angle;
+
+ AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
+ : tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
+ , levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
+ , size(bbox.size()), offs(bbox.min), z(z)
+ , cos_angle(std::cos(angle)), sin_angle(std::sin(angle))
+ {}
+
+ // The pattern is scaled around the center of the lobe, morphing into the pattern of a neighbouring lobe near the
+ // side between them. In the 2D modes only within the section, with the interior frequency along the axis.
+ // The radial field is in the object frame, the fill is rotated by -angle.
+ float get_scalar(const Coord &p) const
+ {
+ const Point pt = to_Point(p);
+ const double x = unscaled(pt.x());
+ const double y = unscaled(pt.y());
+ const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
+ if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) {
+ // The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
+ auto lattice = [this, x, y](int level) {
+ const double frequency = tpms.surface_frequency * levels.scale(level);
+ return tpms.equation(float(frequency * x), float(frequency * y), float(frequency * z));
+ };
+ if (levels.count == 0)
+ return lattice(0);
+ const double c = std::clamp(levels.level(radial_field.depth(obj)), 0., double(levels.count));
+ const int k = std::min(int(c), levels.count - 1);
+ const double u = c - k;
+ const double w = u * u * (3. - 2. * u);
+ return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
+ }
+ const int axis = radial_field.axis();
+ TpmsRadialField::Radials radials;
+ const size_t count = radial_field.radial(obj, radials);
+ float value = 0.f;
+ for (size_t i = 0; i < count; ++i) {
+ const auto &[center, t, weight] = radials[i];
+ Vec3d q = tpms.surface_frequency * scale(t) * (obj - center);
+ if (axis >= 0)
+ q[axis] = tpms.interior_frequency * obj[axis];
+ value += weight * tpms.equation(float(cos_angle * q.x() + sin_angle * q.y()), float(cos_angle * q.y() - sin_angle * q.x()), float(q.z()));
+ }
+ return value;
+ }
+
+ inline coord_t to_coord(long x) const { return x * rsize; }
+ inline long to_coordr(coord_t x) const { return x / rsize; }
+ inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
+};
+
+template<> struct _RasterTraits
+{
+ using ValueType = float;
+ static float get(const AdaptiveTpmsField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
+ static size_t rows(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.y()); }
+ static size_t cols(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.x()); }
+};
+
+// Continuous density level of the depth over a layer, in the object frame.
+struct TpmsLevelField
+{
+ static constexpr float gsizef = 0.5f;
+ static constexpr float rsizef = 0.05f;
+ const coord_t rsize = scaled(rsizef);
+ const long gsize = std::lround(gsizef / rsizef);
+
+ const TpmsRadialField &field;
+ const DensityLevels &levels;
+ Point size;
+ Point offs;
+ double z;
+
+ TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
+ : field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
+ {}
+
+ float get_scalar(const Coord &p) const
+ {
+ const Point pt = to_Point(p);
+ return float(levels.level(field.depth(Vec3d(unscaled(pt.x()), unscaled(pt.y()), z))));
+ }
+
+ inline coord_t to_coord(long x) const { return x * rsize; }
+ inline long to_coordr(coord_t x) const { return x / rsize; }
+ inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
+};
+
+template<> struct _RasterTraits
+{
+ using ValueType = float;
+ static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
+ static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); }
+ static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); }
+};
+
+} // namespace marchsq
+
+namespace Slic3r {
+
+Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
+ coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
+{
+ // A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
+ const coord_t cell = scaled(marchsq::AdaptiveTpmsField::gsizef);
+ bbox.offset(cell);
+ bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
+ const marchsq::AdaptiveTpmsField raster(tpms, field, bbox, z - 0.5 * layer_height, angle);
+ const std::vector rings = marchsq::execute_with_policy(ex_tbb, raster, 0.f, {raster.gsize, raster.gsize});
+
+ // Loops narrower than two lines print as blobs.
+ const double min_loop_length = scaled(2. * PI * spacing);
+ Polylines polylines;
+ polylines.reserve(rings.size());
+ for (const marchsq::Ring &ring : rings) {
+ Polyline polyline;
+ polyline.points.reserve(ring.size() + 1);
+ for (const marchsq::Coord &crd : ring)
+ polyline.points.emplace_back(raster.to_Point(crd));
+ polyline.points.push_back(polyline.points.front());
+ polyline.simplify(SCALED_SPARSE_INFILL_RESOLUTION);
+ if (polyline.length() >= min_loop_length)
+ polylines.push_back(std::move(polyline));
+ }
+ return polylines;
+}
+
+std::vector make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
+ float surface_density, float interior_density, TpmsAdaptiveGradient gradient)
+{
+ const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
+ if (levels.count == 0)
+ return {{surface_density, {expolygon}}};
+ // A fixed sampling grid, so that every region of a layer gets the same shells.
+ const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef);
+ BoundingBox bbox = get_extents(expolygon);
+ bbox.offset(cell);
+ bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
+ const marchsq::TpmsLevelField raster(field, levels, bbox, z);
+
+ // Each level takes the part deeper than the middle between it and the previous one.
+ std::vector shells;
+ ExPolygons remaining{expolygon};
+ for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
+ Polygons deeper;
+ for (const marchsq::Ring &ring : marchsq::execute_with_policy(ex_tbb, raster, float(level + 0.5), {raster.gsize, raster.gsize})) {
+ Polygon &polygon = deeper.emplace_back();
+ polygon.points.reserve(ring.size());
+ for (const marchsq::Coord &crd : ring)
+ polygon.points.emplace_back(raster.to_Point(crd));
+ }
+ ExPolygons inner = intersection_ex(union_ex(deeper), remaining);
+ shells.push_back({float(surface_density * levels.scale(level)), diff_ex(remaining, inner)});
+ remaining = std::move(inner);
+ }
+ if (!remaining.empty())
+ shells.push_back({interior_density, std::move(remaining)});
+ return shells;
+}
+
+void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
+ const std::function &fill_shell)
+{
+ FillParams shell_params = params;
+ shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
+ for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
+ shell_params.density = shell.density;
+ for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
+ fill_shell(shell_params, part);
+ }
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/Fill/FillTpmsAdaptive.hpp b/src/libslic3r/Fill/FillTpmsAdaptive.hpp
new file mode 100644
index 0000000000..8e7b9db6cd
--- /dev/null
+++ b/src/libslic3r/Fill/FillTpmsAdaptive.hpp
@@ -0,0 +1,143 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "../libslic3r.h"
+#include "FillBase.hpp"
+#include "../BoundingBox.hpp"
+#include "../ExPolygon.hpp"
+#include "../Point.hpp"
+#include "../Polyline.hpp"
+#include "../PrintConfig.hpp"
+
+namespace Slic3r {
+
+// Radial coordinate inside the lobes of the bodies of an object, sampled from its slices: 0 at the center of a
+// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
+// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
+// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
+class TpmsRadialField
+{
+public:
+ struct Slice
+ {
+ coordf_t bottom_z;
+ coordf_t top_z;
+ const ExPolygons *expolygons;
+ };
+
+ struct Radial
+ {
+ Vec3d center;
+ double t;
+ float weight;
+ };
+
+ // Slices sorted by z, in the XY coordinates of the fill and the print Z.
+ TpmsRadialField(const std::vector &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
+ const std::function &throw_if_canceled);
+
+ // Lobes blended near the sides between them, from a body or from each of the two sections around a point.
+ static constexpr size_t MaxMorph = 4;
+ using Radials = std::array;
+
+ // Without a body, as when the object is thinner than the grid cells.
+ bool empty() const { return m_bodies.empty(); }
+
+ // Radial coordinates of pt in unscaled coordinates towards the lobe it belongs to, and towards the neighbouring
+ // lobes near the sides between them, with weights summing to 1. In the 2D modes, those of the two sections around
+ // pt. Returns their count.
+ size_t radial(const Vec3d &pt, Radials &out) const;
+
+ // Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
+ int axis() const { return m_axis; }
+ TpmsAdaptiveMode mode() const { return m_mode; }
+
+ // In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
+ // the surface to 1.
+ double depth(const Vec3d &pt) const;
+
+private:
+ struct Lobe
+ {
+ Vec3d center;
+ double depth;
+ // Distance from the center to the surface on a latitude-longitude grid of directions.
+ std::vector reach;
+ // Distance warp: mean depth over the ball along each direction, sampled up to the reach.
+ std::vector mean_depth;
+ };
+
+ struct Body
+ {
+ size_t first_lobe;
+ size_t lobes;
+ // Distance from the deepest point to the surface.
+ double depth;
+ };
+
+ double radial(const Lobe &lobe, const Vec3d &pt) const;
+ size_t body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const;
+ // Offset of pt from a center, within the section in the 2D modes.
+ Vec3d offset(const Vec3d &pt, const Vec3d ¢er) const;
+ int directions() const { return m_axis < 0 ? Polar * Azimuth : Azimuth; }
+
+ // Directions of the reach of a lobe, on a latitude-longitude grid, or a circle in the 2D modes.
+ static constexpr int Polar = 24;
+ static constexpr int Azimuth = 48;
+
+ TpmsAdaptiveMode m_mode;
+ int m_axis;
+ Vec3d m_origin;
+ double m_cell;
+ Vec3i32 m_size;
+ // Nearest body of every grid node.
+ std::vector m_body;
+ std::vector m_bodies;
+ std::vector m_lobes;
+ // In the 2D modes, the nearest section with a body to every section.
+ std::vector m_section;
+ // In the modes following the distance to the surface, the depth of every grid node.
+ std::vector m_depth;
+};
+
+using TpmsRadialFieldPtr = std::unique_ptr;
+// A field for every adaptive mode in use, indexed by the mode.
+using TpmsRadialFields = std::array;
+
+struct AdaptiveTpms
+{
+ // Implicit TPMS equation with a period of 2 PI.
+ float (*equation)(float x, float y, float z);
+ // Pattern frequencies at the surface and at the center, in radians per mm.
+ double surface_frequency;
+ double interior_frequency;
+ TpmsAdaptiveGradient gradient;
+};
+
+// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
+Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
+ coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
+
+struct TpmsShell
+{
+ float density;
+ ExPolygons expolygons;
+};
+
+// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
+// middle of the layer.
+std::vector make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
+ float surface_density, float interior_density, TpmsAdaptiveGradient gradient);
+
+// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
+// so the lines connected along the boundaries of two shells don't overlap.
+void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
+ const std::function &fill_shell);
+
+} // namespace Slic3r
diff --git a/src/libslic3r/Fill/FillTpmsD.cpp b/src/libslic3r/Fill/FillTpmsD.cpp
index bb1a338691..04f8222ca5 100644
--- a/src/libslic3r/Fill/FillTpmsD.cpp
+++ b/src/libslic3r/Fill/FillTpmsD.cpp
@@ -14,8 +14,10 @@
#include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
+#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "FillTpmsD.hpp"
+#include "FillTpmsAdaptive.hpp"
namespace Slic3r {
@@ -23,6 +25,11 @@ static double scaled_floor(double x,double scale){
return std::floor(x/scale)*scale;
}
+static float schwarz_d(float x, float y, float z)
+{
+ return std::sin(x) * std::sin(y) * std::sin(z) - std::cos(x) * std::cos(y) * std::cos(z);
+}
+
static Polylines make_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
{
const double scaleFactor = scale_(line_spacing) / density_adjusted;
@@ -110,31 +117,49 @@ void FillTpmsD::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
+ if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
+ fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
+ [&](const FillParams &shell_params, const ExPolygon &shell) {
+ this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
+ });
+ return;
+ }
+
auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
if(std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle);
- BoundingBox bb = expolygon.contour.bounding_box();
- // Density adjusted to have a good %of weight.
- double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline);
- // Distance between the gyroid waves in scaled coordinates.
- coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
+ Polylines polylines;
+ if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
+ // Radians per mm of the regular pattern at a density.
+ auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
+ BoundingBox bbox = expolygon.contour.bounding_box();
+ bbox.offset(scale_((params.multiline + 1) * this->spacing));
+ polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
+ *this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
+ } else {
+ BoundingBox bb = expolygon.contour.bounding_box();
+ // Density adjusted to have a good %of weight.
+ double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline);
+ // Distance between the gyroid waves in scaled coordinates.
+ coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
- // align bounding box to a multiple of our grid module
- bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
+ // align bounding box to a multiple of our grid module
+ bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
- // generate pattern
- Polylines polylines = make_waves(
- scale_(this->z),
- density_adjusted,
- this->spacing,
- ceil(bb.size()(0) / distance) + 1.,
- ceil(bb.size()(1) / distance) + 1.);
+ // generate pattern
+ polylines = make_waves(
+ scale_(this->z),
+ density_adjusted,
+ this->spacing,
+ ceil(bb.size()(0) / distance) + 1.,
+ ceil(bb.size()(1) / distance) + 1.);
+
+ // shift the polyline to the grid origin
+ for (Polyline &pl : polylines)
+ pl.translate(bb.min);
+ }
- // shift the polyline to the grid origin
- for (Polyline &pl : polylines)
- pl.translate(bb.min);
-
// Apply multiline offset if needed
multiline_fill(polylines, params, spacing);
diff --git a/src/libslic3r/Fill/FillTpmsD.hpp b/src/libslic3r/Fill/FillTpmsD.hpp
index 8dfa12196a..ce041c98fe 100644
--- a/src/libslic3r/Fill/FillTpmsD.hpp
+++ b/src/libslic3r/Fill/FillTpmsD.hpp
@@ -31,6 +31,7 @@ public:
Polylines& polylines_out) override;
bool is_self_crossing() override { return false; }
+ bool aligned_to_origin() const override { return true; }
// Density adjustment to have a good %of weight.
static constexpr double DensityAdjust = 2.1;
diff --git a/src/libslic3r/Fill/FillTpmsFK.cpp b/src/libslic3r/Fill/FillTpmsFK.cpp
index 164e240280..819ef2998e 100644
--- a/src/libslic3r/Fill/FillTpmsFK.cpp
+++ b/src/libslic3r/Fill/FillTpmsFK.cpp
@@ -8,6 +8,7 @@
#include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "FillTpmsFK.hpp"
+#include "FillTpmsAdaptive.hpp"
#include
#include
#include
@@ -17,6 +18,18 @@
#include
#include
#include "libslic3r/Polygon.hpp"
+#include "libslic3r/PrintConfig.hpp"
+
+namespace Slic3r {
+
+// Fischer - Koch S equation:
+// cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0
+static float fischer_koch(float x, float y, float z)
+{
+ return cosf(2 * x) * sinf(y) * cosf(z) + cosf(2 * y) * sinf(z) * cosf(x) + cosf(2 * z) * sinf(x) * cosf(y);
+}
+
+} // namespace Slic3r
namespace marchsq {
using namespace Slic3r;
@@ -42,16 +55,7 @@ struct ScalarField
{}
// Get the scalar field value at x,y,z in coordf_t coordinates.
- float get_scalar(coordf_t x, coordf_t y, coordf_t z) const
- {
- const float fx = freq * x;
- const float fy = freq * y;
- const float fz = freq * z;
-
- // Fischer - Koch S equation:
- // cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0
- return cosf(2 * fx) * sinf(fy) * cosf(fz) + cosf(2 * fy) * sinf(fz) * cosf(fx) + cosf(2 * fz) * sinf(fx) * cosf(fy);
- }
+ float get_scalar(coordf_t x, coordf_t y, coordf_t z) const { return fischer_koch(freq * x, freq * y, freq * z); }
// Get the scalar field value at a Coord for the current z value.
float get_scalar(Coord p) const
@@ -128,20 +132,34 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
ExPolygon expolygon,
Polylines& polylines_out)
{
+ if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
+ fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
+ [&](const FillParams &shell_params, const ExPolygon &shell) {
+ this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
+ });
+ return;
+ }
+
auto infill_angle = float(this->angle + (CorrectionAngle * 2 * M_PI) / 360.);
if (std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle);
- float density_factor = std::min(0.9f, params.density);
// Density (field period) adjusted to have a good %of weight.
- const float vari_T = 4.18f * spacing * params.multiline / density_factor;
+ auto period = [¶ms, this](float density) { return 4.18f * spacing * params.multiline / std::min(0.9f, density); };
BoundingBox bbox = expolygon.contour.bounding_box();
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
bbox.offset(scale_((params.multiline + 1) * spacing));
- marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, vari_T);
- // Get simplified lines using coarse tolerance of 0.1mm (this is infill).
- Polylines polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
+ Polylines polylines;
+ if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
+ polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density),
+ params.tpms_adaptive_gradient},
+ *this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle);
+ } else {
+ marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
+ // Get simplified lines using coarse tolerance of 0.1mm (this is infill).
+ polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
+ }
// Apply multiline offset if needed
multiline_fill(polylines, params, spacing);
diff --git a/src/libslic3r/Fill/FillTpmsFK.hpp b/src/libslic3r/Fill/FillTpmsFK.hpp
index d1fec0e93f..e9b6b89913 100644
--- a/src/libslic3r/Fill/FillTpmsFK.hpp
+++ b/src/libslic3r/Fill/FillTpmsFK.hpp
@@ -31,6 +31,7 @@ public:
Polylines& polylines_out) override;
bool is_self_crossing() override { return false; }
+ bool aligned_to_origin() const override { return true; }
};
diff --git a/src/libslic3r/Format/AMF.cpp b/src/libslic3r/Format/AMF.cpp
index fa28d93f09..42583419b4 100644
--- a/src/libslic3r/Format/AMF.cpp
+++ b/src/libslic3r/Format/AMF.cpp
@@ -314,8 +314,13 @@ void AMFParserContext::startElement(const char *name, const char **atts)
case 2:
if (strcmp(name, "metadata") == 0) {
if (m_path[1] == NODE_TYPE_MATERIAL || m_path[1] == NODE_TYPE_OBJECT) {
- m_value[0] = get_attribute(atts, "type");
- node_type_new = NODE_TYPE_METADATA;
+ const char *type = get_attribute(atts, "type");
+ if (type == nullptr)
+ this->stop();
+ else {
+ m_value[0] = type;
+ node_type_new = NODE_TYPE_METADATA;
+ }
}
}/* else if (strcmp(name, "layer_config_ranges") == 0 && m_path[1] == NODE_TYPE_OBJECT)
node_type_new = NODE_TYPE_LAYER_CONFIG;*/
diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp
index e555e6ebe5..4ae316a055 100644
--- a/src/libslic3r/Format/bbs_3mf.cpp
+++ b/src/libslic3r/Format/bbs_3mf.cpp
@@ -1041,10 +1041,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
void _stop_object_xml_parser(const std::string& msg = std::string())
{
assert(! obj_parse_error);
- assert(obj_parse_error_message.empty());
assert(object_xml_parser != nullptr);
obj_parse_error = true;
- obj_parse_error_message = msg;
+ if (! msg.empty() || obj_parse_error_message.empty()) // a handler may have set the message already
+ obj_parse_error_message = msg;
XML_StopParser(object_xml_parser, false);
}
@@ -1395,7 +1395,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_start_relationship(const char** attributes, unsigned int num_attributes);
- void _generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects);
+ bool _generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects);
bool _generate_volumes_new(ModelObject& object, const std::vector &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions);
//bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions);
@@ -2117,7 +2117,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return false;
}
std::vector object_id_list;
- _generate_current_object_list(object_id_list, object.first, m_current_objects);
+ if (!_generate_current_object_list(object_id_list, object.first, m_current_objects))
+ return false;
ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
@@ -2216,7 +2217,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}*/
std::vector object_id_list;
- _generate_current_object_list(object_id_list, object.first, m_current_objects);
+ if (!_generate_current_object_list(object_id_list, object.first, m_current_objects))
+ return false;
ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
@@ -3901,11 +3903,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
// appends the vertex coordinates
// missing values are set equal to ZERO
- if (m_curr_object)
- m_curr_object->geometry.vertices.emplace_back(
- m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
- m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
- m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
+ if (m_curr_object) {
+ const Vec3f v(m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
+ m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
+ m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
+ // A non-finite coordinate ("nan", "inf") used to be accepted and crashed
+ // qhull in ModelVolume's convex hull while the file was still loading. Refuse the file.
+ if (! v.allFinite()) {
+ _stop_xml_parser("Invalid vertex coordinate: not a finite number");
+ return true; // the parser is stopped; returning false would overwrite the message
+ }
+ m_curr_object->geometry.vertices.emplace_back(v);
+ }
return true;
}
@@ -5064,11 +5073,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
- void _BBS_3MF_Importer::_generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects)
+ bool _BBS_3MF_Importer::_generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects)
{
+ // A cycle in the component graph would expand forever, and an acyclic graph can still expand
+ // exponentially, so bound the number of component references queued. Checking before they are
+ // queued bounds the work list itself, whatever the fan-out. A valid file over the budget is
+ // rejected too, but the budget is way above the component references of any real object.
+ static constexpr size_t max_components = 100000;
+
std::list> id_list;
id_list.push_back(std::make_pair(Component(object_id, Transform3d::Identity()), Transform3d::Identity()));
+ size_t num_components = 0;
while (!id_list.empty())
{
auto current_item = id_list.front();
@@ -5078,6 +5094,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (current_object != current_objects.end()) {
//found one
if (!current_object->second.components.empty()) {
+ num_components += current_object->second.components.size();
+ if (num_components > max_components) {
+ add_error("invalid 3mf: cyclic or too many component references");
+ sub_objects.clear();
+ return false;
+ }
for (const Component &comp : current_object->second.components) {
id_list.push_back(std::pair(comp, current_item.second * comp.transform));
}
@@ -5089,6 +5111,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}
}
+ return true;
}
bool _BBS_3MF_Importer::_generate_volumes_new(ModelObject& object, const std::vector &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions)
@@ -5195,6 +5218,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}
+ for (const Vec3f &v : sub_object->geometry.vertices)
+ if (! v.allFinite()) { // Qhull cannot take a NaN vertex
+ add_error("invalid (non-finite) vertex in object " + std::to_string(sub_object->id));
+ return false;
+ }
its.vertices.assign(sub_object->geometry.vertices.begin(), sub_object->geometry.vertices.end());
// BBS
@@ -5708,11 +5736,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
// appends the vertex coordinates
// missing values are set equal to ZERO
- if (current_object)
- current_object->geometry.vertices.emplace_back(
- object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
- object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
- object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
+ if (current_object) {
+ const Vec3f v(object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
+ object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
+ object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
+ // See _BBS_3MF_Importer::_handle_start_vertex: a non-finite coordinate
+ // crashed qhull while the file loaded. The dispatcher stops this parser on `false`.
+ if (! v.allFinite()) {
+ obj_parse_error_message = "Invalid vertex coordinate: not a finite number";
+ return false;
+ }
+ current_object->geometry.vertices.emplace_back(v);
+ }
return true;
}
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index 62b5897f39..d29363e639 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -29,10 +29,13 @@
#include "libslic3r.h"
#include "I18N.hpp"
#include "GCode.hpp"
+#include
#include "Exception.hpp"
#include "LifecycleEvents.hpp"
#include "ExtrusionEntity.hpp"
#include "EdgeGrid.hpp"
+#include "BeltTransform.hpp"
+#include "Geometry.hpp"
#include "Geometry/ConvexHull.hpp"
#include "GCode/PrintExtents.hpp"
#include "GCode/Thumbnails.hpp"
@@ -55,6 +58,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -101,6 +105,7 @@
#include
#include "calib.hpp"
#include "libslic3r_version.h"
+#include "GCode/BeltKinematics.hpp"
// Intel redesigned some TBB interface considerably when merging TBB with their oneAPI set of libraries, see GH #7332.
// We are using quite an old TBB 2017 U7. Before we update our build servers, let's use the old API, which is deprecated in up to date TBB.
#if ! defined(TBB_VERSION_MAJOR)
@@ -2193,7 +2198,23 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables()
// Collect pairs of object_layer + support_layer sorted by print_z.
// object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON.
-std::vector GCode::collect_layers_to_print(const PrintObject& object)
+// Belt printers: whether an object layer writes anything, its own extrusions or a belt
+// brim band riding on it. Shared by collect_layers_to_print() (which drops the layers
+// that do not) and the layer count.
+static bool belt_object_layer_prints_something(const PrintObject &object, const Layer &layer)
+{
+ if (layer.has_extrusions())
+ return true;
+ if (object.has_belt_brim()) {
+ const auto &by_layer = object.belt_brim_by_layer();
+ const size_t id = layer.id();
+ if (id < by_layer.size() && ! by_layer[id].empty())
+ return true;
+ }
+ return false;
+}
+
+std::vector GCode::collect_layers_to_print(const PrintObject& object, bool skip_empty_first_layer)
{
std::vector layers_to_print;
layers_to_print.reserve(object.layers().size() + object.support_layers().size());
@@ -2217,10 +2238,19 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
std::vector> warning_ranges;
// Pair the object layers with the support layers by z.
+ //
+ // Belt printers add a third stream: brim apron bands, which sit on the belt AHEAD of
+ // the part and so print below the object's first layer. They are merged here rather
+ // than pushed as standalone records, because a band's print_z can coincide with a
+ // support layer of this same object - and the print-wide merge downstream keeps only
+ // one record per object per z, so a standalone band would be silently overwritten.
size_t idx_object_layer = 0;
size_t idx_support_layer = 0;
+ size_t idx_brim_band = 0;
+ const auto &brim_bands = object.belt_brim_prologue(); // ordered by ascending print_z
const LayerToPrint* last_extrusion_layer = nullptr;
- while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()) {
+ while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()
+ || idx_brim_band < brim_bands.size()) {
LayerToPrint layer_to_print;
double print_z_min = std::numeric_limits::max();
if (idx_object_layer < object.layers().size()) {
@@ -2233,6 +2263,11 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
print_z_min = std::min(print_z_min, layer_to_print.support_layer->print_z);
}
+ if (idx_brim_band < brim_bands.size()) {
+ layer_to_print.belt_brim_band = &brim_bands[idx_brim_band++];
+ print_z_min = std::min(print_z_min, layer_to_print.belt_brim_band->print_z);
+ }
+
if (layer_to_print.object_layer && layer_to_print.object_layer->print_z > print_z_min + EPSILON) {
layer_to_print.object_layer = nullptr;
--idx_object_layer;
@@ -2243,16 +2278,29 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
--idx_support_layer;
}
+ if (layer_to_print.belt_brim_band && layer_to_print.belt_brim_band->print_z > print_z_min + EPSILON) {
+ layer_to_print.belt_brim_band = nullptr;
+ --idx_brim_band;
+ }
+
layer_to_print.original_object = &object;
layers_to_print.push_back(layer_to_print);
bool has_extrusions = (layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions())
- || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions());
+ || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions())
+ || (layer_to_print.belt_brim_band && ! layer_to_print.belt_brim_band->fills.empty());
// Check that there are extrusions on the very first layer. The case with empty
// first layer may result in skirt/brim in the air and maybe other issues.
+ // Skip this check for belt printers. The shear transform tilts the
+ // model so the first horizontal layer plane intersects only a thin
+ // sliver of the model (width ≈ first_layer_height / shear_factor).
+ // This sliver is often narrower than the nozzle diameter, producing
+ // zero perimeters and an empty first layer — which is expected, not
+ // an error. In global shear mode the object may also start above
+ // Z=0 on the tilted belt surface.
if (layers_to_print.size() == 1u) {
- if (!has_extrusions)
+ if (!has_extrusions && !skip_empty_first_layer)
throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id);
}
@@ -2283,14 +2331,32 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
+ std::max(0., extra_gap);
// Negative support_contact_z is not taken into account, it can result in false positives in cases
- if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON)
- warning_ranges.emplace_back(std::make_pair((last_extrusion_layer ? last_extrusion_layer->print_z() : 0.), layers_to_print.back().print_z()));
+ if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON) {
+ // Belt printers: a *leading* empty range (no prior extrusion layer, so the
+ // gap starts at Z=0) is not a floating object — it is just the belt lead-in.
+ // The part rests on the conveyor as it advances, so the first material can
+ // legitimately appear well above Z=0. This empty-layer check assumes a fixed
+ // bed, where material with nothing below it is unprintable; that assumption
+ // does not hold on a belt for the lead-in. Suppress only this leading case,
+ // and keep flagging genuine *internal* gaps (which on a belt may still be an
+ // over-angle overhang that would print into air).
+ const bool belt_leading_gap = object.print()->config().belt_printer.value
+ && last_extrusion_layer == nullptr;
+ if (!belt_leading_gap)
+ warning_ranges.emplace_back(std::make_pair((last_extrusion_layer ? last_extrusion_layer->print_z() : 0.), layers_to_print.back().print_z()));
+ }
}
// Remember last layer with extrusions.
if (has_extrusions)
last_extrusion_layer = &layers_to_print.back();
}
+ // ORCA-Belt: objects print at their position along the belt, so the first
+ // extrusions legitimately start far above Z=0. Drop the spurious
+ // "empty layers from the bed" range while keeping genuine mid-print gaps.
+ if (skip_empty_first_layer && !warning_ranges.empty() && warning_ranges.front().first == 0.)
+ warning_ranges.erase(warning_ranges.begin());
+
if (! warning_ranges.empty()) {
std::string warning;
size_t i = 0;
@@ -2304,6 +2370,22 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
PrintStateBase::WarningLevel::CRITICAL, warning, PrintStateBase::SlicingEmptyGcodeLayers);
}
+ // Belt printers: drop the layers that print nothing at all. An object's slicing
+ // frame starts at the belt below its leading end, so its first layers are empty,
+ // and with several objects along the belt those empty layers fall between other
+ // objects' printing layers. A layer change with no moves is noise in the file, and
+ // the preview (libvgcode) numbers its layers from the moves it sees, so a gap folds
+ // every later layer into the one before it. Both print sequences collect their
+ // layers here, so neither writes such a layer.
+ if (object.print()->config().belt_printer.value)
+ layers_to_print.erase(
+ std::remove_if(layers_to_print.begin(), layers_to_print.end(), [&object](const LayerToPrint <p) {
+ return ! ((ltp.object_layer != nullptr && belt_object_layer_prints_something(object, *ltp.object_layer)) ||
+ (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) ||
+ (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty()));
+ }),
+ layers_to_print.end());
+
return layers_to_print;
}
@@ -2325,11 +2407,14 @@ std::vector>> GCode::collec
for (size_t i = 0; i < print.objects().size(); ++i) {
try {
- per_object[i] = collect_layers_to_print(*print.objects()[i]);
+ per_object[i] = collect_layers_to_print(*print.objects()[i], print.config().belt_printer.value);
} catch (const Slic3r::SlicingError &e) {
errors.push_back(e);
continue;
}
+ // On a belt an object may be left without a layer to print at all.
+ if (per_object[i].empty())
+ continue;
OrderingItem ordering_item;
ordering_item.object_idx = i;
ordering.reserve(ordering.size() + per_object[i].size());
@@ -3055,21 +3140,71 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_fan_mover.release();
m_ordering_cache.clear();
-
+
+ // Belt printer: initialize belt-specific writer via virtual hook.
+ this->init_belt_writer(print);
m_writer.set_is_bbl_machine(is_bbl_printers);
+ // G-code axis remap. Only belt printers get one (see
+ // BeltTransformPipeline::axis_remap_enabled): a remap left in a profile must
+ // not change a non-belt print. Sync the writer's remap state to the current
+ // export UNCONDITIONALLY — even at the identity mapping (0,1,2) — so a reused
+ // writer never retains a stale non-identity mapping from a prior export.
+ // has_axis_remap() returns false at identity, so identity/default output stays
+ // unchanged.
+ {
+ const bool remap = BeltTransformPipeline::axis_remap_enabled(print.config());
+ int rx = remap ? int(print.config().gcode_remap_x.value) : int(RemapAxis::PosX);
+ int ry = remap ? int(print.config().gcode_remap_y.value) : int(RemapAxis::PosY);
+ int rz = remap ? int(print.config().gcode_remap_z.value) : int(RemapAxis::PosZ);
+ m_writer.set_axis_remap(rx, ry, rz);
+ BoundingBoxf bbox_bed(print.config().printable_area.values);
+ m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(),
+ print.config().printable_height.value));
+ }
+
+ // Belt writers only: travel-speed selection becomes per-point (see
+ // GCodeWriter::uses_pointwise_travel_speed()), which must not change for
+ // non-belt printers. The writer gets the same test the extrusions use, so a
+ // travel is judged against the belt surface (belt_height_above_floor) exactly
+ // like the path it leads to. Writer points carry the G-code origin and
+ // extruder offset that point_to_gcode() added; the belt surface is described
+ // in the object's own frame.
+ if (print.config().belt_printer.value) {
+ m_writer.set_first_layer_point_test([this](const Vec3d &point_logical) {
+ const Vec2d extruder_offset = m_writer.filament() != nullptr ? EXTRUDER_CONFIG(extruder_offset) : Vec2d::Zero();
+ // Undo what point_to_gcode() added (m_origin, minus the extruder offset) and
+ // what the writer then took off (its XY offset, the plate origin).
+ const Vec2d plate_offset = m_writer.get_xy_offset().cast();
+ return this->on_first_layer(Vec3d(point_logical.x() - (m_origin.x() - plate_offset.x()) + extruder_offset.x(),
+ point_logical.y() - (m_origin.y() - plate_offset.y()) + extruder_offset.y(),
+ point_logical.z()));
+ });
+ }
+
// How many times will be change_layer() called?
// change_layer() in turn increments the progress bar status.
m_layer_count = 0;
+ // On a belt, collect_layers_to_print() drops the layers that print nothing (an
+ // object's empty lead-in), so they must not be counted here either or the layer
+ // count in the file disagrees with its layer changes.
+ const bool belt = print.config().belt_printer.value;
if (print.config().print_sequence == PrintSequence::ByObject) {
// Add each of the object's layers separately.
for (auto object : print.objects()) {
std::vector