Merge branch 'main' into belt/baseChanges

This commit is contained in:
Joseph Robertson
2026-06-20 15:56:59 -05:00
committed by GitHub
77 changed files with 17916 additions and 14070 deletions
+16 -3
View File
@@ -55,11 +55,23 @@ jobs:
build_linux: build_linux:
strategy: strategy:
fail-fast: false fail-fast: false
# Build both arches on every event (PRs included), through the same
# build_check_cache -> build_deps -> build_orca chain (the AppImage).
# aarch64 always uses the GitHub-hosted arm runner (there is no arm
# self-hosted server). amd64's empty arch is load-bearing: it keeps the
# historical 'linux-clang' deps cache key and the unsuffixed asset names.
matrix:
include:
- arch: ""
os: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }}
- arch: "aarch64"
os: ubuntu-24.04-arm
# Don't run scheduled builds on forks: # Don't run scheduled builds on forks:
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }} if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_check_cache.yml uses: ./.github/workflows/build_check_cache.yml
with: with:
os: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }} os: ${{ matrix.os }}
arch: ${{ matrix.arch }}
build-deps-only: ${{ inputs.build-deps-only || false }} build-deps-only: ${{ inputs.build-deps-only || false }}
secrets: inherit secrets: inherit
build_windows: build_windows:
@@ -99,7 +111,9 @@ jobs:
secrets: inherit secrets: inherit
unit_tests: unit_tests:
name: Unit Tests name: Unit Tests
runs-on: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }} # Tests are built on the aarch64 leg by default (faster GitHub arm runner),
# so run them there; self-hosted builds them on the amd64 server instead.
runs-on: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04-arm' }}
needs: build_linux needs: build_linux
if: ${{ !cancelled() && success() }} if: ${{ !cancelled() && success() }}
steps: steps:
@@ -237,4 +251,3 @@ jobs:
asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream 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 max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
+4 -2
View File
@@ -33,8 +33,10 @@ jobs:
- name: set outputs - name: set outputs
id: set_outputs id: set_outputs
env: env:
# Keep macOS cache keys and paths architecture-specific. # Keep macOS/Linux cache keys architecture-specific. amd64 Linux passes
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && 'windows' || 'linux-clang') }} # no arch (key stays 'linux-clang', preserving the existing cache);
# aarch64 gets its own 'linux-clang-aarch64' key.
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && 'windows' || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || '/OrcaSlicer_dep' }} dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || '/OrcaSlicer_dep' }}
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}} output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
run: | run: |
+45 -27
View File
@@ -65,6 +65,11 @@ jobs:
echo "ver_pure=$ver_pure" >> $GITHUB_ENV echo "ver_pure=$ver_pure" >> $GITHUB_ENV
echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV
# Per-arch Linux AppImage naming: amd64 keeps the historical unsuffixed
# name (arch_suffix empty). Unused on macOS/Windows.
if [ '${{ inputs.arch }}' = 'aarch64' ]; then
echo "arch_suffix=_aarch64" >> $GITHUB_ENV
fi
shell: bash shell: bash
- name: Get the version and date on Windows - name: Get the version and date on Windows
@@ -153,14 +158,6 @@ jobs:
run: | run: |
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15 ./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15
- name: Delete intermediate per-arch artifacts
if: runner.os == 'macOS' && inputs.macos-combine-only
uses: geekyeggo/delete-artifact@v6
with:
name: |
OrcaSlicer_Mac_bundle_arm64_${{ github.sha }}
OrcaSlicer_Mac_bundle_x86_64_${{ github.sha }}
# Thanks to RaySajuuk, it's working now # Thanks to RaySajuuk, it's working now
- name: Sign app and notary - name: Sign app and notary
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || 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
@@ -171,6 +168,8 @@ jobs:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
CERTIFICATE_ID: ${{ secrets.MACOS_CERTIFICATE_ID }} CERTIFICATE_ID: ${{ secrets.MACOS_CERTIFICATE_ID }}
run: | run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
source ${{ github.workspace }}/scripts/retry.sh
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH
@@ -193,7 +192,7 @@ jobs:
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/* rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/ cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications
hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg retry hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" OrcaSlicer_Mac_universal_${{ env.ver }}.dmg codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Create separate OrcaSlicer_profile_validator DMG if the app exists # Create separate OrcaSlicer_profile_validator DMG if the app exists
@@ -202,7 +201,7 @@ jobs:
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/* rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/ cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications
hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg retry hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi fi
@@ -220,11 +219,13 @@ jobs:
if: github.ref != 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only if: github.ref != 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
run: | run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
source ${{ github.workspace }}/scripts/retry.sh
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_dmg mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/* rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/ cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications
hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg retry hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Create separate OrcaSlicer_profile_validator DMG if the app exists # Create separate OrcaSlicer_profile_validator DMG if the app exists
if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
@@ -232,9 +233,19 @@ jobs:
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/* rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/ cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications
hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg retry hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi fi
# Delete the per-arch bundles only after signing/DMG creation succeeded, so a
# failed run keeps them available for a re-run instead of forcing a full rebuild.
- name: Delete intermediate per-arch artifacts
if: success() && runner.os == 'macOS' && inputs.macos-combine-only
uses: geekyeggo/delete-artifact@v6
with:
name: |
OrcaSlicer_Mac_bundle_arm64_${{ github.sha }}
OrcaSlicer_Mac_bundle_x86_64_${{ github.sha }}
- name: Upload artifacts mac - name: Upload artifacts mac
if: runner.os == 'macOS' && inputs.macos-combine-only if: runner.os == 'macOS' && inputs.macos-combine-only
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
@@ -401,16 +412,23 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
shell: bash shell: bash
run: | run: |
./build_linux.sh -istrlL # Build + tar the unit tests (-t) only on the leg that runs them: the
# aarch64 leg by default (faster GitHub arm runner), or amd64 when using
# self-hosted runners (no arm self-hosted server). unit_tests downloads
# this tarball. The profile validator is built with -s, so amd64 keeps it.
tests=${{ (!vars.SELF_HOSTED && inputs.arch == 'aarch64') || (vars.SELF_HOSTED && inputs.arch != 'aarch64') }}
if $tests; then flags=-istrlL; else flags=-isrlL; fi
./build_linux.sh "$flags"
./scripts/check_appimage_libs.sh ./build/package ./build/package/bin/orca-slicer ./scripts/check_appimage_libs.sh ./build/package ./build/package/bin/orca-slicer
mv -n ./build/OrcaSlicer_Linux_V${{ env.ver_pure }}.AppImage ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_${{ env.ver }}.AppImage appimage=./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
chmod +x ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_${{ env.ver }}.AppImage mv -n ./build/OrcaSlicer_Linux_V${{ env.ver_pure }}.AppImage "$appimage"
tar -cvpf build_tests.tar build/tests chmod +x "$appimage"
if $tests; then tar -cvpf build_tests.tar build/tests; fi
# Use tar because upload-artifacts won't always preserve directory structure # Use tar because upload-artifacts won't always preserve directory structure
# and doesn't preserve file permissions # and doesn't preserve file permissions
- name: Upload Test Artifact - name: Upload Test Artifact
if: runner.os == 'Linux' if: runner.os == 'Linux' && ((!vars.SELF_HOSTED && inputs.arch == 'aarch64') || (vars.SELF_HOSTED && inputs.arch != 'aarch64'))
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: ${{ github.sha }}-tests name: ${{ github.sha }}-tests
@@ -420,7 +438,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Run external slicer regression tests - name: Run external slicer regression tests
if: runner.os == 'Linux' if: runner.os == 'Linux' && inputs.arch != 'aarch64'
timeout-minutes: 20 timeout-minutes: 20
shell: bash shell: bash
run: | run: |
@@ -430,7 +448,7 @@ jobs:
python3 "$test_repo_dir/run_test.py" "${{ github.workspace }}/build/package/bin/orca-slicer" python3 "$test_repo_dir/run_test.py" "${{ github.workspace }}/build/package/bin/orca-slicer"
- name: Build orca_custom_preset_tests - name: Build orca_custom_preset_tests
if: github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED if: github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64'
working-directory: ${{ github.workspace }}/build/src/Release working-directory: ${{ github.workspace }}/build/src/Release
shell: bash shell: bash
run: | run: |
@@ -442,11 +460,11 @@ jobs:
if: ${{ ! env.ACT && runner.os == 'Linux' }} if: ${{ ! env.ACT && runner.os == 'Linux' }}
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: OrcaSlicer_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} name: OrcaSlicer_Linux_ubuntu_${{ env.ubuntu-ver }}${{ env.arch_suffix }}_${{ env.ver }}
path: './build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_${{ env.ver }}.AppImage' path: "./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage"
- name: Upload OrcaSlicer_profile_validator Ubuntu - name: Upload OrcaSlicer_profile_validator Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
@@ -458,12 +476,12 @@ jobs:
with: with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723 release_id: 137995723
asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}_${{ env.ver }}.AppImage 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 }}_nightly.AppImage asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly.AppImage
asset_content_type: application/octet-stream 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 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 - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: rickstaa/action-create-tag@v1 uses: rickstaa/action-create-tag@v1
with: with:
tag: "nightly-builds" tag: "nightly-builds"
@@ -472,7 +490,7 @@ jobs:
message: "nightly-builds" message: "nightly-builds"
- name: Deploy Ubuntu OrcaSlicer_profile_validator release - name: Deploy Ubuntu OrcaSlicer_profile_validator release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
with: with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
@@ -483,7 +501,7 @@ jobs:
max_releases: 1 max_releases: 1
- name: Deploy orca_custom_preset_tests - name: Deploy orca_custom_preset_tests
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
with: with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
+2
View File
@@ -161,6 +161,8 @@ It can also be installed through graphical software managers (KDE Discover, GNOM
### AppImage ### AppImage
AppImages are published for both **x86_64** and **aarch64** (ARM64). Pick the file matching your CPU — the ARM64 build has `aarch64` in its name (e.g. `OrcaSlicer_Linux_AppImage_Ubuntu2404_aarch64_*.AppImage`).
1. Download App image from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases). 1. Download App image from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases).
2. Double click the downloaded file to run it. 2. Double click the downloaded file to run it.
+6
View File
@@ -10,7 +10,13 @@ if (APPLE AND CMAKE_OSX_ARCHITECTURES)
set(_context_arch_line "-DBOOST_CONTEXT_ARCHITECTURE:STRING=${CMAKE_OSX_ARCHITECTURES}") set(_context_arch_line "-DBOOST_CONTEXT_ARCHITECTURE:STRING=${CMAKE_OSX_ARCHITECTURES}")
endif () endif ()
set(_options "")
if (MSVC AND DEP_DEBUG)
set(_options "FORWARD_CONFIG")
endif ()
orcaslicer_add_cmake_project(Boost orcaslicer_add_cmake_project(Boost
${_options}
URL "https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.gz" URL "https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.gz"
URL_HASH SHA256=4d27e9efed0f6f152dc28db6430b9d3dfb40c0345da7342eaa5a987dde57bd95 URL_HASH SHA256=4d27e9efed0f6f152dc28db6430b9d3dfb40c0345da7342eaa5a987dde57bd95
LIST_SEPARATOR | LIST_SEPARATOR |
+14 -6
View File
@@ -155,17 +155,25 @@ if (NOT _is_multi AND NOT CMAKE_BUILD_TYPE)
endif () endif ()
function(orcaslicer_add_cmake_project projectname) function(orcaslicer_add_cmake_project projectname)
cmake_parse_arguments(P_ARGS "" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN}) cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN})
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}) set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
if (_is_multi OR MSVC) if (_is_multi OR MSVC)
if (ORCA_INCLUDE_DEBUG_INFO AND NOT DEP_DEBUG) if (P_ARGS_FORWARD_CONFIG)
set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE})
elseif (ORCA_INCLUDE_DEBUG_INFO AND NOT DEP_DEBUG)
set(_configs_line "-DCMAKE_C_FLAGS_RELEASE:STRING=${CMAKE_C_FLAGS_RELWITHDEBINFO} -DCMAKE_CXX_FLAGS_RELEASE:STRING=${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") set(_configs_line "-DCMAKE_C_FLAGS_RELEASE:STRING=${CMAKE_C_FLAGS_RELWITHDEBINFO} -DCMAKE_CXX_FLAGS_RELEASE:STRING=${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
else () else ()
set(_configs_line "") set(_configs_line "")
endif () endif ()
endif () endif ()
if (P_ARGS_FORWARD_CONFIG)
set(_target_config "$<CONFIG>")
else()
set(_target_config "Release")
endif()
if (MSVC) if (MSVC)
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}") set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}")
else() else()
@@ -208,8 +216,8 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
${DEP_CMAKE_OPTS} ${DEP_CMAKE_OPTS}
${P_ARGS_CMAKE_ARGS} ${P_ARGS_CMAKE_ARGS}
${P_ARGS_UNPARSED_ARGUMENTS} ${P_ARGS_UNPARSED_ARGUMENTS}
BUILD_COMMAND ${CMAKE_COMMAND} --build . --config Release -- ${_build_j} BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${_target_config} -- ${_build_j}
INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config Release INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config ${_target_config}
) )
if (FLATPAK) if (FLATPAK)
@@ -251,8 +259,8 @@ else()
${DEP_CMAKE_OPTS} ${DEP_CMAKE_OPTS}
${P_ARGS_CMAKE_ARGS} ${P_ARGS_CMAKE_ARGS}
${P_ARGS_UNPARSED_ARGUMENTS} ${P_ARGS_UNPARSED_ARGUMENTS}
BUILD_COMMAND ${CMAKE_COMMAND} --build . --config Release -- ${_build_j} BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${_target_config} -- ${_build_j}
INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config Release INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config ${_target_config}
) )
endif() endif()
+6
View File
@@ -1,4 +1,10 @@
set(_options "")
if (MSVC AND DEP_DEBUG)
set(_options "FORWARD_CONFIG")
endif ()
orcaslicer_add_cmake_project(Draco orcaslicer_add_cmake_project(Draco
${_options}
URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip
URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77 URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
) )
+5
View File
@@ -1,7 +1,11 @@
if (MSVC) if (MSVC)
set(_use_IPP "-DWITH_IPP=ON") set(_use_IPP "-DWITH_IPP=ON")
if (DEP_DEBUG)
set(_options "FORWARD_CONFIG")
endif ()
else () else ()
set(_use_IPP "-DWITH_IPP=OFF") set(_use_IPP "-DWITH_IPP=OFF")
set(_options "")
endif () endif ()
if (IN_GIT_REPO) if (IN_GIT_REPO)
@@ -9,6 +13,7 @@ if (IN_GIT_REPO)
endif () endif ()
orcaslicer_add_cmake_project(OpenCV orcaslicer_add_cmake_project(OpenCV
${_options}
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277 URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277
PATCH_COMMAND git apply ${OpenCV_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-vs.patch ${CMAKE_CURRENT_LIST_DIR}/0002-clang19-macos.patch PATCH_COMMAND git apply ${OpenCV_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-vs.patch ${CMAKE_CURRENT_LIST_DIR}/0002-clang19-macos.patch
@@ -1123,18 +1123,17 @@ private:
std::vector<RawShape> objs,excludes; std::vector<RawShape> objs,excludes;
for (const Item &item : items_) { for (const Item &item : items_) {
if (item.isFixed()) continue; if (item.isFixed())
objs.push_back(item.transformedShape()); excludes.push_back(item.transformedShape());
else
objs.push_back(item.transformedShape());
} }
if (objs.empty()) if (objs.empty())
return; return;
// Without fixed items this inner-fit NFP can exceed clipper's range and crash MSVC.
if (!excludes.empty())
{ // find a best position inside NFP of fixed items (excluded regions), so the center of pile is cloest to bed center { // find a best position inside NFP of fixed items (excluded regions), so the center of pile is cloest to bed center
RawShape objs_convex_hull = sl::convexHull(objs); RawShape objs_convex_hull = sl::convexHull(objs);
for (const Item &item : items_) {
if (item.isFixed()) {
excludes.push_back(item.transformedShape());
}
}
auto nfps = calcnfp(objs_convex_hull, excludes, bbin, Lvl<MaxNfpLevel::value>()); auto nfps = calcnfp(objs_convex_hull, excludes, bbin, Lvl<MaxNfpLevel::value>());
if (nfps.empty()) { if (nfps.empty()) {
+18 -3
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-18 09:16-0300\n" "POT-Creation-Date: 2026-06-19 10:33-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -7779,6 +7779,15 @@ msgstr ""
msgid "Renders cast shadows on the plate in realistic view." msgid "Renders cast shadows on the plate in realistic view."
msgstr "" msgstr ""
msgid "Smooth normals"
msgstr ""
msgid ""
"Applies smooth normals to the realistic view.\n"
"\n"
"Requires manual scene reload to take effect (right-click on 3D view → \"Reload All\")."
msgstr ""
msgid "Anti-aliasing" msgid "Anti-aliasing"
msgstr "" msgstr ""
@@ -12379,7 +12388,7 @@ msgstr ""
msgid "First layer height" msgid "First layer height"
msgstr "" msgstr ""
msgid "This is the height of the first layer. Making the first layer height thicker can improve build plate adhesion." msgid "Height of the first layer. Making the first layer height thicker can improve build plate adhesion."
msgstr "" msgstr ""
msgid "This is the speed for the first layer except for solid infill sections." msgid "This is the speed for the first layer except for solid infill sections."
@@ -12397,7 +12406,7 @@ msgstr ""
msgid "Travel speed of the first layer." msgid "Travel speed of the first layer."
msgstr "" msgstr ""
msgid "This is the number of top interface layers." msgid "Number of slow layers"
msgstr "" msgstr ""
msgid "The first few layers are printed slower than normal. The speed is gradually increased in a linear fashion over the specified number of layers." msgid "The first few layers are printed slower than normal. The speed is gradually increased in a linear fashion over the specified number of layers."
@@ -14210,6 +14219,9 @@ msgstr ""
msgid "Top interface layers" msgid "Top interface layers"
msgstr "" msgstr ""
msgid "This is the number of top interface layers."
msgstr ""
msgid "Bottom interface layers" msgid "Bottom interface layers"
msgstr "" msgstr ""
@@ -16870,6 +16882,9 @@ msgstr ""
msgid "Unable to calibrate: maybe because the set calibration value range is too large, or the step is too small" msgid "Unable to calibrate: maybe because the set calibration value range is too large, or the step is too small"
msgstr "" msgstr ""
msgid "Physical Printer"
msgstr ""
msgid "Print Host upload" msgid "Print Host upload"
msgstr "" msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+18 -3
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-18 09:16-0300\n" "POT-Creation-Date: 2026-06-19 10:33-0300\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n" "Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n" "Language-Team: \n"
@@ -7775,6 +7775,15 @@ msgstr ""
msgid "Renders cast shadows on the plate in realistic view." msgid "Renders cast shadows on the plate in realistic view."
msgstr "" msgstr ""
msgid "Smooth normals"
msgstr ""
msgid ""
"Applies smooth normals to the realistic view.\n"
"\n"
"Requires manual scene reload to take effect (right-click on 3D view → \"Reload All\")."
msgstr ""
msgid "Anti-aliasing" msgid "Anti-aliasing"
msgstr "" msgstr ""
@@ -12375,7 +12384,7 @@ msgstr ""
msgid "First layer height" msgid "First layer height"
msgstr "" msgstr ""
msgid "This is the height of the first layer. Making the first layer height thicker can improve build plate adhesion." msgid "Height of the first layer. Making the first layer height thicker can improve build plate adhesion."
msgstr "" msgstr ""
msgid "This is the speed for the first layer except for solid infill sections." msgid "This is the speed for the first layer except for solid infill sections."
@@ -12393,7 +12402,7 @@ msgstr ""
msgid "Travel speed of the first layer." msgid "Travel speed of the first layer."
msgstr "" msgstr ""
msgid "This is the number of top interface layers." msgid "Number of slow layers"
msgstr "" msgstr ""
msgid "The first few layers are printed slower than normal. The speed is gradually increased in a linear fashion over the specified number of layers." msgid "The first few layers are printed slower than normal. The speed is gradually increased in a linear fashion over the specified number of layers."
@@ -14206,6 +14215,9 @@ msgstr ""
msgid "Top interface layers" msgid "Top interface layers"
msgstr "" msgstr ""
msgid "This is the number of top interface layers."
msgstr ""
msgid "Bottom interface layers" msgid "Bottom interface layers"
msgstr "" msgstr ""
@@ -16866,6 +16878,9 @@ msgstr ""
msgid "Unable to calibrate: maybe because the set calibration value range is too large, or the step is too small" msgid "Unable to calibrate: maybe because the set calibration value range is too large, or the step is too small"
msgstr "" msgstr ""
msgid "Physical Printer"
msgstr ""
msgid "Print Host upload" msgid "Print Host upload"
msgstr "" msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -123,7 +123,7 @@
"0" "0"
], ],
"machine_pause_gcode": "M600", "machine_pause_gcode": "M600",
"machine_start_gcode": "M140 S60\nM104 S140\nM190 S[first_layer_bed_temperature]\nM109 S{temperature_vitrification[0]}\nG28;\nNOZZLE_WIPE\nM140 S[first_layer_bed_temperature];\nM104 S[first_layer_temperature];\nDRAW_LINE_ONLY", "machine_start_gcode": "M140 S60\nM104 S140\nM190 S[first_layer_bed_temperature]\nM109 S{temperature_vitrification[0]}\nG28;\nNOZZLE_WIPE\nM140 S[first_layer_bed_temperature];\nM109 S[first_layer_temperature];\nDRAW_LINE_ONLY",
"machine_unload_filament_time": "0", "machine_unload_filament_time": "0",
"manual_filament_change": "0", "manual_filament_change": "0",
"max_layer_height": [ "max_layer_height": [
@@ -126,7 +126,7 @@
"0" "0"
], ],
"machine_pause_gcode": "M600", "machine_pause_gcode": "M600",
"machine_start_gcode": "M140 S60\nM104 S140\nM190 S[first_layer_bed_temperature]\nM109 S{temperature_vitrification[0]}\nG28;\nNOZZLE_WIPE\nM140 S[first_layer_bed_temperature];\nM104 S[first_layer_temperature];\nDRAW_LINE_ONLY", "machine_start_gcode": "M140 S60\nM104 S140\nM190 S[first_layer_bed_temperature]\nM109 S{temperature_vitrification[0]}\nG28;\nNOZZLE_WIPE\nM140 S[first_layer_bed_temperature];\nM109 S[first_layer_temperature];\nDRAW_LINE_ONLY",
"machine_unload_filament_time": "0", "machine_unload_filament_time": "0",
"manual_filament_change": "0", "manual_filament_change": "0",
"max_layer_height": [ "max_layer_height": [
@@ -126,7 +126,7 @@
"0" "0"
], ],
"machine_pause_gcode": "M600", "machine_pause_gcode": "M600",
"machine_start_gcode": "M104 S140\nM190 S[first_layer_bed_temperature]\nM109 S{temperature_vitrification[0]}\nG28;\nG1 X230 Y300 Z10 F5000\nSET_KINEMATIC_POSITION Y=0\nG1 Y20 F4000\nG1 X230 F4000\nG1 Z-1 F600 \nG1 X270 F4000\nG1 Y25 F4000\nG1 X230 F4000\nG92 E0\nG1 Z10 F1200\nG1 Y0 F5000\nG1 E-1 F3000\nM400\nSET_KINEMATIC_POSITION Y=300\nG92 E-1\nM140 S[first_layer_bed_temperature];\nM104 S[first_layer_temperature];\nG1 X0 Y0.8 Z0.8 F18000\nG92 E0\nG1 X0 Y0.8 Z0.3 E8 F600\nG92 E0\nG1 X200 Y0.8 Z0.3 F1800.0 E20.0;draw line\nG92 E0\nG1 X200 Y0 Z0.3 F1800.0 E0.08;draw line\nG92 E0\nG1 X100 Y0 Z0.3 F1800.0 E10.0;draw line\nG92 E0\nG1 X100 Y1.6 Z0.3 F1800.0 E0.16;draw line\nG92 E0\nG1 X180 Y1.6 Z0.3 F1800.0 E8;draw line\nG92 E0\nG1 X180 Y0 Z0.3 F1800.0 E0.16;draw line\nG92 E0\nG1 E-1 Z5 F18000\nG92 E0", "machine_start_gcode": "M104 S140\nM190 S[first_layer_bed_temperature]\nM109 S{temperature_vitrification[0]}\nG28;\nG1 X230 Y300 Z10 F5000\nSET_KINEMATIC_POSITION Y=0\nG1 Y20 F4000\nG1 X230 F4000\nG1 Z-1 F600 \nG1 X270 F4000\nG1 Y25 F4000\nG1 X230 F4000\nG92 E0\nG1 Z10 F1200\nG1 Y0 F5000\nG1 E-1 F3000\nM400\nSET_KINEMATIC_POSITION Y=300\nG92 E-1\nM140 S[first_layer_bed_temperature];\nM109 S[first_layer_temperature];\nG1 X0 Y0.8 Z0.8 F18000\nG92 E0\nG1 X0 Y0.8 Z0.3 E8 F600\nG92 E0\nG1 X200 Y0.8 Z0.3 F1800.0 E20.0;draw line\nG92 E0\nG1 X200 Y0 Z0.3 F1800.0 E0.08;draw line\nG92 E0\nG1 X100 Y0 Z0.3 F1800.0 E10.0;draw line\nG92 E0\nG1 X100 Y1.6 Z0.3 F1800.0 E0.16;draw line\nG92 E0\nG1 X180 Y1.6 Z0.3 F1800.0 E8;draw line\nG92 E0\nG1 X180 Y0 Z0.3 F1800.0 E0.16;draw line\nG92 E0\nG1 E-1 Z5 F18000\nG92 E0",
"machine_unload_filament_time": "0", "machine_unload_filament_time": "0",
"manual_filament_change": "0", "manual_filament_change": "0",
"max_layer_height": [ "max_layer_height": [
@@ -126,7 +126,7 @@
"0" "0"
], ],
"machine_pause_gcode": "M600", "machine_pause_gcode": "M600",
"machine_start_gcode": "M104 S140\nM190 S[first_layer_bed_temperature]\nM109 S{temperature_vitrification[0]}\nG28;\nG1 X180 Y247 Z10 F5000\nSET_KINEMATIC_POSITION Y=0\nG1 Y11 F4000\nG1 X180 F4000\nG1 Z-1 F600 \nG1 X230 F4000\nG1 Y15 F4000\nG1 X180 F4000\nG92 E0\nG1 Z10 F1200\nG1 Y0 F5000\nG1 E-1 F3000\nM400\nSET_KINEMATIC_POSITION Y=247\nG92 E-1\nM140 S[first_layer_bed_temperature];\nM104 S[first_layer_temperature];\nG1 X0 Y0.8 Z0.8 F18000\nG92 E0\nG1 X0 Y0.8 Z0.3 E8 F600\nG92 E0\nG1 X170 Y0.8 Z0.3 F1800.0 E17.0;draw line\nG92 E0\nG1 X170 Y0 Z0.3 F1800.0 E0.08;draw line\nG92 E0\nG1 X70 Y0 Z0.3 F1800.0 E10.0;draw line\nG92 E0\nG1 X70 Y1.6 Z0.3 F1800.0 E0.16;draw line\nG92 E0\nG1 X150 Y1.6 Z0.3 F1800.0 E8;draw line\nG92 E0\nG1 X150 Y0 Z0.3 F1800.0 E0.16;draw line\nG92 E0\nG1 E-1 Z5 F18000\nG92 E0\n", "machine_start_gcode": "M104 S140\nM190 S[first_layer_bed_temperature]\nM109 S{temperature_vitrification[0]}\nG28;\nG1 X180 Y247 Z10 F5000\nSET_KINEMATIC_POSITION Y=0\nG1 Y11 F4000\nG1 X180 F4000\nG1 Z-1 F600 \nG1 X230 F4000\nG1 Y15 F4000\nG1 X180 F4000\nG92 E0\nG1 Z10 F1200\nG1 Y0 F5000\nG1 E-1 F3000\nM400\nSET_KINEMATIC_POSITION Y=247\nG92 E-1\nM140 S[first_layer_bed_temperature];\nM109 S[first_layer_temperature];\nG1 X0 Y0.8 Z0.8 F18000\nG92 E0\nG1 X0 Y0.8 Z0.3 E8 F600\nG92 E0\nG1 X170 Y0.8 Z0.3 F1800.0 E17.0;draw line\nG92 E0\nG1 X170 Y0 Z0.3 F1800.0 E0.08;draw line\nG92 E0\nG1 X70 Y0 Z0.3 F1800.0 E10.0;draw line\nG92 E0\nG1 X70 Y1.6 Z0.3 F1800.0 E0.16;draw line\nG92 E0\nG1 X150 Y1.6 Z0.3 F1800.0 E8;draw line\nG92 E0\nG1 X150 Y0 Z0.3 F1800.0 E0.16;draw line\nG92 E0\nG1 E-1 Z5 F18000\nG92 E0\n",
"machine_unload_filament_time": "0", "machine_unload_filament_time": "0",
"manual_filament_change": "0", "manual_filament_change": "0",
"max_layer_height": [ "max_layer_height": [
@@ -125,7 +125,7 @@
"0" "0"
], ],
"machine_pause_gcode": "M25", "machine_pause_gcode": "M25",
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z2 F6000\nG1 X50 Y220 Z0.25 F4800\nG1 X70 E1.8 F900\nG1 X200 E6 F1800\nG0 Y219.6\nG1 X130 E3 F1800\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z2 F6000\nG1 X50 Y220 Z0.25 F4800\nG1 X70 E1.8 F900\nG1 X200 E6 F1800\nG0 Y219.6\nG1 X130 E3 F1800\nG92 E0",
"machine_tool_change_time": "0", "machine_tool_change_time": "0",
"machine_unload_filament_time": "39", "machine_unload_filament_time": "39",
"manual_filament_change": "0", "manual_filament_change": "0",
@@ -125,7 +125,7 @@
"0" "0"
], ],
"machine_pause_gcode": "M25", "machine_pause_gcode": "M25",
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z2 F6000\nG1 X50 Y220 Z0.25 F4800\nG1 X70 E3 F900\nG1 X200 E10 F1800\nG0 Y219.6\nG1 X130 E5 F1800\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z2 F6000\nG1 X50 Y220 Z0.25 F4800\nG1 X70 E3 F900\nG1 X200 E10 F1800\nG0 Y219.6\nG1 X130 E5 F1800\nG92 E0",
"machine_tool_change_time": "0", "machine_tool_change_time": "0",
"machine_unload_filament_time": "39", "machine_unload_filament_time": "39",
"manual_filament_change": "0", "manual_filament_change": "0",
@@ -125,7 +125,7 @@
"0" "0"
], ],
"machine_pause_gcode": "M25", "machine_pause_gcode": "M25",
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z2 F6000\nG1 X50 Y220 Z0.25 F4800\nG1 X70 E3 F900\nG1 X200 E10 F1800\nG0 Y219.6\nG1 X130 E5 F1800\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z2 F6000\nG1 X50 Y220 Z0.25 F4800\nG1 X70 E3 F900\nG1 X200 E10 F1800\nG0 Y219.6\nG1 X130 E5 F1800\nG92 E0",
"machine_tool_change_time": "0", "machine_tool_change_time": "0",
"machine_unload_filament_time": "39", "machine_unload_filament_time": "39",
"manual_filament_change": "0", "manual_filament_change": "0",
@@ -126,7 +126,7 @@
"0" "0"
], ],
"machine_pause_gcode": "M25", "machine_pause_gcode": "M25",
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z2 F6000\nG1 X50 Y220 Z0.25 F4800\nG1 X70 E3 F900\nG1 X200 E10 F1800\nG0 Y219.6\nG1 X130 E5 F1800\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z2 F6000\nG1 X50 Y220 Z0.25 F4800\nG1 X70 E3 F900\nG1 X200 E10 F1800\nG0 Y219.6\nG1 X130 E5 F1800\nG92 E0",
"machine_tool_change_time": "0", "machine_tool_change_time": "0",
"machine_unload_filament_time": "39", "machine_unload_filament_time": "39",
"manual_filament_change": "0", "manual_filament_change": "0",
@@ -17,7 +17,7 @@
"min_layer_height": [ "min_layer_height": [
"0.08" "0.08"
], ],
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG90 E0\nM83\nG1 E-1 F600\nG1 E8 F300\nG1 X85 Y110 Z0.2 F1200\nG1 X-110 E15 F2400\nG1 Y0 E4 F2400\nG1 X-109.6 F2400\nG1 Y110 E5 F2400\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG90 E0\nM83\nG1 E-1 F600\nG1 E8 F300\nG1 X85 Y110 Z0.2 F1200\nG1 X-110 E15 F2400\nG1 Y0 E4 F2400\nG1 X-109.6 F2400\nG1 Y110 E5 F2400\nG92 E0",
"retraction_length": [ "retraction_length": [
"1" "1"
], ],
@@ -11,7 +11,7 @@
"0.8" "0.8"
], ],
"printer_variant": "0.8", "printer_variant": "0.8",
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG1 E-1.5 F600\nG1 E12 F800\nG1 X85 Y110 Z0.3 F1200\nG1 X-110 E30 F2400\nG1 Y0 E8 F2400\nG1 X-109.6 F2400\nG1 Y110 E10 F2400\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG1 E-1.5 F600\nG1 E12 F800\nG1 X85 Y110 Z0.3 F1200\nG1 X-110 E30 F2400\nG1 Y0 E8 F2400\nG1 X-109.6 F2400\nG1 Y110 E10 F2400\nG92 E0",
"max_layer_height": [ "max_layer_height": [
"0.56" "0.56"
], ],
@@ -17,7 +17,7 @@
"min_layer_height": [ "min_layer_height": [
"0.08" "0.08"
], ],
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG90 E0\nM83\nG1 E-1 F600\nG1 E8 F300\nG1 X85 Y110 Z0.2 F1200\nG1 X-110 E15 F2400\nG1 Y0 E4 F2400\nG1 X-109.6 F2400\nG1 Y110 E5 F2400\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG90 E0\nM83\nG1 E-1 F600\nG1 E8 F300\nG1 X85 Y110 Z0.2 F1200\nG1 X-110 E15 F2400\nG1 Y0 E4 F2400\nG1 X-109.6 F2400\nG1 Y110 E5 F2400\nG92 E0",
"retraction_length": [ "retraction_length": [
"1" "1"
], ],
@@ -11,7 +11,7 @@
"0.8" "0.8"
], ],
"printer_variant": "0.8", "printer_variant": "0.8",
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG1 E-1.5 F600\nG1 E12 F800\nG1 X85 Y110 Z0.3 F1200\nG1 X-110 E30 F2400\nG1 Y0 E8 F2400\nG1 X-109.6 F2400\nG1 Y110 E10 F2400\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG1 E-1.5 F600\nG1 E12 F800\nG1 X85 Y110 Z0.3 F1200\nG1 X-110 E30 F2400\nG1 Y0 E8 F2400\nG1 X-109.6 F2400\nG1 Y110 E10 F2400\nG92 E0",
"max_layer_height": [ "max_layer_height": [
"0.56" "0.56"
], ],
@@ -96,7 +96,7 @@
"default_filament_profile": [ "default_filament_profile": [
"Flashforge Generic PLA" "Flashforge Generic PLA"
], ],
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG1 E-0.2 F800\nG1 X110 Y-110 F6000\nG1 E2 F800\nG1 Y-110 X55 Z0.25 F4800\nG1 X-55 E8 F2400\nG1 Y-109.6 F2400\nG1 X55 E5 F2400\nG1 Y-110 X55 Z0.45 F4800\nG1 X-55 E8 F2400\nG1 Y-109.6 F2400\nG1 X55 E5 F2400\nG92 E0", "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F6000\nG1 E-0.2 F800\nG1 X110 Y-110 F6000\nG1 E2 F800\nG1 Y-110 X55 Z0.25 F4800\nG1 X-55 E8 F2400\nG1 Y-109.6 F2400\nG1 X55 E5 F2400\nG1 Y-110 X55 Z0.45 F4800\nG1 X-55 E8 F2400\nG1 Y-109.6 F2400\nG1 X55 E5 F2400\nG92 E0",
"machine_end_gcode": "G1 E-3 F3600\nG0 X50 Y50 F30000\nM104 S0 ; turn off temperature", "machine_end_gcode": "G1 E-3 F3600\nG0 X50 Y50 F30000\nM104 S0 ; turn off temperature",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]", "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
+11 -11
View File
@@ -10,18 +10,18 @@ const float EPSILON = 0.0001;
// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) // normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31)
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) #define LIGHT_TOP_DIFFUSE (0.85 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.8 * INTENSITY_CORRECTION) #define LIGHT_TOP_SPECULAR (0.35 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 128.0 #define LIGHT_TOP_SHININESS 32.0
// normalized values for (1./1.43, 0.2/1.43, 1./1.43) // normalized values for (1./1.43, 0.2/1.43, 1./1.43)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) #define LIGHT_FRONT_DIFFUSE (0.35 * INTENSITY_CORRECTION)
#define LIGHT_FRONT_SPECULAR (0.28 * INTENSITY_CORRECTION) #define LIGHT_FRONT_SPECULAR (0.12 * INTENSITY_CORRECTION)
#define LIGHT_FRONT_SHININESS 64.0 #define LIGHT_FRONT_SHININESS 16.0
#define INTENSITY_AMBIENT 0.22 #define INTENSITY_AMBIENT 0.25
#define WINDOW_REFLECTION_INTENSITY 0.55 #define WINDOW_REFLECTION_INTENSITY 0.30
struct PrintVolumeDetection struct PrintVolumeDetection
{ {
@@ -162,11 +162,11 @@ vec3 compute_window_reflection(vec3 normal, vec3 view_dir)
float bars = 1.0; float bars = 1.0;
// Fresnel effect for edge glow // Fresnel effect for edge glow
float fresnel = pow(1.0 - max(dot(normal, view_dir), 0.0), 1.0); float fresnel = pow(1.0 - max(dot(normal, view_dir), 0.0), 1.2);
float facing = smoothstep(-0.4, 0.6, reflect_light.z); float facing = smoothstep(-0.4, 0.6, reflect_light.z);
float intensity = window_light * bars * (0.25 + 0.25 * fresnel) * facing; float intensity = window_light * bars * (0.15 + 0.15 * fresnel) * facing;
intensity = clamp(intensity, 0.0, 0.45); intensity = clamp(intensity, 0.0, 0.25);
return vec3(intensity); return vec3(intensity);
} }
+13 -12
View File
@@ -10,18 +10,18 @@ const float EPSILON = 0.0001;
// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) // normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31)
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) #define LIGHT_TOP_DIFFUSE (0.85 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.8 * INTENSITY_CORRECTION) #define LIGHT_TOP_SPECULAR (0.35 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 128.0 #define LIGHT_TOP_SHININESS 32.0
// normalized values for (1./1.43, 0.2/1.43, 1./1.43) // normalized values for (1./1.43, 0.2/1.43, 1./1.43)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) #define LIGHT_FRONT_DIFFUSE (0.35 * INTENSITY_CORRECTION)
#define LIGHT_FRONT_SPECULAR (0.28 * INTENSITY_CORRECTION) #define LIGHT_FRONT_SPECULAR (0.12 * INTENSITY_CORRECTION)
#define LIGHT_FRONT_SHININESS 64.0 #define LIGHT_FRONT_SHININESS 16.0
#define INTENSITY_AMBIENT 0.22 #define INTENSITY_AMBIENT 0.25
#define WINDOW_REFLECTION_INTENSITY 0.55 #define WINDOW_REFLECTION_INTENSITY 0.30
struct PrintVolumeDetection struct PrintVolumeDetection
{ {
@@ -164,12 +164,13 @@ vec3 compute_window_reflection(vec3 normal, vec3 view_dir)
// No bars - just pure circular glass // No bars - just pure circular glass
float bars = 1.0; float bars = 1.0;
// Fresnel effect for edge glow
float fresnel = pow(1.0 - max(dot(normal, view_dir), 0.0), 1.0); float fresnel = pow(1.0 - max(dot(normal, view_dir), 0.0), 1.2);
float facing = smoothstep(-0.4, 0.6, reflect_light.z); float facing = smoothstep(-0.4, 0.6, reflect_light.z);
float intensity = window_light * bars * (0.25 + 0.25 * fresnel) * facing;
intensity = clamp(intensity, 0.0, 0.45); float intensity = window_light * bars * (0.15 + 0.15 * fresnel) * facing;
intensity = clamp(intensity, 0.0, 0.25);
return vec3(intensity); return vec3(intensity);
} }
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Sourceable helper: `retry <cmd...>` runs a command, retrying up to 3 times
# with a 5-minute wait between attempts. Useful for flaky commands such as
# `hdiutil create` intermittently failing with "Resource busy".
retry() {
local attempt=1 max_attempts=3 delay=300
until "$@"; do
if [ "$attempt" -ge "$max_attempts" ]; then
echo "::error::Command failed after $attempt attempts: $*"
return 1
fi
echo "Attempt $attempt failed: $*. Retrying in $((delay / 60)) minutes..."
sleep "$delay"
attempt=$((attempt + 1))
done
}
+3
View File
@@ -276,6 +276,9 @@ void AppConfig::set_defaults()
if (get(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS).empty()) if (get(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS).empty())
set_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS, false); set_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS, false);
if (get(SETTING_OPENGL_PHONG_SMOOTH_NORMALS).empty())
set_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS, false);
if (get(SETTING_OPENGL_PHONG_SSAO).empty()) if (get(SETTING_OPENGL_PHONG_SSAO).empty())
set_bool(SETTING_OPENGL_PHONG_SSAO, false); set_bool(SETTING_OPENGL_PHONG_SSAO, false);
+1
View File
@@ -39,6 +39,7 @@ using namespace nlohmann;
#define SETTING_OPENGL_SHADING_MODEL "opengl_shading_model" #define SETTING_OPENGL_SHADING_MODEL "opengl_shading_model"
#define SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS "opengl_phong_basic_plate_shadows" #define SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS "opengl_phong_basic_plate_shadows"
#define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao" #define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao"
#define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals"
#if defined(_WIN32) || defined(_WIN64) #if defined(_WIN32) || defined(_WIN64)
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09" #define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
+17 -2
View File
@@ -879,7 +879,9 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
std::map<ObjectID, ExtrusionEntityCollection>& brimMap, std::map<ObjectID, ExtrusionEntityCollection>& brimMap,
std::map<ObjectID, ExtrusionEntityCollection>& supportBrimMap, std::map<ObjectID, ExtrusionEntityCollection>& supportBrimMap,
std::vector<std::pair<ObjectID, unsigned int>> &objPrintVec, std::vector<std::pair<ObjectID, unsigned int>> &objPrintVec,
std::vector<unsigned int>& printExtruders) std::vector<unsigned int>& printExtruders,
std::map<ObjectID, ExPolygons>* objectBrimAreasOut,
std::map<ObjectID, ExPolygons>* supportBrimAreasOut)
{ {
// Belt printer: brim is not compatible with belt printing. // Belt printer: brim is not compatible with belt printing.
if (print.config().belt_printer.value) if (print.config().belt_printer.value)
@@ -932,12 +934,25 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
for (size_t iia = 0; iia < islands_area.size(); ++iia) for (size_t iia = 0; iia < islands_area.size(); ++iia)
islands_area[iia].translate(plate_shift); islands_area[iia].translate(plate_shift);
// Orca: keep translated brim footprints for skirt grouping.
auto translate_area_map = [plate_shift](const std::map<ObjectID, ExPolygons>& src) {
std::map<ObjectID, ExPolygons> dst = src;
for (auto& [_, areas] : dst)
for (ExPolygon& area : areas)
area.translate(plate_shift);
return dst;
};
if (objectBrimAreasOut != nullptr)
*objectBrimAreasOut = translate_area_map(brimAreaMap);
if (supportBrimAreasOut != nullptr)
*supportBrimAreasOut = translate_area_map(supportBrimAreaMap);
const bool combine_brims = print.config().combine_brims.value; const bool combine_brims = print.config().combine_brims.value;
const bool is_by_object = (print.config().print_sequence == PrintSequence::ByObject); const bool is_by_object = (print.config().print_sequence == PrintSequence::ByObject);
const bool can_combine_brims = combine_brims && !is_by_object; const bool can_combine_brims = combine_brims && !is_by_object;
if (!can_combine_brims) { if (!can_combine_brims) {
// Orca: Generate brims separately for each object when multiple extruders are used // Orca: Generate brims separately when brims cannot be combined.
for (auto iter = brimAreaMap.begin(); iter != brimAreaMap.end(); ++iter) { for (auto iter = brimAreaMap.begin(); iter != brimAreaMap.end(); ++iter) {
if (!iter->second.empty()) { if (!iter->second.empty()) {
brimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area))); brimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area)));
+4 -1
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_Brim_hpp_ #ifndef slic3r_Brim_hpp_
#define slic3r_Brim_hpp_ #define slic3r_Brim_hpp_
#include "ExPolygon.hpp"
#include "Point.hpp" #include "Point.hpp"
#include<map> #include<map>
@@ -19,7 +20,9 @@ void make_brim(const Print& print, PrintTryCancel try_cancel,
Polygons& islands_area, std::map<ObjectID, ExtrusionEntityCollection>& brimMap, Polygons& islands_area, std::map<ObjectID, ExtrusionEntityCollection>& brimMap,
std::map<ObjectID, ExtrusionEntityCollection>& supportBrimMap, std::map<ObjectID, ExtrusionEntityCollection>& supportBrimMap,
std::vector<std::pair<ObjectID, unsigned int>>& objPrintVec, std::vector<std::pair<ObjectID, unsigned int>>& objPrintVec,
std::vector<unsigned int>& printExtruders); std::vector<unsigned int>& printExtruders,
std::map<ObjectID, ExPolygons>* objectBrimAreasOut = nullptr,
std::map<ObjectID, ExPolygons>* supportBrimAreasOut = nullptr);
// BBS: automatically make brim // BBS: automatically make brim
ExtrusionEntityCollection make_brim_auto(const Print &print, PrintTryCancel try_cancel, Polygons &islands_area); ExtrusionEntityCollection make_brim_auto(const Print &print, PrintTryCancel try_cancel, Polygons &islands_area);
+53 -36
View File
@@ -5276,44 +5276,30 @@ LayerResult GCode::process_layer(
bool has_insert_wrapping_detection_gcode = false; bool has_insert_wrapping_detection_gcode = false;
// Extrude the skirt, brim, support, perimeters, infill ordered by the extruders. // Extrude the skirt, brim, support, perimeters, infill ordered by the extruders.
// Orca: Print unified global brim before any object.
// Only do this if `combine_brims` is enabled and we are printing by layer.
if (first_layer && sequence_by_layer && m_config.combine_brims && !print.m_brimMap.empty()) {
const ObjectID unified_object_id = [&]() -> ObjectID {
ObjectID id;
bool found = false;
for (const auto& [obj_id, brim] : print.m_brimMap) {
const bool has_printable_entities = std::any_of(brim.entities.begin(), brim.entities.end(),
[](const ExtrusionEntity* ee) { return ee != nullptr; });
if (!has_printable_entities)
continue;
if (found)
return ObjectID();
id = obj_id;
found = true;
}
return found ? id : ObjectID();
}();
if (unified_object_id.valid()) {
const auto it = print.m_brimMap.find(unified_object_id);
if (it != print.m_brimMap.end()) {
this->set_origin(0., 0.);
for (const ExtrusionEntity* ee : it->second.entities)
if (ee != nullptr)
gcode += this->extrude_entity(*ee, "brim", m_config.support_speed.value);
// Mark brim as printed for this object to avoid per-object brim emission later.
this->m_objsWithBrim.erase(unified_object_id);
}
}
}
for (unsigned int extruder_id : layer_tools.extruders) for (unsigned int extruder_id : layer_tools.extruders)
{ {
if (print.config().skirt_type == stCombined && !print.skirt().empty()) if ((print.config().skirt_type == stCombined ||
gcode += generate_skirt(print, print.skirt(), Point(0, 0), layer.object()->config().skirt_start_angle, layer_tools, layer, (print.config().skirt_type == stPerObject && print.config().print_sequence == PrintSequence::ByLayer)) &&
extruder_id); !print.skirt_groups().empty()) {
bool skirt_generated_for_current_print_z = false;
for (const ExtrusionEntityCollection& skirt_group : print.skirt_groups()) {
if (skirt_group.empty())
continue;
// Orca: each grouped skirt is emitted as its own collection so higher skirt layers
// follow the same per-group behavior as the first layer.
if (first_layer)
m_skirt_done.clear();
else if (skirt_generated_for_current_print_z && !m_skirt_done.empty())
m_skirt_done.pop_back();
std::string skirt_gcode = generate_skirt(print, skirt_group, Point(0, 0), layer.object()->config().skirt_start_angle,
layer_tools, layer, extruder_id);
if (!skirt_gcode.empty())
skirt_generated_for_current_print_z = true;
gcode += std::move(skirt_gcode);
}
}
if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) { if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) {
std::vector<size_t> filament_instances_id; std::vector<size_t> filament_instances_id;
@@ -5414,6 +5400,37 @@ LayerResult GCode::process_layer(
} }
} }
// Orca: Print unified global brim after the skirt and before any object.
// Only do this if `combine_brims` is enabled and we are printing by layer.
if (first_layer && sequence_by_layer && m_config.combine_brims && !print.m_brimMap.empty()) {
const ObjectID unified_object_id = [&]() -> ObjectID {
ObjectID id;
for (const auto& [obj_id, brim] : print.m_brimMap) {
const bool has_printable_entities = std::any_of(brim.entities.begin(), brim.entities.end(),
[](const ExtrusionEntity* ee) { return ee != nullptr; });
if (!has_printable_entities)
continue;
if (id.valid())
return ObjectID();
id = obj_id;
}
return id;
}();
if (unified_object_id.valid() && this->m_objsWithBrim.find(unified_object_id) != this->m_objsWithBrim.end()) {
const ExtrusionEntityCollection& unified_brim = print.m_brimMap.at(unified_object_id);
this->set_origin(0., 0.);
for (const ExtrusionEntity* ee : unified_brim.entities)
if (ee != nullptr)
gcode += this->extrude_entity(*ee, "brim", m_config.support_speed.value);
// Mark brim as printed for this object to avoid per-object brim emission later.
this->m_objsWithBrim.erase(unified_object_id);
}
}
// We are almost ready to print. However, we must go through all the objects twice to print the overridden extrusions first (infill/perimeter wiping feature): // We are almost ready to print. However, we must go through all the objects twice to print the overridden extrusions first (infill/perimeter wiping feature):
std::vector<ObjectByExtruder::Island::Region> by_region_per_copy_cache; std::vector<ObjectByExtruder::Island::Region> by_region_per_copy_cache;
for (int print_wipe_extrusions = is_anything_overridden; print_wipe_extrusions>=0; --print_wipe_extrusions) { for (int print_wipe_extrusions = is_anything_overridden; print_wipe_extrusions>=0; --print_wipe_extrusions) {
@@ -8,6 +8,7 @@
#include <sstream> #include <sstream>
#include <iostream> #include <iostream>
#include <cmath> #include <cmath>
#include <cctype>
namespace Slic3r { namespace Slic3r {
@@ -282,4 +283,81 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) {
return output.str(); return output.str();
} }
std::string AdaptivePAProcessor::validate_adaptive_pa_model(const std::string& model_str)
{
if (model_str.empty())
return {}; // Empty model is valid
std::istringstream model_stream(model_str);
std::string line;
int line_number = 0;
while (std::getline(model_stream, line)) {
++line_number;
// Trim whitespace
const auto first = line.find_first_not_of(" \t\r\n");
if (first == std::string::npos)
continue; // Skip empty lines
const auto last = line.find_last_not_of(" \t\r\n");
line = line.substr(first, last - first + 1);
// Only numbers, commas and dots are allowed (no letters or other characters)
for (char c : line) {
if (!std::isdigit(static_cast<unsigned char>(c)) && c != ',' && c != '.') {
return "Line " + std::to_string(line_number) +
": only numbers, commas and dots are allowed";
}
}
// Count commas to validate format (should be exactly 2 for 3 values)
int comma_count = 0;
for (char c : line) {
if (c == ',') comma_count++;
}
if (comma_count != 2) {
return "Line " + std::to_string(line_number) +
": must contain exactly 3 comma-separated values (PA, flow, acceleration)";
}
// Parse and validate the values
try {
std::istringstream line_stream(line);
std::string value;
// Parse PA
if (!std::getline(line_stream, value, ','))
return "Line " + std::to_string(line_number) + ": missing PA value";
double pa = std::stod(value);
// Parse flow
if (!std::getline(line_stream, value, ','))
return "Line " + std::to_string(line_number) + ": missing flow value";
double flow = std::stod(value);
// Parse acceleration
if (!std::getline(line_stream, value, ','))
return "Line " + std::to_string(line_number) + ": missing acceleration value";
double accel = std::stod(value);
// Validate constraints
if (pa >= 2.0) {
return "Line " + std::to_string(line_number) + ": PA value must be less than 2";
}
if (flow <= pa) {
return "Line " + std::to_string(line_number) + ": flow value must be greater than PA value";
}
if (accel <= flow) {
return "Line " + std::to_string(line_number) + ": acceleration value must be greater than flow value";
}
} catch (const std::exception&) {
return "Line " + std::to_string(line_number) + ": invalid numeric value";
}
}
return {}; // All validations passed
}
} // namespace Slic3r } // namespace Slic3r
@@ -54,6 +54,20 @@ public:
*/ */
void resetPreviousPA(double PA){ m_last_predicted_pa = PA; }; void resetPreviousPA(double PA){ m_last_predicted_pa = PA; };
/**
* @brief Validates an adaptive pressure advance model string.
*
* Checks that:
* - Each non-empty line has exactly 3 comma-separated values (PA, flow, accel)
* - PA value is less than 2
* - Flow value is greater than PA value
* - Accel value is greater than flow value
*
* @param model_str The model string to validate (typically from config)
* @return Empty string if valid, or an error message describing the first issue found
*/
static std::string validate_adaptive_pa_model(const std::string& model_str);
private: private:
GCode &m_gcodegen; ///< Reference to the GCode object. GCode &m_gcodegen; ///< Reference to the GCode object.
std::unordered_map<unsigned int, std::unique_ptr<AdaptivePAInterpolator>> m_AdaptivePAInterpolators; ///< Map between Interpolator objects and tool ID's std::unordered_map<unsigned int, std::unique_ptr<AdaptivePAInterpolator>> m_AdaptivePAInterpolators; ///< Map between Interpolator objects and tool ID's
+174 -89
View File
@@ -24,7 +24,9 @@
#include <algorithm> #include <algorithm>
#include <limits> #include <limits>
#include <numeric>
#include <unordered_set> #include <unordered_set>
#include <sstream>
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
#include <boost/format.hpp> #include <boost/format.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
@@ -1987,6 +1989,23 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter) if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter)
warn(L("The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences."), "precise_outer_wall"); warn(L("The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences."), "precise_outer_wall");
// check adaptive pressure advance model
for (unsigned int extruder_id : extruders) {
if (m_config.adaptive_pressure_advance.get_at(extruder_id) &&
m_config.enable_pressure_advance.get_at(extruder_id)) {
const std::string pa_model = m_config.adaptive_pressure_advance_model.get_at(extruder_id);
if (!pa_model.empty()) {
std::string validation_error = AdaptivePAProcessor::validate_adaptive_pa_model(pa_model);
if (!validation_error.empty()) {
warn(L("The Adaptive Pressure Advance model for one or more extruders may contain invalid values."),
"adaptive_pressure_advance_model");
break;
}
}
}
}
} catch (std::exception& e) { } catch (std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "Orca: validate motion ability failed: " << e.what() << std::endl; BOOST_LOG_TRIVIAL(warning) << "Orca: validate motion ability failed: " << e.what() << std::endl;
} }
@@ -2511,7 +2530,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
start_time = (long long)Slic3r::Utils::get_current_time_utc(); start_time = (long long)Slic3r::Utils::get_current_time_utc();
m_skirt.clear(); m_skirt.clear();
m_skirt_groups.clear();
m_skirt_convex_hull.clear(); m_skirt_convex_hull.clear();
m_objectBrimAreas.clear();
m_supportBrimAreas.clear();
m_first_layer_convex_hull.points.clear(); m_first_layer_convex_hull.points.clear();
for (PrintObject *object : m_objects) object->m_skirt.clear(); for (PrintObject *object : m_objects) object->m_skirt.clear();
@@ -2605,7 +2627,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (this->has_brim()) { if (this->has_brim()) {
Polygons islands_area; Polygons islands_area;
make_brim(*this, this->make_try_cancel(), islands_area, m_brimMap, make_brim(*this, this->make_try_cancel(), islands_area, m_brimMap,
m_supportBrimMap, objPrintVec, printExtruders); m_supportBrimMap, objPrintVec, printExtruders, &m_objectBrimAreas, &m_supportBrimAreas);
for (Polygon& poly_ex : islands_area) for (Polygon& poly_ex : islands_area)
poly_ex.douglas_peucker(SCALED_RESOLUTION); poly_ex.douglas_peucker(SCALED_RESOLUTION);
for (Polygon &poly : union_(this->first_layer_islands(), islands_area)) for (Polygon &poly : union_(this->first_layer_islands(), islands_area))
@@ -2736,11 +2758,13 @@ void Print::_make_skirt()
skirt_height_z = std::max(skirt_height_z, object->m_layers[skirt_layers-1]->print_z); skirt_height_z = std::max(skirt_height_z, object->m_layers[skirt_layers-1]->print_z);
} }
// Collect points from all layers contained in skirt height. struct ObjectSkirtHull {
Points points; PrintObject* object;
Polygon hull;
};
// BBS // Orca: build one local occupied hull per object from object and support geometry up to skirt height.
std::map<PrintObject*, Polygon> object_convex_hulls; std::vector<ObjectSkirtHull> object_convex_hulls;
for (PrintObject *object : m_objects) { for (PrintObject *object : m_objects) {
Points object_points; Points object_points;
// Get object layers up to skirt_height_z. // Get object layers up to skirt_height_z.
@@ -2758,30 +2782,13 @@ void Print::_make_skirt()
layer->support_fills.collect_points(object_points); layer->support_fills.collect_points(object_points);
} }
object_convex_hulls.insert({ object, Slic3r::Geometry::convex_hull(object_points) }); object_convex_hulls.push_back({ object, Slic3r::Geometry::convex_hull(object_points) });
// Repeat points for each object copy.
for (const PrintInstance &instance : object->instances()) {
Points copy_points = object_points;
for (Point &pt : copy_points)
pt += instance.shift;
append(points, copy_points);
}
} }
// Include the wipe tower. if (object_convex_hulls.empty())
append(points, this->first_layer_wipe_tower_corners());
// Unless draft shield is enabled, include all brims as well.
if (config().draft_shield == dsDisabled)
append(points, m_first_layer_convex_hull.points);
if (points.size() < 3)
// At least three points required for a convex hull.
return; return;
this->throw_if_canceled(); this->throw_if_canceled();
Polygon convex_hull = Slic3r::Geometry::convex_hull(points);
// Skirt may be printed on several layers, having distinct layer heights, // Skirt may be printed on several layers, having distinct layer heights,
// but loops must be aligned so can't vary width/spacing // but loops must be aligned so can't vary width/spacing
@@ -2803,13 +2810,13 @@ void Print::_make_skirt()
} }
} }
// Initial offset of the brim inner edge from the object (possible with a support & raft). // Initial skirt centerline offset from the occupied outline.
// The skirt will touch the brim if the brim is extruded. // The skirt will touch the occupied outline if skirt_distance is zero.
auto distance = float(scale_(m_config.skirt_distance.value - spacing/2.)); // Generate loops inward to outward; callers reverse them before G-code export.
// Draw outlines from outside to inside.
// Loop while we have less skirts than required or any extruder hasn't reached the min length if any. // Loop while we have less skirts than required or any extruder hasn't reached the min length if any.
std::vector<coordf_t> extruded_length(extruders.size(), 0.); auto append_skirt_loops_for_hull = [&](const Polygon& hull, ExtrusionEntityCollection& dst, bool collect_skirt_hull) {
if (m_config.skirt_type == stCombined) { float distance = float(scale_(m_config.skirt_distance.value - spacing/2.));
std::vector<coordf_t> extruded_length(extruders.size(), 0.);
for (size_t i = m_config.skirt_loops, extruder_idx = 0; i > 0; -- i) { for (size_t i = m_config.skirt_loops, extruder_idx = 0; i > 0; -- i) {
this->throw_if_canceled(); this->throw_if_canceled();
// Offset the skirt outside. // Offset the skirt outside.
@@ -2817,8 +2824,8 @@ void Print::_make_skirt()
// Generate the skirt centerline. // Generate the skirt centerline.
Polygon loop; Polygon loop;
{ {
// BBS. skirt_distance is defined as the gap between skirt and outer most brim, so no need to add max_brim_width // Orca: the hull already represents the occupied outline used for this skirt.
Polygons loops = offset(convex_hull, distance, ClipperLib::jtRound, float(scale_(0.1))); Polygons loops = offset(hull, distance, ClipperLib::jtRound, float(scale_(0.1)));
Geometry::simplify_polygons(loops, scale_(0.05), &loops); Geometry::simplify_polygons(loops, scale_(0.05), &loops);
if (loops.empty()) if (loops.empty())
break; break;
@@ -2834,7 +2841,7 @@ void Print::_make_skirt()
(float)initial_layer_print_height // this will be overridden at G-code export time (float)initial_layer_print_height // this will be overridden at G-code export time
))); )));
eloop.paths.back().polyline = Polyline3(loop.split_at_first_point()); eloop.paths.back().polyline = Polyline3(loop.split_at_first_point());
m_skirt.append(eloop); dst.append(eloop);
if (m_config.min_skirt_length.value > 0) { if (m_config.min_skirt_length.value > 0) {
// The skirt length is limited. Sum the total amount of filament length extruded, in mm. // The skirt length is limited. Sum the total amount of filament length extruded, in mm.
extruded_length[extruder_idx] += unscale<double>(loop.length()) * extruders_e_per_mm[extruder_idx]; extruded_length[extruder_idx] += unscale<double>(loop.length()) * extruders_e_per_mm[extruder_idx];
@@ -2850,69 +2857,147 @@ void Print::_make_skirt()
++ extruder_idx; ++ extruder_idx;
} }
} else { } else {
// The skirt lenght is not limited, extrude the skirt with the 1st extruder only. // The skirt length is not limited, extrude the skirt with the 1st extruder only.
} }
} }
} else {
m_skirt.clear();
}
// Brims were generated inside out, reverse to print the outmost contour first.
m_skirt.reverse();
// Remember the outer edge of the last skirt line extruded as m_skirt_convex_hull. if (collect_skirt_hull)
for (Polygon &poly : offset(convex_hull, distance + 0.5f * float(scale_(spacing)), ClipperLib::jtRound, float(scale_(0.1)))) for (Polygon &poly : offset(hull, distance + 0.5f * float(scale_(spacing)), ClipperLib::jtRound, float(scale_(0.1))))
append(m_skirt_convex_hull, std::move(poly.points)); append(m_skirt_convex_hull, std::move(poly.points));
};
if (m_config.skirt_type == stPerObject) { m_skirt.clear();
// BBS m_skirt_groups.clear();
for (auto obj_cvx_hull : object_convex_hulls) {
double object_skirt_distance = float(scale_(m_config.skirt_distance.value - spacing/2.));
PrintObject* object = obj_cvx_hull.first;
object->m_skirt.clear();
extruded_length.assign(extruded_length.size(), 0.);
for (size_t i = m_config.skirt_loops.value, extruder_idx = 0; i > 0; -- i) {
object_skirt_distance += float(scale_(spacing));
Polygon loop;
{
// BBS. skirt_distance is defined as the gap between skirt and outer most brim, so no need to add max_brim_width
Polygons loops = offset(obj_cvx_hull.second, object_skirt_distance, ClipperLib::jtRound, float(scale_(0.1)));
Geometry::simplify_polygons(loops, scale_(0.05), &loops);
if (loops.empty())
break;
loop = loops.front();
}
// Extrude the skirt loop. if (m_config.skirt_type == stPerObject && m_config.print_sequence == PrintSequence::ByObject) {
ExtrusionLoop eloop(elrSkirt); for (const ObjectSkirtHull& object_hull : object_convex_hulls) {
eloop.paths.emplace_back(ExtrusionPath( object_hull.object->m_skirt.clear();
ExtrusionPath( append_skirt_loops_for_hull(object_hull.hull, object_hull.object->m_skirt, false);
erSkirt, object_hull.object->m_skirt.reverse();
(float)mm3_per_mm, // this will be overridden at G-code export time }
flow.width(), } else if (m_config.skirt_type == stCombined || m_config.skirt_type == stPerObject) {
(float)initial_layer_print_height // this will be overridden at G-code export time struct SkirtGroupItem {
))); Points occupied_points;
eloop.paths.back().polyline = Polyline3(loop.split_at_first_point()); bool emits_skirt;
object->m_skirt.append(std::move(eloop)); };
if (m_config.min_skirt_length.value > 0) {
// The skirt length is limited. Sum the total amount of filament length extruded, in mm.
extruded_length[extruder_idx] += unscale<double>(loop.length()) * extruders_e_per_mm[extruder_idx];
if (extruded_length[extruder_idx] < m_config.min_skirt_length.value) {
// Not extruded enough yet with the current extruder. Add another loop.
if (i == 1)
++ i;
} else {
assert(extruded_length[extruder_idx] >= m_config.min_skirt_length.value);
// Enough extruded with the current extruder. Extrude with the next one,
// until the prescribed number of skirt loops is extruded.
if (extruder_idx + 1 < extruders.size())
++ extruder_idx;
}
} else {
// The skirt lenght is not limited, extrude the skirt with the 1st extruder only.
}
// Orca: group items represent occupied first-layer areas. Object items emit skirts;
// obstacle-only items, such as wipe tower, only force nearby object groups to merge.
std::vector<SkirtGroupItem> group_items;
const coord_t grouping_offset = scale_(m_config.skirt_distance.value + m_config.skirt_loops.value * spacing);
for (const ObjectSkirtHull& object_hull : object_convex_hulls) {
PrintObject* object = object_hull.object;
Points occupied_points;
for (const PrintInstance &instance : object->instances()) {
Points copy_points = object_hull.hull.points;
for (Point &pt : copy_points)
pt += instance.shift;
append(occupied_points, copy_points);
}
auto append_brim_points = [&occupied_points](const ExPolygons& areas) {
for (const ExPolygon& area : areas)
append(occupied_points, area.contour.points);
};
if (auto it = m_objectBrimAreas.find(object->id()); it != m_objectBrimAreas.end())
append_brim_points(it->second);
if (auto it = m_supportBrimAreas.find(object->id()); it != m_supportBrimAreas.end())
append_brim_points(it->second);
if (occupied_points.size() < 3)
continue;
// Orca: include the object's brim/support-brim footprint before checking skirt collisions.
group_items.push_back({ std::move(occupied_points), true });
}
// Orca: the wipe tower contributes occupied area, but does not emit a skirt by itself.
Points wipe_tower_points = this->first_layer_wipe_tower_corners();
if (wipe_tower_points.size() >= 3)
group_items.push_back({ std::move(wipe_tower_points), false });
std::vector<size_t> parent(group_items.size());
std::iota(parent.begin(), parent.end(), 0);
// Orca: union-find keeps collision merging local without repeatedly rebuilding item lists.
auto find_parent = [&parent](size_t idx) {
while (parent[idx] != idx) {
parent[idx] = parent[parent[idx]];
idx = parent[idx];
}
return idx;
};
auto unite = [&parent, &find_parent](size_t a, size_t b) {
a = find_parent(a);
b = find_parent(b);
if (a != b)
parent[b] = a;
};
// Orca: combined skirt is the same grouping model with all items forced into one group.
if (m_config.skirt_type == stCombined && !group_items.empty())
for (size_t i = 1; i < group_items.size(); ++i)
unite(0, i);
auto build_grouped_points = [&]() {
struct GroupData {
Points points;
bool emits_skirt = false;
};
std::map<size_t, GroupData> grouped;
for (size_t i = 0; i < group_items.size(); ++i) {
GroupData& group = grouped[find_parent(i)];
append(group.points, group_items[i].occupied_points);
group.emits_skirt = group.emits_skirt || group_items[i].emits_skirt;
}
return grouped;
};
bool groups_changed = m_config.skirt_type == stPerObject;
while (groups_changed) {
groups_changed = false;
auto grouped_points = build_grouped_points();
std::vector<std::pair<size_t, Polygon>> group_envelopes;
for (const auto& [root, group] : grouped_points) {
if (group.points.size() < 3)
continue;
// Orca: emitting groups are expanded to their final skirt reach; obstacle groups are not.
Polygon envelope = Geometry::convex_hull(group.points);
if (group.emits_skirt) {
// Orca: merge groups when a skirt envelope intersects another group or obstacle.
Polygons envelopes = offset(envelope, grouping_offset, ClipperLib::jtRound, float(scale_(0.1)));
if (envelopes.empty())
continue;
envelope = std::move(envelopes.front());
}
group_envelopes.emplace_back(root, std::move(envelope));
}
for (size_t i = 0; i < group_envelopes.size(); ++i) {
for (size_t j = i + 1; j < group_envelopes.size(); ++j) {
const size_t root_i = find_parent(group_envelopes[i].first);
const size_t root_j = find_parent(group_envelopes[j].first);
if (root_i != root_j && !intersection(group_envelopes[i].second, group_envelopes[j].second).empty()) {
unite(root_i, root_j);
groups_changed = true;
}
}
}
}
auto grouped_points = build_grouped_points();
for (auto& [_, group] : grouped_points) {
if (!group.emits_skirt || group.points.size() < 3)
continue;
// Orca: after merging, use the occupied outline directly; do not add skirt distance twice.
ExtrusionEntityCollection group_skirt;
append_skirt_loops_for_hull(Geometry::convex_hull(group.points), group_skirt, true);
if (!group_skirt.empty()) {
group_skirt.reverse();
// Orca: keep m_skirt as a flattened compatibility mirror for preview/extents.
m_skirt.append(group_skirt.entities);
m_skirt_groups.push_back(std::move(group_skirt));
} }
object->m_skirt.reverse();
} }
} }
} }
+5
View File
@@ -999,6 +999,7 @@ public:
PrintRegionPtrs& print_regions_mutable() { return m_print_regions; } PrintRegionPtrs& print_regions_mutable() { return m_print_regions; }
std::vector<size_t> layers_sorted_for_object(float start, float end, std::vector<LayerPtrs> &layers_of_objects, std::vector<BoundingBox> &boundingBox_for_objects, VecOfPoints& objects_instances_shift); std::vector<size_t> layers_sorted_for_object(float start, float end, std::vector<LayerPtrs> &layers_of_objects, std::vector<BoundingBox> &boundingBox_for_objects, VecOfPoints& objects_instances_shift);
const ExtrusionEntityCollection& skirt() const { return m_skirt; } const ExtrusionEntityCollection& skirt() const { return m_skirt; }
const std::vector<ExtrusionEntityCollection>& skirt_groups() const { return m_skirt_groups; }
// Convex hull of the 1st layer extrusions, for bed leveling and placing the initial purge line. // Convex hull of the 1st layer extrusions, for bed leveling and placing the initial purge line.
// It encompasses the object extrusions, support extrusions, skirt, brim, wipe tower. // It encompasses the object extrusions, support extrusions, skirt, brim, wipe tower.
// It does NOT encompass user extrusions generated by custom G-code, // It does NOT encompass user extrusions generated by custom G-code,
@@ -1172,9 +1173,13 @@ private:
// Ordered collections of extrusion paths to build skirt loops and brim. // Ordered collections of extrusion paths to build skirt loops and brim.
ExtrusionEntityCollection m_skirt; ExtrusionEntityCollection m_skirt;
std::vector<ExtrusionEntityCollection> m_skirt_groups;
// BBS: collecting extrusion paths to build brim by objs // BBS: collecting extrusion paths to build brim by objs
std::map<ObjectID, ExtrusionEntityCollection> m_brimMap; std::map<ObjectID, ExtrusionEntityCollection> m_brimMap;
std::map<ObjectID, ExtrusionEntityCollection> m_supportBrimMap; std::map<ObjectID, ExtrusionEntityCollection> m_supportBrimMap;
// Orca: cached occupied brim footprints used when grouping per-object skirts.
std::map<ObjectID, ExPolygons> m_objectBrimAreas;
std::map<ObjectID, ExPolygons> m_supportBrimAreas;
// Convex hull of the 1st layer extrusions. // Convex hull of the 1st layer extrusions.
// It encompasses the object extrusions, support extrusions, skirt, brim, wipe tower. // It encompasses the object extrusions, support extrusions, skirt, brim, wipe tower.
// It does NOT encompass user extrusions generated by custom G-code, // It does NOT encompass user extrusions generated by custom G-code,
+2 -2
View File
@@ -3397,7 +3397,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("initial_layer_print_height", coFloat); def = this->add("initial_layer_print_height", coFloat);
def->label = L("First layer height"); def->label = L("First layer height");
def->category = L("Quality"); def->category = L("Quality");
def->tooltip = L("This is the height of the first layer. Making the first layer height thicker can improve build plate adhesion."); def->tooltip = L("Height of the first layer. Making the first layer height thicker can improve build plate adhesion.");
def->sidetext = L("mm"); // millimeters, CIS languages need translation def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->min = 0; def->min = 0;
def->set_default_value(new ConfigOptionFloat(0.2)); def->set_default_value(new ConfigOptionFloat(0.2));
@@ -3437,7 +3437,7 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionFloatOrPercent(100, true)); def->set_default_value(new ConfigOptionFloatOrPercent(100, true));
def = this->add("slow_down_layers", coInt); def = this->add("slow_down_layers", coInt);
def->label = L("This is the number of top interface layers."); def->label = L("Number of slow layers");
def->tooltip = L("The first few layers are printed slower than normal. " def->tooltip = L("The first few layers are printed slower than normal. "
"The speed is gradually increased in a linear fashion over the specified number of layers."); "The speed is gradually increased in a linear fashion over the specified number of layers.");
def->category = L("Speed"); def->category = L("Speed");
+1 -2
View File
@@ -24,8 +24,7 @@
// Enable rendering of objects using environment map // Enable rendering of objects using environment map
#define ENABLE_ENVIRONMENT_MAP 0 #define ENABLE_ENVIRONMENT_MAP 0
// Enable smoothing of objects normals
#define ENABLE_SMOOTH_NORMALS 0
// Enable rendering markers for options in preview as fixed screen size points // Enable rendering markers for options in preview as fixed screen size points
#define ENABLE_FIXED_SCREEN_SIZE_POINT_MARKERS 1 #define ENABLE_FIXED_SCREEN_SIZE_POINT_MARKERS 1
-8
View File
@@ -759,12 +759,8 @@ int GLVolumeCollection::load_object_volume(
v.set_color(color_from_model_volume(*model_volume)); v.set_color(color_from_model_volume(*model_volume));
v.name = model_volume->name; v.name = model_volume->name;
#if ENABLE_SMOOTH_NORMALS
v.model.init_from(mesh, true);
#else
v.model.init_from(*mesh); v.model.init_from(*mesh);
if (need_raycaster) { v.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(mesh); } if (need_raycaster) { v.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(mesh); }
#endif // ENABLE_SMOOTH_NORMALS
v.composite_id = GLVolume::CompositeID(obj_idx, volume_idx, instance_idx); v.composite_id = GLVolume::CompositeID(obj_idx, volume_idx, instance_idx);
if (model_volume->is_model_part()) if (model_volume->is_model_part())
@@ -815,13 +811,9 @@ void GLVolumeCollection::load_object_auxiliary(
const ModelInstance& model_instance = *print_object->model_object()->instances[instance_idx.first]; const ModelInstance& model_instance = *print_object->model_object()->instances[instance_idx.first];
this->volumes.emplace_back(new GLVolume((milestone == slaposPad) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR)); this->volumes.emplace_back(new GLVolume((milestone == slaposPad) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR));
GLVolume& v = *this->volumes.back(); GLVolume& v = *this->volumes.back();
#if ENABLE_SMOOTH_NORMALS
v.model.init_from(mesh, true);
#else
v.model.init_from(mesh); v.model.init_from(mesh);
v.model.set_color((milestone == slaposPad) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR); v.model.set_color((milestone == slaposPad) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR);
v.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<const TriangleMesh>(mesh)); v.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<const TriangleMesh>(mesh));
#endif // ENABLE_SMOOTH_NORMALS
v.composite_id = GLVolume::CompositeID(obj_idx, -int(milestone), (int)instance_idx.first); v.composite_id = GLVolume::CompositeID(obj_idx, -int(milestone), (int)instance_idx.first);
v.geometry_id = std::pair<size_t, size_t>(timestamp, model_instance.id().id); v.geometry_id = std::pair<size_t, size_t>(timestamp, model_instance.id().id);
// Create a copy of the convex hull mesh for each instance. Use a move operator on the last instance. // Create a copy of the convex hull mesh for each instance. Use a move operator on the last instance.
+40
View File
@@ -9,13 +9,29 @@
#include "libslic3r/MaterialType.hpp" #include "libslic3r/MaterialType.hpp"
#include "MsgDialog.hpp" #include "MsgDialog.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "libslic3r/GCode/AdaptivePAProcessor.hpp"
#include "Plater.hpp" #include "Plater.hpp"
#include <sstream>
#include <wx/msgdlg.h> #include <wx/msgdlg.h>
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
namespace {
std::string trim_copy(const std::string& text)
{
const auto first = text.find_first_not_of(" \t\r\n");
if (first == std::string::npos)
return {};
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
} // namespace
void ConfigManipulation::apply(DynamicPrintConfig* config, DynamicPrintConfig* new_config) void ConfigManipulation::apply(DynamicPrintConfig* config, DynamicPrintConfig* new_config)
{ {
bool modified = false; bool modified = false;
@@ -141,6 +157,30 @@ void ConfigManipulation::check_nozzle_temperature_initial_layer_range(DynamicPri
} }
} }
void ConfigManipulation::check_adaptive_pressure_advance_model(DynamicPrintConfig* config)
{
if (is_msg_dlg_already_exist || !config->has("adaptive_pressure_advance_model"))
return;
const auto* model = config->option<ConfigOptionStrings>("adaptive_pressure_advance_model");
if (model == nullptr || model->values.empty())
return;
std::string raw_model;
for (const std::string& chunk : model->values)
raw_model += chunk;
std::string error = AdaptivePAProcessor::validate_adaptive_pa_model(raw_model);
if (!error.empty()) {
wxString msg_text = _L("Adaptive Pressure Advance model validation failed:\n");
msg_text += from_u8(error);
MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK);
is_msg_dlg_already_exist = true;
dialog.ShowModal();
is_msg_dlg_already_exist = false;
}
}
void ConfigManipulation::check_filament_max_volumetric_speed(DynamicPrintConfig *config) void ConfigManipulation::check_filament_max_volumetric_speed(DynamicPrintConfig *config)
{ {
+1
View File
@@ -77,6 +77,7 @@ public:
void check_nozzle_recommended_temperature_range(DynamicPrintConfig *config); void check_nozzle_recommended_temperature_range(DynamicPrintConfig *config);
void check_nozzle_temperature_range(DynamicPrintConfig* config); void check_nozzle_temperature_range(DynamicPrintConfig* config);
void check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config); void check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config);
void check_adaptive_pressure_advance_model(DynamicPrintConfig* config);
void check_filament_max_volumetric_speed(DynamicPrintConfig *config); void check_filament_max_volumetric_speed(DynamicPrintConfig *config);
void check_chamber_temperature(DynamicPrintConfig* config); void check_chamber_temperature(DynamicPrintConfig* config);
void set_is_BBL_Printer(bool is_bbl_printer) { is_BBL_Printer = is_bbl_printer; }; void set_is_BBL_Printer(bool is_bbl_printer) { is_BBL_Printer = is_bbl_printer; };
-12
View File
@@ -2793,22 +2793,14 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
TriangleMesh mesh = print_object->get_mesh(slaposDrillHoles); TriangleMesh mesh = print_object->get_mesh(slaposDrillHoles);
assert(! mesh.empty()); assert(! mesh.empty());
mesh.transform(sla_print->sla_trafo(*m_model->objects[volume.object_idx()]).inverse()); mesh.transform(sla_print->sla_trafo(*m_model->objects[volume.object_idx()]).inverse());
#if ENABLE_SMOOTH_NORMALS
volume.model.init_from(mesh, true);
#else
volume.model.init_from(mesh); volume.model.init_from(mesh);
volume.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<TriangleMesh>(mesh)); volume.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<TriangleMesh>(mesh));
#endif // ENABLE_SMOOTH_NORMALS
} }
else { else {
// Reload the original volume. // Reload the original volume.
#if ENABLE_SMOOTH_NORMALS
volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true);
#else
const TriangleMesh& new_mesh = m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(); const TriangleMesh& new_mesh = m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh();
volume.model.init_from(new_mesh); volume.model.init_from(new_mesh);
volume.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<TriangleMesh>(new_mesh)); volume.mesh_raycaster = std::make_unique<GUI::MeshRaycaster>(std::make_shared<TriangleMesh>(new_mesh));
#endif // ENABLE_SMOOTH_NORMALS
} }
} }
//FIXME it is an ugly hack to write the timestamp into the "offsets" field to not have to add another member variable //FIXME it is an ugly hack to write the timestamp into the "offsets" field to not have to add another member variable
@@ -10009,11 +10001,7 @@ void GLCanvas3D::_load_sla_shells()
const TriangleMesh& mesh, const ColorRGBA& color, bool outside_printer_detection_enabled) { const TriangleMesh& mesh, const ColorRGBA& color, bool outside_printer_detection_enabled) {
m_volumes.volumes.emplace_back(new GLVolume(color)); m_volumes.volumes.emplace_back(new GLVolume(color));
GLVolume& v = *m_volumes.volumes.back(); GLVolume& v = *m_volumes.volumes.back();
#if ENABLE_SMOOTH_NORMALS
v.model.init_from(mesh, true);
#else
v.model.init_from(mesh); v.model.init_from(mesh);
#endif // ENABLE_SMOOTH_NORMALS
v.shader_outside_printer_detection_enabled = outside_printer_detection_enabled; v.shader_outside_printer_detection_enabled = outside_printer_detection_enabled;
v.composite_id.volume_id = volume_id; v.composite_id.volume_id = volume_id;
v.set_instance_offset(unscale(instance.shift.x(), instance.shift.y(), 0.0)); v.set_instance_offset(unscale(instance.shift.x(), instance.shift.y(), 0.0));
+40 -15
View File
@@ -14,18 +14,20 @@
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#if ENABLE_SMOOTH_NORMALS #if defined(L)
#undef L
#endif
#include <igl/per_face_normals.h> #include <igl/per_face_normals.h>
#include <igl/per_corner_normals.h> #include <igl/per_corner_normals.h>
#include <igl/per_vertex_normals.h> #include <igl/per_vertex_normals.h>
#endif // ENABLE_SMOOTH_NORMALS
#include <glad/gl.h> #include <glad/gl.h>
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
#if ENABLE_SMOOTH_NORMALS
static void smooth_normals_corner(const TriangleMesh& mesh, std::vector<stl_normal>& normals) static void smooth_normals_corner(const TriangleMesh& mesh, std::vector<stl_normal>& normals)
{ {
using MapMatrixXfUnaligned = Eigen::Map<const Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor | Eigen::DontAlign>>; using MapMatrixXfUnaligned = Eigen::Map<const Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor | Eigen::DontAlign>>;
@@ -41,7 +43,7 @@ static void smooth_normals_corner(const TriangleMesh& mesh, std::vector<stl_norm
Eigen::Index(face_normals.size()), 3).cast<double>(); Eigen::Index(face_normals.size()), 3).cast<double>();
Eigen::MatrixXd out_normals; Eigen::MatrixXd out_normals;
igl::per_corner_normals(vertices, indices, in_normals, 1.0, out_normals); igl::per_corner_normals(vertices, indices, 1.0, out_normals);
normals = std::vector<stl_normal>(mesh.its.vertices.size()); normals = std::vector<stl_normal>(mesh.its.vertices.size());
for (size_t i = 0; i < mesh.its.indices.size(); ++i) { for (size_t i = 0; i < mesh.its.indices.size(); ++i) {
@@ -50,7 +52,6 @@ static void smooth_normals_corner(const TriangleMesh& mesh, std::vector<stl_norm
} }
} }
} }
#endif // ENABLE_SMOOTH_NORMALS
void GLModel::Geometry::add_vertex(const Vec2f& position) void GLModel::Geometry::add_vertex(const Vec2f& position)
{ {
@@ -455,17 +456,41 @@ void GLModel::init_from(const indexed_triangle_set& its)
data.reserve_vertices(3 * its.indices.size()); data.reserve_vertices(3 * its.indices.size());
data.reserve_indices(3 * its.indices.size()); data.reserve_indices(3 * its.indices.size());
// vertices + indices // Read user preference: smooth normals enabled
unsigned int vertices_counter = 0; const bool realistic_mode = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE);
for (uint32_t i = 0; i < its.indices.size(); ++i) { const bool smooth_normals_enabled = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS);
const stl_triangle_vertex_indices face = its.indices[i];
const stl_vertex vertex[3] = { its.vertices[face[0]], its.vertices[face[1]], its.vertices[face[2]] }; if (realistic_mode && smooth_normals_enabled) {
const stl_vertex n = face_normal_normalized(vertex); // Use per-corner smooth normals (via IGL)
for (size_t j = 0; j < 3; ++j) { using MapMatrixXfUnaligned = Eigen::Map<const Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor | Eigen::DontAlign>>;
data.add_vertex(vertex[j], n); using MapMatrixXiUnaligned = Eigen::Map<const Eigen::Matrix<int, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor | Eigen::DontAlign>>;
Eigen::MatrixXd vertices = MapMatrixXfUnaligned(its.vertices.front().data(), Eigen::Index(its.vertices.size()), 3).cast<double>();
Eigen::MatrixXi indices = MapMatrixXiUnaligned(its.indices.front().data(), Eigen::Index(its.indices.size()), 3);
Eigen::MatrixXd corner_normals;
igl::per_corner_normals(vertices, indices, 5.0, corner_normals);
unsigned int vertices_counter = 0;
for (uint32_t i = 0; i < its.indices.size(); ++i) {
const stl_triangle_vertex_indices face = its.indices[i];
for (size_t j = 0; j < 3; ++j) {
const Vec3f normal = corner_normals.row(Eigen::Index(i * 3 + j)).cast<float>();
data.add_vertex(its.vertices[face[j]], normal);
}
data.add_triangle(vertices_counter, vertices_counter + 1, vertices_counter + 2);
vertices_counter += 3;
}
} else {
//Original flat (per-face) normals
unsigned int vertices_counter = 0;
for (uint32_t i = 0; i < its.indices.size(); ++i) {
const stl_triangle_vertex_indices face = its.indices[i];
const stl_vertex vertex[3] = {its.vertices[face[0]], its.vertices[face[1]], its.vertices[face[2]]};
const stl_vertex n = face_normal_normalized(vertex);
for (size_t j = 0; j < 3; ++j)
data.add_vertex(vertex[j], n);
data.add_triangle(vertices_counter, vertices_counter + 1, vertices_counter + 2);
vertices_counter += 3;
} }
vertices_counter += 3;
data.add_triangle(vertices_counter - 3, vertices_counter - 2, vertices_counter - 1);
} }
// update bounding box // update bounding box
+5 -2
View File
@@ -6241,8 +6241,11 @@ bool GUI_App::maybe_migrate_user_presets_on_login()
bool GUI_App::check_preset_parent_available(const std::pair<std::string, std::map<std::string, std::string>>& preset_data) bool GUI_App::check_preset_parent_available(const std::pair<std::string, std::map<std::string, std::string>>& preset_data)
{ {
std::string inherits_name = preset_data.second.at(BBL_JSON_KEY_INHERITS); auto it = preset_data.second.find(BBL_JSON_KEY_INHERITS);
// // If contains "fdm_", "@System", and "@base", is a common base template that doesn't need to be installed if (it == preset_data.second.end() || it->second.empty())
return true;
const std::string& inherits_name = it->second;
// If contains "fdm_", "@System", and "@base", is a common base template that doesn't need to be installed
if (inherits_name.find("fdm_") != std::string::npos || inherits_name.find("@System") != std::string::npos || inherits_name.find("@base") != std::string::npos) if (inherits_name.find("fdm_") != std::string::npos || inherits_name.find("@System") != std::string::npos || inherits_name.find("@base") != std::string::npos)
return true; return true;
+1 -1
View File
@@ -53,7 +53,7 @@ namespace GUI {
//------------------------------------------ //------------------------------------------
PhysicalPrinterDialog::PhysicalPrinterDialog(wxWindow* parent) : PhysicalPrinterDialog::PhysicalPrinterDialog(wxWindow* parent) :
DPIDialog(parent, wxID_ANY, _L("Printer"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), -1), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) DPIDialog(parent, wxID_ANY, _L("Physical Printer"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), -1), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{ {
SetFont(wxGetApp().normal_font()); SetFont(wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE); SetBackgroundColour(*wxWHITE);
+9
View File
@@ -1578,6 +1578,15 @@ void PreferencesDialog::create_items()
); );
g_sizer->Add(item_realistic_shadows); g_sizer->Add(item_realistic_shadows);
auto item_realistic_smooth_normals = create_item_checkbox(
_L("Smooth normals"),
_L("Applies smooth normals to the realistic view.\n\nRequires manual scene reload to take effect "
"(right-click on 3D view → \"Reload All\")."),
SETTING_OPENGL_PHONG_SMOOTH_NORMALS
);
g_sizer->Add(item_realistic_smooth_normals);
//// GRAPHICS > Anti-aliasing //// GRAPHICS > Anti-aliasing
g_sizer->Add(create_item_title(_L("Anti-aliasing")), 1, wxEXPAND); g_sizer->Add(create_item_title(_L("Anti-aliasing")), 1, wxEXPAND);
+9
View File
@@ -3954,6 +3954,15 @@ void TabFilament::build()
option.opt.is_code = true; option.opt.is_code = true;
option.opt.height = 15; option.opt.height = 15;
optgroup->append_single_option_line(option); optgroup->append_single_option_line(option);
optgroup->m_on_change = [this](const t_config_option_key& opt_key, const boost::any& value) {
DynamicPrintConfig& filament_config = m_preset_bundle->filaments.get_edited_preset().config;
update_dirty();
if (opt_key == "adaptive_pressure_advance_model")
m_config_manipulation.check_adaptive_pressure_advance_model(&filament_config);
on_value_change(opt_key, value);
};
// //
optgroup = page->new_optgroup(L("Print chamber temperature"), L"param_chamber_temp"); optgroup = page->new_optgroup(L("Print chamber temperature"), L"param_chamber_temp");
+5 -1
View File
@@ -1,8 +1,12 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests_main.cpp ${_TEST_NAME}_tests.cpp
test_geometry.cpp
test_nfp.cpp
test_nfp_placer.cpp
printer_parts.cpp printer_parts.cpp
printer_parts.hpp printer_parts.hpp
libnest2d_test_utils.hpp
) )
target_link_libraries(${_TEST_NAME}_tests test_common libnest2d Catch2::Catch2WithMain) target_link_libraries(${_TEST_NAME}_tests test_common libnest2d Catch2::Catch2WithMain)
+46
View File
@@ -0,0 +1,46 @@
#pragma once
// Shared setup for the libnest2d test suite.
//
// The no-fit-polygon numeric backend specialised below changes how NFP is
// computed for the whole program, so every translation unit that instantiates
// NFP (the geometry, nfp and placer tests) must see the same definition. Keep
// it here and include this header from every libnest2d test file.
#include <cstdint>
#include <libnest2d/libnest2d.hpp>
#include <libnest2d/utils/rotcalipers.hpp>
#if defined(_MSC_VER) && defined(__clang__)
#define BOOST_NO_CXX17_HDR_STRING_VIEW
#endif
#include "boost/multiprecision/integer.hpp"
#include "boost/rational.hpp"
namespace libnest2d {
#if !defined(_MSC_VER) && defined(__SIZEOF_INT128__) && !defined(__APPLE__)
using LargeInt = __int128;
#else
using LargeInt = boost::multiprecision::int128_t;
template<> struct _NumTag<LargeInt> { using Type = ScalarTag; };
#endif
template<class T> struct _NumTag<boost::rational<T>> { using Type = RationalTag; };
using RectangleItem = libnest2d::Rectangle;
namespace nfp {
// Use exact rational arithmetic for the convex NFP so the tests are not at the
// mercy of floating-point rounding.
template<class S>
struct NfpImpl<S, NfpLevel::CONVEX_ONLY> {
NfpResult<S> operator()(const S &sh, const S &other) {
return nfpConvexOnly<S, boost::rational<LargeInt>>(sh, other);
}
};
} // namespace nfp
} // namespace libnest2d
+49
View File
@@ -0,0 +1,49 @@
#include <catch2/catch_all.hpp>
#include "libnest2d_test_utils.hpp"
using namespace libnest2d;
// Basic behaviour of the Item type and the high-level nest() entry point:
// items copy independently, and nest() leaves degenerate or oversized items
// untouched.
TEST_CASE("Item construction and copy", "[Nesting]") {
Item sh = { {0, 0}, {1, 0}, {1, 1}, {0, 1} };
REQUIRE(sh.vertexCount() == 4u);
Item sh2({ {0, 0}, {1, 0}, {1, 1}, {0, 1} });
REQUIRE(sh2.vertexCount() == 4u);
Item sh3 = sh2; // copy
REQUIRE(sh3.vertexCount() == 4u);
sh2 = {}; // clearing the original leaves the copy intact
REQUIRE(sh2.vertexCount() == 0u);
REQUIRE(sh3.vertexCount() == 4u);
}
TEST_CASE("nest() leaves an empty or zero-area item untouched", "[Nesting]") {
auto bin = Box(250000000, 210000000);
std::vector<Item> items;
items.emplace_back(Item{}); // empty item
items.emplace_back(Item{ {0, 200} }); // zero-area item
size_t bins = nest(items, bin);
REQUIRE(bins == 0u);
for (const auto &itm : items) REQUIRE(itm.binId() == BIN_ID_UNSET);
}
TEST_CASE("nest() leaves an item larger than the bin untouched", "[Nesting]") {
auto bin = Box(250000000, 210000000);
std::vector<Item> items;
items.emplace_back(RectangleItem{250000001, 210000001}); // larger than the bin
size_t bins = nest(items, bin);
REQUIRE(bins == 0u);
REQUIRE(items.front().binId() == BIN_ID_UNSET);
}
File diff suppressed because it is too large Load Diff
+190
View File
@@ -0,0 +1,190 @@
#include <catch2/catch_all.hpp>
#include "libnest2d_test_utils.hpp"
#include "printer_parts.hpp"
using namespace libnest2d;
namespace {
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
// Geometry values round-trip through floating point, so compare with a small
// tolerance that works both near and away from zero.
void require_close(double value, double expected) {
REQUIRE_THAT(value, WithinRel(expected, 1e-9) || WithinAbs(expected, 1e-9));
}
// The printer parts as nestable items, computed once.
const std::vector<Item> &prusa_parts() {
static const std::vector<Item> parts = [] {
std::vector<Item> ret;
ret.reserve(PRINTER_PART_POLYGONS.size());
for (auto &inp : PRINTER_PART_POLYGONS) {
auto inp_cpy = inp;
if (ClosureTypeV<PathImpl> == Closure::OPEN)
inp_cpy.points.pop_back();
if constexpr (!is_clockwise<PathImpl>())
std::reverse(inp_cpy.begin(), inp_cpy.end());
ret.emplace_back(inp_cpy);
}
return ret;
}();
return parts;
}
} // namespace
TEST_CASE("Degree and radian conversion round-trips", "[Geometry]") {
Degrees deg(180);
Radians rad(deg);
require_close(rad, Pi);
require_close(deg, 180);
require_close(Degrees(rad), 180);
require_close(rad, Radians(deg));
require_close(Degrees(rad), deg);
REQUIRE(rad == deg);
}
TEST_CASE("Segment angle to the X axis", "[Geometry]") {
auto quadrant = [](Point to) { return Degrees(Segment({0, 0}, to).angleToXaxis()); };
REQUIRE(quadrant({12, -10}) > 270); REQUIRE(quadrant({12, -10}) < 360);
REQUIRE(quadrant({12, 10}) > 0); REQUIRE(quadrant({12, 10}) < 90);
REQUIRE(quadrant({-12, 10}) > 90); REQUIRE(quadrant({-12, 10}) < 180);
REQUIRE(quadrant({-12, -10}) > 180); REQUIRE(quadrant({-12, -10}) < 270);
require_close(quadrant({1, 0}), 0);
require_close(quadrant({0, 1}), 90);
require_close(quadrant({-1, 0}), 180);
require_close(quadrant({0, -1}), 270);
}
TEST_CASE("Point to segment distance", "[Geometry]") {
Point p2 = {10, 0};
Segment seg({0, 0}, {10, 10});
auto check = [](TCompute<Coord> val, TCompute<Coord> expected) {
if (std::is_floating_point<TCompute<Coord>>::value)
require_close(double(val), double(expected));
else
REQUIRE(val == expected);
};
auto h = pointlike::horizontalDistance(p2, seg);
REQUIRE(h.second);
check(h.first, 10);
auto v = pointlike::verticalDistance(p2, seg);
REQUIRE(v.second);
check(v.first, -10);
v = pointlike::verticalDistance(Point{10, 20}, seg);
REQUIRE(v.second);
check(v.first, 10);
Point p4 = {80, 0};
Segment seg2({0, 0}, {0, 40});
h = pointlike::horizontalDistance(p4, seg2);
REQUIRE(h.second);
check(h.first, 80);
v = pointlike::verticalDistance(p4, seg2);
REQUIRE_FALSE(v.second); // the point does not project onto the segment
}
TEST_CASE("Item area", "[Geometry]") {
require_close(RectangleItem(10, 10).area(), 100);
require_close(RectangleItem(100, 100).area(), 10000);
Item item = {
{61, 97}, {70, 151}, {176, 151}, {189, 138},
{189, 59}, {70, 59}, {61, 77}, {61, 97}
};
REQUIRE(std::abs(shapelike::area(item.transformedShape())) > 0);
}
TEST_CASE("Point inside polygon", "[Geometry]") {
RectangleItem rect(10, 10);
REQUIRE(rect.isInside(Point{1, 1}));
REQUIRE(rect.isInside(Point{3, 3}));
REQUIRE_FALSE(rect.isInside(Point{11, 11}));
REQUIRE_FALSE(rect.isInside(Point{11, 12}));
}
TEST_CASE("Bounding circle of the printer parts", "[Geometry]") {
PolygonImpl p = {{{0, 10}, {10, 0}, {0, -10}, {0, 10}}, {}};
Circle c = placers::boundingCircle(p);
require_close(getX(c.center()), 0);
require_close(getY(c.center()), 0);
require_close(c.radius(), 10);
shapelike::translate(p, PointImpl{10, 10});
c = placers::boundingCircle(p);
require_close(getX(c.center()), 10);
require_close(getY(c.center()), 10);
require_close(c.radius(), 10);
for (auto &part : prusa_parts()) {
c = placers::boundingCircle(part.transformedShape());
REQUIRE_FALSE(std::isnan(c.radius()));
for (auto v : shapelike::contour(part.transformedShape())) {
auto d = pointlike::distance(v, c.center());
if (d > c.radius())
REQUIRE(std::abs(1.0 - d / c.radius()) <= 1e-3); // on the circle
}
}
}
TEST_CASE("Convex hull of a printer part", "[Geometry]") {
PathImpl poly = PRINTER_PART_POLYGONS[0];
auto chull = sl::convexHull(poly);
REQUIRE(chull.size() == poly.size()); // the part is already convex
}
namespace {
using Unit = int64_t;
using Ratio = boost::rational<boost::multiprecision::int128_t>;
// Reference minimum-area bounding box, found by brute force over every edge
// direction, to validate the rotating-calipers implementation.
long double ref_min_area_box(const PolygonImpl &p) {
long double min_area = std::numeric_limits<long double>::max();
auto update_min = [&](const Point &a, const Point &b) {
PolygonImpl rotated = p;
sl::rotate(rotated, -Segment(a, b).angleToXaxis());
min_area = std::min(min_area, cast<long double>(sl::area(sl::boundingBox(rotated))));
};
auto it = sl::cbegin(p), itx = std::next(it);
while (itx != sl::cend(p)) { update_min(*it, *itx); ++it; ++itx; }
update_min(*std::prev(sl::cend(p)), *sl::cbegin(p));
return min_area;
}
} // namespace
TEST_CASE("Minimum-area bounding box via rotating calipers", "[Geometry]") {
const long double tolerance = 500e6l;
for (const PathImpl &part : PRINTER_PART_POLYGONS) {
auto area = cast<long double>(minAreaBoundingBox<PathImpl, Unit, Ratio>(part).area());
REQUIRE(std::abs(ref_min_area_box(PolygonImpl(part)) - area) < tolerance);
}
for (PathImpl part : STEGOSAUR_POLYGONS) {
std::reverse(part.begin(), part.end());
PolygonImpl poly(removeCollinearPoints<PathImpl, PointImpl, Unit>(part, 1000000));
auto area = cast<long double>(minAreaBoundingBox<PolygonImpl, Unit, Ratio>(poly).area());
REQUIRE(std::abs(ref_min_area_box(poly) - area) < tolerance);
}
}
+267
View File
@@ -0,0 +1,267 @@
#include <catch2/catch_all.hpp>
#include "libnest2d_test_utils.hpp"
using namespace libnest2d;
namespace {
struct ItemPair {
Item orbiter;
Item stationary;
};
std::vector<ItemPair> nfp_testdata = {
{
{
{80, 50},
{100, 70},
{120, 50}
},
{
{10, 10},
{10, 40},
{40, 40},
{40, 10}
}
},
{
{
{80, 50},
{60, 70},
{80, 90},
{120, 90},
{140, 70},
{120, 50}
},
{
{10, 10},
{10, 40},
{40, 40},
{40, 10}
}
},
{
{
{40, 10},
{30, 10},
{20, 20},
{20, 30},
{30, 40},
{40, 40},
{50, 30},
{50, 20}
},
{
{80, 0},
{80, 30},
{110, 30},
{110, 0}
}
},
{
{
{117, 107},
{118, 109},
{120, 112},
{122, 113},
{128, 113},
{130, 112},
{132, 109},
{133, 107},
{133, 103},
{132, 101},
{130, 98},
{128, 97},
{122, 97},
{120, 98},
{118, 101},
{117, 103}
},
{
{102, 116},
{111, 126},
{114, 126},
{144, 106},
{148, 100},
{148, 85},
{147, 84},
{102, 84}
}
},
{
{
{99, 122},
{108, 140},
{110, 142},
{139, 142},
{151, 122},
{151, 102},
{142, 70},
{139, 68},
{111, 68},
{108, 70},
{99, 102}
},
{
{107, 124},
{128, 125},
{133, 125},
{136, 124},
{140, 121},
{142, 119},
{143, 116},
{143, 109},
{141, 93},
{139, 89},
{136, 86},
{134, 85},
{108, 85},
{107, 86}
}
},
{
{
{91, 100},
{94, 144},
{117, 153},
{118, 153},
{159, 112},
{159, 110},
{156, 66},
{133, 57},
{132, 57},
{91, 98}
},
{
{101, 90},
{103, 98},
{107, 113},
{114, 125},
{115, 126},
{135, 126},
{136, 125},
{144, 114},
{149, 90},
{149, 89},
{148, 87},
{145, 84},
{105, 84},
{102, 87},
{101, 89}
}
}
};
// libnest2d's vertex order depends on the backend; normalise to clockwise.
Item reversed_if_ccw(Item it) {
if (!is_clockwise<PolygonImpl>()) {
auto raw = it.rawShape();
std::reverse(sl::begin(raw), sl::end(raw));
it = Item{raw};
}
return it;
}
// Sliding `orbiter` around `stationary` along their no-fit polygon must keep the
// two shapes touching at every NFP vertex, and `stationary` must lie inside the
// resulting inner-fit polygon.
template<nfp::NfpLevel lvl, Coord SCALE>
void check_nfp(const std::vector<ItemPair> &testdata) {
auto check_pair = [](Item orbiter, Item stationary) {
orbiter.translate({210 * SCALE, 0});
auto &&nfp = nfp::noFitPolygon<lvl>(stationary.rawShape(), orbiter.transformedShape());
placers::correctNfpPosition(nfp, stationary, orbiter);
REQUIRE(shapelike::isValid(nfp.first).first);
Item infp(nfp.first);
REQUIRE(stationary.isInside(infp));
auto vo = nfp::referenceVertex(orbiter.transformedShape());
for (auto v : infp) {
Item moved = orbiter;
moved.translate({getX(v) - getX(vo), getY(v) - getY(vo)});
REQUIRE(Item::touches(moved, stationary));
}
};
for (const ItemPair &td : testdata) {
check_pair(reversed_if_ccw(td.orbiter), reversed_if_ccw(td.stationary));
check_pair(reversed_if_ccw(td.stationary), reversed_if_ccw(td.orbiter));
}
}
} // namespace
TEST_CASE("No-fit polygon of convex shapes keeps the items touching", "[Geometry][NFP]") {
check_nfp<nfp::NfpLevel::CONVEX_ONLY, 1>(nfp_testdata);
}
TEST_CASE("BottomLeftPlacer left and down polygons", "[Geometry][NFP]") {
Box bin(100, 100);
BottomLeftPlacer placer(bin);
PathImpl pitem = {{70, 75}, {88, 60}, {65, 50}, {60, 30}, {80, 20},
{42, 20}, {35, 35}, {35, 55}, {40, 75}};
PathImpl left_control = {{40, 75}, {35, 55}, {35, 35}, {42, 20}, {0, 20}, {0, 75}};
PathImpl down_control = {{88, 60}, {88, 0}, {35, 0}, {35, 35},
{42, 20}, {80, 20}, {60, 30}, {65, 50}};
if constexpr (!is_clockwise<PathImpl>()) {
std::reverse(sl::begin(pitem), sl::end(pitem));
std::reverse(sl::begin(left_control), sl::end(left_control));
std::reverse(sl::begin(down_control), sl::end(down_control));
}
if constexpr (ClosureTypeV<PathImpl> == Closure::CLOSED) {
sl::addVertex(pitem, sl::front(pitem));
sl::addVertex(left_control, sl::front(left_control));
sl::addVertex(down_control, sl::front(down_control));
}
auto require_same_vertices = [](const Item &got, const Item &expected) {
REQUIRE(shapelike::isValid(got.rawShape()).first);
REQUIRE(got.vertexCount() == expected.vertexCount());
for (unsigned long i = 0; i < expected.vertexCount(); ++i) {
REQUIRE(getX(got.vertex(i)) == getX(expected.vertex(i)));
REQUIRE(getY(got.vertex(i)) == getY(expected.vertex(i)));
}
};
Item item{pitem};
require_same_vertices(Item(placer.leftPoly(item)), Item{left_control});
require_same_vertices(Item(placer.downPoly(item)), Item{down_control});
}
TEST_CASE("EdgeCache maps a parameter to a contour point", "[Geometry][NFP]") {
RectangleItem input(10, 10);
placers::EdgeCache<PolygonImpl> ecache(input);
auto first = *input.begin();
REQUIRE(getX(first) == getX(ecache.coords(0)));
REQUIRE(getY(first) == getY(ecache.coords(0)));
auto last = *std::prev(input.end());
REQUIRE(getX(last) == getX(ecache.coords(1.0)));
REQUIRE(getY(last) == getY(ecache.coords(1.0)));
for (int i = 0; i <= 100; ++i)
REQUIRE(shapelike::touches(ecache.coords(i * 0.01), input.transformedShape()));
}
TEST_CASE("Merging a pile with a polygon", "[Geometry][NFP]") {
RectangleItem rect1(10, 15), rect2(15, 15), rect3(20, 15);
rect2.translate({10, 0});
rect3.translate({25, 0});
TMultiShape<PolygonImpl> pile;
pile.push_back(rect1.transformedShape());
pile.push_back(rect2.transformedShape());
auto result = nfp::merge(pile, rect3.transformedShape());
REQUIRE(result.size() == 1); // the three abutting rectangles merge into one
RectangleItem ref(45, 15);
REQUIRE_THAT(shapelike::area(result.front()),
Catch::Matchers::WithinRel(ref.area(), 1e-9));
}
+140
View File
@@ -0,0 +1,140 @@
#include <catch2/catch_all.hpp>
#include "libnest2d_test_utils.hpp"
using namespace libnest2d;
// NfpPlacer is the No-Fit-Polygon placement engine that Orca's arranger drives
// (via _Nester/FirstFitSelection in Arrange.cpp). These exercise the placer
// directly: pack()/accept() are the core geometric placement primitives.
namespace {
struct NfpPlacerFixture {
using Cfg = NfpPlacer::Config;
Box bin{250000000, 210000000}; // 250 x 210 mm bed at 1e6 scale
NfpPlacer placer_with(Cfg cfg = {}) const {
cfg.parallel = false; // deterministic, single-threaded for tests
NfpPlacer p{bin};
p.configure(cfg);
return p;
}
// pack + accept; returns whether the item was placed.
static bool place(NfpPlacer &p, Item &item) {
auto res = p.pack(item);
if (res) p.accept(res);
return bool(res);
}
// Place every item and REQUIRE each one is packed.
static void place_all(NfpPlacer &p, std::vector<RectangleItem> &items) {
for (size_t i = 0; i < items.size(); ++i) {
INFO("packing item " << i);
REQUIRE(place(p, items[i]));
}
}
// No two items overlap (a shared edge is allowed) and each stays in the bin.
void require_disjoint_in_bin(std::vector<RectangleItem> &items) const {
for (size_t i = 0; i < items.size(); ++i) {
REQUIRE(sl::isInside(items[i].boundingBox(), bin));
for (size_t j = i + 1; j < items.size(); ++j) {
const bool overlaps = Item::intersects(items[i], items[j]) &&
!Item::touches(items[i], items[j]);
INFO("items " << i << " and " << j);
REQUIRE_FALSE(overlaps);
}
}
}
static std::vector<RectangleItem> squares(size_t n, Coord side) {
return std::vector<RectangleItem>(n, RectangleItem{side, side});
}
};
} // namespace
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer places a single item inside the bin", "[Nesting][Placer]") {
NfpPlacer placer = placer_with();
RectangleItem item{100000000, 100000000};
REQUIRE(place(placer, item));
REQUIRE(placer.getItems().size() == 1u);
REQUIRE(sl::isInside(item.boundingBox(), bin));
}
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer rejects an item larger than the bin", "[Nesting][Placer]") {
NfpPlacer placer = placer_with();
RectangleItem big{300000000, 300000000}; // wider and taller than the bin
auto res = placer.pack(big);
REQUIRE_FALSE(bool(res));
REQUIRE(placer.getItems().empty());
}
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer positions the first item for any starting point", "[Nesting][Placer]") {
// setInitialPosition() seeds the first item from the configured starting
// corner; pack() (without accept()) drives that switch for every value.
using A = Cfg::Alignment;
auto start = GENERATE(A::CENTER, A::BOTTOM_LEFT, A::BOTTOM_RIGHT,
A::TOP_LEFT, A::TOP_RIGHT, A::USER_DEFINED, A::DONT_ALIGN);
CAPTURE(int(start));
Cfg cfg;
cfg.starting_point = start;
cfg.best_object_pos = bin.center();
NfpPlacer placer = placer_with(cfg);
RectangleItem item{100000000, 100000000};
auto res = placer.pack(item);
REQUIRE(bool(res));
REQUIRE(sl::isInside(item.boundingBox(), bin));
}
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer packs many items without overlap", "[Nesting][Placer]") {
// Each item is placed against the no-fit polygon of the growing pile.
auto items = squares(GENERATE(2u, 6u, 9u), 60000000);
NfpPlacer placer = placer_with();
place_all(placer, items);
REQUIRE(placer.getItems().size() == items.size());
require_disjoint_in_bin(items);
}
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer evaluates the rotation candidates", "[Nesting][Placer]") {
Cfg cfg;
cfg.rotations = {0.0, Pi / 2.0}; // exercise the rotation search loop
NfpPlacer placer = placer_with(cfg);
std::vector<RectangleItem> rects = {
{180000000, 40000000}, {180000000, 40000000}, {180000000, 40000000}};
place_all(placer, rects);
require_disjoint_in_bin(rects);
}
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer's final alignment keeps the pile clear of a fixed obstacle", "[Nesting][Placer]") {
// A preloaded fixed item makes finalAlign's recentring keep the pile clear of
// it instead of dropping it straight onto the bin centre. Box{w,h} centres on
// the origin, so the obstacle sits there too; virtual keeps it in place.
RectangleItem obstacle{80000000, 80000000};
obstacle.translation({-40000000, -40000000}); // 80x80 mm centred in the bin (origin)
obstacle.markAsFixedInBin(0);
obstacle.is_virt_object = true;
auto items = squares(4, 30000000);
{
NfpPlacer placer = placer_with();
NfpPlacer::ItemGroup fixed;
fixed.emplace_back(obstacle);
placer.preload(fixed);
place_all(placer, items);
} // the placer's destructor runs finalAlign, translating the packed items
for (size_t i = 0; i < items.size(); ++i) {
INFO("item " << i);
const bool overlaps = Item::intersects(items[i], obstacle) &&
!Item::touches(items[i], obstacle);
REQUIRE_FALSE(overlaps);
}
}
+1
View File
@@ -6,6 +6,7 @@ add_executable(${_TEST_NAME}_tests
test_aabbindirect.cpp test_aabbindirect.cpp
test_appconfig.cpp test_appconfig.cpp
test_arachne_walls.cpp test_arachne_walls.cpp
test_arrange.cpp
test_bambu_networking.cpp test_bambu_networking.cpp
test_clipper_offset.cpp test_clipper_offset.cpp
test_clipper_utils.cpp test_clipper_utils.cpp
+224
View File
@@ -0,0 +1,224 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/Arrange.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
using namespace Slic3r;
using namespace Slic3r::arrangement;
namespace {
using Catch::Matchers::WithinRel;
// Square of the given (scaled) side, lower-left at the origin. bed_idx starts at
// 0 because arrange() seeds the nester's bin from it (see ModelArrange.cpp).
ArrangePolygon make_square(coord_t side)
{
ArrangePolygon ap;
Polygon p;
p.points = {Point(0, 0), Point(side, 0), Point(side, side), Point(0, side)};
ap.poly = ExPolygon(p);
ap.bed_idx = 0;
return ap;
}
ArrangePolygons squares(int n, double side_mm)
{
ArrangePolygons items;
for (int i = 0; i < n; ++i)
items.emplace_back(make_square(scaled(side_mm)));
return items;
}
// Bed [0,0]..[w,h] in scaled coordinates.
BoundingBox bed(double w_mm, double h_mm)
{
return BoundingBox(Point(0, 0), Point(scaled(w_mm), scaled(h_mm)));
}
// The default progress callback prints to stdout; silence it.
ArrangeParams quiet_params(coord_t min_dist = 0)
{
ArrangeParams p{min_dist};
p.progressind = [](unsigned, std::string) {};
return p;
}
ExPolygons placed_shapes(const ArrangePolygons &items)
{
ExPolygons out;
out.reserve(items.size());
for (const ArrangePolygon &ap : items)
out.emplace_back(ap.transformed_poly());
return out;
}
// Area double-counted across the shapes: the sum counts overlaps twice, the
// union once, so the difference is the overlapping area (0 when disjoint).
double overlap_area(const ExPolygons &shapes)
{
double sum = 0;
for (const ExPolygon &e : shapes)
sum += e.area();
double uni = 0;
for (const ExPolygon &e : union_ex(shapes))
uni += e.area();
return sum - uni;
}
// Relative tolerance absorbs the area-unit rounding the clipper union introduces.
bool disjoint(const ExPolygons &shapes)
{
double total = 0;
for (const ExPolygon &e : shapes)
total += e.area();
return overlap_area(shapes) <= total * 1e-9;
}
void require_no_overlap(const ArrangePolygons &items)
{
REQUIRE(disjoint(placed_shapes(items)));
}
} // namespace
// Prove the overlap check the other tests rely on actually detects overlap.
TEST_CASE("overlap_area detects overlap and ignores touching edges", "[Arrange]")
{
auto square_at = [](double x_mm) {
ArrangePolygon ap = make_square(scaled(20.));
ap.translation = Vec2crd(scaled(x_mm), 0);
return ap.transformed_poly();
};
ExPolygon a = square_at(0.);
SECTION("disjoint shapes are reported disjoint") {
REQUIRE(disjoint({a, square_at(30.)}));
}
SECTION("edge-touching shapes are reported disjoint") {
REQUIRE(disjoint({a, square_at(20.)}));
}
SECTION("overlapping shapes are not, and the area is measured") {
REQUIRE_FALSE(disjoint({a, square_at(10.)}));
REQUIRE_THAT(overlap_area({a, square_at(10.)}),
WithinRel(double(scaled(10.)) * scaled(20.), 1e-9)); // 10x20 mm
}
}
TEST_CASE("Arrange places every item on the physical bed", "[Arrange]")
{
ArrangePolygons items = squares(5, 20.);
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
for (const ArrangePolygon &ap : items)
REQUIRE(ap.bed_idx == 0);
}
TEST_CASE("Arranged items stay within the bed", "[Arrange]")
{
ArrangePolygons items = squares(6, 30.);
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
for (const ArrangePolygon &ap : items) {
REQUIRE(ap.bed_idx == 0);
REQUIRE(bed(200, 200).contains(ap.transformed_poly().contour.bounding_box()));
}
}
TEST_CASE("Arranged items do not overlap", "[Arrange]")
{
ArrangePolygons items = squares(6, 40.);
arrange(items, bed(250, 250), quiet_params(scaled(2.)));
require_no_overlap(items);
}
TEST_CASE("Arrange spaces items by their inflation", "[Arrange]")
{
// Per-item inflation is how the arranger enforces clearance (the GUI fills it
// from min_obj_distance). Two items inflated 4mm each end up >= 8mm apart.
ArrangePolygons items = squares(4, 20.);
for (ArrangePolygon &ap : items)
ap.inflation = scaled(4.);
arrange(items, bed(200, 200), quiet_params());
// Axis-aligned squares are their own bounding boxes, so the clearance between
// a pair is the distance between their boxes (1mm slack for nester rounding).
std::vector<BoundingBox> boxes;
for (const ExPolygon &e : placed_shapes(items))
boxes.push_back(e.contour.bounding_box());
double min_gap = std::numeric_limits<double>::max();
for (size_t i = 0; i < boxes.size(); ++i)
for (size_t j = i + 1; j < boxes.size(); ++j) {
coord_t sx = std::max<coord_t>(0, std::max(boxes[j].min.x() - boxes[i].max.x(),
boxes[i].min.x() - boxes[j].max.x()));
coord_t sy = std::max<coord_t>(0, std::max(boxes[j].min.y() - boxes[i].max.y(),
boxes[i].min.y() - boxes[j].max.y()));
min_gap = std::min(min_gap, std::sqrt(double(sx) * sx + double(sy) * sy));
}
REQUIRE(min_gap >= double(scaled(8.)) - double(scaled(0.5)));
}
TEST_CASE("An item larger than the bed cannot be placed", "[Arrange]")
{
ArrangePolygons items;
items.emplace_back(make_square(scaled(20.)));
items.emplace_back(make_square(scaled(400.))); // far bigger than the bed
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
REQUIRE(items[0].bed_idx == 0);
REQUIRE(items[1].bed_idx == UNARRANGED);
}
TEST_CASE("Items overflowing one bed spill onto virtual beds", "[Arrange]")
{
ArrangePolygons items = squares(8, 90.); // eight 90mm squares cannot share a 200x200 bed
arrange(items, bed(200, 200), quiet_params(scaled(2.)));
int max_bed = 0;
for (const ArrangePolygon &ap : items) {
REQUIRE(ap.bed_idx >= 0); // placed somewhere
max_bed = std::max(max_bed, ap.bed_idx);
}
REQUIRE(max_bed >= 1); // at least one on a virtual bed
}
TEST_CASE("Arrange handles an empty input", "[Arrange]")
{
ArrangePolygons items;
REQUIRE_NOTHROW(arrange(items, bed(200, 200), quiet_params()));
REQUIRE(items.empty());
}
TEST_CASE("Arrange without final alignment keeps items disjoint", "[Arrange]")
{
// do_final_align = false selects Alignment::DONT_ALIGN (skips recentering).
ArrangePolygons items = squares(6, 40.);
ArrangeParams params = quiet_params(scaled(2.));
params.do_final_align = false;
arrange(items, bed(250, 250), params);
for (const ArrangePolygon &ap : items)
REQUIRE(ap.bed_idx == 0);
require_no_overlap(items);
}
TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]")
{
// align_center != (0.5, 0.5) selects Alignment::USER_DEFINED.
ArrangePolygons items = squares(5, 30.);
ArrangeParams params = quiet_params(scaled(2.));
params.align_center = Vec2d(0.3, 0.7);
arrange(items, bed(250, 250), params);
for (const ArrangePolygon &ap : items)
REQUIRE(ap.bed_idx == 0);
require_no_overlap(items);
}